@saulwade/swl-ses 1.3.8 → 1.4.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 (128) hide show
  1. package/CLAUDE.md +12 -4
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-webhook-server.js +198 -0
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +21 -1
  7. package/comandos/swl/claudemd.md +14 -1
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/exportar-vault.md +108 -0
  10. package/comandos/swl/nuevo-proyecto.md +24 -2
  11. package/gateway/adapters/base.js +109 -0
  12. package/gateway/adapters/discord.js +167 -0
  13. package/gateway/adapters/email.js +221 -0
  14. package/gateway/adapters/slack.js +192 -0
  15. package/gateway/adapters/telegram.js +183 -0
  16. package/gateway/adapters/webhook.js +113 -0
  17. package/gateway/adapters/whatsapp.js +214 -0
  18. package/gateway/agent-executor.js +322 -0
  19. package/gateway/command-relay.js +271 -0
  20. package/gateway/cron/jobs.js +263 -0
  21. package/gateway/cron/scheduler.js +322 -0
  22. package/gateway/cron/store.js +335 -0
  23. package/gateway/index.js +320 -0
  24. package/gateway/lib/event-channel.js +191 -0
  25. package/gateway/session.js +131 -0
  26. package/gateway/webhook-server.js +324 -0
  27. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  28. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  29. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  30. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  31. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  32. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  33. package/habilidades/eval-framework/SKILL.md +212 -212
  34. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  35. package/habilidades/harness-claude-code/SKILL.md +299 -299
  36. package/habilidades/infra-github-actions/SKILL.md +166 -166
  37. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  38. package/habilidades/manejo-errores/.evolved.json +8 -8
  39. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  40. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  41. package/habilidades/nextjs-testing/SKILL.md +89 -5
  42. package/habilidades/node-experto/SKILL.md +37 -1
  43. package/habilidades/patrones-python/SKILL.md +229 -229
  44. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  45. package/habilidades/planear-fase/SKILL.md +319 -319
  46. package/habilidades/react-experto/SKILL.md +45 -4
  47. package/habilidades/release-semver/.evolved.json +8 -8
  48. package/habilidades/tdd-workflow/SKILL.md +36 -4
  49. package/habilidades/testing-python/SKILL.md +340 -340
  50. package/hooks/claudemd-bloat-detector.js +161 -161
  51. package/hooks/inyeccion-contexto.js +8 -3
  52. package/hooks/lib/agent-routing.js +107 -107
  53. package/hooks/lib/auto-consolidator.js +335 -335
  54. package/hooks/lib/error-classifier.js +308 -308
  55. package/hooks/lib/merkle-audit.js +96 -96
  56. package/hooks/lib/provenance-tracker.js +191 -191
  57. package/hooks/lib/rate-limit-ip.js +177 -0
  58. package/hooks/lib/rate-limit-tracker.js +253 -253
  59. package/hooks/lib/resource-quota.js +122 -122
  60. package/hooks/lib/retry-jitter.js +165 -165
  61. package/hooks/lib/skill-auditor.js +588 -588
  62. package/hooks/lib/sync-status.js +228 -228
  63. package/hooks/lib/taint-tracker.js +107 -107
  64. package/hooks/lib/text-similarity.js +241 -241
  65. package/hooks/lib/toon-compressor.js +245 -245
  66. package/hooks/lib/webhook-dedup.js +184 -0
  67. package/hooks/lib/webhook-verify.js +123 -0
  68. package/hooks/proteccion-rutas.js +120 -15
  69. package/hooks/registro-turnos.js +209 -209
  70. package/hooks/sugerir-regenerar-inventario.js +170 -170
  71. package/hooks/validar-formato-post-subagente.js +140 -140
  72. package/hooks/validar-memoria-hook.js +218 -218
  73. package/instintos/prompt-appendices.yaml +57 -57
  74. package/manifiestos/agent-output-schemas.json +57 -57
  75. package/manifiestos/modulos.json +1 -0
  76. package/manifiestos/skills-lock.json +34 -34
  77. package/package.json +5 -3
  78. package/plantillas/auditor-veto-template.md +105 -105
  79. package/plantillas/github-workflows/README.md +47 -47
  80. package/plantillas/github-workflows/release-please.yml +44 -44
  81. package/plantillas/github-workflows/swl-ci.yml +107 -107
  82. package/plantillas/github-workflows/swl-security.yml +51 -51
  83. package/plugin.json +1 -1
  84. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  85. package/reglas/arreglar-al-detectar.md +147 -147
  86. package/reglas/fragmentos-compartidos.md +152 -152
  87. package/reglas/harness-claude-code.md +213 -213
  88. package/reglas/usar-context7.md +226 -226
  89. package/reglas/usar-sistema-swl.md +251 -0
  90. package/schemas/diary-entry.schema.json +80 -80
  91. package/scripts/benchmark-memoria.js +167 -167
  92. package/scripts/comandos/skills.js +251 -2
  93. package/scripts/configurar-branch-protection.js +418 -418
  94. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  95. package/scripts/field-report.js +199 -199
  96. package/scripts/generar-checklists-consolidados.js +273 -273
  97. package/scripts/generar-inventario.js +420 -420
  98. package/scripts/generar-matriz-lenguajes.js +271 -271
  99. package/scripts/lib/artefactos-python.js +43 -43
  100. package/scripts/lib/benchmark-metrics.js +160 -160
  101. package/scripts/lib/budget-enforcer.js +252 -252
  102. package/scripts/lib/configurar-ci.js +380 -380
  103. package/scripts/lib/contadores-inventario.js +217 -217
  104. package/scripts/lib/detectar-stack-detallado.js +307 -307
  105. package/scripts/lib/diary-entry.js +234 -234
  106. package/scripts/lib/eval-metrics-store.js +218 -218
  107. package/scripts/lib/eval-quality.js +171 -171
  108. package/scripts/lib/eval-schemas.js +144 -144
  109. package/scripts/lib/eval-self-correct.js +106 -106
  110. package/scripts/lib/eval-validator.js +185 -185
  111. package/scripts/lib/jaccard-similarity.js +98 -98
  112. package/scripts/lib/longmemeval-runner.js +125 -125
  113. package/scripts/lib/npm-version.js +261 -261
  114. package/scripts/lib/paquetes-conocidos.js +50 -50
  115. package/scripts/lib/prompt-builder.js +264 -264
  116. package/scripts/lib/rrf-fusion.js +175 -175
  117. package/scripts/lib/scoring-instintos.js +277 -277
  118. package/scripts/lib/semantic-search.js +252 -252
  119. package/scripts/limpiar-artefactos-python.js +131 -131
  120. package/scripts/mcp-server/README.md +128 -128
  121. package/scripts/mcp-server/handlers.js +206 -206
  122. package/scripts/migrar-csv-a-array.js +168 -168
  123. package/scripts/migrar-fase-dominio.js +201 -201
  124. package/scripts/publicar.js +511 -511
  125. package/scripts/run-eval.js +141 -141
  126. package/scripts/validar-manifest.js +195 -195
  127. package/scripts/validar-userland-vacio.js +110 -110
  128. package/scripts/verificar-release.js +110 -0
