@saulwade/swl-ses 1.7.4 → 1.9.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 (97) hide show
  1. package/CLAUDE.md +196 -196
  2. package/README.md +579 -579
  3. package/agentes/auto-evolucion-swl.md +7 -7
  4. package/agentes/disenador-ui-swl.md +12 -0
  5. package/agentes/investigador-ux-swl.md +9 -0
  6. package/agentes/orquestador-swl.md +89 -1
  7. package/agentes/perfilador-usuario-swl.md +2 -2
  8. package/agentes/revisor-codigo-swl.md +34 -10
  9. package/agentes/revisor-seguridad-swl.md +7 -0
  10. package/agentes/tdd-qa-swl.md +23 -2
  11. package/agentes/ux-disenador-swl.md +6 -0
  12. package/comandos/swl/autoresearch.md +102 -6
  13. package/comandos/swl/evaluar-skill.md +1 -1
  14. package/comandos/swl/evolucion-estado.md +5 -5
  15. package/comandos/swl/evolucionar.md +2 -2
  16. package/comandos/swl/inbox.md +1 -1
  17. package/comandos/swl/metricas.md +34 -0
  18. package/comandos/swl/nemesis.md +42 -1
  19. package/comandos/swl/planear-fase.md +8 -0
  20. package/comandos/swl/predecir.md +139 -0
  21. package/comandos/swl/reflect-skills.md +2 -2
  22. package/comandos/swl/salud.md +1 -1
  23. package/comandos/swl/verificar.md +50 -7
  24. package/habilidades/ai-runtime-security/SKILL.md +2 -2
  25. package/habilidades/angular-moderno/SKILL.md +44 -1
  26. package/habilidades/auto-evolucion-protocolo/SKILL.md +2 -2
  27. package/habilidades/autoresearch/SKILL.md +15 -1
  28. package/habilidades/benchmark-memoria/SKILL.md +2 -2
  29. package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
  30. package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
  31. package/habilidades/checklist-seguridad/SKILL.md +29 -1
  32. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
  33. package/habilidades/css-moderno/SKILL.md +3 -1
  34. package/habilidades/drift-detection/SKILL.md +3 -3
  35. package/habilidades/eval-framework/SKILL.md +1 -1
  36. package/habilidades/fastapi-experto/SKILL.md +56 -5
  37. package/habilidades/guardrail-semantico/SKILL.md +4 -4
  38. package/habilidades/patrones-python/SKILL.md +8 -5
  39. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  40. package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
  41. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
  42. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
  43. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
  44. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
  45. package/habilidades/swl-claudemd/SKILL.md +2 -2
  46. package/habilidades/tdd-workflow/SKILL.md +14 -1
  47. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
  48. package/habilidades/testing-python/SKILL.md +1 -1
  49. package/habilidades/tracing-processor/SKILL.md +1 -1
  50. package/hooks/actualizar-perfil-usuario.js +2 -2
  51. package/hooks/aiisms-detector.js +2 -2
  52. package/hooks/auto-evolucion.js +1 -1
  53. package/hooks/captura-feedback-usuario.js +2 -2
  54. package/hooks/claudemd-bloat-detector.js +2 -2
  55. package/hooks/claudemd-duplicacion-detector.js +1 -1
  56. package/hooks/contexto-iteracion.js +144 -0
  57. package/hooks/guardrail-modelo.js +2 -2
  58. package/hooks/lib/loop-telemetry.js +321 -0
  59. package/hooks/lib/memory-search.js +1 -1
  60. package/hooks/lib/nudge-tracker.js +1 -1
  61. package/hooks/metricas-evolucion.js +3 -3
  62. package/hooks/notificacion-telegram.js +11 -3
  63. package/hooks/rotar-audit-auto.js +2 -2
  64. package/hooks/validar-formato-post-subagente.js +2 -2
  65. package/hooks/validar-intent-spec.js +1 -1
  66. package/hooks/validar-planning-paths.js +134 -0
  67. package/llms.txt +29 -0
  68. package/manifiestos/hooks-config.json +30 -12
  69. package/manifiestos/modulos.json +1358 -1351
  70. package/manifiestos/planning-paths.json +44 -0
  71. package/manifiestos/skills-lock.json +1275 -1254
  72. package/package.json +93 -92
  73. package/plugin.json +375 -372
  74. package/reglas/arquitectura.evolved.json +7 -0
  75. package/reglas/arquitectura.md +65 -0
  76. package/reglas/gobernanza.md +1 -1
  77. package/reglas/memoria-consolidada.md +7 -7
  78. package/reglas/seguridad.evolved.json +7 -0
  79. package/reglas/seguridad.md +144 -0
  80. package/reglas/sin-duplicacion-reglas-globales.md +1 -1
  81. package/scripts/auditar-agentes-gaps.js +1 -1
  82. package/scripts/auditar-cobertura-frameworks.js +2 -2
  83. package/scripts/auditar-skills-gaps.js +2 -2
  84. package/scripts/benchmark-memoria.js +3 -3
  85. package/scripts/generar-inventario.js +64 -1
  86. package/scripts/inferir-herramientas-permitidas.js +1 -1
  87. package/scripts/instalador.js +80 -2
  88. package/scripts/lib/dashboard-widgets.js +3 -3
  89. package/scripts/lib/drift-detector.js +3 -3
  90. package/scripts/lib/eval-metrics-store.js +3 -3
  91. package/scripts/lib/gitignore-manifest.js +3 -3
  92. package/scripts/mcp-server/README.md +1 -1
  93. package/scripts/mcp-server/telemetry.js +2 -2
  94. package/scripts/reflect-skills.js +4 -4
  95. package/scripts/rotar-audit-logs.js +2 -2
  96. package/scripts/run-skill-evals.js +2 -2
  97. package/scripts/smoke-test.js +24 -2
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: contexto-iteracion.js
6
+ * Tipo: UserPromptSubmit (matcher vacío)
7
+ *
8
+ * Anti-context-rot para loops iterativos: cuando hay una corrida activa en
9
+ * .planning/loops/ (autoresearch, nemesis --remediar, verificar
10
+ * --until-converge, debate adversarial), inyecta al contexto un bloque
11
+ * compacto con el estado del loop — iteración actual, últimas 3 filas del
12
+ * TSV y la recomendación de trayectoria (continuar / detener por plateau /
13
+ * cambiar estrategia).
14
+ *
15
+ * En sesiones largas el modelo pierde de vista en qué iteración va y si la
16
+ * métrica sigue mejorando; este hook re-ancla ese estado sin que el agente
17
+ * tenga que releer archivos. Patrón adoptado del análisis de autoresearch
18
+ * v2.1 (hook iteration-context), adaptado a SWL: estado en el directorio de
19
+ * la corrida (no /tmp) y throttle por tiempo (no por conteo de prompts).
20
+ *
21
+ * Throttle: máximo 1 inyección cada 4 minutos por corrida (estado en
22
+ * .inject-state.json dentro del directorio de la corrida — se limpia solo
23
+ * con la corrida). 4 min < TTL del prompt cache (5 min): la inyección no
24
+ * coincide dos veces dentro de la misma ventana de cache.
25
+ *
26
+ * Zero-deps (solo hooks/lib/loop-telemetry). Nunca bloquea.
27
+ * Opt-out: SWL_CONTEXTO_ITERACION=0
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const { atomicWriteJSON } = require('./lib/atomic-write');
34
+ const { corridaActiva, analizarTrayectoria } = require('./lib/loop-telemetry');
35
+
36
+ /** Intervalo mínimo entre inyecciones para la misma corrida (4 minutos). */
37
+ const THROTTLE_MS = 4 * 60 * 1000;
38
+
39
+ const ARCHIVO_ESTADO = '.inject-state.json';
40
+
41
+ /**
42
+ * Decide si toca inyectar según el estado de throttle de la corrida.
43
+ * @param {string} dirCorrida - Directorio de la corrida activa.
44
+ * @param {number} [ahora=Date.now()]
45
+ * @returns {boolean}
46
+ */
47
+ function debeInyectar(dirCorrida, ahora = Date.now()) {
48
+ try {
49
+ const estado = JSON.parse(
50
+ fs.readFileSync(path.join(dirCorrida, ARCHIVO_ESTADO), 'utf8')
51
+ );
52
+ return ahora - (estado.ultimaInyeccion || 0) > THROTTLE_MS;
53
+ } catch {
54
+ return true; // sin estado = primera inyección
55
+ }
56
+ }
57
+
58
+ /** Registra la inyección para el throttle. Best-effort, escritura atómica. */
59
+ function registrarInyeccion(dirCorrida, ahora = Date.now()) {
60
+ try {
61
+ atomicWriteJSON(
62
+ path.join(dirCorrida, ARCHIVO_ESTADO),
63
+ { ultimaInyeccion: ahora }
64
+ );
65
+ } catch { /* silencioso */ }
66
+ }
67
+
68
+ /**
69
+ * Construye el bloque de contexto a inyectar.
70
+ * @param {{dir: string, tipo: string, ultimasFilas: object[], totalFilas: number}} activa
71
+ * @param {object} trayectoria - Resultado de analizarTrayectoria.
72
+ * @returns {string}
73
+ */
74
+ function construirBloque(activa, trayectoria) {
75
+ const lineas = [];
76
+ lineas.push(`🔁 Loop activo: \`${activa.tipo}\` (${path.basename(activa.dir)})`);
77
+ lineas.push(
78
+ `Iteraciones: ${trayectoria.totalIteraciones} | keep: ${trayectoria.keeps} | ` +
79
+ `revert: ${trayectoria.reverts} | métrica: ${trayectoria.metricaInicial} → ${trayectoria.metricaFinal}`
80
+ );
81
+
82
+ if (activa.ultimasFilas.length > 0) {
83
+ lineas.push('Últimas iteraciones:');
84
+ for (const fila of activa.ultimasFilas) {
85
+ const desc = (fila.descripcion || '').slice(0, 80);
86
+ lineas.push(` ${fila.iteracion ?? '?'}. [${fila.estado || '?'}] métrica=${fila.metrica ?? '?'} — ${desc}`);
87
+ }
88
+ }
89
+
90
+ if (trayectoria.plateau) {
91
+ lineas.push(
92
+ '⚠️ PLATEAU detectado: las últimas iteraciones no mejoran la métrica. ' +
93
+ 'Considera cerrar el loop y reportar el mejor estado alcanzado en lugar de seguir iterando.'
94
+ );
95
+ } else {
96
+ lineas.push(`Recomendación de trayectoria: ${trayectoria.recomendacion}.`);
97
+ }
98
+ lineas.push(`Registro completo: ${path.join(activa.dir, 'iteraciones.tsv')}`);
99
+
100
+ return lineas.join('\n');
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Entrypoint
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function main() {
108
+ let inputRaw = '';
109
+ process.stdin.on('data', (c) => { inputRaw += c; });
110
+
111
+ process.stdin.on('end', () => {
112
+ try {
113
+ if (process.env.SWL_CONTEXTO_ITERACION === '0') return;
114
+
115
+ const activa = corridaActiva(process.cwd(), { maxEdadMin: 30 });
116
+ if (!activa) return;
117
+ if (!debeInyectar(activa.dir)) return;
118
+
119
+ const trayectoria = analizarTrayectoria(activa.dir);
120
+ // Sin iteraciones más allá del baseline no hay nada útil que anclar
121
+ if (trayectoria.totalIteraciones === 0) return;
122
+
123
+ const output = {
124
+ hookSpecificOutput: {
125
+ hookEventName: 'UserPromptSubmit',
126
+ additionalContext: construirBloque(activa, trayectoria),
127
+ },
128
+ };
129
+ // Marcar el throttle SOLO tras un write exitoso: si stdout falla
130
+ // (broken pipe, harness cerró el canal), no registrar la inyección
131
+ // para que el próximo prompt reintente en vez de saltarse 4 min con
132
+ // un mensaje que nunca llegó. Best-effort: el hook nunca bloquea.
133
+ process.stdout.write(JSON.stringify(output), (err) => {
134
+ if (!err) {
135
+ try { registrarInyeccion(activa.dir); } catch { /* throttle best-effort */ }
136
+ }
137
+ });
138
+ } catch { /* nunca bloquea */ }
139
+ });
140
+ }
141
+
142
+ if (require.main === module) main();
143
+
144
+ module.exports = { debeInyectar, registrarInyeccion, construirBloque, THROTTLE_MS, ARCHIVO_ESTADO };
@@ -38,7 +38,7 @@ if (!process.env.SWL_GUARDRAIL_MODELO) {
38
38
  // ---------------------------------------------------------------------------
39
39
 
40
40
  const CWD = process.cwd();
41
- const DIR_EVOL = path.join(CWD, '.planning', 'evolucion');
41
+ const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
42
42
  const GUARDRAIL_LOG = path.join(DIR_EVOL, 'guardrail-observaciones.jsonl');
43
43
 
44
44
  // ---------------------------------------------------------------------------
@@ -237,7 +237,7 @@ async function main() {
237
237
  `[guardrail-modelo] El agente '${agenteNombre}' podría ejecutarse con ` +
238
238
  `'${evento.modelo_sugerido}' (menos costoso). ` +
239
239
  `Razón: ${evento.razon_tripwire}. ` +
240
- `Ver .planning/evolucion/guardrail-observaciones.jsonl\n`
240
+ `Ver .planning/evolution/guardrail-observaciones.jsonl\n`
241
241
  );
242
242
 
243
243
  // Modo observacional — exit 0 siempre
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Loop Telemetry — Registro tabular de iteraciones para loops autónomos.
5
+ *
6
+ * Da trazabilidad a cualquier loop iterativo del sistema (autoresearch,
7
+ * /swl:verificar --until-converge, /swl:nemesis --remediar): cada corrida
8
+ * deja una carpeta en .planning/loops/ con un TSV por iteración, un
9
+ * handoff.json para encadenamiento entre comandos, y utilidades de análisis
10
+ * de trayectoria (plateau, revert rate, mejora acumulada).
11
+ *
12
+ * Patrón adoptado del análisis de autoresearch v2.1 (temp/autoresearch-master,
13
+ * sesión 2026-06-10): TSV por iteración + handoff.json + detección de plateau.
14
+ * Adaptado a convenciones SWL: estado en .planning/ (no /tmp), escrituras
15
+ * atómicas para JSON, append para el TSV (alta frecuencia relativa).
16
+ *
17
+ * Formato del TSV (línea 1 = comentario de dirección, línea 2 = header):
18
+ *
19
+ * # metric_direction: higher_is_better
20
+ * iteracion timestamp metrica delta estado descripcion
21
+ * 0 2026-06-10T20:00:00Z 62 0 baseline estado inicial
22
+ * 1 2026-06-10T20:05:00Z 65 3 keep agrega test de frontera X
23
+ *
24
+ * Estados canónicos: baseline | keep | revert | crash | no-op | error-metrica
25
+ *
26
+ * Zero-deps (solo node:fs/path y hooks/lib/atomic-write).
27
+ *
28
+ * @module hooks/lib/loop-telemetry
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { atomicWriteSync, atomicWriteJSON } = require('./atomic-write');
34
+
35
+ const DIR_LOOPS = path.join('.planning', 'loops');
36
+ const COLUMNAS_DEFAULT = ['iteracion', 'timestamp', 'metrica', 'delta', 'estado', 'descripcion'];
37
+ const DIRECCIONES = ['higher_is_better', 'lower_is_better'];
38
+ const VERSION_HANDOFF = '1.0-swl';
39
+
40
+ /** Sanitiza un valor para celda TSV: sin tabs ni saltos de línea. */
41
+ function sanitizarCelda(valor) {
42
+ if (valor === null || valor === undefined) return '';
43
+ return String(valor).replace(/[\t\r\n]+/g, ' ').trim();
44
+ }
45
+
46
+ function timestampCompacto(fecha) {
47
+ const f = fecha || new Date();
48
+ const pad = (n) => String(n).padStart(2, '0');
49
+ return (
50
+ String(f.getFullYear()).slice(2) + pad(f.getMonth() + 1) + pad(f.getDate()) +
51
+ '-' + pad(f.getHours()) + pad(f.getMinutes()) + pad(f.getSeconds())
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Inicia una corrida de loop: crea .planning/loops/<tipo>-<YYMMDD-HHMMSS>/
57
+ * con iteraciones.tsv (comentario de dirección + header) y meta.json.
58
+ *
59
+ * @param {object} opciones
60
+ * @param {string} opciones.tipo - Identificador del loop (ej: 'autoresearch', 'nemesis', 'verificar').
61
+ * @param {string} [opciones.direccion='higher_is_better'] - Dirección de la métrica.
62
+ * @param {string[]} [opciones.columnas] - Columnas del TSV (default COLUMNAS_DEFAULT).
63
+ * @param {string} [opciones.raiz=process.cwd()] - Raíz del proyecto.
64
+ * @param {object} [opciones.config={}] - Config inicial persistida en meta.json.
65
+ * @returns {{dir: string, tsvPath: string}}
66
+ */
67
+ function iniciarCorrida({ tipo, direccion = 'higher_is_better', columnas, raiz = process.cwd(), config = {} }) {
68
+ if (!tipo || !/^[a-z0-9-]+$/.test(tipo)) {
69
+ throw new TypeError(`loop-telemetry: tipo inválido "${tipo}" — usar kebab-case (ej: "autoresearch")`);
70
+ }
71
+ if (!DIRECCIONES.includes(direccion)) {
72
+ throw new TypeError(`loop-telemetry: dirección inválida "${direccion}" — usar ${DIRECCIONES.join(' | ')}`);
73
+ }
74
+ const cols = Array.isArray(columnas) && columnas.length > 0 ? columnas : COLUMNAS_DEFAULT;
75
+
76
+ // Colisiones en el mismo segundo: el sufijo se agrega sobre la base
77
+ // inmutable (tipo-YYMMDD-HHMMSS-N). NUNCA recortar del nombre acumulado —
78
+ // un replace de "último bloque numérico" se come el HHMMSS del timestamp
79
+ // y rompe la extracción de tipo en corridaActiva().
80
+ const base = path.join(raiz, DIR_LOOPS, `${tipo}-${timestampCompacto()}`);
81
+ let dir = base;
82
+ let sufijo = 1;
83
+ while (fs.existsSync(dir)) {
84
+ sufijo += 1;
85
+ dir = `${base}-${sufijo}`;
86
+ }
87
+ fs.mkdirSync(dir, { recursive: true });
88
+
89
+ const tsvPath = path.join(dir, 'iteraciones.tsv');
90
+ // Header del TSV con escritura atómica: un header truncado rompe
91
+ // parsearTsv() para toda la vida de la corrida. Las filas posteriores sí
92
+ // van con appendFileSync (excepción sancionada para appends).
93
+ atomicWriteSync(tsvPath, `# metric_direction: ${direccion}\n${cols.join('\t')}\n`);
94
+
95
+ atomicWriteJSON(path.join(dir, 'meta.json'), {
96
+ tipo,
97
+ direccion,
98
+ columnas: cols,
99
+ iniciada: new Date().toISOString(),
100
+ config,
101
+ });
102
+
103
+ return { dir, tsvPath };
104
+ }
105
+
106
+ /**
107
+ * Registra una iteración: agrega una fila al TSV de la corrida.
108
+ * Las columnas se toman del header del TSV; campos ausentes quedan vacíos.
109
+ *
110
+ * @param {string} dir - Directorio de la corrida (retornado por iniciarCorrida).
111
+ * @param {object} fila - Objeto con valores por columna (ej: {iteracion: 1, metrica: 65, estado: 'keep'}).
112
+ */
113
+ function registrarIteracion(dir, fila) {
114
+ const tsvPath = path.join(dir, 'iteraciones.tsv');
115
+ const { columnas } = parsearTsv(tsvPath);
116
+ const valores = columnas.map((col) => {
117
+ if (col === 'timestamp' && !(col in fila)) return new Date().toISOString();
118
+ return sanitizarCelda(fila[col]);
119
+ });
120
+ fs.appendFileSync(tsvPath, valores.join('\t') + '\n', 'utf8');
121
+ }
122
+
123
+ /** Parsea el TSV de una corrida. @returns {{direccion: string, columnas: string[], filas: object[]}} */
124
+ function parsearTsv(tsvPath) {
125
+ const contenido = fs.readFileSync(tsvPath, 'utf8');
126
+ const lineas = contenido.split('\n').filter((l) => l.trim().length > 0);
127
+
128
+ let direccion = 'higher_is_better';
129
+ let inicio = 0;
130
+ if (lineas[0] && lineas[0].startsWith('#')) {
131
+ const m = lineas[0].match(/metric_direction:\s*(\S+)/);
132
+ if (m && DIRECCIONES.includes(m[1])) direccion = m[1];
133
+ inicio = 1;
134
+ }
135
+ const columnas = (lineas[inicio] || '').split('\t');
136
+ const filas = lineas.slice(inicio + 1).map((linea) => {
137
+ const celdas = linea.split('\t');
138
+ const obj = {};
139
+ columnas.forEach((col, i) => { obj[col] = celdas[i] !== undefined ? celdas[i] : ''; });
140
+ return obj;
141
+ });
142
+ return { direccion, columnas, filas };
143
+ }
144
+
145
+ /** Lee las iteraciones de una corrida. */
146
+ function leerIteraciones(dir) {
147
+ return parsearTsv(path.join(dir, 'iteraciones.tsv'));
148
+ }
149
+
150
+ /**
151
+ * Detecta plateau: las últimas `ventana` filas con métrica numérica no
152
+ * muestran mejora respecto al mejor valor previo a la ventana.
153
+ *
154
+ * @param {object[]} filas - Filas parseadas del TSV.
155
+ * @param {object} [opciones]
156
+ * @param {number} [opciones.ventana=3]
157
+ * @param {string} [opciones.campo='metrica']
158
+ * @param {string} [opciones.direccion='higher_is_better']
159
+ * @returns {boolean}
160
+ */
161
+ function detectarPlateau(filas, { ventana = 3, campo = 'metrica', direccion = 'higher_is_better' } = {}) {
162
+ const numericas = filas
163
+ .map((f) => parseFloat(f[campo]))
164
+ .filter((n) => Number.isFinite(n));
165
+ if (numericas.length < ventana + 1) return false;
166
+
167
+ const previas = numericas.slice(0, numericas.length - ventana);
168
+ const ultimas = numericas.slice(-ventana);
169
+ const mejor = direccion === 'higher_is_better' ? Math.max(...previas) : Math.min(...previas);
170
+
171
+ return ultimas.every((n) =>
172
+ direccion === 'higher_is_better' ? n <= mejor : n >= mejor
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Analiza la trayectoria completa de una corrida.
178
+ *
179
+ * @param {string} dir - Directorio de la corrida.
180
+ * @returns {{
181
+ * totalIteraciones: number, keeps: number, reverts: number, crashes: number,
182
+ * revertRate: number, metricaInicial: number|null, metricaFinal: number|null,
183
+ * mejora: number|null, plateau: boolean, mayorSalto: {iteracion: string, delta: number}|null,
184
+ * recomendacion: string
185
+ * }}
186
+ */
187
+ function analizarTrayectoria(dir) {
188
+ const { direccion, filas } = leerIteraciones(dir);
189
+ const datos = filas.filter((f) => f.estado !== 'baseline');
190
+ const keeps = datos.filter((f) => f.estado === 'keep').length;
191
+ const reverts = datos.filter((f) => f.estado === 'revert').length;
192
+ const crashes = datos.filter((f) => f.estado === 'crash').length;
193
+ const total = datos.length;
194
+
195
+ const metricas = filas
196
+ .map((f) => parseFloat(f.metrica))
197
+ .filter((n) => Number.isFinite(n));
198
+ const metricaInicial = metricas.length > 0 ? metricas[0] : null;
199
+ const metricaFinal = metricas.length > 0 ? metricas[metricas.length - 1] : null;
200
+ const mejora = metricaInicial !== null && metricaFinal !== null
201
+ ? metricaFinal - metricaInicial
202
+ : null;
203
+
204
+ let mayorSalto = null;
205
+ for (const f of datos) {
206
+ const delta = parseFloat(f.delta);
207
+ if (!Number.isFinite(delta)) continue;
208
+ const magnitud = direccion === 'higher_is_better' ? delta : -delta;
209
+ if (mayorSalto === null || magnitud > (direccion === 'higher_is_better' ? mayorSalto.delta : -mayorSalto.delta)) {
210
+ mayorSalto = { iteracion: f.iteracion, delta };
211
+ }
212
+ }
213
+
214
+ const plateau = detectarPlateau(filas, { direccion });
215
+ const revertRate = total > 0 ? Math.round((reverts / total) * 100) / 100 : 0;
216
+
217
+ let recomendacion;
218
+ if (plateau) {
219
+ recomendacion = 'detener: plateau detectado — las últimas iteraciones no mejoran la métrica';
220
+ } else if (total >= 3 && revertRate >= 0.67) {
221
+ recomendacion = 'cambiar estrategia: la mayoría de las mutaciones se revierten';
222
+ } else if (total === 0) {
223
+ recomendacion = 'sin datos: la corrida no registró iteraciones más allá del baseline';
224
+ } else {
225
+ recomendacion = 'continuar: la trayectoria muestra mejora activa';
226
+ }
227
+
228
+ return {
229
+ totalIteraciones: total, keeps, reverts, crashes, revertRate,
230
+ metricaInicial, metricaFinal, mejora, plateau, mayorSalto, recomendacion,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Escribe handoff.json: contrato de encadenamiento entre comandos iterativos.
236
+ *
237
+ * @param {string} dir - Directorio de la corrida.
238
+ * @param {object} datos
239
+ * @param {string} datos.source - Comando origen (ej: 'swl:nemesis').
240
+ * @param {string} datos.status - COMPLETO | ACOTADO | PLATEAU | INTERRUMPIDO | ERROR
241
+ * @param {object[]} [datos.findings=[]] - Hallazgos transferibles ({id, severidad, archivo_linea, resumen}).
242
+ * @param {object} [datos.config={}] - Config consumible por el comando siguiente.
243
+ */
244
+ function escribirHandoff(dir, { source, status, findings = [], config = {} }) {
245
+ if (!source || !status) {
246
+ throw new TypeError('loop-telemetry: handoff requiere source y status');
247
+ }
248
+ atomicWriteJSON(path.join(dir, 'handoff.json'), {
249
+ version: VERSION_HANDOFF,
250
+ source,
251
+ status,
252
+ timestamp: new Date().toISOString(),
253
+ iteraciones_tsv: 'iteraciones.tsv',
254
+ findings,
255
+ config,
256
+ });
257
+ }
258
+
259
+ /** Lee el handoff.json de una corrida. @returns {object|null} */
260
+ function leerHandoff(dir) {
261
+ try {
262
+ return JSON.parse(fs.readFileSync(path.join(dir, 'handoff.json'), 'utf8'));
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Encuentra la corrida activa más reciente: directorio en .planning/loops/
270
+ * cuyo iteraciones.tsv fue modificado hace menos de maxEdadMin minutos.
271
+ *
272
+ * @param {string} [raiz=process.cwd()]
273
+ * @param {object} [opciones]
274
+ * @param {number} [opciones.maxEdadMin=30]
275
+ * @returns {{dir: string, tipo: string, ultimasFilas: object[], totalFilas: number}|null}
276
+ */
277
+ function corridaActiva(raiz = process.cwd(), { maxEdadMin = 30 } = {}) {
278
+ const base = path.join(raiz, DIR_LOOPS);
279
+ let candidatos;
280
+ try {
281
+ candidatos = fs.readdirSync(base, { withFileTypes: true })
282
+ .filter((e) => e.isDirectory())
283
+ .map((e) => path.join(base, e.name));
284
+ } catch {
285
+ return null;
286
+ }
287
+
288
+ const limite = Date.now() - maxEdadMin * 60 * 1000;
289
+ let mejor = null;
290
+ for (const dir of candidatos) {
291
+ const tsvPath = path.join(dir, 'iteraciones.tsv');
292
+ let stat;
293
+ try { stat = fs.statSync(tsvPath); } catch { continue; }
294
+ if (stat.mtimeMs < limite) continue;
295
+ if (mejor === null || stat.mtimeMs > mejor.mtimeMs) {
296
+ mejor = { dir, mtimeMs: stat.mtimeMs };
297
+ }
298
+ }
299
+ if (!mejor) return null;
300
+
301
+ const { filas } = leerIteraciones(mejor.dir);
302
+ return {
303
+ dir: mejor.dir,
304
+ tipo: path.basename(mejor.dir).replace(/-\d{6}-\d{6}(-\d+)?$/, ''),
305
+ ultimasFilas: filas.slice(-3),
306
+ totalFilas: filas.length,
307
+ };
308
+ }
309
+
310
+ module.exports = {
311
+ DIR_LOOPS,
312
+ COLUMNAS_DEFAULT,
313
+ iniciarCorrida,
314
+ registrarIteracion,
315
+ leerIteraciones,
316
+ detectarPlateau,
317
+ analizarTrayectoria,
318
+ escribirHandoff,
319
+ leerHandoff,
320
+ corridaActiva,
321
+ };
@@ -544,7 +544,7 @@ function fetch(baseDir, id) {
544
544
  function _logUsage(op, baseDir, query, resultsCount) {
545
545
  if (process.env.SWL_MEMORY_TELEMETRY === '0') return;
546
546
  try {
547
- const dir = path.join(baseDir, '.planning', 'evolucion');
547
+ const dir = path.join(baseDir, '.planning', 'evolution');
548
548
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
549
549
  const entry = {
550
550
  ts: new Date().toISOString(),
@@ -45,7 +45,7 @@ try {
45
45
  // Configuración
46
46
  // ---------------------------------------------------------------------------
47
47
 
48
- const DIR_EVOL = path.join(process.cwd(), '.planning', 'evolucion');
48
+ const DIR_EVOL = path.join(process.cwd(), '.planning', 'evolution');
49
49
  const LOG_PATH = path.join(DIR_EVOL, 'nudges.jsonl');
50
50
  const ALERT_PATH = path.join(DIR_EVOL, 'alertas-persistentes.json');
51
51
 
@@ -6,7 +6,7 @@
6
6
  * Tipo: Stop (async: true — fire-and-forget)
7
7
  *
8
8
  * Cada sesión consolida métricas del ciclo de evolución y actualiza
9
- * .planning/evolucion/metricas.json con:
9
+ * .planning/evolution/metricas.json con:
10
10
  * - nudges emitidos / accionados / pendientes (14d)
11
11
  * - tasa de acción por tipo de nudge
12
12
  * - instintos populados vs capacidad
@@ -37,12 +37,12 @@ try {
37
37
  }
38
38
 
39
39
  const CWD = process.cwd();
40
- const DIR_EVOL = path.join(CWD, '.planning', 'evolucion');
40
+ const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
41
41
  const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
42
42
  const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
43
43
  const INSTINTOS_GLOBAL = path.join(CWD, 'instintos', 'global.yaml');
44
44
  const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
45
- const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolucion', 'agentes.jsonl');
45
+ const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolution', 'agentes.jsonl');
46
46
  const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
47
47
  const EVOLUCIONES_LOG = path.join(DIR_EVOL, 'evoluciones.jsonl');
48
48
  const MEMORY_USAGE_LOG = path.join(DIR_EVOL, 'memory-usage.jsonl');
@@ -3,7 +3,13 @@
3
3
  /**
4
4
  * Hook de notificaciones Telegram para Claude Code.
5
5
  *
6
- * Se dispara en Stop, Notification y SubagentStop.
6
+ * Se dispara en Stop y Notification por default. SubagentStop es opt-in:
7
+ * cuando un turno usa subagentes, SubagentStop dispara casi simultáneo al
8
+ * Stop final y el usuario recibe el mismo contenido dos veces ("Subagente
9
+ * terminó" + "Claude terminó"). Para recibir también las terminaciones de
10
+ * subagentes (útil con agentes largos en background):
11
+ * CLAUDE_NOTIFY_EVENTS=Stop,Notification,SubagentStop
12
+ *
7
13
  * Best-effort: siempre termina con exit 0, nunca bloquea el flujo de Claude.
8
14
  * Registra actividad en ~/.claude/notifications/hook.log (append-only).
9
15
  *
@@ -214,9 +220,11 @@ async function run() {
214
220
 
215
221
  log('evento', nombreEvento, 'proyecto', nombreProyecto);
216
222
 
217
- // Filtrar eventos no deseados (configurable vía variable de entorno)
223
+ // Filtrar eventos no deseados (configurable vía variable de entorno).
224
+ // SubagentStop NO está en el default: duplica la notificación del Stop
225
+ // final cuando el turno usó subagentes (mismo cuerpo, dos mensajes).
218
226
  const eventosActivos = (
219
- process.env.CLAUDE_NOTIFY_EVENTS || 'Stop,Notification,SubagentStop'
227
+ process.env.CLAUDE_NOTIFY_EVENTS || 'Stop,Notification'
220
228
  ).split(',').map(s => s.trim());
221
229
 
222
230
  if (!eventosActivos.includes(nombreEvento)) {
@@ -8,7 +8,7 @@
8
8
  * Se ejecuta al terminar una respuesta. Verifica si los archivos de auditoría
9
9
  * (.planning/audit.jsonl y audit-merkle.jsonl) superan el umbral configurable
10
10
  * y, de ser así, invoca scripts/rotar-audit-logs.js para archivar entradas
11
- * antiguas a .planning/archivo/audit/<nombre>-YYYY-MM.jsonl.gz.
11
+ * antiguas a .planning/archive/audit/<nombre>-YYYY-MM.jsonl.gz.
12
12
  *
13
13
  * Política:
14
14
  * - Umbral default: 5 MB por archivo (UMBRAL_BYTES). Configurable vía env
@@ -18,7 +18,7 @@
18
18
  * - Desactivable con SWL_AUDIT_ROTATE_OFF=1 (para CI o escenarios donde
19
19
  * la rotación debe ser manual).
20
20
  * - Cooldown: no se re-ejecuta si hubo una rotación exitosa en las últimas
21
- * 6 horas (marca en .planning/archivo/audit/.ultima-rotacion).
21
+ * 6 horas (marca en .planning/archive/audit/.ultima-rotacion).
22
22
  *
23
23
  * Resultado:
24
24
  * - stdout: 1-2 líneas si se rotó, silencioso si no
@@ -10,7 +10,7 @@
10
10
  * agente no tiene schema declarado, el hook no hace nada — no asume
11
11
  * que todo agente debe seguir el formato compacto.
12
12
  *
13
- * Registra el resultado en `.planning/evolucion/formato-violaciones.jsonl`
13
+ * Registra el resultado en `.planning/evolution/formato-violaciones.jsonl`
14
14
  * para que `metricas-evolucion.js` calcule la tasa de violación por
15
15
  * agente y la incluya en el dashboard de calidad conductual.
16
16
  *
@@ -27,7 +27,7 @@ const path = require('path');
27
27
 
28
28
  const CWD = process.cwd();
29
29
  const SCHEMAS_PATH = path.join(CWD, 'manifiestos', 'agent-output-schemas.json');
30
- const LOG_PATH = path.join(CWD, '.planning', 'evolucion', 'formato-violaciones.jsonl');
30
+ const LOG_PATH = path.join(CWD, '.planning', 'evolution', 'formato-violaciones.jsonl');
31
31
 
32
32
  // ---------------------------------------------------------------------------
33
33
  // Carga del schema (una vez)
@@ -207,7 +207,7 @@ const nudge = {
207
207
  accionado: false,
208
208
  };
209
209
 
210
- const nudgesDir = path.join(CWD, '.planning', 'evolucion');
210
+ const nudgesDir = path.join(CWD, '.planning', 'evolution');
211
211
  const nudgesFile = path.join(nudgesDir, 'nudges.jsonl');
212
212
 
213
213
  try {