@saulwade/swl-ses 1.3.3 → 1.3.5

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 (102) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-ses.js +4 -62
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +207 -207
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  9. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  10. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  11. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  12. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  13. package/habilidades/eval-framework/SKILL.md +212 -212
  14. package/habilidades/extractor-de-aprendizajes/SKILL.md +321 -321
  15. package/habilidades/harness-claude-code/SKILL.md +299 -299
  16. package/habilidades/infra-github-actions/SKILL.md +166 -166
  17. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  18. package/habilidades/manejo-errores/.evolved.json +8 -8
  19. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  20. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  21. package/habilidades/patrones-python/SKILL.md +229 -229
  22. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  23. package/habilidades/planear-fase/SKILL.md +319 -319
  24. package/habilidades/release-semver/.evolved.json +8 -8
  25. package/habilidades/swl-claudemd/SKILL.md +220 -220
  26. package/habilidades/testing-python/SKILL.md +340 -340
  27. package/hooks/claudemd-bloat-detector.js +161 -161
  28. package/hooks/extraccion-aprendizajes.js +43 -12
  29. package/hooks/lib/agent-routing.js +107 -107
  30. package/hooks/lib/auto-consolidator.js +335 -335
  31. package/hooks/lib/error-classifier.js +308 -308
  32. package/hooks/lib/merkle-audit.js +96 -96
  33. package/hooks/lib/provenance-tracker.js +191 -191
  34. package/hooks/lib/rate-limit-tracker.js +253 -253
  35. package/hooks/lib/resource-quota.js +122 -122
  36. package/hooks/lib/retry-jitter.js +165 -165
  37. package/hooks/lib/skill-auditor.js +588 -588
  38. package/hooks/lib/sync-status.js +228 -228
  39. package/hooks/lib/taint-tracker.js +107 -107
  40. package/hooks/lib/text-similarity.js +241 -241
  41. package/hooks/lib/toon-compressor.js +245 -245
  42. package/hooks/registro-turnos.js +209 -209
  43. package/hooks/sugerir-regenerar-inventario.js +170 -170
  44. package/hooks/validar-formato-post-subagente.js +140 -140
  45. package/hooks/validar-memoria-hook.js +218 -218
  46. package/instintos/prompt-appendices.yaml +57 -57
  47. package/manifiestos/agent-output-schemas.json +57 -57
  48. package/manifiestos/skills-lock.json +27 -27
  49. package/package.json +1 -1
  50. package/plantillas/auditor-veto-template.md +105 -105
  51. package/plantillas/github-workflows/README.md +47 -47
  52. package/plantillas/github-workflows/release-please.yml +44 -44
  53. package/plantillas/github-workflows/swl-ci.yml +107 -107
  54. package/plantillas/github-workflows/swl-security.yml +51 -51
  55. package/plugin.json +1 -1
  56. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  57. package/reglas/arreglar-al-detectar.md +147 -147
  58. package/reglas/fragmentos-compartidos.md +152 -152
  59. package/reglas/harness-claude-code.md +213 -213
  60. package/reglas/usar-context7.md +226 -226
  61. package/schemas/diary-entry.schema.json +80 -80
  62. package/scripts/benchmark-memoria.js +167 -167
  63. package/scripts/configurar-branch-protection.js +418 -418
  64. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  65. package/scripts/doctor.js +77 -3
  66. package/scripts/field-report.js +199 -199
  67. package/scripts/generar-checklists-consolidados.js +273 -273
  68. package/scripts/generar-inventario.js +420 -420
  69. package/scripts/generar-matriz-lenguajes.js +271 -271
  70. package/scripts/instalador.js +38 -1
  71. package/scripts/lib/artefactos-python.js +43 -43
  72. package/scripts/lib/benchmark-metrics.js +160 -160
  73. package/scripts/lib/budget-enforcer.js +252 -252
  74. package/scripts/lib/configurar-ci.js +380 -380
  75. package/scripts/lib/contadores-inventario.js +217 -217
  76. package/scripts/lib/detectar-stack-detallado.js +307 -307
  77. package/scripts/lib/diary-entry.js +234 -234
  78. package/scripts/lib/eval-metrics-store.js +218 -218
  79. package/scripts/lib/eval-quality.js +171 -171
  80. package/scripts/lib/eval-schemas.js +144 -144
  81. package/scripts/lib/eval-self-correct.js +106 -106
  82. package/scripts/lib/eval-validator.js +185 -185
  83. package/scripts/lib/jaccard-similarity.js +98 -98
  84. package/scripts/lib/longmemeval-runner.js +125 -125
  85. package/scripts/lib/npm-version.js +261 -261
  86. package/scripts/lib/paquetes-conocidos.js +50 -50
  87. package/scripts/lib/parsear-opciones.js +136 -0
  88. package/scripts/lib/prompt-builder.js +264 -264
  89. package/scripts/lib/rrf-fusion.js +175 -175
  90. package/scripts/lib/scoring-instintos.js +277 -277
  91. package/scripts/lib/semantic-search.js +252 -252
  92. package/scripts/lib/transformadores/claude.js +200 -200
  93. package/scripts/limpiar-artefactos-python.js +131 -131
  94. package/scripts/mcp-server/README.md +128 -128
  95. package/scripts/mcp-server/handlers.js +206 -206
  96. package/scripts/migrar-csv-a-array.js +168 -168
  97. package/scripts/migrar-fase-dominio.js +201 -201
  98. package/scripts/publicar.js +511 -511
  99. package/scripts/run-eval.js +141 -141
  100. package/scripts/validar-manifest.js +195 -195
  101. package/scripts/validar-userland-vacio.js +110 -110
  102. package/scripts/verificar-release.js +5 -1
