@ingeniomaps/cauce 0.53.0 → 0.53.2

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 (52) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/automatization/hooks/guard-dependencies.sh +1 -1
  3. package/automatization/hooks/guard-destructive.sh +1 -1
  4. package/automatization/hooks/guard-engine.sh +1 -1
  5. package/automatization/hooks/guard-generated.sh +1 -1
  6. package/automatization/hooks/guard-git-add.sh +1 -1
  7. package/automatization/hooks/guard-governance.sh +1 -1
  8. package/automatization/hooks/guard-integration-snapshot.sh +1 -1
  9. package/automatization/hooks/guard-migrations.sh +1 -1
  10. package/automatization/hooks/guard-planning-drift.sh +1 -1
  11. package/automatization/hooks/guard-secrets.sh +1 -1
  12. package/automatization/hooks/guard-test-evidence.sh +1 -1
  13. package/automatization/hooks/guard-verify.sh +1 -1
  14. package/automatization/hooks/guard-workspace-boundary.sh +1 -1
  15. package/automatization/hooks/run-hook.sh +3 -2
  16. package/automatization/runners/antigravity/hook.js +13 -14
  17. package/automatization/shared/eval-only.js +3 -3
  18. package/automatization/workflows/agent-eval.js +3 -3
  19. package/automatization/workflows/agent-promote.js +4 -4
  20. package/automatization/workflows/autobuild.js +12 -12
  21. package/automatization/workflows/flow-eval.js +10 -7
  22. package/automatization/workflows/flow.js +9 -10
  23. package/automatization/workflows/onboard.js +4 -5
  24. package/engine/agents/evaluations.js +18 -16
  25. package/engine/agents/learning-files.js +111 -0
  26. package/engine/agents/learning-sources.js +166 -0
  27. package/engine/agents/learning.js +93 -300
  28. package/engine/automation/config.js +175 -0
  29. package/engine/automation/hooks.js +96 -0
  30. package/engine/automation/index.js +17 -431
  31. package/engine/automation/roles.js +72 -0
  32. package/engine/automation/runners.js +162 -0
  33. package/engine/cli/catalog.js +7 -12
  34. package/engine/cli/instance.js +11 -10
  35. package/engine/cli/io.js +1 -1
  36. package/engine/cli/ops.js +12 -10
  37. package/engine/cli/wiring.js +2 -4
  38. package/engine/config/validate.js +2 -1
  39. package/engine/core/frontmatter.js +2 -1
  40. package/engine/core/ownership.js +5 -4
  41. package/engine/core/scan.js +3 -0
  42. package/engine/flows/registry.js +2 -2
  43. package/engine/hooks/files.js +160 -0
  44. package/engine/hooks/input.js +129 -0
  45. package/engine/hooks/run.js +24 -449
  46. package/engine/hooks/shell.js +197 -0
  47. package/engine/integrations/registry.js +2 -0
  48. package/engine/planning/contracts.js +12 -10
  49. package/engine/planning/parser.js +4 -3
  50. package/engine/planning/state.js +2 -1
  51. package/package.json +5 -5
  52. package/template/tools/ops.js +2 -2
package/CHANGELOG.md CHANGED
@@ -14,6 +14,41 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
14
14
  unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
15
15
  diseño — eso vive en el commit y en el código.
16
16
 
17
+ ## [0.53.2] - 2026-08-29
18
+
19
+ ### Cambiado
20
+
21
+ - **`tools/ops.js` se reemplaza y no cambia lo que hace.** Su comentario prohibía `import` y dos líneas
22
+ después nombraba el `import()` dinámico que el propio shim usa; ahora dice que lo prohibido es el
23
+ estático. Y citaba `team list`, un comando que dejó de existir. Se lo nombra porque vive en tu
24
+ instancia: `upgrade` lo va a reemplazar y, hasta que lo corras, aparece como desactualizado.
25
+
26
+ ## [0.53.1] - 2026-08-29
27
+
28
+ ### Corregido
29
+
30
+ - **`init` no instalaba el motor en Windows.** Lanzaba `npm.cmd` directo, y la documentación de Node
31
+ dice que un `.cmd` no es ejecutable por sí solo: ahora va por `cmd.exe` con el comando de argumento.
32
+ Fallaba en silencio útil —imprimía el error de npm y dejaba `npm install` entre los pasos
33
+ pendientes—, así que si venías corriéndolo a mano en Windows, ya no hace falta. Sin comprobar en un
34
+ Windows real: acá no hay uno, y el comentario del código lo dice.
35
+
36
+ ### Cambiado
37
+
38
+ - **Los trece shims de guards se reemplazan y ninguno cambia lo que bloquea.** Su comentario prometía
39
+ «qué bloquea y cómo lo hace» en `engine/hooks/run.js`, que es un registro de una línea por guard: lo
40
+ que bloquea vive en otro módulo, y el puntero mandaba al lugar equivocado. Los trece cambian sólo en
41
+ comentarios, sin una línea ejecutable distinta. Se los nombra porque viven en tu instancia: `upgrade`
42
+ los va a reemplazar y, hasta que lo corras, `automation check` los reporta desactualizados.
43
+ - **El motor se repartió en más archivos de los que tenía**, sin cambiar una sola conducta:
44
+ `engine/automation/index.js` en cinco, `engine/agents/learning.js` en tres y `engine/hooks/run.js` en
45
+ cuatro, cada uno cortado por lo que lo hace cambiar. Llega con `npm install` y no pide nada de tu
46
+ parte; se menciona porque un `require` a una ruta interna del motor —que nunca fue superficie
47
+ pública— puede haber dejado de resolver.
48
+ - Los recorridos que instala tu runner cambiaron de texto —comentarios y nombres internos, ahora en
49
+ inglés como pide la convención—. Para recibirlos hay que reinstalar el wiring; si no lo hacés, los
50
+ que ya tenés siguen funcionando igual.
51
+
17
52
  ## [0.53.0] - 2026-08-27
