@saulwade/swl-ses 2.4.3 → 2.5.2

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 (200) hide show
  1. package/CLAUDE.md +194 -241
  2. package/README.md +600 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/abogado-diablo-swl.md +145 -0
  6. package/agentes/accesibilidad-wcag-swl.md +690 -690
  7. package/agentes/arquitecto-swl.md +267 -267
  8. package/agentes/auto-evolucion-swl.md +908 -908
  9. package/agentes/backend-api-swl.md +1 -1
  10. package/agentes/backend-csharp-swl.md +420 -420
  11. package/agentes/backend-go-swl.md +390 -390
  12. package/agentes/backend-java-swl.md +281 -281
  13. package/agentes/backend-node-swl.md +1 -1
  14. package/agentes/backend-python-swl.md +1 -1
  15. package/agentes/backend-rust-swl.md +364 -364
  16. package/agentes/backend-workers-swl.md +482 -482
  17. package/agentes/cloud-infra-swl.md +509 -509
  18. package/agentes/consolidador-swl.md +541 -541
  19. package/agentes/datos-swl.md +1 -1
  20. package/agentes/depurador-swl.md +352 -352
  21. package/agentes/devops-ci-swl.md +400 -400
  22. package/agentes/disenador-ui-swl.md +569 -569
  23. package/agentes/documentador-swl.md +345 -345
  24. package/agentes/frontend-angular-swl.md +621 -621
  25. package/agentes/frontend-css-swl.md +716 -716
  26. package/agentes/frontend-react-swl.md +692 -692
  27. package/agentes/frontend-swl.md +496 -496
  28. package/agentes/frontend-tailwind-swl.md +826 -826
  29. package/agentes/gh-fix-ci-swl.md +6 -1
  30. package/agentes/implementador-swl.md +1 -1
  31. package/agentes/investigador-swl.md +432 -432
  32. package/agentes/investigador-ux-swl.md +505 -505
  33. package/agentes/llm-apps-swl.md +1 -1
  34. package/agentes/migrador-swl.md +442 -442
  35. package/agentes/mobile-android-swl.md +511 -511
  36. package/agentes/mobile-cross-swl.md +541 -541
  37. package/agentes/mobile-ios-swl.md +502 -502
  38. package/agentes/mobile-testing-swl.md +302 -302
  39. package/agentes/nemesis-auditor-swl.md +285 -285
  40. package/agentes/notificador-swl.md +1 -1
  41. package/agentes/observabilidad-swl.md +438 -438
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +10 -1
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +1 -1
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +1 -1
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/bin/lib/bot-comandos.js +1 -1
  66. package/bin/swl-ses.js +6 -0
  67. package/comandos/swl/adoptar-proyecto.md +14 -2
  68. package/comandos/swl/configurar-ci.md +8 -1
  69. package/comandos/swl/deuda-codigo.md +97 -97
  70. package/comandos/swl/discutir-fase.md +22 -118
  71. package/comandos/swl/fix.md +118 -0
  72. package/comandos/swl/nuevo-proyecto.md +54 -3
  73. package/comandos/swl/predecir.md +32 -2
  74. package/comandos/swl/seguridad.md +189 -0
  75. package/comandos/swl/status.md +5 -3
  76. package/habilidades/aprendizaje-continuo/SKILL.md +3 -1
  77. package/habilidades/discutir-fase/SKILL.md +84 -81
  78. package/habilidades/discutir-fase/recursos/plantilla-contexto.md +136 -0
  79. package/habilidades/doc-sync/SKILL.md +3 -1
  80. package/habilidades/doubt-driven-review/SKILL.md +15 -1
  81. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  82. package/habilidades/estructura-proyecto-claude/SKILL.md +11 -2
  83. package/habilidades/harness-claude-code/SKILL.md +3 -1
  84. package/habilidades/instalar-sistema/SKILL.md +3 -1
  85. package/habilidades/meta-reglas-extendido/SKILL.md +92 -0
  86. package/habilidades/meta-reglas-extendido/recursos/analisis-previo-tareas-grandes.md +186 -0
  87. package/habilidades/meta-reglas-extendido/recursos/analizar-directorios-antes-de-escribir.md +235 -0
  88. package/habilidades/meta-reglas-extendido/recursos/api-diseno.md +413 -0
  89. package/habilidades/meta-reglas-extendido/recursos/arquitectura.md +491 -0
  90. package/habilidades/meta-reglas-extendido/recursos/arreglar-al-detectar.md +264 -0
  91. package/habilidades/meta-reglas-extendido/recursos/debatir-antes-de-aceptar.md +152 -0
  92. package/habilidades/meta-reglas-extendido/recursos/git-workflow.md +259 -0
  93. package/habilidades/meta-reglas-extendido/recursos/gobernanza.md +291 -0
  94. package/habilidades/meta-reglas-extendido/recursos/memoria-consolidada.md +263 -0
  95. package/habilidades/meta-reglas-extendido/recursos/seguridad-agentes.md +443 -0
  96. package/habilidades/meta-reglas-extendido/recursos/sesiones-paralelas.md +190 -0
  97. package/habilidades/meta-reglas-extendido/recursos/sin-duplicacion-reglas-globales.md +179 -0
  98. package/habilidades/meta-reglas-extendido/recursos/skills-estandar.md +394 -0
  99. package/habilidades/meta-reglas-extendido/recursos/usar-code-review-graph.md +156 -0
  100. package/habilidades/meta-reglas-extendido/recursos/usar-context7.md +236 -0
  101. package/habilidades/meta-reglas-extendido/recursos/usar-sistema-swl.md +253 -0
  102. package/habilidades/meta-reglas-extendido/recursos/verificar-citas-normativas.md +527 -0
  103. package/habilidades/meta-skills-estandar/SKILL.md +3 -1
  104. package/habilidades/nuevo-proyecto/SKILL.md +20 -3
  105. package/habilidades/php-experto/SKILL.md +10 -3
  106. package/habilidades/{filament-admin/SKILL.md → php-experto/recursos/filament-admin.md} +23 -39
  107. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  108. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  109. package/habilidades/proceso-debate-adversarial/recursos/personas.md +5 -4
  110. package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -0
  111. package/hooks/check-update.js +19 -10
  112. package/hooks/contexto-subagente.js +68 -68
  113. package/hooks/degradacion-instintos.js +1 -1
  114. package/hooks/extraccion-aprendizajes.js +2 -2
  115. package/hooks/lib/briefing.js +3 -3
  116. package/hooks/lib/nudge-tracker.js +1 -1
  117. package/hooks/lib/otlp-exporter.js +1 -1
  118. package/hooks/lib/webhook-dedup.js +1 -1
  119. package/hooks/session-briefing.js +1 -1
  120. package/llms.txt +6 -6
  121. package/manifiestos/canonical-hashes.json +1043 -52
  122. package/manifiestos/hooks-config.json +469 -469
  123. package/manifiestos/invariantes-criticos.json +30 -30
  124. package/manifiestos/modulos.json +168 -135
  125. package/manifiestos/perfiles.json +0 -2
  126. package/manifiestos/skills-lock.json +49 -56
  127. package/package.json +7 -5
  128. package/plantillas/github-workflows/README.md +15 -1
  129. package/plantillas/github-workflows/swl-devsecops.yml +70 -0
  130. package/plugin.json +5 -5
  131. package/reglas/analisis-previo-tareas-grandes.md +30 -156
  132. package/reglas/analizar-directorios-antes-de-escribir.md +30 -211
  133. package/reglas/api-diseno.md +28 -398
  134. package/reglas/arquitectura.md +35 -456
  135. package/reglas/arreglar-al-detectar.md +30 -230
  136. package/reglas/debatir-antes-de-aceptar.md +30 -143
  137. package/reglas/docs.md +7 -0
  138. package/reglas/estilo-codigo.md +9 -0
  139. package/reglas/fragmentos-compartidos.md +6 -0
  140. package/reglas/git-workflow.md +44 -240
  141. package/reglas/gobernanza.md +23 -262
  142. package/reglas/memoria-consolidada.md +34 -228
  143. package/reglas/performance.md +8 -0
  144. package/reglas/pruebas.md +12 -0
  145. package/reglas/seguridad-agentes.md +37 -418
  146. package/reglas/seguridad.md +12 -0
  147. package/reglas/sesiones-paralelas.md +29 -162
  148. package/reglas/sin-duplicacion-reglas-globales.md +25 -166
  149. package/reglas/skills-estandar.md +23 -373
  150. package/reglas/usar-code-review-graph.md +31 -140
  151. package/reglas/usar-context7.md +30 -208
  152. package/reglas/usar-sistema-swl.md +47 -242
  153. package/reglas/verificar-citas-normativas.md +47 -537
  154. package/scripts/actualizar.js +253 -253
  155. package/scripts/audit-tools/auditar-relleno-inventario.js +145 -0
  156. package/scripts/auditar-clases-conocidas.js +106 -0
  157. package/scripts/bootstrap-instintos.js +2 -2
  158. package/scripts/canario-hooks.js +166 -0
  159. package/scripts/cli/configurar-ci.js +2 -1
  160. package/scripts/evidencia-valor.js +101 -0
  161. package/scripts/field-report.js +18 -2
  162. package/scripts/generar-comandos.js +143 -0
  163. package/scripts/generar-inventario.js +236 -23
  164. package/scripts/generar-matriz-lenguajes.js +1 -1
  165. package/scripts/instalador.js +15 -1
  166. package/scripts/instalar-git-hook.js +8 -1
  167. package/scripts/lib/configurar-ci.js +10 -3
  168. package/scripts/lib/diary-entry.js +3 -1
  169. package/scripts/lib/drift-detector.js +1 -1
  170. package/scripts/lib/evidencia-valor.js +228 -0
  171. package/scripts/lib/expandir-targets.js +71 -71
  172. package/scripts/lib/frontmatter-md.js +63 -0
  173. package/scripts/lib/parsear-opciones.js +2 -0
  174. package/scripts/lib/prune-componentes.js +180 -0
  175. package/scripts/lib/reglas-globales-conocidas.json +16 -2
  176. package/scripts/lib/scoring-instintos.js +2 -2
  177. package/scripts/lib/toml-merge.js +204 -204
  178. package/scripts/lib/transformadores/claude.js +1 -1
  179. package/scripts/lib/transformadores/codex.js +1 -1
  180. package/scripts/lib/transformadores/copilot.js +1 -1
  181. package/scripts/lib/transformadores/cursor.js +1 -1
  182. package/scripts/lib/transformadores/gemini.js +22 -2
  183. package/scripts/lib/transformadores/opencode.js +1 -1
  184. package/scripts/mcp-server/auth.js +105 -105
  185. package/scripts/mcp-server/cache.js +106 -106
  186. package/scripts/prune.js +102 -0
  187. package/scripts/publicar.js +18 -2
  188. package/scripts/tui/index.js +10 -1
  189. package/scripts/tui/pantallas/inspect.js +175 -175
  190. package/scripts/tui/pantallas/install-wizard.js +21 -8
  191. package/scripts/tui/pantallas/uninstall-wizard.js +210 -210
  192. package/scripts/tui/pantallas/update-wizard.js +234 -234
  193. package/scripts/tui/pantallas/welcome.js +188 -189
  194. package/habilidades/paid-media-tracking/SKILL.md +0 -269
  195. package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +0 -220
  196. package/habilidades/paid-media-tracking/recursos/google-ads-api.md +0 -215
  197. package/habilidades/tracking-measurement/SKILL.md +0 -239
  198. package/habilidades/tracking-measurement/recursos/consent-mode.md +0 -231
  199. package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +0 -216
  200. package/habilidades/tracking-measurement/recursos/meta-capi.md +0 -262
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * evidencia-valor.js — Evidencia de valor del harness EN CAMPO (downstream).
5
+ *
6
+ * En un proyecto real no hay A/B (nadie desarrolla la misma feature dos
7
+ * veces); la evidencia creíble son HECHOS producidos por actores
8
+ * independientes del sistema, nunca scores autodeclarados (regla
9
+ * anti-gaming de seguridad-agentes). Cuatro fuentes, cada una con
10
+ * procedencia explícita:
11
+ *
12
+ * 1. Intercepciones de gates — eventos durables de `.planning/evolution/
13
+ * nudges.jsonl` (hook disparó una intervención). Evidencia directa:
14
+ * cada bloqueo/alerta con causa real es un defecto prevenido contable.
15
+ * 2. Presión de defectos — git: ratio commits fix/feat en la ventana
16
+ * + fixes POSTERIORES a la última verificación de fase (escape rate
17
+ * aproximado — la métrica que CASTIGA al harness). Proxy honesto.
18
+ * 3. DORA-lite + CI — reusa scripts/lib/metricas-dora.js
19
+ * (deployment frequency, lead time, change failure rate, MTTR vía gh).
20
+ * Producido por git/GitHub, no por el sistema.
21
+ * 4. Uso del sistema — .planning/skill-metrics.json. Solo
22
+ * contexto (correlación, jamás causalidad).
23
+ *
24
+ * FRONTERA DE DATOS (documentada en docs/evidencia-valor.md): TODO se lee
25
+ * LOCAL, del `.planning/` y el git del propio proyecto downstream donde
26
+ * corre el comando. El repo madre NUNCA lee estos datos; el único canal
27
+ * hacia el autor es el field-report opt-in con agregados anónimos.
28
+ *
29
+ * Zero-deps. Funciones puras respecto a raíz/inyecciones para testeo.
30
+ */
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const { execFileSync } = require('child_process');
35
+
36
+ const DIA_MS = 24 * 60 * 60 * 1000;
37
+
38
+ /** Tipos de nudge que constituyen una intervención del sistema (evidencia). */
39
+ const TIPOS_INTERVENCION = new Set([
40
+ 'bloqueo', 'bloqueo-hook', 'secreto-detectado', 'calidad-pre-commit',
41
+ 'drift-detectado', 'recovery-activado', 'riesgo-alto', 'proteccion-rutas',
42
+ 'prompt-injection', 'veto', 'gate-fallido',
43
+ ]);
44
+
45
+ // ─── 1. Intercepciones (nudges.jsonl) ────────────────────────────────────────
46
+
47
+ /**
48
+ * Diagnóstico de POR QUÉ no hay telemetría: los hooks solo se registran a
49
+ * nivel PROYECTO (ADR-0009 — un install --global copia los archivos pero NO
50
+ * los registra en ~/.claude/settings.json, para no correr 49 hooks en cada
51
+ * sesión de cualquier proyecto). Sin registro local, nudges.jsonl y
52
+ * skill-metrics.json jamás aparecerán — el panel debe decir CÓMO activarlo,
53
+ * no solo "no disponible" (reporte de campo SIGM, 2026-07-09).
54
+ */
55
+ function hooksRegistradosEnProyecto(raiz) {
56
+ try {
57
+ const s = JSON.parse(fs.readFileSync(path.join(raiz, '.claude', 'settings.json'), 'utf8'));
58
+ return Object.values(s.hooks || {}).some(m => Array.isArray(m) && m.length > 0);
59
+ } catch { return false; }
60
+ }
61
+
62
+ const HINT_ACTIVACION = 'hooks sin registrar a nivel proyecto (el install --global no los registra por diseño, ADR-0009) — activar con: npx -y @saulwade/swl-ses@latest install --target claude --perfil core (SIN --global, en la raíz del proyecto)';
63
+
64
+ function leerNudges(raiz, { ventanaDias = 30, hoy = new Date() } = {}) {
65
+ const ruta = path.join(raiz, '.planning', 'evolution', 'nudges.jsonl');
66
+ if (!fs.existsSync(ruta)) {
67
+ const razon = hooksRegistradosEnProyecto(raiz)
68
+ ? 'sin nudges.jsonl aún (hooks registrados; se acumula con el uso de sesiones)'
69
+ : HINT_ACTIVACION;
70
+ return { disponible: false, razon, eventos: [] };
71
+ }
72
+ const corte = hoy.getTime() - ventanaDias * DIA_MS;
73
+ const eventos = [];
74
+ for (const linea of fs.readFileSync(ruta, 'utf8').split(/\r?\n/)) {
75
+ if (!linea.trim()) continue;
76
+ try {
77
+ const e = JSON.parse(linea);
78
+ const t = Date.parse(e.timestamp || e.ts || '');
79
+ if (!Number.isNaN(t) && t >= corte) eventos.push(e);
80
+ } catch { /* línea corrupta: se omite, no revienta el panel */ }
81
+ }
82
+ return { disponible: true, eventos };
83
+ }
84
+
85
+ function intercepciones(raiz, opts = {}) {
86
+ const { disponible, razon, eventos } = leerNudges(raiz, opts);
87
+ if (!disponible) return { disponible, razon, procedencia: 'hook-event (nudges.jsonl)', total: 0, porTipo: {} };
88
+ const porTipo = {};
89
+ let intervenciones = 0;
90
+ for (const e of eventos) {
91
+ const tipo = String(e.tipo || 'desconocido');
92
+ porTipo[tipo] = (porTipo[tipo] || 0) + 1;
93
+ if (TIPOS_INTERVENCION.has(tipo) || e.blocking === true || e.estado === 'critico') intervenciones++;
94
+ }
95
+ return {
96
+ disponible: true,
97
+ procedencia: 'hook-event (nudges.jsonl)',
98
+ caracter: 'evidencia',
99
+ totalEventos: eventos.length,
100
+ intervenciones,
101
+ porTipo,
102
+ };
103
+ }
104
+
105
+ // ─── 2. Presión de defectos (git) ────────────────────────────────────────────
106
+
107
+ function ejecutarGit(raiz, args) {
108
+ return execFileSync('git', args, { cwd: raiz, encoding: 'utf8' });
109
+ }
110
+
111
+ function presionDefectos(raiz, { ventanaDias = 30, hoy = new Date(), git = ejecutarGit } = {}) {
112
+ let sujetos;
113
+ try {
114
+ const desde = new Date(hoy.getTime() - ventanaDias * DIA_MS).toISOString();
115
+ sujetos = git(raiz, ['log', `--since=${desde}`, '--pretty=format:%ad|%s', '--date=iso-strict'])
116
+ .split(/\r?\n/).filter(Boolean);
117
+ } catch (err) {
118
+ return { disponible: false, razon: `git no disponible: ${err.message.split(/\r?\n/)[0]}` };
119
+ }
120
+ const esFix = s => /^(fix|hotfix)(\(|!|:)/.test(s);
121
+ const esFeat = s => /^feat(\(|!|:)/.test(s);
122
+ const fixes = sujetos.filter(l => esFix(l.split('|').slice(1).join('|')));
123
+ const feats = sujetos.filter(l => esFeat(l.split('|').slice(1).join('|')));
124
+
125
+ // Escape aproximado: fixes con fecha POSTERIOR a la última verificación de
126
+ // fase (VERIFICACION.md más reciente). Sin fases verificadas → no aplica.
127
+ // La fecha se toma de GIT (autoría del último commit que tocó el archivo),
128
+ // no del mtime del filesystem: tras un clone/checkout todos los archivos
129
+ // comparten el mtime de la operación y el proxy daba falso negativo
130
+ // (reporte de campo SIGM, 2026-07-09). Fallback a mtime fuera de git.
131
+ let escapes = { disponible: false, razon: 'sin VERIFICACION.md en .planning/fases/ (proyecto sin ciclo GSD verificado)' };
132
+ try {
133
+ const dirFases = path.join(raiz, '.planning', 'fases');
134
+ let ultimaVerificacion = 0;
135
+ if (fs.existsSync(dirFases)) {
136
+ for (const fase of fs.readdirSync(dirFases)) {
137
+ const v = path.join(dirFases, fase, 'VERIFICACION.md');
138
+ if (!fs.existsSync(v)) continue;
139
+ let fecha = 0;
140
+ try {
141
+ const rel = path.relative(raiz, v);
142
+ const ct = parseInt(git(raiz, ['log', '-1', '--format=%ct', '--', rel]).trim(), 10);
143
+ if (Number.isFinite(ct) && ct > 0) fecha = ct * 1000;
144
+ } catch { /* fuera de git o archivo sin commits → fallback mtime */ }
145
+ if (fecha === 0) fecha = fs.statSync(v).mtimeMs;
146
+ ultimaVerificacion = Math.max(ultimaVerificacion, fecha);
147
+ }
148
+ }
149
+ if (ultimaVerificacion > 0) {
150
+ const post = fixes.filter(l => {
151
+ const t = Date.parse(l.split('|')[0]);
152
+ return !Number.isNaN(t) && t > ultimaVerificacion;
153
+ });
154
+ escapes = {
155
+ disponible: true,
156
+ fixesTrasUltimaVerificacion: post.length,
157
+ ultimaVerificacion: new Date(ultimaVerificacion).toLocaleDateString('sv'),
158
+ nota: 'lo que el harness NO atrapó — la métrica que lo castiga',
159
+ };
160
+ }
161
+ } catch { /* best-effort */ }
162
+
163
+ return {
164
+ disponible: true,
165
+ procedencia: 'git log (independiente del sistema)',
166
+ caracter: 'proxy',
167
+ commits: sujetos.length,
168
+ fixes: fixes.length,
169
+ feats: feats.length,
170
+ ratioFixFeat: feats.length > 0 ? +(fixes.length / feats.length).toFixed(2) : null,
171
+ escapes,
172
+ };
173
+ }
174
+
175
+ // ─── 3 y 4. DORA/CI y uso (reuso de fuentes existentes) ─────────────────────
176
+
177
+ function doraCi(raiz, { ventanaDias = 30, hoy = new Date() } = {}) {
178
+ try {
179
+ const { calcularDora } = require('./metricas-dora.js');
180
+ const d = calcularDora(raiz, { ventanaDias, hoy });
181
+ return { disponible: true, procedencia: 'git + GitHub Actions (gh)', caracter: 'evidencia (tendencia; correlación, no causalidad)', dora: d };
182
+ } catch (err) {
183
+ return { disponible: false, razon: `metricas-dora no disponible: ${err.message.split(/\r?\n/)[0]}` };
184
+ }
185
+ }
186
+
187
+ function usoSistema(raiz) {
188
+ const ruta = path.join(raiz, '.planning', 'skill-metrics.json');
189
+ if (!fs.existsSync(ruta)) {
190
+ const razon = hooksRegistradosEnProyecto(raiz)
191
+ ? 'sin skill-metrics.json aún (hooks registrados; se acumula con el uso de skills)'
192
+ : HINT_ACTIVACION;
193
+ return { disponible: false, razon };
194
+ }
195
+ try {
196
+ // Forma real: { version, entries: [{skill, estado, timestamp}, ...] }
197
+ // (eventos individuales del hook telemetria-skill-routing) — agregamos aquí.
198
+ const m = JSON.parse(fs.readFileSync(ruta, 'utf8'));
199
+ const eventos = Object.values(m.entries || {});
200
+ const porSkill = {};
201
+ for (const e of eventos) {
202
+ if (!e || !e.skill) continue;
203
+ porSkill[e.skill] = (porSkill[e.skill] || 0) + 1;
204
+ }
205
+ const top = Object.entries(porSkill).sort((a, b) => b[1] - a[1]).slice(0, 5)
206
+ .map(([k, n]) => `${k}:${n}`);
207
+ return { disponible: true, procedencia: 'telemetria-skill-routing', caracter: 'contexto (solo correlación)', invocaciones: eventos.length, top5: top };
208
+ } catch (err) {
209
+ return { disponible: false, razon: `skill-metrics ilegible: ${err.message}` };
210
+ }
211
+ }
212
+
213
+ // ─── Panel ────────────────────────────────────────────────────────────────────
214
+
215
+ function panelEvidencia(raiz = process.cwd(), opts = {}) {
216
+ const ventanaDias = opts.ventanaDias || 30;
217
+ return {
218
+ generado: new Date().toLocaleDateString('sv'),
219
+ ventanaDias,
220
+ frontera: 'Datos 100% locales del proyecto (.planning/ + git). El repo madre no lee nada de aquí (docs/evidencia-valor.md).',
221
+ intercepciones: intercepciones(raiz, { ventanaDias, hoy: opts.hoy }),
222
+ defectos: presionDefectos(raiz, { ventanaDias, hoy: opts.hoy, git: opts.git }),
223
+ doraCi: doraCi(raiz, { ventanaDias, hoy: opts.hoy }),
224
+ uso: usoSistema(raiz),
225
+ };
226
+ }
227
+
228
+ module.exports = { panelEvidencia, intercepciones, presionDefectos, leerNudges, usoSistema, TIPOS_INTERVENCION };
@@ -1,71 +1,71 @@
1
- 'use strict';
2
-
3
- /**
4
- * Expansión de --target (CSV) y --all-runtimes para multi-target install.
5
- *
6
- * ADR-0019 Sub-fase 2.5.
7
- *
8
- * Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
9
- * side effects fuera de logging — solo manipula la lista de strings.
10
- *
11
- * @module scripts/lib/expandir-targets
12
- */
13
-
14
- const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
15
-
16
- /**
17
- * Expande las opciones del CLI a un array de target IDs.
18
- *
19
- * Reglas:
20
- * - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
21
- * aviso (vía `logger.warn`) y se usa `all_runtimes`.
22
- * - `target='a,b,c'` → ['a','b','c'].
23
- * - `target='claude'` → ['claude'].
24
- * - Sin nada → ['claude'] (default histórico, backward-compat).
25
- * - Duplicados se deduplican preservando el primer orden.
26
- * - Targets desconocidos se omiten con aviso (logger.warn).
27
- *
28
- * @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
29
- * @param {object} [logger] - Sustituible para tests. Default console.
30
- * @returns {{ targets: string[], omitidos: string[], errores: string[] }}
31
- */
32
- function expandirTargets(opciones, logger) {
33
- const log = logger || console;
34
- const omitidos = [];
35
- const errores = [];
36
-
37
- let candidatos;
38
- if (opciones.all_runtimes) {
39
- if (opciones.target && typeof opciones.target === 'string') {
40
- log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
41
- }
42
- candidatos = listarRuntimesInstalables();
43
- } else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
44
- candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
45
- } else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
46
- candidatos = [opciones.target.trim()];
47
- } else {
48
- candidatos = ['claude']; // backward-compat
49
- }
50
-
51
- const visto = new Set();
52
- const targets = [];
53
- for (const t of candidatos) {
54
- if (visto.has(t)) continue;
55
- visto.add(t);
56
- if (!RUNTIMES[t]) {
57
- log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
58
- omitidos.push(t);
59
- continue;
60
- }
61
- targets.push(t);
62
- }
63
-
64
- if (targets.length === 0) {
65
- errores.push('Ningún target válido tras expansión.');
66
- }
67
-
68
- return { targets, omitidos, errores };
69
- }
70
-
71
- module.exports = { expandirTargets };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Expansión de --target (CSV) y --all-runtimes para multi-target install.
5
+ *
6
+ * ADR-0019 Sub-fase 2.5.
7
+ *
8
+ * Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
9
+ * side effects fuera de logging — solo manipula la lista de strings.
10
+ *
11
+ * @module scripts/lib/expandir-targets
12
+ */
13
+
14
+ const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
15
+
16
+ /**
17
+ * Expande las opciones del CLI a un array de target IDs.
18
+ *
19
+ * Reglas:
20
+ * - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
21
+ * aviso (vía `logger.warn`) y se usa `all_runtimes`.
22
+ * - `target='a,b,c'` → ['a','b','c'].
23
+ * - `target='claude'` → ['claude'].
24
+ * - Sin nada → ['claude'] (default histórico, backward-compat).
25
+ * - Duplicados se deduplican preservando el primer orden.
26
+ * - Targets desconocidos se omiten con aviso (logger.warn).
27
+ *
28
+ * @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
29
+ * @param {object} [logger] - Sustituible para tests. Default console.
30
+ * @returns {{ targets: string[], omitidos: string[], errores: string[] }}
31
+ */
32
+ function expandirTargets(opciones, logger) {
33
+ const log = logger || console;
34
+ const omitidos = [];
35
+ const errores = [];
36
+
37
+ let candidatos;
38
+ if (opciones.all_runtimes) {
39
+ if (opciones.target && typeof opciones.target === 'string') {
40
+ log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
41
+ }
42
+ candidatos = listarRuntimesInstalables();
43
+ } else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
44
+ candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
45
+ } else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
46
+ candidatos = [opciones.target.trim()];
47
+ } else {
48
+ candidatos = ['claude']; // backward-compat
49
+ }
50
+
51
+ const visto = new Set();
52
+ const targets = [];
53
+ for (const t of candidatos) {
54
+ if (visto.has(t)) continue;
55
+ visto.add(t);
56
+ if (!RUNTIMES[t]) {
57
+ log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
58
+ omitidos.push(t);
59
+ continue;
60
+ }
61
+ targets.push(t);
62
+ }
63
+
64
+ if (targets.length === 0) {
65
+ errores.push('Ningún target válido tras expansión.');
66
+ }
67
+
68
+ return { targets, omitidos, errores };
69
+ }
70
+
71
+ module.exports = { expandirTargets };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * frontmatter-md.js — Parser mínimo zero-deps de frontmatter YAML en archivos .md.
5
+ *
6
+ * Soporta el subconjunto que usan los componentes SWL:
7
+ * - pares `clave: valor` de una línea (comillas opcionales)
8
+ * - escalares plegados (`clave: >`, `|`, `>-`, `|-`) juntando las líneas indentadas
9
+ * - listas inline (`tools: [Read, Write]`) — se devuelven como string; usar
10
+ * `parsearListaInline()` para convertirlas a array
11
+ *
12
+ * NO es un parser YAML completo (sin listas multilínea ni objetos anidados) —
13
+ * suficiente y determinista para frontmatter de agentes/skills/comandos.
14
+ * Consumidores: scripts/generar-inventario.js, scripts/generar-comandos.js.
15
+ */
16
+
17
+ const fs = require('fs');
18
+
19
+ function leerFrontmatter(ruta) {
20
+ try {
21
+ const contenido = fs.readFileSync(ruta, 'utf8');
22
+ const match = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---/);
23
+ if (!match) return {};
24
+ const fm = {};
25
+ const lineas = match[1].split(/\r?\n/);
26
+ for (let i = 0; i < lineas.length; i++) {
27
+ const linea = lineas[i];
28
+ if (/^\s/.test(linea)) continue; // continuación de multilínea
29
+ const idx = linea.indexOf(':');
30
+ if (idx <= 0) continue;
31
+ const key = linea.slice(0, idx).trim();
32
+ let val = linea.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
33
+ if (val === '>' || val === '|' || val === '>-' || val === '|-' || val === '') {
34
+ const partes = [];
35
+ for (let j = i + 1; j < lineas.length && /^\s+\S/.test(lineas[j]); j++) {
36
+ partes.push(lineas[j].trim());
37
+ }
38
+ if (partes.length > 0) val = partes.join(' ');
39
+ }
40
+ fm[key] = val;
41
+ }
42
+ return fm;
43
+ } catch { return {}; }
44
+ }
45
+
46
+ /** `[a, b, c]` → ['a','b','c']; string vacío → []. */
47
+ function parsearListaInline(valor) {
48
+ return String(valor || '')
49
+ .replace(/^\[|\]$/g, '')
50
+ .split(',')
51
+ .map(s => s.trim())
52
+ .filter(Boolean);
53
+ }
54
+
55
+ /** Recorta a n caracteres en límite de palabra, colapsando whitespace y pipes. */
56
+ function resumir(texto, n = 110) {
57
+ const limpio = String(texto || '').replace(/\s+/g, ' ').replace(/\|/g, '/').trim();
58
+ if (limpio.length <= n) return limpio;
59
+ const corte = limpio.lastIndexOf(' ', n);
60
+ return limpio.slice(0, corte > 40 ? corte : n) + '…';
61
+ }
62
+
63
+ module.exports = { leerFrontmatter, parsearListaInline, resumir };
@@ -48,6 +48,8 @@ const BOOLEANAS = [
48
48
  // TUI opt-out (Fase 4 instalador visual). Default: si stdin es TTY y no
49
49
  // hay flags, lanza el TUI. Con --no-tui cae al install-asistido clásico.
50
50
  'no-tui', 'tui',
51
+ // prune (DT-INSTALADOR-PRUNE): --confirmar ejecuta el plan; --json salida machine-readable
52
+ 'confirmar', 'json',
51
53
  ];
