@ingeniomaps/cauce 0.87.0 → 0.89.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.
@@ -6,6 +6,7 @@
6
6
  // instalar. Y como `upgrade` no reinstala, `check` y `doctor` comparan lo instalado con lo vigente.
7
7
 
8
8
  const fs = require('node:fs')
9
+ const path = require('node:path')
9
10
  const F = require('../core/files')
10
11
  const O = require('../core/ownership')
11
12
 
@@ -20,14 +21,100 @@ const START_AT = '<!-- cauce:reglas inicio'
20
21
  // Lo que un `CLAUDE.md` o un `GEMINI.md` instalado antes de 0.82.0 trae en el lugar del bloque.
21
22
  const LEGACY_IMPORT = /^@\S*planning\/rules\/\S+\.md\s*$/
22
23
 
24
+ // Una regla que declara `aplica:` rige igual, y se carga sólo cuando se toca esa superficie. Lo que se
25
+ // ahorra está medido: el contexto de arranque de **cada** agente son ~16 K tokens sólo de lo que Cauce
26
+ // pone, y una regla que importa en una tarea de cada cien se leía cien veces (caso 141).
27
+ //
28
+ // El default es lo que vuelve seguro esto: sin el campo, la regla se carga como siempre. Al revés —pedir
29
+ // que declare `siempre` para seguir cargándose— cada instancia habría perdido sus reglas propias en el
30
+ // próximo `upgrade`, una por una y sin que nada lo dijera.
31
+ //
32
+ // `effectiveRules` no cambia: sigue devolviendo todo lo que rige, que es lo que `ops context` le entrega
33
+ // al recorrido. Acá sólo se decide qué se **carga**, y por eso la partición vive de este lado.
34
+ const SURFACE = /^aplica:[ \t]*(\S.*?)[ \t]*$/m
35
+ function surfaceOf(root, file) {
36
+ try {
37
+ const text = fs.readFileSync(path.join(root, file), 'utf8')
38
+ // Sólo en el frontmatter: un `aplica:` en la prosa de una regla es una frase, no una declaración.
39
+ if (!text.startsWith('---')) return ''
40
+ const end = text.indexOf('\n---', 3)
41
+ return end === -1 ? '' : ((text.slice(0, end).match(SURFACE) || [])[1] || '')
42
+ } catch { return '' }
43
+ }
44
+
45
+ // Las que se cargan y las que sólo se nombran. Las dos listas salen de la misma lectura para que el
46
+ // bloque que se escribe y el aviso que lo audita no puedan discrepar: con dos recorridos, `doctor`
47
+ // reportaría como faltante la que a propósito no se carga, para siempre y sin forma de callarlo.
48
+ function split(root) {
49
+ const loaded = []
50
+ const named = []
51
+ for (const file of O.effectiveRules(root)) {
52
+ const surface = surfaceOf(root, file)
53
+ if (surface) named.push({ file, surface })
54
+ else loaded.push(file)
55
+ }
56
+ return { loaded, named }
57
+ }
58
+
59
+ // Cuánto pesa lo que el bloque carga, en bytes. En bytes y no en tokens a propósito: los bytes los mide
60
+ // el motor y se pueden comprobar, mientras que la equivalencia en tokens depende del modelo y del
61
+ // tokenizador. Un número estimado en una salida que alguien cita para decidir vale menos que uno exacto.
62
+ //
63
+ // Sólo `loaded`: una regla declarada por superficie no viaja en el arranque, y contarla haría que
64
+ // declararla no sirviera de nada. Vive acá —y no en quien lo imprime— porque lo miran dos, `install` al
65
+ // instalar y `check` en cada corrida, y con dos cuentas separadas se contradicen el día que alguien toque
66
+ // una sola.
67
+ function weight(root) {
68
+ const { loaded } = split(root)
69
+ let bytes = 0
70
+ const files = []
71
+ for (const file of loaded) {
72
+ try {
73
+ const size = fs.statSync(path.join(root, file)).size
74
+ bytes += size
75
+ files.push({ file, size })
76
+ } catch { /* la que no está en disco ya la reporta `check` por su lado */ }
77
+ }
78
+ return { count: loaded.length, bytes, files: files.sort((a, b) => b.size - a.size) }
79
+ }
80
+
81
+ const KB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`
82
+
83
+ // A partir de dónde el peso deja de ser el costo de arrancar y pasa a ser una decisión que conviene mirar.
84
+ // El piso que Cauce impone —las cuatro reglas del sistema— son 38,3 KB, así que un umbral por debajo de
85
+ // eso avisaría en toda instancia recién creada y se apagaría por ruido el primer día: eso descartó los
86
+ // 60 KB que el caso 141 proponía. 64 KB deja ~26 KB para lo propio, que son varias reglas de tamaño
87
+ // normal, antes de que el aviso hable.
88
+ const HEAVY = 64 * 1024
89
+
90
+ // La línea que declara el peso, para que la digan igual `install` y `check`. Nombra las dos más grandes
91
+ // porque es lo accionable: saber que el bloque pesa no dice cuál conviene declarar por superficie.
92
+ function weightLine(root) {
93
+ const { count, bytes, files } = weight(root)
94
+ const top = files.slice(0, 2).map((one) => path.basename(one.file)).join(', ')
95
+ return `el bloque de reglas carga ${count} archivo(s), ${KB(bytes)} en cada agente`
96
+ + (top ? ` (las más grandes: ${top})` : '')
97
+ }
98
+
99
+ // Sólo cuando pasó el umbral. Devuelve lista porque es lo que `check` empalma con el resto de avisos.
100
+ function heavyRules(root) {
101
+ return weight(root).bytes > HEAVY ? [weightLine(root)] : []
102
+ }
103
+
23
104
  // Sin raíz el marcador queda como está —así lo leen las pruebas que revisan el texto de un adaptador—: un
24
105
  // bloque vacío se leería igual que un proyecto sin reglas, y un marcador sin resolver se ve.
25
106
  function fill(text, root) {
26
107
  if (!root || !text.includes('{{RULES:')) return text
27
- const rules = O.effectiveRules(root)
28
- return text.replace(MARKER, (_, format) => [START, ...rules.map((file) => (format === 'imports'
29
- ? `@{{OPS_DIR}}${file}`
30
- : `- \`{{OPS_DIR}}${file}\``)), END].join('\n'))
108
+ const { loaded, named } = split(root)
109
+ return text.replace(MARKER, (_, format) => [
110
+ START,
111
+ ...loaded.map((file) => (format === 'imports' ? `@{{OPS_DIR}}${file}` : `- \`{{OPS_DIR}}${file}\``)),
112
+ // Nombrada y no cargada: pesa una línea en vez de su archivo entero, y quien la lea sabe qué la
113
+ // dispara. Va en el mismo bloque a propósito — fuera de él, una sesión que no corre un recorrido no
114
+ // se enteraría de que existe, y una regla que nadie sabe que existe es una que no rige.
115
+ ...named.map((one) => `- \`{{OPS_DIR}}${one.file}\` (aplica: ${one.surface})`),
116
+ END,
117
+ ].join('\n'))
31
118
  }
