@saulwade/swl-ses 1.8.0 → 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 (44) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +5 -5
  3. package/agentes/orquestador-swl.md +89 -1
  4. package/agentes/revisor-codigo-swl.md +34 -10
  5. package/agentes/revisor-seguridad-swl.md +7 -0
  6. package/agentes/tdd-qa-swl.md +23 -2
  7. package/comandos/swl/autoresearch.md +102 -6
  8. package/comandos/swl/metricas.md +34 -0
  9. package/comandos/swl/nemesis.md +42 -1
  10. package/comandos/swl/planear-fase.md +8 -0
  11. package/comandos/swl/predecir.md +139 -0
  12. package/comandos/swl/verificar.md +50 -7
  13. package/habilidades/angular-moderno/SKILL.md +44 -1
  14. package/habilidades/autoresearch/SKILL.md +15 -1
  15. package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
  16. package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
  17. package/habilidades/checklist-seguridad/SKILL.md +29 -1
  18. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
  19. package/habilidades/css-moderno/SKILL.md +3 -1
  20. package/habilidades/fastapi-experto/SKILL.md +56 -5
  21. package/habilidades/patrones-python/SKILL.md +8 -5
  22. package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
  23. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
  24. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
  25. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
  26. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
  27. package/habilidades/tdd-workflow/SKILL.md +14 -1
  28. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
  29. package/hooks/contexto-iteracion.js +144 -0
  30. package/hooks/lib/loop-telemetry.js +321 -0
  31. package/hooks/notificacion-telegram.js +11 -3
  32. package/llms.txt +29 -0
  33. package/manifiestos/hooks-config.json +10 -1
  34. package/manifiestos/modulos.json +7 -1
  35. package/manifiestos/skills-lock.json +45 -24
  36. package/package.json +4 -3
  37. package/plugin.json +5 -2
  38. package/reglas/arquitectura.evolved.json +7 -0
  39. package/reglas/arquitectura.md +65 -0
  40. package/reglas/seguridad.evolved.json +7 -0
  41. package/reglas/seguridad.md +144 -0
  42. package/scripts/generar-inventario.js +64 -1
  43. package/scripts/instalador.js +32 -2
  44. package/scripts/smoke-test.js +24 -2
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: tdd-workflow
3
3
  description: Flujo completo de Test-Driven Development. Ciclo RED (el test falla) → GREEN (implementación mínima) → REFACTOR (limpieza). Incluye cobertura mínima obligatoria, tests de frontera, factories, fixtures y estrategias para diferentes tipos de código (APIs, services, componentes Angular).
4
- version: "1.0.6"
4
+ version: "1.1.0"
5
5
  evolved: true
6
6
  evolved-from: "1.0.4"
7
7
  evolved-at: "2026-05-16"
@@ -40,6 +40,19 @@ que los tests exigen — ni más, ni menos.
40
40
 
41
41
  ---
42
42
 
43
+ ## Etapa opcional previa: Gherkin (BDD) y gate de mutación
44
+
45
+ Dos extensiones opt-in del ciclo, ambas con guía completa en recursos:
46
+
47
+ - **Antes del ciclo** — si la fase tiene criterios de aceptación de negocio,
48
+ convertirlos en escenarios Given–When–Then validados por el usuario ANTES de
49
+ implementar; cada escenario es el test RED de su criterio. Guía, runners por
50
+ stack y anti-patrones en [recursos/gherkin-bdd.md](recursos/gherkin-bdd.md).
51
+ - **Después del ciclo** — en módulos críticos, verificar la calidad de los
52
+ asserts con mutation testing incremental sobre el diff:
53
+ `Skill("calidad-mutation-testing")`. La cobertura mide ejecución; los
54
+ mutantes sobrevivientes miden si los tests detectarían un bug.
55
+
43
56
  ## El ciclo fundamental RED → GREEN → REFACTOR
44
57
 
45
58
  ### Fase RED — El test debe fallar por la razón correcta
