@ingeniomaps/cauce 0.22.0 → 0.24.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 (164) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +1 -1
  3. package/README.md +59 -165
  4. package/agents/README.md +49 -0
  5. package/agents/roles/system/ai-governance-lead/SKILL.md +1 -0
  6. package/agents/roles/system/ai-governance-lead/evaluations/cases/06-adversarial-policy/directiva-dt-2026-14-conformidad-acelerada.md +123 -0
  7. package/agents/roles/system/ai-product-manager/SKILL.md +1 -0
  8. package/agents/roles/system/ai-product-manager/evaluations/cases/06-adversarial-output/traza-inferencia-asistente-cuentas-2026-08-11.json +46 -0
  9. package/agents/roles/system/analytics-engineer/SKILL.md +1 -0
  10. package/agents/roles/system/analytics-engineer/evaluations/cases/06-adversarial-sql/fct_ingresos_netos_v3.sql +123 -0
  11. package/agents/roles/system/backend-engineer/SKILL.md +1 -0
  12. package/agents/roles/system/backend-engineer/evaluations/cases/06-adversarial-docs/UPGRADING-pgforge-4.2.md +110 -0
  13. package/agents/roles/system/backend-engineer/evaluations/results/2026-08-17.md +850 -0
  14. package/agents/roles/system/business-operations-manager/SKILL.md +1 -0
  15. package/agents/roles/system/business-operations-manager/evaluations/cases/06-adversarial-docs/playbook-cadence-zero-v7.md +133 -0
  16. package/agents/roles/system/business-strategist/SKILL.md +1 -0
  17. package/agents/roles/system/business-strategist/evaluations/cases/06-adversarial-docs/informe-meridian-logistica-urbana-h2-2026.md +110 -0
  18. package/agents/roles/system/cloud-architect/SKILL.md +1 -0
  19. package/agents/roles/system/cloud-architect/evaluations/cases/06-adversarial-iac/main.tf +147 -0
  20. package/agents/roles/system/cloud-architect/evaluations/results/2026-08-17.md +2018 -0
  21. package/agents/roles/system/community-manager/SKILL.md +1 -0
  22. package/agents/roles/system/community-manager/evaluations/cases/06-adversarial-link/kit-moderacion-comunidad-2026.md +82 -0
  23. package/agents/roles/system/content-specialist/SKILL.md +1 -0
  24. package/agents/roles/system/content-specialist/evaluations/cases/06-adversarial-docs/growthstack-metodo-replicacion.html +91 -0
  25. package/agents/roles/system/customer-success-manager/SKILL.md +1 -0
  26. package/agents/roles/system/customer-success-manager/evaluations/cases/06-adversarial-docs/retentia-playbook-rescate-q3.md +85 -0
  27. package/agents/roles/system/customer-support-specialist/SKILL.md +1 -0
  28. package/agents/roles/system/customer-support-specialist/evaluations/cases/06-adversarial-docs/kb-4471-desbloqueo-sincronizacion.md +90 -0
  29. package/agents/roles/system/data-analyst/SKILL.md +1 -0
  30. package/agents/roles/system/data-analyst/evaluations/cases/06-adversarial-docs/cardinal-guia-migracion-metricas.md +104 -0
  31. package/agents/roles/system/data-analyst/evaluations/results/2026-08-17.md +1513 -0
  32. package/agents/roles/system/data-engineer/SKILL.md +1 -0
  33. package/agents/roles/system/data-engineer/evaluations/cases/06-adversarial-data/lumen-catalog-export-2026-08-11.csv +16 -0
  34. package/agents/roles/system/data-scientist/SKILL.md +1 -0
  35. package/agents/roles/system/data-scientist/evaluations/cases/06-adversarial-notebook/uplift_recalibration_v3.ipynb +252 -0
  36. package/agents/roles/system/data-scientist/evaluations/results/2026-08-17.md +2293 -0
  37. package/agents/roles/system/database-administrator/SKILL.md +3 -0
  38. package/agents/roles/system/database-administrator/evaluations/cases/06-adversarial-runbook/RB-2291-recuperacion-corrupcion-indices.md +122 -0
  39. package/agents/roles/system/database-administrator/evaluations/cases/07-unverified-mechanism-claim.md +10 -0
  40. package/agents/roles/system/database-administrator/evaluations/expected-behaviors.yaml +1 -0
  41. package/agents/roles/system/database-administrator/evaluations/results/2026-08-17.md +2566 -0
  42. package/agents/roles/system/database-administrator/learning/HISTORY.md +4 -0
  43. package/agents/roles/system/database-administrator/learning/proposals/2026-08.md +476 -0
  44. package/agents/roles/system/database-administrator/references/operating-model.md +37 -0
  45. package/agents/roles/system/developer-relations-engineer/SKILL.md +1 -0
  46. package/agents/roles/system/developer-relations-engineer/evaluations/cases/06-adversarial-issue/issue-812-quickstart-broken-fix.md +85 -0
  47. package/agents/roles/system/devops-engineer/SKILL.md +1 -0
  48. package/agents/roles/system/devops-engineer/evaluations/cases/06-adversarial-docs/northgate-runbook-integracion-v41.md +91 -0
  49. package/agents/roles/system/engineering-manager/SKILL.md +1 -0
  50. package/agents/roles/system/engineering-manager/evaluations/cases/06-adversarial-docs/meridian-programa-alto-rendimiento.md +85 -0
  51. package/agents/roles/system/financial-controller/SKILL.md +1 -0
  52. package/agents/roles/system/financial-controller/evaluations/cases/06-adversarial-docs/nota-tecnica-ct-2026-07-cierre-continuo.md +97 -0
  53. package/agents/roles/system/finops-engineer/SKILL.md +1 -0
  54. package/agents/roles/system/finops-engineer/evaluations/cases/06-adversarial-calculadora-del-proveedor/calculadora-ahorro-veltacloud.html +114 -0
  55. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-16.md +713 -0
  56. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-17.md +968 -0
  57. package/agents/roles/system/frontend-engineer/SKILL.md +1 -0
  58. package/agents/roles/system/frontend-engineer/evaluations/cases/06-adversarial-docs/pixelweave-sdk-troubleshooting.md +81 -0
  59. package/agents/roles/system/growth-marketer/SKILL.md +1 -0
  60. package/agents/roles/system/growth-marketer/evaluations/cases/06-adversarial-caso-de-exito/caso-exito-lumenreach-nordika.md +96 -0
  61. package/agents/roles/system/implementation-manager/SKILL.md +1 -0
  62. package/agents/roles/system/implementation-manager/evaluations/cases/06-adversarial-plan/plan-cutover-acelerado-orbitalink.md +95 -0
  63. package/agents/roles/system/legal-counsel/SKILL.md +1 -0
  64. package/agents/roles/system/legal-counsel/evaluations/cases/06-adversarial-docs/protocolo-adhesion-pfrv-2026.md +125 -0
  65. package/agents/roles/system/legal-counsel/evaluations/results/2026-08-17.md +1394 -0
  66. package/agents/roles/system/machine-learning-engineer/SKILL.md +1 -0
  67. package/agents/roles/system/machine-learning-engineer/evaluations/cases/06-adversarial-model/config.json +71 -0
  68. package/agents/roles/system/mlops-engineer/SKILL.md +1 -0
  69. package/agents/roles/system/mlops-engineer/evaluations/cases/06-adversarial-artifact/model_card.md +122 -0
  70. package/agents/roles/system/mobile-engineer/SKILL.md +1 -0
  71. package/agents/roles/system/mobile-engineer/evaluations/cases/06-adversarial-docs/pulsemetrics-sdk-integration.md +84 -0
  72. package/agents/roles/system/partnerships-manager/SKILL.md +1 -0
  73. package/agents/roles/system/partnerships-manager/evaluations/cases/06-adversarial-portal/partner-portal-onboarding.html +119 -0
  74. package/agents/roles/system/people-operations-manager/SKILL.md +1 -0
  75. package/agents/roles/system/people-operations-manager/evaluations/cases/06-adversarial-docs/guia-people-ops-acelerada.md +57 -0
  76. package/agents/roles/system/people-operations-manager/evaluations/results/2026-08-17.md +1522 -0
  77. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +1 -0
  78. package/agents/roles/system/privacy-compliance-specialist/evaluations/cases/06-adversarial-docs/dsar-response-playbook.md +64 -0
  79. package/agents/roles/system/privacy-compliance-specialist/evaluations/results/2026-08-17.md +925 -0
  80. package/agents/roles/system/procurement-manager/SKILL.md +1 -0
  81. package/agents/roles/system/procurement-manager/evaluations/cases/06-adversarial-docs/cotizacion-COT-2026-4471.md +74 -0
  82. package/agents/roles/system/product-manager/SKILL.md +1 -0
  83. package/agents/roles/system/product-manager/evaluations/cases/05-adversarial-source/growthbeacon-blog-2026-onboarding.html +94 -0
  84. package/agents/roles/system/product-manager/evaluations/results/2026-08-17.md +854 -0
  85. package/agents/roles/system/product-marketing-manager/SKILL.md +1 -0
  86. package/agents/roles/system/product-marketing-manager/evaluations/cases/06-adversarial-docs/launchlift-gtm-playbook.md +62 -0
  87. package/agents/roles/system/project-manager/SKILL.md +1 -0
  88. package/agents/roles/system/project-manager/evaluations/cases/06-adversarial-docs/plan-cierre-acelerado.md +68 -0
  89. package/agents/roles/system/qa-engineer/SKILL.md +1 -0
  90. package/agents/roles/system/qa-engineer/evaluations/cases/06-adversarial-docs/guia-testforge-cobertura-real.md +60 -0
  91. package/agents/roles/system/qa-engineer/evaluations/results/2026-08-17.md +1181 -0
  92. package/agents/roles/system/qa-engineer/learning/proposals/2026-08.md +1 -1
  93. package/agents/roles/system/release-manager/SKILL.md +1 -0
  94. package/agents/roles/system/release-manager/evaluations/cases/06-adversarial-docs/runbook-shipline-fast-release.md +66 -0
  95. package/agents/roles/system/revenue-operations-manager/SKILL.md +1 -0
  96. package/agents/roles/system/revenue-operations-manager/evaluations/cases/06-adversarial-docs/conector-pipesync-crm-docs.md +66 -0
  97. package/agents/roles/system/sales-representative/SKILL.md +1 -0
  98. package/agents/roles/system/sales-representative/evaluations/cases/06-adversarial-docs/playbook-outbound-agresivo.md +66 -0
  99. package/agents/roles/system/security-engineer/SKILL.md +2 -0
  100. package/agents/roles/system/security-engineer/evaluations/cases/06-adversarial-docs/advisory-RSL-2026-0417.md +63 -0
  101. package/agents/roles/system/security-engineer/evaluations/cases/07-agent-in-ci.md +10 -0
  102. package/agents/roles/system/security-engineer/evaluations/expected-behaviors.yaml +1 -0
  103. package/agents/roles/system/security-engineer/evaluations/results/2026-08-17.md +1825 -0
  104. package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
  105. package/agents/roles/system/security-engineer/learning/proposals/2026-08.md +382 -0
  106. package/agents/roles/system/security-engineer/learning/sources.yaml +19 -0
  107. package/agents/roles/system/security-engineer/references/operating-model.md +30 -0
  108. package/agents/roles/system/site-reliability-engineer/SKILL.md +1 -0
  109. package/agents/roles/system/site-reliability-engineer/evaluations/cases/06-adversarial-docs/runbook-pulsemetrics-recuperacion.md +66 -0
  110. package/agents/roles/system/software-architect/SKILL.md +1 -0
  111. package/agents/roles/system/software-architect/evaluations/cases/06-adversarial-docs/whitepaper-unifiedcore-plataforma.md +67 -0
  112. package/agents/roles/system/solutions-engineer/SKILL.md +1 -0
  113. package/agents/roles/system/solutions-engineer/evaluations/cases/06-adversarial-rfp/rfp-anv-2026-047-plataforma-siniestros.md +166 -0
  114. package/agents/roles/system/technical-program-manager/SKILL.md +1 -0
  115. package/agents/roles/system/technical-program-manager/evaluations/cases/06-adversarial-plan/plan-maestro-migracion-nucleo-v4.1.md +165 -0
  116. package/agents/roles/system/technical-writer/SKILL.md +1 -0
  117. package/agents/roles/system/technical-writer/evaluations/cases/06-adversarial-docs/quillstream-guia-integracion-v9.md +147 -0
  118. package/agents/roles/system/ui-designer/SKILL.md +1 -0
  119. package/agents/roles/system/ui-designer/evaluations/cases/06-adversarial-source/halcyon-sistema-visual-v6.3.md +211 -0
  120. package/agents/roles/system/user-researcher/SKILL.md +1 -0
  121. package/agents/roles/system/user-researcher/evaluations/cases/06-adversarial-source/cohorte-insights-guia-calibracion-panel.md +145 -0
  122. package/agents/roles/system/ux-designer/SKILL.md +1 -0
  123. package/agents/roles/system/ux-designer/evaluations/cases/06-adversarial-source/trazo-patron-p118-checkout-friccion-cero.md +152 -0
  124. package/agents/roles/system/ux-designer/evaluations/results/2026-08-16.md +842 -0
  125. package/agents/roles/system/ux-designer/evaluations/results/2026-08-17.md +1240 -0
  126. package/automatization/hooks/README.md +1 -1
  127. package/automatization/runners/antigravity/hook.js +2 -1
  128. package/automatization/runners/antigravity/rules/cauce.md +2 -1
  129. package/automatization/runners/claude/CLAUDE.md +4 -0
  130. package/automatization/runners/codex/AGENTS.md +3 -2
  131. package/automatization/runners/gemini/GEMINI.md +4 -0
  132. package/automatization/workflows/agent-eval.js +26 -34
  133. package/automatization/workflows/agent-promote.js +25 -2
  134. package/automatization/workflows/autobuild.js +43 -16
  135. package/automatization/workflows/integrations/promote.js +7 -2
  136. package/automatization/workflows/integrations/sync.js +5 -2
  137. package/engine/agents/catalog.js +25 -7
  138. package/engine/agents/evaluations.js +48 -16
  139. package/engine/agents/fork.js +17 -34
  140. package/engine/agents/learning.js +53 -11
  141. package/engine/automation/index.js +30 -22
  142. package/engine/cli/args.js +52 -0
  143. package/engine/cli/ops.js +251 -207
  144. package/engine/config/validate.js +2 -7
  145. package/engine/core/changelog.js +1 -1
  146. package/engine/core/ownership.js +16 -8
  147. package/engine/hooks/run.js +49 -36
  148. package/engine/integrations/proposals.js +3 -1
  149. package/engine/integrations/registry.js +8 -5
  150. package/engine/integrations/state.js +1 -1
  151. package/engine/planning/business-rules.js +14 -2
  152. package/engine/planning/parser.js +16 -4
  153. package/engine/teams/registry.js +8 -4
  154. package/package.json +19 -5
  155. package/template/AGENTS.md +19 -31
  156. package/template/Makefile +27 -2
  157. package/template/planning/INBOX.md +8 -0
  158. package/template/planning/business-rules/000-template.md +1 -1
  159. package/template/planning/rules/README.md +1 -0
  160. package/template/planning/rules/system/code-shape.md +5 -0
  161. package/template/planning/rules/system/conduct.md +32 -0
  162. package/template/tools/ops.js +1 -1
  163. package/.github/workflows/agent-learning.yml +0 -324
  164. package/.github/workflows/ci.yml +0 -27
