@ingeniomaps/cauce 0.70.0 → 0.72.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 (133) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/agents/README.md +73 -0
  3. package/agents/roles/system/accounting-specialist/learning/HISTORY.md +2 -0
  4. package/agents/roles/system/ai-governance-lead/learning/HISTORY.md +6 -1
  5. package/agents/roles/system/ai-governance-lead/learning/sources.yaml +4 -4
  6. package/agents/roles/system/ai-governance-lead/references/operating-model.md +2 -2
  7. package/agents/roles/system/ai-product-manager/learning/HISTORY.md +4 -1
  8. package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
  9. package/agents/roles/system/ai-product-manager/references/operating-model.md +2 -2
  10. package/agents/roles/system/analytics-engineer/learning/HISTORY.md +4 -1
  11. package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -2
  12. package/agents/roles/system/analytics-engineer/references/operating-model.md +2 -2
  13. package/agents/roles/system/backend-engineer/learning/HISTORY.md +2 -0
  14. package/agents/roles/system/business-strategist/learning/HISTORY.md +2 -0
  15. package/agents/roles/system/business-strategist/learning/sources.yaml +1 -1
  16. package/agents/roles/system/business-strategist/references/operating-model.md +2 -2
  17. package/agents/roles/system/cloud-architect/learning/HISTORY.md +1 -1
  18. package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
  19. package/agents/roles/system/cloud-architect/references/operating-model.md +2 -2
  20. package/agents/roles/system/community-manager/learning/HISTORY.md +4 -1
  21. package/agents/roles/system/community-manager/learning/sources.yaml +2 -2
  22. package/agents/roles/system/community-manager/references/operating-model.md +1 -1
  23. package/agents/roles/system/content-specialist/learning/HISTORY.md +2 -0
  24. package/agents/roles/system/customer-success-manager/learning/HISTORY.md +2 -0
  25. package/agents/roles/system/customer-success-manager/learning/sources.yaml +4 -4
  26. package/agents/roles/system/customer-success-manager/references/operating-model.md +3 -3
  27. package/agents/roles/system/customer-support-specialist/learning/HISTORY.md +2 -0
  28. package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -2
  29. package/agents/roles/system/customer-support-specialist/references/operating-model.md +2 -2
  30. package/agents/roles/system/data-analyst/learning/HISTORY.md +2 -0
  31. package/agents/roles/system/data-engineer/learning/HISTORY.md +4 -1
  32. package/agents/roles/system/data-engineer/learning/sources.yaml +3 -3
  33. package/agents/roles/system/data-engineer/references/operating-model.md +2 -2
  34. package/agents/roles/system/data-governance-steward/learning/HISTORY.md +2 -0
  35. package/agents/roles/system/data-governance-steward/learning/sources.yaml +17 -7
  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/integrations-engineer/learning/sources.yaml +7 -5
  60. package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +3 -2
  61. package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +1 -1
  62. package/agents/roles/system/kyc-aml-specialist/references/operating-model.md +1 -1
  63. package/agents/roles/system/legal-counsel/learning/HISTORY.md +6 -1
  64. package/agents/roles/system/legal-counsel/learning/sources.yaml +1 -1
  65. package/agents/roles/system/legal-counsel/references/operating-model.md +1 -1
  66. package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
  67. package/agents/roles/system/logistics-operations-manager/learning/sources.yaml +17 -9
  68. package/agents/roles/system/machine-learning-engineer/learning/HISTORY.md +4 -1
  69. package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +6 -6
  70. package/agents/roles/system/machine-learning-engineer/references/operating-model.md +3 -3
  71. package/agents/roles/system/mlops-engineer/learning/HISTORY.md +4 -1
  72. package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
  73. package/agents/roles/system/mlops-engineer/references/operating-model.md +2 -2
  74. package/agents/roles/system/mobile-engineer/learning/HISTORY.md +2 -0
  75. package/agents/roles/system/mobile-engineer/learning/sources.yaml +1 -1
  76. package/agents/roles/system/partnerships-manager/learning/HISTORY.md +4 -1
  77. package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -2
  78. package/agents/roles/system/partnerships-manager/references/operating-model.md +2 -2
  79. package/agents/roles/system/people-operations-manager/learning/HISTORY.md +4 -1
  80. package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -2
  81. package/agents/roles/system/people-operations-manager/references/operating-model.md +2 -2
  82. package/agents/roles/system/privacy-compliance-specialist/learning/HISTORY.md +2 -0
  83. package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +1 -1
  84. package/agents/roles/system/privacy-compliance-specialist/references/operating-model.md +1 -1
  85. package/agents/roles/system/procurement-manager/learning/HISTORY.md +1 -1
  86. package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -2
  87. package/agents/roles/system/procurement-manager/references/operating-model.md +2 -2
  88. package/agents/roles/system/product-manager/learning/HISTORY.md +1 -1
  89. package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +2 -0
  90. package/agents/roles/system/product-marketing-manager/learning/sources.yaml +1 -1
  91. package/agents/roles/system/project-manager/learning/HISTORY.md +4 -1
  92. package/agents/roles/system/project-manager/learning/sources.yaml +1 -1
  93. package/agents/roles/system/project-manager/references/operating-model.md +2 -2
  94. package/agents/roles/system/qa-engineer/learning/HISTORY.md +2 -0
  95. package/agents/roles/system/qa-engineer/learning/sources.yaml +9 -11
  96. package/agents/roles/system/qa-engineer/references/operating-model.md +2 -2
  97. package/agents/roles/system/release-manager/learning/HISTORY.md +1 -1
  98. package/agents/roles/system/sales-representative/learning/HISTORY.md +2 -0
  99. package/agents/roles/system/sales-representative/learning/sources.yaml +2 -2
  100. package/agents/roles/system/sales-representative/references/operating-model.md +1 -1
  101. package/agents/roles/system/security-engineer/learning/HISTORY.md +2 -0
  102. package/agents/roles/system/security-engineer/learning/sources.yaml +1 -1
  103. package/agents/roles/system/security-engineer/references/operating-model.md +1 -1
  104. package/agents/roles/system/site-reliability-engineer/learning/HISTORY.md +2 -0
  105. package/agents/roles/system/software-architect/learning/HISTORY.md +2 -0
  106. package/agents/roles/system/software-architect/learning/sources.yaml +2 -2
  107. package/agents/roles/system/software-architect/references/operating-model.md +1 -1
  108. package/agents/roles/system/solutions-engineer/learning/HISTORY.md +4 -1
  109. package/agents/roles/system/solutions-engineer/learning/sources.yaml +4 -4
  110. package/agents/roles/system/solutions-engineer/references/operating-model.md +2 -2
  111. package/agents/roles/system/tech-lead/learning/HISTORY.md +2 -0
  112. package/agents/roles/system/tech-lead/learning/sources.yaml +9 -8
  113. package/agents/roles/system/technical-program-manager/learning/HISTORY.md +4 -1
  114. package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -2
  115. package/agents/roles/system/technical-program-manager/references/operating-model.md +3 -3
  116. package/agents/roles/system/technical-writer/learning/HISTORY.md +4 -1
  117. package/agents/roles/system/treasury-analyst/learning/HISTORY.md +2 -0
  118. package/agents/roles/system/treasury-analyst/learning/sources.yaml +14 -11
  119. package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
  120. package/agents/roles/system/ui-designer/learning/sources.yaml +2 -2
  121. package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -1
  122. package/agents/roles/system/user-researcher/learning/sources.yaml +1 -1
  123. package/agents/roles/system/ux-designer/learning/HISTORY.md +2 -0
  124. package/agents/roles/system/ux-designer/learning/sources.yaml +1 -1
  125. package/agents/roles/system/ux-designer/references/operating-model.md +1 -1
  126. package/automatization/workflows/autobuild.js +39 -21
  127. package/engine/agents/evaluations.js +18 -4
  128. package/engine/agents/learning-seal.js +36 -5
  129. package/engine/agents/learning-sources.js +113 -5
  130. package/engine/agents/learning.js +13 -1
  131. package/engine/cli/planning.js +29 -0
  132. package/package.json +1 -1
  133. package/template/planning/PROTOCOL.md +14 -0
