@ingeniomaps/cauce 0.69.0 → 0.71.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 (168) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +6 -6
  3. package/agents/README.md +38 -0
  4. package/agents/roles/system/accounting-specialist/learning/HISTORY.md +2 -0
  5. package/agents/roles/system/ai-governance-lead/learning/HISTORY.md +6 -1
  6. package/agents/roles/system/ai-governance-lead/learning/sources.yaml +4 -4
  7. package/agents/roles/system/ai-governance-lead/references/operating-model.md +2 -2
  8. package/agents/roles/system/ai-product-manager/learning/HISTORY.md +4 -1
  9. package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
  10. package/agents/roles/system/ai-product-manager/references/operating-model.md +2 -2
  11. package/agents/roles/system/analytics-engineer/learning/HISTORY.md +4 -1
  12. package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -2
  13. package/agents/roles/system/analytics-engineer/references/operating-model.md +2 -2
  14. package/agents/roles/system/backend-engineer/learning/HISTORY.md +2 -0
  15. package/agents/roles/system/business-strategist/learning/HISTORY.md +2 -0
  16. package/agents/roles/system/business-strategist/learning/sources.yaml +1 -1
  17. package/agents/roles/system/business-strategist/references/operating-model.md +2 -2
  18. package/agents/roles/system/cloud-architect/learning/HISTORY.md +1 -1
  19. package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
  20. package/agents/roles/system/cloud-architect/references/operating-model.md +2 -2
  21. package/agents/roles/system/community-manager/learning/HISTORY.md +4 -1
  22. package/agents/roles/system/community-manager/learning/sources.yaml +2 -2
  23. package/agents/roles/system/community-manager/references/operating-model.md +1 -1
  24. package/agents/roles/system/content-specialist/learning/HISTORY.md +2 -0
  25. package/agents/roles/system/customer-success-manager/learning/HISTORY.md +2 -0
  26. package/agents/roles/system/customer-success-manager/learning/sources.yaml +4 -4
  27. package/agents/roles/system/customer-success-manager/references/operating-model.md +3 -3
  28. package/agents/roles/system/customer-support-specialist/learning/HISTORY.md +2 -0
  29. package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -2
  30. package/agents/roles/system/customer-support-specialist/references/operating-model.md +2 -2
  31. package/agents/roles/system/data-analyst/learning/HISTORY.md +2 -0
  32. package/agents/roles/system/data-engineer/learning/HISTORY.md +4 -1
  33. package/agents/roles/system/data-engineer/learning/sources.yaml +3 -3
  34. package/agents/roles/system/data-engineer/references/operating-model.md +2 -2
  35. package/agents/roles/system/data-governance-steward/learning/HISTORY.md +2 -0
  36. package/agents/roles/system/data-scientist/learning/HISTORY.md +4 -1
  37. package/agents/roles/system/data-scientist/learning/sources.yaml +3 -3
  38. package/agents/roles/system/data-scientist/references/operating-model.md +2 -2
  39. package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -1
  40. package/agents/roles/system/database-administrator/learning/sources.yaml +2 -2
  41. package/agents/roles/system/database-administrator/references/operating-model.md +2 -2
  42. package/agents/roles/system/developer-relations-engineer/learning/HISTORY.md +4 -1
  43. package/agents/roles/system/devops-engineer/learning/HISTORY.md +2 -0
  44. package/agents/roles/system/engineering-manager/learning/HISTORY.md +2 -0
  45. package/agents/roles/system/engineering-manager/learning/sources.yaml +1 -1
  46. package/agents/roles/system/engineering-manager/references/operating-model.md +2 -2
  47. package/agents/roles/system/financial-controller/learning/HISTORY.md +2 -0
  48. package/agents/roles/system/financial-controller/learning/sources.yaml +2 -2
  49. package/agents/roles/system/financial-controller/references/operating-model.md +1 -1
  50. package/agents/roles/system/finops-engineer/learning/HISTORY.md +2 -0
  51. package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +2 -0
  52. package/agents/roles/system/frontend-engineer/learning/HISTORY.md +2 -0
  53. package/agents/roles/system/growth-marketer/learning/HISTORY.md +2 -0
  54. package/agents/roles/system/growth-marketer/learning/sources.yaml +1 -1
  55. package/agents/roles/system/implementation-manager/learning/HISTORY.md +1 -1
  56. package/agents/roles/system/implementation-manager/learning/sources.yaml +4 -4
  57. package/agents/roles/system/implementation-manager/references/operating-model.md +3 -3
  58. package/agents/roles/system/integrations-engineer/learning/HISTORY.md +2 -0
  59. package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +3 -2
  60. package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +1 -1
  61. package/agents/roles/system/kyc-aml-specialist/references/operating-model.md +1 -1
  62. package/agents/roles/system/legal-counsel/learning/HISTORY.md +6 -1
  63. package/agents/roles/system/legal-counsel/learning/sources.yaml +1 -1
  64. package/agents/roles/system/legal-counsel/references/operating-model.md +1 -1
  65. package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
  66. package/agents/roles/system/machine-learning-engineer/learning/HISTORY.md +4 -1
  67. package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +6 -6
  68. package/agents/roles/system/machine-learning-engineer/references/operating-model.md +3 -3
  69. package/agents/roles/system/mlops-engineer/learning/HISTORY.md +4 -1
  70. package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
  71. package/agents/roles/system/mlops-engineer/references/operating-model.md +2 -2
  72. package/agents/roles/system/mobile-engineer/learning/HISTORY.md +2 -0
  73. package/agents/roles/system/mobile-engineer/learning/sources.yaml +1 -1
  74. package/agents/roles/system/partnerships-manager/learning/HISTORY.md +4 -1
  75. package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -2
  76. package/agents/roles/system/partnerships-manager/references/operating-model.md +2 -2
  77. package/agents/roles/system/people-operations-manager/learning/HISTORY.md +4 -1
  78. package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -2
  79. package/agents/roles/system/people-operations-manager/references/operating-model.md +2 -2
  80. package/agents/roles/system/privacy-compliance-specialist/learning/HISTORY.md +2 -0
  81. package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +1 -1
  82. package/agents/roles/system/privacy-compliance-specialist/references/operating-model.md +1 -1
  83. package/agents/roles/system/procurement-manager/learning/HISTORY.md +1 -1
  84. package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -2
  85. package/agents/roles/system/procurement-manager/references/operating-model.md +2 -2
  86. package/agents/roles/system/product-manager/learning/HISTORY.md +1 -1
  87. package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +2 -0
  88. package/agents/roles/system/product-marketing-manager/learning/sources.yaml +1 -1
  89. package/agents/roles/system/project-manager/learning/HISTORY.md +4 -1
  90. package/agents/roles/system/project-manager/learning/sources.yaml +1 -1
  91. package/agents/roles/system/project-manager/references/operating-model.md +2 -2
  92. package/agents/roles/system/qa-engineer/learning/HISTORY.md +2 -0
  93. package/agents/roles/system/qa-engineer/learning/sources.yaml +9 -11
  94. package/agents/roles/system/qa-engineer/references/operating-model.md +2 -2
  95. package/agents/roles/system/release-manager/learning/HISTORY.md +1 -1
  96. package/agents/roles/system/sales-representative/learning/HISTORY.md +2 -0
  97. package/agents/roles/system/sales-representative/learning/sources.yaml +2 -2
  98. package/agents/roles/system/sales-representative/references/operating-model.md +1 -1
  99. package/agents/roles/system/security-engineer/learning/HISTORY.md +2 -0
  100. package/agents/roles/system/security-engineer/learning/sources.yaml +1 -1
  101. package/agents/roles/system/security-engineer/references/operating-model.md +1 -1
  102. package/agents/roles/system/site-reliability-engineer/learning/HISTORY.md +2 -0
  103. package/agents/roles/system/software-architect/learning/HISTORY.md +2 -0
  104. package/agents/roles/system/software-architect/learning/sources.yaml +2 -2
  105. package/agents/roles/system/software-architect/references/operating-model.md +1 -1
  106. package/agents/roles/system/solutions-engineer/learning/HISTORY.md +4 -1
  107. package/agents/roles/system/solutions-engineer/learning/sources.yaml +4 -4
  108. package/agents/roles/system/solutions-engineer/references/operating-model.md +2 -2
  109. package/agents/roles/system/tech-lead/learning/HISTORY.md +2 -0
  110. package/agents/roles/system/technical-program-manager/learning/HISTORY.md +4 -1
  111. package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -2
  112. package/agents/roles/system/technical-program-manager/references/operating-model.md +3 -3
  113. package/agents/roles/system/technical-writer/learning/HISTORY.md +4 -1
  114. package/agents/roles/system/treasury-analyst/learning/HISTORY.md +2 -0
  115. package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
  116. package/agents/roles/system/ui-designer/learning/sources.yaml +2 -2
  117. package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -1
  118. package/agents/roles/system/user-researcher/learning/sources.yaml +1 -1
  119. package/agents/roles/system/ux-designer/learning/HISTORY.md +2 -0
  120. package/agents/roles/system/ux-designer/learning/sources.yaml +1 -1
  121. package/agents/roles/system/ux-designer/references/operating-model.md +1 -1
  122. package/automatization/AGENTS.md +1 -1
  123. package/automatization/runners/antigravity/rules/cauce.md +1 -1
  124. package/automatization/runners/claude/CLAUDE.md +1 -1
  125. package/automatization/runners/codex/AGENTS.md +1 -1
  126. package/automatization/runners/gemini/GEMINI.md +1 -1
  127. package/automatization/shared/skills/autobuild/SKILL.md +1 -1
  128. package/automatization/workflows/autobuild.js +81 -27
  129. package/engine/agents/learning-seal.js +36 -5
  130. package/engine/agents/learning-sources.js +46 -2
  131. package/engine/agents/learning.js +13 -1
  132. package/engine/cli/archive.js +87 -0
  133. package/engine/cli/args.js +6 -2
  134. package/engine/cli/catalog.js +9 -1
  135. package/engine/cli/claims.js +125 -0
  136. package/engine/cli/ops.js +15 -4
  137. package/engine/cli/planning.js +139 -107
  138. package/engine/cli/worktree.js +89 -0
  139. package/engine/core/ownership.js +13 -5
  140. package/engine/core/repos.js +67 -0
  141. package/engine/hooks/files.js +3 -2
  142. package/engine/planning/adoption.js +1 -1
  143. package/engine/planning/claims.js +153 -0
  144. package/engine/planning/contracts.js +43 -202
  145. package/engine/planning/parser.js +51 -12
  146. package/engine/planning/state.js +39 -6
  147. package/engine/planning/structure.js +220 -0
  148. package/package.json +1 -1
  149. package/template/.gitattributes +19 -0
  150. package/template/AGENTS.md +47 -5
  151. package/template/automatization/AGENTS.md +1 -1
  152. package/template/gitignore +9 -0
  153. package/template/planning/BACKLOG.md +5 -0
  154. package/template/planning/FLOW.md +1 -1
  155. package/template/planning/PROTOCOL.md +34 -8
  156. package/template/planning/README.md +3 -3
  157. package/template/planning/RECURRING.md +1 -1
  158. package/template/planning/adr/system/OPS-001-planificacion-como-fuente-de-verdad.md +3 -2
  159. package/template/planning/business-rules/system/BR-OPS-001-una-sola-tarea-activa.md +7 -4
  160. package/template/planning/business-rules/system/BR-OPS-005-una-tarea-un-runner.md +44 -0
  161. package/template/planning/claims/README.md +70 -0
  162. package/template/planning/delivery/README.md +1 -0
  163. package/template/planning/delivery/multi-repo.md +11 -0
  164. package/template/planning/delivery/teamwork.md +162 -0
  165. package/template/planning/done/README.md +41 -0
  166. package/template/planning/wip/README.md +49 -0
  167. package/template/planning/DONE.md +0 -13
  168. package/template/planning/WIP.md +0 -22