@@ -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);
@@ -147,9 +147,14 @@ function generarSugerenciaSkillsComunidad(cwd) {
147
147
  const { skills, agentes } = _techDetector.sugerirSkills(cwd);
148
148
  if (skills.length === 0) return '';
149
149
 
150
- // Verificar si ya hay skills de comunidad instalados (.agents/skills/)
150
+ // Verificar si ya hay skills de comunidad instalados. Hay 2 señales:
151
+ // 1. Marker `.claude/skills/.autoskills-installed.json` (escrito por
152
+ // `swl-ses skills auto`). Es la señal canónica desde v1.4.x.
153
+ // 2. Existencia de `.agents/skills/` — compatibilidad hacia atrás con
154
+ // proyectos que usaron `npx autoskills` directamente sin el wrapper.
155
+ const markerAutoskills = path.join(cwd, '.claude', 'skills', '.autoskills-installed.json');
151
156
  const agentsSkillsDir = path.join(cwd, '.agents', 'skills');
152
- const yaTieneComunidad = fs.existsSync(agentsSkillsDir);
157
+ const yaTieneComunidad = fs.existsSync(markerAutoskills) || fs.existsSync(agentsSkillsDir);
153
158
 
154
159
  // Generar resumen de stack
155
160
  const resumen = _techDetector.generarResumenStack(cwd);
@@ -163,7 +168,7 @@ function generarSugerenciaSkillsComunidad(cwd) {
163
168
 
164
169
  if (!yaTieneComunidad) {
165
170
  // Solo sugerir si no tiene skills de comunidad aun
166
- lineas.push(' Skills de comunidad: ejecuta `npx autoskills --dry-run` para ver skills oficiales disponibles');
171
+ lineas.push(' Skills de comunidad: ejecuta `npx -y @saulwade/swl-ses@latest skills auto --dry-run` para ver skills disponibles');
167
172
  }
168
173
 
169
174
  lineas.push('--- FIN STACK ---');
@@ -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 };