@@ -1,4 +1,6 @@
1
1
  # Historial de aprendizaje
2
2
 
3
+ Una fila por propuesta cerrada: cuándo, cuál, qué se decidió —aplicarla o archivarla—, quién lo decidió y qué cambió.
4
+
3
5
  | Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |
4
6
  |---|---|---|---|---|
@@ -13,7 +13,7 @@ sources:
13
13
  tier: standard
14
14
  topics: [accessibility, wcag, cognitive-accessibility]
15
15
  - name: ISO Human-centred Design
16
- url: https://www.iso.org/standard/77520.html
16
+ url: https://committee.iso.org/standard/77520.html
17
17
  tier: standard
18
18
  topics: [human-centred-design, lifecycle]
19
19
  - name: Nielsen Norman Group
@@ -79,7 +79,7 @@ Diseñar para que la experiencia sea perceptible, operable, comprensible y robus
79
79
 
80
80
  Modelo sintetizado con fuentes revisadas en agosto de 2026:
81
81
 
82
- - [ISO 9241-210:2019](https://www.iso.org/standard/77520.html): integrar diseño centrado en las personas durante el ciclo de vida de sistemas interactivos.
82
+ - [ISO 9241-210:2019](https://committee.iso.org/standard/77520.html): integrar diseño centrado en las personas durante el ciclo de vida de sistemas interactivos.
83
83
  - [WCAG 2.2](https://www.w3.org/TR/WCAG22/): criterios comprobables para contenido perceptible, operable, comprensible y robusto.
84
84
  - [Nielsen Norman Group: heurísticas de usabilidad](https://media.nngroup.com/media/articles/attachments/Heuristic_Summary1_A4_compressed.pdf): principios para inspeccionar feedback, control, consistencia, prevención y recuperación.
85
85
  - [GOV.UK Design System Patterns](https://design-system.service.gov.uk/patterns/): patrones documentados para tareas concretas, adaptables al contexto.
@@ -9,7 +9,7 @@ export const meta = {
9
9
  // la primera fase. Escribirlas también les da un detalle propio, que es lo que se lee al autorizar.
10
10
  phases: [
11
11
  { title: 'Triage', detail: 'Contrato del proyecto y estado de planning' },
12
- { title: 'Pick', detail: 'La próxima tarea, o la épica que falta expandir' },
12
+ { title: 'Pick', detail: 'La próxima tarea del hito, y la reserva antes de construirla' },
13
13
  { title: 'Classify', detail: 'Carril y reparto de la tarea que no los declara' },
14
14
  { title: 'Ready', detail: 'Aceptación concreta y sin decisiones pendientes' },
15
15
  { title: 'Decompose', detail: 'Partir la tarea que no entra en el tope de horas' },
@@ -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' },
@@ -73,10 +77,6 @@ const CLAIM = {
73
77
  type: 'object', additionalProperties: false, required: ['claimed'],
74
78
  properties: { claimed: { type: 'boolean' }, details: { type: 'string' } },
75
79
  }
76
- const EXPANSION = {
77
- type: 'object', additionalProperties: false, required: ['expanded'],
78
- properties: { expanded: { type: 'boolean' }, hito: { type: 'string' }, reason: { type: 'string' } },
79
- }
80
80
  const READY = {
81
81
  type: 'object', additionalProperties: false, required: ['ready', 'needsHuman'],
82
82
  properties: {
@@ -229,8 +229,16 @@ const CLASSIFICATION = {
229
229
 
230
230
  const CONTRACT = {
231
231
  type: 'object', additionalProperties: false,
232
- required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts'],
232
+ required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts',
233
+ 'rootOk'],
233
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' },
234
242
  project: { type: 'string' }, workspaceRoots: { type: 'array', minItems: 1, items: { type: 'string' } },
235
243
  // La puerta que el proyecto declara, si la declara. Viaja con la raíz porque es de la base de código
236
244
  // y no del runner: un monorepo tiene una por servicio, y uno solo tiene una sola.
@@ -289,7 +297,9 @@ phase('Triage')
289
297
  // subagente como «Límites del proyecto» eran sólo los genéricos del toolkit.
290
298
  const contract = await agent(
291
299
  `${BASE}\n\nLeé ${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} y ${P}/PROTOCOL.md una sola vez y no ` +
292
- `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 ` +
293
303
  `valores de configuración textualmente: project, workspaceRoots como entradas "nombre → ruta", ` +
294
304
  `runner.maxTaskHours, runner.commitPerTask y runner.humanCheckpointBetweenMilestones como humanCheckpoint. ` +
295
305
  `En gates poné una entrada "ruta → comando" por cada workspaceRoot que declare \`verify\`, y ninguna por ` +
@@ -301,6 +311,12 @@ const contract = await agent(
301
311
  { schema: CONTRACT, label: 'contract-digest' },
302
312
  )
303
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
+ }
304
320
 
305
321
  const bounds = contract.boundaries || []
306
322
  const limits = bounds.length ? ` Límites del proyecto: ${bounds.join('; ')}.` : ''
@@ -329,12 +345,17 @@ const readContext = () => read(
329
345
  `de task.tier; copiá slug, ` +
330
346
  `hito, service, acceptance, ` +
331
347
  `epic y cast de task, y epicContext de epic.context —vacío si no hay épica—. El comando es la fuente de ` +
332
- `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.`,
333
351
  { schema: CONTEXT, label: 'planning-context' },
334
352
  )
335
353
 
336
354
  let planning = await readContext()
337
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`)
338
359
  if (planning.blocked) return stop('awaiting-human-review', `${GATE} tiene un checkpoint humano sin resolver`)
339
360
 
340
361
  let currentMilestone = planning.wipActive ? planning.hito : ''
@@ -350,18 +371,15 @@ const classified = new Set()
350
371
 
351
372
  while (rounds++ < MAX_TASKS) {
352
373
  phase('Pick')
353
- if (!planning.hasTask && !planning.queued) {
354
- const expansion = await write(
355
- `Leé ${ROADMAP}. Expandí sólo la próxima épica abierta y aprobada en un hito nuevo de ${BACKLOG}, ` +
356
- `conservando el slug de cada historia, sus referencias a criterios y su servicio. Nunca promuevas ` +
357
- `${P}/INBOX.md. Reportá si escribiste algo.`,
358
- { schema: EXPANSION },
359
- )
360
- if (!expansion) return stop('agent-unavailable', 'Pick no devolvió resultado')
361
- if (!expansion.expanded) break
362
- planning = await readContext()
363
- if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
364
- }
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.
365
383
  if (!planning.hasTask || (currentMilestone && planning.hito !== currentMilestone)) break
366
384
  const task = {
367
385
  id: planning.slug, hito: planning.hito, service: planning.service,
@@ -132,10 +132,21 @@ function behaviors(root, agent, kind) {
132
132
  // tiene `SKILL.md`— así que se le podía agregar una dimensión al gate y sus veredictos anteriores
133
133
  // seguían leyéndose vigentes. Pasó el mismo día que esto se escribió: `change-review` ganó la pregunta
134
134
  // por las superficies críticas y sus tres casos aprobados no dijeron una palabra.
135
+ //
136
+ // Y hay un entorno donde git **no puede** contestar: un clon `--depth 1` tiene un solo commit y le
137
+ // atribuye todo el árbol, así que `log -1 -- <archivo>` devuelve la fecha del checkout para cualquier
138
+ // archivo. Comprobado con git 2.43.0 clonando este repositorio a profundidad 1: el `SKILL.md` de
139
+ // `site-reliability-engineer` daba el día del clon contra el 2026-09-02, que es cuando se editó.
140
+ //
141
+ // Ahí la respuesta no es una fecha vieja sino ninguna, y **se dice**: un aviso que anuncia un cambio
142
+ // que no ocurrió se lee igual que uno verdadero, y tres informes de dos cargos gastaron un hallazgo
143
+ // cada uno investigándolo antes de que saliera como el caso 052.
135
144
  function contractChangedAt(dir, kind) {
136
145
  const file = kind === 'flow' ? 'flow.json' : 'SKILL.md'
146
+ const shallow = spawnSync('git', ['-C', dir, 'rev-parse', '--is-shallow-repository'], { encoding: 'utf8' })
147
+ if ((shallow.stdout || '').trim() === 'true') return { date: '', truncated: true }
137
148
  const git = spawnSync('git', ['-C', dir, 'log', '-1', '--format=%cs', '--', file], { encoding: 'utf8' })
138
- return git.status === 0 ? (git.stdout || '').trim() : ''
149
+ return { date: git.status === 0 ? (git.stdout || '').trim() : '', truncated: false }
139
150
  }
140
151
 
141
152
  function resultsDir(root, agent, kind) {
@@ -264,10 +275,13 @@ function validate(root, agent, kind) {
264
275
  if (state.failed.length) {
265
276
  warnings.push(`${state.failed.length} caso(s) no pasan: ${state.failed.join(', ')}`)
266
277
  }
267
- const changedAt = contractChangedAt(subject(root, agent, kind), kind)
268
- if (changedAt && changedAt > state.oldest) {
278
+ const contract = contractChangedAt(subject(root, agent, kind), kind)
279
+ if (contract.truncated) {
280
+ warnings.push('no se puede saber si el contrato cambió después de estos veredictos: el checkout '
281
+ + 'está truncado y git le atribuye todo a su único commit (se destraba con fetch-depth: 0)')
282
+ } else if (contract.date && contract.date > state.oldest) {
269
283
  const which = state.oldest === state.newest ? 'la última corrida es' : 'el veredicto más viejo es'
270
- warnings.push(`el contrato cambió el ${changedAt} y ${which} del ${state.oldest}: `
284
+ warnings.push(`el contrato cambió el ${contract.date} y ${which} del ${state.oldest}: `
271
285
  + 'mide una versión anterior')
272
286
  }
273
287
  return { errors, warnings, cases: total, last, state }
@@ -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 }
@@ -86,27 +86,119 @@ function cadence(root, agent) {
86
86
  // Basta con las líneas `tier:`: el archivo es del catálogo, no de un tercero, y agregar un parser de
87
87
  // YAML por un campo rompería la regla de cero dependencias.
88
88
  function sourceTiers(text) {
89
- const body = text.includes('sources:') ? text.slice(text.indexOf('sources:')) : ''
90
- return [...body.matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
89
+ return [...sourcesBody(text).matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
91
90
  }
92
91
 
92
+ // El cuerpo de `sources:` termina donde empieza `pending:`. Sin este corte, una pendiente entraba como
93
+ // fuente declarada y el chequeo semanal la reportaba rota todas las semanas — que es exactamente el
94
+ // aviso permanente que la lista existe para no producir.
95
+ function sourcesBody(text) {
96
+ if (!text.includes('sources:')) return ''
97
+ const body = text.slice(text.indexOf('sources:'))
98
+ const corte = body.search(/^pending:/m)
99
+ return corte === -1 ? body : body.slice(0, corte)
100
+ }
101
+
102
+ // Una entrada escrita en una sola línea: seis cargos del catálogo la escriben así y cuarenta y siete la
103
+ // reparten en varias. Leyendo sólo la segunda forma, esos seis no tenían el chequeo de URL duplicada que
104
+ // hay más abajo, y nada lo decía porque no encontrar duplicados y no mirar se ven igual.
105
+ const FLOW_ENTRY = /^\s*-\s*\{[^}]*\bname:\s*([^,}]+?)\s*,[^}]*\burl:\s*"?([^",}\s]+)/
106
+ const quitar = (value) => value.replace(/^['"]|['"]$/g, '')
107
+
93
108
  // Las fuentes de un cargo, por su URL. La misma URL con dos nombres es una sola fuente contada dos
94
109
  // veces: el catálogo llegó a tener la especificación OpenAPI bajo tres —`OpenAPI Specification`,
95
110
  // `...latest published` y `...3.2.0`— así que arreglarle el `tier` a un cargo no se lo arreglaba a los
96
111
  // otros, y quien leyera el informe vería la misma página citada como si fueran tres.
97
112
  function sourceUrls(text) {
98
- const body = text.includes('sources:') ? text.slice(text.indexOf('sources:')) : ''
113
+ const body = sourcesBody(text)
99
114
  const out = []
100
115
  let name = ''
101
116
  for (const line of body.split('\n')) {
117
+ const flow = line.match(FLOW_ENTRY)
118
+ if (flow) { out.push({ name: quitar(flow[1]), url: flow[2].replace(/\/+$/, '') }); continue }
102
119
  const declared = line.match(/^\s*-\s*name:\s*(.+?)\s*$/)
103
- if (declared) { name = declared[1].replace(/^['"]|['"]$/g, ''); continue }
120
+ if (declared) { name = quitar(declared[1]); continue }
104
121
  const url = line.match(/^\s*url:\s*(\S+)/)
105
122
  if (url && name) out.push({ name, url: url[1].replace(/\/+$/, '') })
106
123
  }
107
124
  return out
108
125
  }
109
126
 
127
+ // Todo lo que un cargo cita y alguien va a abrir. `sources.yaml` es lo que investiga; `references/` y
128
+ // `SKILL.md` son el método que sigue, y hasta 0.71.0 nadie las miraba: 29 de las 207 URLs de esos
129
+ // documentos no servían, entre ellas dos 404 de páginas que se habían movido.
130
+ //
131
+ // `evaluations/` queda afuera y no por costo: los casos adversariales **inventan** dominios a propósito
132
+ // —veintiuna URLs bajo `.example` y marcas que no existen— y comprobarlas reportaría rotas las que están
133
+ // bien escritas. `learning/reports` y `learning/proposals` también, por lo contrario: son evidencia
134
+ // fechada de que algo dio 403 el día que se consultó, y eso no se arregla.
135
+ const DOC_URL = /https?:\/\/[^\s)>"`\]]+/g
136
+ function documentUrls(dir) {
137
+ const out = []
138
+ const seen = new Set()
139
+ const add = (raw, origin) => {
140
+ const url = raw.replace(/[.,;:]+$/, '')
141
+ if (seen.has(url)) return
142
+ seen.add(url)
143
+ out.push({ url, origin })
144
+ }
145
+ const below = (base, relative) => {
146
+ let entries = []
147
+ try { entries = fs.readdirSync(path.join(base, relative), { withFileTypes: true }) } catch { return }
148
+ for (const entry of entries) {
149
+ const next = `${relative}/${entry.name}`
150
+ if (entry.isDirectory()) { below(base, next); continue }
151
+ if (!entry.name.endsWith('.md')) continue
152
+ for (const hit of fs.readFileSync(path.join(base, next), 'utf8').match(DOC_URL) || []) add(hit, next)
153
+ }
154
+ }
155
+ below(dir, 'references')
156
+ const skill = path.join(dir, 'SKILL.md')
157
+ if (fs.existsSync(skill)) {
158
+ for (const hit of fs.readFileSync(skill, 'utf8').match(DOC_URL) || []) add(hit, 'SKILL.md')
159
+ }
160
+ return out
161
+ }
162
+
163
+ // Lo que el cargo probó, no pudo abrir y va a volver a necesitar. Cinco cargos lo escribían ya como
164
+ // comentario en su propio archivo —el steward hasta puso «Registrar cuando exista una ficha legible»,
165
+ // que es un recordatorio que nadie iba a revisar—, así que la forma existía y lo que faltaba era que
166
+ // alguien la mirara.
167
+ //
168
+ // `url` es opcional a propósito: la mitad de esas entradas están pendientes **porque no hay ninguna URL
169
+ // que responda** —ISO/IEC/IEEE 24765, la ley federal mexicana—, y exigirla habría dejado fuera
170
+ // justamente las que más cuesta resolver. Lo que no es opcional es `why`: sin la razón, la lista es un
171
+ // cementerio de enlaces que nadie sabe por qué están.
172
+ //
173
+ // La continuación de línea se une en vez de prohibirse: `why` es una frase y una frase se envuelve. La
174
+ // primera versión la cortaba en el primer salto y **no avisaba** —cuatro razones quedaron a media
175
+ // oración sin que nada fallara—, y prohibir la forma no evita que la próxima persona la escriba.
176
+ const PENDING_FIELD = /^\s{4}(name|url|why|since):\s*(.+?)\s*$/
177
+ function pendingSources(text) {
178
+ const corte = text.search(/^pending:/m)
179
+ if (corte === -1) return []
180
+ const out = []
181
+ let one = null
182
+ let last = ''
183
+ for (const line of text.slice(corte).split('\n').slice(1)) {
184
+ if (/^\S/.test(line)) break
185
+ if (/^\s{2}-\s/.test(line)) {
186
+ if (one) out.push(one)
187
+ one = {}
188
+ last = ''
189
+ const primero = line.match(/^\s{2}-\s*(name|url|why|since):\s*(.+?)\s*$/)
190
+ if (primero) { one[primero[1]] = quitar(primero[2]); last = primero[1] }
191
+ continue
192
+ }
193
+ const campo = line.match(PENDING_FIELD)
194
+ if (campo && one) { one[campo[1]] = quitar(campo[2]); last = campo[1]; continue }
195
+ const sigue = line.match(/^\s{6,}(\S.*?)\s*$/)
196
+ if (sigue && one && last) one[last] += ` ${sigue[1]}`
197
+ }
198
+ if (one) out.push(one)
199
+ return out
200
+ }
201
+
110
202
  function evaluate(root, agent) {
111
203
  const target = catalog.resolve(root, agent)
112
204
  const errors = []
@@ -145,6 +237,20 @@ function evaluate(root, agent) {
145
237
  }
146
238
  byUrl.set(one.url, one.name)
147
239
  }
240
+ // Una pendiente sin razón es un enlace muerto con fecha, y sin fecha no se puede ver que lleva
241
+ // meses ahí. Los dos campos son la mitad del valor de la lista: lo que la vuelve revisable.
242
+ for (const one of pendingSources(fs.readFileSync(sourcesFile, 'utf8'))) {
243
+ const falta = ['name', 'why', 'since'].filter((campo) => !one[campo])
244
+ if (falta.length) {
245
+ errors.push(`sources.yaml: una pendiente no declara ${falta.join(' ni ')}`
246
+ + `${one.name ? ` (${one.name})` : ''}`)
247
+ }
248
+ // Declarada y pendiente a la vez es una contradicción que el chequeo semanal no puede resolver:
249
+ // la reportaría rota como fuente y recuperada como pendiente en la misma corrida.
250
+ if (one.url && byUrl.has(one.url)) {
251
+ errors.push(`sources.yaml: ${one.url} está declarada como fuente y también como pendiente`)
252
+ }
253
+ }
148
254
  }
149
255
  const skill = fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8').toLowerCase()
150
256
  for (const phrase of ['no inventar', 'autorización', 'evidencia observable']) {
@@ -193,4 +299,6 @@ function evaluate(root, agent) {
193
299
  return { errors, warnings, proposals: proposals.length, pending, cases }
194
300
  }
195
301
 
196
- module.exports = { SOURCE_TIERS, cadence, evaluate, evaluateTeam }
302
+ module.exports = {
303
+ SOURCE_TIERS, cadence, documentUrls, evaluate, evaluateTeam, pendingSources, sourceUrls,
304
+ }
@@ -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
 
@@ -266,6 +266,9 @@ function treeJson({ epics, milestones, done, wips, inbox, queued, claims }) {
266
266
 
267
267
  function tree(dir, cli) {
268
268
  const root = path.resolve(dir || '.')
269
+ // Mismo motivo que en `context`, y por eso comparten la comprobación: sin ella un planning ausente
270
+ // dibujaba un árbol vacío, que se lee como un roadmap sin épicas en vez de como una ruta equivocada.
271
+ assertPlanning(root)
269
272
  const state = ST.snapshot(root)
270
273
  if (cli.has('--json')) return treeJson(state)
271
274
  const { epics, milestones, done, wips, inbox, queued, claims } = state
@@ -304,9 +307,29 @@ function tree(dir, cli) {
304
307
  console.log(`${paint('1', 'DONE')} ${done.entries.length} tareas\n`)
305
308
  }
306
309
 
310
+ // Lo que no se pudo leer no contesta como si se hubiera leído: la misma regla que `stagedFiles` aplica
311
+ // sobre el índice de git, y que esta familia ya aplicaba al `--hito` inexistente. Faltaba la raíz, y ahí
312
+ // pesa más, porque el consumidor no siempre es una persona: `autobuild` toma la cola vacía como permiso
313
+ // para expandir una épica, así que un error de ruta promovía trabajo en vez de fallar.
314
+ //
315
+ // La ruta va **resuelta** y no como se escribió, porque el error que ataca es de resolución: en sidecar
316
+ // `<empresa>-ops/planning` desde adentro de la raíz apunta a `<empresa>-ops/<empresa>-ops/planning`.
317
+ function assertPlanning(root) {
318
+ if (!fs.existsSync(root)) {
319
+ return fail(`no existe el planning en ${root} (ruta resuelta). Comprobá desde dónde estás invocando.`, 2)
320
+ }
321
+ // Existir no alcanza: un directorio cualquiera contestaría cola vacía igual. `BACKLOG.md` es el archivo
322
+ // del que sale la cola, así que sin él la respuesta no significa nada.
323
+ if (!fs.existsSync(path.join(root, 'BACKLOG.md'))) {
324
+ return fail(`${root} (ruta resuelta) no es un planning: falta BACKLOG.md.`, 2)
325
+ }
326
+ return null
327
+ }
328
+
307
329
  // Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
308
330
  function context(dir, cli) {
309
331
  const root = path.resolve(dir || '.')
332
+ assertPlanning(root)
310
333
  const state = ST.snapshot(root)
311
334
  // Acotar la cola a un hito es como un equipo se reparte trabajo sin coordinarse: dos personas en hitos
312
335
  // distintos casi nunca dependen entre sí ni tocan los mismos archivos. Lo que se acota es qué se
@@ -372,6 +395,10 @@ function context(dir, cli) {
372
395
  // vuelta. Que aparezca es la señal.
373
396
  recurring: RC.status({ ...RC.read(root), done: state.done, today: TODAY() })
374
397
  .filter((one) => one.overdue),
398
+ // La próxima sin promover: se nombra, no se encola, igual que la recurrencia de arriba y por su
399
+ // mismo criterio. Por qué no la encola una máquina está en la fase Pick de `autobuild`.
400
+ nextEpic: (!task && [...state.epics].filter((one) => one.status === 'open')
401
+ .sort((a, b) => String(a.num).localeCompare(String(b.num)))[0]) || null,
375
402
  }
376
403
  if (cli.has('--json')) return console.log(JSON.stringify(report))
377
404
 
@@ -403,6 +430,8 @@ function context(dir, cli) {
403
430
  }
404
431
  if (!report.task) {
405
432
  console.log('TASK (sin tarea disponible)')
433
+ const { nextEpic: next } = report
434
+ if (next) console.log(`EPIC ${next.num}: ${next.title} — sin promover`)
406
435
  // Mismo motivo que `blocked` arriba, con otra causa: acá la cola no la traba una persona, la tiene
407
436
  // el equipo, y lo que corresponde es hablar con quien la tiene.
408
437
  for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.70.0",
3
+ "version": "0.72.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -81,6 +81,20 @@ El lane reduce ceremonia, nunca seguridad, aceptación ni evidencia: Verify y el
81
81
  cuatro. Lo que decide el carril es la superficie del cambio y no su tamaño en líneas — un `if` en el
82
82
  chequeo de permisos es `full`, y un componente entero de presentación puede ser `directo`.
83
83
 
84
+ Escribir la línea es también contrastarla. Declara cuatro cosas —qué hace, en qué carril, quién entrega y
85
+ revisa, con qué se comprueba— y las cuatro salen de la misma mano en el mismo acto, así que nada las cruza
86
+ después. Releerlas no encuentra el hueco: una aceptación incompleta se lee perfecta, porque todo lo que
87
+ dice es cierto.
88
+
89
+ Antes de dar la tarea por escrita se recorre su descripción frase por frase y se contesta, por cada cosa
90
+ que promete, cuál condición de aceptación la comprueba; se lee el carril contra la superficie que toca y
91
+ no contra su tamaño; y se comprueba que el cast entregue a quien construye. Lo que quede sin condición se
92
+ agrega o se declara fuera de alcance en la línea.
93
+
94
+ Es el único momento en que las cuatro se pueden mirar juntas, y por eso la pasada vive acá y no en una
95
+ fase: después el carril ya decide cuáles corren, y la que revisaría es una de las que ese carril puede
96
+ saltar. Una tarea mal marcada `express` es justamente la que se salta la fase donde alguien lo notaría.
97
+
84
98
  ## Invariantes
85
99
 
86
100
  1. Una tarea tiene un dueño de estado: roadmap → BACKLOG → overlay WIP → DONE.