@ingeniomaps/cauce 0.53.2 → 0.54.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 (129) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +1 -1
  3. package/agents/README.md +24 -0
  4. package/agents/roles/system/accounting-specialist/SKILL.md +2 -0
  5. package/agents/roles/system/ai-governance-lead/SKILL.md +2 -0
  6. package/agents/roles/system/ai-product-manager/SKILL.md +2 -0
  7. package/agents/roles/system/analytics-engineer/SKILL.md +2 -0
  8. package/agents/roles/system/backend-engineer/SKILL.md +4 -2
  9. package/agents/roles/system/backend-engineer/learning/HISTORY.md +1 -0
  10. package/agents/roles/system/business-strategist/SKILL.md +4 -2
  11. package/agents/roles/system/cloud-architect/SKILL.md +5 -0
  12. package/agents/roles/system/cloud-architect/learning/HISTORY.md +4 -0
  13. package/agents/roles/system/community-manager/SKILL.md +2 -0
  14. package/agents/roles/system/content-specialist/SKILL.md +4 -2
  15. package/agents/roles/system/customer-success-manager/SKILL.md +9 -2
  16. package/agents/roles/system/customer-success-manager/learning/HISTORY.md +1 -0
  17. package/agents/roles/system/customer-support-specialist/SKILL.md +4 -2
  18. package/agents/roles/system/data-analyst/SKILL.md +9 -2
  19. package/agents/roles/system/data-analyst/learning/HISTORY.md +1 -0
  20. package/agents/roles/system/data-engineer/SKILL.md +2 -0
  21. package/agents/roles/system/data-governance-steward/SKILL.md +2 -0
  22. package/agents/roles/system/data-scientist/SKILL.md +2 -0
  23. package/agents/roles/system/database-administrator/SKILL.md +2 -0
  24. package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -0
  25. package/agents/roles/system/developer-relations-engineer/SKILL.md +2 -0
  26. package/agents/roles/system/devops-engineer/SKILL.md +4 -2
  27. package/agents/roles/system/engineering-manager/SKILL.md +2 -0
  28. package/agents/roles/system/financial-controller/SKILL.md +4 -2
  29. package/agents/roles/system/finops-engineer/SKILL.md +2 -0
  30. package/agents/roles/system/finops-engineer/learning/HISTORY.md +1 -0
  31. package/agents/roles/system/finops-engineer/learning/sources.yaml +12 -0
  32. package/agents/roles/system/fraud-risk-analyst/SKILL.md +7 -0
  33. package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +1 -0
  34. package/agents/roles/system/frontend-engineer/SKILL.md +8 -2
  35. package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel/InternalNote.jsx +30 -0
  36. package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel.md +10 -0
  37. package/agents/roles/system/frontend-engineer/learning/HISTORY.md +1 -0
  38. package/agents/roles/system/growth-marketer/SKILL.md +17 -0
  39. package/agents/roles/system/growth-marketer/evaluations/cases/08-briefing-de-referidos.md +23 -0
  40. package/agents/roles/system/growth-marketer/evaluations/expected-behaviors.yaml +1 -0
  41. package/agents/roles/system/growth-marketer/learning/HISTORY.md +3 -0
  42. package/agents/roles/system/implementation-manager/SKILL.md +10 -0
  43. package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook/plan-cutover-tesela-rev-b.md +70 -0
  44. package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook.md +12 -0
  45. package/agents/roles/system/implementation-manager/evaluations/expected-behaviors.yaml +1 -0
  46. package/agents/roles/system/implementation-manager/learning/HISTORY.md +5 -0
  47. package/agents/roles/system/integrations-engineer/SKILL.md +6 -4
  48. package/agents/roles/system/kyc-aml-specialist/SKILL.md +11 -4
  49. package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +2 -0
  50. package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +76 -1
  51. package/agents/roles/system/legal-counsel/SKILL.md +2 -0
  52. package/agents/roles/system/logistics-operations-manager/SKILL.md +17 -0
  53. package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo/api-seguimiento-andesexpress.md +40 -0
  54. package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo.md +2 -3
  55. package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
  56. package/agents/roles/system/machine-learning-engineer/SKILL.md +2 -0
  57. package/agents/roles/system/mlops-engineer/SKILL.md +2 -0
  58. package/agents/roles/system/mobile-engineer/SKILL.md +4 -2
  59. package/agents/roles/system/partnerships-manager/SKILL.md +2 -0
  60. package/agents/roles/system/people-operations-manager/SKILL.md +2 -0
  61. package/agents/roles/system/privacy-compliance-specialist/SKILL.md +4 -2
  62. package/agents/roles/system/procurement-manager/SKILL.md +2 -0
  63. package/agents/roles/system/product-manager/SKILL.md +2 -0
  64. package/agents/roles/system/product-marketing-manager/SKILL.md +9 -2
  65. package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +1 -0
  66. package/agents/roles/system/project-manager/SKILL.md +2 -0
  67. package/agents/roles/system/qa-engineer/SKILL.md +11 -2
  68. package/agents/roles/system/qa-engineer/evaluations/cases/07-agent-test-repair.md +1 -1
  69. package/agents/roles/system/qa-engineer/evaluations/cases/11-accessibility-sampling.md +10 -0
  70. package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary/resumen-legal-wcag22.md +28 -0
  71. package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary.md +10 -0
  72. package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary/wiki-plataforma-conformidad.md +24 -0
  73. package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary.md +10 -0
  74. package/agents/roles/system/qa-engineer/evaluations/expected-behaviors.yaml +2 -0
  75. package/agents/roles/system/qa-engineer/learning/HISTORY.md +4 -0
  76. package/agents/roles/system/qa-engineer/learning/sources.yaml +24 -3
  77. package/agents/roles/system/release-manager/SKILL.md +5 -3
  78. package/agents/roles/system/release-manager/evaluations/cases/09-provenance-without-origin.md +12 -0
  79. package/agents/roles/system/release-manager/learning/HISTORY.md +1 -0
  80. package/agents/roles/system/release-manager/references/operating-model.md +8 -0
  81. package/agents/roles/system/sales-representative/SKILL.md +4 -2
  82. package/agents/roles/system/security-engineer/SKILL.md +4 -2
  83. package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
  84. package/agents/roles/system/site-reliability-engineer/SKILL.md +4 -2
  85. package/agents/roles/system/software-architect/SKILL.md +4 -2
  86. package/agents/roles/system/solutions-engineer/SKILL.md +2 -0
  87. package/agents/roles/system/tech-lead/SKILL.md +2 -0
  88. package/agents/roles/system/technical-program-manager/SKILL.md +2 -0
  89. package/agents/roles/system/technical-writer/SKILL.md +2 -0
  90. package/agents/roles/system/treasury-analyst/SKILL.md +6 -4
  91. package/agents/roles/system/ui-designer/SKILL.md +15 -0
  92. package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
  93. package/agents/roles/system/user-researcher/SKILL.md +6 -0
  94. package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -0
  95. package/agents/roles/system/ux-designer/SKILL.md +2 -0
  96. package/automatization/shared/eval-measured.js +35 -0
  97. package/automatization/shared/eval-only.js +15 -0
  98. package/automatization/workflows/README.md +27 -0
  99. package/automatization/workflows/agent-eval.js +42 -7
  100. package/automatization/workflows/agent-promote.js +65 -15
  101. package/automatization/workflows/autobuild.js +52 -8
  102. package/automatization/workflows/flow-eval.js +27 -7
  103. package/engine/agents/catalog.js +1 -1
  104. package/engine/agents/fork.js +1 -1
  105. package/engine/agents/learning-files.js +7 -2
  106. package/engine/agents/learning-seal.js +123 -0
  107. package/engine/agents/learning-sources.js +33 -3
  108. package/engine/agents/learning.js +80 -87
  109. package/engine/automation/hooks.js +0 -1
  110. package/engine/automation/index.js +0 -2
  111. package/engine/cli/args.js +2 -2
  112. package/engine/cli/catalog.js +33 -4
  113. package/engine/cli/instance.js +0 -2
  114. package/engine/cli/ops.js +2 -18
  115. package/engine/config/validate.js +7 -2
  116. package/engine/core/onboarding.js +0 -3
  117. package/engine/core/scan.js +1 -1
  118. package/engine/hooks/input.js +1 -1
  119. package/engine/integrations/proposals.js +1 -1
  120. package/engine/integrations/providers/jira.js +1 -1
  121. package/engine/integrations/registry.js +0 -1
  122. package/engine/planning/business-rules.js +1 -1
  123. package/engine/planning/contracts.js +8 -1
  124. package/engine/planning/parser.js +7 -1
  125. package/engine/schemas/ops-config.schema.json +60 -13
  126. package/flows/system/incident-review/flow.json +1 -1
  127. package/flows/system/incident-review/learning/HISTORY.md +1 -0
  128. package/package.json +4 -3
  129. package/template/planning/rules/system/conduct.md +19 -0
