@saulwade/swl-ses 2.2.4 → 2.3.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.
@@ -0,0 +1,392 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Lib: captura-acciones.js
5
+ *
6
+ * Núcleo puro y testeable de la captura de SEÑALES DE ACCIÓN del usuario
7
+ * (Fase 17, "Aprendizaje de señales de usuario"). Mientras
8
+ * `captura-feedback-usuario.js` captura las señales VERBALES (palabras tecleadas
9
+ * en UserPromptSubmit), esta lib alimenta las señales de ACCIÓN — la mitad
10
+ * procedural que hoy se pierde:
11
+ * - revert/reset de commits hechos por el agente,
12
+ * - edición manual de archivos que el agente escribió,
13
+ * - rechazo de un PLAN propuesto,
14
+ * - override de un veredicto de /swl:verificar.
15
+ *
16
+ * Las señales se encolan en el MISMO `.planning/evolution/feedback-queue.jsonl`
17
+ * con `tipo:"accion"` (esquema extendido backward-compatible con las verbales),
18
+ * para el mismo consumidor (perfilador-usuario-swl + ciclo AGP). Alcance Fase 17:
19
+ * **solo capturar + telemetría** — sin conversión a memoria procedural.
20
+ *
21
+ * Consumida por los hooks `captura-acciones-post.js` (PostToolUse) y
22
+ * `captura-acciones-session.js` (SessionStart). Zero-deps salvo `atomic-write`
23
+ * (sibling en hooks/lib/). Todas las funciones son puras o reciben `baseDir`
24
+ * explícito → testeables en aislamiento.
25
+ *
26
+ * Opt-out unificado (en los hooks, no aquí): SWL_CAPTURAR_FEEDBACK=0.
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const crypto = require('crypto');
32
+
33
+ // atomic-write es sibling en hooks/lib/ (y queda en el mismo dir tras el
34
+ // aplanado del instalador). Fallback inline si faltara.
35
+ let atomicWriteJSON;
36
+ try {
37
+ ({ atomicWriteJSON } = require('./atomic-write'));
38
+ } catch (_) {
39
+ atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Sanitización de evidencia (REQ-17-07)
44
+ //
45
+ // Port de los 5 patrones de `registro-turnos.js:67-82` + `reglas/seguridad.md
46
+ // § Sanitización PII centralizada`. La sanitización vive en el punto de
47
+ // persistencia (encolarAccion) — ningún path de encolado puede omitirla.
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const PATRONES_REDACT = [
51
+ /\b[A-Za-z0-9_-]{32,}\b/g, // tokens largos
52
+ /(?:sk|pk|key|token|secret|password|pwd|api[_-]?key)["':\s=]+[A-Za-z0-9_\-./]{8,}/gi,
53
+ /\bey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, // JWT
54
+ /https?:\/\/[^:]+:[^@\s]+@/g, // basic auth en URL
55
+ /-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, // PEM blocks
56
+ ];
57
+
58
+ /** Redacta secretos de un texto libre (REQ-17-07). Best-effort, no lanza. */
59
+ function sanitizarEvidencia(texto) {
60
+ if (!texto) return '';
61
+ let r = String(texto);
62
+ for (const re of PATRONES_REDACT) r = r.replace(re, '[REDACTED]');
63
+ return r;
64
+ }
65
+
66
+ /** SHA-256 hex de un string (módulo nativo, sin deps). */
67
+ function hashContenido(str) {
68
+ return crypto.createHash('sha256').update(String(str == null ? '' : str)).digest('hex');
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Rutas runtime (gitignored)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const MAX_EVIDENCIA_LEN = 500;
76
+
77
+ function rutaCola(baseDir) {
78
+ return path.join(baseDir, '.planning', 'evolution', 'feedback-queue.jsonl');
79
+ }
80
+ function rutaIndiceWrites(baseDir) {
81
+ return path.join(baseDir, '.planning', 'evolution', 'agent-writes-index.json');
82
+ }
83
+ function rutaCommits(baseDir) {
84
+ return path.join(baseDir, '.planning', 'evolution', 'agent-commits.jsonl');
85
+ }
86
+
87
+ function asegurarEvolutionDir(baseDir) {
88
+ const dir = path.join(baseDir, '.planning', 'evolution');
89
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
90
+ return dir;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Encolado (REQ-17-06, REQ-17-07)
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Encola una señal de acción en feedback-queue.jsonl. Esquema extendido
99
+ * backward-compatible: las entradas verbales legacy (sin `accion`) siguen
100
+ * parseando. La `evidencia` se sanitiza SIEMPRE aquí (invariante REQ-17-07).
101
+ *
102
+ * @param {string} baseDir raíz del proyecto (cwd del hook).
103
+ * @param {object} accion { accion, archivo?, evidencia?, sessionId?, cwd? }.
104
+ */
105
+ function encolarAccion(baseDir, accion) {
106
+ try {
107
+ asegurarEvolutionDir(baseDir);
108
+ const entrada = {
109
+ ts: new Date().toISOString(),
110
+ sessionId: String(accion.sessionId || 'default'),
111
+ tipo: 'accion',
112
+ accion: accion.accion,
113
+ cwd: accion.cwd || baseDir,
114
+ procesado: false,
115
+ };
116
+ if (accion.archivo) entrada.archivo = String(accion.archivo);
117
+ if (accion.evidencia != null) {
118
+ entrada.evidencia = sanitizarEvidencia(accion.evidencia).slice(0, MAX_EVIDENCIA_LEN);
119
+ }
120
+ fs.appendFileSync(rutaCola(baseDir), JSON.stringify(entrada) + '\n');
121
+ } catch (_) {
122
+ /* nunca bloquear */
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Índice de escrituras del agente (REQ-17-02, REQ-17-03)
128
+ // ---------------------------------------------------------------------------
129
+
130
+ /** Lee el índice de hashes de escritura del agente; {} si ausente/corrupto. */
131
+ function leerIndiceWrites(baseDir) {
132
+ try {
133
+ const p = rutaIndiceWrites(baseDir);
134
+ if (!fs.existsSync(p)) return {};
135
+ const obj = JSON.parse(fs.readFileSync(p, 'utf8'));
136
+ return obj && typeof obj === 'object' ? obj : {};
137
+ } catch (_) {
138
+ return {};
139
+ }
140
+ }
141
+
142
+ function escribirIndiceWrites(baseDir, indice) {
143
+ try {
144
+ asegurarEvolutionDir(baseDir);
145
+ atomicWriteJSON(rutaIndiceWrites(baseDir), indice);
146
+ } catch (_) {
147
+ /* best-effort */
148
+ }
149
+ }
150
+
151
+ /** Registra el hash del último write del agente sobre `rutaRel`. */
152
+ function registrarEscrituraAgente(baseDir, rutaRel, contenido, meta = {}) {
153
+ const indice = leerIndiceWrites(baseDir);
154
+ indice[rutaRel] = {
155
+ sha256: hashContenido(contenido),
156
+ ts: new Date().toISOString(),
157
+ sessionId: String(meta.sessionId || 'default'),
158
+ };
159
+ escribirIndiceWrites(baseDir, indice);
160
+ return indice;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Registro de commits del agente (REQ-17-01) — sin co-author (git-coauthor.md)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /** Registra un SHA commiteado por el agente en agent-commits.jsonl. */
168
+ function registrarCommitAgente(baseDir, { sessionId, sha, mensaje } = {}) {
169
+ try {
170
+ if (!sha) return;
171
+ asegurarEvolutionDir(baseDir);
172
+ const entrada = { ts: new Date().toISOString(), sessionId: String(sessionId || 'default'), sha: String(sha) };
173
+ // `mensaje` también se sanitiza (un mensaje de commit puede llevar un token
174
+ // pegado): este es un segundo path de persistencia y la invariante de no
175
+ // filtrar secretos aplica a TODO path, no solo a encolarAccion (sec BAJA-1).
176
+ if (mensaje) entrada.mensaje = sanitizarEvidencia(String(mensaje)).slice(0, 200);
177
+ fs.appendFileSync(rutaCommits(baseDir), JSON.stringify(entrada) + '\n');
178
+ } catch (_) {
179
+ /* best-effort */
180
+ }
181
+ }
182
+
183
+ /** Lee los SHAs registrados por el agente; [] si ausente/corrupto. */
184
+ function leerCommitsAgente(baseDir) {
185
+ try {
186
+ const p = rutaCommits(baseDir);
187
+ if (!fs.existsSync(p)) return [];
188
+ return fs
189
+ .readFileSync(p, 'utf8')
190
+ .split(/\r?\n/)
191
+ .filter(Boolean)
192
+ .map((l) => {
193
+ try {
194
+ return JSON.parse(l);
195
+ } catch (_) {
196
+ return null;
197
+ }
198
+ })
199
+ .filter(Boolean);
200
+ } catch (_) {
201
+ return [];
202
+ }
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Detectores puros
207
+ // ---------------------------------------------------------------------------
208
+
209
+ /**
210
+ * SHAs registrados por el agente que ya NO están en el historial actual
211
+ * (revert/reset). REQ-17-01.
212
+ * @param {Array<{sha:string}>|string[]} commitsRegistrados
213
+ * @param {string[]} shasHistorialActual
214
+ * @returns {string[]}
215
+ */
216
+ function detectarRevert(commitsRegistrados, shasHistorialActual) {
217
+ const hist = new Set(shasHistorialActual || []);
218
+ const vistos = new Set();
219
+ const out = [];
220
+ for (const c of commitsRegistrados || []) {
221
+ const sha = typeof c === 'string' ? c : c && c.sha;
222
+ if (!sha || vistos.has(sha)) continue;
223
+ vistos.add(sha);
224
+ if (!hist.has(sha)) out.push(sha);
225
+ }
226
+ return out;
227
+ }
228
+
229
+ /**
230
+ * Archivos cuyo hash en disco difiere del último write del agente (edición
231
+ * neta del usuario fuera de sesión). REQ-17-02.
232
+ * @param {Object} indiceWrites { ruta: { sha256 } }
233
+ * @param {Object} hashActualPorArchivo { ruta: sha256 }
234
+ * @returns {string[]}
235
+ */
236
+ function detectarEdicionNeta(indiceWrites, hashActualPorArchivo) {
237
+ const out = [];
238
+ for (const ruta of Object.keys(indiceWrites || {})) {
239
+ const actual = hashActualPorArchivo ? hashActualPorArchivo[ruta] : undefined;
240
+ if (actual === undefined) continue; // archivo borrado/ilegible → no inferir
241
+ const registrado = indiceWrites[ruta] && indiceWrites[ruta].sha256;
242
+ if (registrado && actual !== registrado) out.push(ruta);
243
+ }
244
+ return out;
245
+ }
246
+
247
+ /**
248
+ * true si el contenido re-leído difiere del último write del agente (edición
249
+ * manual en caliente). false si coincide o no hay registro. REQ-17-03.
250
+ */
251
+ function detectarEdicionManual(rutaArchivo, contenidoReleido, indiceWrites) {
252
+ const rec = indiceWrites && indiceWrites[rutaArchivo];
253
+ if (!rec || !rec.sha256) return false;
254
+ return hashContenido(contenidoReleido) !== rec.sha256;
255
+ }
256
+
257
+ function _frontmatterCampo(contenido, campo) {
258
+ const m = contenido.match(new RegExp(`^${campo}:\\s*(.+)$`, 'm'));
259
+ return m ? m[1].trim() : null;
260
+ }
261
+
262
+ function _faseDeNombre(nombre) {
263
+ const m = nombre.match(/^(\d{1,3})-/);
264
+ return m ? parseInt(m[1], 10) : null;
265
+ }
266
+
267
+ /**
268
+ * PLANs que parecen rechazados: estado `rechazado` explícito, o estado
269
+ * `borrador` sin lock G1 mientras la fase ya cerró (existe NN-RESUMEN.md).
270
+ * REQ-17-04.
271
+ * @returns {Array<{fase:number, archivo:string}>}
272
+ */
273
+ function _evaluarPlanRechazado(archivo, fasesDir, locksDir) {
274
+ let contenido = '';
275
+ try {
276
+ contenido = fs.readFileSync(path.join(fasesDir, archivo), 'utf8');
277
+ } catch (_) {
278
+ return null;
279
+ }
280
+ const estado = (_frontmatterCampo(contenido, 'estado') || '').toLowerCase();
281
+ const fase = _faseDeNombre(archivo);
282
+ if (estado === 'rechazado') return { fase, archivo };
283
+ if (estado === 'borrador') {
284
+ const lock = path.join(locksDir, archivo + '.lock');
285
+ const resumen = path.join(fasesDir, archivo.replace('-PLAN.md', '-RESUMEN.md'));
286
+ if (!fs.existsSync(lock) && fs.existsSync(resumen)) return { fase, archivo };
287
+ }
288
+ return null;
289
+ }
290
+
291
+ function detectarPlanRechazado(baseDir) {
292
+ const fasesDir = path.join(baseDir, '.planning', 'fases');
293
+ const locksDir = path.join(baseDir, '.planning', 'locks');
294
+ let archivos;
295
+ try {
296
+ archivos = fs.readdirSync(fasesDir).filter((f) => /-PLAN\.md$/.test(f));
297
+ } catch (_) {
298
+ return [];
299
+ }
300
+ return archivos.map((a) => _evaluarPlanRechazado(a, fasesDir, locksDir)).filter(Boolean);
301
+ }
302
+
303
+ /**
304
+ * VERIFICACIONes con veredicto no-aprobatorio (caso determinista de override).
305
+ * La inferencia "se procedió igual" queda como inspección (T-10). REQ-17-05.
306
+ * @returns {Array<{fase:number, archivo:string, veredicto:string}>}
307
+ */
308
+ function detectarVeredictoOverride(baseDir) {
309
+ const out = [];
310
+ const fasesDir = path.join(baseDir, '.planning', 'fases');
311
+ let archivos;
312
+ try {
313
+ archivos = fs.readdirSync(fasesDir).filter((f) => /-VERIFICACION\.md$/.test(f));
314
+ } catch (_) {
315
+ return out;
316
+ }
317
+ for (const archivo of archivos) {
318
+ let contenido = '';
319
+ try {
320
+ contenido = fs.readFileSync(path.join(fasesDir, archivo), 'utf8');
321
+ } catch (_) {
322
+ continue;
323
+ }
324
+ const m = contenido.match(/VEREDICTO:?\s*(RECHAZADO|REQUIERE[_\s]CORRECCIONES)/i);
325
+ if (m) out.push({ fase: _faseDeNombre(archivo), archivo, veredicto: m[1].toUpperCase().replace(/\s/g, '_') });
326
+ }
327
+ return out;
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Telemetría (REQ-17-09)
332
+ // ---------------------------------------------------------------------------
333
+
334
+ const SUBTIPOS = ['revert', 'edicion-manual', 'plan-rechazado', 'veredicto-override'];
335
+
336
+ /** Conteo por subtipo de acción en la cola; ignora entradas verbales. */
337
+ function contarPorSubtipo(baseDir) {
338
+ const conteo = Object.fromEntries(SUBTIPOS.map((s) => [s, 0]));
339
+ try {
340
+ const p = rutaCola(baseDir);
341
+ if (!fs.existsSync(p)) return conteo;
342
+ for (const linea of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
343
+ if (!linea) continue;
344
+ let e;
345
+ try {
346
+ e = JSON.parse(linea);
347
+ } catch (_) {
348
+ continue;
349
+ }
350
+ if (e && e.tipo === 'accion' && Object.prototype.hasOwnProperty.call(conteo, e.accion)) {
351
+ conteo[e.accion] += 1;
352
+ }
353
+ }
354
+ } catch (_) {
355
+ /* devuelve ceros */
356
+ }
357
+ return conteo;
358
+ }
359
+
360
+ module.exports = {
361
+ sanitizarEvidencia,
362
+ hashContenido,
363
+ encolarAccion,
364
+ leerIndiceWrites,
365
+ escribirIndiceWrites,
366
+ registrarEscrituraAgente,
367
+ registrarCommitAgente,
368
+ leerCommitsAgente,
369
+ detectarRevert,
370
+ detectarEdicionNeta,
371
+ detectarEdicionManual,
372
+ detectarPlanRechazado,
373
+ detectarVeredictoOverride,
374
+ contarPorSubtipo,
375
+ SUBTIPOS,
376
+ MAX_EVIDENCIA_LEN,
377
+ _internals: { PATRONES_REDACT, rutaCola, rutaIndiceWrites, rutaCommits },
378
+ };
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // CLI: `node hooks/lib/captura-acciones.js --conteo` (REQ-17-09 consultable)
382
+ // ---------------------------------------------------------------------------
383
+
384
+ if (require.main === module) {
385
+ try {
386
+ if (process.argv.includes('--conteo')) {
387
+ process.stdout.write(JSON.stringify(contarPorSubtipo(process.cwd()), null, 2) + '\n');
388
+ }
389
+ } catch (_) {
390
+ /* exit 0 siempre */
391
+ }
392
+ }
package/llms.txt CHANGED
@@ -1,29 +1,29 @@
1
- # swl-ses (@saulwade/swl-ses)
2
-
3
- > Sistema de ingeniería de software auto-evolutivo multi-runtime polyglot (SDLC completo), distribuido como paquete npm y plugin de Claude Code. 60 agentes, 182 habilidades, 44 comandos, 37 reglas base y 48 hooks. Soporta 11 lenguajes y 7 runtimes (Claude Code, OpenClaude, OpenCode, Gemini, Cursor, Codex, Copilot). Versión 2.2.4.
4
-
5
- Archivo generado por `node scripts/generar-inventario.js` — no editar a mano. Las cifras se sincronizan con INVENTARIO.md en cada regeneración.
6
-
7
- ## Documentación
8
-
9
- - [README](README.md): overview público y quickstart
10
- - [Manual de uso](MANUAL_USO.md): manual operacional completo
11
- - [Comandos](COMANDOS.md): referencia detallada de cada comando `/swl:*`
12
- - [Agentes](AGENTS.md): catálogo de agentes con capacidades
13
- - [Inventario](INVENTARIO.md): conteos oficiales de todos los componentes
14
- - [Instalación](INSTALACION.md): instalación, perfiles y configuración
15
- - [Instrucciones del proyecto](CLAUDE.md): convenciones, stack y reglas
16
-
17
- ## Componentes
18
-
19
- - 60 agentes especializados en `agentes/` (orquestación, implementación por stack, revisión, calidad, diseño)
20
- - 182 habilidades cargables bajo demanda en `habilidades/` (conocimiento operacional con divulgación progresiva)
21
- - 44 comandos `/swl:*` en `comandos/swl/` (ciclo GSD, calidad, release, diagnóstico)
22
- - 37 reglas base + 40 reglas por lenguaje en `reglas/` (políticas obligatorias por matcher)
23
- - 48 hooks en `hooks/` (telemetría, validación, seguridad; zero-deps, escrituras atómicas)
24
-
25
- ## Opcional
26
-
27
- - [CHANGELOG](CHANGELOG.md): historial de versiones
28
- - [Índice de ADRs](.planning/adrs/README.md): decisiones de arquitectura
29
- - [Variables de entorno](docs/variables-entorno.md): configuración opt-in
1
+ # swl-ses (@saulwade/swl-ses)
2
+
3
+ > Sistema de ingeniería de software auto-evolutivo multi-runtime polyglot (SDLC completo), distribuido como paquete npm y plugin de Claude Code. 60 agentes, 182 habilidades, 44 comandos, 37 reglas base y 50 hooks. Soporta 11 lenguajes y 7 runtimes (Claude Code, OpenClaude, OpenCode, Gemini, Cursor, Codex, Copilot). Versión 2.3.0.
4
+
5
+ Archivo generado por `node scripts/generar-inventario.js` — no editar a mano. Las cifras se sincronizan con INVENTARIO.md en cada regeneración.
6
+
7
+ ## Documentación
8
+
9
+ - [README](README.md): overview público y quickstart
10
+ - [Manual de uso](MANUAL_USO.md): manual operacional completo
11
+ - [Comandos](COMANDOS.md): referencia detallada de cada comando `/swl:*`
12
+ - [Agentes](AGENTS.md): catálogo de agentes con capacidades
13
+ - [Inventario](INVENTARIO.md): conteos oficiales de todos los componentes
14
+ - [Instalación](INSTALACION.md): instalación, perfiles y configuración
15
+ - [Instrucciones del proyecto](CLAUDE.md): convenciones, stack y reglas
16
+
17
+ ## Componentes
18
+
19
+ - 60 agentes especializados en `agentes/` (orquestación, implementación por stack, revisión, calidad, diseño)
20
+ - 182 habilidades cargables bajo demanda en `habilidades/` (conocimiento operacional con divulgación progresiva)
21
+ - 44 comandos `/swl:*` en `comandos/swl/` (ciclo GSD, calidad, release, diagnóstico)
22
+ - 37 reglas base + 40 reglas por lenguaje en `reglas/` (políticas obligatorias por matcher)
23
+ - 50 hooks en `hooks/` (telemetría, validación, seguridad; zero-deps, escrituras atómicas)
24
+
25
+ ## Opcional
26
+
27
+ - [CHANGELOG](CHANGELOG.md): historial de versiones
28
+ - [Índice de ADRs](.planning/adrs/README.md): decisiones de arquitectura
29
+ - [Variables de entorno](docs/variables-entorno.md): configuración opt-in