@@ -0,0 +1,111 @@
1
+ # Etapa Gherkin (BDD) — de criterios de aceptación a tests ejecutables
2
+
3
+ Etapa opt-in previa al ciclo RED→GREEN→REFACTOR que convierte los criterios de
4
+ aceptación del CONTEXTO.md/PRD en escenarios **Given–When–Then** ejecutables.
5
+ Cierra el hueco entre "lo que el negocio pidió" y "lo que los tests verifican":
6
+ cada escenario es a la vez especificación legible por el usuario y esqueleto
7
+ del test.
8
+
9
+ Inspirada en el flujo de Robert C. Martin (spec → hard spec → Gherkin → TDD →
10
+ mutation testing): el Gherkin es la "hard spec" verificable; el ciclo TDD la
11
+ implementa; el mutation testing (`Skill("calidad-mutation-testing")`) verifica
12
+ la suite resultante.
13
+
14
+ ## Cuándo usar la etapa (y cuándo no)
15
+
16
+ **Usar cuando**: la fase tiene criterios de aceptación de negocio (PRD o
17
+ CONTEXTO.md con decisiones cerradas), el comportamiento tiene reglas con casos
18
+ distinguibles (descuentos, permisos, estados), o el usuario validará la spec
19
+ sin leer código.
20
+
21
+ **NO usar cuando**: la fase es técnica pura (refactor, migración, infra), los
22
+ criterios son triviales (CRUD sin reglas), o no hay quien lea los escenarios —
23
+ Gherkin sin lector de negocio es ceremonia que duplica los tests.
24
+
25
+ ## Formato
26
+
27
+ ```gherkin
28
+ # language: es
29
+ Característica: Descuento por nivel de cliente
30
+ Como cliente premium quiero recibir mi descuento automático
31
+ para no capturar cupones manualmente.
32
+
33
+ Escenario: Cliente premium recibe 15% en compras normales
34
+ Dado un cliente con nivel "premium"
35
+ Cuando realiza una compra de $100.00 MXN
36
+ Entonces el descuento aplicado es de $15.00 MXN
37
+
38
+ Esquema del escenario: Descuento por nivel
39
+ Dado un cliente con nivel "<nivel>"
40
+ Cuando realiza una compra de $<monto> MXN
41
+ Entonces el descuento aplicado es de $<descuento> MXN
42
+
43
+ Ejemplos:
44
+ | nivel | monto | descuento |
45
+ | normal | 100.00 | 0.00 |
46
+ | premium | 100.00 | 15.00 |
47
+ | mayorista| 100.00 | 22.00 |
48
+ ```
49
+
50
+ Reglas de redacción:
51
+
52
+ - **Un comportamiento por escenario** — si necesitas "Y cuando..." encadenados,
53
+ son dos escenarios.
54
+ - **Lenguaje del dominio, no de la implementación**: "Dado un cliente premium",
55
+ NUNCA "Dado un row en la tabla clientes con tipo=2".
56
+ - **Valores concretos** en los ejemplos — los criterios vagos ("un monto
57
+ válido") no son verificables.
58
+ - **Esquema del escenario** para reglas con tabla de casos — es la forma
59
+ natural de los tests de frontera de `pruebas.md`.
60
+ - Español de México (`# language: es`) — los escenarios los lee el usuario.
61
+
62
+ ## Derivación desde CONTEXTO.md / PRD
63
+
64
+ 1. Tomar cada criterio de aceptación cerrado del CONTEXTO.md (o historia del PRD).
65
+ 2. Reescribirlo como 1-N escenarios: el caso feliz + las fronteras + el caso de error.
66
+ 3. Presentar los escenarios al usuario para validación ANTES de implementar —
67
+ este es el checkpoint humano del flujo: corregir una spec cuesta una
68
+ conversación; corregir la implementación cuesta un refactor.
69
+ 4. Los escenarios validados se guardan en `tests/features/<dominio>.feature`
70
+ y el PLAN.md referencia qué tarea implementa cada escenario.
71
+
72
+ ## Runners por stack
73
+
74
+ | Stack | Runner | Binding típico |
75
+ |-------|--------|----------------|
76
+ | Python | `pytest-bdd` (o `behave`) | `@scenario("features/descuento.feature", "Cliente premium...")` + steps con `@given/@when/@then` |
77
+ | JS/TS | `@cucumber/cucumber` | steps en `features/steps/*.ts` con `Given/When/Then` |
78
+ | C#/.NET | Reqnroll (sucesor de SpecFlow) | bindings `[Given]/[When]/[Then]` |
79
+ | Java | Cucumber-JVM | anotaciones `@Given/@When/@Then` |
80
+
81
+ Verificar versión vigente con Context7 antes de instalar (regla `usar-context7.md`).
82
+
83
+ Los steps son **pegamento delgado**: parsean el Gherkin y llaman al mismo
84
+ código de test que usaría el ciclo TDD (factories incluidas). La lógica de
85
+ verificación vive en los asserts, no en los steps.
86
+
87
+ ## Integración con el ciclo TDD
88
+
89
+ ```
90
+ CONTEXTO.md/PRD → escenarios Gherkin → validación del usuario (checkpoint)
91
+ → por cada escenario: RED (step sin implementar falla)
92
+ → GREEN (implementación mínima) → REFACTOR
93
+ → al cierre de fase: mutation testing opcional sobre el diff
94
+ ```
95
+
96
+ Cada escenario Gherkin ES un test RED al inicio: el runner reporta steps sin
97
+ implementar como fallos — exactamente la fase RED del ciclo. No escribir tests
98
+ unitarios duplicados del mismo criterio: el escenario cubre el comportamiento
99
+ de negocio; los tests unitarios cubren los detalles internos que el Gherkin
100
+ no expresa (errores de infraestructura, edge cases técnicos).
101
+
102
+ ## Anti-patrones
103
+
104
+ - **Gherkin imperativo de UI**: "Cuando hago clic en el botón #submit" — eso
105
+ es un script de Selenium disfrazado. Los escenarios describen comportamiento
106
+ de dominio; la UI cambia sin que la regla de negocio cambie.
107
+ - **Escenarios escritos DESPUÉS de implementar** para "documentar" — pierde el
108
+ checkpoint de validación, que es el valor de la etapa.
109
+ - **Steps con lógica de negocio** — la duplican; los steps solo traducen.
110
+ - **Un .feature gigante por módulo** — un archivo por característica, como el
111
+ código.
@@ -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 };
@@ -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
+ };
@@ -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)) {
package/llms.txt ADDED
@@ -0,0 +1,29 @@
1
+ # swl-ses (@saulwade/swl-ses)
2
+
3
+ > Sistema de ingeniería de software auto-evolutivo multi-runtime polyglot (SDLC completo), distribuido como paquete npm y plugin de Claude Code. 61 agentes, 181 habilidades, 45 comandos, 31 reglas base y 45 hooks. Soporta 11 lenguajes y 7 runtimes (Claude Code, OpenClaude, OpenCode, Gemini, Cursor, Codex, Copilot). Versión 1.9.0.
4
+
5
+ Archivo generado por `node scripts/generar-inventario.js` — no editar a mano. Las cifras se sincronizan con INVENTARIO.md en cada regeneración.
6
+
7
+ ## Documentación
8
+
9
+ - [README](README.md): overview público y quickstart
10
+ - [Manual de uso](MANUAL_USO.md): manual operacional completo
11
+ - [Comandos](COMANDOS.md): referencia detallada de cada comando `/swl:*`
12
+ - [Agentes](AGENTS.md): catálogo de agentes con capacidades
13
+ - [Inventario](INVENTARIO.md): conteos oficiales de todos los componentes
14
+ - [Instalación](INSTALACION.md): instalación, perfiles y configuración
15
+ - [Instrucciones del proyecto](CLAUDE.md): convenciones, stack y reglas
16
+
17
+ ## Componentes
18
+
19
+ - 61 agentes especializados en `agentes/` (orquestación, implementación por stack, revisión, calidad, diseño)
20
+ - 181 habilidades cargables bajo demanda en `habilidades/` (conocimiento operacional con divulgación progresiva)
21
+ - 45 comandos `/swl:*` en `comandos/swl/` (ciclo GSD, calidad, release, diagnóstico)
22
+ - 31 reglas base + 40 reglas por lenguaje en `reglas/` (políticas obligatorias por matcher)
23
+ - 45 hooks en `hooks/` (telemetría, validación, seguridad; zero-deps, escrituras atómicas)
24
+
25
+ ## Opcional
26
+
27
+ - [CHANGELOG](CHANGELOG.md): historial de versiones
28
+ - [Índice de ADRs](.planning/adrs/README.md): decisiones de arquitectura
29
+ - [Variables de entorno](docs/variables-entorno.md): configuración opt-in