@ingeniomaps/cauce 0.53.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/automatization/hooks/guard-dependencies.sh +1 -1
  3. package/automatization/hooks/guard-destructive.sh +1 -1
  4. package/automatization/hooks/guard-engine.sh +1 -1
  5. package/automatization/hooks/guard-generated.sh +1 -1
  6. package/automatization/hooks/guard-git-add.sh +1 -1
  7. package/automatization/hooks/guard-governance.sh +1 -1
  8. package/automatization/hooks/guard-integration-snapshot.sh +1 -1
  9. package/automatization/hooks/guard-migrations.sh +1 -1
  10. package/automatization/hooks/guard-planning-drift.sh +1 -1
  11. package/automatization/hooks/guard-secrets.sh +1 -1
  12. package/automatization/hooks/guard-test-evidence.sh +1 -1
  13. package/automatization/hooks/guard-verify.sh +1 -1
  14. package/automatization/hooks/guard-workspace-boundary.sh +1 -1
  15. package/automatization/hooks/run-hook.sh +3 -2
  16. package/automatization/runners/antigravity/hook.js +13 -14
  17. package/automatization/shared/eval-only.js +3 -3
  18. package/automatization/workflows/agent-eval.js +3 -3
  19. package/automatization/workflows/agent-promote.js +4 -4
  20. package/automatization/workflows/autobuild.js +12 -12
  21. package/automatization/workflows/flow-eval.js +10 -7
  22. package/automatization/workflows/flow.js +9 -10
  23. package/automatization/workflows/onboard.js +4 -5
  24. package/engine/agents/evaluations.js +18 -16
  25. package/engine/agents/learning-files.js +111 -0
  26. package/engine/agents/learning-sources.js +166 -0
  27. package/engine/agents/learning.js +93 -300
  28. package/engine/automation/config.js +175 -0
  29. package/engine/automation/hooks.js +96 -0
  30. package/engine/automation/index.js +17 -431
  31. package/engine/automation/roles.js +72 -0
  32. package/engine/automation/runners.js +162 -0
  33. package/engine/cli/catalog.js +7 -12
  34. package/engine/cli/instance.js +11 -10
  35. package/engine/cli/io.js +1 -1
  36. package/engine/cli/ops.js +12 -10
  37. package/engine/cli/wiring.js +2 -4
  38. package/engine/config/validate.js +2 -1
  39. package/engine/core/frontmatter.js +2 -1
  40. package/engine/core/ownership.js +5 -4
  41. package/engine/core/scan.js +3 -0
  42. package/engine/flows/registry.js +2 -2
  43. package/engine/hooks/files.js +160 -0
  44. package/engine/hooks/input.js +129 -0
  45. package/engine/hooks/run.js +24 -449
  46. package/engine/hooks/shell.js +197 -0
  47. package/engine/integrations/registry.js +2 -0
  48. package/engine/planning/contracts.js +12 -10
  49. package/engine/planning/parser.js +4 -3
  50. package/engine/planning/state.js +2 -1
  51. package/package.json +5 -5