18
53
 
19
54
  ### Corregido
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js dependencies().
2
+ # Shim: delega `dependencies` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" dependencies
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js destructive().
2
+ # Shim: delega `destructive` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" destructive
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js engine().
2
+ # Shim: delega `engine` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" engine
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js generated().
2
+ # Shim: delega `generated` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" generated
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js gitAdd().
2
+ # Shim: delega `git-add` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" git-add
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js governance().
2
+ # Shim: delega `governance` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" governance
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js integrationSnapshot().
2
+ # Shim: delega `integration-snapshot` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" integration-snapshot
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js migrations().
2
+ # Shim: delega `migrations` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" migrations
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js planningDrift().
2
+ # Shim: delega `planning-drift` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" planning-drift
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js secrets().
2
+ # Shim: delega `secrets` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" secrets
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js testEvidence().
2
+ # Shim: delega `test-evidence` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" test-evidence
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js verify().
2
+ # Shim: delega `verify` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" verify
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bash
2
- # Shim: qué bloquea y cómo lo hace están en engine/hooks/run.js workspaceBoundary().
2
+ # Shim: delega `workspace-boundary` en run-hook.sh; el registro de engine/hooks/run.js nombra su módulo.
3
3
  exec "$(dirname "$0")/run-hook.sh" workspace-boundary
@@ -8,14 +8,14 @@ ops_root=$(CDPATH= cd -- "$hook_dir/../.." && pwd)
8
8
  # abre en la carpeta de la compañía y la raíz ops es un hermano, que ninguna búsqueda hacia arriba
9
9
  # encuentra: sin esto el guard no halla `ops.config.json` y deja pasar todo en silencio.
10
10
  export OPS_ROOT="$ops_root"
11
- # Mismo orden que tools/ops.js: la dependencia npm y, por último, el propio repositorio del toolkit.
12
- # Un guard que no encuentra su motor bloquea, nunca permite.
13
11
  # Primero el argumento, que además es lo que nombra el error de abajo.
14
12
  if [ -z "$hook_name" ]; then
15
13
  echo "BLOQUEADO: run-hook.sh requiere el nombre del guard o grupo que debe ejecutar." >&2
16
14
  exit 2
17
15
  fi
18
16
 
17
+ # La misma cascada que `packagePath` en engine/core/ownership.js, repetida acá porque el shim corre
18
+ # antes de poder cargar el motor: si cambia allá, cambia acá.
19
19
  runner=""
20
20
  for candidate in \
21
21
  "$ops_root/node_modules/@ingeniomaps/cauce/engine/hooks/run.js" \
@@ -24,6 +24,7 @@ do
24
24
  if [ -f "$candidate" ]; then runner="$candidate"; break; fi
25
25
  done
26
26
 
27
+ # Un guard que no encuentra su motor bloquea, nunca permite.
27
28
  if [ -z "$runner" ]; then
28
29
  echo "BLOQUEADO [$hook_name]: no se encontró el motor de hooks de Cauce." >&2
29
30
  echo " Buscado en node_modules/@ingeniomaps/cauce y engine/ bajo $ops_root" >&2
@@ -28,7 +28,7 @@ const OPS_ROOT = '{{OPS_ROOT}}'
28
28
  // desde el repositorio lo que sólo existe instalado. Sin eso, la resolución de la raíz —donde ya se
29
29
  // escondieron dos fallas que negaban cada llamada a herramienta— sólo se puede probar sobre una copia,
30
30
  // y una copia no la mide ninguna cobertura.
31
- const MARCAS = { dir: OPS_DIR, root: OPS_ROOT, plugin: __dirname }
31
+ const MARKERS = { dir: OPS_DIR, root: OPS_ROOT, plugin: __dirname }
32
32
 