32
119
 
33
120
  function blockOf(text) {
@@ -69,6 +156,13 @@ function drift(root, name) {
69
156
  const { runnerManifest, runnerPaths, resolveItem } = require('./runners')
70
157
  const runner = runnerManifest(root, name)
71
158
  const paths = runnerPaths(root, name, runner)
159
+ // Contra todo lo que rige, y no contra la partición: una regla declarada por superficie igual aparece
160
+ // en el bloque —nombrada en vez de importada—, y `listed` extrae la ruta de las dos formas. Así que el
161
+ // audit la ve y no la extraña, sin que este lado tenga que saber que la partición existe.
162
+ //
163
+ // Se intentó filtrar acá por simetría con `fill` y no cambiaba nada: la mutación que lo revertía dejaba
164
+ // las pruebas en verde. Queda dicho porque la simetría es tentadora y el código que no altera ninguna
165
+ // conducta se lee como si sostuviera algo.
72
166
  const expected = O.effectiveRules(root)
73
167
  const found = []
74
168
  for (const item of [...(runner.instructions || []), ...(runner.artifacts || [])]) {
@@ -119,4 +213,4 @@ function staleLines(root) {
119
213
  return lines
120
214
  }
121
215
 
122
- module.exports = { fill, refresh, drift, driftLine, staleLines }
216
+ module.exports = { fill, refresh, drift, driftLine, staleLines, split, weightLine, heavyRules }
@@ -122,9 +122,13 @@ function inline(text, automationRoot) {
122
122
  })
123
123
  }
124
124
 
125
- // `{{OPS_ROOT}}` es la raíz absoluta. La necesita quien no puede deducirla de dónde lo ejecutaron
126
- // —el puente de Antigravity—, y por eso no reemplaza a `{{OPS_DIR}}`: una ruta absoluta escrita en un
127
- // archivo se rompe si el proyecto se mueve, así que la lleva sólo el que se queda sin alternativa.
125
+ // `{{OPS_ROOT}}` es la raíz absoluta, y la lleva quien no puede deducirla de dónde lo ejecutaron: el
126
+ // puente de Antigravity, los hooks de Codex y —desde 0.89.0— los recorridos, que dictan comandos a un
127
+ // agente cuyo directorio nadie promete (caso 139).
128
+ //
129
+ // No reemplaza a `{{OPS_DIR}}` en general, y el costo dice por qué: un archivo que lleva la raíz escrita
130
+ // se rompe si el proyecto se mueve de carpeta, y se repara reinstalando el adaptador. Lo paga el que no
131
+ // tiene alternativa; lo que se resuelve contra la carpeta donde se abre la herramienta sigue con el otro.
128
132
  const OPS_ROOT = '{{OPS_ROOT}}'
129
133
 
130
134
  // `{{RULES:…}}` va antes que `{{OPS_DIR}}` por lo mismo que el include: las rutas que escribe llevan el prefijo.
@@ -7,6 +7,7 @@ const fs = require('node:fs')
7
7
  const path = require('node:path')
8
8
  const { spawnSync } = require('node:child_process')
9
9
  const L = require('../agents/learning')
10
+ const LF = require('../agents/learning-files')
10
11
  const AG = require('../agents/catalog')
11
12
  const EV = require('../agents/evaluations')
12
13
  const T = require('../flows/registry')
@@ -242,9 +243,15 @@ function learn(agent, cli) {
242
243
  if (cli.has('--archived')) {
243
244
  const result = L.archive(opsRoot(), agent, cli.value('--period'), kind)
244
245
  const relative = path.relative(opsRoot(), result.file)
246
+ // Los dos archivados no son lo mismo y el mensaje lo dice: uno es una decisión —se miró y no
247
+ // cambia nada— y el otro es tirar un andamio que nadie llegó a llenar. Afirmar el primero sobre
248
+ // el segundo le cuenta a quien archiva que hubo una revisión que no hubo.
249
+ const porque = result.blank
250
+ ? 'nadie decidió el cambio y el documento quedó con el molde'
251
+ : 'se miró y no cambia nada'
245
252
  return console.log(result.already
246
253
  ? `= ${relative} ya estaba archivada`
247
- : `✓ ${relative} queda archivada: se miró y no cambia nada`)
254
+ : `✓ ${relative} queda archivada: ${porque}`)
248
255
  }
249
256
  if (cli.has('--applied')) {
250
257
  const result = L.seal(opsRoot(), agent, cli.value('--period'), kind)
@@ -282,6 +289,13 @@ function learn(agent, cli) {
282
289
  ? ` ${result.reports} corrida(s) consolidada(s), ${result.findings} hallazgo(s)`
283
290
  : ` ${result.reports} informe(s) semanal(es) incluidos`)
284
291
  }
292
+ // Lo lee quien automatiza el ciclo para no pedir una firma por un documento que no decide nada, y
293
+ // también quien lo corre a mano: sin esta línea el archivo se ve terminado y no lo está. Se pregunta
294
+ // sobre el documento y no sobre lo que devolvió el motor, porque `prepareProposal` sale por cinco
295
+ // lugares y sólo uno compone: puesto en ése, el dato falta en los otros cuatro.
296
+ if (LF.blankProposal(result.file)) {
297
+ console.log(' sin cambio decidido: falta correr agent-propose antes de que esto se pueda firmar')
298
+ }
285
299
  } catch (error) { fail(error.message, 2) }
