@ingeniomaps/cauce 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/automatization/hooks/README.md +2 -2
  3. package/automatization/runners/antigravity/rules/cauce.md +7 -2
  4. package/automatization/runners/claude/CLAUDE.md +1 -4
  5. package/automatization/runners/codex/AGENTS.md +7 -3
  6. package/automatization/runners/gemini/GEMINI.md +1 -4
  7. package/automatization/shared/inbox.js +28 -0
  8. package/automatization/workflows/agent-eval.js +1 -1
  9. package/automatization/workflows/autobuild.js +52 -18
  10. package/automatization/workflows/flow.js +33 -8
  11. package/automatization/workflows/onboard.js +25 -3
  12. package/engine/automation/index.js +19 -4
  13. package/engine/automation/rules.js +122 -0
  14. package/engine/automation/runners.js +3 -1
  15. package/engine/cli/instance.js +35 -6
  16. package/engine/cli/planning.js +16 -2
  17. package/engine/config/validate.js +53 -3
  18. package/engine/core/onboarding.js +37 -7
  19. package/engine/core/ownership.js +64 -1
  20. package/engine/core/scan.js +23 -9
  21. package/engine/hooks/approval.js +3 -2
  22. package/engine/hooks/chat.js +59 -10
  23. package/engine/hooks/input.js +1 -11
  24. package/engine/hooks/push.js +147 -0
  25. package/engine/hooks/shell.js +7 -5
  26. package/engine/integrations/registry.js +13 -1
  27. package/engine/planning/inbox.js +36 -0
  28. package/engine/planning/parser.js +22 -9
  29. package/engine/planning/recurring.js +13 -2
  30. package/engine/schemas/ops-config.schema.json +21 -0
  31. package/package.json +1 -1
  32. package/template/AGENTS.md +10 -4
  33. package/template/planning/INBOX.md +2 -1
  34. package/template/planning/RECURRING.md +6 -5
  35. package/template/planning/rules/system/commits.md +3 -1
package/CHANGELOG.md CHANGED
@@ -14,6 +14,134 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
14
14
  unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
15
15
  diseño — eso vive en el commit y en el código.
16
16
 
