@ingeniomaps/cauce 0.80.0 → 0.82.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 +177 -0
- package/automatization/hooks/README.md +41 -11
- package/automatization/hooks/guard-chat.sh +5 -0
- package/automatization/hooks/guard-secrets-shell.sh +3 -0
- package/automatization/runners/antigravity/rules/cauce.md +7 -2
- package/automatization/runners/claude/CLAUDE.md +1 -4
- package/automatization/runners/claude/README.md +5 -4
- package/automatization/runners/claude/manifest.json +26 -1
- package/automatization/runners/claude/settings.json +8 -24
- package/automatization/runners/codex/AGENTS.md +7 -3
- package/automatization/runners/codex/README.md +4 -0
- package/automatization/runners/codex/hooks.json +7 -0
- package/automatization/runners/gemini/GEMINI.md +1 -4
- package/automatization/runners/gemini/README.md +5 -2
- package/automatization/runners/gemini/settings.json +11 -1
- package/automatization/shared/inbox.js +28 -0
- package/automatization/workflows/agent-eval.js +1 -1
- package/automatization/workflows/autobuild.js +52 -18
- package/automatization/workflows/flow.js +33 -8
- package/automatization/workflows/onboard.js +25 -3
- package/engine/automation/index.js +28 -5
- package/engine/automation/rules.js +122 -0
- package/engine/automation/runners.js +3 -1
- package/engine/cli/instance.js +35 -6
- package/engine/cli/planning.js +16 -2
- package/engine/config/validate.js +53 -3
- package/engine/core/onboarding.js +37 -7
- package/engine/core/ownership.js +64 -1
- package/engine/core/scan.js +23 -9
- package/engine/hooks/approval.js +40 -9
- package/engine/hooks/chat.js +171 -0
- package/engine/hooks/files.js +28 -16
- package/engine/hooks/input.js +8 -12
- package/engine/hooks/push.js +147 -0
- package/engine/hooks/run.js +23 -3
- package/engine/hooks/secrets-shell.js +62 -0
- package/engine/hooks/self-approval.js +31 -0
- package/engine/hooks/shell.js +37 -31
- package/engine/integrations/registry.js +13 -1
- package/engine/planning/inbox.js +36 -0
- package/engine/planning/parser.js +22 -9
- package/engine/planning/recurring.js +13 -2
- package/engine/schemas/ops-config.schema.json +21 -0
- package/package.json +1 -1
- package/template/AGENTS.md +23 -7
- package/template/planning/INBOX.md +2 -1
- package/template/planning/RECURRING.md +6 -5
- package/template/planning/rules/system/commits.md +3 -1
package/engine/hooks/shell.js
CHANGED
|
@@ -9,10 +9,12 @@ const os = require('node:os')
|
|
|
9
9
|
const path = require('node:path')
|
|
10
10
|
const { spawnSync } = require('node:child_process')
|
|
11
11
|
const {
|
|
12
|
-
commandOf, cwdOf, block, isCommit, stagedForCommit,
|
|
13
|
-
writableRoots, outsideRoots, DECLARE_IT, unquoted,
|
|
12
|
+
commandOf, cwdOf, block, isCommit, stagedForCommit,
|
|
13
|
+
writableRoots, outsideRoots, DECLARE_IT, unquoted, opsRoot, withoutGitGlobals,
|
|
14
14
|
} = require('./input')
|
|
15
15
|
const AP = require('./approval')
|
|
16
|
+
const { publish } = require('./push')
|
|
17
|
+
const { selfApproval } = require('./self-approval')
|
|
16
18
|
const EV = require('../core/evidence')
|
|
17
19
|
|
|
18
20
|
// Dónde empieza y dónde termina una palabra dentro de un comando. Tres reglas de la tabla de abajo lo
|
|
@@ -48,12 +50,6 @@ const MISMO = String.raw`[^;&|\n]`
|
|
|
48
50
|
// `bash -c "git push origin main"` y `eval "git reset --hard"` siguen cayendo, comprobado. Queda afuera
|
|
49
51
|
// la sustitución dentro del propio mensaje —`git commit -m "$(...)"` corre y ya no se ve—, que es
|
|
50
52
|
// evasión y no la forma habitual.
|
|
51
|
-
// La raíz donde vive `planning/`, que es donde se busca la aprobación. Los cuatro guards que la
|
|
52
|
-
// consultan la resuelven igual, así que se resuelve una vez.
|
|
53
|
-
function opsRoot(input) {
|
|
54
|
-
return findOpsRoot(process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || cwdOf(input))
|
|
55
|
-
}
|
|
56
|
-
|
|
57
53
|
function destructive(input) {
|
|
58
54
|
const raw = commandOf(input)
|
|
59
55
|
// Las opciones globales de `git` se sacan acá y no en cada regla: toda regla de abajo que mire un
|
|
@@ -69,14 +65,15 @@ function destructive(input) {
|
|
|
69
65
|
// matchea igual las dos formas, así que `allowPush` habilitaba el force-push sin que nadie lo decidiera
|
|
70
66
|
// y el párrafo de autonomía de `AGENTS.md` tenía que confesarlo. R8 prohíbe `force` sin excepción
|
|
71
67
|
// configurable, así que esta rama va antes del permiso y no lo consulta.
|
|
72
|
-
|
|
68
|
+
//
|
|
69
|
+
// El `+` delante de una rama es el mismo force escrito en el refspec —`git push origin +main`—, y
|
|
70
|
+
// pasaba como un push normal: la regla miraba sólo las banderas (caso 103).
|
|
71
|
+
if (new RegExp(String.raw`\bgit\s+push\b${MISMO}*\s(?:(?:-f|--force(?:-with-lease|-if-includes)?)\b|\+\S)`)
|
|
73
72
|
.test(command)) {
|
|
74
73
|
block("'git push --force' reescribe historia ya publicada. R8 lo prohíbe y runner.allowPush no lo "
|
|
75
74
|
+ 'habilita: publicá con un push normal, o registrá una acción humana.')
|
|
76
75
|
}
|
|
77
|
-
|
|
78
|
-
block("'git push' publica cambios y requiere una acción humana. Se habilita con runner.allowPush.")
|
|
79
|
-
}
|
|
76
|
+
publish(input, command)
|
|
80
77
|
const rules = [
|
|
81
78
|
[/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales."],
|
|
82
79
|
// R8 lo prohíbe sin excepción configurable y ningún guard lo miraba: `grep -rn amend engine/hooks/`
|
|
@@ -181,7 +178,7 @@ function dependencies(input) {
|
|
|
181
178
|
// `package.json` dice «este manifiesto va sin su lock a propósito» y deja de valer en cuanto el
|
|
182
179
|
// conjunto cambie. La rama de publicar no pasa por acá y no tiene ruta: sigue arriba, con su variable.
|
|
183
180
|
const sinAprobar = (parent, names) => AP.pending(opsRoot(input),
|
|
184
|
-
names.map((name) => path.posix.join(parent === '.' ? '' : parent, name)))
|
|
181
|
+
names.map((name) => path.posix.join(parent === '.' ? '' : parent, name)), input)
|
|
185
182
|
// Un lock cuenta si está en disco **o** si el commit lo va a llevar, y la unión no es un detalle: el
|
|
186
183
|
// disco solo perdía el que alguien borró del árbol sin stagear el borrado —sigue en el índice, sigue
|
|
187
184
|
// en el próximo commit— y ahí la comprobación dejaba de dispararse justo cuando más hacía falta. Es
|
|
@@ -204,12 +201,12 @@ function dependencies(input) {
|
|
|
204
201
|
const manifests = sinAprobar(parent, state.manifests)
|
|
205
202
|
if (state.manifests.length && existingLocks.length && !state.locks.length && manifests.length) {
|
|
206
203
|
block(`${parent}: cambió ${state.manifests.join(', ')} sin actualizar su lockfile.\n`
|
|
207
|
-
+ AP.HOW('OPS_DEPENDENCIES_OVERRIDE', manifests))
|
|
204
|
+
+ AP.HOW('OPS_DEPENDENCIES_OVERRIDE', manifests, input))
|
|
208
205
|
}
|
|
209
206
|
const lockfiles = sinAprobar(parent, state.locks)
|
|
210
207
|
if (state.locks.length && !state.manifests.length && lockfiles.length) {
|
|
211
208
|
block(`${parent}: cambió ${state.locks.join(', ')} sin un cambio explícito en el manifest.\n`
|
|
212
|
-
+ AP.HOW('OPS_DEPENDENCIES_OVERRIDE', lockfiles))
|
|
209
|
+
+ AP.HOW('OPS_DEPENDENCIES_OVERRIDE', lockfiles, input))
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
212
|
}
|
|
@@ -306,17 +303,21 @@ function writesWithBase(command, cwd) {
|
|
|
306
303
|
|
|
307
304
|
function shellBoundary(input) {
|
|
308
305
|
const allowed = writableRoots(input)
|
|
309
|
-
if (!allowed) return
|
|
310
306
|
for (const { raw, base } of writesWithBase(commandOf(input), cwdOf(input))) {
|
|
311
307
|
// Una ruta absoluta no depende del `cd`, así que un destino que no se sabe no la vuelve injuzgable.
|
|
312
308
|
// Al revés sí: sin saber desde dónde se resuelve, una relativa no se puede verificar, y un guard que
|
|
313
309
|
// no puede verificar no autoriza —el criterio que fijó el 031 para el índice—.
|
|
314
310
|
if (!path.isAbsolute(raw) && base === null) {
|
|
311
|
+
if (!allowed) continue
|
|
315
312
|
block(`el comando hace \`cd\` a un destino que no se puede resolver acá, así que no hay contra qué `
|
|
316
313
|
+ `resolver ${raw}. Escribí la ruta absoluta, o hacé el \`cd\` en un comando aparte.`)
|
|
317
314
|
}
|
|
318
|
-
const file = path.resolve(base, raw)
|
|
319
|
-
|
|
315
|
+
const file = path.resolve(base || '/', raw)
|
|
316
|
+
// El canal por el que la persona aprueba no es un destino más: se juzga aunque no haya raíces
|
|
317
|
+
// declaradas y aunque caiga en el temporal, que el resto de este guard deja pasar (caso 098).
|
|
318
|
+
const own = selfApproval(input, file)
|
|
319
|
+
if (own) block(own)
|
|
320
|
+
if (!allowed || NEUTRAL.some((pattern) => pattern.test(file))) continue
|
|
320
321
|
if (outsideRoots(file, allowed)) {
|
|
321
322
|
block(`el comando escribe en ${file}, fuera de las raíces declaradas en ops.config.json. ${DECLARE_IT}`)
|
|
322
323
|
}
|
|
@@ -347,9 +348,9 @@ function governance(input) {
|
|
|
347
348
|
// La aprobación vale para lo que nombra y para nada más: lo que quede sin cubrir es lo que se
|
|
348
349
|
// reporta. Así una aprobación vieja no autoriza el archivo que se sumó después, que es la diferencia
|
|
349
350
|
// entre una llave por operación y una puerta que quedó abierta.
|
|
350
|
-
const pendientes = AP.pending(opsRoot(input), governed)
|
|
351
|
+
const pendientes = AP.pending(opsRoot(input), governed, input)
|
|
351
352
|
if (!pendientes.length) return
|
|
352
|
-
block(`El commit toca gobernanza protegida.\n${AP.HOW('OPS_GOVERNANCE_OVERRIDE', pendientes)}`)
|
|
353
|
+
block(`El commit toca gobernanza protegida.\n${AP.HOW('OPS_GOVERNANCE_OVERRIDE', pendientes, input)}`)
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
function run(program, args, cwd, extra = {}) {
|
|
@@ -488,20 +489,20 @@ function verify(input) {
|
|
|
488
489
|
// Acá lo aprobado es el conjunto staged entero: decir «autorizo commitear exactamente estas rutas»
|
|
489
490
|
// es lo que un gate en rojo necesita, y cambia en cuanto se stagea una más. La lista sale del índice
|
|
490
491
|
// y no de una regla, que es lo que la vuelve una operación y no un permiso.
|
|
491
|
-
const sinAprobar = AP.pending(opsRoot(input), staged)
|
|
492
|
+
const sinAprobar = AP.pending(opsRoot(input), staged, input)
|
|
492
493
|
const aprobado = !sinAprobar.length
|
|
493
494
|
if (changedOpenApi && !hasApiGenerated && !aprobado) {
|
|
494
495
|
block('Cambió una fuente OpenAPI/Swagger sin incluir código regenerado. Ejecuta el generador y '
|
|
495
|
-
+ `stagea su salida.\n${AP.HOW('OPS_SKIP_VERIFY', sinAprobar)}`)
|
|
496
|
+
+ `stagea su salida.\n${AP.HOW('OPS_SKIP_VERIFY', sinAprobar, input)}`)
|
|
496
497
|
}
|
|
497
498
|
if (changedSqlSource && !hasSqlGenerated && !aprobado) {
|
|
498
499
|
block('Cambió una consulta SQL fuente sin artefactos regenerados. Ejecuta el generador.\n'
|
|
499
|
-
+ AP.HOW('OPS_SKIP_VERIFY', sinAprobar))
|
|
500
|
+
+ AP.HOW('OPS_SKIP_VERIFY', sinAprobar, input))
|
|
500
501
|
}
|
|
501
502
|
if (!staged.some((file) => /\.(?:ts|tsx|js|jsx|mjs|cjs|go|py|html|css|scss|prisma)$/.test(file))) return
|
|
502
503
|
const { root, temp, env } = commitTree(dir)
|
|
503
504
|
try {
|
|
504
|
-
verifyGates(root, dir, sinAprobar, env,
|
|
505
|
+
verifyGates(root, dir, sinAprobar, env, input)
|
|
505
506
|
} finally {
|
|
506
507
|
if (temp) fs.rmSync(temp, { recursive: true, force: true })
|
|
507
508
|
}
|
|
@@ -522,11 +523,15 @@ function verify(input) {
|
|
|
522
523
|
// Se muestra **una** línea y acotada: la salida de un gate puede traer cualquier cosa del entorno, y lo
|
|
523
524
|
// que hace falta para diagnosticar es la primera línea de error, no el volcado.
|
|
524
525
|
const ERROR_LINE = /error|err[_!]|fail|abort|not found|cannot|no such/i
|
|
525
|
-
// Cómo marca un reporte de pruebas cada resultado
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
// Cómo marca un reporte de pruebas cada resultado. Van sólo las comprobadas contra la herramienta (caso
|
|
527
|
+
// 094): el nombre de una prueba verde puede decir «error», y sin mirar la marca la búsqueda por palabra se
|
|
528
|
+
// quedaba con ella y el mensaje escondía la roja. Comprobadas con la salida entubada, como la ve un gate:
|
|
529
|
+
// `node --test` en spec y en TAP (Node 24.18.0), `go test` (go 1.26.3), jest 30.5.1 (`● nombre`, y
|
|
530
|
+
// `● Test suite failed to run`), vitest 5.0.0 (`× nombre`), mocha 12.0.1 (`1) nombre`; la verde es `✔`) y
|
|
531
|
+
// pytest 9.1.1 (`FAILED archivo::prueba` en el resumen; `::prueba PASSED` la verde con `-v`). Jest y vitest
|
|
532
|
+
// no imprimen las verdes sin `--verbose`, así que de ellos no hay marca de éxito.
|
|
533
|
+
const FAILED_TEST = /^(?:✖|not ok\b|--- FAIL:|● |× |\d+\) |FAILED )/
|
|
534
|
+
const PASSED_TEST = /^(?:✔|ok\b|--- PASS:)|::\S+ PASSED\b/
|
|
530
535
|
const MAX_LINE = 160
|
|
531
536
|
function fallo(gate, result) {
|
|
532
537
|
// La línea que empieza con `>` es el eco del script que npm y pnpm imprimen antes de correrlo, así
|
|
@@ -555,7 +560,8 @@ function comoSeLee(failures) {
|
|
|
555
560
|
+ 'una suite, así que mirá si llegaron a ejecutarse antes de aprobar esto como un rojo conocido.'
|
|
556
561
|
}
|
|
557
562
|
|
|
558
|
-
function verifyGates(root, dir, sinAprobar, env,
|
|
563
|
+
function verifyGates(root, dir, sinAprobar, env, input) {
|
|
564
|
+
const ops = opsRoot(input)
|
|
559
565
|
const failures = []
|
|
560
566
|
if (fs.existsSync(path.join(root, 'package.json'))) {
|
|
561
567
|
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
@@ -595,7 +601,7 @@ function verifyGates(root, dir, sinAprobar, env, ops) {
|
|
|
595
601
|
const donde = root === dir ? '' : '\nCorrió sobre el índice, que es lo que el commit graba: si en tu '
|
|
596
602
|
+ 'directorio pasa, es que en disco tenés algo que no está staged.'
|
|
597
603
|
block(`Verify falló en ${path.basename(dir)}: ${comoSeLee(failures)}\nNo se commitea en rojo.${donde}\n`
|
|
598
|
-
+ AP.HOW('OPS_SKIP_VERIFY', sinAprobar))
|
|
604
|
+
+ AP.HOW('OPS_SKIP_VERIFY', sinAprobar, input))
|
|
599
605
|
}
|
|
600
606
|
|
|
601
607
|
module.exports = { destructive, gitAdd, dependencies, governance, verify, shellBoundary, run }
|
|
@@ -64,11 +64,22 @@ function adapter(root, name, entry = {}) {
|
|
|
64
64
|
return impl
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Qué nombre tiene forma de secreto. Es una sola regla para los tres que la necesitan: la configuración
|
|
68
|
+
// de una integración, la declaración de secretos y el aviso de credenciales sin dueño de «ops check».
|
|
69
|
+
// La clave cuenta sólo como palabra propia —API_KEY sí, projectKey no—, porque el campo de Jira con la
|
|
70
|
+
// clave del proyecto está en instancias reales y rechazarlo les rompería la validación (caso 102, donde
|
|
71
|
+
// está la medición).
|
|
72
|
+
const SENSITIVE = /(password|secret|token|authorization|cookie|dsn|credentials?|(?:^|_)key)$/i
|
|
73
|
+
|
|
74
|
+
function sensitiveKey(key) {
|
|
75
|
+
return SENSITIVE.test(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
function sensitivePath(value, trail = '') {
|
|
68
79
|
if (!value || typeof value !== 'object') return ''
|
|
69
80
|
for (const [key, child] of Object.entries(value)) {
|
|
70
81
|
const next = trail ? `${trail}.${key}` : key
|
|
71
|
-
if (
|
|
82
|
+
if (sensitiveKey(key)) return next
|
|
72
83
|
const nested = sensitivePath(child, next)
|
|
73
84
|
if (nested) return nested
|
|
74
85
|
}
|
|
@@ -442,6 +453,7 @@ module.exports = {
|
|
|
442
453
|
providerConfig,
|
|
443
454
|
reconcile,
|
|
444
455
|
safeSegment,
|
|
456
|
+
sensitiveKey,
|
|
445
457
|
sensitivePath,
|
|
446
458
|
sync,
|
|
447
459
|
validate,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Lo que `check` dice sobre el INBOX. Son advertencias y nunca errores: el INBOX es de la persona, y un
|
|
4
|
+
// `check` rojo por lo que tiene adentro la frenaría a ella por lo que escribió un recorrido (caso 101).
|
|
5
|
+
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const P = require('./parser')
|
|
8
|
+
|
|
9
|
+
const FILE = 'INBOX.md'
|
|
10
|
+
// Un archivo que todavía se recorre de una sentada; el molde vacío ocupa menos de treinta. La instancia
|
|
11
|
+
// lo cambia en `inbox.warnLines`.
|
|
12
|
+
const WARN_LINES = 300
|
|
13
|
+
|
|
14
|
+
function warnings(root, done, config) {
|
|
15
|
+
const text = P.read(path.join(root, FILE))
|
|
16
|
+
if (!text.trim()) return []
|
|
17
|
+
const found = []
|
|
18
|
+
const declared = config && config.inbox && config.inbox.warnLines
|
|
19
|
+
const limit = Number.isInteger(declared) && declared > 0 ? declared : WARN_LINES
|
|
20
|
+
const count = text.replace(/\n$/, '').split('\n').length
|
|
21
|
+
if (count > limit) {
|
|
22
|
+
found.push(`${FILE}: ${count} líneas, más que el umbral de ${limit} (inbox.warnLines en ops.config.json); `
|
|
23
|
+
+ 'recorrelo y borrá lo que ya se decidió')
|
|
24
|
+
}
|
|
25
|
+
// Una entrada que se llama como una tarea cerrada probablemente se promovió y nadie la borró. Es un
|
|
26
|
+
// indicio y no una prueba —el molde no obliga a que la tarea conserve el nombre del ítem—, y por eso
|
|
27
|
+
// avisa en vez de fallar (caso 106).
|
|
28
|
+
for (const name of Object.values(P.inboxHeads(root)).flat()) {
|
|
29
|
+
if (done.set.has(name)) {
|
|
30
|
+
found.push(`${FILE}: **${name}** se llama como done/${name}.md; si ya se promovió, borrala`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return found
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { warnings }
|
|
@@ -405,18 +405,31 @@ function readWips(dir) {
|
|
|
405
405
|
// La plantilla no traía ningún ejemplo, así que quien escribía viñetas planas veía cero ítems sobre un
|
|
406
406
|
// archivo con doce y nada se lo decía. `skipped` es lo que vuelve visible esa diferencia.
|
|
407
407
|
function readInbox(dir) {
|
|
408
|
-
const
|
|
408
|
+
const { heads, skipped } = inboxSections(dir)
|
|
409
|
+
return { ...Object.fromEntries(Object.entries(heads).map(([key, names]) => [key, names.length])), skipped }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Los nombres en negrita de cada sección, que es lo que un recorrido necesita para no volver a escribir
|
|
413
|
+
// lo que ya está: el nombre y no la entrada, porque pasar el archivo entero a cada tarea cuesta lo que
|
|
414
|
+
// el INBOX pesa (caso 101).
|
|
415
|
+
function inboxHeads(dir) {
|
|
416
|
+
return inboxSections(dir).heads
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function inboxSections(dir) {
|
|
420
|
+
const heads = { deuda: [], ideas: [], propuestas: [], lecciones: [] }
|
|
421
|
+
let skipped = 0
|
|
409
422
|
for (const part of read(path.join(dir, 'INBOX.md')).split(/^##\s+/m)) {
|
|
410
423
|
const title = part.split('\n')[0]
|
|
411
424
|
const bullets = (part.match(/^[-*]\s+(?:\[[ xX]\]\s+)?/gm) || []).length
|
|
412
|
-
const
|
|
413
|
-
if (/Deuda|Ideas|Visi[oó]n|Propuestas|Lecciones/i.test(title))
|
|
414
|
-
if (/Deuda/i.test(title))
|
|
415
|
-
if (/Ideas|Visi[oó]n/i.test(title))
|
|
416
|
-
if (/Propuestas/i.test(title))
|
|
417
|
-
if (/Lecciones/i.test(title))
|
|
425
|
+
const names = [...part.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?\*\*([^*\n]*)/gm)].map((hit) => hit[1].trim())
|
|
426
|
+
if (/Deuda|Ideas|Visi[oó]n|Propuestas|Lecciones/i.test(title)) skipped += bullets - names.length
|
|
427
|
+
if (/Deuda/i.test(title)) heads.deuda = names
|
|
428
|
+
if (/Ideas|Visi[oó]n/i.test(title)) heads.ideas = names
|
|
429
|
+
if (/Propuestas/i.test(title)) heads.propuestas = names
|
|
430
|
+
if (/Lecciones/i.test(title)) heads.lecciones = names
|
|
418
431
|
}
|
|
419
|
-
return
|
|
432
|
+
return { heads, skipped }
|
|
420
433
|
}
|
|
421
434
|
|
|
422
435
|
module.exports = {
|
|
@@ -424,5 +437,5 @@ module.exports = {
|
|
|
424
437
|
TASK_LINE, TASK_LINE_ANY_LANE,
|
|
425
438
|
read, section, withoutComments, frontmatter, readEpics, readBacklog, readDone, readWip, readWips, wipName,
|
|
426
439
|
acceptanceConditions, tableRows, taskFromLine,
|
|
427
|
-
readInbox, readHumanActions,
|
|
440
|
+
readInbox, inboxHeads, readHumanActions,
|
|
428
441
|
}
|
|
@@ -19,6 +19,10 @@ const CADENCES = { mensual: 1, trimestral: 3, semestral: 6, anual: 12 }
|
|
|
19
19
|
|
|
20
20
|
const ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
21
21
|
const DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
22
|
+
// La fila que el molde trae activa escribe su `Desde` como marcador, porque la fecha depende del día en
|
|
23
|
+
// que se crea la instancia y la reemplazan `init` y `upgrade`. Sin resolver sólo existe en el molde
|
|
24
|
+
// mismo —el que `npm run check` valida en el toolkit—, y ahí no vence: `status` la descarta por fecha.
|
|
25
|
+
const PLACEHOLDER = /^\{\{[A-Z_]+\}\}$/
|
|
22
26
|
// `- **qué** AAAA-MM-DD — razón`. El nombre en negrita adelante es la misma convención del INBOX, y por
|
|
23
27
|
// el mismo motivo: es con lo que se cita la fila desde otro lado.
|
|
24
28
|
const POSTPONEMENT = /^-\s+\*\*([^*]+)\*\*\s+(\S+)\s+[—-]\s+(.+)$/
|
|
@@ -77,7 +81,7 @@ function validate({ exists, rows, postponements }) {
|
|
|
77
81
|
if (!CADENCES[row.cadence]) {
|
|
78
82
|
errors.push(`${at}: cadencia "${row.cadence}" fuera de ${Object.keys(CADENCES).join(' | ')}`)
|
|
79
83
|
}
|
|
80
|
-
if (!DATE.test(row.since)) errors.push(`${at}: Desde debe ser AAAA-MM-DD`)
|
|
84
|
+
if (!DATE.test(row.since) && !PLACEHOLDER.test(row.since)) errors.push(`${at}: Desde debe ser AAAA-MM-DD`)
|
|
81
85
|
// Se juzga la línea armada y no la celda suelta: lo que se promueve es esa línea, y quien la va a
|
|
82
86
|
// leer es el mismo lector de BACKLOG. Una celda que pasa acá y una línea que BACKLOG rechaza es el
|
|
83
87
|
// error que aparece un mes después, con la tarea ya pegada.
|
|
@@ -145,4 +149,11 @@ function warnings(state) {
|
|
|
145
149
|
return lines
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
|
|
152
|
+
// Los marcadores de fecha del molde, resueltos para una instancia que nace hoy. `Desde` es la primera
|
|
153
|
+
// fecha de vencimiento y no el día en que se declara la fila, así que va un período después: con la
|
|
154
|
+
// fecha de hoy la fila nacía «vence hoy» (caso 106). Trimestral es la cadencia de la fila del molde.
|
|
155
|
+
function sinceValues(today) {
|
|
156
|
+
return { '{{INBOX_SINCE}}': addMonths(today, CADENCES.trimestral) }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { FILE, read, validate, status, warnings, taskLine, sinceValues }
|
|
@@ -83,6 +83,15 @@
|
|
|
83
83
|
},
|
|
84
84
|
"allowPush": {
|
|
85
85
|
"type": "boolean"
|
|
86
|
+
},
|
|
87
|
+
"pushToLiveBranches": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"description": "Las ramas vivas —main, master y la rama por defecto de cada remoto— en las que se puede publicar. Sin nombrarla acá, ni allowPush ni una orden en el chat llegan a una rama viva. Nombres exactos, sin patrones.",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"pattern": "^[^\\s*?\\[]+$"
|
|
93
|
+
},
|
|
94
|
+
"uniqueItems": true
|
|
86
95
|
}
|
|
87
96
|
},
|
|
88
97
|
"additionalProperties": false
|
|
@@ -102,6 +111,18 @@
|
|
|
102
111
|
"minItems": 1
|
|
103
112
|
}
|
|
104
113
|
}
|
|
114
|
+
},
|
|
115
|
+
"inbox": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"description": "El aviso de tamaño del INBOX. `check` advierte —nunca falla— cuando `planning/INBOX.md` pasa de este número de líneas: el INBOX es de la persona, y lo que el aviso pide es recorrerlo, no dejar de escribir.",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"properties": {
|
|
120
|
+
"warnLines": {
|
|
121
|
+
"type": "integer",
|
|
122
|
+
"minimum": 1,
|
|
123
|
+
"description": "Líneas a partir de las cuales `check` avisa. Por defecto 300."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
105
126
|
}
|
|
106
127
|
},
|
|
107
128
|
"additionalProperties": false
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Este archivo gobierna el qué y el cuándo. `planning/PROTOCOL.md` gobierna el flujo y
|
|
4
4
|
`planning/rules/` el cómo: sus reglas rigen cada tarea y se leen antes de empezar, no cuando algo sale
|
|
5
|
-
mal.
|
|
5
|
+
mal. Este archivo, el protocolo y `planning/rules/system/` los mantiene Cauce y valen para cualquier
|
|
6
|
+
proyecto; las reglas propias viven junto a `system/`, y donde una de ellas sobrescribe, contradice o
|
|
7
|
+
restringe una del sistema, rige la del proyecto. Lo que este proyecto tiene de propio
|
|
6
8
|
—su mapa, sus integraciones, hasta dónde llega la autonomía acá— vive en `organization/workspace.md`.
|
|
7
9
|
|
|
8
10
|
## Qué sabe este proyecto y no este archivo
|
|
@@ -103,9 +105,19 @@ Un guard existente **no se edita**: `upgrade` detecta el cambio y se detiene ant
|
|
|
103
105
|
Algunos bloqueos tienen salida, y conviene saber cuál antes de necesitarla — el momento en que un guard
|
|
104
106
|
te frena es el peor para elegir bien.
|
|
105
107
|
|
|
106
|
-
**
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
**Si lo pediste vos en el chat, no hace falta nada.** Los guards contienen al agente cuando decide solo o
|
|
109
|
+
cuando trabaja dentro de un recorrido; lo que vos pedís directo no se frena. Nombrá lo que querés que
|
|
110
|
+
toque —«borrá la prueba de altas», «reescribí la migración 004»— y pasa sin preguntarte de nuevo. Si tu
|
|
111
|
+
pedido no lo nombraba y algo se frena, el agente te dice qué y por qué: contestá «dale» y pasa exactamente
|
|
112
|
+
eso. Y `plan-first` no te pide un plan cuando el cambio lo pediste vos: el plan es para el trabajo que va
|
|
113
|
+
por tareas. Funciona en Claude Code, Codex y Gemini, que le avisan a Cauce cuando mandás un mensaje; en
|
|
114
|
+
Antigravity, y cuando nadie está en el chat —CI, un recorrido, un subagente—, queda el archivo de abajo.
|
|
115
|
+
|
|
116
|
+
**Sin chat, la salida de todos ellos es la misma**: escribir en `planning/.ops-approval` las rutas que
|
|
117
|
+
autorizás, una por línea, con `#` para lo que no sea una ruta. En sidecar es el `planning/` de la
|
|
118
|
+
instancia y no una carpeta al lado de tus proyectos; el bloqueo dice la ruta exacta. Es un solo archivo
|
|
119
|
+
para todos los guards, porque lo que escribís son rutas y quién las mira lo decide qué guard esté
|
|
120
|
+
juzgando esa ruta. Lo escribís vos: si el agente intenta escribírselo, un guard lo frena.
|
|
109
121
|
|
|
110
122
|
| lo que te frena | qué ruta aprobás |
|
|
111
123
|
|---|---|
|
|
@@ -240,12 +252,16 @@ aprobación que pide BR-OPS-002 — `context` la nombra para que la vea una pers
|
|
|
240
252
|
`BACKLOG.md` es esa persona.
|
|
241
253
|
|
|
242
254
|
Publicar es lo único de todo eso que este proyecto puede habilitar, y `runner.allowPush` en
|
|
243
|
-
`ops.config.json` es la autorización que R10 pide
|
|
244
|
-
|
|
255
|
+
`ops.config.json` es la autorización que R10 pide para las ramas de trabajo. La rama viva —`main`,
|
|
256
|
+
`master` o la rama por defecto del remoto— no la alcanza si el proyecto no la nombra en
|
|
257
|
+
`runner.pushToLiveBranches`, y un subagente no publica con ningún permiso. Sin la llave, pasa el push
|
|
258
|
+
que la persona pide en el chat nombrando el remoto y la rama, o el que ella aprueba contestando «dale».
|
|
259
|
+
Reescribir historia publicada no entra en el trato: un `push --force` se frena con la llave prendida o
|
|
260
|
+
apagada.
|
|
245
261
|
|
|
246
262
|
Eso rige sin que nadie escriba nada. Lo que este proyecto amplíe o restrinja va en
|
|
247
263
|
`organization/workspace.md`, con su razón; ninguna de esas prohibiciones se amplía ahí, y la
|
|
248
|
-
publicación tampoco se decide ahí: la
|
|
264
|
+
publicación tampoco se decide ahí: la deciden `allowPush` y `pushToLiveBranches`.
|
|
249
265
|
|
|
250
266
|
## Definición de terminado
|
|
251
267
|
|
|
@@ -14,7 +14,8 @@ Lo que separa a las cuatro secciones es el sujeto del ítem, no su tamaño ni su
|
|
|
14
14
|
|
|
15
15
|
- **Deuda** — un costo que ya estamos cargando en código nuestro, conocido y no bloqueante.
|
|
16
16
|
- **Ideas** — una pregunta abierta, sin respuesta propuesta.
|
|
17
|
-
- **Propuestas** — un cambio concreto del producto
|
|
17
|
+
- **Propuestas** — un cambio concreto del producto y su fix propuesto. La evidencia no se copia acá: se
|
|
18
|
+
cita dónde vive —el `done/` de la tarea, el informe—.
|
|
18
19
|
- **Lecciones** — sobre cómo trabajamos; es lo que alimenta reglas y propuestas de cargo.
|
|
19
20
|
|
|
20
21
|
Ideas y Propuestas se separan por si hay una respuesta propuesta. Deuda y Propuestas, por si el costo
|
|
@@ -16,7 +16,8 @@ vencimiento se calcula cuando alguien corre el CLI. Promover sigue siendo un act
|
|
|
16
16
|
- **Cada** — vocabulario cerrado: `mensual`, `trimestral`, `semestral`, `anual`. No hay expresiones de
|
|
17
17
|
cron, y esa ausencia es el enunciado: si hiciera falta una, lo que estarías declarando es otra cosa.
|
|
18
18
|
Tampoco hay `semanal`, porque el slug de cada vuelta tiene grano de mes.
|
|
19
|
-
- **Desde** — `AAAA-MM-DD`. Ancla la primera vuelta y después no se toca.
|
|
19
|
+
- **Desde** — `AAAA-MM-DD`. Ancla la primera vuelta y después no se toca. Es la primera fecha en que
|
|
20
|
+
vence, no el día en que se escribe la fila: con la fecha de hoy nace vencida.
|
|
20
21
|
- **Tarea y aceptación** — lo que se va a promover, con su aceptación observable escrita una sola vez y
|
|
21
22
|
con calma. Improvisada en cada vuelta, la misma recurrencia termina significando cosas distintas sin
|
|
22
23
|
que nadie lo decida.
|
|
@@ -47,12 +48,12 @@ slug son un error de `check`, y ese error llega un mes tarde.
|
|
|
47
48
|
|
|
48
49
|
| Qué | Cada | Desde | Tarea y aceptación observable |
|
|
49
50
|
|---|---|---|---|
|
|
51
|
+
| inbox | trimestral | {{INBOX_SINCE}} | Recorrer el INBOX entero. _Aceptación: ninguna viñeta queda sin decisión de promover, dejar o borrar._ (service: planning) |
|
|
50
52
|
|
|
51
53
|
<!--
|
|
52
|
-
| deps | mensual | 2026-09-01 | Actualizar dependencias. _Aceptación: `npm outdated` no deja una versión mayor sin decisión escrita y la puerta queda verde._ |
|
|
53
|
-
| accesos | trimestral | 2026-07-01 | Revisar quién tiene acceso a producción. _Aceptación: cada cuenta activa figura en `organization/`, y las demás están dadas de baja._ |
|
|
54
|
-
| costos | mensual | 2026-09-01 | Revisar el gasto de infraestructura del mes. _Aceptación: cada línea que subió más de 20% tiene una razón escrita._ |
|
|
55
|
-
| inbox | trimestral | 2026-08-01 | Recorrer el INBOX entero. _Aceptación: ninguna viñeta queda sin decisión de promover, dejar o borrar._ |
|
|
54
|
+
| deps | mensual | 2026-09-01 | Actualizar dependencias. _Aceptación: `npm outdated` no deja una versión mayor sin decisión escrita y la puerta queda verde._ (service: .) |
|
|
55
|
+
| accesos | trimestral | 2026-07-01 | Revisar quién tiene acceso a producción. _Aceptación: cada cuenta activa figura en `organization/`, y las demás están dadas de baja._ (service: organization) |
|
|
56
|
+
| costos | mensual | 2026-09-01 | Revisar el gasto de infraestructura del mes. _Aceptación: cada línea que subió más de 20% tiene una razón escrita._ (service: .) |
|
|
56
57
|
-->
|
|
57
58
|
|
|
58
59
|
## Postergaciones
|
|
@@ -65,7 +65,9 @@ dice qué había que hacer, no qué se apoyaba en lo que había.
|
|
|
65
65
|
|
|
66
66
|
Push, PR, merge, tags, deploy y rollback requieren la autorización configurada para el proyecto.
|
|
67
67
|
|
|
68
|
-
De esos seis, el motor comprueba uno: el push, contra `runner.allowPush
|
|
68
|
+
De esos seis, el motor comprueba uno: el push, contra `runner.allowPush` —que no llega a la rama viva
|
|
69
|
+
sin `runner.pushToLiveBranches`, ni a un subagente— o contra la orden que la persona da en el chat
|
|
70
|
+
nombrando el remoto y la rama. Reescribir historia publicada
|
|
69
71
|
no entra en esa autorización y se frena siempre, igual que `--amend`. Los otros cinco no tienen una
|
|
70
72
|
forma reconocible en un comando —un deploy es `kubectl`, `terraform`, un script o un botón— y los
|
|
71
73
|
sostiene esta regla y el review, no un guard.
|