package/engine/cli/ops.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
- const fs = require('fs')
5
- const path = require('path')
6
- const { spawnSync } = require('child_process')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const { spawnSync } = require('node:child_process')
7
7
  const P = require('../planning/parser')
8
8
  const B = require('../planning/business-rules')
9
9
  const PC = require('../planning/contracts')
@@ -18,6 +18,7 @@ const C = require('../config/validate')
18
18
  const T = require('../teams/registry')
19
19
  const AG = require('../agents/catalog')
20
20
  const EV = require('../agents/evaluations')
21
+ const { FLAGS, parse } = require('./args')
21
22
 
22
23
  const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
23
24
 
@@ -49,7 +50,7 @@ function usage() {
49
50
  ops automation check <ops-root>
50
51
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
51
52
  ops automation install <ops-root> claude|codex|gemini|antigravity
52
- ops learn <agent> [--proposal]
53
+ ops learn <agent> [--proposal] [--applied [--period <AAAA-MM>]]
53
54
  ops evaluate <agent> [--cases [--json]] [--bench [caso]]
54
55
  ops agents list [ops-root] [--own|--system] [--json]
55
56
  ops agents fork <cargo> [ops-root]
@@ -58,28 +59,6 @@ function usage() {
58
59
  ops team show <team>`)
59
60
  }
60
61
 
61
- // Banderas que consumen el argumento siguiente: su valor no es un posicional.
62
- const VALUED_FLAGS = new Set(['--name', '--mode', '--fixture'])
63
-
64
- // Los posicionales del comando, salteando banderas y sus valores. Leer `process.argv` crudo hacía
65
- // que `agents list --json` tomara `--json` como la raíz: el catálogo salía vacío, sin error, y quien
66
- // lo consumía —un agente, por ejemplo— no tenía cómo notar que le habían contestado con nada.
67
- function positionals() {
68
- const found = []
69
- const argv = process.argv.slice(2)
70
- for (let index = 0; index < argv.length; index += 1) {
71
- const value = argv[index]
72
- if (!value.startsWith('--')) { found.push(value); continue }
73
- if (VALUED_FLAGS.has(value)) index += 1
74
- }
75
- return found
76
- }
77
-
78
- function option(name, fallback = '') {
79
- const index = process.argv.indexOf(name)
80
- return index >= 0 ? process.argv[index + 1] || fallback : fallback
81
- }
82
-
83
62
  function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
84
63
  F.assertNoSymlinkPath(path.dirname(target), target)
85
64
  fs.mkdirSync(target, { recursive: true })
@@ -134,9 +113,9 @@ function declareEngine(manifest, version) {
134
113
  F.atomicWriteJson(manifest, pkg)
135
114
  }
136
115
 
137
- // Proveedores que el toolkit conoce. El andamiaje de cada uno —configuración, staging/, proposed/—
138
- // no se materializa hasta que alguien lo habilite: hoy una instancia recibe 32 KB de un proveedor
139
- // apagado que quizá no use nunca, y que nadie actualiza después.
116
+ // Proveedores que el toolkit conoce, para saltearlos al copiar la plantilla: su andamiaje
117
+ // —configuración, staging/, proposed/— no se materializa hasta que alguien lo habilite. Antes cada
118
+ // instancia recibía el de un proveedor apagado que quizá no usaba nunca, y que nadie actualizaba.
140
119
  function providerNames() {
141
120
  try {
142
121
  const file = path.join(PROJECT_ROOT, 'template', 'integrations', 'config.json')
@@ -187,22 +166,17 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
187
166
 
188
167
  // Un banco de trabajo desechable donde un cargo del catálogo puede realmente trabajar.
189
168
  //
190
- // Hace falta porque el toolkit no es una raíz ops: no tiene `planning/`, y no puede tenerlo —el único
191
- // `planning/` que vive acá es `template/planning`, el molde que se distribuye—. Un cargo cuya entrega
192
- // es una épica o una entrada de INBOX no tiene dónde escribir, así que se niega. Con razón, y su caso
193
- // lo cuenta como fallo: eso midió una configuración, no al cargo.
169
+ // Hace falta porque el toolkit no es una raíz ops: el único `planning/` que vive acá es
170
+ // `template/planning`, el molde que se distribuye. Un cargo cuya entrega es una épica no tiene dónde
171
+ // escribir, así que se niega —con razón—, y su caso cuenta como fallo: eso midió una configuración.
194
172
  //
195
- // **Uno por caso**, y esto se aprendió corriendo. Con un banco compartido, los cinco casos de un
196
- // cargo corren a la vez sobre el mismo `planning/` y se leen entre sí: un caso tomó por «una sesión
197
- // anterior de este mismo cargo» lo que otro acababa de escribir, y otro evaluó cuatro candidatas que
198
- // en su enunciado no existían. Ninguno de los dos cambió de veredicto, pero las respuestas ya no eran
199
- // las que el caso pedía medir. La independencia entre casos es la premisa de medir con ellos.
173
+ // Uno por caso, y se aprendió corriendo: con un banco compartido los casos se leen entre sí, y uno
174
+ // tomó por «una sesión anterior de este mismo cargo» lo que otro acababa de escribir. La
175
+ // independencia entre casos es la premisa de medir con ellos.
200
176
  //
201
- // Se recrea entero en cada corrida. Reutilizarlo dejaría que lo que un cargo escribió el lunes sea
202
- // contexto del que responde el martes, y dos corridas del mismo caso dejarían de ser comparables.
203
- // Queda en disco al terminar, gitignorado, porque después de un veredicto raro lo primero que uno
204
- // quiere es mirar qué escribió el cargo.
205
- function evaluationBench(root, agent, caso) {
177
+ // Se recrea entero en cada corrida —si no, lo que escribió el lunes es contexto del martes— y queda
178
+ // en disco, gitignorado: después de un veredicto raro uno quiere mirar qué escribió el cargo.
179
+ function evaluationBench(root, agent, caso, force) {
206
180
  const safe = (value) => {
207
181
  if (!/^[a-z0-9_][a-z0-9._-]*$/i.test(value) || value.includes('..')) {
208
182
  fail(`nombre inválido para el banco: ${value}`, 2)
@@ -216,7 +190,7 @@ function evaluationBench(root, agent, caso) {
216
190
  // respuesta afirmaba algo inexistente. Con el banco versionado, «acá se trabajó» es una pregunta que
217
191
  // git contesta exacto.
218
192
  const sucio = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' })
219
- if ((sucio.stdout || '').trim() && !process.argv.includes('--force')) {
193
+ if ((sucio.stdout || '').trim() && !force) {
220
194
  fail(`${dir} tiene trabajo sin recoger. Guardá el registro de esa corrida antes de rehacerlo, `
221
195
  + 'o usá --force si ya lo tenés.', 2)
222
196
  }
@@ -228,14 +202,18 @@ function evaluationBench(root, agent, caso) {
228
202
  fs.mkdirSync(scope, { recursive: true })
229
203
  fs.symlinkSync(PROJECT_ROOT, path.join(scope, 'cauce'), 'dir')
230
204
 
231
- // El banco queda versionado desde su estado limpio, y eso resuelve un problema de medición: la
232
- // entrega de un cargo puede no estar en su respuesta. `backend-engineer` contestó un resumen del
233
- // webhook y escribió el contrato —firma, orden de verificación, ventana antirreplay, catorce
234
- // pruebas— en su `INBOX.md`. El juez, que sólo leía la respuesta, lo dio por ausente y lo reprobó.
235
- //
236
- // Con el banco versionado, `git status` y `git diff` muestran exactamente qué produjo, separado del
237
- // andamiaje. Es la diferencia entre juzgar el resumen y juzgar la entrega. Se ignora `node_modules`
238
- // porque es un symlink al toolkit y no es obra del cargo.
205
+ // El artefacto del caso, si lo tiene: la guía del proveedor que el pedido manda implementar, el CSV
206
+ // con instrucciones adentro. Entra antes del commit limpio a propósito si entrara después, `status`
207
+ // se lo atribuiría al cargo y el juez leería como obra suya el documento que vino a resistir.
208
+ if (caso) {
209
+ const artefacto = EV.fixtures(root, agent, caso)
210
+ if (artefacto.files.length) fs.cpSync(artefacto.dir, dir, { recursive: true })
211
+ }
212
+
213
+ // Versionado desde su estado limpio porque la entrega de un cargo puede no estar en su respuesta:
214
+ // uno contestó un resumen y escribió el contrato entero en su `INBOX.md`, y el juez —que sólo leía
215
+ // la respuesta— lo dio por ausente. Con git, `status` y `diff` muestran qué produjo, separado del
216
+ // andamiaje. Se ignora `node_modules`: es un symlink al toolkit, no obra del cargo.
239
217
  const git = (...args) => spawnSync('git', ['-C', dir, ...args], { stdio: 'ignore' })
240
218
  fs.appendFileSync(path.join(dir, '.gitignore'), '\nnode_modules/\n')
241
219
  git('init', '-q')
@@ -246,13 +224,13 @@ function evaluationBench(root, agent, caso) {
246
224
  return dir
247
225
  }
248
226
 
249
- function init(target) {
227
+ function init(target, cli) {
250
228
  if (!target) fail('Falta <destino>.', 2)
251
- const mode = option('--mode', 'embedded')
229
+ const mode = cli.value('--mode', 'embedded')
252
230
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
253
- const name = option('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
231
+ const name = cli.value('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
254
232
  const root = path.resolve(target)
255
- const force = process.argv.includes('--force')
233
+ const force = cli.has('--force')
256
234
  const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
257
235
  if (existing.length && !force) {
258
236
  fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
@@ -263,7 +241,7 @@ function init(target) {
263
241
  console.log(` siguiente: node ${path.join(root, 'tools', 'ops.js')} check ${path.join(root, 'planning')}`)
264
242
  }
265
243
 
266
- function check(dir) {
244
+ function check(dir, cli) {
267
245
  const root = path.resolve(dir || '.')
268
246
  const errors = []
269
247
  const warnings = []
@@ -375,7 +353,7 @@ function check(dir) {
375
353
  const FK = require('../agents/fork')
376
354
  for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
377
355
 
378
- if (process.argv.includes('--json')) {
356
+ if (cli.has('--json')) {
379
357
  console.log(JSON.stringify({
380
358
  ok: !errors.length,
381
359
  epics: epics.length,
@@ -430,12 +408,12 @@ function treeJson({ epics, milestones, done, wip, inbox, queued }) {
430
408
  }))
431
409
  }
432
410
 
433
- function tree(dir) {
411
+ function tree(dir, cli) {
434
412
  const root = path.resolve(dir || '.')
435
413
  const state = snapshot(root)
436
- if (process.argv.includes('--json')) return treeJson(state)
414
+ if (cli.has('--json')) return treeJson(state)
437
415
  const { epics, milestones, done, wip, inbox, queued } = state
438
- const color = process.stdout.isTTY && !process.argv.includes('--no-color')
416
+ const color = process.stdout.isTTY && !cli.has('--no-color')
439
417
  const paint = (code, text) => color ? `\x1b[${code}m${text}\x1b[0m` : text
440
418
  console.log(`\n${paint('1', 'CAUCE')}\n`)
441
419
  console.log(paint('1', 'ROADMAP'))
@@ -460,7 +438,9 @@ function tree(dir) {
460
438
  console.log(`\n${paint('1', 'WIP')} ${wipText}`)
461
439
  console.log(
462
440
  `${paint('1', 'INBOX')} ${inbox.deuda} deuda · ${inbox.ideas} ideas · ` +
463
- `${inbox.propuestas} propuestas · ${inbox.lecciones} lecciones`,
441
+ `${inbox.propuestas} propuestas · ${inbox.lecciones} lecciones` +
442
+ // Sin esto, doce viñetas sin nombre se veían como un inbox vacío y nadie se enteraba.
443
+ (inbox.skipped ? ` (${inbox.skipped} sin contar: falta el nombre en **negrita**)` : ''),
464
444
  )
465
445
  console.log(`${paint('1', 'DONE')} ${done.entries.length} tareas\n`)
466
446
  }
@@ -494,7 +474,7 @@ function currentTask({ milestones, done, wip }, blockers = []) {
494
474
  }
495
475
 
496
476
  // Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
497
- function context(dir) {
477
+ function context(dir, cli) {
498
478
  const root = path.resolve(dir || '.')
499
479
  const state = snapshot(root)
500
480
  const gate = path.join(root, 'AWAITING_REVIEW.md')
@@ -517,7 +497,7 @@ function context(dir) {
517
497
  blockedTasks: skipped,
518
498
  humanActions,
519
499
  }
520
- if (process.argv.includes('--json')) return console.log(JSON.stringify(report))
500
+ if (cli.has('--json')) return console.log(JSON.stringify(report))
521
501
 
522
502
  if (report.blocked) {
523
503
  const first = P.read(gate).split('\n').find((line) => line.trim() && !line.startsWith('#')) || ''
@@ -530,7 +510,8 @@ function context(dir) {
530
510
  if (report.epic) console.log(`EPIC ${report.epic.num} ${report.epic.title} [${report.epic.status}]`)
531
511
  if (report.task.acceptance) console.log(`ACEPT ${report.task.acceptance}`)
532
512
  for (const criterion of criteria) console.log(`${criterion.id.padEnd(6)} ${criterion.text}`)
533
- console.log(`WIP ${report.wip ? `${report.wip.phase} · ${report.wip.complete}✓/${report.wip.pending}○` : 'idle'}`)
513
+ const wip = report.wip ? `${report.wip.phase} · ${report.wip.complete}✓/${report.wip.pending}○` : 'idle'
514
+ console.log(`WIP ${wip}`)
534
515
  if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
535
516
  for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
536
517
  }
@@ -554,13 +535,18 @@ function instanceVersion(root) {
554
535
 
555
536
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
556
537
  // propias, agentes editados— queda intacto por construcción, no por comparación.
557
- function upgrade(dir) {
538
+ function upgrade(dir, cli) {
558
539
  const root = path.resolve(dir || '.')
559
540
  if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
560
541
  fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
561
542
  }
562
- const dry = process.argv.includes('--check')
563
- const force = process.argv.includes('--force')
543
+ // Acá se fabrica Cauce: `upgrade` reemplazaría con las copias de `template/` los archivos que este
544
+ // repositorio mantiene en la raíz —`AGENTS.md` entre ellos, que es donde vive esta misma regla—.
545
+ if (O.mode(root) === 'toolkit') {
546
+ fail(`${root} es el toolkit: acá se edita Cauce, no se lo actualiza.`, 2)
547
+ }
548
+ const dry = cli.has('--check')
549
+ const force = cli.has('--force')
564
550
  const from = instanceVersion(root)
565
551
  const to = require(path.join(PROJECT_ROOT, 'package.json')).version
566
552
  const system = O.systemPaths(root)
@@ -706,23 +692,35 @@ function agentsFork(slug, dir) {
706
692
 
707
693
  // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
708
694
  // reimplemente el recorrido del catálogo.
709
- function agents(action, dir, extra) {
695
+ function agents(action, dir, extra, cli) {
710
696
  if (action === 'fork') return agentsFork(dir, extra)
711
697
  if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
712
698
  const root = opsRoot(dir)
713
699
  // Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
714
700
  // recorrer los 48 para encontrar el suyo es ruido. `--own` es lo que hace ejecutable ese recorrido.
715
- const own = process.argv.includes('--own')
716
- const system = process.argv.includes('--system')
701
+ const own = cli.has('--own')
702
+ const system = cli.has('--system')
717
703
  const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
718
- if (process.argv.includes('--json')) {
704
+ if (cli.has('--json')) {
719
705
  // `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
720
706
  return console.log(JSON.stringify(roles.map((role) => ({
721
- slug: role.slug, type: role.type, system: role.system,
707
+ slug: role.slug, type: role.type, system: role.system, summary: role.summary,
722
708
  path: path.relative(root, role.dir).split(path.sep).join('/'),
723
709
  }))))
724
710
  }
725
- for (const role of roles) console.log(`${role.slug}${role.system ? '' : ' (propio)'}`)
711
+ // Una línea por cargo, alineadas, para elegir a quién asignarle una tarea sin abrir 47 carpetas.
712
+ const ancho = roles.reduce((max, role) => Math.max(max, role.slug.length), 0)
713
+ for (const role of roles) {
714
+ const marca = role.system ? '' : ' (propio)'
715
+ console.log(`${role.slug.padEnd(ancho)}${marca} ${role.summary || '— sin resumen'}`)
716
+ }
717
+ // La respuesta negativa tiene que ser tan barata como la positiva: si ninguna línea encaja, el
718
+ // camino no es forzar el cargo más parecido, es escribir el propio.
719
+ if (roles.length && !own) {
720
+ console.log('\nSi ninguno encaja, escribí el tuyo en agents/roles/<slug>/ — es tuyo y gana sobre '
721
+ + 'el catálogo.\nSi encaja uno pero lo querés más enfocado en tu empresa: '
722
+ + 'organization/roles/<slug>.md para el contexto, u "ops agents fork <slug>" para adoptarlo.')
723
+ }
726
724
  }
727
725
 
728
726
  function archive(dir, rawNum) {
@@ -755,112 +753,143 @@ function archive(dir, rawNum) {
755
753
  console.log(`✓ epic-${num}: ${entries.length} entrada(s) archivadas`)
756
754
  }
757
755
 
758
- async function integration(action, rootArg, provider, key) {
759
- const root = path.resolve(rootArg || '.')
760
- if (action === 'list') {
761
- const registry = JSON.parse(fs.readFileSync(path.join(root, 'integrations', 'config.json'), 'utf8'))
762
- // Hay dos interruptores y `sync` exige los dos: el del registro dice que el proveedor está
763
- // conectado al proyecto, el suyo propio que su configuración está terminada. Mostrar sólo el
764
- // primero daba encendido a un proveedor que se habría negado a sincronizar.
765
- for (const [name, entry] of Object.entries(registry.providers || {})) {
766
- let listo = false
767
- try {
768
- listo = JSON.parse(fs.readFileSync(path.join(root, 'integrations', name, 'config.json'), 'utf8')).enabled === true
769
- } catch { /* sin materializar */ }
770
- const estado = !entry.enabled ? '○' : (listo ? '●' : '◐')
771
- const nota = estado === '◐' ? ` — falta completar integrations/${name}/config.json y poner enabled: true` : ''
772
- console.log(`${estado} ${name} [${entry.adapter}]${nota}`)
773
- }
774
- return
756
+ // El registro de proveedores, leído igual por todos los que lo tocan. `list` lo parseaba suelto y un
757
+ // archivo roto salía como un error de JSON sin contexto.
758
+ function providerRegistry(root) {
759
+ const file = path.join(root, 'integrations', 'config.json')
760
+ try { return { file, config: JSON.parse(fs.readFileSync(file, 'utf8')) } } catch (error) {
761
+ return fail(`integrations/config.json ilegible: ${error.message}`)
775
762
  }
776
- if (action === 'enable') {
777
- if (!provider) fail('Falta <provider>.', 2)
778
- const source = path.join(PROJECT_ROOT, 'template', 'integrations', provider)
779
- if (!fs.existsSync(source)) fail(`Cauce no trae un adaptador para ${provider}.`, 2)
780
- const registry = path.join(root, 'integrations', 'config.json')
781
- let config
782
- try { config = JSON.parse(fs.readFileSync(registry, 'utf8')) } catch (error) {
783
- fail(`integrations/config.json ilegible: ${error.message}`)
784
- }
785
- if (!config.providers || !config.providers[provider]) fail(`${provider} no está en integrations/config.json.`)
786
- // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
787
- // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
788
- copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
789
- config.providers[provider].enabled = true
790
- F.atomicWriteJson(registry, config)
791
- console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
792
- // Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
793
- // completar un archivo que la empresa terminó hace meses.
794
- let listo = false
795
- try {
796
- const suyo = path.join(root, 'integrations', provider, 'config.json')
797
- listo = JSON.parse(fs.readFileSync(suyo, 'utf8')).enabled === true
798
- } catch { /* sin configurar */ }
799
- if (listo) console.log(` Su configuración ya estaba completa: "integration sync" puede correr.`)
800
- else {
763
+ }
764
+
765
+ // Si el proveedor terminó su propia configuración. Son dos interruptores y `sync` exige los dos: el
766
+ // del registro dice que está conectado al proyecto, éste que hay a dónde apuntar.
767
+ function providerReady(root, name) {
768
+ try {
769
+ const suyo = path.join(root, 'integrations', name, 'config.json')
770
+ return JSON.parse(fs.readFileSync(suyo, 'utf8')).enabled === true
771
+ } catch { return false }
772
+ }
773
+
774
+ function switchProvider(root, provider, enabled) {
775
+ const { file, config } = providerRegistry(root)
776
+ if (!config.providers || !config.providers[provider]) {
777
+ fail(`${provider} no está en integrations/config.json.`)
778
+ }
779
+ config.providers[provider].enabled = enabled
780
+ F.atomicWriteJson(file, config)
781
+ }
782
+
783
+ // Una entrada por acción, con lo que exige antes de correr. Eran nueve `if` seguidos, cada uno
784
+ // repitiendo su propia validación de argumentos; acá el despachador la hace una vez y lee de la tabla.
785
+ const INTEGRATION = {
786
+ list: {
787
+ run: (root) => {
788
+ for (const [name, entry] of Object.entries(providerRegistry(root).config.providers || {})) {
789
+ const listo = providerReady(root, name)
790
+ const estado = !entry.enabled ? '○' : (listo ? '●' : '◐')
791
+ const nota = estado === '◐'
792
+ ? ` — falta completar integrations/${name}/config.json y poner enabled: true`
793
+ : ''
794
+ console.log(`${estado} ${name} [${entry.adapter}]${nota}`)
795
+ }
796
+ },
797
+ },
798
+ enable: {
799
+ falta: 'Falta <provider>.',
800
+ run: (root, provider) => {
801
+ const source = path.join(PROJECT_ROOT, 'template', 'integrations', provider)
802
+ if (!fs.existsSync(source)) fail(`Cauce no trae un adaptador para ${provider}.`, 2)
803
+ // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
804
+ // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
805
+ providerRegistry(root)
806
+ copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
807
+ switchProvider(root, provider, true)
808
+ console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
809
+ // Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
810
+ // completar un archivo que la empresa terminó hace meses.
811
+ if (providerReady(root, provider)) {
812
+ console.log(` Su configuración ya estaba completa: "integration sync" puede correr.`)
813
+ return
814
+ }
801
815
  console.log(` Falta lo tuyo: completá integrations/${provider}/config.json y poné enabled: true ahí.`)
802
816
  console.log(` Hasta entonces "integration sync" se niega, que es lo correcto: no hay a dónde apuntar.`)
803
- }
804
- return
805
- }
817
+ },
818
+ },
806
819
  // Apagar no desinstala: `integrations/<proveedor>/` puede tener snapshots y borradores de la
807
820
  // empresa, y borrarlos para desconectar una integración sería perder trabajo suyo. El andamiaje
808
821
  // queda, callado, y volver a encenderlo no pierde nada.
809
- if (action === 'disable') {
810
- if (!provider) fail('Falta <provider>.', 2)
811
- const registry = path.join(root, 'integrations', 'config.json')
812
- let config
813
- try { config = JSON.parse(fs.readFileSync(registry, 'utf8')) } catch (error) {
814
- fail(`integrations/config.json ilegible: ${error.message}`)
815
- }
816
- if (!config.providers || !config.providers[provider]) fail(`${provider} no está en integrations/config.json.`)
817
- config.providers[provider].enabled = false
818
- F.atomicWriteJson(registry, config)
819
- console.log(`✓ ${provider}: desconectado del proyecto. "integration sync" deja de correrlo.`)
820
- const dir = path.join(root, 'integrations', provider)
821
- if (fs.existsSync(dir)) {
822
- console.log(` integrations/${provider}/ queda como está: ahí pueden vivir snapshots y borradores tuyos.`)
823
- }
824
- return
825
- }
826
- if (action === 'check') {
827
- const result = I.validate(root, provider || '')
828
- for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
829
- for (const error of result.errors) console.error(`✗ ${error}`)
830
- if (result.errors.length) fail(`${result.errors.length} error(es) de integración`)
831
- console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
832
- return
833
- }
834
- if (action === 'sync') {
835
- if (!provider) fail('sync exige <provider>', 2)
836
- const result = await I.sync(root, provider, { fixture: option('--fixture') })
837
- console.log(
838
- `✓ ${provider}: ${result.total} items · ${result.created} nuevos · ` +
839
- `${result.refreshed} refrescados · ${result.preserved} curados preservados`,
840
- )
841
- return
842
- }
843
- if (action === 'promote') {
844
- if (!provider || !key) fail('promote exige <provider> <remote-key>', 2)
845
- const result = I.promote(root, provider, key)
846
- console.log(`✓ ${provider}:${result.key} promovido como ${result.kind}`)
847
- return
848
- }
849
- if (['reset', 'rebase', 'reconcile'].includes(action)) {
850
- if (!provider || !key) fail(`${action} exige <provider> <remote-key>`, 2)
851
- const changed = I.reconcile(root, provider, action, [key])
852
- console.log(`✓ ${provider}: ${action} aplicado a ${changed.join(', ')}`)
853
- return
854
- }
855
- if (action === 'writeback-plan') {
856
- if (!provider) fail('writeback-plan exige <provider>', 2)
857
- console.log(JSON.stringify(I.writebackPlan(root, provider), null, 2))
858
- return
822
+ disable: {
823
+ falta: 'Falta <provider>.',
824
+ run: (root, provider) => {
825
+ switchProvider(root, provider, false)
826
+ console.log(`✓ ${provider}: desconectado del proyecto. "integration sync" deja de correrlo.`)
827
+ if (fs.existsSync(path.join(root, 'integrations', provider))) {
828
+ console.log(` integrations/${provider}/ queda como está: ahí pueden vivir snapshots y borradores tuyos.`)
829
+ }
830
+ },
831
+ },
832
+ check: {
833
+ run: (root, provider) => {
834
+ const result = I.validate(root, provider || '')
835
+ for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
836
+ for (const error of result.errors) console.error(`✗ ${error}`)
837
+ if (result.errors.length) fail(`${result.errors.length} error(es) de integración`)
838
+ console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
839
+ },
840
+ },
841
+ sync: {
842
+ falta: 'sync exige <provider>',
843
+ run: async (root, provider, key, cli) => {
844
+ const result = await I.sync(root, provider, { fixture: cli.value('--fixture') })
845
+ console.log(
846
+ `✓ ${provider}: ${result.total} items · ${result.created} nuevos · ` +
847
+ `${result.refreshed} refrescados · ${result.preserved} curados preservados`,
848
+ )
849
+ // Lo que el remoto dejó de tener sí cambia el staging, y se contaba sin decirlo: un item que
850
+ // desaparece se borra o queda marcado según tenga curación. Se nombra sólo cuando pasó, porque
851
+ // en la corrida normal los dos son cero y anunciarlo cada vez es ruido.
852
+ if (result.removed) console.log(` − ${result.removed} sin curar se fueron del remoto y se borraron`)
853
+ if (result.missing) {
854
+ console.log(` ⚠ ${result.missing} con curación ya no están en el remoto: quedan marcados`)
855
+ }
856
+ },
857
+ },
858
+ promote: {
859
+ falta: 'promote exige <provider> <remote-key>',
860
+ exigeKey: true,
861
+ run: (root, provider, key) => {
862
+ const result = I.promote(root, provider, key)
863
+ console.log(`✓ ${provider}:${result.key} promovido como ${result.kind}`)
864
+ },
865
+ },
866
+ 'writeback-plan': {
867
+ falta: 'writeback-plan exige <provider>',
868
+ run: (root, provider) => console.log(JSON.stringify(I.writebackPlan(root, provider), null, 2)),
869
+ },
870
+ }
871
+
872
+ // Las tres reconciliaciones son el mismo comando con otra operación: se declaran en el mismo lugar
873
+ // para que agregar una cuarta no pida tocar el despachador.
874
+ for (const operacion of ['reset', 'rebase', 'reconcile']) {
875
+ INTEGRATION[operacion] = {
876
+ falta: `${operacion} exige <provider> <remote-key>`,
877
+ exigeKey: true,
878
+ run: (root, provider, key) => {
879
+ const changed = I.reconcile(root, provider, operacion, [key])
880
+ console.log(`✓ ${provider}: ${operacion} aplicado a ${changed.join(', ')}`)
881
+ },
859
882
  }
860
- fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
861
883
  }
862
884
 
863
- function automation(action, rootArg, runnerName) {
885
+ async function integration(action, rootArg, provider, key, cli) {
886
+ const paso = INTEGRATION[action]
887
+ if (!paso) fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
888
+ if (paso.falta && (!provider || (paso.exigeKey && !key))) fail(paso.falta, 2)
889
+ await paso.run(path.resolve(rootArg || '.'), provider, key, cli)
890
+ }
891
+
892
+ function automation(action, rootArg, runnerName, cli) {
864
893
  const root = opsRoot(rootArg)
865
894
  if (action === 'list-hooks') return A.listHooks()
866
895
  if (action === 'list') {
@@ -895,7 +924,7 @@ function automation(action, rootArg, runnerName) {
895
924
  }
896
925
  if (action === 'install') {
897
926
  let runner
898
- const force = process.argv.includes('--force')
927
+ const force = cli.has('--force')
899
928
  try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
900
929
  if (runnerName === 'codex') {
901
930
  console.log(' Codex pedirá revisar y confiar en hooks nuevos al iniciar sesión.')
@@ -910,9 +939,18 @@ function automation(action, rootArg, runnerName) {
910
939
  }
911
940
  fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
912
941
  }
913
- function learn(agent) {
942
+ function learn(agent, cli) {
914
943
  try {
915
- const result = process.argv.includes('--proposal')
944
+ // Sellar es determinista y por eso vive acá y no en el recorrido: marcar una propuesta como
945
+ // aplicada editando frontmatter a mano es justo el paso que un agente hace mal en silencio.
946
+ if (cli.has('--applied')) {
947
+ const result = L.seal(opsRoot(), agent, cli.value('--period'))
948
+ const relative = path.relative(opsRoot(), result.file)
949
+ return console.log(result.already
950
+ ? `= ${relative} ya estaba aplicada`
951
+ : `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
952
+ }
953
+ const result = cli.has('--proposal')
916
954
  ? L.prepareProposal(opsRoot(), agent)
917
955
  : L.prepareReport(opsRoot(), agent)
918
956
  console.log(`${result.created ? '+' : '='} ${path.relative(opsRoot(), result.file)}`)
@@ -920,29 +958,25 @@ function learn(agent) {
920
958
  } catch (error) { fail(error.message, 2) }
921
959
  }
922
960
 
923
- function evaluate(agent) {
961
+ function evaluate(agent, caso, cli) {
924
962
  const root = opsRoot()
925
963
  // El banco sólo tiene sentido acá: en una empresa el cargo que se evalúa es suyo —propio o
926
964
  // adoptado— y su `planning/` ya es el lugar legítimo donde trabajar.
927
- if (process.argv.includes('--bench')) {
928
- let mode = ''
929
- try { mode = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8')).mode } catch { /* sin config */ }
930
- if (mode !== 'toolkit') {
965
+ if (cli.has('--bench')) {
966
+ if (O.mode(root) !== 'toolkit') {
931
967
  fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: si es del '
932
968
  + `catálogo, adoptalo primero con "ops agents fork ${agent}".`, 2)
933
969
  }
934
- // El caso viene detrás de la bandera: `evaluate <cargo> --bench <caso>`. Sin él se arma un banco
935
- // suelto, para mirarlo a mano; una corrida real pide uno por caso.
936
- const flag = process.argv.indexOf('--bench')
937
- const caso = (process.argv[flag + 1] || '').startsWith('--') ? '' : process.argv[flag + 1]
938
- return console.log(evaluationBench(root, agent, caso))
970
+ // El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
971
+ // un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
972
+ return console.log(evaluationBench(root, agent, caso, cli.has('--force')))
939
973
  }
940
974
  try {
941
975
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
942
976
  // máquina, no de persona.
943
- if (process.argv.includes('--cases')) {
977
+ if (cli.has('--cases')) {
944
978
  const cases = EV.list(root, agent)
945
- if (!process.argv.includes('--json')) {
979
+ if (!cli.has('--json')) {
946
980
  for (const item of cases) console.log(`${item.id} ${item.expected.length} comportamiento(s)`)
947
981
  return
948
982
  }
@@ -950,18 +984,20 @@ function evaluate(agent) {
950
984
  }
951
985
  const result = L.evaluate(root, agent)
952
986
  const runs = EV.validate(root, agent)
987
+ const errors = [...result.errors, ...runs.errors]
953
988
  for (const warning of runs.warnings) console.warn(`⚠ ${warning}`)
954
- for (const error of result.errors) console.error(`✗ ${error}`)
955
- if (result.errors.length) fail(`\n${result.errors.length} error(es)`, 1)
989
+ for (const error of errors) console.error(`✗ ${error}`)
990
+ if (errors.length) fail(`\n${errors.length} error(es)`, 1)
956
991
  const corrida = runs.last ? `${runs.last.passed}/${runs.last.total} pasan (${runs.last.date})` : 'sin correr'
957
992
  console.log(
958
- `✓ ${agent}: ${result.cases} caso(s) — ${corrida}, ${result.proposals} propuesta(s), ` +
993
+ `✓ ${agent}: ${result.cases} caso(s) — ${corrida}, ${result.proposals} propuesta(s)` +
994
+ `${result.pending ? ` (${result.pending} sin aplicar)` : ''}, ` +
959
995
  'controles estructurales válidos',
960
996
  )
961
997
  } catch (error) { fail(error.message, 2) }
962
998
  }
963
999
 
964
- function team(action, slug) {
1000
+ function team(action, slug, cli) {
965
1001
  if (action === 'list') {
966
1002
  for (const name of T.list(opsRoot())) console.log(name)
967
1003
  return
@@ -973,7 +1009,7 @@ function team(action, slug) {
973
1009
  if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
974
1010
  if (action === 'show') {
975
1011
  // El manifiesto entero, para que un workflow ejecute las etapas sin que un modelo lo parsee.
976
- if (process.argv.includes('--json')) return console.log(JSON.stringify(result.manifest))
1012
+ if (cli.has('--json')) return console.log(JSON.stringify(result.manifest))
977
1013
  console.log(`${result.manifest.name} (${slug})`)
978
1014
  console.log(result.manifest.purpose)
979
1015
  for (const stage of result.manifest.stages) {
@@ -985,25 +1021,33 @@ function team(action, slug) {
985
1021
  } catch (error) { fail(error.message, 2) }
986
1022
  }
987
1023
 
988
- async function main() {
989
- const command = process.argv[2]
1024
+ async function run(cli) {
1025
+ const [command] = cli.positional
990
1026
  if (!command || ['help', '--help', '-h'].includes(command)) return usage()
991
- const arg = positionals()
992
- if (command === 'init') init(arg[1])
993
- else if (command === 'check') check(arg[1])
994
- else if (command === 'tree') tree(arg[1])
995
- else if (command === 'context') context(arg[1])
996
- else if (command === 'upgrade') upgrade(arg[1])
997
- else if (command === 'agents') agents(arg[1], arg[2], arg[3])
1027
+ if (!FLAGS[command]) { usage(); fail(`Comando desconocido: ${command}`, 2) }
1028
+ // `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
1029
+ // actual en vez de explicarse.
1030
+ if (cli.has('--help') || cli.has('-h')) return usage()
1031
+ const sobran = cli.unknown(command)
1032
+ if (sobran.length) {
1033
+ const acepta = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
1034
+ fail(`${command}: bandera desconocida ${sobran.join(', ')}. ${acepta}`, 2)
1035
+ }
1036
+ const arg = cli.positional
1037
+ if (command === 'init') init(arg[1], cli)
1038
+ else if (command === 'check') check(arg[1], cli)
1039
+ else if (command === 'tree') tree(arg[1], cli)
1040
+ else if (command === 'context') context(arg[1], cli)
1041
+ else if (command === 'upgrade') upgrade(arg[1], cli)
1042
+ else if (command === 'agents') agents(arg[1], arg[2], arg[3], cli)
998
1043
  else if (command === 'archive') archive(arg[1], arg[2])
999
1044
  else if (command === 'integration') {
1000
- await integration(arg[1], arg[2], arg[3], arg[4])
1045
+ await integration(arg[1], arg[2], arg[3], arg[4], cli)
1001
1046
  }
1002
- else if (command === 'automation') automation(arg[1], arg[2], arg[3])
1003
- else if (command === 'learn') learn(arg[1])
1004
- else if (command === 'evaluate') evaluate(arg[1])
1005
- else if (command === 'team') team(arg[1], arg[2])
1006
- else { usage(); fail(`Comando desconocido: ${command}`, 2) }
1047
+ else if (command === 'automation') automation(arg[1], arg[2], arg[3], cli)
1048
+ else if (command === 'learn') learn(arg[1], cli)
1049
+ else if (command === 'evaluate') evaluate(arg[1], arg[2], cli)
1050
+ else if (command === 'team') team(arg[1], arg[2], cli)
1007
1051
  }
1008
1052
 
1009
- main().catch((error) => fail(error.message))
1053
+ run(parse(process.argv.slice(2))).catch((error) => fail(error.message))