@saulwade/swl-ses 2.3.1 → 2.4.2

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 (123) hide show
  1. package/CLAUDE.md +242 -240
  2. package/README.md +597 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +690 -690
  6. package/agentes/arquitecto-swl.md +267 -267
  7. package/agentes/auto-evolucion-swl.md +908 -908
  8. package/agentes/backend-api-swl.md +472 -472
  9. package/agentes/backend-csharp-swl.md +420 -420
  10. package/agentes/backend-go-swl.md +390 -390
  11. package/agentes/backend-java-swl.md +281 -281
  12. package/agentes/backend-node-swl.md +479 -479
  13. package/agentes/backend-python-swl.md +605 -605
  14. package/agentes/backend-rust-swl.md +364 -364
  15. package/agentes/backend-workers-swl.md +482 -482
  16. package/agentes/cloud-infra-swl.md +509 -509
  17. package/agentes/consolidador-swl.md +541 -541
  18. package/agentes/datos-swl.md +605 -605
  19. package/agentes/depurador-swl.md +352 -352
  20. package/agentes/devops-ci-swl.md +400 -400
  21. package/agentes/disenador-ui-swl.md +569 -569
  22. package/agentes/documentador-swl.md +345 -345
  23. package/agentes/frontend-angular-swl.md +621 -621
  24. package/agentes/frontend-css-swl.md +716 -716
  25. package/agentes/frontend-react-swl.md +692 -692
  26. package/agentes/frontend-swl.md +496 -496
  27. package/agentes/frontend-tailwind-swl.md +826 -826
  28. package/agentes/gh-fix-ci-swl.md +275 -275
  29. package/agentes/implementador-swl.md +328 -328
  30. package/agentes/investigador-swl.md +432 -432
  31. package/agentes/investigador-ux-swl.md +505 -505
  32. package/agentes/llm-apps-swl.md +278 -278
  33. package/agentes/migrador-swl.md +442 -442
  34. package/agentes/mobile-android-swl.md +511 -511
  35. package/agentes/mobile-cross-swl.md +541 -541
  36. package/agentes/mobile-ios-swl.md +502 -502
  37. package/agentes/mobile-testing-swl.md +302 -302
  38. package/agentes/nemesis-auditor-swl.md +285 -285
  39. package/agentes/notificador-swl.md +918 -918
  40. package/agentes/observabilidad-swl.md +438 -438
  41. package/agentes/orquestador-swl.md +1053 -1026
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +245 -245
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +538 -505
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +291 -291
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/comandos/swl/contexto.md +0 -2
  66. package/comandos/swl/deuda-codigo.md +97 -0
  67. package/habilidades/angular-moderno/SKILL.md +5 -1
  68. package/habilidades/contenedores-docker/SKILL.md +3 -1
  69. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  70. package/habilidades/harness-claude-code/SKILL.md +5 -4
  71. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
  72. package/habilidades/legacy-code-rescue/SKILL.md +2 -1
  73. package/habilidades/meta-skills-estandar/SKILL.md +1 -1
  74. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
  75. package/habilidades/postgresql-experto/SKILL.md +3 -1
  76. package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
  77. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
  78. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
  79. package/habilidades/tdd-workflow/SKILL.md +3 -1
  80. package/hooks/audit-trail.js +4 -4
  81. package/hooks/calidad-pre-commit.js +15 -11
  82. package/hooks/captura-acciones-post.js +3 -2
  83. package/hooks/captura-acciones-session.js +3 -2
  84. package/hooks/contexto-iteracion.js +2 -1
  85. package/hooks/contexto-subagente.js +68 -0
  86. package/hooks/guardrail-modelo.js +13 -3
  87. package/hooks/inyeccion-contexto.js +12 -12
  88. package/hooks/registro-turnos.js +5 -4
  89. package/hooks/spec-gate.js +2 -1
  90. package/hooks/sugerir-contribuir.js +2 -1
  91. package/hooks/sugerir-regenerar-inventario.js +3 -2
  92. package/hooks/tdd-gate.js +2 -1
  93. package/llms.txt +3 -3
  94. package/manifiestos/canonical-hashes.json +995 -10
  95. package/manifiestos/hook-profiles.json +0 -2
  96. package/manifiestos/hooks-config.json +469 -477
  97. package/manifiestos/invariantes-criticos.json +30 -0
  98. package/manifiestos/modulos.json +1390 -1390
  99. package/manifiestos/skills-lock.json +24 -24
  100. package/package.json +93 -93
  101. package/plugin.json +369 -371
  102. package/reglas/arreglar-al-detectar.md +16 -0
  103. package/reglas/pruebas.md +7 -3
  104. package/schemas/agent-frontmatter.schema.json +3 -1
  105. package/scripts/actualizar.js +253 -254
  106. package/scripts/doctor.js +25 -17
  107. package/scripts/instalador.js +4 -1
  108. package/scripts/lib/detectar-runtime.js +58 -0
  109. package/scripts/lib/expandir-targets.js +71 -71
  110. package/scripts/lib/hooks-settings.js +40 -3
  111. package/scripts/lib/preservar-usuario.js +39 -5
  112. package/scripts/lib/toml-merge.js +204 -204
  113. package/scripts/mcp-server/auth.js +105 -105
  114. package/scripts/mcp-server/cache.js +106 -106
  115. package/scripts/tui/pantallas/inspect.js +175 -173
  116. package/scripts/tui/pantallas/uninstall-wizard.js +210 -208
  117. package/scripts/tui/pantallas/update-wizard.js +234 -232
  118. package/scripts/tui/pantallas/welcome.js +189 -187
  119. package/scripts/validar.js +1 -1
  120. package/scripts/verificar-release.js +51 -0
  121. package/hooks/lib/context-compressor.js +0 -657
  122. package/hooks/linea-estado.js +0 -328
  123. package/hooks/monitor-contexto.js +0 -373
