@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +1 -0
  3. package/agents/roles/system/accounting-specialist/learning/AUTOMATION.md +2 -2
  4. package/agents/roles/system/ai-governance-lead/learning/AUTOMATION.md +1 -1
  5. package/agents/roles/system/ai-product-manager/learning/AUTOMATION.md +1 -1
  6. package/agents/roles/system/analytics-engineer/learning/AUTOMATION.md +1 -1
  7. package/agents/roles/system/backend-engineer/learning/AUTOMATION.md +2 -2
  8. package/agents/roles/system/business-strategist/learning/AUTOMATION.md +2 -2
  9. package/agents/roles/system/cloud-architect/learning/AUTOMATION.md +1 -1
  10. package/agents/roles/system/community-manager/learning/AUTOMATION.md +1 -1
  11. package/agents/roles/system/content-specialist/learning/AUTOMATION.md +2 -2
  12. package/agents/roles/system/customer-success-manager/learning/AUTOMATION.md +2 -2
  13. package/agents/roles/system/customer-support-specialist/learning/AUTOMATION.md +2 -2
  14. package/agents/roles/system/data-analyst/learning/AUTOMATION.md +2 -2
  15. package/agents/roles/system/data-engineer/learning/AUTOMATION.md +1 -1
  16. package/agents/roles/system/data-governance-steward/learning/AUTOMATION.md +2 -2
  17. package/agents/roles/system/data-scientist/learning/AUTOMATION.md +1 -1
  18. package/agents/roles/system/database-administrator/learning/AUTOMATION.md +1 -1
  19. package/agents/roles/system/developer-relations-engineer/learning/AUTOMATION.md +1 -1
  20. package/agents/roles/system/devops-engineer/learning/AUTOMATION.md +2 -2
  21. package/agents/roles/system/engineering-manager/learning/AUTOMATION.md +1 -1
  22. package/agents/roles/system/financial-controller/learning/AUTOMATION.md +2 -2
  23. package/agents/roles/system/finops-engineer/learning/AUTOMATION.md +2 -2
  24. package/agents/roles/system/fraud-risk-analyst/learning/AUTOMATION.md +2 -2
  25. package/agents/roles/system/frontend-engineer/learning/AUTOMATION.md +2 -2
  26. package/agents/roles/system/growth-marketer/learning/AUTOMATION.md +2 -2
  27. package/agents/roles/system/implementation-manager/learning/AUTOMATION.md +1 -1
  28. package/agents/roles/system/integrations-engineer/learning/AUTOMATION.md +3 -3
  29. package/agents/roles/system/kyc-aml-specialist/learning/AUTOMATION.md +3 -3
  30. package/agents/roles/system/legal-counsel/learning/AUTOMATION.md +1 -1
  31. package/agents/roles/system/logistics-operations-manager/learning/AUTOMATION.md +2 -2
  32. package/agents/roles/system/machine-learning-engineer/learning/AUTOMATION.md +1 -1
  33. package/agents/roles/system/mlops-engineer/learning/AUTOMATION.md +1 -1
  34. package/agents/roles/system/mobile-engineer/learning/AUTOMATION.md +2 -2
  35. package/agents/roles/system/partnerships-manager/learning/AUTOMATION.md +1 -1
  36. package/agents/roles/system/people-operations-manager/learning/AUTOMATION.md +1 -1
  37. package/agents/roles/system/privacy-compliance-specialist/learning/AUTOMATION.md +2 -2
  38. package/agents/roles/system/procurement-manager/learning/AUTOMATION.md +1 -1
  39. package/agents/roles/system/product-manager/learning/AUTOMATION.md +3 -3
  40. package/agents/roles/system/product-marketing-manager/learning/AUTOMATION.md +2 -2
  41. package/agents/roles/system/project-manager/learning/AUTOMATION.md +1 -1
  42. package/agents/roles/system/qa-engineer/learning/AUTOMATION.md +2 -2
  43. package/agents/roles/system/release-manager/learning/AUTOMATION.md +1 -1
  44. package/agents/roles/system/sales-representative/learning/AUTOMATION.md +2 -2
  45. package/agents/roles/system/security-engineer/learning/AUTOMATION.md +2 -2
  46. package/agents/roles/system/site-reliability-engineer/learning/AUTOMATION.md +2 -2
  47. package/agents/roles/system/software-architect/learning/AUTOMATION.md +2 -2
  48. package/agents/roles/system/solutions-engineer/learning/AUTOMATION.md +1 -1
  49. package/agents/roles/system/tech-lead/learning/AUTOMATION.md +2 -2
  50. package/agents/roles/system/technical-program-manager/learning/AUTOMATION.md +1 -1
  51. package/agents/roles/system/technical-writer/learning/AUTOMATION.md +1 -1
  52. package/agents/roles/system/treasury-analyst/learning/AUTOMATION.md +2 -2
  53. package/agents/roles/system/ui-designer/learning/AUTOMATION.md +3 -3
  54. package/agents/roles/system/user-researcher/learning/AUTOMATION.md +3 -3
  55. package/agents/roles/system/ux-designer/learning/AUTOMATION.md +3 -3
  56. package/automatization/hooks/README.md +4 -3
  57. package/automatization/workflows/agent-propose.js +2 -2
  58. package/automatization/workflows/autobuild.js +52 -12
  59. package/engine/agents/learning-files.js +18 -0
  60. package/engine/agents/learning-seal.js +24 -12
  61. package/engine/agents/learning-sources.js +12 -10
  62. package/engine/agents/learning.js +29 -7
  63. package/engine/cli/args.js +2 -2
  64. package/engine/cli/catalog.js +12 -12
  65. package/engine/cli/ops.js +2 -1
  66. package/engine/hooks/approval.js +5 -3
  67. package/engine/hooks/chat.js +19 -7
  68. package/engine/hooks/push.js +3 -2
  69. package/engine/hooks/self-approval.js +2 -2
  70. package/engine/hooks/shell.js +34 -0
  71. package/package.json +1 -1
  72. package/template/AGENTS.md +5 -4
  73. package/template/Makefile +1 -1
