@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.
- package/CLAUDE.md +3 -3
- package/README.md +5 -5
- package/agentes/orquestador-swl.md +89 -1
- 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/comandos/swl/autoresearch.md +102 -6
- 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/verificar.md +50 -7
- package/habilidades/angular-moderno/SKILL.md +44 -1
- package/habilidades/autoresearch/SKILL.md +15 -1
- 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/fastapi-experto/SKILL.md +56 -5
- package/habilidades/patrones-python/SKILL.md +8 -5
- 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/tdd-workflow/SKILL.md +14 -1
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
- package/hooks/contexto-iteracion.js +144 -0
- package/hooks/lib/loop-telemetry.js +321 -0
- package/hooks/notificacion-telegram.js +11 -3
- package/llms.txt +29 -0
- package/manifiestos/hooks-config.json +10 -1
- package/manifiestos/modulos.json +7 -1
- package/manifiestos/skills-lock.json +45 -24
- package/package.json +4 -3
- package/plugin.json +5 -2
- package/reglas/arquitectura.evolved.json +7 -0
- package/reglas/arquitectura.md +65 -0
- package/reglas/seguridad.evolved.json +7 -0
- package/reglas/seguridad.md +144 -0
- package/scripts/generar-inventario.js +64 -1
- package/scripts/instalador.js +32 -2
- 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
|
|
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
|
|
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)) {
|
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
|