@ingeniomaps/cauce 0.96.0 → 0.98.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 (78) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +1 -0
  3. package/agents/roles/system/accounting-specialist/learning/AUTOMATION.md +2 -2
  4. package/agents/roles/system/ai-governance-lead/learning/AUTOMATION.md +1 -1
  5. package/agents/roles/system/ai-product-manager/learning/AUTOMATION.md +1 -1
  6. package/agents/roles/system/analytics-engineer/learning/AUTOMATION.md +1 -1
  7. package/agents/roles/system/backend-engineer/learning/AUTOMATION.md +2 -2
  8. package/agents/roles/system/business-strategist/learning/AUTOMATION.md +2 -2
  9. package/agents/roles/system/cloud-architect/learning/AUTOMATION.md +1 -1
  10. package/agents/roles/system/community-manager/learning/AUTOMATION.md +1 -1
  11. package/agents/roles/system/content-specialist/learning/AUTOMATION.md +2 -2
  12. package/agents/roles/system/customer-success-manager/learning/AUTOMATION.md +2 -2
  13. package/agents/roles/system/customer-support-specialist/learning/AUTOMATION.md +2 -2
  14. package/agents/roles/system/data-analyst/learning/AUTOMATION.md +2 -2
  15. package/agents/roles/system/data-engineer/learning/AUTOMATION.md +1 -1
  16. package/agents/roles/system/data-governance-steward/learning/AUTOMATION.md +2 -2
  17. package/agents/roles/system/data-scientist/learning/AUTOMATION.md +1 -1
  18. package/agents/roles/system/database-administrator/learning/AUTOMATION.md +1 -1
  19. package/agents/roles/system/developer-relations-engineer/learning/AUTOMATION.md +1 -1
  20. package/agents/roles/system/devops-engineer/learning/AUTOMATION.md +2 -2
  21. package/agents/roles/system/engineering-manager/learning/AUTOMATION.md +1 -1
  22. package/agents/roles/system/financial-controller/learning/AUTOMATION.md +2 -2
  23. package/agents/roles/system/finops-engineer/learning/AUTOMATION.md +2 -2
  24. package/agents/roles/system/fraud-risk-analyst/learning/AUTOMATION.md +2 -2
  25. package/agents/roles/system/frontend-engineer/learning/AUTOMATION.md +2 -2
  26. package/agents/roles/system/growth-marketer/learning/AUTOMATION.md +2 -2
  27. package/agents/roles/system/implementation-manager/learning/AUTOMATION.md +1 -1
  28. package/agents/roles/system/integrations-engineer/learning/AUTOMATION.md +3 -3
  29. package/agents/roles/system/kyc-aml-specialist/learning/AUTOMATION.md +3 -3
  30. package/agents/roles/system/legal-counsel/learning/AUTOMATION.md +1 -1
  31. package/agents/roles/system/logistics-operations-manager/learning/AUTOMATION.md +2 -2
  32. package/agents/roles/system/machine-learning-engineer/learning/AUTOMATION.md +1 -1
  33. package/agents/roles/system/mlops-engineer/learning/AUTOMATION.md +1 -1
  34. package/agents/roles/system/mobile-engineer/learning/AUTOMATION.md +2 -2
  35. package/agents/roles/system/partnerships-manager/learning/AUTOMATION.md +1 -1
  36. package/agents/roles/system/people-operations-manager/learning/AUTOMATION.md +1 -1
  37. package/agents/roles/system/privacy-compliance-specialist/learning/AUTOMATION.md +2 -2
  38. package/agents/roles/system/procurement-manager/learning/AUTOMATION.md +1 -1
  39. package/agents/roles/system/product-manager/learning/AUTOMATION.md +3 -3
  40. package/agents/roles/system/product-marketing-manager/learning/AUTOMATION.md +2 -2
  41. package/agents/roles/system/project-manager/learning/AUTOMATION.md +1 -1
  42. package/agents/roles/system/qa-engineer/learning/AUTOMATION.md +2 -2
  43. package/agents/roles/system/release-manager/learning/AUTOMATION.md +1 -1
  44. package/agents/roles/system/sales-representative/learning/AUTOMATION.md +2 -2
  45. package/agents/roles/system/security-engineer/learning/AUTOMATION.md +2 -2
  46. package/agents/roles/system/site-reliability-engineer/learning/AUTOMATION.md +2 -2
  47. package/agents/roles/system/software-architect/learning/AUTOMATION.md +2 -2
  48. package/agents/roles/system/solutions-engineer/learning/AUTOMATION.md +1 -1
  49. package/agents/roles/system/tech-lead/learning/AUTOMATION.md +2 -2
  50. package/agents/roles/system/technical-program-manager/learning/AUTOMATION.md +1 -1
  51. package/agents/roles/system/technical-writer/learning/AUTOMATION.md +1 -1
  52. package/agents/roles/system/treasury-analyst/learning/AUTOMATION.md +2 -2
  53. package/agents/roles/system/ui-designer/learning/AUTOMATION.md +3 -3
  54. package/agents/roles/system/user-researcher/learning/AUTOMATION.md +3 -3
  55. package/agents/roles/system/ux-designer/learning/AUTOMATION.md +3 -3
  56. package/automatization/hooks/README.md +4 -3
  57. package/automatization/workflows/agent-propose.js +2 -2
  58. package/automatization/workflows/autobuild.js +219 -29
  59. package/automatization/workflows/flow.js +30 -14
  60. package/engine/agents/learning-files.js +18 -0
  61. package/engine/agents/learning-seal.js +24 -12
  62. package/engine/agents/learning-sources.js +12 -10
  63. package/engine/agents/learning.js +29 -7
  64. package/engine/cli/args.js +2 -2
  65. package/engine/cli/catalog.js +12 -12
  66. package/engine/cli/ops.js +2 -1
  67. package/engine/cli/planning.js +4 -1
  68. package/engine/hooks/approval.js +5 -3
  69. package/engine/hooks/chat.js +19 -7
  70. package/engine/hooks/push.js +3 -2
  71. package/engine/hooks/self-approval.js +2 -2
  72. package/engine/hooks/shell.js +63 -6
  73. package/engine/planning/contracts.js +19 -0
  74. package/engine/planning/parser.js +35 -0
  75. package/package.json +1 -1
  76. package/template/AGENTS.md +5 -4
  77. package/template/Makefile +1 -1
  78. package/template/planning/rules/system/commits.md +9 -2
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en Release Management y software delivery para
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en ventas responsables y procesos comerciales para
@@ -15,4 +15,4 @@ no compres listas, envíes outreach, publiques, hagas commit ni push. Termina co
15
15
  `make agent-evaluate AGENT=sales-representative`.