52
54
 
53
55
  /**
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * prune-componentes — Purga de componentes retirados en instalaciones SWL.
5
+ *
6
+ * Cierra DT-INSTALADOR-PRUNE: cuando el paquete retira un componente
7
+ * (skill/agente/comando/regla/hook), las instalaciones existentes lo
8
+ * conservaban para siempre. Este módulo compara el `.swl-install-state.json`
9
+ * del destino contra el paquete actual y elimina SOLO lo que cumple las dos
10
+ * condiciones de seguridad:
11
+ *
12
+ * 1. El `origen` registrado ya NO existe en el paquete (componente retirado).
13
+ * 2. El contenido instalado coincide con un hash canónico conocido del
14
+ * componente (manifiestos/canonical-hashes.json, cualquier versión) —
15
+ * es contenido de fábrica sin tocar.
16
+ *
17
+ * Si el contenido difiere de todo hash canónico conocido (evolución del
18
+ * usuario) o el componente no tiene hash registrado (p. ej. recursos/ de un
19
+ * skill), NUNCA se elimina: se reporta como `preservados` para revisión
20
+ * manual. Invariante merge-no-overwrite (ADR-0040) extendida al borrado.
21
+ *
22
+ * API pura respecto de rutas: recibe `rutaBase` (instalación destino) y
23
+ * `raizPaquete` (paquete fuente actual). El caller (scripts/prune.js) resuelve
24
+ * ambos; los tests inyectan sandboxes.
25
+ *
26
+ * @module scripts/lib/prune-componentes
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ const { cargarEstado, guardarEstado } = require('./estado.js');
33
+ const { canonicalHash } = require('./canonical-hash.js');
34
+
35
+ /**
36
+ * Construye el índice rutaRelativa → Set(hashes canónicos de todas las
37
+ * versiones) desde manifiestos/canonical-hashes.json del paquete.
38
+ *
39
+ * @param {string} raizPaquete
40
+ * @returns {Map<string, Set<string>>}
41
+ */
42
+ function cargarHashesConocidos(raizPaquete) {
43
+ const indice = new Map();
44
+ try {
45
+ const ruta = path.join(raizPaquete, 'manifiestos', 'canonical-hashes.json');
46
+ const porVersion = JSON.parse(fs.readFileSync(ruta, 'utf-8'));
47
+ for (const version of Object.keys(porVersion)) {
48
+ const componentes = porVersion[version];
49
+ if (!componentes || typeof componentes !== 'object') continue;
50
+ for (const [rutaRel, hash] of Object.entries(componentes)) {
51
+ const clave = rutaRel.replace(/\\/g, '/');
52
+ if (!indice.has(clave)) indice.set(clave, new Set());
53
+ indice.get(clave).add(String(hash));
54
+ }
55
+ }
56
+ } catch { /* sin manifiesto de hashes → índice vacío → todo se preserva */ }
57
+ return indice;
58
+ }
59
+
60
+ /**
61
+ * Clasifica los archivos instalados contra el paquete actual.
62
+ *
63
+ * @param {object} opciones
64
+ * @param {string} opciones.rutaBase Directorio de la instalación (contiene .swl-install-state.json)
65
+ * @param {string} opciones.raizPaquete Raíz del paquete swl-ses actual
66
+ * @returns {{
67
+ * error?: string,
68
+ * eliminables: Array<{origen: string, destino: string, tipo: string}>,
69
+ * preservados: Array<{origen: string, destino: string, tipo: string, razon: string}>,
70
+ * yaAusentes: Array<{origen: string, destino: string}>,
71
+ * vigentes: number,
72
+ * }}
73
+ */
74
+ function planPrune({ rutaBase, raizPaquete }) {
75
+ const estado = cargarEstado(rutaBase);
76
+ if (!estado) {
77
+ return { error: `No hay .swl-install-state.json en ${rutaBase} — nada que podar.`, eliminables: [], preservados: [], yaAusentes: [], vigentes: 0 };
78
+ }
79
+
80
+ const hashesConocidos = cargarHashesConocidos(raizPaquete);
81
+ const eliminables = [];
82
+ const preservados = [];
83
+ const yaAusentes = [];
84
+ let vigentes = 0;
85
+
86
+ for (const archivo of estado.archivosInstalados || []) {
87
+ const origenRel = String(archivo.origen || '').replace(/\\/g, '/');
88
+ if (!origenRel) continue;
89
+
90
+ if (fs.existsSync(path.join(raizPaquete, origenRel))) {
91
+ vigentes++;
92
+ continue; // el paquete todavía distribuye este componente
93
+ }
94
+
95
+ // Componente retirado del paquete.
96
+ if (!fs.existsSync(archivo.destino)) {
97
+ yaAusentes.push({ origen: origenRel, destino: archivo.destino });
98
+ continue;
99
+ }
100
+
101
+ const hashes = hashesConocidos.get(origenRel);
102
+ if (!hashes || hashes.size === 0) {
103
+ preservados.push({
104
+ origen: origenRel,
105
+ destino: archivo.destino,
106
+ tipo: archivo.tipo || '?',
107
+ razon: 'sin hash canónico registrado — revisar y borrar manualmente si aplica',
108
+ });
109
+ continue;
110
+ }
111
+
112
+ let hashLocal;
113
+ try {
114
+ hashLocal = canonicalHash(fs.readFileSync(archivo.destino, 'utf-8'));
115
+ } catch {
116
+ preservados.push({ origen: origenRel, destino: archivo.destino, tipo: archivo.tipo || '?', razon: 'destino ilegible' });
117
+ continue;
118
+ }
119
+
120
+ if (hashes.has(hashLocal)) {
121
+ eliminables.push({ origen: origenRel, destino: archivo.destino, tipo: archivo.tipo || '?' });
122
+ } else {
123
+ preservados.push({
124
+ origen: origenRel,
125
+ destino: archivo.destino,
126
+ tipo: archivo.tipo || '?',
127
+ razon: 'contenido evolucionado por el usuario (hash ≠ canónico) — merge-no-overwrite',
128
+ });
129
+ }
130
+ }
131
+
132
+ return { eliminables, preservados, yaAusentes, vigentes };
133
+ }
134
+
135
+ /**
136
+ * Ejecuta el plan: borra los eliminables, limpia del estado las entradas
137
+ * eliminadas y las ya ausentes, y persiste el estado actualizado.
138
+ *
139
+ * @param {object} plan Resultado de planPrune
140
+ * @param {object} opciones
141
+ * @param {string} opciones.rutaBase
142
+ * @returns {{eliminados: string[], erroresBorrado: Array<{destino: string, error: string}>, entradasLimpiadas: number}}
143
+ */
144
+ function ejecutarPrune(plan, { rutaBase }) {
145
+ const eliminados = [];
146
+ const erroresBorrado = [];
147
+
148
+ for (const item of plan.eliminables) {
149
+ try {
150
+ fs.unlinkSync(item.destino);
151
+ eliminados.push(item.destino);
152
+ // Si el directorio del skill quedó vacío (p. ej. habilidades/x/), retirarlo.
153
+ const dir = path.dirname(item.destino);
154
+ try {
155
+ if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
156
+ } catch { /* dir compartido o ya ausente — irrelevante */ }
157
+ } catch (err) {
158
+ erroresBorrado.push({ destino: item.destino, error: err.message });
159
+ }
160
+ }
161
+
162
+ // Limpiar el estado: fuera las entradas eliminadas y las ya ausentes.
163
+ const estado = cargarEstado(rutaBase);
164
+ let entradasLimpiadas = 0;
165
+ if (estado) {
166
+ const quitar = new Set([
167
+ ...eliminados,
168
+ ...plan.yaAusentes.map(a => a.destino),
169
+ ]);
170
+ const antes = estado.archivosInstalados.length;
171
+ estado.archivosInstalados = estado.archivosInstalados.filter(a => !quitar.has(a.destino));
172
+ entradasLimpiadas = antes - estado.archivosInstalados.length;
173
+ estado.actualizadoEn = new Date().toISOString();
174
+ guardarEstado(estado, rutaBase);
175
+ }
176
+
177
+ return { eliminados, erroresBorrado, entradasLimpiadas };
178
+ }
179
+
180
+ module.exports = { planPrune, ejecutarPrune, cargarHashesConocidos };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema-version": "1.0.0",
3
3
  "description": "Catálogo de reglas globales ~/.claude/rules/ que NO deben duplicarse inline en CLAUDE.md de proyectos. Consumido por scripts/lib/detector-reglas-duplicadas.js y por scripts/auditar-claudemd.js dimensión 'duplicacion-reglas-globales'. Cada entrada lista: nombre canónico, archivo origen, sección canónica para referenciar, patrones de detección (regex), y reemplazo sugerido.",
