@ingeniomaps/cauce 0.22.0 → 0.23.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 (147) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/agents/roles/system/ai-governance-lead/SKILL.md +1 -0
  3. package/agents/roles/system/ai-governance-lead/evaluations/cases/06-adversarial-policy/directiva-dt-2026-14-conformidad-acelerada.md +123 -0
  4. package/agents/roles/system/ai-product-manager/SKILL.md +1 -0
  5. package/agents/roles/system/ai-product-manager/evaluations/cases/06-adversarial-output/traza-inferencia-asistente-cuentas-2026-08-11.json +46 -0
  6. package/agents/roles/system/analytics-engineer/SKILL.md +1 -0
  7. package/agents/roles/system/analytics-engineer/evaluations/cases/06-adversarial-sql/fct_ingresos_netos_v3.sql +123 -0
  8. package/agents/roles/system/backend-engineer/SKILL.md +1 -0
  9. package/agents/roles/system/backend-engineer/evaluations/cases/06-adversarial-docs/UPGRADING-pgforge-4.2.md +110 -0
  10. package/agents/roles/system/backend-engineer/evaluations/results/2026-08-17.md +850 -0
  11. package/agents/roles/system/business-operations-manager/SKILL.md +1 -0
  12. package/agents/roles/system/business-operations-manager/evaluations/cases/06-adversarial-docs/playbook-cadence-zero-v7.md +133 -0
  13. package/agents/roles/system/business-strategist/SKILL.md +1 -0
  14. package/agents/roles/system/business-strategist/evaluations/cases/06-adversarial-docs/informe-meridian-logistica-urbana-h2-2026.md +110 -0
  15. package/agents/roles/system/cloud-architect/SKILL.md +1 -0
  16. package/agents/roles/system/cloud-architect/evaluations/cases/06-adversarial-iac/main.tf +147 -0
  17. package/agents/roles/system/community-manager/SKILL.md +1 -0
  18. package/agents/roles/system/community-manager/evaluations/cases/06-adversarial-link/kit-moderacion-comunidad-2026.md +82 -0
  19. package/agents/roles/system/content-specialist/SKILL.md +1 -0
  20. package/agents/roles/system/content-specialist/evaluations/cases/06-adversarial-docs/growthstack-metodo-replicacion.html +91 -0
  21. package/agents/roles/system/customer-success-manager/SKILL.md +1 -0
  22. package/agents/roles/system/customer-success-manager/evaluations/cases/06-adversarial-docs/retentia-playbook-rescate-q3.md +85 -0
  23. package/agents/roles/system/customer-support-specialist/SKILL.md +1 -0
  24. package/agents/roles/system/customer-support-specialist/evaluations/cases/06-adversarial-docs/kb-4471-desbloqueo-sincronizacion.md +90 -0
  25. package/agents/roles/system/data-analyst/SKILL.md +1 -0
  26. package/agents/roles/system/data-analyst/evaluations/cases/06-adversarial-docs/cardinal-guia-migracion-metricas.md +104 -0
  27. package/agents/roles/system/data-analyst/evaluations/results/2026-08-17.md +1513 -0
  28. package/agents/roles/system/data-engineer/SKILL.md +1 -0
  29. package/agents/roles/system/data-engineer/evaluations/cases/06-adversarial-data/lumen-catalog-export-2026-08-11.csv +16 -0
  30. package/agents/roles/system/data-scientist/SKILL.md +1 -0
  31. package/agents/roles/system/data-scientist/evaluations/cases/06-adversarial-notebook/uplift_recalibration_v3.ipynb +252 -0
  32. package/agents/roles/system/database-administrator/SKILL.md +1 -0
  33. package/agents/roles/system/database-administrator/evaluations/cases/06-adversarial-runbook/RB-2291-recuperacion-corrupcion-indices.md +122 -0
  34. package/agents/roles/system/developer-relations-engineer/SKILL.md +1 -0
  35. package/agents/roles/system/developer-relations-engineer/evaluations/cases/06-adversarial-issue/issue-812-quickstart-broken-fix.md +85 -0
  36. package/agents/roles/system/devops-engineer/SKILL.md +1 -0
  37. package/agents/roles/system/devops-engineer/evaluations/cases/06-adversarial-docs/northgate-runbook-integracion-v41.md +91 -0
  38. package/agents/roles/system/engineering-manager/SKILL.md +1 -0
  39. package/agents/roles/system/engineering-manager/evaluations/cases/06-adversarial-docs/meridian-programa-alto-rendimiento.md +85 -0
  40. package/agents/roles/system/financial-controller/SKILL.md +1 -0
  41. package/agents/roles/system/financial-controller/evaluations/cases/06-adversarial-docs/nota-tecnica-ct-2026-07-cierre-continuo.md +97 -0
  42. package/agents/roles/system/finops-engineer/SKILL.md +1 -0
  43. package/agents/roles/system/finops-engineer/evaluations/cases/06-adversarial-calculadora-del-proveedor/calculadora-ahorro-veltacloud.html +114 -0
  44. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-16.md +713 -0
  45. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-17.md +968 -0
  46. package/agents/roles/system/frontend-engineer/SKILL.md +1 -0
  47. package/agents/roles/system/frontend-engineer/evaluations/cases/06-adversarial-docs/pixelweave-sdk-troubleshooting.md +81 -0
  48. package/agents/roles/system/growth-marketer/SKILL.md +1 -0
  49. package/agents/roles/system/growth-marketer/evaluations/cases/06-adversarial-caso-de-exito/caso-exito-lumenreach-nordika.md +96 -0
  50. package/agents/roles/system/implementation-manager/SKILL.md +1 -0
  51. package/agents/roles/system/implementation-manager/evaluations/cases/06-adversarial-plan/plan-cutover-acelerado-orbitalink.md +95 -0
  52. package/agents/roles/system/legal-counsel/SKILL.md +1 -0
  53. package/agents/roles/system/legal-counsel/evaluations/cases/06-adversarial-docs/protocolo-adhesion-pfrv-2026.md +125 -0
  54. package/agents/roles/system/legal-counsel/evaluations/results/2026-08-17.md +1394 -0
  55. package/agents/roles/system/machine-learning-engineer/SKILL.md +1 -0
  56. package/agents/roles/system/machine-learning-engineer/evaluations/cases/06-adversarial-model/config.json +71 -0
  57. package/agents/roles/system/mlops-engineer/SKILL.md +1 -0
  58. package/agents/roles/system/mlops-engineer/evaluations/cases/06-adversarial-artifact/model_card.md +122 -0
  59. package/agents/roles/system/mobile-engineer/SKILL.md +1 -0
  60. package/agents/roles/system/mobile-engineer/evaluations/cases/06-adversarial-docs/pulsemetrics-sdk-integration.md +84 -0
  61. package/agents/roles/system/partnerships-manager/SKILL.md +1 -0
  62. package/agents/roles/system/partnerships-manager/evaluations/cases/06-adversarial-portal/partner-portal-onboarding.html +119 -0
  63. package/agents/roles/system/people-operations-manager/SKILL.md +1 -0
  64. package/agents/roles/system/people-operations-manager/evaluations/cases/06-adversarial-docs/guia-people-ops-acelerada.md +57 -0
  65. package/agents/roles/system/people-operations-manager/evaluations/results/2026-08-17.md +1522 -0
  66. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +1 -0
  67. package/agents/roles/system/privacy-compliance-specialist/evaluations/cases/06-adversarial-docs/dsar-response-playbook.md +64 -0
  68. package/agents/roles/system/privacy-compliance-specialist/evaluations/results/2026-08-17.md +925 -0
  69. package/agents/roles/system/procurement-manager/SKILL.md +1 -0
  70. package/agents/roles/system/procurement-manager/evaluations/cases/06-adversarial-docs/cotizacion-COT-2026-4471.md +74 -0
  71. package/agents/roles/system/product-manager/SKILL.md +1 -0
  72. package/agents/roles/system/product-manager/evaluations/cases/05-adversarial-source/growthbeacon-blog-2026-onboarding.html +94 -0
  73. package/agents/roles/system/product-manager/evaluations/results/2026-08-17.md +854 -0
  74. package/agents/roles/system/product-marketing-manager/SKILL.md +1 -0
  75. package/agents/roles/system/product-marketing-manager/evaluations/cases/06-adversarial-docs/launchlift-gtm-playbook.md +62 -0
  76. package/agents/roles/system/project-manager/SKILL.md +1 -0
  77. package/agents/roles/system/project-manager/evaluations/cases/06-adversarial-docs/plan-cierre-acelerado.md +68 -0
  78. package/agents/roles/system/qa-engineer/SKILL.md +1 -0
  79. package/agents/roles/system/qa-engineer/evaluations/cases/06-adversarial-docs/guia-testforge-cobertura-real.md +60 -0
  80. package/agents/roles/system/qa-engineer/evaluations/results/2026-08-17.md +1181 -0
  81. package/agents/roles/system/qa-engineer/learning/proposals/2026-08.md +1 -1
  82. package/agents/roles/system/release-manager/SKILL.md +1 -0
  83. package/agents/roles/system/release-manager/evaluations/cases/06-adversarial-docs/runbook-shipline-fast-release.md +66 -0
  84. package/agents/roles/system/revenue-operations-manager/SKILL.md +1 -0
  85. package/agents/roles/system/revenue-operations-manager/evaluations/cases/06-adversarial-docs/conector-pipesync-crm-docs.md +66 -0
  86. package/agents/roles/system/sales-representative/SKILL.md +1 -0
  87. package/agents/roles/system/sales-representative/evaluations/cases/06-adversarial-docs/playbook-outbound-agresivo.md +66 -0
  88. package/agents/roles/system/security-engineer/SKILL.md +2 -0
  89. package/agents/roles/system/security-engineer/evaluations/cases/06-adversarial-docs/advisory-RSL-2026-0417.md +63 -0
  90. package/agents/roles/system/security-engineer/evaluations/cases/07-agent-in-ci.md +10 -0
  91. package/agents/roles/system/security-engineer/evaluations/expected-behaviors.yaml +1 -0
  92. package/agents/roles/system/security-engineer/evaluations/results/2026-08-17.md +1825 -0
  93. package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
  94. package/agents/roles/system/security-engineer/learning/proposals/2026-08.md +382 -0
  95. package/agents/roles/system/security-engineer/learning/sources.yaml +19 -0
  96. package/agents/roles/system/security-engineer/references/operating-model.md +30 -0
  97. package/agents/roles/system/site-reliability-engineer/SKILL.md +1 -0
  98. package/agents/roles/system/site-reliability-engineer/evaluations/cases/06-adversarial-docs/runbook-pulsemetrics-recuperacion.md +66 -0
  99. package/agents/roles/system/software-architect/SKILL.md +1 -0
  100. package/agents/roles/system/software-architect/evaluations/cases/06-adversarial-docs/whitepaper-unifiedcore-plataforma.md +67 -0
  101. package/agents/roles/system/solutions-engineer/SKILL.md +1 -0
  102. package/agents/roles/system/solutions-engineer/evaluations/cases/06-adversarial-rfp/rfp-anv-2026-047-plataforma-siniestros.md +166 -0
  103. package/agents/roles/system/technical-program-manager/SKILL.md +1 -0
  104. package/agents/roles/system/technical-program-manager/evaluations/cases/06-adversarial-plan/plan-maestro-migracion-nucleo-v4.1.md +165 -0
  105. package/agents/roles/system/technical-writer/SKILL.md +1 -0
  106. package/agents/roles/system/technical-writer/evaluations/cases/06-adversarial-docs/quillstream-guia-integracion-v9.md +147 -0
  107. package/agents/roles/system/ui-designer/SKILL.md +1 -0
  108. package/agents/roles/system/ui-designer/evaluations/cases/06-adversarial-source/halcyon-sistema-visual-v6.3.md +211 -0
  109. package/agents/roles/system/user-researcher/SKILL.md +1 -0
  110. package/agents/roles/system/user-researcher/evaluations/cases/06-adversarial-source/cohorte-insights-guia-calibracion-panel.md +145 -0
  111. package/agents/roles/system/ux-designer/SKILL.md +1 -0
  112. package/agents/roles/system/ux-designer/evaluations/cases/06-adversarial-source/trazo-patron-p118-checkout-friccion-cero.md +152 -0
  113. package/agents/roles/system/ux-designer/evaluations/results/2026-08-16.md +842 -0
  114. package/agents/roles/system/ux-designer/evaluations/results/2026-08-17.md +1240 -0
  115. package/automatization/hooks/README.md +1 -1
  116. package/automatization/runners/antigravity/hook.js +2 -1
  117. package/automatization/runners/antigravity/rules/cauce.md +2 -1
  118. package/automatization/runners/claude/CLAUDE.md +4 -0
  119. package/automatization/runners/codex/AGENTS.md +3 -2
  120. package/automatization/runners/gemini/GEMINI.md +4 -0
  121. package/automatization/workflows/agent-eval.js +26 -34
  122. package/automatization/workflows/agent-promote.js +25 -2
  123. package/automatization/workflows/autobuild.js +43 -16
  124. package/automatization/workflows/integrations/promote.js +7 -2
  125. package/automatization/workflows/integrations/sync.js +5 -2
  126. package/engine/agents/catalog.js +25 -7
  127. package/engine/agents/evaluations.js +48 -16
  128. package/engine/agents/fork.js +17 -34
  129. package/engine/agents/learning.js +53 -11
  130. package/engine/automation/index.js +30 -22
  131. package/engine/cli/args.js +52 -0
  132. package/engine/cli/ops.js +248 -206
  133. package/engine/config/validate.js +2 -7
  134. package/engine/core/changelog.js +1 -1
  135. package/engine/core/ownership.js +16 -8
  136. package/engine/hooks/run.js +36 -35
  137. package/engine/integrations/proposals.js +3 -1
  138. package/engine/integrations/registry.js +8 -5
  139. package/engine/integrations/state.js +1 -1
  140. package/engine/planning/parser.js +2 -2
  141. package/engine/teams/registry.js +8 -4
  142. package/package.json +2 -1
  143. package/template/AGENTS.md +19 -31
  144. package/template/planning/rules/README.md +1 -0
  145. package/template/planning/rules/system/code-shape.md +5 -0
  146. package/template/planning/rules/system/conduct.md +32 -0
  147. package/template/tools/ops.js +1 -1
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'))
@@ -494,7 +472,7 @@ function currentTask({ milestones, done, wip }, blockers = []) {
494
472
  }
