@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.
- package/CLAUDE.md +194 -192
- package/README.md +600 -600
- package/agentes/auto-evolucion-swl.md +27 -3
- package/bin/swl-ses.js +32 -6
- package/comandos/swl/actualizar.md +174 -174
- package/comandos/swl/adoptar-proyecto.md +265 -265
- package/comandos/swl/aprender.md +836 -823
- package/comandos/swl/aprobar-plan.md +146 -146
- package/comandos/swl/auditar-deps.md +134 -134
- package/comandos/swl/autoresearch.md +264 -264
- package/comandos/swl/ayuda.md +224 -224
- package/comandos/swl/brainstorm.md +51 -51
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/checkpoint.md +325 -325
- package/comandos/swl/claudemd.md +234 -234
- package/comandos/swl/compactar.md +310 -310
- package/comandos/swl/configurar-ci.md +235 -235
- package/comandos/swl/contexto.md +110 -110
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/crear-skill.md +292 -292
- package/comandos/swl/cron.md +194 -194
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/discutir-fase.md +169 -169
- package/comandos/swl/ejecutar-fase.md +233 -233
- package/comandos/swl/evaluar-skill.md +520 -505
- package/comandos/swl/evolucion-continua.md +73 -0
- package/comandos/swl/evolucionar.md +267 -254
- package/comandos/swl/exportar-vault.md +583 -583
- package/comandos/swl/fix.md +118 -118
- package/comandos/swl/gateway.md +158 -158
- package/comandos/swl/inbox.md +116 -116
- package/comandos/swl/instalar.md +220 -220
- package/comandos/swl/instintos.md +86 -86
- package/comandos/swl/mapear-codebase.md +312 -312
- package/comandos/swl/mcp-status.md +175 -175
- package/comandos/swl/modelo.md +100 -100
- package/comandos/swl/nemesis.md +433 -433
- package/comandos/swl/notificaciones.md +299 -299
- package/comandos/swl/nuevo-proyecto.md +251 -251
- package/comandos/swl/planear-fase.md +263 -263
- package/comandos/swl/plugins.md +256 -256
- package/comandos/swl/predecir.md +169 -169
- package/comandos/swl/reflect-skills.md +125 -125
- package/comandos/swl/release.md +450 -450
- package/comandos/swl/revisar-impacto.md +201 -201
- package/comandos/swl/revisar.md +330 -330
- package/comandos/swl/seguridad.md +189 -189
- package/comandos/swl/sesiones.md +200 -200
- package/comandos/swl/skill-search.md +113 -113
- package/comandos/swl/status.md +343 -343
- package/comandos/swl/verificar.md +817 -817
- package/comandos/swl/wiki.md +620 -620
- package/gateway/cron/jobs.example.json +12 -0
- package/habilidades/auto-evolucion-protocolo/SKILL.md +294 -276
- package/habilidades/autoresearch/SKILL.md +3 -2
- package/habilidades/benchmark-memoria/SKILL.md +7 -7
- package/habilidades/changelog-generator/SKILL.md +174 -174
- package/habilidades/changelog-generator/scripts/parse-commits.js +2 -1
- package/habilidades/checkpoints-verificacion/SKILL.md +6 -0
- package/habilidades/context-builder/SKILL.md +4 -0
- package/habilidades/doubt-driven-review/SKILL.md +207 -191
- package/habilidades/drift-detection/SKILL.md +6 -1
- package/habilidades/ejecutar-fase/SKILL.md +6 -6
- package/habilidades/eval-framework/SKILL.md +8 -3
- package/habilidades/harness-claude-code/SKILL.md +314 -308
- package/habilidades/infra-github-actions/SKILL.md +4 -3
- package/habilidades/instalar-sistema/SKILL.md +227 -223
- package/habilidades/memoria-busqueda/SKILL.md +31 -39
- package/habilidades/planear-fase/SKILL.md +358 -350
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +3 -2
- package/habilidades/release-semver/SKILL.md +4 -2
- package/habilidades/swl-claudemd/SKILL.md +6 -7
- package/habilidades/swl-dashboard/SKILL.md +11 -43
- package/habilidades/tdd-workflow/SKILL.md +749 -744
- package/habilidades/validacion-ci-sistema/SKILL.md +1 -1
- package/hooks/agente-lifecycle.js +2 -1
- package/hooks/aiisms-detector.js +13 -4
- package/hooks/audit-trail.js +2 -1
- package/hooks/auto-consolidacion.js +2 -1
- package/hooks/captura-acciones-post.js +2 -1
- package/hooks/captura-acciones-session.js +2 -1
- package/hooks/captura-feedback-usuario.js +3 -2
- package/hooks/claudemd-bloat-detector.js +12 -3
- package/hooks/claudemd-duplicacion-detector.js +13 -3
- package/hooks/contexto-iteracion.js +2 -1
- package/hooks/degradacion-instintos.js +2 -1
- package/hooks/extraccion-aprendizajes.js +109 -15
- package/hooks/grafo-contexto.js +2 -1
- package/hooks/guardrail-modelo.js +2 -1
- package/hooks/inbox-aviso.js +2 -1
- package/hooks/inyeccion-contexto.js +2 -1
- package/hooks/lib/agent-matcher.js +2 -1
- package/hooks/lib/agent-routing.js +2 -1
- package/hooks/lib/autonomia.js +5 -3
- package/hooks/lib/captura-acciones.js +2 -1
- package/hooks/lib/consolidation-lock.js +21 -10
- package/hooks/lib/etapa-auto-evolucion.js +10 -4
- package/hooks/lib/etapa-metricas.js +2 -1
- package/hooks/lib/etapa-perfil-usuario.js +20 -4
- package/hooks/lib/evolution-tracker.js +2 -1
- package/hooks/lib/gateway-notify.js +193 -179
- package/hooks/lib/loop-telemetry.js +5 -4
- package/hooks/lib/mcp-health.js +2 -1
- package/hooks/lib/memory-search.js +4 -0
- package/hooks/lib/merkle-audit.js +58 -6
- package/hooks/lib/nudge-tracker.js +2 -1
- package/hooks/lib/otlp-exporter.js +2 -1
- package/hooks/lib/propose-step.js +3 -2
- package/hooks/lib/raiz-proyecto.js +102 -0
- package/hooks/lib/run-log.js +2 -1
- package/hooks/lib/singleton-guard.js +218 -27
- package/hooks/lib/telegram-cliente.js +17 -8
- package/hooks/preservar-estado-pre-compact.js +2 -1
- package/hooks/proteccion-rutas.js +59 -3
- package/hooks/registro-turnos.js +2 -1
- package/hooks/resumen-sesion.js +2 -1
- package/hooks/risk-scoring.js +2 -1
- package/hooks/rotar-audit-auto.js +46 -20
- package/hooks/session-briefing.js +127 -1
- package/hooks/spec-gate.js +2 -1
- package/hooks/sugerir-contribuir.js +6 -3
- package/hooks/sugerir-regenerar-inventario.js +3 -2
- package/hooks/tdd-gate.js +2 -1
- package/hooks/telemetria-agentes.js +2 -1
- package/hooks/telemetria-skill-routing.js +2 -1
- package/hooks/tracking-costos.js +4 -3
- package/hooks/validar-formato-post-subagente.js +2 -1
- package/hooks/validar-intent-spec.js +2 -1
- package/hooks/validar-memoria-hook.js +13 -3
- package/hooks/validar-planning-paths.js +2 -1
- package/instintos/.backups/perfil-usuario.yaml.2026-07-10-165128.bak +53 -0
- package/instintos/.backups/proyecto.yaml.2026-07-10-165128.bak +372 -0
- package/instintos/perfil-usuario.yaml +506 -3
- package/instintos/proyecto.yaml +78 -0
- package/llms.txt +2 -2
- package/manifiestos/canonical-hashes.json +664 -2
- package/manifiestos/modulos.json +19 -14
- package/manifiestos/planning-paths.json +1 -0
- package/manifiestos/skills-lock.json +53 -53
- package/package.json +2 -3
- package/plugin.json +2 -2
- package/scripts/actualizar.js +3 -0
- package/scripts/auditar-clases-conocidas.js +32 -4
- package/scripts/benchmark-memoria.js +1 -0
- package/scripts/cli/autonomia.js +23 -0
- package/scripts/cli/benchmark-memoria.js +37 -0
- package/scripts/cli/ciclo-autonomo.js +73 -0
- package/scripts/cli/ciclo-fase-b.js +102 -0
- package/scripts/cli/guardrail-metrics.js +39 -0
- package/scripts/cli/loop-telemetry.js +4 -2
- package/scripts/cli/memoria-search.js +69 -0
- package/scripts/cli/nudge-accionar.js +39 -0
- package/scripts/cli/run-eval.js +38 -0
- package/scripts/cli/run-skill-evals.js +13 -2
- package/scripts/derivar-feature-list.js +15 -14
- package/scripts/desinstalar.js +11 -0
- package/scripts/doctor.js +24 -10
- package/scripts/instalador.js +106 -7
- package/scripts/lib/activar-hooks-proyecto.js +116 -0
- package/scripts/lib/auditar-invocaciones-comandos.js +96 -6
- package/scripts/lib/ciclo-autonomo/candidatos.js +174 -0
- package/scripts/lib/ciclo-autonomo/config.js +165 -0
- package/scripts/lib/ciclo-autonomo/drenador-feedback.js +174 -0
- package/scripts/lib/ciclo-autonomo/fallback.js +77 -0
- package/scripts/lib/ciclo-autonomo/guard-convivencia.js +139 -0
- package/scripts/lib/ciclo-autonomo/higiene-nudges.js +112 -0
- package/scripts/lib/ciclo-autonomo/index.js +301 -0
- package/scripts/lib/ciclo-autonomo/lock.js +124 -0
- package/scripts/lib/ciclo-autonomo/presupuesto.js +122 -0
- package/scripts/lib/ciclo-autonomo/puente-degradacion.js +240 -0
- package/scripts/lib/ciclo-autonomo/runner-fase-b.js +248 -0
- package/scripts/lib/ciclo-autonomo/writer-instintos.js +190 -0
- package/scripts/lib/ciclo-autonomo/yaml-instintos.js +535 -0
- package/scripts/lib/estado.js +9 -0
- package/scripts/lib/evidencia-valor.js +1 -1
- package/scripts/lib/gitignore-manifest.js +8 -1
- package/scripts/lib/hooks-settings.js +45 -0
- package/scripts/rotar-audit-logs.js +48 -2
- package/scripts/run-eval.js +1 -0
- package/scripts/run-skill-evals.js +287 -8
- package/scripts/smoke-test.js +16 -8
- package/scripts/tui/pantallas/install-wizard.js +403 -347
- 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 };
|