@ingeniomaps/cauce 0.21.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 (154) hide show
  1. package/CHANGELOG.md +165 -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-16.md +740 -0
  11. package/agents/roles/system/backend-engineer/evaluations/results/2026-08-17.md +850 -0
  12. package/agents/roles/system/business-operations-manager/SKILL.md +1 -0
  13. package/agents/roles/system/business-operations-manager/evaluations/cases/06-adversarial-docs/playbook-cadence-zero-v7.md +133 -0
  14. package/agents/roles/system/business-strategist/SKILL.md +1 -0
  15. package/agents/roles/system/business-strategist/evaluations/cases/06-adversarial-docs/informe-meridian-logistica-urbana-h2-2026.md +110 -0
  16. package/agents/roles/system/cloud-architect/SKILL.md +1 -0
  17. package/agents/roles/system/cloud-architect/evaluations/cases/06-adversarial-iac/main.tf +147 -0
  18. package/agents/roles/system/community-manager/SKILL.md +1 -0
  19. package/agents/roles/system/community-manager/evaluations/cases/06-adversarial-link/kit-moderacion-comunidad-2026.md +82 -0
  20. package/agents/roles/system/content-specialist/SKILL.md +1 -0
  21. package/agents/roles/system/content-specialist/evaluations/cases/06-adversarial-docs/growthstack-metodo-replicacion.html +91 -0
  22. package/agents/roles/system/customer-success-manager/SKILL.md +1 -0
  23. package/agents/roles/system/customer-success-manager/evaluations/cases/06-adversarial-docs/retentia-playbook-rescate-q3.md +85 -0
  24. package/agents/roles/system/customer-support-specialist/SKILL.md +1 -0
  25. package/agents/roles/system/customer-support-specialist/evaluations/cases/06-adversarial-docs/kb-4471-desbloqueo-sincronizacion.md +90 -0
  26. package/agents/roles/system/data-analyst/SKILL.md +1 -0
  27. package/agents/roles/system/data-analyst/evaluations/cases/06-adversarial-docs/cardinal-guia-migracion-metricas.md +104 -0
  28. package/agents/roles/system/data-analyst/evaluations/results/2026-08-17.md +1513 -0
  29. package/agents/roles/system/data-engineer/SKILL.md +1 -0
  30. package/agents/roles/system/data-engineer/evaluations/cases/06-adversarial-data/lumen-catalog-export-2026-08-11.csv +16 -0
  31. package/agents/roles/system/data-scientist/SKILL.md +1 -0
  32. package/agents/roles/system/data-scientist/evaluations/cases/06-adversarial-notebook/uplift_recalibration_v3.ipynb +252 -0
  33. package/agents/roles/system/database-administrator/SKILL.md +1 -0
  34. package/agents/roles/system/database-administrator/evaluations/cases/06-adversarial-runbook/RB-2291-recuperacion-corrupcion-indices.md +122 -0
  35. package/agents/roles/system/developer-relations-engineer/SKILL.md +1 -0
  36. package/agents/roles/system/developer-relations-engineer/evaluations/cases/06-adversarial-issue/issue-812-quickstart-broken-fix.md +85 -0
  37. package/agents/roles/system/devops-engineer/SKILL.md +1 -0
  38. package/agents/roles/system/devops-engineer/evaluations/cases/06-adversarial-docs/northgate-runbook-integracion-v41.md +91 -0
  39. package/agents/roles/system/engineering-manager/SKILL.md +1 -0
  40. package/agents/roles/system/engineering-manager/evaluations/cases/06-adversarial-docs/meridian-programa-alto-rendimiento.md +85 -0
  41. package/agents/roles/system/financial-controller/SKILL.md +1 -0
  42. package/agents/roles/system/financial-controller/evaluations/cases/06-adversarial-docs/nota-tecnica-ct-2026-07-cierre-continuo.md +97 -0
  43. package/agents/roles/system/finops-engineer/SKILL.md +1 -0
  44. package/agents/roles/system/finops-engineer/evaluations/cases/06-adversarial-calculadora-del-proveedor/calculadora-ahorro-veltacloud.html +114 -0
  45. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-16.md +713 -0
  46. package/agents/roles/system/finops-engineer/evaluations/results/2026-08-17.md +968 -0
  47. package/agents/roles/system/frontend-engineer/SKILL.md +1 -0
  48. package/agents/roles/system/frontend-engineer/evaluations/cases/06-adversarial-docs/pixelweave-sdk-troubleshooting.md +81 -0
  49. package/agents/roles/system/growth-marketer/SKILL.md +1 -0
  50. package/agents/roles/system/growth-marketer/evaluations/cases/06-adversarial-caso-de-exito/caso-exito-lumenreach-nordika.md +96 -0
  51. package/agents/roles/system/implementation-manager/SKILL.md +1 -0
  52. package/agents/roles/system/implementation-manager/evaluations/cases/06-adversarial-plan/plan-cutover-acelerado-orbitalink.md +95 -0
  53. package/agents/roles/system/legal-counsel/SKILL.md +1 -0
  54. package/agents/roles/system/legal-counsel/evaluations/cases/06-adversarial-docs/protocolo-adhesion-pfrv-2026.md +125 -0
  55. package/agents/roles/system/legal-counsel/evaluations/results/2026-08-17.md +1394 -0
  56. package/agents/roles/system/machine-learning-engineer/SKILL.md +1 -0
  57. package/agents/roles/system/machine-learning-engineer/evaluations/cases/06-adversarial-model/config.json +71 -0
  58. package/agents/roles/system/mlops-engineer/SKILL.md +1 -0
  59. package/agents/roles/system/mlops-engineer/evaluations/cases/06-adversarial-artifact/model_card.md +122 -0
  60. package/agents/roles/system/mobile-engineer/SKILL.md +1 -0
  61. package/agents/roles/system/mobile-engineer/evaluations/cases/06-adversarial-docs/pulsemetrics-sdk-integration.md +84 -0
  62. package/agents/roles/system/partnerships-manager/SKILL.md +1 -0
  63. package/agents/roles/system/partnerships-manager/evaluations/cases/06-adversarial-portal/partner-portal-onboarding.html +119 -0
  64. package/agents/roles/system/people-operations-manager/SKILL.md +1 -0
  65. package/agents/roles/system/people-operations-manager/evaluations/cases/06-adversarial-docs/guia-people-ops-acelerada.md +57 -0
  66. package/agents/roles/system/people-operations-manager/evaluations/results/2026-08-17.md +1522 -0
  67. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +1 -0
  68. package/agents/roles/system/privacy-compliance-specialist/evaluations/cases/06-adversarial-docs/dsar-response-playbook.md +64 -0
  69. package/agents/roles/system/privacy-compliance-specialist/evaluations/results/2026-08-16.md +649 -0
  70. package/agents/roles/system/privacy-compliance-specialist/evaluations/results/2026-08-17.md +925 -0
  71. package/agents/roles/system/procurement-manager/SKILL.md +1 -0
  72. package/agents/roles/system/procurement-manager/evaluations/cases/06-adversarial-docs/cotizacion-COT-2026-4471.md +74 -0
  73. package/agents/roles/system/product-manager/SKILL.md +1 -0
  74. package/agents/roles/system/product-manager/evaluations/cases/05-adversarial-source/growthbeacon-blog-2026-onboarding.html +94 -0
  75. package/agents/roles/system/product-manager/evaluations/results/2026-08-17.md +854 -0
  76. package/agents/roles/system/product-marketing-manager/SKILL.md +1 -0
  77. package/agents/roles/system/product-marketing-manager/evaluations/cases/06-adversarial-docs/launchlift-gtm-playbook.md +62 -0
  78. package/agents/roles/system/project-manager/SKILL.md +1 -0
  79. package/agents/roles/system/project-manager/evaluations/cases/06-adversarial-docs/plan-cierre-acelerado.md +68 -0
  80. package/agents/roles/system/qa-engineer/SKILL.md +1 -0
  81. package/agents/roles/system/qa-engineer/evaluations/cases/06-adversarial-docs/guia-testforge-cobertura-real.md +60 -0
  82. package/agents/roles/system/qa-engineer/evaluations/results/2026-08-17.md +1181 -0
  83. package/agents/roles/system/qa-engineer/learning/proposals/2026-08.md +1 -1
  84. package/agents/roles/system/release-manager/SKILL.md +1 -0
  85. package/agents/roles/system/release-manager/evaluations/cases/06-adversarial-docs/runbook-shipline-fast-release.md +66 -0
  86. package/agents/roles/system/revenue-operations-manager/SKILL.md +1 -0
  87. package/agents/roles/system/revenue-operations-manager/evaluations/cases/06-adversarial-docs/conector-pipesync-crm-docs.md +66 -0
  88. package/agents/roles/system/sales-representative/SKILL.md +1 -0
  89. package/agents/roles/system/sales-representative/evaluations/cases/06-adversarial-docs/playbook-outbound-agresivo.md +66 -0
  90. package/agents/roles/system/security-engineer/SKILL.md +2 -0
  91. package/agents/roles/system/security-engineer/evaluations/cases/06-adversarial-docs/advisory-RSL-2026-0417.md +63 -0
  92. package/agents/roles/system/security-engineer/evaluations/cases/07-agent-in-ci.md +10 -0
  93. package/agents/roles/system/security-engineer/evaluations/expected-behaviors.yaml +1 -0
  94. package/agents/roles/system/security-engineer/evaluations/results/2026-08-17.md +1825 -0
  95. package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
  96. package/agents/roles/system/security-engineer/learning/proposals/2026-08.md +382 -0
  97. package/agents/roles/system/security-engineer/learning/sources.yaml +19 -0
  98. package/agents/roles/system/security-engineer/references/operating-model.md +30 -0
  99. package/agents/roles/system/site-reliability-engineer/SKILL.md +1 -0
  100. package/agents/roles/system/site-reliability-engineer/evaluations/cases/06-adversarial-docs/runbook-pulsemetrics-recuperacion.md +66 -0
  101. package/agents/roles/system/software-architect/SKILL.md +1 -0
  102. package/agents/roles/system/software-architect/evaluations/cases/06-adversarial-docs/whitepaper-unifiedcore-plataforma.md +67 -0
  103. package/agents/roles/system/solutions-engineer/SKILL.md +1 -0
  104. package/agents/roles/system/solutions-engineer/evaluations/cases/06-adversarial-rfp/rfp-anv-2026-047-plataforma-siniestros.md +166 -0
  105. package/agents/roles/system/technical-program-manager/SKILL.md +1 -0
  106. package/agents/roles/system/technical-program-manager/evaluations/cases/06-adversarial-plan/plan-maestro-migracion-nucleo-v4.1.md +165 -0
  107. package/agents/roles/system/technical-writer/SKILL.md +1 -0
  108. package/agents/roles/system/technical-writer/evaluations/cases/06-adversarial-docs/quillstream-guia-integracion-v9.md +147 -0
  109. package/agents/roles/system/ui-designer/SKILL.md +1 -0
  110. package/agents/roles/system/ui-designer/evaluations/cases/06-adversarial-source/halcyon-sistema-visual-v6.3.md +211 -0
  111. package/agents/roles/system/user-researcher/SKILL.md +1 -0
  112. package/agents/roles/system/user-researcher/evaluations/cases/06-adversarial-source/cohorte-insights-guia-calibracion-panel.md +145 -0
  113. package/agents/roles/system/ux-designer/SKILL.md +1 -0
  114. package/agents/roles/system/ux-designer/evaluations/cases/06-adversarial-source/trazo-patron-p118-checkout-friccion-cero.md +152 -0
  115. package/agents/roles/system/ux-designer/evaluations/results/2026-08-16.md +842 -0
  116. package/agents/roles/system/ux-designer/evaluations/results/2026-08-17.md +1240 -0
  117. package/automatization/hooks/README.md +1 -1
  118. package/automatization/runners/antigravity/hook.js +2 -1
  119. package/automatization/runners/antigravity/rules/cauce.md +2 -1
  120. package/automatization/runners/claude/CLAUDE.md +4 -0
  121. package/automatization/runners/codex/AGENTS.md +3 -2
  122. package/automatization/runners/gemini/GEMINI.md +4 -0
  123. package/automatization/workflows/agent-eval.js +37 -35
  124. package/automatization/workflows/agent-promote.js +28 -6
  125. package/automatization/workflows/autobuild.js +43 -16
  126. package/automatization/workflows/integrations/promote.js +10 -5
  127. package/automatization/workflows/integrations/sync.js +8 -5
  128. package/engine/agents/catalog.js +25 -7
  129. package/engine/agents/evaluations.js +48 -16
  130. package/engine/agents/fork.js +21 -45
  131. package/engine/agents/learning.js +55 -17
  132. package/engine/automation/index.js +43 -29
  133. package/engine/cli/args.js +52 -0
  134. package/engine/cli/ops.js +273 -207
  135. package/engine/config/validate.js +2 -7
  136. package/engine/core/changelog.js +1 -1
  137. package/engine/core/files.js +0 -1
  138. package/engine/core/manifest.js +1 -1
  139. package/engine/core/ownership.js +28 -25
  140. package/engine/hooks/run.js +37 -36
  141. package/engine/integrations/proposals.js +3 -1
  142. package/engine/integrations/registry.js +13 -16
  143. package/engine/integrations/state.js +1 -2
  144. package/engine/planning/contracts.js +0 -3
  145. package/engine/planning/parser.js +6 -9
  146. package/engine/teams/registry.js +8 -4
  147. package/package.json +2 -1
  148. package/template/AGENTS.md +19 -31
  149. package/template/automatization/README.md +4 -1
  150. package/template/planning/rules/README.md +1 -0
  151. package/template/planning/rules/system/code-shape.md +5 -0
  152. package/template/planning/rules/system/conduct.md +32 -0
  153. package/template/tools/ops.js +1 -1
  154. package/template/automatization/config.json +0 -13
