@saulwade/swl-ses 1.0.1 → 1.1.2
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 +8 -5
- package/README.md +3 -3
- package/agentes/accesibilidad-wcag-swl.md +5 -7
- package/agentes/arquitecto-swl.md +5 -3
- package/agentes/auto-evolucion-swl.md +42 -12
- package/agentes/backend-api-swl.md +5 -3
- package/agentes/backend-csharp-swl.md +5 -3
- package/agentes/backend-go-swl.md +5 -3
- package/agentes/backend-java-swl.md +5 -3
- package/agentes/backend-node-swl.md +5 -3
- package/agentes/backend-python-swl.md +5 -3
- package/agentes/backend-rust-swl.md +5 -3
- package/agentes/backend-workers-swl.md +5 -3
- package/agentes/cloud-infra-swl.md +5 -6
- package/agentes/consolidador-swl.md +5 -3
- package/agentes/datos-swl.md +5 -7
- package/agentes/depurador-swl.md +6 -3
- package/agentes/devops-ci-swl.md +5 -3
- package/agentes/disenador-ui-swl.md +5 -7
- package/agentes/documentador-swl.md +5 -3
- package/agentes/frontend-angular-swl.md +5 -11
- package/agentes/frontend-css-swl.md +5 -9
- package/agentes/frontend-react-swl.md +5 -9
- package/agentes/frontend-swl.md +5 -9
- package/agentes/frontend-tailwind-swl.md +5 -9
- package/agentes/implementador-swl.md +6 -3
- package/agentes/investigador-swl.md +5 -3
- package/agentes/investigador-ux-swl.md +5 -9
- package/agentes/llm-apps-swl.md +5 -3
- package/agentes/migrador-swl.md +6 -3
- package/agentes/mobile-android-swl.md +5 -3
- package/agentes/mobile-cross-swl.md +5 -3
- package/agentes/mobile-ios-swl.md +5 -3
- package/agentes/mobile-testing-swl.md +5 -3
- package/agentes/notificador-swl.md +5 -3
- package/agentes/observabilidad-swl.md +5 -3
- package/agentes/orquestador-swl.md +29 -8
- package/agentes/pagos-swl.md +5 -3
- package/agentes/perfilador-usuario-swl.md +4 -2
- package/agentes/planificador-swl.md +5 -3
- package/agentes/producto-prd-swl.md +5 -3
- package/agentes/red-team-swl.md +4 -2
- package/agentes/release-manager-swl.md +6 -8
- package/agentes/rendimiento-swl.md +5 -6
- package/agentes/resolutor-build-swl.md +5 -3
- package/agentes/revisor-angular-swl.md +5 -3
- package/agentes/revisor-codigo-swl.md +90 -4
- package/agentes/revisor-csharp-swl.md +5 -3
- package/agentes/revisor-go-swl.md +5 -3
- package/agentes/revisor-java-swl.md +5 -3
- package/agentes/revisor-kotlin-swl.md +5 -3
- package/agentes/revisor-nextjs-swl.md +5 -3
- package/agentes/revisor-php-swl.md +5 -3
- package/agentes/revisor-react-swl.md +5 -3
- package/agentes/revisor-rust-swl.md +5 -3
- package/agentes/revisor-seguridad-swl.md +5 -3
- package/agentes/revisor-swift-swl.md +5 -3
- package/agentes/revisor-typescript-swl.md +5 -3
- package/agentes/sre-swl.md +5 -3
- package/agentes/tdd-qa-swl.md +5 -3
- package/agentes/ux-disenador-swl.md +5 -9
- package/comandos/swl/evaluar-skill.md +18 -0
- package/comandos/swl/evolucion-estado.md +49 -0
- package/comandos/swl/release.md +77 -1
- package/comandos/swl/salud.md +23 -0
- package/habilidades/checklist-seguridad/SKILL.md +57 -1
- package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
- package/habilidades/fastapi-experto/SKILL.md +10 -1
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/manejo-errores/SKILL.md +63 -4
- package/habilidades/patrones-python/SKILL.md +5 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/release-semver/SKILL.md +85 -1
- package/hooks/auto-evolucion.js +35 -1
- package/hooks/clasificador-mensajes.js +50 -3
- package/hooks/lib/agent-routing.js +107 -0
- package/hooks/lib/delegation-tracker.js +162 -44
- package/hooks/lib/evolution-tracker.js +12 -3
- package/hooks/lib/memory-search.js +59 -1
- package/hooks/lib/nudge-tracker.js +10 -1
- package/hooks/lib/provenance-tracker.js +11 -3
- package/hooks/lib/text-similarity.js +241 -0
- package/hooks/metricas-evolucion.js +168 -1
- package/hooks/monitor-contexto.js +54 -6
- package/hooks/preservar-estado-pre-compact.js +11 -1
- package/hooks/risk-scoring.js +10 -1
- package/hooks/tracking-costos.js +10 -1
- package/hooks/validar-formato-post-subagente.js +140 -0
- package/hooks/validar-memoria-hook.js +218 -0
- package/manifiestos/agent-output-schemas.json +57 -0
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +3 -0
- package/manifiestos/skills-lock.json +1065 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/reglas/arquitectura.md +20 -0
- package/reglas/fragmentos-compartidos.md +152 -0
- package/reglas/gobernanza.md +10 -1
- package/reglas/seguridad-agentes.md +12 -0
- package/reglas/skills-estandar.md +19 -0
- package/schemas/agent-frontmatter.schema.json +18 -0
- package/scripts/auditar-agentes-gaps.js +9 -1
- package/scripts/auditar-cobertura-frameworks.js +9 -1
- package/scripts/auditar-skills-gaps.js +9 -1
- package/scripts/bootstrap-instintos.js +11 -1
- package/scripts/generar-inventario.js +112 -9
- package/scripts/generar-matriz-lenguajes.js +271 -0
- package/scripts/generar-skills-lock.js +190 -0
- package/scripts/lib/estado.js +12 -2
- package/scripts/lib/gitignore-manifest.js +32 -2
- package/scripts/migrar-csv-a-array.js +168 -0
- package/scripts/migrar-fase-dominio.js +201 -0
- package/scripts/publicar.js +88 -18
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* text-similarity.js — utilidades para fuzzy matching en español.
|
|
5
|
+
*
|
|
6
|
+
* Cero dependencias. Compatible Node 18+.
|
|
7
|
+
*
|
|
8
|
+
* Funciones expuestas:
|
|
9
|
+
* - quitarAcentos(texto): normaliza diacríticos preservando ñ
|
|
10
|
+
* - tokenizar(texto): divide en palabras (≥1 char)
|
|
11
|
+
* - stemES(palabra): stemmer ligero español (Porter-light)
|
|
12
|
+
* - levenshtein(a, b): distancia de edición
|
|
13
|
+
* - fuzzyContains(haystack, needle, opts): true si needle aparece de forma
|
|
14
|
+
* aproximada en haystack
|
|
15
|
+
*
|
|
16
|
+
* Diseño:
|
|
17
|
+
* - El stemmer remueve UN solo sufijo por palabra (no recursivo).
|
|
18
|
+
* - Palabras de ≤3 chars no se stemean.
|
|
19
|
+
* - El threshold de Levenshtein es adaptativo: 0 (≤3), 1 (4-7), 2 (8+).
|
|
20
|
+
*
|
|
21
|
+
* Origen: ADR 0013 sección 3B (mayo 2026).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Normalización
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const COMBINADORES_RE = /[̀-ͯ]/g;
|
|
29
|
+
// ñ y Ñ en NFD se descomponen a n/N + U+0303. Usamos sentinels (caracteres
|
|
30
|
+
// privados de uso +) para preservarlos sin colisión con texto real.
|
|
31
|
+
const N_TILDE_NFD = /ñ/g;
|
|
32
|
+
const N_TILDE_NFD_UPPER = /Ñ/g;
|
|
33
|
+
const SENTINEL_LOWER = '';
|
|
34
|
+
const SENTINEL_UPPER = '';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remueve acentos preservando ñ. Lowercase y uppercase.
|
|
38
|
+
* @param {string} texto
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function quitarAcentos(texto) {
|
|
42
|
+
if (!texto) return '';
|
|
43
|
+
return String(texto)
|
|
44
|
+
.normalize('NFD')
|
|
45
|
+
.replace(N_TILDE_NFD, SENTINEL_LOWER)
|
|
46
|
+
.replace(N_TILDE_NFD_UPPER, SENTINEL_UPPER)
|
|
47
|
+
.replace(COMBINADORES_RE, '')
|
|
48
|
+
.replace(new RegExp(SENTINEL_LOWER, 'g'), 'ñ')
|
|
49
|
+
.replace(new RegExp(SENTINEL_UPPER, 'g'), 'Ñ');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Divide texto en tokens (palabras) preservando solo letras y números.
|
|
54
|
+
* @param {string} texto
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function tokenizar(texto) {
|
|
58
|
+
if (!texto) return [];
|
|
59
|
+
const matches = String(texto).toLowerCase().match(/[a-záéíóúñü0-9]+/g);
|
|
60
|
+
return matches || [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Stemmer Porter-light español
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sufijos ordenados de más largo a más corto. El primer match gana.
|
|
69
|
+
* Cada entry es [sufijo, longitudMinimaResto].
|
|
70
|
+
* Si la palabra tras quitar el sufijo es < longitudMinimaResto, no se aplica.
|
|
71
|
+
*
|
|
72
|
+
* Nota: el sufijo 's' tiene minResto=4 (no 3) para evitar que palabras como
|
|
73
|
+
* "tres" o "más" se stemmen a stems de 3 chars que generan falsos matches.
|
|
74
|
+
*/
|
|
75
|
+
const SUFIJOS_ES = [
|
|
76
|
+
// Verbos largos (gerundio + clítico)
|
|
77
|
+
['iendolo', 4], ['iendola', 4], ['iendolos', 4], ['iendolas', 4],
|
|
78
|
+
['andolo', 4], ['andola', 4], ['andolos', 4], ['andolas', 4],
|
|
79
|
+
// Sustantivos compuestos
|
|
80
|
+
['amientos', 4], ['imientos', 4], ['amiento', 4], ['imiento', 4],
|
|
81
|
+
// Adjetivos -ación/-ición/-mente
|
|
82
|
+
['aciones', 4], ['iciones', 4], ['acion', 4], ['icion', 4],
|
|
83
|
+
['mente', 4],
|
|
84
|
+
// Plurales -dades / abstractos
|
|
85
|
+
['idades', 4], ['idad', 4], ['dades', 4], ['dad', 3],
|
|
86
|
+
// Adjetivos -ico/a + plurales
|
|
87
|
+
['icos', 3], ['icas', 3], ['ico', 3], ['ica', 3],
|
|
88
|
+
// Participios
|
|
89
|
+
['ados', 3], ['adas', 3], ['ado', 3], ['ada', 3],
|
|
90
|
+
['idos', 3], ['idas', 3], ['ido', 3], ['ida', 3],
|
|
91
|
+
// Gerundios
|
|
92
|
+
['iendo', 3], ['ando', 3],
|
|
93
|
+
// Plurales / verbos comunes
|
|
94
|
+
['ciones', 3], ['cion', 3],
|
|
95
|
+
// Infinitivos
|
|
96
|
+
['ar', 3], ['er', 3], ['ir', 3],
|
|
97
|
+
// Plurales simples
|
|
98
|
+
['es', 3], ['as', 3], ['os', 3],
|
|
99
|
+
['s', 4],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stemmer ligero para español. Remueve UN sufijo por palabra. No recursivo.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} palabra
|
|
106
|
+
* @returns {string} stem normalizado (sin acentos, lowercase)
|
|
107
|
+
*/
|
|
108
|
+
function stemES(palabra) {
|
|
109
|
+
if (!palabra || palabra.length <= 3) {
|
|
110
|
+
return palabra ? quitarAcentos(palabra.toLowerCase()) : palabra;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sin = quitarAcentos(palabra.toLowerCase());
|
|
114
|
+
|
|
115
|
+
for (const [sufijo, minResto] of SUFIJOS_ES) {
|
|
116
|
+
if (sin.endsWith(sufijo) && sin.length - sufijo.length >= minResto) {
|
|
117
|
+
return sin.slice(0, sin.length - sufijo.length);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return sin;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Distancia de Levenshtein (DP iterativa con dos filas)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Distancia de edición clásica. O(min(a,b)) memoria.
|
|
129
|
+
* @param {string} a
|
|
130
|
+
* @param {string} b
|
|
131
|
+
* @returns {number}
|
|
132
|
+
*/
|
|
133
|
+
function levenshtein(a, b) {
|
|
134
|
+
if (a === b) return 0;
|
|
135
|
+
if (!a) return b.length;
|
|
136
|
+
if (!b) return a.length;
|
|
137
|
+
|
|
138
|
+
if (a.length > b.length) [a, b] = [b, a];
|
|
139
|
+
|
|
140
|
+
let prev = new Array(a.length + 1);
|
|
141
|
+
let curr = new Array(a.length + 1);
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i <= a.length; i++) prev[i] = i;
|
|
144
|
+
|
|
145
|
+
for (let j = 1; j <= b.length; j++) {
|
|
146
|
+
curr[0] = j;
|
|
147
|
+
for (let i = 1; i <= a.length; i++) {
|
|
148
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
149
|
+
curr[i] = Math.min(
|
|
150
|
+
curr[i - 1] + 1,
|
|
151
|
+
prev[i] + 1,
|
|
152
|
+
prev[i - 1] + cost
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
[prev, curr] = [curr, prev];
|
|
156
|
+
}
|
|
157
|
+
return prev[a.length];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Fuzzy contains
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Threshold de Levenshtein adaptativo según longitud de la palabra.
|
|
166
|
+
* @param {string} palabra
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
function thresholdAdaptativo(palabra) {
|
|
170
|
+
if (palabra.length <= 3) return 0;
|
|
171
|
+
if (palabra.length <= 7) return 1;
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Verifica si `needle` aparece en `haystack` con tolerancia.
|
|
177
|
+
*
|
|
178
|
+
* Estrategia (orden):
|
|
179
|
+
* 1. Match exacto de substring sobre normalizado (sin acentos, lowercase).
|
|
180
|
+
* 2. Multi-token: cada token del needle debe matchear con algún token del
|
|
181
|
+
* haystack vía exacto, stem o Levenshtein.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} haystack
|
|
184
|
+
* @param {string} needle
|
|
185
|
+
* @param {object} [opts]
|
|
186
|
+
* @param {boolean} [opts.exact=false] - solo match exacto (sin fuzzy)
|
|
187
|
+
* @param {boolean} [opts.useStem=true] - aplicar stemming
|
|
188
|
+
* @param {number} [opts.threshold=null] - override threshold
|
|
189
|
+
* @returns {boolean}
|
|
190
|
+
*/
|
|
191
|
+
function fuzzyContains(haystack, needle, opts = {}) {
|
|
192
|
+
const { exact = false, useStem = true, threshold = null } = opts;
|
|
193
|
+
|
|
194
|
+
if (!haystack || !needle) return false;
|
|
195
|
+
|
|
196
|
+
const haystackNorm = quitarAcentos(haystack.toLowerCase());
|
|
197
|
+
const needleNorm = quitarAcentos(needle.toLowerCase());
|
|
198
|
+
|
|
199
|
+
if (haystackNorm.includes(needleNorm)) return true;
|
|
200
|
+
if (exact) return false;
|
|
201
|
+
|
|
202
|
+
const needleTokens = tokenizar(needleNorm);
|
|
203
|
+
const haystackTokens = tokenizar(haystackNorm);
|
|
204
|
+
|
|
205
|
+
if (needleTokens.length === 0 || haystackTokens.length === 0) return false;
|
|
206
|
+
|
|
207
|
+
return needleTokens.every((nt) =>
|
|
208
|
+
haystackTokens.some((ht) => coincideToken(ht, nt, useStem, threshold))
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Compara dos tokens individuales con tolerancia.
|
|
214
|
+
* @param {string} ht
|
|
215
|
+
* @param {string} nt
|
|
216
|
+
* @param {boolean} useStem
|
|
217
|
+
* @param {number|null} threshold
|
|
218
|
+
* @returns {boolean}
|
|
219
|
+
*/
|
|
220
|
+
function coincideToken(ht, nt, useStem, threshold) {
|
|
221
|
+
if (ht === nt) return true;
|
|
222
|
+
|
|
223
|
+
if (useStem) {
|
|
224
|
+
const sht = stemES(ht);
|
|
225
|
+
const snt = stemES(nt);
|
|
226
|
+
if (sht === snt && sht.length >= 3) return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const t = threshold !== null ? threshold : thresholdAdaptativo(nt);
|
|
230
|
+
if (t === 0) return false;
|
|
231
|
+
return levenshtein(ht, nt) <= t;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
quitarAcentos,
|
|
236
|
+
tokenizar,
|
|
237
|
+
stemES,
|
|
238
|
+
levenshtein,
|
|
239
|
+
fuzzyContains,
|
|
240
|
+
_internals: { thresholdAdaptativo, coincideToken, SUFIJOS_ES },
|
|
241
|
+
};
|
|
@@ -45,6 +45,8 @@ const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
|
|
|
45
45
|
const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolucion', 'agentes.jsonl');
|
|
46
46
|
const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
|
|
47
47
|
const EVOLUCIONES_LOG = path.join(DIR_EVOL, 'evoluciones.jsonl');
|
|
48
|
+
const MEMORY_USAGE_LOG = path.join(DIR_EVOL, 'memory-usage.jsonl');
|
|
49
|
+
const FORMATO_VIOLACIONES_LOG = path.join(DIR_EVOL, 'formato-violaciones.jsonl');
|
|
48
50
|
|
|
49
51
|
// ---------------------------------------------------------------------------
|
|
50
52
|
// Utilidades
|
|
@@ -119,7 +121,171 @@ function resumenEvoluciones() {
|
|
|
119
121
|
const entries = leerJsonl(EVOLUCIONES_LOG, 30);
|
|
120
122
|
const aplicadas = entries.filter(e => e.tipo === 'aplicada').length;
|
|
121
123
|
const revertidas = entries.filter(e => e.tipo === 'revertida').length;
|
|
122
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
aplicadas,
|
|
126
|
+
revertidas,
|
|
127
|
+
neta: aplicadas - revertidas,
|
|
128
|
+
rollbackRatio: aplicadas > 0 ? Math.round((revertidas / aplicadas) * 1000) / 10 : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Métricas de calidad real (telemetría conductual, no estructural).
|
|
134
|
+
*
|
|
135
|
+
* Calculables desde fuentes ya disponibles en el sistema:
|
|
136
|
+
* - Tasa de fallos por tipo (tipo_fallo en agentes.jsonl).
|
|
137
|
+
* - Reintentos consecutivos del mismo agente sobre la misma tarea.
|
|
138
|
+
* - Latencia mediana y p95 por agente.
|
|
139
|
+
* - Rollback ratio (ya calculado en resumenEvoluciones).
|
|
140
|
+
*
|
|
141
|
+
* Pendientes de instrumentación (no se calculan aún — placeholder):
|
|
142
|
+
* - Precisión de routing por fase/dominio (requiere ground truth).
|
|
143
|
+
* - Utilidad de memoria recuperada (requiere feedback explícito).
|
|
144
|
+
* - Violaciones de formato post-ejecución (requiere validador post-ejec).
|
|
145
|
+
*/
|
|
146
|
+
function resumenCalidad() {
|
|
147
|
+
const entries = leerJsonl(AGENTES_LOG, 14);
|
|
148
|
+
const noTriviales = entries.filter(e => !e.trivial);
|
|
149
|
+
|
|
150
|
+
// Tasa de fallos por tipo
|
|
151
|
+
const fallosPorTipo = {};
|
|
152
|
+
for (const e of noTriviales) {
|
|
153
|
+
if (e.status === 'failed' && e.tipo_fallo) {
|
|
154
|
+
fallosPorTipo[e.tipo_fallo] = (fallosPorTipo[e.tipo_fallo] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Reintentos consecutivos: mismo agente + misma task_id en ventana de 30 min
|
|
159
|
+
const reintentos = {};
|
|
160
|
+
const VENTANA_REINTENTO_MS = 30 * 60 * 1000;
|
|
161
|
+
const sortedByTime = [...noTriviales].sort((a, b) => Date.parse(a.ts || 0) - Date.parse(b.ts || 0));
|
|
162
|
+
for (let i = 1; i < sortedByTime.length; i++) {
|
|
163
|
+
const prev = sortedByTime[i - 1];
|
|
164
|
+
const curr = sortedByTime[i];
|
|
165
|
+
if (prev.agente !== curr.agente) continue;
|
|
166
|
+
const taskPrev = prev.task_id || prev.taskId || null;
|
|
167
|
+
const taskCurr = curr.task_id || curr.taskId || null;
|
|
168
|
+
if (!taskPrev || taskPrev !== taskCurr) continue;
|
|
169
|
+
const dt = Date.parse(curr.ts) - Date.parse(prev.ts);
|
|
170
|
+
if (dt > 0 && dt < VENTANA_REINTENTO_MS) {
|
|
171
|
+
reintentos[curr.agente] = (reintentos[curr.agente] || 0) + 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const reintentosTotal = Object.values(reintentos).reduce((a, b) => a + b, 0);
|
|
175
|
+
|
|
176
|
+
// Latencia por agente (mediana + p95)
|
|
177
|
+
const latenciasPorAgente = {};
|
|
178
|
+
for (const e of noTriviales) {
|
|
179
|
+
if (typeof e.duracion_ms !== 'number') continue;
|
|
180
|
+
if (!latenciasPorAgente[e.agente]) latenciasPorAgente[e.agente] = [];
|
|
181
|
+
latenciasPorAgente[e.agente].push(e.duracion_ms);
|
|
182
|
+
}
|
|
183
|
+
const latencia = {};
|
|
184
|
+
for (const [ag, arr] of Object.entries(latenciasPorAgente)) {
|
|
185
|
+
arr.sort((a, b) => a - b);
|
|
186
|
+
const mediana = arr[Math.floor(arr.length / 2)];
|
|
187
|
+
const p95Idx = Math.min(arr.length - 1, Math.floor(arr.length * 0.95));
|
|
188
|
+
latencia[ag] = { medianaMs: mediana, p95Ms: arr[p95Idx], n: arr.length };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Routing precision por celda (fase, dominio) — proxy basado en
|
|
192
|
+
// tasa de éxito del agente invocado en cada celda. Una celda con
|
|
193
|
+
// tasa baja sugiere routing impreciso.
|
|
194
|
+
const routingCells = {};
|
|
195
|
+
for (const e of noTriviales) {
|
|
196
|
+
if (!e.routed_phase || !e.routed_domain) continue;
|
|
197
|
+
const key = `${e.routed_phase}/${e.routed_domain}`;
|
|
198
|
+
if (!routingCells[key]) {
|
|
199
|
+
routingCells[key] = { runs: 0, fallos: 0, agentes: new Set() };
|
|
200
|
+
}
|
|
201
|
+
routingCells[key].runs += 1;
|
|
202
|
+
routingCells[key].agentes.add(e.agente);
|
|
203
|
+
if (e.status === 'failed') routingCells[key].fallos += 1;
|
|
204
|
+
}
|
|
205
|
+
const precisionRouting = {};
|
|
206
|
+
for (const [key, datos] of Object.entries(routingCells)) {
|
|
207
|
+
const tasaExito = datos.runs > 0
|
|
208
|
+
? Math.round((1 - datos.fallos / datos.runs) * 1000) / 10
|
|
209
|
+
: null;
|
|
210
|
+
precisionRouting[key] = {
|
|
211
|
+
runs: datos.runs,
|
|
212
|
+
fallos: datos.fallos,
|
|
213
|
+
tasaExito: tasaExito,
|
|
214
|
+
agentes: [...datos.agentes],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Utilidad de memoria recuperada (proxy):
|
|
219
|
+
// - Tasa de búsquedas con resultsCount > 0 vs total
|
|
220
|
+
// - Tasa de búsquedas con ≥3 resultados (umbral de "útil de verdad")
|
|
221
|
+
// - Distribución por operación (search/timeline/fetch)
|
|
222
|
+
const memoryEntries = leerJsonl(MEMORY_USAGE_LOG, 14);
|
|
223
|
+
const utilidadMemoria = {
|
|
224
|
+
totalInvocaciones: memoryEntries.length,
|
|
225
|
+
porOperacion: { search: 0, timeline: 0, fetch: 0 },
|
|
226
|
+
conResultados: 0,
|
|
227
|
+
conResultadosRicos: 0, // ≥3
|
|
228
|
+
tasaConResultados: null,
|
|
229
|
+
tasaResultadosRicos: null,
|
|
230
|
+
};
|
|
231
|
+
for (const e of memoryEntries) {
|
|
232
|
+
if (utilidadMemoria.porOperacion[e.op] !== undefined) {
|
|
233
|
+
utilidadMemoria.porOperacion[e.op] += 1;
|
|
234
|
+
}
|
|
235
|
+
const n = Number(e.resultsCount) || 0;
|
|
236
|
+
if (n > 0) utilidadMemoria.conResultados += 1;
|
|
237
|
+
if (n >= 3) utilidadMemoria.conResultadosRicos += 1;
|
|
238
|
+
}
|
|
239
|
+
if (utilidadMemoria.totalInvocaciones > 0) {
|
|
240
|
+
utilidadMemoria.tasaConResultados = Math.round(
|
|
241
|
+
(utilidadMemoria.conResultados / utilidadMemoria.totalInvocaciones) * 1000,
|
|
242
|
+
) / 10;
|
|
243
|
+
utilidadMemoria.tasaResultadosRicos = Math.round(
|
|
244
|
+
(utilidadMemoria.conResultadosRicos / utilidadMemoria.totalInvocaciones) * 1000,
|
|
245
|
+
) / 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Violaciones de formato post-ejecución (cuando un agente termina, el
|
|
249
|
+
// hook validar-formato-post-subagente.js compara el output contra el
|
|
250
|
+
// schema declarado y registra violaciones).
|
|
251
|
+
const formatoEntries = leerJsonl(FORMATO_VIOLACIONES_LOG, 14);
|
|
252
|
+
const violacionesFormato = {
|
|
253
|
+
totalEvaluadas: 0,
|
|
254
|
+
totalViolaciones: 0,
|
|
255
|
+
porAgente: {},
|
|
256
|
+
tasaViolacion: null,
|
|
257
|
+
};
|
|
258
|
+
for (const e of formatoEntries) {
|
|
259
|
+
if (!e.agente) continue;
|
|
260
|
+
if (!violacionesFormato.porAgente[e.agente]) {
|
|
261
|
+
violacionesFormato.porAgente[e.agente] = { evaluadas: 0, violaciones: 0 };
|
|
262
|
+
}
|
|
263
|
+
violacionesFormato.porAgente[e.agente].evaluadas += 1;
|
|
264
|
+
violacionesFormato.totalEvaluadas += 1;
|
|
265
|
+
if (e.violation === true || (Array.isArray(e.errors) && e.errors.length > 0)) {
|
|
266
|
+
violacionesFormato.porAgente[e.agente].violaciones += 1;
|
|
267
|
+
violacionesFormato.totalViolaciones += 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (violacionesFormato.totalEvaluadas > 0) {
|
|
271
|
+
violacionesFormato.tasaViolacion = Math.round(
|
|
272
|
+
(violacionesFormato.totalViolaciones / violacionesFormato.totalEvaluadas) * 1000,
|
|
273
|
+
) / 10;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
fallosPorTipo,
|
|
278
|
+
reintentos: {
|
|
279
|
+
total: reintentosTotal,
|
|
280
|
+
porAgente: reintentos,
|
|
281
|
+
ventanaMin: 30,
|
|
282
|
+
},
|
|
283
|
+
latencia,
|
|
284
|
+
precisionRouting,
|
|
285
|
+
utilidadMemoria,
|
|
286
|
+
violacionesFormato,
|
|
287
|
+
pendientesInstrumentacion: [],
|
|
288
|
+
};
|
|
123
289
|
}
|
|
124
290
|
|
|
125
291
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +316,7 @@ process.stdin.on('end', () => {
|
|
|
150
316
|
aprendizajes_totales: contarAprendizajes(),
|
|
151
317
|
agentes: resumenAgentes(),
|
|
152
318
|
evoluciones: resumenEvoluciones(),
|
|
319
|
+
calidad: resumenCalidad(),
|
|
153
320
|
};
|
|
154
321
|
|
|
155
322
|
// Health score compuesto (0-100). Útil para dashboard.
|
|
@@ -6,10 +6,23 @@
|
|
|
6
6
|
* Tipo: PostToolUse
|
|
7
7
|
*
|
|
8
8
|
* Lee el archivo bridge generado por linea-estado.js y emite advertencias
|
|
9
|
-
* cuando el contexto estimado supera umbrales críticos
|
|
9
|
+
* cuando el contexto estimado supera umbrales críticos.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Umbrales por defecto (calibrados con evidencia empírica de
|
|
12
|
+
* github.com/anthropics/claude-code issue #34685 — degradación detectable
|
|
13
|
+
* ~20%, auto-compactación de Claude Code dispara ~40%, sweet spot 40-50%):
|
|
14
|
+
*
|
|
15
|
+
* ≥ 40%: WARNING — degradación detectable, evitar trabajo complejo
|
|
16
|
+
* ≥ 55%: CRITICAL — detenerse, guardar estado, compactar
|
|
17
|
+
* ≥ 35% en punto natural: sugerencia de compactación proactiva
|
|
18
|
+
*
|
|
19
|
+
* Override por env var (útil para experimentos o sesiones cortas):
|
|
20
|
+
* SWL_CTX_WARNING (default 40)
|
|
21
|
+
* SWL_CTX_CRITICAL (default 55)
|
|
22
|
+
* SWL_CTX_PROACTIVA (default 35)
|
|
23
|
+
*
|
|
24
|
+
* Histórico: hasta v1.1.0 los umbrales eran 65/75/50. Se bajaron en v1.1.1
|
|
25
|
+
* tras analizar el issue #34685 que documenta degradación al 20-40%.
|
|
13
26
|
*
|
|
14
27
|
* Debounce: solo emite aviso cada N_DEBOUNCE invocaciones de herramienta.
|
|
15
28
|
* La escalación de severidad (pasar de WARNING a CRITICAL) bypasea el debounce.
|
|
@@ -28,11 +41,46 @@ const { phase1_PruneToolResults, shouldCompress } = require('./lib/context-compr
|
|
|
28
41
|
// Constantes de configuración
|
|
29
42
|
// ---------------------------------------------------------------------------
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Lee un umbral numérico desde env var con default. Inválido o fuera de rango
|
|
46
|
+
* (0-99) → usar default.
|
|
47
|
+
*/
|
|
48
|
+
function umbralEnv(nombre, def) {
|
|
49
|
+
const valor = parseInt(process.env[nombre] || '', 10);
|
|
50
|
+
if (Number.isFinite(valor) && valor >= 0 && valor < 100) return valor;
|
|
51
|
+
return def;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resuelve los tres umbrales aplicando defaults y validando el invariante de
|
|
56
|
+
* ordenamiento: PROACTIVA < WARNING < CRITICAL.
|
|
57
|
+
*
|
|
58
|
+
* Si los env vars violan el orden (p.ej. WARNING=80 y CRITICAL=50, que haría
|
|
59
|
+
* CRITICAL inalcanzable lógicamente) o PROACTIVA >= WARNING (haciendo la
|
|
60
|
+
* sugerencia proactiva inalcanzable), se descarta la configuración inválida
|
|
61
|
+
* y se usan los defaults para los tres. La fallback es silenciosa porque el
|
|
62
|
+
* hook nunca debe romper el flujo principal.
|
|
63
|
+
*/
|
|
64
|
+
function resolverUmbrales() {
|
|
65
|
+
const defaults = { proactiva: 35, warning: 40, critical: 55 };
|
|
66
|
+
|
|
67
|
+
const proactiva = umbralEnv('SWL_CTX_PROACTIVA', defaults.proactiva);
|
|
68
|
+
const warning = umbralEnv('SWL_CTX_WARNING', defaults.warning);
|
|
69
|
+
const critical = umbralEnv('SWL_CTX_CRITICAL', defaults.critical);
|
|
70
|
+
|
|
71
|
+
if (proactiva < warning && warning < critical) {
|
|
72
|
+
return { proactiva, warning, critical };
|
|
73
|
+
}
|
|
74
|
+
return defaults;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const UMBRALES = resolverUmbrales();
|
|
78
|
+
|
|
31
79
|
/** Porcentaje de USO a partir del cual se emite WARNING. */
|
|
32
|
-
const UMBRAL_WARNING =
|
|
80
|
+
const UMBRAL_WARNING = UMBRALES.warning;
|
|
33
81
|
|
|
34
82
|
/** Porcentaje de USO a partir del cual se emite CRITICAL. */
|
|
35
|
-
const UMBRAL_CRITICAL =
|
|
83
|
+
const UMBRAL_CRITICAL = UMBRALES.critical;
|
|
36
84
|
|
|
37
85
|
/**
|
|
38
86
|
* Número de invocaciones de herramienta entre avisos consecutivos del mismo nivel.
|
|
@@ -44,7 +92,7 @@ const N_DEBOUNCE = 5;
|
|
|
44
92
|
const N_COMPACTACION_PROACTIVA = 40;
|
|
45
93
|
|
|
46
94
|
/** Umbral de uso para activar compactacion proactiva (antes del warning). */
|
|
47
|
-
const UMBRAL_COMPACTACION_PROACTIVA =
|
|
95
|
+
const UMBRAL_COMPACTACION_PROACTIVA = UMBRALES.proactiva;
|
|
48
96
|
|
|
49
97
|
/**
|
|
50
98
|
* Ruta del archivo de estado interno de este monitor.
|
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
|
|
25
|
+
// Atomic write para backups pre-compact (.planning/backups/). Si la
|
|
26
|
+
// compactación interrumpe el backup a media escritura, perdemos el snapshot
|
|
27
|
+
// que precisamente protege el estado pre-compact.
|
|
28
|
+
let atomicWriteJSON;
|
|
29
|
+
try {
|
|
30
|
+
({ atomicWriteJSON } = require('./lib/atomic-write'));
|
|
31
|
+
} catch {
|
|
32
|
+
atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
const CWD = process.cwd();
|
|
26
36
|
|
|
27
37
|
// ---------------------------------------------------------------------------
|
|
@@ -124,7 +134,7 @@ process.stdin.on('end', () => {
|
|
|
124
134
|
|
|
125
135
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
126
136
|
const backupPath = path.join(backupDir, `${nombre}-pre-compact-${ts}.json`);
|
|
127
|
-
|
|
137
|
+
atomicWriteJSON(backupPath, datos);
|
|
128
138
|
|
|
129
139
|
// Generar resumen para re-inyección
|
|
130
140
|
if (nombre === 'execution-state') {
|
package/hooks/risk-scoring.js
CHANGED
|
@@ -29,6 +29,15 @@ const fs = require('fs');
|
|
|
29
29
|
const path = require('path');
|
|
30
30
|
const os = require('os');
|
|
31
31
|
|
|
32
|
+
// Atomic write para AUDITORIA.md (creación inicial del log de seguridad).
|
|
33
|
+
// El append posterior usa appendFileSync (seguro). Fallback defensivo.
|
|
34
|
+
let atomicWriteSync;
|
|
35
|
+
try {
|
|
36
|
+
({ atomicWriteSync } = require(path.join(__dirname, 'lib', 'atomic-write.js')));
|
|
37
|
+
} catch {
|
|
38
|
+
atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// Cargar el motor de scoring desde lib/ relativo al directorio de este script
|
|
33
42
|
const { calcularRiskScore, UMBRALES_DEFAULT } = require(
|
|
34
43
|
path.join(__dirname, 'lib', 'risk-engine.js')
|
|
@@ -122,7 +131,7 @@ function trackHighOverride(score, factors, toolName) {
|
|
|
122
131
|
} else {
|
|
123
132
|
const dir = path.dirname(auditPath);
|
|
124
133
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
125
|
-
|
|
134
|
+
atomicWriteSync(auditPath, `# Registro de Auditoría\n\nRegistro automático de operaciones de alto riesgo generado por \`risk-scoring.js\`.\n${auditEntry}`, 'utf8');
|
|
126
135
|
}
|
|
127
136
|
state.auditWritten = true;
|
|
128
137
|
} catch { /* no bloquear por fallo de audit */ }
|
package/hooks/tracking-costos.js
CHANGED
|
@@ -24,6 +24,15 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const os = require('os');
|
|
26
26
|
|
|
27
|
+
// Escritura atómica para METRICAS.md (archivo persistente en .planning/).
|
|
28
|
+
// El bridge en /tmp queda en writeFileSync simple — es ephemeral por diseño.
|
|
29
|
+
let atomicWriteSync;
|
|
30
|
+
try {
|
|
31
|
+
({ atomicWriteSync } = require('./lib/atomic-write'));
|
|
32
|
+
} catch {
|
|
33
|
+
atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
// ---------------------------------------------------------------------------
|
|
28
37
|
// Dependencias internas
|
|
29
38
|
// ---------------------------------------------------------------------------
|
|
@@ -552,7 +561,7 @@ function escribirMetrics(estado, presupuesto) {
|
|
|
552
561
|
fs.mkdirSync(dir, { recursive: true });
|
|
553
562
|
}
|
|
554
563
|
const contenido = generarMetrics(estado, presupuesto);
|
|
555
|
-
|
|
564
|
+
atomicWriteSync(RUTA_METRICS, contenido, 'utf8');
|
|
556
565
|
} catch (err) {
|
|
557
566
|
// Error al escribir METRICAS.md — ignorar silenciosamente
|
|
558
567
|
}
|