17
+ ## [0.82.0] - 2026-09-11
18
+
19
+ ### Cambiado
20
+
21
+ - **`allowPush: true` ya no publica en la rama viva ni desde un subagente.** La llave dejaba pasar cualquier
22
+ push, `main` incluido, y el de un subagente igual que el tuyo. Ahora no alcanza a `main`, `master` ni a la
23
+ rama por defecto del remoto, y un subagente no publica con ningún permiso. Tampoco la alcanza una orden en
24
+ el chat.
25
+
26
+ **Qué cambia para vos**: si publicabas en la rama viva con `allowPush: true`, agregá
27
+ `"pushToLiveBranches": ["main"]` en `runner` de `ops.config.json`, o pegá `push origin main` en
28
+ `planning/.ops-approval` para un push puntual.
29
+
30
+ - **R10 y el párrafo de publicación de `AGENTS.md` se reescribieron.** Los dos decían que el push se
31
+ comprueba contra `runner.allowPush` y nada más; ahora dicen lo que de verdad comprueba el motor: esa llave
32
+ —que no llega a la rama viva sin `runner.pushToLiveBranches`, ni a un subagente— o la orden que la persona
33
+ da en el chat nombrando el remoto y la rama. Los dos archivos los mantiene Cauce, así que `upgrade` los
34
+ reemplaza enteros.
35
+
36
+ **Qué cambia para vos**: nada que hacer. Si tu equipo cita R10 en algún documento propio, el texto que
37
+ baja ahora es más largo y dice qué sostiene un guard y qué sostiene una persona.
38
+
39
+ ### Corregido
40
+
41
+ - **Nombrar algo en el chat ya no lo autoriza: hay que pedirlo.** Desde 0.81.0, lo que nombrabas en el chat
42
+ pasaba los guards aunque no lo pidieras: «¿para qué sirven las credentials?» dejaba leer `credentials`, y
43
+ «el .env tiene algo raro?» dejaba leer el `.env`. Ahora la frase que lo nombra tiene que pedir una acción
44
+ —«leé», «abrime», «borrá», «read»…—, y la negación sigue frenando como antes.
45
+
46
+ **Qué cambia para vos**: si pedís algo sin un verbo que Cauce reconozca, el agente te dice qué se frenó y
47
+ con un «dale» pasa.
48
+
49
+ - **`upgrade` ya no pisa un guard tuyo que se llama como uno nuevo del paquete.** Cuando una versión empezaba
50
+ a traer un nombre que ya usabas en `automatization/hooks/`, lo reemplazaba sin decir nada: 0.81.0 lo hizo
51
+ con `guard-chat.sh`. Ahora lo conserva y lo avisa; `--check` lo cuenta, y `--force` lo reemplaza diciéndolo.
52
+
53
+ **Qué cambia para vos**: si ves «ya existía y Cauce no lo entregó», renombrá tu guard y repetí `upgrade`;
54
+ mientras tanto, el guard del paquete con ese nombre no está instalado.
55
+
56
+ - **Un guard propio ya no aparece como «editado localmente».** `upgrade` registraba todo lo que había en
57
+ `automatization/hooks/`, así que editar un guard tuyo lo volvía una edición del molde: `upgrade --check`
58
+ salía con 1, `check` lo contaba como congelado, y `--force` anunciaba que lo descartaba sin tocarlo. Ahora
59
+ sólo registra lo que trae el paquete, y el primer `upgrade` suelta lo que una versión anterior registró de
60
+ más.
61
+
62
+ **Qué cambia para vos**: nada que hacer; tus guards dejan de aparecer en esos avisos.
63
+
64
+ - **El aviso de credenciales sin dueño avisa sólo credenciales.** `check` listaba toda variable que el
65
+ proyecto declaraba y ningún contrato nombraba: en un proyecto real fueron ciento ocho nombres de build, y
66
+ la única credencial quedó en «y 1 más». Ahora cuenta las que tienen nombre de secreto —`…_PASSWORD`,
67
+ `…_TOKEN`, `…_KEY`, `…_DSN`…—, dice en qué servicio está cada una y dónde se escribe su dueño, y avisa
68
+ aparte cuando un servicio pasó el tope de variables que se revisan.
69
+
70
+ **Qué cambia para vos**: la declaración de secretos y la configuración de una integración ahora también
71
+ rechazan una clave que termine en `_key`, `dsn` o `credentials`, igual que ya rechazaban `…token`. Si
72
+ tenés una, cambiala por la referencia a una variable de entorno. Y una credencial con nombre de
73
+ configuración —`STRIPE_LIVE`— no aparece en el aviso: el criterio es el nombre.
74
+
75
+ - **Una variable ya no se da por cargada porque otra la contiene en su nombre.** Con `API_SECRET_ROTATION`
76
+ escrita en `organization/workspace.md`, `API_SECRET` dejaba de avisarse aunque nadie la nombrara. Ahora
77
+ cuenta sólo el nombre entero.
78
+
79
+ **Qué cambia para vos**: puede aparecer en el aviso una credencial que antes quedaba tapada; escribí su
80
+ dueño y se va.
81
+
82
+ - **Un push que pedís en el chat ya no se frena.** Con `allowPush: false`, `git push` se frenaba aunque lo
83
+ hubieras pedido con todas las letras. Ahora pasa si tu mensaje nombra el remoto y la rama —«subí feat/x a
84
+ origin», `git push origin feat/x`—, y si no los nombra, el agente te dice qué se frenó y con un «dale»
85
+ pasa ese push y ningún otro. `planning/.ops-approval` también acepta la línea `push <remoto> <rama>`, tal
86
+ cual y sin patrones. Un `git push origin +rama` ahora se frena como el `--force` que es.
87
+
88
+ **Qué cambia para vos**: para publicar una rama de trabajo alcanza con pedirlo nombrando remoto y rama, o
89
+ con contestar «dale».
90
+
91
+ - **La sesión carga las reglas que rigen tu proyecto, no las cuatro de `system/` fijas.** El archivo de
92
+ instrucciones de cada runner importaba siempre `planning/rules/system/`: cargaba la regla que tu proyecto
93
+ había sobrescrito y ninguna de las tuyas. Ahora `automation install` pone las vigentes —las tuyas y las de
94
+ `system/` que no sobrescribiste— en Claude, Gemini, Codex y Antigravity, y un `CLAUDE.md` que editaste
95
+ recibe el bloque nuevo sin perder lo tuyo. Como `upgrade` no reinstala, `check` y `automation doctor`
96
+ avisan cuando lo instalado quedó atrás. `AGENTS.md` dice ahora que, donde una regla tuya y una del
97
+ sistema chocan, rige la tuya.
98
+
99
+ **Qué cambia para vos**: después de actualizar, o cada vez que escribas o sobrescribas una regla,
100
+ reinstalá el adaptador (`make install-claude`, o el de tu runner); `check` te dice cuándo hace falta. Si
101
+ tu `CLAUDE.md` es tuyo y no tiene nada de Cauce, poné las marcas `<!-- cauce:reglas inicio -->` y
102
+ `<!-- cauce:reglas fin -->` donde quieras el bloque y reinstalá.
103
+
104
+ - **`autobuild` trabaja con las reglas de tu proyecto y ya no cita una que retiraste.** Los subagentes
105
+ planificaban, construían y revisaban sin conocer tus reglas, y la fila que pedía partir una tarea citaba
106
+ R17 aunque la hubieras retirado. Ahora `context` dice qué reglas rigen —línea `RULES`, campo `rules` en
107
+ `--json`—, cada fase recibe esa lista, y Review tiene que nombrar contra cuáles revisó o la corrida para
108
+ con `review-unbacked`. Ningún prompt cita una regla por número.
109
+
110
+ **Qué cambia para vos**: nada que hacer.
111
+
112
+ - **Los recorridos ya no llenan el INBOX sin medida.** `autobuild` volcaba en Propuestas todo lo que Review
113
+ anotaba sin frenar, entero, y `flow` y `onboard` escribían sin tope ni forma. Ahora cada recorrido escribe
114
+ como mucho tres entradas por tarea o por corrida, de una línea, con la forma del molde y sin repetir un
115
+ nombre que ya está. Lo que pasa del tope no se pierde: `autobuild` lo cuenta en el `done/` de la tarea,
116
+ `flow` lo deja en el informe y `onboard` lo nombra al cerrar. Y `check` avisa, sin fallar, cuando
117
+ `INBOX.md` pasa de 300 líneas.
118
+
119
+ **Qué cambia para vos**: nada que hacer. Si querés otro umbral para el aviso, poné
120
+ `"inbox": { "warnLines": N }` en `ops.config.json`. El molde de `INBOX.md` ya no pide la evidencia dentro
121
+ de una propuesta; tu `INBOX.md` no se toca.
122
+
123
+ - **La recurrencia que recorre el INBOX viene activa, y descomentar los ejemplos de `RECURRING.md` ya no
124
+ rompe `check`.** Una instancia nueva trae la fila `inbox`, trimestral, que vence por primera vez tres meses
125
+ después de `init`. Las filas de ejemplo declaran el `(service: …)` que `check` exige. Y `check` avisa una
126
+ entrada del INBOX que se llama como una tarea de `done/`: probablemente se promovió y quedó ahí.
127
+
128
+ **Qué cambia para vos**: `RECURRING.md` es tuyo y `upgrade` no lo toca. Para tener la recurrencia en una
129
+ instancia que ya existe, agregá debajo de la tabla
130
+ `| inbox | trimestral | AAAA-MM-DD | Recorrer el INBOX entero. _Aceptación: ninguna viñeta queda sin decisión de promover, dejar o borrar._ (service: planning) |`
131
+ con una fecha futura: `Desde` es cuándo vence por primera vez, no el día en que escribís la fila.
132
+
133
+ - **Dos repositorios que terminan en una carpeta con el mismo nombre ya no salen con el mismo nombre.** Con
134
+ varias raíces declaradas, el inventario nombraba cada servicio por la **carpeta** de su raíz, así que
135
+ `../gouduet/keycloak` y `../hypixo/keycloak` salían los dos `keycloak` en `check`, en `onboard` y en
136
+ `scan`, y no había con qué saber de qué repositorio era cada credencial. Ahora los nombra el `name` que
137
+ cada raíz declara en `ops.config.json`, y `check` avisa cuando dos raíces se llaman igual, diciendo
138
+ cuáles son las dos.
139
+
140
+ **Qué cambia para vos**: si dos raíces de tu `ops.config.json` comparten `name`, `check` te lo avisa y
141
+ sigue pasando —renombrá una cuando puedas—. Y donde el `name` y la carpeta difieren, los servicios pasan
142
+ a listarse con el `name` —el que escribiste vos— en vez de con la carpeta; `onboard --json` sigue
143
+ emitiendo `roots` como lista de rutas.
144
+
17
145
  ## [0.81.0] - 2026-09-11
