@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
@@ -67,11 +67,18 @@ function isPidAlive(pid) {
67
67
 
68
68
  /**
69
69
  * Ruta del lock file.
70
+ *
70
71
  * @param {string} baseDir - Raíz del proyecto.
72
+ * @param {object} [opts] - Opciones.
73
+ * @param {string} [opts.lockFilename] - Nombre de archivo alterno al default
74
+ * `.consolidation-lock`. Permite a otros consumidores (ej. el ciclo
75
+ * autónomo, Fase 21 T-02) usar un lockfile propio sin colisionar con el
76
+ * de auto-consolidación, reutilizando el mismo patrón PID+staleness.
71
77
  * @returns {string}
72
78
  */
73
- function lockPath(baseDir) {
74
- return path.join(baseDir, '.planning', LOCK_FILENAME);
79
+ function lockPath(baseDir, opts = {}) {
80
+ const filename = (opts && opts.lockFilename) || LOCK_FILENAME;
81
+ return path.join(baseDir, '.planning', filename);
75
82
  }
76
83
 
77
84
  // ---------------------------------------------------------------------------
@@ -82,10 +89,11 @@ function lockPath(baseDir) {
82
89
  * Obtiene el timestamp de la última consolidación exitosa.
83
90
  *
84
91
  * @param {string} baseDir - Raíz del proyecto.
92
+ * @param {object} [opts] - Ver {@link lockPath}.
85
93
  * @returns {number} Epoch ms de la última consolidación, 0 si nunca.
86
94
  */
87
- function getLastConsolidatedAt(baseDir) {
88
- const lp = lockPath(baseDir);
95
+ function getLastConsolidatedAt(baseDir, opts = {}) {
96
+ const lp = lockPath(baseDir, opts);
89
97
  try {
90
98
  const stat = fs.statSync(lp);
91
99
  return stat.mtimeMs;
@@ -150,12 +158,13 @@ function countNewSessions(baseDir) {
150
158
  * Intenta adquirir el lock de consolidación.
151
159
  *
152
160
  * @param {string} baseDir
161
+ * @param {object} [opts] - Ver {@link lockPath}.
153
162
  * @returns {{ acquired: boolean, priorMtime: number }}
154
163
  * acquired: true si el lock fue adquirido
155
164
  * priorMtime: mtime anterior (para rollback)
156
165
  */
157
- function tryAcquireLock(baseDir) {
158
- const lp = lockPath(baseDir);
166
+ function tryAcquireLock(baseDir, opts = {}) {
167
+ const lp = lockPath(baseDir, opts);
159
168
  const dir = path.dirname(lp);
160
169
 
161
170
  // Asegurar directorio
@@ -200,9 +209,10 @@ function tryAcquireLock(baseDir) {
200
209
  * Libera el lock actualizando el mtime a "ahora" (consolidación exitosa).
201
210
  *
202
211
  * @param {string} baseDir
212
+ * @param {object} [opts] - Ver {@link lockPath}.
203
213
  */
204
- function releaseLock(baseDir) {
205
- const lp = lockPath(baseDir);
214
+ function releaseLock(baseDir, opts = {}) {
215
+ const lp = lockPath(baseDir, opts);
206
216
  try {
207
217
  // Actualizar mtime a ahora = "última consolidación fue ahora"
208
218
  const now = Date.now() / 1000;
@@ -216,9 +226,10 @@ function releaseLock(baseDir) {
216
226
  *
217
227
  * @param {string} baseDir
218
228
  * @param {number} priorMtime - Epoch ms del mtime anterior.
229
+ * @param {object} [opts] - Ver {@link lockPath}.
219
230
  */
220
- function rollbackLock(baseDir, priorMtime) {
221
- const lp = lockPath(baseDir);
231
+ function rollbackLock(baseDir, priorMtime, opts = {}) {
232
+ const lp = lockPath(baseDir, opts);
222
233
  try {
223
234
  if (priorMtime === 0) {
224
235
  // No había lock antes → eliminar
@@ -40,6 +40,7 @@
40
40
  */
41
41
 
42
42
  const fs = require('fs');
43
+ const { raizProyecto } = require('./raiz-proyecto');
43
44
  const path = require('path');
44
45
 
45
46
  // Escritura atómica obligatoria para estado mutable (regla CLAUDE.md).
@@ -74,9 +75,13 @@ try {
74
75
 
75
76
  let driftDetector;
76
77
  try {
77
- driftDetector = require('../scripts/lib/drift-detector');
78
+ driftDetector = require('../scripts/lib/drift-detector'); // repo madre
78
79
  } catch {
79
- driftDetector = null;
80
+ try {
81
+ driftDetector = require('./drift-detector'); // destino: scripts/lib → hooks/lib (siblings)
82
+ } catch {
83
+ driftDetector = null;
84
+ }
80
85
  }
81
86
 
82
87
  let agentRouting;
@@ -105,8 +110,9 @@ const VENTANA_DIAS = 14;
105
110
  const MIN_TOOL_CALLS = 5; // por debajo se considera trivial
106
111
  const MIN_INVOCACIONES_LOOP = 6; // mínimo para que el ratio de convergencia sea significativo
107
112
 
108
- const DIR_AUTOEVOL = path.join(process.cwd(), '.planning', 'auto-evolution');
109
- const DIR_TRACES = path.join(process.cwd(), '.planning', 'traces');
113
+ const RAIZ = raizProyecto(process.cwd()); // una sola resolución por proceso
114
+ const DIR_AUTOEVOL = path.join(RAIZ, '.planning', 'auto-evolution');
115
+ const DIR_TRACES = path.join(RAIZ, '.planning', 'traces');
110
116
  const LOG_PATH = path.join(DIR_AUTOEVOL, 'agentes.jsonl');
111
117
  const NUDGES_PATH = path.join(DIR_AUTOEVOL, 'nudges.json');
112
118
 
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  const fs = require('fs');
23
+ const { raizProyecto } = require('./raiz-proyecto');
23
24
  const path = require('path');
24
25
 
25
26
  let atomicWriteJSON;
@@ -36,7 +37,7 @@ try {
36
37
  nudgeTracker = null;
37
38
  }
38
39
 
39
- const CWD = process.cwd();
40
+ const CWD = raizProyecto(process.cwd());
40
41
  const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
41
42
  const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
42
43
  const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
@@ -28,6 +28,8 @@
28
28
  */
29
29
 
30
30
  const fs = require('fs');
31
+ const { raizProyecto } = require('./raiz-proyecto');
32
+ const { conLockRetry } = require('./singleton-guard');
31
33
  const path = require('path');
32
34
 
33
35
  let atomicWriteJSON;
@@ -60,11 +62,14 @@ try {
60
62
  // ---------------------------------------------------------------------------
61
63
 
62
64
  const UMBRAL_NUDGE = parseInt(process.env.SWL_PERFIL_UMBRAL || '3', 10);
63
- const DIR_PERFIL = path.join(process.cwd(), '.planning', 'user-profile');
65
+ // Una sola resolución de la raíz por carga del módulo — las 4 rutas comparten
66
+ // ancla; resolverla 4 veces repetía el walk ascendente con su I/O (nemesis S2-3).
67
+ const RAIZ = raizProyecto(process.cwd());
68
+ const DIR_PERFIL = path.join(RAIZ, '.planning', 'user-profile');
64
69
  const DIRTY_PATH = path.join(DIR_PERFIL, 'dirty.json');
65
- const PERFIL_PATH = path.join(process.cwd(), 'instintos', 'perfil-usuario.yaml');
66
- const APRENDIZAJES_PATH = path.join(process.cwd(), '.planning', 'APRENDIZAJES.md');
67
- const INSTINTOS_PATH = path.join(process.cwd(), 'instintos', 'proyecto.yaml');
70
+ const PERFIL_PATH = path.join(RAIZ, 'instintos', 'perfil-usuario.yaml');
71
+ const APRENDIZAJES_PATH = path.join(RAIZ, '.planning', 'APRENDIZAJES.md');
72
+ const INSTINTOS_PATH = path.join(RAIZ, 'instintos', 'proyecto.yaml');
68
73
 
69
74
  // Keywords de corrección (señal fuerte: el usuario está corrigiendo)
70
75
  const KEYWORDS_CORRECCION = [
@@ -237,6 +242,17 @@ function extraerPromptsUsuario(inputRaw) {
237
242
  * @param {string} inputRaw transcripción de la sesión (payload del hook Stop)
238
243
  */
239
244
  function ejecutar(inputRaw) {
245
+ // El cuerpo hace read-modify-write sobre dirty.json (leerDirty → mutar →
246
+ // escribirDirty). Dos hooks Stop casi-simultáneos podían pisar el contador
247
+ // (nemesis S2-2). Serializar con lock; bajo contención se OMITE — perder un
248
+ // incremento de señal es aceptable (la próxima sesión re-dispara).
249
+ conLockRetry('etapa-perfil-usuario', () => _ejecutarInterno(inputRaw), {
250
+ maxWaitMs: 500,
251
+ siNoAdquiere: 'omitir',
252
+ });
253
+ }
254
+
255
+ function _ejecutarInterno(inputRaw) {
240
256
  try {
241
257
  const textoUsuario = extraerPromptsUsuario(inputRaw);
242
258
 
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  const fs = require('fs');
20
+ const { raizProyecto } = require('./raiz-proyecto');
20
21
  const path = require('path');
21
22
 
22
23
  // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
@@ -248,7 +249,7 @@ function _readSidecar(filePath) {
248
249
  * @returns {boolean}
249
250
  */
250
251
  function isPackageRoot(cwd) {
251
- const dir = cwd || process.cwd();
252
+ const dir = cwd || raizProyecto(process.cwd());
252
253
  try {
253
254
  const pkgPath = path.join(dir, 'package.json');
254
255
  if (!fs.existsSync(pkgPath)) return false;
@@ -1,179 +1,193 @@
1
- 'use strict';
2
-
3
- /**
4
- * Gateway Notify — Helper para encolar notificaciones al gateway multi-plataforma.
5
- *
6
- * Escribe un archivo JSON en .planning/comms/ que el GatewayRunner consume
7
- * mediante polling (ver gateway/index.js _pollComms). Zero-deps, escritura
8
- * atómica, opt-in por configuración en manifiestos/gateway-config.json.
9
- *
10
- * Uso:
11
- * const { notificarGateway } = require('./lib/gateway-notify');
12
- * notificarGateway({ tipo: 'session-stop', titulo: 'Sesión terminada', texto: '...' });
13
- *
14
- * No bloquea ni lanza: si el gateway está deshabilitado o no hay comms dir,
15
- * retorna silenciosamente.
16
- *
17
- * @module hooks/lib/gateway-notify
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const { atomicWriteJSON } = require('./atomic-write');
23
-
24
- // Deep links opt-in (ADR-0029). Si el módulo no carga (instalación incompleta),
25
- // el enriquecimiento se omite silenciosamente comportamiento default sin
26
- // cambios respecto a versiones previas.
27
- let construirDeepLink, formatearEnlace;
28
- try {
29
- ({ construirDeepLink, formatearEnlace } = require('./deep-links'));
30
- } catch {
31
- construirDeepLink = () => null;
32
- formatearEnlace = (_, etiqueta) => etiqueta;
33
- }
34
-
35
- const COMMS_DIR = '.planning/comms';
36
- const CONFIG_PATH = 'manifiestos/gateway-config.json';
37
-
38
- /** Receptores reconocidos para formato de deep link. */
39
- const RECEPTORES = new Set(['telegram', 'discord', 'slack', 'email', 'plain']);
40
-
41
- /**
42
- * Verifica si el gateway está habilitado en configuración.
43
- * @returns {boolean}
44
- */
45
- function gatewayHabilitado() {
46
- try {
47
- const cfgPath = path.join(process.cwd(), CONFIG_PATH);
48
- if (!fs.existsSync(cfgPath)) return false;
49
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
50
- return cfg && cfg.enabled === true;
51
- } catch (_) {
52
- return false;
53
- }
54
- }
55
-
56
- /**
57
- * Verifica si un tipo de notificación está habilitado.
58
- * Lee el mapa notifications de gateway-config.json.
59
- * @param {string} tipo - session-stop, checkpoint, error, release, build-fail
60
- * @returns {boolean}
61
- */
62
- function tipoHabilitado(tipo) {
63
- try {
64
- const cfgPath = path.join(process.cwd(), CONFIG_PATH);
65
- if (!fs.existsSync(cfgPath)) return true;
66
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
67
- const map = cfg.notifications || {};
68
- const claves = {
69
- 'session-stop': 'onSessionComplete',
70
- 'checkpoint': 'onCheckpoint',
71
- 'error': 'onError',
72
- 'release': 'onRelease',
73
- 'build-fail': 'onBuildFail',
74
- };
75
- const clave = claves[tipo];
76
- if (!clave) return true;
77
- return map[clave] !== false;
78
- } catch (_) {
79
- return true;
80
- }
81
- }
82
-
83
- /**
84
- * Si el payload incluye `fileRef` + `idePreferido`, retorna el enlace
85
- * formateado al receptor; en otro caso retorna null. No modifica el payload.
86
- *
87
- * Estructura esperada del payload (opt-in, ADR-0029):
88
- * payload.fileRef = {
89
- * archivo: '/abs/path/al/archivo.js',
90
- * linea?: 42,
91
- * columna?: 8,
92
- * etiqueta?: 'Abrir archivo.js:42', // default: archivo:linea
93
- * proyecto?: 'mi-proyecto', // requerido solo para JetBrains
94
- * }
95
- * payload.idePreferido = 'vscode' | 'cursor' | ...
96
- *
97
- * @param {object} payload
98
- * @param {string} receptor - 'telegram' | 'discord' | 'slack' | 'email' | 'plain'
99
- * @returns {string|null}
100
- * @private
101
- */
102
- function _construirEnlaceFileRef(payload, receptor) {
103
- if (!payload || !payload.fileRef || !payload.idePreferido) return null;
104
- const { archivo, linea, columna, etiqueta, proyecto } = payload.fileRef;
105
- if (!archivo) return null;
106
-
107
- const url = construirDeepLink({
108
- ide: payload.idePreferido,
109
- rutaAbsoluta: archivo,
110
- linea,
111
- columna,
112
- proyecto,
113
- });
114
-
115
- const label = etiqueta || (linea ? `${path.basename(archivo)}:${linea}` : path.basename(archivo));
116
- const formato = RECEPTORES.has(receptor) ? receptor : 'plain';
117
- return formatearEnlace(url, label, formato);
118
- }
119
-
120
- /**
121
- * Encola una notificación para el gateway.
122
- * No bloquea ni lanza. Retorna true si se encoló, false si fue descartada.
123
- *
124
- * Enriquecimiento opt-in (ADR-0029): si `params.payload.fileRef` +
125
- * `params.payload.idePreferido` están presentes, el mensaje agrega el campo
126
- * `enlace` con un deep link formateado al receptor. Sin esos campos, el
127
- * comportamiento es idéntico al previo (compatibilidad hacia atrás).
128
- *
129
- * @param {object} params
130
- * @param {string} params.tipo - session-stop, checkpoint, error, release, build-fail, custom
131
- * @param {string} [params.titulo] - Título corto.
132
- * @param {string} [params.texto] - Texto del mensaje (soporta Markdown).
133
- * @param {string} [params.to='all'] - Destino: 'all', 'telegram', 'discord', etc.
134
- * @param {object} [params.payload] - Payload adicional para el adaptador.
135
- * @returns {boolean}
136
- */
137
- function notificarGateway(params) {
138
- try {
139
- if (!gatewayHabilitado()) return false;
140
- if (!tipoHabilitado(params.tipo)) return false;
141
-
142
- const commsDir = path.join(process.cwd(), COMMS_DIR);
143
- if (!fs.existsSync(commsDir)) {
144
- fs.mkdirSync(commsDir, { recursive: true });
145
- }
146
-
147
- const id = `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
148
- const receptor = params.to || 'all';
149
- const payloadBase = {
150
- tipo: params.tipo,
151
- titulo: params.titulo || '',
152
- texto: params.texto || '',
153
- ...(params.payload || {}),
154
- };
155
-
156
- // Enriquecimiento opt-in con deep link (ADR-0029).
157
- const enlace = _construirEnlaceFileRef(payloadBase, receptor);
158
- if (enlace) payloadBase.enlace = enlace;
159
-
160
- const msg = {
161
- id,
162
- type: 'gateway_notification',
163
- from: 'swl-system',
164
- to: receptor,
165
- payload: payloadBase,
166
- text: params.texto || '',
167
- timestamp: new Date().toISOString(),
168
- status: 'pending',
169
- };
170
-
171
- const filePath = path.join(commsDir, `${id}.json`);
172
- atomicWriteJSON(filePath, msg);
173
- return true;
174
- } catch (_) {
175
- return false;
176
- }
177
- }
178
-
179
- module.exports = { notificarGateway, gatewayHabilitado, tipoHabilitado };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Gateway Notify — Helper para encolar notificaciones al gateway multi-plataforma.
5
+ *
6
+ * Escribe un archivo JSON en .planning/comms/ que el GatewayRunner consume
7
+ * mediante polling (ver gateway/index.js _pollComms). Zero-deps, escritura
8
+ * atómica, opt-in por configuración en manifiestos/gateway-config.json.
9
+ *
10
+ * Uso:
11
+ * const { notificarGateway } = require('./lib/gateway-notify');
12
+ * notificarGateway({ tipo: 'session-stop', titulo: 'Sesión terminada', texto: '...' });
13
+ *
14
+ * No bloquea ni lanza: si el gateway está deshabilitado o no hay comms dir,
15
+ * retorna silenciosamente.
16
+ *
17
+ * @module hooks/lib/gateway-notify
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const { raizProyecto } = require('./raiz-proyecto');
22
+ const path = require('path');
23
+ const { atomicWriteJSON } = require('./atomic-write');
24
+
25
+ // Deep links opt-in (ADR-0029). Si el módulo no carga (instalación incompleta),
26
+ // el enriquecimiento se omite silenciosamente — comportamiento default sin
27
+ // cambios respecto a versiones previas.
28
+ let construirDeepLink, formatearEnlace;
29
+ try {
30
+ ({ construirDeepLink, formatearEnlace } = require('./deep-links'));
31
+ } catch {
32
+ construirDeepLink = () => null;
33
+ formatearEnlace = (_, etiqueta) => etiqueta;
34
+ }
35
+
36
+ const COMMS_DIR = '.planning/comms';
37
+ const CONFIG_PATH = 'manifiestos/gateway-config.json';
38
+
39
+ // Memo de la raíz del proyecto POR cwd: una notificación consulta config +
40
+ // comms en la misma invocación — sin memo el walk ascendente de raizProyecto
41
+ // se repetiría 3 veces con su I/O de existsSync. La clave por cwd evita
42
+ // congelar la raíz cuando el proceso cambia de directorio (tests, daemons).
43
+ let _raizCache = null;
44
+ function raizBase() {
45
+ const cwd = process.cwd();
46
+ if (!_raizCache || _raizCache.cwd !== cwd) {
47
+ _raizCache = { cwd, raiz: raizProyecto(cwd) };
48
+ }
49
+ return _raizCache.raiz;
50
+ }
51
+
52
+ /** Receptores reconocidos para formato de deep link. */
53
+ const RECEPTORES = new Set(['telegram', 'discord', 'slack', 'email', 'plain']);
54
+
55
+ /**
56
+ * Verifica si el gateway está habilitado en configuración.
57
+ * @returns {boolean}
58
+ */
59
+ function gatewayHabilitado() {
60
+ try {
61
+ const cfgPath = path.join(raizBase(), CONFIG_PATH);
62
+ if (!fs.existsSync(cfgPath)) return false;
63
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
64
+ return cfg && cfg.enabled === true;
65
+ } catch (_) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Verifica si un tipo de notificación está habilitado.
72
+ * Lee el mapa notifications de gateway-config.json.
73
+ * @param {string} tipo - session-stop, checkpoint, error, release, build-fail
74
+ * @returns {boolean}
75
+ */
76
+ function tipoHabilitado(tipo) {
77
+ try {
78
+ const cfgPath = path.join(raizBase(), CONFIG_PATH);
79
+ if (!fs.existsSync(cfgPath)) return true;
80
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
81
+ const map = cfg.notifications || {};
82
+ const claves = {
83
+ 'session-stop': 'onSessionComplete',
84
+ 'checkpoint': 'onCheckpoint',
85
+ 'error': 'onError',
86
+ 'release': 'onRelease',
87
+ 'build-fail': 'onBuildFail',
88
+ };
89
+ const clave = claves[tipo];
90
+ if (!clave) return true;
91
+ return map[clave] !== false;
92
+ } catch (_) {
93
+ return true;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Si el payload incluye `fileRef` + `idePreferido`, retorna el enlace
99
+ * formateado al receptor; en otro caso retorna null. No modifica el payload.
100
+ *
101
+ * Estructura esperada del payload (opt-in, ADR-0029):
102
+ * payload.fileRef = {
103
+ * archivo: '/abs/path/al/archivo.js',
104
+ * linea?: 42,
105
+ * columna?: 8,
106
+ * etiqueta?: 'Abrir archivo.js:42', // default: archivo:linea
107
+ * proyecto?: 'mi-proyecto', // requerido solo para JetBrains
108
+ * }
109
+ * payload.idePreferido = 'vscode' | 'cursor' | ...
110
+ *
111
+ * @param {object} payload
112
+ * @param {string} receptor - 'telegram' | 'discord' | 'slack' | 'email' | 'plain'
113
+ * @returns {string|null}
114
+ * @private
115
+ */
116
+ function _construirEnlaceFileRef(payload, receptor) {
117
+ if (!payload || !payload.fileRef || !payload.idePreferido) return null;
118
+ const { archivo, linea, columna, etiqueta, proyecto } = payload.fileRef;
119
+ if (!archivo) return null;
120
+
121
+ const url = construirDeepLink({
122
+ ide: payload.idePreferido,
123
+ rutaAbsoluta: archivo,
124
+ linea,
125
+ columna,
126
+ proyecto,
127
+ });
128
+
129
+ const label = etiqueta || (linea ? `${path.basename(archivo)}:${linea}` : path.basename(archivo));
130
+ const formato = RECEPTORES.has(receptor) ? receptor : 'plain';
131
+ return formatearEnlace(url, label, formato);
132
+ }
133
+
134
+ /**
135
+ * Encola una notificación para el gateway.
136
+ * No bloquea ni lanza. Retorna true si se encoló, false si fue descartada.
137
+ *
138
+ * Enriquecimiento opt-in (ADR-0029): si `params.payload.fileRef` +
139
+ * `params.payload.idePreferido` están presentes, el mensaje agrega el campo
140
+ * `enlace` con un deep link formateado al receptor. Sin esos campos, el
141
+ * comportamiento es idéntico al previo (compatibilidad hacia atrás).
142
+ *
143
+ * @param {object} params
144
+ * @param {string} params.tipo - session-stop, checkpoint, error, release, build-fail, custom
145
+ * @param {string} [params.titulo] - Título corto.
146
+ * @param {string} [params.texto] - Texto del mensaje (soporta Markdown).
147
+ * @param {string} [params.to='all'] - Destino: 'all', 'telegram', 'discord', etc.
148
+ * @param {object} [params.payload] - Payload adicional para el adaptador.
149
+ * @returns {boolean}
150
+ */
151
+ function notificarGateway(params) {
152
+ try {
153
+ if (!gatewayHabilitado()) return false;
154
+ if (!tipoHabilitado(params.tipo)) return false;
155
+
156
+ const commsDir = path.join(raizBase(), COMMS_DIR);
157
+ if (!fs.existsSync(commsDir)) {
158
+ fs.mkdirSync(commsDir, { recursive: true });
159
+ }
160
+
161
+ const id = `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
162
+ const receptor = params.to || 'all';
163
+ const payloadBase = {
164
+ tipo: params.tipo,
165
+ titulo: params.titulo || '',
166
+ texto: params.texto || '',
167
+ ...(params.payload || {}),
168
+ };
169
+
170
+ // Enriquecimiento opt-in con deep link (ADR-0029).
171
+ const enlace = _construirEnlaceFileRef(payloadBase, receptor);
172
+ if (enlace) payloadBase.enlace = enlace;
173
+
174
+ const msg = {
175
+ id,
176
+ type: 'gateway_notification',
177
+ from: 'swl-system',
178
+ to: receptor,
179
+ payload: payloadBase,
180
+ text: params.texto || '',
181
+ timestamp: new Date().toISOString(),
182
+ status: 'pending',
183
+ };
184
+
185
+ const filePath = path.join(commsDir, `${id}.json`);
186
+ atomicWriteJSON(filePath, msg);
187
+ return true;
188
+ } catch (_) {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ module.exports = { notificarGateway, gatewayHabilitado, tipoHabilitado };
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  const fs = require('fs');
32
+ const { raizProyecto } = require('./raiz-proyecto');
32
33
  const path = require('path');
33
34
  const { atomicWriteSync, atomicWriteJSON } = require('./atomic-write');
34
35
 
@@ -60,11 +61,11 @@ function timestampCompacto(fecha) {
60
61
  * @param {string} opciones.tipo - Identificador del loop (ej: 'autoresearch', 'nemesis', 'verificar').
61
62
  * @param {string} [opciones.direccion='higher_is_better'] - Dirección de la métrica.
62
63
  * @param {string[]} [opciones.columnas] - Columnas del TSV (default COLUMNAS_DEFAULT).
63
- * @param {string} [opciones.raiz=process.cwd()] - Raíz del proyecto.
64
+ * @param {string} [opciones.raiz=raizProyecto(process.cwd())] - Raíz del proyecto.
64
65
  * @param {object} [opciones.config={}] - Config inicial persistida en meta.json.
65
66
  * @returns {{dir: string, tsvPath: string}}
66
67
  */
67
- function iniciarCorrida({ tipo, direccion = 'higher_is_better', columnas, raiz = process.cwd(), config = {} }) {
68
+ function iniciarCorrida({ tipo, direccion = 'higher_is_better', columnas, raiz = raizProyecto(process.cwd()), config = {} }) {
68
69
  if (!tipo || !/^[a-z0-9-]+$/.test(tipo)) {
69
70
  throw new TypeError(`loop-telemetry: tipo inválido "${tipo}" — usar kebab-case (ej: "autoresearch")`);
70
71
  }
@@ -269,12 +270,12 @@ function leerHandoff(dir) {
269
270
  * Encuentra la corrida activa más reciente: directorio en .planning/loops/
270
271
  * cuyo iteraciones.tsv fue modificado hace menos de maxEdadMin minutos.
271
272
  *
272
- * @param {string} [raiz=process.cwd()]
273
+ * @param {string} [raiz=raizProyecto(process.cwd())]
273
274
  * @param {object} [opciones]
274
275
  * @param {number} [opciones.maxEdadMin=30]
275
276
  * @returns {{dir: string, tipo: string, ultimasFilas: object[], totalFilas: number}|null}
276
277
  */
277
- function corridaActiva(raiz = process.cwd(), { maxEdadMin = 30 } = {}) {
278
+ function corridaActiva(raiz = raizProyecto(process.cwd()), { maxEdadMin = 30 } = {}) {
278
279
  const base = path.join(raiz, DIR_LOOPS);
279
280
  let candidatos;
280
281
  try {
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  const fs = require('fs');
20
+ const { raizProyecto } = require('./raiz-proyecto');
20
21
  const path = require('path');
21
22
  const os = require('os');
22
23
 
@@ -151,7 +152,7 @@ function getHealthReport() {
151
152
  * @returns {string[]} Nombres de servidores MCP configurados.
152
153
  */
153
154
  function detectConfiguredMCPs(cwd) {
154
- const dir = cwd || process.cwd();
155
+ const dir = cwd || raizProyecto(process.cwd());
155
156
  const mcpPath = path.join(dir, '.mcp.json');
156
157
  try {
157
158
  const config = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
@@ -246,6 +246,10 @@ function _buscarEnSesionesFallback(baseDir, terminos) {
246
246
  * @returns {object|null}
247
247
  */
248
248
  function obtenerSesion(baseDir, id) {
249
+ // El id interpola en un path: solo caracteres de id legítimo (ses-...,
250
+ // apr-N) — sin separadores ni '..' que resuelvan fuera de sessions/
251
+ // (defensa en profundidad para el CLI `swl-ses memoria-search fetch`).
252
+ if (!/^[a-zA-Z0-9_-]+$/.test(String(id))) return null;
249
253
  const sessionPath = path.join(baseDir, '.planning', 'sessions', `${id}.json`);
250
254
  if (!fs.existsSync(sessionPath)) return null;
251
255