@saulwade/swl-ses 2.5.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CLAUDE.md +194 -192
  2. package/README.md +600 -600
  3. package/agentes/auto-evolucion-swl.md +27 -3
  4. package/bin/swl-ses.js +32 -6
  5. package/comandos/swl/actualizar.md +174 -174
  6. package/comandos/swl/adoptar-proyecto.md +265 -265
  7. package/comandos/swl/aprender.md +836 -823
  8. package/comandos/swl/aprobar-plan.md +146 -146
  9. package/comandos/swl/auditar-deps.md +134 -134
  10. package/comandos/swl/autoresearch.md +264 -264
  11. package/comandos/swl/ayuda.md +224 -224
  12. package/comandos/swl/brainstorm.md +51 -51
  13. package/comandos/swl/briefing.md +119 -119
  14. package/comandos/swl/checkpoint.md +325 -325
  15. package/comandos/swl/claudemd.md +234 -234
  16. package/comandos/swl/compactar.md +310 -310
  17. package/comandos/swl/configurar-ci.md +235 -235
  18. package/comandos/swl/contexto.md +110 -110
  19. package/comandos/swl/contribuir.md +233 -233
  20. package/comandos/swl/crear-skill.md +292 -292
  21. package/comandos/swl/cron.md +194 -194
  22. package/comandos/swl/deuda-codigo.md +97 -97
  23. package/comandos/swl/discutir-fase.md +169 -169
  24. package/comandos/swl/ejecutar-fase.md +233 -233
  25. package/comandos/swl/evaluar-skill.md +520 -505
  26. package/comandos/swl/evolucion-continua.md +73 -0
  27. package/comandos/swl/evolucionar.md +267 -254
  28. package/comandos/swl/exportar-vault.md +583 -583
  29. package/comandos/swl/fix.md +118 -118
  30. package/comandos/swl/gateway.md +158 -158
  31. package/comandos/swl/inbox.md +116 -116
  32. package/comandos/swl/instalar.md +220 -220
  33. package/comandos/swl/instintos.md +86 -86
  34. package/comandos/swl/mapear-codebase.md +312 -312
  35. package/comandos/swl/mcp-status.md +175 -175
  36. package/comandos/swl/modelo.md +100 -100
  37. package/comandos/swl/nemesis.md +433 -433
  38. package/comandos/swl/notificaciones.md +299 -299
  39. package/comandos/swl/nuevo-proyecto.md +251 -251
  40. package/comandos/swl/planear-fase.md +263 -263
  41. package/comandos/swl/plugins.md +256 -256
  42. package/comandos/swl/predecir.md +169 -169
  43. package/comandos/swl/reflect-skills.md +125 -125
  44. package/comandos/swl/release.md +450 -450
  45. package/comandos/swl/revisar-impacto.md +201 -201
  46. package/comandos/swl/revisar.md +330 -330
  47. package/comandos/swl/seguridad.md +189 -189
  48. package/comandos/swl/sesiones.md +200 -200
  49. package/comandos/swl/skill-search.md +113 -113
  50. package/comandos/swl/status.md +343 -343
  51. package/comandos/swl/verificar.md +817 -817
  52. package/comandos/swl/wiki.md +620 -620
  53. package/gateway/cron/jobs.example.json +12 -0
  54. package/habilidades/auto-evolucion-protocolo/SKILL.md +294 -276
  55. package/habilidades/autoresearch/SKILL.md +3 -2
  56. package/habilidades/benchmark-memoria/SKILL.md +7 -7
  57. package/habilidades/changelog-generator/SKILL.md +174 -174
  58. package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
  59. package/habilidades/checkpoints-verificacion/SKILL.md +6 -0
  60. package/habilidades/context-builder/SKILL.md +4 -0
  61. package/habilidades/doubt-driven-review/SKILL.md +207 -191
  62. package/habilidades/drift-detection/SKILL.md +6 -1
  63. package/habilidades/ejecutar-fase/SKILL.md +6 -6
  64. package/habilidades/eval-framework/SKILL.md +8 -3
  65. package/habilidades/harness-claude-code/SKILL.md +314 -308
  66. package/habilidades/infra-github-actions/SKILL.md +4 -3
  67. package/habilidades/instalar-sistema/SKILL.md +227 -223
  68. package/habilidades/memoria-busqueda/SKILL.md +31 -39
  69. package/habilidades/planear-fase/SKILL.md +358 -350
  70. package/habilidades/proceso-ddia-fundamentos/SKILL.md +3 -2
  71. package/habilidades/release-semver/SKILL.md +4 -2
  72. package/habilidades/swl-claudemd/SKILL.md +6 -7
  73. package/habilidades/swl-dashboard/SKILL.md +11 -43
  74. package/habilidades/tdd-workflow/SKILL.md +749 -744
  75. package/habilidades/validacion-ci-sistema/SKILL.md +1 -1
  76. package/hooks/agente-lifecycle.js +2 -1
  77. package/hooks/aiisms-detector.js +13 -4
  78. package/hooks/audit-trail.js +2 -1
  79. package/hooks/auto-consolidacion.js +2 -1
  80. package/hooks/captura-acciones-post.js +2 -1
  81. package/hooks/captura-acciones-session.js +2 -1
  82. package/hooks/captura-feedback-usuario.js +3 -2
  83. package/hooks/claudemd-bloat-detector.js +12 -3
  84. package/hooks/claudemd-duplicacion-detector.js +13 -3
  85. package/hooks/contexto-iteracion.js +2 -1
  86. package/hooks/degradacion-instintos.js +2 -1
  87. package/hooks/extraccion-aprendizajes.js +109 -15
  88. package/hooks/grafo-contexto.js +2 -1
  89. package/hooks/guardrail-modelo.js +2 -1
  90. package/hooks/inbox-aviso.js +2 -1
  91. package/hooks/inyeccion-contexto.js +2 -1
  92. package/hooks/lib/agent-matcher.js +2 -1
  93. package/hooks/lib/agent-routing.js +2 -1
  94. package/hooks/lib/autonomia.js +5 -3
  95. package/hooks/lib/captura-acciones.js +2 -1
  96. package/hooks/lib/consolidation-lock.js +21 -10
  97. package/hooks/lib/etapa-auto-evolucion.js +10 -4
  98. package/hooks/lib/etapa-metricas.js +2 -1
  99. package/hooks/lib/etapa-perfil-usuario.js +20 -4
  100. package/hooks/lib/evolution-tracker.js +2 -1
  101. package/hooks/lib/gateway-notify.js +193 -179
  102. package/hooks/lib/loop-telemetry.js +5 -4
  103. package/hooks/lib/mcp-health.js +2 -1
  104. package/hooks/lib/memory-search.js +4 -0
  105. package/hooks/lib/merkle-audit.js +58 -6
  106. package/hooks/lib/nudge-tracker.js +2 -1
  107. package/hooks/lib/otlp-exporter.js +2 -1
  108. package/hooks/lib/propose-step.js +3 -2
  109. package/hooks/lib/raiz-proyecto.js +102 -0
  110. package/hooks/lib/run-log.js +2 -1
  111. package/hooks/lib/singleton-guard.js +218 -27
  112. package/hooks/lib/telegram-cliente.js +17 -8
  113. package/hooks/preservar-estado-pre-compact.js +2 -1
  114. package/hooks/proteccion-rutas.js +59 -3
  115. package/hooks/registro-turnos.js +2 -1
  116. package/hooks/resumen-sesion.js +2 -1
  117. package/hooks/risk-scoring.js +2 -1
  118. package/hooks/rotar-audit-auto.js +46 -20
  119. package/hooks/session-briefing.js +127 -1
  120. package/hooks/spec-gate.js +2 -1
  121. package/hooks/sugerir-contribuir.js +6 -3
  122. package/hooks/sugerir-regenerar-inventario.js +3 -2
  123. package/hooks/tdd-gate.js +2 -1
  124. package/hooks/telemetria-agentes.js +2 -1
  125. package/hooks/telemetria-skill-routing.js +2 -1
  126. package/hooks/tracking-costos.js +4 -3
  127. package/hooks/validar-formato-post-subagente.js +2 -1
  128. package/hooks/validar-intent-spec.js +2 -1
  129. package/hooks/validar-memoria-hook.js +13 -3
  130. package/hooks/validar-planning-paths.js +2 -1
  131. package/instintos/.backups/perfil-usuario.yaml.2026-07-10-165128.bak +53 -0
  132. package/instintos/.backups/proyecto.yaml.2026-07-10-165128.bak +372 -0
  133. package/instintos/perfil-usuario.yaml +506 -3
  134. package/instintos/proyecto.yaml +78 -0
  135. package/llms.txt +2 -2
  136. package/manifiestos/canonical-hashes.json +664 -2
  137. package/manifiestos/modulos.json +19 -14
  138. package/manifiestos/planning-paths.json +1 -0
  139. package/manifiestos/skills-lock.json +53 -53
  140. package/package.json +2 -3
  141. package/plugin.json +2 -2
  142. package/scripts/actualizar.js +3 -0
  143. package/scripts/auditar-clases-conocidas.js +32 -4
  144. package/scripts/benchmark-memoria.js +1 -0
  145. package/scripts/cli/autonomia.js +23 -0
  146. package/scripts/cli/benchmark-memoria.js +37 -0
  147. package/scripts/cli/ciclo-autonomo.js +73 -0
  148. package/scripts/cli/ciclo-fase-b.js +102 -0
  149. package/scripts/cli/guardrail-metrics.js +39 -0
  150. package/scripts/cli/loop-telemetry.js +4 -2
  151. package/scripts/cli/memoria-search.js +69 -0
  152. package/scripts/cli/nudge-accionar.js +39 -0
  153. package/scripts/cli/run-eval.js +38 -0
  154. package/scripts/cli/run-skill-evals.js +13 -2
  155. package/scripts/derivar-feature-list.js +15 -14
  156. package/scripts/desinstalar.js +11 -0
  157. package/scripts/doctor.js +24 -10
  158. package/scripts/instalador.js +106 -7
  159. package/scripts/lib/activar-hooks-proyecto.js +116 -0
  160. package/scripts/lib/auditar-invocaciones-comandos.js +96 -6
  161. package/scripts/lib/ciclo-autonomo/candidatos.js +174 -0
  162. package/scripts/lib/ciclo-autonomo/config.js +165 -0
  163. package/scripts/lib/ciclo-autonomo/drenador-feedback.js +174 -0
  164. package/scripts/lib/ciclo-autonomo/fallback.js +77 -0
  165. package/scripts/lib/ciclo-autonomo/guard-convivencia.js +139 -0
  166. package/scripts/lib/ciclo-autonomo/higiene-nudges.js +112 -0
  167. package/scripts/lib/ciclo-autonomo/index.js +301 -0
  168. package/scripts/lib/ciclo-autonomo/lock.js +124 -0
  169. package/scripts/lib/ciclo-autonomo/presupuesto.js +122 -0
  170. package/scripts/lib/ciclo-autonomo/puente-degradacion.js +240 -0
  171. package/scripts/lib/ciclo-autonomo/runner-fase-b.js +248 -0
  172. package/scripts/lib/ciclo-autonomo/writer-instintos.js +190 -0
  173. package/scripts/lib/ciclo-autonomo/yaml-instintos.js +535 -0
  174. package/scripts/lib/estado.js +9 -0
  175. package/scripts/lib/evidencia-valor.js +1 -1
  176. package/scripts/lib/gitignore-manifest.js +8 -1
  177. package/scripts/lib/hooks-settings.js +45 -0
  178. package/scripts/rotar-audit-logs.js +48 -2
  179. package/scripts/run-eval.js +1 -0
  180. package/scripts/run-skill-evals.js +287 -8
  181. package/scripts/smoke-test.js +16 -8
  182. package/scripts/tui/pantallas/install-wizard.js +403 -347
  183. package/scripts/validar.js +40 -1
