@saulwade/swl-ses 1.6.3 → 1.6.6
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 +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +148 -20
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +92 -92
- package/plugin.json +371 -362
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/instalador.js +72 -4
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/notificaciones-telegram.js +14 -0
- package/scripts/lib/transformadores/codex.js +4 -0
- package/scripts/lib/transformadores/cursor.js +5 -0
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- package/scripts/verificar-release.js +199 -1
package/reglas/arquitectura.md
CHANGED
|
@@ -274,6 +274,44 @@ Documentar en un ADR qué patrón se usa y por qué.
|
|
|
274
274
|
|
|
275
275
|
---
|
|
276
276
|
|
|
277
|
+
## Split modular con compositor por herencia múltiple
|
|
278
|
+
|
|
279
|
+
Cuando un módulo backend (router, service, repository) supera ~1500 LOC en un
|
|
280
|
+
solo archivo y contiene sub-dominios identificables, aplicar el playbook de
|
|
281
|
+
split documentado en `Skill("proceso-modular-split")`. El patrón clave:
|
|
282
|
+
|
|
283
|
+
- **Compositor por herencia múltiple, NO `__getattr__`**: el compositor (clase
|
|
284
|
+
monolítica que mantiene API pública previa) hereda de cada sub-service. MRO
|
|
285
|
+
inspeccionable, type checker feliz, IDE autocompleta. `__getattr__` para
|
|
286
|
+
delegación es opaco al tipado y al stack trace.
|
|
287
|
+
|
|
288
|
+
- **Helpers transversales en clase base, no duplicados entre sub-services**: si
|
|
289
|
+
un helper como `_validar_predio_existe` lo usan ≥80% de sub-services, vive
|
|
290
|
+
en `<modulo>/service_base.py` (`<Modulo>ServiceBase`). Cada sub-service
|
|
291
|
+
hereda de la base. Si solo lo usan 1-2 sub-services, vive en uno de ellos y
|
|
292
|
+
los demás importan por composición (no por herencia).
|
|
293
|
+
|
|
294
|
+
- **Helper transversal cross-sub-service NO se duplica**: validación que
|
|
295
|
+
aparece en ≥3 sub-services es candidata inmediata a la clase base. La
|
|
296
|
+
duplicación es señal de que el split fue prematuro o de que se perdió el
|
|
297
|
+
helper común durante la migración.
|
|
298
|
+
|
|
299
|
+
- **Validar a escala**: el patrón se validó a ~20,000 LOC totales en SIGM
|
|
300
|
+
(recaudación ADR-0016 + catastro ADR-0017). Aplicar al primer módulo es
|
|
301
|
+
prudente; antes de aplicar a un tercero, confirmar que el feedback de los
|
|
302
|
+
dos primeros no requiere ajuste al playbook.
|
|
303
|
+
|
|
304
|
+
Anti-patrones a evitar (las auditorías técnicas NO los detectan):
|
|
305
|
+
|
|
306
|
+
- Router que importa el compositor cuando existe sub-service específico.
|
|
307
|
+
- Router que accede a `servicio._repo` o métodos privados del sub-service.
|
|
308
|
+
- Default `None` pasado explícitamente bypasea el default del callee.
|
|
309
|
+
- CLAUDE.md y AGENTS.md desincronizados tras el refactor.
|
|
310
|
+
|
|
311
|
+
Estos los detecta revisión de arquitectura humana o senior, no `nemesis-auditor-swl`.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
277
315
|
## Reglas de desempate entre principios (conflict resolution)
|
|
278
316
|
|
|
279
317
|
Los principios de ingeniería pueden entrar en tensión. Cuando dos reglas
|
|
@@ -86,6 +86,99 @@ principio:
|
|
|
86
86
|
reportan", "p95 > 60s en producción documentado", "uso > N veces/mes",
|
|
87
87
|
no "cuando sea relevante" o "más adelante".
|
|
88
88
|
|
|
89
|
+
### Hallazgos colaterales con blast radius alto — patrón Hallazgo A/B/C
|
|
90
|
+
|
|
91
|
+
Durante el trabajo principal puedes detectar un problema secundario cuyo fix
|
|
92
|
+
**no cabe** en la regla general "detectar → informar → arreglar en mismo
|
|
93
|
+
turno" porque su blast radius es alto: toca infra compartida, requiere
|
|
94
|
+
downtime, modifica contratos públicos, exige decisión arquitectural, o su
|
|
95
|
+
remediación dura más que el trabajo principal en curso.
|
|
96
|
+
|
|
97
|
+
Aplicar el catálogo de tres opciones explícitas — NUNCA mezclar el fix con
|
|
98
|
+
el trabajo principal sin etiquetarlo y NUNCA dejarlo como deuda silenciosa.
|
|
99
|
+
|
|
100
|
+
#### Definición operacional
|
|
101
|
+
|
|
102
|
+
Un hallazgo colateral cumple **al menos uno** de estos atributos:
|
|
103
|
+
|
|
104
|
+
- Su fix toca archivos fuera del scope del trabajo principal (>3 archivos
|
|
105
|
+
no relacionados con la tarea actual).
|
|
106
|
+
- Requiere operación destructiva (`git filter-branch`, `git filter-repo`,
|
|
107
|
+
drop de tabla, rotación de credencial productiva).
|
|
108
|
+
- Modifica configuración de infra compartida (CI/CD, branch protection,
|
|
109
|
+
permisos de repo, secrets de organización).
|
|
110
|
+
- Exige decisión arquitectural ambigua que el agente no puede tomar solo.
|
|
111
|
+
- Su remediación dura más que el commit actual del trabajo principal.
|
|
112
|
+
|
|
113
|
+
Si NO cumple ninguno de estos atributos, NO es Hallazgo A/B/C — aplicar la
|
|
114
|
+
regla general (arreglar en mismo turno).
|
|
115
|
+
|
|
116
|
+
#### Las tres opciones explícitas
|
|
117
|
+
|
|
118
|
+
| Opción | Cuándo | Acción |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| **Hallazgo A — Resolver ahora** | Fix < 30 min, reversible con `git revert`, sin blast radius en infra compartida, sin decisión arquitectural | Pausar trabajo principal, fix en commit separado etiquetado, retomar |
|
|
121
|
+
| **Hallazgo B — DT formal con trigger verificable** | Fix con blast radius alto pero NO bloqueante para el trabajo principal. Tiene criterio observable que define cuándo cerrarlo | Redactar entry en `.planning/DEUDA-TECNICA.md` con ID, trigger verificable, plan de cierre paso a paso. Continuar trabajo principal |
|
|
122
|
+
| **Hallazgo C — Escalar al usuario** | Fix excede autorización del agente: requiere decisión arquitectural, operación destructiva irreversible, o modifica contratos productivos | Pausar trabajo principal, reportar al usuario con 3 opciones concretas y recomendación, esperar decisión explícita |
|
|
123
|
+
|
|
124
|
+
#### Reglas duras
|
|
125
|
+
|
|
126
|
+
- **Reportar siempre, independientemente de la opción elegida**: el usuario
|
|
127
|
+
ve el hallazgo en el mismo turno, no se entera en el commit posterior.
|
|
128
|
+
- **DT formal NO es "lo apunto y veremos"**: requiere ID (`DT-NOMBRE-X`),
|
|
129
|
+
trigger verificable observable, plan de cierre con pasos concretos, y
|
|
130
|
+
entry visible en `.planning/DEUDA-TECNICA.md` commiteada en mismo turno.
|
|
131
|
+
- **NUNCA degradar Hallazgo C a Hallazgo B sin pedirlo**: una decisión
|
|
132
|
+
arquitectural disfrazada de DT es deuda silenciosa con cara de proceso.
|
|
133
|
+
- **NUNCA "arreglar como parte del trabajo principal" un Hallazgo B/C
|
|
134
|
+
sin etiquetarlo**: aunque el fix sea pequeño, si su blast radius es alto
|
|
135
|
+
el commit debe ser separado con mensaje explícito ("colateral: cierra
|
|
136
|
+
DT-X" o "colateral: aplica fix urgente fuera de scope original").
|
|
137
|
+
|
|
138
|
+
#### Ejemplo validado (SIGAF, sesión 2026-05-20)
|
|
139
|
+
|
|
140
|
+
Durante implementación de pipeline DevSecOps (gates gitleaks + SAST + deps
|
|
141
|
+
+ containers), el agente detectó tres hallazgos colaterales:
|
|
142
|
+
|
|
143
|
+
- **Hallazgo A — Validator JWT con frozenset + regex**: bug detectado en
|
|
144
|
+
`backend/app/core/config.py` donde `_CENTINELA` hardcodeado divergía del
|
|
145
|
+
`.env.example` real. Fix < 30 min, reversible, alcance acotado a
|
|
146
|
+
validators. Aplicado en commit separado mismo turno + 8 tests de regresión.
|
|
147
|
+
|
|
148
|
+
- **Hallazgo B — DT-GHAS-HABILITAR**: detectado que repo PRIVATE en
|
|
149
|
+
organización sin GitHub Advanced Security responde 403 al upload SARIF.
|
|
150
|
+
Mitigación inmediata con `continue-on-error: true` en step de upload.
|
|
151
|
+
DT formal con trigger verificable: "equipo crece >2 personas, auditoría
|
|
152
|
+
externa, o licencia GHAS adquirida". Entry en `.planning/DEUDA-TECNICA.md`
|
|
153
|
+
con plan de cierre (eliminar `continue-on-error` cuando GHAS activo).
|
|
154
|
+
|
|
155
|
+
- **Hallazgo C — DT-HISTORIAL-ENV**: detectado que commit `203a603` en
|
|
156
|
+
historial git contenía `ADMIN_PASSWORD=Admin2026!` (ya rotado, ya en
|
|
157
|
+
`.gitignore`, pero presente en `git log -p`). Fix requiere `git
|
|
158
|
+
filter-branch` o `git filter-repo` (destructivo, irreversible para
|
|
159
|
+
colaboradores con clones locales). El agente escaló al usuario; usuario
|
|
160
|
+
respondió "estamos en desarrollo y etapa de pruebas" → degradado a DT
|
|
161
|
+
formal con trigger "antes del primer deploy productivo, repo público
|
|
162
|
+
o colaborador externo".
|
|
163
|
+
|
|
164
|
+
Los tres hallazgos quedaron visibles, etiquetados y con trigger observable.
|
|
165
|
+
Ninguno se mezcló silenciosamente con el trabajo principal del pipeline.
|
|
166
|
+
|
|
167
|
+
#### Anti-patrones específicos
|
|
168
|
+
|
|
169
|
+
- **"Lo arreglo de paso porque ya estoy aquí"**: si el fix tiene blast
|
|
170
|
+
radius alto, NO va de paso. Va etiquetado o no va.
|
|
171
|
+
- **DT sin trigger verificable**: "cuando sea posible", "más adelante",
|
|
172
|
+
"cuando tengamos tiempo" — viola la regla general arriba. Trigger debe
|
|
173
|
+
ser condición observable.
|
|
174
|
+
- **Reportar Hallazgo C como informativo sin pedir decisión**: si la
|
|
175
|
+
decisión requiere autorización del usuario, la respuesta NO es
|
|
176
|
+
"documentado para tu consideración" — es "elige A, B o C".
|
|
177
|
+
- **Aplicar Hallazgo A descubriendo en medio que era Hallazgo C**: si al
|
|
178
|
+
empezar el fix detectas que tiene blast radius mayor del estimado,
|
|
179
|
+
detente, revierte el WIP, y re-clasifica. NO terminar "porque ya
|
|
180
|
+
empezamos".
|
|
181
|
+
|
|
89
182
|
---
|
|
90
183
|
|
|
91
184
|
## Excepciones legítimas
|
|
@@ -112,6 +112,44 @@ agent-browser, @dbml/core, likec4, etc.) debe documentarse en
|
|
|
112
112
|
7. **Limitaciones conocidas**.
|
|
113
113
|
8. **Origen** (ADR, sesión, paper, repo).
|
|
114
114
|
|
|
115
|
+
### Para prosa cuantificada en campos descriptivos de manifiestos JSON
|
|
116
|
+
|
|
117
|
+
Los campos `description` de `package.json` y `plugin.json` contienen prosa
|
|
118
|
+
con cifras agregadas del sistema (típicamente "60 agentes + N habilidades +
|
|
119
|
+
M comandos + K reglas + L hooks"). Esa prosa es **fuente de verdad parcial
|
|
120
|
+
para usuarios de npm y de Claude Code marketplace** — npm muestra el
|
|
121
|
+
description en la página del paquete; Claude Code lo lee al listar plugins.
|
|
122
|
+
|
|
123
|
+
Estos campos NO son agregados estructurales que los gates estándar
|
|
124
|
+
auditen (los gates escanean `.md` y comparan contra `INVENTARIO.md`),
|
|
125
|
+
por lo que necesitan tratamiento explícito:
|
|
126
|
+
|
|
127
|
+
1. **Universo a chequear**: `package.json#description` Y `plugin.json#description`.
|
|
128
|
+
Ambos deben extraerse, parsearse para sus cifras, y compararse entre sí.
|
|
129
|
+
|
|
130
|
+
2. **Verificación cross-manifest**: las cifras de
|
|
131
|
+
`package.json#description` deben coincidir letra por letra con las de
|
|
132
|
+
`plugin.json#description`. Cualquier divergencia es bug de manifiesto
|
|
133
|
+
inconsistente.
|
|
134
|
+
|
|
135
|
+
3. **Verificación vs fuente de verdad estructural**: las cifras de ambos
|
|
136
|
+
manifiestos deben coincidir con los conteos reales que reporta
|
|
137
|
+
`INVENTARIO.md` (regenerado por `node scripts/generar-inventario.js`).
|
|
138
|
+
|
|
139
|
+
4. **Gate automatizado**: `scripts/verificar-release.js` incluye gate
|
|
140
|
+
"Gate de description" desde v1.6.4 que ejecuta exactamente esa
|
|
141
|
+
triple validación. Confiar en su exit code antes de release.
|
|
142
|
+
|
|
143
|
+
**Anti-patrón documentado**: actualizar `plugin.json#description` con
|
|
144
|
+
cifras nuevas tras agregar 2 skills, asumir que `package.json#description`
|
|
145
|
+
está alineado por simetría, y commitear sin verificar. Origen del bug
|
|
146
|
+
v1.6.4 (sesión 2026-05-18): el commit del ADR-0028 actualizó
|
|
147
|
+
`plugin.json#description` (171/69/42 + ADR-0028) pero dejó
|
|
148
|
+
`package.json#description` con valores stale de v1.6.2 (162/67/41 +
|
|
149
|
+
ADR-0025). Los 4 gates pre-existentes no lo detectaron porque ninguno
|
|
150
|
+
escaneaba campos JSON descriptivos. El gate dedicado se agregó tras este
|
|
151
|
+
bug; la regla queda anti-recurrente.
|
|
152
|
+
|
|
115
153
|
---
|
|
116
154
|
|
|
117
155
|
## Procedimiento al ejecutar una auditoría documental
|
|
@@ -46,6 +46,7 @@ listados según el tipo:
|
|
|
46
46
|
| **Schema** (`schemas/X.schema.json`) | `INVENTARIO.md`, `SALUD.md`. Si valida frontmatter de algún componente, actualizar `scripts/validar.js` para que lo use | `node scripts/generar-inventario.js` |
|
|
47
47
|
| **Plantilla** (`plantillas/X.md`) | `manifiestos/modulos.json` si la copia el instalador, `INVENTARIO.md` | `node scripts/generar-inventario.js` |
|
|
48
48
|
| **Bump de versión** (`package.json`) | 15+ ubicaciones — usar checklist en `/swl:release` paso 6, ejecutar `node scripts/verificar-release.js` para verificar sincronización | `node scripts/verificar-release.js` |
|
|
49
|
+
| **Cifras en `description` de manifiestos** (cualquier cambio de conteo de agentes / skills / comandos / reglas / hooks) | **Ambos** `package.json#description` Y `plugin.json#description` con cifras nuevas. **NO tocar uno sin tocar el otro.** Adicionalmente: la frase "60 agentes + N habilidades + M comandos + K reglas + L hooks" en `README.md`, `CLAUDE.md`, `MANUAL_USO.md`, `COMANDOS.md`, `INSTALACION.md`, `MAPEO_SKILLS_AGENTES.md` debe actualizarse con las cifras nuevas. | `node scripts/verificar-release.js` (gate "description" detecta drift cross-manifest + drift vs INVENTARIO.md) |
|
|
49
50
|
|
|
50
51
|
**Ambos manifiestos cuando aplique** son obligatorios. Si un hook se registra
|
|
51
52
|
solo en `.claude/settings.json` pero no en `manifiestos/hooks-config.json`, el
|
|
@@ -133,6 +134,19 @@ Falso. La regla `git-workflow.md` exige commits atómicos. Un componente
|
|
|
133
134
|
nuevo + su registro = un commit atómico. Mezclar varios componentes nuevos
|
|
134
135
|
en un commit es violación separada.
|
|
135
136
|
|
|
137
|
+
### Description stale de un manifiesto que el otro sí actualizó
|
|
138
|
+
|
|
139
|
+
Olvidar editar `package.json#description` cuando se actualizó
|
|
140
|
+
`plugin.json#description` (o viceversa). Origen del bug v1.6.4: el commit del
|
|
141
|
+
ADR-0028 actualizó `plugin.json#description` con cifras nuevas (171 skills, 69
|
|
142
|
+
reglas, 42 hooks, ADR-0028) pero dejó `package.json#description` con valores
|
|
143
|
+
de v1.6.2 (162/67/41, ADR-0025). El gate de versión no lo detectó porque solo
|
|
144
|
+
chequea `version`; el gate de contadores no lo detectó porque solo escanea
|
|
145
|
+
`.md`. Ahora hay gate dedicado (`ejecutarGateDescription` en
|
|
146
|
+
`scripts/verificar-release.js`) que detecta drift cross-manifest. Antes de
|
|
147
|
+
commitear cualquier cambio que afecte conteos, ejecutar
|
|
148
|
+
`node scripts/verificar-release.js` y mirar la sección "Gate de description".
|
|
149
|
+
|
|
136
150
|
### Confiar en `node -e` para validar conteos sin re-leer manifiestos
|
|
137
151
|
|
|
138
152
|
Falso. `node -e "console.log(require('./plugin.json').skills.length)"`
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Regla: Cleanup obligatorio de directorios temporales en tests
|
|
2
|
+
|
|
3
|
+
Esta regla es **OBLIGATORIA** y aplica a todo test Node.js del sistema SWL
|
|
4
|
+
que cree directorios temporales bajo `os.tmpdir()` (típicamente
|
|
5
|
+
`C:\Users\<usuario>\AppData\Local\Temp` en Windows, `/tmp` en POSIX).
|
|
6
|
+
|
|
7
|
+
Promueve a regla obligatoria el aprendizaje L1 #19 + entrada extensa de
|
|
8
|
+
APRENDIZAJES.md (2026-05-17 "Tests sin cleanup acumularon 5,800+ carpetas
|
|
9
|
+
en %TEMP%") tras reincidencia documentada el 2026-05-18 en
|
|
10
|
+
`tests/hooks/validar-intent-spec.test.js` (192 residuos acumulados).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Principio
|
|
15
|
+
|
|
16
|
+
> Todo test que necesite un directorio temporal DEBE usar
|
|
17
|
+
> `setupSandboxes(prefix)` de `tests/_helpers/sandbox.js`. NUNCA
|
|
18
|
+
> `fs.mkdtempSync(path.join(os.tmpdir(), ...))` directo.
|
|
19
|
+
|
|
20
|
+
El helper registra automáticamente `after()` del módulo `node:test` para
|
|
21
|
+
limpiar al final del archivo. Sin él, cada corrida de `npm test:all` deja
|
|
22
|
+
N carpetas residuales (N = número de tests × archivos sin cleanup).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Patrón obligatorio
|
|
27
|
+
|
|
28
|
+
### MAL — `mkdtempSync` directo sin cleanup
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const os = require('node:os');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
const { test } = require('node:test');
|
|
35
|
+
|
|
36
|
+
function crearSandbox() {
|
|
37
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'mi-test-'));
|
|
38
|
+
// ✗ Nunca se limpia. Acumula residuos en %TEMP%.
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('hace algo', () => {
|
|
42
|
+
const dir = crearSandbox();
|
|
43
|
+
// ... usar dir ...
|
|
44
|
+
// ✗ Sin try/finally con rmSync.
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### BIEN — `setupSandboxes` con cleanup automático
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const { test } = require('node:test');
|
|
52
|
+
const { setupSandboxes } = require('../_helpers/sandbox');
|
|
53
|
+
|
|
54
|
+
const sandboxes = setupSandboxes('mi-test-');
|
|
55
|
+
// ✓ setupSandboxes registra after() global del archivo.
|
|
56
|
+
|
|
57
|
+
test('hace algo', () => {
|
|
58
|
+
const dir = sandboxes.create();
|
|
59
|
+
// ... usar dir ...
|
|
60
|
+
// ✓ Cleanup automático al terminar el archivo de tests.
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Cuándo aplicar
|
|
67
|
+
|
|
68
|
+
OBLIGATORIO en:
|
|
69
|
+
|
|
70
|
+
- Cualquier archivo `tests/**/*.test.js` que cree dirs temporales para
|
|
71
|
+
aislar el test del CWD del proyecto.
|
|
72
|
+
- Tests de hooks que necesitan sandbox con `.planning/`, `agentes/`, etc.
|
|
73
|
+
- Tests de scripts que escriben archivos generados (`generar-*`,
|
|
74
|
+
`auditar-*`).
|
|
75
|
+
- Cualquier helper de fixtures que cree filesystem temporal.
|
|
76
|
+
|
|
77
|
+
NO aplicar (excepciones legítimas):
|
|
78
|
+
|
|
79
|
+
- Tests que NO crean dirs temporales (unit tests puros con stubs/mocks).
|
|
80
|
+
- Tests que usan `process.chdir` sin crear directorio nuevo (raros).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Convención del prefijo
|
|
85
|
+
|
|
86
|
+
El prefijo pasado a `setupSandboxes(prefix)` DEBE:
|
|
87
|
+
|
|
88
|
+
1. Empezar con `swl-` para identificar el origen del proyecto.
|
|
89
|
+
2. Incluir el nombre del componente bajo test (corto).
|
|
90
|
+
3. Terminar con guión final (el helper NO lo añade automáticamente).
|
|
91
|
+
|
|
92
|
+
Ejemplos válidos:
|
|
93
|
+
|
|
94
|
+
- `setupSandboxes('swl-mi-hook-')`
|
|
95
|
+
- `setupSandboxes('swl-claudemd-')`
|
|
96
|
+
- `setupSandboxes('swl-evolucion-')`
|
|
97
|
+
|
|
98
|
+
Ejemplos inválidos:
|
|
99
|
+
|
|
100
|
+
- `setupSandboxes('test')` — sin prefijo `swl-`, indistinguible de otros proyectos.
|
|
101
|
+
- `setupSandboxes('swl-hook')` — falta guión final, `mkdtemp` añade chars random pegados al nombre.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Casos especiales: `process.chdir` en tests
|
|
106
|
+
|
|
107
|
+
Si el test hace `process.chdir(sandbox)` en top-level, registrar
|
|
108
|
+
**ANTES** de `setupSandboxes()` un `after()` que restaure el CWD:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const originalCwd = process.cwd();
|
|
112
|
+
const { after } = require('node:test');
|
|
113
|
+
after(() => process.chdir(originalCwd));
|
|
114
|
+
// ↑ chdir-restore registrado primero
|
|
115
|
+
const sandboxes = setupSandboxes('swl-mi-test-');
|
|
116
|
+
// ↑ cleanup de sandboxes registrado segundo
|
|
117
|
+
// FIFO en node:test: restaurar CWD primero, luego limpiar dirs.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Sin esto, Windows EBUSY al intentar `rmSync` un directorio que es el CWD activo.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Verificación
|
|
125
|
+
|
|
126
|
+
### Test de regresión post-suite
|
|
127
|
+
|
|
128
|
+
Tras ejecutar `npm test:all`, verificar 0 residuos:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Bash
|
|
132
|
+
ls "$LOCALAPPDATA/Temp" 2>/dev/null | grep -c "^swl-" || echo "0"
|
|
133
|
+
|
|
134
|
+
# PowerShell
|
|
135
|
+
(Get-ChildItem $env:TEMP -Directory -Filter "swl-*").Count
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Resultado esperado: `0`.
|
|
139
|
+
|
|
140
|
+
### Auditoría continua del repo
|
|
141
|
+
|
|
142
|
+
Para detectar archivos de test que ignoran la regla:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Archivos con mkdtempSync directo SIN usar el helper
|
|
146
|
+
grep -rlE "mkdtempSync\(.*os\.tmpdir|fs\.mkdtempSync" tests/ \
|
|
147
|
+
| grep -v "_helpers" \
|
|
148
|
+
| xargs -I {} grep -L "setupSandboxes" {}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Resultado esperado tras migración completa: `0` archivos.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Anti-patrones explícitos
|
|
156
|
+
|
|
157
|
+
- **Copiar de un test "legacy" como referencia**: si el patrón a copiar
|
|
158
|
+
usa `mkdtempSync` directo, el nuevo test heredará el bug. Verificar
|
|
159
|
+
que el test fuente use `setupSandboxes` antes de copiar como
|
|
160
|
+
plantilla.
|
|
161
|
+
- **`afterEach` con `rmSync` manual cuando ya existe `setupSandboxes`**:
|
|
162
|
+
duplica responsabilidad. Si necesitas cleanup por test (no por
|
|
163
|
+
archivo), el helper expone `sandboxes.cleanup()` invocable.
|
|
164
|
+
- **Path construido manualmente** con `path.join(os.tmpdir(),
|
|
165
|
+
\`prefix-${Date.now()}\`)` + `mkdirSync`: escapa al regex de
|
|
166
|
+
auditoría que busca `mkdtempSync`. Si el patrón aparece, refactorizar
|
|
167
|
+
a `setupSandboxes` igual.
|
|
168
|
+
- **Ignorar Windows EBUSY**: si en CI Linux pasa pero localmente
|
|
169
|
+
Windows falla con `EBUSY: resource busy or locked`, casi siempre es
|
|
170
|
+
cleanup intentando borrar el CWD. Aplicar el patrón `chdir-restore
|
|
171
|
+
ANTES de setupSandboxes`.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Excepciones documentadas
|
|
176
|
+
|
|
177
|
+
Si un test legítimamente NO puede usar `setupSandboxes` (caso raro), DEBE:
|
|
178
|
+
|
|
179
|
+
1. Documentar en comentario al inicio del archivo por qué no aplica.
|
|
180
|
+
2. Implementar cleanup manual con `after()` + `try/finally`.
|
|
181
|
+
3. Incluir test de regresión: tras el test, `Get-ChildItem $env:TEMP` no debe mostrar el dir.
|
|
182
|
+
|
|
183
|
+
Sin esos 3 elementos, el test será marcado como violación de la regla
|
|
184
|
+
en `/swl:revisar`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Origen
|
|
189
|
+
|
|
190
|
+
- **Aprendizaje L1 #19** (2026-05-17): "Test nuevo que necesita
|
|
191
|
+
directorio temporal | Usar `setupSandboxes('prefix-')` de
|
|
192
|
+
`tests/_helpers/sandbox.js`; nunca `fs.mkdtempSync` directo sin
|
|
193
|
+
cleanup".
|
|
194
|
+
- **Entrada extensa APRENDIZAJES.md líneas 6793-6852**: el usuario
|
|
195
|
+
descubrió manualmente más de 5,800 carpetas con prefijo `swl-*` en
|
|
196
|
+
`C:\Users\Saul\AppData\Local\Temp` y las limpió con CCleaner.
|
|
197
|
+
Investigación reveló 14 archivos de test sin cleanup en refactor PR #35.
|
|
198
|
+
- **Reincidencia 2026-05-18**: tras refactor de 14 archivos quedaron
|
|
199
|
+
32 sin migrar. Yo creé `tests/hooks/validar-intent-spec.test.js`
|
|
200
|
+
copiando patrón de `claudemd-bloat-detector.test.js` (no migrado) →
|
|
201
|
+
192 residuos en 1 sesión. La regla estaba en APRENDIZAJES.md pero NO
|
|
202
|
+
era obligatoria en `reglas/` ni `~/.claude/rules/pruebas.md`.
|
|
203
|
+
|
|
204
|
+
Esta regla cierra ese gap promoviendo el aprendizaje a regla obligatoria
|
|
205
|
+
del proyecto + actualización del skill `tdd-workflow` para enseñar el
|
|
206
|
+
patrón correcto.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Relación con otras reglas
|
|
211
|
+
|
|
212
|
+
- `~/.claude/rules/pruebas.md § Tests deterministas — sin sleep ni delays`
|
|
213
|
+
— esta regla extiende el principio "Limpiar estado en teardown" al
|
|
214
|
+
caso específico de directorios temporales.
|
|
215
|
+
- `~/.claude/rules/arreglar-al-detectar.md` — la deuda silenciosa que
|
|
216
|
+
esta regla cierra es exactamente el tipo de deuda que esa regla
|
|
217
|
+
prohíbe acumular.
|
|
218
|
+
- `reglas/registro-componentes-nuevos.md` — al crear un test nuevo se
|
|
219
|
+
considera componente nuevo del sistema; la regla aplica como parte
|
|
220
|
+
del registro.
|
package/scripts/instalador.js
CHANGED
|
@@ -42,6 +42,26 @@ const { actualizarGitignore, entradasParaRuntime, limpiarTracked, leerManifest,
|
|
|
42
42
|
const RAIZ_PKG = path.resolve(__dirname, '..');
|
|
43
43
|
const VERSION = require('../package.json').version;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Devuelve un nombre legible para mostrar al usuario en la lista de
|
|
47
|
+
* componentes evolucionados. Cuando el archivo se llama "SKILL.md" (caso
|
|
48
|
+
* canónico de habilidades), usa el nombre del directorio padre para que
|
|
49
|
+
* el output muestre `tdd-workflow/SKILL.md` en lugar de `SKILL.md` repetido
|
|
50
|
+
* N veces sin identificación posible.
|
|
51
|
+
*
|
|
52
|
+
* FIX v1.6.6 — antes el output mostraba 49 `SKILL.md` indistinguibles.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} rutaAbs - Ruta absoluta del archivo evolucionado.
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function nombreLegibleEvolucion(rutaAbs) {
|
|
58
|
+
const base = path.basename(rutaAbs);
|
|
59
|
+
if (base === 'SKILL.md') {
|
|
60
|
+
return path.basename(path.dirname(rutaAbs)) + '/SKILL.md';
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
/**
|
|
46
66
|
* Contrato adicional: `opciones.onProgress(evento)`
|
|
47
67
|
*
|
|
@@ -201,6 +221,18 @@ async function install(opciones) {
|
|
|
201
221
|
stackDetectado = detectarStack(process.cwd());
|
|
202
222
|
}
|
|
203
223
|
|
|
224
|
+
// FIX v1.6.6: si el usuario pasa --all-langs pero el perfil actual no incluye
|
|
225
|
+
// reglas/lenguajes/, el flag se ignora silenciosamente. Antes la única pista
|
|
226
|
+
// era el conteo final de archivos. Ahora emitimos warning explícito.
|
|
227
|
+
if (allLangs && !tieneReglasLenguaje) {
|
|
228
|
+
console.log(
|
|
229
|
+
'\n[stack] Aviso: --all-langs ignorado — el perfil actual (' +
|
|
230
|
+
(resolucion.perfil || 'core') +
|
|
231
|
+
') no incluye reglas de lenguajes. ' +
|
|
232
|
+
'Usa --perfil completo o --perfil polyglot para activar las reglas por lenguaje.'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
204
236
|
if (tieneReglasLenguaje) {
|
|
205
237
|
if (allLangs) {
|
|
206
238
|
console.log('\n[stack] --all-langs activado: se instalan reglas de todos los lenguajes.');
|
|
@@ -454,9 +486,28 @@ async function install(opciones) {
|
|
|
454
486
|
if (evolved.length > 0) {
|
|
455
487
|
console.log(`[evolución] ${evolved.length} componente(s) evolucionado(s) detectado(s):`);
|
|
456
488
|
for (const e of evolved) {
|
|
457
|
-
console.log(` ★ ${
|
|
489
|
+
console.log(` ★ ${nombreLegibleEvolucion(e.path)} (por ${e.evolvedBy || 'auto-evolución'}, desde v${e.evolvedFrom || '?'})`);
|
|
458
490
|
}
|
|
459
491
|
console.log('');
|
|
492
|
+
|
|
493
|
+
// FIX v1.6.6: avisar al usuario cuando el target copia habilidades como
|
|
494
|
+
// directorio completo (Cursor, Codex). Ese path no pasa por
|
|
495
|
+
// decideUpdateStrategy a nivel de SKILL.md, así que las evoluciones se
|
|
496
|
+
// sobreescriben silenciosamente. Cierre completo del gap = DT-EVOL-DIR
|
|
497
|
+
// documentado en .planning/DEUDA-TECNICA.md.
|
|
498
|
+
const targetCopiaDirectorios = ['cursor', 'codex'].includes(target);
|
|
499
|
+
const tieneSkillEvolucionado = evolved.some((e) => path.basename(e.path) === 'SKILL.md');
|
|
500
|
+
if (targetCopiaDirectorios && tieneSkillEvolucionado) {
|
|
501
|
+
console.log(
|
|
502
|
+
` ⚠ Aviso: en target "${target}" las habilidades se copian como directorio. Las ` +
|
|
503
|
+
`evoluciones de SKILL.md serán sobreescritas. Backup de seguridad creado en ` +
|
|
504
|
+
`.planning/backups/v${versionAnterior || 'previa'}/.`
|
|
505
|
+
);
|
|
506
|
+
console.log(
|
|
507
|
+
' (Ver DT-EVOL-DIR en .planning/DEUDA-TECNICA.md para el plan de cierre.)'
|
|
508
|
+
);
|
|
509
|
+
console.log('');
|
|
510
|
+
}
|
|
460
511
|
}
|
|
461
512
|
}
|
|
462
513
|
|
|
@@ -741,10 +792,14 @@ async function install(opciones) {
|
|
|
741
792
|
console.log(' ~ Notificaciones Telegram: omitido en modo no-interactivo (usar /swl:notificaciones init para activarlo después).');
|
|
742
793
|
} else {
|
|
743
794
|
// TTY → flujo interactivo
|
|
795
|
+
// FIX v1.6.6: si el install corre con --force, pasar asumirReusar
|
|
796
|
+
// para que el prompt "¿Reusar credenciales?" se salte automáticamente.
|
|
797
|
+
// Antes el flujo multi-target generaba un prompt por cada target.
|
|
744
798
|
const resInteractivo = await notif.init({
|
|
745
|
-
esTty:
|
|
746
|
-
hooksDir:
|
|
747
|
-
dryRun:
|
|
799
|
+
esTty: true,
|
|
800
|
+
hooksDir: rutas.hooks || null,
|
|
801
|
+
dryRun: dryRun,
|
|
802
|
+
asumirReusar: force === true,
|
|
748
803
|
});
|
|
749
804
|
if (resInteractivo.resultado === 'completado') {
|
|
750
805
|
console.log(' + Notificaciones Telegram configuradas.');
|
|
@@ -831,6 +886,19 @@ async function install(opciones) {
|
|
|
831
886
|
}
|
|
832
887
|
}
|
|
833
888
|
|
|
889
|
+
// FIX v1.6.6: invalidar el flag de throttle de check-update.js para que el
|
|
890
|
+
// próximo `doctor` reporte la versión instalada actual y no la cacheada de
|
|
891
|
+
// hasta hace 24h. Sin esto, el doctor justo tras un upgrade dice cosas
|
|
892
|
+
// engañosas como "local=1.6.4, remota=1.6.3, hace 2.8h" cuando el binario
|
|
893
|
+
// que corre ya es 1.6.5.
|
|
894
|
+
try {
|
|
895
|
+
const os = require('os');
|
|
896
|
+
const flagPath = process.env.SWL_UPDATE_FLAG_PATH || path.join(os.tmpdir(), 'swl-ses-update-check.json');
|
|
897
|
+
if (fs.existsSync(flagPath)) {
|
|
898
|
+
fs.unlinkSync(flagPath);
|
|
899
|
+
}
|
|
900
|
+
} catch { /* nunca bloquear install por esto */ }
|
|
901
|
+
|
|
834
902
|
console.log('\nSiguiente paso:');
|
|
835
903
|
console.log(' npx @saulwade/swl-ses@latest doctor');
|
|
836
904
|
console.log('');
|
|
@@ -23,6 +23,7 @@ seccion "Code style": settings.local.json sobrescribe por key, no reemplaza.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
|
+
import os
|
|
26
27
|
import sys
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Any
|
|
@@ -30,9 +31,9 @@ from typing import Any
|
|
|
30
31
|
# Orden de capas: base -> override. La ultima capa gana por clave al hacer
|
|
31
32
|
# merge, igual que el deep merge interno de Claude Code.
|
|
32
33
|
CAPAS_DEFAULT = (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
"mcp-servers.json",
|
|
35
|
+
".claude/settings.json",
|
|
36
|
+
".claude/settings.local.json",
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
|
@@ -45,10 +46,10 @@ def _leer_servers(ruta: Path) -> dict:
|
|
|
45
46
|
if not ruta.exists():
|
|
46
47
|
return {}
|
|
47
48
|
try:
|
|
48
|
-
data = json.loads(ruta.read_text(encoding=
|
|
49
|
+
data = json.loads(ruta.read_text(encoding="utf-8"))
|
|
49
50
|
except (OSError, json.JSONDecodeError):
|
|
50
51
|
return {}
|
|
51
|
-
servers = data.get(
|
|
52
|
+
servers = data.get("mcpServers")
|
|
52
53
|
if not isinstance(servers, dict):
|
|
53
54
|
return {}
|
|
54
55
|
return servers
|
|
@@ -71,12 +72,14 @@ def _fusionar_servidor(base: dict, override: dict) -> dict:
|
|
|
71
72
|
resultado: dict[str, Any] = dict(base)
|
|
72
73
|
|
|
73
74
|
for clave, valor in override.items():
|
|
74
|
-
if clave ==
|
|
75
|
-
env_base =
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
if clave == "env" and isinstance(valor, dict):
|
|
76
|
+
env_base = (
|
|
77
|
+
resultado.get("env") if isinstance(resultado.get("env"), dict) else {}
|
|
78
|
+
)
|
|
79
|
+
resultado["env"] = {**env_base, **valor}
|
|
80
|
+
elif clave == "args":
|
|
78
81
|
# args reemplaza, no mergea: una lista parcial seria peligrosa.
|
|
79
|
-
resultado[
|
|
82
|
+
resultado["args"] = valor
|
|
80
83
|
elif isinstance(valor, dict) and isinstance(resultado.get(clave), dict):
|
|
81
84
|
resultado[clave] = {**resultado[clave], **valor}
|
|
82
85
|
else:
|
|
@@ -111,17 +114,29 @@ def cargar_config_mcp(cwd: Path, config_path: str | None = None) -> dict:
|
|
|
111
114
|
return fusionado
|
|
112
115
|
|
|
113
116
|
|
|
117
|
+
def build_stdio_env(cfg: dict) -> dict:
|
|
118
|
+
"""Entorno para StdioServerParameters: siempre hereda os.environ.
|
|
119
|
+
|
|
120
|
+
Con env: {} en settings.json, devolver None hacia el SDK MCP dejaba al
|
|
121
|
+
subproceso sin variables del padre (p. ej. OBSIDIAN_API_KEY vía setx).
|
|
122
|
+
"""
|
|
123
|
+
extra = cfg.get("env") if isinstance(cfg.get("env"), dict) else {}
|
|
124
|
+
merged = dict(os.environ)
|
|
125
|
+
merged.update(extra)
|
|
126
|
+
return merged
|
|
127
|
+
|
|
128
|
+
|
|
114
129
|
def _main(argv: list[str]) -> int:
|
|
115
130
|
"""Entry point CLI: imprime el JSON fusionado para uso en tests."""
|
|
116
|
-
cwd = Path(argv[0]) if argv else Path(
|
|
131
|
+
cwd = Path(argv[0]) if argv else Path(".")
|
|
117
132
|
if not cwd.is_dir():
|
|
118
|
-
sys.stderr.write(f
|
|
133
|
+
sys.stderr.write(f"[mcp_config] CWD no es directorio: {cwd}\n")
|
|
119
134
|
return 1
|
|
120
135
|
resultado = cargar_config_mcp(cwd)
|
|
121
136
|
sys.stdout.write(json.dumps(resultado, indent=2, ensure_ascii=False))
|
|
122
|
-
sys.stdout.write(
|
|
137
|
+
sys.stdout.write("\n")
|
|
123
138
|
return 0
|
|
124
139
|
|
|
125
140
|
|
|
126
|
-
if __name__ ==
|
|
141
|
+
if __name__ == "__main__":
|
|
127
142
|
sys.exit(_main(sys.argv[1:]))
|
|
@@ -446,6 +446,11 @@ async function init(opciones = {}) {
|
|
|
446
446
|
sobreescribir: sobreescribir = false,
|
|
447
447
|
dryRun: dryRun = false,
|
|
448
448
|
hooksDir: hooksDir = null,
|
|
449
|
+
// FIX v1.6.6: si el caller pasa `asumirReusar: true` (típicamente cuando
|
|
450
|
+
// el install corre con --force), omitir el prompt interactivo "¿Reusar
|
|
451
|
+
// las credenciales existentes?" y asumir sí. Evita el cuelgue en CI y
|
|
452
|
+
// el prompt redundante en multi-target install (3 targets = 3 prompts).
|
|
453
|
+
asumirReusar: asumirReusar = false,
|
|
449
454
|
// Overrides para tests — permiten inyectar directorios temporales
|
|
450
455
|
_hooksOrigenOverride: _hooksOrigenOverride = null,
|
|
451
456
|
_hooksGlobalDirOverride: _hooksGlobalDirOverride = null,
|
|
@@ -483,6 +488,15 @@ async function init(opciones = {}) {
|
|
|
483
488
|
if (!modoHeadless && esTty) {
|
|
484
489
|
// Detectar .env preexistente
|
|
485
490
|
if (fs.existsSync(envPath) && !sobreescribir) {
|
|
491
|
+
// FIX v1.6.6: si el caller pasó asumirReusar=true (e.g. install --force),
|
|
492
|
+
// saltar el prompt y reusar directamente. Evita cuelgue en CI y prompt
|
|
493
|
+
// redundante en multi-target install.
|
|
494
|
+
if (asumirReusar) {
|
|
495
|
+
console.log('');
|
|
496
|
+
console.log(' [notificaciones] Reusando credenciales existentes en ~/.claude/notifications/.env (--force).');
|
|
497
|
+
return _soloMergeSettings(_hooksGlobalDirOverride, dryRun, _settingsPathOverride);
|
|
498
|
+
}
|
|
499
|
+
|
|
486
500
|
console.log('');
|
|
487
501
|
console.log(' [notificaciones] Ya existe ~/.claude/notifications/.env con credenciales.');
|
|
488
502
|
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|