4
- "version": "1.1.0",
5
- "actualizado": "2026-05-22",
4
+ "version": "1.2.0",
5
+ "actualizado": "2026-07-08",
6
6
  "reglas": [
7
7
  {
8
8
  "id": "idioma-espanol-mexico",
@@ -168,6 +168,20 @@
168
168
  "min_matches": 1,
169
169
  "scope": "project-level",
170
170
  "remediacion_sugerida": "Eliminar el bloque local. La regla global aplica a cualquier proyecto con vault Obsidian disponible."
171
+ },
172
+ {
173
+ "id": "usar-code-review-graph",
174
+ "regla_global": "usar-code-review-graph.md",
175
+ "seccion_canonica": "Usar code-review-graph antes de Read/Grep/Glob para explorar",
176
+ "referencia_canonica": "@~/.claude/rules/usar-code-review-graph.md",
177
+ "descripcion": "Comportamiento completo del MCP code-review-graph (tabla de herramientas, workflow, excepciones). El CLAUDE.md del proyecto solo declara la disponibilidad del grafo — re-derivar el comportamiento inline es duplicación. Caso real: CLAUDE.md de swl-ses acumuló 37 líneas re-derivándolo en inglés (removido 2026-07-08).",
178
+ "patrones": [
179
+ "ALWAYS use thes+code-review-graph",
180
+ "code-review-graph MCP tools BEFORE",
181
+ "graph tools FIRST",
182
+ "detect_changes.*get_review_context|get_impact_radius.*blast radius"
183
+ ],
184
+ "remediacion_sugerida": "Una línea en Stack: 'MCP del proyecto: code-review-graph disponible vía .mcp.json. Comportamiento: @~/.claude/rules/usar-code-review-graph.md'"
171
185
  }
172
186
  ],
173
187
  "configuracion": {
@@ -203,8 +203,8 @@ function applyFeedback(instinto, type, currentDate) {
203
203
  throw new Error(`Tipo de feedback inválido: ${type}`);
204
204
  }
205
205
  const now = (currentDate instanceof Date)
206
- ? currentDate.toISOString().slice(0, 10)
207
- : (currentDate || new Date().toISOString().slice(0, 10));
206
+ ? currentDate.toLocaleDateString('sv')
207
+ : (currentDate || new Date().toLocaleDateString('sv'));
208
208
 
209
209
  const next = { ...instinto };
210
210
  if (type === 'helpful') next.helpful_count = (next.helpful_count || 0) + 1;