@ingeniomaps/cauce 0.53.2 → 0.55.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 (130) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/agents/README.md +24 -0
  4. package/agents/roles/system/accounting-specialist/SKILL.md +2 -0
  5. package/agents/roles/system/ai-governance-lead/SKILL.md +2 -0
  6. package/agents/roles/system/ai-product-manager/SKILL.md +2 -0
  7. package/agents/roles/system/analytics-engineer/SKILL.md +2 -0
  8. package/agents/roles/system/backend-engineer/SKILL.md +4 -2
  9. package/agents/roles/system/backend-engineer/learning/HISTORY.md +1 -0
  10. package/agents/roles/system/business-strategist/SKILL.md +4 -2
  11. package/agents/roles/system/cloud-architect/SKILL.md +5 -0
  12. package/agents/roles/system/cloud-architect/learning/HISTORY.md +4 -0
  13. package/agents/roles/system/community-manager/SKILL.md +2 -0
  14. package/agents/roles/system/content-specialist/SKILL.md +4 -2
  15. package/agents/roles/system/customer-success-manager/SKILL.md +9 -2
  16. package/agents/roles/system/customer-success-manager/learning/HISTORY.md +1 -0
  17. package/agents/roles/system/customer-support-specialist/SKILL.md +4 -2
  18. package/agents/roles/system/data-analyst/SKILL.md +9 -2
  19. package/agents/roles/system/data-analyst/learning/HISTORY.md +1 -0
  20. package/agents/roles/system/data-engineer/SKILL.md +2 -0
  21. package/agents/roles/system/data-governance-steward/SKILL.md +2 -0
  22. package/agents/roles/system/data-scientist/SKILL.md +2 -0
  23. package/agents/roles/system/database-administrator/SKILL.md +2 -0
  24. package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -0
  25. package/agents/roles/system/developer-relations-engineer/SKILL.md +2 -0
  26. package/agents/roles/system/devops-engineer/SKILL.md +4 -2
  27. package/agents/roles/system/engineering-manager/SKILL.md +2 -0
  28. package/agents/roles/system/financial-controller/SKILL.md +4 -2
  29. package/agents/roles/system/finops-engineer/SKILL.md +2 -0
  30. package/agents/roles/system/finops-engineer/learning/HISTORY.md +1 -0
  31. package/agents/roles/system/finops-engineer/learning/sources.yaml +12 -0
  32. package/agents/roles/system/fraud-risk-analyst/SKILL.md +7 -0
  33. package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +1 -0
  34. package/agents/roles/system/frontend-engineer/SKILL.md +8 -2
  35. package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel/InternalNote.jsx +30 -0
  36. package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel.md +10 -0
  37. package/agents/roles/system/frontend-engineer/learning/HISTORY.md +1 -0
  38. package/agents/roles/system/growth-marketer/SKILL.md +17 -0
  39. package/agents/roles/system/growth-marketer/evaluations/cases/08-briefing-de-referidos.md +23 -0
  40. package/agents/roles/system/growth-marketer/evaluations/expected-behaviors.yaml +1 -0
  41. package/agents/roles/system/growth-marketer/learning/HISTORY.md +3 -0
  42. package/agents/roles/system/implementation-manager/SKILL.md +10 -0
  43. package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook/plan-cutover-tesela-rev-b.md +70 -0
  44. package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook.md +12 -0
  45. package/agents/roles/system/implementation-manager/evaluations/expected-behaviors.yaml +1 -0
  46. package/agents/roles/system/implementation-manager/learning/HISTORY.md +5 -0
  47. package/agents/roles/system/integrations-engineer/SKILL.md +6 -4
  48. package/agents/roles/system/kyc-aml-specialist/SKILL.md +11 -4
  49. package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +2 -0
  50. package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +76 -1
  51. package/agents/roles/system/legal-counsel/SKILL.md +2 -0
  52. package/agents/roles/system/logistics-operations-manager/SKILL.md +17 -0
  53. package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo/api-seguimiento-andesexpress.md +40 -0
  54. package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo.md +2 -3
  55. package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
  56. package/agents/roles/system/machine-learning-engineer/SKILL.md +2 -0
  57. package/agents/roles/system/mlops-engineer/SKILL.md +2 -0
  58. package/agents/roles/system/mobile-engineer/SKILL.md +4 -2
  59. package/agents/roles/system/partnerships-manager/SKILL.md +2 -0
  60. package/agents/roles/system/people-operations-manager/SKILL.md +2 -0
  61. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +4 -2
  62. package/agents/roles/system/procurement-manager/SKILL.md +2 -0
  63. package/agents/roles/system/product-manager/SKILL.md +2 -0
  64. package/agents/roles/system/product-marketing-manager/SKILL.md +9 -2
  65. package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +1 -0
  66. package/agents/roles/system/project-manager/SKILL.md +2 -0
  67. package/agents/roles/system/qa-engineer/SKILL.md +11 -2
  68. package/agents/roles/system/qa-engineer/evaluations/cases/07-agent-test-repair.md +1 -1
  69. package/agents/roles/system/qa-engineer/evaluations/cases/11-accessibility-sampling.md +10 -0
  70. package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary/resumen-legal-wcag22.md +28 -0
  71. package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary.md +10 -0
  72. package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary/wiki-plataforma-conformidad.md +24 -0
  73. package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary.md +10 -0
  74. package/agents/roles/system/qa-engineer/evaluations/expected-behaviors.yaml +2 -0
  75. package/agents/roles/system/qa-engineer/learning/HISTORY.md +4 -0
  76. package/agents/roles/system/qa-engineer/learning/sources.yaml +24 -3
  77. package/agents/roles/system/release-manager/SKILL.md +5 -3
  78. package/agents/roles/system/release-manager/evaluations/cases/09-provenance-without-origin.md +12 -0
  79. package/agents/roles/system/release-manager/learning/HISTORY.md +1 -0
  80. package/agents/roles/system/release-manager/references/operating-model.md +8 -0
  81. package/agents/roles/system/sales-representative/SKILL.md +4 -2
  82. package/agents/roles/system/security-engineer/SKILL.md +4 -2
  83. package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
  84. package/agents/roles/system/site-reliability-engineer/SKILL.md +4 -2
  85. package/agents/roles/system/software-architect/SKILL.md +4 -2
  86. package/agents/roles/system/solutions-engineer/SKILL.md +2 -0
  87. package/agents/roles/system/tech-lead/SKILL.md +2 -0
  88. package/agents/roles/system/technical-program-manager/SKILL.md +2 -0
  89. package/agents/roles/system/technical-writer/SKILL.md +2 -0
  90. package/agents/roles/system/treasury-analyst/SKILL.md +6 -4
  91. package/agents/roles/system/ui-designer/SKILL.md +15 -0
  92. package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
  93. package/agents/roles/system/user-researcher/SKILL.md +6 -0
  94. package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -0
  95. package/agents/roles/system/ux-designer/SKILL.md +2 -0
  96. package/automatization/shared/eval-measured.js +35 -0
  97. package/automatization/shared/eval-only.js +15 -0
  98. package/automatization/workflows/README.md +27 -0
  99. package/automatization/workflows/agent-eval.js +42 -7
  100. package/automatization/workflows/agent-promote.js +65 -15
  101. package/automatization/workflows/autobuild.js +52 -8
  102. package/automatization/workflows/flow-eval.js +27 -7
  103. package/engine/agents/catalog.js +1 -1
  104. package/engine/agents/fork.js +1 -1
  105. package/engine/agents/learning-files.js +7 -2
  106. package/engine/agents/learning-seal.js +123 -0
  107. package/engine/agents/learning-sources.js +33 -3
  108. package/engine/agents/learning.js +80 -87
  109. package/engine/automation/config.js +43 -11
  110. package/engine/automation/hooks.js +0 -1
  111. package/engine/automation/index.js +19 -4
  112. package/engine/cli/args.js +2 -2
  113. package/engine/cli/catalog.js +33 -4
  114. package/engine/cli/instance.js +43 -12
  115. package/engine/cli/ops.js +16 -21
  116. package/engine/config/validate.js +7 -2
  117. package/engine/core/onboarding.js +0 -3
  118. package/engine/core/scan.js +1 -1
  119. package/engine/hooks/input.js +1 -1
  120. package/engine/integrations/proposals.js +1 -1
  121. package/engine/integrations/providers/jira.js +1 -1
  122. package/engine/integrations/registry.js +0 -1
  123. package/engine/planning/business-rules.js +1 -1
  124. package/engine/planning/contracts.js +8 -1
  125. package/engine/planning/parser.js +7 -1
  126. package/engine/schemas/ops-config.schema.json +60 -13
  127. package/flows/system/incident-review/flow.json +1 -1
  128. package/flows/system/incident-review/learning/HISTORY.md +1 -0
  129. package/package.json +4 -3
  130. package/template/planning/rules/system/conduct.md +19 -0
