@ingeniomaps/cauce 0.67.0 → 0.69.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 +50 -0
- package/README.md +7 -0
- package/engine/cli/args.js +2 -1
- package/engine/cli/instance.js +16 -1
- package/engine/cli/ops.js +2 -0
- package/engine/cli/planning.js +64 -2
- package/engine/cli/upgrade-report.js +17 -6
- package/engine/core/ownership.js +12 -0
- package/engine/planning/parser.js +38 -21
- package/engine/planning/recurring.js +148 -0
- package/package.json +1 -1
- package/template/AGENTS.md +9 -2
- package/template/Makefile +4 -1
- package/template/planning/FLOW.md +2 -0
- package/template/planning/PROTOCOL.md +5 -0
- package/template/planning/README.md +1 -0
- package/template/planning/RECURRING.md +77 -0
- package/template/planning/rules/system/commits.md +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,56 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.69.0] - 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **`planning/RECURRING.md`: el trabajo que vuelve se declara una vez.** Actualizar dependencias, revisar
|
|
22
|
+
quién tiene acceso a producción, mirar el gasto del mes: una fila con su cadencia —`mensual`,
|
|
23
|
+
`trimestral`, `semestral` o `anual`— y la celda de tarea escrita como la cola de su línea de BACKLOG,
|
|
24
|
+
así que la aceptación se decide una vez y no se improvisa en cada vuelta.
|
|
25
|
+
|
|
26
|
+
**Nada se dispara.** No hay cron ni cola: el vencimiento se calcula cuando alguien corre el CLI, y rueda
|
|
27
|
+
desde el período que cerró la última vuelta en `DONE.md` en vez de una celda que haya que acordarse de
|
|
28
|
+
actualizar. `node tools/ops.js recurring planning` dice qué venció, y `--promote <qué>` emite la línea de
|
|
29
|
+
esa vuelta —la emite y no la escribe: pegarla en `BACKLOG.md` es el acto de promoción—. `check` rechaza
|
|
30
|
+
la fila ilegible y avisa la vencida sin frenar nada; `context` la nombra con `DUE`. Postergar se escribe
|
|
31
|
+
a mano con su razón, compra un período, y tres seguidas se avisan porque ahí lo que falla es la cadencia.
|
|
32
|
+
|
|
33
|
+
**Lo que te pide algo**: el archivo llega vacío con esta actualización y, hasta que declares una fila, el
|
|
34
|
+
motor no dice una palabra. Y si tu runner ya está andando, la regla nueva que necesita está en
|
|
35
|
+
`AGENTS.md`: una recurrencia vencida no la promueve él, por más que `context` la nombre sola.
|
|
36
|
+
|
|
37
|
+
## [0.68.0] - 2026-09-07
|
|
38
|
+
|
|
39
|
+
### Cambiado
|
|
40
|
+
|
|
41
|
+
- **`upgrade` deja de borrar una ruta retirada cuyo contenido no puede probar suyo.** Retirar una ruta
|
|
42
|
+
nunca volvió al toolkit dueño de lo que hay adentro, y hasta acá se borraba el directorio entero: quien
|
|
43
|
+
tenía sus propios workflows en `automatization/workflows` los perdía sin confirmación y sin vuelta
|
|
44
|
+
atrás. De las seis rutas retiradas, cuatro viven bajo `system/` o son un archivo con nombre del
|
|
45
|
+
toolkit y se siguen retirando igual; las dos que un proyecto también usa para lo suyo
|
|
46
|
+
—`automatization/runners` y `automatization/workflows`— se conservan.
|
|
47
|
+
|
|
48
|
+
**Lo que te pide algo**: esas dos rutas quedan en disco con lo que tengas adentro, la corrida te dice
|
|
49
|
+
cuántos archivos son, y `check` las cuenta en cada corrida hasta que las muevas o las borres. Si lo
|
|
50
|
+
que tenés ahí son restos viejos del toolkit y querés limpiarlos, `--force` las retira.
|
|
51
|
+
|
|
52
|
+
- **La regla R9 dice cómo se prueba una quita.** Una prueba que comprueba que aparece lo nuevo no
|
|
53
|
+
comprueba que desapareció lo viejo, y lo que se quita tiene dependientes que no se anuncian —el
|
|
54
|
+
mensaje que afirmaba la invariante, la condición que la deducía—. Sale de una regresión de 0.67.0 que
|
|
55
|
+
entró exactamente por ahí.
|
|
56
|
+
|
|
57
|
+
### Corregido
|
|
58
|
+
|
|
59
|
+
- **El informe de `upgrade` afirmaba descartes que no ocurrieron.** 0.67.0 pasó a conservar los archivos
|
|
60
|
+
editados y la salida siguió enumerándolos como `− descartado tu cambio en …`, uno por uno, después de
|
|
61
|
+
la línea que anuncia que terminó bien. En una instancia real fueron diecinueve renglones falsos, y
|
|
62
|
+
entre ellos iba la única línea destructiva verdadera de esa corrida, indistinguible. Ahora el informe
|
|
63
|
+
recibe qué pasó —descartado, conservado, pendiente— en vez de deducirlo de una condición que dejó de
|
|
64
|
+
valer, y la línea «planning, organization y todo lo propio quedaron intactos» sale justo cuando es
|
|
65
|
+
cierto, que es cuando se conservó algo.
|
|
66
|
+
|
|
17
67
|
## [0.67.0] - 2026-09-07
|
|
18
68
|
|
|
19
69
|
### Agregado
|
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ el contexto de cada empresa vive en su propia instancia.
|
|
|
9
9
|
- Una tarea tiene una sola fuente de verdad durante todo su ciclo de vida.
|
|
10
10
|
- Una sesión interrumpida se recupera desde `WIP.md`, sin reconstruir la intención.
|
|
11
11
|
- Las ideas del agente no entran solas a la cola: quedan en `INBOX.md` hasta promoción humana.
|
|
12
|
+
- El trabajo que vuelve cada tanto se declara una vez en `RECURRING.md`; el CLI dice cuándo venció y
|
|
13
|
+
nadie lo encola solo.
|
|
12
14
|
- Épicas, criterios, tareas y evidencia son validados de forma determinista.
|
|
13
15
|
- Vive en su propia carpeta `ops/` dentro del repo, como sidecar `proyecto-ops` para varios repos, o
|
|
14
16
|
embebido en la raíz.
|
|
@@ -175,6 +177,11 @@ idea → INBOX → roadmap → BACKLOG → WIP → DONE → done/epic-NNN.md
|
|
|
175
177
|
5. Tras Build, Review, Verify y QA, mueve la entrada a `DONE.md` con evidencia.
|
|
176
178
|
6. Al cerrar la épica, ejecuta `ops archive` para mover su evidencia a un histórico inmutable.
|
|
177
179
|
|
|
180
|
+
Lo que vuelve cada tanto —actualizar dependencias, revisar accesos, mirar el gasto del mes— entra por
|
|
181
|
+
un costado: se declara una vez en `RECURRING.md` con su cadencia, y `ops recurring planning` dice qué
|
|
182
|
+
venció y emite la línea de esa vuelta. Nada se dispara; pegarla en `BACKLOG.md` es el paso 3 de
|
|
183
|
+
arriba, hecho por una persona.
|
|
184
|
+
|
|
178
185
|
Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contrato completo y
|
|
179
186
|
[template/planning/FLOW.md](template/planning/FLOW.md) para operar el ciclo.
|
|
180
187
|
|
package/engine/cli/args.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Banderas que consumen el argumento siguiente: su valor no es un posicional.
|
|
8
8
|
const VALUED_FLAGS = new Set([
|
|
9
9
|
'--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
|
|
10
|
-
'--task',
|
|
10
|
+
'--task', '--promote',
|
|
11
11
|
])
|
|
12
12
|
|
|
13
13
|
// Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
|
|
@@ -20,6 +20,7 @@ const FLAGS = {
|
|
|
20
20
|
check: ['--json'],
|
|
21
21
|
tree: ['--json', '--no-color'],
|
|
22
22
|
context: ['--json'],
|
|
23
|
+
recurring: ['--json', '--promote'],
|
|
23
24
|
evidence: ['--json', '--task'],
|
|
24
25
|
upgrade: ['--check', '--force'],
|
|
25
26
|
destroy: ['--force'],
|
package/engine/cli/instance.js
CHANGED
|
@@ -370,10 +370,19 @@ function upgrade(dir, cli) {
|
|
|
370
370
|
|
|
371
371
|
// Retirar lo que el toolkit ya no distribuye, después de haber actualizado lo que sí.
|
|
372
372
|
const retired = []
|
|
373
|
+
const pendientes = []
|
|
373
374
|
for (const relative of O.RETIRED) {
|
|
374
375
|
const target = path.join(root, relative)
|
|
375
376
|
if (!fs.existsSync(target)) continue
|
|
376
377
|
F.assertNoSymlinkPath(root, target)
|
|
378
|
+
// Retirar una ruta no vuelve al toolkit dueño de lo que hay adentro. En las dos que un proyecto
|
|
379
|
+
// también usa para lo suyo, borrar el directorio entero se llevaba puesto contenido que nadie
|
|
380
|
+
// había entregado —un `autobuild.js` propio, los workflows de una empresa— sin confirmación y sin
|
|
381
|
+
// vuelta atrás. Se conserva y se nombra; `--force` es la salida, igual que para una edición local.
|
|
382
|
+
const contenido = O.RETIRED_COMPARTIDO.includes(relative) && fs.statSync(target).isDirectory()
|
|
383
|
+
? O.treeFiles(target)
|
|
384
|
+
: []
|
|
385
|
+
if (contenido.length && !force) { pendientes.push({ relative, files: contenido }); continue }
|
|
377
386
|
fs.rmSync(target, { recursive: true, force: true })
|
|
378
387
|
retired.push(relative)
|
|
379
388
|
}
|
|
@@ -404,7 +413,13 @@ function upgrade(dir, cli) {
|
|
|
404
413
|
// Acá es donde las tres versiones se saben a la vez, así que es donde pueden quedar diciendo lo mismo.
|
|
405
414
|
const pinned = pinEngine(root, to)
|
|
406
415
|
|
|
407
|
-
|
|
416
|
+
// El informe recibe qué pasó y no una condición de la que deducirlo: `changed` es «lo que estaba
|
|
417
|
+
// editado», y sin `--force` eso se conserva. Deducirlo hizo que 0.67.0 afirmara diecinueve descartes
|
|
418
|
+
// que no ocurrieron (caso 048).
|
|
419
|
+
reportUpgrade({
|
|
420
|
+
root, from, to, system, retired, added, overrides, pinned, droppedBlocks,
|
|
421
|
+
descartados: force ? changed : [], conservados: [...conservados], pendientes,
|
|
422
|
+
})
|
|
408
423
|
}
|
|
409
424
|
|
|
410
425
|
module.exports = { copyTemplate, scaffold, providerNames, upgrade, destroy, PROJECT_ROOT }
|
package/engine/cli/ops.js
CHANGED
|
@@ -143,6 +143,7 @@ function usage() {
|
|
|
143
143
|
ops check <planning-dir> [--json]
|
|
144
144
|
ops tree <planning-dir> [--no-color] [--json]
|
|
145
145
|
ops context <planning-dir> [--json]
|
|
146
|
+
ops recurring <planning-dir> [--promote <qué>] [--json]
|
|
146
147
|
ops evidence <planning-dir> [--task <slug>] [--json]
|
|
147
148
|
ops upgrade <ops-root> [--check] [--force]
|
|
148
149
|
ops destroy <ops-root> [--force]
|
|
@@ -196,6 +197,7 @@ async function run(cli) {
|
|
|
196
197
|
else if (command === 'check') PL.check(arg[1], cli)
|
|
197
198
|
else if (command === 'tree') PL.tree(arg[1], cli)
|
|
198
199
|
else if (command === 'context') PL.context(arg[1], cli)
|
|
200
|
+
else if (command === 'recurring') PL.recurring(arg[1], cli)
|
|
199
201
|
else if (command === 'evidence') PL.evidence(arg[1], cli)
|
|
200
202
|
else if (command === 'upgrade') IN.upgrade(arg[1], cli)
|
|
201
203
|
else if (command === 'destroy') IN.destroy(arg[1], cli)
|
package/engine/cli/planning.js
CHANGED
|
@@ -9,6 +9,7 @@ const P = require('../planning/parser')
|
|
|
9
9
|
const B = require('../planning/business-rules')
|
|
10
10
|
const PC = require('../planning/contracts')
|
|
11
11
|
const SZ = require('../planning/sizing')
|
|
12
|
+
const RC = require('../planning/recurring')
|
|
12
13
|
const ST = require('../planning/state')
|
|
13
14
|
const AD = require('../planning/adoption')
|
|
14
15
|
const AP = require('../hooks/approval')
|
|
@@ -66,6 +67,10 @@ function evidence(dir, cli) {
|
|
|
66
67
|
+ 'que la prueba nombrada haya corrido: eso depende del runner, y varios no la nombran al pasar.')
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// La fecha de hoy, en un solo lugar: los comandos que la usan tienen que estar mirando el mismo día, y
|
|
71
|
+
// el módulo que calcula vencimientos la recibe en vez de preguntarla.
|
|
72
|
+
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
73
|
+
|
|
69
74
|
function check(dir, cli) {
|
|
70
75
|
const root = path.resolve(dir || '.')
|
|
71
76
|
const errors = []
|
|
@@ -125,6 +130,12 @@ function check(dir, cli) {
|
|
|
125
130
|
epics, milestones, done, wip, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
|
|
126
131
|
}))
|
|
127
132
|
warnings.push(...AD.report({ done, epics, adopted }))
|
|
133
|
+
// Sin `RECURRING.md` no dice una palabra: una instancia que actualiza y no declara trabajo recurrente
|
|
134
|
+
// no tiene por qué enterarse de que el contrato existe. Vencida avisa y no frena — lo que frena vive
|
|
135
|
+
// en `HUMAN_ACTIONS.md`, y un aviso que salta siempre se termina apagando.
|
|
136
|
+
const recurring = RC.read(root)
|
|
137
|
+
errors.push(...RC.validate(recurring))
|
|
138
|
+
warnings.push(...RC.warnings(RC.status({ ...recurring, done, today: TODAY() })))
|
|
128
139
|
warnings.push(...AD.sealWarnings(root))
|
|
129
140
|
// Una aprobación vale para el conjunto que nombra, así que olvidada sigue autorizando
|
|
130
141
|
// esas mismas rutas la próxima vez que alguien las stagee. No caduca sola: lo que la cierra es que se
|
|
@@ -144,6 +155,15 @@ function check(dir, cli) {
|
|
|
144
155
|
+ '`upgrade` los conserva y no les trae mejoras')
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
// Y lo que `upgrade` no retiró porque no pudo demostrar que fuera suyo: queda ahí, sin colgar de
|
|
159
|
+
// ningún mecanismo, hasta que alguien lo mueva o lo borre. Se cuenta por lo mismo que los congelados
|
|
160
|
+
// — un resto que no se ve se vuelve permanente.
|
|
161
|
+
const restos = O.RETIRED_COMPARTIDO.filter((relative) => fs.existsSync(path.join(root, '..', relative)))
|
|
162
|
+
if (restos.length) {
|
|
163
|
+
warnings.push(`${restos.length} ruta(s) retiradas siguen en disco con contenido tuyo `
|
|
164
|
+
+ `(${restos.join(', ')}); Cauce ya no las distribuye ni las toca`)
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
const integration = I.validate(path.resolve(root, '..'))
|
|
148
168
|
errors.push(...integration.errors)
|
|
149
169
|
warnings.push(...integration.warnings)
|
|
@@ -275,6 +295,11 @@ function context(dir, cli) {
|
|
|
275
295
|
queued: state.milestones.reduce((total, milestone) => total + milestone.tasks.length, 0),
|
|
276
296
|
blockedTasks: skipped,
|
|
277
297
|
humanActions,
|
|
298
|
+
// Sólo las vencidas: la fila que todavía no vence no tiene nada que decirle a quien va a tomar una
|
|
299
|
+
// tarea, y una recurrencia que hablara siempre sería ruido en el único comando que se corre en cada
|
|
300
|
+
// vuelta. Que aparezca es la señal.
|
|
301
|
+
recurring: RC.status({ ...RC.read(root), done: state.done, today: TODAY() })
|
|
302
|
+
.filter((one) => one.overdue),
|
|
278
303
|
}
|
|
279
304
|
if (cli.has('--json')) return console.log(JSON.stringify(report))
|
|
280
305
|
|
|
@@ -290,9 +315,16 @@ function context(dir, cli) {
|
|
|
290
315
|
// que una instancia recién arrancada —`onboard` deja filas pendientes y ninguna tarea todavía—
|
|
291
316
|
// respondía «sin tarea disponible» y se tragaba las siete cosas que una persona tenía que desbloquear.
|
|
292
317
|
// Es el comando que existe para decir qué toca ahora, contestando «nada» cuando lo que toca es eso.
|
|
318
|
+
const due = () => {
|
|
319
|
+
for (const one of report.recurring) {
|
|
320
|
+
const when = one.overdueDays === 0 ? 'vence hoy' : `vencida hace ${one.overdueDays} día(s)`
|
|
321
|
+
console.log(`DUE ${one.id}: ${when}`)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
293
324
|
if (!report.task) {
|
|
294
325
|
console.log('TASK (sin tarea disponible)')
|
|
295
326
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
327
|
+
due()
|
|
296
328
|
return
|
|
297
329
|
}
|
|
298
330
|
console.log(`TASK ${report.task.slug}${report.task.tier ? ` [${report.task.tier}]` : ''}` +
|
|
@@ -315,6 +347,36 @@ function context(dir, cli) {
|
|
|
315
347
|
console.log(`WIP ${wip}`)
|
|
316
348
|
if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
|
|
317
349
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
350
|
+
due()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Qué trabajo recurrente vence, y la línea con la que se promueve. Emite esa línea y no la escribe:
|
|
354
|
+
// `BACKLOG.md` es la cola de lo aprobado y la escribe una persona — ningún comando del motor la toca,
|
|
355
|
+
// ni siquiera `integration promote`, que aterriza en el roadmap. Pegarla es el acto de promoción.
|
|
356
|
+
function recurring(dir, cli) {
|
|
357
|
+
const root = path.resolve(dir || '.')
|
|
358
|
+
const file = RC.read(root)
|
|
359
|
+
if (!file.exists) return console.log(`= este planning no declara trabajo recurrente (${RC.FILE})`)
|
|
360
|
+
const state = RC.status({ ...file, done: P.readDone(root), today: TODAY() })
|
|
361
|
+
const promote = cli.value('--promote')
|
|
362
|
+
if (promote) {
|
|
363
|
+
const one = state.find((candidate) => candidate.id === promote)
|
|
364
|
+
if (!one) return fail(`${RC.FILE} no declara ${promote}`, 2)
|
|
365
|
+
// La línea sale sola por stdout para que se pueda pegar o redirigir sin recortar nada; el destino,
|
|
366
|
+
// que es lo único que falta decidir, va por stderr.
|
|
367
|
+
console.error(`Pegala en el hito que corresponda de BACKLOG.md:`)
|
|
368
|
+
return console.log(RC.taskLine(one, TODAY().slice(0, 7)))
|
|
369
|
+
}
|
|
370
|
+
if (cli.has('--json')) return console.log(JSON.stringify(state))
|
|
371
|
+
if (!state.length) return console.log(`= ${RC.FILE} no declara ninguna fila legible`)
|
|
372
|
+
for (const one of state) {
|
|
373
|
+
const when = one.overdue
|
|
374
|
+
? (one.overdueDays === 0 ? 'vence hoy' : `vencida hace ${one.overdueDays} día(s)`)
|
|
375
|
+
: `vence ${one.due}`
|
|
376
|
+
const last = one.last ? `última ${one.last}` : 'nunca corrió'
|
|
377
|
+
const held = one.postponed ? `, postergada ${one.postponed}` : ''
|
|
378
|
+
console.log(`${one.overdue ? 'DUE' : 'OK '} ${one.id.padEnd(16)} ${when} (${last}${held})`)
|
|
379
|
+
}
|
|
318
380
|
}
|
|
319
381
|
|
|
320
382
|
// El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
|
|
@@ -346,7 +408,7 @@ function adopt(dir) {
|
|
|
346
408
|
if (!pending.length) {
|
|
347
409
|
return console.log('= no hay nada que exentar: todas las entradas de DONE cumplen el contrato')
|
|
348
410
|
}
|
|
349
|
-
const today =
|
|
411
|
+
const today = TODAY()
|
|
350
412
|
const slugs = pending.map((entry) => entry.slug)
|
|
351
413
|
F.atomicWrite(target, `# Entradas anteriores a la adopción de Cauce (${today}). No se agregan nuevas:\n`
|
|
352
414
|
+ '# desde esa fecha rige el contrato completo, y `check` avisa cuando una de éstas pasa a\n'
|
|
@@ -404,4 +466,4 @@ function archive(dir, rawNum) {
|
|
|
404
466
|
console.log(`✓ epic-${num}: ${entries.length} entrada(s) archivadas`)
|
|
405
467
|
}
|
|
406
468
|
|
|
407
|
-
module.exports = { check, evidence, tree, context, archive, adopt }
|
|
469
|
+
module.exports = { check, evidence, tree, context, archive, adopt, recurring }
|
|
@@ -72,12 +72,23 @@ function adviceFor(changed) {
|
|
|
72
72
|
// propias, agentes editados— queda intacto por construcción, no por comparación.
|
|
73
73
|
// Qué le pasó a la instancia en esta actualización, contado a quien la corrió. Recibe el resultado
|
|
74
74
|
// entero en vez de recalcular nada: lo que se informa es exactamente lo que ocurrió.
|
|
75
|
-
function reportUpgrade({
|
|
75
|
+
function reportUpgrade({
|
|
76
|
+
root, from, to, system, retired, added, overrides, pinned, droppedBlocks,
|
|
77
|
+
descartados, conservados, pendientes,
|
|
78
|
+
}) {
|
|
76
79
|
console.log(`✓ Cauce ${from || '(previa)'} → ${to}`)
|
|
77
80
|
// Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
|
|
78
|
-
// que es la evidencia que el protocolo pide para cualquier cambio.
|
|
79
|
-
|
|
81
|
+
// que es la evidencia que el protocolo pide para cualquier cambio. Y lo que se conservó no se vuelve
|
|
82
|
+
// a enumerar acá: ya salió con su consejo antes de escribir nada, y repetirlo con el glifo del
|
|
83
|
+
// descarte es lo que volvía ilegible el bloque.
|
|
84
|
+
for (const file of descartados) console.log(`− descartado tu cambio en ${file}`)
|
|
85
|
+
if (conservados.length) console.log(`= ${conservados.length} archivo(s) conservados por tu edición`)
|
|
80
86
|
for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
|
|
87
|
+
// Sin glifo de acción porque no hubo ninguna: la ruta sigue ahí y el contenido también.
|
|
88
|
+
for (const { relative, files } of pendientes) {
|
|
89
|
+
console.log(` ${relative}: ${files.length} archivo(s) que Cauce no entregó; la ruta se retiró y `
|
|
90
|
+
+ 'el contenido queda. Movelo adonde lo quieras y borrala, o repetí con --force.')
|
|
91
|
+
}
|
|
81
92
|
// Nombrarlos importa tanto como crearlos: existen para que los completes, y uno que aparece sin
|
|
82
93
|
// aviso no lo completa nadie.
|
|
83
94
|
for (const relative of added) console.log(`+ ${relative}: lo agrega esta versión, completalo`)
|
|
@@ -90,9 +101,9 @@ function reportUpgrade({ root, from, to, system, changed, retired, added, overri
|
|
|
90
101
|
for (const override of overrides) {
|
|
91
102
|
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
92
103
|
}
|
|
93
|
-
// Sólo cuando es cierto
|
|
94
|
-
//
|
|
95
|
-
if (!
|
|
104
|
+
// Sólo cuando es cierto, y ahora lo decide lo que pasó y no una condición: se dice justo cuando no
|
|
105
|
+
// se descartó nada, que incluye la corrida que conservó veinte archivos.
|
|
106
|
+
if (!descartados.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
96
107
|
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
97
108
|
// decide una persona.
|
|
98
109
|
if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
|
package/engine/core/ownership.js
CHANGED
|
@@ -205,6 +205,9 @@ const TEMPLATE_OWN = {
|
|
|
205
205
|
'planning/DONE.md': 'init',
|
|
206
206
|
'planning/HUMAN_ACTIONS.md': 'init',
|
|
207
207
|
'planning/INBOX.md': 'init',
|
|
208
|
+
// 0.69.0. El contrato nace con esta versión, así que ninguna instancia anterior lo tiene: por `init`
|
|
209
|
+
// no llegaría nunca a la que ya existe, que es justo la que iba a usarlo.
|
|
210
|
+
'planning/RECURRING.md': 'upgrade',
|
|
208
211
|
'planning/WIP.md': 'init',
|
|
209
212
|
'planning/delivery/project.md': 'init',
|
|
210
213
|
'planning/done/.gitkeep': 'init',
|
|
@@ -231,6 +234,14 @@ const RETIRED = [
|
|
|
231
234
|
|
|
232
235
|
// Aprendizaje que quedó dentro de una ruta retirada. Es lo único ahí que no se puede reponer, así
|
|
233
236
|
// que se detecta antes de borrar nada: perderlo en silencio sería peor que dejar el directorio.
|
|
237
|
+
// De las rutas retiradas, las que un proyecto también usa para lo suyo. El resto de la lista vive bajo
|
|
238
|
+
// `system/` o es un archivo con nombre propio del toolkit, y ahí nadie más escribe; éstas dos se llaman
|
|
239
|
+
// como el concepto general, así que quien adoptó Cauce cuando se distribuían tiene los suyos justo ahí.
|
|
240
|
+
//
|
|
241
|
+
// El manifiesto no las puede desempatar: `trackedPaths()` nunca las registró, así que para el toolkit
|
|
242
|
+
// todo su contenido es igual de ajeno. Sin poder demostrar que lo entregó, no lo borra.
|
|
243
|
+
const RETIRED_COMPARTIDO = ['automatization/runners', 'automatization/workflows']
|
|
244
|
+
|
|
234
245
|
function retiredWithLearning(root) {
|
|
235
246
|
const found = []
|
|
236
247
|
for (const relative of RETIRED) {
|
|
@@ -294,6 +305,7 @@ function localChanges(root) {
|
|
|
294
305
|
|
|
295
306
|
module.exports = {
|
|
296
307
|
RETIRED,
|
|
308
|
+
RETIRED_COMPARTIDO,
|
|
297
309
|
TEMPLATE_OWN,
|
|
298
310
|
TEMPLATE_PREFIXES,
|
|
299
311
|
addedPaths,
|
|
@@ -193,6 +193,26 @@ function acceptanceConditions(value) {
|
|
|
193
193
|
return text.split(';').map((one) => one.trim()).filter(Boolean).length
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// Una línea de tarea, leída en un solo lugar: `readBacklog` la usa para armar la cola y `recurring.js`
|
|
197
|
+
// para juzgar la línea que va a emitir. Es el mismo motivo por el que `TASK_LINE` no está duplicado —
|
|
198
|
+
// lo que emite una y lee la otra tiene que ser la misma forma, o el emisor produce lo que el lector
|
|
199
|
+
// descarta.
|
|
200
|
+
function taskFromLine(line) {
|
|
201
|
+
const task = line.match(TASK_LINE)
|
|
202
|
+
if (!task) return null
|
|
203
|
+
const rest = task[3]
|
|
204
|
+
const acceptance = ((rest.match(ACCEPTANCE) || [])[1] || '').trim()
|
|
205
|
+
return {
|
|
206
|
+
slug: task[1].trim(), tier: task[2] || '', cast: readCast(rest),
|
|
207
|
+
epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
|
|
208
|
+
service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
|
|
209
|
+
acceptance,
|
|
210
|
+
conditions: acceptanceConditions(acceptance),
|
|
211
|
+
criteria: criteriaRefs(rest),
|
|
212
|
+
noSplit: noSplitReason(rest),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
196
216
|
function readBacklog(dir) {
|
|
197
217
|
const text = withoutComments(read(path.join(dir, 'BACKLOG.md')))
|
|
198
218
|
const milestones = []
|
|
@@ -208,19 +228,8 @@ function readBacklog(dir) {
|
|
|
208
228
|
continue
|
|
209
229
|
}
|
|
210
230
|
if (/^##\s+/.test(line)) current = null
|
|
211
|
-
const task = line
|
|
212
|
-
if (
|
|
213
|
-
const rest = task[3]
|
|
214
|
-
const acceptance = ((rest.match(ACCEPTANCE) || [])[1] || '').trim()
|
|
215
|
-
current.tasks.push({
|
|
216
|
-
slug: task[1].trim(), tier: task[2] || '', cast: readCast(rest),
|
|
217
|
-
epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
|
|
218
|
-
service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
|
|
219
|
-
acceptance,
|
|
220
|
-
conditions: acceptanceConditions(acceptance),
|
|
221
|
-
criteria: criteriaRefs(rest),
|
|
222
|
-
noSplit: noSplitReason(rest),
|
|
223
|
-
})
|
|
231
|
+
const task = current ? taskFromLine(line) : null
|
|
232
|
+
if (task) current.tasks.push(task)
|
|
224
233
|
}
|
|
225
234
|
return milestones
|
|
226
235
|
}
|
|
@@ -302,16 +311,24 @@ const SEPARADORES = /^\|\s*:?-+/
|
|
|
302
311
|
// afectada —`archive` reescribe `raw`, la línea original, no las celdas—, así que quitarlo no pierde nada.
|
|
303
312
|
const celda = (cell) => cell.trim().replace(/\\\|/g, '|')
|
|
304
313
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
314
|
+
// Las filas de datos de las tablas de un texto, con las celdas ya normalizadas. Vive acá y no en cada
|
|
315
|
+
// lector porque el escape del pipe tiene que leerse igual en los dos archivos que traen tabla: escrito
|
|
316
|
+
// dos veces, una de las dos copias se pudre sin que nada falle.
|
|
317
|
+
//
|
|
318
|
+
// En markdown la cabecera es la fila anterior a la de separadores, diga lo que diga su primera celda.
|
|
319
|
+
// Se marcan todas y no la primera: un archivo con una tabla por sección tiene una cabecera por tabla,
|
|
320
|
+
// y con `findIndex` la segunda y la tercera vuelven a leerse como datos. Nada más se mueve, porque una
|
|
321
|
+
// fila de datos nunca está inmediatamente antes de los guiones.
|
|
322
|
+
function tableRows(text) {
|
|
323
|
+
const lineas = text.split('\n')
|
|
311
324
|
const cabeceras = new Set(lineas.map((line, i) => (SEPARADORES.test(line) ? i - 1 : -1)))
|
|
312
|
-
|
|
325
|
+
return lineas
|
|
313
326
|
.filter((line, i) => /^\|/.test(line) && !SEPARADORES.test(line) && !cabeceras.has(i))
|
|
314
327
|
.map((line) => ({ line, cells: line.split(SEPARADOR).slice(1, -1).map(celda) }))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function readHumanActions(dir) {
|
|
331
|
+
const rows = tableRows(withoutComments(read(path.join(dir, 'HUMAN_ACTIONS.md'))))
|
|
315
332
|
// El literal queda como resguardo de la tabla escrita sin su fila de separadores: markdown no la
|
|
316
333
|
// renderiza como tabla, y este parser lee sus filas igual.
|
|
317
334
|
return rows.filter(({ cells }) => cells.length >= 4 && !/^tarea$/i.test(cells[0]))
|
|
@@ -362,6 +379,6 @@ module.exports = {
|
|
|
362
379
|
EPIC_STATES, HUMAN_ACTION_STATES, LANES, MILESTONE_HEADING, STOP_REASONS,
|
|
363
380
|
TASK_LINE, TASK_LINE_ANY_LANE,
|
|
364
381
|
read, section, withoutComments, frontmatter, readEpics, readBacklog, readDone, readWip,
|
|
365
|
-
acceptanceConditions,
|
|
382
|
+
acceptanceConditions, tableRows, taskFromLine,
|
|
366
383
|
readInbox, readHumanActions,
|
|
367
384
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// El contrato de `RECURRING.md`: qué se declaró que vuelve, cuándo vence y qué línea se promueve.
|
|
4
|
+
// No ejecuta ni encola —eso lo decide una persona editando `BACKLOG.md`—; acá se lee el archivo y se
|
|
5
|
+
// calcula una fecha, que es todo lo que la máquina puede aportar sin decidir por nadie.
|
|
6
|
+
//
|
|
7
|
+
// La fecha de hoy entra por parámetro. Preguntarla adentro dejaría cada prueba de vencimiento válida
|
|
8
|
+
// sólo el día que se escribió, y el vencimiento es justamente lo único que este módulo calcula.
|
|
9
|
+
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
const P = require('./parser')
|
|
12
|
+
|
|
13
|
+
const FILE = 'RECURRING.md'
|
|
14
|
+
|
|
15
|
+
// Vocabulario cerrado, en meses, por la misma razón que el estado de `HUMAN_ACTIONS.md`. `semanal` no
|
|
16
|
+
// está y no es un olvido: el período viaja en el slug de cada vuelta con grano de mes, así que dos
|
|
17
|
+
// vueltas de la misma semana no se distinguirían.
|
|
18
|
+
const CADENCES = { mensual: 1, trimestral: 3, semestral: 6, anual: 12 }
|
|
19
|
+
|
|
20
|
+
const ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
21
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/
|
|
22
|
+
// `- **qué** AAAA-MM-DD — razón`. El nombre en negrita adelante es la misma convención del INBOX, y por
|
|
23
|
+
// el mismo motivo: es con lo que se cita la fila desde otro lado.
|
|
24
|
+
const POSTPONEMENT = /^-\s+\*\*([^*]+)\*\*\s+(\S+)\s+[—-]\s+(.+)$/
|
|
25
|
+
|
|
26
|
+
// Suma meses sobre `AAAA-MM-DD` en UTC. El día se recorta al último del mes destino: un ancla escrita
|
|
27
|
+
// el 31 no puede caer en un 31 de febrero, y correrla al 3 de marzo movería la vuelta de mes.
|
|
28
|
+
function addMonths(iso, months) {
|
|
29
|
+
const [year, month, day] = iso.split('-').map(Number)
|
|
30
|
+
const target = new Date(Date.UTC(year, month - 1 + months, 1))
|
|
31
|
+
const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate()
|
|
32
|
+
target.setUTCDate(Math.min(day, last))
|
|
33
|
+
return target.toISOString().slice(0, 10)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function daysBetween(from, to) {
|
|
37
|
+
return Math.round((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86400000)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Las dos listas del archivo, cada una acotada a su sección. Buscarlas en el texto entero haría que una
|
|
41
|
+
// viñeta de la prosa —el archivo explica sus propias columnas con viñetas— entrara como postergación.
|
|
42
|
+
function read(dir) {
|
|
43
|
+
const text = P.withoutComments(P.read(path.join(dir, FILE)))
|
|
44
|
+
if (!text.trim()) return { exists: false, rows: [], postponements: [] }
|
|
45
|
+
const rows = P.tableRows(P.section(text, /Recurrencias/))
|
|
46
|
+
// El literal, por el mismo motivo que en `readHumanActions`, donde está escrito.
|
|
47
|
+
.filter(({ cells }) => cells.length >= 4 && !/^qué$/i.test(cells[0]))
|
|
48
|
+
.map(({ line, cells }) => ({
|
|
49
|
+
id: cells[0], cadence: cells[1], since: cells[2], task: cells[3], raw: line,
|
|
50
|
+
}))
|
|
51
|
+
const postponements = P.section(text, /Postergaciones/).split('\n')
|
|
52
|
+
.filter((line) => /^-\s/.test(line))
|
|
53
|
+
.map((line) => {
|
|
54
|
+
const match = line.match(POSTPONEMENT)
|
|
55
|
+
return match
|
|
56
|
+
? { id: match[1].trim(), date: match[2], reason: match[3].trim(), raw: line }
|
|
57
|
+
: { id: '', date: '', reason: '', raw: line }
|
|
58
|
+
})
|
|
59
|
+
return { exists: true, rows, postponements }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// La línea que se pega en BACKLOG. La celda de la tabla es literalmente la cola de esa línea, así que
|
|
63
|
+
// lo que una persona escribió una vez es lo que se promueve, sin retipear.
|
|
64
|
+
function taskLine(row, period) {
|
|
65
|
+
return `- [ ] **${row.id}-${period}** — ${row.task}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validate({ exists, rows, postponements }) {
|
|
69
|
+
if (!exists) return []
|
|
70
|
+
const errors = []
|
|
71
|
+
const seen = new Set()
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const at = `${FILE} ${row.id || '(fila sin nombre)'}`
|
|
74
|
+
if (!ID.test(row.id)) errors.push(`${at}: el identificador va en minúsculas, sin espacios`)
|
|
75
|
+
else if (seen.has(row.id)) errors.push(`${FILE}: identificador duplicado ${row.id}`)
|
|
76
|
+
seen.add(row.id)
|
|
77
|
+
if (!CADENCES[row.cadence]) {
|
|
78
|
+
errors.push(`${at}: cadencia "${row.cadence}" fuera de ${Object.keys(CADENCES).join(' | ')}`)
|
|
79
|
+
}
|
|
80
|
+
if (!DATE.test(row.since)) errors.push(`${at}: Desde debe ser AAAA-MM-DD`)
|
|
81
|
+
// 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
|
+
// leer es el mismo lector de BACKLOG. Una celda que pasa acá y una línea que BACKLOG rechaza es el
|
|
83
|
+
// error que aparece un mes después, con la tarea ya pegada.
|
|
84
|
+
const task = P.taskFromLine(taskLine(row, '0000-00'))
|
|
85
|
+
if (!task) errors.push(`${at}: la celda no arma una línea de tarea`)
|
|
86
|
+
else {
|
|
87
|
+
if (!task.acceptance) errors.push(`${at}: la tarea no declara _Aceptación: ..._`)
|
|
88
|
+
if (!task.service) errors.push(`${at}: la tarea no declara (service: <ruta>)`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const one of postponements) {
|
|
92
|
+
if (!one.id) {
|
|
93
|
+
errors.push(`${FILE}: una postergación se escribe "- **qué** AAAA-MM-DD — razón": ${one.raw.trim()}`)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (!DATE.test(one.date)) errors.push(`${FILE}: postergación de ${one.id}: la fecha va en AAAA-MM-DD`)
|
|
97
|
+
if (!seen.has(one.id)) errors.push(`${FILE}: postergación de ${one.id}, que la tabla no declara`)
|
|
98
|
+
}
|
|
99
|
+
return errors
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// El último período cerrado, leído de DONE y no de una celda: una fecha que alguien tiene que acordarse
|
|
103
|
+
// de actualizar miente a los tres meses, y la entrada de DONE es además la evidencia de que se hizo.
|
|
104
|
+
function lastPeriod(id, done) {
|
|
105
|
+
const pattern = new RegExp(`^${id}-(\\d{4}-\\d{2})$`)
|
|
106
|
+
const periods = done.entries.map((entry) => (entry.slug.match(pattern) || [])[1]).filter(Boolean)
|
|
107
|
+
return periods.sort().pop() || ''
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function status({ rows, postponements, done, today }) {
|
|
111
|
+
return rows
|
|
112
|
+
.filter((row) => ID.test(row.id) && CADENCES[row.cadence] && DATE.test(row.since))
|
|
113
|
+
.map((row) => {
|
|
114
|
+
const months = CADENCES[row.cadence]
|
|
115
|
+
const last = lastPeriod(row.id, done)
|
|
116
|
+
// Cerrada una vuelta, la siguiente se cuenta desde el período que la cerró; sin ninguna, desde el
|
|
117
|
+
// ancla escrita al declarar la fila. Es lo que hace rodante al vencimiento: saltearse una vuelta
|
|
118
|
+
// corre la próxima en vez de deber dos.
|
|
119
|
+
const base = last ? addMonths(`${last}-01`, months) : row.since
|
|
120
|
+
// Y las postergaciones que cuentan son las escritas después de que terminó el período cerrado:
|
|
121
|
+
// las anteriores ya las absorbió ese cierre, que es lo que reinicia el contador.
|
|
122
|
+
const from = last ? addMonths(`${last}-01`, 1) : row.since
|
|
123
|
+
const postponed = postponements.filter((one) => one.id === row.id && one.date >= from).length
|
|
124
|
+
const due = addMonths(base, months * postponed)
|
|
125
|
+
const overdueDays = daysBetween(due, today)
|
|
126
|
+
return {
|
|
127
|
+
id: row.id, cadence: row.cadence, task: row.task, last, due, postponed,
|
|
128
|
+
overdueDays, overdue: overdueDays >= 0,
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function warnings(state) {
|
|
134
|
+
const lines = []
|
|
135
|
+
for (const one of state.filter((candidate) => candidate.overdue)) {
|
|
136
|
+
const when = one.overdueDays === 0 ? 'vence hoy' : `vencida hace ${one.overdueDays} día(s)`
|
|
137
|
+
lines.push(`${FILE}: ${one.id} ${when} (${one.due})`)
|
|
138
|
+
}
|
|
139
|
+
// Tres seguidas no son un atraso: son una fila que no se va a hacer con la cadencia que declara. El
|
|
140
|
+
// contador es la alarma, y por eso se cuenta desde el último cierre y no desde siempre.
|
|
141
|
+
for (const one of state.filter((candidate) => candidate.postponed >= 3)) {
|
|
142
|
+
lines.push(`${FILE}: ${one.id} postergada ${one.postponed} veces desde el último cierre; `
|
|
143
|
+
+ 'revisá la cadencia o la fila')
|
|
144
|
+
}
|
|
145
|
+
return lines
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { FILE, read, validate, status, warnings, taskLine }
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -153,6 +153,8 @@ muta nada.
|
|
|
153
153
|
- `node tools/ops.js context planning` — gate, mutex de WIP y la tarea que corresponde ahora, con su
|
|
154
154
|
aceptación y sus criterios. Es la entrada correcta para empezar a trabajar.
|
|
155
155
|
- `node tools/ops.js tree planning` — panorama de roadmap, backlog, WIP, inbox y done.
|
|
156
|
+
- `node tools/ops.js recurring planning [--promote <qué>]` — qué trabajo recurrente venció y con
|
|
157
|
+
qué línea se promueve. Emite esa línea; escribirla en `BACKLOG.md` es de una persona.
|
|
156
158
|
- `node tools/ops.js check planning` — validación de contratos y trazabilidad.
|
|
157
159
|
- `node tools/ops.js evidence planning [--task <slug>]` — contrasta la evidencia de una entrada de DONE
|
|
158
160
|
contra lo que no escribió su autor: si el artefacto que `tests:` nombra existe en las raíces de
|
|
@@ -161,8 +163,8 @@ muta nada.
|
|
|
161
163
|
nombrada haya corrido —eso depende del runner, y varios no la nombran al pasar— ni reemplaza a leer
|
|
162
164
|
su fuente, que es lo que R9 pide.
|
|
163
165
|
|
|
164
|
-
Los
|
|
165
|
-
falta editarlos o cuando el CLI no responda la pregunta.
|
|
166
|
+
Los cinco aceptan `--json`. Leer `BACKLOG.md`, `WIP.md`, `HUMAN_ACTIONS.md` o `RECURRING.md` completos
|
|
167
|
+
sólo cuando haga falta editarlos o cuando el CLI no responda la pregunta.
|
|
166
168
|
|
|
167
169
|
## Autonomía
|
|
168
170
|
|
|
@@ -178,6 +180,11 @@ Nunca amplía el alcance, promueve sus propias ideas, reescribe el proceso duran
|
|
|
178
180
|
`git add .`/`git add -A`, reescribe historia con `--force` o `--amend`, ni afirma éxito sin evidencia
|
|
179
181
|
real. Tampoco publica: sin autorización no hay `push`.
|
|
180
182
|
|
|
183
|
+
Una recurrencia vencida tampoco la promueve, y ésta es la que más se parece a una excepción: la
|
|
184
|
+
aceptación ya está escrita, la fecha la calculó el CLI y `context` la nombra sola. Nada de eso es la
|
|
185
|
+
aprobación que pide BR-OPS-002 — `context` la nombra para que la vea una persona, y quien la pega en
|
|
186
|
+
`BACKLOG.md` es esa persona.
|
|
187
|
+
|
|
181
188
|
Publicar es lo único de todo eso que este proyecto puede habilitar, y `runner.allowPush` en
|
|
182
189
|
`ops.config.json` es la autorización que R10 pide. Reescribir historia publicada no entra en el trato:
|
|
183
190
|
un `push --force` se frena con la llave prendida o apagada.
|
package/template/Makefile
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.DEFAULT_GOAL := help
|
|
2
2
|
|
|
3
|
-
.PHONY: help check tree context upgrade destroy automation-check
|
|
3
|
+
.PHONY: help check tree context recurring upgrade destroy automation-check
|
|
4
4
|
.PHONY: integration-check integration-sync require-key integration-promote
|
|
5
5
|
.PHONY: require-agent agent-learn agent-propose agent-evaluate require-flow flow-list flow-check flow-show
|
|
6
6
|
.PHONY: install-claude install-codex install-gemini install-antigravity
|
|
@@ -23,6 +23,9 @@ tree: ## Muestra roadmap, backlog, WIP y Done
|
|
|
23
23
|
context: ## Muestra el contexto mínimo de la tarea vigente
|
|
24
24
|
@node tools/ops.js context planning
|
|
25
25
|
|
|
26
|
+
recurring: ## Muestra qué trabajo recurrente vence y con qué línea se promueve
|
|
27
|
+
@node tools/ops.js recurring planning
|
|
28
|
+
|
|
26
29
|
# `init` fija la versión exacta del motor, así que `npm update` no la mueve: hay que pedir @latest.
|
|
27
30
|
# Sin este primer paso `upgrade` compara contra el motor instalado y contesta «al día» para siempre.
|
|
28
31
|
destroy: ## Muestra qué se pierde al borrar esta instancia (borrar exige --force a mano)
|
|
@@ -15,6 +15,8 @@ INBOX ──promoción humana──▶ roadmap ──historias listas──▶ B
|
|
|
15
15
|
|
|
16
16
|
## Preparar
|
|
17
17
|
|
|
18
|
+
0. Si `ops recurring planning` nombra una recurrencia vencida, decidir si esta vuelta se promueve —el
|
|
19
|
+
comando emite su línea— o se posterga con su razón.
|
|
18
20
|
1. Curar una idea desde INBOX.
|
|
19
21
|
2. Escribir una épica con resultados observables, contexto actual y criterios `C1..CN`.
|
|
20
22
|
3. Descomponerla en historias de máximo cuatro horas, cada una rastreada a uno o más criterios.
|
|
@@ -20,6 +20,11 @@ invariantes.
|
|
|
20
20
|
en el vocabulario cerrado `pendiente | resuelta` —la fecha puede ir detrás—. Mientras la fila no
|
|
21
21
|
esté resuelta, su tarea no se toma; un estado fuera del vocabulario es un error de `check` y no un
|
|
22
22
|
bloqueo silencioso.
|
|
23
|
+
- Recurrencia: fila `| qué | cada | desde | tarea y aceptación |` bajo `## Recurrencias`, con `cada` en
|
|
24
|
+
el vocabulario cerrado `mensual | trimestral | semestral | anual` y la celda de tarea escrita como la
|
|
25
|
+
cola de su línea de BACKLOG. Vencer no bloquea: cada vuelta se promueve con el período en el slug
|
|
26
|
+
—`<qué>-AAAA-MM`— y esa promoción la escribe una persona. Postergar se registra bajo
|
|
27
|
+
`## Postergaciones` con `- **qué** AAAA-MM-DD — razón`.
|
|
23
28
|
- WIP activo: frontmatter y checklist; inactivo: `status: IDLE`.
|
|
24
29
|
|
|
25
30
|
## Gates de arranque
|
|
@@ -19,6 +19,7 @@ Se decide antes de ejecutar y no cambia dentro de una tarea.
|
|
|
19
19
|
| Pieza | Responsabilidad |
|
|
20
20
|
|---|---|
|
|
21
21
|
| `INBOX.md` | Ideas y deuda sin autorización de ejecución. |
|
|
22
|
+
| `RECURRING.md` | Trabajo que vuelve cada tanto; declarado, nunca encolado solo. |
|
|
22
23
|
| `roadmap/` | Especificaciones de épicas y criterios del QUÉ. |
|
|
23
24
|
| `adr/` | Decisiones arquitectónicas durables. |
|
|
24
25
|
| `business-rules/` | Invariantes observables de negocio y operación. |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Trabajo que vuelve
|
|
2
|
+
|
|
3
|
+
Lo que hay que hacer cada tanto y no una vez: actualizar dependencias, revisar quién tiene acceso a
|
|
4
|
+
producción, mirar el gasto del mes, recorrer el INBOX entero. No es una categoría de trabajo —puede ser
|
|
5
|
+
cualquier cosa— sino una forma de declararlo: acá vive el enunciado, y cada vuelta se promueve a
|
|
6
|
+
`BACKLOG.md` como una tarea más.
|
|
7
|
+
|
|
8
|
+
**Nada se dispara.** Este archivo no ejecuta ni encola: declara cada cuánto algo debería mirarse, y el
|
|
9
|
+
vencimiento se calcula cuando alguien corre el CLI. Promover sigue siendo un acto humano, igual que en
|
|
10
|
+
`INBOX.md`. Lo que la máquina aporta es que no se te pase, no decidir por vos.
|
|
11
|
+
|
|
12
|
+
## Cada fila
|
|
13
|
+
|
|
14
|
+
- **Qué** — un identificador estable, en minúsculas y sin espacios. No cambia nunca: es lo que ata la
|
|
15
|
+
fila a las vueltas que ya se hicieron.
|
|
16
|
+
- **Cada** — vocabulario cerrado: `mensual`, `trimestral`, `semestral`, `anual`. No hay expresiones de
|
|
17
|
+
cron, y esa ausencia es el enunciado: si hiciera falta una, lo que estarías declarando es otra cosa.
|
|
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.
|
|
20
|
+
- **Tarea y aceptación** — lo que se va a promover, con su aceptación observable escrita una sola vez y
|
|
21
|
+
con calma. Improvisada en cada vuelta, la misma recurrencia termina significando cosas distintas sin
|
|
22
|
+
que nadie lo decida.
|
|
23
|
+
|
|
24
|
+
## Cuándo vence
|
|
25
|
+
|
|
26
|
+
Se cuenta desde la última vez que se cerró, no desde un día fijo del calendario: una recurrencia
|
|
27
|
+
atrasada no debe tres vueltas, debe una, la que no se hizo.
|
|
28
|
+
|
|
29
|
+
La fecha de ese último cierre **no se escribe acá**. Sale de `DONE.md` — la entrada más nueva cuyo slug
|
|
30
|
+
sea `<qué>-AAAA-MM`—, que es la evidencia de que efectivamente se hizo y no la afirmación de que se
|
|
31
|
+
hizo. Una celda que alguien tiene que acordarse de actualizar miente a los tres meses, y el estado no se
|
|
32
|
+
copia para representar progreso.
|
|
33
|
+
|
|
34
|
+
Por eso cada vuelta se promueve con su período en el slug —`deps-2026-10`, después `deps-2026-11`— y no
|
|
35
|
+
con el identificador pelado. Reusarlo tiene además su propio castigo: dos entradas de DONE con el mismo
|
|
36
|
+
slug son un error de `check`, y ese error llega un mes tarde.
|
|
37
|
+
|
|
38
|
+
## Lo que este archivo no es
|
|
39
|
+
|
|
40
|
+
- **No bloquea.** Una recurrencia vencida no frena ninguna tarea. Lo que sí frena vive en
|
|
41
|
+
`HUMAN_ACTIONS.md`; ponerlo acá entrena a ignorar lo que vence, que es lo único que este archivo hace.
|
|
42
|
+
- **No es una cola.** Nada de acá está aprobado para ejecutarse. `BACKLOG.md` sigue siendo la única cola
|
|
43
|
+
y se escribe a mano.
|
|
44
|
+
- **No es el INBOX.** Una idea se promueve una vez y se borra; esto vuelve, y por eso se queda.
|
|
45
|
+
|
|
46
|
+
## Recurrencias
|
|
47
|
+
|
|
48
|
+
| Qué | Cada | Desde | Tarea y aceptación observable |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
|
|
51
|
+
<!--
|
|
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._ |
|
|
56
|
+
-->
|
|
57
|
+
|
|
58
|
+
## Postergaciones
|
|
59
|
+
|
|
60
|
+
Saltear una vuelta es legítimo y se escribe. Lo que no puede perderse es la razón: sin ella queda una
|
|
61
|
+
recurrencia atrasada y nadie sabe si fue una decisión o un olvido.
|
|
62
|
+
|
|
63
|
+
Postergar compra **un período**, no una fecha elegida: la vuelta siguiente vuelve a preguntar. Las
|
|
64
|
+
postergaciones se cuentan desde el último cierre y el contador se reinicia al cerrar. Tres seguidas no
|
|
65
|
+
son un atraso — son una recurrencia mal declarada, y lo que hay que revisar es la cadencia o la fila
|
|
66
|
+
entera.
|
|
67
|
+
|
|
68
|
+
La salida definitiva es borrar la fila. «Esto ya no lo hacemos» es un diff que alguien revisa; un estado
|
|
69
|
+
`retirada` es una línea que nadie vuelve a leer.
|
|
70
|
+
|
|
71
|
+
Cada una se escribe con el nombre de su fila en negrita, la fecha y la razón —`- **qué** AAAA-MM-DD —
|
|
72
|
+
razón`—, igual que un ítem del INBOX y por el mismo motivo: el nombre es con lo que se cita la fila.
|
|
73
|
+
Sólo se agrega al final; una línea escrita acá no se edita ni se borra.
|
|
74
|
+
|
|
75
|
+
<!--
|
|
76
|
+
- **deps** 2026-10-02 — Esperando el release de la 2.0; subir dependencias antes lo ensucia.
|
|
77
|
+
-->
|
|
@@ -31,6 +31,18 @@ y la cobertura no lo va a decir: mide qué líneas se ejecutan, no qué defectos
|
|
|
31
31
|
La precondición del caso también cuenta. Si el estado en que arranca no puede ocurrir por el camino de
|
|
32
32
|
producción, lo que prueba tampoco: queda verde para siempre sobre algo que nadie va a vivir.
|
|
33
33
|
|
|
34
|
+
**Y quitar un comportamiento se prueba al revés que agregarlo.** Una prueba que comprueba que aparece lo
|
|
35
|
+
nuevo no comprueba que desapareció lo viejo: los dos pueden convivir, y ahí el verde dice que la mitad
|
|
36
|
+
del cambio ocurrió. La aserción que hace falta es de ausencia —que la salida vieja ya no esté, que la
|
|
37
|
+
rama vieja ya no corra—, y es la que no se escribe sola porque nadie la extraña.
|
|
38
|
+
|
|
39
|
+
Lo que se quita, además, tiene dependientes, y no se anuncian. Una invariante que deja de valer se lleva
|
|
40
|
+
puesto a quien la daba por cierta: el mensaje que la afirmaba, la condición que la deducía, el comentario
|
|
41
|
+
que la explicaba. Suelen vivir en otro archivo, que es donde una premisa vieja se pudre sin que nada
|
|
42
|
+
falle. Antes de entregar una quita se busca quién dependía de ella —qué la afirmaba, qué la deducía— y
|
|
43
|
+
cada uno se corrige o se declara. Recorrer la enumeración de la tarea no encuentra esto: la enumeración
|
|
44
|
+
dice qué había que hacer, no qué se apoyaba en lo que había.
|
|
45
|
+
|
|
34
46
|
## R10 — Publicación humana por defecto
|
|
35
47
|
|
|
36
48
|
Push, PR, merge, tags, deploy y rollback requieren la autorización configurada para el proyecto.
|