@ingeniomaps/cauce 0.70.0 → 0.72.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 +178 -0
- package/agents/README.md +73 -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-governance-steward/learning/sources.yaml +17 -7
- 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/integrations-engineer/learning/sources.yaml +7 -5
- 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/logistics-operations-manager/learning/sources.yaml +17 -9
- 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/tech-lead/learning/sources.yaml +9 -8
- 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/treasury-analyst/learning/sources.yaml +14 -11
- 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/workflows/autobuild.js +39 -21
- package/engine/agents/evaluations.js +18 -4
- package/engine/agents/learning-seal.js +36 -5
- package/engine/agents/learning-sources.js +113 -5
- package/engine/agents/learning.js +13 -1
- package/engine/cli/planning.js +29 -0
- package/package.json +1 -1
- package/template/planning/PROTOCOL.md +14 -0
|
@@ -13,7 +13,7 @@ sources:
|
|
|
13
13
|
tier: standard
|
|
14
14
|
topics: [accessibility, wcag, cognitive-accessibility]
|
|
15
15
|
- name: ISO Human-centred Design
|
|
16
|
-
url: https://
|
|
16
|
+
url: https://committee.iso.org/standard/77520.html
|
|
17
17
|
tier: standard
|
|
18
18
|
topics: [human-centred-design, lifecycle]
|
|
19
19
|
- name: Nielsen Norman Group
|
|
@@ -79,7 +79,7 @@ Diseñar para que la experiencia sea perceptible, operable, comprensible y robus
|
|
|
79
79
|
|
|
80
80
|
Modelo sintetizado con fuentes revisadas en agosto de 2026:
|
|
81
81
|
|
|
82
|
-
- [ISO 9241-210:2019](https://
|
|
82
|
+
- [ISO 9241-210:2019](https://committee.iso.org/standard/77520.html): integrar diseño centrado en las personas durante el ciclo de vida de sistemas interactivos.
|
|
83
83
|
- [WCAG 2.2](https://www.w3.org/TR/WCAG22/): criterios comprobables para contenido perceptible, operable, comprensible y robusto.
|
|
84
84
|
- [Nielsen Norman Group: heurísticas de usabilidad](https://media.nngroup.com/media/articles/attachments/Heuristic_Summary1_A4_compressed.pdf): principios para inspeccionar feedback, control, consistencia, prevención y recuperación.
|
|
85
85
|
- [GOV.UK Design System Patterns](https://design-system.service.gov.uk/patterns/): patrones documentados para tareas concretas, adaptables al contexto.
|
|
@@ -9,7 +9,7 @@ export const meta = {
|
|
|
9
9
|
// la primera fase. Escribirlas también les da un detalle propio, que es lo que se lee al autorizar.
|
|
10
10
|
phases: [
|
|
11
11
|
{ title: 'Triage', detail: 'Contrato del proyecto y estado de planning' },
|
|
12
|
-
{ title: 'Pick', detail: 'La próxima tarea,
|
|
12
|
+
{ title: 'Pick', detail: 'La próxima tarea del hito, y la reserva antes de construirla' },
|
|
13
13
|
{ title: 'Classify', detail: 'Carril y reparto de la tarea que no los declara' },
|
|
14
14
|
{ title: 'Ready', detail: 'Aceptación concreta y sin decisiones pendientes' },
|
|
15
15
|
{ title: 'Decompose', detail: 'Partir la tarea que no entra en el tope de horas' },
|
|
@@ -40,8 +40,12 @@ const ROADMAP = `${P}/roadmap`
|
|
|
40
40
|
// Estado de planning tal como lo emite `ops context --json`; ningún modelo parsea BACKLOG ni WIP.
|
|
41
41
|
const CONTEXT = {
|
|
42
42
|
type: 'object', additionalProperties: false,
|
|
43
|
-
required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast'],
|
|
43
|
+
required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
|
|
44
44
|
properties: {
|
|
45
|
+
// Si el comando salió con error no hay estado que reportar, y `hasTask: false, queued: 0` es
|
|
46
|
+
// exactamente lo que un modelo completa cuando no tiene qué poner. Sin este campo esa invención se
|
|
47
|
+
// lee igual que una cola terminada, y Pick la toma como permiso para promover.
|
|
48
|
+
readOk: { type: 'boolean' },
|
|
45
49
|
blocked: { type: 'string' }, hasTask: { type: 'boolean' }, wipActive: { type: 'boolean' },
|
|
46
50
|
queued: { type: 'integer' }, slug: { type: 'string' }, hito: { type: 'string' },
|
|
47
51
|
service: { type: 'string' }, acceptance: { type: 'string' }, epic: { type: 'string' },
|
|
@@ -73,10 +77,6 @@ const CLAIM = {
|
|
|
73
77
|
type: 'object', additionalProperties: false, required: ['claimed'],
|
|
74
78
|
properties: { claimed: { type: 'boolean' }, details: { type: 'string' } },
|
|
75
79
|
}
|
|
76
|
-
const EXPANSION = {
|
|
77
|
-
type: 'object', additionalProperties: false, required: ['expanded'],
|
|
78
|
-
properties: { expanded: { type: 'boolean' }, hito: { type: 'string' }, reason: { type: 'string' } },
|
|
79
|
-
}
|
|
80
80
|
const READY = {
|
|
81
81
|
type: 'object', additionalProperties: false, required: ['ready', 'needsHuman'],
|
|
82
82
|
properties: {
|
|
@@ -229,8 +229,16 @@ const CLASSIFICATION = {
|
|
|
229
229
|
|
|
230
230
|
const CONTRACT = {
|
|
231
231
|
type: 'object', additionalProperties: false,
|
|
232
|
-
required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts'
|
|
232
|
+
required: ['project', 'workspaceRoots', 'maxTaskHours', 'commitPerTask', 'humanCheckpoint', 'contracts',
|
|
233
|
+
'rootOk'],
|
|
233
234
|
properties: {
|
|
235
|
+
// `ROOT` viaja escrito en el workflow y es relativo al cwd de los agentes: si la sesión abrió en otra
|
|
236
|
+
// carpeta, todas las rutas resuelven a `<raíz>/<raíz>/…` y ninguna existe. Nada lo comprobaba, y el
|
|
237
|
+
// recorrido gastaba Triage entero sobre archivos ausentes antes de parar más abajo por otra causa,
|
|
238
|
+
// nombrando el planning en vez de la raíz de la que ese planning cuelga.
|
|
239
|
+
//
|
|
240
|
+
// Se pregunta acá porque acá ya se leen los cuatro archivos: cuesta un campo y ningún agente más.
|
|
241
|
+
rootOk: { type: 'boolean' },
|
|
234
242
|
project: { type: 'string' }, workspaceRoots: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
235
243
|
// La puerta que el proyecto declara, si la declara. Viaja con la raíz porque es de la base de código
|
|
236
244
|
// y no del runner: un monorepo tiene una por servicio, y uno solo tiene una sola.
|
|
@@ -289,7 +297,9 @@ phase('Triage')
|
|
|
289
297
|
// subagente como «Límites del proyecto» eran sólo los genéricos del toolkit.
|
|
290
298
|
const contract = await agent(
|
|
291
299
|
`${BASE}\n\nLeé ${ROOT}/AGENTS.md, ${ORG}/workspace.md, ${CONFIG} y ${P}/PROTOCOL.md una sola vez y no ` +
|
|
292
|
-
`leas nada más.
|
|
300
|
+
`leas nada más. Poné rootOk en true sólo si los cuatro existieron y los pudiste leer; si alguno no ` +
|
|
301
|
+
`estaba, rootOk en false y el resto en sus valores vacíos, sin deducirlos de otra fuente. ` +
|
|
302
|
+
`Reportá los ` +
|
|
293
303
|
`valores de configuración textualmente: project, workspaceRoots como entradas "nombre → ruta", ` +
|
|
294
304
|
`runner.maxTaskHours, runner.commitPerTask y runner.humanCheckpointBetweenMilestones como humanCheckpoint. ` +
|
|
295
305
|
`En gates poné una entrada "ruta → comando" por cada workspaceRoot que declare \`verify\`, y ninguna por ` +
|
|
@@ -301,6 +311,12 @@ const contract = await agent(
|
|
|
301
311
|
{ schema: CONTRACT, label: 'contract-digest' },
|
|
302
312
|
)
|
|
303
313
|
if (!contract) return stop('contract-unavailable', `no se pudo leer ${CONFIG} ni ${P}/PROTOCOL.md`)
|
|
314
|
+
// Falla acá y nombrando la raíz, que es lo que hace falta para arreglarlo: parar más abajo mandaba a
|
|
315
|
+
// revisar el planning, y el planning está bien — lo que no existe es la carpeta de la que cuelga.
|
|
316
|
+
if (!contract.rootOk) {
|
|
317
|
+
return stop('root-unreadable', `${ROOT} no se pudo leer entero. Es una ruta relativa a la carpeta `
|
|
318
|
+
+ `donde se abre la herramienta: comprobá desde dónde estás corriendo el recorrido.`)
|
|
319
|
+
}
|
|
304
320
|
|
|
305
321
|
const bounds = contract.boundaries || []
|
|
306
322
|
const limits = bounds.length ? ` Límites del proyecto: ${bounds.join('; ')}.` : ''
|
|
@@ -329,12 +345,17 @@ const readContext = () => read(
|
|
|
329
345
|
`de task.tier; copiá slug, ` +
|
|
330
346
|
`hito, service, acceptance, ` +
|
|
331
347
|
`epic y cast de task, y epicContext de epic.context —vacío si no hay épica—. El comando es la fuente de ` +
|
|
332
|
-
`verdad: no abras archivos de planning para completarlo
|
|
348
|
+
`verdad: no abras archivos de planning para completarlo. Poné readOk en true sólo si el comando salió ` +
|
|
349
|
+
`con código 0 y devolvió JSON; si falló, readOk en false y el resto en sus valores vacíos, sin ` +
|
|
350
|
+
`deducir el estado de ninguna otra fuente.`,
|
|
333
351
|
{ schema: CONTEXT, label: 'planning-context' },
|
|
334
352
|
)
|
|
335
353
|
|
|
336
354
|
let planning = await readContext()
|
|
337
355
|
if (!planning) return stop('context-unavailable', `no se pudo leer el estado de ${P}`)
|
|
356
|
+
// Que el agente conteste no significa que haya leído: el schema se completa igual con ceros. Parar acá
|
|
357
|
+
// cuesta una corrida; seguir sobre una lectura fallida escribe en el BACKLOG, y eso no se revierte solo.
|
|
358
|
+
if (!planning.readOk) return stop('context-unavailable', `${P} no se pudo leer; revisá la ruta y el cwd`)
|
|
338
359
|
if (planning.blocked) return stop('awaiting-human-review', `${GATE} tiene un checkpoint humano sin resolver`)
|
|
339
360
|
|
|
340
361
|
let currentMilestone = planning.wipActive ? planning.hito : ''
|
|
@@ -350,18 +371,15 @@ const classified = new Set()
|
|
|
350
371
|
|
|
351
372
|
while (rounds++ < MAX_TASKS) {
|
|
352
373
|
phase('Pick')
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
planning = await readContext()
|
|
363
|
-
if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
|
|
364
|
-
}
|
|
374
|
+
// Sin tarea y con la cola vacía, la corrida termina. **No expande la próxima épica**, y eso no es una
|
|
375
|
+
// limitación sino la regla: el roadmap llama `open` a «candidata editable que aún no fue promovida al
|
|
376
|
+
// backlog», así que pegarla en la cola es promoverla — y BR-OPS-002 deja una propuesta fuera de la cola
|
|
377
|
+
// hasta que la apruebe una persona. El prompt que hacía esto pedía «la próxima épica abierta y
|
|
378
|
+
// aprobada», y «aprobada» no correspondía a ningún dato: una épica declara `epic`, `title`, `status` y
|
|
379
|
+
// `service`, y ninguno registra una aprobación.
|
|
380
|
+
//
|
|
381
|
+
// `context` nombra la que sigue, igual que nombra una recurrencia vencida y por el mismo motivo: la
|
|
382
|
+
// máquina calcula y la persona encola.
|
|
365
383
|
if (!planning.hasTask || (currentMilestone && planning.hito !== currentMilestone)) break
|
|
366
384
|
const task = {
|
|
367
385
|
id: planning.slug, hito: planning.hito, service: planning.service,
|
|
@@ -132,10 +132,21 @@ function behaviors(root, agent, kind) {
|
|
|
132
132
|
// tiene `SKILL.md`— así que se le podía agregar una dimensión al gate y sus veredictos anteriores
|
|
133
133
|
// seguían leyéndose vigentes. Pasó el mismo día que esto se escribió: `change-review` ganó la pregunta
|
|
134
134
|
// por las superficies críticas y sus tres casos aprobados no dijeron una palabra.
|
|
135
|
+
//
|
|
136
|
+
// Y hay un entorno donde git **no puede** contestar: un clon `--depth 1` tiene un solo commit y le
|
|
137
|
+
// atribuye todo el árbol, así que `log -1 -- <archivo>` devuelve la fecha del checkout para cualquier
|
|
138
|
+
// archivo. Comprobado con git 2.43.0 clonando este repositorio a profundidad 1: el `SKILL.md` de
|
|
139
|
+
// `site-reliability-engineer` daba el día del clon contra el 2026-09-02, que es cuando se editó.
|
|
140
|
+
//
|
|
141
|
+
// Ahí la respuesta no es una fecha vieja sino ninguna, y **se dice**: un aviso que anuncia un cambio
|
|
142
|
+
// que no ocurrió se lee igual que uno verdadero, y tres informes de dos cargos gastaron un hallazgo
|
|
143
|
+
// cada uno investigándolo antes de que saliera como el caso 052.
|
|
135
144
|
function contractChangedAt(dir, kind) {
|
|
136
145
|
const file = kind === 'flow' ? 'flow.json' : 'SKILL.md'
|
|
146
|
+
const shallow = spawnSync('git', ['-C', dir, 'rev-parse', '--is-shallow-repository'], { encoding: 'utf8' })
|
|
147
|
+
if ((shallow.stdout || '').trim() === 'true') return { date: '', truncated: true }
|
|
137
148
|
const git = spawnSync('git', ['-C', dir, 'log', '-1', '--format=%cs', '--', file], { encoding: 'utf8' })
|
|
138
|
-
return git.status === 0 ? (git.stdout || '').trim() : ''
|
|
149
|
+
return { date: git.status === 0 ? (git.stdout || '').trim() : '', truncated: false }
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
function resultsDir(root, agent, kind) {
|
|
@@ -264,10 +275,13 @@ function validate(root, agent, kind) {
|
|
|
264
275
|
if (state.failed.length) {
|
|
265
276
|
warnings.push(`${state.failed.length} caso(s) no pasan: ${state.failed.join(', ')}`)
|
|
266
277
|
}
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
278
|
+
const contract = contractChangedAt(subject(root, agent, kind), kind)
|
|
279
|
+
if (contract.truncated) {
|
|
280
|
+
warnings.push('no se puede saber si el contrato cambió después de estos veredictos: el checkout '
|
|
281
|
+
+ 'está truncado y git le atribuye todo a su único commit (se destraba con fetch-depth: 0)')
|
|
282
|
+
} else if (contract.date && contract.date > state.oldest) {
|
|
269
283
|
const which = state.oldest === state.newest ? 'la última corrida es' : 'el veredicto más viejo es'
|
|
270
|
-
warnings.push(`el contrato cambió el ${
|
|
284
|
+
warnings.push(`el contrato cambió el ${contract.date} y ${which} del ${state.oldest}: `
|
|
271
285
|
+ 'mide una versión anterior')
|
|
272
286
|
}
|
|
273
287
|
return { errors, warnings, cases: total, last, state }
|
|
@@ -12,6 +12,8 @@ const path = require('node:path')
|
|
|
12
12
|
const { atomicWrite } = require('../core/files')
|
|
13
13
|
const { isoDate, proposalFiles, proposalState, assertWritable, lastOfPeriod } = require('./learning-files')
|
|
14
14
|
const { section } = require('../planning/parser')
|
|
15
|
+
// La misma identidad con la que se reclama una tarea: quién es la persona, no qué runner corre.
|
|
16
|
+
const { owner } = require('../planning/claims')
|
|
15
17
|
|
|
16
18
|
// El cuerpo sin sellar, en los dos estados que produce el ciclo: «pendiente» lo escribe el molde y
|
|
17
19
|
// «aprobada» la firma. Lo lee la guarda de más abajo y lo reemplaza el sello, así que vive una vez.
|
|
@@ -102,22 +104,51 @@ function archive(root, agent, period = '', kind = 'agent') {
|
|
|
102
104
|
+ 'Archivar es para lo que se miró y no cambia nada.',
|
|
103
105
|
)
|
|
104
106
|
}
|
|
107
|
+
// Quién archivó, que es la mitad que faltaba. Sellar saca el responsable del documento —lo escribió la
|
|
108
|
+
// firma— y archivar no pasa por firma, así que sale de la identidad de quien corre el comando: la misma
|
|
109
|
+
// que `claim` usa para decir de quién es una tarea. Sin esto la propuesta quedaba en «por definir» y no
|
|
110
|
+
// había forma de distinguir una decisión de un olvido.
|
|
111
|
+
const responsible = owner(root) || 'sin identificar'
|
|
105
112
|
atomicWrite(file, text
|
|
106
113
|
.replace(/^status:\s*\S+\s*$/m, 'status: archived')
|
|
107
114
|
.replace(/^-[ \t]*Estado:[ \t]*pendiente[ \t]*$/mi, '- Estado: archivada')
|
|
115
|
+
.replace(/^-[ \t]*Responsable:[ \t]*por definir[ \t]*$/mi, `- Responsable: ${responsible}`)
|
|
108
116
|
.replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`))
|
|
117
|
+
// La fila va para los dos tipos, y no sólo para los recorridos como en `seal`: allá los cargos los
|
|
118
|
+
// registra `agent-promote` al aplicar, y archivar no pasa por ningún workflow que lo haga.
|
|
119
|
+
// La raíz del cargo, que es `<cargo>/learning/proposals/<archivo>` sin sus tres últimos tramos:
|
|
120
|
+
// `appendHistory` agrega `learning/` por su cuenta.
|
|
121
|
+
//
|
|
122
|
+
// La celda del cambio lleva el criterio y no queda vacía. Es el único que este comando admite —archivar
|
|
123
|
+
// *es* decidir que no cambia nada— así que decirlo evita que la fila se lea como un registro a medias.
|
|
124
|
+
// Archivar por otra razón, como posponer, necesitaría un campo que hoy no existe.
|
|
125
|
+
appendHistory(path.dirname(path.dirname(path.dirname(file))), file, responsible,
|
|
126
|
+
'Se miró y no cambia nada.', 'archivada')
|
|
109
127
|
return { file, already: false }
|
|
110
128
|
}
|
|
111
129
|
|
|
112
|
-
// Una fila por propuesta
|
|
113
|
-
// una tabla que lo repite entero deja de leerse.
|
|
114
|
-
|
|
130
|
+
// Una fila por propuesta cerrada, cualquiera sea el destino. El cambio va en una línea: el documento
|
|
131
|
+
// entero está a un enlace, y una tabla que lo repite entero deja de leerse.
|
|
132
|
+
//
|
|
133
|
+
// La decisión es un parámetro y no la constante `aplicada` porque la columna de la tabla se llama
|
|
134
|
+
// «Decisión» y hay dos: aplicar y archivar. Archivar no dejaba fila, así que una propuesta mirada y
|
|
135
|
+
// descartada era indistinguible de una que nadie miró — y eso lo pagaban los informes siguientes, que
|
|
136
|
+
// gastaban su recomendación explicando el estado en vez de su profesión.
|
|
137
|
+
const HISTORY_HEADER = '| Fecha | Propuesta | Decisión | Aprobó | Cambio aplicado |\n|---|---|---|---|---|\n'
|
|
138
|
+
|
|
139
|
+
function appendHistory(target, file, responsible, change, decision = 'aplicada') {
|
|
115
140
|
const history = path.join(target, 'learning', 'HISTORY.md')
|
|
116
141
|
if (!fs.existsSync(history)) return
|
|
142
|
+
const previous = fs.readFileSync(history, 'utf8')
|
|
117
143
|
const line = change.split('\n').map((one) => one.trim()).filter(Boolean)[0] || ''
|
|
118
|
-
const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` |
|
|
144
|
+
const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` | ${decision} | ${responsible} `
|
|
119
145
|
+ `| ${line.slice(0, 160)} |\n`
|
|
120
|
-
|
|
146
|
+
// Dieciséis de los cincuenta y tres cargos tienen el archivo sin la tabla, sólo con su párrafo de
|
|
147
|
+
// encabezado, y la fila quedaba pegada ahí: en markdown eso no es una tabla sino texto con barras, y
|
|
148
|
+
// nadie lo veía porque el archivo se lee dos veces al año. Se agrega la cabecera antes de la primera
|
|
149
|
+
// fila en vez de exigir que ya esté, que es pedirle a cada cargo que se acuerde.
|
|
150
|
+
const cabecera = /^\|\s*Fecha\s*\|/m.test(previous) ? '' : `\n${HISTORY_HEADER}`
|
|
151
|
+
fs.appendFileSync(history, `${previous.endsWith('\n') ? '' : '\n'}${cabecera}${row}`)
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
module.exports = { seal, archive }
|
|
@@ -86,27 +86,119 @@ function cadence(root, agent) {
|
|
|
86
86
|
// Basta con las líneas `tier:`: el archivo es del catálogo, no de un tercero, y agregar un parser de
|
|
87
87
|
// YAML por un campo rompería la regla de cero dependencias.
|
|
88
88
|
function sourceTiers(text) {
|
|
89
|
-
|
|
90
|
-
return [...body.matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
|
|
89
|
+
return [...sourcesBody(text).matchAll(/tier:\s*([A-Za-z-]+)/g)].map((hit) => hit[1])
|
|
91
90
|
}
|
|
92
91
|
|
|
92
|
+
// El cuerpo de `sources:` termina donde empieza `pending:`. Sin este corte, una pendiente entraba como
|
|
93
|
+
// fuente declarada y el chequeo semanal la reportaba rota todas las semanas — que es exactamente el
|
|
94
|
+
// aviso permanente que la lista existe para no producir.
|
|
95
|
+
function sourcesBody(text) {
|
|
96
|
+
if (!text.includes('sources:')) return ''
|
|
97
|
+
const body = text.slice(text.indexOf('sources:'))
|
|
98
|
+
const corte = body.search(/^pending:/m)
|
|
99
|
+
return corte === -1 ? body : body.slice(0, corte)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Una entrada escrita en una sola línea: seis cargos del catálogo la escriben así y cuarenta y siete la
|
|
103
|
+
// reparten en varias. Leyendo sólo la segunda forma, esos seis no tenían el chequeo de URL duplicada que
|
|
104
|
+
// hay más abajo, y nada lo decía porque no encontrar duplicados y no mirar se ven igual.
|
|
105
|
+
const FLOW_ENTRY = /^\s*-\s*\{[^}]*\bname:\s*([^,}]+?)\s*,[^}]*\burl:\s*"?([^",}\s]+)/
|
|
106
|
+
const quitar = (value) => value.replace(/^['"]|['"]$/g, '')
|
|
107
|
+
|
|
93
108
|
// Las fuentes de un cargo, por su URL. La misma URL con dos nombres es una sola fuente contada dos
|
|
94
109
|
// veces: el catálogo llegó a tener la especificación OpenAPI bajo tres —`OpenAPI Specification`,
|
|
95
110
|
// `...latest published` y `...3.2.0`— así que arreglarle el `tier` a un cargo no se lo arreglaba a los
|
|
96
111
|
// otros, y quien leyera el informe vería la misma página citada como si fueran tres.
|
|
97
112
|
function sourceUrls(text) {
|
|
98
|
-
const body =
|
|
113
|
+
const body = sourcesBody(text)
|
|
99
114
|
const out = []
|
|
100
115
|
let name = ''
|
|
101
116
|
for (const line of body.split('\n')) {
|
|
117
|
+
const flow = line.match(FLOW_ENTRY)
|
|
118
|
+
if (flow) { out.push({ name: quitar(flow[1]), url: flow[2].replace(/\/+$/, '') }); continue }
|
|
102
119
|
const declared = line.match(/^\s*-\s*name:\s*(.+?)\s*$/)
|
|
103
|
-
if (declared) { name = declared[1]
|
|
120
|
+
if (declared) { name = quitar(declared[1]); continue }
|
|
104
121
|
const url = line.match(/^\s*url:\s*(\S+)/)
|
|
105
122
|
if (url && name) out.push({ name, url: url[1].replace(/\/+$/, '') })
|
|
106
123
|
}
|
|
107
124
|
return out
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
// Todo lo que un cargo cita y alguien va a abrir. `sources.yaml` es lo que investiga; `references/` y
|
|
128
|
+
// `SKILL.md` son el método que sigue, y hasta 0.71.0 nadie las miraba: 29 de las 207 URLs de esos
|
|
129
|
+
// documentos no servían, entre ellas dos 404 de páginas que se habían movido.
|
|
130
|
+
//
|
|
131
|
+
// `evaluations/` queda afuera y no por costo: los casos adversariales **inventan** dominios a propósito
|
|
132
|
+
// —veintiuna URLs bajo `.example` y marcas que no existen— y comprobarlas reportaría rotas las que están
|
|
133
|
+
// bien escritas. `learning/reports` y `learning/proposals` también, por lo contrario: son evidencia
|
|
134
|
+
// fechada de que algo dio 403 el día que se consultó, y eso no se arregla.
|
|
135
|
+
const DOC_URL = /https?:\/\/[^\s)>"`\]]+/g
|
|
136
|
+
function documentUrls(dir) {
|
|
137
|
+
const out = []
|
|
138
|
+
const seen = new Set()
|
|
139
|
+
const add = (raw, origin) => {
|
|
140
|
+
const url = raw.replace(/[.,;:]+$/, '')
|
|
141
|
+
if (seen.has(url)) return
|
|
142
|
+
seen.add(url)
|
|
143
|
+
out.push({ url, origin })
|
|
144
|
+
}
|
|
145
|
+
const below = (base, relative) => {
|
|
146
|
+
let entries = []
|
|
147
|
+
try { entries = fs.readdirSync(path.join(base, relative), { withFileTypes: true }) } catch { return }
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const next = `${relative}/${entry.name}`
|
|
150
|
+
if (entry.isDirectory()) { below(base, next); continue }
|
|
151
|
+
if (!entry.name.endsWith('.md')) continue
|
|
152
|
+
for (const hit of fs.readFileSync(path.join(base, next), 'utf8').match(DOC_URL) || []) add(hit, next)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
below(dir, 'references')
|
|
156
|
+
const skill = path.join(dir, 'SKILL.md')
|
|
157
|
+
if (fs.existsSync(skill)) {
|
|
158
|
+
for (const hit of fs.readFileSync(skill, 'utf8').match(DOC_URL) || []) add(hit, 'SKILL.md')
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Lo que el cargo probó, no pudo abrir y va a volver a necesitar. Cinco cargos lo escribían ya como
|
|
164
|
+
// comentario en su propio archivo —el steward hasta puso «Registrar cuando exista una ficha legible»,
|
|
165
|
+
// que es un recordatorio que nadie iba a revisar—, así que la forma existía y lo que faltaba era que
|
|
166
|
+
// alguien la mirara.
|
|
167
|
+
//
|
|
168
|
+
// `url` es opcional a propósito: la mitad de esas entradas están pendientes **porque no hay ninguna URL
|
|
169
|
+
// que responda** —ISO/IEC/IEEE 24765, la ley federal mexicana—, y exigirla habría dejado fuera
|
|
170
|
+
// justamente las que más cuesta resolver. Lo que no es opcional es `why`: sin la razón, la lista es un
|
|
171
|
+
// cementerio de enlaces que nadie sabe por qué están.
|
|
172
|
+
//
|
|
173
|
+
// La continuación de línea se une en vez de prohibirse: `why` es una frase y una frase se envuelve. La
|
|
174
|
+
// primera versión la cortaba en el primer salto y **no avisaba** —cuatro razones quedaron a media
|
|
175
|
+
// oración sin que nada fallara—, y prohibir la forma no evita que la próxima persona la escriba.
|
|
176
|
+
const PENDING_FIELD = /^\s{4}(name|url|why|since):\s*(.+?)\s*$/
|
|
177
|
+
function pendingSources(text) {
|
|
178
|
+
const corte = text.search(/^pending:/m)
|
|
179
|
+
if (corte === -1) return []
|
|
180
|
+
const out = []
|
|
181
|
+
let one = null
|
|
182
|
+
let last = ''
|
|
183
|
+
for (const line of text.slice(corte).split('\n').slice(1)) {
|
|
184
|
+
if (/^\S/.test(line)) break
|
|
185
|
+
if (/^\s{2}-\s/.test(line)) {
|
|
186
|
+
if (one) out.push(one)
|
|
187
|
+
one = {}
|
|
188
|
+
last = ''
|
|
189
|
+
const primero = line.match(/^\s{2}-\s*(name|url|why|since):\s*(.+?)\s*$/)
|
|
190
|
+
if (primero) { one[primero[1]] = quitar(primero[2]); last = primero[1] }
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const campo = line.match(PENDING_FIELD)
|
|
194
|
+
if (campo && one) { one[campo[1]] = quitar(campo[2]); last = campo[1]; continue }
|
|
195
|
+
const sigue = line.match(/^\s{6,}(\S.*?)\s*$/)
|
|
196
|
+
if (sigue && one && last) one[last] += ` ${sigue[1]}`
|
|
197
|
+
}
|
|
198
|
+
if (one) out.push(one)
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
110
202
|
function evaluate(root, agent) {
|
|
111
203
|
const target = catalog.resolve(root, agent)
|
|
112
204
|
const errors = []
|
|
@@ -145,6 +237,20 @@ function evaluate(root, agent) {
|
|
|
145
237
|
}
|
|
146
238
|
byUrl.set(one.url, one.name)
|
|
147
239
|
}
|
|
240
|
+
// Una pendiente sin razón es un enlace muerto con fecha, y sin fecha no se puede ver que lleva
|
|
241
|
+
// meses ahí. Los dos campos son la mitad del valor de la lista: lo que la vuelve revisable.
|
|
242
|
+
for (const one of pendingSources(fs.readFileSync(sourcesFile, 'utf8'))) {
|
|
243
|
+
const falta = ['name', 'why', 'since'].filter((campo) => !one[campo])
|
|
244
|
+
if (falta.length) {
|
|
245
|
+
errors.push(`sources.yaml: una pendiente no declara ${falta.join(' ni ')}`
|
|
246
|
+
+ `${one.name ? ` (${one.name})` : ''}`)
|
|
247
|
+
}
|
|
248
|
+
// Declarada y pendiente a la vez es una contradicción que el chequeo semanal no puede resolver:
|
|
249
|
+
// la reportaría rota como fuente y recuperada como pendiente en la misma corrida.
|
|
250
|
+
if (one.url && byUrl.has(one.url)) {
|
|
251
|
+
errors.push(`sources.yaml: ${one.url} está declarada como fuente y también como pendiente`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
148
254
|
}
|
|
149
255
|
const skill = fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8').toLowerCase()
|
|
150
256
|
for (const phrase of ['no inventar', 'autorización', 'evidencia observable']) {
|
|
@@ -193,4 +299,6 @@ function evaluate(root, agent) {
|
|
|
193
299
|
return { errors, warnings, proposals: proposals.length, pending, cases }
|
|
194
300
|
}
|
|
195
301
|
|
|
196
|
-
module.exports = {
|
|
302
|
+
module.exports = {
|
|
303
|
+
SOURCE_TIERS, cadence, documentUrls, evaluate, evaluateTeam, pendingSources, sourceUrls,
|
|
304
|
+
}
|
|
@@ -40,15 +40,21 @@ function prepareReport(root, agent, now = new Date()) {
|
|
|
40
40
|
// Los sellos de estado de este módulo escriben atómico y esto no, y la diferencia es qué se pierde
|
|
41
41
|
// si la escritura se corta: allá el archivo ya existía y quedaría truncado, acá no había nada. Un
|
|
42
42
|
// documento nuevo a medio escribir se ve; uno viejo a medio pisar se lee como si estuviera entero.
|
|
43
|
+
// `propone` nace sin contestar y lo completa quien escribe el informe. No se deduce del texto de
|
|
44
|
+
// «Recomendación»: los veinte informes del 2026-09-07 tenían texto ahí y once no proponían ningún
|
|
45
|
+
// cambio —explicaban por qué, que es lo correcto—, así que «sección vacía» no distingue nada. Con
|
|
46
|
+
// el campo, un informe que no cambia ningún contrato se mergea solo en vez de gastar una revisión
|
|
47
|
+
// humana que no puede decidir nada.
|
|
43
48
|
fs.writeFileSync(file, `---
|
|
44
49
|
agent: ${agent}
|
|
45
50
|
date: ${isoDate(now)}
|
|
46
51
|
status: draft
|
|
52
|
+
propone: por-completar
|
|
47
53
|
---
|
|
48
54
|
|
|
49
55
|
# Investigación semanal — ${isoDate(now)}
|
|
50
56
|
|
|
51
|
-
<!--
|
|
57
|
+
<!-- Tres convenciones que el ciclo necesita y que nada más sostiene:
|
|
52
58
|
|
|
53
59
|
· Etiquetá cada hallazgo H1, H2, … en el orden en que aparecen. «Evidencia» y «Recomendación» se
|
|
54
60
|
refieren a ellos por esa clave, y la propuesta mensual la cita para decir de qué hallazgo sale
|
|
@@ -57,6 +63,12 @@ status: draft
|
|
|
57
63
|
· No renombres los títulos. «## Recomendación» se lee con un patrón exacto y es lo único que la
|
|
58
64
|
propuesta consolida de cada informe: renombrarlo no da error, deja la propuesta vacía.
|
|
59
65
|
|
|
66
|
+
· Contestá «propone» en el frontmatter, con «si» o con «no». Es «si» cuando el informe pide tocar
|
|
67
|
+
algún archivo del cargo —SKILL.md, sources.yaml, references/, un caso— y «no» cuando lo hallado
|
|
68
|
+
no cambia ningún contrato, aunque «Recomendación» explique largamente por qué. Un «no» se
|
|
69
|
+
mergea sin revisión humana: no hay nada que decidir. Ante la duda va «si», que sólo cuesta una
|
|
70
|
+
mirada.
|
|
71
|
+
|
|
60
72
|
Este comentario vive fuera de toda sección a propósito — dentro de «Recomendación» viajaría a cada
|
|
61
73
|
propuesta consolidada. -->
|
|
62
74
|
|
package/engine/cli/planning.js
CHANGED
|
@@ -266,6 +266,9 @@ function treeJson({ epics, milestones, done, wips, inbox, queued, claims }) {
|
|
|
266
266
|
|
|
267
267
|
function tree(dir, cli) {
|
|
268
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)
|
|
269
272
|
const state = ST.snapshot(root)
|
|
270
273
|
if (cli.has('--json')) return treeJson(state)
|
|
271
274
|
const { epics, milestones, done, wips, inbox, queued, claims } = state
|
|
@@ -304,9 +307,29 @@ function tree(dir, cli) {
|
|
|
304
307
|
console.log(`${paint('1', 'DONE')} ${done.entries.length} tareas\n`)
|
|
305
308
|
}
|
|
306
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
|
+
|
|
307
329
|
// Contexto mínimo suficiente para ejecutar una tarea, en lugar de releer roadmap, BACKLOG y WIP enteros.
|
|
308
330
|
function context(dir, cli) {
|
|
309
331
|
const root = path.resolve(dir || '.')
|
|
332
|
+
assertPlanning(root)
|
|
310
333
|
const state = ST.snapshot(root)
|
|
311
334
|
// Acotar la cola a un hito es como un equipo se reparte trabajo sin coordinarse: dos personas en hitos
|
|
312
335
|
// distintos casi nunca dependen entre sí ni tocan los mismos archivos. Lo que se acota es qué se
|
|
@@ -372,6 +395,10 @@ function context(dir, cli) {
|
|
|
372
395
|
// vuelta. Que aparezca es la señal.
|
|
373
396
|
recurring: RC.status({ ...RC.read(root), done: state.done, today: TODAY() })
|
|
374
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,
|
|
375
402
|
}
|
|
376
403
|
if (cli.has('--json')) return console.log(JSON.stringify(report))
|
|
377
404
|
|
|
@@ -403,6 +430,8 @@ function context(dir, cli) {
|
|
|
403
430
|
}
|
|
404
431
|
if (!report.task) {
|
|
405
432
|
console.log('TASK (sin tarea disponible)')
|
|
433
|
+
const { nextEpic: next } = report
|
|
434
|
+
if (next) console.log(`EPIC ${next.num}: ${next.title} — sin promover`)
|
|
406
435
|
// Mismo motivo que `blocked` arriba, con otra causa: acá la cola no la traba una persona, la tiene
|
|
407
436
|
// el equipo, y lo que corresponde es hablar con quien la tiene.
|
|
408
437
|
for (const one of report.taken) console.log(`TAKEN ${one.slug} (${dueño(one)})`)
|
package/package.json
CHANGED
|
@@ -81,6 +81,20 @@ El lane reduce ceremonia, nunca seguridad, aceptación ni evidencia: Verify y el
|
|
|
81
81
|
cuatro. Lo que decide el carril es la superficie del cambio y no su tamaño en líneas — un `if` en el
|
|
82
82
|
chequeo de permisos es `full`, y un componente entero de presentación puede ser `directo`.
|
|
83
83
|
|
|
84
|
+
Escribir la línea es también contrastarla. Declara cuatro cosas —qué hace, en qué carril, quién entrega y
|
|
85
|
+
revisa, con qué se comprueba— y las cuatro salen de la misma mano en el mismo acto, así que nada las cruza
|
|
86
|
+
después. Releerlas no encuentra el hueco: una aceptación incompleta se lee perfecta, porque todo lo que
|
|
87
|
+
dice es cierto.
|
|
88
|
+
|
|
89
|
+
Antes de dar la tarea por escrita se recorre su descripción frase por frase y se contesta, por cada cosa
|
|
90
|
+
que promete, cuál condición de aceptación la comprueba; se lee el carril contra la superficie que toca y
|
|
91
|
+
no contra su tamaño; y se comprueba que el cast entregue a quien construye. Lo que quede sin condición se
|
|
92
|
+
agrega o se declara fuera de alcance en la línea.
|
|
93
|
+
|
|
94
|
+
Es el único momento en que las cuatro se pueden mirar juntas, y por eso la pasada vive acá y no en una
|
|
95
|
+
fase: después el carril ya decide cuáles corren, y la que revisaría es una de las que ese carril puede
|
|
96
|
+
saltar. Una tarea mal marcada `express` es justamente la que se salta la fase donde alguien lo notaría.
|
|
97
|
+
|
|
84
98
|
## Invariantes
|
|
85
99
|
|
|
86
100
|
1. Una tarea tiene un dueño de estado: roadmap → BACKLOG → overlay WIP → DONE.
|