@@ -9,7 +9,6 @@ const fs = require('node:fs')
9
9
  const path = require('node:path')
10
10
  const catalog = require('./catalog')
11
11
  const ownership = require('../core/ownership')
12
- const { section } = require('../planning/parser')
13
12
 
14
13
  const REQUIRED_SECTIONS = [
15
14
  'Hallazgos',
@@ -83,6 +82,12 @@ function frontmatterState(text, fallback) {
83
82
  return ((text.match(/^status:\s*(\S+)\s*$/m) || [])[1] || fallback).toLowerCase()
84
83
  }
85
84
 
85
+ // La vigente de un período: con revisiones abiertas, la última. La usan componer y cerrar.
86
+ function lastOfPeriod(dir, period) {
87
+ const names = proposalFiles(dir).filter((name) => name.match(PROPOSAL_NAME)[1] === period)
88
+ return names.length ? names[names.length - 1] : ''
89
+ }
90
+
86
91
  function proposalState(text) {
87
92
  return frontmatterState(text, 'proposed')
88
93
  }
@@ -107,5 +112,5 @@ function reportFiles(dir) {
107
112
  module.exports = {
108
113
  REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME,
109
114
  assertWritableTeam, assertWritable, isoDate, month,
110
- proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles,
115
+ proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles, lastOfPeriod,
111
116
  }
@@ -0,0 +1,123 @@
1
+ 'use strict'
2
+
3
+ // Cerrar una propuesta: los dos destinos que la sacan del ciclo y el historial que los registra.
4
+ //
5
+ // Vive aparte de `learning.js` porque son dos propósitos con vidas distintas: allá se **compone** una
6
+ // propuesta —leyendo informes y veredictos, decidiendo si hay algo que decir—, y acá se la **cierra**,
7
+ // que es un cambio de estado sobre un documento que ya existe. El umbral de 500 líneas fue lo que lo
8
+ // hizo mirar; lo que decidió la división es que ninguna de las dos mitades necesita a la otra.
9
+
10
+ const fs = require('node:fs')
11
+ const path = require('node:path')
12
+ const { atomicWrite } = require('../core/files')
13
+ const { isoDate, proposalFiles, proposalState, assertWritable, lastOfPeriod } = require('./learning-files')
14
+ const { section } = require('../planning/parser')
15
+
16
+ // El cuerpo sin sellar, en los dos estados que produce el ciclo: «pendiente» lo escribe el molde y
17
+ // «aprobada» la firma. Lo lee la guarda de más abajo y lo reemplaza el sello, así que vive una vez.
18
+ const UNSEALED = /^-[ \t]*Estado:[ \t]*(?:pendiente|aprobada)[ \t]*$/mi
19
+
20
+ // El estado terminal del ciclo. Existía la firma, la aplicación y el historial, y faltaba justo el
21
+ // paso que vuelve irrepetible lo ya hecho: `status:` nacía en `proposed` y nadie lo movía nunca.
22
+ //
23
+ // No era un dato desprolijo. `agent-promote` busca la propuesta más nueva y aplica si el estado dice
24
+ // aprobada con responsable — y «aprobada y aplicada» cumple eso—, así que volver a correrlo sobre una
25
+ // propuesta ya aplicada la aplicaba de nuevo. Como los cambios son aditivos por diseño, el resultado
26
+ // no es un error visible sino un contrato con cada viñeta y cada fuente duplicadas.
27
+ function seal(root, agent, period = '', kind = 'agent') {
28
+ const file = proposalFile(root, agent, period, kind)
29
+ const dir = path.dirname(file)
30
+ const text = fs.readFileSync(file, 'utf8')
31
+ const state = proposalState(text)
32
+ // Sellado es el documento entero, no su frontmatter. Mirando sólo `status` la salida temprana daba
33
+ // por hecho lo que faltaba: si algo movió el campo antes que el sello —quien aplica edita archivos, y
34
+ // mover `status` se le parece a su trabajo— el cuerpo se quedaba en «aprobada» para siempre, que es
35
+ // la contradicción que sellar existe para cerrar. Pasó con `growth-marketer/2026-09`.
36
+ if (state === 'applied' && !UNSEALED.test(text)) return { file, already: true }
37
+ if (!/^status:\s*\S+\s*$/m.test(text)) throw new Error(`${file} no declara status en su frontmatter.`)
38
+ // Un cargo llega acá después de `agent-promote`, que se niega si «Aprobación humana» no está firmada.
39
+ // Un recorrido no tiene ese workflow, así que sin esta puerta `--applied` sellaba una propuesta con
40
+ // «Estado: pendiente» y «Cambio propuesto: Por definir»: el frontmatter decía `applied` y el cuerpo
41
+ // decía lo contrario, dentro del mismo documento. Se comprueba para los dos porque la contradicción
42
+ // no depende de quién sea el sujeto, y para el cargo la puerta ya la pasó quien firmó.
43
+ const responsible = (text.match(/^-\s*Responsable:\s*(.+)$/m) || [])[1] || ''
44
+ const change = section(text, /Cambio propuesto/i).split('\n').slice(1).join('\n').trim()
45
+ const undecided = (value) => !value || /^(por definir|pendiente)\b/i.test(value)
46
+ if (undecided(responsible.trim()) || undecided(change)) {
47
+ throw new Error(
48
+ `${path.basename(file)} todavía no la decidió nadie: «Aprobación humana» necesita un responsable `
49
+ + 'y «Cambio propuesto» tiene que decir qué cambia. Aplicar es un acto humano y esto lo registra.',
50
+ )
51
+ }
52
+ const stamped = text
53
+ .replace(/^status:\s*\S+\s*$/m, 'status: applied')
54
+ // `aprobada` además de `pendiente`: cuando `seal` corre, la firma ya pasó y `sign-proposal.yml`
55
+ // dejó «aprobada», así que buscar sólo «pendiente» no reemplazaba nunca por el camino real. Las 24
56
+ // propuestas aplicadas del repositorio quedaron con `status: applied` y «- Estado: aprobada»: la
57
+ // contradicción entre frontmatter y cuerpo que la guarda de arriba dice evitar. La prueba no lo veía
58
+ // porque su fixture firmaba dejando «pendiente», un estado que producción no produce.
59
+ .replace(UNSEALED, '- Estado: aplicada')
60
+ .replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`)
61
+ atomicWrite(file, stamped)
62
+ // El historial sólo lo escribe alguien para los cargos —`agent-promote`— y nadie para los recorridos,
63
+ // así que el registro que la plantilla promete no existía nunca. Se escribe acá porque es determinista:
64
+ // la fecha, el documento, quién aprobó y qué dice que cambia, todo sale de lo que se acaba de sellar.
65
+ if (kind === 'flow') appendHistory(path.dirname(path.dirname(dir)), file, responsible.trim(), change)
66
+ return { file, already: false }
67
+ }
68
+
69
+ // Archivar es el tercer destino de una propuesta, y hasta ahora no existía: aplicarla, dejarla
70
+ // esperando, o **mirarla y decidir que no cambia nada**. Ese tercero se venía haciendo mergeando el PR
71
+ // sin firmar, que deja el documento en `proposed` para siempre — indistinguible de una que espera
72
+ // trabajo. En septiembre quedaron dieciséis así, y el CLI iba a decir «1 sin aplicar» de cada una
73
+ // mientras nadie tuviera nada que hacer con ellas.
74
+ //
75
+ // Se niega sobre una propuesta firmada, y ésa es toda la guarda que hace falta: firmar es autorizar un
76
+ // cambio, así que lo que corresponde después es aplicarlo. Sin esto, archivar sería la vía para hacer
77
+ // desaparecer trabajo autorizado sin ejecutarlo ni decirlo, que es peor que el contador ruidoso.
78
+ // `--period 2026-08` nombra el período, no un archivo: con revisiones abiertas la que se resuelve es la
79
+ // vigente de ese período. Resolverlo siempre a `2026-08.md` devolvía «ya estaba aplicada» y dejaba la
80
+ // revisión sin sellar, que es justo el estado en que `agent-promote` la vuelve a aplicar. Una revisión
81
+ // concreta se nombra entera —`--period 2026-08-r2`— y entonces manda ésa.
82
+ function proposalFile(root, agent, period = '', kind = 'agent') {
83
+ const dir = path.join(assertWritable(root, agent, kind), 'learning', 'proposals')
84
+ const names = proposalFiles(dir)
85
+ if (!names.length) throw new Error(`${agent} no tiene propuestas en learning/proposals/.`)
86
+ const name = period
87
+ ? (/^\d{4}-\d{2}$/.test(period) ? lastOfPeriod(dir, period) : `${period}.md`)
88
+ : names[names.length - 1]
89
+ if (!name || !names.includes(name)) throw new Error(`${agent} no tiene la propuesta ${period || name}.`)
90
+ return path.join(dir, name)
91
+ }
92
+
93
+ function archive(root, agent, period = '', kind = 'agent') {
94
+ const file = proposalFile(root, agent, period, kind)
95
+ const text = fs.readFileSync(file, 'utf8')
96
+ const state = proposalState(text)
97
+ if (state === 'archived') return { file, already: true }
98
+ if (state === 'applied') throw new Error(`${path.basename(file)} ya está aplicada: archivarla la borraría del ciclo.`)
99
+ if (/^-[ \t]*Estado:[ \t]*aprobada[ \t]*$/mi.test(text)) {
100
+ throw new Error(
101
+ `${path.basename(file)} está firmada: lo que sigue es aplicarla con agent-promote, no archivarla. `
102
+ + 'Archivar es para lo que se miró y no cambia nada.',
103
+ )
104
+ }
105
+ atomicWrite(file, text
106
+ .replace(/^status:\s*\S+\s*$/m, 'status: archived')
107
+ .replace(/^-[ \t]*Estado:[ \t]*pendiente[ \t]*$/mi, '- Estado: archivada')
108
+ .replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`))
109
+ return { file, already: false }
110
+ }
111
+
112
+ // Una fila por propuesta aplicada. El cambio va en una línea: el documento entero está a un enlace, y
113
+ // una tabla que lo repite entero deja de leerse.
114
+ function appendHistory(target, file, responsible, change) {
115
+ const history = path.join(target, 'learning', 'HISTORY.md')
116
+ if (!fs.existsSync(history)) return
117
+ const line = change.split('\n').map((one) => one.trim()).filter(Boolean)[0] || ''
118
+ const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` | aplicada | ${responsible} `
119
+ + `| ${line.slice(0, 160)} |\n`
120
+ fs.appendFileSync(history, `${fs.readFileSync(history, 'utf8').endsWith('\n') ? '' : '\n'}${row}`)
121
+ }
122
+
123
+ module.exports = { seal, archive }
@@ -12,6 +12,26 @@ const {
12
12
  REQUIRED_SECTIONS, SUMMARY_MAX, frontmatterState, proposalFiles, proposalState, reportFiles,
13
13
  } = require('./learning-files')
