@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
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * drenador-feedback.js — Drena `.planning/evolution/feedback-queue.jsonl`
5
+ * hacia `instintos/perfil-usuario.yaml` como señales draft (Fase 21 T-05,
6
+ * ADR-0041/0042, REQ-21-03/REQ-21-04).
7
+ *
8
+ * Ventana de 90 días sobre `ts`: lo reciente se consolida como señal draft
9
+ * de baja confianza (0.3 — nunca se inyecta a contexto, umbral en
10
+ * `hooks/inyeccion-contexto.js`); lo viejo se archiva SIN procesar. La cola
11
+ * activa se reescribe al final solo con lo que no pudo clasificarse (en la
12
+ * práctica, nada) — nunca crece sin límite.
13
+ *
14
+ * Tolerancia a líneas corruptas (patrón de `scripts/lib/evidencia-valor.js`
15
+ * función `leerNudges`): JSON inválido o sin `ts` parseable → se cuenta y
16
+ * se descarta, NUNCA crashea el drenado completo por una línea mala.
17
+ *
18
+ * Zero-deps, CommonJS.
19
+ *
20
+ * @module scripts/lib/ciclo-autonomo/drenador-feedback
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { atomicWriteSync } = require('../../../hooks/lib/atomic-write');
26
+ const { agregarSenalesPerfil } = require('./yaml-instintos');
27
+
28
+ const DIA_MS = 24 * 60 * 60 * 1000;
29
+ const VENTANA_DIAS = 90;
30
+ const CONFIDENCE_DRAFT = 0.3;
31
+
32
+ const RUTA_COLA_RELATIVA = path.join('.planning', 'evolution', 'feedback-queue.jsonl');
33
+ const RUTA_ARCHIVE_RELATIVA = path.join('.planning', 'evolution', 'feedback-queue.archive.jsonl');
34
+ const RUTA_PERFIL_RELATIVA = path.join('instintos', 'perfil-usuario.yaml');
35
+
36
+ /**
37
+ * Mapa `tipo` de señal → sección real de `perfil-usuario.yaml`.
38
+ *
39
+ * `preferencia` apunta a `preferencias_comunicacion`, que en el YAML real es
40
+ * un objeto de campos fijos (no una lista) — `agregarSenalesPerfil` lo trata
41
+ * como merge superficial de patch. `prohibicion`/`limite` apuntan a
42
+ * `limites_explicitos.no_sugerir`: el YAML real NO tiene una sub-lista
43
+ * `no_hacer` (solo `no_guardar`/`no_sugerir`); `no_sugerir` es la más
44
+ * genérica de las dos para una señal de "evitar X" sin más contexto.
45
+ */
46
+ const MAPA_SECCION_POR_TIPO = {
47
+ correccion: 'correcciones_repetidas',
48
+ preferencia: 'preferencias_comunicacion',
49
+ prohibicion: 'limites_explicitos.no_sugerir',
50
+ limite: 'limites_explicitos.no_sugerir',
51
+ accion: 'patrones_trabajo',
52
+ };
53
+ const SECCION_POR_DEFECTO = 'patrones_trabajo';
54
+
55
+ function rutaCola(baseDir) { return path.join(baseDir, RUTA_COLA_RELATIVA); }
56
+ function rutaArchive(baseDir) { return path.join(baseDir, RUTA_ARCHIVE_RELATIVA); }
57
+ function rutaPerfil(baseDir) { return path.join(baseDir, RUTA_PERFIL_RELATIVA); }
58
+
59
+ function seccionParaTipo(tipo) {
60
+ return MAPA_SECCION_POR_TIPO[tipo] || SECCION_POR_DEFECTO;
61
+ }
62
+
63
+ /** Resumen legible de la señal para dedupe y para el campo `texto` del draft. */
64
+ function resumenSenal(entrada) {
65
+ const partes = [entrada.accion, entrada.archivo].filter(Boolean);
66
+ if (partes.length > 0) return partes.join(' — ');
67
+ return String(entrada.tipo || 'señal');
68
+ }
69
+
70
+ /**
71
+ * Drena la cola de feedback hacia `perfil-usuario.yaml` con ventana de 90
72
+ * días y rotación de la cola.
73
+ *
74
+ * @param {string} baseDir - Directorio raíz del proyecto.
75
+ * @param {{ ahora?: Date }} [opts]
76
+ * @returns {{ drenados: number, archivados: number, descartados: number }}
77
+ */
78
+ function drenar(baseDir, { ahora = new Date() } = {}) {
79
+ const resultado = { drenados: 0, archivados: 0, descartados: 0 };
80
+
81
+ const rutaColaAbs = rutaCola(baseDir);
82
+ if (!fs.existsSync(rutaColaAbs)) return resultado;
83
+
84
+ const lineas = fs.readFileSync(rutaColaAbs, 'utf8').split(/\r?\n/);
85
+ const corte = ahora.getTime() - VENTANA_DIAS * DIA_MS;
86
+
87
+ const paraArchive = [];
88
+ // Grupos de dedupe: sección → (texto normalizado → entrada draft acumulada).
89
+ const gruposPorSeccion = new Map();
90
+
91
+ for (const linea of lineas) {
92
+ if (!linea.trim()) continue;
93
+
94
+ let entrada;
95
+ try {
96
+ entrada = JSON.parse(linea);
97
+ } catch (_) {
98
+ resultado.descartados += 1;
99
+ continue;
100
+ }
101
+ if (!entrada || typeof entrada !== 'object') {
102
+ resultado.descartados += 1;
103
+ continue;
104
+ }
105
+
106
+ if (entrada.procesado === true) {
107
+ // Ya consumida en una corrida previa: solo limpiar de la cola activa.
108
+ paraArchive.push(linea);
109
+ continue;
110
+ }
111
+
112
+ const tsMs = Date.parse(entrada.ts);
113
+ if (Number.isNaN(tsMs)) {
114
+ resultado.descartados += 1;
115
+ continue;
116
+ }
117
+
118
+ if (tsMs <= corte) {
119
+ // >90 días: archivar SIN procesar.
120
+ paraArchive.push(linea);
121
+ resultado.archivados += 1;
122
+ continue;
123
+ }
124
+
125
+ // ≤90 días: consolidar como señal draft.
126
+ const seccion = seccionParaTipo(entrada.tipo);
127
+ const texto = resumenSenal(entrada);
128
+ const clave = texto.trim().toLowerCase();
129
+
130
+ if (!gruposPorSeccion.has(seccion)) gruposPorSeccion.set(seccion, new Map());
131
+ const grupo = gruposPorSeccion.get(seccion);
132
+
133
+ if (grupo.has(clave)) {
134
+ grupo.get(clave).ocurrencias += 1;
135
+ } else {
136
+ grupo.set(clave, {
137
+ texto,
138
+ confidence: CONFIDENCE_DRAFT,
139
+ origen: 'ciclo-autonomo',
140
+ fecha: ahora.toLocaleDateString('sv'), // fecha local — gate C1
141
+ evidencia: entrada.sessionId || null,
142
+ ocurrencias: 1,
143
+ });
144
+ }
145
+
146
+ entrada.procesado = true;
147
+ paraArchive.push(JSON.stringify(entrada));
148
+ resultado.drenados += 1;
149
+ }
150
+
151
+ if (gruposPorSeccion.size > 0) {
152
+ const rutaPerfilAbs = rutaPerfil(baseDir);
153
+ for (const [seccion, grupo] of gruposPorSeccion) {
154
+ const entradas = [...grupo.values()];
155
+ const valores = seccion.startsWith('limites_explicitos.')
156
+ ? entradas.map((e) => e.texto)
157
+ : entradas;
158
+ agregarSenalesPerfil(rutaPerfilAbs, seccion, valores);
159
+ }
160
+ }
161
+
162
+ // Append al archive (JSONL de alta frecuencia — nunca reescritura total).
163
+ if (paraArchive.length > 0) {
164
+ fs.appendFileSync(rutaArchive(baseDir), paraArchive.join('\n') + '\n');
165
+ }
166
+
167
+ // Rotación: la cola activa solo conserva lo que no se pudo clasificar
168
+ // (en la práctica, nada — drenados/archivados/descartados cubren todo).
169
+ atomicWriteSync(rutaColaAbs, '');
170
+
171
+ return resultado;
172
+ }
173
+
174
+ module.exports = { drenar };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * fallback.js — Predicado puro del fallback de disparo del ciclo autónomo
5
+ * desde `hooks/session-briefing.js` (Fase 21, T-11, REQ-21-11).
6
+ *
7
+ * El scheduler cron (`gateway/cron/jobs.example.json`) es el disparador
8
+ * primario de la Fase A. Este predicado cubre el caso sin scheduler
9
+ * configurado: si el motor está habilitado y pasaron >=24h desde la última
10
+ * corrida exitosa (o nunca corrió), señala que toca disparar. El lock propio
11
+ * del ciclo (`lock.js`, T-02) es quien garantiza la exclusión mutua real
12
+ * entre el scheduler y este fallback — `debeDisparar` NO consulta el estado
13
+ * del lock, solo el reloj.
14
+ *
15
+ * Puro y testeable: recibe `baseDir` y una fecha inyectable (`ahora`), sin
16
+ * spawnear procesos — el caller (el hook) decide cómo lanzar la Fase A.
17
+ *
18
+ * Guard barato PRIMERO (`fs.existsSync` de `.planning/evolution/`): si el
19
+ * directorio de señales no existe, el proyecto no tiene telemetría SWL y se
20
+ * retorna de inmediato SIN requerir `config.js` ni `lock.js` — ese costo no
21
+ * debe pagarse en el hot path de un hook SessionStart (presupuesto <200ms,
22
+ * REQ-12-02). Antes el guard era la existencia de `ciclo-config.json`
23
+ * (opt-in); con el default encendido (decisión del usuario 2026-07-10,
24
+ * opt-out) el config puede no existir y el motor debe correr igual.
25
+ *
26
+ * Zero-deps, CommonJS.
27
+ *
28
+ * @module scripts/lib/ciclo-autonomo/fallback
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+
34
+ /** Ventana mínima entre corridas del fallback: 24 horas en ms. */
35
+ const VENTANA_MS = 24 * 60 * 60 * 1000;
36
+
37
+ /** Directorio de señales SWL — su existencia es el guard barato del hot path. */
38
+ const EVOLUTION_RELATIVA = path.join('.planning', 'evolution');
39
+
40
+ /**
41
+ * Decide si el fallback de `session-briefing` debe disparar la Fase A.
42
+ *
43
+ * @param {string} baseDir - Directorio raíz del proyecto.
44
+ * @param {{ahora?: Date}} [opciones]
45
+ * @returns {{disparar: boolean, razon: string}}
46
+ */
47
+ function debeDisparar(baseDir, { ahora } = {}) {
48
+ const rutaEvolution = path.join(baseDir, EVOLUTION_RELATIVA);
49
+ if (!fs.existsSync(rutaEvolution)) {
50
+ return { disparar: false, razon: 'sin-telemetria-swl' };
51
+ }
52
+
53
+ // Solo en proyectos con telemetría SWL se paga el costo de requerir las
54
+ // libs del ciclo (el default es encendido — opt-out estricto).
55
+ const config = require('./config');
56
+ const lock = require('./lock');
57
+
58
+ if (!config.estaHabilitado(baseDir)) {
59
+ return { disparar: false, razon: 'deshabilitado' };
60
+ }
61
+
62
+ const momento = ahora || new Date();
63
+ const ultimaMs = lock.ultimaCorridaMs(baseDir);
64
+
65
+ if (ultimaMs === null) {
66
+ return { disparar: true, razon: 'sin corridas previas' };
67
+ }
68
+
69
+ const transcurridoMs = momento.getTime() - ultimaMs;
70
+ if (transcurridoMs >= VENTANA_MS) {
71
+ return { disparar: true, razon: `>=24h desde última corrida (${transcurridoMs}ms)` };
72
+ }
73
+
74
+ return { disparar: false, razon: `<24h desde última corrida (${transcurridoMs}ms)` };
75
+ }
76
+
77
+ module.exports = { debeDisparar, VENTANA_MS };
@@ -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 };