@saulwade/swl-ses 1.9.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 (142) 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/accesibilidad-wcag-swl.md +3 -3
  5. package/agentes/auto-evolucion-swl.md +908 -908
  6. package/agentes/disenador-ui-swl.md +6 -5
  7. package/agentes/frontend-angular-swl.md +2 -2
  8. package/agentes/frontend-css-swl.md +2 -2
  9. package/agentes/frontend-react-swl.md +4 -4
  10. package/agentes/frontend-swl.md +6 -6
  11. package/agentes/implementador-swl.md +2 -0
  12. package/agentes/investigador-ux-swl.md +5 -5
  13. package/agentes/orquestador-swl.md +9 -7
  14. package/agentes/perfilador-usuario-swl.md +321 -308
  15. package/agentes/producto-prd-swl.md +1 -1
  16. package/agentes/red-team-swl.md +218 -218
  17. package/agentes/tdd-qa-swl.md +17 -1
  18. package/bin/swl-ses.js +1 -1
  19. package/comandos/swl/actualizar.md +1 -1
  20. package/comandos/swl/aprender.md +2 -2
  21. package/comandos/swl/aprobar-plan.md +153 -0
  22. package/comandos/swl/ayuda.md +3 -3
  23. package/comandos/swl/briefing.md +122 -0
  24. package/comandos/swl/compactar.md +29 -2
  25. package/comandos/swl/discutir-fase.md +23 -2
  26. package/comandos/swl/ejecutar-fase.md +59 -6
  27. package/comandos/swl/evolucionar.md +1 -1
  28. package/comandos/swl/inbox.md +1 -1
  29. package/comandos/swl/instalar.md +1 -1
  30. package/comandos/swl/nemesis.md +1 -1
  31. package/comandos/swl/planear-fase.md +19 -1
  32. package/comandos/swl/plugins.md +1 -1
  33. package/comandos/swl/release.md +47 -1
  34. package/comandos/swl/status.md +348 -0
  35. package/comandos/swl/verificar.md +27 -1
  36. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  37. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  38. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  39. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  40. package/habilidades/changelog-generator/SKILL.md +9 -2
  41. package/habilidades/changelog-generator/scripts/parse-commits.js +13 -1
  42. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  43. package/habilidades/drift-detection/SKILL.md +179 -179
  44. package/habilidades/ejecutar-fase/SKILL.md +541 -468
  45. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  46. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  47. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  48. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  49. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  50. package/habilidades/harness-claude-code/SKILL.md +10 -7
  51. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  52. package/habilidades/instalar-sistema/SKILL.md +3 -3
  53. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  54. package/habilidades/perfil-usuario/SKILL.md +200 -200
  55. package/habilidades/planear-fase/SKILL.md +26 -4
  56. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  57. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  58. package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
  59. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  60. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  61. package/habilidades/swl-claudemd/SKILL.md +50 -210
  62. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  63. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  64. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  65. package/habilidades/swl-dashboard/SKILL.md +9 -9
  66. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  67. package/habilidades/tdd-workflow/SKILL.md +715 -673
  68. package/habilidades/validacion-ci-sistema/SKILL.md +20 -4
  69. package/hooks/calidad-pre-commit.js +344 -3
  70. package/hooks/check-update.js +39 -1
  71. package/hooks/ciclo-evolucion-subagente.js +26 -0
  72. package/hooks/ciclo-evolucion.js +26 -0
  73. package/hooks/extraccion-aprendizajes.js +13 -0
  74. package/hooks/lib/autonomia.js +208 -0
  75. package/hooks/lib/briefing.js +474 -0
  76. package/hooks/lib/ciclo-evolucion.js +47 -0
  77. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  78. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  79. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  80. package/hooks/lib/evolution-tracker.js +24 -3
  81. package/hooks/lib/propose-step.js +357 -0
  82. package/hooks/session-briefing.js +98 -0
  83. package/hooks/spec-gate.js +211 -0
  84. package/hooks/tdd-gate.js +241 -0
  85. package/hooks/telemetria-skill-routing.js +100 -0
  86. package/hooks/validar-intent-spec.js +30 -10
  87. package/instintos/autonomia.yaml +27 -0
  88. package/llms.txt +6 -6
  89. package/manifiestos/hooks-config.json +44 -17
  90. package/manifiestos/modulos.json +40 -15
  91. package/manifiestos/skills-lock.json +64 -57
  92. package/package.json +93 -93
  93. package/plugin.json +371 -375
  94. package/reglas/accesibilidad.md +10 -0
  95. package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
  96. package/reglas/api-diseno.md +9 -0
  97. package/reglas/auditorias-documentales-estructurales.md +7 -0
  98. package/reglas/cloud-infra.md +8 -0
  99. package/reglas/consultar-vault-primero.md +195 -0
  100. package/reglas/debatir-antes-de-aceptar.md +158 -0
  101. package/reglas/fragmentos-compartidos.md +5 -0
  102. package/reglas/git-coauthor.md +100 -0
  103. package/reglas/gobernanza.md +4 -4
  104. package/reglas/hooks.md +6 -0
  105. package/reglas/intent-engineering.md +4 -0
  106. package/reglas/markitdown.md +8 -0
  107. package/reglas/memoria-consolidada.md +1 -1
  108. package/reglas/monitor-ci.md +309 -0
  109. package/reglas/patrones.md +6 -0
  110. package/reglas/registro-componentes-nuevos.md +39 -2
  111. package/reglas/seguridad-agentes.md +1 -1
  112. package/reglas/sesiones-paralelas.md +180 -0
  113. package/reglas/skills-estandar.md +6 -0
  114. package/reglas/testing.md +7 -0
  115. package/reglas/tests-cleanup.md +4 -0
  116. package/reglas/usar-code-review-graph.md +155 -0
  117. package/reglas/usar-sistema-swl.md +1 -1
  118. package/reglas/verificar-citas-normativas.md +548 -0
  119. package/scripts/instalador.js +52 -6
  120. package/scripts/lib/ci-reader.js +193 -0
  121. package/scripts/lib/detectar-host-swl.js +175 -0
  122. package/scripts/lib/evidencia-release.js +322 -0
  123. package/scripts/lib/gate-hooks-requires.js +249 -0
  124. package/scripts/lib/gate-licencias.js +212 -0
  125. package/scripts/lib/git-metricas.js +257 -0
  126. package/scripts/lib/gitignore-manifest.js +29 -1
  127. package/scripts/lib/metricas-dora.js +204 -0
  128. package/scripts/lib/plan-lock.js +275 -0
  129. package/scripts/migrar-fase-dominio.js +0 -1
  130. package/scripts/tui/ejecutores.js +1 -1
  131. package/scripts/validar-manifest.js +92 -1
  132. package/scripts/verificar-evolucion.js +54 -4
  133. package/scripts/verificar-release.js +102 -0
  134. package/scripts/verificar-trazabilidad.js +298 -0
  135. package/agentes/ux-disenador-swl.md +0 -503
  136. package/comandos/swl/dashboard.md +0 -146
  137. package/comandos/swl/evolucion-estado.md +0 -191
  138. package/comandos/swl/metricas.md +0 -376
  139. package/comandos/swl/salud.md +0 -481
  140. package/reglas/arquitectura.evolved.json +0 -7
  141. package/reglas/seguridad.evolved.json +0 -7
  142. package/reglas/verificar-citas-temporales.md +0 -139
