@ingeniomaps/cauce 0.95.0 → 0.97.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.
- package/CHANGELOG.md +150 -0
- package/automatization/workflows/autobuild.js +168 -18
- package/automatization/workflows/flow.js +30 -14
- package/engine/automation/check.js +6 -0
- package/engine/cli/archive.js +3 -3
- package/engine/cli/bench.js +6 -6
- package/engine/cli/catalog.js +34 -15
- package/engine/cli/claims.js +12 -12
- package/engine/cli/contract.js +5 -4
- package/engine/cli/dependency.js +2 -2
- package/engine/cli/instance.js +7 -6
- package/engine/cli/io.js +26 -4
- package/engine/cli/ops.js +18 -13
- package/engine/cli/planning.js +8 -5
- package/engine/cli/validate.js +2 -2
- package/engine/cli/wiring.js +16 -16
- package/engine/cli/worktree.js +8 -8
- package/engine/core/ownership.js +7 -0
- package/engine/hooks/shell.js +29 -6
- package/engine/planning/contracts.js +19 -0
- package/engine/planning/parser.js +35 -0
- package/package.json +1 -1
- package/template/planning/rules/system/commits.md +9 -2
package/engine/cli/wiring.js
CHANGED
|
@@ -14,7 +14,7 @@ const SC = require('../core/scan')
|
|
|
14
14
|
const OB = require('../core/onboarding')
|
|
15
15
|
const IN = require('./instance')
|
|
16
16
|
const ST = require('../planning/state')
|
|
17
|
-
const { fail, opsRoot } = require('./io')
|
|
17
|
+
const { fail, opsRoot, USAGE, REFUSED } = require('./io')
|
|
18
18
|
|
|
19
19
|
// Cuántos servicios se listan en pantalla antes de recortar. El resto sigue en `--json`, que es lo que
|
|
20
20
|
// consume el recorrido de arranque: recortar la lista es para leerla, no para acotar lo que se sabe.
|
|
@@ -45,7 +45,7 @@ const INTEGRATION = {
|
|
|
45
45
|
&& fs.existsSync(path.join(root, 'integrations', provider))
|
|
46
46
|
if (!fromCauce && !own) {
|
|
47
47
|
fail(`Cauce no trae un adaptador para ${provider}. Uno propio vive en integrations/${provider}/ y se `
|
|
48
|
-
+ `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`,
|
|
48
|
+
+ `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`, USAGE)
|
|
49
49
|
}
|
|
50
50
|
// Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
|
|
51
51
|
// trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
|
|
@@ -86,7 +86,7 @@ const INTEGRATION = {
|
|
|
86
86
|
const result = I.validate(root, provider || '')
|
|
87
87
|
for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
|
|
88
88
|
for (const error of result.errors) console.error(`✗ ${error}`)
|
|
89
|
-
if (result.errors.length) fail(`${result.errors.length} error(es) de integración
|
|
89
|
+
if (result.errors.length) fail(`${result.errors.length} error(es) de integración`, REFUSED)
|
|
90
90
|
console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
|
|
91
91
|
},
|
|
92
92
|
},
|
|
@@ -203,14 +203,14 @@ function onboard(rootArg, cli, runner = '') {
|
|
|
203
203
|
function providerRegistry(root) {
|
|
204
204
|
const file = path.join(root, 'integrations', 'config.json')
|
|
205
205
|
try { return { file, config: JSON.parse(fs.readFileSync(file, 'utf8')) } } catch (error) {
|
|
206
|
-
return fail(`integrations/config.json ilegible: ${error.message}
|
|
206
|
+
return fail(`integrations/config.json ilegible: ${error.message}`, USAGE)
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function switchProvider(root, provider, enabled) {
|
|
211
211
|
const { file, config } = providerRegistry(root)
|
|
212
212
|
if (!config.providers || !config.providers[provider]) {
|
|
213
|
-
fail(`${provider} no está en integrations/config.json
|
|
213
|
+
fail(`${provider} no está en integrations/config.json.`, USAGE)
|
|
214
214
|
}
|
|
215
215
|
config.providers[provider].enabled = enabled
|
|
216
216
|
F.atomicWriteJson(file, config)
|
|
@@ -218,8 +218,8 @@ function switchProvider(root, provider, enabled) {
|
|
|
218
218
|
|
|
219
219
|
async function integration(action, rootArg, provider, key, cli) {
|
|
220
220
|
const step = INTEGRATION[action]
|
|
221
|
-
if (!step) fail(`Acción de integración desconocida: ${action || '(vacía)'}`,
|
|
222
|
-
if (step.missing && (!provider || (step.needsKey && !key))) fail(step.missing,
|
|
221
|
+
if (!step) fail(`Acción de integración desconocida: ${action || '(vacía)'}`, USAGE)
|
|
222
|
+
if (step.missing && (!provider || (step.needsKey && !key))) fail(step.missing, USAGE)
|
|
223
223
|
await step.run(path.resolve(rootArg || '.'), provider, key, cli)
|
|
224
224
|
}
|
|
225
225
|
|
|
@@ -245,7 +245,7 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
245
245
|
if (action === 'check') {
|
|
246
246
|
const errors = A.check(root)
|
|
247
247
|
for (const error of errors) console.error(`✗ ${error}`)
|
|
248
|
-
if (errors.length) fail(`${errors.length} error(es) de automatización
|
|
248
|
+
if (errors.length) fail(`${errors.length} error(es) de automatización`, REFUSED)
|
|
249
249
|
console.log(
|
|
250
250
|
`✓ automatización válida: ${A.GUARD_NAMES.length} guards, ${A.RUNNER_NAMES.length} adaptadores`,
|
|
251
251
|
)
|
|
@@ -260,22 +260,22 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
260
260
|
}
|
|
261
261
|
if (action === 'doctor') {
|
|
262
262
|
let result
|
|
263
|
-
try { result = A.doctor(root, runnerName) } catch (error) { fail(error.message,
|
|
263
|
+
try { result = A.doctor(root, runnerName) } catch (error) { fail(error.message, USAGE) }
|
|
264
264
|
if (result.errors.length) {
|
|
265
|
-
fail(`${runnerName}: ${result.errors.length} error(es), ${result.warnings.length} advertencia(s)
|
|
265
|
+
fail(`${runnerName}: ${result.errors.length} error(es), ${result.warnings.length} advertencia(s)`, REFUSED)
|
|
266
266
|
}
|
|
267
267
|
console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
|
|
268
268
|
return
|
|
269
269
|
}
|
|
270
270
|
if (action === 'uninstall') {
|
|
271
|
-
try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message,
|
|
271
|
+
try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message, USAGE) }
|
|
272
272
|
console.log(' la instancia sigue en pie: borrar la carpeta ops es una decisión aparte.')
|
|
273
273
|
return
|
|
274
274
|
}
|
|
275
275
|
if (action === 'install') {
|
|
276
276
|
let runner
|
|
277
277
|
const force = cli.has('--force')
|
|
278
|
-
try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message,
|
|
278
|
+
try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, USAGE) }
|
|
279
279
|
if (runnerName === 'codex') {
|
|
280
280
|
console.log(' Codex deja los hooks nuevos sin correr hasta que los confíes: abrí una sesión')
|
|
281
281
|
console.log(' y usá /hooks para revisarlos y marcarlos como confiables.')
|
|
@@ -284,20 +284,20 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
284
284
|
console.log(` ${runnerName} no expone hooks nativos; aplica guards como prechecks.`)
|
|
285
285
|
}
|
|
286
286
|
const result = A.doctor(root, runnerName)
|
|
287
|
-
if (result.errors.length) fail(`${runnerName}: instalación incompleta
|
|
287
|
+
if (result.errors.length) fail(`${runnerName}: instalación incompleta`, REFUSED)
|
|
288
288
|
console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
|
|
289
289
|
return
|
|
290
290
|
}
|
|
291
|
-
fail(`Acción de automatización desconocida: ${action || '(vacía)'}`,
|
|
291
|
+
fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, USAGE)
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
function secrets(action, rootArg) {
|
|
295
|
-
if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`,
|
|
295
|
+
if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`, USAGE)
|
|
296
296
|
const result = SE.check(opsRoot(rootArg))
|
|
297
297
|
if (!result.declared) return console.log(`Sin ${SE.DECLARATION}: no hay contrato de secretos que comprobar.`)
|
|
298
298
|
for (const warning of result.warnings) console.log(`⚠ ${warning}`)
|
|
299
299
|
for (const error of result.errors) console.error(`✗ ${error}`)
|
|
300
|
-
if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos
|
|
300
|
+
if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos`, REFUSED)
|
|
301
301
|
console.log(`✓ contrato de secretos: ${result.current} servicio(s) al día`)
|
|
302
302
|
}
|
|
303
303
|
|
package/engine/cli/worktree.js
CHANGED
|
@@ -15,7 +15,7 @@ const ST = require('../planning/state')
|
|
|
15
15
|
const CL = require('../planning/claims')
|
|
16
16
|
const R = require('../core/repos')
|
|
17
17
|
const O = require('../core/ownership')
|
|
18
|
-
const { fail, planningRoot } = require('./io')
|
|
18
|
+
const { fail, planningRoot, USAGE, REFUSED } = require('./io')
|
|
19
19
|
|
|
20
20
|
const git = (cwd, ...args) => spawnSync('git', args, { cwd, encoding: 'utf8' })
|
|
21
21
|
|
|
@@ -34,38 +34,38 @@ function existing(repo, branch) {
|
|
|
34
34
|
|
|
35
35
|
function worktree(dir, slug, cli) {
|
|
36
36
|
const root = planningRoot(dir)
|
|
37
|
-
if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`',
|
|
37
|
+
if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`', USAGE)
|
|
38
38
|
const state = ST.snapshot(root)
|
|
39
39
|
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
40
|
-
if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`,
|
|
40
|
+
if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`, USAGE)
|
|
41
41
|
|
|
42
42
|
// No reserva —eso es `claim`— pero se niega a montar sobre lo de otro: preparar un árbol para una
|
|
43
43
|
// tarea ajena es trabajo que se va a tirar, y el aviso cuesta menos que descubrirlo después.
|
|
44
44
|
const taken = state.claims.find((one) => one.slug === slug)
|
|
45
45
|
if (taken && taken.runner !== CL.runner()) {
|
|
46
|
-
return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie
|
|
46
|
+
return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie.`, REFUSED)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const candidatos = R.reposFor(path.join(root, '..'), task.service)
|
|
50
50
|
if (candidatos.length > 1) {
|
|
51
51
|
return fail(`${task.service || '.'} existe en más de un repositorio (${candidatos.join(', ')}), así que `
|
|
52
|
-
+ 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.',
|
|
52
|
+
+ 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.', USAGE)
|
|
53
53
|
}
|
|
54
54
|
const repo = candidatos[0]
|
|
55
55
|
if (!repo) {
|
|
56
56
|
return fail(`no encontré el repositorio de ${task.service || '(sin service)'}: revisá workspaceRoots `
|
|
57
|
-
+ 'en ops.config.json y que la ruta del servicio exista.',
|
|
57
|
+
+ 'en ops.config.json y que la ruta del servicio exista.', USAGE)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const branch = CL.branchOf(slug)
|
|
61
61
|
const already = existing(repo, branch)
|
|
62
62
|
const target = already || path.join(path.dirname(repo), `${path.basename(repo)}-${slug}`)
|
|
63
63
|
if (!already) {
|
|
64
|
-
if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama
|
|
64
|
+
if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama.`, REFUSED)
|
|
65
65
|
const hasBranch = git(repo, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`).status === 0
|
|
66
66
|
const args = hasBranch ? ['worktree', 'add', target, branch] : ['worktree', 'add', '-b', branch, target]
|
|
67
67
|
const added = git(repo, ...args)
|
|
68
|
-
if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}
|
|
68
|
+
if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}`, REFUSED)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (cli.has('--json')) {
|
package/engine/core/ownership.js
CHANGED
|
@@ -164,9 +164,16 @@ function engineAt(root, relative = '') {
|
|
|
164
164
|
// de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
|
|
165
165
|
// proyecto deba crear. Una sola implementación para las dos, o divergen.
|
|
166
166
|
function packageDir(root, name) {
|
|
167
|
+
// La raíz declarada va también acá, y no sólo en `packagePath`. Las dos contestan dónde está el paquete
|
|
168
|
+
// sobre la misma instancia y desde el 158 contestaban distinto: en el layout documentado —`npm install`
|
|
169
|
+
// en la carpeta de la empresa y la instancia adentro— el motor se resolvía y el catálogo no, así que
|
|
170
|
+
// `check` pasaba en verde y `agents list` devolvía una lista vacía con exit 0, indistinguible de «esta
|
|
171
|
+
// instancia no tiene cargos». `evaluate` era peor: culpaba a un `SKILL.md` que falta (caso 171).
|
|
172
|
+
const above = declaredRoot(root)
|
|
167
173
|
const candidates = [
|
|
168
174
|
path.join(root, 'node_modules', '@ingeniomaps', 'cauce', name),
|
|
169
175
|
path.join(root, name),
|
|
176
|
+
...(above ? [path.join(above, 'node_modules', '@ingeniomaps', 'cauce', name)] : []),
|
|
170
177
|
]
|
|
171
178
|
return candidates.find((dir) => fs.existsSync(path.join(dir, 'system'))
|
|
172
179
|
|| fs.existsSync(path.join(dir, 'roles', 'system'))) || ''
|
package/engine/hooks/shell.js
CHANGED
|
@@ -45,6 +45,20 @@ const COMANDO = String.raw`$|[;&|)'"\`]`
|
|
|
45
45
|
// de frenar trabajo legítimo nombraba una violación que no estaba.
|
|
46
46
|
const MISMO = String.raw`[^;&|\n]`
|
|
47
47
|
|
|
48
|
+
// Si algún remoto alcanza al commit que está en HEAD. Lectura local e inocua: `branch -r` mira lo que ya
|
|
49
|
+
// está en `refs/remotes/`, no habla con ningún servidor. Un `fetch` que nadie corrió deja la respuesta
|
|
50
|
+
// desactualizada hacia el lado seguro — cree que no está publicado algo que sí lo está—, y por eso esto
|
|
51
|
+
// acota un bloqueo y no autoriza nada: lo que decide publicar sigue siendo `push`, que mira otra cosa.
|
|
52
|
+
//
|
|
53
|
+
// Sin git, sin repositorio o con el comando fallando, la respuesta es **sí**: cerrado por defecto (R27).
|
|
54
|
+
// No saber si algo se publicó no es lo mismo que saber que no.
|
|
55
|
+
function publishedHead(input) {
|
|
56
|
+
const cwd = (input && input.cwd) || process.cwd()
|
|
57
|
+
const result = spawnSync('git', ['branch', '-r', '--contains', 'HEAD'], { cwd, encoding: 'utf8' })
|
|
58
|
+
if (result.status !== 0) return true
|
|
59
|
+
return Boolean(result.stdout.trim())
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
// Un mensaje de commit es dato, no código. `git commit -m "fix: bloquear git push --force"` disparaba
|
|
49
63
|
// el guard de publicación, y lo mismo `rm -rf /` nombrado en una explicación; con el heredoc que se usa
|
|
50
64
|
// para un mensaje largo, el cuerpo entero entra en el comando, así que la línea que arregla esto no se
|
|
@@ -80,13 +94,22 @@ function destructive(input) {
|
|
|
80
94
|
publish(input, command)
|
|
81
95
|
const rules = [
|
|
82
96
|
[/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales.", true],
|
|
83
|
-
// R8
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
97
|
+
// Lo que R8 protege es la historia que **otro ya leyó**, y el motor ya hace esa distinción para el
|
|
98
|
+
// push: publicar se autoriza, reescribir lo publicado no. Acá se hace la misma, mirando si algún
|
|
99
|
+
// remoto alcanza a HEAD.
|
|
100
|
+
//
|
|
101
|
+
// Antes se bloqueaba siempre, «por política y no por daño», y eso no impedía el resultado sino el
|
|
102
|
+
// comando que lo nombra: `git reset --soft HEAD~1` y volver a commitear produce exactamente lo mismo,
|
|
103
|
+
// no lo frena ningún guard y no deja constancia de nada. Una regla que se cumple mejor esquivándola se
|
|
104
|
+
// termina esquivando siempre (caso 178).
|
|
105
|
+
//
|
|
106
|
+
// Y no lleva salida por chat ni publicado ni sin publicar: sin publicar no la necesita, y publicado es
|
|
107
|
+
// la misma reescritura que el force-push, que tampoco la tiene.
|
|
108
|
+
...(publishedHead(input) ? [[
|
|
87
109
|
new RegExp(String.raw`\bgit\s+commit\b${MISMO}*\s--amend\b`),
|
|
88
|
-
"'git commit --amend'
|
|
89
|
-
|
|
110
|
+
"'git commit --amend' sobre un commit ya publicado reescribe historia que otro leyó. R8 lo "
|
|
111
|
+
+ 'prohíbe: hacé otro commit encima.',
|
|
112
|
+
]] : []),
|
|
90
113
|
[/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento.", true],
|
|
91
114
|
// `git checkout -- .` destruye lo mismo que `reset --hard` y sin recuperación, pero se escribe como
|
|
92
115
|
// una limpieza. Se bloquea sólo la forma ancha —`.`, `*`, `:/`, o sin ruta—: revertir un archivo
|
|
@@ -383,6 +383,25 @@ function validateState({
|
|
|
383
383
|
if (!row.valid) {
|
|
384
384
|
errors.push(`HUMAN_ACTIONS ${row.task}: estado "${row.state}" fuera de `
|
|
385
385
|
+ `${P.HUMAN_ACTION_STATES.join(' | ')}; mientras no se entienda, la tarea queda bloqueada`)
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
388
|
+
// La primera columna es a la vez lo que una persona lee y la clave con la que el motor bloquea:
|
|
389
|
+
// la selección de tarea (`state.js`) arma su conjunto de bloqueadas con ella tal cual. Esa doble
|
|
390
|
+
// función es libre a propósito —el molde manda
|
|
391
|
+
// nombrar la épica o el recorrido cuando la tarea todavía no existe— así que lo que no se puede
|
|
392
|
+
// recortar es la libertad, y lo que sí se puede es la forma intermedia: la celda que **menciona** una
|
|
393
|
+
// tarea de la cola sin ser su slug.
|
|
394
|
+
//
|
|
395
|
+
// Es la peor de las tres porque promete un bloqueo que no ocurre, y nada lo dice: la fila se escribe
|
|
396
|
+
// sin error, sale en `ops context` bajo HUMAN como si estuviera registrada, y la tarea se sigue
|
|
397
|
+
// ofreciendo. `**slug: de qué se trata**` es la que sale natural, porque esta tabla la lee una
|
|
398
|
+
// persona. Sin esto la ausencia no deja rastro, que es la forma de R15 aplicada a un mecanismo.
|
|
399
|
+
if (backlogSlugs.has(row.task)) continue
|
|
400
|
+
const casi = [...backlogSlugs].find((slug) => new RegExp(`\\b${slug}\\b`).test(row.task))
|
|
401
|
+
if (casi) {
|
|
402
|
+
errors.push(`HUMAN_ACTIONS: la fila "${row.task}" nombra a ${casi} y no bloquea nada, porque el `
|
|
403
|
+
+ `motor bloquea por la primera columna exacta. Dejá "${casi}" sola ahí y contá el resto en la `
|
|
404
|
+
+ 'acción, o nombrá la épica o el recorrido si lo que se frena no es esa tarea')
|
|
386
405
|
}
|
|
387
406
|
}
|
|
388
407
|
|
|
@@ -176,6 +176,10 @@ function readCast(rest) {
|
|
|
176
176
|
// devolvía `MAX` con `check` en verde, que es la forma cara del error —la tarea se lee completa y no
|
|
177
177
|
// lo está—. Cierra el `_` que markdown cerraría: el que no está entre caracteres de palabra.
|
|
178
178
|
const ACCEPTANCE = /_Aceptaci[oó]n:\s*(.*?\S)_(?![A-Za-z0-9])/i
|
|
179
|
+
// Los paréntesis del contrato de una línea de tarea. Se enumeran por su clave y no como «cualquier
|
|
180
|
+
// paréntesis» para no llevarse puesta una aclaración de la prosa: la descripción usa paréntesis igual
|
|
181
|
+
// que cualquier texto, y el criterio `(→ C1)` va acá porque `criteria` ya lo extrae.
|
|
182
|
+
const MARKERS = /\((?:→|->|criterios?\s|epic:|service:|cast:|depende:|sin partir:)[^)]*\)/gi
|
|
179
183
|
|
|
180
184
|
// Cuántas condiciones tiene una aceptación escrita en prosa. Estuvo mucho tiempo sin contarse con una
|
|
181
185
|
// razón buena —contar condiciones en una frase es una lectura, y un número inventado es peor que
|
|
@@ -207,6 +211,19 @@ function taskFromLine(line) {
|
|
|
207
211
|
epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
|
|
208
212
|
service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
|
|
209
213
|
acceptance,
|
|
214
|
+
// Lo que la aceptación no puede decir y alguien ya decidió: dónde vive un símbolo, qué queda fuera de
|
|
215
|
+
// alcance, con qué se produce la evidencia. Por contrato la aceptación describe estado observable del
|
|
216
|
+
// producto, así que una decisión de diseño no cabe ahí — y hasta acá tampoco salía del BACKLOG: el
|
|
217
|
+
// texto vivía en `rest` y se descartaba.
|
|
218
|
+
//
|
|
219
|
+
// Sin ella el que planifica vuelve a decidir lo ya decidido, y decide distinto. La crítica **sí** abre
|
|
220
|
+
// el BACKLOG y bloquea el plan citando la línea palabra por palabra: una compuerta juzga contra un
|
|
221
|
+
// texto que la otra no recibió. Dos corridas reales se pagaron enteras para descubrirlo, 1,10 M y
|
|
222
|
+
// 815 k tokens, la segunda con sus cuatro objeciones diciendo lo mismo (caso 177).
|
|
223
|
+
//
|
|
224
|
+
// Se recorta la aceptación y los marcadores del contrato porque cada uno ya tiene su campo: repetirlos
|
|
225
|
+
// acá los pone dos veces en el prompt de cada fase, que es lo que R16 cobra una vez por etapa.
|
|
226
|
+
description: rest.replace(ACCEPTANCE, '').replace(MARKERS, '').replace(/\s+/g, ' ').trim(),
|
|
210
227
|
conditions: acceptanceConditions(acceptance),
|
|
211
228
|
criteria: criteriaRefs(rest),
|
|
212
229
|
// De qué otras tareas depende. El orden del BACKLOG alcanzaba mientras hubiera un runner: con dos,
|
|
@@ -388,6 +405,23 @@ function readWip(dir, runner) {
|
|
|
388
405
|
return parseWip(read(path.join(dir, 'wip', `${name}.md`)), name)
|
|
389
406
|
}
|
|
390
407
|
|
|
408
|
+
// Si el checkpoint de hito sigue frenando, leído del archivo y no de que el archivo esté. Es la misma
|
|
409
|
+
// forma que el WIP de acá arriba —`status: IDLE` es un estado escrito— y la razón es R28: un centinela
|
|
410
|
+
// cuya única información es existir obliga a que el borrado sea parte de la resolución, y esa es una
|
|
411
|
+
// convención que alguien va a olvidar. Cuando se olvida, quien revisa lee que el hito ya se revisó
|
|
412
|
+
// mientras el mecanismo sigue leyendo que el archivo está, y la corrida siguiente muere en la puerta de
|
|
413
|
+
// entrada habiendo cargado el estado entero.
|
|
414
|
+
//
|
|
415
|
+
// Cerrado por defecto (R27): frena salvo que diga `resuelta`. Un archivo de una instancia anterior no
|
|
416
|
+
// trae `status`, y abrirlo por eso destrabaría en silencio al actualizar, que es la quita escrita como
|
|
417
|
+
// agregado que R9 nombra. El vocabulario es el mismo de `HUMAN_ACTIONS.md` a propósito: es el mismo acto
|
|
418
|
+
// —una persona contesta— y dos palabras para eso serían dos convenciones que aprender.
|
|
419
|
+
function checkpointHolds(dir) {
|
|
420
|
+
const text = read(path.join(dir, 'AWAITING_REVIEW.md'))
|
|
421
|
+
if (!text) return false
|
|
422
|
+
return !/^status:\s*resuelta\b/mi.test(text)
|
|
423
|
+
}
|
|
424
|
+
|
|
391
425
|
// Todos los que hay. Lo pregunta `check`, que juzga si cada plan apunta a una tarea que existe, y `tree`,
|
|
392
426
|
// que muestra qué está en vuelo: las dos son preguntas sobre la instancia y no sobre quien pregunta.
|
|
393
427
|
function readWips(dir) {
|
|
@@ -436,6 +470,7 @@ module.exports = {
|
|
|
436
470
|
EPIC_STATES, HUMAN_ACTION_STATES, LANES, MILESTONE_HEADING, STOP_REASONS,
|
|
437
471
|
TASK_LINE, TASK_LINE_ANY_LANE,
|
|
438
472
|
read, section, withoutComments, frontmatter, readEpics, readBacklog, readDone, readWip, readWips, wipName,
|
|
473
|
+
checkpointHolds,
|
|
439
474
|
acceptanceConditions, tableRows, taskFromLine,
|
|
440
475
|
readInbox, inboxHeads, readHumanActions,
|
|
441
476
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
## R8 — Un commit por naturaleza
|
|
4
4
|
|
|
5
5
|
Stagear rutas explícitas, revisar el diff staged y crear un Conventional Commit en inglés. No usar
|
|
6
|
-
`git add .`, `git add -A`,
|
|
6
|
+
`git add .`, `git add -A`, force ni trailers de IA.
|
|
7
|
+
|
|
8
|
+
**Y no reescribir historia que otro ya leyó.** Lo que se protege es eso, no el comando: un `--amend`
|
|
9
|
+
sobre un commit publicado es la misma reescritura que un force-push y se frena igual; sobre uno que no
|
|
10
|
+
salió de tu máquina es la corrección, y prohibirlo no impide el resultado —`git reset --soft HEAD~1` y
|
|
11
|
+
volver a commitear produce exactamente lo mismo— sino el comando que lo nombra. Una regla que se cumple
|
|
12
|
+
mejor esquivándola se termina esquivando siempre.
|
|
7
13
|
|
|
8
14
|
Dónde corta un commit lo decide la naturaleza del diff, no su tamaño ni un conteo. Una tarea suele
|
|
9
15
|
tener una sola, y por eso un commit por tarea es lo habitual; cuando tiene dos, se hacen dos. Un
|
|
@@ -90,7 +96,8 @@ Push, PR, merge, tags, deploy y rollback requieren la autorización configurada
|
|
|
90
96
|
De esos seis, el motor comprueba uno: el push, contra `runner.allowPush` —que no llega a la rama viva
|
|
91
97
|
sin `runner.pushToLiveBranches`, ni a un subagente— o contra la orden que la persona da en el chat
|
|
92
98
|
nombrando el remoto y la rama. Reescribir historia publicada
|
|
93
|
-
no entra en esa autorización y se frena siempre
|
|
99
|
+
no entra en esa autorización y se frena siempre — también cuando la reescritura es un `--amend`, que el
|
|
100
|
+
guard distingue mirando si algún remoto alcanza al commit. Los otros cinco no tienen una
|
|
94
101
|
forma reconocible en un comando —un deploy es `kubectl`, `terraform`, un script o un botón— y los
|
|
95
102
|
sostiene esta regla y el review, no un guard.
|
|
96
103
|
|