@@ -228,8 +228,62 @@ const RUTAS_PERMITIDAS_EXTERNAS = [
228
228
  path.join(os.homedir(), '.claude', 'commands'),
229
229
  // Reglas globales del usuario (modificables durante consolidación cross-proyecto)
230
230
  path.join(os.homedir(), '.claude', 'rules'),
231
+ // Scratchpad de sesión del harness de Claude Code (os.tmpdir()/claude/<proyecto>/<sesión>/)
232
+ // — el propio harness instruye usarlo para todo temporal (caso real 2026-07-10:
233
+ // el hook bloqueaba al agente escribiendo en su scratchpad oficial).
234
+ path.join(os.tmpdir(), 'claude'),
231
235
  ];
232
236
 
237
+ /**
238
+ * Asciende desde `desde` buscando la raíz del repositorio git que lo contiene
239
+ * (directorio con `.git` — directorio normal o archivo, para worktrees).
240
+ *
241
+ * La frontera de seguridad correcta es el PROYECTO, no el subdirectorio donde
242
+ * se abrió la sesión: con CWD=<repo>/backend, escribir `<repo>/.planning/...`
243
+ * es legítimo (todo el sistema SWL escribe `.planning/` en la raíz). Caso real
244
+ * SIGM 2026-07-10: el loop de auditoría se auto-bloqueaba en un monorepo.
245
+ *
246
+ * @param {string} desde - Directorio inicial (típicamente el CWD).
247
+ * @returns {string|null} Raíz del repo, o null si `desde` no está en un repo git.
248
+ */
249
+ function raizRepoGit(desde) {
250
+ const fs = require('fs');
251
+ let actual = path.resolve(desde);
252
+ for (let i = 0; i < 40; i++) {
253
+ try {
254
+ if (fs.existsSync(path.join(actual, '.git'))) return actual;
255
+ } catch (_) {
256
+ return null;
257
+ }
258
+ const padre = path.dirname(actual);
259
+ if (padre === actual) return null; // raíz del filesystem
260
+ actual = padre;
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Verifica si la ruta resuelta cae dentro de la raíz del repositorio git que
267
+ * contiene al CWD. Boundary check por componente completo (prefijo + '/'),
268
+ * case-insensitive en Windows — un sibling `<repo>-evil` NO pasa.
269
+ *
270
+ * @param {string} rutaResuelta
271
+ * @param {string} cwdActual
272
+ * @returns {boolean}
273
+ */
274
+ function estaDentroDeRaizGit(rutaResuelta, cwdActual) {
275
+ const raiz = raizRepoGit(cwdActual);
276
+ if (!raiz) return false;
277
+
278
+ const raizNorm = normalizarParaComparacion(raiz);
279
+ const rutaNorm = normalizarParaComparacion(rutaResuelta);
280
+ const [raizCmp, rutaCmp] = process.platform === 'win32'
281
+ ? [raizNorm.toLowerCase(), rutaNorm.toLowerCase()]
282
+ : [raizNorm, rutaNorm];
283
+
284
+ return rutaCmp === raizCmp || rutaCmp.startsWith(raizCmp + '/');
285
+ }
286
+
233
287
  /**
234
288
  * Parsea la variable de entorno SWL_PROTECCION_RUTAS_PERMITIR (CSV de rutas
235
289
  * absolutas) y devuelve un array de paths normalizados.
@@ -361,7 +415,8 @@ function construirMensajeBloqueo(tipoViolacion, rutaRaw, rutaResuelta, cwdActual
361
415
  `Directorio CWD: ${cwdActual}`,
362
416
  ``,
363
417
  `Reglas de seguridad:`,
364
- ` - La ruta de destino debe estar dentro del directorio de trabajo actual.`,
418
+ ` - La ruta de destino debe estar dentro del directorio de trabajo actual`,
419
+ ` o de la raíz del repositorio git que lo contiene (monorepos).`,
365
420
  ` - No se permiten secuencias de path traversal (../, ~/, %2e%2e, etc.).`,
366
421
  ` - No se permiten rutas absolutas fuera del proyecto.`,
367
422
  ];
@@ -440,9 +495,10 @@ process.stdin.on('end', () => {
440
495
  process.exit(2);
441
496
  }
442
497
 
443
- // --- Verificación 2: destino dentro del CWD o en rutas externas permitidas ---
498
+ // --- Verificación 2: destino dentro del CWD, de la raíz del repo git
499
+ // del CWD, o en rutas externas permitidas ---
444
500
  const { dentro, rutaResuelta } = verificarDentroDeCwd(rutaRaw, cwdActual);
445
- if (!dentro && !esRutaExternaPermitida(rutaResuelta)) {
501
+ if (!dentro && !estaDentroDeRaizGit(rutaResuelta, cwdActual) && !esRutaExternaPermitida(rutaResuelta)) {
446
502
  const razon = construirMensajeBloqueo(
447
503
  `Destino fuera del directorio de trabajo actual`,
448
504
  rutaRaw,
@@ -54,12 +54,13 @@
54
54
  */
55
55
 
56
56
  const fs = require('fs');
57
+ const { raizProyecto } = require('./lib/raiz-proyecto');
57
58
  const path = require('path');
58
59
 
59
60
  const LIMITE_TURNOS = parseInt(process.env.SWL_REGISTRO_TURNOS_LIMITE || '1000', 10);
60
61
  const MAX_INPUT_CHARS = 200;
61
62
  const MAX_OUTPUT_CHARS = 500;
62
- const DIR_BASE = path.join(process.cwd(), '.planning', 'sessions');
63
+ const DIR_BASE = path.join(raizProyecto(process.cwd()), '.planning', 'sessions');
63
64
 
64
65
  // Patrones de sanitización conservadores: tokens largos, llaves API, JWT,
65
66
  // password en URLs, paths con .env. Si aparece un match, se reemplaza con
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  const fs = require('fs');
21
+ const { raizProyecto } = require('./lib/raiz-proyecto');
21
22
  const path = require('path');
22
23
  const os = require('os');
23
24
  const { execFile } = require('child_process');
@@ -83,7 +84,7 @@ function hooksDisparados(sessionId) {
83
84
 
84
85
  // Fallback: heurística por archivos JSONL de hooks conocidos en .planning/
85
86
  try {
86
- const planningDir = path.join(process.cwd(), '.planning');
87
+ const planningDir = path.join(raizProyecto(process.cwd()), '.planning');
87
88
  const jsonlCandidatos = [
88
89
  ['evolution/nudges.jsonl', 'nudge-tracker'],
89
90
  ['auto-evolution/agentes.jsonl', 'auto-evolucion'],
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  const fs = require('fs');
29
+ const { raizProyecto } = require('./lib/raiz-proyecto');
29
30
  const path = require('path');
30
31
  const os = require('os');
31
32
 
@@ -104,7 +105,7 @@ function trackHighOverride(score, factors, toolName) {
104
105
  });
105
106
 
106
107
  if (state.highOverrides >= MAX_HIGH_OVERRIDES && !state.auditWritten) {
107
- const auditPath = path.join(process.cwd(), '.planning', 'AUDITORIA.md');
108
+ const auditPath = path.join(raizProyecto(process.cwd()), '.planning', 'AUDITORIA.md');
108
109
  try {
109
110
  const auditEntry = [
110
111
  '',
@@ -29,16 +29,30 @@
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
31
  const { spawnSync } = require('child_process');
32
+ const { raizProyecto } = require('./lib/raiz-proyecto');
33
+ const guard = require('./lib/singleton-guard');
32
34
 
33
- const RAIZ_PROYECTO = path.resolve(__dirname, '..');
35
+ // Ancla: raíz del PROYECTO (no __dirname/.. — en instalaciones destino el hook
36
+ // vive en ~/.claude/hooks/ y esa ancla apuntaba a ~/.claude/, nunca al
37
+ // proyecto; misma clase que DT-HOOKS-RAIZ-GIT).
38
+ const RAIZ_PROYECTO = raizProyecto(process.cwd());
34
39
  const DIR_PLANNING = path.join(RAIZ_PROYECTO, '.planning');
35
40
  const ARCHIVOS_AUDIT = [
36
41
  path.join(DIR_PLANNING, 'audit.jsonl'),
37
42
  path.join(DIR_PLANNING, 'audit-merkle.jsonl'),
38
43
  ];
39
- const DIR_ARCHIVO = path.join(DIR_PLANNING, 'archivo', 'audit');
44
+ const DIR_ARCHIVO = path.join(DIR_PLANNING, 'archive', 'audit');
40
45
  const MARCA_ULTIMA_ROTACION = path.join(DIR_ARCHIVO, '.ultima-rotacion');
41
- const SCRIPT_ROTAR = path.join(RAIZ_PROYECTO, 'scripts', 'rotar-audit-logs.js');
46
+ // El script de rotación es EJECUTABLE de la distribución SWL: se resuelve
47
+ // SOLO desde la ubicación instalada del hook (__dirname) — repo madre en
48
+ // scripts/, destino aplanado junto al hook. NUNCA desde la raíz del
49
+ // proyecto: un repo no confiable podría plantar el script y ejecutarse en
50
+ // el primer Stop (hallazgo MAYOR revisor-seguridad 2026-07-10). Los DATOS
51
+ // (DIR_PLANNING) sí anclan a raizProyecto.
52
+ const SCRIPT_ROTAR = [
53
+ path.join(__dirname, '..', 'scripts', 'rotar-audit-logs.js'),
54
+ path.join(__dirname, 'rotar-audit-logs.js'),
55
+ ].find(p => { try { return fs.existsSync(p); } catch (_) { return false; } }) || '';
42
56
 
43
57
  const COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6 horas
44
58
 
@@ -93,24 +107,36 @@ function main() {
93
107
  if (!check.excedido) return;
94
108
  if (enCooldown()) return;
95
109
 
96
- const dias = diasRetencion();
97
- const nodeBin = process.execPath;
98
- const result = spawnSync(
99
- nodeBin,
100
- [SCRIPT_ROTAR, `--dias=${dias}`, `--raiz=${DIR_PLANNING}`],
101
- { encoding: 'utf8', timeout: 30_000, cwd: RAIZ_PROYECTO }
102
- );
103
-
104
- if (result.status === 0) {
105
- marcarRotacion();
106
- const sizeMb = (check.size / 1024 / 1024).toFixed(1);
107
- const limiteMb = (check.limite / 1024 / 1024).toFixed(1);
108
- const nombreArchivo = path.basename(check.ruta);
109
- const match = (result.stdout || '').match(/(\d+) archivadas/g);
110
- const archivadas = match ? match.map((m) => m.replace(' archivadas', '')).join('+') : '?';
111
- process.stdout.write(
112
- `[rotacion-audit] ${nombreArchivo}=${sizeMb}MB > ${limiteMb}MB → archivadas ${archivadas} entradas (>${dias}d)\n`
110
+ // Lock cross-proceso (nemesis iter-1 S2-1/S3-4): dos sesiones con Stop
111
+ // simultáneo podían rotar a la vez → re-archivar las mismas entradas
112
+ // (duplicados en el .gz). Si otra rotación está activa, salir — esa corrida
113
+ // ya archiva. Ancla del lock = raíz del proyecto (misma que RAIZ_PROYECTO).
114
+ if (!guard.adquirir('rotar-audit', { staleMs: 300_000 })) return;
115
+ try {
116
+ const dias = diasRetencion();
117
+ const nodeBin = process.execPath;
118
+ const result = spawnSync(
119
+ nodeBin,
120
+ [SCRIPT_ROTAR, `--dias=${dias}`, `--raiz=${DIR_PLANNING}`],
121
+ // Archivos de auditoría de decenas de MB pueden tardar; 30s los mataba a
122
+ // mitad de rotación (nemesis iter-1 S3-4). 5 min da margen; el escribirActivo
123
+ // atómico protege contra la interrupción residual.
124
+ { encoding: 'utf8', timeout: 300_000, cwd: RAIZ_PROYECTO }
113
125
  );
126
+
127
+ if (result.status === 0) {
128
+ marcarRotacion();
129
+ const sizeMb = (check.size / 1024 / 1024).toFixed(1);
130
+ const limiteMb = (check.limite / 1024 / 1024).toFixed(1);
131
+ const nombreArchivo = path.basename(check.ruta);
132
+ const match = (result.stdout || '').match(/(\d+) archivadas/g);
133
+ const archivadas = match ? match.map((m) => m.replace(' archivadas', '')).join('+') : '?';
134
+ process.stdout.write(
135
+ `[rotacion-audit] ${nombreArchivo}=${sizeMb}MB > ${limiteMb}MB → archivadas ${archivadas} entradas (>${dias}d)\n`
136
+ );
137
+ }
138
+ } finally {
139
+ guard.liberar('rotar-audit');
114
140
  }
115
141
  }
116
142
 
@@ -19,7 +19,9 @@
19
19
  */
20
20
 
21
21
  const fs = require('fs');
22
+ const { raizProyecto } = require('./lib/raiz-proyecto');
22
23
  const path = require('path');
24
+ const { spawn } = require('child_process');
23
25
 
24
26
  // Require con fallback dual: el instalador aplana `hooks/lib/X.js → <hooks>/X.js`
25
27
  // en el destino, pero en el repo madre vive en `lib/`. Cubre ambos layouts (D-17).
@@ -32,6 +34,116 @@ function requireDual(nombre) {
32
34
  }
33
35
  }
34
36
 
37
+ // ---------------------------------------------------------------------------
38
+ // Fallback de disparo del ciclo autónomo (Fase 21, T-11, ADR-0041/0042,
39
+ // REQ-21-11). El scheduler cron (`gateway/cron/jobs.example.json`) es el
40
+ // disparador primario de la Fase A; este fallback cubre proyectos sin
41
+ // scheduler configurado, disparando desde el propio SessionStart cuando toca.
42
+ //
43
+ // Fuera de alcance intencional en instalaciones destino (Fase 26): las libs
44
+ // `scripts/lib/ciclo-autonomo/*.js` viajan en `hooks-core` (resuelven cross-dir
45
+ // igual que `git-metricas`/`npm-version`), pero el wrapper `scripts/cli/
46
+ // ciclo-autonomo.js` se distribuye SOLO por `package.json#files` — en destino
47
+ // puede no resolver. Cuando no resuelve: silencio, nunca crashea el hook.
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Resuelve la lib pura de decisión del fallback (`scripts/lib/ciclo-autonomo/
52
+ * fallback.js`). Dual: repo madre / destino copy-mode (`../scripts/lib/...`)
53
+ * vs destino aplanado (`./lib/ciclo-autonomo/fallback`, mismo criterio que
54
+ * `hooks/check-update.js` con `scripts/lib/npm-version.js`). Ausente → null.
55
+ */
56
+ function resolverFallbackCiclo() {
57
+ try {
58
+ return require('../scripts/lib/ciclo-autonomo/fallback');
59
+ } catch (e1) {
60
+ if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
61
+ try {
62
+ return require('./lib/ciclo-autonomo/fallback');
63
+ } catch (e2) {
64
+ if (e2 && e2.code !== 'MODULE_NOT_FOUND') throw e2;
65
+ return null;
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Resuelve la ruta absoluta del wrapper CLI del ciclo
72
+ * (`scripts/cli/ciclo-autonomo.js`) SIN requerirlo — `require.resolve` nunca
73
+ * ejecuta el módulo, evita cargar en este proceso el árbol completo del ciclo
74
+ * (config/lock/writer/drenador/puente) solo para calcular un path.
75
+ */
76
+ function resolverRutaCliCiclo() {
77
+ try {
78
+ return require.resolve('../scripts/cli/ciclo-autonomo');
79
+ } catch (e1) {
80
+ if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
81
+ try {
82
+ return require.resolve('./lib/ciclo-autonomo/ciclo-autonomo');
83
+ } catch (e2) {
84
+ if (e2 && e2.code !== 'MODULE_NOT_FOUND') throw e2;
85
+ return null;
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Dispara la Fase A del ciclo autónomo en un proceso separado
92
+ * (fire-and-forget, `detached` + `unref`) cuando el motor está habilitado y
93
+ * pasaron >=24h desde la última corrida (o nunca corrió). Guard barato
94
+ * PRIMERO: si no existe `.planning/evolution/ciclo-config.json`, retorna sin
95
+ * requerir ninguna lib del ciclo — la inmensa mayoría de proyectos nunca lo
96
+ * habilitó y el presupuesto de este hook (<200ms) no debe pagar ese costo.
97
+ * El hook NO espera el resultado; el lock propio del ciclo (T-02) garantiza
98
+ * que no haya doble corrida con el scheduler cron.
99
+ */
100
+ function dispararCicloAutonomoSiToca(cwd, hoy) {
101
+ if (!fs.existsSync(path.join(cwd, '.planning', 'evolution', 'ciclo-config.json'))) return;
102
+
103
+ const fallback = resolverFallbackCiclo();
104
+ if (!fallback) return;
105
+
106
+ const { disparar } = fallback.debeDisparar(cwd, { ahora: hoy });
107
+ if (!disparar) return;
108
+
109
+ const rutaCli = resolverRutaCliCiclo();
110
+ if (!rutaCli) return; // no resuelve en esta instalación: silencio, nunca crashea
111
+
112
+ spawn(
113
+ process.execPath,
114
+ ['-e', `require(${JSON.stringify(rutaCli)})({})`],
115
+ { detached: true, stdio: 'ignore', cwd }
116
+ ).unref();
117
+ }
118
+
119
+ /** Texto de un archivo o `null` si no existe / no se puede leer. */
120
+ function leerTextoSeguro(p) {
121
+ try {
122
+ return fs.readFileSync(p, 'utf8');
123
+ } catch (_) {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Línea de resumen de la última corrida del ciclo autónomo para el digest,
130
+ * cuando `ciclo-config.json` existe y tiene `ultima_corrida` registrada.
131
+ */
132
+ function lineaResumenCiclo(cwd) {
133
+ const txt = leerTextoSeguro(path.join(cwd, '.planning', 'evolution', 'ciclo-config.json'));
134
+ if (!txt) return null;
135
+ let cfg;
136
+ try {
137
+ cfg = JSON.parse(txt);
138
+ } catch (_) {
139
+ return null;
140
+ }
141
+ const u = cfg && cfg.ultima_corrida;
142
+ if (!u) return null;
143
+ return `Ciclo autónomo: última corrida ${cfg.last_run || 'desconocida'} — ` +
144
+ `promovió ${u.promovidos || 0}, degradó ${u.degradados || 0}, drenó ${u.drenados || 0}`;
145
+ }
146
+
35
147
  let briefing;
36
148
  let atomicWriteJSON;
37
149
  try {
@@ -53,7 +165,7 @@ process.stdin.on('end', () => {
53
165
  if (process.env.SWL_BRIEFING === '0') return; // opt-out
54
166
  if (!briefing) return; // lib ausente: degradar a silencio
55
167
 
56
- const cwd = process.cwd();
168
+ const cwd = raizProyecto(process.cwd());
57
169
  if (!fs.existsSync(path.join(cwd, '.planning'))) return; // zero-config
58
170
 
59
171
  const hoy = new Date();
@@ -78,8 +190,22 @@ process.stdin.on('end', () => {
78
190
  const digest = briefing.armarDigest(items, estadoPrevio, dia, {
79
191
  categoriasSilenciadas: silenciadas,
80
192
  });
193
+
194
+ // Fallback de disparo del ciclo autónomo (Fase 21, T-11, REQ-21-11): aislado
195
+ // en su propio try/catch y evaluado ANTES del early-return de silencio total
196
+ // — es una acción de fondo independiente de si hay señales que mostrar hoy.
197
+ try {
198
+ dispararCicloAutonomoSiToca(cwd, hoy);
199
+ } catch (_) { /* fire-and-forget best-effort: nunca bloquear la sesión */ }
200
+
81
201
  if (!digest) return; // silencio total: sin señales nuevas
82
202
 
203
+ // Línea de resumen del ciclo autónomo (si hubo corrida reciente registrada).
204
+ try {
205
+ const lineaCiclo = lineaResumenCiclo(cwd);
206
+ if (lineaCiclo) digest.texto = `${digest.texto}\n${lineaCiclo}`;
207
+ } catch (_) { /* best-effort: no romper el digest por esto */ }
208
+
83
209
  // Persistir el estado de dedupe (best-effort).
84
210
  try {
85
211
  const dir = path.join(cwd, '.planning', 'user-profile');
@@ -32,6 +32,7 @@
32
32
  */
33
33
 
34
34
  const fs = require('fs');
35
+ const { raizProyecto } = require('./lib/raiz-proyecto');
35
36
  const path = require('path');
36
37
  const crypto = require('crypto');
37
38
 
@@ -180,7 +181,7 @@ function main() {
180
181
  process.exit(0);
181
182
  }
182
183
 
183
- const cwd = process.cwd();
184
+ const cwd = raizProyecto(process.cwd());
184
185
  const toolName = evento?.tool_name;
185
186
  const filePath = evento?.tool_input?.file_path;
186
187
 
@@ -33,6 +33,7 @@
33
33
  */
34
34
 
35
35
  const fs = require('fs');
36
+ const { raizProyecto } = require('./lib/raiz-proyecto');
36
37
  const path = require('path');
37
38
 
38
39
  let isPackageRoot, readEvolutionMeta;
@@ -52,7 +53,7 @@ const CARPETAS_EVOLUCIONABLES = [
52
53
  ];
53
54
 
54
55
  const STATE_FILE = path.join(
55
- process.cwd(),
56
+ raizProyecto(process.cwd()),
56
57
  '.planning',
57
58
  'comms',
58
59
  'sugerir-contribuir-cooldown.json'
@@ -68,7 +69,7 @@ function rutaRelativaAlProyecto(rutaRaw) {
68
69
  let normalizada = normalizarSeparadores(rutaRaw);
69
70
 
70
71
  if (path.isAbsolute(normalizada)) {
71
- const cwd = normalizarSeparadores(process.cwd());
72
+ const cwd = normalizarSeparadores(raizProyecto(process.cwd()));
72
73
  if (normalizada.toLowerCase().startsWith(cwd.toLowerCase())) {
73
74
  normalizada = normalizada.slice(cwd.length).replace(/^\/+/, '');
74
75
  } else {
@@ -174,7 +175,9 @@ function evaluar(toolName, toolInput) {
174
175
  if (!ruta) return null;
175
176
  if (!esEvolucionable(ruta)) return null;
176
177
 
177
- const rutaAbsoluta = path.isAbsolute(rutaRaw) ? rutaRaw : path.join(process.cwd(), ruta);
178
+ // `ruta` ya es relativa a la raíz del proyecto (rutaRelativaAlProyecto la
179
+ // despoja contra esa misma ancla) — reconstruir contra la raíz, no el cwd.
180
+ const rutaAbsoluta = path.isAbsolute(rutaRaw) ? rutaRaw : path.join(raizProyecto(process.cwd()), ruta);
178
181
  if (!fs.existsSync(rutaAbsoluta)) return null;
179
182
 
180
183
  const evo = readEvolutionMeta(rutaAbsoluta);
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  const fs = require('fs');
30
+ const { raizProyecto } = require('./lib/raiz-proyecto');
30
31
  const path = require('path');
31
32
 
32
33
  // Carpetas cuyo cambio invalida los contadores de INVENTARIO.md
@@ -38,7 +39,7 @@ const CARPETAS_INVENTARIADAS = [
38
39
  { ruta: 'reglas/', etiqueta: 'reglas' },
39
40
  ];
40
41
 
41
- const STATE_FILE = path.join(process.cwd(), '.planning', 'inventario-aviso-state.json');
42
+ const STATE_FILE = path.join(raizProyecto(process.cwd()), '.planning', 'inventario-aviso-state.json');
42
43
  const COOLDOWN_MS = 30 * 60 * 1000; // 30 min
43
44
 
44
45
  function normalizarSeparadores(p) {
@@ -52,7 +53,7 @@ function detectarCarpeta(rutaRaw) {
52
53
  // Solo nos interesan rutas relativas al proyecto. Si es absoluta, intentamos
53
54
  // hacerla relativa al CWD actual.
54
55
  if (path.isAbsolute(normalizada)) {
55
- const cwd = normalizarSeparadores(process.cwd());
56
+ const cwd = normalizarSeparadores(raizProyecto(process.cwd()));
56
57
  if (normalizada.toLowerCase().startsWith(cwd.toLowerCase())) {
57
58
  normalizada = normalizada.slice(cwd.length).replace(/^\/+/, '');
58
59
  } else {
package/hooks/tdd-gate.js CHANGED
@@ -30,6 +30,7 @@
30
30
  */
31
31
 
32
32
  const fs = require('fs');
33
+ const { raizProyecto } = require('./lib/raiz-proyecto');
33
34
  const path = require('path');
34
35
  const { execFileSync } = require('child_process');
35
36
 
@@ -209,7 +210,7 @@ function main() {
209
210
 
210
211
  if (evento?.tool_name !== 'Bash') process.exit(0);
211
212
  const comando = evento?.tool_input?.command;
212
- const cwd = process.cwd();
213
+ const cwd = raizProyecto(process.cwd());
213
214
 
214
215
  // Zero-config: sin .planning/ no hay ciclo GSD que vigilar.
215
216
  if (!fs.existsSync(path.join(cwd, '.planning'))) process.exit(0);
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  const { registrar } = require('./lib/otlp-exporter');
20
+ const { raizProyecto } = require('./lib/raiz-proyecto');
20
21
 
21
22
  // Importar esquema de spans con guard de robustez — el hook no debe fallar
22
23
  // si el módulo aún no existe en instalaciones anteriores del sistema.
@@ -27,7 +28,7 @@ try {
27
28
  // Módulo no disponible — continuar sin tipado de span
28
29
  }
29
30
 
30
- const CWD = process.cwd();
31
+ const CWD = raizProyecto(process.cwd());
31
32
 
32
33
  // ---------------------------------------------------------------------------
33
34
  // Mapping de eventos a tipos de span
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ const { raizProyecto } = require('./lib/raiz-proyecto');
3
4
 
4
5
  /**
5
6
  * Hook: telemetria-skill-routing.js
@@ -91,7 +92,7 @@ if (!require.main || require.main === module) {
91
92
  process.stdin.on('data', (chunk) => { inputRaw += chunk; });
92
93
  process.stdin.on('end', () => {
93
94
  try {
94
- procesar(JSON.parse(inputRaw), process.cwd());
95
+ procesar(JSON.parse(inputRaw), raizProyecto(process.cwd()));
95
96
  } catch (_) {
96
97
  // Silencioso — nunca interrumpir la sesión por un fallo de telemetría.
97
98
  }
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  const fs = require('fs');
24
+ const { raizProyecto } = require('./lib/raiz-proyecto');
24
25
  const path = require('path');
25
26
  const os = require('os');
26
27
 
@@ -75,10 +76,10 @@ const PRESUPUESTO_DEFAULT = {
75
76
  };
76
77
 
77
78
  /** Ruta del manifesto de configuración de hooks. */
78
- const RUTA_HOOKS_CONFIG = path.join(process.cwd(), 'manifiestos', 'hooks-config.json');
79
+ const RUTA_HOOKS_CONFIG = path.join(raizProyecto(process.cwd()), 'manifiestos', 'hooks-config.json');
79
80
 
80
81
  /** Ruta del archivo de métricas de sesión. */
81
- const RUTA_METRICS = path.join(process.cwd(), '.planning', 'METRICAS.md');
82
+ const RUTA_METRICS = path.join(raizProyecto(process.cwd()), '.planning', 'METRICAS.md');
82
83
 
83
84
  // ---------------------------------------------------------------------------
84
85
  // Utilidades de configuración
@@ -673,7 +674,7 @@ process.stdin.on('end', () => {
673
674
  if (estado.toolCalls % INTERVALO_ACTUALIZACION === 0) {
674
675
  try {
675
676
  const { actualizarSesion } = require('./lib/session-store');
676
- const cwd = process.cwd();
677
+ const cwd = raizProyecto(process.cwd());
677
678
  actualizarSesion(cwd, {
678
679
  toolCall: toolName,
679
680
  tokensEstimated: estado.tokensTotal,
@@ -23,9 +23,10 @@
23
23
  */
24
24
 
25
25
  const fs = require('fs');
26
+ const { raizProyecto } = require('./lib/raiz-proyecto');
26
27
  const path = require('path');
27
28
 
28
- const CWD = process.cwd();
29
+ const CWD = raizProyecto(process.cwd());
29
30
  const SCHEMAS_PATH = path.join(CWD, 'manifiestos', 'agent-output-schemas.json');
30
31
  const LOG_PATH = path.join(CWD, '.planning', 'evolution', 'formato-violaciones.jsonl');
31
32
 
@@ -44,6 +44,7 @@
44
44
  */
45
45
 
46
46
  const fs = require('fs');
47
+ const { raizProyecto } = require('./lib/raiz-proyecto');
47
48
  const path = require('path');
48
49
  const crypto = require('crypto');
49
50
 
@@ -188,7 +189,7 @@ if (faltan.length === 0) {
188
189
  }
189
190
 
190
191
  // ─── Emitir nudge ─────────────────────────────────────────────────────────
191
- const CWD = process.cwd();
192
+ const CWD = raizProyecto(process.cwd());
192
193
  const rutaRelativa = path.relative(CWD, filePath).replace(/\\/g, '/');
193
194
  const nombreAgente = path.basename(filePath, '.md');
194
195
 
@@ -34,6 +34,7 @@
34
34
  */
35
35
 
36
36
  const fs = require('fs');
37
+ const { raizProyecto } = require('./lib/raiz-proyecto');
37
38
  const path = require('path');
38
39
  const { spawnSync } = require('child_process');
39
40
 
@@ -49,8 +50,17 @@ const ARCHIVOS_MEMORIA = [
49
50
  'instintos/global.yaml',
50
51
  ];
51
52
 
52
- /** Path del validador. */
53
- const VALIDADOR = path.join(process.cwd(), 'scripts', 'validar-memoria.js');
53
+ /**
54
+ * Path del validador — EJECUTABLE de la distribución SWL: se resuelve SOLO
55
+ * desde la ubicación instalada del hook (__dirname), NUNCA desde el árbol
56
+ * del proyecto (un repo no confiable podría plantar el script; hallazgo
57
+ * revisor-seguridad 2026-07-10). Los DATOS que valida sí anclan a la raíz
58
+ * del proyecto (cwd del spawn).
59
+ */
60
+ const VALIDADOR = [
61
+ path.join(__dirname, '..', 'scripts', 'validar-memoria.js'), // repo madre
62
+ path.join(__dirname, 'validar-memoria.js'), // destino aplanado
63
+ ].find(p => { try { return fs.existsSync(p); } catch (_) { return false; } }) || '';
54
64
 
55
65
  /** Timeout para el validador (ms). Si tarda más, se aborta sin bloquear. */
56
66
  const TIMEOUT_MS = 5000;
@@ -106,7 +116,7 @@ function ejecutarValidador() {
106
116
  if (!fs.existsSync(VALIDADOR)) return null;
107
117
  try {
108
118
  const r = spawnSync(process.execPath, [VALIDADOR, '--json'], {
109
- cwd: process.cwd(),
119
+ cwd: raizProyecto(process.cwd()),
110
120
  encoding: 'utf8',
111
121
  timeout: TIMEOUT_MS,
112
122
  windowsHide: true,
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  const fs = require('fs');
22
+ const { raizProyecto } = require('./lib/raiz-proyecto');
22
23
  const path = require('path');
23
24
  const crypto = require('crypto');
24
25
 
@@ -76,7 +77,7 @@ const DIRS_CANONICOS = [
76
77
  ];
77
78
 
78
79
  // ─── Cargar allowlist (manifest si existe, embebida si no) ──────────────────
79
- const CWD = process.cwd();
80
+ const CWD = raizProyecto(process.cwd());
80
81
  const allowlistPath = path.join(CWD, 'manifiestos', 'planning-paths.json');
81
82
  let permitidos;
82
83
  try {