@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.
Files changed (46) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/agentes/gh-fix-ci-swl.md +275 -0
  4. package/agentes/nemesis-auditor-swl.md +90 -1
  5. package/comandos/swl/exportar-vault.md +106 -14
  6. package/comandos/swl/nemesis.md +70 -3
  7. package/comandos/swl/release.md +62 -2
  8. package/comandos/swl/salud.md +32 -0
  9. package/comandos/swl/verificar.md +116 -2
  10. package/habilidades/agent-browser/SKILL.md +111 -4
  11. package/habilidades/agent-deep-links/SKILL.md +148 -0
  12. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  13. package/habilidades/backend-error-design/SKILL.md +221 -0
  14. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  15. package/habilidades/browser-research-domains/SKILL.md +635 -0
  16. package/habilidades/changelog-generator/SKILL.md +172 -0
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  18. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  19. package/habilidades/fastapi-experto/SKILL.md +49 -4
  20. package/habilidades/harness-claude-code/SKILL.md +4 -1
  21. package/habilidades/postgresql-experto/SKILL.md +80 -4
  22. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  23. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  24. package/habilidades/tdd-workflow/SKILL.md +12 -5
  25. package/hooks/extraccion-aprendizajes.js +8 -0
  26. package/hooks/lib/deep-links.js +185 -0
  27. package/hooks/lib/evolution-tracker.js +148 -20
  28. package/hooks/lib/gateway-notify.js +70 -7
  29. package/manifiestos/modulos.json +13 -3
  30. package/manifiestos/skills-lock.json +1247 -1191
  31. package/package.json +92 -92
  32. package/plugin.json +371 -362
  33. package/reglas/arquitectura.md +38 -0
  34. package/reglas/arreglar-al-detectar.md +93 -0
  35. package/reglas/auditorias-documentales-estructurales.md +38 -0
  36. package/reglas/registro-componentes-nuevos.md +14 -0
  37. package/reglas/tests-cleanup.md +220 -0
  38. package/scripts/instalador.js +72 -4
  39. package/scripts/lib/mcp_config.py +29 -14
  40. package/scripts/lib/notificaciones-telegram.js +14 -0
  41. package/scripts/lib/transformadores/codex.js +4 -0
  42. package/scripts/lib/transformadores/cursor.js +5 -0
  43. package/scripts/mcp-orchestrator.py +153 -131
  44. package/scripts/mcp-pool-manager.py +132 -107
  45. package/scripts/mcp-telemetry.py +139 -120
  46. package/scripts/verificar-release.js +199 -1
@@ -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.
@@ -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(` ★ ${path.basename(e.path)} (por ${e.evolvedBy || 'auto-evolución'}, desde v${e.evolvedFrom || '?'})`);
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: true,
746
- hooksDir: rutas.hooks || null,
747
- dryRun: 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
- 'mcp-servers.json',
34
- '.claude/settings.json',
35
- '.claude/settings.local.json',
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='utf-8'))
49
+ data = json.loads(ruta.read_text(encoding="utf-8"))
49
50
  except (OSError, json.JSONDecodeError):
50
51
  return {}
51
- servers = data.get('mcpServers')
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 == 'env' and isinstance(valor, dict):
75
- env_base = resultado.get('env') if isinstance(resultado.get('env'), dict) else {}
76
- resultado['env'] = {**env_base, **valor}
77
- elif clave == 'args':
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['args'] = valor
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'[mcp_config] CWD no es directorio: {cwd}\n')
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('\n')
137
+ sys.stdout.write("\n")
123
138
  return 0
124
139
 
125
140
 
126
- if __name__ == '__main__':
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 });