Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/) y [SemVer](htt

## [No publicado]

## [3.4.0] - 2026-10-30

Completa el soporte para [Pi Agent](https://github.com/earendil-works/pi): reglas por carpeta en monorepos y `/loop-task`. Para Claude Code no cambia nada.

### Notas de actualización
- `npx -y github:lacasoft/dev-starter-kit#v3.4.0 --update --yes`. Si ya instalaste con `--pi`, la extensión de Pi se actualiza sola, salvo que la hayas editado: en ese caso queda la versión nueva como `*.kit-new`.

### Added
- **Reglas por carpeta en Pi (monorepos).** Las reglas de `.claude/rules/` con `paths:` (un stack por carpeta) entran como en Claude Code: la primera vez que el agente lee o edita un archivo de su carpeta, la extensión anexa la regla al resultado, sin mezclar stacks. Vuelven a entrar después de compactar. `claude-rules.cjs` gana `appliesTo` (globs relativos a la raíz, incluidas las carpetas con corchetes que escapa el instalador).
- **`/loop-task` en Pi.** La extensión corre la puerta del ciclo al cerrar cada turno, como el hook `Stop` de Claude: mismo verificador, mismo tope, mismas protecciones. Pi no tiene subagentes, así que el cierre lo aprueba la persona:
- Con el verificador en verde, el agente repasa el diff y escribe el *Resumen para la persona*.
- Pi pregunta si se aprueba el cierre; si no, pide qué falta y el ciclo sigue.
- Sin interfaz (`pi -p`), el ciclo queda `awaiting-approval` hasta que la persona corra `loop.cjs approve` o `reject --reason "…"` en su terminal.
- Es una garantía más fuerte que la de Claude: allí la revisión del `reviewer` es una instrucción, y aquí es una confirmación explícita.
- `loop.cjs`: `gate --approval`, `approve` y `reject`. Sin `--approval`, la puerta de Claude se comporta igual que antes; sus pruebas no cambiaron.

### Security
- `pre-bash` niega que el agente corra `loop.cjs approve` o `reject`: aprobar el cierre lo decide la persona, no quien hizo el trabajo.

## [3.3.0] - 2026-10-30

Soporte opcional para [Pi Agent](https://github.com/earendil-works/pi) ≥ 1.4.0: contexto y guardrails. Para Claude Code no cambia nada.
Expand Down
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Dentro de **cualquier proyecto** (nuevo o heredado), sin clonar nada:
```bash
npx -y github:lacasoft/dev-starter-kit # interactivo
npx -y github:lacasoft/dev-starter-kit --yes --all # desatendido (todo)
npx -y github:lacasoft/dev-starter-kit#v3.3.0 # fijar una versión (recomendado)
npx -y github:lacasoft/dev-starter-kit#v3.4.0 # fijar una versión (recomendado)
```

Toma siempre la última versión de `master` (o el tag indicado). Repo **público (MIT)**.
Expand Down Expand Up @@ -257,7 +257,7 @@ git switch -c feat/migracion-billing # nunca en la rama principal
| **Solo comandos de verificación** | El verificador solo admite test, lint, typecheck o build del proyecto encadenados con `&&`: el hook no ejecuta shell arbitrario. |
| **No se puede hacer trampa** | El estado del ciclo no se edita (hooks `pre-edit` y `pre-bash`), y si cambia el script de `package.json` que usa el verificador, la puerta lo rechaza. |
| **Línea base** | Si el verificador ya pasa antes de empezar, el ciclo no arranca: no mide lo que quieres lograr. |
| **Quien hace no aprueba** | En verde, el ciclo exige pasar el agente `reviewer` por el diff y un *Resumen para la persona* antes de cerrar. |
| **Quien hace no aprueba** | En verde, el ciclo exige pasar el agente `reviewer` por el diff y un *Resumen para la persona* antes de cerrar. En [Pi](#también-con-pi-agent), sin subagentes, el cierre lo apruebas tú. |
| **Tope** | 10 turnos por defecto (máximo 50). Al agotarse, se detiene y deja el informe en `.loop/PROGRESS.md`. |

El ciclo no hace commit, push ni merge: el resultado se revisa como cualquier PR. Para finales que
Expand Down Expand Up @@ -287,8 +287,8 @@ modelos de varios proveedores, también locales. Con `--pi`, el kit añade una c
las mismas skills, reglas y helpers: no hay una segunda copia que mantener.

```bash
npx -y github:lacasoft/dev-starter-kit#v3.3.0 --pi # proyecto nuevo
npx -y github:lacasoft/dev-starter-kit#v3.3.0 --update --pi --yes # proyecto que ya usa el kit
npx -y github:lacasoft/dev-starter-kit#v3.4.0 --pi # proyecto nuevo
npx -y github:lacasoft/dev-starter-kit#v3.4.0 --update --pi --yes # proyecto que ya usa el kit
```

**Requisitos:** Pi 1.4.0 o superior (paquete `@earendil-works/pi-coding-agent`, pide Node.js 22.19+),
Expand All @@ -302,12 +302,20 @@ sin fijarla) y un modelo que use herramientas de forma confiable.
| `CLAUDE.md` con `@imports` y reglas de `.claude/rules/` | La extensión expande los imports y añade las reglas |
| Hooks `PreToolUse` y permisos de `settings.json` | La extensión aplica las mismas reglas (ver abajo) |
| `/swarm`, subagentes de `.claude/agents/` | No disponibles: Pi no tiene subagentes nativos |
| `/loop-task` | Todavía no: su puerta es un hook `Stop` de Claude |
| `/loop-task` con el `reviewer` al cerrar | `/skill:loop-task`: la extensión corre la puerta al cerrar cada turno y el cierre lo apruebas tú |

**Contexto.** Pi lee `CLAUDE.md`, pero no resuelve sus `@imports` ni carga `.claude/rules/`. La extensión
expande `CLAUDE.project.md` y la baseline, añade las reglas que Claude carga siempre (las del stack en un
proyecto de un solo stack y las propias del equipo) y los avisos del inicio de sesión. En un monorepo, las
reglas por carpeta (`paths:`) todavía no se cargan en Pi.
proyecto de un solo stack y las propias del equipo) y los avisos del inicio de sesión. En un monorepo,
cada regla por carpeta (`paths:`) entra como en Claude: la primera vez que el agente lee o edita un
archivo de su carpeta, y otra vez después de compactar.

**`/loop-task`.** La puerta del ciclo corre al cerrar cada turno, con el mismo verificador y el mismo
tope que en Claude. La diferencia es el cierre: Pi no tiene subagentes, así que no hay `reviewer`.
Con el verificador en verde y el resumen escrito, Pi te pregunta si apruebas el cierre; si dices que
no, le explicas qué falta y el ciclo sigue. Sin interfaz (`pi -p`), el ciclo queda esperando hasta que
corras `node .claude/helpers/loop.cjs approve` (o `reject --reason "…"`) en tu terminal; el agente no
puede correrlos.

**Seguridad en Pi.** Pi no tiene sistema de permisos: sin la extensión, ejecuta cualquier herramienta sin
preguntar. Con `--pi`, antes de cada herramienta, incluidas las llamadas anidadas desde codemode, la extensión aplica:
Expand Down Expand Up @@ -418,7 +426,7 @@ recibir la copia vieja. La solución buena es la misma que exige la baseline §6
versión** — y de paso hace la instalación reproducible:

```bash
npx -y github:lacasoft/dev-starter-kit#v3.3.0 --update --yes # por tag
npx -y github:lacasoft/dev-starter-kit#v3.4.0 --update --yes # por tag
npx -y github:lacasoft/dev-starter-kit#<commit> --update --yes # por commit (el SHA del tag)
```

Expand All @@ -440,4 +448,4 @@ Repo **público (MIT)** distribuido por `npx github:` → la rama **`master` deb
(dry-run) y en un monorepo multi-stack, en cada PR, más un job que prueba la capa de Pi dentro de Pi 1.4.0. La lista sale de `scripts/stacks.cjs`, que la deriva de `install.js`.
- **Cómo añadir** agentes, skills, stacks o externos: ver [CONTRIBUTING.md](CONTRIBUTING.md).
- **Versionado**: SemVer en `package.json` + `CHANGELOG.md`, con tag anotado por versión
(`v3.3.0`). Instala siempre pineando (`#v3.3.0`): es reproducible y evita la cache de npx.
(`v3.4.0`). Instala siempre pineando (`#v3.4.0`): es reproducible y evita la cache de npx.
3 changes: 2 additions & 1 deletion install.js
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,8 @@ async function main() {
console.log(" • Las skills se invocan como /skill:<nombre> (/skill:project-context, /skill:tdd…) y el estado del kit con /status.");
console.log(" • Guardrails: antes de cada herramienta se aplican las reglas del kit y las deny/ask de .claude/settings.json;");
console.log(" lo que pide confirmación se bloquea si no hay interfaz (pi -p). No es aislamiento: para eso, un contenedor o una VM.");
console.log(" • En Pi no hay subagentes: /swarm y los agentes de .claude/agents/ son solo de Claude Code.");
console.log(" • En Pi no hay subagentes: /swarm y los agentes de .claude/agents/ son solo de Claude Code. En /loop-task,");
console.log(" el cierre lo apruebas tú (sin interfaz: node .claude/helpers/loop.cjs approve | reject).");
console.log(" • Pi envía telemetría anónima de instalación por defecto: PI_TELEMETRY=0 la apaga.");
console.log(" • Instala ripgrep y fd con el gestor de paquetes de tu sistema: si faltan, Pi descarga la última versión sin fijarla.");
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lacasoft/dev-starter-kit",
"version": "3.3.0",
"version": "3.4.0",
"private": true,
"description": "Base .claude coherente (agentes, skills, helpers, orquestación multiagente nativa) instalable en cualquier proyecto lacasoft (backend/frontend/mobile/blockchain).",
"bin": {
Expand Down
4 changes: 4 additions & 0 deletions shared/.claude/helpers/claude-permissions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ function globToRegex(glob) {
let re = "";
for (let i = 0; i < glob.length; i++) {
const c = glob[i];
if (c === "\\" && i + 1 < glob.length) {
re += glob[++i].replace(/[.+^${}()|[\]\\*?]/g, "\\$&"); // \[ y similares: el carácter literal
continue;
}
if (c === "*" && glob[i + 1] === "*") {
i++;
if (glob[i + 1] === "/") {
Expand Down
13 changes: 11 additions & 2 deletions shared/.claude/helpers/claude-rules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*
* Como en Claude Code: una regla sin `paths:` en su frontmatter se carga siempre (proyecto de un
* stack, reglas propias del equipo); una con `paths:` solo al trabajar en archivos que coinciden
* (en monorepo, la carpeta de cada stack).
* (en monorepo, la carpeta de cada stack): `appliesTo` dice si una regla toca un archivo.
*/
const fs = require("fs");
const path = require("path");
const { globToRegex } = require("./claude-permissions.cjs");

const ROOT = process.env.CLAUDE_PROJECT_DIR || path.resolve(__dirname, "..", "..");

Expand Down Expand Up @@ -54,7 +55,15 @@ function load(root = ROOT) {
return out;
}

module.exports = { parse, load };
/** ¿Aplica la regla a este archivo? Las de `paths:` se comparan con la ruta relativa a la raíz. */
function appliesTo(rule, file, root = ROOT) {
if (rule.always) return true;
const rel = path.relative(root, path.resolve(root, file)).split(path.sep).join("/");
if (!rel || rel.startsWith("..")) return false;
return rule.paths.some((g) => globToRegex(g.replace(/^\.\//, "")).test(rel));
}

module.exports = { parse, load, appliesTo };

if (require.main === module) {
const reglas = load();
Expand Down
1 change: 1 addition & 0 deletions shared/.claude/helpers/hook-handler.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const BASH_DENY = [
[/\b(?:cat|cp|scp|tar|curl|base64)\b[^\n;]*(?:~|\$HOME)\/\.(?:ssh|aws|gnupg)\b/, "lee o copia credenciales del usuario (~/.ssh, ~/.aws, ~/.gnupg)"],
[/\bgit\s+(?:commit|push)\b[^\n;]*--no-verify\b/, "salta los hooks de calidad y el escáner de secretos (--no-verify)"],
[/^(?!\s*node\s+\.claude\/helpers\/loop\.cjs\b).*\.claude\/loop\/state\.json/, "toca el estado del ciclo (.claude/loop/state.json), que solo escribe loop.cjs: el verificador no se negocia; usa loop.cjs status o stop"],
[/\bloop\.cjs\s+(?:approve|reject)\b/, "aprueba o rechaza el cierre del ciclo: lo decide la persona en su terminal, no el agente que hizo el trabajo"],
];

// Archivos de entorno reales (.env, .env.local, .envrc…): los ejemplos (.example, .sample,
Expand Down
75 changes: 67 additions & 8 deletions shared/.claude/helpers/loop.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
* node .claude/helpers/loop.cjs status
* node .claude/helpers/loop.cjs stop
* node .claude/helpers/loop.cjs gate ← hook Stop (lo llama Claude Code, no la persona)
* node .claude/helpers/loop.cjs gate --approval ← la puerta en Pi Agent (la llama la extensión del kit)
* node .claude/helpers/loop.cjs approve | reject [--reason "…"] ← la persona, en su terminal
*
* La idea: Claude no decide cuándo terminó. Al final de cada turno, el hook Stop corre el
* verificador (tests, lint, build). Si falla, Claude sigue con el error a la vista; si pasa,
* se exige una revisión independiente antes de cerrar; si se agota el tope de turnos, para y
* deja el informe.
*
* Con --approval (Pi Agent, que no tiene subagentes) la revisión independiente es de la persona:
* con el verificador en verde y el resumen escrito, el ciclo queda "awaiting-approval" y la
* puerta lo dice en { approval: {…} }. Lo cierra `approve` o lo devuelve al trabajo `reject`;
* el agente no puede correrlos (el hook pre-bash se lo niega).
*
* Dos lugares, a propósito:
* - .claude/loop/state.json — el contrato (verificador, tope, turnos). Solo lo escribe este helper;
* Claude Code protege .claude/ de las ediciones del agente y los hooks del kit también.
Expand Down Expand Up @@ -155,7 +162,7 @@ function status() {

function stop() {
const s = read();
if (!s || !["active", "reviewing"].includes(s.status)) return console.log("No hay ciclo activo.");
if (!s || !["active", "reviewing", "awaiting-approval"].includes(s.status)) return console.log("No hay ciclo activo.");
write({ ...s, status: "stopped", finishedAt: new Date().toISOString() });
appendProgress(`\n**Detenido a mano en el turno ${s.turns}.**\n`);
console.log("⏹️ Ciclo detenido.");
Expand All @@ -165,12 +172,47 @@ function block(reason) {
process.stdout.write(JSON.stringify({ decision: "block", reason }));
}

// La sección "## Resumen para la persona" de PROGRESS.md, o "" si todavía no está. Termina en el
// siguiente título o en lo que añade este helper (las líneas "- Turno N · …" y las de "**…**").
function summary() {
let progress = "";
try {
progress = fs.readFileSync(PROGRESS, "utf8");
} catch (_) {}
const m = progress.match(/^## Resumen para la persona[^\n]*\n?([\s\S]*?)(?=^## |^\*\*|^- Turno \d+ · |(?![\s\S]))/m);
return m ? m[1].trim() || "(vacío)" : "";
}

function askApproval(s) {
process.stdout.write(JSON.stringify({ approval: { objective: s.objective, turns: s.turns, summary: summary() } }));
}

// La persona aprueba el cierre (o lo rechaza con un motivo) desde su terminal o desde la extensión.
function approve() {
const s = read();
if (!s || s.status !== "awaiting-approval") return fail("No hay un ciclo esperando aprobación.");
write({ ...s, status: "passed", finishedAt: new Date().toISOString() });
appendProgress(`\n**Ciclo cerrado en ${s.turns} turnos: verificador en verde y cierre aprobado por la persona.**\n`);
console.log(`✅ Ciclo "${s.objective}" cerrado. Revisa el diff y .loop/PROGRESS.md antes de hacer merge.`);
}
function reject() {
const s = read();
if (!s || s.status !== "awaiting-approval") return fail("No hay un ciclo esperando aprobación.");
const reason = arg("reason") || "sin motivo";
write({ ...s, status: "active" });
appendProgress(`\n**La persona no aprobó el cierre (turno ${s.turns}):** ${reason}\n`);
console.log(`↩️ Cierre rechazado: ${reason}. El ciclo sigue activo.`);
}

// Hook Stop. Sin ciclo activo no hace nada (y no cuesta nada).
function gate() {
try {
fs.readFileSync(0, "utf8");
} catch (_) {}
const approval = flag("approval");
const s = read();
// Esperando a la persona: no se trabaja ni se vuelve a correr el verificador; se repite la petición.
if (s && s.status === "awaiting-approval") return approval ? askApproval(s) : undefined;
if (!s || !["active", "reviewing"].includes(s.status)) return;
const turns = s.turns + 1;
const changed = Object.entries(s.scripts || {}).filter(([k, v]) => scriptsFingerprint(s.check)[k] !== v).map(([k]) => k);
Expand Down Expand Up @@ -209,6 +251,18 @@ function gate() {
return;
}

if (s.status === "active" && approval) {
// En verde, en Pi: sin subagentes, quien revisa es la persona. El agente deja el resumen.
write({ ...s, turns, status: "reviewing" });
block(
`Ciclo "${s.objective}": el verificador pasa (turno ${turns}). Último paso antes de cerrar:\n` +
"1. Repasa el diff completo del ciclo (git diff contra el punto de partida) y corrige lo que encuentres.\n" +
"2. Añade al final de .loop/PROGRESS.md la sección \"## Resumen para la persona\": qué cambió, por qué, y qué debería revisar ella con atención.\n" +
"No amplíes el alcance. Al terminar, el verificador corre otra vez y la persona decide si aprueba el cierre: quien hizo el trabajo no lo aprueba."
);
return;
}

if (s.status === "active") {
// En verde. Antes de cerrar, quien revisa no puede ser quien hizo el trabajo.
write({ ...s, turns, status: "reviewing" });
Expand All @@ -221,17 +275,20 @@ function gate() {
return;
}

let progress = "";
try {
progress = fs.readFileSync(PROGRESS, "utf8");
} catch (_) {}
if (!/^## Resumen para la persona/m.test(progress)) {
if (!summary()) {
write({ ...s, turns });
block(
`Ciclo "${s.objective}": falta el cierre. Lanza el agente \`reviewer\` sobre el diff si no lo hiciste, y añade a .loop/PROGRESS.md la sección "## Resumen para la persona" (qué cambió, por qué y qué revisar con atención).`
approval
? `Ciclo "${s.objective}": falta el cierre. Añade a .loop/PROGRESS.md la sección "## Resumen para la persona" (qué cambió, por qué y qué revisar con atención); después, la persona decide.`
: `Ciclo "${s.objective}": falta el cierre. Lanza el agente \`reviewer\` sobre el diff si no lo hiciste, y añade a .loop/PROGRESS.md la sección "## Resumen para la persona" (qué cambió, por qué y qué revisar con atención).`
);
return;
}
if (approval) {
write({ ...s, turns, status: "awaiting-approval" });
appendProgress(`\n**Verificador en verde y resumen listo (turno ${turns}): esperando la aprobación de la persona.**\n`);
return askApproval({ ...s, turns });
}
write({ ...s, turns, status: "passed", finishedAt: stamp });
appendProgress(`\n**Ciclo cerrado en ${turns} turnos: verificador en verde y revisión hecha.**\n`);
process.stdout.write(
Expand All @@ -250,7 +307,9 @@ if (require.main === module) {
else if (action === "status") status();
else if (action === "stop") stop();
else if (action === "gate") gate();
else fail("Uso: loop.cjs start --check \"<comando>\" [--max-turns N] [--objective \"…\"] | status | stop | gate");
else if (action === "approve") approve();
else if (action === "reject") reject();
else fail("Uso: loop.cjs start --check \"<comando>\" [--max-turns N] [--objective \"…\"] | status | stop | gate [--approval] | approve | reject [--reason \"…\"]");
}

module.exports = { validateCheck };
Loading
Loading