18
146
 
19
147
  ### Cambiado
@@ -72,8 +72,8 @@ pidió con todas las letras y lo que el agente decidió solo. `chat` corre sobre
72
72
  runner lo dispara cuando ella manda algo, nunca por el resultado de una herramienta— y lo deja donde los
73
73
  guards lo leen:
74
74
 
75
- - lo que la persona **nombró** en su mensaje pasa: «leé el `.env`» autoriza leer el `.env`, y «no toques
76
- el `.env`» no;
75
+ - lo que la persona **pidió nombrándolo** pasa: «leé el `.env`» autoriza leer el `.env`; «no toques el
76
+ `.env`» no, y tampoco una pregunta o un comentario que sólo lo nombra —«¿qué tiene el `.env`?»—;
77
77
  - lo que se frenó sin que lo nombrara queda anotado, y un «dale» en el mensaje siguiente aprueba
78
78
  exactamente eso;
79
79
  - `plan-first` no aplica: el plan es del trabajo que va por tareas.
@@ -1,7 +1,12 @@
1
1
  # Cauce
2
2
 
3
- Lee y cumple `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/rules/system/` antes
4
- de ejecutar trabajo. `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex de
3
+ Lee y cumple `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y las reglas que rigen este proyecto
4
+ antes de ejecutar trabajo: las propias y las de `system/` que el proyecto no sobrescribió. Donde una propia
5
+ contradice a una del sistema, rige la propia.
6
+
7
+ {{RULES:list}}
8
+
9
+ `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex de
5
10
  ejecución y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
6
11
  inventes aprobaciones o credenciales y no hagas push ni deploy. Cierra cada tarea con verificación real y
7
12
  evidencia en DONE.
@@ -2,10 +2,7 @@
2
2
 
3
3
  @{{OPS_DIR}}AGENTS.md
4
4
  @{{OPS_DIR}}planning/PROTOCOL.md
5
- @{{OPS_DIR}}planning/rules/system/process.md
6
- @{{OPS_DIR}}planning/rules/system/code-shape.md
7
- @{{OPS_DIR}}planning/rules/system/commits.md
8
- @{{OPS_DIR}}planning/rules/system/conduct.md
5
+ {{RULES:imports}}
9
6
 
10
7
  Los hooks de `.claude/settings.json` son obligatorios. En una instancia recién creada, empezá corriendo
11
8
  `node {{OPS_DIR}}tools/ops.js onboard`: es instantáneo, y lo primero que imprime es la pregunta con la que
@@ -1,8 +1,12 @@
1
1
  # Cauce para Codex
2
2
 
3
- `{{OPS_DIR}}AGENTS.md` tiene las reglas del sistema, `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de
4
- verdad del proceso y `{{OPS_DIR}}planning/rules/system/` son las reglas que rigen cada tarea. Leelos
5
- antes de trabajar —los tres, no cuando algo sale mal—: acá sólo está lo específico de este runner.
3
+ `{{OPS_DIR}}AGENTS.md` tiene las reglas del sistema y `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de
4
+ verdad del proceso. Las reglas que rigen cada tarea en este proyecto son éstas —las propias y las de
5
+ `system/` que el proyecto no sobrescribió—, y donde una propia contradice a una del sistema, rige la propia:
6
+
7
+ {{RULES:list}}
8
+
9
+ Leé todo eso antes de trabajar, no cuando algo sale mal: acá sólo está lo específico de este runner.
6
10
 
7
11
  > Codex lee el `AGENTS.md` de la raíz, que es un nombre compartido entre herramientas. Cuando el repo
8
12
  > ops **es** la raíz, este archivo no se instala: el `AGENTS.md` de la empresa ya está ahí y manda.
@@ -2,10 +2,7 @@
2
2
 
3
3
  @{{OPS_DIR}}AGENTS.md
4
4
  @{{OPS_DIR}}planning/PROTOCOL.md
5
- @{{OPS_DIR}}planning/rules/system/process.md
6
- @{{OPS_DIR}}planning/rules/system/code-shape.md
7
- @{{OPS_DIR}}planning/rules/system/commits.md
8
- @{{OPS_DIR}}planning/rules/system/conduct.md
5
+ {{RULES:imports}}
9
6
 
10
7
  `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/cauce:autobuild` fase por fase; los
11
8
  workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex
@@ -0,0 +1,28 @@
1
+ // Lo que un recorrido le pide a un agente cuando escribe en el INBOX (caso 101). El tope lo aplica el
2
+ // recorrido sobre lo que le pasa al agente, no el agente: pedirle que se limite no es un tope. Lo que no
3
+ // entra se cuenta donde cada recorrido lo deja, y ninguno lo tira en silencio.
4
+ const INBOX_CAP = 3
5
+ // La forma de una entrada, igual a la que declara el molde de `INBOX.md`: quien escribe no tiene que
6
+ // abrir el archivo para saberla. Una prueba ata las dos.
7
+ const INBOX_ENTRY = '- **slug-del-item** — Qué es, y qué se decide o se resuelve con esto.'
8
+ // Los nombres que ya hay, por sección, tal como los imprime `ops context --json` en su campo inbox.
9
+ const INBOX_HEADS = { type: 'object', additionalProperties: false, properties: Object.fromEntries(
10
+ ['deuda', 'ideas', 'propuestas', 'lecciones'].map((key) => [key, { type: 'array', items: { type: 'string' } }]),
11
+ ) }
12
+ // Una entrada es una línea. Un hallazgo de largo libre se recorta antes de llegar a quien lo escribe,
13
+ // porque lo que llega entero es lo que termina copiado entero.
14
+ const oneLine = (text) => {
15
+ const first = String(text || '').split('\n')[0].trim()
16
+ return first.length > 240 ? `${first.slice(0, 239)}…` : first
17
+ }
18
+ // La forma y los nombres que ya hay en las secciones donde se va a escribir. Van los nombres y no el
19
+ // archivo: con ellos alcanza para no repetir una entrada, y el archivo entero pesaría lo que el INBOX.
20
+ function inboxAsk(sections, heads) {
21
+ const known = sections.map((name) => {
22
+ const names = (heads || {})[name.toLowerCase()] || []
23
+ return names.length ? `en ${name} ya están ${names.join(', ')}` : `${name} no tiene entradas`
24
+ }).join('; ')
25
+ return `Cada entrada con la forma del molde —${INBOX_ENTRY}—: un nombre y una línea, y la evidencia se ` +
26
+ `cita donde ya vive, no se copia. Por nombre, ${known}: lo que ya esté con uno de esos nombres no se ` +
27
+ `vuelve a escribir.`
28
+ }
@@ -225,7 +225,7 @@ const verdicts = await pipeline(
225
225
  `normas o sistemas de terceros. Enumeralas con el registro que cada una lleva —verificado, ` +
226
226
  `documentado, hipótesis, o ninguno— y comprobá las que se puedan comprobar barato: abrí el archivo ` +
227
227
  `que cita y leé si dice eso, reproducí la invocación inocua que declara (\`--help\`, \`--version\`, ` +
228
- `un comando de sólo lectura), consultá la fuente pública que nombra. Llegá hasta donde R12 permite: ` +
228
+ `un comando de sólo lectura), consultá la fuente pública que nombra. Sin salirte de la lectura: ` +
229
229
  `nunca conectarte a un sistema real ni ejecutar la operación cuyo efecto se describe.\n\n` +
230
230
  `Empezá por la afirmación de la que depende la recomendación del cargo, no por la que parezca más ` +
231
231
  `discutible: son distintas, y la segunda suele estar bien rotulada porque el cargo esperaba que se la ` +
@@ -27,6 +27,7 @@ export const meta = {
27
27
  }
28
28
 
29
29
  {{INCLUDE:shared/workflow-root.js}}
30
+ {{INCLUDE:shared/inbox.js}}
30
31
  const CONFIG = `${ROOT}/ops.config.json`
31
32
  const P = `${ROOT}/planning`
32
33
  const ORG = `${ROOT}/organization`
@@ -82,6 +83,10 @@ const CONTEXT = {
82
83
  // Dónde va el plan de este runner. El nombre sale de su id y el recorrido no lo deriva: lo
83
84
  // pregunta, igual que la fecha.
84
85
  wipFile: { type: 'string' },
86
+ // Con los nombres que ya hay en el INBOX, Review no vuelve a anotar uno.
87
+ inbox: { ...INBOX_HEADS },
88
+ // Las reglas que rigen el proyecto, con los overrides ya resueltos por el motor (caso 105).
89
+ rules: { type: 'array', items: { type: 'string' } },
85
90
  },
86
91
  }
