@saulwade/swl-ses 1.3.4 → 1.3.7
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/CLAUDE.md +2 -2
- package/README.md +34 -34
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-ses.js +4 -62
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +207 -207
- package/comandos/swl/contribuir.md +233 -233
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/swl-claudemd/SKILL.md +220 -220
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/extraccion-aprendizajes.js +19 -12
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/skills-lock.json +1093 -1093
- package/package.json +1 -1
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/comandos/info.js +1 -1
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +77 -3
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/inicializar.js +2 -2
- package/scripts/instalador.js +40 -3
- package/scripts/instalar-git-hook.js +2 -2
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/gitignore-manifest.js +1 -1
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/parsear-opciones.js +136 -0
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/transformadores/claude.js +200 -200
- package/scripts/lib/transformadores/codex.js +1 -1
- package/scripts/lib/transformadores/copilot.js +1 -1
- package/scripts/lib/transformadores/gemini.js +1 -1
- package/scripts/lib/transformadores/opencode.js +1 -1
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- 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
|
+
});
|