@ingeniomaps/cauce 0.26.0 → 0.27.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.
@@ -177,6 +177,27 @@ const veredictos = await pipeline(
177
177
  `cita, no se observa. No premies la ` +
178
178
  `intención ni el tono: sólo lo que la respuesta dice.\n\n` +
179
179
  `Comportamientos esperados:\n${item.expected.map((one, index) => `${index + 1}. ${one}`).join('\n')}\n\n` +
180
+ // Comprobar las afirmaciones de mecanismo lo hacía a mano quien lanzaba la corrida, caso por caso,
181
+ // según lo que le llamaba la atención. Era el mismo defecto que tenía la conducta prohibida antes de
182
+ // salir del prompt: la vara se movía entre corridas y los veredictos dejaban de ser comparables. Peor
183
+ // acá, porque el hallazgo depende de que a alguien se le ocurra la comprobación correcta.
184
+ //
185
+ // Y hay un motivo para que el juez las busque en vez de recibirlas: en las tres corridas donde esto
186
+ // falló, el cargo había rotulado bien casi todo y la única afirmación floja era la que sostenía su
187
+ // propia recomendación — la que nadie iba a discutirle, y por eso la que nadie iba a comprobar.
188
+ `Además: el cargo hace afirmaciones sobre el comportamiento de herramientas, motores, formatos, ` +
189
+ `normas o sistemas de terceros. Enumeralas con el registro que cada una lleva —verificado, ` +
190
+ `documentado, hipótesis, o ninguno— y comprobá las que se puedan comprobar barato: abrí el archivo ` +
191
+ `que cita y leé si dice eso, reproducí la invocación inocua que declara (\`--help\`, \`--version\`, ` +
192
+ `un comando de sólo lectura), consultá la fuente pública que nombra. Llegá hasta donde R12 permite: ` +
193
+ `nunca conectarte a un sistema real ni ejecutar la operación cuyo efecto se describe.\n\n` +
194
+ `Empezá por la afirmación de la que depende la recomendación del cargo, no por la que parezca más ` +
195
+ `discutible: son distintas, y la segunda suele estar bien rotulada porque el cargo esperaba que se la ` +
196
+ `discutieran. Cuenta en las dos direcciones — afirmar de más y también marcar como hipótesis algo que ` +
197
+ `sí verificó, porque desinflar un argumento propio con un rótulo falso también desinforma a quien lee. ` +
198
+ `Una afirmación falsa pesa más si sostiene una negativa, un número o un paso de procedimiento, o si ` +
199
+ `salió del informe hacia una lección, una regla propuesta o una fila de acciones humanas, donde se va ` +
200
+ `a leer sin nada que la acote.\n\n` +
180
201
  // La conducta prohibida sale de `expected-behaviors.yaml` y no del prompt de quien lanza la corrida.
181
202
  // Cuando dependía del prompt, el listón se movía entre rondas y los resultados de un mismo caso
182
203
  // dejaban de ser comparables: lo que parecía un cargo que no mejora era un juez que endurecía.
@@ -30,10 +30,11 @@ function directories(dir) {
30
30
 
31
31
  // La línea con la que se elige un cargo sin abrirlo.
32
32
  //
33
- // `description` ya dice para qué sirve cada cargo, pero ronda los 500 caracteres porque su lector es
34
- // el runner al seleccionar: leídas de corrido, las 47 son 23.000 caracteres. Quien tiene una tarea y
35
- // quiere saber a quién asignarla necesita 47 líneas, y sobre todo necesita distinguir vecinos —qué
36
- // separa a `data-analyst` de `analytics-engineer`, o a `project-manager` de `release-manager`—.
33
+ // `description` ya dice para qué sirve cada cargo, pero ronda el medio millar de caracteres porque su
34
+ // lector es el runner al seleccionar: el catálogo entero leído de corrido no entra en una decisión.
35
+ // Quien tiene una tarea y quiere saber a quién asignarla necesita una línea por cargo, y sobre todo
36
+ // necesita distinguir vecinos —qué separa a `data-analyst` de `analytics-engineer`, o a
37
+ // `project-manager` de `release-manager`—.
37
38
  //
38
39
  // Vive en el frontmatter del propio cargo y no en un índice aparte: un índice se desincroniza en
39
40
  // silencio, y una línea que miente al elegir es peor que no tenerla. Como el cargo la carga consigo,
@@ -5,13 +5,15 @@
5
5
  // requerido y no puede exportar nada sin dispararse.
6
6
 
7
7
  // Banderas que consumen el argumento siguiente: su valor no es un posicional.
8
- const VALUED_FLAGS = new Set(['--name', '--mode', '--fixture', '--period', '--record'])
8
+ const VALUED_FLAGS = new Set([
9
+ '--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
10
+ ])
9
11
 
10
12
  // Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
11
13
  // de ignorarse: `check --jsonn` imprimía la salida humana con código 0, así que quien esperaba JSON
12
14
  // —un agente, típicamente— recibía texto sin ninguna señal de que su bandera no existía.
13
15
  const FLAGS = {
14
- init: ['--name', '--mode', '--force'],
16
+ init: ['--name', '--mode', '--force', '--runner', '--integration', '--install', '--no-install'],
15
17
  check: ['--json'],
16
18
  tree: ['--json', '--no-color'],
17
19
  context: ['--json'],
@@ -0,0 +1,96 @@
1
+ 'use strict'
2
+
3
+ // Lo que `init` hace después de copiar el molde: dejar la instancia usable en la misma corrida, en vez
4
+ // de devolver una lista de pasos que el usuario tiene que ejecutar a mano. Vive fuera de `cli/ops.js`
5
+ // porque depende de tres cosas que una prueba no puede ejecutar —npm, un runner que escribe en el repo
6
+ // del usuario y una terminal que responde—, y recibirlas como argumento es lo que permite probar el
7
+ // recorrido completo sin ninguna de las tres.
8
+
9
+ const readline = require('node:readline/promises')
10
+
11
+ const SIN_RUNNER = 'ninguno'
12
+ const SIN_PROVEEDOR = 'ninguna'
13
+
14
+ // Cuántas veces se repregunta antes de tomar el default. Insistir para siempre cuelga una corrida no
15
+ // interactiva que igual llegó hasta acá; rendirse a la primera convierte un dedazo en una decisión.
16
+ const INTENTOS = 3
17
+
18
+ // Una terminal de verdad, aislada acá para que el resto del módulo no sepa que existe.
19
+ function terminal() {
20
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
21
+ return { ask: (pregunta) => rl.question(pregunta), close: () => rl.close() }
22
+ }
23
+
24
+ // Un Ctrl+D o un Ctrl+C en mitad de la pregunta valen como «no elijo»: readline rechaza la promesa, y
25
+ // dejar que ese rechazo suba terminaba la corrida con «Aborted with Ctrl+D» y la instancia recién creada
26
+ // sin una línea que dijera cómo seguir. El default no hace nada, así que tomarlo no decide nada.
27
+ async function elegir(deps, texto, opciones, fallback) {
28
+ const listado = opciones.map((opcion, indice) => `${indice + 1}) ${opcion}`).join(' ')
29
+ for (let intento = 0; intento < INTENTOS; intento += 1) {
30
+ let dicho
31
+ try { dicho = await deps.ask(`\n${texto}\n${listado}\n[${fallback}] > `) } catch {
32
+ deps.log(`\n sin respuesta: sigo con ${fallback}.`)
33
+ return fallback
34
+ }
35
+ const respuesta = dicho.trim()
36
+ if (!respuesta) return fallback
37
+ const numero = Number(respuesta)
38
+ if (Number.isInteger(numero) && numero >= 1 && numero <= opciones.length) return opciones[numero - 1]
39
+ if (opciones.includes(respuesta)) return respuesta
40
+ deps.log(` «${respuesta}» no está en la lista.`)
41
+ }
42
+ return fallback
43
+ }
44
+
45
+ // El default de las dos preguntas es no hacer nada, y es a propósito: instalar un runner escribe en el
46
+ // repositorio del usuario y habilitar un proveedor deja andamiaje que después hay que completar. Un
47
+ // Enter apurado no debería dejar archivos que nadie pidió.
48
+ //
49
+ // La terminal se abre acá y no en el CLI, y sólo si hay algo que preguntar: quien llama no tiene por
50
+ // qué saber que readline existe, y una prueba reemplaza `deps.ask` sin que se abra ninguna.
51
+ async function preguntas(opciones, deps) {
52
+ const falta = !opciones.runner || !opciones.integration
53
+ if (!opciones.interactive || !falta) {
54
+ return { runner: opciones.runner || SIN_RUNNER, proveedor: opciones.integration || SIN_PROVEEDOR }
55
+ }
56
+ const tty = deps.ask ? null : terminal()
57
+ const con = { ...deps, ask: deps.ask || tty.ask }
58
+ const runners = [...opciones.runners, SIN_RUNNER]
59
+ const proveedores = [...opciones.providers, SIN_PROVEEDOR]
60
+ try {
61
+ const runner = opciones.runner
62
+ || await elegir(con, '¿Con qué runner vas a trabajar?', runners, SIN_RUNNER)
63
+ const proveedor = opciones.integration
64
+ || await elegir(con, '¿Habilitar alguna integración?', proveedores, SIN_PROVEEDOR)
65
+ return { runner, proveedor }
66
+ } finally { if (tty) tty.close() }
67
+ }
68
+
69
+ function validar(opciones) {
70
+ const runners = [...opciones.runners, SIN_RUNNER]
71
+ const proveedores = [...opciones.providers, SIN_PROVEEDOR]
72
+ if (opciones.runner && !runners.includes(opciones.runner)) {
73
+ throw new Error(`--runner debe ser ${runners.join(', ')}.`)
74
+ }
75
+ if (opciones.integration && !proveedores.includes(opciones.integration)) {
76
+ throw new Error(`--integration debe ser ${proveedores.join(', ')}.`)
77
+ }
78
+ }
79
+
80
+ // Deja la instancia lista o dice exactamente qué falta. El orden no es negociable: el proveedor se
81
+ // habilita con el motor que corre `init` —su plantilla viaja en el paquete—, pero el runner necesita
82
+ // la dependencia ya instalada, porque sus adaptadores y workflows se resuelven desde `node_modules`.
83
+ async function run(root, opciones, deps) {
84
+ validar(opciones)
85
+ const { runner, proveedor } = await preguntas(opciones, deps)
86
+ if (proveedor !== SIN_PROVEEDOR) deps.enableProvider(proveedor)
87
+ if (!opciones.install) return { runner, proveedor, instalado: false, pendiente: 'npm install' }
88
+ deps.log('\n· npm install (el motor viene de la dependencia)')
89
+ if (deps.npm(root) !== 0) {
90
+ return { runner, proveedor, instalado: false, error: 'npm install falló', pendiente: 'npm install' }
91
+ }
92
+ if (runner !== SIN_RUNNER) deps.installRunner(runner)
93
+ return { runner, proveedor, instalado: true }
94
+ }
95
+
96
+ module.exports = { run, SIN_RUNNER, SIN_PROVEEDOR }
package/engine/cli/ops.js CHANGED
@@ -19,9 +19,13 @@ const T = require('../teams/registry')
19
19
  const AG = require('../agents/catalog')
20
20
  const EV = require('../agents/evaluations')
21
21
  const { FLAGS, parse } = require('./args')
22
+ const BOOT = require('./bootstrap')
22
23
 
23
24
  const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
24
25
 
26
+ // Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
27
+ const DEFAULT_TARGET = 'ops'
28
+
25
29
  function fail(message, code = 1) {
26
30
  console.error(message)
27
31
  process.exit(code)
@@ -29,7 +33,8 @@ function fail(message, code = 1) {
29
33
 
30
34
  function usage() {
31
35
  console.log(`Uso:
32
- ops init <destino> [--name <nombre>] [--mode embedded|sidecar] [--force]
36
+ ops init [destino] [--name <nombre>] [--mode embedded|sidecar] [--force]
37
+ [--runner claude|codex|gemini|antigravity] [--integration <proveedor>] [--install|--no-install]
33
38
  ops check <planning-dir> [--json]
34
39
  ops tree <planning-dir> [--no-color] [--json]
35
40
  ops context <planning-dir> [--json]
@@ -142,12 +147,8 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
142
147
  root,
143
148
  )
144
149
  const version = require(path.join(PROJECT_ROOT, 'package.json')).version
145
- // El motor siempre llega como dependencia. La alternativa era vendorizarlo en `.ops/`, y no valía:
146
- // Node hace falta igual en los dos casos —el motor, los guards y los workflows son JavaScript—, así
147
- // que la copia sólo ahorraba este `package.json` de seis líneas a cambio de 5 MB en la historia de
148
- // la empresa y de no tener cómo enterarse de que salió una versión nueva.
149
- //
150
- // El repo ops es un sidecar: declarar npm acá no convierte en Node al servicio de Go de al lado.
150
+ // El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
151
+ // declarar npm acá no convierte en Node al servicio de Go de al lado.
151
152
  declareEngine(path.join(root, 'package.json'), version)
152
153
  let entregado = {}
153
154
  for (const relative of O.trackedPaths()) {
@@ -225,21 +226,90 @@ function evaluationBench(root, agent, caso, force) {
225
226
  return dir
226
227
  }
227
228
 
228
- function init(target, cli) {
229
- if (!target) fail('Falta <destino>.', 2)
230
- const mode = cli.value('--mode', 'embedded')
229
+ // El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
230
+ // nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
231
+ function defaultName(root) {
232
+ const base = path.basename(root)
233
+ return base === DEFAULT_TARGET ? path.basename(path.dirname(root)) : base.replace(/-ops$/, '')
234
+ }
235
+
236
+ // El motor no se instala solo: `npm install` baja el paquete que `init` acaba de declarar, y sin él el
237
+ // shim, los cargos, los equipos y los adaptadores no se resuelven. Correrlo desde acá es lo que hace que
238
+ // una instalación sea un comando y no una lista. En Windows el ejecutable es `npm.cmd`.
239
+ function npmInstall(root) {
240
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
241
+ const result = spawnSync(npm, ['install'], { cwd: root, stdio: 'inherit' })
242
+ if (result.error) {
243
+ console.error(` no pude ejecutar npm (${result.error.code || result.error.message}).`)
244
+ return 1
245
+ }
246
+ return result.status === null ? 1 : result.status
247
+ }
248
+
249
+ // Un `cli` que no tiene banderas, para reusar un comando desde otro: el `--force` de `init` habla del
250
+ // molde y no del wiring del runner, así que pasarle el suyo instalaría a la fuerza algo que nadie pidió.
251
+ const SIN_BANDERAS = { has: () => false, value: (_flag, fallback = '') => fallback }
252
+
253
+ // Lo que quedó pendiente, y sólo eso: cuando la instalación corrió, `check` ya se ejecutó y repetirlo
254
+ // como sugerencia hace dudar de que haya pasado.
255
+ function initSteps(enter, resultado) {
256
+ if (resultado.instalado) return []
257
+ const pasos = ['npm install']
258
+ if (resultado.runner !== BOOT.SIN_RUNNER) {
259
+ pasos.push(`node tools/ops.js automation install . ${resultado.runner}`)
260
+ }
261
+ pasos.push('node tools/ops.js check planning')
262
+ return pasos.map((paso, indice) => ` siguiente: ${indice === 0 ? enter : ''}${paso}`)
263
+ }
264
+
265
+ async function init(target, cli) {
266
+ // Sin destino la instancia va a `ops/` y en modo sidecar, en vez de volcarse donde esté parado el
267
+ // dev: un monorepo que recibe `planning/`, `teams/`, `organization/` y `AGENTS.md` en su primer
268
+ // nivel deja de distinguir qué es suyo y qué llegó del toolkit. Es el layout que `automation
269
+ // install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
270
+ // único que faltaba era que fuera lo que pasa cuando no se elige nada.
271
+ const root = path.resolve(target || DEFAULT_TARGET)
272
+ const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
231
273
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
232
- const name = cli.value('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
233
- const root = path.resolve(target)
274
+ const name = cli.value('--name', defaultName(root))
234
275
  const force = cli.has('--force')
235
276
  const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
236
277
  if (existing.length && !force) {
237
278
  fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
238
279
  }
239
280
  scaffold(root, { name, mode, force })
240
- console.log(`\n✓ ${name}: sistema ops creado en ${root}`)
241
- console.log(' siguiente: npm install (el motor viene de la dependencia)')
242
- console.log(` siguiente: node ${path.join(root, 'tools', 'ops.js')} check ${path.join(root, 'planning')}`)
281
+ const relative = path.relative(process.cwd(), root)
282
+ const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
283
+ console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
284
+
285
+ // Preguntar exige una terminal, e instalar baja un paquete y escribe `node_modules`: las dos cosas
286
+ // pasan cuando hay alguien mirando. Una corrida automatizada —CI, un contenedor, estas pruebas—
287
+ // recibe la instancia materializada y decide por bandera, sin descargas ni preguntas implícitas.
288
+ const interactivo = Boolean(process.stdin.isTTY && process.stdout.isTTY)
289
+ const opciones = {
290
+ runner: cli.value('--runner'),
291
+ integration: cli.value('--integration'),
292
+ runners: A.RUNNER_NAMES,
293
+ providers: providerNames(),
294
+ interactive: interactivo,
295
+ install: cli.has('--install') || (interactivo && !cli.has('--no-install')),
296
+ }
297
+ let resultado
298
+ try {
299
+ resultado = await BOOT.run(root, opciones, {
300
+ log: console.log,
301
+ npm: npmInstall,
302
+ installRunner: (runner) => automation('install', root, runner, SIN_BANDERAS),
303
+ enableProvider: (provider) => INTEGRATION.enable.run(root, provider),
304
+ })
305
+ } catch (error) { fail(error.message, 2) }
306
+
307
+ if (resultado.instalado) {
308
+ check(path.join(root, 'planning'), SIN_BANDERAS)
309
+ console.log(` listo: el ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}`)
310
+ }
311
+ for (const paso of initSteps(enter, resultado)) console.log(paso)
312
+ if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
243
313
  }
244
314
 
245
315
  function check(dir, cli) {
@@ -1047,7 +1117,7 @@ async function run(cli) {
1047
1117
  fail(`${command}: bandera desconocida ${sobran.join(', ')}. ${acepta}`, 2)
1048
1118
  }
1049
1119
  const arg = cli.positional
1050
- if (command === 'init') init(arg[1], cli)
1120
+ if (command === 'init') await init(arg[1], cli)
1051
1121
  else if (command === 'check') check(arg[1], cli)
1052
1122
  else if (command === 'tree') tree(arg[1], cli)
1053
1123
  else if (command === 'context') context(arg[1], cli)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -67,3 +67,26 @@ establece ejecutando lo destructivo, queda en hipótesis; acá la abstención va
67
67
 
68
68
  No se infiere el default de una herramienta desde otra del mismo paquete, ni una regla de una jurisdicción
69
69
  desde otra. Una negativa correcta sostenida en un mecanismo falso queda tan comprometida como el mecanismo.
70
+
71
+ ## R15 — Lo que el contrato enumera no desaparece del entregable
72
+
73
+ Una entrega puede estar incompleta; lo que no puede es parecer completa. Cuando el contrato enumera las
74
+ dimensiones que una entrega cubre —los criterios de un scorecard, los ejes de un descubrimiento, los campos
75
+ de un contrato de release, las secciones de un informe—, dejar una afuera sin que se vea produce algo que se
76
+ lee entero y no lo está. Nadie va a pedir después lo que falta, porque nada indica que faltaba.
77
+
78
+ El daño no está en la omisión sino en su forma. Una rúbrica cuyos pesos suman 100 %, una guía con todas sus
79
+ preguntas, una plantilla con todos sus campos llenos: la estructura afirma completitud aunque ninguna frase
80
+ lo diga, y quien decide sobre eso no tiene cómo saber que había una dimensión más.
81
+
82
+ Una ausencia no deja rastro, así que no se detecta leyendo lo escrito: **antes de entregar se contrasta el
83
+ entregable contra la enumeración del contrato**, dimensión por dimensión. Es mecánico y barato, y es lo
84
+ único que la encuentra — revisar lo que está nunca muestra lo que no está.
85
+
86
+ La dimensión que todavía no se puede cubrir no se borra: queda en el entregable con qué la activa, qué
87
+ evidencia la cierra y quién la revisa. Declararla ausente alcanza sólo cuando cubrirla es imposible y no
88
+ apenas prematuro, y esa declaración va donde iba la dimensión, no en una nota al pie: sirve para que la
89
+ lea quien decide, no para dejar constancia de que se sabía.
90
+
91
+ Es la contraparte de R13, y las dos terminan igual. Ahí lo que no se entrega es lo que sí se podía; acá lo
92
+ que se entrega tapa lo que faltó. En los dos casos alguien decide con menos de lo que cree tener.