@ingeniomaps/cauce 0.97.0 → 0.99.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 +162 -0
- package/README.md +1 -0
- package/agents/roles/system/accounting-specialist/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/ai-governance-lead/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/ai-product-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/analytics-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/backend-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/business-strategist/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/cloud-architect/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/community-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/content-specialist/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/customer-success-manager/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/customer-support-specialist/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/data-analyst/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/data-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/data-governance-steward/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/data-scientist/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/database-administrator/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/developer-relations-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/devops-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/engineering-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/financial-controller/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/finops-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/fraud-risk-analyst/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/frontend-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/growth-marketer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/implementation-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/integrations-engineer/learning/AUTOMATION.md +3 -3
- package/agents/roles/system/kyc-aml-specialist/learning/AUTOMATION.md +3 -3
- package/agents/roles/system/legal-counsel/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/logistics-operations-manager/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/machine-learning-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/mlops-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/mobile-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/partnerships-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/people-operations-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/privacy-compliance-specialist/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/procurement-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/product-manager/learning/AUTOMATION.md +3 -3
- package/agents/roles/system/product-marketing-manager/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/project-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/qa-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/release-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/sales-representative/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/security-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/site-reliability-engineer/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/software-architect/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/solutions-engineer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/tech-lead/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/technical-program-manager/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/technical-writer/learning/AUTOMATION.md +1 -1
- package/agents/roles/system/treasury-analyst/learning/AUTOMATION.md +2 -2
- package/agents/roles/system/ui-designer/learning/AUTOMATION.md +3 -3
- package/agents/roles/system/user-researcher/learning/AUTOMATION.md +3 -3
- package/agents/roles/system/ux-designer/learning/AUTOMATION.md +3 -3
- package/automatization/hooks/README.md +10 -5
- package/automatization/runners/antigravity/README.md +4 -1
- package/automatization/runners/antigravity/hook.js +84 -22
- package/automatization/runners/antigravity/manifest.json +1 -0
- package/automatization/shared/acceptance.js +26 -0
- package/automatization/workflows/agent-propose.js +2 -2
- package/automatization/workflows/autobuild.js +121 -38
- package/engine/agents/learning-files.js +18 -0
- package/engine/agents/learning-seal.js +24 -12
- package/engine/agents/learning-sources.js +12 -10
- package/engine/agents/learning.js +29 -7
- package/engine/automation/index.js +11 -2
- package/engine/automation/registration.js +89 -0
- package/engine/cli/args.js +2 -2
- package/engine/cli/catalog.js +12 -12
- package/engine/cli/ops.js +2 -1
- package/engine/cli/validate.js +3 -0
- package/engine/cli/wiring.js +1 -1
- package/engine/config/validate.js +30 -11
- package/engine/core/migrations.js +275 -0
- package/engine/core/repos.js +18 -4
- package/engine/hooks/approval.js +38 -7
- package/engine/hooks/chat.js +97 -27
- package/engine/hooks/files.js +6 -104
- package/engine/hooks/input.js +67 -11
- package/engine/hooks/migrations.js +93 -0
- package/engine/hooks/push.js +8 -5
- package/engine/hooks/run.js +10 -5
- package/engine/hooks/self-approval.js +2 -2
- package/engine/hooks/shell.js +34 -0
- package/engine/hooks/verify.js +79 -22
- package/engine/planning/acceptance.js +18 -0
- package/engine/planning/contracts.js +78 -45
- package/engine/schemas/ops-config.schema.json +9 -0
- package/package.json +1 -1
- package/template/AGENTS.md +8 -5
- package/template/Makefile +1 -1
- package/template/planning/PROTOCOL.md +6 -2
|
@@ -28,6 +28,7 @@ export const meta = {
|
|
|
28
28
|
|
|
29
29
|
{{INCLUDE:shared/workflow-root.js}}
|
|
30
30
|
{{INCLUDE:shared/inbox.js}}
|
|
31
|
+
{{INCLUDE:shared/acceptance.js}}
|
|
31
32
|
const CONFIG = `${ROOT}/ops.config.json`
|
|
32
33
|
const P = `${ROOT}/planning`
|
|
33
34
|
const ORG = `${ROOT}/organization`
|
|
@@ -43,6 +44,13 @@ const GATE = `${P}/AWAITING_REVIEW.md`
|
|
|
43
44
|
// suelta no es un envoltorio. Por eso también la escapada en vez de alternar el estilo de comillas.
|
|
44
45
|
const QUOTES = ['\'', '"']
|
|
45
46
|
|
|
47
|
+
// Si la fila que registró una parada quedó pendiente. `context` sólo lista las pendientes, así que
|
|
48
|
+
// preguntar por la presencia de la tarea alcanza, y no hace falta que un modelo lea el estado.
|
|
49
|
+
const HUMAN_ROW = {
|
|
50
|
+
type: 'object', additionalProperties: false, required: ['readOk', 'pending'],
|
|
51
|
+
properties: { readOk: { type: 'boolean' }, pending: { type: 'boolean' } },
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
const CONTEXT = {
|
|
47
55
|
type: 'object', additionalProperties: false,
|
|
48
56
|
required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
|
|
@@ -183,19 +191,28 @@ const REVIEWED = { ...DECISION, required: [...DECISION.required, 'rules'],
|
|
|
183
191
|
// menos —o que ni existe— sale verde igual, y el guard de verify tampoco lo ve porque también mira exit
|
|
184
192
|
// codes. Por eso `uncovered` se contrasta contra la aceptación leyendo el fuente, no la salida (R9).
|
|
185
193
|
const VERIFY = {
|
|
186
|
-
type: 'object', additionalProperties: false, required: ['passed', 'commands', 'details', 'uncovered'],
|
|
194
|
+
type: 'object', additionalProperties: false, required: ['passed', 'commands', 'details', 'uncovered', 'covered'],
|
|
187
195
|
properties: {
|
|
188
196
|
passed: { type: 'boolean' }, details: { type: 'string' },
|
|
189
|
-
//
|
|
190
|
-
// el propio recorrido puede hacer, a
|
|
191
|
-
//
|
|
197
|
+
// Tres causas que se leen igual en el resultado y piden cosas distintas: a una le falta trabajo que
|
|
198
|
+
// el propio recorrido puede hacer, a otra le falta una decisión que no es suya, y la tercera no tiene
|
|
199
|
+
// prueba posible porque su entregable no se ejecuta —un ADR, una política—. Sin separarlas, la corrida
|
|
200
|
+
// frena por las tres, una persona resuelve lo que se resolvía solo y la tarea de decisión no cierra
|
|
201
|
+
// nunca (caso 189). `reason` es lo que Done escribe en `tests: n/a — <razón>`.
|
|
192
202
|
uncovered: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
193
203
|
required: ['criterion', 'cause'],
|
|
194
204
|
properties: {
|
|
195
205
|
criterion: { type: 'string' },
|
|
196
|
-
cause: { type: 'string', enum: ['missing-test', 'ambiguous'] },
|
|
206
|
+
cause: { type: 'string', enum: ['missing-test', 'ambiguous', 'no-surface'] },
|
|
207
|
+
reason: { type: 'string' },
|
|
197
208
|
},
|
|
198
209
|
} },
|
|
210
|
+
// El mapeo que Verify arma al contrastar y del que sale `tests: CN → prueba`. Sin viajar, Done lo
|
|
211
|
+
// componía de memoria (hallazgo del 189).
|
|
212
|
+
covered: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
213
|
+
required: ['criterion', 'test'],
|
|
214
|
+
properties: { criterion: { type: 'string' }, test: { type: 'string' } },
|
|
215
|
+
} },
|
|
199
216
|
commands: { type: 'array', items: { type: 'object', required: ['cmd', 'exitCode'], properties: {
|
|
200
217
|
cmd: { type: 'string' }, exitCode: { type: 'integer' }, note: { type: 'string' },
|
|
201
218
|
ranTests: { type: 'boolean' },
|
|
@@ -440,13 +457,32 @@ const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
|
|
|
440
457
|
const run = (prompt, options = {}) => agent(`${SCOPE()}\n\n${prompt}`, options)
|
|
441
458
|
const write = (prompt, options = {}) => agent(`${LEDGER()}\n\n${prompt}`, options)
|
|
442
459
|
|
|
443
|
-
// Las
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
460
|
+
// Las paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es el
|
|
461
|
+
// único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso se
|
|
462
|
+
// espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó (caso 087).
|
|
463
|
+
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó como tiene que quedar.
|
|
464
|
+
//
|
|
465
|
+
// Contestar no alcanza: el agente que acaba de escribir el diagnóstico entero lo lee como ya resuelto, y
|
|
466
|
+
// en una corrida real escribió la fila `resuelta` y la firmó como «decidido por el dueño», desbloqueando
|
|
467
|
+
// la tarea sin que nadie decidiera nada (caso 180). Por eso cada pedido arranca diciendo que la fila nace
|
|
468
|
+
// pendiente, y cuando la fila es de la propia tarea se relee en `context`, que sólo lista las pendientes.
|
|
469
|
+
const HUMAN_ROW_STATE = 'La fila nace con estado `pendiente`, sin excepción: registrás el bloqueo, no lo '
|
|
470
|
+
+ 'resolvés —lo resuelve una persona—. No escribas una decisión ni se la atribuyas a nadie.'
|
|
471
|
+
const registerHuman = async (prompt, label, slug = '') => {
|
|
472
|
+
if (!(await write(`${HUMAN_ROW_STATE}\n\n${prompt}`, { label }))) {
|
|
473
|
+
return ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`
|
|
474
|
+
}
|
|
475
|
+
if (!slug) return ''
|
|
476
|
+
const row = await read(
|
|
477
|
+
`Corré "node tools/ops.js context ${P} --json" desde ${ROOT}. Poné pending en true sólo si humanActions `
|
|
478
|
+
+ `trae una fila cuya task sea ${slug}, y readOk en true sólo si el comando salió con código 0 y devolvió `
|
|
479
|
+
+ 'JSON. El comando es la fuente de verdad: no abras archivos de planning.',
|
|
480
|
+
{ schema: HUMAN_ROW, label: 'human-row' },
|
|
481
|
+
)
|
|
482
|
+
if (!row || !row.readOk) return ` — no se pudo comprobar la fila de ${slug} en ${HUMAN}: revisala a mano`
|
|
483
|
+
return row.pending ? ''
|
|
484
|
+
: ` — la fila de ${slug} en ${HUMAN} no quedó pendiente: la resuelve una persona, revisala a mano`
|
|
485
|
+
}
|
|
450
486
|
|
|
451
487
|
// Gate, mutex de WIP y selección de tarea salen de un comando determinista: AWAITING_REVIEW, BACKLOG,
|
|
452
488
|
// WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
|
|
@@ -541,6 +577,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
541
577
|
const DECIDED = () => (task.description
|
|
542
578
|
? ` Lo que la línea de la tarea ya decidió, y no se re-decide acá: ${task.description}`
|
|
543
579
|
: '')
|
|
580
|
+
// Sobrevive al bloque del plan porque Build vive afuera —y porque una corrida que reanuda desde un WIP
|
|
581
|
+
// no tiene plan en memoria—. Sin esto la estrategia no era que se descartara: es que no estaba en
|
|
582
|
+
// alcance, que es la forma que ninguna prueba de la fase ve.
|
|
583
|
+
let testStrategy = ''
|
|
544
584
|
const task = {
|
|
545
585
|
id: planning.slug, hito: planning.hito, service: planning.service,
|
|
546
586
|
acceptance: planning.acceptance, epic: planning.epic, epicContext: planning.epicContext || '',
|
|
@@ -552,13 +592,13 @@ while (rounds++ < MAX_TASKS) {
|
|
|
552
592
|
// por eso perder la carrera no es un error: se relee y se sigue con la que quedó libre.
|
|
553
593
|
if (!planning.claimed && !planning.wipActive) {
|
|
554
594
|
phase('Claim')
|
|
555
|
-
const
|
|
595
|
+
const claim = await write(
|
|
556
596
|
`Corré "node tools/ops.js claim ${P} ${task.id}" desde ${ROOT}. No escribas ningún archivo vos: lo ` +
|
|
557
597
|
`escribe el comando. claimed=true sólo con exit 0; si falla porque la tomó otro, claimed=false y ` +
|
|
558
598
|
`copiá el mensaje en details.`,
|
|
559
599
|
{ schema: CLAIM, label: `claim:${task.id}` },
|
|
560
600
|
)
|
|
561
|
-
if (!
|
|
601
|
+
if (!claim || !claim.claimed) {
|
|
562
602
|
planning = await readContext()
|
|
563
603
|
if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
|
|
564
604
|
// Perder la carrera es legítimo y se ve en que la cola pasa a ofrecer **otra** tarea: quien la
|
|
@@ -577,7 +617,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
577
617
|
if (planning.hasTask && planning.slug === task.id && !planning.claimed) {
|
|
578
618
|
return stop('claim-stuck', `${task.id} sigue siendo la próxima tarea y no se pudo reclamar. `
|
|
579
619
|
+ `context la ofrece y claim la rechaza, así que repetir no cambia nada. `
|
|
580
|
-
+ `El reclamo contestó: ${(
|
|
620
|
+
+ `El reclamo contestó: ${(claim && claim.details) || '(sin detalle)'}`)
|
|
581
621
|
}
|
|
582
622
|
// Con qué sigue, que es lo que cambia respecto de lo que esperaba quien autorizó la corrida: se
|
|
583
623
|
// pidió un hito y se va a construir otra tarea de ese hito. Sin decirlo, el cambio sólo aparece al
|
|
@@ -691,12 +731,12 @@ while (rounds++ < MAX_TASKS) {
|
|
|
691
731
|
|
|
692
732
|
const planRejected = async (reason, unit, found) => {
|
|
693
733
|
const detail = found.join('; ') || 'sin condiciones nombradas'
|
|
694
|
-
const
|
|
734
|
+
const note = await registerHuman(
|
|
695
735
|
`Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
|
|
696
736
|
+ `Motivo: ${detail}. La acción humana es revisar si la unidad son dos resultados con vidas `
|
|
697
|
-
+ `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human')
|
|
737
|
+
+ `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human', unit.id)
|
|
698
738
|
await releaseBlocked()
|
|
699
|
-
return stop(reason, `${detail}${
|
|
739
|
+
return stop(reason, `${detail}${note}`)
|
|
700
740
|
}
|
|
701
741
|
|
|
702
742
|
if (!planning.wipActive) {
|
|
@@ -711,11 +751,11 @@ while (rounds++ < MAX_TASKS) {
|
|
|
711
751
|
)
|
|
712
752
|
if (!ready) return stop('agent-unavailable', 'Ready no devolvió resultado')
|
|
713
753
|
if (!ready.ready) {
|
|
714
|
-
const
|
|
754
|
+
const note = await registerHuman(
|
|
715
755
|
`Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
|
|
716
|
-
'ready-human')
|
|
756
|
+
'ready-human', task.id)
|
|
717
757
|
await releaseBlocked()
|
|
718
|
-
return stop('not-ready', `${ready.reason}${
|
|
758
|
+
return stop('not-ready', `${ready.reason}${note}`)
|
|
719
759
|
}
|
|
720
760
|
if (ready.refinedAcceptance) task.acceptance = ready.refinedAcceptance
|
|
721
761
|
}
|
|
@@ -821,6 +861,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
821
861
|
if (!critique.consulted.length) return stop('critique-unbacked', 'aprobó el plan sin declarar qué inspeccionó')
|
|
822
862
|
}
|
|
823
863
|
}
|
|
864
|
+
testStrategy = plan.testStrategy || ''
|
|
824
865
|
phase('WIP')
|
|
825
866
|
// Esta llamada escribe un archivo y nada más, y hay que decirlo con todas las letras. En una corrida
|
|
826
867
|
// real hizo el trabajo entero: leyó los pasos del plan como una orden, implementó, corrió RED/GREEN,
|
|
@@ -833,7 +874,15 @@ while (rounds++ < MAX_TASKS) {
|
|
|
833
874
|
`en DONE. Los pasos van sin tildar porque todavía no ocurrieron. task=${task.id}, ` +
|
|
834
875
|
`hito=${JSON.stringify(task.hito)}, phase=Build, service=${task.service}, ` +
|
|
835
876
|
`acceptance=${JSON.stringify(task.acceptance)}, lane=${planning.lane || 'sin clasificar'}, ` +
|
|
836
|
-
`pasos sin tildar=${JSON.stringify(plan.steps)}
|
|
877
|
+
`pasos sin tildar=${JSON.stringify(plan.steps)}, ` +
|
|
878
|
+
// La estrategia de prueba es `required` en el plan y hasta acá se descartaba, así que un paso que
|
|
879
|
+
// decía «correr la mutación declarada en testStrategy» apuntaba a un lugar que no existía: quien
|
|
880
|
+
// revisa no podía distinguir la mutación corrida de la pensada, y la única salida que le quedaba
|
|
881
|
+
// era rehacer la revisión. R9 pide la mutación **declarada**, y el WIP es donde queda escrita.
|
|
882
|
+
//
|
|
883
|
+
// Es la tercera vez que algo decidido no llega a quien decide después: el contexto de la épica
|
|
884
|
+
// (027), la descripción de la tarea (177) y esto. Las tres se arreglan igual — que viaje.
|
|
885
|
+
`testStrategy=${JSON.stringify(testStrategy)}. ` +
|
|
837
886
|
`Registrá el reparto de cargos ${JSON.stringify(cast)} en las decisiones del WIP, para que después se ` +
|
|
838
887
|
`pueda auditar quién revisó qué. Seguí el contrato de WIP exactamente y reportá con qué status quedó.`,
|
|
839
888
|
{ label: 'wip', schema: {
|
|
@@ -877,7 +926,8 @@ while (rounds++ < MAX_TASKS) {
|
|
|
877
926
|
`nombrás en test y anotás en redFirst—, kind=open si lo notaste y no impide entregar la aceptación: se ` +
|
|
878
927
|
`registra para que lo decida quien corresponde y el recorrido sigue. Si de verdad no podés entregar sin ` +
|
|
879
928
|
`esa decisión, eso no va en discovered: es completed=false con su blocker. ` +
|
|
880
|
-
`Aceptación: ${task.acceptance}.${DECIDED()}
|
|
929
|
+
`Aceptación: ${task.acceptance}.${DECIDED()}`
|
|
930
|
+
+ (testStrategy ? ` Estrategia de prueba que el plan fijó: ${testStrategy}` : ''),
|
|
881
931
|
{ schema: BUILD, label: 'build' },
|
|
882
932
|
)
|
|
883
933
|
if (!build) return stop('agent-unavailable', 'Build no devolvió resultado')
|
|
@@ -1005,11 +1055,11 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1005
1055
|
if (filed.length) {
|
|
1006
1056
|
// La nota que devuelve viaja al hecho: sin ella la entrega afirma una fila que el disco no tiene,
|
|
1007
1057
|
// que es el caso 087 entrando por otra puerta.
|
|
1008
|
-
const
|
|
1058
|
+
const note = await registerHuman(`Registrá en ${HUMAN} una fila por cada decisión que la revisión de `
|
|
1009
1059
|
+ `${task.id} dejó abierta, con qué la cierra y quién puede tomarla. La primera columna nunca es `
|
|
1010
1060
|
+ `${task.id} —el porqué es el mismo que en Build—: va la épica, el hito o el recorrido al que `
|
|
1011
1061
|
+ `alcanza. No inventes responsables ni fechas: ${JSON.stringify(filed)}`, 'review-human')
|
|
1012
|
-
decidedNote = `${
|
|
1062
|
+
decidedNote = `${note}`
|
|
1013
1063
|
}
|
|
1014
1064
|
reviewFact = `${review.verdict} por ${cast.review}, sobre ${review.consulted.join(', ')}`
|
|
1015
1065
|
+ (filed.length ? ` · ${filed.length} decisión(es) registrada(s)${decidedNote}` : '')
|
|
@@ -1043,11 +1093,25 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1043
1093
|
}
|
|
1044
1094
|
|
|
1045
1095
|
phase('Verify')
|
|
1096
|
+
// Lo declarado `(fuera de verify: …)` se separa acá y no se le explica a Verify: la aceptación es texto
|
|
1097
|
+
// conocido antes de preguntar, y dejar que el modelo reconozca la marca en la respuesta es apostar a que
|
|
1098
|
+
// enumere los criterios con el mismo corte. `check` ofrecía la marca y el recorrido la mandaba igual,
|
|
1099
|
+
// así que la condición terminaba en `uncovered` y la corrida en `verify-hollow` (caso 195).
|
|
1100
|
+
const conditions = acceptanceConditions(task.acceptance)
|
|
1101
|
+
const outOfVerify = conditions.filter((one) => OUT_OF_VERIFY.test(one))
|
|
1102
|
+
const checkable = outOfVerify.length
|
|
1103
|
+
? conditions.filter((one) => !OUT_OF_VERIFY.test(one)).join('; ')
|
|
1104
|
+
|| 'ninguna: todas se declararon fuera de verify'
|
|
1105
|
+
: task.acceptance
|
|
1046
1106
|
const VERIFY_ASK = `${asRole(cast.verify)}Abrí el fuente de los tests que la tarea agregó o cambió y ` +
|
|
1047
1107
|
`contrastá cada criterio ` +
|
|
1048
1108
|
`de aceptación contra sus aserciones: en uncovered va el criterio que ningún test codifica, con su causa ` +
|
|
1049
1109
|
`—missing-test si el test falta o no asercia la propiedad, ambiguous si el criterio no dice qué habría ` +
|
|
1050
|
-
`que aserciar
|
|
1110
|
+
`que aserciar, no-surface si se cumple en un artefacto que no se ejecuta, como un documento o una ` +
|
|
1111
|
+
`decisión escrita, y con reason diciendo cuál—. no-surface vale sólo si la tarea no tocó ningún archivo ` +
|
|
1112
|
+
`que no termine en ${NON_EXECUTABLE.join(', ')}; con cualquier otro en el diff es missing-test. En ` +
|
|
1113
|
+
`covered va cada criterio que un test sí codifica, con el nombre de ese test. ` +
|
|
1114
|
+
`Un test que pasa sin aserciarla no la cubre. Después corré los gates reales de ${task.service}. ` +
|
|
1051
1115
|
// Descubrir la puerta es trabajo de modelo repetido en cada tarea sobre una respuesta que no cambia,
|
|
1052
1116
|
// y encima adivinable: el proyecto la declara en `verify` y ahí deja de adivinarse. Cuando no la
|
|
1053
1117
|
// declara se vuelve a descubrir, que es lo que pasaba siempre.
|
|
@@ -1060,7 +1124,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1060
1124
|
`Leé los exit codes de verdad. ` +
|
|
1061
1125
|
`passed=true exige comandos corridos y ninguna regresión causada por la tarea. Marcá ranTests en el ` +
|
|
1062
1126
|
`comando que haya corrido las pruebas, sea cual sea su nombre. ` +
|
|
1063
|
-
`Aceptación: ${
|
|
1127
|
+
`Aceptación: ${checkable}.`
|
|
1064
1128
|
let verified = await run(VERIFY_ASK, { schema: VERIFY, label: 'verify' })
|
|
1065
1129
|
if (!verified) return stop('agent-unavailable', 'Verify no devolvió resultado')
|
|
1066
1130
|
// Un criterio que nadie sabe cómo aserciar no es trabajo que falta sino una definición que falta, y
|
|
@@ -1068,14 +1132,18 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1068
1132
|
// hacer parar a una persona por eso le cobra una interrupción por algo que se resolvía solo.
|
|
1069
1133
|
const ambiguous = verified.uncovered.find((entry) => entry.cause === 'ambiguous')
|
|
1070
1134
|
if (ambiguous) {
|
|
1071
|
-
const
|
|
1135
|
+
const note = await registerHuman(
|
|
1072
1136
|
`Registrá ${task.id} en ${HUMAN}: el criterio "${ambiguous.criterion}" no dice qué habría ` +
|
|
1073
|
-
`que aserciar, y hace falta la decisión que lo fija.`, 'verify-human')
|
|
1074
|
-
return stop('acceptance-ambiguous', `${ambiguous.criterion}${
|
|
1137
|
+
`que aserciar, y hace falta la decisión que lo fija.`, 'verify-human', task.id)
|
|
1138
|
+
return stop('acceptance-ambiguous', `${ambiguous.criterion}${note}`)
|
|
1075
1139
|
}
|
|
1076
|
-
|
|
1140
|
+
// Lo que no tiene superficie no frena ni rebota: viaja a Done, que lo escribe como `tests: n/a`. Se filtra
|
|
1141
|
+
// por exclusión y no por `missing-test` para que una causa que no se conozca siga frenando (R27). Que
|
|
1142
|
+
// el modelo no lo use para cerrar sin pruebas lo sostiene `check`, que mira qué tocó el commit.
|
|
1143
|
+
const lacking = () => verified.uncovered.filter((entry) => entry.cause !== 'no-surface')
|
|
1144
|
+
if (lacking().length) {
|
|
1077
1145
|
await run(`${asRole(cast.build)}Escribí sólo las pruebas que faltan en ${task.id}, con el mismo rojo ` +
|
|
1078
|
-
`previo, y no toques el código de producción: ${
|
|
1146
|
+
`previo, y no toques el código de producción: ${lacking().map((e) => e.criterion).join('; ')}`,
|
|
1079
1147
|
{ label: 'missing-tests' })
|
|
1080
1148
|
verified = await run(VERIFY_ASK, { schema: VERIFY, label: 'verify' })
|
|
1081
1149
|
if (!verified) return stop('agent-unavailable', 'la segunda pasada de Verify no devolvió resultado')
|
|
@@ -1087,21 +1155,29 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1087
1155
|
if (build.redFirst.length && !ranTests) {
|
|
1088
1156
|
return stop('verify-untested', `${task.id} escribió pruebas y ningún gate corrió una`)
|
|
1089
1157
|
}
|
|
1090
|
-
if (
|
|
1091
|
-
return stop('verify-hollow', `sin test que lo codifique: ${
|
|
1158
|
+
if (lacking().length) {
|
|
1159
|
+
return stop('verify-hollow', `sin test que lo codifique: ${lacking().map((e) => e.criterion).join('; ')}`)
|
|
1092
1160
|
}
|
|
1161
|
+
const noSurface = verified.uncovered.filter((entry) => entry.cause === 'no-surface')
|
|
1162
|
+
const covered = verified.covered || []
|
|
1163
|
+
// Toda la tarea sin superficie: no hay comportamiento que ejercitar, y saltear QA en silencio dejaría sin
|
|
1164
|
+
// mirar lo único que se puede mirar, que el documento esté y diga lo que la aceptación enumera.
|
|
1165
|
+
const onlyDocument = noSurface.length > 0 && !covered.length
|
|
1093
1166
|
|
|
1094
1167
|
// QA ejercita comportamiento, y lo mecánico no lo cambia: el valor literal que la aceptación nombra
|
|
1095
1168
|
// ya lo comprobó Verify contra el test, y en `directo` además lo mira el revisor que nombra el cast.
|
|
1096
1169
|
let qa = { passed: true, evidence: 'carril mecánico: la aceptación queda comprobada en Verify' }
|
|
1097
1170
|
if (!mechanical) {
|
|
1098
1171
|
phase('QA')
|
|
1099
|
-
qa = await run(
|
|
1100
|
-
`${asRole(cast.qa)}${
|
|
1172
|
+
qa = await run(onlyDocument
|
|
1173
|
+
? `${asRole(cast.qa)}${task.id} no tiene superficie ejecutable: comprobá que el documento existe y cubre ` +
|
|
1174
|
+
`cada elemento que la aceptación enumera, y en evidence decí cuáles encontraste y dónde. ` +
|
|
1175
|
+
`Aceptación: ${checkable}.`
|
|
1176
|
+
: `${asRole(cast.qa)}${lite
|
|
1101
1177
|
? 'Hacé la comprobación de aceptación real más barata'
|
|
1102
1178
|
: 'Ejercitá el comportamiento real que ve quien lo usa'} para ` +
|
|
1103
1179
|
`${task.id}. Las pruebas unitarias solas no son QA. Levantá el mínimo runtime necesario y bajalo ` +
|
|
1104
|
-
`después. Aceptación: ${
|
|
1180
|
+
`después. Aceptación: ${checkable}.`,
|
|
1105
1181
|
{ schema: QA, label: 'qa' },
|
|
1106
1182
|
)
|
|
1107
1183
|
if (!qa) return stop('agent-unavailable', 'QA no devolvió resultado')
|
|
@@ -1129,7 +1205,14 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1129
1205
|
`"node tools/ops.js release ${P} ${task.id}". En decisions no nombres una fase ni un cargo ` +
|
|
1130
1206
|
`que no figure en estos hechos. Hechos: lane=${planning.lane || 'sin clasificar'}; ` +
|
|
1131
1207
|
`review=${reviewFact}; fases=${ran.join(' → ')}; build=${build.summary}; ` +
|
|
1132
|
-
`verify=${JSON.stringify(verified.commands)};
|
|
1208
|
+
`verify=${JSON.stringify(verified.commands)}; cubiertos=${JSON.stringify(covered)}; ` +
|
|
1209
|
+
(noSurface.length ? `sin-superficie=${JSON.stringify(noSurface.map(({ criterion, reason }) => ({
|
|
1210
|
+
criterion, reason: reason || 'no se ejecuta' })))}; ` : '') +
|
|
1211
|
+
(outOfVerify.length ? `fuera-de-verify=${JSON.stringify(outOfVerify)}; ` : '') +
|
|
1212
|
+
`qa=${qa.evidence}; commit=${commit.hash || commit.reason}. En tests rastreá cada criterio con la ` +
|
|
1213
|
+
`prueba que cubiertos le asigna` +
|
|
1214
|
+
(noSurface.length ? ', y los de sin-superficie con tests: n/a — <razón>' : '') +
|
|
1215
|
+
(outOfVerify.length ? '; cada condición de fuera-de-verify queda cumplida en tests, qa o commit' : '') + '.',
|
|
1133
1216
|
{ label: 'done' },
|
|
1134
1217
|
)
|
|
1135
1218
|
completed.push(task.id)
|
|
@@ -138,6 +138,23 @@ function proposalState(text) {
|
|
|
138
138
|
return frontmatterState(text, 'proposed')
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// Las propuestas que alguien descartó diciendo por qué. Es lo que el informe siguiente necesita para no
|
|
142
|
+
// volver a recomendar lo mismo: sin el motivo escrito, una archivada no se distingue de un olvido, y el
|
|
143
|
+
// hallazgo vuelve cada semana con el mismo texto — pasó con cuatro cargos en septiembre de 2026.
|
|
144
|
+
//
|
|
145
|
+
// Lee el documento y no el historial: la fila de `HISTORY.md` recorta la celda a 160 caracteres, y un
|
|
146
|
+
// motivo cortado a la mitad es justo el que se malinterpreta.
|
|
147
|
+
const REASON_LINE = /^-[ \t]*Motivo:[ \t]*(.+)$/m
|
|
148
|
+
|
|
149
|
+
function discardedProposals(dir) {
|
|
150
|
+
return proposalFiles(dir).map((name) => {
|
|
151
|
+
const text = fs.readFileSync(path.join(dir, name), 'utf8')
|
|
152
|
+
const reason = (text.match(REASON_LINE) || [])[1]
|
|
153
|
+
if (proposalState(text) !== 'archived' || !reason) return null
|
|
154
|
+
return { name, reason: reason.trim() }
|
|
155
|
+
}).filter(Boolean)
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
// El sufijo `-N` es la segunda corrida del mismo día, y es la que trae el veredicto más nuevo. Sin él
|
|
142
159
|
// en el patrón, más de una cuarta parte de los registros que existían —de cargos y de recorridos—
|
|
143
160
|
// quedaban fuera del ciclo: no entraban a ninguna propuesta y nada lo delataba, que es el modo de fallo
|
|
@@ -159,4 +176,5 @@ module.exports = {
|
|
|
159
176
|
REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME, undecided, blankProposal, SIGNED, CLOSED,
|
|
160
177
|
assertWritableTeam, assertWritable, isoDate, month,
|
|
161
178
|
proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles, lastOfPeriod,
|
|
179
|
+
discardedProposals,
|
|
162
180
|
}
|
|
@@ -93,7 +93,7 @@ function proposalFile(root, agent, period = '', kind = 'agent') {
|
|
|
93
93
|
return path.join(dir, name)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function archive(root, agent, period = '', kind = 'agent') {
|
|
96
|
+
function archive(root, agent, period = '', kind = 'agent', reason = '') {
|
|
97
97
|
const file = proposalFile(root, agent, period, kind)
|
|
98
98
|
const text = fs.readFileSync(file, 'utf8')
|
|
99
99
|
const state = proposalState(text)
|
|
@@ -116,31 +116,43 @@ function archive(root, agent, period = '', kind = 'agent') {
|
|
|
116
116
|
// que `claim` usa para decir de quién es una tarea. Sin esto la propuesta quedaba en «por definir» y no
|
|
117
117
|
// había forma de distinguir una decisión de un olvido.
|
|
118
118
|
const responsible = owner(root) || 'sin identificar'
|
|
119
|
-
|
|
119
|
+
// Descartar lo que alguien decidió exige decir por qué, y el motivo va al documento porque es lo que lee
|
|
120
|
+
// el informe siguiente (`discardedProposals`). Sin él el hallazgo volvía cada semana: nada distinguía
|
|
121
|
+
// «se miró y no va» de «nadie lo miró», y el cargo no tenía cómo saber que ya estaba contestado. Una
|
|
122
|
+
// propuesta con el molde intacto no lo exige: ahí no hubo decisión que explicar, y lo que traía sigue
|
|
123
|
+
// vivo para la próxima — a menos que quien archiva sí haya mirado los hallazgos y lo diga.
|
|
124
|
+
const why = reason.replace(/\s+/g, ' ').trim()
|
|
125
|
+
const blank = undecided(change)
|
|
126
|
+
if (!blank && !why) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${path.basename(file)} decide un cambio: descartarlo pide --reason "<por qué no va>". `
|
|
129
|
+
+ 'El motivo es lo que evita que el próximo informe lo vuelva a recomendar.',
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
const stamped = text
|
|
120
133
|
.replace(/^status:\s*\S+\s*$/m, 'status: archived')
|
|
121
134
|
// `UNSEALED` y no sólo «pendiente»: desde que se puede archivar una firmada sin decidir, el cuerpo
|
|
122
135
|
// llega diciendo «aprobada» y se quedaba así con el frontmatter en `archived`. Es la contradicción
|
|
123
136
|
// que el comentario de `seal` nombra, por el otro destino.
|
|
124
137
|
.replace(UNSEALED, '- Estado: archivada')
|
|
125
138
|
.replace(/^-[ \t]*Responsable:[ \t]*por definir[ \t]*$/mi, `- Responsable: ${responsible}`)
|
|
126
|
-
.replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`)
|
|
139
|
+
.replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`)
|
|
140
|
+
atomicWrite(file, why ? stamped.replace(/^(-[ \t]*Estado:[ \t]*archivada[ \t]*)$/m, `$1\n- Motivo: ${why}`) : stamped)
|
|
127
141
|
// La fila va para los dos tipos, y no sólo para los recorridos como en `seal`: allá los cargos los
|
|
128
142
|
// registra `agent-promote` al aplicar, y archivar no pasa por ningún workflow que lo haga.
|
|
129
143
|
// La raíz del cargo, que es `<cargo>/learning/proposals/<archivo>` sin sus tres últimos tramos:
|
|
130
144
|
// `appendHistory` agrega `learning/` por su cuenta.
|
|
131
145
|
//
|
|
132
146
|
// La celda del cambio lleva el criterio y no queda vacía, para que la fila no se lea como un registro a
|
|
133
|
-
// medias. Son dos y dicen cosas distintas:
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
appendHistory(path.dirname(path.dirname(path.dirname(file))), file, responsible,
|
|
139
|
-
blank ? 'Se archivó sin decidir: el documento quedó con el molde.' : 'Se miró y no cambia nada.',
|
|
140
|
-
'archivada')
|
|
147
|
+
// medias. Son dos y dicen cosas distintas: descartar con motivo **es** decidir, y archivar un documento
|
|
148
|
+
// que nadie decidió es tirar el andamio — la firma se gastó sobre el molde intacto y no hubo nada que
|
|
149
|
+
// aprobar. Escribir el primero sobre el segundo pondría en el historial que se miró algo que nadie
|
|
150
|
+
// miró, y ese archivo es lo que alguien lee dentro de seis meses.
|
|
151
|
+
const verdict = why ? `Descartada: ${why}` : 'Se archivó sin decidir: el documento quedó con el molde.'
|
|
152
|
+
appendHistory(path.dirname(path.dirname(path.dirname(file))), file, responsible, verdict, 'archivada')
|
|
141
153
|
// `blank` sale acá y no lo recalcula quien imprime: son la misma pregunta, y dos lecturas de la misma
|
|
142
154
|
// sección se separan sin que nada falle.
|
|
143
|
-
return { file, already: false, blank }
|
|
155
|
+
return { file, already: false, blank, reason: why }
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
// Una fila por propuesta cerrada, cualquiera sea el destino. El cambio va en una línea: el documento
|
|
@@ -52,15 +52,17 @@ function evaluateTeam(root, slug) {
|
|
|
52
52
|
// un nombre compuesto. Cuando eran uno solo el catálogo llegó a 51 etiquetas para estas seis.
|
|
53
53
|
const SOURCE_TIERS = ['advisory', 'platform', 'project', 'regulation', 'standard', 'profession']
|
|
54
54
|
|
|
55
|
-
// Cada cuánto vale la pena volver a mirar cada tipo.
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
55
|
+
// Cada cuánto vale la pena volver a mirar cada tipo. La cadencia de un cargo la fija su fuente más
|
|
56
|
+
// rápida, y la más rápida es mensual aunque un aviso publique todos los días: lo que el ciclo produce es
|
|
57
|
+
// un cambio de contrato, que se firma una vez al mes y llega a cada empresa con `upgrade`, así que
|
|
58
|
+
// enterarse el día 7 en vez del 24 no adelanta nada. Hubo una cadencia semanal y se midió (caso 182):
|
|
59
|
+
// en tres semanas, ~353 hallazgos, ninguno urgente, ninguno aplicado y la mitad repetidos.
|
|
60
|
+
// Una norma, en cambio, se revisa por edición, y la profesión cambia más despacio que las dos.
|
|
59
61
|
const TIER_CADENCE = {
|
|
60
|
-
advisory: '
|
|
62
|
+
advisory: 'mensual', platform: 'mensual', project: 'mensual',
|
|
61
63
|
regulation: 'mensual', standard: 'mensual', profession: 'trimestral',
|
|
62
64
|
}
|
|
63
|
-
const CADENCES = ['
|
|
65
|
+
const CADENCES = ['mensual', 'trimestral']
|
|
64
66
|
|
|
65
67
|
// Sale del árbol y no de una lista escrita a mano, por la misma razón que la matriz del cron sale del
|
|
66
68
|
// árbol: una lista paralela se pudre el día que un cargo cambia sus fuentes y nadie la toca.
|
|
@@ -79,7 +81,7 @@ function sourceTiers(text) {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
// El cuerpo de `sources:` termina donde empieza `pending:`. Sin este corte, una pendiente entraba como
|
|
82
|
-
// fuente declarada y
|
|
84
|
+
// fuente declarada y cada corrida la reportaba rota, una vez tras otra — que es exactamente el
|
|
83
85
|
// aviso permanente que la lista existe para no producir.
|
|
84
86
|
function sourcesBody(text) {
|
|
85
87
|
if (!text.includes('sources:')) return ''
|
|
@@ -207,8 +209,8 @@ function evaluate(root, agent) {
|
|
|
207
209
|
const sourcesFile = path.join(target, 'learning', 'sources.yaml')
|
|
208
210
|
if (fs.existsSync(sourcesFile)) {
|
|
209
211
|
const tiers = sourceTiers(fs.readFileSync(sourcesFile, 'utf8'))
|
|
210
|
-
// Sin fuentes el ciclo
|
|
211
|
-
//
|
|
212
|
+
// Sin fuentes el ciclo no tiene literatura que leer y devuelve un informe vacío en cada
|
|
213
|
+
// corrida. Avisa y no bloquea: un cargo que se está escribiendo todavía no las tiene.
|
|
212
214
|
if (!tiers.length) warnings.push('sources.yaml sin fuentes: la investigación no tiene qué leer')
|
|
213
215
|
for (const tier of tiers) {
|
|
214
216
|
if (!SOURCE_TIERS.includes(tier)) {
|
|
@@ -234,7 +236,7 @@ function evaluate(root, agent) {
|
|
|
234
236
|
errors.push(`sources.yaml: una pendiente no declara ${missing.join(' ni ')}`
|
|
235
237
|
+ `${one.name ? ` (${one.name})` : ''}`)
|
|
236
238
|
}
|
|
237
|
-
// Declarada y pendiente a la vez es una contradicción que el chequeo
|
|
239
|
+
// Declarada y pendiente a la vez es una contradicción que el chequeo de cada corrida no puede resolver:
|
|
238
240
|
// la reportaría rota como fuente y recuperada como pendiente en la misma corrida.
|
|
239
241
|
if (one.url && byUrl.has(one.url)) {
|
|
240
242
|
errors.push(`sources.yaml: ${one.url} está declarada como fuente y también como pendiente`)
|
|
@@ -9,6 +9,7 @@ const { atomicWrite } = require('../core/files')
|
|
|
9
9
|
const {
|
|
10
10
|
REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME, assertWritableTeam, assertWritable, lastOfPeriod,
|
|
11
11
|
isoDate, month, proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles, CLOSED,
|
|
12
|
+
discardedProposals,
|
|
12
13
|
} = require('./learning-files')
|
|
13
14
|
// Cerrar una propuesta vive en su propio módulo; se reexporta para no mover a cada llamador.
|
|
14
15
|
const { seal, archive } = require('./learning-seal')
|
|
@@ -42,7 +43,8 @@ function markConsolidated(file) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function prepareReport(root, agent, now = new Date()) {
|
|
45
|
-
const
|
|
46
|
+
const learning = path.join(assertWritable(root, agent), 'learning')
|
|
47
|
+
const reports = path.join(learning, 'reports')
|
|
46
48
|
const file = path.join(reports, `${isoDate(now)}.md`)
|
|
47
49
|
fs.mkdirSync(reports, { recursive: true })
|
|
48
50
|
if (fs.existsSync(file)) return { file, created: false }
|
|
@@ -61,8 +63,8 @@ status: draft
|
|
|
61
63
|
propone: por-completar
|
|
62
64
|
---
|
|
63
65
|
|
|
64
|
-
# Investigación
|
|
65
|
-
|
|
66
|
+
# Investigación — ${isoDate(now)}
|
|
67
|
+
${decided(discardedProposals(path.join(learning, 'proposals')))}
|
|
66
68
|
<!-- Tres convenciones que el ciclo necesita y que nada más sostiene:
|
|
67
69
|
|
|
68
70
|
· Etiquetá cada hallazgo H1, H2, … en el orden en que aparecen. «Evidencia» y «Recomendación» se
|
|
@@ -96,8 +98,28 @@ propuesta consolidada. -->
|
|
|
96
98
|
return { file, created: true }
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
+
// Lo que ya se descartó, dicho donde el cargo lo va a leer: en el informe que está por completar, y no
|
|
102
|
+
// en su prompt ni en el `AUTOMATION.md` de cada cargo. Un prompt lo repetiría en cincuenta archivos, y
|
|
103
|
+
// basta uno que no lo copie para que ese cargo vuelva a proponer lo descartado en cada corrida.
|
|
104
|
+
//
|
|
105
|
+
// Va en un comentario, fuera de toda sección, por lo mismo que las convenciones de abajo: dentro de
|
|
106
|
+
// «Recomendación» viajaría a la propuesta mensual.
|
|
107
|
+
function decided(discarded) {
|
|
108
|
+
if (!discarded.length) return ''
|
|
109
|
+
const lines = discarded.map(({ name, reason }) => ` · learning/proposals/${name}: ${reason}`)
|
|
110
|
+
return `
|
|
111
|
+
<!-- Ya se decidió no aplicar estas propuestas. Lo que proponían está en cada archivo, en «Cambio
|
|
112
|
+
propuesto» y «Hallazgos»; el motivo, acá:
|
|
113
|
+
|
|
114
|
+
${lines.join('\n')}
|
|
115
|
+
|
|
116
|
+
No vuelvas a recomendar lo descartado. Si encontrás un hecho que el motivo no contemplaba —una versión,
|
|
117
|
+
una fecha, una norma nueva—, recomendalo nombrando la propuesta y qué cambió desde entonces. -->
|
|
118
|
+
`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Abre la revisión siguiente. No vuelve a consolidar los informes de investigación: ya se consolidaron
|
|
122
|
+
// en la propuesta que ésta corrige, y repetirlos haría que el mismo hallazgo entre dos veces al contrato. El
|
|
101
123
|
// insumo de una revisión es otro —qué mostró la evaluación posterior a aplicar—, y por eso el molde
|
|
102
124
|
// pregunta eso y no otra cosa.
|
|
103
125
|
//
|
|
@@ -394,11 +416,11 @@ automatic_apply: false
|
|
|
394
416
|
|
|
395
417
|
## Hallazgos
|
|
396
418
|
|
|
397
|
-
${summaries.join('\n\n') || 'No hay informes
|
|
419
|
+
${summaries.join('\n\n') || 'No hay informes de investigación para este período.'}
|
|
398
420
|
|
|
399
421
|
## Evidencia
|
|
400
422
|
|
|
401
|
-
Revisar las fuentes primarias enlazadas desde cada informe
|
|
423
|
+
Revisar las fuentes primarias enlazadas desde cada informe de investigación.
|
|
402
424
|
|
|
403
425
|
## Cambio propuesto
|
|
404
426
|
|
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
// Qué le falta a la superficie de automatización, que no comparte ayudantes con los tres verbos que
|
|
26
26
|
// escriben. Se reexporta para que sus consumidores sigan pidiéndoselo a este módulo.
|
|
27
27
|
const { check } = require('./check')
|
|
28
|
+
const { registrationProblems } = require('./registration')
|
|
28
29
|
|
|
29
30
|
// Ejecuta el puente del runner tal como él lo invoca, y desde otra carpeta. Instalado no es lo mismo que
|
|
30
31
|
// operativo: un bridge que el runner no puede lanzar —porque su ruta es relativa y el cwd es otro, o
|
|
@@ -48,7 +49,12 @@ function probeBridge(paths, runner) {
|
|
|
48
49
|
const launchDirs = [paths.install, path.dirname(script)]
|
|
49
50
|
for (const event of hookEvents) {
|
|
50
51
|
for (const cwd of launchDirs) {
|
|
51
|
-
|
|
52
|
+
// Una llamada inocua para el evento que la juzga, y ninguna para los demás: un `pre-files` con un
|
|
53
|
+
// comando no describe ningún archivo, y el puente niega lo que no sabe describir (caso 200). La
|
|
54
|
+
// entrada vacía es la forma legítima de no describir nada, y ejercita igual el arranque y la raíz.
|
|
55
|
+
const payload = JSON.stringify(event === 'pre-shell'
|
|
56
|
+
? { toolCall: { args: { CommandLine: 'ls', Cwd: paths.install } } }
|
|
57
|
+
: {})
|
|
52
58
|
const result = spawnSync(process.execPath, [script, event], { cwd, input: payload, encoding: 'utf8' })
|
|
53
59
|
let response = {}
|
|
54
60
|
try { response = JSON.parse((result.stdout || '').trim()) } catch { response = {} }
|
|
@@ -68,7 +74,9 @@ function probeBridge(paths, runner) {
|
|
|
68
74
|
return problems
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
// `afterInstall` es la llamada con la que cierra `install`: ahí una copia registrada vieja es el paso que sigue
|
|
78
|
+
// —registrar lo que se acaba de instalar—, no una instalación rota, así que se avisa en vez de fallar.
|
|
79
|
+
function doctor(root, name, output = console, { afterInstall = false } = {}) {
|
|
72
80
|
const runner = runnerManifest(root, name)
|
|
73
81
|
const paths = runnerPaths(root, name, runner)
|
|
74
82
|
const errors = []
|
|
@@ -151,6 +159,7 @@ function doctor(root, name, output = console) {
|
|
|
151
159
|
}
|
|
152
160
|
// Un puente que no responde niega cada llamada del runner: es error, no advertencia.
|
|
153
161
|
for (const problem of probeBridge(paths, runner)) errors.push(problem)
|
|
162
|
+
for (const problem of registrationProblems(paths, runner)) (afterInstall ? warnings : errors).push(problem)
|
|
154
163
|
|
|
155
164
|
const executable = spawnSync(
|
|
156
165
|
'sh',
|