@@ -8,7 +8,7 @@
8
8
  const fs = require('node:fs')
9
9
  const path = require('node:path')
10
10
  const F = require('../core/files')
11
- const { supersededGuards } = require('./hooks')
11
+ const { supersededGuards, expectedHooks } = require('./hooks')
12
12
 
13
13
  function mergeConfig(current, incoming) {
14
14
  if (Array.isArray(incoming)) {
@@ -32,19 +32,40 @@ function mergeConfig(current, incoming) {
32
32
  return incoming
33
33
  }
34
34
 
35
- // Una entrada de hook que puso Cauce se reconoce por el guard al que apunta: `automatization/hooks/`
36
- // es nuestro y ninguna empresa escribe ahí. Se saca del archivo del usuario antes de fusionar para que
37
- // el merge deje exactamente las de esta versión, ni una más.
38
- const DELIVERED = /automatization\/hooks\/guard-[a-z-]+\.sh/
35
+ // Una entrada de hook que puso Cauce se reconoce por el guard al que apunta, y lo nuestro es lo que
36
+ // efectivamente entregamos, no todo lo que vive en nuestra carpeta. Reconocer por directorio
37
+ // desregistraba el guard propio del proyecto en cada reinstalación: `template/AGENTS.md` invita a
38
+ // ponerlo justo ahí y promete que sobrevive a cada actualización, el archivo quedaba en disco, y lo
39
+ // único que se veía era una línea diciendo que se había quitado una entrada obsoleta.
40
+ // Se saca del archivo del usuario antes de fusionar para que el merge deje exactamente las de esta
41
+ // versión, ni una más.
42
+ const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
39
43
 
40
- function withoutDeliveredHooks(config, live) {
44
+ // Dos fuentes, y la segunda es la que cierra el borde. `expectedHooks()` dice qué entrega el motor de
45
+ // hoy y alcanza para una instancia que nunca instaló; `delivered` es lo que esta instancia registró
46
+ // haber recibido la última vez, y es lo único que reconoce un guard que entregamos en una versión y
47
+ // retiramos en la siguiente: su nombre ya no está en la lista de hoy, así que sin el registro quedaría
48
+ // vivo en la configuración llamando a un guard que Cauce ya no mantiene. Un guard propio del proyecto
49
+ // no está en ninguna de las dos, que es exactamente por lo que sobrevive.
50
+ function isDelivered(command, delivered) {
51
+ const hit = String(command).match(HOOK_PATH)
52
+ if (!hit) return false
53
+ return expectedHooks().includes(hit[1]) || delivered.has(String(command))
54
+ }
55
+
56
+ // Lo que de un conjunto de comandos es wiring de guards nuestro, para anotarlo como entregado.
57
+ function deliveredHookCommands(commands) {
58
+ return [...commands].filter((command) => HOOK_PATH.test(String(command)))
59
+ }
60
+
61
+ function withoutDeliveredHooks(config, live, delivered = new Set()) {
41
62
  const dropped = []
42
63
  const walk = (node) => {
43
64
  if (Array.isArray(node)) {
44
65
  return node
45
66
  .filter((item) => {
46
67
  const command = item && typeof item === 'object' ? String(item.command || '') : ''
47
- if (!DELIVERED.test(command)) return true
68
+ if (!isDelivered(command, delivered)) return true
48
69
  // Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
49
70
  // que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
50
71
  if (!live.has(command)) dropped.push(command)
@@ -83,9 +104,20 @@ function reportRemoved(name, dropped, live, output) {
83
104
 
84
105
  function includesConfig(actual, expected) {
85
106
  if (Array.isArray(expected)) {
86
- return Array.isArray(actual) && expected.every((item) => {
87
- return actual.some((value) => JSON.stringify(value) === JSON.stringify(item))
88
- })
107
+ return Array.isArray(actual) && expected.every((item) => actual.some((value) => {
108
+ // Un grupo de hooks se compara por su contenido, no por su forma serializada. La comprobación es
109
+ // «contiene», que es lo correcto, pero con el ítem entero como unidad un grupo `{matcher, hooks}`
110
+ // con un hook de más dejaba de ser el mismo objeto y contaba como ausente: `doctor` llamaba
111
+ // divergente a una configuración que tenía todo lo esperado, y el anidamiento —donde vive la
112
+ // diferencia real— no se miraba nunca. La instalación promete conservar lo que el proyecto ya
113
+ // tenía, así que sumar un hook propio a un grupo nuestro es exactamente lo que permite.
114
+ if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
115
+ return value && typeof value === 'object'
116
+ && value.matcher === item.matcher
117
+ && includesConfig(value.hooks, item.hooks)
118
+ }
119
+ return JSON.stringify(value) === JSON.stringify(item)
120
+ }))
89
121
  }
90
122
  if (expected && typeof expected === 'object') {
91
123
  return actual && typeof actual === 'object' && Object.entries(expected).every(([key, value]) => {
@@ -170,6 +202,6 @@ function blockUpToDate(file, name, content) {
170
202
 
171
203
  module.exports = {
172
204
  blockStart,
173
- mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
205
+ mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
174
206
  unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
175
207
  }
@@ -7,7 +7,6 @@
7
7
  const fs = require('node:fs')
8
8
  const path = require('node:path')
9
9
  const H = require('../hooks/run')
10
- const O = require('../core/ownership')
11
10
  const M = require('../core/manifest')
12
11
  const { packagedAutomation } = require('./runners')
13
12
 
@@ -4,8 +4,6 @@ const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
  const { spawnSync } = require('node:child_process')
6
6
  const F = require('../core/files')
7
- const H = require('../hooks/run')
8
- const P = require('../planning/parser')
9
7
  const catalog = require('../agents/catalog')
10
8
  const O = require('../core/ownership')
11
9
  const M = require('../core/manifest')
@@ -19,7 +17,7 @@ const {
19
17
  legacyGuardWiring, staleHooks, listHooks,
20
18
  } = require('./hooks')
21
19
  const {
22
- blockStart, mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
20
+ blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
23
21
  unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
24
22
  } = require('./config')
25
23
 
@@ -228,6 +226,11 @@ function deliveryKey(name, target) {
228
226
  return `${name}/${target.split(path.sep).join('/')}`
229
227
  }
230
228
 
229
+ // La sección `runners` del manifiesto anota archivo → digest; ésta es la única entrada que guarda una
230
+ // lista, porque lo que registra no es un archivo sino las entradas que dejamos dentro del archivo de
231
+ // configuración del usuario. No colisiona con ningún `target`: ninguno se llama así.
232
+ const HOOKS_KEY = 'config.hooks'
233
+
231
234
  // En qué estado quedó un archivo que este adaptador entregó alguna vez.
232
235
  //
233
236
  // nuevo no existe todavía
@@ -279,6 +282,7 @@ function uninstall(root, name, output = console) {
279
282
 
280
283
  const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
281
284
  const deliveredPaths = { ...recorded }
285
+ delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
282
286
  for (const item of items) {
283
287
  const resolved = { item, ...resolveItem(paths, root, name, item) }
284
288
  const key = deliveryKey(name, item.target)
@@ -380,7 +384,12 @@ function install(root, name, output = console, options = {}) {
380
384
  // guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
381
385
  const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
382
386
  .map((entry) => JSON.parse(`{${entry}}`).command))
383
- const clean = runner.config.owned ? { config: {}, dropped: [] } : withoutDeliveredHooks(current, live)
387
+ // Lo que este adaptador anotó haber entregado la última vez. Es lo único que reconoce una entrada
388
+ // nuestra cuyo guard el motor ya no trae: por nombre no se distingue de la que agregó el proyecto.
389
+ const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
390
+ const clean = runner.config.owned
391
+ ? { config: {}, dropped: [] }
392
+ : withoutDeliveredHooks(current, live, previous)
384
393
  reportRemoved(name, clean.dropped, live, output)
385
394
  F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
386
395
  // Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
@@ -390,6 +399,12 @@ function install(root, name, output = console, options = {}) {
390
399
  }
391
400
  output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
392
401
  const deliveredPaths = { ...recorded }
402
+ // Qué wiring de guards dejamos puesto, para poder retirarlo el día que el motor deje de traerlo. Se
403
+ // anota lo que instalamos y no lo que quedó en el archivo: ahí conviven las entradas del proyecto, y
404
+ // registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
405
+ const ourHooks = deliveredHookCommands(live)
406
+ if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
407
+ else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
393
408
  for (const resolved of resolvedItems) {
394
409
  const status = state.get(resolved)
395
410
  const ownFile = runner.instructions.includes(resolved.item)
@@ -25,7 +25,7 @@ const FLAGS = {
25
25
  agents: ['--json', '--own', '--system'],
26
26
  integration: ['--fixture'],
27
27
  automation: ['--force'],
28
- learn: ['--flow', '--proposal', '--applied', '--period'],
28
+ learn: ['--flow', '--proposal', '--applied', '--archived', '--period'],
29
29
  evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
30
30
  flow: ['--json'],
31
31
  }
@@ -54,4 +54,4 @@ function parse(argv) {
54
54
  }
55
55
  }
56
56
 
57
- module.exports = { FLAGS, VALUED_FLAGS, parse }
57
+ module.exports = { FLAGS, parse }
@@ -132,6 +132,16 @@ function learn(agent, cli) {
132
132
  // aplicada editando frontmatter a mano es justo el paso que un agente hace mal en silencio.
133
133
  // Un recorrido aprende de sus corridas y un cargo de su profesión: mismo ciclo, distinto insumo.
134
134
  const kind = cli.has('--flow') ? 'flow' : 'agent'
135
+ // El tercer destino: se miró y no cambia nada. Sin esto sólo había aplicarla o dejarla esperando,
136
+ // y lo que se hacía era mergear el PR sin firmar — que deja el documento diciendo «sin aplicar»
137
+ // para siempre, indistinguible de una que espera trabajo.
138
+ if (cli.has('--archived')) {
139
+ const result = L.archive(opsRoot(), agent, cli.value('--period'), kind)
140
+ const relative = path.relative(opsRoot(), result.file)
141
+ return console.log(result.already
142
+ ? `= ${relative} ya estaba archivada`
143
+ : `✓ ${relative} queda archivada: se miró y no cambia nada`)
144
+ }
135
145
  if (cli.has('--applied')) {
136
146
  const result = L.seal(opsRoot(), agent, cli.value('--period'), kind)
137
147
  const relative = path.relative(opsRoot(), result.file)
@@ -139,14 +149,28 @@ function learn(agent, cli) {
139
149
  ? `= ${relative} ya estaba aplicada`
140
150
  : `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
141
151
  }
152
+ // Un recorrido no tiene informe semanal: su propuesta se compone de los veredictos de sus propias
153
+ // corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre el informe
154
+ // de la semana— no tiene entonces qué abrir acá, y `prepareReport` lo decía resolviendo con el
155
+ // `kind` por defecto: «no existe agents/<tipo>/<slug>/SKILL.md», que manda a crear un cargo que no
156
+ // falta. Negarse nombrando el comando que sí corresponde es lo que cierra R13.
157
+ if (kind === 'flow' && !cli.has('--proposal')) {
158
+ fail(`${agent} es un recorrido: aprende de sus corridas, no de informes semanales.\n`
159
+ + ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, 2)
160
+ }
142
161
  // `--period` es para consolidar a mano un mes que no es el de hoy. El ciclo automático no lo
143
162
  // pasa: la propuesta se llama por el mes en que se abre y arrastra lo que todavía no entró.
144
163
  const result = cli.has('--proposal')
145
164
  ? L.prepareProposal(opsRoot(), agent, new Date(), cli.value('--period'), kind)
146
165
  : L.prepareReport(opsRoot(), agent)
147
- // Sin informes pendientes no hay archivo que nombrar, y decirlo es el resultado: el ciclo corrió
148
- // y no había nada que consolidar.
149
- if (!result.file) return console.log('= sin informes pendientes: no se abre propuesta')
166
+ // Sin archivo que nombrar hay dos resultados distintos y decir cuál es el punto: no había material,
167
+ // o lo había y ninguno propuso un cambio. El segundo no es un error ni un mes perdido —el informe
168
+ // es histórico igual— y confundirlo con el primero manda a buscar por qué no se investigó.
169
+ if (!result.file) {
170
+ return console.log(result.quiet
171
+ ? `= ${result.quiet} informe(s) sin recomendación: no hay qué proponer, quedan para el mes que viene`
172
+ : '= sin informes pendientes: no se abre propuesta')
173
+ }
150
174
  console.log(`${result.created ? '+' : '='} ${path.relative(opsRoot(), result.file)}`)
151
175
  // Un cargo consolida informes de su profesión; un recorrido, las corridas que lo midieron.
152
176
  if (typeof result.reports === 'number') {
@@ -170,7 +194,12 @@ function evaluate(agent, caso, cli) {
170
194
  }
171
195
  // El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
172
196
  // un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
173
- return console.log(evaluationBench(root, agent, caso, cli.has('--force'), kind))
197
+ //
198
+ // Relativa a la raíz, como la de `--record`: esta salida la lee el agente que prepara los bancos,
199
+ // y de ahí la ruta viaja a los informes y a las propuestas que después lee otro cargo. Absoluta
200
+ // nombraba el directorio personal de una máquina, y así quedaron mil cuatrocientas ochenta y cinco
201
+ // citas que dejaron de resolver el día que este repositorio cambió de nombre.
202
+ return console.log(path.relative(root, evaluationBench(root, agent, caso, cli.has('--force'), kind)))
174
203
  }
175
204
  try {
176
205
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
@@ -7,7 +7,6 @@
7
7
 
8
8
  const fs = require('node:fs')
9
9
  const path = require('node:path')
10
- const { spawnSync } = require('node:child_process')
11
10
  const F = require('../core/files')
12
11
  const O = require('../core/ownership')
13
12
  const CL = require('../core/changelog')
@@ -15,7 +14,6 @@ const M = require('../core/manifest')
15
14
  const OB = require('../core/onboarding')
16
15
  const P = require('../planning/parser')
17
16
  const ST = require('../planning/state')
18
- const I = require('../integrations/registry')
19
17
  const A = require('../automation')
20
18
  const { fail } = require('./io')
21
19
 
@@ -31,9 +29,14 @@ function providerNames() {
31
29
  } catch { return [] }
32
30
  }
33
31
 
32
+ // Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
33
+ // un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
34
+ // el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
35
+ // que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
34
36
  function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
35
37
  F.assertNoSymlinkPath(path.dirname(target), target)
36
38
  fs.mkdirSync(target, { recursive: true })
39
+ const preserved = {}
37
40
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
38
41
  if (skip.includes(entry.name)) continue
39
42
  const from = path.join(source, entry.name)
@@ -41,11 +44,14 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
41
44
  // 11.16.0: lo deja afuera en la raíz y en subdirectorios—, así que viaja sin punto y se restituye
42
45
  // acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
43
46
  const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
44
- if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
47
+ if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
45
48
  else {
46
49
  if (fs.existsSync(to)) {
47
50
  if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
48
51
  if (!quiet) console.log(`= conservado ${to}`)
52
+ let would = fs.readFileSync(from, 'utf8')
53
+ for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
54
+ preserved[to] = M.digestText(would)
49
55
  continue
50
56
  }
51
57
  let content = fs.readFileSync(from, 'utf8')
@@ -55,22 +61,29 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
55
61
  if (!quiet) console.log(`+ ${to}`)
56
62
  }
57
63
  }
64
+ return preserved
58
65
  }
59
66
 
67
+ // Devuelve lo conservado igual que `copyTemplate`, y por la misma razón: acá el runtime no lleva
68
+ // reemplazos, así que lo que habríamos escrito es el archivo del paquete tal cual.
60
69
  function copyRuntime(source, target, preserve = false, boundary = target, skip = []) {
61
70
  F.assertNoSymlinkPath(boundary, target)
62
71
  fs.mkdirSync(target, { recursive: true })
72
+ const preserved = {}
63
73
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
64
74
  if (skip.includes(entry.name)) continue
65
75
  const from = path.join(source, entry.name)
66
76
  const to = path.join(target, entry.name)
67
- if (entry.isDirectory()) copyRuntime(from, to, preserve, boundary, skip)
68
- else if (preserve && fs.existsSync(to)) console.log(`= conservado ${to}`)
69
- else {
77
+ if (entry.isDirectory()) Object.assign(preserved, copyRuntime(from, to, preserve, boundary, skip))
78
+ else if (preserve && fs.existsSync(to)) {
79
+ console.log(`= conservado ${to}`)
80
+ preserved[to] = M.digest(from)
81
+ } else {
70
82
  F.assertNoSymlinkPath(boundary, to)
71
83
  fs.copyFileSync(from, to)
72
84
  }
73
85
  }
86
+ return preserved
74
87
  }
75
88
 
76
89
  // Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
@@ -118,7 +131,7 @@ function undeclareEngine(manifest) {
118
131
  // el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
119
132
  // cómo venga escrita la línea de comandos.
120
133
  function scaffold(root, { name, mode, force = false, quiet = false }) {
121
- copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
134
+ const preserved = copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
122
135
  '{{PROJECT_NAME}}': name,
123
136
  '{{MODE}}': mode,
124
137
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
@@ -126,12 +139,12 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
126
139
  // No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
127
140
  // el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
128
141
  // `.github/workflows/` vacío en cada instancia.
129
- copyRuntime(
142
+ Object.assign(preserved, copyRuntime(
130
143
  path.join(PROJECT_ROOT, 'automatization', 'hooks'),
131
144
  path.join(root, 'automatization', 'hooks'),
132
145
  force,
133
146
  root,
134
- )
147
+ ))
135
148
  const version = require(path.join(PROJECT_ROOT, 'package.json')).version
136
149
  // El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
137
150
  // declarar npm acá no convierte en Node al servicio de Go de al lado.
@@ -142,6 +155,14 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
142
155
  if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
143
156
  }
144
157
  deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
158
+ // Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
159
+ // justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
160
+ // archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
161
+ // rastrea, y un archivo propio fuera de esa frontera no le incumbe.
162
+ for (const [file, hash] of Object.entries(preserved)) {
163
+ const relative = path.relative(root, file).split(path.sep).join('/')
164
+ if (relative in deliveredPaths) deliveredPaths[relative] = hash
165
+ }
145
166
  M.write(root, deliveredPaths)
146
167
  // La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
147
168
  const configFile = path.join(root, 'ops.config.json')
@@ -307,12 +328,20 @@ function upgrade(dir, cli) {
307
328
  const overrides = O.overrides(root)
308
329
 
309
330
  if (dry) {
331
+ // Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
332
+ // —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
333
+ // útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
334
+ // que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
335
+ // la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
336
+ for (const file of changed) console.log(` editado localmente: ${file}`)
310
337
  if (from === to) {
311
338
  // Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
312
339
  // importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
313
340
  // a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
314
341
  console.log(`= ${to}: la instancia está al día con el motor instalado`)
315
- return console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
342
+ console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
343
+ if (changed.length) process.exit(1)
344
+ return
316
345
  }
317
346
  // Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
318
347
  // una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
@@ -324,7 +353,6 @@ function upgrade(dir, cli) {
324
353
  console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
325
354
  printChangelog(from, to)
326
355
  }
327
- for (const file of changed) console.log(` editado localmente: ${file}`)
328
356
  process.exit(1)
329
357
  }
330
358
 
@@ -429,7 +457,10 @@ function upgrade(dir, cli) {
429
457
  for (const override of overrides) {
430
458
  console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
431
459
  }
432
- console.log(' planning, organization y todo lo propio quedaron intactos')
460
+ // Sólo cuando es cierto. Llegar acá con algo en `changed` significa que se corrió con --force y se
461
+ // descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
462
+ // todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
463
+ if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
433
464
  // No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
434
465
  // decide una persona.
435
466
  if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
package/engine/cli/ops.js CHANGED
@@ -4,25 +4,9 @@
4
4
  const fs = require('node:fs')
5
5
  const path = require('node:path')
6
6
  const { spawnSync } = require('node:child_process')
7
- const P = require('../planning/parser')
8
- const B = require('../planning/business-rules')
9
- const PC = require('../planning/contracts')
10
- const ST = require('../planning/state')
11
- const I = require('../integrations/registry')
12
- const L = require('../agents/learning')
13
7
  const A = require('../automation')
14
- const F = require('../core/files')
15
- const O = require('../core/ownership')
16
- const CL = require('../core/changelog')
17
- const M = require('../core/manifest')
18
- const SC = require('../core/scan')
19
- const OB = require('../core/onboarding')
20
- const C = require('../config/validate')
21
- const T = require('../flows/registry')
22
- const AG = require('../agents/catalog')
23
- const EV = require('../agents/evaluations')
24
8
  const { FLAGS, parse } = require('./args')
25
- const { fail, opsRoot } = require('./io')
9
+ const { fail } = require('./io')
26
10
  const IN = require('./instance')
27
11
  const PL = require('./planning')
28
12
  const CAT = require('./catalog')
@@ -37,8 +21,14 @@ const DEFAULT_TARGET = 'ops'
37
21
  // de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
38
22
  // instancia; no hay una segunda adentro.
39
23
  function implicitTarget(cwd) {
40
- const base = path.basename(cwd)
41
- return base === DEFAULT_TARGET || base.endsWith('-ops') ? '.' : DEFAULT_TARGET
24
+ return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
25
+ }
26
+
27
+ // La misma heurística, aplicada al destino ya resuelto: una carpeta que ya nombra al toolkit **es** la
28
+ // instancia, y el código de la empresa vive al lado.
29
+ function isInstanceDir(dir) {
30
+ const base = path.basename(dir)
31
+ return base === DEFAULT_TARGET || base.endsWith('-ops')
42
32
  }
43
33
 
44
34
  // El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
@@ -89,7 +79,12 @@ async function init(target, cli) {
89
79
  // install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
90
80
  // único que faltaba era que fuera lo que pasa cuando no se elige nada.
91
81
  const root = path.resolve(target || implicitTarget(process.cwd()))
92
- const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
82
+ // El modo lo decide a dónde apunta el destino, no si alguien escribió el argumento. `init` e `init .`
83
+ // resuelven el mismo directorio y elegían modos opuestos: escribir el punto —la forma más natural de
84
+ // decir «acá»— daba embedded, y ahí la raíz del workspace pasa a ser la carpeta ops misma, así que
85
+ // `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
86
+ // adentro en vez de al lado, donde el dev abre su herramienta.
87
+ const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
93
88
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
94
89
  const name = cli.value('--name', defaultName(root))
95
90
  const force = cli.has('--force')
@@ -170,7 +165,7 @@ function usage() {
170
165
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
171
166
  ops automation install <ops-root> claude|codex|gemini|antigravity
172
167
  ops automation uninstall <ops-root> claude|codex|gemini|antigravity
173
- ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>]]
168
+ ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied|--archived [--period <AAAA-MM>]]
174
169
  ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
175
170
  ops agents list [ops-root] [--own|--system] [--json]
176
171
  ops agents fork <cargo> [ops-root]
@@ -45,10 +45,15 @@ function validateWorkspaces(workspaces, errors) {
45
45
  continue
46
46
  }
47
47
  for (const key of Object.keys(workspace)) {
48
- if (!['name', 'path'].includes(key)) {
48
+ if (!['name', 'path', 'verify'].includes(key)) {
49
49
  errors.push(`ops.config.json: workspaceRoots[${index}].${key} no está permitido`)
50
50
  }
51
51
  }
52
+ // Opcional: la raíz que no lo declara deja que quien verifica descubra la puerta, como siempre.
53
+ // Declararlo vacío es peor que no declararlo —promete una puerta y no la da—, así que se rechaza.
54
+ if ('verify' in workspace && (typeof workspace.verify !== 'string' || !workspace.verify.trim())) {
55
+ errors.push(`ops.config.json: workspaceRoots[${index}].verify debe ser el comando, o no estar`)
56
+ }
52
57
  if (typeof workspace.name !== 'string' || !workspace.name.trim()) {
53
58
  errors.push(`ops.config.json: workspaceRoots[${index}].name es obligatorio`)
54
59
  }
@@ -78,4 +83,4 @@ function validateRunner(runner, errors) {
78
83
  }
79
84
  }
80
85
 
81
- module.exports = { MODES, validateOpsConfig }
86
+ module.exports = { validateOpsConfig }
@@ -120,8 +120,5 @@ module.exports = {
120
120
  missingSections,
121
121
  orphanCredentials,
122
122
  guide,
123
- OPENING,
124
- DIMENSIONS,
125
- FOLLOW_UPS,
126
123
  PLACEHOLDERS,
127
124
  }
@@ -218,4 +218,4 @@ module.exports = {
218
218
  workspaceRoots,
219
219
  candidates,
220
220
  inventory,
221
- commandsLine, scan, services, expectedEnv, IGNORED, DEPTH, ENV_MAX }
221
+ commandsLine, scan, services }
@@ -124,6 +124,6 @@ function findOpsRoot(start) {
124
124
  }
125
125
 
126
126
  module.exports = {
127
- readInput, commandOf, fileOf, patchOf, filesOf, contentOf, cwdOf, block, configOf,
127
+ readInput, commandOf, patchOf, filesOf, contentOf, cwdOf, block, configOf,
128
128
  gitDirectory, isCommit, stagedFiles, pushAllowed, findOpsRoot,
129
129
  }
@@ -79,4 +79,4 @@ function validate(root, provider, workspaces = []) {
79
79
  return errors
80
80
  }
81
81
 
82
- module.exports = { STATES, TYPES, read, validate }
82
+ module.exports = { read, validate }
@@ -161,4 +161,4 @@ async function fetchItems(config, options = {}) {
161
161
  return issues.map((issue) => normalizeIssue(issue, config))
162
162
  }
163
163
 
164
- module.exports = { adfToMarkdown, deriveAcceptance, fetchItems, normalizeFixture, normalizeIssue, validateConfig }
164
+ module.exports = { fetchItems, normalizeFixture, validateConfig }
@@ -397,7 +397,6 @@ function providerState(root, name) {
397
397
 
398
398
  module.exports = {
399
399
  providerState,
400
- STATES,
401
400
  adapter,
402
401
  frontmatter,
403
402
  promote,
@@ -53,4 +53,4 @@ function validate(root) {
53
53
  return errors
54
54
  }
55
55
 
56
- module.exports = { markdownFiles, validate }
56
+ module.exports = { validate }
@@ -297,6 +297,14 @@ function validateState({ epics, milestones, done, wip, roles = new Set(), humanA
297
297
  for (const milestone of milestones) {
298
298
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(milestone.slug)) errors.push(`hito con slug inválido: ${milestone.slug}`)
299
299
  if (milestoneSlugs.has(milestone.slug)) errors.push(`hito duplicado: ${milestone.slug}`)
300
+ // La misma puerta que la épica y la tarea, en el nivel que no la tenía. Un hito agrupa trabajo bajo
301
+ // un nombre, y abrirlo con el nombre puesto para después es el camino natural: nadie promueve una
302
+ // tarea sin aceptación, y en cambio el título del hito se decide al final, cuando ya se construyó
303
+ // debajo. El marcador es lo único que se puede juzgar sin leer — un título vago sigue pasando, y
304
+ // eso es deliberado: acá también un heurístico sobre prosa se equivocaría en los dos sentidos.
305
+ if (PLACEHOLDERS.test(milestone.title)) {
306
+ errors.push(`hito ${milestone.slug}: el título no está decidido — "${milestone.title}"`)
307
+ }
300
308
  milestoneSlugs.add(milestone.slug)
301
309
  for (const task of milestone.tasks) {
302
310
  if (!task.service) errors.push(`BACKLOG ${task.slug}: falta (service: <ruta>)`)
@@ -406,7 +414,6 @@ function oversizedUnits({ epics = [], milestones = [] }) {
406
414
  }
407
415
 
408
416
  module.exports = {
409
- testedCriteria,
410
417
  validateState,
411
418
  oversizedUnits,
412
419
  validateAdr,
@@ -153,6 +153,12 @@ function readCast(rest) {
153
153
  }
154
154
  }
155
155
 
156
+ // El texto de la aceptación, delimitado por las cursivas que lo envuelven. El cierre no es «el
157
+ // próximo guión bajo»: un identificador como `MAX_ATTEMPTS` trae los suyos, y cortar en el primero
158
+ // devolvía `MAX` con `check` en verde, que es la forma cara del error —la tarea se lee completa y no
159
+ // lo está—. Cierra el `_` que markdown cerraría: el que no está entre caracteres de palabra.
160
+ const ACCEPTANCE = /_Aceptaci[oó]n:\s*(.*?\S)_(?![A-Za-z0-9])/i
161
+
156
162
  function readBacklog(dir) {
157
163
  const text = withoutComments(read(path.join(dir, 'BACKLOG.md')))
158
164
  const milestones = []
@@ -175,7 +181,7 @@ function readBacklog(dir) {
175
181
  slug: task[1].trim(), tier: task[2] || '', cast: readCast(rest),
176
182
  epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
177
183
  service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
178
- acceptance: ((rest.match(/_Aceptaci[oó]n:\s*([^_]+)_/i) || [])[1] || '').trim(),
184
+ acceptance: ((rest.match(ACCEPTANCE) || [])[1] || '').trim(),
179
185
  criteria: criteriaRefs(rest),
180
186
  noSplit: noSplitReason(rest),
181
187
  })