@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
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * guard-convivencia.js — Guard de convivencia con una sesión interactiva de
5
+ * Claude Code en la misma máquina (Fase 22, T-03, REQ-22-02).
6
+ *
7
+ * El runner headless de Fase B (`runner-fase-b.js`, T-05) NUNCA debe competir
8
+ * por recursos/rate-limit con un usuario trabajando en una sesión interactiva.
9
+ * Este módulo detecta si hay un proceso con imagen `claude` corriendo:
10
+ * `tasklist /FO CSV /NH` en win32, `ps -eo comm=` en POSIX.
11
+ *
12
+ * El `detector` es INYECTABLE — una función sin argumentos que devuelve la
13
+ * salida cruda del listado de procesos (string) o lanza. Los tests SIEMPRE
14
+ * inyectan el detector; nunca se spawnea un proceso real en la suite. El
15
+ * detector por defecto usa `child_process.spawnSync` con un timeout corto
16
+ * (2s) para que un binario colgado nunca bloquee el runner.
17
+ *
18
+ * Semántica CONSERVADORA (dictamen adversarial, supuesto 2 del CONTEXTO):
19
+ * proceso `claude` presente → diferir; ausente → no diferir; CUALQUIER fallo
20
+ * de detección (spawn falla, binario ausente, timeout, salida ilegible) →
21
+ * diferir también. Un falso positivo (diferir de más) es aceptable; competir
22
+ * con un usuario interactivo no lo es. El guard NUNCA lanza hacia el caller.
23
+ *
24
+ * LIMITACIÓN DOCUMENTADA (fase dry-run, F22 no la resuelve): el detector
25
+ * matchea por NOMBRE de imagen de proceso, no por árbol de PIDs. Si el propio
26
+ * runner (headless, `node`) fuese lanzado como hijo de un proceso `claude`
27
+ * padre (p. ej. invocado manualmente desde dentro de una sesión interactiva),
28
+ * este guard lo tomaría como sesión interactiva activa y diferiría — no hay
29
+ * forma de distinguir "mi propio ancestro" de "otra sesión del usuario" sin
30
+ * caminar el árbol de procesos por PID/PPID, algo que el CONTEXTO de esta fase
31
+ * no exige y el plan no especifica. El proceso `node` del propio runner NUNCA
32
+ * cuenta como match (solo se busca la imagen `claude`), pero un ancestro
33
+ * `claude` sí produce el mismo diferir que una sesión ajena. F24 (ciclo con
34
+ * mutaciones reales) es quien debe afinar esta distinción si el patrón de
35
+ * disparo cambia (p. ej. invocación manual desde dentro de una sesión).
36
+ *
37
+ * Zero-deps, CommonJS.
38
+ *
39
+ * @module scripts/lib/ciclo-autonomo/guard-convivencia
40
+ */
41
+
42
+ const { spawnSync } = require('child_process');
43
+
44
+ /** Timeout del spawn de detección — nunca debe colgar el runner headless. */
45
+ const TIMEOUT_MS = 2000;
46
+
47
+ /** Nombre de imagen de proceso que identifica una sesión interactiva. */
48
+ const NOMBRE_PROCESO = 'claude';
49
+
50
+ /**
51
+ * Resuelve el comando de listado de procesos según la plataforma. Puro y
52
+ * exportado para poder verificar la selección win32/POSIX en tests sin
53
+ * spawnear nada real.
54
+ *
55
+ * @param {string} plataforma - Típicamente `process.platform`.
56
+ * @returns {{cmd: string, args: string[]}}
57
+ */
58
+ function resolverComando(plataforma) {
59
+ if (plataforma === 'win32') {
60
+ return { cmd: 'tasklist', args: ['/FO', 'CSV', '/NH'] };
61
+ }
62
+ return { cmd: 'ps', args: ['-eo', 'comm='] };
63
+ }
64
+
65
+ /**
66
+ * Detector por defecto: spawnSync del listado de procesos de la plataforma.
67
+ * Lanza si el spawn falla, sale con código distinto de 0, o no produce una
68
+ * salida de texto utilizable — el caller (`evaluar`) trata cualquier lanzamiento
69
+ * como fallo de detección conservador.
70
+ *
71
+ * @param {string} plataforma
72
+ * @returns {string} Salida cruda del comando.
73
+ */
74
+ function detectorPorDefecto(plataforma) {
75
+ const { cmd, args } = resolverComando(plataforma);
76
+ const resultado = spawnSync(cmd, args, { timeout: TIMEOUT_MS, encoding: 'utf8' });
77
+
78
+ if (!resultado || resultado.error || resultado.status !== 0 || typeof resultado.stdout !== 'string') {
79
+ throw new Error(`guard-convivencia: "${cmd}" no produjo salida utilizable`);
80
+ }
81
+
82
+ return resultado.stdout;
83
+ }
84
+
85
+ /**
86
+ * `true` si la salida cruda del listado de procesos contiene una imagen
87
+ * `claude` (case-insensitive; matchea tanto `claude` como `claude.exe` en
88
+ * una línea CSV de tasklist o en la salida de `ps -eo comm=`).
89
+ *
90
+ * @param {string} salidaCruda
91
+ * @returns {boolean}
92
+ */
93
+ function contieneProcesoClaude(salidaCruda) {
94
+ return salidaCruda
95
+ .split(/\r?\n/)
96
+ .some((linea) => linea.toLowerCase().includes(NOMBRE_PROCESO));
97
+ }
98
+
99
+ /**
100
+ * `true` si hay una sesión interactiva de Claude Code corriendo, según el
101
+ * detector inyectado (o el detector por defecto para la plataforma dada).
102
+ * Lanza si la detección falla — `evaluar` es quien decide el fallback
103
+ * conservador; esta función expone el booleano crudo.
104
+ *
105
+ * @param {{detector?: () => string, plataforma?: string}} [opciones]
106
+ * @returns {boolean}
107
+ */
108
+ function haySesionInteractiva({ detector, plataforma = process.platform } = {}) {
109
+ const salidaCruda = detector ? detector() : detectorPorDefecto(plataforma);
110
+ if (typeof salidaCruda !== 'string') {
111
+ throw new Error('guard-convivencia: el detector no devolvió una salida de texto');
112
+ }
113
+ return contieneProcesoClaude(salidaCruda);
114
+ }
115
+
116
+ /**
117
+ * Evalúa si el runner de Fase B debe diferirse por convivencia con una
118
+ * sesión interactiva. NUNCA lanza — cualquier error de detección se
119
+ * convierte en `{diferir:true, razon:'deteccion-fallida'}` (conservador).
120
+ *
121
+ * @param {{detector?: () => string, plataforma?: string}} [opciones]
122
+ * @returns {{diferir: boolean, razon?: string}}
123
+ */
124
+ function evaluar({ detector, plataforma } = {}) {
125
+ let activa;
126
+ try {
127
+ activa = haySesionInteractiva({ detector, plataforma });
128
+ } catch (_) {
129
+ return { diferir: true, razon: 'deteccion-fallida' };
130
+ }
131
+
132
+ if (activa) {
133
+ return { diferir: true, razon: 'sesion-interactiva' };
134
+ }
135
+
136
+ return { diferir: false };
137
+ }
138
+
139
+ module.exports = { evaluar, haySesionInteractiva, resolverComando };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Higiene del ciclo de nudges (T-09, Fase 21 — ADR-0041).
5
+ *
6
+ * Opera sobre `.planning/evolution/nudges.jsonl` con formato compatible con
7
+ * `hooks/lib/nudge-tracker.js` ({id, kind, target, ts, accionado,
8
+ * accionado_ts, accionado_por}), pero con `baseDir` inyectable — el tracker
9
+ * fija sus rutas al cwd del require y no sirve para el motor ni para tests
10
+ * en sandbox.
11
+ *
12
+ * - marcarAccionado(baseDir, {id | kinds, por, ahora}) → {marcados}
13
+ * - archivarViejos(baseDir, {ahora, dias=90}) → {archivados}
14
+ *
15
+ * Zero-deps. Reescritura completa vía atomic-write; líneas con schema viejo
16
+ * o corruptas se conservan intactas (nunca se pierden por higiene).
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { atomicWriteSync } = require('../../../hooks/lib/atomic-write');
22
+
23
+ const DIAS_VENTANA_DEFAULT = 90;
24
+
25
+ function rutaNudges(baseDir) {
26
+ return path.join(baseDir, '.planning', 'evolution', 'nudges.jsonl');
27
+ }
28
+
29
+ function rutaArchive(baseDir) {
30
+ return path.join(baseDir, '.planning', 'evolution', 'nudges.archive.jsonl');
31
+ }
32
+
33
+ // Lee el JSONL preservando las líneas crudas: las que no parsean se devuelven
34
+ // con `nudge: null` para reescribirlas tal cual (tolerancia sin pérdida).
35
+ function leerLineas(baseDir) {
36
+ const ruta = rutaNudges(baseDir);
37
+ if (!fs.existsSync(ruta)) return null;
38
+ return fs
39
+ .readFileSync(ruta, 'utf8')
40
+ .split('\n')
41
+ .filter((l) => l.trim() !== '')
42
+ .map((cruda) => {
43
+ try {
44
+ return { cruda, nudge: JSON.parse(cruda) };
45
+ } catch {
46
+ return { cruda, nudge: null };
47
+ }
48
+ });
49
+ }
50
+
51
+ function escribirLineas(baseDir, lineas) {
52
+ const contenido = lineas.map((l) => l.cruda).join('\n') + (lineas.length ? '\n' : '');
53
+ atomicWriteSync(rutaNudges(baseDir), contenido, 'utf8');
54
+ }
55
+
56
+ /**
57
+ * Marca nudges como accionados, por `id` exacto o por lista de `kinds`
58
+ * (solo los aún no accionados). Nunca pisa un `accionado_por` previo.
59
+ */
60
+ function marcarAccionado(baseDir, { id, kinds, por = 'desconocido', ahora = new Date() } = {}) {
61
+ const lineas = leerLineas(baseDir);
62
+ if (!lineas) return { marcados: 0 };
63
+
64
+ let marcados = 0;
65
+ for (const linea of lineas) {
66
+ const n = linea.nudge;
67
+ if (!n || n.accionado === true) continue;
68
+ const porId = id != null && n.id === id;
69
+ const porKind = Array.isArray(kinds) && kinds.includes(n.kind);
70
+ if (!porId && !porKind) continue;
71
+
72
+ n.accionado = true;
73
+ n.accionado_ts = ahora.toISOString();
74
+ n.accionado_por = por;
75
+ linea.cruda = JSON.stringify(n);
76
+ marcados++;
77
+ }
78
+
79
+ if (marcados > 0) escribirLineas(baseDir, lineas);
80
+ return { marcados };
81
+ }
82
+
83
+ /**
84
+ * Mueve al archive los nudges SIN accionar con `ts` más viejo que la ventana.
85
+ * Los nudges sin `ts` parseable y las líneas corruptas se conservan activos
86
+ * (mejor conservar de más que archivar señal viva).
87
+ */
88
+ function archivarViejos(baseDir, { ahora = new Date(), dias = DIAS_VENTANA_DEFAULT } = {}) {
89
+ const lineas = leerLineas(baseDir);
90
+ if (!lineas) return { archivados: 0 };
91
+
92
+ const limiteMs = ahora.getTime() - dias * 24 * 3600 * 1000;
93
+ const activos = [];
94
+ const archivados = [];
95
+
96
+ for (const linea of lineas) {
97
+ const n = linea.nudge;
98
+ const tsMs = n && n.ts ? Date.parse(n.ts) : NaN;
99
+ const esViejoSinAccionar = n && n.accionado !== true && Number.isFinite(tsMs) && tsMs < limiteMs;
100
+ (esViejoSinAccionar ? archivados : activos).push(linea);
101
+ }
102
+
103
+ if (archivados.length > 0) {
104
+ const bloque = archivados.map((l) => l.cruda).join('\n') + '\n';
105
+ fs.appendFileSync(rutaArchive(baseDir), bloque, 'utf8');
106
+ escribirLineas(baseDir, activos);
107
+ }
108
+
109
+ return { archivados: archivados.length };
110
+ }
111
+
112
+ module.exports = { marcarAccionado, archivarViejos, DIAS_VENTANA_DEFAULT };
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * index.js — Orquestador Fase A del ciclo autónomo (Fase 21, T-07,
5
+ * ADR-0041/0042, REQ-21-02/REQ-21-09). Compone las tres sub-fases
6
+ * deterministas bajo opt-out estricto (nace encendido; el off es sagrado), con lock de exclusión mutua propio
7
+ * (T-02) y telemetría no-op-aware.
8
+ *
9
+ * Orden de composición: writer-instintos → puente-degradacion →
10
+ * drenador-feedback. El writer corre primero porque sus transiciones
11
+ * (promoción/degradación por decay o harmful_ratio) son la fuente primaria
12
+ * de scoring de `instintos/proyecto.yaml`. El puente corre ANTES que el
13
+ * drenador para que las degradaciones del día detectadas en APRENDIZAJES.md
14
+ * (que bajan confidence de instintos derivados) ya queden reflejadas en el
15
+ * YAML antes de que el drenador consolide señales nuevas de
16
+ * `perfil-usuario.yaml` en esta misma corrida.
17
+ *
18
+ * Cada sub-fase corre en su propio try/catch: una falla no aborta las
19
+ * otras — se acumula en `errores[]` con el nombre de la sub-fase.
20
+ *
21
+ * Opt-OUT estricto (regla de negocio 1 de la Fase 21, reformulada 2026-07-10
22
+ * por decisión del usuario — el motor nace encendido): este orquestador es el
23
+ * ÚNICO punto que consulta `config.estaHabilitado()` — las libs de sub-fase
24
+ * son puras y no consultan config.
25
+ *
26
+ * Fase 22 (T-06, REQ-22-06): al final de una corrida CON acciones (nunca en
27
+ * un no-op — el detector no debe pagar su costo cuando no hubo nada que
28
+ * hacer), detecta candidatos a Fase B (`candidatos.detectar`) y, si hay ≥1
29
+ * nuevo y `estaFaseBHabilitada`, spawnea el runner headless como proceso
30
+ * detached fire-and-forget (`stdio:'ignore'` + `unref`, NUNCA `execSync` —
31
+ * dictamen PM3, `scheduler.js:126` bloquearía otros jobs). El spawner es
32
+ * inyectable (`opts.spawner`) para tests deterministas sin procesos reales;
33
+ * el default resuelve el CLI de Fase B con `require.resolve` tolerante
34
+ * (ausente en el destino ⇒ no spawnea, nunca crashea). El spawn NUNCA
35
+ * bloquea ni espera, y cualquier error se traga con razón registrada — el
36
+ * hook/CLI padre no debe morir por esto. La detección SIEMPRE corre en la
37
+ * rama con acciones (aunque Fase B esté apagada) porque el conteo de
38
+ * candidatos va a la telemetría igual.
39
+ *
40
+ * Zero-deps, CommonJS.
41
+ *
42
+ * @module scripts/lib/ciclo-autonomo/index
43
+ */
44
+
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+ const { spawn } = require('child_process');
48
+
49
+ const config = require('./config');
50
+ const lock = require('./lock');
51
+ const writerInstintos = require('./writer-instintos');
52
+ const drenadorFeedback = require('./drenador-feedback');
53
+ const puenteDegradacion = require('./puente-degradacion');
54
+ const higieneNudges = require('./higiene-nudges');
55
+ const candidatos = require('./candidatos');
56
+
57
+ /** Razones de no-ejecución expuestas para consumo de tests/CLI. */
58
+ const RAZONES = Object.freeze({
59
+ DESHABILITADO: 'deshabilitado',
60
+ LOCK_OCUPADO: 'lock ocupado',
61
+ });
62
+
63
+ const RUTA_TELEMETRIA_RELATIVA = path.join('evolution', 'ciclo-autonomo.jsonl');
64
+
65
+ function rutaTelemetria(baseDir) {
66
+ return path.join(baseDir, '.planning', RUTA_TELEMETRIA_RELATIVA);
67
+ }
68
+
69
+ function contadoresVacios() {
70
+ return {
71
+ promovidos: 0, degradados: 0, archivados: 0, candidatosGlobal: 0,
72
+ bajados: 0, drenados: 0, archivadosCola: 0, descartados: 0,
73
+ nudgesAtendidos: 0, nudgesArchivados: 0,
74
+ };
75
+ }
76
+
77
+ /** Ejecuta una sub-fase con try/catch propio; una falla se acumula en errores[] sin abortar. */
78
+ function correrSubFase(nombre, fn, errores) {
79
+ try {
80
+ return fn();
81
+ } catch (_err) {
82
+ errores.push(nombre);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /** Fecha-hora local legible ("YYYY-MM-DD HH:mm:ss") — NUNCA toISOString (gate C1). */
88
+ function fechaHoraLocal(momento) {
89
+ return `${momento.toLocaleDateString('sv')} ${momento.toLocaleTimeString('sv')}`;
90
+ }
91
+
92
+ /** Suma las tres sub-fases en los totales acumulados de la corrida. */
93
+ function acumularSubFases(baseDir, momento, errores) {
94
+ const totales = contadoresVacios();
95
+
96
+ const resWriter = correrSubFase(
97
+ 'writer-instintos',
98
+ () => writerInstintos.aplicarTransiciones(baseDir, { ahora: momento }),
99
+ errores
100
+ );
101
+ if (resWriter) {
102
+ totales.promovidos += resWriter.promovidos;
103
+ totales.degradados += resWriter.degradados;
104
+ totales.archivados += resWriter.archivados;
105
+ totales.candidatosGlobal += resWriter.candidatosGlobal;
106
+ }
107
+
108
+ const resPuente = correrSubFase(
109
+ 'puente-degradacion',
110
+ () => puenteDegradacion.puentear(baseDir, { ahora: momento }),
111
+ errores
112
+ );
113
+ if (resPuente) totales.bajados += resPuente.bajados;
114
+
115
+ const resDrenador = correrSubFase(
116
+ 'drenador-feedback',
117
+ () => drenadorFeedback.drenar(baseDir, { ahora: momento }),
118
+ errores
119
+ );
120
+ if (resDrenador) {
121
+ totales.drenados += resDrenador.drenados;
122
+ totales.archivadosCola += resDrenador.archivados;
123
+ totales.descartados += resDrenador.descartados;
124
+ }
125
+
126
+ // Higiene del ciclo de nudges (T-09/REQ-21-05 y REQ-21-13): si el drenador
127
+ // consolidó señales al perfil, los nudges `perfil-usuario` pendientes quedan
128
+ // atendidos por el ciclo; los nudges sin accionar más viejos que la ventana
129
+ // se archivan sin procesar (misma ventana de 90 días del bootstrap).
130
+ const resHigiene = correrSubFase('higiene-nudges', () => {
131
+ const atendidos = totales.drenados > 0
132
+ ? higieneNudges.marcarAccionado(baseDir, { kinds: ['perfil-usuario'], por: 'ciclo-autonomo', ahora: momento })
133
+ : { marcados: 0 };
134
+ const viejos = higieneNudges.archivarViejos(baseDir, { ahora: momento });
135
+ return { atendidos: atendidos.marcados, archivados: viejos.archivados };
136
+ }, errores);
137
+ if (resHigiene) {
138
+ totales.nudgesAtendidos += resHigiene.atendidos;
139
+ totales.nudgesArchivados += resHigiene.archivados;
140
+ }
141
+
142
+ return totales;
143
+ }
144
+
145
+ /** Total de acciones que decide si la corrida es no-op (candidatosGlobal y descartados NO cuentan). */
146
+ function totalAcciones(totales) {
147
+ return totales.promovidos + totales.degradados + totales.archivados
148
+ + totales.bajados + totales.drenados + totales.archivadosCola
149
+ + totales.nudgesAtendidos + totales.nudgesArchivados;
150
+ }
151
+
152
+ /**
153
+ * Resuelve la ruta del CLI de Fase B (`scripts/cli/ciclo-fase-b.js`, T-07)
154
+ * con `require.resolve` tolerante — ausente en este destino (p.ej. antes de
155
+ * que T-07 lo registre, o un downstream que no distribuye `scripts/cli/`)
156
+ * ⇒ `null`, nunca crashea.
157
+ */
158
+ function resolverRutaCliFaseB() {
159
+ try {
160
+ return require.resolve('../../cli/ciclo-fase-b');
161
+ } catch (err) {
162
+ if (err && err.code !== 'MODULE_NOT_FOUND') throw err;
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Spawner por defecto del runner de Fase B: proceso detached fire-and-forget
169
+ * (`stdio:'ignore'` + `unref`) — NUNCA `execSync` (dictamen PM3,
170
+ * `scheduler.js:126` bloquearía otros jobs). Si el CLI no resuelve en este
171
+ * destino, no spawnea y registra la razón en vez de crashear.
172
+ */
173
+ function spawnerFaseBPorDefecto(baseDir) {
174
+ const rutaCli = resolverRutaCliFaseB();
175
+ if (!rutaCli) return { spawneado: false, razon: 'cli-no-resuelve' };
176
+
177
+ const hijo = spawn(process.execPath, [rutaCli, '--dry-run'], {
178
+ cwd: baseDir,
179
+ detached: true,
180
+ stdio: 'ignore',
181
+ });
182
+ hijo.unref();
183
+ return { spawneado: true, pid: hijo.pid };
184
+ }
185
+
186
+ /**
187
+ * Detecta candidatos a Fase B y, si hay nuevos y Fase B está habilitada,
188
+ * spawnea el runner headless. La detección SIEMPRE corre en este punto (para
189
+ * que el conteo llegue a telemetría aunque Fase B esté apagada); el spawn se
190
+ * traga cualquier error del `spawner` (inyectado o por defecto) sin
191
+ * propagarlo — el orquestador de Fase A nunca debe morir por esto.
192
+ *
193
+ * @param {string} baseDir
194
+ * @param {Date} momento
195
+ * @param {Function} [spawner] - Inyectable para tests; default: proceso real.
196
+ * @returns {{candidatosNuevos: number, faseBSpawneada: boolean}}
197
+ */
198
+ function intentarSpawnFaseB(baseDir, momento, spawner) {
199
+ const deteccion = candidatos.detectar(baseDir, { ahora: momento });
200
+ const candidatosNuevos = deteccion.nuevos;
201
+
202
+ if (candidatosNuevos < 1 || !config.estaFaseBHabilitada(baseDir)) {
203
+ return { candidatosNuevos, faseBSpawneada: false };
204
+ }
205
+
206
+ const spawnearFn = typeof spawner === 'function' ? spawner : spawnerFaseBPorDefecto;
207
+ let resultado;
208
+ try {
209
+ resultado = spawnearFn(baseDir);
210
+ } catch (_err) {
211
+ // El spawn NUNCA debe tumbar al proceso padre (hook/CLI): se traga el error.
212
+ resultado = { spawneado: false, razon: 'spawn-lanzo-error' };
213
+ }
214
+
215
+ return { candidatosNuevos, faseBSpawneada: Boolean(resultado && resultado.spawneado) };
216
+ }
217
+
218
+ /** Append de una entrada de telemetría a `.planning/evolution/ciclo-autonomo.jsonl`. */
219
+ function escribirTelemetria(baseDir, totales, errores, faseB) {
220
+ const ruta = rutaTelemetria(baseDir);
221
+ const dir = path.dirname(ruta);
222
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
223
+
224
+ const entrada = {
225
+ ts: new Date().toISOString(), // telemetría de evento puntual — no fecha de artefacto (gate C1 no aplica)
226
+ origen: 'ciclo-autonomo',
227
+ promovidos: totales.promovidos,
228
+ degradados: totales.degradados,
229
+ archivados: totales.archivados,
230
+ candidatosGlobal: totales.candidatosGlobal,
231
+ bajados: totales.bajados,
232
+ drenados: totales.drenados,
233
+ archivadosCola: totales.archivadosCola,
234
+ descartados: totales.descartados,
235
+ nudgesAtendidos: totales.nudgesAtendidos,
236
+ nudgesArchivados: totales.nudgesArchivados,
237
+ candidatosNuevos: faseB.candidatosNuevos,
238
+ faseBSpawneada: faseB.faseBSpawneada,
239
+ errores,
240
+ };
241
+ fs.appendFileSync(ruta, JSON.stringify(entrada) + '\n');
242
+ }
243
+
244
+ /**
245
+ * Corre la Fase A del ciclo autónomo sobre `baseDir`.
246
+ *
247
+ * @param {string} baseDir - Directorio raíz del proyecto.
248
+ * @param {{ahora?: Date, forzar?: boolean, spawner?: Function}} [opciones]
249
+ * `spawner` es inyectable (T-06, REQ-22-06): reemplaza el spawn real del
250
+ * runner de Fase B en tests deterministas.
251
+ * @returns {object} `{ejecutado:false, razon}` si no corrió;
252
+ * `{ejecutado:true, acciones:0}` si no-op; o el detalle completo de la
253
+ * corrida (totales + `errores[]` + `candidatosNuevos` + `faseBSpawneada`)
254
+ * si hubo al menos una acción.
255
+ */
256
+ function correrFaseA(baseDir, { ahora, forzar = false, spawner } = {}) {
257
+ if (!config.estaHabilitado(baseDir) && !forzar) {
258
+ return { ejecutado: false, razon: RAZONES.DESHABILITADO };
259
+ }
260
+
261
+ const momento = ahora || new Date();
262
+ const { acquired, priorMtime } = lock.adquirir(baseDir);
263
+ if (!acquired) {
264
+ return { ejecutado: false, razon: RAZONES.LOCK_OCUPADO };
265
+ }
266
+
267
+ try {
268
+ const errores = [];
269
+ const totales = acumularSubFases(baseDir, momento, errores);
270
+ const acciones = totalAcciones(totales);
271
+
272
+ if (acciones === 0 && errores.length === 0) {
273
+ // NO-OP: la corrida no cuenta como última corrida real.
274
+ lock.rollback(baseDir, priorMtime);
275
+ return { ejecutado: true, acciones: 0 };
276
+ }
277
+
278
+ const resultadoFaseB = intentarSpawnFaseB(baseDir, momento, spawner);
279
+
280
+ escribirTelemetria(baseDir, totales, errores, resultadoFaseB);
281
+ config.escribirConfig(baseDir, {
282
+ last_run: fechaHoraLocal(momento),
283
+ ultima_corrida: {
284
+ promovidos: totales.promovidos,
285
+ degradados: totales.degradados,
286
+ drenados: totales.drenados,
287
+ archivados: totales.archivadosCola,
288
+ bajados: totales.bajados,
289
+ },
290
+ });
291
+ lock.liberar(baseDir);
292
+
293
+ return { ejecutado: true, acciones, ...totales, errores, ...resultadoFaseB };
294
+ } catch (err) {
295
+ // Error catastrófico fuera de las sub-fases: revertir el lock y relanzar.
296
+ lock.rollback(baseDir, priorMtime);
297
+ throw err;
298
+ }
299
+ }
300
+
301
+ module.exports = { correrFaseA, RAZONES };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Lock propio del ciclo autónomo — envuelve `hooks/lib/consolidation-lock.js`
5
+ * con lockfile dedicado `.ciclo-autonomo-lock` en `.planning/` (Fase 21,
6
+ * T-02). Garantiza exclusión mutua entre el scheduler cron y el fallback de
7
+ * `session-briefing` (REQ-21-11, T-11) sin compartir el lockfile de
8
+ * auto-consolidación (`.consolidation-lock`) — corridas concurrentes de
9
+ * ambos motores no se bloquean entre sí.
10
+ *
11
+ * Diferencia deliberada frente al patrón original: `liberar()` SÍ libera el
12
+ * mutex de inmediato (borra el lockfile) en vez de solo tocar su mtime. El
13
+ * módulo original está pensado para un daemon de sesión donde "liberar"
14
+ * significa "marcar el mtime como última corrida exitosa" y el siguiente
15
+ * intento normalmente lo hace un proceso nuevo (PID distinto, el anterior ya
16
+ * murió). El ciclo autónomo, en cambio, corre como invocación CLI de un solo
17
+ * tiro (`swl-ses ciclo-autonomo`) y el guard de "mínimo 24h entre corridas"
18
+ * lo evalúa el caller (T-11, fallback de `session-briefing`) leyendo
19
+ * `ultimaCorridaMs()` — no el propio lock. Por eso "última corrida" se
20
+ * persiste aparte, en el marcador `.planning/evolution/.ciclo-last-run`
21
+ * (epoch ms en texto plano), en vez de reusar el mtime del lockfile: si
22
+ * reusáramos el mtime, `liberar()` no podría borrar el archivo sin perder
23
+ * ese dato.
24
+ *
25
+ * Zero-deps, CommonJS.
26
+ *
27
+ * @module scripts/lib/ciclo-autonomo/lock
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const {
34
+ tryAcquireLock,
35
+ rollbackLock,
36
+ lockPath,
37
+ } = require('../../../hooks/lib/consolidation-lock');
38
+ const { atomicWriteSync } = require('../../../hooks/lib/atomic-write');
39
+
40
+ /** Nombre del lockfile propio del ciclo (independiente del de auto-consolidación). */
41
+ const LOCK_FILENAME = '.ciclo-autonomo-lock';
42
+
43
+ /** Ruta relativa del marcador de última corrida exitosa (epoch ms). */
44
+ const MARCADOR_RELATIVO = path.join('evolution', '.ciclo-last-run');
45
+
46
+ /**
47
+ * Ruta absoluta del marcador de última corrida.
48
+ *
49
+ * @param {string} baseDir - Directorio raíz del proyecto.
50
+ * @returns {string}
51
+ */
52
+ function rutaMarcador(baseDir) {
53
+ return path.join(baseDir, '.planning', MARCADOR_RELATIVO);
54
+ }
55
+
56
+ /**
57
+ * Intenta adquirir el lock del ciclo autónomo.
58
+ *
59
+ * @param {string} baseDir - Directorio raíz del proyecto.
60
+ * @returns {{ acquired: boolean, priorMtime: number }}
61
+ */
62
+ function adquirir(baseDir) {
63
+ return tryAcquireLock(baseDir, { lockFilename: LOCK_FILENAME });
64
+ }
65
+
66
+ /**
67
+ * Libera el lock tras una corrida exitosa: borra el lockfile (mutex libre de
68
+ * inmediato) y registra el epoch ms de la corrida en el marcador de última
69
+ * corrida. El marcador es best-effort — un fallo al escribirlo no debe
70
+ * tumbar el ciclo (ya completó su trabajo real).
71
+ *
72
+ * @param {string} baseDir - Directorio raíz del proyecto.
73
+ */
74
+ function liberar(baseDir) {
75
+ const lp = lockPath(baseDir, { lockFilename: LOCK_FILENAME });
76
+ try {
77
+ fs.unlinkSync(lp);
78
+ } catch {
79
+ // Ya no existía o no se pudo borrar — no bloqueante.
80
+ }
81
+
82
+ try {
83
+ atomicWriteSync(rutaMarcador(baseDir), String(Date.now()), 'utf8');
84
+ } catch {
85
+ // Best-effort: el marcador no es crítico para la corrida ya completada.
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Revierte el lock tras una corrida fallida o no-op: restaura el mtime
91
+ * previo (o borra el lockfile si no existía antes de adquirir). NO toca el
92
+ * marcador de última corrida — una corrida fallida/no-op no cuenta como
93
+ * "última corrida".
94
+ *
95
+ * @param {string} baseDir - Directorio raíz del proyecto.
96
+ * @param {number} priorMtime - Epoch ms devuelto por `adquirir()`.
97
+ */
98
+ function rollback(baseDir, priorMtime) {
99
+ rollbackLock(baseDir, priorMtime, { lockFilename: LOCK_FILENAME });
100
+ }
101
+
102
+ /**
103
+ * Epoch ms de la última corrida exitosa registrada por `liberar()`.
104
+ *
105
+ * @param {string} baseDir - Directorio raíz del proyecto.
106
+ * @returns {number|null} Epoch ms, o `null` si nunca hubo una corrida.
107
+ */
108
+ function ultimaCorridaMs(baseDir) {
109
+ try {
110
+ const crudo = fs.readFileSync(rutaMarcador(baseDir), 'utf8').trim();
111
+ const ms = Number(crudo);
112
+ return Number.isFinite(ms) ? ms : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ adquirir,
120
+ liberar,
121
+ rollback,
122
+ ultimaCorridaMs,
123
+ LOCK_FILENAME,
124
+ };