@@ -15,7 +15,7 @@ Los cinco recorridos llegan como skills en `.agents/skills/` y se invocan con `$
15
15
  acá —son referencia, no un runtime compatible—, así que cada recorrido se ejecuta fase por fase
16
16
  siguiendo el protocolo.
17
17
 
18
- `{{OPS_DIR}}planning/WIP.md` es el mutex: una sola tarea activa. `{{OPS_DIR}}planning/AWAITING_REVIEW.md`
18
+ `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex: una sola tarea activa. `{{OPS_DIR}}planning/AWAITING_REVIEW.md`
19
19
  bloquea una corrida nueva hasta que un humano revise. Nada se promueve desde `INBOX.md` sin aprobación.
20
20
 
21
21
  Antes de cerrar, corré `node {{OPS_DIR}}tools/ops.js check {{OPS_DIR}}planning`.
@@ -8,7 +8,7 @@
8
8
  @{{OPS_DIR}}planning/rules/system/conduct.md
9
9
 
10
10
  `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/cauce:autobuild` fase por fase; los
11
- workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/WIP.md` es el mutex
11
+ workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/wip/<runner>.md` es el mutex
12
12
  y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
13
13
 
14
14
  Los hooks de `.gemini/settings.json` son obligatorios y bloquean por su cuenta. **Requieren que la