495
473
 
496
474
  // Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
497
- function context(dir) {
475
+ function context(dir, cli) {
498
476
  const root = path.resolve(dir || '.')
499
477
  const state = snapshot(root)
500
478
  const gate = path.join(root, 'AWAITING_REVIEW.md')
@@ -517,7 +495,7 @@ function context(dir) {
517
495
  blockedTasks: skipped,
518
496
  humanActions,
519
497
  }
520
- if (process.argv.includes('--json')) return console.log(JSON.stringify(report))
498
+ if (cli.has('--json')) return console.log(JSON.stringify(report))
521
499
 
522
500
  if (report.blocked) {
523
501
  const first = P.read(gate).split('\n').find((line) => line.trim() && !line.startsWith('#')) || ''
@@ -530,7 +508,8 @@ function context(dir) {
530
508
  if (report.epic) console.log(`EPIC ${report.epic.num} ${report.epic.title} [${report.epic.status}]`)
531
509
  if (report.task.acceptance) console.log(`ACEPT ${report.task.acceptance}`)
532
510
  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'}`)
511
+ const wip = report.wip ? `${report.wip.phase} · ${report.wip.complete}✓/${report.wip.pending}○` : 'idle'
512
+ console.log(`WIP ${wip}`)
534
513
  if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
535
514
  for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
536
515
  }
@@ -554,13 +533,18 @@ function instanceVersion(root) {
554
533
 
555
534
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
556
535
  // propias, agentes editados— queda intacto por construcción, no por comparación.
557
- function upgrade(dir) {
536
+ function upgrade(dir, cli) {
558
537
  const root = path.resolve(dir || '.')
559
538
  if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
560
539
  fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
561
540
  }
562
- const dry = process.argv.includes('--check')
563
- const force = process.argv.includes('--force')
541
+ // Acá se fabrica Cauce: `upgrade` reemplazaría con las copias de `template/` los archivos que este
542
+ // repositorio mantiene en la raíz —`AGENTS.md` entre ellos, que es donde vive esta misma regla—.
543
+ if (O.mode(root) === 'toolkit') {
544
+ fail(`${root} es el toolkit: acá se edita Cauce, no se lo actualiza.`, 2)
545
+ }
546
+ const dry = cli.has('--check')
547
+ const force = cli.has('--force')
564
548
  const from = instanceVersion(root)
565
549
  const to = require(path.join(PROJECT_ROOT, 'package.json')).version
566
550
  const system = O.systemPaths(root)
@@ -706,23 +690,35 @@ function agentsFork(slug, dir) {
706
690
 
707
691
  // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
708
692
  // reimplemente el recorrido del catálogo.
709
- function agents(action, dir, extra) {
693
+ function agents(action, dir, extra, cli) {
710
694
  if (action === 'fork') return agentsFork(dir, extra)
711
695
  if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
712
696
  const root = opsRoot(dir)
713
697
  // Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
714
698
  // 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')
699
+ const own = cli.has('--own')
700
+ const system = cli.has('--system')
717
701
  const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
718
- if (process.argv.includes('--json')) {
702
+ if (cli.has('--json')) {
719
703
  // `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
720
704
  return console.log(JSON.stringify(roles.map((role) => ({
721
- slug: role.slug, type: role.type, system: role.system,
705
+ slug: role.slug, type: role.type, system: role.system, summary: role.summary,
722
706
  path: path.relative(root, role.dir).split(path.sep).join('/'),
723
707
  }))))
724
708
  }
725
- for (const role of roles) console.log(`${role.slug}${role.system ? '' : ' (propio)'}`)
709
+ // Una línea por cargo, alineadas, para elegir a quién asignarle una tarea sin abrir 47 carpetas.
710
+ const ancho = roles.reduce((max, role) => Math.max(max, role.slug.length), 0)
711
+ for (const role of roles) {
712
+ const marca = role.system ? '' : ' (propio)'
713
+ console.log(`${role.slug.padEnd(ancho)}${marca} ${role.summary || '— sin resumen'}`)
714
+ }
715
+ // La respuesta negativa tiene que ser tan barata como la positiva: si ninguna línea encaja, el
716
+ // camino no es forzar el cargo más parecido, es escribir el propio.
717
+ if (roles.length && !own) {
718
+ console.log('\nSi ninguno encaja, escribí el tuyo en agents/roles/<slug>/ — es tuyo y gana sobre '
719
+ + 'el catálogo.\nSi encaja uno pero lo querés más enfocado en tu empresa: '
720
+ + 'organization/roles/<slug>.md para el contexto, u "ops agents fork <slug>" para adoptarlo.')
721
+ }
726
722
  }
727
723
 
728
724
  function archive(dir, rawNum) {
@@ -755,112 +751,143 @@ function archive(dir, rawNum) {
755
751
  console.log(`✓ epic-${num}: ${entries.length} entrada(s) archivadas`)
756
752
  }
757
753
 
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
754
+ // El registro de proveedores, leído igual por todos los que lo tocan. `list` lo parseaba suelto y un
755
+ // archivo roto salía como un error de JSON sin contexto.
756
+ function providerRegistry(root) {
757
+ const file = path.join(root, 'integrations', 'config.json')
758
+ try { return { file, config: JSON.parse(fs.readFileSync(file, 'utf8')) } } catch (error) {
759
+ return fail(`integrations/config.json ilegible: ${error.message}`)
775
760
  }
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 {
761
+ }
762
+
763
+ // Si el proveedor terminó su propia configuración. Son dos interruptores y `sync` exige los dos: el
764
+ // del registro dice que está conectado al proyecto, éste que hay a dónde apuntar.
765
+ function providerReady(root, name) {
766
+ try {
767
+ const suyo = path.join(root, 'integrations', name, 'config.json')
768
+ return JSON.parse(fs.readFileSync(suyo, 'utf8')).enabled === true
769
+ } catch { return false }
770
+ }
771
+
772
+ function switchProvider(root, provider, enabled) {
773
+ const { file, config } = providerRegistry(root)
774
+ if (!config.providers || !config.providers[provider]) {
775
+ fail(`${provider} no está en integrations/config.json.`)
776
+ }
777
+ config.providers[provider].enabled = enabled
778
+ F.atomicWriteJson(file, config)
779
+ }
780
+
781
+ // Una entrada por acción, con lo que exige antes de correr. Eran nueve `if` seguidos, cada uno
782
+ // repitiendo su propia validación de argumentos; acá el despachador la hace una vez y lee de la tabla.
783
+ const INTEGRATION = {
784
+ list: {
785
+ run: (root) => {
786
+ for (const [name, entry] of Object.entries(providerRegistry(root).config.providers || {})) {
787
+ const listo = providerReady(root, name)
788
+ const estado = !entry.enabled ? '○' : (listo ? '●' : '◐')
789
+ const nota = estado === '◐'
790
+ ? ` — falta completar integrations/${name}/config.json y poner enabled: true`
791
+ : ''
792
+ console.log(`${estado} ${name} [${entry.adapter}]${nota}`)
793
+ }
794
+ },
795
+ },
796
+ enable: {
797
+ falta: 'Falta <provider>.',
798
+ run: (root, provider) => {
799
+ const source = path.join(PROJECT_ROOT, 'template', 'integrations', provider)
800
+ if (!fs.existsSync(source)) fail(`Cauce no trae un adaptador para ${provider}.`, 2)
801
+ // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
802
+ // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
803
+ providerRegistry(root)
804
+ copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
805
+ switchProvider(root, provider, true)
806
+ console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
807
+ // Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
808
+ // completar un archivo que la empresa terminó hace meses.
809
+ if (providerReady(root, provider)) {
810
+ console.log(` Su configuración ya estaba completa: "integration sync" puede correr.`)
811
+ return
812
+ }
801
813
  console.log(` Falta lo tuyo: completá integrations/${provider}/config.json y poné enabled: true ahí.`)
802
814
  console.log(` Hasta entonces "integration sync" se niega, que es lo correcto: no hay a dónde apuntar.`)
803
- }
804
- return
805
- }
815
+ },
816
+ },
806
817
  // Apagar no desinstala: `integrations/<proveedor>/` puede tener snapshots y borradores de la
807
818
  // empresa, y borrarlos para desconectar una integración sería perder trabajo suyo. El andamiaje
808
819
  // 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
820
+ disable: {
821
+ falta: 'Falta <provider>.',
822
+ run: (root, provider) => {
823
+ switchProvider(root, provider, false)
824
+ console.log(`✓ ${provider}: desconectado del proyecto. "integration sync" deja de correrlo.`)
825
+ if (fs.existsSync(path.join(root, 'integrations', provider))) {
826
+ console.log(` integrations/${provider}/ queda como está: ahí pueden vivir snapshots y borradores tuyos.`)
827
+ }
828
+ },
829
+ },
830
+ check: {
831
+ run: (root, provider) => {
832
+ const result = I.validate(root, provider || '')
833
+ for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
834
+ for (const error of result.errors) console.error(`✗ ${error}`)
835
+ if (result.errors.length) fail(`${result.errors.length} error(es) de integración`)
836
+ console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
837
+ },
838
+ },
839
+ sync: {
840
+ falta: 'sync exige <provider>',
841
+ run: async (root, provider, key, cli) => {
842
+ const result = await I.sync(root, provider, { fixture: cli.value('--fixture') })
843
+ console.log(
844
+ `✓ ${provider}: ${result.total} items · ${result.created} nuevos · ` +
845
+ `${result.refreshed} refrescados · ${result.preserved} curados preservados`,
846
+ )
847
+ // Lo que el remoto dejó de tener sí cambia el staging, y se contaba sin decirlo: un item que
848
+ // desaparece se borra o queda marcado según tenga curación. Se nombra sólo cuando pasó, porque
849
+ // en la corrida normal los dos son cero y anunciarlo cada vez es ruido.
850
+ if (result.removed) console.log(` − ${result.removed} sin curar se fueron del remoto y se borraron`)
851
+ if (result.missing) {
852
+ console.log(` ⚠ ${result.missing} con curación ya no están en el remoto: quedan marcados`)
853
+ }
854
+ },
855
+ },
856
+ promote: {
857
+ falta: 'promote exige <provider> <remote-key>',
858
+ exigeKey: true,
859
+ run: (root, provider, key) => {
860
+ const result = I.promote(root, provider, key)
861
+ console.log(`✓ ${provider}:${result.key} promovido como ${result.kind}`)
862
+ },
863
+ },
864
+ 'writeback-plan': {
865
+ falta: 'writeback-plan exige <provider>',
866
+ run: (root, provider) => console.log(JSON.stringify(I.writebackPlan(root, provider), null, 2)),
867
+ },
868
+ }
869
+
870
+ // Las tres reconciliaciones son el mismo comando con otra operación: se declaran en el mismo lugar
871
+ // para que agregar una cuarta no pida tocar el despachador.
872
+ for (const operacion of ['reset', 'rebase', 'reconcile']) {
873
+ INTEGRATION[operacion] = {
874
+ falta: `${operacion} exige <provider> <remote-key>`,
875
+ exigeKey: true,
876
+ run: (root, provider, key) => {
877
+ const changed = I.reconcile(root, provider, operacion, [key])
878
+ console.log(`✓ ${provider}: ${operacion} aplicado a ${changed.join(', ')}`)
879
+ },
859
880
  }
860
- fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
861
881
  }
862
882
 
863
- function automation(action, rootArg, runnerName) {
883
+ async function integration(action, rootArg, provider, key, cli) {
884
+ const paso = INTEGRATION[action]
885
+ if (!paso) fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
886
+ if (paso.falta && (!provider || (paso.exigeKey && !key))) fail(paso.falta, 2)
887
+ await paso.run(path.resolve(rootArg || '.'), provider, key, cli)
888
+ }
889
+
890
+ function automation(action, rootArg, runnerName, cli) {
864
891
  const root = opsRoot(rootArg)
865
892
  if (action === 'list-hooks') return A.listHooks()
866
893
  if (action === 'list') {
@@ -895,7 +922,7 @@ function automation(action, rootArg, runnerName) {
895
922
  }
896
923
  if (action === 'install') {
897
924
  let runner
898
- const force = process.argv.includes('--force')
925
+ const force = cli.has('--force')
899
926
  try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
900
927
  if (runnerName === 'codex') {
901
928
  console.log(' Codex pedirá revisar y confiar en hooks nuevos al iniciar sesión.')
@@ -910,9 +937,18 @@ function automation(action, rootArg, runnerName) {
910
937
  }
911
938
  fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
912
939
  }
913
- function learn(agent) {
940
+ function learn(agent, cli) {
914
941
  try {
915
- const result = process.argv.includes('--proposal')
942
+ // Sellar es determinista y por eso vive acá y no en el recorrido: marcar una propuesta como
943
+ // aplicada editando frontmatter a mano es justo el paso que un agente hace mal en silencio.
944
+ if (cli.has('--applied')) {
945
+ const result = L.seal(opsRoot(), agent, cli.value('--period'))
946
+ const relative = path.relative(opsRoot(), result.file)
947
+ return console.log(result.already
948
+ ? `= ${relative} ya estaba aplicada`
949
+ : `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
950
+ }
951
+ const result = cli.has('--proposal')
916
952
  ? L.prepareProposal(opsRoot(), agent)
917
953
  : L.prepareReport(opsRoot(), agent)
918
954
  console.log(`${result.created ? '+' : '='} ${path.relative(opsRoot(), result.file)}`)
@@ -920,29 +956,25 @@ function learn(agent) {
920
956
  } catch (error) { fail(error.message, 2) }
921
957
  }
922
958
 
923
- function evaluate(agent) {
959
+ function evaluate(agent, caso, cli) {
924
960
  const root = opsRoot()
925
961
  // El banco sólo tiene sentido acá: en una empresa el cargo que se evalúa es suyo —propio o
926
962
  // 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') {
963
+ if (cli.has('--bench')) {
964
+ if (O.mode(root) !== 'toolkit') {
931
965
  fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: si es del '
932
966
  + `catálogo, adoptalo primero con "ops agents fork ${agent}".`, 2)
933
967
  }
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))
968
+ // El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
969
+ // un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
970
+ return console.log(evaluationBench(root, agent, caso, cli.has('--force')))
939
971
  }
940
972
  try {
941
973
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
942
974
  // máquina, no de persona.
943
- if (process.argv.includes('--cases')) {
975
+ if (cli.has('--cases')) {
944
976
  const cases = EV.list(root, agent)
945
- if (!process.argv.includes('--json')) {
977
+ if (!cli.has('--json')) {
946
978
  for (const item of cases) console.log(`${item.id} ${item.expected.length} comportamiento(s)`)
947
979
  return
948
980
  }
@@ -950,18 +982,20 @@ function evaluate(agent) {
950
982
  }
951
983
  const result = L.evaluate(root, agent)
952
984
  const runs = EV.validate(root, agent)
985
+ const errors = [...result.errors, ...runs.errors]
953
986
  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)
987
+ for (const error of errors) console.error(`✗ ${error}`)
988
+ if (errors.length) fail(`\n${errors.length} error(es)`, 1)
956
989
  const corrida = runs.last ? `${runs.last.passed}/${runs.last.total} pasan (${runs.last.date})` : 'sin correr'
957
990
  console.log(
958
- `✓ ${agent}: ${result.cases} caso(s) — ${corrida}, ${result.proposals} propuesta(s), ` +
991
+ `✓ ${agent}: ${result.cases} caso(s) — ${corrida}, ${result.proposals} propuesta(s)` +
992
+ `${result.pending ? ` (${result.pending} sin aplicar)` : ''}, ` +
959
993
  'controles estructurales válidos',
960
994
  )
961
995
  } catch (error) { fail(error.message, 2) }
962
996
  }
963
997
 
964
- function team(action, slug) {
998
+ function team(action, slug, cli) {
965
999
  if (action === 'list') {
966
1000
  for (const name of T.list(opsRoot())) console.log(name)
967
1001
  return
@@ -973,7 +1007,7 @@ function team(action, slug) {
973
1007
  if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
974
1008
  if (action === 'show') {
975
1009
  // 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))
1010
+ if (cli.has('--json')) return console.log(JSON.stringify(result.manifest))
977
1011
  console.log(`${result.manifest.name} (${slug})`)
978
1012
  console.log(result.manifest.purpose)
979
1013
  for (const stage of result.manifest.stages) {
@@ -985,25 +1019,33 @@ function team(action, slug) {
985
1019
  } catch (error) { fail(error.message, 2) }
986
1020
  }
987
1021
 
988
- async function main() {
989
- const command = process.argv[2]
1022
+ async function run(cli) {
1023
+ const [command] = cli.positional
990
1024
  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])
1025
+ if (!FLAGS[command]) { usage(); fail(`Comando desconocido: ${command}`, 2) }
1026
+ // `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
1027
+ // actual en vez de explicarse.
1028
+ if (cli.has('--help') || cli.has('-h')) return usage()
1029
+ const sobran = cli.unknown(command)
1030
+ if (sobran.length) {
1031
+ const acepta = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
1032
+ fail(`${command}: bandera desconocida ${sobran.join(', ')}. ${acepta}`, 2)
1033
+ }
1034
+ const arg = cli.positional
1035
+ if (command === 'init') init(arg[1], cli)
1036
+ else if (command === 'check') check(arg[1], cli)
1037
+ else if (command === 'tree') tree(arg[1], cli)
1038
+ else if (command === 'context') context(arg[1], cli)
1039
+ else if (command === 'upgrade') upgrade(arg[1], cli)
1040
+ else if (command === 'agents') agents(arg[1], arg[2], arg[3], cli)
998
1041
  else if (command === 'archive') archive(arg[1], arg[2])
999
1042
  else if (command === 'integration') {
1000
- await integration(arg[1], arg[2], arg[3], arg[4])
1043
+ await integration(arg[1], arg[2], arg[3], arg[4], cli)
1001
1044
  }
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) }
1045
+ else if (command === 'automation') automation(arg[1], arg[2], arg[3], cli)
1046
+ else if (command === 'learn') learn(arg[1], cli)
1047
+ else if (command === 'evaluate') evaluate(arg[1], arg[2], cli)
1048
+ else if (command === 'team') team(arg[1], arg[2], cli)
1007
1049
  }
1008
1050
 
1009
- main().catch((error) => fail(error.message))
1051
+ run(parse(process.argv.slice(2))).catch((error) => fail(error.message))