@@ -1,161 +1,161 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: claudemd-bloat-detector.js
6
- * Tipo: PostToolUse (aplica a: Write, Edit, MultiEdit)
7
- *
8
- * Ejecuta `scripts/auditar-claudemd.js` contra archivos `CLAUDE.md`
9
- * recién modificados y emite un nudge a `.planning/evolucion/nudges.jsonl`
10
- * si el veredicto es WARN o ERROR.
11
- *
12
- * Aplica ADR-0016 (best practices Anthropic "The CLAUDE.md file"):
13
- * detecta inflación (líneas excesivas, bullets monolíticos, secciones
14
- * canónicas ausentes, ausencia de @references) y sugiere intervención
15
- * con `/swl:claudemd refactor`.
16
- *
17
- * Opt-out: SWL_CLAUDEMD_BLOAT=0 desactiva completamente el hook.
18
- *
19
- * Comportamiento:
20
- * - Nunca bloquea operaciones (exit code 0 siempre)
21
- * - Solo emite nudge cuando veredicto != OK — ruido mínimo
22
- * - Solo se dispara con archivos cuyo basename sea exactamente CLAUDE.md
23
- * - Respeta exclusiones: temp/, node_modules/, respositorios-git/
24
- *
25
- * Formato del nudge:
26
- * {
27
- * id: string,
28
- * kind: "claudemd-bloat",
29
- * target: "documentador-swl",
30
- * source: "hooks/claudemd-bloat-detector.js",
31
- * message: "...",
32
- * data: { archivo, veredicto, lineas, hallazgos_count },
33
- * ts: ISO,
34
- * accionado: false
35
- * }
36
- */
37
-
38
- const fs = require('fs');
39
- const path = require('path');
40
- const crypto = require('crypto');
41
-
42
- // ─── Opt-out global ───────────────────────────────────────────────────────
43
- if (process.env.SWL_CLAUDEMD_BLOAT === '0') {
44
- process.exit(0);
45
- }
46
-
47
- let hookInput = '';
48
- try {
49
- hookInput = fs.readFileSync(0, 'utf-8');
50
- } catch (_) {
51
- process.exit(0);
52
- }
53
-
54
- let evento;
55
- try {
56
- evento = JSON.parse(hookInput);
57
- } catch (_) {
58
- process.exit(0);
59
- }
60
-
61
- const toolName = evento?.tool_name;
62
- const toolInput = evento?.tool_input;
63
-
64
- if (!toolName || !['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
65
- process.exit(0);
66
- }
67
-
68
- const filePath = toolInput?.file_path;
69
- if (!filePath) {
70
- process.exit(0);
71
- }
72
-
73
- // Solo CLAUDE.md (basename exacto, case-sensitive)
74
- const basename = path.basename(filePath);
75
- if (basename !== 'CLAUDE.md') {
76
- process.exit(0);
77
- }
78
-
79
- const pathNormalized = filePath.replace(/\\/g, '/');
80
- const RUTAS_EXCLUIDAS = [
81
- '/temp/',
82
- '/node_modules/',
83
- '/respositorios-git/',
84
- '/.planning/',
85
- ];
86
- if (RUTAS_EXCLUIDAS.some((excluida) => pathNormalized.includes(excluida))) {
87
- process.exit(0);
88
- }
89
-
90
- // El archivo debe existir
91
- if (!fs.existsSync(filePath)) {
92
- process.exit(0);
93
- }
94
-
95
- // ─── Ejecutar auditor (módulo, no subproceso) ─────────────────────────────
96
- const CWD = process.cwd();
97
- const auditorPath = path.join(CWD, 'scripts', 'auditar-claudemd.js');
98
- if (!fs.existsSync(auditorPath)) {
99
- // No hay auditor instalado en este destino; salir silenciosamente
100
- process.exit(0);
101
- }
102
-
103
- let resultado;
104
- try {
105
- const { auditar } = require(auditorPath);
106
- resultado = auditar(filePath);
107
- } catch (_) {
108
- // Cualquier error del auditor: salir silenciosamente, no romper el hook
109
- process.exit(0);
110
- }
111
-
112
- // Solo emitir nudge si veredicto != OK
113
- if (!resultado || resultado.veredicto === 'OK') {
114
- process.exit(0);
115
- }
116
-
117
- // ─── Construir nudge ──────────────────────────────────────────────────────
118
- const rutaRelativa = path.relative(CWD, filePath).replace(/\\/g, '/');
119
- const topHallazgos = (resultado.hallazgos || [])
120
- .slice(0, 3)
121
- .map((h) => ` - [${h.severidad}] ${h.mensaje}`)
122
- .join('\n');
123
-
124
- const nudge = {
125
- id: crypto.randomBytes(8).toString('hex'),
126
- kind: 'claudemd-bloat',
127
- target: 'documentador-swl',
128
- source: 'hooks/claudemd-bloat-detector.js',
129
- message:
130
- `[claudemd] ${rutaRelativa} veredicto: ${resultado.veredicto} ` +
131
- `(${resultado.hallazgos.length} hallazgos)\n` +
132
- topHallazgos + '\n' +
133
- ` Ejecutar \`/swl:claudemd audit\` para detalle, ` +
134
- `\`/swl:claudemd refactor\` para sugerencias de extracción.`,
135
- data: {
136
- archivo: rutaRelativa,
137
- veredicto: resultado.veredicto,
138
- lineas: resultado.metricas?.lineas,
139
- secciones_ausentes: resultado.metricas?.secciones_ausentes || [],
140
- tiene_at_references: resultado.metricas?.tiene_at_references,
141
- hallazgos_count: resultado.hallazgos.length,
142
- },
143
- ts: new Date().toISOString(),
144
- accionado: false,
145
- accionado_ts: null,
146
- accionado_por: null,
147
- };
148
-
149
- // ─── Persistir a nudges.jsonl ─────────────────────────────────────────────
150
- try {
151
- const nudgesPath = path.join(CWD, '.planning', 'evolucion', 'nudges.jsonl');
152
- const nudgesDir = path.dirname(nudgesPath);
153
- if (!fs.existsSync(nudgesDir)) {
154
- fs.mkdirSync(nudgesDir, { recursive: true });
155
- }
156
- fs.appendFileSync(nudgesPath, JSON.stringify(nudge) + '\n', 'utf-8');
157
- } catch (_) {
158
- // No fallar el hook por error de escritura
159
- }
160
-
161
- process.exit(0);
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: claudemd-bloat-detector.js
6
+ * Tipo: PostToolUse (aplica a: Write, Edit, MultiEdit)
7
+ *
8
+ * Ejecuta `scripts/auditar-claudemd.js` contra archivos `CLAUDE.md`
9
+ * recién modificados y emite un nudge a `.planning/evolucion/nudges.jsonl`
10
+ * si el veredicto es WARN o ERROR.
11
+ *
12
+ * Aplica ADR-0016 (best practices Anthropic "The CLAUDE.md file"):
13
+ * detecta inflación (líneas excesivas, bullets monolíticos, secciones
14
+ * canónicas ausentes, ausencia de @references) y sugiere intervención
15
+ * con `/swl:claudemd refactor`.
16
+ *
17
+ * Opt-out: SWL_CLAUDEMD_BLOAT=0 desactiva completamente el hook.
18
+ *
19
+ * Comportamiento:
20
+ * - Nunca bloquea operaciones (exit code 0 siempre)
21
+ * - Solo emite nudge cuando veredicto != OK — ruido mínimo
22
+ * - Solo se dispara con archivos cuyo basename sea exactamente CLAUDE.md
23
+ * - Respeta exclusiones: temp/, node_modules/, respositorios-git/
24
+ *
25
+ * Formato del nudge:
26
+ * {
27
+ * id: string,
28
+ * kind: "claudemd-bloat",
29
+ * target: "documentador-swl",
30
+ * source: "hooks/claudemd-bloat-detector.js",
31
+ * message: "...",
32
+ * data: { archivo, veredicto, lineas, hallazgos_count },
33
+ * ts: ISO,
34
+ * accionado: false
35
+ * }
36
+ */
37
+
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+ const crypto = require('crypto');
41
+
42
+ // ─── Opt-out global ───────────────────────────────────────────────────────
43
+ if (process.env.SWL_CLAUDEMD_BLOAT === '0') {
44
+ process.exit(0);
45
+ }
46
+
47
+ let hookInput = '';
48
+ try {
49
+ hookInput = fs.readFileSync(0, 'utf-8');
50
+ } catch (_) {
51
+ process.exit(0);
52
+ }
53
+
54
+ let evento;
55
+ try {
56
+ evento = JSON.parse(hookInput);
57
+ } catch (_) {
58
+ process.exit(0);
59
+ }
60
+
61
+ const toolName = evento?.tool_name;
62
+ const toolInput = evento?.tool_input;
63
+
64
+ if (!toolName || !['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
65
+ process.exit(0);
66
+ }
67
+
68
+ const filePath = toolInput?.file_path;
69
+ if (!filePath) {
70
+ process.exit(0);
71
+ }
72
+
73
+ // Solo CLAUDE.md (basename exacto, case-sensitive)
74
+ const basename = path.basename(filePath);
75
+ if (basename !== 'CLAUDE.md') {
76
+ process.exit(0);
77
+ }
78
+
79
+ const pathNormalized = filePath.replace(/\\/g, '/');
80
+ const RUTAS_EXCLUIDAS = [
81
+ '/temp/',
82
+ '/node_modules/',
83
+ '/respositorios-git/',
84
+ '/.planning/',
85
+ ];
86
+ if (RUTAS_EXCLUIDAS.some((excluida) => pathNormalized.includes(excluida))) {
87
+ process.exit(0);
88
+ }
89
+
90
+ // El archivo debe existir
91
+ if (!fs.existsSync(filePath)) {
92
+ process.exit(0);
93
+ }
94
+
95
+ // ─── Ejecutar auditor (módulo, no subproceso) ─────────────────────────────
96
+ const CWD = process.cwd();
97
+ const auditorPath = path.join(CWD, 'scripts', 'auditar-claudemd.js');
98
+ if (!fs.existsSync(auditorPath)) {
99
+ // No hay auditor instalado en este destino; salir silenciosamente
100
+ process.exit(0);
101
+ }
102
+
103
+ let resultado;
104
+ try {
105
+ const { auditar } = require(auditorPath);
106
+ resultado = auditar(filePath);
107
+ } catch (_) {
108
+ // Cualquier error del auditor: salir silenciosamente, no romper el hook
109
+ process.exit(0);
110
+ }
111
+
112
+ // Solo emitir nudge si veredicto != OK
113
+ if (!resultado || resultado.veredicto === 'OK') {
114
+ process.exit(0);
115
+ }
116
+
117
+ // ─── Construir nudge ──────────────────────────────────────────────────────
118
+ const rutaRelativa = path.relative(CWD, filePath).replace(/\\/g, '/');
119
+ const topHallazgos = (resultado.hallazgos || [])
120
+ .slice(0, 3)
121
+ .map((h) => ` - [${h.severidad}] ${h.mensaje}`)
122
+ .join('\n');
123
+
124
+ const nudge = {
125
+ id: crypto.randomBytes(8).toString('hex'),
126
+ kind: 'claudemd-bloat',
127
+ target: 'documentador-swl',
128
+ source: 'hooks/claudemd-bloat-detector.js',
129
+ message:
130
+ `[claudemd] ${rutaRelativa} veredicto: ${resultado.veredicto} ` +
131
+ `(${resultado.hallazgos.length} hallazgos)\n` +
132
+ topHallazgos + '\n' +
133
+ ` Ejecutar \`/swl:claudemd audit\` para detalle, ` +
134
+ `\`/swl:claudemd refactor\` para sugerencias de extracción.`,
135
+ data: {
136
+ archivo: rutaRelativa,
137
+ veredicto: resultado.veredicto,
138
+ lineas: resultado.metricas?.lineas,
139
+ secciones_ausentes: resultado.metricas?.secciones_ausentes || [],
140
+ tiene_at_references: resultado.metricas?.tiene_at_references,
141
+ hallazgos_count: resultado.hallazgos.length,
142
+ },
143
+ ts: new Date().toISOString(),
144
+ accionado: false,
145
+ accionado_ts: null,
146
+ accionado_por: null,
147
+ };
148
+
149
+ // ─── Persistir a nudges.jsonl ─────────────────────────────────────────────
150
+ try {
151
+ const nudgesPath = path.join(CWD, '.planning', 'evolucion', 'nudges.jsonl');
152
+ const nudgesDir = path.dirname(nudgesPath);
153
+ if (!fs.existsSync(nudgesDir)) {
154
+ fs.mkdirSync(nudgesDir, { recursive: true });
155
+ }
156
+ fs.appendFileSync(nudgesPath, JSON.stringify(nudge) + '\n', 'utf-8');
157
+ } catch (_) {
158
+ // No fallar el hook por error de escritura
159
+ }
160
+
161
+ process.exit(0);
@@ -599,21 +599,28 @@ function esHerramientaRelevante(toolName) {
599
599
  * mantenimiento del sistema SWL, no descubrimiento en tiempo de ejecución.
600
600
  * Capturar fragmentos de esos archivos produce ruido auto-referente.
601
601
  */
602
+ // Bug I v1.3.5: los regex usaban `[\\/]<dir>[\\/]` que requiere un caracter
603
+ // slash/backslash ANTES del nombre del directorio. Eso falla cuando el hook
604
+ // recibe un path relativo sin slash inicial (ej. `scripts/lib/foo.js` en lugar
605
+ // de `/path/to/scripts/lib/foo.js`). Cambiamos a `(?:^|[\\/])<dir>[\\/]` para
606
+ // que acepte tanto el inicio del string como un separator previo. Detectado
607
+ // cuando un Edit a `scripts/lib/parsear-opciones.js` generó una entrada
608
+ // placeholder en APRENDIZAJES.md durante el flujo de release v1.3.5.
602
609
  const PATRONES_ARCHIVO_SWL_EXCLUIDO = [
603
- /[\\/]habilidades[\\/]/,
604
- /[\\/]agentes[\\/]/,
605
- /[\\/]reglas[\\/]/,
606
- /[\\/]comandos[\\/]swl[\\/]/,
607
- /[\\/]hooks[\\/]/,
608
- /[\\/]scripts[\\/]/,
609
- /[\\/]schemas[\\/]/,
610
- /[\\/]manifiestos[\\/]/,
611
- /[\\/]plantillas[\\/]/,
612
- /[\\/]contextos[\\/]/,
613
- /[\\/]instintos[\\/]/,
610
+ /(?:^|[\\/])habilidades[\\/]/,
611
+ /(?:^|[\\/])agentes[\\/]/,
612
+ /(?:^|[\\/])reglas[\\/]/,
613
+ /(?:^|[\\/])comandos[\\/]swl[\\/]/,
614
+ /(?:^|[\\/])hooks[\\/]/,
615
+ /(?:^|[\\/])scripts[\\/]/,
616
+ /(?:^|[\\/])schemas[\\/]/,
617
+ /(?:^|[\\/])manifiestos[\\/]/,
618
+ /(?:^|[\\/])plantillas[\\/]/,
619
+ /(?:^|[\\/])contextos[\\/]/,
620
+ /(?:^|[\\/])instintos[\\/]/,
614
621
  // Todo .planning/ salvo wiki/ (que puede contener conocimiento del proyecto usuario).
615
622
  // En swl-ses .planning/ es meta del sistema; los aprendizajes se gestionan manualmente.
616
- /[\\/]\.planning[\\/](?!wiki[\\/])/,
623
+ /(?:^|[\\/])\.planning[\\/](?!wiki[\\/])/,
617
624
  /[\\/]CHANGELOG\.md$/i,
618
625
  /[\\/]CLAUDE\.md$/i,
619
626
  /[\\/]AGENTS\.md$/i,
@@ -623,6 +630,17 @@ const PATRONES_ARCHIVO_SWL_EXCLUIDO = [
623
630
  /[\\/]INSTALACION\.md$/i,
624
631
  /[\\/]INVENTARIO\.md$/i,
625
632
  /[\\/]SALUD\.md$/i,
633
+ // Artefactos meta del flujo de release/planning. Su contenido suele
634
+ // ser narrativa de proceso (estructura, headings, listas) que el hook
635
+ // confunde con aprendizajes genuinos. Ver L-NN (2026-05-11) en
636
+ // APRENDIZAJES.md sobre el bug recurrente del placeholder generado.
637
+ /[\\/]RELEASE-NOTES[-_].*\.md$/i,
638
+ /[\\/]RELEASE_NOTES\.md$/i,
639
+ /[\\/]RESUMEN\.md$/i,
640
+ /[\\/]PLAN\.md$/i,
641
+ /[\\/]CONTEXTO\.md$/i,
642
+ /[\\/]ROADMAP.*\.md$/i,
643
+ /[\\/]MAPEO[_-].*\.md$/i,
626
644
  /[\\/]package\.json$/,
627
645
  /[\\/]plugin\.json$/,
628
646
  /[\\/]package-lock\.json$/,
@@ -672,6 +690,19 @@ function pareceContenidoEstructuralSwl(contexto) {
672
690
  return true;
673
691
  }
674
692
 
693
+ // Contexto compuesto mayoritariamente de headings sin cuerpo. Detecta
694
+ // placeholders auto-generados al hacer Write de archivos meta (RELEASE-NOTES,
695
+ // RESUMEN, PLAN) cuya estructura es solo `## Sección 1`, `## Sección 2`...
696
+ // sin párrafos sustantivos. Acepta tanto headings desnudos (`## Titulo`)
697
+ // como indentados como blockquote (`> ## Titulo`) porque el hook envuelve
698
+ // el contexto con `> ` antes de evaluar el resultado.
699
+ if (noVacias.length >= 3) {
700
+ const headings = noVacias.filter(l => /^(>\s*)?#{1,6}\s+\S/.test(l));
701
+ if (headings.length / noVacias.length > 0.6) {
702
+ return true;
703
+ }
704
+ }
705
+
675
706
  return false;
676
707
  }
677
708
 
@@ -1,107 +1,107 @@
1
- 'use strict';
2
-
3
- /**
4
- * agent-routing.js — Helper para inferir fase y dominio del routing.
5
- *
6
- * Lee el frontmatter del agente (agentes/<nombre>.md) y extrae los campos
7
- * `fase` y `dominio` introducidos en v1.1.0 (ADR 0012). El orquestador no
8
- * registra explícitamente "qué fase/dominio motivó la elección" — pero
9
- * cuando elige un agente, podemos asumir que lo eligió porque su fase y
10
- * dominio matcheaban. Por lo tanto, leer el frontmatter del agente
11
- * efectivamente invocado nos da la celda fase×dominio del routing.
12
- *
13
- * Esto es proxy de precisión, no precisión absoluta:
14
- * - Tasa de éxito por celda (fase, dominio) sirve como indicador de
15
- * que el routing está enviando trabajo correctamente al agente
16
- * adecuado.
17
- * - Una celda con tasa de éxito baja sugiere routing impreciso (el
18
- * agente recibe trabajo que no le compete) o agente sub-óptimo.
19
- *
20
- * Cache simple en memoria del proceso para evitar I/O repetida (cada
21
- * invocación de hook es proceso fresco, así que el cache no persiste,
22
- * pero protege dentro de la misma invocación).
23
- *
24
- * @module hooks/lib/agent-routing
25
- */
26
-
27
- const fs = require('fs');
28
- const path = require('path');
29
-
30
- const _cache = Object.create(null);
31
-
32
- /**
33
- * Resuelve la ruta del archivo de agente. Acepta tanto el repo madre como
34
- * proyectos consumidores (donde los agentes pueden estar en otras rutas).
35
- *
36
- * @param {string} agentName - p.ej. "backend-python-swl"
37
- * @param {string} [cwd] - Directorio de proyecto (default process.cwd())
38
- * @returns {string|null} - Ruta absoluta o null si no existe
39
- */
40
- function resolveAgentPath(agentName, cwd) {
41
- const baseDir = cwd || process.cwd();
42
- const candidates = [
43
- path.join(baseDir, 'agentes', `${agentName}.md`),
44
- path.join(baseDir, '.claude', 'agents', `${agentName}.md`),
45
- ];
46
- for (const c of candidates) {
47
- try { if (fs.statSync(c).isFile()) return c; } catch { /* siguiente */ }
48
- }
49
- return null;
50
- }
51
-
52
- /**
53
- * Extrae fase y dominio del frontmatter de un agente.
54
- *
55
- * @param {string} agentName - Nombre del agente sin extensión
56
- * @param {string} [cwd]
57
- * @returns {{ fase: string|null, dominio: string|null, source: string }}
58
- * - source: 'frontmatter' | 'cache' | 'unknown'
59
- */
60
- function getRouting(agentName, cwd) {
61
- if (!agentName) return { fase: null, dominio: null, source: 'unknown' };
62
- if (_cache[agentName]) {
63
- return { ..._cache[agentName], source: 'cache' };
64
- }
65
-
66
- const agentPath = resolveAgentPath(agentName, cwd);
67
- if (!agentPath) {
68
- const result = { fase: null, dominio: null, source: 'unknown' };
69
- _cache[agentName] = { fase: null, dominio: null };
70
- return result;
71
- }
72
-
73
- let raw;
74
- try {
75
- raw = fs.readFileSync(agentPath, 'utf8');
76
- } catch {
77
- return { fase: null, dominio: null, source: 'unknown' };
78
- }
79
-
80
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
81
- if (!fmMatch) {
82
- _cache[agentName] = { fase: null, dominio: null };
83
- return { fase: null, dominio: null, source: 'unknown' };
84
- }
85
- const fm = fmMatch[1];
86
-
87
- // Parser minimalista — solo busca las dos líneas que necesitamos.
88
- // Evitamos depender de un parser YAML completo (zero-deps).
89
- const faseMatch = fm.match(/^fase:\s*(\S+)/m);
90
- const dominioMatch = fm.match(/^dominio:\s*(\S+)/m);
91
-
92
- const result = {
93
- fase: faseMatch ? faseMatch[1].trim() : null,
94
- dominio: dominioMatch ? dominioMatch[1].trim() : null,
95
- };
96
- _cache[agentName] = result;
97
- return { ...result, source: 'frontmatter' };
98
- }
99
-
100
- /**
101
- * Limpia el cache (para tests o tras modificar agentes).
102
- */
103
- function clearCache() {
104
- for (const k of Object.keys(_cache)) delete _cache[k];
105
- }
106
-
107
- module.exports = { getRouting, resolveAgentPath, clearCache };
1
+ 'use strict';
2
+
3
+ /**
4
+ * agent-routing.js — Helper para inferir fase y dominio del routing.
5
+ *
6
+ * Lee el frontmatter del agente (agentes/<nombre>.md) y extrae los campos
7
+ * `fase` y `dominio` introducidos en v1.1.0 (ADR 0012). El orquestador no
8
+ * registra explícitamente "qué fase/dominio motivó la elección" — pero
9
+ * cuando elige un agente, podemos asumir que lo eligió porque su fase y
10
+ * dominio matcheaban. Por lo tanto, leer el frontmatter del agente
11
+ * efectivamente invocado nos da la celda fase×dominio del routing.
12
+ *
13
+ * Esto es proxy de precisión, no precisión absoluta:
14
+ * - Tasa de éxito por celda (fase, dominio) sirve como indicador de
15
+ * que el routing está enviando trabajo correctamente al agente
16
+ * adecuado.
17
+ * - Una celda con tasa de éxito baja sugiere routing impreciso (el
18
+ * agente recibe trabajo que no le compete) o agente sub-óptimo.
19
+ *
20
+ * Cache simple en memoria del proceso para evitar I/O repetida (cada
21
+ * invocación de hook es proceso fresco, así que el cache no persiste,
22
+ * pero protege dentro de la misma invocación).
23
+ *
24
+ * @module hooks/lib/agent-routing
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const _cache = Object.create(null);
31
+
32
+ /**
33
+ * Resuelve la ruta del archivo de agente. Acepta tanto el repo madre como
34
+ * proyectos consumidores (donde los agentes pueden estar en otras rutas).
35
+ *
36
+ * @param {string} agentName - p.ej. "backend-python-swl"
37
+ * @param {string} [cwd] - Directorio de proyecto (default process.cwd())
38
+ * @returns {string|null} - Ruta absoluta o null si no existe
39
+ */
40
+ function resolveAgentPath(agentName, cwd) {
41
+ const baseDir = cwd || process.cwd();
42
+ const candidates = [
43
+ path.join(baseDir, 'agentes', `${agentName}.md`),
44
+ path.join(baseDir, '.claude', 'agents', `${agentName}.md`),
45
+ ];
46
+ for (const c of candidates) {
47
+ try { if (fs.statSync(c).isFile()) return c; } catch { /* siguiente */ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Extrae fase y dominio del frontmatter de un agente.
54
+ *
55
+ * @param {string} agentName - Nombre del agente sin extensión
56
+ * @param {string} [cwd]
57
+ * @returns {{ fase: string|null, dominio: string|null, source: string }}
58
+ * - source: 'frontmatter' | 'cache' | 'unknown'
59
+ */
60
+ function getRouting(agentName, cwd) {
61
+ if (!agentName) return { fase: null, dominio: null, source: 'unknown' };
62
+ if (_cache[agentName]) {
63
+ return { ..._cache[agentName], source: 'cache' };
64
+ }
65
+
66
+ const agentPath = resolveAgentPath(agentName, cwd);
67
+ if (!agentPath) {
68
+ const result = { fase: null, dominio: null, source: 'unknown' };
69
+ _cache[agentName] = { fase: null, dominio: null };
70
+ return result;
71
+ }
72
+
73
+ let raw;
74
+ try {
75
+ raw = fs.readFileSync(agentPath, 'utf8');
76
+ } catch {
77
+ return { fase: null, dominio: null, source: 'unknown' };
78
+ }
79
+
80
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
81
+ if (!fmMatch) {
82
+ _cache[agentName] = { fase: null, dominio: null };
83
+ return { fase: null, dominio: null, source: 'unknown' };
84
+ }
85
+ const fm = fmMatch[1];
86
+
87
+ // Parser minimalista — solo busca las dos líneas que necesitamos.
88
+ // Evitamos depender de un parser YAML completo (zero-deps).
89
+ const faseMatch = fm.match(/^fase:\s*(\S+)/m);
90
+ const dominioMatch = fm.match(/^dominio:\s*(\S+)/m);
91
+
92
+ const result = {
93
+ fase: faseMatch ? faseMatch[1].trim() : null,
94
+ dominio: dominioMatch ? dominioMatch[1].trim() : null,
95
+ };
96
+ _cache[agentName] = result;
97
+ return { ...result, source: 'frontmatter' };
98
+ }
99
+
100
+ /**
101
+ * Limpia el cache (para tests o tras modificar agentes).
102
+ */
103
+ function clearCache() {
104
+ for (const k of Object.keys(_cache)) delete _cache[k];
105
+ }
106
+
107
+ module.exports = { getRouting, resolveAgentPath, clearCache };