@@ -0,0 +1,175 @@
1
+ 'use strict'
2
+
3
+ // Cómo se fusiona y se retira lo nuestro dentro de un archivo del usuario: la configuración del runner
4
+ // y el bloque enmarcado de un `AGENTS.md` compartido. Es una sola pregunta —qué entregamos y cómo se
5
+ // saca sin llevarse lo ajeno— y cambia cuando cambia el formato de esos archivos, no cuando cambia un
6
+ // comando.
7
+
8
+ const fs = require('node:fs')
9
+ const path = require('node:path')
10
+ const F = require('../core/files')
11
+ const { supersededGuards } = require('./hooks')
12
+
13
+ function mergeConfig(current, incoming) {
14
+ if (Array.isArray(incoming)) {
15
+ const values = [...(Array.isArray(current) ? current : []), ...incoming]
16
+ const seen = new Set()
17
+ return values.filter((value) => {
18
+ const key = JSON.stringify(value)
19
+ if (seen.has(key)) return false
20
+ seen.add(key)
21
+ return true
22
+ })
23
+ }
24
+ if (incoming && typeof incoming === 'object') {
25
+ const object = current && typeof current === 'object' && !Array.isArray(current)
26
+ const result = object ? { ...current } : {}
27
+ for (const [key, value] of Object.entries(incoming)) {
28
+ result[key] = mergeConfig(result[key], value)
29
+ }
30
+ return result
31
+ }
32
+ return incoming
33
+ }
34
+
35
+ // Una entrada de hook que puso Cauce se reconoce por el guard al que apunta: `automatization/hooks/`
36
+ // es nuestro y ninguna empresa escribe ahí. Se saca del archivo del usuario antes de fusionar para que
37
+ // el merge deje exactamente las de esta versión, ni una más.
38
+ const DELIVERED = /automatization\/hooks\/guard-[a-z-]+\.sh/
39
+
40
+ function withoutDeliveredHooks(config, live) {
41
+ const dropped = []
42
+ const walk = (node) => {
43
+ if (Array.isArray(node)) {
44
+ return node
45
+ .filter((item) => {
46
+ const command = item && typeof item === 'object' ? String(item.command || '') : ''
47
+ if (!DELIVERED.test(command)) return true
48
+ // Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
49
+ // que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
50
+ if (!live.has(command)) dropped.push(command)
51
+ return false
52
+ })
53
+ .map(walk)
54
+ .filter((item) => !(item && typeof item === 'object' && Array.isArray(item.hooks) && !item.hooks.length))
55
+ }
56
+ if (node && typeof node === 'object') {
57
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, walk(value)]))
58
+ }
59
+ return node
60
+ }
61
+ return { config: walk(config), dropped }
62
+ }
63
+
64
+ // Por qué se fue cada entrada nuestra. Un guard suelto que ahora cubre un grupo no es lo mismo que una
65
+ // ruta que dejó de existir: el primero se ejecutaba dos veces por herramienta —con `verify`, la suite
66
+ // entera del proyecto dos veces por commit—, y el segundo no se ejecutaba nunca. Decirlo distinto es lo
67
+ // único que le permite a alguien darse cuenta de cuál de los dos tenía.
68
+ function reportRemoved(name, dropped, live, output) {
69
+ const wrappers = new Map()
70
+ const loose = []
71
+ for (const command of dropped) {
72
+ const hit = supersededGuards().find(
73
+ (entry) => command.endsWith(entry.file) && [...live].some((v) => v.endsWith(entry.wrapper)),
74
+ )
75
+ if (hit) wrappers.set(hit.wrapper, [...(wrappers.get(hit.wrapper) || []), hit.file])
76
+ else loose.push(command)
77
+ }
78
+ for (const [wrapper, files] of wrappers) {
79
+ output.log(`− ${name}: reemplazado ${[...new Set(files)].join(', ')} por ${wrapper}`)
80
+ }
81
+ for (const command of loose) output.log(`− ${name}: quitada una entrada obsoleta (${command})`)
82
+ }
83
+
84
+ function includesConfig(actual, expected) {
85
+ if (Array.isArray(expected)) {
86
+ return Array.isArray(actual) && expected.every((item) => {
87
+ return actual.some((value) => JSON.stringify(value) === JSON.stringify(item))
88
+ })
89
+ }
90
+ if (expected && typeof expected === 'object') {
91
+ return actual && typeof actual === 'object' && Object.entries(expected).every(([key, value]) => {
92
+ return includesConfig(actual[key], value)
93
+ })
94
+ }
95
+ return actual === expected
96
+ }
97
+
98
+ function hasHooks(config) {
99
+ if (config.hooks && Object.keys(config.hooks).length) return true
100
+ const events = [
101
+ 'PreToolUse',
102
+ 'PostToolUse',
103
+ 'PreInvocation',
104
+ 'PostInvocation',
105
+ 'Stop',
106
+ 'SessionEnd',
107
+ ]
108
+ return Object.values(config).some((entry) => {
109
+ return entry && typeof entry === 'object' && events.some((event) => event in entry)
110
+ })
111
+ }
112
+
113
+ // Quita de una estructura de configuración exactamente lo que este adaptador habría puesto, y nada más.
114
+ // Es el inverso de `mergeConfig`: una entrada del usuario nunca coincide literalmente con la nuestra, así
115
+ // que sobrevive; una que editó tampoco coincide, y por eso se conserva y se avisa en vez de borrarse.
116
+ function unmergeConfig(current, incoming) {
117
+ if (Array.isArray(incoming)) {
118
+ if (!Array.isArray(current)) return current
119
+ const ours = new Set(incoming.map((value) => JSON.stringify(value)))
120
+ return current.filter((value) => !ours.has(JSON.stringify(value)))
121
+ }
122
+ if (incoming && typeof incoming === 'object') {
123
+ if (!current || typeof current !== 'object' || Array.isArray(current)) return current
124
+ const result = { ...current }
125
+ for (const [key, value] of Object.entries(incoming)) {
126
+ if (!(key in result)) continue
127
+ const clean = unmergeConfig(result[key], value)
128
+ // Una clave que queda vacía por habernos ido no es del usuario: la creamos nosotros al instalar.
129
+ const empty = clean === undefined
130
+ || (Array.isArray(clean) && !clean.length)
131
+ || (clean && typeof clean === 'object' && !Array.isArray(clean) && !Object.keys(clean).length)
132
+ if (empty) delete result[key]
133
+ else result[key] = clean
134
+ }
135
+ return result
136
+ }
137
+ return JSON.stringify(current) === JSON.stringify(incoming) ? undefined : current
138
+ }
139
+
140
+ const blockStart = (name) => `<!-- cauce:${name} inicio — lo reescribe "automation install", no editar -->`
141
+ const blockEnd = (name) => `<!-- cauce:${name} fin -->`
142
+
143
+ function isSharedFile(root, target) {
144
+ return path.resolve(target) === path.resolve(root, 'AGENTS.md')
145
+ }
146
+
147
+ function withoutBlock(text, name) {
148
+ const sourceRoot = text.indexOf(blockStart(name))
149
+ if (sourceRoot === -1) return text
150
+ const until = text.indexOf(blockEnd(name), sourceRoot)
151
+ if (until === -1) return text
152
+ return `${text.slice(0, sourceRoot)}${text.slice(until + blockEnd(name).length)}`.trimEnd()
153
+ }
154
+
155
+ function mergeInstruction(file, name, content) {
156
+ const actual = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
157
+ const instructionBody = withoutBlock(actual, name).trimEnd()
158
+ const block = `${blockStart(name)}\n\n${content.trim()}\n\n${blockEnd(name)}\n`
159
+ F.atomicWrite(file, instructionBody ? `${instructionBody}\n\n${block}` : block)
160
+ }
161
+
162
+ function blockUpToDate(file, name, content) {
163
+ if (!fs.existsSync(file)) return false
164
+ const body = fs.readFileSync(file, 'utf8')
165
+ const sourceRoot = body.indexOf(blockStart(name))
166
+ const until = body.indexOf(blockEnd(name))
167
+ if (sourceRoot === -1 || until === -1) return false
168
+ return body.slice(sourceRoot + blockStart(name).length, until).trim() === content.trim()
169
+ }
170
+
171
+ module.exports = {
172
+ blockStart,
173
+ mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
174
+ unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
175
+ }
@@ -0,0 +1,96 @@
1
+ 'use strict'
2
+
3
+ // El wiring de guards: cuáles existen, cuáles corren agrupados y cuáles quedaron registrados de a uno
4
+ // en una instancia vieja. Su reloj es el registro que los ejecuta —un guard nuevo entra por ahí—, no
5
+ // el de los comandos que los instalan.
6
+
7
+ const fs = require('node:fs')
8
+ const path = require('node:path')
9
+ const H = require('../hooks/run')
10
+ const O = require('../core/ownership')
11
+ const M = require('../core/manifest')
12
+ const { packagedAutomation } = require('./runners')
13
+
14
+ // Los guards que el motor implementa, tomados del registro que los ejecuta. Sale de ahí y no de un
15
+ // número escrito a mano: `automation check` anunciaba «11 guards» cuando hacía rato que eran doce, y
16
+ // un conteo que envejece solo es peor que ninguno —dice que revisó menos de lo que revisó—.
17
+ const GUARD_NAMES = Object.keys(H.guards)
18
+
19
+ // Un `.sh` por grupo de más de un guard: es el que registra el runner para correrlos en un proceso.
20
+ function groupWrappers() {
21
+ return Object.entries(H.hookGroups)
22
+ .filter(([, names]) => names.length > 1)
23
+ .map(([group]) => [group, `guard-${group.replace('pre-', '')}.sh`])
24
+ }
25
+
26
+ // Los scripts que la instancia debe tener, derivados del registro que los ejecuta en vez de copiados
27
+ // a mano: la copia envejecía sin avisar, porque un guard nuevo del motor no entraba en la cuenta.
28
+ //
29
+ // Se comprueba en una sola dirección a propósito. Un `.sh` que no está acá no sobra: así es como una
30
+ // empresa agrega el suyo —`guard-acme.sh` al lado de los nuestros—, que es lo que `upgrade` le dice
31
+ // que haga y lo único que sobrevive a cada actualización.
32
+ function expectedHooks() {
33
+ return [
34
+ 'run-hook.sh',
35
+ ...groupWrappers().map(([, wrapper]) => wrapper),
36
+ ...GUARD_NAMES.map((name) => `guard-${name}.sh`),
37
+ ]
38
+ }
39
+
40
+ // Guards que hoy viven dentro de un grupo, con el wrapper que los reemplaza.
41
+ function supersededGuards() {
42
+ const entries = []
43
+ for (const [group, wrapper] of groupWrappers()) {
44
+ for (const name of H.hookGroups[group]) entries.push({ file: `guard-${name}.sh`, wrapper })
45
+ }
46
+ return entries
47
+ }
48
+
49
+ // Wiring heredado: guards que ahora corren agrupados pero siguen registrados uno por uno.
50
+ // Conviven sin romper nada, a costa de ejecutar el guard dos veces por herramienta.
51
+ function legacyGuardWiring(config) {
52
+ const text = JSON.stringify(config)
53
+ const superseded = []
54
+ for (const [group, names] of Object.entries(H.hookGroups)) {
55
+ if (names.length < 2 || !text.includes(`guard-${group.replace('pre-', '')}.sh`)) continue
56
+ superseded.push(...names.filter((guard) => text.includes(`guard-${guard}.sh`)))
57
+ }
58
+ return superseded
59
+ }
60
+
61
+ // Guards de la instancia que ya no coinciden con los del paquete. Existir y ser ejecutable no
62
+ // alcanza: un guard viejo no falla, deja de proteger sin decir nada. La instancia declaraba una
63
+ // versión y nadie comprobaba que su runtime fuera realmente esa.
64
+ function staleHooks(root) {
65
+ const packaged = packagedAutomation(root)
66
+ if (!packaged) return []
67
+ const shipped = path.join(packaged, 'hooks')
68
+ const mine = path.join(root, 'automatization', 'hooks')
69
+ const recorded = M.read(root)
70
+ const stale = []
71
+ let names = []
72
+ try { names = fs.readdirSync(shipped) } catch { return [] }
73
+ for (const file of names) {
74
+ const local = path.join(mine, file)
75
+ // Los que faltan ya los reporta el chequeo de arriba; acá sólo interesa el que quedó atrás.
76
+ if (!fs.existsSync(local)) continue
77
+ const current = M.digest(local)
78
+ if (current === M.digest(path.join(shipped, file))) continue
79
+ const delivered = recorded[`automatization/hooks/${file}`]
80
+ stale.push({ file, edited: Boolean(delivered) && delivered !== current })
81
+ }
82
+ return stale
83
+ }
84
+
85
+ function listHooks(output = console) {
86
+ const nameWidth = Math.max(...H.hookMetadata.map((hook) => hook.name.length))
87
+ const eventWidth = Math.max(...H.hookMetadata.map((hook) => hook.event.length))
88
+ for (const hook of H.hookMetadata) {
89
+ output.log(`${hook.name.padEnd(nameWidth)} ${hook.event.padEnd(eventWidth)} ${hook.purpose}`)
90
+ }
91
+ }
92
+
93
+ module.exports = {
94
+ GUARD_NAMES, groupWrappers, expectedHooks, supersededGuards,
95
+ legacyGuardWiring, staleHooks, listHooks,
96
+ }