@saulwade/swl-ses 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CLAUDE.md +8 -8
  2. package/README.md +13 -13
  3. package/agentes/accesibilidad-wcag-swl.md +3 -3
  4. package/agentes/auto-evolucion-swl.md +908 -908
  5. package/agentes/disenador-ui-swl.md +6 -5
  6. package/agentes/frontend-angular-swl.md +2 -2
  7. package/agentes/frontend-css-swl.md +2 -2
  8. package/agentes/frontend-react-swl.md +4 -4
  9. package/agentes/frontend-swl.md +6 -6
  10. package/agentes/investigador-ux-swl.md +5 -5
  11. package/agentes/orquestador-swl.md +96 -8
  12. package/agentes/perfilador-usuario-swl.md +308 -308
  13. package/agentes/producto-prd-swl.md +1 -1
  14. package/agentes/red-team-swl.md +218 -218
  15. package/agentes/revisor-codigo-swl.md +34 -10
  16. package/agentes/revisor-seguridad-swl.md +7 -0
  17. package/agentes/tdd-qa-swl.md +39 -2
  18. package/comandos/swl/actualizar.md +1 -1
  19. package/comandos/swl/aprender.md +2 -2
  20. package/comandos/swl/aprobar-plan.md +152 -0
  21. package/comandos/swl/autoresearch.md +102 -6
  22. package/comandos/swl/ayuda.md +3 -3
  23. package/comandos/swl/discutir-fase.md +20 -2
  24. package/comandos/swl/ejecutar-fase.md +53 -6
  25. package/comandos/swl/evolucionar.md +1 -1
  26. package/comandos/swl/inbox.md +1 -1
  27. package/comandos/swl/instalar.md +1 -1
  28. package/comandos/swl/nemesis.md +42 -1
  29. package/comandos/swl/planear-fase.md +25 -1
  30. package/comandos/swl/plugins.md +1 -1
  31. package/comandos/swl/predecir.md +139 -0
  32. package/comandos/swl/release.md +1 -1
  33. package/comandos/swl/status.md +279 -0
  34. package/comandos/swl/verificar.md +75 -7
  35. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  36. package/habilidades/angular-moderno/SKILL.md +44 -1
  37. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  38. package/habilidades/autoresearch/SKILL.md +15 -1
  39. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  40. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  41. package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
  42. package/habilidades/changelog-generator/SKILL.md +9 -2
  43. package/habilidades/changelog-generator/scripts/parse-commits.js +12 -1
  44. package/habilidades/checklist-seguridad/SKILL.md +29 -1
  45. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
  46. package/habilidades/css-moderno/SKILL.md +3 -1
  47. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  48. package/habilidades/drift-detection/SKILL.md +179 -179
  49. package/habilidades/ejecutar-fase/SKILL.md +64 -14
  50. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  51. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  52. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  53. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  54. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  55. package/habilidades/fastapi-experto/SKILL.md +56 -5
  56. package/habilidades/harness-claude-code/SKILL.md +10 -7
  57. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  58. package/habilidades/instalar-sistema/SKILL.md +3 -3
  59. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  60. package/habilidades/patrones-python/SKILL.md +8 -5
  61. package/habilidades/perfil-usuario/SKILL.md +200 -200
  62. package/habilidades/planear-fase/SKILL.md +25 -4
  63. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  64. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  65. package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
  66. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
  67. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
  68. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
  69. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
  70. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  71. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  72. package/habilidades/swl-claudemd/SKILL.md +50 -210
  73. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  74. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  75. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  76. package/habilidades/swl-dashboard/SKILL.md +9 -9
  77. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  78. package/habilidades/tdd-workflow/SKILL.md +58 -5
  79. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
  80. package/habilidades/validacion-ci-sistema/SKILL.md +3 -3
  81. package/hooks/calidad-pre-commit.js +340 -3
  82. package/hooks/ciclo-evolucion-subagente.js +26 -0
  83. package/hooks/ciclo-evolucion.js +26 -0
  84. package/hooks/contexto-iteracion.js +144 -0
  85. package/hooks/extraccion-aprendizajes.js +13 -0
  86. package/hooks/lib/ciclo-evolucion.js +47 -0
  87. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  88. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  89. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  90. package/hooks/lib/evolution-tracker.js +24 -3
  91. package/hooks/lib/loop-telemetry.js +321 -0
  92. package/hooks/notificacion-telegram.js +11 -3
  93. package/hooks/spec-gate.js +211 -0
  94. package/hooks/tdd-gate.js +241 -0
  95. package/hooks/validar-intent-spec.js +30 -10
  96. package/llms.txt +29 -0
  97. package/manifiestos/hooks-config.json +36 -18
  98. package/manifiestos/modulos.json +23 -14
  99. package/manifiestos/skills-lock.json +100 -72
  100. package/package.json +4 -3
  101. package/plugin.json +9 -10
  102. package/reglas/accesibilidad.md +10 -0
  103. package/reglas/api-diseno.md +9 -0
  104. package/reglas/arquitectura.evolved.json +7 -0
  105. package/reglas/arquitectura.md +65 -0
  106. package/reglas/auditorias-documentales-estructurales.md +7 -0
  107. package/reglas/cloud-infra.md +8 -0
  108. package/reglas/fragmentos-compartidos.md +5 -0
  109. package/reglas/gobernanza.md +4 -4
  110. package/reglas/hooks.md +6 -0
  111. package/reglas/intent-engineering.md +4 -0
  112. package/reglas/markitdown.md +8 -0
  113. package/reglas/memoria-consolidada.md +1 -1
  114. package/reglas/patrones.md +6 -0
  115. package/reglas/registro-componentes-nuevos.md +10 -1
  116. package/reglas/seguridad-agentes.md +1 -1
  117. package/reglas/seguridad.evolved.json +7 -0
  118. package/reglas/seguridad.md +144 -0
  119. package/reglas/skills-estandar.md +6 -0
  120. package/reglas/testing.md +7 -0
  121. package/reglas/tests-cleanup.md +4 -0
  122. package/reglas/usar-sistema-swl.md +1 -1
  123. package/scripts/generar-inventario.js +64 -1
  124. package/scripts/instalador.js +32 -2
  125. package/scripts/lib/gitignore-manifest.js +29 -1
  126. package/scripts/lib/plan-lock.js +275 -0
  127. package/scripts/migrar-fase-dominio.js +0 -1
  128. package/scripts/smoke-test.js +24 -2
  129. package/scripts/verificar-trazabilidad.js +292 -0
  130. package/agentes/ux-disenador-swl.md +0 -503
  131. package/comandos/swl/dashboard.md +0 -146
  132. package/comandos/swl/evolucion-estado.md +0 -191
  133. package/comandos/swl/metricas.md +0 -342
  134. package/comandos/swl/salud.md +0 -481
  135. package/reglas/verificar-citas-temporales.md +0 -139
