@saulwade/swl-ses 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CLAUDE.md +225 -209
  2. package/README.md +578 -561
  3. package/agentes/arquitecto-swl.md +33 -1
  4. package/agentes/nemesis-auditor-swl.md +59 -19
  5. package/bin/swl-mcp-server.js +214 -214
  6. package/bin/swl-ses.js +49 -7
  7. package/comandos/swl/.evolved.json +22 -22
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/nemesis.md +230 -56
  10. package/gateway/lib/event-channel.js +191 -191
  11. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  12. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  13. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  14. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  15. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  16. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  17. package/habilidades/eval-framework/SKILL.md +212 -212
  18. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  19. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  20. package/habilidades/harness-claude-code/SKILL.md +299 -299
  21. package/habilidades/infra-github-actions/SKILL.md +166 -166
  22. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  23. package/habilidades/manejo-errores/.evolved.json +8 -8
  24. package/habilidades/meta-skills-estandar/SKILL.md +207 -4
  25. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  26. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  27. package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
  28. package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
  29. package/habilidades/node-experto/SKILL.md +94 -4
  30. package/habilidades/patrones-python/SKILL.md +229 -229
  31. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  32. package/habilidades/planear-fase/SKILL.md +319 -319
  33. package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
  34. package/habilidades/release-semver/.evolved.json +8 -8
  35. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  36. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  37. package/habilidades/tdd-workflow/SKILL.md +121 -4
  38. package/habilidades/testing-python/SKILL.md +340 -340
  39. package/habilidades/web-fetcher-routing/SKILL.md +75 -75
  40. package/hooks/check-update.js +31 -3
  41. package/hooks/claudemd-bloat-detector.js +161 -161
  42. package/hooks/extraccion-aprendizajes.js +11 -0
  43. package/hooks/lib/agent-routing.js +107 -107
  44. package/hooks/lib/auto-consolidator.js +335 -335
  45. package/hooks/lib/error-classifier.js +308 -308
  46. package/hooks/lib/merkle-audit.js +96 -96
  47. package/hooks/lib/provenance-tracker.js +191 -191
  48. package/hooks/lib/rate-limit-tracker.js +253 -253
  49. package/hooks/lib/resource-quota.js +122 -122
  50. package/hooks/lib/retry-jitter.js +165 -165
  51. package/hooks/lib/security-net.js +201 -201
  52. package/hooks/lib/skill-auditor.js +588 -588
  53. package/hooks/lib/sync-status.js +228 -228
  54. package/hooks/lib/taint-tracker.js +107 -107
  55. package/hooks/lib/text-similarity.js +241 -241
  56. package/hooks/lib/toon-compressor.js +245 -245
  57. package/hooks/registro-turnos.js +209 -209
  58. package/hooks/sugerir-regenerar-inventario.js +170 -170
  59. package/hooks/validar-formato-post-subagente.js +140 -140
  60. package/hooks/validar-memoria-hook.js +218 -218
  61. package/instintos/prompt-appendices.yaml +57 -57
  62. package/manifiestos/agent-output-schemas.json +57 -57
  63. package/manifiestos/modulos.json +1324 -1321
  64. package/manifiestos/skills-lock.json +1142 -1114
  65. package/package.json +5 -4
  66. package/plantillas/auditor-veto-template.md +105 -105
  67. package/plantillas/github-workflows/README.md +47 -47
  68. package/plantillas/github-workflows/release-please.yml +44 -44
  69. package/plantillas/github-workflows/swl-ci.yml +107 -107
  70. package/plantillas/github-workflows/swl-security.yml +51 -51
  71. package/plugin.json +355 -351
  72. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  73. package/reglas/arreglar-al-detectar.md +147 -147
  74. package/reglas/fragmentos-compartidos.md +152 -152
  75. package/reglas/harness-claude-code.md +213 -213
  76. package/reglas/registro-componentes-nuevos.md +192 -0
  77. package/reglas/usar-context7.md +226 -226
  78. package/schemas/diary-entry.schema.json +80 -80
  79. package/scripts/actualizar.js +110 -1
  80. package/scripts/audit-tools/audit-history.js +330 -330
  81. package/scripts/audit-tools/bundle-tracker.js +290 -290
  82. package/scripts/audit-tools/canary-monitor.js +352 -352
  83. package/scripts/audit-tools/code-profiler.js +605 -605
  84. package/scripts/audit-tools/dep-doctor.js +320 -320
  85. package/scripts/audit-tools/env-validator.js +206 -206
  86. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  87. package/scripts/audit-tools/lib/output.js +23 -23
  88. package/scripts/audit-tools/migration-checker.js +392 -392
  89. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  90. package/scripts/benchmark-memoria.js +167 -167
  91. package/scripts/configurar-branch-protection.js +418 -418
  92. package/scripts/derivar-feature-list.js +489 -489
  93. package/scripts/desinstalar.js +105 -24
  94. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  95. package/scripts/doctor.js +27 -0
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/instalador.js +55 -4
  101. package/scripts/lib/artefactos-python.js +43 -43
  102. package/scripts/lib/benchmark-metrics.js +160 -160
  103. package/scripts/lib/budget-enforcer.js +252 -252
  104. package/scripts/lib/configurar-ci.js +380 -380
  105. package/scripts/lib/contadores-inventario.js +217 -217
  106. package/scripts/lib/detectar-stack-detallado.js +307 -307
  107. package/scripts/lib/diary-entry.js +234 -234
  108. package/scripts/lib/eval-metrics-store.js +218 -218
  109. package/scripts/lib/eval-quality.js +171 -171
  110. package/scripts/lib/eval-schemas.js +144 -144
  111. package/scripts/lib/eval-self-correct.js +106 -106
  112. package/scripts/lib/eval-validator.js +185 -185
  113. package/scripts/lib/expandir-targets.js +71 -71
  114. package/scripts/lib/jaccard-similarity.js +98 -98
  115. package/scripts/lib/longmemeval-runner.js +125 -125
  116. package/scripts/lib/mcp_config.py +127 -0
  117. package/scripts/lib/npm-version.js +261 -261
  118. package/scripts/lib/paquetes-conocidos.js +50 -50
  119. package/scripts/lib/parsear-opciones.js +3 -0
  120. package/scripts/lib/prompt-builder.js +264 -264
  121. package/scripts/lib/rrf-fusion.js +175 -175
  122. package/scripts/lib/scoring-instintos.js +277 -277
  123. package/scripts/lib/semantic-search.js +252 -252
  124. package/scripts/lib/toml-merge.js +204 -204
  125. package/scripts/lib/transformadores/codex.js +375 -375
  126. package/scripts/lib/transformadores/cursor.js +359 -359
  127. package/scripts/lib/ui.js +148 -22
  128. package/scripts/limpiar-artefactos-python.js +131 -131
  129. package/scripts/mcp-orchestrator.py +8 -18
  130. package/scripts/mcp-pool-manager.py +12 -23
  131. package/scripts/mcp-server/README.md +170 -170
  132. package/scripts/mcp-server/auth.js +105 -105
  133. package/scripts/mcp-server/cache.js +106 -106
  134. package/scripts/mcp-server/telemetry.js +78 -78
  135. package/scripts/migrar-csv-a-array.js +168 -168
  136. package/scripts/migrar-fase-dominio.js +201 -201
  137. package/scripts/publicar.js +511 -511
  138. package/scripts/run-eval.js +141 -141
  139. package/scripts/tui/componentes/selector-multi.js +189 -0
  140. package/scripts/tui/componentes/selector-unico.js +158 -0
  141. package/scripts/tui/ejecutores.js +375 -0
  142. package/scripts/tui/index.js +162 -0
  143. package/scripts/tui/lib/colores.js +129 -0
  144. package/scripts/tui/lib/render.js +264 -0
  145. package/scripts/tui/lib/teclas.js +113 -0
  146. package/scripts/tui/pantallas/inspect.js +173 -0
  147. package/scripts/tui/pantallas/install-wizard.js +334 -0
  148. package/scripts/tui/pantallas/menu-principal.js +52 -0
  149. package/scripts/tui/pantallas/progreso.js +274 -0
  150. package/scripts/tui/pantallas/resumen.js +132 -0
  151. package/scripts/tui/pantallas/uninstall-wizard.js +208 -0
  152. package/scripts/tui/pantallas/update-wizard.js +232 -0
  153. package/scripts/tui/pantallas/welcome.js +187 -0
  154. package/scripts/validar-userland-vacio.js +110 -110
  155. package/scripts/verificar-docs-vs-codigo.js +425 -0