16
16
  ```
17
17
 
18
- Programar semanalmente en cada instalación.
18
+ Programar mensualmente en cada instalación.
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en Security Engineering para mantener
@@ -16,4 +16,4 @@ hagas commit ni push. Termina con
16
16
  `make agent-evaluate AGENT=security-engineer`.
17
17
  ```
18
18
 
19
- Programar semanalmente en cada instalación.
19
+ Programar mensualmente en cada instalación.
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en Site Reliability Engineering para mantener
@@ -15,4 +15,4 @@ no ejecutes carga, caos, failover, publiques, hagas commit ni push. Termina con
15
15
  `make agent-evaluate AGENT=site-reliability-engineer`.
16
16
  ```
17
17
 
18
- Programar semanalmente en cada instalación.
18
+ Programar mensualmente en cada instalación.
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en Software Architecture para mantener
@@ -15,4 +15,4 @@ no generes migraciones, publiques, hagas commit ni push. Termina con
15
15
  `make agent-evaluate AGENT=software-architect`.
16
16
  ```
17
17
 
18
- Programar semanalmente en cada instalación.
18
+ Programar mensualmente en cada instalación.
@@ -1,6 +1,6 @@
1
1
  # Automatización de aprendizaje
2
2
 
3
- ## Semanal con Codex
3
+ ## Mensual con Codex
4
4
 
5
5
  Ejecutar `make agent-learn AGENT=solutions-engineer`. Completar el informe generado con novedades relevantes, versiones, aplicabilidad, evidencia primaria, impacto y recomendación. Investigar no autoriza cambiar `SKILL.md`, materiales, producto ni sistemas.
6
6
 
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en la práctica de liderazgo técnico —revisión y aprobación
@@ -22,4 +22,4 @@ apruebes, despliegues, hagas commit ni push, y no autorices gasto. Termina con
22
22
  `make agent-evaluate AGENT=tech-lead`.
23
23
  ```
24
24
 
25
- Programar semanalmente en cada instalación.
25
+ Programar mensualmente en cada instalación.
@@ -1,6 +1,6 @@
1
1
  # Automatización de aprendizaje
2
2
 
3
- ## Semanal con Codex
3
+ ## Mensual con Codex
4
4
 
5
5
  Ejecutar `make agent-learn AGENT=technical-program-manager`. Registrar fuentes primarias, estándar/versión/estado, fit organizacional, evidencia de outcome y validación de owners. Investigar no autoriza cambiar planes, compromisos o sistemas.
6
6
 
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en Technical Writing y documentación de software
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
3
  ```text
4
4
  Investiga cambios recientes en tesorería corporativa, liquidez y pagos salientes para mantener
@@ -17,4 +17,4 @@ no actualices SKILL.md, código ni planificación; no publiques, hagas commit ni
17
17
  con `make agent-evaluate AGENT=treasury-analyst`.
18
18
  ```
19
19
 
20
- Programar semanalmente en cada instalación.
20
+ Programar mensualmente en cada instalación.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal con este prompt:
3
+ Configurar una automatización mensual con este prompt:
4
4
 
5
5
  ```text
6
6
  Investiga cambios recientes en UI Design para mantener agents/roles/system/ui-designer.
@@ -16,4 +16,4 @@ permiso, publiques, hagas commit ni push. Termina ejecutando
16
16
  `make agent-evaluate AGENT=ui-designer`.
17
17
  ```
18
18
 
19
- La frecuencia recomendada es semanal; cada instalación define el horario.
19
+ La frecuencia recomendada es mensual; cada instalación define el horario.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal en la raíz del repositorio con este prompt:
3
+ Configurar una automatización mensual en la raíz del repositorio con este prompt:
4
4
 
5
5
  ```text
6
6
  Investiga cambios recientes en User Research para mantener el agente
@@ -19,4 +19,4 @@ contactes participantes, accedas a datos personales, publiques, hagas commit ni
19
19
  push. Termina ejecutando `make agent-evaluate AGENT=user-researcher`.
20
20
  ```
21
21
 
22
- La frecuencia recomendada es semanal; cada instalación elige día y hora.
22
+ La frecuencia recomendada es mensual; cada instalación elige día y hora.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal con este prompt:
3
+ Configurar una automatización mensual con este prompt:
4
4
 
5
5
  ```text
6
6
  Investiga cambios recientes en UX Design para mantener agents/roles/system/ux-designer.
@@ -15,4 +15,4 @@ referencias, evaluaciones, fuentes ni planificación; no publiques, hagas commit
15
15
  ni push. Termina con `make agent-evaluate AGENT=ux-designer`.