33
33
  function isRoot(dir) {
34
34
  const instance = fs.existsSync(path.join(dir, 'planning'))
@@ -36,11 +36,11 @@ function isRoot(dir) {
36
36
  return fs.existsSync(path.join(dir, 'ops.config.json')) && (instance || toolkit)
37
37
  }
38
38
 
39
- function declaredRoot(marcas) {
40
- if (!marcas.root.startsWith('{{') && isRoot(marcas.root)) return marcas.root
39
+ function declaredRoot(markers) {
40
+ if (!markers.root.startsWith('{{') && isRoot(markers.root)) return markers.root
41
41
  // El plugin corriendo desde donde `automation install` lo dejó, que es el caso sin registrar.
42
- const installed = path.resolve(marcas.plugin, '..', '..', '..')
43
- const root = marcas.dir.startsWith('{{') ? installed : path.join(installed, marcas.dir)
42
+ const installed = path.resolve(markers.plugin, '..', '..', '..')
43
+ const root = markers.dir.startsWith('{{') ? installed : path.join(installed, markers.dir)
44
44
  return fs.existsSync(path.join(root, 'ops.config.json')) ? root : ''
45
45
  }
46
46
 
@@ -58,8 +58,8 @@ function childRoot(dir) {
58
58
  return roots.length === 1 ? roots[0] : ''
59
59
  }
60
60
 
61
- function findRoot(input, marcas = MARCAS) {
62
- const declared = declaredRoot(marcas)
61
+ function findRoot(input, markers = MARKERS) {
62
+ const declared = declaredRoot(markers)
63
63
  if (declared) return declared
64
64
  const args = input.toolCall && input.toolCall.args || {}
65
65
  const starts = [args.Cwd, process.cwd(), ...(input.workspacePaths || [])].filter(Boolean)
@@ -79,7 +79,6 @@ function findRoot(input, marcas = MARCAS) {
79
79
  }
80
80
 
81
81
  function runtimeAt(root) {
82
- // Mismo orden que tools/ops.js: dependencia npm y, por último, el repositorio del toolkit.
83
82
  const candidates = [
84
83
  path.join(root, 'node_modules', '@ingeniomaps', 'cauce', 'engine', 'hooks', 'run.js'),
85
84
  path.join(root, 'engine', 'hooks', 'run.js'),
@@ -93,8 +92,8 @@ function runtimeAt(root) {
93
92
 
94
93
  // La carpeta que el runner abrió, deducida de la raíz: en sidecar la raíz ops es su hija, y en modo
95
94
  // embebido son la misma.
96
- function workspaceOf(root, marcas) {
97
- const relative = marcas.dir.startsWith('{{') ? '' : marcas.dir.replace(/\/+$/, '')
95
+ function workspaceOf(root, markers) {
96
+ const relative = markers.dir.startsWith('{{') ? '' : markers.dir.replace(/\/+$/, '')
98
97
  if (!relative) return root
99
98
  return path.resolve(root, ...relative.split('/').map(() => '..'))
100
99
  }
@@ -109,20 +108,20 @@ function within(dir, base) {
109
108
  // juzgando otro archivo, y uno que busque el repo git lo buscaría fuera del proyecto. Se respeta el
110
109
  // que manda sólo si cae adentro del workspace —si algún día manda uno real, es mejor que el nuestro—;
111
110
  // si no, el workspace, que es donde el runner dice estar trabajando.
112
- function cwdFor(args, root, marcas) {
111
+ function cwdFor(args, root, markers) {
113
112
  const declared = args.Cwd && path.resolve(String(args.Cwd))
114
- const workspace = workspaceOf(root, marcas)
113
+ const workspace = workspaceOf(root, markers)
115
114
  return declared && within(declared, workspace) ? declared : workspace
116
115
  }
117
116
 
118
- function normalize(input, root, marcas = MARCAS) {
117
+ function normalize(input, root, markers = MARKERS) {
119
118
  const args = input.toolCall && input.toolCall.args || {}
120
119
  const file = args.TargetFile || args.AbsolutePath || ''
121
120
  const content = args.CodeContent || args.ReplacementContent
122
121
  || (args.ReplacementChunks && JSON.stringify(args.ReplacementChunks)) || ''
123
122
  return {
124
123
  sessionId: input.conversationId,
125
- cwd: cwdFor(args, root, marcas),
124
+ cwd: cwdFor(args, root, markers),
126
125
  tool_input: {
127
126
  command: args.CommandLine || '',
128
127
  file_path: file,
@@ -18,7 +18,7 @@ const onlyCases = (input) => (Array.isArray(input.cases) ? input.cases : String(
18
18
 
19
19
  // Devuelve los casos pedidos, o los que faltan para que quien llama frene con su propio vocabulario.
20
20
  function pickCases(items, only) {
21
- const existen = items.map((item) => item.id)
22
- const missing = only.filter((id) => !existen.includes(id))
23
- return { existen, missing, items: items.filter((item) => only.includes(item.id)) }
21
+ const present = items.map((item) => item.id)
22
+ const missing = only.filter((id) => !present.includes(id))
23
+ return { present, missing, items: items.filter((item) => only.includes(item.id)) }
24
24
  }
@@ -5,7 +5,7 @@
5
5
  // misma razón por la que nadie corrige su propio examen.
6
6
  //
7
7
  // Dónde trabaja el cargo lo decide el modo: en el toolkit, un banco desechable por caso; en una
8
- // empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/ops.js).
8
+ // empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/catalog.js).
9
9
  // El veredicto, en cambio, se escribe siempre junto al cargo: el banco se borra, el contrato queda.
10
10
  export const meta = {
11
11
  name: 'agent-eval',
@@ -105,10 +105,10 @@ if (ONLY.length) {
105
105
  const pick = pickCases(context.items, ONLY)
106
106
  if (pick.missing.length) {
107
107
  return stop('caso-inexistente',
108
- `${AGENT} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.existen.join(', ')}`)
108
+ `${AGENT} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.present.join(', ')}`)
109
109
  }
110
110
  context.items = pick.items
111
- log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.existen.length}`)
111
+ log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.present.length}`)
112
112
  }
113
113
  // Un banco por caso, preparados por un solo agente: son comandos deterministas, y después la ruta de
114
114
  // cada caso se arma sola.
@@ -29,7 +29,7 @@ export const meta = {
29
29
  const AGENT = String((typeof args === 'string' ? args : (args || {}).agent) || '').trim()
30
30
  const PERIOD = String((args || {}).period || '').trim()
31
31
 
32
- const FIRMA = {
32
+ const SIGNATURE = {
33
33
  type: 'object', additionalProperties: false, required: ['dir', 'proposal', 'approved'],
34
34
  properties: {
35
35
  dir: { type: 'string' },
@@ -42,7 +42,7 @@ const FIRMA = {
42
42
  },
43
43
  }
44
44
 
45
- const APLICADO = {
45
+ const APPLIED = {
46
46
  type: 'object', additionalProperties: false, required: ['applied', 'files'],
47
47
  properties: {
48
48
  applied: { type: 'boolean' },
@@ -74,7 +74,7 @@ const signature = await agent(
74
74
  `- state: the literal state line.\n` +
75
75
  `- status: the literal value of the frontmatter "status:" field.\n` +
76
76
  `- hasChange: true only if "Cambio propuesto" carries a concrete change; "por definir" means false.`,
77
- { schema: FIRMA, label: 'signature' },
77
+ { schema: SIGNATURE, label: 'signature' },
78
78
  )
79
79
  if (!signature) return stop('sin-propuesta', `no se pudo leer una propuesta de ${AGENT}`)
80
80
  if (!signature.hasChange) {
@@ -119,7 +119,7 @@ const applied = await agent(
119
119
  `aplicó de lo que firmó.\n\n` +
120
120
  `No toques otros cargos. No hagas commit ni push. Devolvé qué archivos modificaste, el caso nuevo si ` +
121
121
  `lo creaste, y las desviaciones —o cadena vacía si no hubo—.`,
122
- { schema: APLICADO, label: `aplica:${AGENT}` },
122
+ { schema: APPLIED, label: `aplica:${AGENT}` },
123
123
  )
124
124
  if (!applied || !applied.applied) {
125
125
  return stop('no-aplicada', 'el cambio no se pudo aplicar; la propuesta queda como estaba')
@@ -86,6 +86,7 @@ const PLAN = {
86
86
  // Un veredicto sin manifiesto no se puede contrastar: `consulted` enumera lo que quien revisó abrió de
87
87
  // verdad, y es lo único que separa al que miró del que aprobó de memoria. No prueba que lo haya leído bien
88
88
  // —para eso habría que releerlo—, y esa asimetría es la que lo deja barato (R14).
89
+ //
89
90
  // Tres estados porque hay tres cosas distintas que decir, y con un booleano dos se pisan: «no puedo
90
91
  // aprobar esto» y «apruebo con algo que hay que corregir antes de entregar» caían las dos en el mismo
91
92
  // `false`, así que la primera gastaba igual una vuelta de corrección sobre algo que la corrección no
@@ -147,11 +148,9 @@ const BUILD = {
147
148
  properties: { test: { type: 'string' }, failure: { type: 'string' } },
148
149
  } },
149
150
  blockers: { type: 'array', items: { type: 'string' } },
150
- // Estricto en el cómo, flexible en el qué: lo que aparece y el plan no previó tiene dos destinos y
151
- // ninguno es el silencio. Un caso que esta tarea puede fijar entra con la prueba que lo fija —por eso
152
- // su `test` tiene que estar en `redFirst`—; una decisión que no le toca queda registrada con quién
153
- // puede tomarla. Implementarlo sin prueba lo vuelve invisible y descartarlo lo pierde: en los dos
154
- // casos el próximo que lo encuentre empieza de cero.
151
+ // Estricto en el cómo, flexible en el qué: un `kind` por cada uno de los dos destinos que R6 le da a
152
+ // lo que aparece y el plan no previó. Lo que agrega este recorrido es el enganche el caso que esta
153
+ // tarea puede fijar entra con la prueba que lo fija, y por eso su `test` tiene que estar en `redFirst`.
155
154
  discovered: { type: 'array', items: { type: 'object', additionalProperties: false,
156
155
  required: ['kind', 'detail'],
157
156
  properties: {
@@ -228,23 +227,23 @@ const BASE = `Nunca inventes credenciales ni decisiones; registrá los bloqueos
228
227
  // Acompaña a todo prompt con schema DECISION: el schema obliga a llenar `consulted`, y esto obliga a
229
228
  // llenarlo con lo que se abrió en vez de con lo que se pensaba mirar.
230
229
  const MANIFEST = ' Enumerá en consulted cada archivo, diff o comando que hayas abierto de verdad, con su ruta.'
231
- // Acompaña a todo prompt con schema DECISION. El criterio va escrito porque tres estados sin criterio son
232
- // tres nombres, y el del medio —el que evita gastar una corrección en lo que no se corrige— es el que se
233
- // pierde primero.
230
+ // Acompaña a todo prompt con schema DECISION. Sin el criterio escrito los tres estados son tres nombres
231
+ // y el del medio se pierde primero; por qué son tres está donde se declaran.
234
232
  const VERDICT = ' Cerrá con verdict=aprobado si no queda nada por corregir antes de entregar; ' +
235
233
  'verdict=con-condiciones si lo que falta se corrige dentro de este mismo cambio; y verdict=bloqueado ' +
236
234
  'si algo no se resuelve acá —el diseño no lo cubre, falta una decisión ajena, o la corrección excede el ' +
237
235
  'alcance—. Marcá blocking=true sólo en el hallazgo que impide entregar: el resto queda registrado y no ' +
238
236
  'manda a tocar código.'
237
+ // Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
238
+ const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
239
239
  // Atajo para reconocer un gate que corrió pruebas sin preguntarle a nadie. No alcanza solo y no
240
240
  // pretende hacerlo: `mvn verify`, `gradle build`, `tox`, `bin/rails t` y cualquier `make` con nombre
241
241
  // propio corren pruebas y no se parecen a esto, así que el que corrió el comando además lo declara en
242
242
  // `ranTests` y vale cualquiera de los dos. Una lista de nombres siempre le va a faltar el siguiente;
243
243
  // lo que no puede es frenar una corrida legítima por no conocerlo.
244
+ //
244
245
  // El borde izquierdo va explícito en vez de `\b` porque `\b(?:` se lee igual que una llamada a `b()`
245
246
  // y la comprobación de identificadores del paquete de pruebas la marca como función inexistente.
246
- // Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
247
- const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
248
247
  const RUNS_TESTS = /(?:^|[\s/:=-])(?:tests?|specs?|pytest|jest|vitest|mocha|rspec|phpunit|ci|check)\b/i
249
248
  {{INCLUDE:shared/workflow-finish.js}}
250
249
 
@@ -512,8 +511,8 @@ while (rounds++ < MAX_TASKS) {
512
511
  // Lo que queda abierto no lo cierra quien lo encuentra, pero tampoco frena lo que sí se pudo entregar.
513
512
  // Tres corridas reales terminaron acá y las tres traían `completed: true`: el hueco nunca fue «no puedo»
514
513
  // sino «hay un borde que alguien tiene que decidir», y una aceptación escrita en prosa siempre tiene uno.
515
- // Frenar por eso frenaba siempre, y un freno que salta siempre se termina apagando. Lo que de verdad
516
- // bloquea ya tiene camino —`completed: false` con su blocker—; esto se registra y sigue.
514
+ // Frenar por eso frenaba siempre, que es el freno que R6 desaconseja. Lo que de verdad bloquea ya
515
+ // tiene camino —`completed: false` con su blocker—; esto se registra y sigue.
517
516
  const openDecisions = build.discovered.filter((entry) => entry.kind === 'open')
518
517
  if (openDecisions.length) {
519
518
  await write(`Registrá en ${HUMAN} una fila por cada decisión que ${task.id} dejó abierta, con qué la ` +
@@ -522,6 +521,7 @@ while (rounds++ < MAX_TASKS) {
522
521
  }
523
522
  // Y un caso que sí se fijó acá entra con su prueba o no entró: sin ella el comportamiento nuevo queda
524
523
  // sin nada que lo sostenga, y nadie sabe después que debía existir.
524
+ //
525
525
  // Los dos campos salen de la misma respuesta pero se escriben por separado, así que pedirles la misma
526
526
  // cadena exacta frena una tarea correcta por haber nombrado el test de dos formas —`TestAlta` acá y
527
527
  // `users_test.go::TestAlta` allá—. Alcanza con que uno nombre al otro; lo que sigue frenando, que es de
@@ -39,10 +39,9 @@ const CASES = {
39
39
  },
40
40
  } },
41
41
  forbidden: { type: 'array', items: { type: 'string' } },
42
- // Qué CLI existe acá. Una empresa tiene el shim `tools/ops.js`; el repo del toolkit no lo tiene y su
43
- // motor está en `engine/cli/ops.js`. `agent-eval` lo aprendió esta mañana y esto quedó con la ruta
44
- // puesta a mano: el agente de bancos reportó los cuatro casos como fallidos y el freno de banco
45
- // viejo detuvo cuatro corridas cuyos bancos estaban recién borrados.
42
+ // Qué CLI existe acá, por lo mismo que en `agent-eval` y con el mismo síntoma: sin la ruta puesta,
43
+ // el agente de bancos reportó los cuatro casos como fallidos y el freno de banco viejo detuvo cuatro
44
+ // corridas cuyos bancos estaban recién borrados.
46
45
  cli: { type: 'string' },
47
46
  mode: { type: 'string' },
48
47
  },
@@ -92,10 +91,10 @@ if (ONLY.length) {
92
91
  const pick = pickCases(context.items, ONLY)
93
92
  if (pick.missing.length) {
94
93
  return stop('caso-inexistente',
95
- `${FLOW} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.existen.join(', ')}`)
94
+ `${FLOW} no tiene ${pick.missing.join(', ')}. Tiene: ${pick.present.join(', ')}`)
96
95
  }
97
96
  context.items = pick.items
98
- log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.existen.length}`)
97
+ log(`Sólo ${ONLY.join(', ')}: el registro va a cubrir ${ONLY.length} de ${pick.present.length}`)
99
98
  }
100
99
 
101
100
  // El recorrido escribe: épica candidata, INBOX, acciones humanas. Sin un `planning/` propio no puede
@@ -117,9 +116,13 @@ const benches = await agent(
117
116
  )
118
117
  if (!benches || !benches.path) return stop('sin-banco', 'no se pudieron preparar los bancos')
119
118
  if (benches.failed && benches.failed.length) {
119
+ // Los bancos de los casos que fallaron, no el del recorrido entero. Nombrar `BENCH_ROOT` se llevaba
120
+ // por delante los bancos de los casos que nadie estaba re-corriendo: pasó re-midiendo uno solo de
121
+ // `change-review`, y el mensaje proponía borrar también el del caso vecino. Los ids ya están acá.
120
122
  return stop('banco-sin-rehacer',
121
123
  `${benches.failed.join(', ')}: su banco conserva trabajo sin recoger de una corrida anterior. ` +
122
- `Guardá el registro de esa corrida y volvé a armarlo con --force, o borrá ${BENCH_ROOT}.`)
124
+ `Guardá el registro de esa corrida y volvé a armarlo con --force, o borrá ` +
125
+ `${benches.failed.map((id) => `${BENCH_ROOT}/${id}`).join(', ')}.`)
123
126
  }
124
127
  // Un caso de recorrido gasta el recorrido entero, no dos agentes: de ahí que se diga aparte.
125
128
  log(`${context.items.length} caso(s) de ${FLOW} — cada uno corre el recorrido completo`)
@@ -69,9 +69,8 @@ const MANIFEST = {
69
69
  id: { type: 'string' }, agent: { type: 'string' }, exitGate: { type: 'string' },
70
70
  phase: { type: 'string', enum: ['discovery', 'delivery'] },
71
71
  produces: { type: 'array', items: { type: 'string' } },
72
- // El recorrido no lo usa —las etapas corren en orden—, pero el contrato lo trae y al agente se le
73
- // pide que reporte lo que el comando imprimió. Rechazarlo hacía que el reintento volviera a
74
- // copiarlo hasta agotar el cap, y la corrida moría sin haber hecho nada.
72
+ // El recorrido no lo usa —las etapas corren en orden—, pero el contrato lo trae y el agente lo
73
+ // copia, así que rechazarlo mataba la corrida por el reintento que se describe en `completion`.
75
74
  dependsOn: { type: 'array', items: { type: 'string' } },
76
75
  // Resuelto acá una vez: sin esto cada etapa gasta llamadas buscando el contrato de su cargo,
77
76
  // que además ya no vive en el proyecto sino en el paquete.
@@ -190,11 +189,11 @@ const owners = (contract.owners || []).map((owner) => `${owner.domain}=${owner.a
190
189
  // Qué recorridos existen. Va en las reglas comunes y no sólo en la etapa que enruta: cualquier etapa
191
190
  // puede nombrar un destino al cerrar, y son unos pocos slugs. Sin esto `intake` —que existe para
192
191
  // enrutar— recomendaba de memoria, que es la conducta que los casos de los cargos castigan.
193
- const catalogo = (contract.flows || []).filter((slug) => slug !== FLOW)
192
+ const catalog = (contract.flows || []).filter((slug) => slug !== FLOW)
194
193
  const RULES = `${BASE}\n\nRecorrido ${contract.name}: ${contract.purpose}\n` +
195
194
  `Guardrails: ${contract.guardrails.join(' ')}\n` +
196
195
  `${owners ? `Dueños de decisión: ${owners}. Ningún otro cargo resuelve en su dominio.\n` : ''}` +
197
- `${catalogo.length ? `Recorridos que existen además de éste: ${catalogo.join(', ')}. Si nombrás un `
196
+ `${catalog.length ? `Recorridos que existen además de éste: ${catalog.join(', ')}. Si nombrás un `
198
197
  + `destino, sale de esa lista; si ninguno sirve, decilo con su razón en vez de inventar uno.\n` : ''}` +
199
198
  `Contexto de la empresa en ${WORKDIR}/organization/. Intención a evaluar: ${GOAL}`
200
199
 
@@ -219,8 +218,8 @@ function ancestors(stage, index) {
219
218
  if (out.has(id)) continue
220
219
  out.add(id)
221
220
  const found = discovery.find((one) => one.id === id)
222
- const arriba = found && found.dependsOn ? found.dependsOn : []
223
- for (const up of arriba) pending.push(up)
221
+ const upstream = found && found.dependsOn ? found.dependsOn : []
222
+ for (const up of upstream) pending.push(up)
224
223
  }
225
224
  return [...out]
226
225
  }
@@ -250,12 +249,12 @@ function levels(stages) {
250
249
 
251
250
  const runStage = (stage, index) => {
252
251
  const visible = handoffs.filter((entry) => ancestors(stage, index).includes(entry.id))
253
- const abiertas = openConditions(visible)
252
+ const open = openConditions(visible)
254
253
  const previous = visible.length
255
254
  ? `Handoffs previos:\n${visible.map((entry) => `- ${entry.id}: ${entry.summary}`).join('\n')}`
256
- + (abiertas.length
255
+ + (open.length
257
256
  ? '\n\nCondiciones que dejaron las etapas anteriores y tenés que respetar:\n'
258
- + abiertas.map((one) => `- ${one}`).join('\n')
257
+ + open.map((one) => `- ${one}`).join('\n')
259
258
  : '')
260
259
  : 'Sos la primera etapa: no hay handoff previo.'
261
260
  return agent(
@@ -80,9 +80,8 @@ const SCAN = {
80
80
  required: ['kind', 'command', 'source'], properties: {
81
81
  kind: { type: 'string' }, command: { type: 'string' }, source: { type: 'string' },
82
82
  } } },
83
- // Los nombres de variable que ese servicio espera, copiados del inventario. En un multirepo cada
84
- // repositorio trae su propio ejemplo, y sin esto las credenciales de tres repos no existían para
85
- // el arranque: las filas terminaban diciendo «la credencial del proveedor» sin nombrarla.
83
+ // Los nombres de variable que ese servicio espera, copiados del inventario y no deducidos: de
84
+ // acá sale la fila que nombra la credencial en vez de decir «la del proveedor».
86
85
  env: { type: 'array', items: { type: 'string' } },
87
86
  } } },
88
87
  externals: { type: 'array', items: { type: 'string' } },
@@ -144,8 +143,8 @@ if (!CONTEXT && state.opening) {
144
143
  return finish({ needsContext: true, opening: state.opening, dimensions, services: services.length })
145
144
  }
146
145
 
147
- const INVENTARIO = { services, externals: state.externals || [], secrets: state.secrets || [] }
148
- const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTARIO)}` +
146
+ const INVENTORY = { services, externals: state.externals || [], secrets: state.secrets || [] }
147
+ const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTORY)}` +
149
148
  `${CONTEXT ? `\n\nContexto aportado por la persona, que vale como hecho: ${CONTEXT}` : ''}` +
150
149
  `${services.length ? '' : '\n\nNo hay ningún servicio en el workspace: el código todavía no está acá.'}`
151
150
 
@@ -39,7 +39,7 @@ function caseFiles(dir) {
39
39
  //
40
40
  // Existe porque un caso que *describe* un artefacto externo sin entregarlo mide algo más fácil de lo
41
41
  // que dice medir: al cargo se le pregunta si obedecería un documento del que se le está hablando, y un
42
- // texto que nunca leyó no puede inyectarlo. Los 47 casos adversariales del catálogo nacieron así, y uno
42
+ // texto que nunca leyó no puede inyectarlo. Los casos adversariales del catálogo nacieron así, y uno
43
43
  // produjo un fallo falso: el cargo escribió que había leído una guía inexistente porque el arnés se la
44
44
  // había afirmado.
45
45
  function fixtureFiles(dir, prefix = '') {
@@ -89,7 +89,7 @@ function list(root, agent, kind) {
89
89
  })
90
90
  }
91
91
 
92
- // Lo que el cargo no debe hacer, declarado por cargo en `expected-behaviors.yaml`.
92
+ // Lo que el cargo debe y no debe hacer, declarado por cargo en `expected-behaviors.yaml`.
93
93
  //
94
94
  // Se parsea a mano porque el archivo es una lista de escalares y el repositorio no tiene dependencias:
95
95
  // traer un parser de YAML para leer dos listas sería pagar una dependencia por un `split`.
@@ -116,7 +116,8 @@ function behaviors(root, agent, kind) {
116
116
  }
117
117
 
118
118
  // Cuándo cambió por última vez el contrato que la corrida midió. Sale de git y no del mtime del
119
- // archivo: un `npm ci` o un checkout reescriben mtimes y dirían que todo cambió hoy.
119
+ // archivo: un `npm ci` o un checkout reescriben mtimes y dirían que todo cambió hoy —comprobado con
120
+ // git 2.43.0, un `git checkout <commit> -- <archivo>` le deja el mtime de la corrida—.
120
121
  //
121
122
  // Sin esto, un contrato que se endurece deja atrás registros que siguen diciendo «pasa» — y el que
122
123
  // endurece es justo el que puede hacerlos fallar. Es la misma confusión que el resultado que cubre
@@ -231,17 +232,18 @@ function composed(root, agent, kind) {
231
232
  }
232
233
  }
233
234
 
234
- // Coherencia entre lo que hay y lo que se corrió. Todo sale como advertencia y ninguno afecta el
235
- // código de salida, y no es blandura: correr los casos exige un modelo, y CI no lo tiene. Un `evaluate`
236
- // que fallara por un resultado viejo obligaría a pagar una corrida para poder integrar, y volvería a
237
- // fallar cada vez que el contrato cambie. Quien falla fuerte es el recorrido que sí los ejecuta.
235
+ // Coherencia entre lo que hay y lo que se corrió. Lo que mide una corrida sale como advertencia y no
236
+ // toca el código de salida, y no es blandura: correr los casos exige un modelo, y CI no lo tiene. Un
237
+ // `evaluate` que fallara por un resultado viejo obligaría a pagar una corrida para poder integrar, y
238
+ // volvería a fallar cada vez que el contrato cambie. Quien falla fuerte es el recorrido que sí los
239
+ // ejecuta. Lo que sí corta con 1 es `errors`, y por qué es la excepción está donde se arma.
238
240
  function validate(root, agent, kind) {
239
241
  const warnings = []
240
242
  const cases = list(root, agent, kind)
241
243
  const total = cases.length
242
244
  // Esto sí es control estructural y no advertencia: que el artefacto esté entregado es una propiedad
243
- // estática del caso, verificable sin modelo, y dejarla en advertencia es lo que permitió que 47 casos
244
- // midieran la versión débil de su propia pregunta.
245
+ // estática del caso, verificable sin modelo, y dejarla en advertencia es lo que permitió que el
246
+ // catálogo entero midiera la versión débil de su propia pregunta.
245
247
  const errors = cases
246
248
  .filter((item) => item.id.includes('adversarial') && !item.fixtures.length)
247
249
  .map((item) => `${item.id}: caso adversarial sin artefacto en cases/${item.id}/`)
@@ -255,17 +257,17 @@ function validate(root, agent, kind) {
255
257
  warnings.push(`sin resultados de casos: corré el recorrido de evaluación para los ${total} casos`)
256
258
  return { errors, warnings, cases: total, last: null }
257
259
  }
258
- const sinMedir = cases.map((item) => item.id).filter((id) => !state.measured.has(id))
259
- if (sinMedir.length) {
260
- warnings.push(`${state.total} de ${total} caso(s) con veredicto: sin medir ${sinMedir.join(', ')}`)
260
+ const unmeasured = cases.map((item) => item.id).filter((id) => !state.measured.has(id))
261
+ if (unmeasured.length) {
262
+ warnings.push(`${state.total} de ${total} caso(s) con veredicto: sin medir ${unmeasured.join(', ')}`)
261
263
  }
262
264
  if (state.failed.length) {
263
265
  warnings.push(`${state.failed.length} caso(s) no pasan: ${state.failed.join(', ')}`)
264
266
  }
265
- const cambio = contractChangedAt(subject(root, agent, kind), kind)
266
- if (cambio && cambio > state.oldest) {
267
- const parte = state.oldest === state.newest ? 'la última corrida es' : 'el veredicto más viejo es'
268
- warnings.push(`el contrato cambió el ${cambio} y ${parte} del ${state.oldest}: `
267
+ const changedAt = contractChangedAt(subject(root, agent, kind), kind)
268
+ if (changedAt && changedAt > state.oldest) {
269
+ 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}: `
269
271
  + 'mide una versión anterior')
270
272
  }
271
273
  return { errors, warnings, cases: total, last, state }