286
300
  }
287
301
 
@@ -76,9 +76,15 @@ function claim(dir, slug, cli) {
76
76
  + `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
77
77
  }
78
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`)
79
+ if (!cli.has('--json')) {
80
+ // Un reclamo sin empujar no protege de nada: el otro runner lee lo que hay en su copia. Decirlo acá
81
+ // es lo único que separa «tomé la tarea» de «creí que la había tomado».
82
+ console.log(` commiteá y empujá ${CL.DIR}/${slug}.md para que el equipo lo vea`)
83
+ // Y con qué id volver. Este dato lo imprimía `ops worktree` al crear el árbol; en sidecar no se crea
84
+ // ninguno, así que ese comando no corre nunca y el id no quedaba escrito en el único momento en que
85
+ // alguien lo tiene delante: ahora, que se acaba de guardar en el reclamo.
86
+ console.log(` export CAUCE_RUNNER=${from}`)
87
+ }
82
88
  }
83
89
 
84
90
  function release(dir, slug) {
package/engine/cli/io.js CHANGED
@@ -10,6 +10,12 @@ function fail(message, code = 1) {
10
10
  process.exit(code)
11
11
  }
12
12
 
13
+ // La fecha de hoy, en un solo lugar: los comandos que la usan tienen que estar mirando el mismo día, y
14
+ // el módulo que calcula vencimientos la recibe en vez de preguntarla. Vive acá desde que la puerta de
15
+ // planning se separó de los comandos que la leen — quedaba en el archivo que se partió, y dejar una copia
16
+ // a cada lado habría roto en silencio lo único que esta función promete.
17
+ const TODAY = () => new Date().toISOString().slice(0, 10)
18
+
13
19
  // La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
14
20
  // vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
15
21
  function opsRoot(dir) {
@@ -44,4 +50,4 @@ function planningRoot(dir) {
44
50
  return root
45
51
  }
46
52
 
47
- module.exports = { fail, opsRoot, planningRoot }
53
+ module.exports = { fail, opsRoot, planningRoot, TODAY }
package/engine/cli/ops.js CHANGED
@@ -9,6 +9,7 @@ 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 VA = require('./validate')
12
13
  const AR = require('./archive')
13
14
  const CLM = require('./claims')
14
15
  const WT = require('./worktree')
@@ -124,7 +125,7 @@ async function init(target, cli) {
124
125
  })
125
126
  } catch (error) { fail(error.message, 2) }