@@ -64,6 +64,7 @@ const EXCLUDED_FILENAME_PATTERNS = [
64
64
  /\.rej$/, // patch reject
65
65
  /\.merge_file_/, // merge tools (kdiff3, etc.)
66
66
  /~$/, // editores tipo Emacs/Vim
67
+ /\.evolved-diff\.(md|txt)$/, // diffs de merge (no son componentes; .md legacy)
67
68
  ];
68
69
 
69
70
  /**
@@ -407,7 +408,13 @@ const DIFF_NOISY_THRESHOLD = 50;
407
408
  *
408
409
  * Estrategia: toma el archivo nuevo como base y re-aplica los campos de
409
410
  * evolución (frontmatter evolved-*). Las mutaciones de contenido se preservan
410
- * generando un archivo .evolved-diff.md que Claude puede re-aplicar.
411
+ * generando un archivo `.evolved-diff.txt` que Claude puede re-aplicar.
412
+ *
413
+ * Extensión `.txt` (no `.md`) deliberada: el diff vive junto al componente
414
+ * evolucionado (incluyendo `commands/`), pero el harness de Claude Code indexa
415
+ * todo `.md` dentro de `commands/` como slash-command — un `aprender.evolved-diff.md`
416
+ * aparecería como `/swl:aprender.evolved-diff`. Con `.txt` el harness no lo indexa
417
+ * y `scanEvolved` (que solo recorre `.md`) tampoco lo confunde con un componente.
411
418
  *
412
419
  * Comparación: solo el body (post-frontmatter) se compara línea-a-línea.
413
420
  * El frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
@@ -415,7 +422,8 @@ const DIFF_NOISY_THRESHOLD = 50;
415
422
  * contarlo como mutación genera ruido por desplazamiento.
416
423
  *
417
424
  * Limpieza: cuando un merge posterior elimina la divergencia (diffs vacíos),
418
- * borra el `.evolved-diff.md` huérfano de sesiones previas si existe.
425
+ * borra el `.evolved-diff.txt` huérfano de sesiones previas si existe (y el
426
+ * `.evolved-diff.md` legacy de versiones anteriores a esta corrección).
419
427
  *
420
428
  * Cap defensivo: si tras alinear correctamente el body aún hay más de
421
429
  * `DIFF_NOISY_THRESHOLD` líneas distintas, genera un resumen estadístico
@@ -470,7 +478,17 @@ function mergeEvolved(destino, origen, versionNueva) {
470
478
  }
471
479
  }
472
480
 
473
- const diffPath = destino.replace(/\.md$/, '.evolved-diff.md');
481
+ const diffPath = destino.replace(/\.md$/, '.evolved-diff.txt');
482
+ // Legacy: versiones previas escribían el diff como `.evolved-diff.md`, que
483
+ // el harness indexaba como slash-command. Se limpia siempre que se toca el
484
+ // componente, exista o no divergencia nueva.
485
+ const diffPathLegacy = destino.replace(/\.md$/, '.evolved-diff.md');
486
+ const limpiarLegacy = () => {
487
+ if (fs.existsSync(diffPathLegacy)) {
488
+ try { fs.unlinkSync(diffPathLegacy); return true; } catch { /* best-effort */ }
489
+ }
490
+ return false;
491
+ };
474
492
 
475
493
  if (diffs.length === 0) {
476
494
  // Sin diferencias reales — limpiar diff huérfano si existe (de sesión
@@ -486,6 +504,7 @@ function mergeEvolved(destino, origen, versionNueva) {
486
504
  // el merge sigue siendo válido.
487
505
  }
488
506
  }
507
+ if (limpiarLegacy()) cleanedDiff = true;
489
508
 
490
509
  // force: true — `mergeEvolved` solo se invoca en contexto de update
491
510
  // intencional. El skip de isPackageRoot() aplica a la primera marca
@@ -557,6 +576,8 @@ function mergeEvolved(destino, origen, versionNueva) {
557
576
  ].join('\n');
558
577
 
559
578
  atomicWriteSync(diffPath, diffContent, 'utf8');
579
+ // Si existía el `.evolved-diff.md` legacy, eliminarlo: el `.txt` lo reemplaza.
580
+ limpiarLegacy();
560
581
 
561
582
  return { merged: true, diffPath, diffsCount: diffs.length, truncated };
562
583
  } catch (err) {
@@ -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
+ });