@saulwade/swl-ses 2.5.3 → 2.6.1

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 (214) hide show
  1. package/CLAUDE.md +9 -9
  2. package/README.md +37 -37
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +690 -690
  6. package/agentes/arquitecto-swl.md +267 -267
  7. package/agentes/auto-evolucion-swl.md +932 -908
  8. package/agentes/backend-csharp-swl.md +420 -420
  9. package/agentes/backend-go-swl.md +390 -390
  10. package/agentes/backend-java-swl.md +281 -281
  11. package/agentes/backend-rust-swl.md +364 -364
  12. package/agentes/backend-workers-swl.md +482 -482
  13. package/agentes/cloud-infra-swl.md +509 -509
  14. package/agentes/consolidador-swl.md +541 -541
  15. package/agentes/depurador-swl.md +352 -352
  16. package/agentes/devops-ci-swl.md +400 -400
  17. package/agentes/disenador-ui-swl.md +569 -569
  18. package/agentes/documentador-swl.md +345 -345
  19. package/agentes/frontend-angular-swl.md +621 -621
  20. package/agentes/frontend-css-swl.md +716 -716
  21. package/agentes/frontend-react-swl.md +692 -692
  22. package/agentes/frontend-swl.md +496 -496
  23. package/agentes/frontend-tailwind-swl.md +826 -826
  24. package/agentes/investigador-swl.md +432 -432
  25. package/agentes/investigador-ux-swl.md +505 -505
  26. package/agentes/migrador-swl.md +442 -442
  27. package/agentes/mobile-android-swl.md +511 -511
  28. package/agentes/mobile-cross-swl.md +541 -541
  29. package/agentes/mobile-ios-swl.md +502 -502
  30. package/agentes/mobile-testing-swl.md +302 -302
  31. package/agentes/nemesis-auditor-swl.md +285 -285
  32. package/agentes/observabilidad-swl.md +438 -438
  33. package/agentes/pagos-swl.md +310 -310
  34. package/agentes/perfilador-usuario-swl.md +321 -321
  35. package/agentes/planificador-swl.md +399 -399
  36. package/agentes/producto-prd-swl.md +589 -589
  37. package/agentes/red-team-swl.md +218 -218
  38. package/agentes/release-manager-swl.md +590 -590
  39. package/agentes/rendimiento-swl.md +713 -713
  40. package/agentes/revisor-angular-swl.md +278 -278
  41. package/agentes/revisor-csharp-swl.md +264 -264
  42. package/agentes/revisor-go-swl.md +259 -259
  43. package/agentes/revisor-java-swl.md +257 -257
  44. package/agentes/revisor-kotlin-swl.md +273 -273
  45. package/agentes/revisor-nextjs-swl.md +281 -281
  46. package/agentes/revisor-php-swl.md +271 -271
  47. package/agentes/revisor-react-swl.md +278 -278
  48. package/agentes/revisor-rust-swl.md +346 -346
  49. package/agentes/revisor-seguridad-swl.md +399 -399
  50. package/agentes/revisor-swift-swl.md +268 -268
  51. package/agentes/revisor-typescript-swl.md +346 -346
  52. package/agentes/tdd-qa-swl.md +393 -393
  53. package/bin/swl-ses.js +32 -7
  54. package/comandos/swl/actualizar.md +3 -3
  55. package/comandos/swl/aprender.md +13 -0
  56. package/comandos/swl/deuda-codigo.md +97 -97
  57. package/comandos/swl/evaluar-skill.md +18 -3
  58. package/comandos/swl/evolucion-continua.md +73 -0
  59. package/comandos/swl/evolucionar.md +13 -0
  60. package/comandos/swl/instalar.md +4 -4
  61. package/comandos/swl/notificaciones.md +1 -1
  62. package/comandos/swl/status.md +2 -2
  63. package/gateway/cron/jobs.example.json +12 -0
  64. package/habilidades/auto-evolucion-protocolo/SKILL.md +19 -1
  65. package/habilidades/autoresearch/SKILL.md +3 -2
  66. package/habilidades/backend-async-postgres-testing/SKILL.md +2 -1
  67. package/habilidades/benchmark-memoria/SKILL.md +7 -7
  68. package/habilidades/changelog-generator/SKILL.md +1 -1
  69. package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
  70. package/habilidades/checkpoints-verificacion/SKILL.md +6 -0
  71. package/habilidades/compactacion-contexto/SKILL.md +2 -1
  72. package/habilidades/contenedores-docker/SKILL.md +4 -2
  73. package/habilidades/context-builder/SKILL.md +4 -0
  74. package/habilidades/doubt-driven-review/SKILL.md +17 -1
  75. package/habilidades/drift-detection/SKILL.md +6 -1
  76. package/habilidades/ejecutar-fase/SKILL.md +6 -6
  77. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  78. package/habilidades/eval-framework/SKILL.md +8 -3
  79. package/habilidades/extractor-de-aprendizajes/SKILL.md +8 -2
  80. package/habilidades/git-worktrees-paralelo/SKILL.md +19 -1
  81. package/habilidades/harness-claude-code/SKILL.md +7 -3
  82. package/habilidades/infra-github-actions/SKILL.md +4 -3
  83. package/habilidades/instalar-sistema/SKILL.md +5 -1
  84. package/habilidades/memoria-busqueda/SKILL.md +31 -39
  85. package/habilidades/planear-fase/SKILL.md +9 -1
  86. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  87. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  88. package/habilidades/proceso-ddia-fundamentos/SKILL.md +3 -2
  89. package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -147
  90. package/habilidades/release-semver/SKILL.md +2 -2
  91. package/habilidades/swl-claudemd/SKILL.md +6 -7
  92. package/habilidades/swl-dashboard/SKILL.md +11 -43
  93. package/habilidades/tdd-workflow/SKILL.md +12 -7
  94. package/habilidades/validacion-ci-sistema/SKILL.md +1 -1
  95. package/hooks/agente-lifecycle.js +2 -1
  96. package/hooks/aiisms-detector.js +13 -4
  97. package/hooks/audit-trail.js +2 -1
  98. package/hooks/auto-consolidacion.js +2 -1
  99. package/hooks/captura-acciones-post.js +2 -1
  100. package/hooks/captura-acciones-session.js +2 -1
  101. package/hooks/captura-feedback-usuario.js +3 -2
  102. package/hooks/claudemd-bloat-detector.js +12 -3
  103. package/hooks/claudemd-duplicacion-detector.js +13 -3
  104. package/hooks/contexto-iteracion.js +2 -1
  105. package/hooks/contexto-subagente.js +68 -68
  106. package/hooks/degradacion-instintos.js +2 -1
  107. package/hooks/extraccion-aprendizajes.js +109 -15
  108. package/hooks/grafo-contexto.js +2 -1
  109. package/hooks/guardrail-modelo.js +2 -1
  110. package/hooks/inbox-aviso.js +2 -1
  111. package/hooks/inyeccion-contexto.js +2 -1
  112. package/hooks/lib/agent-matcher.js +2 -1
  113. package/hooks/lib/agent-routing.js +2 -1
  114. package/hooks/lib/autonomia.js +5 -3
  115. package/hooks/lib/captura-acciones.js +2 -1
  116. package/hooks/lib/consolidation-lock.js +21 -10
  117. package/hooks/lib/etapa-auto-evolucion.js +10 -4
  118. package/hooks/lib/etapa-metricas.js +2 -1
  119. package/hooks/lib/etapa-perfil-usuario.js +20 -4
  120. package/hooks/lib/evolution-tracker.js +2 -1
  121. package/hooks/lib/gateway-notify.js +17 -3
  122. package/hooks/lib/loop-telemetry.js +5 -4
  123. package/hooks/lib/mcp-health.js +2 -1
  124. package/hooks/lib/memory-search.js +4 -0
  125. package/hooks/lib/merkle-audit.js +58 -6
  126. package/hooks/lib/notificacion-formato.js +58 -0
  127. package/hooks/lib/nudge-tracker.js +2 -1
  128. package/hooks/lib/otlp-exporter.js +2 -1
  129. package/hooks/lib/propose-step.js +3 -2
  130. package/hooks/lib/raiz-proyecto.js +127 -0
  131. package/hooks/lib/run-log.js +2 -1
  132. package/hooks/lib/singleton-guard.js +225 -27
  133. package/hooks/lib/telegram-cliente.js +28 -11
  134. package/hooks/notificacion-telegram.js +13 -3
  135. package/hooks/preservar-estado-pre-compact.js +2 -1
  136. package/hooks/proteccion-rutas.js +59 -3
  137. package/hooks/registro-turnos.js +2 -1
  138. package/hooks/resumen-sesion.js +2 -1
  139. package/hooks/risk-scoring.js +2 -1
  140. package/hooks/rotar-audit-auto.js +46 -20
  141. package/hooks/session-briefing.js +127 -1
  142. package/hooks/spec-gate.js +2 -1
  143. package/hooks/sugerir-contribuir.js +6 -3
  144. package/hooks/sugerir-regenerar-inventario.js +3 -2
  145. package/hooks/tdd-gate.js +2 -1
  146. package/hooks/telemetria-agentes.js +2 -1
  147. package/hooks/telemetria-skill-routing.js +2 -1
  148. package/hooks/tracking-costos.js +4 -3
  149. package/hooks/validar-formato-post-subagente.js +2 -1
  150. package/hooks/validar-intent-spec.js +2 -1
  151. package/hooks/validar-memoria-hook.js +13 -3
  152. package/hooks/validar-planning-paths.js +2 -1
  153. package/instintos/perfil-usuario.yaml +506 -3
  154. package/instintos/proyecto.yaml +78 -0
  155. package/llms.txt +29 -29
  156. package/manifiestos/canonical-hashes.json +5588 -4925
  157. package/manifiestos/hooks-config.json +469 -469
  158. package/manifiestos/invariantes-criticos.json +30 -30
  159. package/manifiestos/modulos.json +1429 -1423
  160. package/manifiestos/planning-paths.json +1 -0
  161. package/manifiestos/skills-lock.json +1275 -1275
  162. package/package.json +94 -95
  163. package/plugin.json +369 -369
  164. package/scripts/actualizar.js +3 -0
  165. package/scripts/auditar-clases-conocidas.js +134 -106
  166. package/scripts/benchmark-memoria.js +1 -0
  167. package/scripts/bootstrap-instintos.js +85 -14
  168. package/scripts/canario-hooks.js +166 -166
  169. package/scripts/cli/autonomia.js +23 -0
  170. package/scripts/cli/benchmark-memoria.js +37 -0
  171. package/scripts/cli/ciclo-autonomo.js +73 -0
  172. package/scripts/cli/ciclo-fase-b.js +102 -0
  173. package/scripts/cli/guardrail-metrics.js +39 -0
  174. package/scripts/cli/loop-telemetry.js +4 -2
  175. package/scripts/cli/memoria-search.js +69 -0
  176. package/scripts/cli/nudge-accionar.js +39 -0
  177. package/scripts/cli/run-eval.js +38 -0
  178. package/scripts/cli/run-skill-evals.js +13 -2
  179. package/scripts/derivar-feature-list.js +15 -14
  180. package/scripts/desinstalar.js +11 -0
  181. package/scripts/doctor.js +50 -13
  182. package/scripts/evidencia-valor.js +101 -101
  183. package/scripts/field-report.js +16 -16
  184. package/scripts/instalador.js +98 -7
  185. package/scripts/lib/activar-hooks-proyecto.js +116 -104
  186. package/scripts/lib/auditar-invocaciones-comandos.js +96 -6
  187. package/scripts/lib/ciclo-autonomo/candidatos.js +174 -0
  188. package/scripts/lib/ciclo-autonomo/config.js +165 -0
  189. package/scripts/lib/ciclo-autonomo/drenador-feedback.js +174 -0
  190. package/scripts/lib/ciclo-autonomo/fallback.js +77 -0
  191. package/scripts/lib/ciclo-autonomo/guard-convivencia.js +139 -0
  192. package/scripts/lib/ciclo-autonomo/higiene-nudges.js +112 -0
  193. package/scripts/lib/ciclo-autonomo/index.js +301 -0
  194. package/scripts/lib/ciclo-autonomo/lock.js +124 -0
  195. package/scripts/lib/ciclo-autonomo/presupuesto.js +122 -0
  196. package/scripts/lib/ciclo-autonomo/puente-degradacion.js +240 -0
  197. package/scripts/lib/ciclo-autonomo/runner-fase-b.js +248 -0
  198. package/scripts/lib/ciclo-autonomo/writer-instintos.js +190 -0
  199. package/scripts/lib/ciclo-autonomo/yaml-instintos.js +535 -0
  200. package/scripts/lib/estado.js +9 -0
  201. package/scripts/lib/evidencia-valor.js +228 -228
  202. package/scripts/lib/expandir-targets.js +71 -71
  203. package/scripts/lib/gitignore-manifest.js +8 -1
  204. package/scripts/lib/hooks-settings.js +45 -0
  205. package/scripts/lib/limpiar-basura-global.js +161 -0
  206. package/scripts/lib/toml-merge.js +204 -204
  207. package/scripts/mcp-server/auth.js +105 -105
  208. package/scripts/mcp-server/cache.js +106 -106
  209. package/scripts/rotar-audit-logs.js +48 -2
  210. package/scripts/run-eval.js +1 -0
  211. package/scripts/run-skill-evals.js +287 -8
  212. package/scripts/smoke-test.js +16 -8
  213. package/scripts/tui/pantallas/install-wizard.js +69 -13
  214. package/scripts/validar.js +40 -1