126
127
 
127
- if (result.installed) PL.check(path.join(root, 'planning'), NO_FLAGS)
128
+ if (result.installed) VA.check(path.join(root, 'planning'), NO_FLAGS)
128
129
 
129
130
  // Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el molde
130
131
  // y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es justo
@@ -204,7 +205,7 @@ async function run(cli) {
204
205
  if (command === 'init') await init(arg[1], cli)
205
206
  else if (command === 'scan') W.scan(arg[1], cli)
206
207
  else if (command === 'onboard') W.onboard(arg[1], cli)
207
- else if (command === 'check') PL.check(arg[1], cli)
208
+ else if (command === 'check') VA.check(arg[1], cli)
208
209
  else if (command === 'tree') PL.tree(arg[1], cli)
209
210
  else if (command === 'context') PL.context(arg[1], cli)
210
211
  else if (command === 'recurring') PL.recurring(arg[1], cli)
@@ -1,32 +1,20 @@
1
1
  'use strict'
2
2
 
3
- // Los comandos que leen y validan un planning: qué está mal, qué hay, qué toca ahora y qué se archiva.
4
- // Los cuatro trabajan sobre el mismo estado, que `planning/state.js` compone una vez.
3
+ // Los comandos que leen un planning y contestan sobre él: qué hay, qué toca ahora, qué vence y con qué
4
+ // se cerró una tarea. Todos trabajan sobre el mismo estado, que `planning/state.js` compone una vez.
5
+ //
6
+ // La puerta no está acá: vive en `validate.js` porque decide en vez de informar, y con ella se fueron
7
+ // quince de los veintiún módulos que este archivo importaba.
5
8
 
6
9
  const fs = require('node:fs')
7
10
  const path = require('node:path')
8
11
  const P = require('../planning/parser')
9
- const B = require('../planning/business-rules')
10
- const PC = require('../planning/contracts')
11
- const SR = require('../planning/structure')
12
- const SZ = require('../planning/sizing')
13
12
  const RC = require('../planning/recurring')
14
- const IB = require('../planning/inbox')
15
13
  const CL = require('../planning/claims')
16
- const R = require('../core/repos')
17
14
  const ST = require('../planning/state')
18
- const AD = require('../planning/adoption')
19
- const AP = require('../hooks/approval')
20
- const I = require('../integrations/registry')
21
15
  const O = require('../core/ownership')
22
16
  const EV = require('../core/evidence')
23
- const TR = require('../core/trails')
24
- const OB = require('../core/onboarding')
25
- const C = require('../config/validate')
26
- const CP = require('../config/paths')
27
- const AG = require('../agents/catalog')
28
- const RL = require('../automation/rules')
29
- const { fail, planningRoot } = require('./io')
17
+ const { fail, planningRoot, TODAY } = require('./io')
30
18
 
31
19
  // Qué dimensiones enumera el molde de `organization/` y cuáles dejaron de estar. Un agente que reescribe
32
20
  // esos archivos tiende a quedarse con el contenido y perder la estructura: el resultado se lee entero y
@@ -74,178 +62,6 @@ function evidence(dir, cli) {
74
62
  + 'que la prueba nombrada haya corrido: eso depende del runner, y varios no la nombran al pasar.')
75
63
  }
