@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
@@ -30,7 +30,7 @@ validar y un score de componentes sería engañoso. El gate determinista vive en
30
30
  `/swl:status salud` (Paso 0):
31
31
 
32
32
  ```bash
33
- node scripts/lib/detectar-host-swl.js --json
33
+ swl-ses detectar-host --json
34
34
  ```
35
35
 
36
36
  Si `esHost: false`, NO ejecutar este skill: reportar que `salud` no aplica y
@@ -21,9 +21,10 @@
21
21
  */
22
22
 
23
23
  const { desdePayloadHook, emitirCompletado } = require('./lib/agent-backend');
24
+ const { raizProyecto } = require('./lib/raiz-proyecto');
24
25
  const es = require('./lib/execution-state');
25
26
 
26
- const CWD = process.cwd();
27
+ const CWD = raizProyecto(process.cwd());
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Entrypoint
@@ -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, randomUUID } = require('child_process');
39
40
  const crypto = require('crypto');
@@ -92,10 +93,18 @@ if (!fs.existsSync(filePath)) {
92
93
  process.exit(0);
93
94
  }
94
95
 
95
- // Ruta del detector (zero-deps, stdlib Python)
96
- const CWD = process.cwd();
97
- const detector = path.join(CWD, 'habilidades', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py');
98
- if (!fs.existsSync(detector)) {
96
+ // Ancla de DATOS (nudges, rutas relativas): raíz del proyecto.
97
+ const CWD = raizProyecto(process.cwd());
98
+ // El detector es EJECUTABLE de la distribución SWL: se resuelve SOLO desde
99
+ // la ubicación instalada del hook (__dirname), NUNCA desde el árbol del
100
+ // proyecto — un repo no confiable podría plantar el script (hallazgo
101
+ // revisor-seguridad 2026-07-10, clase spawn/require-desde-raíz).
102
+ const detector = [
103
+ path.join(__dirname, '..', 'habilidades', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py'), // repo madre
104
+ path.join(__dirname, '..', 'skills', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py'), // destino global (~/.claude/hooks ~ skills hermanos)
105
+ path.join(__dirname, '..', '.claude', 'skills', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py'), // destino local (<proyecto>/hooks + <proyecto>/.claude/skills)
106
+ ].find(p => { try { return fs.existsSync(p); } catch (_) { return false; } });
107
+ if (!detector) {
99
108
  process.exit(0);
100
109
  }
101
110
 
@@ -19,13 +19,14 @@
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 http = require('http');
24
25
  const https = require('https');
25
26
 
26
27
  const { appendAudit: merkleAppend } = require(path.join(__dirname, 'lib', 'merkle-audit'));
27
28
 
28
- const CWD = process.cwd();
29
+ const CWD = raizProyecto(process.cwd());
29
30
  const AUDIT_FILE = path.join(CWD, '.planning', 'audit.jsonl');
30
31
  const MERKLE_FILE = path.join(CWD, '.planning', 'audit-merkle.jsonl');
31
32
 
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  const path = require('path');
32
+ const { raizProyecto } = require('./lib/raiz-proyecto');
32
33
  const fs = require('fs');
33
34
  const os = require('os');
34
35
 
@@ -97,7 +98,7 @@ process.stdin.on('end', () => {
97
98
  // Anti-spam: solo ejecutar 1 vez por sesión
98
99
  if (alreadyRan()) return;
99
100
 
100
- const baseDir = process.cwd();
101
+ const baseDir = raizProyecto(process.cwd());
101
102
 
102
103
  // Verificar las 3 gates
103
104
  const gates = checkAllGates(baseDir, {
@@ -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 { execFileSync } = require('child_process');
24
25
 
@@ -142,7 +143,7 @@ if (!require.main || require.main === module) {
142
143
  process.stdin.on('end', () => {
143
144
  try {
144
145
  const data = JSON.parse(inputRaw);
145
- procesarTurno(data, process.cwd());
146
+ procesarTurno(data, raizProyecto(process.cwd()));
146
147
  } catch (_) {
147
148
  /* silencioso */
148
149
  }
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const fs = require('fs');
23
+ const { raizProyecto } = require('./lib/raiz-proyecto');
23
24
  const path = require('path');
24
25
  const { execFileSync } = require('child_process');
25
26
 
@@ -145,7 +146,7 @@ if (!require.main || require.main === module) {
145
146
  process.stdin.on('data', (c) => { inputRaw += c; });
146
147
  process.stdin.on('end', () => {
147
148
  try {
148
- inspeccionar(process.cwd());
149
+ inspeccionar(raizProyecto(process.cwd()));
149
150
  } catch (_) {
150
151
  /* silencioso: el hook nunca bloquea la sesión */
151
152
  }
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  const fs = require('fs');
28
+ const { raizProyecto } = require('./lib/raiz-proyecto');
28
29
  const path = require('path');
29
30
 
30
31
  // ---------------------------------------------------------------------------
@@ -104,7 +105,7 @@ function detectarFeedback(prompt) {
104
105
  */
105
106
  function encolar(hallazgo, meta) {
106
107
  try {
107
- const dir = path.join(process.cwd(), '.planning', 'evolution');
108
+ const dir = path.join(raizProyecto(process.cwd()), '.planning', 'evolution');
108
109
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
109
110
 
110
111
  const entrada = {
@@ -140,7 +141,7 @@ process.stdin.on('end', () => {
140
141
  if (hallazgos.length === 0) return;
141
142
 
142
143
  for (const h of hallazgos) {
143
- encolar(h, { sessionId, cwd: process.cwd() });
144
+ encolar(h, { sessionId, cwd: raizProyecto(process.cwd()) });
144
145
  }
145
146
  } catch (_) { /* silencioso */ }
146
147
  });
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  const fs = require('fs');
39
+ const { raizProyecto } = require('./lib/raiz-proyecto');
39
40
  const path = require('path');
40
41
  const crypto = require('crypto');
41
42
 
@@ -93,9 +94,17 @@ if (!fs.existsSync(filePath)) {
93
94
  }
94
95
 
95
96
  // ─── Ejecutar auditor (módulo, no subproceso) ─────────────────────────────
96
- const CWD = process.cwd();
97
- const auditorPath = path.join(CWD, 'scripts', 'auditar-claudemd.js');
98
- if (!fs.existsSync(auditorPath)) {
97
+ // Ancla de DATOS (nudges, rutas relativas): raíz del proyecto.
98
+ const CWD = raizProyecto(process.cwd());
99
+ // El auditor es MÓDULO de la distribución SWL: se resuelve SOLO desde la
100
+ // ubicación instalada del hook (__dirname), NUNCA desde el árbol del
101
+ // proyecto — un require() de un path plantado por un repo no confiable es
102
+ // ejecución de código (hallazgo revisor-seguridad 2026-07-10).
103
+ const auditorPath = [
104
+ path.join(__dirname, '..', 'scripts', 'auditar-claudemd.js'), // repo madre
105
+ path.join(__dirname, 'auditar-claudemd.js'), // destino aplanado
106
+ ].find(p => { try { return fs.existsSync(p); } catch (_) { return false; } });
107
+ if (!auditorPath) {
99
108
  // No hay auditor instalado en este destino; salir silenciosamente
100
109
  process.exit(0);
101
110
  }
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  const fs = require('fs');
42
+ const { raizProyecto } = require('./lib/raiz-proyecto');
42
43
  const path = require('path');
43
44
  const os = require('os');
44
45
  const crypto = require('crypto');
@@ -102,9 +103,18 @@ if (!fs.existsSync(filePath)) {
102
103
  }
103
104
 
104
105
  // ─── Cargar detector ──────────────────────────────────────────────────────
105
- const CWD = process.cwd();
106
- const detectorPath = path.join(CWD, 'scripts', 'lib', 'detector-reglas-duplicadas.js');
107
- if (!fs.existsSync(detectorPath)) {
106
+ // Ancla de DATOS (nudges, rutas relativas): raíz del proyecto.
107
+ const CWD = raizProyecto(process.cwd());
108
+ // El detector es MÓDULO de la distribución SWL: se resuelve SOLO desde la
109
+ // ubicación instalada del hook (__dirname), NUNCA desde el árbol del
110
+ // proyecto — un require() de un path plantado por un repo no confiable es
111
+ // ejecución de código (hallazgo revisor-seguridad 2026-07-10).
112
+ const detectorPath = [
113
+ path.join(__dirname, '..', 'scripts', 'lib', 'detector-reglas-duplicadas.js'), // repo madre
114
+ path.join(__dirname, 'lib', 'detector-reglas-duplicadas.js'), // destino (scripts/lib → hooks/lib)
115
+ path.join(__dirname, 'detector-reglas-duplicadas.js'), // destino legado aplanado
116
+ ].find(p => { try { return fs.existsSync(p); } catch (_) { return false; } });
117
+ if (!detectorPath) {
108
118
  // No hay detector instalado en este destino; salir silenciosamente
109
119
  process.exit(0);
110
120
  }
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  const fs = require('fs');
31
+ const { raizProyecto } = require('./lib/raiz-proyecto');
31
32
  const path = require('path');
32
33
 
33
34
  const { atomicWriteJSON } = require('./lib/atomic-write');
@@ -112,7 +113,7 @@ function main() {
112
113
  try {
113
114
  if (process.env.SWL_CONTEXTO_ITERACION === '0') return;
114
115
 
115
- const activa = corridaActiva(process.cwd(), { maxEdadMin: 30 });
116
+ const activa = corridaActiva(raizProyecto(process.cwd()), { maxEdadMin: 30 });
116
117
  if (!activa) return;
117
118
  if (!debeInyectar(activa.dir)) return;
118
119
 
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const fs = require('fs');
23
+ const { raizProyecto } = require('./lib/raiz-proyecto');
23
24
  const path = require('path');
24
25
 
25
26
  const { atomicWriteSync } = require('./lib/atomic-write');
@@ -28,7 +29,7 @@ const { atomicWriteSync } = require('./lib/atomic-write');
28
29
  // Constantes
29
30
  // ---------------------------------------------------------------------------
30
31
 
31
- const RUTA_APRENDIZAJES = path.join(process.cwd(), '.planning', 'APRENDIZAJES.md');
32
+ const RUTA_APRENDIZAJES = path.join(raizProyecto(process.cwd()), '.planning', 'APRENDIZAJES.md');
32
33
  const MAX_CONTRADICCIONES = 3;
33
34
 
34
35
  // Palabras clave que indican exito en el output del agente
@@ -31,6 +31,8 @@ const path = require('path');
31
31
  const os = require('os');
32
32
 
33
33
  const { atomicWriteSync } = require('./lib/atomic-write');
34
+ const { raizProyecto } = require('./lib/raiz-proyecto');
35
+ const { conLockRetry } = require('./lib/singleton-guard');
34
36
 
35
37
  // ---------------------------------------------------------------------------
36
38
  // Constantes de configuración
@@ -38,9 +40,13 @@ const { atomicWriteSync } = require('./lib/atomic-write');
38
40
 
39
41
  /**
40
42
  * Ruta del archivo donde se acumulan los aprendizajes.
41
- * Se resuelve relativa al CWD del proceso (raíz del proyecto).
43
+ * Se re-resuelve en el entrypoint con la RAÍZ DEL PROYECTO (raizProyecto:
44
+ * primer .planning/ real ascendiendo, o raíz del repo git, o fallback cwd) —
45
+ * NUNCA con el CWD crudo de la sesión: en monorepos con sesión abierta en un
46
+ * subdirectorio eso fragmentaba la memoria en árboles .planning/ huérfanos
47
+ * (DT-HOOKS-RAIZ-GIT, reportes de campo sigm + oic 2026-07-10).
42
48
  */
43
- const RUTA_APRENDIZAJES = path.join(process.cwd(), '.planning', 'APRENDIZAJES.md');
49
+ let RUTA_APRENDIZAJES = path.join(raizProyecto(process.cwd()), '.planning', 'APRENDIZAJES.md');
44
50
 
45
51
  /**
46
52
  * Número de líneas de contexto a extraer alrededor del keyword detectado.
@@ -364,9 +370,15 @@ function extraerTextoYAgente(data) {
364
370
  * 4. Clasificar por el tipo de keyword que disparó la detección.
365
371
  *
366
372
  * @param {string} texto
373
+ * @param {object} [opts]
374
+ * @param {boolean} [opts.descartarFragmentos=false] - Descarta candidatos cuya
375
+ * descripción quedó TRUNCADA a media frase (termina en '…' sin oración
376
+ * completa). Obligatorio para fuentes Write/Edit: los comentarios de código
377
+ * de dominio largos producían entradas cortadas a medias que no son
378
+ * aprendizajes (reporte de campo sigm 2026-07-10).
367
379
  * @returns {Aprendizaje|null}
368
380
  */
369
- function detectarAprendizaje(texto) {
381
+ function detectarAprendizaje(texto, opts = {}) {
370
382
  if (!texto || texto.trim().length < MIN_CHARS_DESCRIPCION) return null;
371
383
 
372
384
  const lineas = texto.split('\n');
@@ -395,12 +407,28 @@ function detectarAprendizaje(texto) {
395
407
  // Descripción: la línea del keyword, limpiada de símbolos de código.
396
408
  // Preferir cortar al final de la primera oración si cabe en 120 chars;
397
409
  // si no, al último espacio antes del límite.
398
- const lineaLimpia = lineas[i]
410
+ const limpiarLinea = l => l
399
411
  .trim()
400
412
  .replace(/^[#*\/\-–•]+\s*/, '') // Quitar prefijos de comentario/lista
401
413
  .replace(/\*\*/g, '') // Quitar marcadores de negrita Markdown
402
414
  .replace(/`+/g, ''); // Quitar backticks
403
415
 
416
+ let lineaLimpia = limpiarLinea(lineas[i]);
417
+
418
+ // Wrap del editor ≠ fin del pensamiento: si la línea del keyword no
419
+ // cierra oración, unir hasta 2 líneas siguientes antes de buscar el
420
+ // corte — los comentarios largos envuelven la frase y cortar en el
421
+ // wrap producía títulos a media frase (ticket de campo sigm
422
+ // 2026-07-10: "...pueden cerrar la 'corrección' apuntando a").
423
+ if (!/[.!?]/.test(lineaLimpia)) {
424
+ for (let j = i + 1; j < Math.min(i + 3, lineas.length); j++) {
425
+ const continuacion = limpiarLinea(lineas[j]);
426
+ if (!continuacion) break;
427
+ lineaLimpia += ' ' + continuacion;
428
+ if (/[.!?]/.test(continuacion)) break;
429
+ }
430
+ }
431
+
404
432
  // Si la línea tiene un punto temprano, usarlo como fin natural
405
433
  const matchOracion = lineaLimpia.match(/^[^.!?]*[.!?]/);
406
434
  const descripcionBase = matchOracion
@@ -411,6 +439,12 @@ function detectarAprendizaje(texto) {
411
439
 
412
440
  if (descripcion.length < 10) continue;
413
441
 
442
+ // Fragmento a media frase: ni la línea ni sus continuaciones cierran
443
+ // oración Y hubo que truncar — para fuentes de escritura de código
444
+ // eso es ruido. Una oración completa truncada elegante (corte en
445
+ // frontera de cláusula con '…') SÍ es título legítimo.
446
+ if (opts.descartarFragmentos && !matchOracion && descripcion.endsWith('…')) continue;
447
+
414
448
  return { tipo, contexto: contextoFinal, descripcion };
415
449
  }
416
450
  }
@@ -540,6 +574,27 @@ function formatearEntrada(fecha, tipo, descripcion, agente, contexto) {
540
574
  * @param {string} agente
541
575
  */
542
576
  function escribirAprendizaje(aprendizaje, agente) {
577
+ // Read-modify-write sobre APRENDIZAJES.md (leer todo → dedup → reescribir
578
+ // todo). Dos hooks concurrentes leían la misma base y el segundo write
579
+ // pisaba la entrada del primero → aprendizaje perdido (nemesis S2-2).
580
+ // El lock O_EXCL serializa el CASO COMÚN (secciones críticas de <<1s): dos
581
+ // hooks casi-simultáneos se ordenan, sin pérdida. La lectura ocurre DENTRO de
582
+ // la sección crítica para ver base fresca.
583
+ //
584
+ // Límite honesto (re-auditoría nemesis iter-2): siNoAdquiere:'ejecutar'
585
+ // significa que si tras maxWaitMs NO se adquiere el lock —solo posible con un
586
+ // holder VIVO ocupando el lock >1s, patológico para un archivo de KB (un
587
+ // holder muerto se reclama al instante vía pidVivo)— se corre igual SIN lock.
588
+ // En ese caso extremo el rewrite-whole-file PUEDE pisar la entrada de otro
589
+ // proceso. Se prefiere ese riesgo casi-inalcanzable a OMITIR y descartar el
590
+ // aprendizaje actual con certeza.
591
+ conLockRetry('extraccion-aprendizajes-md', () => _escribirAprendizajeInterno(aprendizaje, agente), {
592
+ maxWaitMs: 1000,
593
+ siNoAdquiere: 'ejecutar',
594
+ });
595
+ }
596
+
597
+ function _escribirAprendizajeInterno(aprendizaje, agente) {
543
598
  asegurarDirectorio();
544
599
 
545
600
  const { contenidoExistente, descripciones } = leerAprendizajesExistentes();
@@ -676,6 +731,11 @@ const PATRONES_ARCHIVO_SWL_EXCLUIDO = [
676
731
  // "rules" (inglés). Detectado 2026-06-11: fragmentos de una edición a
677
732
  // verificar-citas-normativas.md (global) se promovieron como "patrón".
678
733
  /(?:^|[\\/])\.claude[\\/]rules[\\/]/,
734
+ // Workflows y config de CI (.github/): sus comentarios YAML describen el
735
+ // proceso con keywords narrativas ("decisión", "aprobación") que el hook
736
+ // confunde con descubrimientos. Detectado 2026-07-10: el comentario del
737
+ // environment de publish-npm.yml se promovió como entrada "decisión".
738
+ /(?:^|[\\/])\.github[\\/]/,
679
739
  ];
680
740
 
681
741
  /**
@@ -715,6 +775,16 @@ function pareceContenidoEstructuralSwl(contexto) {
715
775
  return true;
716
776
  }
717
777
 
778
+ // Bold-labels de docs sin cuerpo: líneas que son SOLO una etiqueta en
779
+ // negritas ("**Uso:**", "**¿Cuándo usarlo?**"), con o sin valor corto de
780
+ // código a continuación. Junto con los headings (abajo) cubren fragmentos
781
+ // estructurales de manuales/skills que el hook confundía con aprendizajes
782
+ // (detectado 2026-07-10: headings y labels de MANUAL_USO promovidos como
783
+ // "gotcha"/"patrón").
784
+ const esLineaEstructural = l =>
785
+ /^(>\s*)?#{1,6}\s+\S/.test(l) ||
786
+ /^(>\s*)?\*\*[^*]{1,60}\*\*:?\s*(`[^`]*`\s*)?$/.test(l);
787
+
718
788
  // Enumeración de encabezados sin cuerpo sustantivo
719
789
  // (3+ líneas tipo "## [fecha] tipo — descripción" juntas)
720
790
  const encabezadosFecha = lineas.filter(l => /^##\s+\[\d{4}-\d{2}-\d{2}\]/.test(l));
@@ -722,15 +792,15 @@ function pareceContenidoEstructuralSwl(contexto) {
722
792
  return true;
723
793
  }
724
794
 
725
- // Contexto compuesto mayoritariamente de headings sin cuerpo. Detecta
726
- // placeholders auto-generados al hacer Write de archivos meta (RELEASE-NOTES,
727
- // RESUMEN, PLAN) cuya estructura es solo `## Sección 1`, `## Sección 2`...
728
- // sin párrafos sustantivos. Acepta tanto headings desnudos (`## Titulo`)
729
- // como indentados como blockquote (`> ## Titulo`) porque el hook envuelve
730
- // el contexto con `> ` antes de evaluar el resultado.
795
+ // Contexto compuesto mayoritariamente de headings/bold-labels sin cuerpo.
796
+ // Detecta placeholders auto-generados al hacer Write de archivos meta
797
+ // (RELEASE-NOTES, RESUMEN, PLAN) y fragmentos de docs de comandos cuya
798
+ // estructura es solo `## Sección`, `**Uso:**`... sin párrafos sustantivos.
799
+ // Acepta tanto líneas desnudas como indentadas como blockquote (`> ## X`)
800
+ // porque el hook envuelve el contexto con `> ` antes de evaluar.
731
801
  if (noVacias.length >= 3) {
732
- const headings = noVacias.filter(l => /^(>\s*)?#{1,6}\s+\S/.test(l));
733
- if (headings.length / noVacias.length > 0.6) {
802
+ const estructurales = noVacias.filter(esLineaEstructural);
803
+ if (estructurales.length / noVacias.length > 0.6) {
734
804
  return true;
735
805
  }
736
806
  }
@@ -781,15 +851,36 @@ process.stdin.on('end', () => {
781
851
  return;
782
852
  }
783
853
 
854
+ // Anclar la memoria a la RAÍZ DEL PROYECTO, no al CWD de la sesión
855
+ // (DT-HOOKS-RAIZ-GIT): una sesión abierta en backend/ de un monorepo
856
+ // escribía backend/.planning/APRENDIZAJES.md huérfano.
857
+ const baseCwd = String(data.cwd || process.cwd());
858
+ const raiz = raizProyecto(baseCwd);
859
+ RUTA_APRENDIZAJES = path.join(raiz, '.planning', 'APRENDIZAJES.md');
860
+
861
+ const esFuenteEscritura =
862
+ toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit';
863
+
784
864
  // Filtrar Write/Edit sobre archivos del sistema SWL (auto-referencia).
785
865
  // Editar habilidades/, agentes/, reglas/, CHANGELOG, CLAUDE.md, etc. es
786
866
  // mantenimiento del sistema, no descubrimiento en tiempo de ejecución.
787
- if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
867
+ if (esFuenteEscritura) {
788
868
  const toolInput = data.tool_input || data.tool?.input || {};
789
869
  const filePath = String(toolInput.file_path || toolInput.path || '');
790
870
  if (esArchivoSwlExcluido(filePath)) {
791
871
  return;
792
872
  }
873
+ // Escrituras FUERA de la raíz del proyecto (scratchpad del harness,
874
+ // temp, otros repos) no son descubrimiento de ESTE proyecto — su
875
+ // contenido meta (borradores, docs en construcción) generaba entradas
876
+ // espurias (detectado 2026-07-10 con archivos de trabajo en scratchpad).
877
+ if (filePath) {
878
+ const abs = path.resolve(baseCwd, filePath);
879
+ const rel = path.relative(raiz, abs);
880
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
881
+ return;
882
+ }
883
+ }
793
884
  }
794
885
 
795
886
  // Extraer texto y nombre del agente del payload
@@ -800,8 +891,11 @@ process.stdin.on('end', () => {
800
891
  return;
801
892
  }
802
893
 
803
- // Detectar aprendizaje en el texto
804
- const aprendizaje = detectarAprendizaje(texto);
894
+ // Detectar aprendizaje en el texto. Las fuentes de escritura de código
895
+ // exigen descripción con oración completa (sin fragmentos truncados).
896
+ const aprendizaje = detectarAprendizaje(texto, {
897
+ descartarFragmentos: esFuenteEscritura,
898
+ });
805
899
 
806
900
  if (!aprendizaje) {
807
901
  // Sin aprendizaje detectado — salir silenciosamente
@@ -19,9 +19,10 @@
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
 
24
- const CWD = process.env.PWD || process.cwd();
25
+ const CWD = raizProyecto(process.env.PWD || process.cwd());
25
26
  const REPORT_FILE = path.join(CWD, '.planning', 'REPORTE-GRAFO.md');
26
27
  const GRAPH_FILE = path.join(CWD, '.planning', 'graph.json');
27
28
 
@@ -23,6 +23,7 @@
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
29
  // ---------------------------------------------------------------------------
@@ -42,7 +43,7 @@ if (ES_ENTRYPOINT && !process.env.SWL_GUARDRAIL_MODELO) {
42
43
  // Rutas
43
44
  // ---------------------------------------------------------------------------
44
45
 
45
- const CWD = process.cwd();
46
+ const CWD = raizProyecto(process.cwd());
46
47
  const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
47
48
  const GUARDRAIL_LOG = path.join(DIR_EVOL, 'guardrail-observaciones.jsonl');
48
49
 
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const fs = require('fs');
19
+ const { raizProyecto } = require('./lib/raiz-proyecto');
19
20
  const path = require('path');
20
21
 
21
22
  const INBOX_DIR = '.planning/inbox';
@@ -51,7 +52,7 @@ process.stdin.on('end', () => {
51
52
  try {
52
53
  if (process.env.SWL_INBOX_AVISO === '0') return;
53
54
 
54
- const { total, preview } = contarPendientes(process.cwd());
55
+ const { total, preview } = contarPendientes(raizProyecto(process.cwd()));
55
56
  if (total === 0) return;
56
57
 
57
58
  const lineas = [];
@@ -23,6 +23,7 @@
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
  const os = require('os');
28
29
 
@@ -205,7 +206,7 @@ process.stdin.on('end', () => {
205
206
  return;
206
207
  }
207
208
 
208
- const cwd = process.cwd();
209
+ const cwd = raizProyecto(process.cwd());
209
210
  const sesion = leerSesionAnterior(cwd);
210
211
 
211
212
  // Solo inyectar si la sesion anterior tiene contenido util
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const fs = require('fs');
19
+ const { raizProyecto } = require('./raiz-proyecto');
19
20
  const path = require('path');
20
21
 
21
22
  // ---------------------------------------------------------------------------
@@ -123,7 +124,7 @@ function loadAgents(agentesDir) {
123
124
  * @returns {Array<{name: string, score: number, skills: string[]}>} Ordenado por score descendente.
124
125
  */
125
126
  function matchAgents(taskText, agentesDir) {
126
- const dir = agentesDir || path.join(process.cwd(), 'agentes');
127
+ const dir = agentesDir || path.join(raizProyecto(process.cwd()), 'agentes');
127
128
  const agents = loadAgents(dir);
128
129
  const taskKeywords = extractKeywords(taskText);
129
130
 
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  const fs = require('fs');
28
+ const { raizProyecto } = require('./raiz-proyecto');
28
29
  const path = require('path');
29
30
 
30
31
  const _cache = Object.create(null);
@@ -38,7 +39,7 @@ const _cache = Object.create(null);
38
39
  * @returns {string|null} - Ruta absoluta o null si no existe
39
40
  */
40
41
  function resolveAgentPath(agentName, cwd) {
41
- const baseDir = cwd || process.cwd();
42
+ const baseDir = cwd || raizProyecto(process.cwd());
42
43
  const candidates = [
43
44
  path.join(baseDir, 'agentes', `${agentName}.md`),
44
45
  path.join(baseDir, '.claude', 'agents', `${agentName}.md`),
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  const fs = require('fs');
25
+ const { raizProyecto } = require('./raiz-proyecto');
25
26
  const path = require('path');
26
27
  const { execFileSync } = require('child_process');
27
28
 
@@ -89,7 +90,7 @@ function _normalizarNivel(valor) {
89
90
  * @returns {{version?:string, defaults?:string, clases:Object}}
90
91
  */
91
92
  function cargarDial(baseDir) {
92
- const ruta = path.join(baseDir || process.cwd(), ...YAML_PATH);
93
+ const ruta = path.join(baseDir || raizProyecto(process.cwd()), ...YAML_PATH);
93
94
  let parsed = {};
94
95
  try {
95
96
  parsed = _parsearYamlPlano(fs.readFileSync(ruta, 'utf8'));
@@ -153,7 +154,7 @@ function _archivosModificados(baseDir) {
153
154
  const MAX_ACCION_LEN = 512; // tope para que el JSONL no crezca sin límite
154
155
 
155
156
  function autoCheckpoint(baseDir, accion, tsISO) {
156
- const base = baseDir || process.cwd();
157
+ const base = baseDir || raizProyecto(process.cwd());
157
158
  const registro = {
158
159
  ts: tsISO || new Date().toISOString(),
159
160
  accion: String(accion || '').slice(0, MAX_ACCION_LEN),
@@ -186,7 +187,7 @@ function _parseAccion(args) {
186
187
 
187
188
  function main(argv) {
188
189
  const accion = _parseAccion(argv.slice(2));
189
- const baseDir = process.cwd();
190
+ const baseDir = raizProyecto(process.cwd());
190
191
  const dial = cargarDial(baseDir);
191
192
  if (requiereAutoCheckpoint(dial, 'cambio_reversible')) {
192
193
  const reg = autoCheckpoint(baseDir, accion || '(sin descripción)');
@@ -204,5 +205,6 @@ module.exports = {
204
205
  cargarDial,
205
206
  requiereAutoCheckpoint,
206
207
  autoCheckpoint,
208
+ main,
207
209
  _parsearYamlPlano,
208
210
  };
@@ -27,6 +27,7 @@
27
27
  */
28
28
 
29
29
  const fs = require('fs');
30
+ const { raizProyecto } = require('./raiz-proyecto');
30
31
  const path = require('path');
31
32
  const crypto = require('crypto');
32
33
 
@@ -384,7 +385,7 @@ module.exports = {
384
385
  if (require.main === module) {
385
386
  try {
386
387
  if (process.argv.includes('--conteo')) {
387
- process.stdout.write(JSON.stringify(contarPorSubtipo(process.cwd()), null, 2) + '\n');
388
+ process.stdout.write(JSON.stringify(contarPorSubtipo(raizProyecto(process.cwd())), null, 2) + '\n');
388
389
  }
389
390
  } catch (_) {
390
391
  /* exit 0 siempre */