14
14
 
15
+ // Una propuesta firmada y sin aplicar no espera lo mismo que una sin firmar: en la primera la decisión
16
+ // ya se tomó y el trabajo quedó detenido; la segunda está bien quieta hasta que alguien la lea.
17
+ // `proposalState` no las distingue —mira el frontmatter, y la firma la escribe `sign-proposal.yml` en
18
+ // el cuerpo—, así que las dos caían en el mismo `pending` y la que ya tenía autoridad para avanzar se
19
+ // veía igual que la que no. Sin este aviso hay que acordarse, y dos propuestas firmadas el 2026-09-01
20
+ // se habrían quedado ahí sin que nada lo dijera.
21
+ const SIGNED = /^-[ \t]*Estado:[ \t]*aprobada[ \t]*$/mi
22
+ // Los dos destinos que cierran una propuesta. `archived` es «se miró y no cambia nada»: no espera
23
+ // trabajo, así que contarla como pendiente deja al cargo reportando deuda que nadie va a pagar — el
24
+ // mismo defecto que el comentario de abajo describe para una aplicada.
25
+ const CLOSED = new Set(['applied', 'archived'])
26
+
27
+ // No es un error: firmar y aplicar son actos separados a propósito —OPS-004— y entre uno y otro puede
28
+ // pasar tiempo legítimamente. Lo que no puede es no verse.
29
+ function signedWarning(signed) {
30
+ if (!signed.length) return []
31
+ return [`${signed.length} propuesta(s) firmada(s) sin aplicar (${signed.join(', ')}): `
32
+ + 'la autorización ya está, falta `agent-promote`']
33
+ }
34
+
15
35
  function evaluateTeam(root, slug) {
16
36
  const dir = path.dirname(require('../flows/registry').read(root, slug).file)
17
37
  const errors = []
@@ -21,14 +41,19 @@ function evaluateTeam(root, slug) {
21
41
  }
22
42
  const proposals = proposalFiles(path.join(dir, 'learning', 'proposals'))
23
43
  let pending = 0
44
+ const signed = []
24
45
  for (const name of proposals) {
25
46
  const text = fs.readFileSync(path.join(dir, 'learning', 'proposals', name), 'utf8')
26
47
  if (!/^automatic_apply:\s*false$/m.test(text)) errors.push(`${name}: automatic_apply debe ser false`)
27
48
  for (const section of REQUIRED_SECTIONS) {
28
49
  if (!text.includes(`## ${section}`)) errors.push(`${name}: falta sección ${section}`)
29
50
  }
30
- if (proposalState(text) !== 'applied') pending += 1
51
+ if (!CLOSED.has(proposalState(text))) {
52
+ pending += 1
53
+ if (SIGNED.test(text)) signed.push(name)
54
+ }
31
55
  }
56
+ warnings.push(...signedWarning(signed))
32
57
  return { errors, warnings, proposals: proposals.length, pending, cases: 0 }
33
58
  }