87
92
  const CLAIM = {
@@ -132,6 +137,10 @@ const DECISION = {
132
137
  consulted: { type: 'array', items: { type: 'string' } },
133
138
  },
134
139
  }
140
+ // Review nombra contra qué reglas revisó (caso 105): recibir las rutas no garantiza abrirlas, y esto es lo único
141
+ // que deja rastro de que se hizo. Critique no lo lleva porque no recibe la lista.
142
+ const REVIEWED = { ...DECISION, required: [...DECISION.required, 'rules'],
143
+ properties: { ...DECISION.properties, rules: { type: 'array', items: { type: 'string' } } } }
135
144
  // Un exit code dice que el test corrió, no que pruebe lo que la tarea prometió: un test que asercia de
136
145
  // menos —o que ni existe— sale verde igual, y el guard de verify tampoco lo ve porque también mira exit
137
146
  // codes. Por eso `uncovered` se contrasta contra la aceptación leyendo el fuente, no la salida (R9).
@@ -274,6 +283,8 @@ const VERDICT = ' Cerrá con verdict=aprobado si no queda nada por corregir ante
274
283
  'si algo no se resuelve acá —el diseño no lo cubre, falta una decisión ajena, o la corrección excede el ' +
275
284
  'alcance—. Marcá blocking=true sólo en el hallazgo que impide entregar: el resto queda registrado y no ' +
276
285
  'manda a tocar código.'
