@ingeniomaps/cauce 0.69.0 → 0.71.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 +266 -0
- package/README.md +6 -6
- package/agents/README.md +38 -0
- package/agents/roles/system/accounting-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/ai-governance-lead/learning/HISTORY.md +6 -1
- package/agents/roles/system/ai-governance-lead/learning/sources.yaml +4 -4
- package/agents/roles/system/ai-governance-lead/references/operating-model.md +2 -2
- package/agents/roles/system/ai-product-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/ai-product-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/ai-product-manager/references/operating-model.md +2 -2
- package/agents/roles/system/analytics-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/analytics-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/analytics-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/backend-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/business-strategist/learning/HISTORY.md +2 -0
- package/agents/roles/system/business-strategist/learning/sources.yaml +1 -1
- package/agents/roles/system/business-strategist/references/operating-model.md +2 -2
- package/agents/roles/system/cloud-architect/learning/HISTORY.md +1 -1
- package/agents/roles/system/cloud-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/cloud-architect/references/operating-model.md +2 -2
- package/agents/roles/system/community-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/community-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/community-manager/references/operating-model.md +1 -1
- package/agents/roles/system/content-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-success-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-success-manager/learning/sources.yaml +4 -4
- package/agents/roles/system/customer-success-manager/references/operating-model.md +3 -3
- package/agents/roles/system/customer-support-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/customer-support-specialist/learning/sources.yaml +2 -2
- package/agents/roles/system/customer-support-specialist/references/operating-model.md +2 -2
- package/agents/roles/system/data-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/data-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/data-engineer/learning/sources.yaml +3 -3
- package/agents/roles/system/data-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/data-governance-steward/learning/HISTORY.md +2 -0
- package/agents/roles/system/data-scientist/learning/HISTORY.md +4 -1
- package/agents/roles/system/data-scientist/learning/sources.yaml +3 -3
- package/agents/roles/system/data-scientist/references/operating-model.md +2 -2
- package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -1
- package/agents/roles/system/database-administrator/learning/sources.yaml +2 -2
- package/agents/roles/system/database-administrator/references/operating-model.md +2 -2
- package/agents/roles/system/developer-relations-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/devops-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/engineering-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/engineering-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/engineering-manager/references/operating-model.md +2 -2
- package/agents/roles/system/financial-controller/learning/HISTORY.md +2 -0
- package/agents/roles/system/financial-controller/learning/sources.yaml +2 -2
- package/agents/roles/system/financial-controller/references/operating-model.md +1 -1
- package/agents/roles/system/finops-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/frontend-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/growth-marketer/learning/HISTORY.md +2 -0
- package/agents/roles/system/growth-marketer/learning/sources.yaml +1 -1
- package/agents/roles/system/implementation-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/implementation-manager/learning/sources.yaml +4 -4
- package/agents/roles/system/implementation-manager/references/operating-model.md +3 -3
- package/agents/roles/system/integrations-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +3 -2
- package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/kyc-aml-specialist/references/operating-model.md +1 -1
- package/agents/roles/system/legal-counsel/learning/HISTORY.md +6 -1
- package/agents/roles/system/legal-counsel/learning/sources.yaml +1 -1
- package/agents/roles/system/legal-counsel/references/operating-model.md +1 -1
- package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/machine-learning-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/machine-learning-engineer/learning/sources.yaml +6 -6
- package/agents/roles/system/machine-learning-engineer/references/operating-model.md +3 -3
- package/agents/roles/system/mlops-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/mlops-engineer/learning/sources.yaml +2 -2
- package/agents/roles/system/mlops-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/mobile-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/mobile-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/partnerships-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/partnerships-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/partnerships-manager/references/operating-model.md +2 -2
- package/agents/roles/system/people-operations-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/people-operations-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/people-operations-manager/references/operating-model.md +2 -2
- package/agents/roles/system/privacy-compliance-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/privacy-compliance-specialist/learning/sources.yaml +1 -1
- package/agents/roles/system/privacy-compliance-specialist/references/operating-model.md +1 -1
- package/agents/roles/system/procurement-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/procurement-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/procurement-manager/references/operating-model.md +2 -2
- package/agents/roles/system/product-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/product-marketing-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/project-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/project-manager/learning/sources.yaml +1 -1
- package/agents/roles/system/project-manager/references/operating-model.md +2 -2
- package/agents/roles/system/qa-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/qa-engineer/learning/sources.yaml +9 -11
- package/agents/roles/system/qa-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/release-manager/learning/HISTORY.md +1 -1
- package/agents/roles/system/sales-representative/learning/HISTORY.md +2 -0
- package/agents/roles/system/sales-representative/learning/sources.yaml +2 -2
- package/agents/roles/system/sales-representative/references/operating-model.md +1 -1
- package/agents/roles/system/security-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/security-engineer/learning/sources.yaml +1 -1
- package/agents/roles/system/security-engineer/references/operating-model.md +1 -1
- package/agents/roles/system/site-reliability-engineer/learning/HISTORY.md +2 -0
- package/agents/roles/system/software-architect/learning/HISTORY.md +2 -0
- package/agents/roles/system/software-architect/learning/sources.yaml +2 -2
- package/agents/roles/system/software-architect/references/operating-model.md +1 -1
- package/agents/roles/system/solutions-engineer/learning/HISTORY.md +4 -1
- package/agents/roles/system/solutions-engineer/learning/sources.yaml +4 -4
- package/agents/roles/system/solutions-engineer/references/operating-model.md +2 -2
- package/agents/roles/system/tech-lead/learning/HISTORY.md +2 -0
- package/agents/roles/system/technical-program-manager/learning/HISTORY.md +4 -1
- package/agents/roles/system/technical-program-manager/learning/sources.yaml +2 -2
- package/agents/roles/system/technical-program-manager/references/operating-model.md +3 -3
- package/agents/roles/system/technical-writer/learning/HISTORY.md +4 -1
- package/agents/roles/system/treasury-analyst/learning/HISTORY.md +2 -0
- package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
- package/agents/roles/system/ui-designer/learning/sources.yaml +2 -2
- package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -1
- package/agents/roles/system/user-researcher/learning/sources.yaml +1 -1
- package/agents/roles/system/ux-designer/learning/HISTORY.md +2 -0
- package/agents/roles/system/ux-designer/learning/sources.yaml +1 -1
- package/agents/roles/system/ux-designer/references/operating-model.md +1 -1
- package/automatization/AGENTS.md +1 -1
- package/automatization/runners/antigravity/rules/cauce.md +1 -1
- package/automatization/runners/claude/CLAUDE.md +1 -1
- package/automatization/runners/codex/AGENTS.md +1 -1
- package/automatization/runners/gemini/GEMINI.md +1 -1
- package/automatization/shared/skills/autobuild/SKILL.md +1 -1
- package/automatization/workflows/autobuild.js +81 -27
- package/engine/agents/learning-seal.js +36 -5
- package/engine/agents/learning-sources.js +46 -2
- package/engine/agents/learning.js +13 -1
- package/engine/cli/archive.js +87 -0
- package/engine/cli/args.js +6 -2
- package/engine/cli/catalog.js +9 -1
- package/engine/cli/claims.js +125 -0
- package/engine/cli/ops.js +15 -4
- package/engine/cli/planning.js +139 -107
- package/engine/cli/worktree.js +89 -0
- package/engine/core/ownership.js +13 -5
- package/engine/core/repos.js +67 -0
- package/engine/hooks/files.js +3 -2
- package/engine/planning/adoption.js +1 -1
- package/engine/planning/claims.js +153 -0
- package/engine/planning/contracts.js +43 -202
- package/engine/planning/parser.js +51 -12
- package/engine/planning/state.js +39 -6
- package/engine/planning/structure.js +220 -0
- package/package.json +1 -1
- package/template/.gitattributes +19 -0
- package/template/AGENTS.md +47 -5
- package/template/automatization/AGENTS.md +1 -1
- package/template/gitignore +9 -0
- package/template/planning/BACKLOG.md +5 -0
- package/template/planning/FLOW.md +1 -1
- package/template/planning/PROTOCOL.md +34 -8
- package/template/planning/README.md +3 -3
- package/template/planning/RECURRING.md +1 -1
- package/template/planning/adr/system/OPS-001-planificacion-como-fuente-de-verdad.md +3 -2
- package/template/planning/business-rules/system/BR-OPS-001-una-sola-tarea-activa.md +7 -4
- package/template/planning/business-rules/system/BR-OPS-005-una-tarea-un-runner.md +44 -0
- package/template/planning/claims/README.md +70 -0
- package/template/planning/delivery/README.md +1 -0
- package/template/planning/delivery/multi-repo.md +11 -0
- package/template/planning/delivery/teamwork.md +162 -0
- package/template/planning/done/README.md +41 -0
- package/template/planning/wip/README.md +49 -0
- package/template/planning/DONE.md +0 -13
- package/template/planning/WIP.md +0 -22
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Tomar y soltar una tarea, y ver quién tiene trabajo abierto. Los dos primeros son los únicos comandos
|
|
4
|
+
// del motor que escriben un archivo de coordinación, y escriben **el propio**: `BACKLOG.md` no lo toca
|
|
5
|
+
// ninguno, porque la cola es de lo aprobado y la escribe una persona.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const CL = require('../planning/claims')
|
|
10
|
+
const R = require('../core/repos')
|
|
11
|
+
const ST = require('../planning/state')
|
|
12
|
+
const { fail } = require('./io')
|
|
13
|
+
|
|
14
|
+
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
15
|
+
|
|
16
|
+
function claim(dir, slug, cli) {
|
|
17
|
+
const root = path.resolve(dir || '.')
|
|
18
|
+
if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`', 2)
|
|
19
|
+
const state = ST.snapshot(root)
|
|
20
|
+
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
21
|
+
if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`, 2)
|
|
22
|
+
|
|
23
|
+
// No se reserva lo que todavía no se puede empezar: una tarea tomada con su dependencia en vuelo
|
|
24
|
+
// bloquea la cola sin que nadie pueda avanzarla, y el runner que la tomó se queda sin poder tomar otra.
|
|
25
|
+
const falta = task.depends.find((dep) => !state.done.set.has(dep))
|
|
26
|
+
if (falta) return fail(`${slug} depende de ${falta}, que todavía no está en DONE.`)
|
|
27
|
+
|
|
28
|
+
const me = CL.owner(root)
|
|
29
|
+
const from = CL.runner()
|
|
30
|
+
if (!me) {
|
|
31
|
+
return fail('No sé quién sos. Configurá `git config user.email` o exportá CAUCE_OWNER: '
|
|
32
|
+
+ 'un reclamo anónimo no dice a quién preguntarle.', 2)
|
|
33
|
+
}
|
|
34
|
+
// Un runner lleva una tarea a la vez (BR-OPS-001), y exigirlo acá hace ruidosa la única forma en que
|
|
35
|
+
// este contrato falla en silencio: dos agentes de la misma máquina compartiendo id porque nadie puso
|
|
36
|
+
// `CAUCE_RUNNER`. Sin esto, el segundo se llevaría la tarea del primero creyéndola suya.
|
|
37
|
+
//
|
|
38
|
+
// La que se está pidiendo queda afuera del conteo: volver a pedir la propia es reintentar, no llevar dos.
|
|
39
|
+
const ocupado = state.claims
|
|
40
|
+
.find((one) => one.runner === from && one.slug !== slug && !state.done.set.has(one.slug))
|
|
41
|
+
if (ocupado) {
|
|
42
|
+
return fail(`este runner ya tiene ${ocupado.slug}. Cerrala o soltala primero; y si sos otro agente `
|
|
43
|
+
+ 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const target = CL.file(root, slug)
|
|
47
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
48
|
+
const cuerpo = CL.content({ task: slug, owner: me, runner: from, started: TODAY(), service: task.service })
|
|
49
|
+
try {
|
|
50
|
+
// Reservar **es** crear el archivo, así que el único juez de quién la tiene es el archivo. `wx` falla
|
|
51
|
+
// si ya está, y de ahí sale la respuesta entera: propia, ajena o perdida en la carrera.
|
|
52
|
+
//
|
|
53
|
+
// Sin `wx` no habría reserva: `atomicWrite` renombra encima, y dos agentes que arrancan con segundos
|
|
54
|
+
// de diferencia ganarían los dos sin que ninguno se entere. Y una comprobación previa tampoco
|
|
55
|
+
// alcanzaría —entre mirar y escribir queda la misma ventana—, así que sería un segundo juez que
|
|
56
|
+
// adelanta un veredicto que este bloque tiene que volver a dar igual.
|
|
57
|
+
fs.writeFileSync(target, cuerpo, { flag: 'wx' })
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code !== 'EEXIST') throw error
|
|
60
|
+
const dueño = CL.read(root).find((one) => one.slug === slug)
|
|
61
|
+
// Existía al crear y ya no está: alguien la soltó entre las dos operaciones. Es una ventana de
|
|
62
|
+
// microsegundos y aun así tiene respuesta, porque la alternativa es reventar con un TypeError.
|
|
63
|
+
if (!dueño) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`)
|
|
64
|
+
if (dueño.runner === from) return console.log(`= ${slug} ya era tuya desde ${dueño.started}`)
|
|
65
|
+
// Mismo dueño y otro runner son dos situaciones que se ven idénticas desde acá —vos retomando la
|
|
66
|
+
// sesión de ayer, o un segundo agente tuyo— y ninguna se puede distinguir mirando el archivo.
|
|
67
|
+
// Retomarla sola le sacaría la tarea al otro agente; crear un runner nuevo dejaría dos trabajando lo
|
|
68
|
+
// mismo. Las dos rompen trabajo, así que decide una persona y acá sólo se dice cuál es cuál.
|
|
69
|
+
if (dueño.owner === me) {
|
|
70
|
+
return fail(`${slug} la tenés vos, tomada el ${dueño.started} desde el runner ${dueño.runner}. `
|
|
71
|
+
+ 'Preguntá si se retoma esa sesión —y entonces corré con ese id— o si es otro agente en '
|
|
72
|
+
+ `paralelo, que toma otra tarea. \`ops runners ${path.relative(process.cwd(), root) || '.'}\` `
|
|
73
|
+
+ 'lista lo que hay abierto.')
|
|
74
|
+
}
|
|
75
|
+
return fail(`${slug} la tomó ${dueño.owner} el ${dueño.started}. Si se abandonó, borrá `
|
|
76
|
+
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
|
|
77
|
+
}
|
|
78
|
+
console.log(`✓ ${slug} tomada por ${me}`)
|
|
79
|
+
// Un reclamo sin empujar no protege de nada: el otro runner lee lo que hay en su copia. Decirlo acá
|
|
80
|
+
// es lo único que separa «tomé la tarea» de «creí que la había tomado».
|
|
81
|
+
if (!cli.has('--json')) console.log(` commiteá y empujá ${CL.DIR}/${slug}.md para que el equipo lo vea`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function release(dir, slug) {
|
|
85
|
+
const root = path.resolve(dir || '.')
|
|
86
|
+
if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`', 2)
|
|
87
|
+
const from = CL.runner()
|
|
88
|
+
const taken = CL.read(root).find((one) => one.slug === slug)
|
|
89
|
+
if (!taken) return fail(`${slug} no está tomada por nadie.`, 2)
|
|
90
|
+
if (taken.runner !== from) {
|
|
91
|
+
return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano.`)
|
|
92
|
+
}
|
|
93
|
+
fs.rmSync(CL.file(root, slug))
|
|
94
|
+
console.log(`✓ ${slug} soltada; volvió a la cola`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Qué runners tienen trabajo abierto, para que un agente pueda preguntar antes de inventarse un id.
|
|
98
|
+
//
|
|
99
|
+
// Es lo primero de una sesión y no lo contesta `context`: `context` responde «qué hago» para un runner ya
|
|
100
|
+
// elegido, y elegirlo viene antes. Sin esta lista, un agente que arranca sin `CAUCE_RUNNER` se crea uno
|
|
101
|
+
// nuevo y deja el trabajo de ayer huérfano, o peor, se lo pisa a otro agente que sigue corriendo.
|
|
102
|
+
//
|
|
103
|
+
// La persona elige; el agente exporta. Pedirle a una persona que escriba una variable de entorno para
|
|
104
|
+
// retomar su propio trabajo es hacerle hacer de intérprete.
|
|
105
|
+
function runners(dir, cli) {
|
|
106
|
+
const root = path.resolve(dir || '.')
|
|
107
|
+
const done = ST.snapshot(root).done
|
|
108
|
+
const abiertos = CL.read(root).filter((one) => !done.set.has(one.slug))
|
|
109
|
+
const hoy = TODAY()
|
|
110
|
+
const filas = abiertos.map((one) => {
|
|
111
|
+
const commit = R.lastCommit(R.repoOf(path.join(root, '..'), one.service), CL.branchOf(one.slug))
|
|
112
|
+
return { runner: one.runner, task: one.slug, owner: one.owner, started: one.started, lastCommit: commit }
|
|
113
|
+
})
|
|
114
|
+
if (cli.has('--json')) return console.log(JSON.stringify(filas))
|
|
115
|
+
if (!filas.length) return console.log('= ningún runner tiene trabajo abierto: arrancá con un id propio')
|
|
116
|
+
const ancho = Math.max(...filas.map((one) => one.runner.length))
|
|
117
|
+
for (const una of filas) {
|
|
118
|
+
const avance = una.lastCommit ? `último commit ${una.lastCommit}` : 'sin commits en su rama'
|
|
119
|
+
console.log(`${una.runner.padEnd(ancho)} ${una.task} (${una.owner}, desde ${una.started}; ${avance})`)
|
|
120
|
+
}
|
|
121
|
+
console.log(`\n${filas.length} runner(s) con trabajo abierto al ${hoy}. `
|
|
122
|
+
+ 'Retomá uno usando su id, o arrancá con uno propio.')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { claim, release, runners }
|
package/engine/cli/ops.js
CHANGED
|
@@ -9,6 +9,9 @@ const { FLAGS, parse } = require('./args')
|
|
|
9
9
|
const { fail } = require('./io')
|
|
10
10
|
const IN = require('./instance')
|
|
11
11
|
const PL = require('./planning')
|
|
12
|
+
const AR = require('./archive')
|
|
13
|
+
const CLM = require('./claims')
|
|
14
|
+
const WT = require('./worktree')
|
|
12
15
|
const CAT = require('./catalog')
|
|
13
16
|
const W = require('./wiring')
|
|
14
17
|
const BOOT = require('./bootstrap')
|
|
@@ -142,12 +145,16 @@ function usage() {
|
|
|
142
145
|
ops onboard [ops-root] [--json]
|
|
143
146
|
ops check <planning-dir> [--json]
|
|
144
147
|
ops tree <planning-dir> [--no-color] [--json]
|
|
145
|
-
ops context <planning-dir> [--json]
|
|
148
|
+
ops context <planning-dir> [--hito <slug>] [--json]
|
|
146
149
|
ops recurring <planning-dir> [--promote <qué>] [--json]
|
|
150
|
+
ops runners <planning-dir> [--json]
|
|
151
|
+
ops claim <planning-dir> <tarea>
|
|
152
|
+
ops release <planning-dir> <tarea>
|
|
153
|
+
ops worktree <planning-dir> <tarea> [--json]
|
|
147
154
|
ops evidence <planning-dir> [--task <slug>] [--json]
|
|
148
155
|
ops upgrade <ops-root> [--check] [--force]
|
|
149
156
|
ops destroy <ops-root> [--force]
|
|
150
|
-
ops archive <planning-dir>
|
|
157
|
+
ops archive <planning-dir> human-actions
|
|
151
158
|
ops adopt <planning-dir>
|
|
152
159
|
ops integration list <ops-root>
|
|
153
160
|
ops integration enable <ops-root> <provider>
|
|
@@ -198,12 +205,16 @@ async function run(cli) {
|
|
|
198
205
|
else if (command === 'tree') PL.tree(arg[1], cli)
|
|
199
206
|
else if (command === 'context') PL.context(arg[1], cli)
|
|
200
207
|
else if (command === 'recurring') PL.recurring(arg[1], cli)
|
|
208
|
+
else if (command === 'runners') CLM.runners(arg[1], cli)
|
|
209
|
+
else if (command === 'claim') CLM.claim(arg[1], arg[2], cli)
|
|
210
|
+
else if (command === 'release') CLM.release(arg[1], arg[2])
|
|
211
|
+
else if (command === 'worktree') WT.worktree(arg[1], arg[2], cli)
|
|
201
212
|
else if (command === 'evidence') PL.evidence(arg[1], cli)
|
|
202
213
|
else if (command === 'upgrade') IN.upgrade(arg[1], cli)
|
|
203
214
|
else if (command === 'destroy') IN.destroy(arg[1], cli)
|
|
204
215
|
else if (command === 'agents') CAT.agents(arg[1], arg[2], arg[3], cli)
|
|
205
|
-
else if (command === 'archive')
|
|
206
|
-
else if (command === 'adopt')
|
|
216
|
+
else if (command === 'archive') AR.archive(arg[1], arg[2])
|
|
217
|
+
else if (command === 'adopt') AR.adopt(arg[1])
|
|
207
218
|
else if (command === 'integration') {
|
|
208
219
|
await W.integration(arg[1], arg[2], arg[3], arg[4], cli)
|
|
209
220
|
}
|
package/engine/cli/planning.js
CHANGED
|
@@ -8,8 +8,11 @@ const path = require('node:path')
|
|
|
8
8
|
const P = require('../planning/parser')
|
|
9
9
|
const B = require('../planning/business-rules')
|
|
10
10
|
const PC = require('../planning/contracts')
|
|
11
|
+
const SR = require('../planning/structure')
|
|
11
12
|
const SZ = require('../planning/sizing')
|
|
12
13
|
const RC = require('../planning/recurring')
|
|
14
|
+
const CL = require('../planning/claims')
|
|
15
|
+
const R = require('../core/repos')
|
|
13
16
|
const ST = require('../planning/state')
|
|
14
17
|
const AD = require('../planning/adoption')
|
|
15
18
|
const AP = require('../hooks/approval')
|
|
@@ -20,7 +23,6 @@ const OB = require('../core/onboarding')
|
|
|
20
23
|
const C = require('../config/validate')
|
|
21
24
|
const CP = require('../config/paths')
|
|
22
25
|
const AG = require('../agents/catalog')
|
|
23
|
-
const F = require('../core/files')
|
|
24
26
|
const { fail } = require('./io')
|
|
25
27
|
|
|
26
28
|
// Qué dimensiones enumera el molde de `organization/` y cuáles dejaron de estar. Un agente que reescribe
|
|
@@ -38,7 +40,9 @@ function evidence(dir, cli) {
|
|
|
38
40
|
const opsDir = path.join(root, '..')
|
|
39
41
|
const entries = P.readDone(root).entries
|
|
40
42
|
const slug = cli.value('--task')
|
|
41
|
-
|
|
43
|
+
// Sin `--task`, la más reciente, y la decide `fecha:` — por qué ese campo existe lo dice el contrato.
|
|
44
|
+
const reciente = [...entries].sort((a, b) => (a.fecha || '').localeCompare(b.fecha || '')).pop()
|
|
45
|
+
const entry = slug ? entries.find((one) => one.slug === slug) : reciente
|
|
42
46
|
if (!entry) return fail(slug ? `DONE no tiene la entrada ${slug}` : 'DONE no tiene ninguna entrada', 2)
|
|
43
47
|
|
|
44
48
|
let config = {}
|
|
@@ -75,7 +79,21 @@ function check(dir, cli) {
|
|
|
75
79
|
const root = path.resolve(dir || '.')
|
|
76
80
|
const errors = []
|
|
77
81
|
const warnings = []
|
|
78
|
-
|
|
82
|
+
// El plan no está: `wip/` es local y gitignoreado, así que un clon nuevo no lo trae y eso no es un
|
|
83
|
+
// error. Ausente se lee como IDLE, que es lo que significa.
|
|
84
|
+
const required = ['BACKLOG.md', 'INBOX.md', 'HUMAN_ACTIONS.md', 'PROTOCOL.md']
|
|
85
|
+
// `DONE.md` se retiró: la evidencia vive en un archivo por tarea. Un `DONE.md` que quede en disco
|
|
86
|
+
// ya no lo lee nadie, y eso no se nota — las épicas dejan de poder cerrar y sus historias figuran
|
|
87
|
+
// sin evidencia, que es lo mismo que se vería si nunca se hubieran hecho.
|
|
88
|
+
// `WIP.md` se retiró por lo mismo que `DONE.md`: era uno solo y lo escribían todos los que corren sobre
|
|
89
|
+
// una instancia sidecar. Uno que quede en disco ya no lo lee nadie, y su plan a medias se pierde sin
|
|
90
|
+
// que nada lo diga.
|
|
91
|
+
if (fs.existsSync(path.join(root, 'WIP.md'))) {
|
|
92
|
+
errors.push('WIP.md ya no se lee: el plan de cada runner vive en `wip/<runner>.md`; movelo y borralo')
|
|
93
|
+
}
|
|
94
|
+
if (fs.existsSync(path.join(root, 'DONE.md'))) {
|
|
95
|
+
errors.push('DONE.md ya no se lee: pasá cada entrada a su propio `done/<slug>.md` y borralo')
|
|
96
|
+
}
|
|
79
97
|
for (const file of required) if (!fs.existsSync(path.join(root, file))) errors.push(`falta ${file}`)
|
|
80
98
|
|
|
81
99
|
const configPath = path.join(root, '..', 'ops.config.json')
|
|
@@ -113,26 +131,39 @@ function check(dir, cli) {
|
|
|
113
131
|
const milestones = P.readBacklog(root)
|
|
114
132
|
const done = P.readDone(root)
|
|
115
133
|
errors.push(...B.validate(path.join(root, 'business-rules')))
|
|
116
|
-
errors.push(...
|
|
117
|
-
errors.push(...
|
|
118
|
-
errors.push(...
|
|
119
|
-
errors.push(...
|
|
134
|
+
errors.push(...SR.validateRoadmapStructure(root))
|
|
135
|
+
errors.push(...SR.validateBacklogStructure(root))
|
|
136
|
+
errors.push(...SR.validateRules(root))
|
|
137
|
+
errors.push(...SR.validateAdr(root))
|
|
120
138
|
const backlog = milestones.flatMap((milestone) => milestone.tasks)
|
|
121
139
|
const backlogSlugs = new Set(backlog.map((task) => task.slug))
|
|
122
140
|
const epicNums = new Set()
|
|
123
141
|
const storySlugs = new Set()
|
|
124
142
|
|
|
125
143
|
const roles = new Set(AG.list(path.resolve(root, '..')).map((role) => role.slug))
|
|
126
|
-
const
|
|
144
|
+
const wips = P.readWips(root)
|
|
127
145
|
const adopted = AD.read(root)
|
|
128
146
|
errors.push(...SZ.oversizedUnits({ epics, milestones }))
|
|
129
147
|
errors.push(...PC.validateState({
|
|
130
|
-
epics, milestones, done,
|
|
148
|
+
epics, milestones, done, wips, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
|
|
131
149
|
}))
|
|
132
150
|
warnings.push(...AD.report({ done, epics, adopted }))
|
|
133
151
|
// Sin `RECURRING.md` no dice una palabra: una instancia que actualiza y no declara trabajo recurrente
|
|
134
152
|
// no tiene por qué enterarse de que el contrato existe. Vencida avisa y no frena — lo que frena vive
|
|
135
153
|
// en `HUMAN_ACTIONS.md`, y un aviso que salta siempre se termina apagando.
|
|
154
|
+
// Un reclamo que nombra una tarea que no existe bloquea la cola sin que nada lo explique, y uno viejo
|
|
155
|
+
// la bloquea para siempre. Lo primero es error; lo segundo avisa, porque abandonar no es un defecto.
|
|
156
|
+
const claims = CL.read(root)
|
|
157
|
+
errors.push(...CL.validate({ claims, milestones, done }))
|
|
158
|
+
// Si la rama de cada tarea tomada se movió, que es lo único barato que distingue una tarea larga de
|
|
159
|
+
// una abandonada. Sin repositorio resoluble el mapa queda vacío y el aviso vuelve a mirar sólo la
|
|
160
|
+
// fecha, que es lo que había antes: degrada, no rompe.
|
|
161
|
+
const activity = new Map()
|
|
162
|
+
for (const claim of claims.filter((one) => !done.set.has(one.slug))) {
|
|
163
|
+
const at = R.lastCommit(R.repoOf(path.join(root, '..'), claim.service), CL.branchOf(claim.slug))
|
|
164
|
+
if (at) activity.set(claim.slug, at)
|
|
165
|
+
}
|
|
166
|
+
warnings.push(...CL.warnings({ claims, done, today: TODAY(), activity }))
|
|
136
167
|
const recurring = RC.read(root)
|
|
137
168
|
errors.push(...RC.validate(recurring))
|
|
138
169
|
warnings.push(...RC.warnings(RC.status({ ...recurring, done, today: TODAY() })))
|
|
@@ -168,14 +199,14 @@ function check(dir, cli) {
|
|
|
168
199
|
errors.push(...integration.errors)
|
|
169
200
|
warnings.push(...integration.warnings)
|
|
170
201
|
|
|
171
|
-
warnings.push(...
|
|
202
|
+
warnings.push(...SR.competingSections(root))
|
|
172
203
|
// Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
|
|
173
204
|
// ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
|
|
174
205
|
for (const override of O.overrides(path.resolve(root, '..'))) {
|
|
175
206
|
// Y con qué se queda el proyecto: un override sano redefine lo que reemplaza, y el que deja IDs
|
|
176
207
|
// afuera los retira sin decirlo. Nombrarlos es lo único que separa una decisión de un descuido.
|
|
177
208
|
const retired = override.collection === 'planning/rules'
|
|
178
|
-
?
|
|
209
|
+
? SR.retiredByOverride(root, override.project)
|
|
179
210
|
: []
|
|
180
211
|
warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} `
|
|
181
212
|
+ `(override explícito)${retired.length ? `; deja de regir ${retired.join(', ')}` : ''}`)
|
|
@@ -193,7 +224,7 @@ function check(dir, cli) {
|
|
|
193
224
|
epics: epics.length,
|
|
194
225
|
queued: backlog.length,
|
|
195
226
|
done: done.entries.length,
|
|
196
|
-
|
|
227
|
+
wips: wips.map((one) => one.task),
|
|
197
228
|
errors,
|
|
198
229
|
warnings,
|
|
199
230
|
}))
|
|
@@ -211,7 +242,7 @@ function check(dir, cli) {
|
|
|
211
242
|
}
|
|
212
243
|
|
|
213
244
|
// Estado observable de planning sin mutar nada; base común de `tree` y de sus salidas.
|
|
214
|
-
function treeJson({ epics, milestones, done,
|
|
245
|
+
function treeJson({ epics, milestones, done, wips, inbox, queued, claims }) {
|
|
215
246
|
const state = (slug) => done.set.has(slug) ? 'done' : queued.has(slug) ? 'queued' : 'pending'
|
|
216
247
|
console.log(JSON.stringify({
|
|
217
248
|
roadmap: epics.map((epic) => ({
|
|
@@ -224,17 +255,23 @@ function treeJson({ epics, milestones, done, wip, inbox, queued }) {
|
|
|
224
255
|
slug: milestone.slug,
|
|
225
256
|
tasks: milestone.tasks.map((task) => ({ slug: task.slug, tier: task.tier || '' })),
|
|
226
257
|
})),
|
|
227
|
-
wip:
|
|
258
|
+
wip: wips.map((one) => ({
|
|
259
|
+
task: one.task, runner: one.runner, phase: one.phase, complete: one.complete, pending: one.pending,
|
|
260
|
+
})),
|
|
228
261
|
inbox: { deuda: inbox.deuda, ideas: inbox.ideas, propuestas: inbox.propuestas, lecciones: inbox.lecciones },
|
|
262
|
+
claims: claims.map((one) => ({ slug: one.slug, owner: one.owner, started: one.started })),
|
|
229
263
|
done: done.entries.length,
|
|
230
264
|
}))
|
|
231
265
|
}
|
|
232
266
|
|
|
233
267
|
function tree(dir, cli) {
|
|
234
268
|
const root = path.resolve(dir || '.')
|
|
269
|
+
// Mismo motivo que en `context`, y por eso comparten la comprobación: sin ella un planning ausente
|
|
270
|
+
// dibujaba un árbol vacío, que se lee como un roadmap sin épicas en vez de como una ruta equivocada.
|
|
271
|
+
assertPlanning(root)
|
|
235
272
|
const state = ST.snapshot(root)
|
|
236
273
|
if (cli.has('--json')) return treeJson(state)
|
|
237
|
-
const { epics, milestones, done,
|
|
274
|
+
const { epics, milestones, done, wips, inbox, queued, claims } = state
|
|
238
275
|
const color = process.stdout.isTTY && !cli.has('--no-color')
|
|
239
276
|
const paint = (code, text) => color ? `\x1b[${code}m${text}\x1b[0m` : text
|
|
240
277
|
console.log(`\n${paint('1', 'CAUCE')}\n`)
|
|
@@ -254,8 +291,8 @@ function tree(dir, cli) {
|
|
|
254
291
|
console.log(` ${milestone.heading}`)
|
|
255
292
|
for (const task of milestone.tasks) console.log(` ☐ ${task.slug}${task.tier ? ` [${task.tier}]` : ''}`)
|
|
256
293
|
}
|
|
257
|
-
const wipText =
|
|
258
|
-
? `▶ ${
|
|
294
|
+
const wipText = wips.length
|
|
295
|
+
? wips.map((one) => `▶ ${one.task} · ${one.phase} · ${one.complete}✓/${one.pending}○ (${one.runner})`).join(' ')
|
|
259
296
|
: 'idle'
|
|
260
297
|
console.log(`\n${paint('1', 'WIP')} ${wipText}`)
|
|
261
298
|
console.log(
|
|
@@ -264,16 +301,62 @@ function tree(dir, cli) {
|
|
|
264
301
|
// Sin esto, doce viñetas sin nombre se veían como un inbox vacío y nadie se enteraba.
|
|
265
302
|
(inbox.skipped ? ` (${inbox.skipped} sin contar: falta el nombre en **negrita**)` : ''),
|
|
266
303
|
)
|
|
304
|
+
if (claims.length) {
|
|
305
|
+
console.log(`${paint('1', 'CLAIM')} ${claims.map((one) => `${one.slug} · ${one.owner}`).join(' ')}`)
|
|
306
|
+
}
|
|
267
307
|
console.log(`${paint('1', 'DONE')} ${done.entries.length} tareas\n`)
|
|
268
308
|
}
|
|
269
309
|
|
|
310
|
+
// Lo que no se pudo leer no contesta como si se hubiera leído: la misma regla que `stagedFiles` aplica
|
|
311
|
+
// sobre el índice de git, y que esta familia ya aplicaba al `--hito` inexistente. Faltaba la raíz, y ahí
|
|
312
|
+
// pesa más, porque el consumidor no siempre es una persona: `autobuild` toma la cola vacía como permiso
|
|
313
|
+
// para expandir una épica, así que un error de ruta promovía trabajo en vez de fallar.
|
|
314
|
+
//
|
|
315
|
+
// La ruta va **resuelta** y no como se escribió, porque el error que ataca es de resolución: en sidecar
|
|
316
|
+
// `<empresa>-ops/planning` desde adentro de la raíz apunta a `<empresa>-ops/<empresa>-ops/planning`.
|
|
317
|
+
function assertPlanning(root) {
|
|
318
|
+
if (!fs.existsSync(root)) {
|
|
319
|
+
return fail(`no existe el planning en ${root} (ruta resuelta). Comprobá desde dónde estás invocando.`, 2)
|
|
320
|
+
}
|
|
321
|
+
// Existir no alcanza: un directorio cualquiera contestaría cola vacía igual. `BACKLOG.md` es el archivo
|
|
322
|
+
// del que sale la cola, así que sin él la respuesta no significa nada.
|
|
323
|
+
if (!fs.existsSync(path.join(root, 'BACKLOG.md'))) {
|
|
324
|
+
return fail(`${root} (ruta resuelta) no es un planning: falta BACKLOG.md.`, 2)
|
|
325
|
+
}
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
|
|
270
329
|
// Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
|
|
271
330
|
function context(dir, cli) {
|
|
272
331
|
const root = path.resolve(dir || '.')
|
|
332
|
+
assertPlanning(root)
|
|
273
333
|
const state = ST.snapshot(root)
|
|
334
|
+
// Acotar la cola a un hito es como un equipo se reparte trabajo sin coordinarse: dos personas en hitos
|
|
335
|
+
// distintos casi nunca dependen entre sí ni tocan los mismos archivos. Lo que se acota es qué se
|
|
336
|
+
// ofrece, no qué se sabe: `done` sigue siendo global, así que una dependencia que vive en otro hito se
|
|
337
|
+
// juzga igual de bien.
|
|
338
|
+
const from = CL.runner()
|
|
339
|
+
const mio = state.wips.find((one) => one.runner === P.wipName(from)) || null
|
|
340
|
+
const hito = cli.value('--hito')
|
|
341
|
+
// Un filtro elige dónde buscar trabajo **nuevo**; no puede esconder el que ya tenés. Sin esto, pedir
|
|
342
|
+
// otro hito mientras sostenías una tarea ofrecía una segunda que `claim` después se niega a dar: el
|
|
343
|
+
// comando que dice qué hacer y el que lo autoriza contestaban distinto, y sólo se veía al reclamar.
|
|
344
|
+
const propio = state.claims.find((one) => one.runner === from && !state.done.set.has(one.slug))
|
|
345
|
+
let hitoOmitido = ''
|
|
346
|
+
if (hito) {
|
|
347
|
+
const existe = state.milestones.some((one) => one.slug === hito)
|
|
348
|
+
// Un hito mal escrito devolvería «sin tarea disponible», que es indistinguible de un hito terminado.
|
|
349
|
+
if (!existe) {
|
|
350
|
+
const hay = state.milestones.map((one) => one.slug).join(', ') || '(ninguno)'
|
|
351
|
+
return fail(`el hito ${hito} no existe. Hay: ${hay}`, 2)
|
|
352
|
+
}
|
|
353
|
+
if (propio) hitoOmitido = `${hito} no se aplica: ya tenés ${propio.slug} tomada`
|
|
354
|
+
else state.milestones = state.milestones.filter((one) => one.slug === hito)
|
|
355
|
+
}
|
|
274
356
|
const gate = path.join(root, 'AWAITING_REVIEW.md')
|
|
275
357
|
const humanActions = ST.pendingHumanActions(root)
|
|
276
|
-
const
|
|
358
|
+
const me = CL.owner(root)
|
|
359
|
+
const { task, skipped, claimed, taken, waiting } = ST.currentTask(state, humanActions, from)
|
|
277
360
|
const epic = task ? state.epics.find((candidate) => candidate.num === task.epic) : null
|
|
278
361
|
const criteria = epic ? epic.criteria.filter((criterion) => task.criteria.includes(criterion.id)) : []
|
|
279
362
|
const report = {
|
|
@@ -291,15 +374,31 @@ function context(dir, cli) {
|
|
|
291
374
|
// un criterio de cumplir su letra. Viaja acá porque el ejecutor tiene prohibido ir a buscarlo:
|
|
292
375
|
// `autobuild` le dice que lea cuatro archivos una sola vez y nada más, y el roadmap no es ninguno.
|
|
293
376
|
epic: epic ? { num: epic.num, title: epic.title, status: epic.status, context: epic.context } : null,
|
|
294
|
-
|
|
377
|
+
// El WIP que este runner tiene, no el de la instancia: es lo único que le corresponde continuar.
|
|
378
|
+
wip: mio ? { phase: mio.phase, complete: mio.complete, pending: mio.pending } : null,
|
|
379
|
+
// Dónde va su plan. Lo dice el motor porque el nombre sale del id del runner, y quien escribe el
|
|
380
|
+
// plan —un workflow— no tiene por qué saber cómo se deriva.
|
|
381
|
+
wipFile: `wip/${P.wipName(from)}.md`,
|
|
295
382
|
queued: state.milestones.reduce((total, milestone) => total + milestone.tasks.length, 0),
|
|
296
383
|
blockedTasks: skipped,
|
|
297
384
|
humanActions,
|
|
385
|
+
owner: me,
|
|
386
|
+
// El día de hoy, para quien no tiene reloj. Un workflow no puede llamar a `new Date` —una puerta se
|
|
387
|
+
// lo impide, porque su salida dejaría de ser reproducible— y necesita la fecha para cerrar una tarea.
|
|
388
|
+
// Sale de acá y no del modelo: es un dato mecánico, y pedírselo a un agente es invitarlo a inventarlo.
|
|
389
|
+
today: TODAY(),
|
|
390
|
+
claimed,
|
|
391
|
+
taken,
|
|
392
|
+
waiting,
|
|
298
393
|
// Sólo las vencidas: la fila que todavía no vence no tiene nada que decirle a quien va a tomar una
|
|
299
394
|
// tarea, y una recurrencia que hablara siempre sería ruido en el único comando que se corre en cada
|
|
300
395
|
// vuelta. Que aparezca es la señal.
|
|
301
396
|
recurring: RC.status({ ...RC.read(root), done: state.done, today: TODAY() })
|
|
302
397
|
.filter((one) => one.overdue),
|
|
398
|
+
// La próxima sin promover: se nombra, no se encola, igual que la recurrencia de arriba y por su
|
|
399
|
+
// mismo criterio. Por qué no la encola una máquina está en la fase Pick de `autobuild`.
|
|
400
|
+
nextEpic: (!task && [...state.epics].filter((one) => one.status === 'open')
|
|
401
|
+
.sort((a, b) => String(a.num).localeCompare(String(b.num)))[0]) || null,
|
|
303
402
|
}
|
|
304
403
|
if (cli.has('--json')) return console.log(JSON.stringify(report))
|
|
305
404
|
|
|
@@ -315,6 +414,14 @@ function context(dir, cli) {
|
|
|
315
414
|
// que una instancia recién arrancada —`onboard` deja filas pendientes y ninguna tarea todavía—
|
|
316
415
|
// respondía «sin tarea disponible» y se tragaba las siete cosas que una persona tenía que desbloquear.
|
|
317
416
|
// Es el comando que existe para decir qué toca ahora, contestando «nada» cuando lo que toca es eso.
|
|
417
|
+
// Tu propio nombre en una tarea «ajena» es la señal de que sos vos desde otro runner, y sin decirlo se
|
|
418
|
+
// lee como que alguien te ganó la tarea.
|
|
419
|
+
const dueño = (one) => (one.owner === report.owner ? `${one.owner} — vos, desde otro runner` : one.owner)
|
|
420
|
+
const espera = () => {
|
|
421
|
+
for (const one of report.waiting) {
|
|
422
|
+
console.log(`WAIT ${one.slug}: espera a ${one.dep}${one.owner ? ` (${one.owner})` : ''}`)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
318
425
|
const due = () => {
|
|
319
426
|
for (const one of report.recurring) {
|
|
320
427
|
const when = one.overdueDays === 0 ? 'vence hoy' : `vencida hace ${one.overdueDays} día(s)`
|
|
@@ -323,6 +430,12 @@ function context(dir, cli) {
|
|
|
323
430
|
}
|
|
324
431
|
if (!report.task) {
|
|
325
432
|
console.log('TASK (sin tarea disponible)')
|
|
433
|
+
const { nextEpic: next } = report
|
|
434
|
+
if (next) console.log(`EPIC ${next.num}: ${next.title} — sin promover`)
|
|
435
|
+
// Mismo motivo que `blocked` arriba, con otra causa: acá la cola no la traba una persona, la tiene
|
|
436
|
+
// el equipo, y lo que corresponde es hablar con quien la tiene.
|
|
437
|
+
for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
|
|
438
|
+
espera()
|
|
326
439
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
327
440
|
due()
|
|
328
441
|
return
|
|
@@ -345,6 +458,12 @@ function context(dir, cli) {
|
|
|
345
458
|
for (const criterion of criteria) console.log(`${criterion.id.padEnd(6)} ${criterion.text}`)
|
|
346
459
|
const wip = report.wip ? `${report.wip.phase} · ${report.wip.complete}✓/${report.wip.pending}○` : 'idle'
|
|
347
460
|
console.log(`WIP ${wip}`)
|
|
461
|
+
if (hitoOmitido) console.log(`HITO ${hitoOmitido}`)
|
|
462
|
+
console.log(report.claimed
|
|
463
|
+
? `CLAIM tuya desde el reclamo (${report.owner})`
|
|
464
|
+
: `CLAIM libre — tomala con \`ops claim <planning> ${report.task.slug}\``)
|
|
465
|
+
for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
|
|
466
|
+
espera()
|
|
348
467
|
if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
|
|
349
468
|
for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
|
|
350
469
|
due()
|
|
@@ -379,91 +498,4 @@ function recurring(dir, cli) {
|
|
|
379
498
|
}
|
|
380
499
|
}
|
|
381
500
|
|
|
382
|
-
|
|
383
|
-
// a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
|
|
384
|
-
// cerró ninguna —que es justo cuando el archivo se vuelve ilegible—.
|
|
385
|
-
// Adoptar es declarar de una vez qué historia llegó con el proyecto. Se genera con lo que hoy no cumple
|
|
386
|
-
// y no se vuelve a correr: un baseline que se regenera perdona de nuevo lo que alguien ya se tomó el
|
|
387
|
-
// trabajo de arreglar, y uno que crece a mano deja de ser una lista de perdones para ser una amnistía.
|
|
388
|
-
// Achicarlo sí es a mano, borrando el renglón que `check` señala.
|
|
389
|
-
function adopt(dir) {
|
|
390
|
-
const root = path.resolve(dir || '.')
|
|
391
|
-
const target = path.join(root, AD.BASELINE)
|
|
392
|
-
if (fs.existsSync(target)) {
|
|
393
|
-
// Un baseline que ya trae huella no se toca: regenerarlo es exactamente lo que la huella impide.
|
|
394
|
-
// Uno sin huella lo generó una versión anterior, y sellarlo no es regenerar nada — se calcula sobre
|
|
395
|
-
// lo que ya está—, así que es la única salida de un aviso que si no no tendría ninguna.
|
|
396
|
-
const existing = fs.readFileSync(target, 'utf8')
|
|
397
|
-
if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
|
|
398
|
-
fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
|
|
399
|
-
+ 'delante; `check` marca los que ya cumplen.')
|
|
400
|
-
}
|
|
401
|
-
const slugs = AD.declared(existing)
|
|
402
|
-
F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
|
|
403
|
-
+ `sha256:${AD.digest(slugs)}\n`))
|
|
404
|
-
return console.log(`✓ ${AD.BASELINE} sellado con ${slugs.length} entrada(s); la lista no cambió`)
|
|
405
|
-
}
|
|
406
|
-
const epics = P.readEpics(root)
|
|
407
|
-
const pending = P.readDone(root).entries.filter((entry) => PC.doneEntryErrors(entry, epics).length)
|
|
408
|
-
if (!pending.length) {
|
|
409
|
-
return console.log('= no hay nada que exentar: todas las entradas de DONE cumplen el contrato')
|
|
410
|
-
}
|
|
411
|
-
const today = TODAY()
|
|
412
|
-
const slugs = pending.map((entry) => entry.slug)
|
|
413
|
-
F.atomicWrite(target, `# Entradas anteriores a la adopción de Cauce (${today}). No se agregan nuevas:\n`
|
|
414
|
-
+ '# desde esa fecha rige el contrato completo, y `check` avisa cuando una de éstas pasa a\n'
|
|
415
|
-
+ '# cumplirlo para que le pongas `#~` delante y quede retirada.\n'
|
|
416
|
-
+ `# huella: ${slugs.length} entradas · sha256:${AD.digest(slugs)}\n`
|
|
417
|
-
+ `${slugs.join('\n')}\n`)
|
|
418
|
-
console.log(`✓ ${pending.length} entrada(s) exentas en ${AD.BASELINE}`)
|
|
419
|
-
return console.log(' revisá la lista: lo que sí cumple el contrato no tiene por qué estar ahí')
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function archiveHumanActions(root) {
|
|
423
|
-
const source = path.join(root, 'HUMAN_ACTIONS.md')
|
|
424
|
-
const rows = P.readHumanActions(root).filter((row) => row.resolved)
|
|
425
|
-
if (!rows.length) return console.log('= no hay filas resueltas')
|
|
426
|
-
const target = path.join(root, 'done', 'human-actions.md')
|
|
427
|
-
const header = '| Tarea | Estado | Origen | Acción concreta y condición de desbloqueo |\n|---|---|---|---|'
|
|
428
|
-
const previous = P.read(target).trimEnd()
|
|
429
|
-
const head = previous || `---\nstatus: archived\n---\n\n# Acciones humanas resueltas\n\n${header}`
|
|
430
|
-
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
431
|
-
F.atomicWrite(target, `${head}\n${rows.map((row) => row.raw).join('\n')}\n`)
|
|
432
|
-
const drop = new Set(rows.map((row) => row.raw))
|
|
433
|
-
const kept = P.read(source).split('\n').filter((line) => !drop.has(line))
|
|
434
|
-
F.atomicWrite(source, `${kept.join('\n').trimEnd()}\n`)
|
|
435
|
-
return console.log(`✓ ${rows.length} fila(s) archivadas`)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function archive(dir, rawNum) {
|
|
439
|
-
const root = path.resolve(dir || '.')
|
|
440
|
-
if (String(rawNum || '') === 'human-actions') return archiveHumanActions(root)
|
|
441
|
-
const num = String(rawNum || '').padStart(3, '0')
|
|
442
|
-
if (!/^\d{3}$/.test(num)) fail('La épica debe ser NNN, o human-actions.', 2)
|
|
443
|
-
const epic = P.readEpics(root).find((candidate) => candidate.num === num)
|
|
444
|
-
if (!epic) fail(`No existe epic-${num}.`, 2)
|
|
445
|
-
if (epic.status !== 'closed') fail(`epic-${num} no está cerrada (status: ${epic.status}).`)
|
|
446
|
-
const target = path.join(root, 'done', `epic-${num}.md`)
|
|
447
|
-
const source = path.join(root, 'DONE.md')
|
|
448
|
-
const content = P.read(source)
|
|
449
|
-
const slugs = new Set(epic.stories.map((story) => story.slug))
|
|
450
|
-
const entries = P.readDone(root).entries.filter((entry) => entry.source === 'DONE.md' && slugs.has(entry.slug))
|
|
451
|
-
if (!entries.length) {
|
|
452
|
-
if (fs.existsSync(target)) return console.log(`= epic-${num} ya estaba archivada`)
|
|
453
|
-
fail(`No hay entradas de epic-${num} en DONE.md.`)
|
|
454
|
-
}
|
|
455
|
-
let updated = content
|
|
456
|
-
for (const entry of entries) updated = updated.replace(entry.raw, '').replace(/\n{3,}/g, '\n\n')
|
|
457
|
-
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
458
|
-
if (!fs.existsSync(target)) {
|
|
459
|
-
F.atomicWrite(
|
|
460
|
-
target,
|
|
461
|
-
`---\nepic: ${num}\nstatus: archived\n---\n\n# DONE — ${epic.title}\n\n` +
|
|
462
|
-
`${entries.map((entry) => entry.raw).join('\n\n')}\n`,
|
|
463
|
-
)
|
|
464
|
-
}
|
|
465
|
-
F.atomicWrite(source, `${updated.trimEnd()}\n`)
|
|
466
|
-
console.log(`✓ epic-${num}: ${entries.length} entrada(s) archivadas`)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
module.exports = { check, evidence, tree, context, archive, adopt, recurring }
|
|
501
|
+
module.exports = { check, evidence, tree, context, recurring }
|