@ingeniomaps/cauce 0.97.0 → 0.98.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 +65 -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 +4 -3
- package/automatization/workflows/agent-propose.js +2 -2
- package/automatization/workflows/autobuild.js +52 -12
- 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/cli/args.js +2 -2
- package/engine/cli/catalog.js +12 -12
- package/engine/cli/ops.js +2 -1
- package/engine/hooks/approval.js +5 -3
- package/engine/hooks/chat.js +19 -7
- package/engine/hooks/push.js +3 -2
- package/engine/hooks/self-approval.js +2 -2
- package/engine/hooks/shell.js +34 -0
- package/package.json +1 -1
- package/template/AGENTS.md +5 -4
- package/template/Makefile +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Automatización
|
|
1
|
+
# Automatización mensual del aprendizaje
|
|
2
2
|
|
|
3
3
|
```text
|
|
4
4
|
Investiga cambios recientes en tesorería corporativa, liquidez y pagos salientes para mantener
|
|
@@ -17,4 +17,4 @@ no actualices SKILL.md, código ni planificación; no publiques, hagas commit ni
|
|
|
17
17
|
con `make agent-evaluate AGENT=treasury-analyst`.
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Programar
|
|
20
|
+
Programar mensualmente en cada instalación.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Automatización
|
|
1
|
+
# Automatización mensual del aprendizaje
|
|
2
2
|
|
|
3
|
-
Configurar una automatización
|
|
3
|
+
Configurar una automatización mensual con este prompt:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Investiga cambios recientes en UI Design para mantener agents/roles/system/ui-designer.
|
|
@@ -16,4 +16,4 @@ permiso, publiques, hagas commit ni push. Termina ejecutando
|
|
|
16
16
|
`make agent-evaluate AGENT=ui-designer`.
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
La frecuencia recomendada es
|
|
19
|
+
La frecuencia recomendada es mensual; cada instalación define el horario.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Automatización
|
|
1
|
+
# Automatización mensual del aprendizaje
|
|
2
2
|
|
|
3
|
-
Configurar una automatización
|
|
3
|
+
Configurar una automatización mensual en la raíz del repositorio con este prompt:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Investiga cambios recientes en User Research para mantener el agente
|
|
@@ -19,4 +19,4 @@ contactes participantes, accedas a datos personales, publiques, hagas commit ni
|
|
|
19
19
|
push. Termina ejecutando `make agent-evaluate AGENT=user-researcher`.
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
La frecuencia recomendada es
|
|
22
|
+
La frecuencia recomendada es mensual; cada instalación elige día y hora.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Automatización
|
|
1
|
+
# Automatización mensual del aprendizaje
|
|
2
2
|
|
|
3
|
-
Configurar una automatización
|
|
3
|
+
Configurar una automatización mensual con este prompt:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Investiga cambios recientes en UX Design para mantener agents/roles/system/ux-designer.
|
|
@@ -15,4 +15,4 @@ referencias, evaluaciones, fuentes ni planificación; no publiques, hagas commit
|
|
|
15
15
|
ni push. Termina con `make agent-evaluate AGENT=ux-designer`.
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
La frecuencia recomendada es
|
|
18
|
+
La frecuencia recomendada es mensual; cada instalación define el horario.
|
|
@@ -74,11 +74,12 @@ guards lo leen:
|
|
|
74
74
|
|
|
75
75
|
- lo que la persona **pidió nombrándolo** pasa: «leé el `.env`» autoriza leer el `.env`; «no toques el
|
|
76
76
|
`.env`» no, y tampoco una pregunta o un comentario que sólo lo nombra —«¿qué tiene el `.env`?»—;
|
|
77
|
-
- lo que se frenó sin que lo nombrara queda anotado, y
|
|
78
|
-
exactamente eso;
|
|
77
|
+
- lo que se frenó sin que lo nombrara queda anotado, y su confirmación en el mensaje siguiente —con las
|
|
78
|
+
palabras que sea— aprueba exactamente eso. Si ese mensaje niega, frena o pregunta, no aprueba nada; y lo
|
|
79
|
+
que se frenó mientras ella decía que no, no queda esperando;
|
|
79
80
|
- lo que un guard dejó pasar porque ella lo pidió sigue valiendo mientras dure la sesión, **salvo en los
|
|
80
81
|
gates de un commit** —`governance`, `verify` y `dependencies`—, que preguntan cada vez: ahí vale lo que
|
|
81
|
-
pidió el mensaje en curso o
|
|
82
|
+
pidió el mensaje en curso o su confirmación al bloqueo, igual que para publicar;
|
|
82
83
|
- `plan-first` no aplica: el plan es del trabajo que va por tareas.
|
|
83
84
|
|
|
84
85
|
No cuenta cuando no hay persona —CI, o un aviso del runner como el de un subagente que terminó—, cuando lo
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Convierte la recomendación de los informes en el cambio concreto que una persona puede aprobar.
|
|
2
2
|
//
|
|
3
|
-
// `learn --proposal` consolida qué recomendaron los informes de
|
|
3
|
+
// `learn --proposal` consolida qué recomendaron los informes de investigación, pero deja «Cambio
|
|
4
4
|
// propuesto: por definir». Eso no es aprobable: nadie firma una intención. Este recorrido escribe el
|
|
5
5
|
// texto exacto que habría que agregar, archivo por archivo, y lo contrasta contra los casos
|
|
6
6
|
// adversariales vigentes antes de proponerlo.
|
|
@@ -70,7 +70,7 @@ phase('Proponer')
|
|
|
70
70
|
|
|
71
71
|
const proposal = await agent(
|
|
72
72
|
`Sos quien mantiene el cargo ${AGENT}. La propuesta ${context.proposal} consolidó lo que ` +
|
|
73
|
-
`recomendaron los informes
|
|
73
|
+
`recomendaron los informes de investigación, pero le falta lo único que una persona puede aprobar: el ` +
|
|
74
74
|
`cambio concreto.\n\n` +
|
|
75
75
|
`Leé los informes citados en su sección «Hallazgos», el SKILL.md del cargo, ` +
|
|
76
76
|
`${context.dir}/learning/sources.yaml, ${context.dir}/evaluations/expected-behaviors.yaml y sus ` +
|
|
@@ -43,6 +43,13 @@ const GATE = `${P}/AWAITING_REVIEW.md`
|
|
|
43
43
|
// suelta no es un envoltorio. Por eso también la escapada en vez de alternar el estilo de comillas.
|
|
44
44
|
const QUOTES = ['\'', '"']
|
|
45
45
|
|
|
46
|
+
// Si la fila que registró una parada quedó pendiente. `context` sólo lista las pendientes, así que
|
|
47
|
+
// preguntar por la presencia de la tarea alcanza, y no hace falta que un modelo lea el estado.
|
|
48
|
+
const HUMAN_ROW = {
|
|
49
|
+
type: 'object', additionalProperties: false, required: ['readOk', 'pending'],
|
|
50
|
+
properties: { readOk: { type: 'boolean' }, pending: { type: 'boolean' } },
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
const CONTEXT = {
|
|
47
54
|
type: 'object', additionalProperties: false,
|
|
48
55
|
required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
|
|
@@ -440,13 +447,32 @@ const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
|
|
|
440
447
|
const run = (prompt, options = {}) => agent(`${SCOPE()}\n\n${prompt}`, options)
|
|
441
448
|
const write = (prompt, options = {}) => agent(`${LEDGER()}\n\n${prompt}`, options)
|
|
442
449
|
|
|
443
|
-
// Las
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
// Las paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es el
|
|
451
|
+
// único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso se
|
|
452
|
+
// espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó (caso 087).
|
|
453
|
+
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó como tiene que quedar.
|
|
454
|
+
//
|
|
455
|
+
// Contestar no alcanza: el agente que acaba de escribir el diagnóstico entero lo lee como ya resuelto, y
|
|
456
|
+
// en una corrida real escribió la fila `resuelta` y la firmó como «decidido por el dueño», desbloqueando
|
|
457
|
+
// la tarea sin que nadie decidiera nada (caso 180). Por eso cada pedido arranca diciendo que la fila nace
|
|
458
|
+
// pendiente, y cuando la fila es de la propia tarea se relee en `context`, que sólo lista las pendientes.
|
|
459
|
+
const HUMAN_ROW_STATE = 'La fila nace con estado `pendiente`, sin excepción: registrás el bloqueo, no lo '
|
|
460
|
+
+ 'resolvés —lo resuelve una persona—. No escribas una decisión ni se la atribuyas a nadie.'
|
|
461
|
+
const registerHuman = async (prompt, label, slug = '') => {
|
|
462
|
+
if (!(await write(`${HUMAN_ROW_STATE}\n\n${prompt}`, { label }))) {
|
|
463
|
+
return ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`
|
|
464
|
+
}
|
|
465
|
+
if (!slug) return ''
|
|
466
|
+
const row = await read(
|
|
467
|
+
`Corré "node tools/ops.js context ${P} --json" desde ${ROOT}. Poné pending en true sólo si humanActions `
|
|
468
|
+
+ `trae una fila cuya task sea ${slug}, y readOk en true sólo si el comando salió con código 0 y devolvió `
|
|
469
|
+
+ 'JSON. El comando es la fuente de verdad: no abras archivos de planning.',
|
|
470
|
+
{ schema: HUMAN_ROW, label: 'human-row' },
|
|
471
|
+
)
|
|
472
|
+
if (!row || !row.readOk) return ` — no se pudo comprobar la fila de ${slug} en ${HUMAN}: revisala a mano`
|
|
473
|
+
return row.pending ? ''
|
|
474
|
+
: ` — la fila de ${slug} en ${HUMAN} no quedó pendiente: la resuelve una persona, revisala a mano`
|
|
475
|
+
}
|
|
450
476
|
|
|
451
477
|
// Gate, mutex de WIP y selección de tarea salen de un comando determinista: AWAITING_REVIEW, BACKLOG,
|
|
452
478
|
// WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
|
|
@@ -541,6 +567,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
541
567
|
const DECIDED = () => (task.description
|
|
542
568
|
? ` Lo que la línea de la tarea ya decidió, y no se re-decide acá: ${task.description}`
|
|
543
569
|
: '')
|
|
570
|
+
// Sobrevive al bloque del plan porque Build vive afuera —y porque una corrida que reanuda desde un WIP
|
|
571
|
+
// no tiene plan en memoria—. Sin esto la estrategia no era que se descartara: es que no estaba en
|
|
572
|
+
// alcance, que es la forma que ninguna prueba de la fase ve.
|
|
573
|
+
let testStrategy = ''
|
|
544
574
|
const task = {
|
|
545
575
|
id: planning.slug, hito: planning.hito, service: planning.service,
|
|
546
576
|
acceptance: planning.acceptance, epic: planning.epic, epicContext: planning.epicContext || '',
|
|
@@ -694,7 +724,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
694
724
|
const nota = await registerHuman(
|
|
695
725
|
`Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
|
|
696
726
|
+ `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')
|
|
727
|
+
+ `distintas y partirla, o dejarla entera con la razón escrita.`, 'plan-human', unit.id)
|
|
698
728
|
await releaseBlocked()
|
|
699
729
|
return stop(reason, `${detail}${nota}`)
|
|
700
730
|
}
|
|
@@ -713,7 +743,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
713
743
|
if (!ready.ready) {
|
|
714
744
|
const nota = await registerHuman(
|
|
715
745
|
`Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
|
|
716
|
-
'ready-human')
|
|
746
|
+
'ready-human', task.id)
|
|
717
747
|
await releaseBlocked()
|
|
718
748
|
return stop('not-ready', `${ready.reason}${nota}`)
|
|
719
749
|
}
|
|
@@ -821,6 +851,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
821
851
|
if (!critique.consulted.length) return stop('critique-unbacked', 'aprobó el plan sin declarar qué inspeccionó')
|
|
822
852
|
}
|
|
823
853
|
}
|
|
854
|
+
testStrategy = plan.testStrategy || ''
|
|
824
855
|
phase('WIP')
|
|
825
856
|
// Esta llamada escribe un archivo y nada más, y hay que decirlo con todas las letras. En una corrida
|
|
826
857
|
// real hizo el trabajo entero: leyó los pasos del plan como una orden, implementó, corrió RED/GREEN,
|
|
@@ -833,7 +864,15 @@ while (rounds++ < MAX_TASKS) {
|
|
|
833
864
|
`en DONE. Los pasos van sin tildar porque todavía no ocurrieron. task=${task.id}, ` +
|
|
834
865
|
`hito=${JSON.stringify(task.hito)}, phase=Build, service=${task.service}, ` +
|
|
835
866
|
`acceptance=${JSON.stringify(task.acceptance)}, lane=${planning.lane || 'sin clasificar'}, ` +
|
|
836
|
-
`pasos sin tildar=${JSON.stringify(plan.steps)}
|
|
867
|
+
`pasos sin tildar=${JSON.stringify(plan.steps)}, ` +
|
|
868
|
+
// La estrategia de prueba es `required` en el plan y hasta acá se descartaba, así que un paso que
|
|
869
|
+
// decía «correr la mutación declarada en testStrategy» apuntaba a un lugar que no existía: quien
|
|
870
|
+
// revisa no podía distinguir la mutación corrida de la pensada, y la única salida que le quedaba
|
|
871
|
+
// era rehacer la revisión. R9 pide la mutación **declarada**, y el WIP es donde queda escrita.
|
|
872
|
+
//
|
|
873
|
+
// Es la tercera vez que algo decidido no llega a quien decide después: el contexto de la épica
|
|
874
|
+
// (027), la descripción de la tarea (177) y esto. Las tres se arreglan igual — que viaje.
|
|
875
|
+
`testStrategy=${JSON.stringify(testStrategy)}. ` +
|
|
837
876
|
`Registrá el reparto de cargos ${JSON.stringify(cast)} en las decisiones del WIP, para que después se ` +
|
|
838
877
|
`pueda auditar quién revisó qué. Seguí el contrato de WIP exactamente y reportá con qué status quedó.`,
|
|
839
878
|
{ label: 'wip', schema: {
|
|
@@ -877,7 +916,8 @@ while (rounds++ < MAX_TASKS) {
|
|
|
877
916
|
`nombrás en test y anotás en redFirst—, kind=open si lo notaste y no impide entregar la aceptación: se ` +
|
|
878
917
|
`registra para que lo decida quien corresponde y el recorrido sigue. Si de verdad no podés entregar sin ` +
|
|
879
918
|
`esa decisión, eso no va en discovered: es completed=false con su blocker. ` +
|
|
880
|
-
`Aceptación: ${task.acceptance}.${DECIDED()}
|
|
919
|
+
`Aceptación: ${task.acceptance}.${DECIDED()}`
|
|
920
|
+
+ (testStrategy ? ` Estrategia de prueba que el plan fijó: ${testStrategy}` : ''),
|
|
881
921
|
{ schema: BUILD, label: 'build' },
|
|
882
922
|
)
|
|
883
923
|
if (!build) return stop('agent-unavailable', 'Build no devolvió resultado')
|
|
@@ -1070,7 +1110,7 @@ while (rounds++ < MAX_TASKS) {
|
|
|
1070
1110
|
if (ambiguous) {
|
|
1071
1111
|
const nota = await registerHuman(
|
|
1072
1112
|
`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')
|
|
1113
|
+
`que aserciar, y hace falta la decisión que lo fija.`, 'verify-human', task.id)
|
|
1074
1114
|
return stop('acceptance-ambiguous', `${ambiguous.criterion}${nota}`)
|
|
1075
1115
|
}
|
|
1076
1116
|
if (verified.uncovered.length) {
|
|
@@ -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
|
|
package/engine/cli/args.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Banderas que consumen el argumento siguiente: su valor no es un posicional.
|
|
8
8
|
const VALUED_FLAGS = new Set([
|
|
9
9
|
'--name', '--mode', '--fixture', '--period', '--record', '--runner', '--integration',
|
|
10
|
-
'--task', '--promote', '--hito',
|
|
10
|
+
'--task', '--promote', '--hito', '--reason',
|
|
11
11
|
])
|
|
12
12
|
|
|
13
13
|
// Qué acepta cada comando, y a la vez qué comandos existen. Una bandera desconocida se rechaza en vez
|
|
@@ -36,7 +36,7 @@ const FLAGS = {
|
|
|
36
36
|
integration: ['--fixture'],
|
|
37
37
|
secrets: [],
|
|
38
38
|
automation: ['--force'],
|
|
39
|
-
learn: ['--flow', '--proposal', '--applied', '--archived', '--period'],
|
|
39
|
+
learn: ['--flow', '--proposal', '--applied', '--archived', '--period', '--reason'],
|
|
40
40
|
evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
|
|
41
41
|
flow: ['--json'],
|
|
42
42
|
}
|
package/engine/cli/catalog.js
CHANGED
|
@@ -91,14 +91,14 @@ function learn(agent, cli) {
|
|
|
91
91
|
// y lo que se hacía era mergear el PR sin firmar — que deja el documento diciendo «sin aplicar»
|
|
92
92
|
// para siempre, indistinguible de una que espera trabajo.
|
|
93
93
|
if (cli.has('--archived')) {
|
|
94
|
-
const result = L.archive(opsRoot(), agent, cli.value('--period'), kind)
|
|
94
|
+
const result = L.archive(opsRoot(), agent, cli.value('--period'), kind, cli.value('--reason') || '')
|
|
95
95
|
const relative = path.relative(opsRoot(), result.file)
|
|
96
|
-
// Los dos archivados no son lo mismo y el mensaje lo dice: uno es una decisión
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
const porque = result.
|
|
100
|
-
?
|
|
101
|
-
: '
|
|
96
|
+
// Los dos archivados no son lo mismo y el mensaje lo dice: uno es una decisión, con su motivo, y
|
|
97
|
+
// el otro es tirar un andamio que nadie llegó a llenar. Afirmar el primero sobre el segundo le
|
|
98
|
+
// cuenta a quien archiva que hubo una revisión que no hubo.
|
|
99
|
+
const porque = result.reason
|
|
100
|
+
? `descartada — ${result.reason}`
|
|
101
|
+
: 'nadie decidió el cambio y el documento quedó con el molde'
|
|
102
102
|
return console.log(result.already
|
|
103
103
|
? `= ${relative} ya estaba archivada`
|
|
104
104
|
: `✓ ${relative} queda archivada: ${porque}`)
|
|
@@ -110,13 +110,13 @@ function learn(agent, cli) {
|
|
|
110
110
|
? `= ${relative} ya estaba aplicada`
|
|
111
111
|
: `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
|
|
112
112
|
}
|
|
113
|
-
// Un recorrido no tiene informe
|
|
114
|
-
// corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre
|
|
115
|
-
//
|
|
113
|
+
// Un recorrido no tiene informe de investigación: su propuesta se compone de los veredictos de sus
|
|
114
|
+
// propias corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre su
|
|
115
|
+
// informe— no tiene entonces qué abrir acá, y `prepareReport` lo decía resolviendo con el
|
|
116
116
|
// `kind` por defecto: «no existe agents/<tipo>/<slug>/SKILL.md», que manda a crear un cargo que no
|
|
117
117
|
// falta. Negarse nombrando el comando que sí corresponde es lo que cierra R13.
|
|
118
118
|
if (kind === 'flow' && !cli.has('--proposal')) {
|
|
119
|
-
fail(`${agent} es un recorrido: aprende de sus corridas, no de informes
|
|
119
|
+
fail(`${agent} es un recorrido: aprende de sus corridas, no de informes de investigación.\n`
|
|
120
120
|
+ ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, USAGE)
|
|
121
121
|
}
|
|
122
122
|
// `--period` es para consolidar a mano un mes que no es el de hoy. El ciclo automático no lo
|
|
@@ -137,7 +137,7 @@ function learn(agent, cli) {
|
|
|
137
137
|
if (typeof result.reports === 'number') {
|
|
138
138
|
console.log(kind === 'flow'
|
|
139
139
|
? ` ${result.reports} corrida(s) consolidada(s), ${result.findings} hallazgo(s)`
|
|
140
|
-
: ` ${result.reports} informe(s)
|
|
140
|
+
: ` ${result.reports} informe(s) de investigación incluidos`)
|
|
141
141
|
}
|
|
142
142
|
// Lo lee quien automatiza el ciclo para no pedir una firma por un documento que no decide nada, y
|
|
143
143
|
// también quien lo corre a mano: sin esta línea el archivo se ve terminado y no lo está. Se pregunta
|
package/engine/cli/ops.js
CHANGED
|
@@ -180,7 +180,8 @@ function usage() {
|
|
|
180
180
|
ops automation doctor <ops-root> claude|codex|gemini|antigravity
|
|
181
181
|
ops automation install <ops-root> claude|codex|gemini|antigravity
|
|
182
182
|
ops automation uninstall <ops-root> claude|codex|gemini|antigravity
|
|
183
|
-
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied
|
|
183
|
+
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>]]
|
|
184
|
+
ops learn <agent|flow> [--flow] --archived [--period <AAAA-MM>] [--reason <motivo>]
|
|
184
185
|
ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
|
|
185
186
|
ops agents list [ops-root] [--own|--system] [--json]
|
|
186
187
|
ops agents fork <cargo> [ops-root]
|
package/engine/hooks/approval.js
CHANGED
|
@@ -104,9 +104,11 @@ function HOW(variable, lines, input, pasteable = lines) {
|
|
|
104
104
|
// `check` la muestra al final de la corrida —es lo que trajo el 117—, y al concederla no la decía nadie
|
|
105
105
|
// (caso 170).
|
|
106
106
|
const ask = chat
|
|
107
|
-
? 'Decile a la persona qué se frenó y por qué, y
|
|
108
|
-
+ '
|
|
109
|
-
+ '
|
|
107
|
+
? 'Decile a la persona qué se frenó y por qué, y pedile que lo confirme con sus palabras: un «dale» '
|
|
108
|
+
+ 'alcanza, pero no hace falta esa palabra. Si lo que contesta es un sí, reintentá el mismo cambio y '
|
|
109
|
+
+ 'pasa; si duda, pregunta o dice que no, no reintentes. Juzgarlo te toca a vos: el guard sólo frena la '
|
|
110
|
+
+ 'respuesta que niega, frena o pregunta. Esa confirmación cubre lo que se frenó y nada más —algo nuevo '
|
|
111
|
+
+ 'vuelve a frenar— y sigue valiendo en los mensajes siguientes hasta que ella lo niegue. '
|
|
110
112
|
: ''
|
|
111
113
|
const paste = pasteable.length
|
|
112
114
|
? (chat ? 'Si prefiere aprobarlo a mano, que pegue ella tal cual en' : 'Aprobalo pegando tal cual en')
|
package/engine/hooks/chat.js
CHANGED
|
@@ -157,10 +157,15 @@ function ordersPush(text, item) {
|
|
|
157
157
|
})
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Quien contesta a un bloqueo que quedó pendiente
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
// Quien contesta a un bloqueo que quedó pendiente lo confirma con sus palabras, y juzgar si lo que dijo
|
|
161
|
+
// es un sí le toca al agente que lo lee. Antes el guard exigía que el mensaje **empezara** con una de once
|
|
162
|
+
// formas —«dale», «sí», «ok»…—, así que «listo», «claro», «confirmo» o «bueno dale» volvían a frenar lo
|
|
163
|
+
// que la persona acababa de aprobar (caso 184). Lo único que el guard sigue decidiendo es la dirección
|
|
164
|
+
// segura: un mensaje que niega, que arranca frenando o que pregunta no aprueba nada. Si eso se equivoca,
|
|
165
|
+
// el costo es volver a preguntar; si se equivocara al revés, un «no» o un «¿para qué sirve?» ejecutaría lo
|
|
166
|
+
// que la persona no aprobó.
|
|
167
|
+
const HALT = /^\s*(?:par[aá]|deten\p{L}*|esper[aá]|cancel\p{L}*|stop|wait|hold on)(?![\p{L}])/iu
|
|
168
|
+
const refuses = (text) => NEGATION.test(text) || HALT.test(text) || /[?¿]/.test(text)
|
|
164
169
|
|
|
165
170
|
// El hook de mensaje. Nunca frena: un mensaje de la persona no se bloquea, y sin registro los guards
|
|
166
171
|
// deciden como antes. Un texto que empieza con una etiqueta no lo escribió una persona —Claude avisa así
|
|
@@ -175,7 +180,7 @@ function record(input) {
|
|
|
175
180
|
const text = String(input.prompt || '')
|
|
176
181
|
const human = !process.env.CI && !/^\s*</.test(text)
|
|
177
182
|
const previous = load(input.session_id)
|
|
178
|
-
const approved = human && previous &&
|
|
183
|
+
const approved = human && previous && !refuses(text)
|
|
179
184
|
? previous.pending.filter((item) => !mentions(text, item).denied)
|
|
180
185
|
: []
|
|
181
186
|
const granted = previous ? (previous.granted || []).filter((one) => !mentions(text, one).denied) : []
|
|
@@ -322,13 +327,20 @@ function unauthorizedNow(input, items) {
|
|
|
322
327
|
return items.filter((item) => !cleared.has(item))
|
|
323
328
|
}
|
|
324
329
|
|
|
325
|
-
// Lo que quedó frenado, para que
|
|
330
|
+
// Lo que quedó frenado, para que la confirmación del mensaje siguiente apruebe exactamente eso y nada más.
|
|
326
331
|
// Devuelve si hay una persona a quien preguntarle.
|
|
332
|
+
//
|
|
333
|
+
// Si el mensaje en curso niega o frena, lo que se ataje en su turno no queda esperando: la persona ya
|
|
334
|
+
// contestó que no antes de que el agente lo intentara. Desde que confirmar no exige una palabra (caso 184),
|
|
335
|
+
// dejarlo pendiente hacía que un «seguí con lo tuyo» aprobara el `.env` o el push que ella acababa de
|
|
336
|
+
// prohibir. Se mira el mensaje y no el ítem porque un push no se nombra como un archivo.
|
|
327
337
|
function hold(input, items) {
|
|
328
338
|
const saved = present(input)
|
|
329
339
|
if (!saved) return false
|
|
330
340
|
try {
|
|
331
|
-
|
|
341
|
+
const text = saved.text || ''
|
|
342
|
+
const open = NEGATION.test(text) || HALT.test(text) ? [] : items
|
|
343
|
+
saved.pending = [...new Set([...saved.pending, ...open])]
|
|
332
344
|
fs.writeFileSync(recordPath(input.session_id), JSON.stringify(saved))
|
|
333
345
|
return true
|
|
334
346
|
} catch { return false }
|
package/engine/hooks/push.js
CHANGED
|
@@ -105,7 +105,7 @@ function liveMessage(live, input) {
|
|
|
105
105
|
// una sesión real, con «pegando tal cual» a secas el agente se ofreció a escribirse la aprobación.
|
|
106
106
|
return `'git push' publica cambios en ${names}, la rama viva, y requiere una acción humana: ni `
|
|
107
107
|
+ 'runner.allowPush ni una orden en el chat llegan ahí sin un permiso por rama. Decile a la persona qué '
|
|
108
|
-
+ 'se frenó y esperá:
|
|
108
|
+
+ 'se frenó y esperá: confirmarlo en el chat no lo destraba. Lo da ella, nombrando la rama en '
|
|
109
109
|
+ 'runner.pushToLiveBranches de ops.config.json, que la deja como una rama de trabajo'
|
|
110
110
|
+ (lines.length ? `, o pegando ella tal cual en ${AP.where(input)} estas líneas:\n`
|
|
111
111
|
+ lines.map((line) => ` ${line}\n`).join('') : '.')
|
|
@@ -115,7 +115,8 @@ function workMessage(items, input) {
|
|
|
115
115
|
const lines = items.filter((item) => item.startsWith('push '))
|
|
116
116
|
const chat = CHAT.hold(input, items)
|
|
117
117
|
const how = chat
|
|
118
|
-
? 'Decile a la persona qué se frenó y
|
|
118
|
+
? 'Decile a la persona qué se frenó y pedile que lo confirme con sus palabras: si lo que contesta es un '
|
|
119
|
+
+ 'sí, reintentá el mismo push y pasa; si duda, pregunta o dice que no, no reintentes. '
|
|
119
120
|
+ 'También pasa si lo pide nombrando el remoto y la rama, como «subí feat/x a origin».'
|
|
120
121
|
: 'Lo destraba una persona pidiéndolo en el chat con el remoto y la rama.'
|
|
121
122
|
const paste = lines.length
|