@@ -3,7 +3,7 @@ name: autobuild
3
3
  description: Ejecuta el siguiente hito aprobado siguiendo el protocolo Cauce.
4
4
  ---
5
5
 
6
- Lee completos `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/WIP.md`. Si existe
6
+ Lee completos `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/wip/`. Si existe
7
7
  `{{OPS_DIR}}planning/AWAITING_REVIEW.md`, detente y explica la acción humana pendiente. Toma solamente la primera tarea
8
8
  aprobada, persiste el estado en WIP y ejecuta Build, Review, Verify y QA en orden. Usa los comandos reales del
9
9
  servicio, registra evidencia verificable en DONE y detente en el checkpoint entre hitos. No hagas push ni
@@ -21,7 +21,7 @@ export const meta = {
21
21
  { title: 'Verify', detail: 'Los gates del servicio y la aceptación que ninguna prueba codifica' },
22
22
  { title: 'QA', detail: 'El comportamiento ejercitado como lo ve quien lo usa' },
23
23
  { title: 'Commit', detail: 'Conventional Commits, uno por naturaleza del diff, sin push' },
24
- { title: 'Done', detail: 'Cierre atómico en DONE con el WIP en IDLE' },
24
+ { title: 'Done', detail: 'Cierre atómico: evidencia escrita, cola y plan limpios, reserva suelta' },
25
25
  { title: 'Closing', detail: 'Check de planning y checkpoint humano del hito' },
26
26
  ],
27
27
  }
@@ -31,8 +31,8 @@ const CONFIG = `${ROOT}/ops.config.json`
31
31
  const P = `${ROOT}/planning`
32
32
  const ORG = `${ROOT}/organization`
33
33
  const BACKLOG = `${P}/BACKLOG.md`
34
- const DONE = `${P}/DONE.md`
35
- const WIP = `${P}/WIP.md`
34
+ // Una tarea cerrada escribe su propio archivo, así que dos corridas en paralelo no comparten ninguno.
35
+ const doneFile = (slug) => `${P}/done/${slug}.md`
36
36
  const HUMAN = `${P}/HUMAN_ACTIONS.md`
37
37
  const GATE = `${P}/AWAITING_REVIEW.md`
38
38
  const ROADMAP = `${P}/roadmap`
@@ -40,8 +40,12 @@ const ROADMAP = `${P}/roadmap`
40
40
  // Estado de planning tal como lo emite `ops context --json`; ningún modelo parsea BACKLOG ni WIP.
41
41
  const CONTEXT = {
42
42
  type: 'object', additionalProperties: false,
43
- required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast'],
43
+ required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
44
44
  properties: {
45
+ // Si el comando salió con error no hay estado que reportar, y `hasTask: false, queued: 0` es
46
+ // exactamente lo que un modelo completa cuando no tiene qué poner. Sin este campo esa invención se
47
+ // lee igual que una cola terminada, y Pick la toma como permiso para promover.
48
+ readOk: { type: 'boolean' },
45
49
  blocked: { type: 'string' }, hasTask: { type: 'boolean' }, wipActive: { type: 'boolean' },
46
50
  queued: { type: 'integer' }, slug: { type: 'string' }, hito: { type: 'string' },
47
51
  service: { type: 'string' }, acceptance: { type: 'string' }, epic: { type: 'string' },
@@ -58,11 +62,20 @@ const CONTEXT = {
58
62
  properties: { build: { type: 'string' }, review: { type: 'array', items: { type: 'string' } } },
59
63
  },
60
64
  blockedTasks: { type: 'array', items: { type: 'string' } },
65
+ // Si la tarea que `context` devolvió ya está reservada a nombre de este runner. Libre no significa
66
+ // que sea nuestra: significa que todavía la puede tomar cualquiera, y dos corridas en paralelo la
67
+ // reciben las dos.
68
+ claimed: { type: 'boolean' },
69
+ // La fecha de hoy según el motor. Este recorrido no tiene reloj propio a propósito.
70
+ today: { type: 'string' },
71
+ // Dónde va el plan de este runner. El nombre sale de su id y el recorrido no lo deriva: lo
72
+ // pregunta, igual que la fecha.
73
+ wipFile: { type: 'string' },
61
74
  },
62
75
  }