286
+ // Acompaña a todo prompt con schema REVIEWED.
287
+ const RULED = ' En rules nombrá, por su ruta, cada una de las reglas que rigen contra la que revisaste el diff.'
277
288
  // Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
278
289
  const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
279
290
  // Atajo para reconocer un gate que corrió pruebas sin preguntarle a nadie. No alcanza solo y no
@@ -331,12 +342,19 @@ if (!contract.rootOk) {
331
342
 
332
343
  const bounds = contract.boundaries || []
333
344
  const limits = bounds.length ? ` Límites del proyecto: ${bounds.join('; ')}.` : ''
345
+ // Las reglas que rigen el proyecto (caso 105). Las lista `context`, que se lee después del contrato, así que
346
+ // entran al preámbulo cuando esa lectura vuelve. Viajan las rutas y no el texto: el preámbulo se reenvía a cada
347
+ // subagente que toca código, y el texto de las reglas multiplicaría su tamaño por cada uno.
348
+ let governing = []
334
349
  // Alcance de escritura: para subagentes que tocan código o ejecutan gates del producto.
335
- const SCOPE = `${BASE}\n\nProyecto ${contract.project}. workspaceRoots es el límite completo de escritura del ` +
336
- `producto: ${contract.workspaceRoots.join('; ')}.${limits} Este preámbulo ya trae el contrato; no vuelvas a leer ` +
337
- `${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} ni ${P}/PROTOCOL.md.`
350
+ const SCOPE = () => `${BASE}\n\nProyecto ${contract.project}. workspaceRoots es el límite completo de escritura ` +
351
+ `del producto: ${contract.workspaceRoots.join('; ')}.${limits} Este preámbulo ya trae el contrato; ` +
352
+ `no vuelvas a leer ${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} ni ${P}/PROTOCOL.md.` +
353
+ (governing.length ? ` Las reglas que rigen este proyecto son éstas, relativas a ${ROOT}: ${governing.join(', ')}. ` +
354
+ 'Leé las que toquen tu fase antes de planificar, construir o revisar; donde una propia contradice a una del ' +
355
+ 'sistema, rige la propia.' : '')
338
356
  // Formatos de planning: sólo para subagentes que escriben roadmap, BACKLOG, WIP, DONE o gates.
339
- const LEDGER = `${SCOPE}\n\nContratos de planning, textuales de ${P}/PROTOCOL.md:\n${contract.contracts}`
357
+ const LEDGER = () => `${SCOPE()}\n\nContratos de planning, textuales de ${P}/PROTOCOL.md:\n${contract.contracts}`
340
358
 
341
359
  // Un subagente puede morir —error terminal tras reintentos, o alguien que lo saltea— y entonces el
342
360
  // runtime devuelve `null`. Sin comprobarlo, la primera propiedad que se le pide revienta el recorrido
@@ -352,8 +370,8 @@ const LEDGER = `${SCOPE}\n\nContratos de planning, textuales de ${P}/PROTOCOL.md
352
370
  // justo la distinción que hace falta. Lo que evita el olvido es el arnés, que rechaza la llamada sin
353
371
  // etiqueta en las cuatro suites del recorrido.
354
372
  const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
355
- const run = (prompt, options = {}) => agent(`${SCOPE}\n\n${prompt}`, options)
356
- const write = (prompt, options = {}) => agent(`${LEDGER}\n\n${prompt}`, options)
373
+ const run = (prompt, options = {}) => agent(`${SCOPE()}\n\n${prompt}`, options)
374
+ const write = (prompt, options = {}) => agent(`${LEDGER()}\n\n${prompt}`, options)
357
375
 
358
376
  // Las tres paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es
359
377
  // el único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso
@@ -368,10 +386,11 @@ const registerHuman = async (prompt, label) => (await write(prompt, { label })
368
386
  const readContext = () => read(
369
387
  `Corré "node tools/ops.js context ${P} --json" desde ${ROOT} y reportá sólo lo que imprimió. Derivá hasTask ` +
370
388
  `de si task es null, wipActive de si wip es null, claimed del campo claimed, today y wipFile de sus ` +
371
- `campos, y lane ` +
389
+ `campos, rules del campo rules tal cual, y lane ` +
372
390
  `de task.tier; copiá slug, ` +
373
391
  `hito, service, acceptance, ` +
374
- `epic y cast de task, y epicContext de epic.context —vacío si no hay épica—. El comando es la fuente de ` +
392
+ `epic y cast de task, epicContext de epic.context —vacío si no hay épica— e inbox tal cual. El comando es ` +
393
+ `la fuente de ` +
375
394
  `verdad: no abras archivos de planning para completarlo. Poné readOk en true sólo si el comando salió ` +
376
395
  `con código 0 y devolvió JSON; si falló, readOk en false y el resto en sus valores vacíos, sin ` +
377
396
  `deducir el estado de ninguna otra fuente.`,
@@ -405,6 +424,7 @@ if (blocker === 'blocked-on-human') {
405
424
  }
406
425
  if (blocker) return stop('context-unavailable', `${P} contestó blocked=${JSON.stringify(planning.blocked)}, `
407
426
  + 'que no es del vocabulario. No se sabe si hay bloqueo, así que no se sigue como si no lo hubiera.')
427
+ governing = planning.rules || []
408
428
 
409
429
  let currentMilestone = planning.wipActive ? planning.hito : ''
410
430
  const completed = []
@@ -565,7 +585,7 @@ while (rounds++ < MAX_TASKS) {
565
585
  const nota = await registerHuman(
566
586
  `Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
567
587
  + `Motivo: ${detail}. La acción humana es revisar si la unidad son dos resultados con vidas `
568
- + `distintas y partirla —R17—, o dejarla entera con la razón escrita.`, 'plan-human')
588
+ + `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human')
569
589
  return stop(reason, `${detail}${nota}`)
570
590
  }
571
591
 
@@ -746,8 +766,8 @@ while (rounds++ < MAX_TASKS) {
746
766
  let review = await run(
747
767
  `${asRole(cast.review)}Revisá el diff real por aceptación, regresiones, seguridad, arquitectura, código ` +
748
768
  `generado, migraciones y alcance accidental. Cada cargo revisa su dominio, no el ajeno.${MANIFEST}` +
749
- `${VERDICT}`,
750
- { schema: DECISION, label: 'review' },
769
+ `${VERDICT}${RULED}`,
770
+ { schema: REVIEWED, label: 'review' },
751
771
  )
752
772
  if (!review) return stop('agent-unavailable', 'Review no devolvió resultado')
753
773
  if (review.verdict === 'bloqueado') {
@@ -756,24 +776,38 @@ while (rounds++ < MAX_TASKS) {
756
776
  if (blockers(review).length) {
757
777
  await write(`Corregí sólo estos hallazgos con evidencia y actualizá el WIP: ${blockers(review).join('; ')}`,
758
778
  { label: 'review-fix' })
759
- review = await run(`Volvé a revisar el diff corregido de ${task.id}.${MANIFEST}${VERDICT}`,
760
- { schema: DECISION, label: 'review' })
779
+ review = await run(`Volvé a revisar el diff corregido de ${task.id}.${MANIFEST}${VERDICT}${RULED}`,
780
+ { schema: REVIEWED, label: 'review' })
761
781
  if (!review) return stop('agent-unavailable', 'la re-revisión no devolvió resultado')
762
782
  if (review.verdict === 'bloqueado' || blockers(review).length) {
763
783
  return stop('review-failed', blockers(review).join('; ') || 'sin condiciones nombradas')
764
784
  }
765
785
  }
786
+ // Con reglas que rigen, aprobar sin nombrar contra cuáles es la misma falla que la de abajo en otro eje.
787
+ if (governing.length && !(review.rules || []).length) {
788
+ return stop('review-unbacked', 'aprobó el diff sin nombrar contra qué reglas revisó')
789
+ }
790
+ if (governing.length) log(`Review contra: ${review.rules.join(', ')}`)
766
791
  // Aprobar sin declarar qué se abrió no se arregla mandando a tocar código: falló quien revisó.
767
792
  if (!review.consulted.length) return stop('review-unbacked', 'aprobó el diff sin declarar qué inspeccionó')
768
793
  reviewFact = `${review.verdict} por ${cast.review}, sobre ${review.consulted.join(', ')}`
769
794
  // Lo que no impide entregar no manda a tocar código, y tampoco desaparece: la mejora opinable que se
770
795
  // corrige a las apuradas cuesta una vuelta y un riesgo que nadie pidió. Va a Propuestas y no a
771
- // Lecciones porque lo que la revisión anotó es un cambio del producto con su evidencia; Lecciones es
772
- // sobre cómo trabajamos, y ahí el hallazgo queda esperando una promoción que nadie va a hacer.
773
- const noted = review.concerns.filter((one) => !one.blocking).map((one) => one.detail)
774
- if (noted.length) {
796
+ // Lecciones porque lo que la revisión anotó es un cambio del producto —su evidencia es la de la
797
+ // tarea, que queda en `done/`—; Lecciones es sobre cómo trabajamos, y ahí el hallazgo queda
798
+ // esperando una promoción que nadie va a hacer.
799
+ const noted = review.concerns.filter((one) => !one.blocking).map((one) => oneLine(one.detail))
800
+ const kept = noted.slice(0, INBOX_CAP)
801
+ // Lo que pasa del tope no se escribe y tampoco desaparece: queda contado en el hecho de revisión, que
802
+ // viaja a `done/`. Una revisión que anota treinta y seis cosas no está priorizando, y el INBOX no las
803
+ // iba a leer (caso 101).
804
+ if (noted.length > kept.length) {
805
+ reviewFact += ` · ${noted.length - kept.length} anotado(s) sin volcar al INBOX`
806
+ }
807
+ if (kept.length) {
775
808
  await write(`Registrá en la sección Propuestas de ${P}/INBOX.md lo que la revisión de ${task.id} dejó ` +
776
- `anotado sin frenar la entrega, sin promover ninguna: ${JSON.stringify(noted)}`, { label: 'review-noted' })
809
+ `anotado sin frenar la entrega, sin promover ninguna. ${inboxAsk(['Propuestas'], planning.inbox)} ` +
810
+ `Lo anotado: ${JSON.stringify(kept)}`, { label: 'review-noted' })
777
811
  }
778
812
  }
779
813
 
@@ -16,6 +16,7 @@ export const meta = {
16
16
  }
17
17
 
18
18
  {{INCLUDE:shared/workflow-root.js}}
19
+ {{INCLUDE:shared/inbox.js}}
19
20
 
20
21
  // Dónde trabaja el recorrido. Normalmente es la raíz donde se lo invocó; `args.root` existe para
21
22
  // correrlo sobre otra instancia —el banco desechable con el que `flow-eval` lo mide—, porque un
@@ -58,6 +59,8 @@ const MANIFEST = {
58
59
  // `dependsOn`, que se arregló mirando sólo las claves de las etapas.
59
60
  completion: { type: 'array', items: { type: 'string' } },
60
61
  conditionalAgents: { type: 'array', items: { type: 'string' } },
62
+ // Con los nombres que ya hay en el INBOX, lo que el recorrido escribe al final no repite uno.
63
+ inbox: INBOX_HEADS,
61
64
  owners: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
62
65
  domain: { type: 'string' }, agent: { type: 'string' },
63
66
  } } },
@@ -161,6 +164,7 @@ const contract = await agent(
161
164
  `only ran on failure the destination came out of memory.\n` +
162
165
  ` If command 1 failed, set exists=false, report flows, and stop.\n` +
163
166
  `3. "node tools/ops.js agents list --json", which gives each role its resolved path.\n` +
167
+ `4. "node tools/ops.js context planning --json" — copy only its inbox field into inbox, verbatim.\n` +
164
168
  `Report exists=true and these manifest fields: name, purpose, outcome, entryAgent, facilitator, ` +
165
169
  `guardrails, decisionOwners flattened into owners as domain/agent pairs, and stages with id, phase, ` +
166
170
  `agent, produces, dependsOn and exitGate. Drop every other field the command printed — the schema ` +
@@ -398,17 +402,38 @@ if (contract.outcome === 'report') {
398
402
  `sólo llevaba lo que la siguiente necesitaba para decidir.\n\n` +
399
403
  `Escribí el informe en ${REPORTS} como ` +
400
404
  `<AAAA-MM-DD>-<slug>.md: qué pasó, qué se sabe con evidencia, qué se supone, qué se decidió y qué ` +
401
- `queda abierto. Separá causa de síntoma y no atribuyas responsabilidad a personas. Registrá cada ` +
402
- `seguimiento en ${INBOX} sin promoverlo, en la sección que le toca por su sujeto: un cambio del ` +
403
- `producto con su evidencia va a Propuestas, lo aprendido sobre cómo trabajamos va a Lecciones. ` +
405
+ `queda abierto. Separá causa de síntoma y no atribuyas responsabilidad a personas. Cada seguimiento ` +
406
+ `va en lo que queda abierto del informe y además en followUps, del más al menos importante, con la ` +
407
+ `sección que le toca por su sujeto: un cambio del producto va a Propuestas, lo aprendido sobre cómo ` +
408
+ `trabajamos va a Lecciones. No escribas en ${INBOX}: eso lo hace el paso siguiente. ` +
404
409
  `Toda acción que requiera una persona, en ${HUMAN}.`,
405
410
  { schema: { type: 'object', required: ['file', 'followUps'], properties: {
406
- file: { type: 'string' }, followUps: { type: 'integer' }, summary: { type: 'string' },
411
+ file: { type: 'string' }, summary: { type: 'string' },
412
+ followUps: { type: 'array', items: { type: 'object', additionalProperties: false,
413
+ required: ['section', 'entry'], properties: {
414
+ section: { type: 'string', enum: ['Propuestas', 'Lecciones'] }, entry: { type: 'string' },
415
+ } } },
407
416
  } }, label: 'report-write' },
408
417
  )
409
418
  if (!report) return stop('report-unavailable', 'el informe no devolvió resultado')
410
- log(`Informe en ${report.file}. ${report.followUps} seguimiento(s) en el INBOX, sin promover.`)
411
- return finish({ flow: FLOW, stages: handoffs.length, report: report.file, promoted: false })
419
+ // El tope lo aplica el recorrido y no quien escribe, y lo que pasa de él ya está en el informe: al
420
+ // INBOX va lo que alguien tiene que decidir, no todo lo que el informe dejó abierto (caso 101).
421
+ const followUps = report.followUps || []
422
+ const listed = followUps.slice(0, INBOX_CAP).map((one) => ({ section: one.section, entry: oneLine(one.entry) }))
423
+ if (listed.length) {
424
+ await agent(
425
+ `${RULES}\n\nRegistrá en ${INBOX} estos seguimientos del informe ${report.file}, cada uno en su ` +
426
+ `sección y sin promover ninguno. ${inboxAsk(['Propuestas', 'Lecciones'], contract.inbox)} ` +
427
+ `Seguimientos: ${JSON.stringify(listed)}`,
428
+ { label: 'report-inbox' },
429
+ )
430
+ }
431
+ const unlisted = followUps.length - listed.length
432
+ log(`Informe en ${report.file}. ${listed.length} seguimiento(s) en el INBOX, sin promover` +
433
+ `${unlisted ? `; ${unlisted} más quedan sólo en el informe, por el tope de ${INBOX_CAP} por corrida` : ''}.`)
434
+ return finish({
435
+ flow: FLOW, stages: handoffs.length, report: report.file, followUps: listed.length, unlisted, promoted: false,
436
+ })
412
437
  }
413
438
 
414
439
  const epic = await agent(
@@ -431,7 +456,7 @@ if (!epic) return stop('draft-unavailable', 'la propuesta de épica no devolvió
431
456
  if (epic.outcome === 'no-hacer') {
432
457
  await agent(
433
458
  `${RULES}\n\nRegistrá la conclusión en la sección Lecciones de ${INBOX}: por qué esta intención no ` +
434
- `es viable hoy y qué la haría viable. Motivo: ${epic.reason}`,
459
+ `es viable hoy y qué la haría viable. ${inboxAsk(['Lecciones'], contract.inbox)} Motivo: ${epic.reason}`,
435
460
  { label: 'inbox-lesson' },
436
461
  )
437
462
  return stop('no-viable', epic.reason)
@@ -442,7 +467,7 @@ if (epic.outcome === 'investigar') {
442
467
  await agent(
443
468
  `${RULES}\n\nRegistrá en ${HUMAN} qué hay que averiguar antes de poder decidir esta intención y quién ` +
444
469
  `puede hacerlo, sin inventar responsables ni fechas, y dejá la conclusión en la sección Ideas de ` +
445
- `${INBOX} sin promoverla. Qué falta averiguar: ${epic.reason}`,
470
+ `${INBOX} sin promoverla. ${inboxAsk(['Ideas'], contract.inbox)} Qué falta averiguar: ${epic.reason}`,
446
471
  { label: 'investigar' },
447
472
  )
448
473
  return finish({ flow: FLOW, stages: handoffs.length, investigate: epic.reason, promoted: false })
@@ -59,6 +59,7 @@ const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsabl
59
59
  `retrasa el único momento en que la herramienta todavía no sirve para nada.`
60
60
 
61
61
  {{INCLUDE:shared/workflow-finish.js}}
62
+ {{INCLUDE:shared/inbox.js}}
62
63
 
63
64
  const SCAN = {
64
65
  type: 'object', additionalProperties: false, required: ['fresh', 'services'],
@@ -86,6 +87,8 @@ const SCAN = {
86
87
  } } },
87
88
  externals: { type: 'array', items: { type: 'string' } },
88
89
  secrets: { type: 'array', items: { type: 'string' } },
90
+ // Los nombres que ya hay en Ideas: con `force` el arranque reescribe una instancia que ya tiene INBOX.
91
+ inboxIdeas: { type: 'array', items: { type: 'string' } },
89
92
  },
90
93
  }
91
94
 
@@ -104,13 +107,14 @@ phase('Scan')
104
107
  // Una sola llamada, y todo lo que hace es correr dos comandos y mirar dos archivos. Lo que sigue depende
105
108
  // de lo que devuelva, así que gastar más antes de saberlo es gastar a ciegas.
106
109
  const state = await agent(
107
- `${BASE}\n\nFrom ${ROOT}, run exactly these two commands and report what they printed. Explore nothing ` +
110
+ `${BASE}\n\nFrom ${ROOT}, run exactly these three commands and report what they printed. Explore nothing ` +
108
111
  `else and open no file other than .env.example at the workspace root.\n` +
109
112
  `1. "node tools/ops.js onboard --json": the instance state, the workspace inventory, the opening ` +
110
113
  `question and the dimensions still uncovered. Copy fresh, opening, followUps, the "need" of each ` +
111
114
  `dimension, and every service with its path, its runtimes, its declared commands keeping the source ` +
112
115
  `file each command came from, and the variable names its "env" carries. Add nothing it did not print.\n` +
113
116
  `2. "node tools/ops.js check planning".\n` +
117
+ `3. "node tools/ops.js context planning --json": copy its inbox.ideas into inboxIdeas, verbatim.\n` +
114
118
  `The inventory already names every credential each service expects: never open a .env file to look for ` +
115
119
  `more. Report those names in secrets and the services they point at in externals. A name the inventory ` +
116
120
  `carries is declared, and saying otherwise is a claim the repository contradicts.`,
@@ -180,16 +184,28 @@ const drafted = await agent(
180
184
  `que no pidas declararlo de nuevo: lo que falta es dónde se carga el valor y quién lo hace, y ningún ` +
181
185
  `valor se propone acá. Además, una por cada sistema externo o MCP a conectar, y una por la autoridad ` +
182
186
  `del runner, que hoy declara runner.allowPush=false.\n` +
183
- `5. Las preguntas que queden abiertas, en la sección Ideas de ${INBOX}, sin promover.\n` +
184
- `Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
187
+ `Devolvé en files cada archivo que tocaste, en assumptions cada supuesto que dejaste marcado y en ` +
188
+ `openQuestions las preguntas que quedaron abiertas, de la más a la menos importante. No las escribas en ` +
189
+ `${INBOX}: eso lo hace el paso siguiente.`,
185
190
  { schema: WRITTEN, label: 'contexto' },