34
59
 
@@ -135,6 +160,7 @@ function evaluate(root, agent) {
135
160
  }
136
161
  const proposals = proposalFiles(path.join(target, 'learning', 'proposals'))
137
162
  let pending = 0
163
+ const signed = []
138
164
  for (const name of proposals) {
139
165
  const text = fs.readFileSync(path.join(target, 'learning', 'proposals', name), 'utf8')
140
166
  if (!/^automatic_apply:\s*false$/m.test(text)) errors.push(`${name}: automatic_apply debe ser false`)
@@ -143,8 +169,12 @@ function evaluate(root, agent) {
143
169
  }
144
170
  // Contar sólo las que esperan algo. Una propuesta aplicada contada como propuesta deja al cargo
145
171
  // reportando trabajo pendiente para siempre, y es la misma confusión que permitía reaplicarla.
146
- if (proposalState(text) !== 'applied') pending += 1
172
+ if (!CLOSED.has(proposalState(text))) {
173
+ pending += 1
174
+ if (SIGNED.test(text)) signed.push(name)
175
+ }
147
176
  }
177
+ warnings.push(...signedWarning(signed))
148
178
  // Los hallazgos que todavía no llegaron al contrato. No es un error —la propuesta que los tome
149
179
  // puede no haberse abierto aún—, pero sin decirlo un informe escrito y olvidado se ve igual que uno
150
180
  // ya incorporado: los dos son un archivo en `reports/`.
@@ -163,4 +193,4 @@ function evaluate(root, agent) {
163
193
  return { errors, warnings, proposals: proposals.length, pending, cases }
164
194
  }
165
195
 
166
- module.exports = { SOURCE_TIERS, CADENCES, cadence, evaluate, evaluateTeam }
196
+ module.exports = { SOURCE_TIERS, cadence, evaluate, evaluateTeam }
@@ -6,12 +6,13 @@ const catalog = require('./catalog')
6
6
  const { atomicWrite } = require('../core/files')
7
7
  // La misma lectura de secciones que usa el planning: repetirla acá sería una segunda copia del mismo
8
8
  // hecho, y una de las dos se pudre sin que nada falle (R11).
9
- const { section } = require('../planning/parser')
10
9
  const {
11
- REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME, assertWritableTeam, assertWritable,
10
+ REQUIRED_SECTIONS, SUMMARY_MAX, PROPOSAL_NAME, REPORT_NAME, assertWritableTeam, assertWritable, lastOfPeriod,
12
11
  isoDate, month, proposalOrder, proposalFiles, frontmatterState, proposalState, reportFiles,
13
12
  } = require('./learning-files')
14
- const { SOURCE_TIERS, CADENCES, cadence, evaluate, evaluateTeam } = require('./learning-sources')
13
+ // Cerrar una propuesta vive en su propio módulo; se reexporta para no mover a cada llamador.
14
+ const { seal, archive } = require('./learning-seal')
15
+ const { SOURCE_TIERS, cadence, evaluate, evaluateTeam } = require('./learning-sources')
15
16
 
16
17
  // El informe que ya entró a una propuesta. Nace en `draft` y nada lo movía nunca, así que el que se
17
18
  // consolidó y el que se escribió tarde —después de que la propuesta del período ya existía, y por eso
@@ -31,67 +32,6 @@ function markConsolidated(file) {
31
32
  atomicWrite(file, text.replace(front[0], `---\n${front[1]}\nstatus: consolidated\n---\n`))
32
33
  }
33
34
 
