@saulwade/swl-ses 1.3.4 → 1.3.5

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 (102) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-ses.js +4 -62
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +207 -207
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  9. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  10. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  11. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  12. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  13. package/habilidades/eval-framework/SKILL.md +212 -212
  14. package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
  15. package/habilidades/harness-claude-code/SKILL.md +299 -299
  16. package/habilidades/infra-github-actions/SKILL.md +166 -166
  17. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  18. package/habilidades/manejo-errores/.evolved.json +8 -8
  19. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  20. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  21. package/habilidades/patrones-python/SKILL.md +229 -229
  22. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  23. package/habilidades/planear-fase/SKILL.md +319 -319
  24. package/habilidades/release-semver/.evolved.json +8 -8
  25. package/habilidades/swl-claudemd/SKILL.md +220 -220
  26. package/habilidades/testing-python/SKILL.md +340 -340
  27. package/hooks/claudemd-bloat-detector.js +161 -161
  28. package/hooks/extraccion-aprendizajes.js +19 -12
  29. package/hooks/lib/agent-routing.js +107 -107
  30. package/hooks/lib/auto-consolidator.js +335 -335
  31. package/hooks/lib/error-classifier.js +308 -308
  32. package/hooks/lib/merkle-audit.js +96 -96
  33. package/hooks/lib/provenance-tracker.js +191 -191
  34. package/hooks/lib/rate-limit-tracker.js +253 -253
  35. package/hooks/lib/resource-quota.js +122 -122
  36. package/hooks/lib/retry-jitter.js +165 -165
  37. package/hooks/lib/skill-auditor.js +588 -588
  38. package/hooks/lib/sync-status.js +228 -228
  39. package/hooks/lib/taint-tracker.js +107 -107
  40. package/hooks/lib/text-similarity.js +241 -241
  41. package/hooks/lib/toon-compressor.js +245 -245
  42. package/hooks/registro-turnos.js +209 -209
  43. package/hooks/sugerir-regenerar-inventario.js +170 -170
  44. package/hooks/validar-formato-post-subagente.js +140 -140
  45. package/hooks/validar-memoria-hook.js +218 -218
  46. package/instintos/prompt-appendices.yaml +57 -57
  47. package/manifiestos/agent-output-schemas.json +57 -57
  48. package/manifiestos/skills-lock.json +1093 -1093
  49. package/package.json +1 -1
  50. package/plantillas/auditor-veto-template.md +105 -105
  51. package/plantillas/github-workflows/README.md +47 -47
  52. package/plantillas/github-workflows/release-please.yml +44 -44
  53. package/plantillas/github-workflows/swl-ci.yml +107 -107
  54. package/plantillas/github-workflows/swl-security.yml +51 -51
  55. package/plugin.json +1 -1
  56. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  57. package/reglas/arreglar-al-detectar.md +147 -147
  58. package/reglas/fragmentos-compartidos.md +152 -152
  59. package/reglas/harness-claude-code.md +213 -213
  60. package/reglas/usar-context7.md +226 -226
  61. package/schemas/diary-entry.schema.json +80 -80
  62. package/scripts/benchmark-memoria.js +167 -167
  63. package/scripts/configurar-branch-protection.js +418 -418
  64. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  65. package/scripts/doctor.js +77 -3
  66. package/scripts/field-report.js +199 -199
  67. package/scripts/generar-checklists-consolidados.js +273 -273
  68. package/scripts/generar-inventario.js +420 -420
  69. package/scripts/generar-matriz-lenguajes.js +271 -271
  70. package/scripts/instalador.js +38 -1
  71. package/scripts/lib/artefactos-python.js +43 -43
  72. package/scripts/lib/benchmark-metrics.js +160 -160
  73. package/scripts/lib/budget-enforcer.js +252 -252
  74. package/scripts/lib/configurar-ci.js +380 -380
  75. package/scripts/lib/contadores-inventario.js +217 -217
  76. package/scripts/lib/detectar-stack-detallado.js +307 -307
  77. package/scripts/lib/diary-entry.js +234 -234
  78. package/scripts/lib/eval-metrics-store.js +218 -218
  79. package/scripts/lib/eval-quality.js +171 -171
  80. package/scripts/lib/eval-schemas.js +144 -144
  81. package/scripts/lib/eval-self-correct.js +106 -106
  82. package/scripts/lib/eval-validator.js +185 -185
  83. package/scripts/lib/jaccard-similarity.js +98 -98
  84. package/scripts/lib/longmemeval-runner.js +125 -125
  85. package/scripts/lib/npm-version.js +261 -261
  86. package/scripts/lib/paquetes-conocidos.js +50 -50
  87. package/scripts/lib/parsear-opciones.js +136 -0
  88. package/scripts/lib/prompt-builder.js +264 -264
  89. package/scripts/lib/rrf-fusion.js +175 -175
  90. package/scripts/lib/scoring-instintos.js +277 -277
  91. package/scripts/lib/semantic-search.js +252 -252
  92. package/scripts/lib/transformadores/claude.js +200 -200
  93. package/scripts/limpiar-artefactos-python.js +131 -131
  94. package/scripts/mcp-server/README.md +128 -128
  95. package/scripts/mcp-server/handlers.js +206 -206
  96. package/scripts/migrar-csv-a-array.js +168 -168
  97. package/scripts/migrar-fase-dominio.js +201 -201
  98. package/scripts/publicar.js +511 -511
  99. package/scripts/run-eval.js +141 -141
  100. package/scripts/validar-manifest.js +195 -195
  101. package/scripts/validar-userland-vacio.js +110 -110
  102. package/scripts/verificar-release.js +5 -1
@@ -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
+ });