186
191
  )
187
192
  if (!drafted) return stop('draft-unavailable', 'los borradores no devolvieron resultado')
188
193
 
189
194
  phase('Epic')
190
195
 
196
+ // Las preguntas abiertas van al INBOX con tope, y el tope lo aplica el recorrido: por eso las escribe
197
+ // este paso con lo que el anterior devolvió, y no el anterior mientras las redactaba (caso 101). Las que
198
+ // no entran se dicen al cerrar, que es donde la persona que arrancó la instancia las lee.
199
+ const questions = (drafted.openQuestions || []).map(oneLine)
200
+ const inboxed = questions.slice(0, INBOX_CAP)
201
+ const INBOX_ASK = inboxed.length
202
+ ? `Registrá además en la sección Ideas de ${INBOX}, sin promover, estas preguntas abiertas: ` +
203
+ `${JSON.stringify(inboxed)}. ${inboxAsk(['Ideas'], { ideas: state.inboxIdeas || [] })}\n\n`
204
+ : ''
205
+
191
206
  const epic = await agent(
192
207
  `${BASE}\n\n${EVIDENCE}\n\nSupuestos que quedaron escritos: ${JSON.stringify(drafted.assumptions || [])}\n\n` +
208
+ INBOX_ASK +
193
209
  `Escribí en ${ROADMAP} la épica epic-001-<slug>.md siguiendo el contrato de ${P}/PROTOCOL.md: ` +
194
210
  `frontmatter epic/title/status/service con status open, criterios **CN** observables, "## Contexto ` +
195
211
  `relevante" con rutas reales e historias con (→ CN) y (service: ruta), cada una de menos de cuatro ` +
@@ -223,11 +239,17 @@ const assumptions = (drafted.assumptions || []).length
223
239
  const humanActions = (drafted.humanActions || []).length
224
240
  log(`Contexto escrito con ${assumptions} supuesto(s) por confirmar y ${humanActions} acción(es) humana(s) en ${HUMAN}.`)
225
241
  log(`Épica en ${epic.file}, sin promover: revisala, promoví una historia a un hito del BACKLOG y corré /autobuild.`)
242
+ const unlisted = questions.slice(INBOX_CAP)
243
+ if (unlisted.length) {
244
+ log(`${unlisted.length} pregunta(s) abierta(s) más no entraron al INBOX (tope de ${INBOX_CAP}): ` +
245
+ unlisted.join(' · '))
246
+ }
226
247
 
227
248
  return finish({
228
249
  services: services.length,
229
250
  assumptions,
230
251
  humanActions,
231
252
  epic: epic.file,
253
+ unlistedQuestions: unlisted,
232
254
  promoted: false,
233
255
  })