@ingeniomaps/cauce 0.53.0 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/automatization/hooks/guard-dependencies.sh +1 -1
- package/automatization/hooks/guard-destructive.sh +1 -1
- package/automatization/hooks/guard-engine.sh +1 -1
- package/automatization/hooks/guard-generated.sh +1 -1
- package/automatization/hooks/guard-git-add.sh +1 -1
- package/automatization/hooks/guard-governance.sh +1 -1
- package/automatization/hooks/guard-integration-snapshot.sh +1 -1
- package/automatization/hooks/guard-migrations.sh +1 -1
- package/automatization/hooks/guard-planning-drift.sh +1 -1
- package/automatization/hooks/guard-secrets.sh +1 -1
- package/automatization/hooks/guard-test-evidence.sh +1 -1
- package/automatization/hooks/guard-verify.sh +1 -1
- package/automatization/hooks/guard-workspace-boundary.sh +1 -1
- package/automatization/hooks/run-hook.sh +3 -2
- package/automatization/runners/antigravity/hook.js +13 -14
- package/automatization/shared/eval-only.js +3 -3
- package/automatization/workflows/agent-eval.js +3 -3
- package/automatization/workflows/agent-promote.js +4 -4
- package/automatization/workflows/autobuild.js +12 -12
- package/automatization/workflows/flow-eval.js +10 -7
- package/automatization/workflows/flow.js +9 -10
- package/automatization/workflows/onboard.js +4 -5
- package/engine/agents/evaluations.js +18 -16
- package/engine/agents/learning-files.js +111 -0
- package/engine/agents/learning-sources.js +166 -0
- package/engine/agents/learning.js +93 -300
- package/engine/automation/config.js +175 -0
- package/engine/automation/hooks.js +96 -0
- package/engine/automation/index.js +17 -431
- package/engine/automation/roles.js +72 -0
- package/engine/automation/runners.js +162 -0
- package/engine/cli/catalog.js +7 -12
- package/engine/cli/instance.js +11 -10
- package/engine/cli/io.js +1 -1
- package/engine/cli/ops.js +12 -10
- package/engine/cli/wiring.js +2 -4
- package/engine/config/validate.js +2 -1
- package/engine/core/frontmatter.js +2 -1
- package/engine/core/ownership.js +5 -4
- package/engine/core/scan.js +3 -0
- package/engine/flows/registry.js +2 -2
- package/engine/hooks/files.js +160 -0
- package/engine/hooks/input.js +129 -0
- package/engine/hooks/run.js +24 -449
- package/engine/hooks/shell.js +197 -0
- package/engine/integrations/registry.js +2 -0
- package/engine/planning/contracts.js +12 -10
- package/engine/planning/parser.js +4 -3
- package/engine/planning/state.js +2 -1
- package/package.json +5 -5
- package/template/tools/ops.js +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// El modelo de un adaptador de runner: qué archivos trae, contra qué carpeta se resuelven y cómo se
|
|
4
|
+
// rellenan sus marcadores. Vive aparte de los comandos porque cambia por otra razón —cuando se agrega
|
|
5
|
+
// un runner o se mueve un marcador— y porque `check`, `doctor`, `install` y `uninstall` lo consumen
|
|
6
|
+
// los cuatro igual.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
const { spawnSync } = require('node:child_process')
|
|
11
|
+
const F = require('../core/files')
|
|
12
|
+
const O = require('../core/ownership')
|
|
13
|
+
|
|
14
|
+
const RUNNER_NAMES = ['claude', 'codex', 'gemini', 'antigravity']
|
|
15
|
+
|
|
16
|
+
// Adaptadores y workflows viven en el paquete, no en la instancia: son definiciones que el motor
|
|
17
|
+
// consume y que ninguna empresa edita —`RUNNER_NAMES` es cerrado, así que ni siquiera puede agregar
|
|
18
|
+
// uno propio—. Los hooks sí se quedan en el proyecto: la configuración del runner los nombra por
|
|
19
|
+
// ruta literal y no sabe resolver en cascada.
|
|
20
|
+
//
|
|
21
|
+
// Se busca por `runners/` y no por `automatization/`: toda instancia tiene el segundo —ahí viven sus
|
|
22
|
+
// hooks— y encontrarlo daría por buena una dependencia sin instalar.
|
|
23
|
+
function packagedAutomation(root) {
|
|
24
|
+
const runners = O.packagePath(root, path.join('automatization', 'runners'))
|
|
25
|
+
return runners ? path.dirname(runners) : ''
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function runnerManifest(root, name) {
|
|
29
|
+
if (!RUNNER_NAMES.includes(name)) {
|
|
30
|
+
throw new Error(`runner debe ser ${RUNNER_NAMES.join(', ')}`)
|
|
31
|
+
}
|
|
32
|
+
const packaged = packagedAutomation(root)
|
|
33
|
+
if (!packaged) {
|
|
34
|
+
throw new Error('no encuentro automatization/: corré "npm install" en la raíz del repo ops')
|
|
35
|
+
}
|
|
36
|
+
const file = path.join(packaged, 'runners', name, 'manifest.json')
|
|
37
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch (error) {
|
|
38
|
+
throw new Error(`${name}: manifest inválido (${error.message})`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Dónde abre el dev su herramienta, que no siempre es la raíz ops. En modo sidecar el repo ops es
|
|
43
|
+
// un hermano de los repos de producto: `<empresa>-ops/` coordina, `<empresa>/` es lo que se abre.
|
|
44
|
+
// Instalar dentro del sidecar dejaría al runner sin ver una sola línea de código.
|
|
45
|
+
function installRoot(root) {
|
|
46
|
+
try {
|
|
47
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
48
|
+
if (config.mode === 'sidecar') return path.resolve(root, '..')
|
|
49
|
+
} catch { /* sin configuración legible, instalar donde está */ }
|
|
50
|
+
return root
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cómo se nombra la raíz ops desde ahí: `<empresa>-ops/` en sidecar, vacío cuando coinciden.
|
|
54
|
+
function opsPrefix(root) {
|
|
55
|
+
const relative = path.relative(installRoot(root), root)
|
|
56
|
+
return relative ? `${relative.split(path.sep).join('/')}/` : ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runnerPaths(root, name, runner) {
|
|
60
|
+
const automationRoot = packagedAutomation(root)
|
|
61
|
+
const sourceDir = path.join(automationRoot, 'runners', name)
|
|
62
|
+
const install = installRoot(root)
|
|
63
|
+
const configSource = F.assertWithin(
|
|
64
|
+
sourceDir,
|
|
65
|
+
path.resolve(sourceDir, runner.config.source),
|
|
66
|
+
`${name}: config.source`,
|
|
67
|
+
)
|
|
68
|
+
const configTarget = F.assertWithin(
|
|
69
|
+
install,
|
|
70
|
+
path.resolve(install, runner.config.target),
|
|
71
|
+
`${name}: config.target`,
|
|
72
|
+
)
|
|
73
|
+
return { automationRoot, sourceDir, configSource, configTarget, install }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveItem(paths, root, name, item) {
|
|
77
|
+
return {
|
|
78
|
+
automationRoot: paths.automationRoot,
|
|
79
|
+
opsRoot: root,
|
|
80
|
+
source: F.assertWithin(
|
|
81
|
+
paths.automationRoot,
|
|
82
|
+
path.resolve(paths.sourceDir, item.source),
|
|
83
|
+
`${name}: source`,
|
|
84
|
+
),
|
|
85
|
+
target: F.assertWithin(
|
|
86
|
+
paths.install,
|
|
87
|
+
path.resolve(paths.install, item.target),
|
|
88
|
+
`${name}: target`,
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Todo lo que un adaptador copia —configuración, instrucciones, workflows— nombra rutas relativas a
|
|
94
|
+
// la carpeta donde se abre la herramienta. Cuando la raíz ops no es esa carpeta, cada una necesita el
|
|
95
|
+
// prefijo. El marcador es explícito en la fuente en vez de adivinarse con reemplazos de texto:
|
|
96
|
+
// `{{OPS_DIR}}` significa "acá va la raíz ops, o nada si coinciden".
|
|
97
|
+
//
|
|
98
|
+
// Un solo render, y `install` escribe exactamente lo que `doctor` compara.
|
|
99
|
+
const OPS_DIR = '{{OPS_DIR}}'
|
|
100
|
+
|
|
101
|
+
// Un fragmento que varios adaptadores comparten, resuelto contra la raíz de `automatization/`. El
|
|
102
|
+
// arranque es el mismo trabajo en tres formatos —la sección de un `AGENTS.md`, el cuerpo de un
|
|
103
|
+
// `SKILL.md`, el prompt de un `.toml`—, y escrito tres veces hizo lo que hace siempre una copia: dos
|
|
104
|
+
// de ellas anunciaban cinco puntos y enumeraban seis, con el sexto doblado dentro del quinto.
|
|
105
|
+
//
|
|
106
|
+
// El workflow de Claude queda afuera a propósito: es un programa con fases y esquemas, no una prosa
|
|
107
|
+
// enmarcada, así que su arranque no es una copia de éste sino otra cosa.
|
|
108
|
+
//
|
|
109
|
+
// Se resuelve antes que `{{OPS_DIR}}` para que el fragmento también reciba el prefijo, y no anida:
|
|
110
|
+
// lo incluido se copia tal cual.
|
|
111
|
+
const INCLUDE = /\{\{INCLUDE:([^}]+)\}\}/g
|
|
112
|
+
|
|
113
|
+
function inline(text, automationRoot) {
|
|
114
|
+
return text.replace(INCLUDE, (_, relative) => {
|
|
115
|
+
const shared = F.assertWithin(
|
|
116
|
+
automationRoot,
|
|
117
|
+
path.resolve(automationRoot, relative.trim()),
|
|
118
|
+
'INCLUDE',
|
|
119
|
+
)
|
|
120
|
+
return fs.readFileSync(shared, 'utf8').trimEnd()
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `{{OPS_ROOT}}` es la raíz absoluta. La necesita quien no puede deducirla de dónde lo ejecutaron
|
|
125
|
+
// —el puente de Antigravity—, y por eso no reemplaza a `{{OPS_DIR}}`: una ruta absoluta escrita en un
|
|
126
|
+
// archivo se rompe si el proyecto se mueve, así que la lleva sólo el que se queda sin alternativa.
|
|
127
|
+
const OPS_ROOT = '{{OPS_ROOT}}'
|
|
128
|
+
|
|
129
|
+
function render(file, prefix, automationRoot, opsRoot = '') {
|
|
130
|
+
return inline(fs.readFileSync(file, 'utf8'), automationRoot)
|
|
131
|
+
.split(OPS_ROOT).join(opsRoot)
|
|
132
|
+
.split(OPS_DIR).join(prefix)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runnerConfig(paths, root) {
|
|
136
|
+
return JSON.parse(render(paths.configSource, opsPrefix(root), paths.automationRoot, root))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Runners que además de los archivos necesitan un registro propio para que el wiring cuente. Copiar
|
|
140
|
+
// y quedarse ahí deja un plugin inerte: los archivos están, `doctor` da verde y nada se ejecuta.
|
|
141
|
+
function activated(runner) {
|
|
142
|
+
if (!runner.activation) return true
|
|
143
|
+
const result = spawnSync(runner.command, runner.activation.verify, { encoding: 'utf8' })
|
|
144
|
+
if (result.status !== 0) return null
|
|
145
|
+
return `${result.stdout || ''}`.includes(runner.activation.expect)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
activated,
|
|
150
|
+
RUNNER_NAMES,
|
|
151
|
+
OPS_DIR,
|
|
152
|
+
OPS_ROOT,
|
|
153
|
+
packagedAutomation,
|
|
154
|
+
runnerManifest,
|
|
155
|
+
installRoot,
|
|
156
|
+
opsPrefix,
|
|
157
|
+
runnerPaths,
|
|
158
|
+
resolveItem,
|
|
159
|
+
inline,
|
|
160
|
+
render,
|
|
161
|
+
runnerConfig,
|
|
162
|
+
}
|
package/engine/cli/catalog.js
CHANGED
|
@@ -14,8 +14,6 @@ const O = require('../core/ownership')
|
|
|
14
14
|
const IN = require('./instance')
|
|
15
15
|
const { fail, opsRoot } = require('./io')
|
|
16
16
|
|
|
17
|
-
// La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
|
|
18
|
-
|
|
19
17
|
function agentsFork(slug, dir) {
|
|
20
18
|
const root = opsRoot(dir)
|
|
21
19
|
if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', 2)
|
|
@@ -37,7 +35,7 @@ function agents(action, dir, extra, cli) {
|
|
|
37
35
|
if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
|
|
38
36
|
const root = opsRoot(dir)
|
|
39
37
|
// Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
|
|
40
|
-
// recorrer
|
|
38
|
+
// recorrer el catálogo entero para encontrar el suyo es ruido. `--own` hace ejecutable ese recorrido.
|
|
41
39
|
const own = cli.has('--own')
|
|
42
40
|
const system = cli.has('--system')
|
|
43
41
|
const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
|
|
@@ -53,7 +51,7 @@ function agents(action, dir, extra, cli) {
|
|
|
53
51
|
path: path.relative(root, role.dir).split(path.sep).join('/'),
|
|
54
52
|
}))))
|
|
55
53
|
}
|
|
56
|
-
// Una línea por cargo, alineadas, para elegir a quién asignarle una tarea sin abrir
|
|
54
|
+
// Una línea por cargo, alineadas, para elegir a quién asignarle una tarea sin abrir una carpeta.
|
|
57
55
|
const width = roles.reduce((max, role) => Math.max(max, role.slug.length), 0)
|
|
58
56
|
for (const role of roles) {
|
|
59
57
|
const mark = role.system ? '' : ' (propio)'
|
|
@@ -161,8 +159,7 @@ function learn(agent, cli) {
|
|
|
161
159
|
|
|
162
160
|
function evaluate(agent, caso, cli) {
|
|
163
161
|
const root = opsRoot()
|
|
164
|
-
// De quién son los casos
|
|
165
|
-
// llamarse igual sin colisionar, y deducirlo los volvería ambiguos el día que eso pase.
|
|
162
|
+
// De quién son los casos, resuelto por bandera y no por el slug. Por qué no se deduce, en `subject`.
|
|
166
163
|
const kind = cli.has('--flow') ? 'flow' : 'agent'
|
|
167
164
|
// El banco sólo tiene sentido acá: en una empresa el cargo que se evalúa es suyo —propio o
|
|
168
165
|
// adoptado— y su `planning/` ya es el lugar legítimo donde trabajar.
|
|
@@ -206,15 +203,13 @@ function evaluate(agent, caso, cli) {
|
|
|
206
203
|
for (const warning of [...result.warnings, ...runs.warnings]) console.warn(`⚠ ${warning}`)
|
|
207
204
|
for (const error of errors) console.error(`✗ ${error}`)
|
|
208
205
|
if (errors.length) fail(`\n${errors.length} error(es)`, 1)
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
// porque lo compuesto es tan viejo como su parte más rancia.
|
|
213
|
-
const cuando = runs.state && runs.state.oldest !== runs.state.newest
|
|
206
|
+
// Cuándo se midió es un rango cuando el veredicto vigente lo aportó más de una corrida. Por qué se
|
|
207
|
+
// compone en vez de leerse la última, en `composed`.
|
|
208
|
+
const measuredAt = runs.state && runs.state.oldest !== runs.state.newest
|
|
214
209
|
? `${runs.state.oldest}…${runs.state.newest}`
|
|
215
210
|
: (runs.state ? runs.state.newest : '')
|
|
216
211
|
const lastRun = runs.state
|
|
217
|
-
? `${runs.state.passed}/${runs.state.total} pasan (${
|
|
212
|
+
? `${runs.state.passed}/${runs.state.total} pasan (${measuredAt})`
|
|
218
213
|
: 'sin correr'
|
|
219
214
|
if (kind === 'flow') {
|
|
220
215
|
return console.log(`✓ ${agent}: ${runs.cases} caso(s) — ${lastRun}, ` +
|
package/engine/cli/instance.js
CHANGED
|
@@ -21,6 +21,9 @@ const { fail } = require('./io')
|
|
|
21
21
|
|
|
22
22
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
23
23
|
|
|
24
|
+
// Proveedores que el toolkit conoce, para saltearlos al copiar la plantilla: su andamiaje
|
|
25
|
+
// —configuración, staging/, proposed/— no se materializa hasta que alguien lo habilite. Antes cada
|
|
26
|
+
// instancia recibía el de un proveedor apagado que quizá no usaba nunca, y que nadie actualizaba.
|
|
24
27
|
function providerNames() {
|
|
25
28
|
try {
|
|
26
29
|
const file = path.join(PROJECT_ROOT, 'template', 'integrations', 'config.json')
|
|
@@ -34,7 +37,8 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
34
37
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
35
38
|
if (skip.includes(entry.name)) continue
|
|
36
39
|
const from = path.join(source, entry.name)
|
|
37
|
-
// npm no incluye un `.gitignore` dentro de un tarball
|
|
40
|
+
// npm no incluye un `.gitignore` dentro de un tarball —comprobado con `npm pack --dry-run` en npm
|
|
41
|
+
// 11.16.0: lo deja afuera en la raíz y en subdirectorios—, así que viaja sin punto y se restituye
|
|
38
42
|
// acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
|
|
39
43
|
const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
|
|
40
44
|
if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
|
|
@@ -99,9 +103,9 @@ function undeclareEngine(manifest) {
|
|
|
99
103
|
if (!('@ingeniomaps/cauce' in dev)) return []
|
|
100
104
|
delete dev['@ingeniomaps/cauce']
|
|
101
105
|
pkg.devDependencies = dev
|
|
102
|
-
const
|
|
106
|
+
const ours = !Object.keys(dev).length && !Object.keys(pkg.dependencies || {}).length
|
|
103
107
|
&& !Object.keys(pkg.scripts || {}).length && pkg.private === true && pkg.version === '0.0.0'
|
|
104
|
-
if (
|
|
108
|
+
if (ours) {
|
|
105
109
|
fs.rmSync(manifest, { force: true })
|
|
106
110
|
return ['package.json (lo había creado init: sin dependencias ni scripts propios)']
|
|
107
111
|
}
|
|
@@ -110,9 +114,6 @@ function undeclareEngine(manifest) {
|
|
|
110
114
|
return ['package.json: se quitó la dependencia del motor y el resto queda como estaba']
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
// Proveedores que el toolkit conoce, para saltearlos al copiar la plantilla: su andamiaje
|
|
114
|
-
// —configuración, staging/, proposed/— no se materializa hasta que alguien lo habilite. Antes cada
|
|
115
|
-
|
|
116
117
|
// El andamiaje de una instancia, sin leer argv. `init` es la cáscara que traduce banderas a esto, y
|
|
117
118
|
// el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
|
|
118
119
|
// cómo venga escrita la línea de comandos.
|
|
@@ -123,8 +124,8 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
123
124
|
'{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
|
|
124
125
|
}, force, providerNames(), quiet)
|
|
125
126
|
// No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
|
|
126
|
-
// el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando
|
|
127
|
-
//
|
|
127
|
+
// el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
|
|
128
|
+
// `.github/workflows/` vacío en cada instancia.
|
|
128
129
|
copyRuntime(
|
|
129
130
|
path.join(PROJECT_ROOT, 'automatization', 'hooks'),
|
|
130
131
|
path.join(root, 'automatization', 'hooks'),
|
|
@@ -173,7 +174,7 @@ function instanceVersion(root) {
|
|
|
173
174
|
// Lo cuenta el mismo parser que usan `check` y `tree`, no una expresión regular propia: los moldes traen
|
|
174
175
|
// ejemplos comentados, y contarlos a mano anunciaba una tarea en cola y otra terminada en una instancia
|
|
175
176
|
// recién creada. Un aviso que exagera lo que se pierde se deja de leer igual que uno que lo minimiza.
|
|
176
|
-
function
|
|
177
|
+
function whatIsLost(root) {
|
|
177
178
|
const planning = path.join(root, 'planning')
|
|
178
179
|
const queued = P.readBacklog(planning).reduce((total, hito) => total + (hito.tasks || []).length, 0)
|
|
179
180
|
const humanActions = ST.pendingHumanActions(planning).length
|
|
@@ -200,7 +201,7 @@ function destroy(dir, cli) {
|
|
|
200
201
|
}
|
|
201
202
|
if (O.mode(root) === 'toolkit') fail(`${root} es el toolkit: acá se fabrica Cauce, no se lo borra.`, 2)
|
|
202
203
|
|
|
203
|
-
const loss =
|
|
204
|
+
const loss = whatIsLost(root)
|
|
204
205
|
const lines = [
|
|
205
206
|
loss.epicas && `${loss.epicas} épica(s) en el roadmap`,
|
|
206
207
|
loss.enCola && `${loss.enCola} tarea(s) en la cola`,
|
package/engine/cli/io.js
CHANGED
|
@@ -4,12 +4,12 @@ const path = require('node:path')
|
|
|
4
4
|
|
|
5
5
|
// Terminar la corrida con un mensaje y un código. Vive aparte porque lo usa cada familia de comandos, y
|
|
6
6
|
// dejarlo en el despacho obligaría a que cada módulo dependa del que lo invoca.
|
|
7
|
-
|
|
8
7
|
function fail(message, code = 1) {
|
|
9
8
|
console.error(message)
|
|
10
9
|
process.exit(code)
|
|
11
10
|
}
|
|
12
11
|
|
|
12
|
+
// La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
|
|
13
13
|
// vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
|
|
14
14
|
function opsRoot(dir) {
|
|
15
15
|
return path.resolve(dir || process.env.OPS_ROOT || '.')
|
package/engine/cli/ops.js
CHANGED
|
@@ -32,15 +32,10 @@ const BOOT = require('./bootstrap')
|
|
|
32
32
|
// Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
|
|
33
33
|
const DEFAULT_TARGET = 'ops'
|
|
34
34
|
|
|
35
|
-
// Cuántos servicios se listan en pantalla antes de recortar. El resto sigue en `--json`, que es lo que
|
|
36
|
-
// consume el recorrido de arranque: recortar la lista es para leerla, no para acotar lo que se sabe.
|
|
37
|
-
|
|
38
35
|
// Dónde va la instancia cuando nadie eligió destino. Parada frecuente: el dev ya creó `acme-ops/` y
|
|
39
36
|
// corre `init` adentro. Sin esto la instancia caía en `acme-ops/ops/` —una carpeta del toolkit dentro
|
|
40
37
|
// de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
|
|
41
38
|
// instancia; no hay una segunda adentro.
|
|
42
|
-
// instancia recibía el de un proveedor apagado que quizá no usaba nunca, y que nadie actualizaba.
|
|
43
|
-
|
|
44
39
|
function implicitTarget(cwd) {
|
|
45
40
|
const base = path.basename(cwd)
|
|
46
41
|
return base === DEFAULT_TARGET || base.endsWith('-ops') ? '.' : DEFAULT_TARGET
|
|
@@ -55,10 +50,19 @@ function defaultName(root) {
|
|
|
55
50
|
|
|
56
51
|
// El motor no se instala solo: `npm install` baja el paquete que `init` acaba de declarar, y sin él el
|
|
57
52
|
// shim, los cargos, los equipos y los adaptadores no se resuelven. Correrlo desde acá es lo que hace que
|
|
58
|
-
// una instalación sea un comando y no una lista.
|
|
53
|
+
// una instalación sea un comando y no una lista.
|
|
54
|
+
//
|
|
55
|
+
// En Windows no alcanza con nombrarlo `npm.cmd`. Node documenta que un `.cmd` «no es ejecutable por sí
|
|
56
|
+
// solo» y nombra como salida lanzar cmd.exe con el comando de argumento —nodejs.org/api/child_process,
|
|
57
|
+
// consultado 2026-08-29—, que es lo que va acá. La otra salida obvia queda descartada: pasar `args`
|
|
58
|
+
// junto a la opción `shell` está deprecado en runtime (DEP0190, nodejs.org/api/deprecations).
|
|
59
|
+
//
|
|
60
|
+
// Documentado y no comprobado: acá no hay Windows, y ninguna prueba llega a esta línea porque el
|
|
61
|
+
// arranque recibe `npm` inyectado.
|
|
59
62
|
function npmInstall(root) {
|
|
60
|
-
const
|
|
61
|
-
|
|
63
|
+
const result = process.platform === 'win32'
|
|
64
|
+
? spawnSync('cmd.exe', ['/c', 'npm', 'install'], { cwd: root, stdio: 'inherit' })
|
|
65
|
+
: spawnSync('npm', ['install'], { cwd: root, stdio: 'inherit' })
|
|
62
66
|
if (result.error) {
|
|
63
67
|
console.error(` no pude ejecutar npm (${result.error.code || result.error.message}).`)
|
|
64
68
|
return 1
|
|
@@ -179,8 +183,6 @@ function usage() {
|
|
|
179
183
|
// molde y no del wiring del runner, así que pasarle el suyo instalaría a la fuerza algo que nadie pidió.
|
|
180
184
|
const NO_FLAGS = { has: () => false, value: (_flag, fallback = '') => fallback }
|
|
181
185
|
|
|
182
|
-
// Si el proveedor terminó su propia configuración. Son dos interruptores y `sync` exige los dos: el
|
|
183
|
-
|
|
184
186
|
async function run(cli) {
|
|
185
187
|
const [command] = cli.positional
|
|
186
188
|
if (!command || ['help', '--help', '-h'].includes(command)) return usage()
|
package/engine/cli/wiring.js
CHANGED
|
@@ -14,9 +14,10 @@ const OB = require('../core/onboarding')
|
|
|
14
14
|
const IN = require('./instance')
|
|
15
15
|
const { fail, opsRoot } = require('./io')
|
|
16
16
|
|
|
17
|
+
// Cuántos servicios se listan en pantalla antes de recortar. El resto sigue en `--json`, que es lo que
|
|
18
|
+
// consume el recorrido de arranque: recortar la lista es para leerla, no para acotar lo que se sabe.
|
|
17
19
|
const MAX_LISTED = 20
|
|
18
20
|
|
|
19
|
-
|
|
20
21
|
const INTEGRATION = {
|
|
21
22
|
list: {
|
|
22
23
|
run: (root) => {
|
|
@@ -111,9 +112,6 @@ const INTEGRATION = {
|
|
|
111
112
|
},
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
// Dónde puede mirar una instancia: exactamente las raíces que declara, y nada por encima de ellas. Sale
|
|
115
|
-
// de `ops.config.json` en vez de suponerse —el sidecar declara `..`, el embebido `.`— para que acotar las
|
|
116
|
-
// raíces acote también el escaneo, y para que nadie termine recorriendo la carpeta de al lado.
|
|
117
115
|
// Las tres reconciliaciones son el mismo comando con otra operación: se declaran en el mismo lugar
|
|
118
116
|
// para que agregar una cuarta no pida tocar el despachador.
|
|
119
117
|
for (const operation of ['reset', 'rebase', 'reconcile']) {
|
|
@@ -6,7 +6,8 @@ const MODES = ['embedded', 'sidecar', 'toolkit']
|
|
|
6
6
|
// quien actualiza merece saber qué hacer con la línea, no sólo que sobra.
|
|
7
7
|
//
|
|
8
8
|
// `planningDir` no configuraba, prometía: nadie lo honraba y la ubicación no es opinable —`findOpsRoot`
|
|
9
|
-
// reconoce una raíz ops
|
|
9
|
+
// reconoce una raíz ops por tener `ops.config.json` y `planning/` en ella, así que moverlo la
|
|
10
|
+
// desconocería—.
|
|
10
11
|
const RETIRED = {
|
|
11
12
|
planningDir: 'el motor siempre busca planning/ en la raíz del repositorio. Borrá la línea',
|
|
12
13
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// draft se leía distinto según quién lo abriera.
|
|
7
7
|
//
|
|
8
8
|
// Se resuelve por el lado estricto: frontmatter es lo que encabeza el documento, y un `---` en el medio
|
|
9
|
-
// es una línea horizontal de markdown. La clave admite
|
|
9
|
+
// es una línea horizontal de markdown. La clave admite letras, dígitos, `_` y `-` —de ahí sale la
|
|
10
|
+
// discrepancia del dígito inicial, resuelta a favor de admitirlo—, y no lo que admitiría YAML entero.
|
|
10
11
|
|
|
11
12
|
function frontmatter(text) {
|
|
12
13
|
const block = (String(text).match(/^---\s*\n([\s\S]*?)\n---/) || [])[1] || ''
|
package/engine/core/ownership.js
CHANGED
|
@@ -117,8 +117,8 @@ function mode(root) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// Dónde quedó el motor. El bridge de Antigravity
|
|
121
|
-
// poder cargar este módulo: si cambia acá, cambia
|
|
120
|
+
// Dónde quedó el motor. El bridge de Antigravity y `run-hook.sh` repiten esta cascada a mano porque
|
|
121
|
+
// corren antes de poder cargar este módulo: si cambia acá, cambia en los dos.
|
|
122
122
|
function engineAt(root, relative = '') {
|
|
123
123
|
return packagePath(root, relative ? path.join('engine', relative) : 'engine')
|
|
124
124
|
}
|
|
@@ -200,8 +200,9 @@ function retiredWithLearning(root) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// Rutas que `upgrade` reemplaza. Todo lo que no aparezca acá pertenece al proyecto.
|
|
203
|
-
//
|
|
204
|
-
// vez de esperar a que alguien lo cree a mano.
|
|
203
|
+
// Los archivos sueltos se listan aunque todavía no existan en la instancia, así que uno nuevo del
|
|
204
|
+
// sistema llega en vez de esperar a que alguien lo cree a mano. Una colección no: entra sólo si su
|
|
205
|
+
// `system/` ya está, porque el que se lista es el directorio y no cada archivo de adentro.
|
|
205
206
|
function systemPaths(root) {
|
|
206
207
|
const paths = [...SYSTEM_FILES]
|
|
207
208
|
for (const collection of SYSTEM_COLLECTIONS) {
|
package/engine/core/scan.js
CHANGED
|
@@ -171,6 +171,9 @@ function scan(root, skip = '') {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// Dónde puede mirar una instancia: exactamente las raíces que declara, y nada por encima de ellas. Sale
|
|
175
|
+
// de `ops.config.json` en vez de suponerse —el sidecar declara `..`, el embebido `.`— para que acotar las
|
|
176
|
+
// raíces acote también el escaneo, y para que nadie termine recorriendo la carpeta de al lado.
|
|
174
177
|
function workspaceRoots(root) {
|
|
175
178
|
try {
|
|
176
179
|
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
package/engine/flows/registry.js
CHANGED
|
@@ -101,8 +101,8 @@ function slugsIn(dir) {
|
|
|
101
101
|
} catch { return [] }
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// Los propios de la empresa y los que trae Cauce, con cada slug una sola vez aunque exista en los
|
|
105
|
+
// dos niveles: el del proyecto ya ganó.
|
|
106
106
|
function list(root) {
|
|
107
107
|
const system = systemTeams(root)
|
|
108
108
|
const slugs = new Set([
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Los guards que juzgan lo que está por escribirse: un secreto, un archivo generado, una migración,
|
|
4
|
+
// una prueba que se apaga, el motor de la dependencia. Todos leen el contenido entrante y no el disco
|
|
5
|
+
// —lo que ya estaba no lo escribió este cambio— y son el grupo `pre-files` del registro.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const { patchOf, filesOf, contentOf, cwdOf, block, configOf, findOpsRoot } = require('./input')
|
|
10
|
+
|
|
11
|
+
function secrets(input) {
|
|
12
|
+
for (const file of filesOf(input)) {
|
|
13
|
+
const base = path.basename(file)
|
|
14
|
+
if (/^(?:\.env|\.env\..+)$/.test(base) && !/\.(?:example|sample|template|schema|dist|tpl)$/.test(base)) {
|
|
15
|
+
block(`${file} parece contener secretos. Edita una plantilla o registra una acción humana.`)
|
|
16
|
+
}
|
|
17
|
+
if (/^(?:accesos\.md|credenciales.*|credentials.*\.json|.*service-account.*\.json|.*\.(?:pem|key))$/i.test(base)) {
|
|
18
|
+
block(`${file} parece un archivo de credenciales en texto plano.`)
|
|
19
|
+
}
|
|
20
|
+
// Nombres de credencial que la herramienta escribe sola y que la lista anterior no cubría:
|
|
21
|
+
// `.npmrc` guarda el token de publicación, `.netrc` el de cualquier host, `id_rsa` y sus tres
|
|
22
|
+
// hermanas una clave privada de SSH, y `credentials` las de AWS. Los cuatro son estándar, no
|
|
23
|
+
// exóticos — y las claves SSH van por nombre de algoritmo, no por prefijo.
|
|
24
|
+
//
|
|
25
|
+
// Esto tapa un caso conocido; no vuelve completo al guard. La forma de decidir sigue siendo el
|
|
26
|
+
// nombre del archivo, así que otro formato pasa igual — ver «Qué son y qué no son» en el README.
|
|
27
|
+
if (/^(?:\.npmrc|\.netrc|_netrc|\.pypirc|\.dockercfg|id_(?:rsa|dsa|ecdsa|ed25519)|credentials)$/i.test(base)) {
|
|
28
|
+
block(`${file} es un archivo de credenciales que su herramienta mantiene. No lo edites a mano.`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function integrationSnapshot(input) {
|
|
34
|
+
for (const raw of filesOf(input)) {
|
|
35
|
+
const file = raw.replace(/\\/g, '/')
|
|
36
|
+
if (/(?:^|\/)integrations\/[^/]+\/staging\/(?:.+\/remote\.json|sync-state\.json)$/.test(file)) {
|
|
37
|
+
block(`${file} pertenece al sincronizador. Cura draft.md; no edites snapshots a mano.`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function generated(input) {
|
|
43
|
+
for (const raw of filesOf(input)) {
|
|
44
|
+
const file = raw.replace(/\\/g, '/')
|
|
45
|
+
const base = path.basename(file)
|
|
46
|
+
if (/(?:^|[._-])generated\.[^.]+$/i.test(base) || /(?:^|[._-])gen\.(?:go|ts|js|py)$/i.test(base)) {
|
|
47
|
+
block(`${file} parece código generado. Modifica su fuente y ejecuta el generador; no lo edites a mano.`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Las dos formas de que una prueba deje de juzgar sin que nadie lo note: apagarla o borrarla. Ninguna
|
|
53
|
+
// sale roja —el runner informa una suite verde más corta—, así que el verde pasa de decir «el
|
|
54
|
+
// comportamiento está» a decir «nadie lo miró», y `verify` tampoco lo ve porque también lee exit codes.
|
|
55
|
+
// Lo que se inspecciona es el contenido entrante, no el archivo: una marca que ya estaba no la apagó
|
|
56
|
+
// este cambio.
|
|
57
|
+
const TEST_OFF = [
|
|
58
|
+
[/\b(?:describe|context|it|test|suite)\s*\.\s*(?:skip|only|todo)\b/, 'skip/only'],
|
|
59
|
+
[/\b[xf](?:it|test|describe|context)\s*[("'`]/, 'xit/fit'],
|
|
60
|
+
[/\bt\.Skip(?:Now)?\s*\(/, 't.Skip'],
|
|
61
|
+
[/@pytest\.mark\.(?:skip|skipif|xfail)\b/, 'pytest.mark.skip'],
|
|
62
|
+
[/@unittest\.skip/, 'unittest.skip'],
|
|
63
|
+
[/@(?:Ignore|Disabled)\b/, 'Ignore/Disabled'],
|
|
64
|
+
[/#\[ignore\]/, 'ignore'],
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
function isTestFile(raw) {
|
|
68
|
+
const file = raw.replace(/\\/g, '/')
|
|
69
|
+
const base = path.basename(file)
|
|
70
|
+
return /(?:^|\/)(?:tests?|specs?|__tests__)\//i.test(file)
|
|
71
|
+
|| /\.(?:test|spec)\.[jt]sx?$/i.test(base)
|
|
72
|
+
|| /_(?:test|spec)\.(?:go|py|rb|ts|js|jsx|tsx|rs|exs?)$/i.test(base)
|
|
73
|
+
|| /^test_.+\.py$/i.test(base)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function testEvidence(input) {
|
|
77
|
+
if (process.env.OPS_TEST_EVIDENCE_OVERRIDE === '1') return
|
|
78
|
+
const why = 'Una prueba apagada no falla y una suite sin ella sale verde igual: el verde deja de ' +
|
|
79
|
+
'decir que el comportamiento está y pasa a decir que nadie lo miró.\n' +
|
|
80
|
+
'Si la aserción está mal, corregila; si el comportamiento cambió, cambialo junto con la prueba que ' +
|
|
81
|
+
'lo fija. Si tiene que quedar afuera igual —flake conocido, entorno que acá no existe—, es una ' +
|
|
82
|
+
'decisión con dueño: OPS_TEST_EVIDENCE_OVERRIDE=1 y que conste en el commit.'
|
|
83
|
+
for (const match of patchOf(input).matchAll(/^\*\*\* Delete File:\s*(.+)$/gm)) {
|
|
84
|
+
const removed = match[1].trim()
|
|
85
|
+
if (isTestFile(removed)) block(`${removed} borra una prueba.\n${why}`)
|
|
86
|
+
}
|
|
87
|
+
const content = contentOf(input)
|
|
88
|
+
if (!content) return
|
|
89
|
+
for (const raw of filesOf(input)) {
|
|
90
|
+
if (!isTestFile(raw)) continue
|
|
91
|
+
for (const [marca, nombre] of TEST_OFF) {
|
|
92
|
+
if (marca.test(content)) block(`${raw} apaga una prueba con ${nombre}.\n${why}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function workspaceBoundary(input) {
|
|
98
|
+
const root = findOpsRoot(process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || cwdOf(input))
|
|
99
|
+
if (!root) return
|
|
100
|
+
const config = configOf(root)
|
|
101
|
+
const allowed = [root, ...(config.workspaceRoots || []).map((entry) => path.resolve(root, entry.path))]
|
|
102
|
+
for (const raw of filesOf(input)) {
|
|
103
|
+
const file = path.resolve(cwdOf(input), raw)
|
|
104
|
+
if (!allowed.some((base) => file === base || file.startsWith(`${base}${path.sep}`))) {
|
|
105
|
+
block(`${file} está fuera de las raíces declaradas en ops.config.json.`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function migrations(input) {
|
|
111
|
+
if (process.env.OPS_MIGRATIONS_OVERRIDE === '1') return
|
|
112
|
+
// Cada rama cierra su propio límite. Cuando el `\b` estaba al final del grupo se aplicaba a las tres, y
|
|
113
|
+
// la de `delete` termina a propósito en `;`: después de un punto y coma no hay límite de palabra, así que
|
|
114
|
+
// `DELETE FROM pedidos;` —la forma que tiene en cualquier migración— pasaba y sólo frenaba la variante sin
|
|
115
|
+
// punto y coma. `drop column` y `drop constraint` faltaban: pierden datos y garantías igual que `drop table`.
|
|
116
|
+
const destructiveSql = new RegExp(
|
|
117
|
+
String.raw`\bdrop\s+(?:table|database|schema|column|constraint)\b` +
|
|
118
|
+
String.raw`|\btruncate\b` +
|
|
119
|
+
String.raw`|\bdelete\s+from\s+\S+\s*(?:;|$)`,
|
|
120
|
+
'i',
|
|
121
|
+
)
|
|
122
|
+
if (destructiveSql.test(contentOf(input))) {
|
|
123
|
+
block('La migración contiene SQL destructivo. Requiere revisión y OPS_MIGRATIONS_OVERRIDE=1.')
|
|
124
|
+
}
|
|
125
|
+
for (const raw of filesOf(input)) {
|
|
126
|
+
const normalized = raw.replace(/\\/g, '/')
|
|
127
|
+
if (!/(?:^|\/)(?:migrations?|migrate)\/.*\.sql$/i.test(normalized)) continue
|
|
128
|
+
const file = path.resolve(cwdOf(input), raw)
|
|
129
|
+
if (fs.existsSync(file)) {
|
|
130
|
+
block(`${raw} es una migración existente. Crea una nueva en vez de reescribir historial.`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Hace falta un guard aparte porque `workspace-boundary` no lo cubre: `node_modules/` cae dentro de
|
|
136
|
+
// la raíz declarada, así que editar el motor le parece legítimo.
|
|
137
|
+
//
|
|
138
|
+
// Editarlo rompe dos veces: el próximo `npm install` borra el cambio sin avisar, y hasta entonces la
|
|
139
|
+
// empresa corre un motor que no coincide con la versión que declara —la clase de diferencia que
|
|
140
|
+
// aparece como un bug irreproducible—. En modo `toolkit` no aplica: ahí el motor es el producto.
|
|
141
|
+
function engineWrites(input) {
|
|
142
|
+
const root = findOpsRoot(process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || cwdOf(input))
|
|
143
|
+
if (!root) return
|
|
144
|
+
const config = configOf(root)
|
|
145
|
+
if (config.mode === 'toolkit') return
|
|
146
|
+
const pkg = path.join(root, 'node_modules', '@ingeniomaps', 'cauce')
|
|
147
|
+
for (const raw of filesOf(input)) {
|
|
148
|
+
const file = path.resolve(cwdOf(input), raw)
|
|
149
|
+
if (file !== pkg && !file.startsWith(`${pkg}${path.sep}`)) continue
|
|
150
|
+
block(`${raw} pertenece al motor de Cauce, que llega por npm.\n` +
|
|
151
|
+
'Un cambio acá lo borra el próximo install y mientras tanto corrés un motor que no coincide ' +
|
|
152
|
+
'con la versión que declarás. Para traer una versión nueva son dos pasos —el motor y después ' +
|
|
153
|
+
'las rutas del sistema de tu instancia—:\n' +
|
|
154
|
+
' npm install --save-dev --save-exact @ingeniomaps/cauce@latest\n' +
|
|
155
|
+
' node tools/ops.js upgrade\n' +
|
|
156
|
+
'Y reportá el problema arriba. Lo que sí es tuyo son tus cargos, equipos e integraciones.')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { secrets, integrationSnapshot, generated, testEvidence, workspaceBoundary, migrations, engineWrites }
|