package/scripts/doctor.js CHANGED
@@ -10,7 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
- const { detectarRuntimes, detectarInstalacionesDuales } = require('./lib/detectar-runtime');
13
+ const { detectarRuntimes, detectarInstalacionesDuales, esDirGlobalDeAlgunRuntime, scopesReales } = require('./lib/detectar-runtime');
14
14
  const { cargarEstado, verificarIntegridad } = require('./lib/estado');
15
15
  const { verificarHooks, repararHooks, rutaSettings, cargarHooksConfig, desregistrarHooks, esHookSWL, verificarOptimizaciones, registrarOptimizaciones, validarEstructuraSettings } = require('./lib/hooks-settings');
16
16
  const {
@@ -106,7 +106,10 @@ async function doctor(opciones = {}) {
106
106
  try {
107
107
  const runtimesParaDrift = detectarRuntimes();
108
108
  for (const runtime of runtimesParaDrift) {
109
- for (const dir of [runtime.global, path.resolve(runtime.local)]) {
109
+ // Dedup + guard anti-fantasma: desde HOME, global y local colapsan al
110
+ // mismo dir (reporte doble) o el local resuelve al GLOBAL de otro
111
+ // runtime (drift "proyecto" espurio). Caso real doctor 2026-07-03.
112
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
110
113
  if (!fs.existsSync(dir)) continue;
111
114
  const estado = cargarEstado(dir);
112
115
  if (!estado || !estado.versionSistema) continue;
@@ -115,7 +118,7 @@ async function doctor(opciones = {}) {
115
118
  driftsDetectados.push({
116
119
  runtime: runtime.nombre,
117
120
  runtimeId: runtime.id,
118
- scope: dir === runtime.global ? 'global' : 'proyecto',
121
+ scope: esGlobal ? 'global' : 'proyecto',
119
122
  dir,
120
123
  versionInstalada: estado.versionSistema,
121
124
  perfil: estado.perfil || 'desconocido',
@@ -259,13 +262,10 @@ async function doctor(opciones = {}) {
259
262
 
260
263
  // 5. Verificar estado de instalación por runtime
261
264
  for (const runtime of runtimes) {
262
- // Deduplicar: cuando global y local resuelven al mismo path (ej: running desde home dir)
263
- const dirsRaw = [runtime.global, path.resolve(runtime.local)];
264
- const dirs = [...new Set(dirsRaw)];
265
- for (const dir of dirs) {
265
+ // Deduplicar + descartar locales que resuelven al global de otro runtime
266
+ // (ej: doctor corrido desde el home dir)
267
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
266
268
  if (!fs.existsSync(dir)) continue;
267
-
268
- const esGlobal = dir === runtime.global;
269
269
  const estado = cargarEstado(dir);
270
270
  if (estado) {
271
271
  const { integro, problemas } = verificarIntegridad(estado);
@@ -379,11 +379,7 @@ async function doctor(opciones = {}) {
379
379
  // Sub-fase 12 v1.5.0: deduplicar paths cuando cwd === homedir (runtime.local
380
380
  // y runtime.global colapsan al mismo directorio) — evita reporte doble.
381
381
  for (const runtime of runtimes) {
382
- const dirsUnicos = Array.from(new Set([
383
- path.resolve(runtime.global),
384
- path.resolve(runtime.local),
385
- ]));
386
- for (const dir of dirsUnicos) {
382
+ for (const { dir } of scopesReales(runtime)) {
387
383
  if (!fs.existsSync(dir)) continue;
388
384
  const estado = cargarEstado(dir);
389
385
  if (!estado || !estado.perfil) continue;
@@ -559,12 +555,20 @@ async function doctor(opciones = {}) {
559
555
  for (const runtime of runtimes) {
560
556
  const dir = path.resolve(runtime.local);
561
557
  if (!fs.existsSync(dir)) continue;
558
+ // Local que resuelve al global de OTRO runtime = scope fantasma (ver scopesReales)
559
+ if (esDirGlobalDeAlgunRuntime(dir) && dir !== path.resolve(runtime.global)) continue;
562
560
  const estado = cargarEstado(dir);
563
561
  if (!estado) continue;
564
562
 
565
563
  const settingsPath = rutaSettings(runtime, dir);
566
564
  if (fs.existsSync(settingsPath)) {
567
- const structResult = validarEstructuraSettings(settingsPath);
565
+ // Cursor usa hooks.json con eventos camelCase propios y entradas PLANAS
566
+ // ({command, type}) — el esquema Claude produce falsos errores
567
+ // ("afterFileEdit: evento no reconocido", "[0].hooks debe ser array").
568
+ // Codex también usa hooks.json pero con FORMA Claude ({matcher,
569
+ // hooks:[...]}, eventos PascalCase) — ver transformadores/codex.js.
570
+ const formatoHooks = runtime.id === 'cursor' ? 'plano' : 'claude';
571
+ const structResult = validarEstructuraSettings(settingsPath, { formatoHooks });
568
572
  if (structResult.ok) {
569
573
  console.log(formatearPaso(`Estructura settings ${runtime.nombre}`, 'Formato válido'));
570
574
  ok++;
@@ -624,8 +628,7 @@ async function doctor(opciones = {}) {
624
628
 
625
629
  // 8. Verificar componentes externos
626
630
  for (const runtime of runtimes) {
627
- const dirs = [runtime.global, path.resolve(runtime.local)];
628
- for (const dir of dirs) {
631
+ for (const { dir } of scopesReales(runtime)) {
629
632
  if (!fs.existsSync(dir)) continue;
630
633
  const estado = cargarEstado(dir);
631
634
  if (estado && estado.componentesExternos && estado.componentesExternos.length > 0) {
@@ -712,6 +715,11 @@ async function doctor(opciones = {}) {
712
715
  } else {
713
716
  console.log(` ${reparados} reparado(s), ${color.rojo(`${fallos} fallo(s)`)}.`);
714
717
  }
718
+ // Las reparaciones no se re-verifican en esta corrida: honestidad sobre
719
+ // el alcance (caso repair-loop 2026-07-03 — un falso positivo del check
720
+ // se "reparaba" y reaparecía indefinidamente).
721
+ console.log(` ${color.dim('Re-ejecuta `doctor` para confirmar. Si el MISMO problema reaparece tras reparar,')}`);
722
+ console.log(` ${color.dim('es un falso positivo del check (repórtalo) — no falta de reparación.')}`);
715
723
  } else {
716
724
  console.log(` ${color.dim('Reparación cancelada. Usa --force para reparar sin confirmar.')}`);
717
725
  }
@@ -26,6 +26,7 @@ const { obtenerTransformador } = require('./lib/transformadores');
26
26
  const {
27
27
  esProtegido,
28
28
  crearBackup,
29
+ rutaBackupSegura,
29
30
  registrarBackup,
30
31
  limpiarBackups,
31
32
  mergeInstintos,
@@ -1245,7 +1246,9 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
1245
1246
  // ALTO (nemesis O6): respaldar el DIRECTORIO COMPLETO (recursos incluidos)
1246
1247
  // antes de sobrescribir — el usuario pudo modificar recursos/ aunque no
1247
1248
  // tocara el SKILL.md. Overwrite controlado en-bloque (no fall-through).
1248
- const backupDir = path.join(process.cwd(), '.planning', 'backups', `v${versionBackup}`, path.relative(process.cwd(), destino));
1249
+ // rutaBackupSegura contiene el backup dentro del dir versionado incluso
1250
+ // cuando destino está fuera del cwd (install --global — bug 2026-07-03).
1251
+ const backupDir = rutaBackupSegura(process.cwd(), versionBackup, destino);
1249
1252
  let backupOk = false;
1250
1253
  try { copiarDirectorio(destino, backupDir); backupOk = true; } catch (e) { console.log(` ⚠ Backup de directorio falló (${e.message})`); }
1251
1254
  if (!backupOk) {
@@ -151,6 +151,48 @@ const RUNTIMES = {
151
151
  },
152
152
  };
153
153
 
154
+ /**
155
+ * ¿El directorio dado es el dir GLOBAL de alguno de los runtimes conocidos?
156
+ *
157
+ * Un dir global jamás debe interpretarse como instalación "de proyecto" de
158
+ * otro runtime: con cwd en el HOME del usuario, todos los `runtime.local`
159
+ * relativos (.claude, .cursor, .codex, …) resuelven dentro del HOME y
160
+ * colisionan con los globales — el caso más dañino es OpenClaude, cuyo
161
+ * local `.claude` colisiona con el global de Claude Code.
162
+ *
163
+ * @param {string} dir - Ruta absoluta o relativa a evaluar.
164
+ * @returns {boolean}
165
+ */
166
+ function esDirGlobalDeAlgunRuntime(dir) {
167
+ const objetivo = path.resolve(dir);
168
+ for (const config of Object.values(RUNTIMES)) {
169
+ if (path.resolve(config.global) === objetivo) return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Scopes REALES de un runtime: [global, proyecto] deduplicados y sin
176
+ * fantasmas. Con cwd en el HOME, el local relativo (.claude/.cursor/.codex)
177
+ * resuelve al dir GLOBAL propio (duplicado) o al de OTRO runtime (scope
178
+ * "proyecto" espurio — p.ej. el local de OpenClaude colisiona con el global
179
+ * de Claude Code, y un uninstall de ese "proyecto" borraría el global).
180
+ *
181
+ * Consumidores: doctor, actualizar, TUI (inspect/uninstall/update/welcome).
182
+ *
183
+ * @param {object} runtime - Config de RUNTIMES (con .global y .local).
184
+ * @returns {Array<{dir: string, esGlobal: boolean}>}
185
+ */
186
+ function scopesReales(runtime) {
187
+ const globalAbs = path.resolve(runtime.global);
188
+ const localAbs = path.resolve(runtime.local);
189
+ const scopes = [{ dir: globalAbs, esGlobal: true }];
190
+ if (localAbs !== globalAbs && !esDirGlobalDeAlgunRuntime(localAbs)) {
191
+ scopes.push({ dir: localAbs, esGlobal: false });
192
+ }
193
+ return scopes;
194
+ }
195
+
154
196
  /**
155
197
  * Detecta qué runtimes están instalados en el sistema
156
198
  */
@@ -174,6 +216,13 @@ function detectarRuntimes() {
174
216
  // Si este runtime comparte su directorio local con otro (ej: openclaude comparte .claude/ con claude),
175
217
  // solo considerarlo instalado localmente si el estado de instalación declara explícitamente este target.
176
218
  let localEfectivo = localExiste;
219
+
220
+ // Guard universal: el dir global de un runtime NUNCA es una instalación de
221
+ // proyecto. Con cwd en el HOME, `<cwd>/.claude` resuelve al GLOBAL de
222
+ // Claude Code y la detección lo trataba como "proyecto" de OpenClaude —
223
+ // produciendo falso "instalación duplicada", 47 hooks "faltantes" y un
224
+ // repair-loop (caso real doctor 2026-07-03).
225
+ if (esDirGlobalDeAlgunRuntime(localAbs)) localEfectivo = false;
177
226
  const comparte = (localUsadoPor[localAbs] || []).length > 1;
178
227
  if (comparte && localExiste && !globalExiste) {
179
228
  // Revisar el estado de instalación para ver si el target coincide
@@ -293,6 +342,13 @@ function detectarInstalacionesDuales(runtimeId, opciones = {}) {
293
342
  return { hayConflicto: false, global: false, local: false, globalDir, localDir };
294
343
  }
295
344
 
345
+ // El dir global de CUALQUIER runtime no puede ser el lado "proyecto" del
346
+ // conflicto: con cwd en HOME, el local de OpenClaude (.claude) resuelve al
347
+ // global de Claude Code y se reportaba "duplicado global+proyecto" espurio.
348
+ if (esDirGlobalDeAlgunRuntime(localDir)) {
349
+ return { hayConflicto: false, global: Boolean(require('./estado').cargarEstado(globalDir)), local: false, globalDir, localDir };
350
+ }
351
+
296
352
  const estadoGlobal = cargarEstado(globalDir);
297
353
  const estadoLocal = cargarEstado(localDir);
298
354
 
@@ -337,6 +393,8 @@ function listarRuntimesInstalables() {
337
393
  module.exports = {
338
394
  RUNTIMES,
339
395
  detectarRuntimes,
396
+ esDirGlobalDeAlgunRuntime,
397
+ scopesReales,
340
398
  detectarInstalacionesDuales,
341
399
  obtenerRuntime,
342
400
  calcularRutas,
@@ -1,71 +1,71 @@
1
- 'use strict';
2
-
3
- /**
4
- * Expansión de --target (CSV) y --all-runtimes para multi-target install.
5
- *
6
- * ADR-0019 Sub-fase 2.5.
7
- *
8
- * Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
9
- * side effects fuera de logging — solo manipula la lista de strings.
10
- *
11
- * @module scripts/lib/expandir-targets
12
- */
13
-
14
- const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
15
-
16
- /**
17
- * Expande las opciones del CLI a un array de target IDs.
18
- *
19
- * Reglas:
20
- * - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
21
- * aviso (vía `logger.warn`) y se usa `all_runtimes`.
22
- * - `target='a,b,c'` → ['a','b','c'].
23
- * - `target='claude'` → ['claude'].
24
- * - Sin nada → ['claude'] (default histórico, backward-compat).
25
- * - Duplicados se deduplican preservando el primer orden.
26
- * - Targets desconocidos se omiten con aviso (logger.warn).
27
- *
28
- * @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
29
- * @param {object} [logger] - Sustituible para tests. Default console.
30
- * @returns {{ targets: string[], omitidos: string[], errores: string[] }}
31
- */
32
- function expandirTargets(opciones, logger) {
33
- const log = logger || console;
34
- const omitidos = [];
35
- const errores = [];
36
-
37
- let candidatos;
38
- if (opciones.all_runtimes) {
39
- if (opciones.target && typeof opciones.target === 'string') {
40
- log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
41
- }
42
- candidatos = listarRuntimesInstalables();
43
- } else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
44
- candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
45
- } else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
46
- candidatos = [opciones.target.trim()];
47
- } else {
48
- candidatos = ['claude']; // backward-compat
49
- }
50
-
51
- const visto = new Set();
52
- const targets = [];
53
- for (const t of candidatos) {
54
- if (visto.has(t)) continue;
55
- visto.add(t);
56
- if (!RUNTIMES[t]) {
57
- log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
58
- omitidos.push(t);
59
- continue;
60
- }
61
- targets.push(t);
62
- }
63
-
64
- if (targets.length === 0) {
65
- errores.push('Ningún target válido tras expansión.');
66
- }
67
-
68
- return { targets, omitidos, errores };
69
- }
70
-
71
- module.exports = { expandirTargets };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Expansión de --target (CSV) y --all-runtimes para multi-target install.
5
+ *
6
+ * ADR-0019 Sub-fase 2.5.
7
+ *
8
+ * Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
9
+ * side effects fuera de logging — solo manipula la lista de strings.
10
+ *
11
+ * @module scripts/lib/expandir-targets
12
+ */
13
+
14
+ const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
15
+
16
+ /**
17
+ * Expande las opciones del CLI a un array de target IDs.
18
+ *
19
+ * Reglas:
20
+ * - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
21
+ * aviso (vía `logger.warn`) y se usa `all_runtimes`.
22
+ * - `target='a,b,c'` → ['a','b','c'].
23
+ * - `target='claude'` → ['claude'].
24
+ * - Sin nada → ['claude'] (default histórico, backward-compat).
25
+ * - Duplicados se deduplican preservando el primer orden.
26
+ * - Targets desconocidos se omiten con aviso (logger.warn).
27
+ *
28
+ * @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
29
+ * @param {object} [logger] - Sustituible para tests. Default console.
30
+ * @returns {{ targets: string[], omitidos: string[], errores: string[] }}
31
+ */
32
+ function expandirTargets(opciones, logger) {
33
+ const log = logger || console;
34
+ const omitidos = [];
35
+ const errores = [];
36
+
37
+ let candidatos;
38
+ if (opciones.all_runtimes) {
39
+ if (opciones.target && typeof opciones.target === 'string') {
40
+ log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
41
+ }
42
+ candidatos = listarRuntimesInstalables();
43
+ } else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
44
+ candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
45
+ } else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
46
+ candidatos = [opciones.target.trim()];
47
+ } else {
48
+ candidatos = ['claude']; // backward-compat
49
+ }
50
+
51
+ const visto = new Set();
52
+ const targets = [];
53
+ for (const t of candidatos) {
54
+ if (visto.has(t)) continue;
55
+ visto.add(t);
56
+ if (!RUNTIMES[t]) {
57
+ log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
58
+ omitidos.push(t);
59
+ continue;
60
+ }
61
+ targets.push(t);
62
+ }
63
+
64
+ if (targets.length === 0) {
65
+ errores.push('Ningún target válido tras expansión.');
66
+ }
67
+
68
+ return { targets, omitidos, errores };
69
+ }
70
+
71
+ module.exports = { expandirTargets };
@@ -689,18 +689,55 @@ function registrarOptimizaciones(settingsPath) {
689
689
  * hooks con formato válido. No valida contenido semántico, solo estructura.
690
690
  *
691
691
  * @param {string} settingsPath
692
+ * @param {object} [opts]
693
+ * @param {'claude'|'plano'} [opts.formatoHooks='claude'] - 'claude' valida el
694
+ * esquema de Claude Code ({matcher, hooks:[{type,command}]}, eventos
695
+ * PascalCase). 'plano' valida hooks.json de Cursor/Codex: entradas planas
696
+ * ({command, type?}) bajo eventos camelCase PROPIOS del runtime — los
697
+ * nombres de evento NO se validan contra whitelist porque cada runtime
698
+ * define su set y lo amplía por versión (afterFileEdit,
699
+ * beforeShellExecution, etc. son legítimos en Cursor; el esquema Claude
700
+ * los marcaba como error — caso real doctor 2026-07-03).
692
701
  * @returns {{ ok: boolean, problemas: string[] }}
693
702
  */
694
- function validarEstructuraSettings(settingsPath) {
703
+ function validarEstructuraSettings(settingsPath, opts = {}) {
704
+ const formatoHooks = opts.formatoHooks || 'claude';
695
705
  const settings = leerSettings(settingsPath);
696
706
  const problemas = [];
697
707
 
698
- // Validar sección hooks
699
- if (settings.hooks) {
708
+ // Formato plano (Cursor/Codex hooks.json): shape-only, sin whitelist de eventos.
709
+ if (settings.hooks && formatoHooks === 'plano') {
710
+ if (typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
711
+ problemas.push('hooks: debe ser un objeto { evento: [entradas] }');
712
+ } else {
713
+ for (const [evento, entradas] of Object.entries(settings.hooks)) {
714
+ if (!Array.isArray(entradas)) {
715
+ problemas.push(`hooks.${evento}: debe ser un array de entradas`);
716
+ continue;
717
+ }
718
+ for (let i = 0; i < entradas.length; i++) {
719
+ const entrada = entradas[i];
720
+ if (!entrada || typeof entrada !== 'object') {
721
+ problemas.push(`hooks.${evento}[${i}]: debe ser un objeto`);
722
+ continue;
723
+ }
724
+ if (typeof entrada.command !== 'string' || !entrada.command) {
725
+ problemas.push(`hooks.${evento}[${i}]: falta campo command (string)`);
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
731
+
732
+ // Validar sección hooks (esquema Claude Code)
733
+ if (settings.hooks && formatoHooks === 'claude') {
700
734
  const eventosValidos = new Set([
701
735
  'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop',
702
736
  'SubagentStart', 'SubagentStop', 'PreCompact', 'PostCompact',
703
737
  'Notification', 'SessionStart', 'TaskCompleted',
738
+ // Codex CLI usa la forma Claude en su hooks.json pero agrega este evento
739
+ // propio (ver transformadores/codex.js EVENTOS_VALIDOS).
740
+ 'PermissionRequest',
704
741
  ]);
705
742
 
706
743
  for (const [evento, entradas] of Object.entries(settings.hooks)) {
@@ -34,7 +34,7 @@ const path = require('path');
34
34
  * - /swl:evolucionar → skills auto-generados en habilidades/
35
35
  * - /swl:crear-skill → skills nuevos en habilidades/
36
36
  * - session-store → .planning/sessions/*.json
37
- * - context-compressor → .planning/COMPACTACION.md
37
+ * - /swl:compactar → .planning/COMPACTACION.md
38
38
  * - cron-store → .planning/cron/
39
39
  * - gateway → .planning/gateway-sessions.json
40
40
  * - agent-comms → .planning/comms/
@@ -181,6 +181,41 @@ function dirBackup(baseDir, version) {
181
181
  return path.join(baseDir, '.planning', 'backups', `v${version}`);
182
182
  }
183
183
 
184
+ /**
185
+ * Subruta segura de un archivo DENTRO del directorio de backups.
186
+ *
187
+ * Un destino fuera de baseDir (install --global: ~/.claude, ~/.openclaude)
188
+ * produce un path.relative con segmentos '..' que ESCAPAN del directorio
189
+ * versionado: `.planning/backups/v2.3.1/../../.claude/...` colapsa a
190
+ * `.planning/.claude/...` (bug 2026-07-03: 1147 backups globales aterrizaron
191
+ * fuera del dir de backups y aparecieron como archivos nuevos en el repo).
192
+ * Se neutralizan '..', '.' y dos puntos de unidad preservando la estructura.
193
+ *
194
+ * @param {string} baseDir - Raíz del proyecto.
195
+ * @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
196
+ * @returns {string} Subruta relativa contenida (sin '..').
197
+ */
198
+ function subrutaBackupSegura(baseDir, rutaAbsoluta) {
199
+ return path.relative(baseDir, rutaAbsoluta)
200
+ .split(/[\\/]/)
201
+ .filter(s => s && s !== '..' && s !== '.')
202
+ .map(s => s.replace(/:/g, ''))
203
+ .join(path.sep);
204
+ }
205
+
206
+ /**
207
+ * Ruta completa de backup para un archivo, garantizada dentro del
208
+ * directorio versionado de backups.
209
+ *
210
+ * @param {string} baseDir - Raíz del proyecto.
211
+ * @param {string} versionAnterior - Versión que se está sobreescribiendo.
212
+ * @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
213
+ * @returns {string} Ruta absoluta del backup.
214
+ */
215
+ function rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta) {
216
+ return path.join(dirBackup(baseDir, versionAnterior), subrutaBackupSegura(baseDir, rutaAbsoluta));
217
+ }
218
+
184
219
  /**
185
220
  * Crea un backup de un archivo antes de sobreescribirlo.
186
221
  *
@@ -195,10 +230,8 @@ function crearBackup(baseDir, rutaAbsoluta, versionAnterior) {
195
230
  }
196
231
 
197
232
  try {
198
- const rutaRelativa = path.relative(baseDir, rutaAbsoluta);
199
- const backupDir = dirBackup(baseDir, versionAnterior);
200
- const rutaBackup = path.join(backupDir, rutaRelativa);
201
- const dirPadre = path.dirname(rutaBackup);
233
+ const rutaBackup = rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta);
234
+ const dirPadre = path.dirname(rutaBackup);
202
235
 
203
236
  if (!fs.existsSync(dirPadre)) {
204
237
  fs.mkdirSync(dirPadre, { recursive: true });
@@ -573,6 +606,7 @@ module.exports = {
573
606
 
574
607
  // Nivel 2: Backup
575
608
  crearBackup,
609
+ rutaBackupSegura,
576
610
  registrarBackup,
577
611
  listarBackups,
578
612
  restaurarBackup,