76
64
 
77
- // La fecha de hoy, en un solo lugar: los comandos que la usan tienen que estar mirando el mismo día, y
78
- // el módulo que calcula vencimientos la recibe en vez de preguntarla.
79
- const TODAY = () => new Date().toISOString().slice(0, 10)
80
-
81
- function check(dir, cli) {
82
- const root = planningRoot(dir)
83
- const errors = []
84
- const warnings = []
85
- // El plan no está: `wip/` es local y gitignoreado, así que un clon nuevo no lo trae y eso no es un
86
- // error. Ausente se lee como IDLE, que es lo que significa.
87
- const required = ['BACKLOG.md', 'INBOX.md', 'HUMAN_ACTIONS.md', 'PROTOCOL.md']
88
- // `DONE.md` se retiró: la evidencia vive en un archivo por tarea. Un `DONE.md` que quede en disco
89
- // ya no lo lee nadie, y eso no se nota — las épicas dejan de poder cerrar y sus historias figuran
90
- // sin evidencia, que es lo mismo que se vería si nunca se hubieran hecho.
91
- // `WIP.md` se retiró por lo mismo que `DONE.md`: era uno solo y lo escribían todos los que corren sobre
92
- // una instancia sidecar. Uno que quede en disco ya no lo lee nadie, y su plan a medias se pierde sin
93
- // que nada lo diga.
94
- if (fs.existsSync(path.join(root, 'WIP.md'))) {
95
- errors.push('WIP.md ya no se lee: el plan de cada runner vive en `wip/<runner>.md`; movelo y borralo')
96
- }
97
- if (fs.existsSync(path.join(root, 'DONE.md'))) {
98
- errors.push('DONE.md ya no se lee: pasá cada entrada a su propio `done/<slug>.md` y borralo')
99
- }
100
- for (const file of required) if (!fs.existsSync(path.join(root, file))) errors.push(`falta ${file}`)
101
-
102
- const configPath = path.join(root, '..', 'ops.config.json')
103
- let config = null
104
- if (fs.existsSync(configPath)) {
105
- try {
106
- const raw = fs.readFileSync(configPath, 'utf8')
107
- config = JSON.parse(raw)
108
- if (!raw.includes('{{')) {
109
- errors.push(...C.validateOpsConfig(config))
110
- warnings.push(...C.configWarnings(config))
111
- if (Array.isArray(config.workspaceRoots)) {
112
- for (const workspace of config.workspaceRoots) {
113
- if (workspace && workspace.name && workspace.path
114
- && !fs.existsSync(path.resolve(path.dirname(configPath), workspace.path))) {
115
- errors.push(`ops.config.json: no existe la raíz ${workspace.name} (${workspace.path})`)
116
- }
117
- }
118
- }
119
- // Es la única parte de la configuración que le levanta el límite a un guard, y quien la escribió
120
- // no es quien la lee dentro de seis meses: va como advertencia permanente, igual que un override.
121
- // Y se muestra resuelta porque resuelta es como la compara el guard — un `~` escrito solo exenta
122
- // la casa entera, y escrito no se nota.
123
- for (const exempt of CP.writableOutsideRoots(path.dirname(configPath), config)) {
124
- warnings.push(`ops.config.json: ${exempt.declared} está exenta del límite `
125
- + `de raíces (${exempt.path})`)
126
- }
127
- }
128
- } catch (error) {
129
- errors.push(`ops.config.json: JSON inválido (${error.message})`)
130
- }
131
- } else {
132
- warnings.push(`no existe ${path.relative(root, configPath)}`)
133
- }
134
-
135
- const epics = P.readEpics(root)
136
- const milestones = P.readBacklog(root)
137
- const done = P.readDone(root)
138
- errors.push(...B.validate(path.join(root, 'business-rules')))
139
- errors.push(...SR.validateRoadmapStructure(root))
140
- errors.push(...SR.validateBacklogStructure(root))
141
- errors.push(...SR.validateRules(root))
142
- errors.push(...SR.validateAdr(root))
143
- const backlog = milestones.flatMap((milestone) => milestone.tasks)
144
- const backlogSlugs = new Set(backlog.map((task) => task.slug))
145
- const epicNums = new Set()
146
- const storySlugs = new Set()
147
-
148
- const roles = new Set(AG.list(path.resolve(root, '..')).map((role) => role.slug))
149
- const wips = P.readWips(root)
150
- const adopted = AD.read(root)
151
- errors.push(...SZ.oversizedUnits({ epics, milestones }))
152
- errors.push(...PC.validateState({
153
- epics, milestones, done, wips, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
154
- }))
155
- warnings.push(...AD.report({ done, epics, adopted }))
156
- warnings.push(...PC.doneCeremonyWarnings(done, new Set(adopted)))
157
- warnings.push(...R.coverageWarnings(path.resolve(root, '..'), done))
158
- // Sin `RECURRING.md` no dice una palabra: una instancia que actualiza y no declara trabajo recurrente
159
- // no tiene por qué enterarse de que el contrato existe. Vencida avisa y no frena — lo que frena vive
160
- // en `HUMAN_ACTIONS.md`, y un aviso que salta siempre se termina apagando.
161
- // Un reclamo que nombra una tarea que no existe bloquea la cola sin que nada lo explique, y uno viejo
162
- // la bloquea para siempre. Lo primero es error; lo segundo avisa, porque abandonar no es un defecto.
163
- const claims = CL.read(root)
164
- errors.push(...CL.validate({ claims, milestones, done }))
165
- // Si la rama de cada tarea tomada se movió, que es lo único barato que distingue una tarea larga de
166
- // una abandonada. Sin repositorio resoluble el mapa queda vacío y el aviso vuelve a mirar sólo la
167
- // fecha, que es lo que había antes: degrada, no rompe.
168
- const activity = new Map()
169
- for (const claim of claims.filter((one) => !done.set.has(one.slug))) {
170
- const at = R.lastCommit(R.repoOf(path.join(root, '..'), claim.service), CL.branchOf(claim.slug))
171
- if (at) activity.set(claim.slug, at)
172
- }
173
- warnings.push(...CL.warnings({ claims, done, today: TODAY(), activity }))
174
- const recurring = RC.read(root)
175
- errors.push(...RC.validate(recurring))
176
- warnings.push(...RC.warnings(RC.status({ ...recurring, done, today: TODAY() })))
177
- warnings.push(...IB.warnings(root, done, config))
178
- warnings.push(...AD.sealWarnings(root))
179
- warnings.push(...R.unrecordedHumanActions(path.resolve(root, '..'), P.readHumanActions(root)))
180
- warnings.push(...AP.warnings(path.resolve(root, '..')))
181
- warnings.push(...TR.warnings(path.resolve(root, '..')))
182
-
183
- // Lo que `upgrade` conserva por estar editado deja de recibir mejoras, y eso es una deuda que no
184
- // avisa sola: la instancia queda con medio molde viejo y todo se ve normal. Sale acá para que se vea
185
- // en cada corrida y no sólo el día que alguien actualiza.
186
- const congelados = O.localChanges(path.resolve(root, '..'))
187
- if (congelados.length) {
188
- warnings.push(`${congelados.length} archivo(s) del molde congelados por edición local; `
189
- + '`upgrade` los conserva y no les trae mejoras')
190
- }
191
-
192
- // Y lo que `upgrade` no retiró porque no pudo demostrar que fuera suyo: queda ahí, sin colgar de
193
- // ningún mecanismo, hasta que alguien lo mueva o lo borre. Se cuenta por lo mismo que los congelados
194
- // — un resto que no se ve se vuelve permanente.
195
- const restos = O.RETIRED_COMPARTIDO.filter((relative) => fs.existsSync(path.join(root, '..', relative)))
196
- if (restos.length) {
197
- warnings.push(`${restos.length} ruta(s) retiradas siguen en disco con contenido tuyo `
198
- + `(${restos.join(', ')}); Cauce ya no las distribuye ni las toca`)
199
- }
200
-
201
- const integration = I.validate(path.resolve(root, '..'))
202
- errors.push(...integration.errors)
203
- warnings.push(...integration.warnings)
204
-
205
- warnings.push(...SR.competingSections(root))
206
- // Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
207
- // ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
208
- for (const override of O.overrides(path.resolve(root, '..'))) {
209
- // Y con qué se queda el proyecto: un override sano redefine lo que reemplaza, y el que deja IDs
210
- // afuera los retira sin decirlo. Nombrarlos es lo único que separa una decisión de un descuido.
211
- const retired = override.collection === 'planning/rules'
212
- ? SR.retiredByOverride(root, override.project)
213
- : []
214
- warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} `
215
- + `(override explícito)${retired.length ? `; deja de regir ${retired.join(', ')}` : ''}`)
216
- }
217
- // Y lo que instaló cada runner, contra esas mismas reglas (caso 099).
218
- warnings.push(...RL.staleLines(path.resolve(root, '..')))
219
- // Misma regla para los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
220
- const FK = require('../agents/fork')
221
- for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
222
-
223
- warnings.push(...OB.missingSections(path.resolve(root, '..')))
224
- warnings.push(...OB.orphanCredentials(path.resolve(root, '..')))
225
-
226
- if (cli.has('--json')) {
227
- console.log(JSON.stringify({
228
- ok: !errors.length,
229
- epics: epics.length,
230
- queued: backlog.length,
231
- done: done.entries.length,
232
- wips: wips.map((one) => one.task),
233
- errors,
234
- warnings,
235
- }))
236
- if (errors.length) process.exit(1)
237
- return
238
- }
239
-
240
- for (const warning of warnings) console.warn(`⚠ ${warning}`)
241
- for (const error of errors) console.error(`✗ ${error}`)
242
- if (errors.length) fail(`\n${errors.length} error(es), ${warnings.length} advertencia(s)`)
243
- console.log(
244
- `✓ planning válido: ${epics.length} épica(s), ${backlog.length} tarea(s) en cola, ` +
245
- `${done.entries.length} terminada(s)`,
246
- )
247
- }
248
-
249
65
  // Estado observable de planning sin mutar nada; base común de `tree` y de sus salidas.
250
66
  function treeJson({ epics, milestones, done, wips, inbox, queued, claims }) {
251
67
  const state = (slug) => done.set.has(slug) ? 'done' : queued.has(slug) ? 'queued' : 'pending'
@@ -407,6 +223,18 @@ function context(dir, cli) {
407
223
  // Tu propio nombre en una tarea «ajena» es la señal de que sos vos desde otro runner, y sin decirlo se
408
224
  // lee como que alguien te ganó la tarea.
409
225
  const dueño = (one) => (one.owner === report.owner ? `${one.owner} — vos, desde otro runner` : one.owner)
226
+ // Que el reclamo sea tuyo desde otro id ya se decía; que además haya un **plan escrito** bajo ese id, no.
227
+ // Esa es la mitad que cuesta la sesión: los pasos ya hechos están en un archivo que nadie nombra, y el id
228
+ // que lo recupera es justo el que esta sesión no supo deducir. Los dos datos están en el reclamo.
229
+ const tomadas = () => {
230
+ for (const one of report.taken) {
231
+ console.log(`TAKEN ${one.slug} (${dueño(one)})`)
232
+ if (one.owner === report.owner && one.wip) {
233
+ console.log(`PLAN ${one.slug}: su plan está en wip/${one.wip} — retomalo con `
234
+ + `\`export CAUCE_RUNNER=${one.runner}\``)
235
+ }
236
+ }
237
+ }
410
238
  const espera = () => {
411
239
  for (const one of report.waiting) {
412
240
  console.log(`WAIT ${one.slug}: espera a ${one.dep}${one.owner ? ` (${one.owner})` : ''}`)
@@ -424,7 +252,7 @@ function context(dir, cli) {
424
252
  if (next) console.log(`EPIC ${next.num}: ${next.title} — sin promover`)
425
253
  // Mismo motivo que `blocked` arriba, con otra causa: acá la cola no la traba una persona, la tiene
426
254
  // el equipo, y lo que corresponde es hablar con quien la tiene.
427
- for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
255
+ tomadas()
428
256
  espera()
429
257
  for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
430
258
  due()
@@ -452,7 +280,7 @@ function context(dir, cli) {
452
280
  console.log(report.claimed
453
281
  ? `CLAIM tuya desde el reclamo (${report.owner})`
454
282
  : `CLAIM libre — tomala con \`ops claim <planning> ${report.task.slug}\``)
455
- for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
283
+ tomadas()
456
284
  espera()
457
285
  if (report.blockedTasks.length) console.log(`SKIP ${report.blockedTasks.join(', ')} (acción humana abierta)`)
458
286
  for (const action of report.humanActions) console.log(`HUMAN ${action.task}: ${action.action}`)
@@ -489,4 +317,4 @@ function recurring(dir, cli) {
489
317
  }
490
318
  }
491
319
 
492
- module.exports = { check, evidence, tree, context, recurring }
320
+ module.exports = { evidence, tree, context, recurring }