@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.
Files changed (113) hide show
  1. package/CLAUDE.md +8 -5
  2. package/README.md +3 -3
  3. package/agentes/accesibilidad-wcag-swl.md +5 -7
  4. package/agentes/arquitecto-swl.md +5 -3
  5. package/agentes/auto-evolucion-swl.md +42 -12
  6. package/agentes/backend-api-swl.md +5 -3
  7. package/agentes/backend-csharp-swl.md +5 -3
  8. package/agentes/backend-go-swl.md +5 -3
  9. package/agentes/backend-java-swl.md +5 -3
  10. package/agentes/backend-node-swl.md +5 -3
  11. package/agentes/backend-python-swl.md +5 -3
  12. package/agentes/backend-rust-swl.md +5 -3
  13. package/agentes/backend-workers-swl.md +5 -3
  14. package/agentes/cloud-infra-swl.md +5 -6
  15. package/agentes/consolidador-swl.md +5 -3
  16. package/agentes/datos-swl.md +5 -7
  17. package/agentes/depurador-swl.md +6 -3
  18. package/agentes/devops-ci-swl.md +5 -3
  19. package/agentes/disenador-ui-swl.md +5 -7
  20. package/agentes/documentador-swl.md +5 -3
  21. package/agentes/frontend-angular-swl.md +5 -11
  22. package/agentes/frontend-css-swl.md +5 -9
  23. package/agentes/frontend-react-swl.md +5 -9
  24. package/agentes/frontend-swl.md +5 -9
  25. package/agentes/frontend-tailwind-swl.md +5 -9
  26. package/agentes/implementador-swl.md +6 -3
  27. package/agentes/investigador-swl.md +5 -3
  28. package/agentes/investigador-ux-swl.md +5 -9
  29. package/agentes/llm-apps-swl.md +5 -3
  30. package/agentes/migrador-swl.md +6 -3
  31. package/agentes/mobile-android-swl.md +5 -3
  32. package/agentes/mobile-cross-swl.md +5 -3
  33. package/agentes/mobile-ios-swl.md +5 -3
  34. package/agentes/mobile-testing-swl.md +5 -3
  35. package/agentes/notificador-swl.md +5 -3
  36. package/agentes/observabilidad-swl.md +5 -3
  37. package/agentes/orquestador-swl.md +29 -8
  38. package/agentes/pagos-swl.md +5 -3
  39. package/agentes/perfilador-usuario-swl.md +4 -2
  40. package/agentes/planificador-swl.md +5 -3
  41. package/agentes/producto-prd-swl.md +5 -3
  42. package/agentes/red-team-swl.md +4 -2
  43. package/agentes/release-manager-swl.md +6 -8
  44. package/agentes/rendimiento-swl.md +5 -6
  45. package/agentes/resolutor-build-swl.md +5 -3
  46. package/agentes/revisor-angular-swl.md +5 -3
  47. package/agentes/revisor-codigo-swl.md +90 -4
  48. package/agentes/revisor-csharp-swl.md +5 -3
  49. package/agentes/revisor-go-swl.md +5 -3
  50. package/agentes/revisor-java-swl.md +5 -3
  51. package/agentes/revisor-kotlin-swl.md +5 -3
  52. package/agentes/revisor-nextjs-swl.md +5 -3
  53. package/agentes/revisor-php-swl.md +5 -3
  54. package/agentes/revisor-react-swl.md +5 -3
  55. package/agentes/revisor-rust-swl.md +5 -3
  56. package/agentes/revisor-seguridad-swl.md +5 -3
  57. package/agentes/revisor-swift-swl.md +5 -3
  58. package/agentes/revisor-typescript-swl.md +5 -3
  59. package/agentes/sre-swl.md +5 -3
  60. package/agentes/tdd-qa-swl.md +5 -3
  61. package/agentes/ux-disenador-swl.md +5 -9
  62. package/comandos/swl/evaluar-skill.md +18 -0
  63. package/comandos/swl/evolucion-estado.md +49 -0
  64. package/comandos/swl/release.md +77 -1
  65. package/comandos/swl/salud.md +23 -0
  66. package/habilidades/checklist-seguridad/SKILL.md +57 -1
  67. package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
  68. package/habilidades/fastapi-experto/SKILL.md +10 -1
  69. package/habilidades/manejo-errores/.evolved.json +8 -8
  70. package/habilidades/manejo-errores/SKILL.md +63 -4
  71. package/habilidades/patrones-python/SKILL.md +5 -4
  72. package/habilidades/release-semver/.evolved.json +8 -8
  73. package/habilidades/release-semver/SKILL.md +85 -1
  74. package/hooks/auto-evolucion.js +35 -1
  75. package/hooks/clasificador-mensajes.js +50 -3
  76. package/hooks/lib/agent-routing.js +107 -0
  77. package/hooks/lib/delegation-tracker.js +162 -44
  78. package/hooks/lib/evolution-tracker.js +12 -3
  79. package/hooks/lib/memory-search.js +59 -1
  80. package/hooks/lib/nudge-tracker.js +10 -1
  81. package/hooks/lib/provenance-tracker.js +11 -3
  82. package/hooks/lib/text-similarity.js +241 -0
  83. package/hooks/metricas-evolucion.js +168 -1
  84. package/hooks/monitor-contexto.js +54 -6
  85. package/hooks/preservar-estado-pre-compact.js +11 -1
  86. package/hooks/risk-scoring.js +10 -1
  87. package/hooks/tracking-costos.js +10 -1
  88. package/hooks/validar-formato-post-subagente.js +140 -0
  89. package/hooks/validar-memoria-hook.js +218 -0
  90. package/manifiestos/agent-output-schemas.json +57 -0
  91. package/manifiestos/hooks-config.json +18 -0
  92. package/manifiestos/modulos.json +3 -0
  93. package/manifiestos/skills-lock.json +1065 -0
  94. package/package.json +1 -1
  95. package/plugin.json +1 -1
  96. package/reglas/arquitectura.md +20 -0
  97. package/reglas/fragmentos-compartidos.md +152 -0
  98. package/reglas/gobernanza.md +10 -1
  99. package/reglas/seguridad-agentes.md +12 -0
  100. package/reglas/skills-estandar.md +19 -0
  101. package/schemas/agent-frontmatter.schema.json +18 -0
  102. package/scripts/auditar-agentes-gaps.js +9 -1
  103. package/scripts/auditar-cobertura-frameworks.js +9 -1
  104. package/scripts/auditar-skills-gaps.js +9 -1
  105. package/scripts/bootstrap-instintos.js +11 -1
  106. package/scripts/generar-inventario.js +112 -9
  107. package/scripts/generar-matriz-lenguajes.js +271 -0
  108. package/scripts/generar-skills-lock.js +190 -0
  109. package/scripts/lib/estado.js +12 -2
  110. package/scripts/lib/gitignore-manifest.js +32 -2
  111. package/scripts/migrar-csv-a-array.js +168 -0
  112. package/scripts/migrar-fase-dominio.js +201 -0
  113. 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 { aplicadas, revertidas, neta: aplicadas - revertidas };
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
- * 35% restante (uso 65%): WARNING — evitar trabajo complejo
12
- * 25% restante (uso ≥ 75%): CRITICAL detenerse, guardar estado, compactar
11
+ * Umbrales por defecto (calibrados con evidencia empírica de
12
+ * github.com/anthropics/claude-code issue #34685degradació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 = 65;
80
+ const UMBRAL_WARNING = UMBRALES.warning;
33
81
 
34
82
  /** Porcentaje de USO a partir del cual se emite CRITICAL. */
35
- const UMBRAL_CRITICAL = 75;
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 = 50;
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
- fs.writeFileSync(backupPath, JSON.stringify(datos, null, 2));
137
+ atomicWriteJSON(backupPath, datos);
128
138
 
129
139
  // Generar resumen para re-inyección
130
140
  if (nombre === 'execution-state') {
@@ -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
- fs.writeFileSync(auditPath, `# Registro de Auditoría\n\nRegistro automático de operaciones de alto riesgo generado por \`risk-scoring.js\`.\n${auditEntry}`);
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 */ }
@@ -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
- fs.writeFileSync(RUTA_METRICS, contenido, 'utf8');
564
+ atomicWriteSync(RUTA_METRICS, contenido, 'utf8');
556
565
  } catch (err) {
557
566
  // Error al escribir METRICAS.md — ignorar silenciosamente
558
567
  }