@ingeniomaps/cauce 0.53.2 → 0.55.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 +86 -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/config.js +43 -11
- package/engine/automation/hooks.js +0 -1
- package/engine/automation/index.js +19 -4
- package/engine/cli/args.js +2 -2
- package/engine/cli/catalog.js +33 -4
- package/engine/cli/instance.js +43 -12
- package/engine/cli/ops.js +16 -21
- 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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const fs = require('node:fs')
|
|
9
9
|
const path = require('node:path')
|
|
10
10
|
const F = require('../core/files')
|
|
11
|
-
const { supersededGuards } = require('./hooks')
|
|
11
|
+
const { supersededGuards, expectedHooks } = require('./hooks')
|
|
12
12
|
|
|
13
13
|
function mergeConfig(current, incoming) {
|
|
14
14
|
if (Array.isArray(incoming)) {
|
|
@@ -32,19 +32,40 @@ function mergeConfig(current, incoming) {
|
|
|
32
32
|
return incoming
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta
|
|
36
|
-
//
|
|
37
|
-
// el
|
|
38
|
-
|
|
35
|
+
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta, y lo nuestro es lo que
|
|
36
|
+
// efectivamente entregamos, no todo lo que vive en nuestra carpeta. Reconocer por directorio
|
|
37
|
+
// desregistraba el guard propio del proyecto en cada reinstalación: `template/AGENTS.md` invita a
|
|
38
|
+
// ponerlo justo ahí y promete que sobrevive a cada actualización, el archivo quedaba en disco, y lo
|
|
39
|
+
// único que se veía era una línea diciendo que se había quitado una entrada obsoleta.
|
|
40
|
+
// Se saca del archivo del usuario antes de fusionar para que el merge deje exactamente las de esta
|
|
41
|
+
// versión, ni una más.
|
|
42
|
+
const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
// Dos fuentes, y la segunda es la que cierra el borde. `expectedHooks()` dice qué entrega el motor de
|
|
45
|
+
// hoy y alcanza para una instancia que nunca instaló; `delivered` es lo que esta instancia registró
|
|
46
|
+
// haber recibido la última vez, y es lo único que reconoce un guard que entregamos en una versión y
|
|
47
|
+
// retiramos en la siguiente: su nombre ya no está en la lista de hoy, así que sin el registro quedaría
|
|
48
|
+
// vivo en la configuración llamando a un guard que Cauce ya no mantiene. Un guard propio del proyecto
|
|
49
|
+
// no está en ninguna de las dos, que es exactamente por lo que sobrevive.
|
|
50
|
+
function isDelivered(command, delivered) {
|
|
51
|
+
const hit = String(command).match(HOOK_PATH)
|
|
52
|
+
if (!hit) return false
|
|
53
|
+
return expectedHooks().includes(hit[1]) || delivered.has(String(command))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Lo que de un conjunto de comandos es wiring de guards nuestro, para anotarlo como entregado.
|
|
57
|
+
function deliveredHookCommands(commands) {
|
|
58
|
+
return [...commands].filter((command) => HOOK_PATH.test(String(command)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withoutDeliveredHooks(config, live, delivered = new Set()) {
|
|
41
62
|
const dropped = []
|
|
42
63
|
const walk = (node) => {
|
|
43
64
|
if (Array.isArray(node)) {
|
|
44
65
|
return node
|
|
45
66
|
.filter((item) => {
|
|
46
67
|
const command = item && typeof item === 'object' ? String(item.command || '') : ''
|
|
47
|
-
if (!
|
|
68
|
+
if (!isDelivered(command, delivered)) return true
|
|
48
69
|
// Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
|
|
49
70
|
// que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
|
|
50
71
|
if (!live.has(command)) dropped.push(command)
|
|
@@ -83,9 +104,20 @@ function reportRemoved(name, dropped, live, output) {
|
|
|
83
104
|
|
|
84
105
|
function includesConfig(actual, expected) {
|
|
85
106
|
if (Array.isArray(expected)) {
|
|
86
|
-
return Array.isArray(actual) && expected.every((item) => {
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
return Array.isArray(actual) && expected.every((item) => actual.some((value) => {
|
|
108
|
+
// Un grupo de hooks se compara por su contenido, no por su forma serializada. La comprobación es
|
|
109
|
+
// «contiene», que es lo correcto, pero con el ítem entero como unidad un grupo `{matcher, hooks}`
|
|
110
|
+
// con un hook de más dejaba de ser el mismo objeto y contaba como ausente: `doctor` llamaba
|
|
111
|
+
// divergente a una configuración que tenía todo lo esperado, y el anidamiento —donde vive la
|
|
112
|
+
// diferencia real— no se miraba nunca. La instalación promete conservar lo que el proyecto ya
|
|
113
|
+
// tenía, así que sumar un hook propio a un grupo nuestro es exactamente lo que permite.
|
|
114
|
+
if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
|
|
115
|
+
return value && typeof value === 'object'
|
|
116
|
+
&& value.matcher === item.matcher
|
|
117
|
+
&& includesConfig(value.hooks, item.hooks)
|
|
118
|
+
}
|
|
119
|
+
return JSON.stringify(value) === JSON.stringify(item)
|
|
120
|
+
}))
|
|
89
121
|
}
|
|
90
122
|
if (expected && typeof expected === 'object') {
|
|
91
123
|
return actual && typeof actual === 'object' && Object.entries(expected).every(([key, value]) => {
|
|
@@ -170,6 +202,6 @@ function blockUpToDate(file, name, content) {
|
|
|
170
202
|
|
|
171
203
|
module.exports = {
|
|
172
204
|
blockStart,
|
|
173
|
-
mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
|
|
205
|
+
mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
|
|
174
206
|
unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
|
|
175
207
|
}
|
|
@@ -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')
|
|
@@ -19,7 +17,7 @@ const {
|
|
|
19
17
|
legacyGuardWiring, staleHooks, listHooks,
|
|
20
18
|
} = require('./hooks')
|
|
21
19
|
const {
|
|
22
|
-
blockStart, mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
|
|
20
|
+
blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
|
|
23
21
|
unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
|
|
24
22
|
} = require('./config')
|
|
25
23
|
|
|
@@ -228,6 +226,11 @@ function deliveryKey(name, target) {
|
|
|
228
226
|
return `${name}/${target.split(path.sep).join('/')}`
|
|
229
227
|
}
|
|
230
228
|
|
|
229
|
+
// La sección `runners` del manifiesto anota archivo → digest; ésta es la única entrada que guarda una
|
|
230
|
+
// lista, porque lo que registra no es un archivo sino las entradas que dejamos dentro del archivo de
|
|
231
|
+
// configuración del usuario. No colisiona con ningún `target`: ninguno se llama así.
|
|
232
|
+
const HOOKS_KEY = 'config.hooks'
|
|
233
|
+
|
|
231
234
|
// En qué estado quedó un archivo que este adaptador entregó alguna vez.
|
|
232
235
|
//
|
|
233
236
|
// nuevo no existe todavía
|
|
@@ -279,6 +282,7 @@ function uninstall(root, name, output = console) {
|
|
|
279
282
|
|
|
280
283
|
const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
|
|
281
284
|
const deliveredPaths = { ...recorded }
|
|
285
|
+
delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
|
|
282
286
|
for (const item of items) {
|
|
283
287
|
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
284
288
|
const key = deliveryKey(name, item.target)
|
|
@@ -380,7 +384,12 @@ function install(root, name, output = console, options = {}) {
|
|
|
380
384
|
// guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
|
|
381
385
|
const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
|
|
382
386
|
.map((entry) => JSON.parse(`{${entry}}`).command))
|
|
383
|
-
|
|
387
|
+
// Lo que este adaptador anotó haber entregado la última vez. Es lo único que reconoce una entrada
|
|
388
|
+
// nuestra cuyo guard el motor ya no trae: por nombre no se distingue de la que agregó el proyecto.
|
|
389
|
+
const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
|
|
390
|
+
const clean = runner.config.owned
|
|
391
|
+
? { config: {}, dropped: [] }
|
|
392
|
+
: withoutDeliveredHooks(current, live, previous)
|
|
384
393
|
reportRemoved(name, clean.dropped, live, output)
|
|
385
394
|
F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
|
|
386
395
|
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
@@ -390,6 +399,12 @@ function install(root, name, output = console, options = {}) {
|
|
|
390
399
|
}
|
|
391
400
|
output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
|
|
392
401
|
const deliveredPaths = { ...recorded }
|
|
402
|
+
// Qué wiring de guards dejamos puesto, para poder retirarlo el día que el motor deje de traerlo. Se
|
|
403
|
+
// anota lo que instalamos y no lo que quedó en el archivo: ahí conviven las entradas del proyecto, y
|
|
404
|
+
// registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
|
|
405
|
+
const ourHooks = deliveredHookCommands(live)
|
|
406
|
+
if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
|
|
407
|
+
else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
|
|
393
408
|
for (const resolved of resolvedItems) {
|
|
394
409
|
const status = state.get(resolved)
|
|
395
410
|
const ownFile = runner.instructions.includes(resolved.item)
|
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
|
|
|
@@ -31,9 +29,14 @@ function providerNames() {
|
|
|
31
29
|
} catch { return [] }
|
|
32
30
|
}
|
|
33
31
|
|
|
32
|
+
// Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
|
|
33
|
+
// un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
|
|
34
|
+
// el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
|
|
35
|
+
// que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
|
|
34
36
|
function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
|
|
35
37
|
F.assertNoSymlinkPath(path.dirname(target), target)
|
|
36
38
|
fs.mkdirSync(target, { recursive: true })
|
|
39
|
+
const preserved = {}
|
|
37
40
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
38
41
|
if (skip.includes(entry.name)) continue
|
|
39
42
|
const from = path.join(source, entry.name)
|
|
@@ -41,11 +44,14 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
41
44
|
// 11.16.0: lo deja afuera en la raíz y en subdirectorios—, así que viaja sin punto y se restituye
|
|
42
45
|
// acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
|
|
43
46
|
const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
|
|
44
|
-
if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
|
|
47
|
+
if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
|
|
45
48
|
else {
|
|
46
49
|
if (fs.existsSync(to)) {
|
|
47
50
|
if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
|
|
48
51
|
if (!quiet) console.log(`= conservado ${to}`)
|
|
52
|
+
let would = fs.readFileSync(from, 'utf8')
|
|
53
|
+
for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
|
|
54
|
+
preserved[to] = M.digestText(would)
|
|
49
55
|
continue
|
|
50
56
|
}
|
|
51
57
|
let content = fs.readFileSync(from, 'utf8')
|
|
@@ -55,22 +61,29 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
55
61
|
if (!quiet) console.log(`+ ${to}`)
|
|
56
62
|
}
|
|
57
63
|
}
|
|
64
|
+
return preserved
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
// Devuelve lo conservado igual que `copyTemplate`, y por la misma razón: acá el runtime no lleva
|
|
68
|
+
// reemplazos, así que lo que habríamos escrito es el archivo del paquete tal cual.
|
|
60
69
|
function copyRuntime(source, target, preserve = false, boundary = target, skip = []) {
|
|
61
70
|
F.assertNoSymlinkPath(boundary, target)
|
|
62
71
|
fs.mkdirSync(target, { recursive: true })
|
|
72
|
+
const preserved = {}
|
|
63
73
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
64
74
|
if (skip.includes(entry.name)) continue
|
|
65
75
|
const from = path.join(source, entry.name)
|
|
66
76
|
const to = path.join(target, entry.name)
|
|
67
|
-
if (entry.isDirectory()) copyRuntime(from, to, preserve, boundary, skip)
|
|
68
|
-
else if (preserve && fs.existsSync(to))
|
|
69
|
-
|
|
77
|
+
if (entry.isDirectory()) Object.assign(preserved, copyRuntime(from, to, preserve, boundary, skip))
|
|
78
|
+
else if (preserve && fs.existsSync(to)) {
|
|
79
|
+
console.log(`= conservado ${to}`)
|
|
80
|
+
preserved[to] = M.digest(from)
|
|
81
|
+
} else {
|
|
70
82
|
F.assertNoSymlinkPath(boundary, to)
|
|
71
83
|
fs.copyFileSync(from, to)
|
|
72
84
|
}
|
|
73
85
|
}
|
|
86
|
+
return preserved
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
// Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
|
|
@@ -118,7 +131,7 @@ function undeclareEngine(manifest) {
|
|
|
118
131
|
// el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
|
|
119
132
|
// cómo venga escrita la línea de comandos.
|
|
120
133
|
function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
121
|
-
copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
|
|
134
|
+
const preserved = copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
|
|
122
135
|
'{{PROJECT_NAME}}': name,
|
|
123
136
|
'{{MODE}}': mode,
|
|
124
137
|
'{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
|
|
@@ -126,12 +139,12 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
126
139
|
// No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
|
|
127
140
|
// el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
|
|
128
141
|
// `.github/workflows/` vacío en cada instancia.
|
|
129
|
-
copyRuntime(
|
|
142
|
+
Object.assign(preserved, copyRuntime(
|
|
130
143
|
path.join(PROJECT_ROOT, 'automatization', 'hooks'),
|
|
131
144
|
path.join(root, 'automatization', 'hooks'),
|
|
132
145
|
force,
|
|
133
146
|
root,
|
|
134
|
-
)
|
|
147
|
+
))
|
|
135
148
|
const version = require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
136
149
|
// El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
|
|
137
150
|
// declarar npm acá no convierte en Node al servicio de Go de al lado.
|
|
@@ -142,6 +155,14 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
142
155
|
if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
|
|
143
156
|
}
|
|
144
157
|
deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
|
|
158
|
+
// Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
|
|
159
|
+
// justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
|
|
160
|
+
// archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
|
|
161
|
+
// rastrea, y un archivo propio fuera de esa frontera no le incumbe.
|
|
162
|
+
for (const [file, hash] of Object.entries(preserved)) {
|
|
163
|
+
const relative = path.relative(root, file).split(path.sep).join('/')
|
|
164
|
+
if (relative in deliveredPaths) deliveredPaths[relative] = hash
|
|
165
|
+
}
|
|
145
166
|
M.write(root, deliveredPaths)
|
|
146
167
|
// La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
|
|
147
168
|
const configFile = path.join(root, 'ops.config.json')
|
|
@@ -307,12 +328,20 @@ function upgrade(dir, cli) {
|
|
|
307
328
|
const overrides = O.overrides(root)
|
|
308
329
|
|
|
309
330
|
if (dry) {
|
|
331
|
+
// Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
|
|
332
|
+
// —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
|
|
333
|
+
// útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
|
|
334
|
+
// que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
|
|
335
|
+
// la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
|
|
336
|
+
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
310
337
|
if (from === to) {
|
|
311
338
|
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
312
339
|
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
313
340
|
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
314
341
|
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
315
|
-
|
|
342
|
+
console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
|
|
343
|
+
if (changed.length) process.exit(1)
|
|
344
|
+
return
|
|
316
345
|
}
|
|
317
346
|
// Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
|
|
318
347
|
// una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
|
|
@@ -324,7 +353,6 @@ function upgrade(dir, cli) {
|
|
|
324
353
|
console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
|
|
325
354
|
printChangelog(from, to)
|
|
326
355
|
}
|
|
327
|
-
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
328
356
|
process.exit(1)
|
|
329
357
|
}
|
|
330
358
|
|
|
@@ -429,7 +457,10 @@ function upgrade(dir, cli) {
|
|
|
429
457
|
for (const override of overrides) {
|
|
430
458
|
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
431
459
|
}
|
|
432
|
-
|
|
460
|
+
// Sólo cuando es cierto. Llegar acá con algo en `changed` significa que se corrió con --force y se
|
|
461
|
+
// descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
|
|
462
|
+
// todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
|
|
463
|
+
if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
433
464
|
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
434
465
|
// decide una persona.
|
|
435
466
|
if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
|
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')
|
|
@@ -37,8 +21,14 @@ const DEFAULT_TARGET = 'ops'
|
|
|
37
21
|
// de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
|
|
38
22
|
// instancia; no hay una segunda adentro.
|
|
39
23
|
function implicitTarget(cwd) {
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// La misma heurística, aplicada al destino ya resuelto: una carpeta que ya nombra al toolkit **es** la
|
|
28
|
+
// instancia, y el código de la empresa vive al lado.
|
|
29
|
+
function isInstanceDir(dir) {
|
|
30
|
+
const base = path.basename(dir)
|
|
31
|
+
return base === DEFAULT_TARGET || base.endsWith('-ops')
|
|
42
32
|
}
|
|
43
33
|
|
|
44
34
|
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
@@ -89,7 +79,12 @@ async function init(target, cli) {
|
|
|
89
79
|
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
90
80
|
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
91
81
|
const root = path.resolve(target || implicitTarget(process.cwd()))
|
|
92
|
-
|
|
82
|
+
// El modo lo decide a dónde apunta el destino, no si alguien escribió el argumento. `init` e `init .`
|
|
83
|
+
// resuelven el mismo directorio y elegían modos opuestos: escribir el punto —la forma más natural de
|
|
84
|
+
// decir «acá»— daba embedded, y ahí la raíz del workspace pasa a ser la carpeta ops misma, así que
|
|
85
|
+
// `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
|
|
86
|
+
// adentro en vez de al lado, donde el dev abre su herramienta.
|
|
87
|
+
const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
|
|
93
88
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
94
89
|
const name = cli.value('--name', defaultName(root))
|
|
95
90
|
const force = cli.has('--force')
|
|
@@ -170,7 +165,7 @@ function usage() {
|
|
|
170
165
|
ops automation doctor <ops-root> claude|codex|gemini|antigravity
|
|
171
166
|
ops automation install <ops-root> claude|codex|gemini|antigravity
|
|
172
167
|
ops automation uninstall <ops-root> claude|codex|gemini|antigravity
|
|
173
|
-
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied [--period <AAAA-MM>]]
|
|
168
|
+
ops learn <agent|flow> [--flow] [--proposal [--period <AAAA-MM>]] [--applied|--archived [--period <AAAA-MM>]]
|
|
174
169
|
ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
|
|
175
170
|
ops agents list [ops-root] [--own|--system] [--json]
|
|
176
171
|
ops agents fork <cargo> [ops-root]
|
|
@@ -45,10 +45,15 @@ function validateWorkspaces(workspaces, errors) {
|
|
|
45
45
|
continue
|
|
46
46
|
}
|
|
47
47
|
for (const key of Object.keys(workspace)) {
|
|
48
|
-
if (!['name', 'path'].includes(key)) {
|
|
48
|
+
if (!['name', 'path', 'verify'].includes(key)) {
|
|
49
49
|
errors.push(`ops.config.json: workspaceRoots[${index}].${key} no está permitido`)
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
// Opcional: la raíz que no lo declara deja que quien verifica descubra la puerta, como siempre.
|
|
53
|
+
// Declararlo vacío es peor que no declararlo —promete una puerta y no la da—, así que se rechaza.
|
|
54
|
+
if ('verify' in workspace && (typeof workspace.verify !== 'string' || !workspace.verify.trim())) {
|
|
55
|
+
errors.push(`ops.config.json: workspaceRoots[${index}].verify debe ser el comando, o no estar`)
|
|
56
|
+
}
|
|
52
57
|
if (typeof workspace.name !== 'string' || !workspace.name.trim()) {
|
|
53
58
|
errors.push(`ops.config.json: workspaceRoots[${index}].name es obligatorio`)
|
|
54
59
|
}
|
|
@@ -78,4 +83,4 @@ function validateRunner(runner, errors) {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
module.exports = {
|
|
86
|
+
module.exports = { validateOpsConfig }
|
package/engine/core/scan.js
CHANGED
package/engine/hooks/input.js
CHANGED
|
@@ -124,6 +124,6 @@ function findOpsRoot(start) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
module.exports = {
|
|
127
|
-
readInput, commandOf,
|
|
127
|
+
readInput, commandOf, patchOf, filesOf, contentOf, cwdOf, block, configOf,
|
|
128
128
|
gitDirectory, isCommit, stagedFiles, pushAllowed, findOpsRoot,
|
|
129
129
|
}
|
|
@@ -161,4 +161,4 @@ async function fetchItems(config, options = {}) {
|
|
|
161
161
|
return issues.map((issue) => normalizeIssue(issue, config))
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
module.exports = {
|
|
164
|
+
module.exports = { fetchItems, normalizeFixture, validateConfig }
|
|
@@ -297,6 +297,14 @@ function validateState({ epics, milestones, done, wip, roles = new Set(), humanA
|
|
|
297
297
|
for (const milestone of milestones) {
|
|
298
298
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(milestone.slug)) errors.push(`hito con slug inválido: ${milestone.slug}`)
|
|
299
299
|
if (milestoneSlugs.has(milestone.slug)) errors.push(`hito duplicado: ${milestone.slug}`)
|
|
300
|
+
// La misma puerta que la épica y la tarea, en el nivel que no la tenía. Un hito agrupa trabajo bajo
|
|
301
|
+
// un nombre, y abrirlo con el nombre puesto para después es el camino natural: nadie promueve una
|
|
302
|
+
// tarea sin aceptación, y en cambio el título del hito se decide al final, cuando ya se construyó
|
|
303
|
+
// debajo. El marcador es lo único que se puede juzgar sin leer — un título vago sigue pasando, y
|
|
304
|
+
// eso es deliberado: acá también un heurístico sobre prosa se equivocaría en los dos sentidos.
|
|
305
|
+
if (PLACEHOLDERS.test(milestone.title)) {
|
|
306
|
+
errors.push(`hito ${milestone.slug}: el título no está decidido — "${milestone.title}"`)
|
|
307
|
+
}
|
|
300
308
|
milestoneSlugs.add(milestone.slug)
|
|
301
309
|
for (const task of milestone.tasks) {
|
|
302
310
|
if (!task.service) errors.push(`BACKLOG ${task.slug}: falta (service: <ruta>)`)
|
|
@@ -406,7 +414,6 @@ function oversizedUnits({ epics = [], milestones = [] }) {
|
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
module.exports = {
|
|
409
|
-
testedCriteria,
|
|
410
417
|
validateState,
|
|
411
418
|
oversizedUnits,
|
|
412
419
|
validateAdr,
|
|
@@ -153,6 +153,12 @@ function readCast(rest) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// El texto de la aceptación, delimitado por las cursivas que lo envuelven. El cierre no es «el
|
|
157
|
+
// próximo guión bajo»: un identificador como `MAX_ATTEMPTS` trae los suyos, y cortar en el primero
|
|
158
|
+
// devolvía `MAX` con `check` en verde, que es la forma cara del error —la tarea se lee completa y no
|
|
159
|
+
// lo está—. Cierra el `_` que markdown cerraría: el que no está entre caracteres de palabra.
|
|
160
|
+
const ACCEPTANCE = /_Aceptaci[oó]n:\s*(.*?\S)_(?![A-Za-z0-9])/i
|
|
161
|
+
|
|
156
162
|
function readBacklog(dir) {
|
|
157
163
|
const text = withoutComments(read(path.join(dir, 'BACKLOG.md')))
|
|
158
164
|
const milestones = []
|
|
@@ -175,7 +181,7 @@ function readBacklog(dir) {
|
|
|
175
181
|
slug: task[1].trim(), tier: task[2] || '', cast: readCast(rest),
|
|
176
182
|
epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
|
|
177
183
|
service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
|
|
178
|
-
acceptance: ((rest.match(
|
|
184
|
+
acceptance: ((rest.match(ACCEPTANCE) || [])[1] || '').trim(),
|
|
179
185
|
criteria: criteriaRefs(rest),
|
|
180
186
|
noSplit: noSplitReason(rest),
|
|
181
187
|
})
|