@@ -1,170 +1,170 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: sugerir-regenerar-inventario.js
6
- * Tipo: PostToolUse (aplica a: Write, Edit, MultiEdit)
7
- *
8
- * Cuando el agente edita o crea un componente registrado en INVENTARIO.md
9
- * (agentes, habilidades, comandos, hooks o reglas), sugiere — sin bloquear —
10
- * regenerar el inventario antes del próximo commit.
11
- *
12
- * Motivado por el drift recurrente detectado el 2026-04-29: tras agregar el
13
- * skill `harness-claude-code` el inventario quedó stale en 5 archivos canónicos
14
- * porque nadie regeneró `INVENTARIO.md` después de la edición. La regeneración
15
- * es 1 comando (`node scripts/generar-inventario.js`) pero es fácil olvidarla.
16
- *
17
- * Cooldown: una vez por sesión por carpeta afectada para no spammear cuando
18
- * el agente edita docenas de archivos seguidos. Estado persistente en
19
- * `.planning/inventario-aviso-state.json`.
20
- *
21
- * Resultado:
22
- * - Edición sobre carpeta inventariada y sin cooldown → sugerencia en stdout (exit 0)
23
- * - Edición sobre carpeta no inventariada → sin output (exit 0)
24
- * - Error interno → sin output (nunca bloquear)
25
- *
26
- * Activo por defecto. Para silenciar: SWL_SUGERIR_REGEN_INVENTARIO=0.
27
- */
28
-
29
- const fs = require('fs');
30
- const path = require('path');
31
-
32
- // Carpetas cuyo cambio invalida los contadores de INVENTARIO.md
33
- const CARPETAS_INVENTARIADAS = [
34
- { ruta: 'agentes/', etiqueta: 'agentes' },
35
- { ruta: 'habilidades/', etiqueta: 'habilidades' },
36
- { ruta: 'comandos/swl/', etiqueta: 'comandos' },
37
- { ruta: 'hooks/', etiqueta: 'hooks', excluir: ['hooks/lib/'] },
38
- { ruta: 'reglas/', etiqueta: 'reglas' },
39
- ];
40
-
41
- const STATE_FILE = path.join(process.cwd(), '.planning', 'inventario-aviso-state.json');
42
- const COOLDOWN_MS = 30 * 60 * 1000; // 30 min
43
-
44
- function normalizarSeparadores(p) {
45
- return String(p).replace(/\\/g, '/');
46
- }
47
-
48
- function detectarCarpeta(rutaRaw) {
49
- if (!rutaRaw) return null;
50
- let normalizada = normalizarSeparadores(rutaRaw);
51
-
52
- // Solo nos interesan rutas relativas al proyecto. Si es absoluta, intentamos
53
- // hacerla relativa al CWD actual.
54
- if (path.isAbsolute(normalizada)) {
55
- const cwd = normalizarSeparadores(process.cwd());
56
- if (normalizada.toLowerCase().startsWith(cwd.toLowerCase())) {
57
- normalizada = normalizada.slice(cwd.length).replace(/^\/+/, '');
58
- } else {
59
- return null;
60
- }
61
- } else {
62
- normalizada = normalizada.replace(/^\.\//, '');
63
- }
64
-
65
- for (const c of CARPETAS_INVENTARIADAS) {
66
- if (normalizada.startsWith(c.ruta)) {
67
- if (c.excluir && c.excluir.some(ex => normalizada.startsWith(ex))) continue;
68
- return c.etiqueta;
69
- }
70
- }
71
- return null;
72
- }
73
-
74
- function leerEstado() {
75
- try {
76
- return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
77
- } catch {
78
- return {};
79
- }
80
- }
81
-
82
- function escribirEstado(estado) {
83
- try {
84
- fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
85
- const tmp = STATE_FILE + '.tmp';
86
- fs.writeFileSync(tmp, JSON.stringify(estado, null, 2), 'utf8');
87
- fs.renameSync(tmp, STATE_FILE);
88
- } catch {
89
- // Falla silenciosa — el cooldown no es crítico
90
- }
91
- }
92
-
93
- function dentroDeCooldown(estado, etiqueta) {
94
- const ultima = estado[etiqueta];
95
- if (!ultima) return false;
96
- return Date.now() - ultima < COOLDOWN_MS;
97
- }
98
-
99
- function marcarAviso(estado, etiqueta) {
100
- estado[etiqueta] = Date.now();
101
- escribirEstado(estado);
102
- }
103
-
104
- function construirMensaje(etiqueta) {
105
- return (
106
- '\n[swl-ses] Tocaste un archivo en `' + etiqueta + '/` — los contadores ' +
107
- 'de INVENTARIO.md pueden quedar stale.\n' +
108
- ' Antes del próximo commit, ejecuta:\n' +
109
- ' node scripts/generar-inventario.js\n' +
110
- ' y verifica con:\n' +
111
- ' node scripts/verificar-release.js\n' +
112
- ' (silenciar con SWL_SUGERIR_REGEN_INVENTARIO=0)\n'
113
- );
114
- }
115
-
116
- function extraerRuta(toolInput) {
117
- if (!toolInput || typeof toolInput !== 'object') return null;
118
- return (
119
- toolInput.file_path ||
120
- toolInput.path ||
121
- toolInput.notebook_path ||
122
- null
123
- );
124
- }
125
-
126
- function ejecutarComoHook() {
127
- let inputRaw = '';
128
-
129
- process.stdin.on('data', chunk => { inputRaw += chunk; });
130
-
131
- process.stdin.on('end', () => {
132
- try {
133
- if (process.env.SWL_SUGERIR_REGEN_INVENTARIO === '0') return;
134
-
135
- const data = JSON.parse(inputRaw || '{}');
136
- const toolName = String(data.tool_name || data.tool?.name || '');
137
- const toolInput = data.tool_input || data.tool?.input || {};
138
-
139
- if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return;
140
-
141
- const ruta = extraerRuta(toolInput);
142
- if (!ruta) return;
143
-
144
- const etiqueta = detectarCarpeta(ruta);
145
- if (!etiqueta) return;
146
-
147
- const estado = leerEstado();
148
- if (dentroDeCooldown(estado, etiqueta)) return;
149
-
150
- process.stdout.write(construirMensaje(etiqueta));
151
- marcarAviso(estado, etiqueta);
152
- } catch {
153
- // Errores internos nunca bloquean
154
- }
155
- });
156
- }
157
-
158
- // Solo escuchar stdin cuando se ejecuta como entrypoint (no al importar para tests)
159
- if (require.main === module) {
160
- ejecutarComoHook();
161
- }
162
-
163
- // Para tests unitarios
164
- module.exports = {
165
- CARPETAS_INVENTARIADAS,
166
- detectarCarpeta,
167
- construirMensaje,
168
- dentroDeCooldown,
169
- COOLDOWN_MS,
170
- };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: sugerir-regenerar-inventario.js
6
+ * Tipo: PostToolUse (aplica a: Write, Edit, MultiEdit)
7
+ *
8
+ * Cuando el agente edita o crea un componente registrado en INVENTARIO.md
9
+ * (agentes, habilidades, comandos, hooks o reglas), sugiere — sin bloquear —
10
+ * regenerar el inventario antes del próximo commit.
11
+ *
12
+ * Motivado por el drift recurrente detectado el 2026-04-29: tras agregar el
13
+ * skill `harness-claude-code` el inventario quedó stale en 5 archivos canónicos
14
+ * porque nadie regeneró `INVENTARIO.md` después de la edición. La regeneración
15
+ * es 1 comando (`node scripts/generar-inventario.js`) pero es fácil olvidarla.
16
+ *
17
+ * Cooldown: una vez por sesión por carpeta afectada para no spammear cuando
18
+ * el agente edita docenas de archivos seguidos. Estado persistente en
19
+ * `.planning/inventario-aviso-state.json`.
20
+ *
21
+ * Resultado:
22
+ * - Edición sobre carpeta inventariada y sin cooldown → sugerencia en stdout (exit 0)
23
+ * - Edición sobre carpeta no inventariada → sin output (exit 0)
24
+ * - Error interno → sin output (nunca bloquear)
25
+ *
26
+ * Activo por defecto. Para silenciar: SWL_SUGERIR_REGEN_INVENTARIO=0.
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ // Carpetas cuyo cambio invalida los contadores de INVENTARIO.md
33
+ const CARPETAS_INVENTARIADAS = [
34
+ { ruta: 'agentes/', etiqueta: 'agentes' },
35
+ { ruta: 'habilidades/', etiqueta: 'habilidades' },
36
+ { ruta: 'comandos/swl/', etiqueta: 'comandos' },
37
+ { ruta: 'hooks/', etiqueta: 'hooks', excluir: ['hooks/lib/'] },
38
+ { ruta: 'reglas/', etiqueta: 'reglas' },
39
+ ];
40
+
41
+ const STATE_FILE = path.join(process.cwd(), '.planning', 'inventario-aviso-state.json');
42
+ const COOLDOWN_MS = 30 * 60 * 1000; // 30 min
43
+
44
+ function normalizarSeparadores(p) {
45
+ return String(p).replace(/\\/g, '/');
46
+ }
47
+
48
+ function detectarCarpeta(rutaRaw) {
49
+ if (!rutaRaw) return null;
50
+ let normalizada = normalizarSeparadores(rutaRaw);
51
+
52
+ // Solo nos interesan rutas relativas al proyecto. Si es absoluta, intentamos
53
+ // hacerla relativa al CWD actual.
54
+ if (path.isAbsolute(normalizada)) {
55
+ const cwd = normalizarSeparadores(process.cwd());
56
+ if (normalizada.toLowerCase().startsWith(cwd.toLowerCase())) {
57
+ normalizada = normalizada.slice(cwd.length).replace(/^\/+/, '');
58
+ } else {
59
+ return null;
60
+ }
61
+ } else {
62
+ normalizada = normalizada.replace(/^\.\//, '');
63
+ }
64
+
65
+ for (const c of CARPETAS_INVENTARIADAS) {
66
+ if (normalizada.startsWith(c.ruta)) {
67
+ if (c.excluir && c.excluir.some(ex => normalizada.startsWith(ex))) continue;
68
+ return c.etiqueta;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function leerEstado() {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
77
+ } catch {
78
+ return {};
79
+ }
80
+ }
81
+
82
+ function escribirEstado(estado) {
83
+ try {
84
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
85
+ const tmp = STATE_FILE + '.tmp';
86
+ fs.writeFileSync(tmp, JSON.stringify(estado, null, 2), 'utf8');
87
+ fs.renameSync(tmp, STATE_FILE);
88
+ } catch {
89
+ // Falla silenciosa — el cooldown no es crítico
90
+ }
91
+ }
92
+
93
+ function dentroDeCooldown(estado, etiqueta) {
94
+ const ultima = estado[etiqueta];
95
+ if (!ultima) return false;
96
+ return Date.now() - ultima < COOLDOWN_MS;
97
+ }
98
+
99
+ function marcarAviso(estado, etiqueta) {
100
+ estado[etiqueta] = Date.now();
101
+ escribirEstado(estado);
102
+ }
103
+
104
+ function construirMensaje(etiqueta) {
105
+ return (
106
+ '\n[swl-ses] Tocaste un archivo en `' + etiqueta + '/` — los contadores ' +
107
+ 'de INVENTARIO.md pueden quedar stale.\n' +
108
+ ' Antes del próximo commit, ejecuta:\n' +
109
+ ' node scripts/generar-inventario.js\n' +
110
+ ' y verifica con:\n' +
111
+ ' node scripts/verificar-release.js\n' +
112
+ ' (silenciar con SWL_SUGERIR_REGEN_INVENTARIO=0)\n'
113
+ );
114
+ }
115
+
116
+ function extraerRuta(toolInput) {
117
+ if (!toolInput || typeof toolInput !== 'object') return null;
118
+ return (
119
+ toolInput.file_path ||
120
+ toolInput.path ||
121
+ toolInput.notebook_path ||
122
+ null
123
+ );
124
+ }
125
+
126
+ function ejecutarComoHook() {
127
+ let inputRaw = '';
128
+
129
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
130
+
131
+ process.stdin.on('end', () => {
132
+ try {
133
+ if (process.env.SWL_SUGERIR_REGEN_INVENTARIO === '0') return;
134
+
135
+ const data = JSON.parse(inputRaw || '{}');
136
+ const toolName = String(data.tool_name || data.tool?.name || '');
137
+ const toolInput = data.tool_input || data.tool?.input || {};
138
+
139
+ if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return;
140
+
141
+ const ruta = extraerRuta(toolInput);
142
+ if (!ruta) return;
143
+
144
+ const etiqueta = detectarCarpeta(ruta);
145
+ if (!etiqueta) return;
146
+
147
+ const estado = leerEstado();
148
+ if (dentroDeCooldown(estado, etiqueta)) return;
149
+
150
+ process.stdout.write(construirMensaje(etiqueta));
151
+ marcarAviso(estado, etiqueta);
152
+ } catch {
153
+ // Errores internos nunca bloquean
154
+ }
155
+ });
156
+ }
157
+
158
+ // Solo escuchar stdin cuando se ejecuta como entrypoint (no al importar para tests)
159
+ if (require.main === module) {
160
+ ejecutarComoHook();
161
+ }
162
+
163
+ // Para tests unitarios
164
+ module.exports = {
165
+ CARPETAS_INVENTARIADAS,
166
+ detectarCarpeta,
167
+ construirMensaje,
168
+ dentroDeCooldown,
169
+ COOLDOWN_MS,
170
+ };
@@ -1,140 +1,140 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: validar-formato-post-subagente.js
6
- * Tipo: SubagentStop (async: true — fire-and-forget)
7
- *
8
- * Cuando un subagente SWL termina, valida que su output cumple con el
9
- * schema declarado en `manifiestos/agent-output-schemas.json`. Si el
10
- * agente no tiene schema declarado, el hook no hace nada — no asume
11
- * que todo agente debe seguir el formato compacto.
12
- *
13
- * Registra el resultado en `.planning/evolucion/formato-violaciones.jsonl`
14
- * para que `metricas-evolucion.js` calcule la tasa de violación por
15
- * agente y la incluya en el dashboard de calidad conductual.
16
- *
17
- * Schema simple: lista de patrones regex que DEBEN aparecer en el output.
18
- * Si todos matchean → conforme. Si falta alguno → violación con detalle.
19
- *
20
- * Opt-out: SWL_FORMATO_VALIDACION=0 desactiva el hook.
21
- *
22
- * El hook nunca bloquea — solo registra para análisis posterior.
23
- */
24
-
25
- const fs = require('fs');
26
- const path = require('path');
27
-
28
- const CWD = process.cwd();
29
- const SCHEMAS_PATH = path.join(CWD, 'manifiestos', 'agent-output-schemas.json');
30
- const LOG_PATH = path.join(CWD, '.planning', 'evolucion', 'formato-violaciones.jsonl');
31
-
32
- // ---------------------------------------------------------------------------
33
- // Carga del schema (una vez)
34
- // ---------------------------------------------------------------------------
35
-
36
- function cargarSchemas() {
37
- try {
38
- const raw = fs.readFileSync(SCHEMAS_PATH, 'utf8');
39
- const parsed = JSON.parse(raw);
40
- return parsed.schemas || {};
41
- } catch {
42
- return {};
43
- }
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Validación
48
- // ---------------------------------------------------------------------------
49
-
50
- /**
51
- * Compara el output contra el schema. Devuelve { violation, errors }.
52
- * @param {string} output - Texto del output del subagente
53
- * @param {object} schema - { patronesRequeridos: string[] }
54
- */
55
- function validarOutput(output, schema) {
56
- const errors = [];
57
- const patrones = Array.isArray(schema.patronesRequeridos)
58
- ? schema.patronesRequeridos
59
- : [];
60
- for (const patron of patrones) {
61
- // El schema puede declarar inline flags estilo (?im) por compatibilidad
62
- // con regex POSIX. JavaScript no los soporta inline — los stripeamos
63
- // y aplicamos 'im' siempre (case-insensitive + multiline).
64
- const limpio = patron.replace(/^\(\?[imsxu]+\)/, '');
65
- let regex;
66
- try { regex = new RegExp(limpio, 'im'); }
67
- catch { errors.push(`Patrón inválido en schema: ${patron}`); continue; }
68
- if (!regex.test(output)) {
69
- errors.push(`Falta patrón requerido: ${patron}`);
70
- }
71
- }
72
- return { violation: errors.length > 0, errors };
73
- }
74
-
75
- /**
76
- * Extrae el output textual del subagente del payload del hook.
77
- * El payload del SubagentStop expone el resultado en distintos lugares
78
- * según versión de Claude Code; intentamos varias rutas.
79
- */
80
- function extraerOutput(data) {
81
- const r = data.tool_response || data.tool_result || data.response || {};
82
- // Posibles ubicaciones del output del subagente
83
- if (typeof r.text === 'string') return r.text;
84
- if (typeof r.content === 'string') return r.content;
85
- if (Array.isArray(r.messages)) {
86
- return r.messages
87
- .map(m => (typeof m.content === 'string' ? m.content : ''))
88
- .filter(Boolean)
89
- .join('\n');
90
- }
91
- if (typeof r.output === 'string') return r.output;
92
- return '';
93
- }
94
-
95
- function ensureDir() {
96
- try { fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true }); } catch { /* ignore */ }
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Entrypoint
101
- // ---------------------------------------------------------------------------
102
-
103
- let inputRaw = '';
104
- process.stdin.on('data', chunk => { inputRaw += chunk; });
105
-
106
- process.stdin.on('end', () => {
107
- try {
108
- if (process.env.SWL_FORMATO_VALIDACION === '0') return;
109
-
110
- const data = JSON.parse(inputRaw);
111
- const subagentType =
112
- data.tool_input?.subagent_type ||
113
- data.tool?.input?.subagent_type ||
114
- data.subagent_type ||
115
- null;
116
- if (!subagentType) return;
117
-
118
- const schemas = cargarSchemas();
119
- const schema = schemas[subagentType];
120
- if (!schema) return; // agente sin schema → no se evalúa
121
-
122
- const output = extraerOutput(data);
123
- if (!output || output.length < 10) return; // output trivial — no evaluar
124
-
125
- const { violation, errors } = validarOutput(output, schema);
126
- const entry = {
127
- ts: new Date().toISOString(),
128
- sessionId: String(data.session_id || 'default'),
129
- agente: subagentType,
130
- violation,
131
- errors,
132
- outputLen: output.length,
133
- };
134
-
135
- ensureDir();
136
- fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n', 'utf8');
137
- } catch {
138
- // SubagentStop hook nunca bloquea por error interno.
139
- }
140
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: validar-formato-post-subagente.js
6
+ * Tipo: SubagentStop (async: true — fire-and-forget)
7
+ *
8
+ * Cuando un subagente SWL termina, valida que su output cumple con el
9
+ * schema declarado en `manifiestos/agent-output-schemas.json`. Si el
10
+ * agente no tiene schema declarado, el hook no hace nada — no asume
11
+ * que todo agente debe seguir el formato compacto.
12
+ *
13
+ * Registra el resultado en `.planning/evolucion/formato-violaciones.jsonl`
14
+ * para que `metricas-evolucion.js` calcule la tasa de violación por
15
+ * agente y la incluya en el dashboard de calidad conductual.
16
+ *
17
+ * Schema simple: lista de patrones regex que DEBEN aparecer en el output.
18
+ * Si todos matchean → conforme. Si falta alguno → violación con detalle.
19
+ *
20
+ * Opt-out: SWL_FORMATO_VALIDACION=0 desactiva el hook.
21
+ *
22
+ * El hook nunca bloquea — solo registra para análisis posterior.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const CWD = process.cwd();
29
+ const SCHEMAS_PATH = path.join(CWD, 'manifiestos', 'agent-output-schemas.json');
30
+ const LOG_PATH = path.join(CWD, '.planning', 'evolucion', 'formato-violaciones.jsonl');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Carga del schema (una vez)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function cargarSchemas() {
37
+ try {
38
+ const raw = fs.readFileSync(SCHEMAS_PATH, 'utf8');
39
+ const parsed = JSON.parse(raw);
40
+ return parsed.schemas || {};
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Validación
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Compara el output contra el schema. Devuelve { violation, errors }.
52
+ * @param {string} output - Texto del output del subagente
53
+ * @param {object} schema - { patronesRequeridos: string[] }
54
+ */
55
+ function validarOutput(output, schema) {
56
+ const errors = [];
57
+ const patrones = Array.isArray(schema.patronesRequeridos)
58
+ ? schema.patronesRequeridos
59
+ : [];
60
+ for (const patron of patrones) {
61
+ // El schema puede declarar inline flags estilo (?im) por compatibilidad
62
+ // con regex POSIX. JavaScript no los soporta inline — los stripeamos
63
+ // y aplicamos 'im' siempre (case-insensitive + multiline).
64
+ const limpio = patron.replace(/^\(\?[imsxu]+\)/, '');
65
+ let regex;
66
+ try { regex = new RegExp(limpio, 'im'); }
67
+ catch { errors.push(`Patrón inválido en schema: ${patron}`); continue; }
68
+ if (!regex.test(output)) {
69
+ errors.push(`Falta patrón requerido: ${patron}`);
70
+ }
71
+ }
72
+ return { violation: errors.length > 0, errors };
73
+ }
74
+
75
+ /**
76
+ * Extrae el output textual del subagente del payload del hook.
77
+ * El payload del SubagentStop expone el resultado en distintos lugares
78
+ * según versión de Claude Code; intentamos varias rutas.
79
+ */
80
+ function extraerOutput(data) {
81
+ const r = data.tool_response || data.tool_result || data.response || {};
82
+ // Posibles ubicaciones del output del subagente
83
+ if (typeof r.text === 'string') return r.text;
84
+ if (typeof r.content === 'string') return r.content;
85
+ if (Array.isArray(r.messages)) {
86
+ return r.messages
87
+ .map(m => (typeof m.content === 'string' ? m.content : ''))
88
+ .filter(Boolean)
89
+ .join('\n');
90
+ }
91
+ if (typeof r.output === 'string') return r.output;
92
+ return '';
93
+ }
94
+
95
+ function ensureDir() {
96
+ try { fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true }); } catch { /* ignore */ }
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Entrypoint
101
+ // ---------------------------------------------------------------------------
102
+
103
+ let inputRaw = '';
104
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
105
+
106
+ process.stdin.on('end', () => {
107
+ try {
108
+ if (process.env.SWL_FORMATO_VALIDACION === '0') return;
109
+
110
+ const data = JSON.parse(inputRaw);
111
+ const subagentType =
112
+ data.tool_input?.subagent_type ||
113
+ data.tool?.input?.subagent_type ||
114
+ data.subagent_type ||
115
+ null;
116
+ if (!subagentType) return;
117
+
118
+ const schemas = cargarSchemas();
119
+ const schema = schemas[subagentType];
120
+ if (!schema) return; // agente sin schema → no se evalúa
121
+
122
+ const output = extraerOutput(data);
123
+ if (!output || output.length < 10) return; // output trivial — no evaluar
124
+
125
+ const { violation, errors } = validarOutput(output, schema);
126
+ const entry = {
127
+ ts: new Date().toISOString(),
128
+ sessionId: String(data.session_id || 'default'),
129
+ agente: subagentType,
130
+ violation,
131
+ errors,
132
+ outputLen: output.length,
133
+ };
134
+
135
+ ensureDir();
136
+ fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n', 'utf8');
137
+ } catch {
138
+ // SubagentStop hook nunca bloquea por error interno.
139
+ }
140
+ });