@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.
- package/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/agents/README.md +24 -0
- package/agents/roles/system/accounting-specialist/SKILL.md +2 -0
- package/agents/roles/system/ai-governance-lead/SKILL.md +2 -0
- package/agents/roles/system/ai-product-manager/SKILL.md +2 -0
- package/agents/roles/system/analytics-engineer/SKILL.md +2 -0
- package/agents/roles/system/backend-engineer/SKILL.md +4 -2
- package/agents/roles/system/backend-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/business-strategist/SKILL.md +4 -2
- package/agents/roles/system/cloud-architect/SKILL.md +5 -0
- package/agents/roles/system/cloud-architect/learning/HISTORY.md +4 -0
- package/agents/roles/system/community-manager/SKILL.md +2 -0
- package/agents/roles/system/content-specialist/SKILL.md +4 -2
- package/agents/roles/system/customer-success-manager/SKILL.md +9 -2
- package/agents/roles/system/customer-success-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/customer-support-specialist/SKILL.md +4 -2
- package/agents/roles/system/data-analyst/SKILL.md +9 -2
- package/agents/roles/system/data-analyst/learning/HISTORY.md +1 -0
- package/agents/roles/system/data-engineer/SKILL.md +2 -0
- package/agents/roles/system/data-governance-steward/SKILL.md +2 -0
- package/agents/roles/system/data-scientist/SKILL.md +2 -0
- package/agents/roles/system/database-administrator/SKILL.md +2 -0
- package/agents/roles/system/database-administrator/learning/HISTORY.md +1 -0
- package/agents/roles/system/developer-relations-engineer/SKILL.md +2 -0
- package/agents/roles/system/devops-engineer/SKILL.md +4 -2
- package/agents/roles/system/engineering-manager/SKILL.md +2 -0
- package/agents/roles/system/financial-controller/SKILL.md +4 -2
- package/agents/roles/system/finops-engineer/SKILL.md +2 -0
- package/agents/roles/system/finops-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/finops-engineer/learning/sources.yaml +12 -0
- package/agents/roles/system/fraud-risk-analyst/SKILL.md +7 -0
- package/agents/roles/system/fraud-risk-analyst/learning/HISTORY.md +1 -0
- package/agents/roles/system/frontend-engineer/SKILL.md +8 -2
- package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel/InternalNote.jsx +30 -0
- package/agents/roles/system/frontend-engineer/evaluations/cases/09-comment-panel.md +10 -0
- package/agents/roles/system/frontend-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/growth-marketer/SKILL.md +17 -0
- package/agents/roles/system/growth-marketer/evaluations/cases/08-briefing-de-referidos.md +23 -0
- package/agents/roles/system/growth-marketer/evaluations/expected-behaviors.yaml +1 -0
- package/agents/roles/system/growth-marketer/learning/HISTORY.md +3 -0
- package/agents/roles/system/implementation-manager/SKILL.md +10 -0
- package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook/plan-cutover-tesela-rev-b.md +70 -0
- package/agents/roles/system/implementation-manager/evaluations/cases/08-signed-cutover-runbook.md +12 -0
- package/agents/roles/system/implementation-manager/evaluations/expected-behaviors.yaml +1 -0
- package/agents/roles/system/implementation-manager/learning/HISTORY.md +5 -0
- package/agents/roles/system/integrations-engineer/SKILL.md +6 -4
- package/agents/roles/system/kyc-aml-specialist/SKILL.md +11 -4
- package/agents/roles/system/kyc-aml-specialist/learning/HISTORY.md +2 -0
- package/agents/roles/system/kyc-aml-specialist/learning/sources.yaml +76 -1
- package/agents/roles/system/legal-counsel/SKILL.md +2 -0
- package/agents/roles/system/logistics-operations-manager/SKILL.md +17 -0
- package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo/api-seguimiento-andesexpress.md +40 -0
- package/agents/roles/system/logistics-operations-manager/evaluations/cases/07-entrega-a-tiempo.md +2 -3
- package/agents/roles/system/logistics-operations-manager/learning/HISTORY.md +2 -0
- package/agents/roles/system/machine-learning-engineer/SKILL.md +2 -0
- package/agents/roles/system/mlops-engineer/SKILL.md +2 -0
- package/agents/roles/system/mobile-engineer/SKILL.md +4 -2
- package/agents/roles/system/partnerships-manager/SKILL.md +2 -0
- package/agents/roles/system/people-operations-manager/SKILL.md +2 -0
- package/agents/roles/system/privacy-compliance-specialist/SKILL.md +4 -2
- package/agents/roles/system/procurement-manager/SKILL.md +2 -0
- package/agents/roles/system/product-manager/SKILL.md +2 -0
- package/agents/roles/system/product-marketing-manager/SKILL.md +9 -2
- package/agents/roles/system/product-marketing-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/project-manager/SKILL.md +2 -0
- package/agents/roles/system/qa-engineer/SKILL.md +11 -2
- package/agents/roles/system/qa-engineer/evaluations/cases/07-agent-test-repair.md +1 -1
- package/agents/roles/system/qa-engineer/evaluations/cases/11-accessibility-sampling.md +10 -0
- package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary/resumen-legal-wcag22.md +28 -0
- package/agents/roles/system/qa-engineer/evaluations/cases/12-quoted-from-a-summary.md +10 -0
- package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary/wiki-plataforma-conformidad.md +24 -0
- package/agents/roles/system/qa-engineer/evaluations/cases/13-matrix-from-a-summary.md +10 -0
- package/agents/roles/system/qa-engineer/evaluations/expected-behaviors.yaml +2 -0
- package/agents/roles/system/qa-engineer/learning/HISTORY.md +4 -0
- package/agents/roles/system/qa-engineer/learning/sources.yaml +24 -3
- package/agents/roles/system/release-manager/SKILL.md +5 -3
- package/agents/roles/system/release-manager/evaluations/cases/09-provenance-without-origin.md +12 -0
- package/agents/roles/system/release-manager/learning/HISTORY.md +1 -0
- package/agents/roles/system/release-manager/references/operating-model.md +8 -0
- package/agents/roles/system/sales-representative/SKILL.md +4 -2
- package/agents/roles/system/security-engineer/SKILL.md +4 -2
- package/agents/roles/system/security-engineer/learning/HISTORY.md +1 -0
- package/agents/roles/system/site-reliability-engineer/SKILL.md +4 -2
- package/agents/roles/system/software-architect/SKILL.md +4 -2
- package/agents/roles/system/solutions-engineer/SKILL.md +2 -0
- package/agents/roles/system/tech-lead/SKILL.md +2 -0
- package/agents/roles/system/technical-program-manager/SKILL.md +2 -0
- package/agents/roles/system/technical-writer/SKILL.md +2 -0
- package/agents/roles/system/treasury-analyst/SKILL.md +6 -4
- package/agents/roles/system/ui-designer/SKILL.md +15 -0
- package/agents/roles/system/ui-designer/learning/HISTORY.md +2 -0
- package/agents/roles/system/user-researcher/SKILL.md +6 -0
- package/agents/roles/system/user-researcher/learning/HISTORY.md +1 -0
- package/agents/roles/system/ux-designer/SKILL.md +2 -0
- package/automatization/shared/eval-measured.js +35 -0
- package/automatization/shared/eval-only.js +15 -0
- package/automatization/workflows/README.md +27 -0
- package/automatization/workflows/agent-eval.js +42 -7
- package/automatization/workflows/agent-promote.js +65 -15
- package/automatization/workflows/autobuild.js +52 -8
- package/automatization/workflows/flow-eval.js +27 -7
- package/engine/agents/catalog.js +1 -1
- package/engine/agents/fork.js +1 -1
- package/engine/agents/learning-files.js +7 -2
- package/engine/agents/learning-seal.js +123 -0
- package/engine/agents/learning-sources.js +33 -3
- package/engine/agents/learning.js +80 -87
- package/engine/automation/hooks.js +0 -1
- package/engine/automation/index.js +0 -2
- package/engine/cli/args.js +2 -2
- package/engine/cli/catalog.js +33 -4
- package/engine/cli/instance.js +0 -2
- package/engine/cli/ops.js +2 -18
- package/engine/config/validate.js +7 -2
- package/engine/core/onboarding.js +0 -3
- package/engine/core/scan.js +1 -1
- package/engine/hooks/input.js +1 -1
- package/engine/integrations/proposals.js +1 -1
- package/engine/integrations/providers/jira.js +1 -1
- package/engine/integrations/registry.js +0 -1
- package/engine/planning/business-rules.js +1 -1
- package/engine/planning/contracts.js +8 -1
- package/engine/planning/parser.js +7 -1
- package/engine/schemas/ops-config.schema.json +60 -13
- package/flows/system/incident-review/flow.json +1 -1
- package/flows/system/incident-review/learning/HISTORY.md +1 -0
- package/package.json +4 -3
- 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)
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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()].
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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(
|
|
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(
|
|
353
|
-
return proposeFromRuns(
|
|
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(
|
|
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
|
|
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 }
|
|
@@ -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')
|
package/engine/cli/args.js
CHANGED
|
@@ -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,
|
|
57
|
+
module.exports = { FLAGS, parse }
|
package/engine/cli/catalog.js
CHANGED
|
@@ -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
|
|
148
|
-
// y no
|
|
149
|
-
|
|
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
|
-
|
|
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
|
package/engine/cli/instance.js
CHANGED
|
@@ -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
|
|
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]
|