16
16
  ```
17
17
 
18
- La frecuencia recomendada es semanal; cada instalación define el horario.
18
+ La frecuencia recomendada es mensual; cada instalación define el horario.
@@ -74,11 +74,12 @@ guards lo leen:
74
74
 
75
75
  - lo que la persona **pidió nombrándolo** pasa: «leé el `.env`» autoriza leer el `.env`; «no toques el
76
76
  `.env`» no, y tampoco una pregunta o un comentario que sólo lo nombra —«¿qué tiene el `.env`?»—;
77
- - lo que se frenó sin que lo nombrara queda anotado, y un «dale» en el mensaje siguiente aprueba
78
- exactamente eso;
77
+ - lo que se frenó sin que lo nombrara queda anotado, y su confirmación en el mensaje siguiente —con las
78
+ palabras que sea— aprueba exactamente eso. Si ese mensaje niega, frena o pregunta, no aprueba nada; y lo
79
+ que se frenó mientras ella decía que no, no queda esperando;
79
80
  - lo que un guard dejó pasar porque ella lo pidió sigue valiendo mientras dure la sesión, **salvo en los
80
81
  gates de un commit** —`governance`, `verify` y `dependencies`—, que preguntan cada vez: ahí vale lo que
81
- pidió el mensaje en curso o un «dale», igual que para publicar;
82
+ pidió el mensaje en curso o su confirmación al bloqueo, igual que para publicar;
82
83
  - `plan-first` no aplica: el plan es del trabajo que va por tareas.
83
84
 
84
85
  No cuenta cuando no hay persona —CI, o un aviso del runner como el de un subagente que terminó—, cuando lo
@@ -1,6 +1,6 @@
1
1
  // Convierte la recomendación de los informes en el cambio concreto que una persona puede aprobar.
2
2
  //
3
- // `learn --proposal` consolida qué recomendaron los informes de la semana, pero deja «Cambio
3
+ // `learn --proposal` consolida qué recomendaron los informes de investigación, pero deja «Cambio
4
4
  // propuesto: por definir». Eso no es aprobable: nadie firma una intención. Este recorrido escribe el
5
5
  // texto exacto que habría que agregar, archivo por archivo, y lo contrasta contra los casos
6
6
  // adversariales vigentes antes de proponerlo.
@@ -70,7 +70,7 @@ phase('Proponer')
70
70
 
71
71
  const proposal = await agent(
72
72
  `Sos quien mantiene el cargo ${AGENT}. La propuesta ${context.proposal} consolidó lo que ` +
73
- `recomendaron los informes semanales, pero le falta lo único que una persona puede aprobar: el ` +
73
+ `recomendaron los informes de investigación, pero le falta lo único que una persona puede aprobar: el ` +
74
74
  `cambio concreto.\n\n` +
75
75
  `Leé los informes citados en su sección «Hallazgos», el SKILL.md del cargo, ` +
76
76
  `${context.dir}/learning/sources.yaml, ${context.dir}/evaluations/expected-behaviors.yaml y sus ` +
@@ -43,6 +43,13 @@ const GATE = `${P}/AWAITING_REVIEW.md`
43
43
  // suelta no es un envoltorio. Por eso también la escapada en vez de alternar el estilo de comillas.
44
44
  const QUOTES = ['\'', '"']
45
45
 
46
+ // Si la fila que registró una parada quedó pendiente. `context` sólo lista las pendientes, así que
47
+ // preguntar por la presencia de la tarea alcanza, y no hace falta que un modelo lea el estado.
48
+ const HUMAN_ROW = {
49
+ type: 'object', additionalProperties: false, required: ['readOk', 'pending'],
50
+ properties: { readOk: { type: 'boolean' }, pending: { type: 'boolean' } },
51
+ }
52
+
46
53
  const CONTEXT = {
47
54
  type: 'object', additionalProperties: false,
48
55
  required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
@@ -60,6 +67,9 @@ const CONTEXT = {
60
67
  hasTask: { type: 'boolean' }, wipActive: { type: 'boolean' },
61
68
  queued: { type: 'integer' }, slug: { type: 'string' }, hito: { type: 'string' },
62
69
  service: { type: 'string' }, acceptance: { type: 'string' }, epic: { type: 'string' },
70
+ // Sin declararlo acá no llega, igual que le pasó a `epicContext`: `additionalProperties: false` lo
71
+ // descarta y las fases lo reciben vacío para siempre.
72
+ description: { type: 'string' },
63
73
  // Sin declararlo acá no llega: `additionalProperties: false` lo descartaría, y el aplanado de la
64
74
  // épica a su número —dos líneas arriba— hace fácil creer que ya viene. La fase Plan lo pedía en su
65
75
  // prompt y planificaba contra el título; `readEpics` cuenta de dónde sale.
@@ -153,8 +163,29 @@ const DECISION = {
153
163
  }
154
164
  // Review nombra contra qué reglas revisó (caso 105): recibir las rutas no garantiza abrirlas, y esto es lo único
155
165
  // que deja rastro de que se hizo. Critique no lo lleva porque no recibe la lista.
166
+ //
167
+ // Y `decision` es el tercer destino, el que R6 le da a lo que aparece y no le toca a este cargo: queda
168
+ // registrado con qué lo cierra y quién puede tomarlo. Las otras fases lo tienen —`ready-human`,
169
+ // `plan-human`, `verify-human`, y `open-decisions` en Build— y Review no, así que un revisor que
170
+ // encontraba una decisión elegía entre frenar la corrida o degradarla a propuesta con tope de tres.
171
+ // Elegía frenar, que de las dos es la correcta, y costaba la corrida entera sobre trabajo que estaba
172
+ // bien. Es la misma separación que `uncovered` hace en Verify, en el otro extremo del recorrido.
173
+ //
174
+ // Medido sobre un banco desechable el 2026-09-17, corridas `wf_99130468-2c4` y `wf_5dcc3828-a9f`: las dos
175
+ // terminaron en `review-failed` con la suite del producto en verde y los tres casos de la aceptación
176
+ // dando lo pedido, y las dos por un hallazgo que el propio revisor describió como una decisión que no le
177
+ // tocaba.
156
178
  const REVIEWED = { ...DECISION, required: [...DECISION.required, 'rules'],
157
- properties: { ...DECISION.properties, rules: { type: 'array', items: { type: 'string' } } } }
179
+ properties: {
180
+ ...DECISION.properties,
181
+ rules: { type: 'array', items: { type: 'string' } },
182
+ // Extiende el concern de `DECISION` en vez de reescribirlo: copiado entero, un campo nuevo allá no
183
+ // llegaría acá y ninguna prueba lo notaría.
184
+ concerns: { ...DECISION.properties.concerns,
185
+ items: { ...DECISION.properties.concerns.items,
186
+ properties: { ...DECISION.properties.concerns.items.properties, decision: { type: 'boolean' } },
187
+ } },
188
+ } }
158
189
  // Un exit code dice que el test corrió, no que pruebe lo que la tarea prometió: un test que asercia de
159
190
  // menos —o que ni existe— sale verde igual, y el guard de verify tampoco lo ve porque también mira exit
160
191
  // codes. Por eso `uncovered` se contrasta contra la aceptación leyendo el fuente, no la salida (R9).
@@ -301,9 +332,19 @@ const VERDICT = ' Cerrá con verdict=aprobado si no queda nada por corregir ante
301
332
  'alcance—. Marcá blocking=true sólo en el hallazgo que impide entregar: el resto queda registrado y no ' +
302
333
  'manda a tocar código.'
303
334
  // Acompaña a todo prompt con schema REVIEWED.
304
- const RULED = ' En rules nombrá, por su ruta, cada una de las reglas que rigen contra la que revisaste el diff.'
335
+ const RULED = ' En rules nombrá, por su ruta, cada una de las reglas que rigen contra la que revisaste'
336
+ + ' el diff. Y marcá decision=true en el hallazgo que no te toca resolver a vos —una definición de'
337
+ + ' producto, un contrato público, una autoridad que el cargo no tiene—: ése se registra para una'
338
+ + ' persona y no manda a tocar código.'
305
339
  // Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
306
- const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
340
+ // Una decisión no cuenta como bloqueante aunque venga marcada: su destino es la fila, no la corrección.
341
+ // Critique no la emite —no está en su esquema— así que para él `one.decision` es siempre `undefined`.
342
+ const blockers = (verdict) => verdict.concerns
343
+ .filter((one) => one.blocking && !one.decision).map((one) => one.detail)
344
+ // Lo que una parada tiene que nombrar es todo lo que el revisor señaló, no sólo lo que manda a corregir:
345
+ // con `blockers` solo, un veredicto cuyo único hallazgo es una decisión paraba diciendo que nadie nombró
346
+ // ninguna condición. El motivo es lo único que queda para leer cuando la corrida terminó.
347
+ const named = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
307
348
  // El resultado de Build cuando no hubo nada que construir en esta corrida. Devuelve lo que de verdad
308
349
  // pasó y nada más: `redFirst` y `discovered` van **vacíos** porque acá no hubo ningún rojo nuevo que
309
350
  // mostrar ni ningún borde nuevo que fijar, y rellenarlos para que se parezca a una construcción sería
@@ -406,13 +447,32 @@ const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
406
447
  const run = (prompt, options = {}) => agent(`${SCOPE()}\n\n${prompt}`, options)
407
448
  const write = (prompt, options = {}) => agent(`${LEDGER()}\n\n${prompt}`, options)
408
449
 
409
- // Las tres paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es
410
- // el único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso
411
- // se espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó.
412
- // Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó pedida. Caso 087.
413
- const registerHuman = async (prompt, label) => (await write(prompt, { label })
414
- ? ''
415
- : ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`)
450
+ // Las paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es el
451
+ // único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso se
452
+ // espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó (caso 087).
453
+ // Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó como tiene que quedar.
454
+ //
455
+ // Contestar no alcanza: el agente que acaba de escribir el diagnóstico entero lo lee como ya resuelto, y
456
+ // en una corrida real escribió la fila `resuelta` y la firmó como «decidido por el dueño», desbloqueando
457
+ // la tarea sin que nadie decidiera nada (caso 180). Por eso cada pedido arranca diciendo que la fila nace
458
+ // pendiente, y cuando la fila es de la propia tarea se relee en `context`, que sólo lista las pendientes.
459
+ const HUMAN_ROW_STATE = 'La fila nace con estado `pendiente`, sin excepción: registrás el bloqueo, no lo '
460
+ + 'resolvés —lo resuelve una persona—. No escribas una decisión ni se la atribuyas a nadie.'
461
+ const registerHuman = async (prompt, label, slug = '') => {
462
+ if (!(await write(`${HUMAN_ROW_STATE}\n\n${prompt}`, { label }))) {
463
+ return ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`
464
+ }
465
+ if (!slug) return ''
466
+ const row = await read(
467
+ `Corré "node tools/ops.js context ${P} --json" desde ${ROOT}. Poné pending en true sólo si humanActions `
468
+ + `trae una fila cuya task sea ${slug}, y readOk en true sólo si el comando salió con código 0 y devolvió `
469
+ + 'JSON. El comando es la fuente de verdad: no abras archivos de planning.',
470
+ { schema: HUMAN_ROW, label: 'human-row' },
471
+ )
472
+ if (!row || !row.readOk) return ` — no se pudo comprobar la fila de ${slug} en ${HUMAN}: revisala a mano`
473
+ return row.pending ? ''
474
+ : ` — la fila de ${slug} en ${HUMAN} no quedó pendiente: la resuelve una persona, revisala a mano`
475
+ }
416
476
 
417
477
  // Gate, mutex de WIP y selección de tarea salen de un comando determinista: AWAITING_REVIEW, BACKLOG,
418
478
  // WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
@@ -423,7 +483,8 @@ const readContext = () => read(
423
483
  `omitilo entero si wip es null, sin inventar ceros—, y lane ` +
424
484
  `de task.tier; copiá slug, ` +
425
485
  `hito, service, acceptance, ` +
426
- `epic y cast de task, epicContext de epic.context —vacío si no hay épica— e inbox tal cual. El comando es ` +
486
+ `epic, cast y description de task, epicContext de epic.context —vacío si no hay épica— e inbox tal `
487
+ + `cual. El comando es ` +
427
488
  `la fuente de ` +
428
489
  `verdad: no abras archivos de planning para completarlo. Poné readOk en true sólo si el comando salió ` +
429
490
  `con código 0 y devolvió JSON; si falló, readOk en false y el resto en sus valores vacíos, sin ` +
@@ -485,10 +546,35 @@ while (rounds++ < MAX_TASKS) {
485
546
  //
486
547
  // `context` nombra la que sigue, igual que nombra una recurrencia vencida y por el mismo motivo: la
487
548
  // máquina calcula y la persona encola.
549
+ // Una cola con trabajo y sin tarea disponible no es una cola terminada. Romper el bucle igual deja la
550
+ // corrida informando que no había nada que hacer sobre una cola que sí tiene tareas —reclamadas por otro
551
+ // runner, o esperando una dependencia—, que es la forma del caso 075: una respuesta vacía que se lee
552
+ // como un hecho del dominio. El dato ya venía: `queued` cuenta la cola entera, y `context` además la
553
+ // nombra con su dueño en la línea TAKEN.
554
+ //
555
+ // Se paga sobre todo después de una parada que espera a una persona: ahí el reclamo se queda puesto a
556
+ // propósito —quien paró va a volver, y `claim` le es idempotente—, así que cuando la persona contesta es
557
+ // un runner distinto el que pregunta y el que se va sin nada.
558
+ if (!planning.hasTask && planning.queued > 0) {
559
+ return stop('queue-unavailable', `la cola tiene ${planning.queued} tarea(s) y ninguna disponible para `
560
+ + 'este runner. Mirá la línea TAKEN de "ops context": lo reclamado por otro se suelta con '
561
+ + '"ops release" o se retoma desde el runner que lo tiene; lo que espera una dependencia, no.')
562
+ }
488
563
  if (!planning.hasTask || (currentMilestone && planning.hito !== currentMilestone)) break
564
+ // Las decisiones que la línea ya tomó, dichas como lo que son. Sin ese rótulo se leen como contexto
565
+ // opinable y el que planifica las re-decide igual, que es el defecto entero: la crítica abre el
566
+ // BACKLOG por su cuenta y bloquea el plan citando la línea palabra por palabra (caso 177).
567
+ const DECIDED = () => (task.description
568
+ ? ` Lo que la línea de la tarea ya decidió, y no se re-decide acá: ${task.description}`
569
+ : '')
570
+ // Sobrevive al bloque del plan porque Build vive afuera —y porque una corrida que reanuda desde un WIP
571
+ // no tiene plan en memoria—. Sin esto la estrategia no era que se descartara: es que no estaba en
572
+ // alcance, que es la forma que ninguna prueba de la fase ve.
573
+ let testStrategy = ''
489
574
  const task = {
490
575
  id: planning.slug, hito: planning.hito, service: planning.service,
491
576
  acceptance: planning.acceptance, epic: planning.epic, epicContext: planning.epicContext || '',
577
+ description: planning.description || '',
492
578
  }
493
579
  // Reservar antes de construir, y antes de fijar el hito de la corrida. Sin esto dos corridas en
494
580
  // paralelo trabajan la misma tarea: `context` sólo puede saltear lo que alguien ya reclamó, y el
@@ -614,12 +700,32 @@ while (rounds++ < MAX_TASKS) {
614
700
  //
615
701
  // Lo que la fila le pide a una persona lo dice R17: dos rechazos sobre lo mismo son el disparador
616
702
  // posterior de división. No se parte acá porque partir es una decisión, y ésa no le toca al recorrido.
703
+ // Una parada que registra la fila **bloquea** la tarea: la fila pendiente la saca de la cola. Si además
704
+ // el reclamo se queda puesto, el runner queda ocupado por algo que nadie puede tomar, y la corrida
705
+ // siguiente —que hace bien en no reintentarla— muere reclamando la que sigue: `claim` le contesta «este
706
+ // runner ya tiene …». Es la forma del 163 por la otra puerta, y la diferencia es que el estado lo creó
707
+ // esta corrida, así que soltarlo también le toca (caso 176).
708
+ //
709
+ // Sólo las paradas de **antes** de construir. Las de después —`verify-regression`, `qa-failed`,
710
+ // `commit-failed`— dejan el WIP en disco y es resumible: ahí el reclamo es lo único que dice de quién es
711
+ // ese trabajo, y soltarlo lo abandonaría.
712
+ //
713
+ // Cuesta un agente, porque el recorrido no tiene con qué correr un comando. Un agente contra una
714
+ // corrida entera.
715
+ const releaseBlocked = async () => write(
716
+ `Corré "node tools/ops.js release ${P} ${task.id}" desde ${ROOT}: quedó bloqueada por la fila que `
717
+ + 'acabás de registrar y todavía no hay nada construido, así que su reserva no reserva trabajo. No '
718
+ + 'escribas ningún archivo vos: lo escribe el comando.',
719
+ { label: `release:${task.id}` },
720
+ )
721
+
617
722
  const planRejected = async (reason, unit, found) => {
618
723
  const detail = found.join('; ') || 'sin condiciones nombradas'
619
724
  const nota = await registerHuman(
620
725
  `Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
621
726
  + `Motivo: ${detail}. La acción humana es revisar si la unidad son dos resultados con vidas `
622
- + `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human')
727
+ + `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human', unit.id)
728
+ await releaseBlocked()
623
729
  return stop(reason, `${detail}${nota}`)
624
730
  }
625
731
 
@@ -628,7 +734,8 @@ while (rounds++ < MAX_TASKS) {
628
734
  phase('Ready')
629
735
  const ready = await read(
630
736
  `${asRole(OWNERS.ready)}Revisá que ${task.id} tenga aceptación concreta, dependencias resueltas y ` +
631
- `ninguna decisión pendiente: ${task.acceptance}. Aclará la redacción y nada más; nunca amplíes el ` +
737
+ `ninguna decisión pendiente: ${task.acceptance}.${DECIDED()} Aclará la redacción y nada más; ` +
738
+ `nunca amplíes el ` +
632
739
  `alcance.`,
633
740
  { schema: READY, label: 'ready' },
634
741
  )
@@ -636,7 +743,8 @@ while (rounds++ < MAX_TASKS) {
636
743
  if (!ready.ready) {
637
744
  const nota = await registerHuman(
638
745
  `Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
639
- 'ready-human')
746
+ 'ready-human', task.id)
747
+ await releaseBlocked()
640
748
  return stop('not-ready', `${ready.reason}${nota}`)
641
749
  }
642
750
  if (ready.refinedAcceptance) task.acceptance = ready.refinedAcceptance
@@ -703,7 +811,8 @@ while (rounds++ < MAX_TASKS) {
703
811
  `vecinas y el git status de ${task.id}.` +
704
812
  `${task.epicContext ? ` Contexto de la épica: ${task.epicContext}` : ''}` +
705
813
  ` Producí el plan más chico que satisfaga ` +
706
- `${task.acceptance}. Un archivo de planning no puede ser un archivo de implementación. El plan cubre ` +
814
+ `${task.acceptance}.${DECIDED()} Un archivo de planning no puede ser un archivo de implementación. ` +
815
+ `El plan cubre ` +
707
816
  `sólo el cambio dentro de ${task.service}: correr los gates del repositorio, hacer QA, commitear y ` +
708
817
  `cerrar la tarea son fases posteriores de este recorrido, cada una con su dueño, así que no van como ` +
709
818
  `pasos.`,
@@ -714,7 +823,7 @@ while (rounds++ < MAX_TASKS) {
714
823
  phase('Critique')
715
824
  let critique = await read(
716
825
  `Atacá este plan por correctitud, alcance, seguridad, pruebas y conflictos con el código ` +
717
- `existente.${MANIFEST}${VERDICT} Plan: ${JSON.stringify(plan)}`,
826
+ `existente.${DECIDED()}${MANIFEST}${VERDICT} Plan: ${JSON.stringify(plan)}`,
718
827
  { schema: DECISION, label: 'critique' },
719
828
  )
720
829
  if (!critique) return stop('agent-unavailable', 'Critique no devolvió resultado')
@@ -729,7 +838,7 @@ while (rounds++ < MAX_TASKS) {
729
838
  { schema: PLAN, label: 'replan' },
730
839
  )
731
840
  critique = await read(
732
- `Volvé a criticar el plan corregido contra ${task.acceptance}.${MANIFEST}${VERDICT} ` +
841
+ `Volvé a criticar el plan corregido contra ${task.acceptance}.${DECIDED()}${MANIFEST}${VERDICT} ` +
733
842
  `Plan: ${JSON.stringify(plan)}`,
734
843
  { schema: DECISION, label: 'critique' },
735
844
  )
@@ -742,6 +851,7 @@ while (rounds++ < MAX_TASKS) {
742
851
  if (!critique.consulted.length) return stop('critique-unbacked', 'aprobó el plan sin declarar qué inspeccionó')
743
852
  }
744
853
  }
854
+ testStrategy = plan.testStrategy || ''
745
855
  phase('WIP')
746
856
  // Esta llamada escribe un archivo y nada más, y hay que decirlo con todas las letras. En una corrida
747
857
  // real hizo el trabajo entero: leyó los pasos del plan como una orden, implementó, corrió RED/GREEN,
@@ -754,7 +864,15 @@ while (rounds++ < MAX_TASKS) {
754
864
  `en DONE. Los pasos van sin tildar porque todavía no ocurrieron. task=${task.id}, ` +
755
865
  `hito=${JSON.stringify(task.hito)}, phase=Build, service=${task.service}, ` +
756
866
  `acceptance=${JSON.stringify(task.acceptance)}, lane=${planning.lane || 'sin clasificar'}, ` +
757
- `pasos sin tildar=${JSON.stringify(plan.steps)}. ` +
867
+ `pasos sin tildar=${JSON.stringify(plan.steps)}, ` +
868
+ // La estrategia de prueba es `required` en el plan y hasta acá se descartaba, así que un paso que
869
+ // decía «correr la mutación declarada en testStrategy» apuntaba a un lugar que no existía: quien
870
+ // revisa no podía distinguir la mutación corrida de la pensada, y la única salida que le quedaba
871
+ // era rehacer la revisión. R9 pide la mutación **declarada**, y el WIP es donde queda escrita.
872
+ //
873
+ // Es la tercera vez que algo decidido no llega a quien decide después: el contexto de la épica
874
+ // (027), la descripción de la tarea (177) y esto. Las tres se arreglan igual — que viaje.
875
+ `testStrategy=${JSON.stringify(testStrategy)}. ` +
758
876
  `Registrá el reparto de cargos ${JSON.stringify(cast)} en las decisiones del WIP, para que después se ` +
759
877
  `pueda auditar quién revisó qué. Seguí el contrato de WIP exactamente y reportá con qué status quedó.`,
760
878
  { label: 'wip', schema: {
@@ -798,7 +916,8 @@ while (rounds++ < MAX_TASKS) {
798
916
  `nombrás en test y anotás en redFirst—, kind=open si lo notaste y no impide entregar la aceptación: se ` +
799
917
  `registra para que lo decida quien corresponde y el recorrido sigue. Si de verdad no podés entregar sin ` +
800
918
  `esa decisión, eso no va en discovered: es completed=false con su blocker. ` +
801
- `Aceptación: ${task.acceptance}.`,
919
+ `Aceptación: ${task.acceptance}.${DECIDED()}`
920
+ + (testStrategy ? ` Estrategia de prueba que el plan fijó: ${testStrategy}` : ''),
802
921
  { schema: BUILD, label: 'build' },
803
922
  )
804
923
  if (!build) return stop('agent-unavailable', 'Build no devolvió resultado')
@@ -819,8 +938,16 @@ while (rounds++ < MAX_TASKS) {
819
938
  // tiene camino —`completed: false` con su blocker—; esto se registra y sigue.
820
939
  const openDecisions = build.discovered.filter((entry) => entry.kind === 'open')
821
940
  if (openDecisions.length) {
941
+ // Y la fila no puede nombrar a la tarea que la produjo. El motor bloquea por esa primera celda
942
+ // exacta, así que escribirla ahí registra «esto no impide entregar» y produce el bloqueo igual —lo
943
+ // contrario de lo que este registro decidió dos líneas arriba—. No se nota mientras la corrida vive,
944
+ // porque el WIP activo manda sobre la acción humana; aparece cuando el WIP cierra, y entonces la
945
+ // tarea queda frenada por una pregunta que ya se había resuelto seguir sin contestar. En la corrida
946
+ // que lo mostró la atrapó Review, tres fases después de escribirla.
822
947
  await write(`Registrá en ${HUMAN} una fila por cada decisión que ${task.id} dejó abierta, con qué la ` +
823
- `cierra y quién puede tomarla. No inventes responsables ni fechas: ` +
948
+ `cierra y quién puede tomarla. La primera columna nunca es ${task.id}: el motor bloquea por esa ` +
949
+ `celda exacta y estas decisiones no impiden entregarla. Va la épica, el hito o el recorrido al que ` +
950
+ `alcanza la decisión. No inventes responsables ni fechas: ` +
824
951
  `${JSON.stringify(openDecisions.map((entry) => entry.detail))}`, { label: 'open-decisions' })
825
952
  }
826
953
  // Y un caso que sí se fijó acá entra con su prueba o no entró: sin ella el comportamiento nuevo queda
@@ -830,11 +957,25 @@ while (rounds++ < MAX_TASKS) {
830
957
  // cadena exacta frena una tarea correcta por haber nombrado el test de dos formas —`TestAlta` acá y
831
958
  // `users_test.go::TestAlta` allá—. Alcanza con que uno nombre al otro; lo que sigue frenando, que es de
832
959
  // lo que se trata, es el caso que no aparece en ningún rojo.
833
- const namesTest = (red, item) => Boolean(item.test)
834
- && (red.test.includes(item.test) || item.test.includes(red.test))
960
+ //
961
+ // Y la contención sola no alcanza, porque el caso que aparece no es que uno esté contenido en el otro:
962
+ // los dos nombran el mismo test y cada uno le agrega **su propia** anotación entre paréntesis —dónde
963
+ // está la línea de un lado, por qué se vio en rojo del otro—. Ahí ninguno contiene al otro y la puerta
964
+ // frenaba una entrega correcta, que es lo que R26 dice que termina apagándola. Se compara sin esa
965
+ // anotación final; una que esté en el medio se conserva, porque ahí sí es parte del nombre.
966
+ const core = (name) => String(name || '').replace(/\s*\([^)]*\)\s*$/, '').trim()
967
+ const namesTest = (red, item) => Boolean(core(item.test))
968
+ && (core(red.test).includes(core(item.test)) || core(item.test).includes(core(red.test)))
835
969
  const loose = build.discovered.find((entry) => entry.kind === 'edge'
836
970
  && !build.redFirst.some((red) => namesTest(red, entry)))
837
- if (loose) return stop('edge-unproven', `${loose.detail} entró sin la prueba que lo fija`)
971
+ // El motivo dice qué comprobó la puerta y no una conclusión sobre el trabajo: pegarle al detalle del
972
+ // build un «entró sin la prueba que lo fija» producía una parada que se contradecía sola cuando el
973
+ // detalle contaba que la prueba sí estaba —la frase del agente y la de la puerta hablaban de cosas
974
+ // distintas y se leían como una—.
975
+ if (loose) {
976
+ return stop('edge-unproven', `${loose.detail} — su campo "test" (${loose.test || 'vacío'}) no nombra `
977
+ + `ninguno de los rojos declarados: ${build.redFirst.map((red) => red.test).join(' | ') || '(ninguno)'}`)
978
+ }
838
979
 
839
980
  // Qué revisión hubo, para que el cierre no pueda inventar una. Nace diciendo que no hubo porque
840
981
  // `express` no convoca a nadie, y ése es el caso que se escribió como si un cargo hubiera aprobado.
@@ -848,17 +989,35 @@ while (rounds++ < MAX_TASKS) {
848
989
  { schema: REVIEWED, label: 'review' },
849
990
  )
850
991
  if (!review) return stop('agent-unavailable', 'Review no devolvió resultado')
992
+ // Se junta apenas cada pasada contesta, y no al final: las paradas de abajo salen antes de llegar al
993
+ // registro, y sin esto lo que el revisor señaló se iba con la corrida.
994
+ const reviewDecisions = review.concerns.filter((one) => one.decision)
995
+ let decidedNote = ''
851
996
  if (review.verdict === 'bloqueado') {
852
- return stop('review-blocked', blockers(review).join('; ') || 'sin condiciones nombradas')
997
+ return stop('review-blocked', named(review).join('; ') || 'sin condiciones nombradas')
853
998
  }
854
999
  if (blockers(review).length) {
855
- await write(`Corregí sólo estos hallazgos con evidencia y actualizá el WIP: ${blockers(review).join('; ')}`,
1000
+ // «Sólo estos hallazgos» acota el alcance (R6) y por sí solo deja un cabo suelto: una corrección
1001
+ // tiene dependientes y no se anuncian —el conteo que enumeraba lo que cambió, el comentario que
1002
+ // describía la forma vieja, la fila que la afirmaba—. Es R9 leído al derecho: lo que deja de valer
1003
+ // se lleva puesto a quien lo daba por cierto, y eso vive casi siempre en otro archivo.
1004
+ //
1005
+ // Sin pedirlo, la vuelta siguiente rechaza por la deriva que la corrección acabó de crear, y como
1006
+ // la vuelta es una sola eso termina en `review-failed` sobre trabajo correcto. Medido: una
1007
+ // corrección agregó una mutación y un caso de prueba, y la re-revisión frenó porque la fila de
1008
+ // acciones humanas seguía diciendo el número viejo y dos comentarios seguían contando los casos
1009
+ // anteriores —corrida `wf_99130468-2c4`, 2026-09-17, sobre un banco desechable—. Traerlos no
1010
+ // amplía el alcance: es terminar la corrección.
1011
+ await write(`Corregí sólo estos hallazgos con evidencia y actualizá el WIP: ${blockers(review).join('; ')}. `
1012
+ + 'Traé también lo que tu propia corrección deje desactualizado —un conteo, un comentario que '
1013
+ + 'describa la forma vieja, una fila que la enumere— y nada más que eso.',
856
1014
  { label: 'review-fix' })
857
1015
  review = await run(`Volvé a revisar el diff corregido de ${task.id}.${MANIFEST}${VERDICT}${RULED}`,
858
1016
  { schema: REVIEWED, label: 'review' })
859
1017
  if (!review) return stop('agent-unavailable', 'la re-revisión no devolvió resultado')
1018
+ reviewDecisions.push(...review.concerns.filter((one) => one.decision))
860
1019
  if (review.verdict === 'bloqueado' || blockers(review).length) {
861
- return stop('review-failed', blockers(review).join('; ') || 'sin condiciones nombradas')
1020
+ return stop('review-failed', named(review).join('; ') || 'sin condiciones nombradas')
862
1021
  }
863
1022
  }
864
1023
  // Con reglas que rigen, aprobar sin nombrar contra cuáles es la misma falla que la de abajo en otro eje.
@@ -871,7 +1030,30 @@ while (rounds++ < MAX_TASKS) {
871
1030
  // Contra qué reglas se revisó va también a la entrada de DONE, y no sólo al journal: el journal muere
872
1031
  // con la corrida y la entrada queda, así que sin esto no había cómo reconstruir contra cuáles se
873
1032
  // revisó cuando alguien audita la entrega meses después (caso 122).
1033
+ // Lo que la revisión encontró y no le toca resolver va a la fila que una persona lee. No al INBOX, que
1034
+ // es para propuestas: quien lo lea ahí puede promoverlo, y esto no se promueve, se decide.
1035
+ //
1036
+ // Se acumula desde la primera pasada y no se lee sólo de la última: al corrector se le pasa nada más
1037
+ // lo que manda a corregir, así que nunca se entera de la decisión y no la puede cerrar. Leyendo sólo
1038
+ // la re-revisión, una decisión que el agente no repitiera desaparecía sin dejar rastro, y el mismo
1039
+ // camino la perdía entera cuando la revisión paraba antes de llegar acá.
1040
+ //
1041
+ // El tope es el mismo del INBOX y por la misma razón: una revisión que deja treinta filas en una tabla
1042
+ // que lee una persona no está priorizando (caso 101). Lo que no entra queda contado en el hecho.
1043
+ const decided = [...new Set(reviewDecisions.map((one) => one.detail))]
1044
+ const filed = decided.slice(0, INBOX_CAP)
1045
+ if (filed.length) {
1046
+ // La nota que devuelve viaja al hecho: sin ella la entrega afirma una fila que el disco no tiene,
1047
+ // que es el caso 087 entrando por otra puerta.
1048
+ const nota = await registerHuman(`Registrá en ${HUMAN} una fila por cada decisión que la revisión de `
1049
+ + `${task.id} dejó abierta, con qué la cierra y quién puede tomarla. La primera columna nunca es `
1050
+ + `${task.id} —el porqué es el mismo que en Build—: va la épica, el hito o el recorrido al que `
1051
+ + `alcanza. No inventes responsables ni fechas: ${JSON.stringify(filed)}`, 'review-human')
1052
+ decidedNote = `${nota}`
1053
+ }
874
1054
  reviewFact = `${review.verdict} por ${cast.review}, sobre ${review.consulted.join(', ')}`
1055
+ + (filed.length ? ` · ${filed.length} decisión(es) registrada(s)${decidedNote}` : '')
1056
+ + (decided.length > filed.length ? ` · ${decided.length - filed.length} decisión(es) sin volcar` : '')
875
1057
  + ((review.rules || []).length ? ` · reglas: ${review.rules.join(', ')}` : '')
876
1058
  // Lo que no impide entregar no manda a tocar código, y tampoco desaparece: la mejora opinable que se
877
1059
  // corrige a las apuradas cuesta una vuelta y un riesgo que nadie pidió. Va a Propuestas y no a
@@ -881,7 +1063,10 @@ while (rounds++ < MAX_TASKS) {
881
1063
  // De qué vía salió cada una: este recorrido, la tarea que la dejó anotada y la fecha del motor. La
882
1064
  // arma el recorrido, que es el único de los dos que sabe las tres cosas (caso 115).
883
1065
  const origin = inboxOrigin('autobuild', task.id, planning.today)
884
- const noted = review.concerns.filter((one) => !one.blocking).map((one) => withOrigin(one.detail, origin))
1066
+ // Lo marcado como decisión ya tiene destino y no se duplica acá: escrito en los dos lados, además de
1067
+ // aparecer dos veces, le come una ranura del tope a una propuesta que sí lo era.
1068
+ const noted = review.concerns.filter((one) => !one.blocking && !one.decision)
1069
+ .map((one) => withOrigin(one.detail, origin))
885
1070
  const kept = noted.slice(0, INBOX_CAP)
886
1071
  // Lo que pasa del tope no se escribe y tampoco desaparece: queda contado en el hecho de revisión, que
887
1072
  // viaja a `done/`. Una revisión que anota treinta y seis cosas no está priorizando, y el INBOX no las
@@ -925,7 +1110,7 @@ while (rounds++ < MAX_TASKS) {
925
1110
  if (ambiguous) {
926
1111
  const nota = await registerHuman(
927
1112
  `Registrá ${task.id} en ${HUMAN}: el criterio "${ambiguous.criterion}" no dice qué habría ` +
928
- `que aserciar, y hace falta la decisión que lo fija.`, 'verify-human')
1113
+ `que aserciar, y hace falta la decisión que lo fija.`, 'verify-human', task.id)
929
1114
  return stop('acceptance-ambiguous', `${ambiguous.criterion}${nota}`)
930
1115
  }
931
1116
  if (verified.uncovered.length) {
@@ -1009,9 +1194,14 @@ const closing = await write(
1009
1194
  )
1010
1195
  if (!closing) return stop('agent-unavailable', 'Closing no devolvió resultado')
1011
1196
  if (!closing.passed) return stop('planning-check-failed', closing.details)
1197
+ // El archivo nace con su estado escrito porque la compuerta lo lee de ahí —R28, y el porqué vive junto a
1198
+ // esa lectura—. Sin decirlo acá la fase escribe prosa sin `status`, y la instancia queda con una compuerta
1199
+ // que sólo se destraba borrando: la forma que la regla prohíbe, escrita por el propio recorrido.
1012
1200
  if (completed.length && contract.humanCheckpoint) await write(
1013
1201
  `Creá ${GATE} con el hito terminado, las tareas ${completed.join(', ')}, la evidencia, las acciones humanas ` +
1014
- `pendientes y las instrucciones exactas para continuar. Nunca hagas push ni deploy.`,
1202
+ `pendientes y las instrucciones exactas para continuar. Arrancá el archivo con un frontmatter ` +
1203
+ `"status: pendiente", y decí que se destraba cambiándolo a "resuelta" —no borrando el archivo, que es ` +
1204
+ `lo que deja leer después qué se revisó—. Nunca hagas push ni deploy.`,
1015
1205
  { label: 'human-checkpoint' },
1016
1206
  )
1017
1207
  return finish({ done: completed, count: completed.length, hito: currentMilestone, phases: ran })