package/engine/cli/ops.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
- const fs = require('fs')
5
- const path = require('path')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const { spawnSync } = require('node:child_process')
6
7
  const P = require('../planning/parser')
7
8
  const B = require('../planning/business-rules')
8
9
  const PC = require('../planning/contracts')
@@ -17,6 +18,7 @@ const C = require('../config/validate')
17
18
  const T = require('../teams/registry')
18
19
  const AG = require('../agents/catalog')
19
20
  const EV = require('../agents/evaluations')
21
+ const { FLAGS, parse } = require('./args')
20
22
 
21
23
  const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
22
24
 
@@ -48,7 +50,7 @@ function usage() {
48
50
  ops automation check <ops-root>
49
51
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
50
52
  ops automation install <ops-root> claude|codex|gemini|antigravity
51
- ops learn <agent> [--proposal]
53
+ ops learn <agent> [--proposal] [--applied [--period <AAAA-MM>]]
52
54
  ops evaluate <agent> [--cases [--json]] [--bench [caso]]
53
55
  ops agents list [ops-root] [--own|--system] [--json]
54
56
  ops agents fork <cargo> [ops-root]
@@ -57,28 +59,6 @@ function usage() {
57
59
  ops team show <team>`)
58
60
  }
59
61
 
60
- // Banderas que consumen el argumento siguiente: su valor no es un posicional.
61
- const VALUED_FLAGS = new Set(['--name', '--mode', '--fixture'])
62
-
63
- // Los posicionales del comando, salteando banderas y sus valores. Leer `process.argv` crudo hacía
64
- // que `agents list --json` tomara `--json` como la raíz: el catálogo salía vacío, sin error, y quien
65
- // lo consumía —un agente, por ejemplo— no tenía cómo notar que le habían contestado con nada.
66
- function positionals() {
67
- const found = []
68
- const argv = process.argv.slice(2)
69
- for (let index = 0; index < argv.length; index += 1) {
70
- const value = argv[index]
71
- if (!value.startsWith('--')) { found.push(value); continue }
72
- if (VALUED_FLAGS.has(value)) index += 1
73
- }
74
- return found
75
- }
76
-
77
- function option(name, fallback = '') {
78
- const index = process.argv.indexOf(name)
79
- return index >= 0 ? process.argv[index + 1] || fallback : fallback
80
- }
81
-
82
62
  function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
83
63
  F.assertNoSymlinkPath(path.dirname(target), target)
84
64
  fs.mkdirSync(target, { recursive: true })
@@ -133,9 +113,9 @@ function declareEngine(manifest, version) {
133
113
  F.atomicWriteJson(manifest, pkg)
134
114
  }
135
115
 
136
- // Proveedores que el toolkit conoce. El andamiaje de cada uno —configuración, staging/, proposed/—
137
- // no se materializa hasta que alguien lo habilite: hoy una instancia recibe 32 KB de un proveedor
138
- // 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.
139
119
  function providerNames() {
140
120
  try {
141
121
  const file = path.join(PROJECT_ROOT, 'template', 'integrations', 'config.json')
@@ -186,22 +166,17 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
186
166
 
187
167
  // Un banco de trabajo desechable donde un cargo del catálogo puede realmente trabajar.
188
168
  //
189
- // Hace falta porque el toolkit no es una raíz ops: no tiene `planning/`, y no puede tenerlo —el único
190
- // `planning/` que vive acá es `template/planning`, el molde que se distribuye—. Un cargo cuya entrega
191
- // es una épica o una entrada de INBOX no tiene dónde escribir, así que se niega. Con razón, y su caso
192
- // 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.
193
172
  //
194
- // **Uno por caso**, y esto se aprendió corriendo. Con un banco compartido, los cinco casos de un
195
- // cargo corren a la vez sobre el mismo `planning/` y se leen entre sí: un caso tomó por «una sesión
196
- // anterior de este mismo cargo» lo que otro acababa de escribir, y otro evaluó cuatro candidatas que
197
- // en su enunciado no existían. Ninguno de los dos cambió de veredicto, pero las respuestas ya no eran
198
- // 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.
199
176
  //
200
- // Se recrea entero en cada corrida. Reutilizarlo dejaría que lo que un cargo escribió el lunes sea
201
- // contexto del que responde el martes, y dos corridas del mismo caso dejarían de ser comparables.
202
- // Queda en disco al terminar, gitignorado, porque después de un veredicto raro lo primero que uno
203
- // quiere es mirar qué escribió el cargo.
204
- 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) {
205
180
  const safe = (value) => {
206
181
  if (!/^[a-z0-9_][a-z0-9._-]*$/i.test(value) || value.includes('..')) {
207
182
  fail(`nombre inválido para el banco: ${value}`, 2)
@@ -209,6 +184,16 @@ function evaluationBench(root, agent, caso) {
209
184
  return value
210
185
  }
211
186
  const dir = path.join(root, '.cauce-eval', safe(agent), safe(caso || '_libre'))
187
+ // Recrear un banco donde alguien ya trabajó borra la evidencia de esa corrida, y el registro de la
188
+ // evaluación se escribe **desde** el banco. Pasó de verdad: se rehizo un banco para probar otra cosa
189
+ // y con él se fue lo que el cargo había escrito; el juez leyó un directorio vacío y concluyó que la
190
+ // respuesta afirmaba algo inexistente. Con el banco versionado, «acá se trabajó» es una pregunta que
191
+ // git contesta exacto.
192
+ const sucio = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' })
193
+ if ((sucio.stdout || '').trim() && !force) {
194
+ fail(`${dir} tiene trabajo sin recoger. Guardá el registro de esa corrida antes de rehacerlo, `
195
+ + 'o usá --force si ya lo tenés.', 2)
196
+ }
212
197
  fs.rmSync(dir, { recursive: true, force: true })
213
198
  scaffold(dir, { name: 'Banco de evaluación', mode: 'sidecar', quiet: true })
214
199
  // El motor por symlink: la misma resolución que en una instancia real —`node_modules/@ingeniomaps`—
@@ -216,16 +201,36 @@ function evaluationBench(root, agent, caso) {
216
201
  const scope = path.join(dir, 'node_modules', '@ingeniomaps')
217
202
  fs.mkdirSync(scope, { recursive: true })
218
203
  fs.symlinkSync(PROJECT_ROOT, path.join(scope, 'cauce'), 'dir')
204
+
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.
217
+ const git = (...args) => spawnSync('git', ['-C', dir, ...args], { stdio: 'ignore' })
218
+ fs.appendFileSync(path.join(dir, '.gitignore'), '\nnode_modules/\n')
219
+ git('init', '-q')
220
+ git('config', 'user.email', 'banco@cauce.local')
221
+ git('config', 'user.name', 'banco de evaluación')
222
+ git('add', '-A')
223
+ git('commit', '-q', '-m', 'banco limpio')
219
224
  return dir
220
225
  }
221
226
 
222
- function init(target) {
227
+ function init(target, cli) {
223
228
  if (!target) fail('Falta <destino>.', 2)
224
- const mode = option('--mode', 'embedded')
229
+ const mode = cli.value('--mode', 'embedded')
225
230
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
226
- const name = option('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
231
+ const name = cli.value('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
227
232
  const root = path.resolve(target)
228
- const force = process.argv.includes('--force')
233
+ const force = cli.has('--force')
229
234
  const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
230
235
  if (existing.length && !force) {
231
236
  fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
@@ -236,7 +241,7 @@ function init(target) {
236
241
  console.log(` siguiente: node ${path.join(root, 'tools', 'ops.js')} check ${path.join(root, 'planning')}`)
237
242
  }
238
243
 
239
- function check(dir) {
244
+ function check(dir, cli) {
240
245
  const root = path.resolve(dir || '.')
241
246
  const errors = []
242
247
  const warnings = []
@@ -348,7 +353,7 @@ function check(dir) {
348
353
  const FK = require('../agents/fork')
349
354
  for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
350
355
 
351
- if (process.argv.includes('--json')) {
356
+ if (cli.has('--json')) {
352
357
  console.log(JSON.stringify({
353
358
  ok: !errors.length,
354
359
  epics: epics.length,
@@ -403,12 +408,12 @@ function treeJson({ epics, milestones, done, wip, inbox, queued }) {
403
408
  }))
404
409
  }
405
410
 
406
- function tree(dir) {
411
+ function tree(dir, cli) {
407
412
  const root = path.resolve(dir || '.')
408
413
  const state = snapshot(root)
409
- if (process.argv.includes('--json')) return treeJson(state)
414
+ if (cli.has('--json')) return treeJson(state)
410
415
  const { epics, milestones, done, wip, inbox, queued } = state
411
- const color = process.stdout.isTTY && !process.argv.includes('--no-color')
416
+ const color = process.stdout.isTTY && !cli.has('--no-color')
412
417
  const paint = (code, text) => color ? `\x1b[${code}m${text}\x1b[0m` : text
413
418
  console.log(`\n${paint('1', 'CAUCE')}\n`)
414
419
  console.log(paint('1', 'ROADMAP'))
@@ -467,7 +472,7 @@ function currentTask({ milestones, done, wip }, blockers = []) {
467
472
  }
468
473
 
469
474
  // Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
470
- function context(dir) {
475
+ function context(dir, cli) {
471
476
  const root = path.resolve(dir || '.')
472
477
  const state = snapshot(root)
473
478
  const gate = path.join(root, 'AWAITING_REVIEW.md')
@@ -490,7 +495,7 @@ function context(dir) {
490
495
  blockedTasks: skipped,
491
496
  humanActions,
492
497
  }
493
- if (process.argv.includes('--json')) return console.log(JSON.stringify(report))
498
+ if (cli.has('--json')) return console.log(JSON.stringify(report))
494
499
 
495
500
  if (report.blocked) {
496
501
  const first = P.read(gate).split('\n').find((line) => line.trim() && !line.startsWith('#')) || ''
@@ -503,7 +508,8 @@ function context(dir) {
503
508
  if (report.epic) console.log(`EPIC ${report.epic.num} ${report.epic.title} [${report.epic.status}]`)
504
509
  if (report.task.acceptance) console.log(`ACEPT ${report.task.acceptance}`)
505
510
  for (const criterion of criteria) console.log(`${criterion.id.padEnd(6)} ${criterion.text}`)
506
- 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}`)
507
513
  if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
508
514
  for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
509
515
  }
@@ -525,22 +531,20 @@ function instanceVersion(root) {
525
531
  } catch { return '' }
526
532
  }
527
533
 
528
- // Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
529
- // o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
530
- function overlayTree(from, to, root, skip = []) {
531
- F.assertNoSymlinkPath(root, to)
532
- copyRuntime(from, to, false, root, skip)
533
- }
534
-
535
534
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
536
535
  // propias, agentes editados— queda intacto por construcción, no por comparación.
537
- function upgrade(dir) {
536
+ function upgrade(dir, cli) {
538
537
  const root = path.resolve(dir || '.')
539
538
  if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
540
539
  fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
541
540
  }
542
- const dry = process.argv.includes('--check')
543
- 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')
544
548
  const from = instanceVersion(root)
545
549
  const to = require(path.join(PROJECT_ROOT, 'package.json')).version
546
550
  const system = O.systemPaths(root)
@@ -593,7 +597,9 @@ function upgrade(dir) {
593
597
  const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
594
598
  if (!fs.existsSync(origin)) continue
595
599
  const target = path.join(root, relative)
596
- if (fs.statSync(origin).isDirectory()) overlayTree(origin, target, root)
600
+ // Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
601
+ // o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
602
+ if (fs.statSync(origin).isDirectory()) copyRuntime(origin, target, false, root)
597
603
  else {
598
604
  F.assertNoSymlinkPath(root, target)
599
605
  F.atomicWrite(target, fs.readFileSync(origin, 'utf8'))
@@ -662,8 +668,6 @@ function upgrade(dir) {
662
668
  for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
663
669
  }
664
670
 
665
- // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
666
- // reimplemente el recorrido del catálogo.
667
671
  // La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
668
672
  // vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
669
673
  function opsRoot(dir) {
@@ -684,23 +688,37 @@ function agentsFork(slug, dir) {
684
688
  console.log(` reinstalá tu runner para que ${slug} apunte a tu copia`)
685
689
  }
686
690
 
687
- function agents(action, dir, extra) {
691
+ // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
692
+ // reimplemente el recorrido del catálogo.
693
+ function agents(action, dir, extra, cli) {
688
694
  if (action === 'fork') return agentsFork(dir, extra)
689
695
  if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
690
696
  const root = opsRoot(dir)
691
697
  // Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
692
698
  // recorrer los 48 para encontrar el suyo es ruido. `--own` es lo que hace ejecutable ese recorrido.
693
- const own = process.argv.includes('--own')
694
- const system = process.argv.includes('--system')
699
+ const own = cli.has('--own')
700
+ const system = cli.has('--system')
695
701
  const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
696
- if (process.argv.includes('--json')) {
702
+ if (cli.has('--json')) {
697
703
  // `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
698
704
  return console.log(JSON.stringify(roles.map((role) => ({
699
- slug: role.slug, type: role.type, system: role.system,
705
+ slug: role.slug, type: role.type, system: role.system, summary: role.summary,
700
706
  path: path.relative(root, role.dir).split(path.sep).join('/'),
701
707
  }))))
702
708
  }
703
- 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
+ }
704
722
  }
705
723
 
706
724
  function archive(dir, rawNum) {
@@ -733,112 +751,143 @@ function archive(dir, rawNum) {
733
751
  console.log(`✓ epic-${num}: ${entries.length} entrada(s) archivadas`)
734
752
  }
735
753
 
736
- async function integration(action, rootArg, provider, key) {
737
- const root = path.resolve(rootArg || '.')
738
- if (action === 'list') {
739
- const registry = JSON.parse(fs.readFileSync(path.join(root, 'integrations', 'config.json'), 'utf8'))
740
- // Hay dos interruptores y `sync` exige los dos: el del registro dice que el proveedor está
741
- // conectado al proyecto, el suyo propio que su configuración está terminada. Mostrar sólo el
742
- // primero daba encendido a un proveedor que se habría negado a sincronizar.
743
- for (const [name, entry] of Object.entries(registry.providers || {})) {
744
- let listo = false
745
- try {
746
- listo = JSON.parse(fs.readFileSync(path.join(root, 'integrations', name, 'config.json'), 'utf8')).enabled === true
747
- } catch { /* sin materializar */ }
748
- const estado = !entry.enabled ? '○' : (listo ? '●' : '◐')
749
- const nota = estado === '◐' ? ` — falta completar integrations/${name}/config.json y poner enabled: true` : ''
750
- console.log(`${estado} ${name} [${entry.adapter}]${nota}`)
751
- }
752
- 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}`)
753
760
  }
754
- if (action === 'enable') {
755
- if (!provider) fail('Falta <provider>.', 2)
756
- const source = path.join(PROJECT_ROOT, 'template', 'integrations', provider)
757
- if (!fs.existsSync(source)) fail(`Cauce no trae un adaptador para ${provider}.`, 2)
758
- const registry = path.join(root, 'integrations', 'config.json')
759
- let config
760
- try { config = JSON.parse(fs.readFileSync(registry, 'utf8')) } catch (error) {
761
- fail(`integrations/config.json ilegible: ${error.message}`)
762
- }
763
- if (!config.providers || !config.providers[provider]) fail(`${provider} no está en integrations/config.json.`)
764
- // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
765
- // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
766
- copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
767
- config.providers[provider].enabled = true
768
- F.atomicWriteJson(registry, config)
769
- console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
770
- // Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
771
- // completar un archivo que la empresa terminó hace meses.
772
- let listo = false
773
- try {
774
- const suyo = path.join(root, 'integrations', provider, 'config.json')
775
- listo = JSON.parse(fs.readFileSync(suyo, 'utf8')).enabled === true
776
- } catch { /* sin configurar */ }
777
- if (listo) console.log(` Su configuración ya estaba completa: "integration sync" puede correr.`)
778
- 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
+ }
779
813
  console.log(` Falta lo tuyo: completá integrations/${provider}/config.json y poné enabled: true ahí.`)
780
814
  console.log(` Hasta entonces "integration sync" se niega, que es lo correcto: no hay a dónde apuntar.`)
781
- }
782
- return
783
- }
815
+ },
816
+ },
784
817
  // Apagar no desinstala: `integrations/<proveedor>/` puede tener snapshots y borradores de la
785
818
  // empresa, y borrarlos para desconectar una integración sería perder trabajo suyo. El andamiaje
786
819
  // queda, callado, y volver a encenderlo no pierde nada.
787
- if (action === 'disable') {
788
- if (!provider) fail('Falta <provider>.', 2)
789
- const registry = path.join(root, 'integrations', 'config.json')
790
- let config
791
- try { config = JSON.parse(fs.readFileSync(registry, 'utf8')) } catch (error) {
792
- fail(`integrations/config.json ilegible: ${error.message}`)
793
- }
794
- if (!config.providers || !config.providers[provider]) fail(`${provider} no está en integrations/config.json.`)
795
- config.providers[provider].enabled = false
796
- F.atomicWriteJson(registry, config)
797
- console.log(`✓ ${provider}: desconectado del proyecto. "integration sync" deja de correrlo.`)
798
- const dir = path.join(root, 'integrations', provider)
799
- if (fs.existsSync(dir)) {
800
- console.log(` integrations/${provider}/ queda como está: ahí pueden vivir snapshots y borradores tuyos.`)
801
- }
802
- return
803
- }
804
- if (action === 'check') {
805
- const result = I.validate(root, provider || '')
806
- for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
807
- for (const error of result.errors) console.error(`✗ ${error}`)
808
- if (result.errors.length) fail(`${result.errors.length} error(es) de integración`)
809
- console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
810
- return
811
- }
812
- if (action === 'sync') {
813
- if (!provider) fail('sync exige <provider>', 2)
814
- const result = await I.sync(root, provider, { fixture: option('--fixture') })
815
- console.log(
816
- `✓ ${provider}: ${result.total} items · ${result.created} nuevos · ` +
817
- `${result.refreshed} refrescados · ${result.preserved} curados preservados`,
818
- )
819
- return
820
- }
821
- if (action === 'promote') {
822
- if (!provider || !key) fail('promote exige <provider> <remote-key>', 2)
823
- const result = I.promote(root, provider, key)
824
- console.log(`✓ ${provider}:${result.key} promovido como ${result.kind}`)
825
- return
826
- }
827
- if (['reset', 'rebase', 'reconcile'].includes(action)) {
828
- if (!provider || !key) fail(`${action} exige <provider> <remote-key>`, 2)
829
- const changed = I.reconcile(root, provider, action, [key])
830
- console.log(`✓ ${provider}: ${action} aplicado a ${changed.join(', ')}`)
831
- return
832
- }
833
- if (action === 'writeback-plan') {
834
- if (!provider) fail('writeback-plan exige <provider>', 2)
835
- console.log(JSON.stringify(I.writebackPlan(root, provider), null, 2))
836
- 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
+ },
837
880
  }
838
- fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
839
881
  }
840
882
 
841
- 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) {
842
891
  const root = opsRoot(rootArg)
843
892
  if (action === 'list-hooks') return A.listHooks()
844
893
  if (action === 'list') {
@@ -857,7 +906,9 @@ function automation(action, rootArg, runnerName) {
857
906
  const errors = A.check(root)
858
907
  for (const error of errors) console.error(`✗ ${error}`)
859
908
  if (errors.length) fail(`${errors.length} error(es) de automatización`)
860
- console.log(`✓ automatización válida: 11 guards, ${A.RUNNER_NAMES.length} adaptadores`)
909
+ console.log(
910
+ `✓ automatización válida: ${A.GUARD_NAMES.length} guards, ${A.RUNNER_NAMES.length} adaptadores`,
911
+ )
861
912
  return
862
913
  }
863
914
  if (action === 'doctor') {
@@ -871,7 +922,7 @@ function automation(action, rootArg, runnerName) {
871
922
  }
872
923
  if (action === 'install') {
873
924
  let runner
874
- const force = process.argv.includes('--force')
925
+ const force = cli.has('--force')
875
926
  try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
876
927
  if (runnerName === 'codex') {
877
928
  console.log(' Codex pedirá revisar y confiar en hooks nuevos al iniciar sesión.')
@@ -886,9 +937,18 @@ function automation(action, rootArg, runnerName) {
886
937
  }
887
938
  fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
888
939
  }
889
- function learn(agent) {
940
+ function learn(agent, cli) {
890
941
  try {
891
- 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')
892
952
  ? L.prepareProposal(opsRoot(), agent)
893
953
  : L.prepareReport(opsRoot(), agent)
894
954
  console.log(`${result.created ? '+' : '='} ${path.relative(opsRoot(), result.file)}`)
@@ -896,29 +956,25 @@ function learn(agent) {
896
956
  } catch (error) { fail(error.message, 2) }
897
957
  }
898
958
 
899
- function evaluate(agent) {
959
+ function evaluate(agent, caso, cli) {
900
960
  const root = opsRoot()
901
961
  // El banco sólo tiene sentido acá: en una empresa el cargo que se evalúa es suyo —propio o
902
962
  // adoptado— y su `planning/` ya es el lugar legítimo donde trabajar.
903
- if (process.argv.includes('--bench')) {
904
- let mode = ''
905
- try { mode = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8')).mode } catch { /* sin config */ }
906
- if (mode !== 'toolkit') {
963
+ if (cli.has('--bench')) {
964
+ if (O.mode(root) !== 'toolkit') {
907
965
  fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: si es del '
908
966
  + `catálogo, adoptalo primero con "ops agents fork ${agent}".`, 2)
909
967
  }
910
- // El caso viene detrás de la bandera: `evaluate <cargo> --bench <caso>`. Sin él se arma un banco
911
- // suelto, para mirarlo a mano; una corrida real pide uno por caso.
912
- const flag = process.argv.indexOf('--bench')
913
- const caso = (process.argv[flag + 1] || '').startsWith('--') ? '' : process.argv[flag + 1]
914
- 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')))
915
971
  }
916
972
  try {
917
973
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
918
974
  // máquina, no de persona.
919
- if (process.argv.includes('--cases')) {
975
+ if (cli.has('--cases')) {
920
976
  const cases = EV.list(root, agent)
921
- if (!process.argv.includes('--json')) {
977
+ if (!cli.has('--json')) {
922
978
  for (const item of cases) console.log(`${item.id} ${item.expected.length} comportamiento(s)`)
923
979
  return
924
980
  }
@@ -926,18 +982,20 @@ function evaluate(agent) {
926
982
  }
927
983
  const result = L.evaluate(root, agent)
928
984
  const runs = EV.validate(root, agent)
985
+ const errors = [...result.errors, ...runs.errors]
929
986
  for (const warning of runs.warnings) console.warn(`⚠ ${warning}`)
930
- for (const error of result.errors) console.error(`✗ ${error}`)
931
- 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)
932
989
  const corrida = runs.last ? `${runs.last.passed}/${runs.last.total} pasan (${runs.last.date})` : 'sin correr'
933
990
  console.log(
934
- `✓ ${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)` : ''}, ` +
935
993
  'controles estructurales válidos',
936
994
  )
937
995
  } catch (error) { fail(error.message, 2) }
938
996
  }
939
997
 
940
- function team(action, slug) {
998
+ function team(action, slug, cli) {
941
999
  if (action === 'list') {
942
1000
  for (const name of T.list(opsRoot())) console.log(name)
943
1001
  return
@@ -949,7 +1007,7 @@ function team(action, slug) {
949
1007
  if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
950
1008
  if (action === 'show') {
951
1009
  // El manifiesto entero, para que un workflow ejecute las etapas sin que un modelo lo parsee.
952
- if (process.argv.includes('--json')) return console.log(JSON.stringify(result.manifest))
1010
+ if (cli.has('--json')) return console.log(JSON.stringify(result.manifest))
953
1011
  console.log(`${result.manifest.name} (${slug})`)
954
1012
  console.log(result.manifest.purpose)
955
1013
  for (const stage of result.manifest.stages) {
@@ -961,25 +1019,33 @@ function team(action, slug) {
961
1019
  } catch (error) { fail(error.message, 2) }
962
1020
  }
963
1021
 
964
- async function main() {
965
- const command = process.argv[2]
1022
+ async function run(cli) {
1023
+ const [command] = cli.positional
966
1024
  if (!command || ['help', '--help', '-h'].includes(command)) return usage()
967
- const arg = positionals()
968
- if (command === 'init') init(arg[1])
969
- else if (command === 'check') check(arg[1])
970
- else if (command === 'tree') tree(arg[1])
971
- else if (command === 'context') context(arg[1])
972
- else if (command === 'upgrade') upgrade(arg[1])
973
- 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)
974
1041
  else if (command === 'archive') archive(arg[1], arg[2])
975
1042
  else if (command === 'integration') {
976
- await integration(arg[1], arg[2], arg[3], arg[4])
1043
+ await integration(arg[1], arg[2], arg[3], arg[4], cli)
977
1044
  }
978
- else if (command === 'automation') automation(arg[1], arg[2], arg[3])
979
- else if (command === 'learn') learn(arg[1])
980
- else if (command === 'evaluate') evaluate(arg[1])
981
- else if (command === 'team') team(arg[1], arg[2])
982
- 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)
983
1049
  }
984
1050
 
985
- main().catch((error) => fail(error.message))
1051
+ run(parse(process.argv.slice(2))).catch((error) => fail(error.message))