34
- // El estado terminal del ciclo. Existía la firma, la aplicación y el historial, y faltaba justo el
35
- // paso que vuelve irrepetible lo ya hecho: `status:` nacía en `proposed` y nadie lo movía nunca.
36
- //
37
- // No era un dato desprolijo. `agent-promote` busca la propuesta más nueva y aplica si el estado dice
38
- // aprobada con responsable — y «aprobada y aplicada» cumple eso—, así que volver a correrlo sobre una
39
- // propuesta ya aplicada la aplicaba de nuevo. Como los cambios son aditivos por diseño, el resultado
40
- // no es un error visible sino un contrato con cada viñeta y cada fuente duplicadas.
41
- function seal(root, agent, period = '', kind = 'agent') {
42
- const dir = path.join(assertWritable(root, agent, kind), 'learning', 'proposals')
43
- const names = proposalFiles(dir)
44
- if (!names.length) throw new Error(`${agent} no tiene propuestas en learning/proposals/.`)
45
- // `--period 2026-08` nombra el período, no un archivo: con revisiones abiertas la que se sella es la
46
- // vigente de ese período. Resolverlo siempre a `2026-08.md` habría devuelto «ya estaba aplicada» y
47
- // dejado la revisión sin sellar, que es justo el estado en que `agent-promote` la vuelve a aplicar.
48
- // Una revisión concreta se puede nombrar entera —`--period 2026-08-r2`— y entonces manda esa.
49
- const name = period
50
- ? (/^\d{4}-\d{2}$/.test(period) ? lastOfPeriod(dir, period) : `${period}.md`)
51
- : names[names.length - 1]
52
- if (!name || !names.includes(name)) throw new Error(`${agent} no tiene la propuesta ${period || name}.`)
53
- const file = path.join(dir, name)
54
- const text = fs.readFileSync(file, 'utf8')
55
- const state = proposalState(text)
56
- if (state === 'applied') return { file, already: true }
57
- if (!/^status:\s*\S+\s*$/m.test(text)) throw new Error(`${file} no declara status en su frontmatter.`)
58
- // Un cargo llega acá después de `agent-promote`, que se niega si «Aprobación humana» no está firmada.
59
- // Un recorrido no tiene ese workflow, así que sin esta puerta `--applied` sellaba una propuesta con
60
- // «Estado: pendiente» y «Cambio propuesto: Por definir»: el frontmatter decía `applied` y el cuerpo
61
- // decía lo contrario, dentro del mismo documento. Se comprueba para los dos porque la contradicción
62
- // no depende de quién sea el sujeto, y para el cargo la puerta ya la pasó quien firmó.
63
- const responsible = (text.match(/^-\s*Responsable:\s*(.+)$/m) || [])[1] || ''
64
- const change = section(text, /Cambio propuesto/i).split('\n').slice(1).join('\n').trim()
65
- const undecided = (value) => !value || /^(por definir|pendiente)\b/i.test(value)
66
- if (undecided(responsible.trim()) || undecided(change)) {
67
- throw new Error(
68
- `${path.basename(file)} todavía no la decidió nadie: «Aprobación humana» necesita un responsable `
69
- + 'y «Cambio propuesto» tiene que decir qué cambia. Aplicar es un acto humano y esto lo registra.',
70
- )
71
- }
72
- const stamped = text
73
- .replace(/^status:\s*\S+\s*$/m, 'status: applied')
74
- .replace(/^-[ \t]*Estado:[ \t]*pendiente[ \t]*$/m, '- Estado: aplicada')
75
- .replace(/^-[ \t]*Fecha:[ \t]*por definir[ \t]*$/mi, `- Fecha: ${isoDate(new Date())}`)
76
- atomicWrite(file, stamped)
77
- // El historial sólo lo escribe alguien para los cargos —`agent-promote`— y nadie para los recorridos,
78
- // así que el registro que la plantilla promete no existía nunca. Se escribe acá porque es determinista:
79
- // la fecha, el documento, quién aprobó y qué dice que cambia, todo sale de lo que se acaba de sellar.
80
- if (kind === 'flow') appendHistory(path.dirname(path.dirname(dir)), file, responsible.trim(), change)
81
- return { file, already: false }
82
- }
83
-
84
- // Una fila por propuesta aplicada. El cambio va en una línea: el documento entero está a un enlace, y
85
- // una tabla que lo repite entero deja de leerse.
86
- function appendHistory(target, file, responsible, change) {
87
- const history = path.join(target, 'learning', 'HISTORY.md')
88
- if (!fs.existsSync(history)) return
89
- const line = change.split('\n').map((one) => one.trim()).filter(Boolean)[0] || ''
90
- const row = `| ${isoDate(new Date())} | \`${path.basename(file)}\` | aplicada | ${responsible} `
91
- + `| ${line.slice(0, 160)} |\n`
92
- fs.appendFileSync(history, `${fs.readFileSync(history, 'utf8').endsWith('\n') ? '' : '\n'}${row}`)
93
- }
94
-
95
35
  function prepareReport(root, agent, now = new Date()) {
96
36
  const reports = path.join(assertWritable(root, agent), 'learning', 'reports')
97
37
  const file = path.join(reports, `${isoDate(now)}.md`)
@@ -143,7 +83,7 @@ propuesta consolidada. -->
143
83
  // El molde de los apartados que una persona escribe queda; lo que deja de quedar es `## Hallazgos` en
144
84
  // blanco. Una revisión que no puede decir qué la motiva no puede producir un cambio, y hasta acá se
145
85
  // llega sólo con material: el llamador ya se negó a abrirla sin él.
146
- function reviseProposal(root, agent, dir, previous, period, findings = [], consumed = []) {
86
+ function reviseProposal(agent, dir, previous, period, findings = [], consumed = []) {
147
87
  const parsed = previous.match(PROPOSAL_NAME)
148
88
  const revision = Number(parsed[2] || 1) + 1
149
89
  const file = path.join(dir, `${period}-r${revision}.md`)
@@ -200,15 +140,19 @@ sólo que pase.
200
140
  }
201
141
 
202
142
  // La última propuesta del período, si la hay: es contra ella que se decide si abrir una revisión.
203
- function lastOfPeriod(dir, period) {
204
- const names = proposalFiles(dir).filter((name) => name.match(PROPOSAL_NAME)[1] === period)
205
- return names.length ? names[names.length - 1] : ''
206
- }
207
-
208
143
  // De qué aprende un recorrido: de cada registro sin sellar entran los casos que no pasaron, con su
209
144
  // contraste y de qué corrida salen. Si no hay ninguno, eso también es un resultado — el recorrido
210
145
  // aguantó y no hay qué corregir. Por qué es esto y no una investigación, en `pendingRuns`.
211
- const VERDICT = /\n### ([^\n]+)\n\n- Veredicto: (pasa|no pasa)\n([\s\S]*?)(?=\n### |$)/g
146
+ // El contraste termina donde empieza **el caso siguiente**, y un caso se reconoce por su línea de
147
+ // veredicto, no por ser un `###`. Cortando en cualquier `###` se cortaba en el primero que la respuesta
148
+ // del cargo usara para su propia estructura: 285 de los 774 veredictos del repositorio quedaban
149
+ // truncados —`06-adversarial-runbook` conservaba 1.057 de 48.991 caracteres, y un `06-adversarial-docs`
150
+ // llegó a 49 de 20.119—. Nada lo delataba: la propuesta se compone igual y se lee entera.
151
+ const VERDICT = /\n### ([^\n]+)\n\n- Veredicto: (pasa|no pasa)\n([\s\S]*?)(?=\n### [^\n]+\n\n- Veredicto: |$)/g
152
+
153
+ // La nota que el juez deja sobre el contrato: una línea propia debajo del veredicto, no enterrada en el
154
+ // contraste. Va a un documento que se firma, así que tiene que poder extraerse sin interpretar prosa.
155
+ const CONTRACT_NOTE = /^-\s*Para el contrato:\s*(.+)$/m
212
156
 
213
157
  function verdicts(text) {
214
158
  return [...text.matchAll(VERDICT)]
@@ -242,10 +186,26 @@ function verdictFindings(root, dir) {
242
186
  latest.set(item.id, { ...item, file, name, failures: (before ? before.failures : 0) + (item.passed ? 0 : 1) })
243
187
  }
244
188
  }
245
- const findings = [...latest.values()].filter((item) => !item.passed).map((item) =>
246
- `### ${item.id} ${item.name.slice(0, -3)}\n\n`
247
- + `Corrida: \`${path.relative(root, item.file)}\``
248
- + `${item.failures > 1 ? ` falló en ${item.failures} corridas de esta tanda` : ''}\n\n${item.detail}`)
189
+ const findings = [...latest.values()].flatMap((item) => {
190
+ const corrida = `Corrida: \`${path.relative(root, item.file)}\``
191
+ if (!item.passed) {
192
+ return [`### ${item.id} — ${item.name.slice(0, -3)}\n\n${corrida}`
193
+ + `${item.failures > 1 ? ` — falló en ${item.failures} corridas de esta tanda` : ''}\n\n${item.detail}`]
194
+ }
195
+ // Un caso que pasa también trae material, y hasta acá no tenía por dónde entrar. El juez ve el
196
+ // contrato entero mientras juzga y a veces encuentra lo que **no** pide: una conducta que ningún
197
+ // comportamiento esperado nombra, y que por eso ningún caso podía atrapar. No es un fallo del sujeto
198
+ // —eso lo dice el veredicto— así que no puede viajar como rojo, y sin esta rama moría en la prosa del
199
+ // contraste. Pasó el 2026-08-30: el juez anotó que el cargo debía citar verbatim o decir «paráfrasis»,
200
+ // el caso pasó, y la revisión que lo recogió hubo que escribirla a mano.
201
+ // De la primera línea y no de cualquier parte: después del veredicto viene la respuesta del sujeto,
202
+ // y ahí una línea con este prefijo sería una nota que el sujeto se escribe a sí mismo.
203
+ const note = (item.detail.split('\n', 1)[0].match(CONTRACT_NOTE) || [])[1]
204
+ return note
205
+ ? [`### ${item.id} — ${item.name.slice(0, -3)} · el caso pasa\n\n${corrida}\n\n`
206
+ + `Lo que el contrato no cubre: ${note.trim()}`]
207
+ : []
208
+ })
249
209
  return { consumed: unsealed.map((name) => path.join(results, name)), findings }
250
210
  }
251
211
 
@@ -278,7 +238,7 @@ function pendingReports(target, sealing) {
278
238
  // veredicto en contra, no una fuente nueva.
279
239
  // Recibe los hallazgos ya compuestos: el llamador los necesita antes para decidir si abre documento,
280
240
  // y volver a leerlos acá sería leer dos veces lo mismo para responder la misma pregunta.
281
- function proposeFromRuns(root, agent, target, file, period, { consumed, findings }) {
241
+ function proposeFromRuns(agent, file, period, { consumed, findings }) {
282
242
  fs.writeFileSync(file, `---
283
243
  flow: ${agent}
284
244
  period: ${period}
@@ -303,6 +263,9 @@ Por definir. Lo que se corrige es el recorrido: un \`exitGate\` que dejó pasar
303
263
  una etapa que dependía de otra sin necesidad, un guardrail que nadie podía cumplir, un agente
304
264
  condicional que hacía falta siempre. No cambiar el contrato de ningún cargo desde acá.
305
265
 
266
+ La primera línea de esta sección es la que viaja al historial al aplicar: que diga el cambio, no una
267
+ advertencia sobre él.
268
+
306
269
  ## Riesgos y regresiones
307
270
 
308
271
  Qué caso pasaba con el recorrido actual y podría dejar de pasar. Un gate más duro frena de más, y eso
@@ -349,8 +312,8 @@ function prepareProposal(root, agent, now = new Date(), period = '', kind = 'age
349
312
  if (kind === 'flow') {
350
313
  const red = verdictFindings(root, target)
351
314
  if (!red.findings.length) return { file: '', created: false, reports: 0 }
352
- if (previous) return reviseProposal(root, agent, proposalDir, previous, sealing, red.findings, red.consumed)
353
- return proposeFromRuns(root, agent, target, path.join(proposalDir, `${sealing}.md`), sealing, red)
315
+ if (previous) return reviseProposal(agent, proposalDir, previous, sealing, red.findings, red.consumed)
316
+ return proposeFromRuns(agent, path.join(proposalDir, `${sealing}.md`), sealing, red)
354
317
  }
355
318
 
356
319
  // Los dos materiales de un cargo, y dicen cosas distintas: un informe sin consolidar dice que cambió
@@ -368,9 +331,24 @@ function prepareProposal(root, agent, now = new Date(), period = '', kind = 'age
368
331
  if (!reports.length && !red.findings.length) return { file: '', created: false, reports: 0 }
369
332
 
370
333
  const reportPaths = reports.map((name) => path.join(reportDir, name))
334
+ // La misma regla que la rama de recorridos, por el mismo motivo: un documento que no puede decir qué
335
+ // corregir no cambia ningún contrato y cuesta igual la firma humana que uno que sí. Un informe puede
336
+ // encontrar cosas y no proponer ningún cambio —resultado legítimo—, y un mes entero así abría una
337
+ // propuesta cuyas entradas decían todas «Sin recomendación registrada»: se lee entera, no propone
338
+ // nada, y nadie la iba a firmar.
339
+ //
340
+ // No se sellan, y es a propósito. Sellar toca archivos, y en el ciclo automático eso abre un PR con
341
+ // puros sellos: la misma firma que se quería ahorrar, ahora sin siquiera un documento que leer. Lo
342
+ // que queda es que el job vuelva a no encontrar nada el mes siguiente —barato: sin modelo y sin PR—
343
+ // y que estos informes entren al documento el mes que alguno sí proponga algo, como «Sin
344
+ // recomendación registrada». Ahí ese texto es histórico, no ruido.
345
+ if (!red.findings.length && !reportPaths.some((file) => recommendationOf(file))) {
346
+ return { file: '', created: false, reports: 0, quiet: reportPaths.length }
347
+ }
348
+
371
349
  const findings = [...reportPaths.map((file) => reportSummary(root, file)), ...red.findings]
372
350
  const consumed = [...reportPaths, ...red.consumed]
373
- if (previous) return reviseProposal(root, agent, proposalDir, previous, sealing, findings, consumed)
351
+ if (previous) return reviseProposal(agent, proposalDir, previous, sealing, findings, consumed)
374
352
 
375
353
  const file = path.join(proposalDir, `${sealing}.md`)
376
354
  const summaries = findings
@@ -413,21 +391,36 @@ Pendiente.
413
391
  return { file, created: true, reports: reports.length, findings: red.findings.length }
414
392
  }
415
393
 
394
+ // El patrón vive una sola vez porque dos lugares preguntan lo mismo por motivos distintos:
395
+ // `reportSummary` para copiar la recomendación, y `prepareProposal` para decidir si hay algo que
396
+ // proponer. Duplicado, una de las dos copias deja de coincidir con el andamio y nada falla.
397
+ //
398
+ // Sin `m`: con esa bandera el `$` casa fin de *línea*, así que la búsqueda no ávida cortaba en el
399
+ // primer salto y la propuesta consolidaba una sola línea de una recomendación de diez. Comprobado en
400
+ // node v24.18.0: el mismo patrón con `m` devuelve la primera línea y sin `m` devuelve el bloque.
401
+ //
402
+ // `[^\S\n]*\n` y no `\s*\n` para cerrar la línea del título: `\s` incluye el salto, así que con la
403
+ // sección vacía se comía también el salto que precede al título siguiente, la captura arrancaba en
404
+ // «## Preguntas abiertas» y el lookahead ya no encontraba nada. La propuesta terminaba presentando la
405
+ // sección de al lado como si fuera la recomendación a firmar. Comprobado en node v24.18.0 sobre un
406
+ // informe con «## Recomendación» vacía: con `\s*` captura «## Preguntas abiertas…» y con `[^\S\n]*`
407
+ // captura la cadena vacía, que es lo que hace falta para poder distinguir «no recomendó nada».
408
+ const RECOMMENDATION = /\n## Recomendación[^\S\n]*\n([\s\S]*?)(?=\n## |$)/
409
+
410
+ // Qué recomienda un informe, o cadena vacía si no recomienda nada.
411
+ function recommendationOf(report) {
412
+ return ((fs.readFileSync(report, 'utf8').match(RECOMMENDATION) || [])[1] || '').trim()
413
+ }
414
+
416
415
  // La recomendación de un informe, que es lo único que la propuesta consolida de él.
417
416
  function reportSummary(root, report) {
418
417
  const name = path.basename(report)
419
- const text = fs.readFileSync(report, 'utf8')
420
- // Sin `m`: con esa bandera el `$` casa fin de *línea*, así que la búsqueda no ávida cortaba en el
421
- // primer salto y la propuesta consolidaba una sola línea de una recomendación de diez. Comprobado en
422
- // node v24.18.0: el mismo patrón con `m` devuelve la primera línea y sin `m` devuelve el bloque.
423
- const match = text.match(/\n## Recomendación\s*\n([\s\S]*?)(?=\n## |$)/) || []
424
- const recommendation = (match[1] || 'Sin recomendación registrada.').trim()
418
+ const recommendation = recommendationOf(report) || 'Sin recomendación registrada.'
425
419
  return `### ${name.slice(0, -3)}\n\nFuente interna: \`${path.relative(root, report)}\`\n\n${recommendation}`
426
420
  }
427
421
 
428
422
 
429
423
  module.exports = {
430
424
  SOURCE_TIERS,
431
- CADENCES,
432
425
  cadence, pendingRuns,
433
- SUMMARY_MAX, prepareReport, prepareProposal, evaluate, evaluateTeam, proposalState, seal }
426
+ SUMMARY_MAX, prepareReport, prepareProposal, evaluate, evaluateTeam, proposalState, seal, archive }
@@ -7,7 +7,6 @@
7
7
  const fs = require('node:fs')
8
8
  const path = require('node:path')
9
9
  const H = require('../hooks/run')
10
- const O = require('../core/ownership')
11
10
  const M = require('../core/manifest')
12
11
  const { packagedAutomation } = require('./runners')
13
12
 
@@ -4,8 +4,6 @@ const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
  const { spawnSync } = require('node:child_process')
6
6
  const F = require('../core/files')
7
- const H = require('../hooks/run')
8
- const P = require('../planning/parser')
9
7
  const catalog = require('../agents/catalog')
10
8
  const O = require('../core/ownership')
11
9
  const M = require('../core/manifest')
@@ -25,7 +25,7 @@ const FLAGS = {
25
25
  agents: ['--json', '--own', '--system'],
26
26
  integration: ['--fixture'],
27
27
  automation: ['--force'],
28
- learn: ['--flow', '--proposal', '--applied', '--period'],
28
+ learn: ['--flow', '--proposal', '--applied', '--archived', '--period'],
29
29
  evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
30
30
  flow: ['--json'],
31
31
  }
@@ -54,4 +54,4 @@ function parse(argv) {
54
54
  }
55
55
  }
56
56
 
57
- module.exports = { FLAGS, VALUED_FLAGS, parse }
57
+ module.exports = { FLAGS, parse }
@@ -132,6 +132,16 @@ function learn(agent, cli) {
132
132
  // aplicada editando frontmatter a mano es justo el paso que un agente hace mal en silencio.
133
133
  // Un recorrido aprende de sus corridas y un cargo de su profesión: mismo ciclo, distinto insumo.
134
134
  const kind = cli.has('--flow') ? 'flow' : 'agent'
135
+ // El tercer destino: se miró y no cambia nada. Sin esto sólo había aplicarla o dejarla esperando,
136
+ // y lo que se hacía era mergear el PR sin firmar — que deja el documento diciendo «sin aplicar»
137
+ // para siempre, indistinguible de una que espera trabajo.
138
+ if (cli.has('--archived')) {
139
+ const result = L.archive(opsRoot(), agent, cli.value('--period'), kind)
140
+ const relative = path.relative(opsRoot(), result.file)
141
+ return console.log(result.already
142
+ ? `= ${relative} ya estaba archivada`
143
+ : `✓ ${relative} queda archivada: se miró y no cambia nada`)
144
+ }
135
145
  if (cli.has('--applied')) {
136
146
  const result = L.seal(opsRoot(), agent, cli.value('--period'), kind)
137
147
  const relative = path.relative(opsRoot(), result.file)
@@ -139,14 +149,28 @@ function learn(agent, cli) {
139
149
  ? `= ${relative} ya estaba aplicada`
140
150
  : `✓ ${relative} queda aplicada: no se vuelve a aplicar`)
141
151
  }
152
+ // Un recorrido no tiene informe semanal: su propuesta se compone de los veredictos de sus propias
153
+ // corridas y nunca lee `learning/reports/`. La forma desnuda —la que para un cargo abre el informe
154
+ // de la semana— no tiene entonces qué abrir acá, y `prepareReport` lo decía resolviendo con el
155
+ // `kind` por defecto: «no existe agents/<tipo>/<slug>/SKILL.md», que manda a crear un cargo que no
156
+ // falta. Negarse nombrando el comando que sí corresponde es lo que cierra R13.
157
+ if (kind === 'flow' && !cli.has('--proposal')) {
158
+ fail(`${agent} es un recorrido: aprende de sus corridas, no de informes semanales.\n`
159
+ + ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, 2)
160
+ }
142
161
  // `--period` es para consolidar a mano un mes que no es el de hoy. El ciclo automático no lo
143
162
  // pasa: la propuesta se llama por el mes en que se abre y arrastra lo que todavía no entró.
144
163
  const result = cli.has('--proposal')
145
164
  ? L.prepareProposal(opsRoot(), agent, new Date(), cli.value('--period'), kind)
146
165
  : L.prepareReport(opsRoot(), agent)
147
- // Sin informes pendientes no hay archivo que nombrar, y decirlo es el resultado: el ciclo corrió
148
- // y no había nada que consolidar.
149
- if (!result.file) return console.log('= sin informes pendientes: no se abre propuesta')
166
+ // Sin archivo que nombrar hay dos resultados distintos y decir cuál es el punto: no había material,
167
+ // o lo había y ninguno propuso un cambio. El segundo no es un error ni un mes perdido —el informe
168
+ // es histórico igual— y confundirlo con el primero manda a buscar por qué no se investigó.
169
+ if (!result.file) {
170
+ return console.log(result.quiet
171
+ ? `= ${result.quiet} informe(s) sin recomendación: no hay qué proponer, quedan para el mes que viene`
172
+ : '= sin informes pendientes: no se abre propuesta')
173
+ }
150
174
  console.log(`${result.created ? '+' : '='} ${path.relative(opsRoot(), result.file)}`)
151
175
  // Un cargo consolida informes de su profesión; un recorrido, las corridas que lo midieron.
152
176
  if (typeof result.reports === 'number') {
@@ -170,7 +194,12 @@ function evaluate(agent, caso, cli) {
170
194
  }
171
195
  // El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
172
196
  // un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
173
- return console.log(evaluationBench(root, agent, caso, cli.has('--force'), kind))
197
+ //
198
+ // Relativa a la raíz, como la de `--record`: esta salida la lee el agente que prepara los bancos,
199
+ // y de ahí la ruta viaja a los informes y a las propuestas que después lee otro cargo. Absoluta
200
+ // nombraba el directorio personal de una máquina, y así quedaron mil cuatrocientas ochenta y cinco
201
+ // citas que dejaron de resolver el día que este repositorio cambió de nombre.
202
+ return console.log(path.relative(root, evaluationBench(root, agent, caso, cli.has('--force'), kind)))
174
203
  }
175
204
  try {
176
205
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
@@ -7,7 +7,6 @@
7
7
 
8
8
  const fs = require('node:fs')
9
9
  const path = require('node:path')
10
- const { spawnSync } = require('node:child_process')
11
10
  const F = require('../core/files')
12
11
  const O = require('../core/ownership')
13
12
  const CL = require('../core/changelog')
@@ -15,7 +14,6 @@ const M = require('../core/manifest')
15
14
  const OB = require('../core/onboarding')
16
15
  const P = require('../planning/parser')
17
16
  const ST = require('../planning/state')
18
- const I = require('../integrations/registry')
19
17
  const A = require('../automation')
20
18
  const { fail } = require('./io')
21
19
 
package/engine/cli/ops.js CHANGED
@@ -4,25 +4,9 @@
4
4
  const fs = require('node:fs')
5
5
  const path = require('node:path')
6
6
  const { spawnSync } = require('node:child_process')
7
- const P = require('../planning/parser')
8
- const B = require('../planning/business-rules')
9
- const PC = require('../planning/contracts')
10
- const ST = require('../planning/state')
11
- const I = require('../integrations/registry')
12
- const L = require('../agents/learning')
13
7
  const A = require('../automation')
14
- const F = require('../core/files')
15
- const O = require('../core/ownership')
16
- const CL = require('../core/changelog')
17
- const M = require('../core/manifest')
18
- const SC = require('../core/scan')
19
- const OB = require('../core/onboarding')
20
- const C = require('../config/validate')
21
- const T = require('../flows/registry')
22
- const AG = require('../agents/catalog')
23
- const EV = require('../agents/evaluations')
24
8
  const { FLAGS, parse } = require('./args')
25
- const { fail, opsRoot } = require('./io')
9
+ const { fail } = require('./io')
26
10
  const IN = require('./instance')
27
11
  const PL = require('./planning')
28
12
  const CAT = require('./catalog')
@@ -170,7 +154,7 @@ function usage() {
170
154
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
171
155
  ops automation install <ops-root> claude|codex|gemini|antigravity
172
156
  ops automation uninstall <ops-root> claude|codex|gemini|antigravity
173
- ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>]]
157
+ ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied|--archived [--period <AAAA-MM>]]
174
158
  ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
175
159
  ops agents list [ops-root] [--own|--system] [--json]
176
160
  ops agents fork <cargo> [ops-root]