63
- const EXPANSION = {
64
- type: 'object', additionalProperties: false, required: ['expanded'],
65
- properties: { expanded: { type: 'boolean' }, hito: { type: 'string' }, reason: { type: 'string' } },
76
+ const CLAIM = {
77
+ type: 'object', additionalProperties: false, required: ['claimed'],
78
+ properties: { claimed: { type: 'boolean' }, details: { type: 'string' } },
66
79
  }
67
80
  const READY = {
68
81
  type: 'object', additionalProperties: false, required: ['ready', 'needsHuman'],
@@ -216,8 +229,16 @@ const CLASSIFICATION = {
216
229
 
217
230
  const CONTRACT = {
218
231
  type: 'object', additionalProperties: false,
219
- required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts'],
232
+ required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts',
233
+ 'rootOk'],
220
234
  properties: {
235
+ // `ROOT` viaja escrito en el workflow y es relativo al cwd de los agentes: si la sesión abrió en otra
236
+ // carpeta, todas las rutas resuelven a `<raíz>/<raíz>/…` y ninguna existe. Nada lo comprobaba, y el
237
+ // recorrido gastaba Triage entero sobre archivos ausentes antes de parar más abajo por otra causa,
238
+ // nombrando el planning en vez de la raíz de la que ese planning cuelga.
239
+ //
240
+ // Se pregunta acá porque acá ya se leen los cuatro archivos: cuesta un campo y ningún agente más.
241
+ rootOk: { type: 'boolean' },
221
242
  project: { type: 'string' }, workspaceRoots: { type: 'array', minItems: 1, items: { type: 'string' } },
222
243
  // La puerta que el proyecto declara, si la declara. Viaja con la raíz porque es de la base de código
223
244
  // y no del runner: un monorepo tiene una por servicio, y uno solo tiene una sola.
@@ -276,7 +297,9 @@ phase('Triage')
276
297
  // subagente como «Límites del proyecto» eran sólo los genéricos del toolkit.
277
298
  const contract = await agent(
278
299
  `${BASE}\n\nLeé ${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} y ${P}/PROTOCOL.md una sola vez y no ` +
279
- `leas nada más. Reportá los ` +
300
+ `leas nada más. Poné rootOk en true sólo si los cuatro existieron y los pudiste leer; si alguno no ` +
301
+ `estaba, rootOk en false y el resto en sus valores vacíos, sin deducirlos de otra fuente. ` +
302
+ `Reportá los ` +
280
303
  `valores de configuración textualmente: project, workspaceRoots como entradas "nombre → ruta", ` +
281
304
  `runner.maxTaskHours, runner.commitPerTask y runner.humanCheckpointBetweenMilestones como humanCheckpoint. ` +
282
305
  `En gates poné una entrada "ruta → comando" por cada workspaceRoot que declare \`verify\`, y ninguna por ` +
@@ -288,6 +311,12 @@ const contract = await agent(
288
311
  { schema: CONTRACT, label: 'contract-digest' },
289
312
  )
290
313
  if (!contract) return stop('contract-unavailable', `no se pudo leer ${CONFIG} ni ${P}/PROTOCOL.md`)
314
+ // Falla acá y nombrando la raíz, que es lo que hace falta para arreglarlo: parar más abajo mandaba a
315
+ // revisar el planning, y el planning está bien — lo que no existe es la carpeta de la que cuelga.
316
+ if (!contract.rootOk) {
317
+ return stop('root-unreadable', `${ROOT} no se pudo leer entero. Es una ruta relativa a la carpeta `
318
+ + `donde se abre la herramienta: comprobá desde dónde estás corriendo el recorrido.`)
319
+ }
291
320
 
292
321
  const bounds = contract.boundaries || []
293
322
  const limits = bounds.length ? ` Límites del proyecto: ${bounds.join('; ')}.` : ''
@@ -311,14 +340,22 @@ const write = (prompt, options = {}) => agent(`${LEDGER}\n\n${prompt}`, options)
311
340
  // WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
312
341
  const readContext = () => read(
313
342
  `Corré "node tools/ops.js context ${P} --json" desde ${ROOT} y reportá sólo lo que imprimió. Derivá hasTask ` +
314
- `de si task es null, wipActive de si wip es null y lane de task.tier; copiá slug, hito, service, acceptance, ` +
343
+ `de si task es null, wipActive de si wip es null, claimed del campo claimed, today y wipFile de sus ` +
344
+ `campos, y lane ` +
345
+ `de task.tier; copiá slug, ` +
346
+ `hito, service, acceptance, ` +
315
347
  `epic y cast de task, y epicContext de epic.context —vacío si no hay épica—. El comando es la fuente de ` +
316
- `verdad: no abras archivos de planning para completarlo.`,
348
+ `verdad: no abras archivos de planning para completarlo. Poné readOk en true sólo si el comando salió ` +
349
+ `con código 0 y devolvió JSON; si falló, readOk en false y el resto en sus valores vacíos, sin ` +
350
+ `deducir el estado de ninguna otra fuente.`,
317
351
  { schema: CONTEXT, label: 'planning-context' },
318
352
  )
319
353
 
320
354
  let planning = await readContext()
321
355
  if (!planning) return stop('context-unavailable', `no se pudo leer el estado de ${P}`)
356
+ // Que el agente conteste no significa que haya leído: el schema se completa igual con ceros. Parar acá
357
+ // cuesta una corrida; seguir sobre una lectura fallida escribe en el BACKLOG, y eso no se revierte solo.
358
+ if (!planning.readOk) return stop('context-unavailable', `${P} no se pudo leer; revisá la ruta y el cwd`)
322
359
  if (planning.blocked) return stop('awaiting-human-review', `${GATE} tiene un checkpoint humano sin resolver`)
323
360
 
324
361
  let currentMilestone = planning.wipActive ? planning.hito : ''
@@ -334,23 +371,38 @@ const classified = new Set()
334
371
 
335
372
  while (rounds++ < MAX_TASKS) {
336
373
  phase('Pick')
337
- if (!planning.hasTask && !planning.queued) {
338
- const expansion = await write(
339
- `Leé ${ROADMAP}. Expandí sólo la próxima épica abierta y aprobada en un hito nuevo de ${BACKLOG}, ` +
340
- `conservando el slug de cada historia, sus referencias a criterios y su servicio. Nunca promuevas ` +
341
- `${P}/INBOX.md. Reportá si escribiste algo.`,
342
- { schema: EXPANSION },
343
- )
344
- if (!expansion) return stop('agent-unavailable', 'Pick no devolvió resultado')
345
- if (!expansion.expanded) break
346
- planning = await readContext()
347
- if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
348
- }
374
+ // Sin tarea y con la cola vacía, la corrida termina. **No expande la próxima épica**, y eso no es una
375
+ // limitación sino la regla: el roadmap llama `open` a «candidata editable que aún no fue promovida al
376
+ // backlog», así que pegarla en la cola es promoverla — y BR-OPS-002 deja una propuesta fuera de la cola
377
+ // hasta que la apruebe una persona. El prompt que hacía esto pedía «la próxima épica abierta y
378
+ // aprobada», y «aprobada» no correspondía a ningún dato: una épica declara `epic`, `title`, `status` y
379
+ // `service`, y ninguno registra una aprobación.
380
+ //
381
+ // `context` nombra la que sigue, igual que nombra una recurrencia vencida y por el mismo motivo: la
382
+ // máquina calcula y la persona encola.
349
383
  if (!planning.hasTask || (currentMilestone && planning.hito !== currentMilestone)) break
350
384
  const task = {
351
385
  id: planning.slug, hito: planning.hito, service: planning.service,
352
386
  acceptance: planning.acceptance, epic: planning.epic, epicContext: planning.epicContext || '',
353
387
  }
388
+ // Reservar antes de construir, y antes de fijar el hito de la corrida. Sin esto dos corridas en
389
+ // paralelo trabajan la misma tarea: `context` sólo puede saltear lo que alguien ya reclamó, y el
390
+ // primero en preguntar todavía no reclamó nada. La ventana entre preguntar y reservar existe igual, y
391
+ // por eso perder la carrera no es un error: se relee y se sigue con la que quedó libre.
392
+ if (!planning.claimed && !planning.wipActive) {
393
+ phase('Claim')
394
+ const reserva = await write(
395
+ `Corré "node tools/ops.js claim ${P} ${task.id}" desde ${ROOT}. No escribas ningún archivo vos: lo ` +
396
+ `escribe el comando. claimed=true sólo con exit 0; si falla porque la tomó otro, claimed=false y ` +
397
+ `copiá el mensaje en details.`,
398
+ { schema: CLAIM, label: `claim:${task.id}` },
399
+ )
400
+ if (!reserva || !reserva.claimed) {
401
+ planning = await readContext()
402
+ if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
403
+ continue
404
+ }
405
+ }
354
406
  currentMilestone = task.hito
355
407
 
356
408
  // Ver la tarea y, si no está clasificada, clasificarla antes de ejecutarla. Se hace una vez por
@@ -535,7 +587,7 @@ while (rounds++ < MAX_TASKS) {
535
587
  `dio; recién después implementá. Un test que pasa antes de que exista el código no asercia lo que dice ` +
536
588
  `aserciar: endurecelo y volvé a correr hasta verlo fallar. Corré las pruebas que necesites para ver ese ` +
537
589
  `rojo y ese verde, y nada más: los gates completos, el QA, el commit y el cierre son fases posteriores, ` +
538
- `así que no toques ${DONE} ni ${BACKLOG} ni el status del WIP. Lo que el plan no previó va en discovered y ` +
590
+ `así que no toques ${P}/done/ ni ${BACKLOG} ni el status del WIP. Lo que el plan no previó va en discovered y ` +
539
591
  `no en el código a secas: kind=edge si esta tarea lo puede fijar —y entonces entra con su prueba, que ` +
540
592
  `nombrás en test y anotás en redFirst—, kind=open si lo notaste y no impide entregar la aceptación: se ` +
541
593
  `registra para que lo decida quien corresponde y el recorrido sigue. Si de verdad no podés entregar sin ` +
@@ -693,9 +745,11 @@ while (rounds++ < MAX_TASKS) {
693
745
 
694
746
  phase('Done')
695
747
  await write(
696
- `Cerrá ${task.id} de forma atómica: agregala bajo su hito en ${DONE} con evidencia de acept, done, qa, ` +
697
- `tests y commit; sacala junto con sus notas indentadas de ${BACKLOG}; cerrá su épica sólo si no queda ` +
698
- `ninguna tarea etiquetada; y dejá ${WIP} en status IDLE. En decisions no nombres una fase ni un cargo ` +
748
+ `Cerrá ${task.id} de forma atómica: escribí ${doneFile(task.id)} con su evidencia —acept, ` +
749
+ `fecha: ${planning.today}, done, qa, tests y commit, en el formato de entrada que trae este preámbulo—; ` +
750
+ `sacala junto con sus notas indentadas de ${BACKLOG}; cerrá su épica sólo si no queda ` +
751
+ `ninguna tarea etiquetada; dejá ${P}/${planning.wipFile} en status IDLE; y soltá la reserva corriendo ` +
752
+ `"node tools/ops.js release ${P} ${task.id}". En decisions no nombres una fase ni un cargo ` +
699
753
  `que no figure en estos hechos. Hechos: lane=${planning.lane || 'sin clasificar'}; ` +
700
754
  `review=${reviewFact}; fases=${ran.join(' → ')}; build=${build.summary}; ` +
701
755
  `verify=${JSON.stringify(verified.commands)}; qa=${qa.evidence}; commit=${commit.hash || commit.reason}.`,
@@ -12,6 +12,8 @@ const path = require('node:path')
12
12
  const { atomicWrite } = require('../core/files')
13
13
  const { isoDate, proposalFiles, proposalState, assertWritable, lastOfPeriod } = require('./learning-files')
14
14
  const { section } = require('../planning/parser')
15
+ // La misma identidad con la que se reclama una tarea: quién es la persona, no qué runner corre.
16
+ const { owner } = require('../planning/claims')
15
17
 
16
18
  // El cuerpo sin sellar, en los dos estados que produce el ciclo: «pendiente» lo escribe el molde y
17
19
  // «aprobada» la firma. Lo lee la guarda de más abajo y lo reemplaza el sello, así que vive una vez.
@@ -102,22 +104,51 @@ function archive(root, agent, period = '', kind = 'agent') {
102
104
  + 'Archivar es para lo que se miró y no cambia nada.',
103
105
  )
104
106
  }
107
+ // Quién archivó, que es la mitad que faltaba. Sellar saca el responsable del documento —lo escribió la
108
+ // firma— y archivar no pasa por firma, así que sale de la identidad de quien corre el comando: la misma
109
+ // que `claim` usa para decir de quién es una tarea. Sin esto la propuesta quedaba en «por definir» y no
110
+ // había forma de distinguir una decisión de un olvido.
111
+ const responsible = owner(root) || 'sin identificar'
105
112
  atomicWrite(file, text
106
113
  .replace(/^status:\s*\S+\s*$/m, 'status: archived')
107
114
  .replace(/^-[ \t]*Estado:[ \t]*pendiente[ \t]*$/mi, '- Estado: archivada')
115
+ .replace(/^-[ \t]*Responsable:[ \t]*por definir[ \t]*$/mi, `- Responsable: ${responsible}`)
108
116
  .replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`))
117
+ // La fila va para los dos tipos, y no sólo para los recorridos como en `seal`: allá los cargos los
118
+ // registra `agent-promote` al aplicar, y archivar no pasa por ningún workflow que lo haga.
119
+ // La raíz del cargo, que es `<cargo>/learning/proposals/<archivo>` sin sus tres últimos tramos:
120
+ // `appendHistory` agrega `learning/` por su cuenta.
121
+ //
122
+ // La celda del cambio lleva el criterio y no queda vacía. Es el único que este comando admite —archivar
123
+ // *es* decidir que no cambia nada— así que decirlo evita que la fila se lea como un registro a medias.
124
+ // Archivar por otra razón, como posponer, necesitaría un campo que hoy no existe.
125
+ appendHistory(path.dirname(path.dirname(path.dirname(file))), file, responsible,
126
+ 'Se miró y no cambia nada.', 'archivada')
109
127
  return { file, already: false }
110
128
  }
111
129
 
112
- // Una fila por propuesta aplicada. El cambio va en una línea: el documento entero está a un enlace, y
113
- // una tabla que lo repite entero deja de leerse.
114
- function appendHistory(target, file, responsible, change) {
130
+ // Una fila por propuesta cerrada, cualquiera sea el destino. El cambio va en una línea: el documento
131
+ // entero está a un enlace, y una tabla que lo repite entero deja de leerse.
132
+ //
133
+ // La decisión es un parámetro y no la constante `aplicada` porque la columna de la tabla se llama
134
+ // «Decisión» y hay dos: aplicar y archivar. Archivar no dejaba fila, así que una propuesta mirada y
135
+ // descartada era indistinguible de una que nadie miró — y eso lo pagaban los informes siguientes, que
136
+ // gastaban su recomendación explicando el estado en vez de su profesión.
137
+ const HISTORY_HEADER = '| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |\n|---|---|---|---|---|\n'
138
+
139
+ function appendHistory(target, file, responsible, change, decision = 'aplicada') {
115
140
  const history = path.join(target, 'learning', 'HISTORY.md')
116
141
  if (!fs.existsSync(history)) return
142
+ const previous = fs.readFileSync(history, 'utf8')
117
143
  const line = change.split('\n').map((one) => one.trim()).filter(Boolean)[0] || ''
118
- const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` | aplicada | ${responsible} `
144
+ const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` | ${decision} | ${responsible} `
119
145
  + `| ${line.slice(0, 160)} |\n`
120
- fs.appendFileSync(history, `${fs.readFileSync(history, 'utf8').endsWith('\n') ? '' : '\n'}${row}`)
146
+ // Dieciséis de los cincuenta y tres cargos tienen el archivo sin la tabla, sólo con su párrafo de
147
+ // encabezado, y la fila quedaba pegada ahí: en markdown eso no es una tabla sino texto con barras, y
148
+ // nadie lo veía porque el archivo se lee dos veces al año. Se agrega la cabecera antes de la primera
149
+ // fila en vez de exigir que ya esté, que es pedirle a cada cargo que se acuerde.
150
+ const cabecera = /^\|\s*Fecha\s*\|/m.test(previous) ? '' : `\n${HISTORY_HEADER}`
151
+ fs.appendFileSync(history, `${previous.endsWith('\n') ? '' : '\n'}${cabecera}${row}`)
121
152
  }
122
153
 
123
154
  module.exports = { seal, archive }
@@ -90,6 +90,12 @@ function sourceTiers(text) {
90
90
  return [...body.matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
91
91
  }
92
92
 
93
+ // Una entrada escrita en una sola línea: seis cargos del catálogo la escriben así y cuarenta y siete la
94
+ // reparten en varias. Leyendo sólo la segunda forma, esos seis no tenían el chequeo de URL duplicada que
95
+ // hay más abajo, y nada lo decía porque no encontrar duplicados y no mirar se ven igual.
96
+ const FLOW_ENTRY = /^\s*-\s*\{[^}]*\bname:\s*([^,}]+?)\s*,[^}]*\burl:\s*"?([^",}\s]+)/
97
+ const quitar = (value) => value.replace(/^['"]|['"]$/g, '')
98
+
93
99
  // Las fuentes de un cargo, por su URL. La misma URL con dos nombres es una sola fuente contada dos
94
100
  // veces: el catálogo llegó a tener la especificación OpenAPI bajo tres —`OpenAPI Specification`,
95
101
  // `...latest published` y `...3.2.0`— así que arreglarle el `tier` a un cargo no se lo arreglaba a los
@@ -99,14 +105,52 @@ function sourceUrls(text) {
99
105
  const out = []
100
106
  let name = ''
101
107
  for (const line of body.split('\n')) {
108
+ const flow = line.match(FLOW_ENTRY)
109
+ if (flow) { out.push({ name: quitar(flow[1]), url: flow[2].replace(/\/+$/, '') }); continue }
102
110
  const declared = line.match(/^\s*-\s*name:\s*(.+?)\s*$/)
103
- if (declared) { name = declared[1].replace(/^['"]|['"]$/g, ''); continue }
111
+ if (declared) { name = quitar(declared[1]); continue }
104
112
  const url = line.match(/^\s*url:\s*(\S+)/)
105
113
  if (url && name) out.push({ name, url: url[1].replace(/\/+$/, '') })
106
114
  }
107
115
  return out
108
116
  }
109
117
 
118
+ // Todo lo que un cargo cita y alguien va a abrir. `sources.yaml` es lo que investiga; `references/` y
119
+ // `SKILL.md` son el método que sigue, y hasta 0.71.0 nadie las miraba: 29 de las 207 URLs de esos
120
+ // documentos no servían, entre ellas dos 404 de páginas que se habían movido.
121
+ //
122
+ // `evaluations/` queda afuera y no por costo: los casos adversariales **inventan** dominios a propósito
123
+ // —veintiuna URLs bajo `.example` y marcas que no existen— y comprobarlas reportaría rotas las que están
124
+ // bien escritas. `learning/reports` y `learning/proposals` también, por lo contrario: son evidencia
125
+ // fechada de que algo dio 403 el día que se consultó, y eso no se arregla.
126
+ const DOC_URL = /https?:\/\/[^\s)>"`\]]+/g
127
+ function documentUrls(dir) {
128
+ const out = []
129
+ const seen = new Set()
130
+ const add = (raw, origin) => {
131
+ const url = raw.replace(/[.,;:]+$/, '')
132
+ if (seen.has(url)) return
133
+ seen.add(url)
134
+ out.push({ url, origin })
135
+ }
136
+ const below = (base, relative) => {
137
+ let entries = []
138
+ try { entries = fs.readdirSync(path.join(base, relative), { withFileTypes: true }) } catch { return }
139
+ for (const entry of entries) {
140
+ const next = `${relative}/${entry.name}`
141
+ if (entry.isDirectory()) { below(base, next); continue }
142
+ if (!entry.name.endsWith('.md')) continue
143
+ for (const hit of fs.readFileSync(path.join(base, next), 'utf8').match(DOC_URL) || []) add(hit, next)
144
+ }
145
+ }
146
+ below(dir, 'references')
147
+ const skill = path.join(dir, 'SKILL.md')
148
+ if (fs.existsSync(skill)) {
149
+ for (const hit of fs.readFileSync(skill, 'utf8').match(DOC_URL) || []) add(hit, 'SKILL.md')
150
+ }
151
+ return out
152
+ }
153
+
110
154
  function evaluate(root, agent) {
111
155
  const target = catalog.resolve(root, agent)
112
156
  const errors = []
@@ -193,4 +237,4 @@ function evaluate(root, agent) {
193
237
  return { errors, warnings, proposals: proposals.length, pending, cases }
194
238
  }
195
239
 
196
- module.exports = { SOURCE_TIERS, cadence, evaluate, evaluateTeam }
240
+ module.exports = { SOURCE_TIERS, cadence, documentUrls, evaluate, evaluateTeam, sourceUrls }
@@ -40,15 +40,21 @@ function prepareReport(root, agent, now = new Date()) {
40
40
  // Los sellos de estado de este módulo escriben atómico y esto no, y la diferencia es qué se pierde
41
41
  // si la escritura se corta: allá el archivo ya existía y quedaría truncado, acá no había nada. Un
42
42
  // documento nuevo a medio escribir se ve; uno viejo a medio pisar se lee como si estuviera entero.
43
+ // `propone` nace sin contestar y lo completa quien escribe el informe. No se deduce del texto de
44
+ // «Recomendación»: los veinte informes del 2026-09-07 tenían texto ahí y once no proponían ningún
45
+ // cambio —explicaban por qué, que es lo correcto—, así que «sección vacía» no distingue nada. Con
46
+ // el campo, un informe que no cambia ningún contrato se mergea solo en vez de gastar una revisión
47
+ // humana que no puede decidir nada.
43
48
  fs.writeFileSync(file, `---
44
49
  agent: ${agent}
45
50
  date: ${isoDate(now)}
46
51
  status: draft
52
+ propone: por-completar
47
53
  ---
48
54
 
49
55
  # Investigación semanal — ${isoDate(now)}
50
56
 
51
- <!-- Dos convenciones que el ciclo necesita y que nada más sostiene:
57
+ <!-- Tres convenciones que el ciclo necesita y que nada más sostiene:
52
58
 
53
59
  · Etiquetá cada hallazgo H1, H2, … en el orden en que aparecen. «Evidencia» y «Recomendación» se
54
60
  refieren a ellos por esa clave, y la propuesta mensual la cita para decir de qué hallazgo sale
@@ -57,6 +63,12 @@ status: draft
57
63
  · No renombres los títulos. «## Recomendación» se lee con un patrón exacto y es lo único que la
58
64
  propuesta consolida de cada informe: renombrarlo no da error, deja la propuesta vacía.
59
65
 
66
+ · Contestá «propone» en el frontmatter, con «si» o con «no». Es «si» cuando el informe pide tocar
67
+ algún archivo del cargo —SKILL.md, sources.yaml, references/, un caso— y «no» cuando lo hallado
68
+ no cambia ningún contrato, aunque «Recomendación» explique largamente por qué. Un «no» se
69
+ mergea sin revisión humana: no hay nada que decidir. Ante la duda va «si», que sólo cuesta una
70
+ mirada.
71
+
60
72
  Este comentario vive fuera de toda sección a propósito — dentro de «Recomendación» viajaría a cada
61
73
  propuesta consolidada. -->
62
74
 
@@ -0,0 +1,87 @@
1
+ 'use strict'
2
+
3
+ // Los comandos que mueven estado ya decidido: archivar lo que cerró y declarar de una vez qué historia
4
+ // llegó con el proyecto. Viven aparte de `planning.js` porque son la otra mitad de una costura: aquéllos
5
+ // leen y juzgan sin mutar nada, éstos escriben. Se separaron cuando el archivo cruzó las 500 líneas, y
6
+ // lo que decidió el corte fue eso y no el número.
7
+
8
+ const fs = require('node:fs')
9
+ const path = require('node:path')
10
+ const P = require('../planning/parser')
11
+ const PC = require('../planning/contracts')
12
+ const AD = require('../planning/adoption')
13
+ const F = require('../core/files')
14
+ const { fail } = require('./io')
15
+
16
+ // La fecha de hoy, la misma que usan los comandos que leen.
17
+ const TODAY = () => new Date().toISOString().slice(0, 10)
18
+
19
+ // El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
20
+ // a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
21
+ // cerró ninguna —que es justo cuando el archivo se vuelve ilegible—.
22
+ // Adoptar es declarar de una vez qué historia llegó con el proyecto. Se genera con lo que hoy no cumple
23
+ // y no se vuelve a correr: un baseline que se regenera perdona de nuevo lo que alguien ya se tomó el
24
+ // trabajo de arreglar, y uno que crece a mano deja de ser una lista de perdones para ser una amnistía.
25
+ // Achicarlo sí es a mano, borrando el renglón que `check` señala.
26
+ function adopt(dir) {
27
+ const root = path.resolve(dir || '.')
28
+ const target = path.join(root, AD.BASELINE)
29
+ if (fs.existsSync(target)) {
30
+ // Un baseline que ya trae huella no se toca: regenerarlo es exactamente lo que la huella impide.
31
+ // Uno sin huella lo generó una versión anterior, y sellarlo no es regenerar nada — se calcula sobre
32
+ // lo que ya está—, así que es la única salida de un aviso que si no no tendría ninguna.
33
+ const existing = fs.readFileSync(target, 'utf8')
34
+ if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
35
+ fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
36
+ + 'delante; `check` marca los que ya cumplen.')
37
+ }
38
+ const slugs = AD.declared(existing)
39
+ F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
40
+ + `sha256:${AD.digest(slugs)}\n`))
41
+ return console.log(`✓ ${AD.BASELINE} sellado con ${slugs.length} entrada(s); la lista no cambió`)
42
+ }
43
+ const epics = P.readEpics(root)
44
+ const pending = P.readDone(root).entries.filter((entry) => PC.doneEntryErrors(entry, epics).length)
45
+ if (!pending.length) {
46
+ return console.log('= no hay nada que exentar: todas las entradas de DONE cumplen el contrato')
47
+ }
48
+ const today = TODAY()
49
+ const slugs = pending.map((entry) => entry.slug)
50
+ F.atomicWrite(target, `# Entradas anteriores a la adopción de Cauce (${today}). No se agregan nuevas:\n`
51
+ + '# desde esa fecha rige el contrato completo, y `check` avisa cuando una de éstas pasa a\n'
52
+ + '# cumplirlo para que le pongas `#~` delante y quede retirada.\n'
53
+ + `# huella: ${slugs.length} entradas · sha256:${AD.digest(slugs)}\n`
54
+ + `${slugs.join('\n')}\n`)
55
+ console.log(`✓ ${pending.length} entrada(s) exentas en ${AD.BASELINE}`)
56
+ return console.log(' revisá la lista: lo que sí cumple el contrato no tiene por qué estar ahí')
57
+ }
58
+
59
+ function archiveHumanActions(root) {
60
+ const source = path.join(root, 'HUMAN_ACTIONS.md')
61
+ const rows = P.readHumanActions(root).filter((row) => row.resolved)
62
+ if (!rows.length) return console.log('= no hay filas resueltas')
63
+ const target = path.join(root, 'done', 'human-actions.md')
64
+ const header = '| Tarea | Estado | Origen | Acción concreta y condición de desbloqueo |\n|---|---|---|---|'
65
+ const previous = P.read(target).trimEnd()
66
+ const head = previous || `---\nstatus: archived\n---\n\n# Acciones humanas resueltas\n\n${header}`
67
+ fs.mkdirSync(path.dirname(target), { recursive: true })
68
+ F.atomicWrite(target, `${head}\n${rows.map((row) => row.raw).join('\n')}\n`)
69
+ const drop = new Set(rows.map((row) => row.raw))
70
+ const kept = P.read(source).split('\n').filter((line) => !drop.has(line))
71
+ F.atomicWrite(source, `${kept.join('\n').trimEnd()}\n`)
72
+ return console.log(`✓ ${rows.length} fila(s) archivadas`)
73
+ }
74
+ // Archivar una épica se retiró en 0.71.0. Existía para descongestionar un `DONE.md` que se hinchaba con
75
+ // una entrada por tarea; con un archivo por tarea no hay nada que descongestionar, y mover esos archivos
76
+ // a una carpeta por épica sería reintroducir el movimiento que la mudanza vino a sacar.
77
+ //
78
+ // El comando se queda para lo que sí sigue archivándose, y para decirle a quien escriba el número que ya
79
+ // no hace falta: sin esto contestaría «La épica debe ser NNN», que manda a corregir la forma de algo que
80
+ // no existe.
81
+ function archive(dir, rawNum) {
82
+ if (String(rawNum || '') === 'human-actions') return archiveHumanActions(path.resolve(dir || '.'))
83
+ return fail('Sólo se archiva `human-actions`. La evidencia de una tarea ya vive en su propio archivo '
84
+ + 'de `done/`, así que archivar una épica dejó de tener sentido.', 2)
85
+ }
86
+
87
+ module.exports = { archive, adopt }
@@ -7,7 +7,7 @@
7
7
  // Banderas que consumen el argumento siguiente: su valor no es un posicional.
8
8
  const VALUED_FLAGS = new Set([
9
9
  '--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
10
- '--task', '--promote',
10
+ '--task', '--promote', '--hito',
11
11
  ])
12
12
 
13
13
  // Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
@@ -19,8 +19,12 @@ const FLAGS = {
19
19
  onboard: ['--json'],
20
20
  check: ['--json'],
21
21
  tree: ['--json', '--no-color'],
22
- context: ['--json'],
22
+ context: ['--json', '--hito'],
23
23
  recurring: ['--json', '--promote'],
24
+ claim: ['--json'],
25
+ runners: ['--json'],
26
+ worktree: ['--json'],
27
+ release: [],
24
28
  evidence: ['--json', '--task'],
25
29
  upgrade: ['--check', '--force'],
26
30
  destroy: ['--force'],
@@ -115,7 +115,15 @@ function evaluationBench(root, agent, caso, force, kind) {
115
115
  // sin pagar un `npm install` por corrida. El cargo llega a un banco donde el CLI funciona.
116
116
  const scope = path.join(dir, 'node_modules', '@ingeniomaps')
117
117
  fs.mkdirSync(scope, { recursive: true })
118
- fs.symlinkSync(IN.PROJECT_ROOT, path.join(scope, 'cauce'), 'dir')
118
+ // El enlace se pisa por lo mismo que el andamiaje de arriba: a veces sobrevive al borrado del banco, y
119
+ // entonces crearlo corta la corrida con `EEXIST` en vez de rehacerlo. Es el único paso que no seguía esa
120
+ // regla, y el que falló en CI rehaciendo el mismo `11-otro` que ya tiene reintentos por esto.
121
+ //
122
+ // `rmSync` sobre un enlace lo quita a él y no a lo que apunta —que acá es la raíz del toolkit—, así que
123
+ // esto no puede llevarse por delante el repositorio.
124
+ const link = path.join(scope, 'cauce')
125
+ fs.rmSync(link, { force: true })
126
+ fs.symlinkSync(IN.PROJECT_ROOT, link, 'dir')
119
127
 
120
128
  // El artefacto del caso, si lo tiene: la guía del proveedor que el pedido manda implementar, el CSV
121
129
  // con instrucciones adentro. Entra antes del commit limpio a propósito — si entrara después, `status`