@@ -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 */
@@ -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;
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  const fs = require('fs');
21
+ const { raizProyecto } = require('./raiz-proyecto');
21
22
  const path = require('path');
22
23
  const { atomicWriteJSON } = require('./atomic-write');
23
24
 
@@ -35,6 +36,19 @@ try {
35
36
  const COMMS_DIR = '.planning/comms';
36
37
  const CONFIG_PATH = 'manifiestos/gateway-config.json';
37
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
+
38
52
  /** Receptores reconocidos para formato de deep link. */
39
53
  const RECEPTORES = new Set(['telegram', 'discord', 'slack', 'email', 'plain']);
40
54
 
@@ -44,7 +58,7 @@ const RECEPTORES = new Set(['telegram', 'discord', 'slack', 'email', 'plain']);
44
58
  */
45
59
  function gatewayHabilitado() {
46
60
  try {
47
- const cfgPath = path.join(process.cwd(), CONFIG_PATH);
61
+ const cfgPath = path.join(raizBase(), CONFIG_PATH);
48
62
  if (!fs.existsSync(cfgPath)) return false;
49
63
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
50
64
  return cfg && cfg.enabled === true;
@@ -61,7 +75,7 @@ function gatewayHabilitado() {
61
75
  */
62
76
  function tipoHabilitado(tipo) {
63
77
  try {
64
- const cfgPath = path.join(process.cwd(), CONFIG_PATH);
78
+ const cfgPath = path.join(raizBase(), CONFIG_PATH);
65
79
  if (!fs.existsSync(cfgPath)) return true;
66
80
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
67
81
  const map = cfg.notifications || {};
@@ -139,7 +153,7 @@ function notificarGateway(params) {
139
153
  if (!gatewayHabilitado()) return false;
140
154
  if (!tipoHabilitado(params.tipo)) return false;
141
155
 
142
- const commsDir = path.join(process.cwd(), COMMS_DIR);
156
+ const commsDir = path.join(raizBase(), COMMS_DIR);
143
157
  if (!fs.existsSync(commsDir)) {
144
158
  fs.mkdirSync(commsDir, { recursive: true });
145
159
  }
@@ -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 {