@@ -1,376 +1,388 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: metricas-evolucion.js
6
- * Tipo: Stop (async: true — fire-and-forget)
7
- *
8
- * Cada sesión consolida métricas del ciclo de evolución y actualiza
9
- * .planning/evolution/metricas.json con:
10
- * - nudges emitidos / accionados / pendientes (14d)
11
- * - tasa de acción por tipo de nudge
12
- * - instintos populados vs capacidad
13
- * - evoluciones aplicadas (del log de /swl:evolucionar)
14
- * - fallos vs éxitos de agentes en 14d
15
- * - alertas persistentes activas
16
- *
17
- * Además dispara escalarSiAplica() del nudge-tracker.
18
- *
19
- * El hook nunca bloquea (siempre exit 0). Zero-deps.
20
- */
21
-
22
- const fs = require('fs');
23
- const path = require('path');
24
-
25
- let atomicWriteJSON;
26
- try {
27
- ({ atomicWriteJSON } = require('./lib/atomic-write'));
28
- } catch {
29
- atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
30
- }
31
-
32
- let nudgeTracker;
33
- try {
34
- nudgeTracker = require('./lib/nudge-tracker');
35
- } catch {
36
- nudgeTracker = null;
37
- }
38
-
39
- const CWD = process.cwd();
40
- const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
41
- const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
42
- const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
43
- const INSTINTOS_GLOBAL = path.join(CWD, 'instintos', 'global.yaml');
44
- const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
45
- const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolution', 'agentes.jsonl');
46
- const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
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');
50
-
51
- // ---------------------------------------------------------------------------
52
- // Utilidades
53
- // ---------------------------------------------------------------------------
54
-
55
- function ensureDir() {
56
- try { fs.mkdirSync(DIR_EVOL, { recursive: true }); } catch { /* ignore */ }
57
- }
58
-
59
- function contarInstintos(archivo) {
60
- try {
61
- const c = fs.readFileSync(archivo, 'utf8');
62
- const m = c.match(/^total_instintos:\s*(\d+)/m);
63
- if (m) return parseInt(m[1], 10);
64
- // Fallback: contar entries `- id:`
65
- const entries = c.match(/^\s*- id:\s*\S+/gm);
66
- return entries ? entries.length : 0;
67
- } catch {
68
- return 0;
69
- }
70
- }
71
-
72
- function contarAprendizajes() {
73
- try {
74
- const c = fs.readFileSync(APRENDIZAJES, 'utf8');
75
- const headers = c.match(/^##\s+\[\d{4}-\d{2}-\d{2}\]/gm);
76
- return headers ? headers.length : 0;
77
- } catch {
78
- return 0;
79
- }
80
- }
81
-
82
- function leerJsonl(p, dias = 14) {
83
- try {
84
- const limite = Date.now() - dias * 24 * 3600 * 1000;
85
- return fs.readFileSync(p, 'utf8')
86
- .split(/\r?\n/)
87
- .filter(Boolean)
88
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
89
- .filter(e => {
90
- if (!e) return false;
91
- const t = Date.parse(e.ts);
92
- return Number.isFinite(t) && t >= limite;
93
- });
94
- } catch {
95
- return [];
96
- }
97
- }
98
-
99
- function resumenAgentes() {
100
- const entries = leerJsonl(AGENTES_LOG, 14);
101
- const porAgente = {};
102
- for (const e of entries) {
103
- if (!porAgente[e.agente]) porAgente[e.agente] = { runs: 0, fallos: 0, triviales: 0 };
104
- if (e.trivial) porAgente[e.agente].triviales++;
105
- else {
106
- porAgente[e.agente].runs++;
107
- if (e.status === 'failed') porAgente[e.agente].fallos++;
108
- }
109
- }
110
- const totalRuns = entries.filter(e => !e.trivial).length;
111
- const totalFallos = entries.filter(e => !e.trivial && e.status === 'failed').length;
112
- return {
113
- totalRuns,
114
- totalFallos,
115
- tasaExito: totalRuns > 0 ? Math.round((1 - totalFallos / totalRuns) * 1000) / 10 : null,
116
- porAgente,
117
- };
118
- }
119
-
120
- function resumenEvoluciones() {
121
- const entries = leerJsonl(EVOLUCIONES_LOG, 30);
122
- const aplicadas = entries.filter(e => e.tipo === 'aplicada').length;
123
- const revertidas = entries.filter(e => e.tipo === 'revertida').length;
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
- };
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Entrypoint
293
- // ---------------------------------------------------------------------------
294
-
295
- let inputRaw = '';
296
- process.stdin.on('data', chunk => { inputRaw += chunk; });
297
-
298
- process.stdin.on('end', () => {
299
- try {
300
- ensureDir();
301
-
302
- const nudges = nudgeTracker ? nudgeTracker.resumen({ dias: 14 }) : null;
303
- if (nudgeTracker) {
304
- try { nudgeTracker.escalarSiAplica({ threshold: 5 }); } catch { /* ignore */ }
305
- }
306
-
307
- const metricas = {
308
- generado: new Date().toISOString(),
309
- ventana_dias: 14,
310
- nudges: nudges || { total: 0, accionados: 0, pendientes: 0 },
311
- instintos: {
312
- proyecto: contarInstintos(INSTINTOS_PROYECTO),
313
- global: contarInstintos(INSTINTOS_GLOBAL),
314
- perfil: contarInstintos(INSTINTOS_PERFIL),
315
- },
316
- aprendizajes_totales: contarAprendizajes(),
317
- agentes: resumenAgentes(),
318
- evoluciones: resumenEvoluciones(),
319
- calidad: resumenCalidad(),
320
- };
321
-
322
- // Health score compuesto (0-100). Útil para dashboard.
323
- metricas.health_score = calcularHealthScore(metricas);
324
-
325
- atomicWriteJSON(METRICAS_PATH, metricas);
326
- } catch {
327
- // Nunca bloquear por error interno
328
- }
329
- });
330
-
331
- /**
332
- * Health score compuesto del ciclo de evolución (0-100).
333
- *
334
- * Componentes (pesos):
335
- * - Tasa de acción de nudges (30%): accionados / total
336
- * - Densidad de instintos (20%): sum vs capacidad objetivo (20 proyecto + 5 global)
337
- * - Tasa de éxito de agentes (30%): (runs - fallos) / runs
338
- * - Evoluciones netas positivas (10%): aplicadas - revertidas
339
- * - Ausencia de alertas persistentes (10%): -5pt por cada alerta activa
340
- */
341
- function calcularHealthScore(m) {
342
- let score = 0;
343
-
344
- // Nudges (30pt)
345
- if (m.nudges.total > 0) {
346
- score += Math.round(m.nudges.accionados / m.nudges.total * 30);
347
- } else {
348
- score += 15; // sin nudges = neutral
349
- }
350
-
351
- // Instintos (20pt) — objetivo: 20 proyecto + 5 global
352
- const totalInst = m.instintos.proyecto + m.instintos.global;
353
- const objetivo = 25;
354
- score += Math.min(20, Math.round(totalInst / objetivo * 20));
355
-
356
- // Agentes (30pt)
357
- if (m.agentes.totalRuns > 0) {
358
- score += Math.round(m.agentes.tasaExito / 100 * 30);
359
- } else {
360
- score += 15;
361
- }
362
-
363
- // Evoluciones (10pt)
364
- if (m.evoluciones.aplicadas > 0) {
365
- const netaRatio = Math.max(0, m.evoluciones.neta / m.evoluciones.aplicadas);
366
- score += Math.round(netaRatio * 10);
367
- } else {
368
- score += 5;
369
- }
370
-
371
- // Alertas (10pt)
372
- const alertas = (m.nudges.alertasActivas || []).length;
373
- score += Math.max(0, 10 - alertas * 5);
374
-
375
- return Math.min(100, Math.max(0, score));
376
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: metricas-evolucion.js
6
+ * Tipo: Stop (async: true — fire-and-forget)
7
+ *
8
+ * Cada sesión consolida métricas del ciclo de evolución y actualiza
9
+ * .planning/evolution/metricas.json con:
10
+ * - nudges emitidos / accionados / pendientes (14d)
11
+ * - tasa de acción por tipo de nudge
12
+ * - instintos populados vs capacidad
13
+ * - evoluciones aplicadas (del log de /swl:evolucionar)
14
+ * - fallos vs éxitos de agentes en 14d
15
+ * - alertas persistentes activas
16
+ *
17
+ * Además dispara escalarSiAplica() del nudge-tracker.
18
+ *
19
+ * El hook nunca bloquea (siempre exit 0). Zero-deps.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ let atomicWriteJSON;
26
+ try {
27
+ ({ atomicWriteJSON } = require('./atomic-write'));
28
+ } catch {
29
+ atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
30
+ }
31
+
32
+ let nudgeTracker;
33
+ try {
34
+ nudgeTracker = require('./nudge-tracker');
35
+ } catch {
36
+ nudgeTracker = null;
37
+ }
38
+
39
+ const CWD = process.cwd();
40
+ const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
41
+ const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
42
+ const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
43
+ const INSTINTOS_GLOBAL = path.join(CWD, 'instintos', 'global.yaml');
44
+ const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
45
+ const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolution', 'agentes.jsonl');
46
+ const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
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');
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Utilidades
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function ensureDir() {
56
+ try { fs.mkdirSync(DIR_EVOL, { recursive: true }); } catch { /* ignore */ }
57
+ }
58
+
59
+ function contarInstintos(archivo) {
60
+ try {
61
+ const c = fs.readFileSync(archivo, 'utf8');
62
+ const m = c.match(/^total_instintos:\s*(\d+)/m);
63
+ if (m) return parseInt(m[1], 10);
64
+ // Fallback: contar entries `- id:`
65
+ const entries = c.match(/^\s*- id:\s*\S+/gm);
66
+ return entries ? entries.length : 0;
67
+ } catch {
68
+ return 0;
69
+ }
70
+ }
71
+
72
+ function contarAprendizajes() {
73
+ try {
74
+ const c = fs.readFileSync(APRENDIZAJES, 'utf8');
75
+ const headers = c.match(/^##\s+\[\d{4}-\d{2}-\d{2}\]/gm);
76
+ return headers ? headers.length : 0;
77
+ } catch {
78
+ return 0;
79
+ }
80
+ }
81
+
82
+ function leerJsonl(p, dias = 14) {
83
+ try {
84
+ const limite = Date.now() - dias * 24 * 3600 * 1000;
85
+ return fs.readFileSync(p, 'utf8')
86
+ .split(/\r?\n/)
87
+ .filter(Boolean)
88
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
89
+ .filter(e => {
90
+ if (!e) return false;
91
+ const t = Date.parse(e.ts);
92
+ return Number.isFinite(t) && t >= limite;
93
+ });
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+
99
+ function resumenAgentes() {
100
+ const entries = leerJsonl(AGENTES_LOG, 14);
101
+ const porAgente = {};
102
+ for (const e of entries) {
103
+ if (!porAgente[e.agente]) porAgente[e.agente] = { runs: 0, fallos: 0, triviales: 0 };
104
+ if (e.trivial) porAgente[e.agente].triviales++;
105
+ else {
106
+ porAgente[e.agente].runs++;
107
+ if (e.status === 'failed') porAgente[e.agente].fallos++;
108
+ }
109
+ }
110
+ const totalRuns = entries.filter(e => !e.trivial).length;
111
+ const totalFallos = entries.filter(e => !e.trivial && e.status === 'failed').length;
112
+ return {
113
+ totalRuns,
114
+ totalFallos,
115
+ tasaExito: totalRuns > 0 ? Math.round((1 - totalFallos / totalRuns) * 1000) / 10 : null,
116
+ porAgente,
117
+ };
118
+ }
119
+
120
+ function resumenEvoluciones() {
121
+ const entries = leerJsonl(EVOLUCIONES_LOG, 30);
122
+ const aplicadas = entries.filter(e => e.tipo === 'aplicada').length;
123
+ const revertidas = entries.filter(e => e.tipo === 'revertida').length;
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
+ };
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Entrypoint
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /**
296
+ * Etapa de métricas del ciclo de evolución (sub-etapa de hooks/ciclo-evolucion.js).
297
+ * No consume la transcripción de stdin — solo lee archivos del proyecto.
298
+ * Best-effort: nunca lanza.
299
+ * @param {string} [_inputRaw] ignorado (firma uniforme con las otras etapas)
300
+ */
301
+ function ejecutar(_inputRaw) {
302
+ try {
303
+ ensureDir();
304
+
305
+ const nudges = nudgeTracker ? nudgeTracker.resumen({ dias: 14 }) : null;
306
+ if (nudgeTracker) {
307
+ try { nudgeTracker.escalarSiAplica({ threshold: 5 }); } catch { /* ignore */ }
308
+ }
309
+
310
+ const metricas = {
311
+ generado: new Date().toISOString(),
312
+ ventana_dias: 14,
313
+ nudges: nudges || { total: 0, accionados: 0, pendientes: 0 },
314
+ instintos: {
315
+ proyecto: contarInstintos(INSTINTOS_PROYECTO),
316
+ global: contarInstintos(INSTINTOS_GLOBAL),
317
+ perfil: contarInstintos(INSTINTOS_PERFIL),
318
+ },
319
+ aprendizajes_totales: contarAprendizajes(),
320
+ agentes: resumenAgentes(),
321
+ evoluciones: resumenEvoluciones(),
322
+ calidad: resumenCalidad(),
323
+ };
324
+
325
+ // Health score compuesto (0-100). Útil para dashboard.
326
+ metricas.health_score = calcularHealthScore(metricas);
327
+
328
+ atomicWriteJSON(METRICAS_PATH, metricas);
329
+ } catch {
330
+ // Nunca bloquear por error interno
331
+ }
332
+ }
333
+
334
+ module.exports = { ejecutar, calcularHealthScore };
335
+
336
+ // Ejecución directa (compat: sigue invocable standalone para diagnóstico).
337
+ if (require.main === module) {
338
+ let inputRaw = '';
339
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
340
+ process.stdin.on('end', () => ejecutar(inputRaw));
341
+ }
342
+
343
+ /**
344
+ * Health score compuesto del ciclo de evolución (0-100).
345
+ *
346
+ * Componentes (pesos):
347
+ * - Tasa de acción de nudges (30%): accionados / total
348
+ * - Densidad de instintos (20%): sum vs capacidad objetivo (20 proyecto + 5 global)
349
+ * - Tasa de éxito de agentes (30%): (runs - fallos) / runs
350
+ * - Evoluciones netas positivas (10%): aplicadas - revertidas
351
+ * - Ausencia de alertas persistentes (10%): -5pt por cada alerta activa
352
+ */
353
+ function calcularHealthScore(m) {
354
+ let score = 0;
355
+
356
+ // Nudges (30pt)
357
+ if (m.nudges.total > 0) {
358
+ score += Math.round(m.nudges.accionados / m.nudges.total * 30);
359
+ } else {
360
+ score += 15; // sin nudges = neutral
361
+ }
362
+
363
+ // Instintos (20pt) — objetivo: 20 proyecto + 5 global
364
+ const totalInst = m.instintos.proyecto + m.instintos.global;
365
+ const objetivo = 25;
366
+ score += Math.min(20, Math.round(totalInst / objetivo * 20));
367
+
368
+ // Agentes (30pt)
369
+ if (m.agentes.totalRuns > 0) {
370
+ score += Math.round(m.agentes.tasaExito / 100 * 30);
371
+ } else {
372
+ score += 15;
373
+ }
374
+
375
+ // Evoluciones (10pt)
376
+ if (m.evoluciones.aplicadas > 0) {
377
+ const netaRatio = Math.max(0, m.evoluciones.neta / m.evoluciones.aplicadas);
378
+ score += Math.round(netaRatio * 10);
379
+ } else {
380
+ score += 5;
381
+ }
382
+
383
+ // Alertas (10pt)
384
+ const alertas = (m.nudges.alertasActivas || []).length;
385
+ score += Math.max(0, 10 - alertas * 5);
386
+
387
+ return Math.min(100, Math.max(0, score));
388
+ }