@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.
- package/CLAUDE.md +196 -196
- package/README.md +579 -579
- package/agentes/auto-evolucion-swl.md +7 -7
- package/agentes/disenador-ui-swl.md +12 -0
- package/agentes/investigador-ux-swl.md +9 -0
- package/agentes/orquestador-swl.md +89 -1
- package/agentes/perfilador-usuario-swl.md +2 -2
- package/agentes/revisor-codigo-swl.md +34 -10
- package/agentes/revisor-seguridad-swl.md +7 -0
- package/agentes/tdd-qa-swl.md +23 -2
- package/agentes/ux-disenador-swl.md +6 -0
- package/comandos/swl/autoresearch.md +102 -6
- package/comandos/swl/evaluar-skill.md +1 -1
- package/comandos/swl/evolucion-estado.md +5 -5
- package/comandos/swl/evolucionar.md +2 -2
- package/comandos/swl/inbox.md +1 -1
- package/comandos/swl/metricas.md +34 -0
- package/comandos/swl/nemesis.md +42 -1
- package/comandos/swl/planear-fase.md +8 -0
- package/comandos/swl/predecir.md +139 -0
- package/comandos/swl/reflect-skills.md +2 -2
- package/comandos/swl/salud.md +1 -1
- package/comandos/swl/verificar.md +50 -7
- package/habilidades/ai-runtime-security/SKILL.md +2 -2
- package/habilidades/angular-moderno/SKILL.md +44 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +2 -2
- package/habilidades/autoresearch/SKILL.md +15 -1
- package/habilidades/benchmark-memoria/SKILL.md +2 -2
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
- package/habilidades/checklist-seguridad/SKILL.md +29 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
- package/habilidades/css-moderno/SKILL.md +3 -1
- package/habilidades/drift-detection/SKILL.md +3 -3
- package/habilidades/eval-framework/SKILL.md +1 -1
- package/habilidades/fastapi-experto/SKILL.md +56 -5
- package/habilidades/guardrail-semantico/SKILL.md +4 -4
- package/habilidades/patrones-python/SKILL.md +8 -5
- package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
- package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
- package/habilidades/swl-claudemd/SKILL.md +2 -2
- package/habilidades/tdd-workflow/SKILL.md +14 -1
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
- package/habilidades/testing-python/SKILL.md +1 -1
- package/habilidades/tracing-processor/SKILL.md +1 -1
- package/hooks/actualizar-perfil-usuario.js +2 -2
- package/hooks/aiisms-detector.js +2 -2
- package/hooks/auto-evolucion.js +1 -1
- package/hooks/captura-feedback-usuario.js +2 -2
- package/hooks/claudemd-bloat-detector.js +2 -2
- package/hooks/claudemd-duplicacion-detector.js +1 -1
- package/hooks/contexto-iteracion.js +144 -0
- package/hooks/guardrail-modelo.js +2 -2
- package/hooks/lib/loop-telemetry.js +321 -0
- package/hooks/lib/memory-search.js +1 -1
- package/hooks/lib/nudge-tracker.js +1 -1
- package/hooks/metricas-evolucion.js +3 -3
- package/hooks/notificacion-telegram.js +11 -3
- package/hooks/rotar-audit-auto.js +2 -2
- package/hooks/validar-formato-post-subagente.js +2 -2
- package/hooks/validar-intent-spec.js +1 -1
- package/hooks/validar-planning-paths.js +134 -0
- package/llms.txt +29 -0
- package/manifiestos/hooks-config.json +30 -12
- package/manifiestos/modulos.json +1358 -1351
- package/manifiestos/planning-paths.json +44 -0
- package/manifiestos/skills-lock.json +1275 -1254
- package/package.json +93 -92
- package/plugin.json +375 -372
- package/reglas/arquitectura.evolved.json +7 -0
- package/reglas/arquitectura.md +65 -0
- package/reglas/gobernanza.md +1 -1
- package/reglas/memoria-consolidada.md +7 -7
- package/reglas/seguridad.evolved.json +7 -0
- package/reglas/seguridad.md +144 -0
- package/reglas/sin-duplicacion-reglas-globales.md +1 -1
- package/scripts/auditar-agentes-gaps.js +1 -1
- package/scripts/auditar-cobertura-frameworks.js +2 -2
- package/scripts/auditar-skills-gaps.js +2 -2
- package/scripts/benchmark-memoria.js +3 -3
- package/scripts/generar-inventario.js +64 -1
- package/scripts/inferir-herramientas-permitidas.js +1 -1
- package/scripts/instalador.js +80 -2
- package/scripts/lib/dashboard-widgets.js +3 -3
- package/scripts/lib/drift-detector.js +3 -3
- package/scripts/lib/eval-metrics-store.js +3 -3
- package/scripts/lib/gitignore-manifest.js +3 -3
- package/scripts/mcp-server/README.md +1 -1
- package/scripts/mcp-server/telemetry.js +2 -2
- package/scripts/reflect-skills.js +4 -4
- package/scripts/rotar-audit-logs.js +2 -2
- package/scripts/run-skill-evals.js +2 -2
- 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', '
|
|
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/
|
|
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', '
|
|
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', '
|
|
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/
|
|
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', '
|
|
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-
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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', '
|
|
30
|
+
const LOG_PATH = path.join(CWD, '.planning', 'evolution', 'formato-violaciones.jsonl');
|
|
31
31
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Carga del schema (una vez)
|