@@ -1,4 +1,4 @@
1
- # Automatización semanal del aprendizaje
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 semanalmente en cada instalación.
20
+ Programar mensualmente en cada instalación.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal con este prompt:
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 semanal; cada instalación define el horario.
19
+ La frecuencia recomendada es mensual; cada instalación define el horario.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal en la raíz del repositorio con este prompt:
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 semanal; cada instalación elige día y hora.
22
+ La frecuencia recomendada es mensual; cada instalación elige día y hora.
@@ -1,6 +1,6 @@
1
- # Automatización semanal del aprendizaje
1
+ # Automatización mensual del aprendizaje
2
2
 
3
- Configurar una automatización semanal con este prompt:
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 semanal; cada instalación define el horario.
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 un «dale» en el mensaje siguiente aprueba
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 un «dale», igual que para publicar;
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 la semana, pero deja «Cambio
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 semanales, pero le falta lo único que una persona puede aprobar: el ` +
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 tres paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es
444
- // el único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso
445
- // se espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó.
446
- // Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó pedida. Caso 087.
447
- const registerHuman = async (prompt, label) => (await write(prompt, { label })
448
- ? ''
449
- : ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`)
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
- atomicWrite(file, text
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: archivar lo que alguien miró **es** decidir que no cambia
134
- // nada, y archivar un documento que nadie decidió es tirar el andamio — la firma se gastó sobre el molde
135
- // intacto y no hubo nada que aprobar. Escribir el primero sobre el segundo pondría en el historial que se
136
- // miró algo que nadie miró, y ese archivo es lo que alguien lee dentro de seis meses.
137
- const blank = undecided(change)
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. Un aviso publica todos los días y llegar un mes
56
- // tarde es llegar tarde; una norma se revisa por edición y mirarla cada lunes devuelve el mismo texto.
57
- // La cadencia de un cargo la fija su fuente más rápida: basta una que corra para que la semana traiga
58
- // algo, y ninguna otra pierde nada por mirarse antes.
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: 'semanal', platform: 'semanal', project: 'semanal',
62
+ advisory: 'mensual', platform: 'mensual', project: 'mensual',
61
63
  regulation: 'mensual', standard: 'mensual', profession: 'trimestral',
62
64
  }
63
- const CADENCES = ['semanal', 'mensual', 'trimestral']
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 el chequeo semanal la reportaba rota todas las semanas — que es exactamente el
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 semanal no tiene literatura que leer y devuelve un informe vacío cada
211
- // semana. Avisa y no bloquea: un cargo que se está escribiendo todavía no las tiene.
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 semanal no puede resolver:
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 reports = path.join(assertWritable(root, agent), 'learning', 'reports')
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 semanal — ${isoDate(now)}
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
- // Abre la revisión siguiente. No vuelve a consolidar los informes semanales: ya se consolidaron en la
100
- // propuesta que ésta corrige, y repetirlos haría que el mismo hallazgo entre dos veces al contrato. El
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 semanales para este período.'}
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 semanal.
423
+ Revisar las fuentes primarias enlazadas desde cada informe de investigación.
402
424
 
403
425
  ## Cambio propuesto
404
426
 
@@ -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
  }
@@ -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 —se miró y no
97
- // cambia nada— y el otro es tirar un andamio que nadie llegó a llenar. Afirmar el primero sobre
98
- // el segundo le cuenta a quien archiva que hubo una revisión que no hubo.
99
- const porque = result.blank
100
- ? 'nadie decidió el cambio y el documento quedó con el molde'
101
- : 'se miró y no cambia nada'
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 semanal: su propuesta se compone de los veredictos de sus propias
114
- // corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre el informe
115
- // de la semana— no tiene entonces qué abrir acá, y `prepareReport` lo decía resolviendo con el
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 semanales.\n`
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) semanal(es) incluidos`)
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|--archived [--period <AAAA-MM>]]
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]
@@ -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 esperá: si contesta «dale», reintentá el mismo cambio y '
108
- + 'pasa. Ese «dale» cubre lo que se frenó y nada más —algo nuevo vuelve a frenar— y sigue valiendo en '
109
- + 'los mensajes siguientes hasta que ella lo niegue. '
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')
@@ -157,10 +157,15 @@ function ordersPush(text, item) {
157
157
  })
158
158
  }
159
159
 
160
- // Quien contesta a un bloqueo que quedó pendiente. Sólo el principio del mensaje: «dale» es la respuesta
161
- // entera o su primera palabra, no algo que aparece en medio de otra frase.
162
- const YES = new RegExp(String.raw`^\s*(?:s[ií]|dale|ok(?:ay)?|hac[eé]lo|hazlo|adelante|aprobado|apruebo`
163
- + String.raw`|aprob[aá]lo|de acuerdo|yes)(?![\p{L}])`, 'iu')
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 && YES.test(text)
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 un «dale» en el mensaje siguiente apruebe exactamente eso y nada más.
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
- saved.pending = [...new Set([...saved.pending, ...items])]
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 }
@@ -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á: un «dale» no lo destraba. Lo da ella, nombrando la rama en '
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 esperá: si contesta «dale», reintentá el mismo push y pasa. '
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