@saulwade/swl-ses 2.0.0 → 2.1.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 (59) hide show
  1. package/CLAUDE.md +196 -196
  2. package/README.md +579 -579
  3. package/agentes/_propose-step.md +90 -0
  4. package/agentes/implementador-swl.md +2 -0
  5. package/agentes/orquestador-swl.md +2 -0
  6. package/agentes/perfilador-usuario-swl.md +14 -1
  7. package/bin/swl-ses.js +1 -1
  8. package/comandos/swl/aprobar-plan.md +3 -2
  9. package/comandos/swl/briefing.md +122 -0
  10. package/comandos/swl/compactar.md +29 -2
  11. package/comandos/swl/discutir-fase.md +8 -5
  12. package/comandos/swl/ejecutar-fase.md +6 -0
  13. package/comandos/swl/planear-fase.md +5 -3
  14. package/comandos/swl/release.md +46 -0
  15. package/comandos/swl/status.md +69 -0
  16. package/comandos/swl/verificar.md +3 -2
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +6 -4
  18. package/habilidades/ejecutar-fase/SKILL.md +541 -518
  19. package/habilidades/planear-fase/SKILL.md +3 -2
  20. package/habilidades/tdd-workflow/SKILL.md +715 -713
  21. package/habilidades/validacion-ci-sistema/SKILL.md +17 -1
  22. package/hooks/calidad-pre-commit.js +5 -1
  23. package/hooks/check-update.js +39 -1
  24. package/hooks/lib/autonomia.js +208 -0
  25. package/hooks/lib/briefing.js +474 -0
  26. package/hooks/lib/propose-step.js +357 -0
  27. package/hooks/session-briefing.js +98 -0
  28. package/hooks/telemetria-skill-routing.js +100 -0
  29. package/instintos/autonomia.yaml +27 -0
  30. package/llms.txt +4 -4
  31. package/manifiestos/hooks-config.json +18 -0
  32. package/manifiestos/modulos.json +25 -3
  33. package/manifiestos/skills-lock.json +14 -14
  34. package/package.json +93 -93
  35. package/plugin.json +371 -371
  36. package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
  37. package/reglas/consultar-vault-primero.md +195 -0
  38. package/reglas/debatir-antes-de-aceptar.md +158 -0
  39. package/reglas/git-coauthor.md +100 -0
  40. package/reglas/monitor-ci.md +309 -0
  41. package/reglas/registro-componentes-nuevos.md +38 -10
  42. package/reglas/sesiones-paralelas.md +180 -0
  43. package/reglas/usar-code-review-graph.md +155 -0
  44. package/reglas/verificar-citas-normativas.md +548 -0
  45. package/scripts/instalador.js +52 -6
  46. package/scripts/lib/ci-reader.js +193 -0
  47. package/scripts/lib/detectar-host-swl.js +175 -0
  48. package/scripts/lib/evidencia-release.js +322 -0
  49. package/scripts/lib/gate-hooks-requires.js +249 -0
  50. package/scripts/lib/gate-licencias.js +212 -0
  51. package/scripts/lib/git-metricas.js +257 -0
  52. package/scripts/lib/metricas-dora.js +204 -0
  53. package/scripts/tui/ejecutores.js +1 -1
  54. package/scripts/validar-manifest.js +92 -1
  55. package/scripts/verificar-evolucion.js +54 -4
  56. package/scripts/verificar-release.js +102 -0
  57. package/scripts/verificar-trazabilidad.js +11 -5
  58. package/reglas/arquitectura.evolved.json +0 -7
  59. package/reglas/seguridad.evolved.json +0 -7
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * hooks/lib/propose-step.js — Fase 13 (ADR-0037): propose-step de adyacencias.
5
+ *
6
+ * Separa PROPONER de ACTUAR: al cerrar una tarea/fase, evalúa el diff contra una
7
+ * checklist mecanizable de adyacencias de riesgo y emite un anexo PROPOSITIVO.
8
+ * Nunca bloquea, nunca ejecuta. El anexo es texto; el usuario decide si actúa.
9
+ *
10
+ * Taxonomía v1 (D-13-01): 2 señales de alta precisión.
11
+ * - auth-pii-pagos: el cambio toca autenticación, PII o pagos.
12
+ * - migracion-schema: el cambio introduce o modifica el esquema de datos.
13
+ *
14
+ * Telemetría de aceptación en archivo separado .planning/user-profile/
15
+ * propose-telemetria.json, reusando la lógica pura de hooks/lib/briefing.js
16
+ * (D-13-07). Opt-out con SWL_PROPOSE=0.
17
+ *
18
+ * Zero-deps (Node stdlib). Require dual ./lib/X → ./X para funcionar tanto en el
19
+ * repo madre como en el destino aplanado por el instalador (patrón D-17 de F12).
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { execFileSync } = require('child_process');
25
+
26
+ // ─── require dual (repo madre: ./lib/X ; destino aplanado: ./X) ──────────────
27
+
28
+ function requireDual(nombre) {
29
+ try {
30
+ return require(`./${nombre}`); // repo madre (mismo dir) o destino aplanado
31
+ } catch (e1) {
32
+ if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
33
+ return require(`./lib/${nombre}`);
34
+ }
35
+ }
36
+
37
+ let atomicWriteJSON;
38
+ try {
39
+ ({ atomicWriteJSON } = requireDual('atomic-write'));
40
+ } catch (_) {
41
+ // Fallback no-atómico: funciona, pierde la garantía de write atómico.
42
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
43
+ }
44
+
45
+ let briefing;
46
+ try {
47
+ briefing = requireDual('briefing');
48
+ } catch (_) {
49
+ briefing = null; // sin telemetría compartida; la detección sigue funcionando
50
+ }
51
+
52
+ // ─── detectores de señales ───────────────────────────────────────────────────
53
+
54
+ // Patrones de auth/PII/pagos. Acotados a alta precisión: word boundaries donde
55
+ // el término es ambiguo (rfc, card, cvv, pago) para no disparar con prosa.
56
+ const RE_AUTH_PII_DIFF = new RegExp(
57
+ [
58
+ 'password', 'passwd', 'contraseña',
59
+ '\\btoken\\b', 'secret', 'api[_-]?key', '\\bjwt\\b', 'oauth',
60
+ 'authorization', '\\bbearer\\b', '\\bcredential', '\\bsession\\b',
61
+ 'stripe', 'payment', '\\bpago\\b', '\\bpagos\\b', 'tarjeta',
62
+ '\\bcvv\\b', '\\bcard\\b', '\\bcurp\\b', '\\brfc\\b',
63
+ ].join('|'),
64
+ 'i',
65
+ );
66
+
67
+ const RE_AUTH_PII_PATH = new RegExp(
68
+ '(^|/)(auth|login|logout|oauth|jwt|session|credential|credentials|password|payment|pagos?|stripe|checkout)([/._-]|$)',
69
+ 'i',
70
+ );
71
+
72
+ // Patrones de migración / esquema de datos.
73
+ const RE_SCHEMA_PATH = new RegExp(
74
+ '(^|/)(migrations?|alembic|models?|schema|prisma)([/._-]|$)|\\.sql$|schema\\.prisma$',
75
+ 'i',
76
+ );
77
+
78
+ const RE_SCHEMA_DIFF = new RegExp(
79
+ [
80
+ '\\bALTER\\s+TABLE\\b', '\\bCREATE\\s+TABLE\\b', '\\bDROP\\s+TABLE\\b',
81
+ '\\bADD\\s+COLUMN\\b', '\\bDROP\\s+COLUMN\\b', '\\bRENAME\\s+(TABLE|COLUMN)\\b',
82
+ 'op\\.(create_table|add_column|drop_column|alter_column)',
83
+ 'createTable|addColumn|dropColumn', // ORMs JS (knex, sequelize)
84
+ ].join('|'),
85
+ 'i',
86
+ );
87
+
88
+ function _primerPathQueMatchea(paths, re) {
89
+ if (!Array.isArray(paths)) return null;
90
+ for (const p of paths) {
91
+ if (typeof p === 'string' && re.test(p)) return p;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // Cota de tamaño del diff antes de aplicar regex: acota ReDoS y memoria. El
97
+ // detector solo necesita el primer match, así que 2 MiB son más que suficientes.
98
+ const MAX_DIFF_SCAN = 2 * 1024 * 1024;
99
+
100
+ // Devuelve SOLO si el diff matchea (boolean), nunca el fragmento matcheado: la
101
+ // evidencia del anexo NO debe contener slices del diff (podrían arrastrar el
102
+ // secreto adyacente al keyword). Hardening por construcción.
103
+ function _diffMatchea(diff, re) {
104
+ if (typeof diff !== 'string') return false;
105
+ return re.test(diff.length > MAX_DIFF_SCAN ? diff.slice(0, MAX_DIFF_SCAN) : diff);
106
+ }
107
+
108
+ /**
109
+ * Detecta si el cambio toca autenticación, PII o pagos.
110
+ * @param {string[]} paths - rutas de archivos del diff.
111
+ * @param {string} diff - contenido del diff.
112
+ * @returns {null|{categoria,titulo,evidencia,accion}}
113
+ */
114
+ function detectarAuthPiiPagos(paths, diff) {
115
+ const porPath = _primerPathQueMatchea(paths, RE_AUTH_PII_PATH);
116
+ const porDiff = porPath ? false : _diffMatchea(diff, RE_AUTH_PII_DIFF);
117
+ if (!porPath && !porDiff) return null;
118
+ return {
119
+ categoria: 'auth-pii-pagos',
120
+ titulo: 'El cambio toca autenticación, PII o pagos',
121
+ evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
122
+ accion:
123
+ 'Confirma revisión de seguridad (revisor-seguridad-swl) y tests de ' +
124
+ 'autorización/validación antes de cerrar; no expongas secretos en logs.',
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Detecta si el cambio introduce o modifica el esquema de datos.
130
+ * @param {string[]} paths
131
+ * @param {string} diff
132
+ * @returns {null|{categoria,titulo,evidencia,accion}}
133
+ */
134
+ function detectarMigracionSchema(paths, diff) {
135
+ const porPath = _primerPathQueMatchea(paths, RE_SCHEMA_PATH);
136
+ const porDiff = porPath ? false : _diffMatchea(diff, RE_SCHEMA_DIFF);
137
+ if (!porPath && !porDiff) return null;
138
+ return {
139
+ categoria: 'migracion-schema',
140
+ titulo: 'El cambio introduce o modifica el esquema de datos',
141
+ evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
142
+ accion:
143
+ 'Confirma plan de rollback / expand-contract y reversibilidad de la ' +
144
+ 'migración (migrador-swl); verifica que no rompe datos existentes.',
145
+ };
146
+ }
147
+
148
+ const DETECTORES = [detectarAuthPiiPagos, detectarMigracionSchema];
149
+
150
+ /**
151
+ * Evalúa todas las señales sobre un diff. Función pura: solo datos, sin side
152
+ * effects (REQ-13-04).
153
+ * @param {string[]} paths
154
+ * @param {string} diff
155
+ * @returns {{señales: Array<{categoria,titulo,evidencia,accion}>}}
156
+ */
157
+ function evaluarSenales(paths, diff) {
158
+ const señales = [];
159
+ for (const detectar of DETECTORES) {
160
+ const s = detectar(paths, diff);
161
+ if (s) señales.push(s);
162
+ }
163
+ return { señales };
164
+ }
165
+
166
+ // ─── anexo propositivo ───────────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Formatea el anexo propositivo. Devuelve null si no hay señales activas (silencio
170
+ * total, D-13-03) o si todas las categorías están silenciadas.
171
+ * @param {Array} señales
172
+ * @param {Set<string>} silenciadas - categorías que la telemetría silenció.
173
+ * @returns {string|null}
174
+ */
175
+ function formatearAnexo(señales, silenciadas) {
176
+ const sil = silenciadas instanceof Set ? silenciadas : new Set();
177
+ const activas = Array.isArray(señales) ? señales.filter((s) => s && !sil.has(s.categoria)) : [];
178
+ if (activas.length === 0) return null;
179
+ const lineas = [
180
+ '## Anexo propositivo — adyacencias de riesgo',
181
+ '',
182
+ 'Sugerencias, **no acciones**: nada se ejecuta ni se bloquea automáticamente. ' +
183
+ 'Revisa si aplican.',
184
+ '',
185
+ ];
186
+ for (const s of activas) {
187
+ lineas.push(`- [${s.categoria}] ${s.titulo} (${s.evidencia}) → ${s.accion}`);
188
+ }
189
+ return lineas.join('\n');
190
+ }
191
+
192
+ // ─── telemetría de aceptación (archivo separado, REQ-13-08/09) ───────────────
193
+ //
194
+ // Modelo a nivel de CATEGORÍA (no por hash). Las señales del propose tienen
195
+ // título fijo por categoría → el conteo por-hash de briefing.actualizarTelemetria
196
+ // nunca alcanzaría MIN_MUESTRAS_SILENCIO. Se reusan los UMBRALES de briefing.js
197
+ // (D-13-07) y categoriasSilenciadas, pero el conteo es por exposición:
198
+ // - registrarPropose: mostrado += 1 por categoría mostrada (automático).
199
+ // - registrarFeedback: actuado/ignorado += 1 (canal de aceptación explícito).
200
+ // - silenciada se recomputa con los mismos umbrales que el briefing.
201
+
202
+ const TELE_PATH = ['.planning', 'user-profile', 'propose-telemetria.json'];
203
+
204
+ // Umbrales: reusar los de briefing.js; fallback a los mismos valores si la lib
205
+ // no está disponible en el destino.
206
+ const UMBRAL = {
207
+ RATIO_SILENCIO: (briefing && briefing.RATIO_SILENCIO) || 0.8,
208
+ MIN_MUESTRAS_SILENCIO: (briefing && briefing.MIN_MUESTRAS_SILENCIO) || 5,
209
+ };
210
+
211
+ function telemetriaPath(baseDir) {
212
+ return path.join(baseDir || process.cwd(), ...TELE_PATH);
213
+ }
214
+
215
+ function _catVacia() {
216
+ return { mostrado: 0, actuado: 0, ignorado: 0, silenciada: false, ultima_ts: null };
217
+ }
218
+
219
+ /** Lee la telemetría de propose; fallback a estructura vacía. */
220
+ function leerTelemetriaPropose(baseDir) {
221
+ try {
222
+ const raw = fs.readFileSync(telemetriaPath(baseDir), 'utf8');
223
+ const obj = JSON.parse(raw);
224
+ return { categorias: obj.categorias && typeof obj.categorias === 'object' ? obj.categorias : {} };
225
+ } catch (_) {
226
+ return { categorias: {} };
227
+ }
228
+ }
229
+
230
+ /** Recalcula silenciada con los umbrales del briefing. Muta la categoría dada. */
231
+ function _recomputarSilenciada(c) {
232
+ c.silenciada =
233
+ c.mostrado >= UMBRAL.MIN_MUESTRAS_SILENCIO &&
234
+ c.ignorado / c.mostrado >= UMBRAL.RATIO_SILENCIO;
235
+ return c;
236
+ }
237
+
238
+ function _persistir(baseDir, tele) {
239
+ try {
240
+ const dir = path.dirname(telemetriaPath(baseDir));
241
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
242
+ atomicWriteJSON(telemetriaPath(baseDir), tele);
243
+ } catch (_) {
244
+ // persistir es best-effort; no romper el cierre de la tarea.
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Registra la exposición de un anexo: mostrado += 1 por cada categoría presente.
250
+ * @param {string} baseDir
251
+ * @param {Array} señales
252
+ * @param {string} hoyISO - timestamp inyectable para tests deterministas.
253
+ * @returns {{categorias}}
254
+ */
255
+ function registrarPropose(baseDir, señales, hoyISO) {
256
+ const tele = leerTelemetriaPropose(baseDir);
257
+ const hoy = hoyISO || new Date().toISOString();
258
+ for (const s of Array.isArray(señales) ? señales : []) {
259
+ if (!s || !s.categoria) continue;
260
+ const c = tele.categorias[s.categoria] || _catVacia();
261
+ c.mostrado += 1;
262
+ c.ultima_ts = hoy;
263
+ _recomputarSilenciada(c);
264
+ tele.categorias[s.categoria] = c;
265
+ }
266
+ _persistir(baseDir, tele);
267
+ return tele;
268
+ }
269
+
270
+ /**
271
+ * Registra feedback de aceptación de una categoría (canal explícito).
272
+ * @param {string} baseDir
273
+ * @param {string} categoria
274
+ * @param {'actuado'|'ignorado'} tipo
275
+ * @param {string} hoyISO
276
+ * @returns {{categorias}}
277
+ */
278
+ function registrarFeedback(baseDir, categoria, tipo, hoyISO) {
279
+ if (tipo !== 'actuado' && tipo !== 'ignorado') {
280
+ throw new Error(`registrarFeedback: tipo inválido "${tipo}" (esperado actuado|ignorado)`);
281
+ }
282
+ const tele = leerTelemetriaPropose(baseDir);
283
+ const c = tele.categorias[categoria] || _catVacia();
284
+ c[tipo] += 1;
285
+ c.ultima_ts = hoyISO || new Date().toISOString();
286
+ _recomputarSilenciada(c);
287
+ tele.categorias[categoria] = c;
288
+ _persistir(baseDir, tele);
289
+ return tele;
290
+ }
291
+
292
+ /** Set de categorías silenciadas según la telemetría de propose. */
293
+ function categoriasSilenciadasPropose(baseDir) {
294
+ const tele = leerTelemetriaPropose(baseDir);
295
+ if (briefing && briefing.categoriasSilenciadas) {
296
+ return briefing.categoriasSilenciadas(tele);
297
+ }
298
+ const set = new Set();
299
+ for (const [cat, c] of Object.entries(tele.categorias || {})) {
300
+ if (c && c.silenciada) set.add(cat);
301
+ }
302
+ return set;
303
+ }
304
+
305
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
306
+
307
+ const MAX_DIFF_BUFFER = 32 * 1024 * 1024; // 32 MiB — tope de salida de git diff
308
+ // Rango git válido: refs/SHAs y rangos `a..b` / `a...b`. Rechaza flags (`--output`,
309
+ // `-G`) que git interpretaría aunque execFileSync evita el shell.
310
+ const RE_RANGO_VALIDO = /^[A-Za-z0-9_./@~^-]+(\.\.\.?[A-Za-z0-9_./@~^-]+)?$/;
311
+
312
+ function _gitDiff(rango) {
313
+ // execFileSync con array de args: no pasa por shell. Además se valida el rango
314
+ // y se usa el separador `--` para forzar que git lo trate como ref, no flag.
315
+ const r = rango || 'HEAD~1..HEAD';
316
+ if (!RE_RANGO_VALIDO.test(r)) return { paths: [], diff: '' };
317
+ try {
318
+ const paths = execFileSync('git', ['diff', '--name-only', r, '--'], { encoding: 'utf8' })
319
+ .split('\n').map((s) => s.trim()).filter(Boolean);
320
+ const diff = execFileSync('git', ['diff', r, '--'], { encoding: 'utf8', maxBuffer: MAX_DIFF_BUFFER });
321
+ return { paths, diff };
322
+ } catch (_) {
323
+ return { paths: [], diff: '' };
324
+ }
325
+ }
326
+
327
+ function main(argv) {
328
+ // Opt-out: SWL_PROPOSE=0 → silencio inmediato.
329
+ if (process.env.SWL_PROPOSE === '0') return 0;
330
+ const args = argv.slice(2);
331
+ let rango = 'HEAD~1..HEAD';
332
+ for (const a of args) {
333
+ if (a.startsWith('--rango=')) rango = a.slice('--rango='.length);
334
+ }
335
+ const baseDir = process.cwd();
336
+ const { paths, diff } = _gitDiff(rango);
337
+ const { señales } = evaluarSenales(paths, diff);
338
+ registrarPropose(baseDir, señales);
339
+ const anexo = formatearAnexo(señales, categoriasSilenciadasPropose(baseDir));
340
+ if (anexo) process.stdout.write(anexo + '\n');
341
+ return 0;
342
+ }
343
+
344
+ if (require.main === module) {
345
+ process.exit(main(process.argv));
346
+ }
347
+
348
+ module.exports = {
349
+ detectarAuthPiiPagos,
350
+ detectarMigracionSchema,
351
+ evaluarSenales,
352
+ formatearAnexo,
353
+ leerTelemetriaPropose,
354
+ registrarPropose,
355
+ registrarFeedback,
356
+ categoriasSilenciadasPropose,
357
+ };
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: session-briefing.js
6
+ * Tipo: SessionStart
7
+ *
8
+ * Briefing proactivo de inicio de sesión (Fase 12, ADR-0036). Al abrir sesión
9
+ * en un proyecto con `.planning/`, presenta un digest no solicitado de señales
10
+ * accionables que el usuario no sabía que tenía que preguntar: ADRs Propuestos
11
+ * con reevaluación vencida, deuda con trigger por fecha cumplido, nudges sin
12
+ * accionar, gates en calibración con ventana cumplida, y trabajo de retoma
13
+ * pendiente. Silencio total cuando no hay señales nuevas.
14
+ *
15
+ * Solo lecturas de filesystem ya computado: cero LLM, cero red, presupuesto
16
+ * <200ms (REQ-12-02). Las señales caras viven en `/swl:briefing`. SIEMPRE sale 0.
17
+ *
18
+ * Zero-deps. Opt-out: SWL_BRIEFING=0. Zero-config: sin `.planning/` → silencio.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // Require con fallback dual: el instalador aplana `hooks/lib/X.js → <hooks>/X.js`
25
+ // en el destino, pero en el repo madre vive en `lib/`. Cubre ambos layouts (D-17).
26
+ function requireDual(nombre) {
27
+ try {
28
+ return require(`./lib/${nombre}`);
29
+ } catch (e1) {
30
+ if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
31
+ return require(`./${nombre}`);
32
+ }
33
+ }
34
+
35
+ let briefing;
36
+ let atomicWriteJSON;
37
+ try {
38
+ briefing = requireDual('briefing');
39
+ } catch (_) {
40
+ briefing = null;
41
+ }
42
+ try {
43
+ ({ atomicWriteJSON } = requireDual('atomic-write'));
44
+ } catch (_) {
45
+ atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
46
+ }
47
+
48
+ let inputRaw = '';
49
+ process.stdin.on('data', (c) => { inputRaw += c; });
50
+
51
+ process.stdin.on('end', () => {
52
+ try {
53
+ if (process.env.SWL_BRIEFING === '0') return; // opt-out
54
+ if (!briefing) return; // lib ausente: degradar a silencio
55
+
56
+ const cwd = process.cwd();
57
+ if (!fs.existsSync(path.join(cwd, '.planning'))) return; // zero-config
58
+
59
+ const hoy = new Date();
60
+ const items = briefing.recolectarTodo(cwd, hoy);
61
+ const estadoPrevio = briefing.leerEstadoBriefing(cwd);
62
+ const dia = hoy.toISOString().slice(0, 10);
63
+
64
+ // Telemetría de aceptación (D-18, REQ-12-05): se actualiza SIEMPRE que el
65
+ // hook corre, comparando lo visto antes con lo presente ahora — independiente
66
+ // del dedupe de display. Alimenta a perfilador-usuario-swl para callar
67
+ // categorías que el usuario ignora consistentemente.
68
+ let silenciadas = new Set();
69
+ try {
70
+ const telePrev = briefing.leerTelemetria(cwd);
71
+ const teleNueva = briefing.actualizarTelemetria(telePrev, items, hoy.toISOString());
72
+ silenciadas = briefing.categoriasSilenciadas(teleNueva);
73
+ const dir = path.join(cwd, '.planning', 'user-profile');
74
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
75
+ atomicWriteJSON(path.join(dir, 'briefing-telemetria.json'), teleNueva);
76
+ } catch (_) { /* telemetría best-effort */ }
77
+
78
+ const digest = briefing.armarDigest(items, estadoPrevio, dia, {
79
+ categoriasSilenciadas: silenciadas,
80
+ });
81
+ if (!digest) return; // silencio total: sin señales nuevas
82
+
83
+ // Persistir el estado de dedupe (best-effort).
84
+ try {
85
+ const dir = path.join(cwd, '.planning', 'user-profile');
86
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
87
+ atomicWriteJSON(path.join(dir, 'briefing-estado.json'), digest.estado);
88
+ } catch (_) { /* persistir es best-effort; no romper el digest */ }
89
+
90
+ const output = {
91
+ hookSpecificOutput: {
92
+ hookEventName: 'SessionStart',
93
+ additionalContext: digest.texto,
94
+ },
95
+ };
96
+ process.stdout.write(JSON.stringify(output));
97
+ } catch (_) { /* silencioso: el hook nunca bloquea la sesión */ }
98
+ });
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: telemetria-skill-routing.js
6
+ * Tipo: PostToolUse (matcher: "Skill")
7
+ *
8
+ * Cablea la lib huérfana `scripts/lib/skill-metrics.js`: por cada invocación
9
+ * del tool Skill registra la frecuencia/estado en `.planning/skill-metrics.json`
10
+ * vía `registrarUso(cwd, skillName, estado, duracionMs)`.
11
+ *
12
+ * Propósito (Revisión Evolutiva 04, P-2 "bloat y precisión de routing"):
13
+ * acumular telemetría de qué skills se invocan para alimentar la futura
14
+ * auditoría de bloat (skills muertos / solapados). Es la instrumentación que
15
+ * el roadmap permite adelantar como "tarea suelta" antes de la fase de auditoría.
16
+ *
17
+ * Distinto de telemetria-agentes.js (spans OTLP a traces/) y de audit-trail.js
18
+ * (todos los tool calls a audit.jsonl): este hook produce el agregado por-skill
19
+ * que la auditoría de bloat necesita y que ningún otro hook genera.
20
+ *
21
+ * NUNCA bloquea (siempre exit 0). Async: corre en background.
22
+ * Opt-out: SWL_SKILL_TELEMETRIA=0.
23
+ *
24
+ * Origen: punto P-2 de la Revisión Evolutiva 04 (instrumentación adelantada).
25
+ */
26
+
27
+ /**
28
+ * Extrae el evento de skill de un payload PostToolUse.
29
+ * Pura y testeable — no toca el filesystem.
30
+ *
31
+ * @param {object} data payload PostToolUse
32
+ * @returns {{skillName: string, estado: 'ok'|'error'}|null} null si no aplica
33
+ */
34
+ function extraerEvento(data) {
35
+ if (!data || typeof data !== 'object') return null;
36
+ const toolName = String(data.tool_name || (data.tool && data.tool.name) || '');
37
+ if (toolName !== 'Skill') return null;
38
+
39
+ const input = data.tool_input || {};
40
+ const skillName = String(input.skill || input.name || '').trim();
41
+ if (!skillName) return null;
42
+
43
+ const esError = Boolean(data.error_message || data.error);
44
+ return { skillName, estado: esError ? 'error' : 'ok' };
45
+ }
46
+
47
+ /**
48
+ * Carga defensiva de la lib de métricas. Si no está disponible (destino
49
+ * aplanado, instalación parcial), retorna null y el hook degrada en silencio.
50
+ * @returns {Function|null} registrarUso o null
51
+ */
52
+ function cargarRegistrar() {
53
+ try {
54
+ return require('../scripts/lib/skill-metrics').registrarUso;
55
+ } catch (_) {
56
+ try {
57
+ return require('./lib/skill-metrics').registrarUso;
58
+ } catch (_2) {
59
+ return null;
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Procesa un payload y registra el uso del skill si aplica.
66
+ * @param {object} data payload PostToolUse
67
+ * @param {string} cwd directorio del proyecto
68
+ * @param {Function} [registrar] inyectable para tests; default carga skill-metrics
69
+ * @returns {boolean} true si registró
70
+ */
71
+ function procesar(data, cwd, registrar) {
72
+ const evento = extraerEvento(data);
73
+ if (!evento) return false;
74
+ const fn = registrar || cargarRegistrar();
75
+ if (!fn) return false;
76
+ fn(cwd, evento.skillName, evento.estado, 0);
77
+ return true;
78
+ }
79
+
80
+ module.exports = { extraerEvento, procesar };
81
+
82
+ // --- Entrypoint -------------------------------------------------------------
83
+ // Claude Code invoca el hook vía `node -e "require('./hooks/...')"`, donde
84
+ // `require.main` es undefined. Adjuntar stdin SOLO en ese caso (o invocación
85
+ // directa), nunca cuando un test requiere el módulo (ahí require.main es el
86
+ // runner, definido y ≠ este módulo) — así los tests no cuelgan en stdin.
87
+ if (!require.main || require.main === module) {
88
+ // Opt-out: no adjuntar el listener (sin process.exit, seguro de requerir).
89
+ if (process.env.SWL_SKILL_TELEMETRIA !== '0') {
90
+ let inputRaw = '';
91
+ process.stdin.on('data', (chunk) => { inputRaw += chunk; });
92
+ process.stdin.on('end', () => {
93
+ try {
94
+ procesar(JSON.parse(inputRaw), process.cwd());
95
+ } catch (_) {
96
+ // Silencioso — nunca interrumpir la sesión por un fallo de telemetría.
97
+ }
98
+ });
99
+ }
100
+ }
@@ -0,0 +1,27 @@
1
+ # Dial de autonomía — SWL-SES (Fase 13, ADR-0037)
2
+ #
3
+ # Presupuesto de autonomía por clase de riesgo. Consumido por hooks/lib/autonomia.js
4
+ # (orquestador y agentes ALTO leen el dial como GUÍA; no es un gate bloqueante en v1).
5
+ #
6
+ # Defaults = los controles vigentes de reglas/seguridad-agentes.md. Este archivo
7
+ # NO relaja ningún control: lo hace visible y ajustable. El dial sube SOLO por
8
+ # decisión explícita del usuario, nunca por conveniencia del agente.
9
+ #
10
+ # Niveles válidos por clase:
11
+ # total -> acción autónoma sin checkpoint (lectura/análisis).
12
+ # con_auto_checkpoint -> autónoma, pero registra un auto-checkpoint antes de actuar.
13
+ # hitl -> requiere confirmación humana siempre.
14
+ # Cualquier valor desconocido se degrada a 'hitl' (nunca relaja).
15
+ #
16
+ # Estructura PLANA obligatoria (1 nivel bajo `clases:`) — ver 13-PLAN.md.
17
+
18
+ version: "1.0"
19
+ generado: "2026-06-11"
20
+ defaults: seguridad-agentes.md
21
+ clases:
22
+ lectura_analisis: total
23
+ # Subido a `total` por decisión explícita del usuario (2026-06-11, cierre Fase 13):
24
+ # cambios reversibles autónomos sin auto-checkpoint. Los commits atómicos siguen
25
+ # siendo el mecanismo de rollback. Default de seguridad-agentes.md era con_auto_checkpoint.
26
+ cambio_reversible: total
27
+ migracion_auth_push_publish: hitl
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # swl-ses (@saulwade/swl-ses)
2
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, 43 comandos, 29 reglas base y 46 hooks. Soporta 11 lenguajes y 7 runtimes (Claude Code, OpenClaude, OpenCode, Gemini, Cursor, Codex, Copilot). Versión 2.0.0.
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.1.0.
4
4
 
5
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
6
 
@@ -18,9 +18,9 @@ Archivo generado por `node scripts/generar-inventario.js` — no editar a mano.
18
18
 
19
19
  - 60 agentes especializados en `agentes/` (orquestación, implementación por stack, revisión, calidad, diseño)
20
20
  - 182 habilidades cargables bajo demanda en `habilidades/` (conocimiento operacional con divulgación progresiva)
21
- - 43 comandos `/swl:*` en `comandos/swl/` (ciclo GSD, calidad, release, diagnóstico)
22
- - 29 reglas base + 40 reglas por lenguaje en `reglas/` (políticas obligatorias por matcher)
23
- - 46 hooks en `hooks/` (telemetría, validación, seguridad; zero-deps, escrituras atómicas)
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
24
 
25
25
  ## Opcional
26
26
 
@@ -213,6 +213,15 @@
213
213
  "maxConsecutiveFailures": 5,
214
214
  "degradeOnFailure": "skip"
215
215
  },
216
+ "telemetria-skill-routing.js": {
217
+ "event": "PostToolUse",
218
+ "matcher": "Skill",
219
+ "description": "Registra frecuencia/estado de cada invocación de skill en .planning/skill-metrics.json (alimenta auditoría de bloat, P-2). Opt-out SWL_SKILL_TELEMETRIA=0",
220
+ "blocking": false,
221
+ "async": true,
222
+ "maxConsecutiveFailures": 5,
223
+ "degradeOnFailure": "skip"
224
+ },
216
225
  "audit-trail.js": {
217
226
  "event": "PostToolUse",
218
227
  "matcher": "",
@@ -428,6 +437,15 @@
428
437
  "maxConsecutiveFailures": 5,
429
438
  "degradeOnFailure": "skip"
430
439
  },
440
+ "session-briefing.js": {
441
+ "event": "SessionStart",
442
+ "matcher": "",
443
+ "description": "Briefing proactivo de inicio de sesión (Fase 12, ADR-0036): digest no solicitado de señales accionables del proyecto destino — ADRs Propuestos con reevaluación vencida, deuda con trigger por fecha cumplido, nudges sin accionar, gates en calibración con ventana cumplida, y trabajo de retoma pendiente. Máximo 5 ítems con acción ejecutable; silencio total sin señales nuevas; dedupe diario. Solo filesystem, presupuesto <200ms. Opt-out: SWL_BRIEFING=0.",
444
+ "blocking": false,
445
+ "async": false,
446
+ "maxConsecutiveFailures": 5,
447
+ "degradeOnFailure": "skip"
448
+ },
431
449
  "validar-planning-paths.js": {
432
450
  "event": "PostToolUse",
433
451
  "matcher": "Write|Edit|MultiEdit",