@saulwade/swl-ses 1.0.1 → 1.1.1

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 (104) hide show
  1. package/CLAUDE.md +7 -5
  2. package/README.md +2 -2
  3. package/agentes/accesibilidad-wcag-swl.md +5 -7
  4. package/agentes/arquitecto-swl.md +5 -3
  5. package/agentes/auto-evolucion-swl.md +42 -12
  6. package/agentes/backend-api-swl.md +5 -3
  7. package/agentes/backend-csharp-swl.md +5 -3
  8. package/agentes/backend-go-swl.md +5 -3
  9. package/agentes/backend-java-swl.md +5 -3
  10. package/agentes/backend-node-swl.md +5 -3
  11. package/agentes/backend-python-swl.md +5 -3
  12. package/agentes/backend-rust-swl.md +5 -3
  13. package/agentes/backend-workers-swl.md +5 -3
  14. package/agentes/cloud-infra-swl.md +5 -6
  15. package/agentes/consolidador-swl.md +5 -3
  16. package/agentes/datos-swl.md +5 -7
  17. package/agentes/depurador-swl.md +6 -3
  18. package/agentes/devops-ci-swl.md +5 -3
  19. package/agentes/disenador-ui-swl.md +5 -7
  20. package/agentes/documentador-swl.md +5 -3
  21. package/agentes/frontend-angular-swl.md +5 -11
  22. package/agentes/frontend-css-swl.md +5 -9
  23. package/agentes/frontend-react-swl.md +5 -9
  24. package/agentes/frontend-swl.md +5 -9
  25. package/agentes/frontend-tailwind-swl.md +5 -9
  26. package/agentes/implementador-swl.md +6 -3
  27. package/agentes/investigador-swl.md +5 -3
  28. package/agentes/investigador-ux-swl.md +5 -9
  29. package/agentes/llm-apps-swl.md +5 -3
  30. package/agentes/migrador-swl.md +6 -3
  31. package/agentes/mobile-android-swl.md +5 -3
  32. package/agentes/mobile-cross-swl.md +5 -3
  33. package/agentes/mobile-ios-swl.md +5 -3
  34. package/agentes/mobile-testing-swl.md +5 -3
  35. package/agentes/notificador-swl.md +5 -3
  36. package/agentes/observabilidad-swl.md +5 -3
  37. package/agentes/orquestador-swl.md +29 -8
  38. package/agentes/pagos-swl.md +5 -3
  39. package/agentes/perfilador-usuario-swl.md +4 -2
  40. package/agentes/planificador-swl.md +5 -3
  41. package/agentes/producto-prd-swl.md +5 -3
  42. package/agentes/red-team-swl.md +4 -2
  43. package/agentes/release-manager-swl.md +6 -8
  44. package/agentes/rendimiento-swl.md +5 -6
  45. package/agentes/resolutor-build-swl.md +5 -3
  46. package/agentes/revisor-angular-swl.md +5 -3
  47. package/agentes/revisor-codigo-swl.md +5 -3
  48. package/agentes/revisor-csharp-swl.md +5 -3
  49. package/agentes/revisor-go-swl.md +5 -3
  50. package/agentes/revisor-java-swl.md +5 -3
  51. package/agentes/revisor-kotlin-swl.md +5 -3
  52. package/agentes/revisor-nextjs-swl.md +5 -3
  53. package/agentes/revisor-php-swl.md +5 -3
  54. package/agentes/revisor-react-swl.md +5 -3
  55. package/agentes/revisor-rust-swl.md +5 -3
  56. package/agentes/revisor-seguridad-swl.md +5 -3
  57. package/agentes/revisor-swift-swl.md +5 -3
  58. package/agentes/revisor-typescript-swl.md +5 -3
  59. package/agentes/sre-swl.md +5 -3
  60. package/agentes/tdd-qa-swl.md +5 -3
  61. package/agentes/ux-disenador-swl.md +5 -9
  62. package/comandos/swl/evaluar-skill.md +18 -0
  63. package/comandos/swl/evolucion-estado.md +49 -0
  64. package/comandos/swl/release.md +41 -0
  65. package/comandos/swl/salud.md +23 -0
  66. package/hooks/auto-evolucion.js +35 -1
  67. package/hooks/clasificador-mensajes.js +50 -3
  68. package/hooks/lib/agent-routing.js +107 -0
  69. package/hooks/lib/delegation-tracker.js +162 -44
  70. package/hooks/lib/evolution-tracker.js +12 -3
  71. package/hooks/lib/memory-search.js +59 -1
  72. package/hooks/lib/nudge-tracker.js +10 -1
  73. package/hooks/lib/provenance-tracker.js +11 -3
  74. package/hooks/lib/text-similarity.js +241 -0
  75. package/hooks/metricas-evolucion.js +168 -1
  76. package/hooks/monitor-contexto.js +54 -6
  77. package/hooks/preservar-estado-pre-compact.js +11 -1
  78. package/hooks/risk-scoring.js +10 -1
  79. package/hooks/tracking-costos.js +10 -1
  80. package/hooks/validar-formato-post-subagente.js +140 -0
  81. package/hooks/validar-memoria-hook.js +218 -0
  82. package/manifiestos/agent-output-schemas.json +57 -0
  83. package/manifiestos/hooks-config.json +18 -0
  84. package/manifiestos/modulos.json +3 -0
  85. package/manifiestos/skills-lock.json +1065 -0
  86. package/package.json +1 -1
  87. package/plugin.json +1 -1
  88. package/reglas/arquitectura.md +20 -0
  89. package/reglas/fragmentos-compartidos.md +152 -0
  90. package/reglas/gobernanza.md +10 -1
  91. package/reglas/seguridad-agentes.md +12 -0
  92. package/reglas/skills-estandar.md +19 -0
  93. package/schemas/agent-frontmatter.schema.json +18 -0
  94. package/scripts/auditar-agentes-gaps.js +9 -1
  95. package/scripts/auditar-cobertura-frameworks.js +9 -1
  96. package/scripts/auditar-skills-gaps.js +9 -1
  97. package/scripts/bootstrap-instintos.js +11 -1
  98. package/scripts/generar-inventario.js +112 -9
  99. package/scripts/generar-matriz-lenguajes.js +271 -0
  100. package/scripts/generar-skills-lock.js +190 -0
  101. package/scripts/lib/estado.js +12 -2
  102. package/scripts/lib/gitignore-manifest.js +32 -2
  103. package/scripts/migrar-csv-a-array.js +168 -0
  104. package/scripts/migrar-fase-dominio.js +201 -0
@@ -9,7 +9,7 @@ description: >
9
9
  disenador-ui-swl (capa visual y tokens). NO invocar para implementacion de
10
10
  codigo — produce specs, no codigo. Para implementar usar frontend-*-swl con
11
11
  la UI-SPEC.md producida.
12
- tools: Read, Write, Grep, Glob, WebSearch
12
+ tools: [Read, Write, Grep, Glob, WebSearch]
13
13
  model: claude-sonnet-4-6
14
14
  modeloAlterno: claude-haiku-4-5-20251001
15
15
  ventanaContexto: 200k
@@ -17,18 +17,14 @@ permissionMode: plan
17
17
  color: pink
18
18
  version: 1.0.0
19
19
  nivelRiesgo: BAJO
20
- skillsInvocables: ux-diseno, wireframes-flujos, design-tokens, accesibilidad-a11y, diseno-responsivo
21
- skillsRestringidos:
22
- - fastapi-python
23
- - django-expert
24
- - postgresql-table-design
25
- - python-patterns
26
- - angular-component
27
- - angular-forms
20
+ skillsInvocables: [ux-diseno, wireframes-flujos, design-tokens, accesibilidad-a11y, diseno-responsivo]
21
+ skillsRestringidos: [fastapi-python, django-expert, postgresql-table-design, python-patterns, angular-component, angular-forms]
28
22
  permisosRed: true
29
23
  permisosEscritura: true
30
24
  permisosComandos: false
31
25
  evolvable: true # nivelRiesgo=BAJO
26
+ fase: plan
27
+ dominio: ux
32
28
  exclusiones:
33
29
  - "No invocar para implementar código de interfaz — este agente produce UI-SPEC.md, no código; usar frontend-react-swl, frontend-angular-swl o frontend-swl para la implementación."
34
30
  - "No invocar cuando el pipeline tiene roles separados: preferir investigador-ux-swl para research y disenador-ui-swl para especificaciones visuales puras."
@@ -356,6 +356,24 @@ Asignar badge:
356
356
  < 60 → Sin badge — no hacer merge hasta corregir
357
357
  ```
358
358
 
359
+ ### Gate de promoción para auto-evolucion-swl (G8)
360
+
361
+ Este comando es el **gate obligatorio** del flujo de promoción de skills
362
+ auto-generados desde `_userland/plugins/` a `habilidades/`. El agente
363
+ `auto-evolucion-swl` invoca este comando antes de mover un skill al core
364
+ (ver `agentes/auto-evolucion-swl.md` sección "Gate G8").
365
+
366
+ Umbral de promoción:
367
+
368
+ | Badge | Acción de auto-evolucion |
369
+ |-------|-------------------------|
370
+ | Platino, Oro, Plata | Promover a `habilidades/`, registrar en manifiestos |
371
+ | Bronce | NO promover, devolver a `_userland/` con feedback |
372
+ | Sin badge | NO promover, registrar rechazo en `.planning/evolucion/promociones-rechazadas.jsonl` |
373
+
374
+ Si un skill es rechazado ≥3 veces consecutivas, el agente escala al usuario
375
+ con análisis de qué dimensiones bajan el score. Origen: ADR 0013 sección 3C.
376
+
359
377
  ## Paso 4 — Emitir reporte
360
378
 
361
379
  Generar el siguiente reporte directamente en la conversación:
@@ -38,6 +38,30 @@ Si el archivo no existe: ejecutar el regenerado del Paso 0. Si sigue sin
38
38
  existir: reportar "el ciclo de evolución no ha corrido aún; ejecuta cualquier
39
39
  tarea para poblar métricas iniciales".
40
40
 
41
+ ## Paso 1.5 — Cargar contexto del kernel (gobernanza)
42
+
43
+ Para el bloque **KERNEL** del dashboard, recolectar:
44
+
45
+ ```bash
46
+ # 1. Conteo de agentes evolvable=false vs total
47
+ TOTAL=$(ls agentes/*.md | grep -v "^agentes/_" | wc -l)
48
+ EVOL_FALSE=$(grep -l '^evolvable: false' agentes/*.md | wc -l)
49
+
50
+ # 2. Último ADR del repo madre — el más reciente por fecha en frontmatter
51
+ ULTIMO_ADR=$(ls .planning/adrs/[0-9]*.md | sort -r | head -1)
52
+ TITULO=$(head -1 "$ULTIMO_ADR" | sed 's/^# //')
53
+ FECHA=$(grep -m1 '^\*\*Fecha\*\*:' "$ULTIMO_ADR" | sed 's/.*\*\*Fecha\*\*:\s*//')
54
+
55
+ echo "ADR_KERNEL_LINEA=\"$TITULO — $FECHA\""
56
+ echo "RATIO=\"$EVOL_FALSE/$TOTAL\""
57
+ ```
58
+
59
+ Estos valores alimentan la sección KERNEL del template.
60
+
61
+ Si `.planning/evolucion/politica-evolvable.md` no existe, reportar como
62
+ hallazgo crítico: el kernel SIN política documentada es violación de
63
+ `reglas/gobernanza.md`.
64
+
41
65
  ## Paso 2 — Emitir el dashboard (formato humano)
42
66
 
43
67
  Template de salida:
@@ -74,6 +98,26 @@ Template de salida:
74
98
  Aplicadas ............. <n>
75
99
  Revertidas ............ <n>
76
100
  Neta .................. <n>
101
+ Rollback ratio ........ <%> (N/A si aplicadas == 0)
102
+
103
+ CALIDAD CONDUCTUAL (14d)
104
+ Fallos por tipo:
105
+ bad_output_format <n>
106
+ tool_error <n>
107
+ timeout <n>
108
+ schema_violation <n>
109
+ task_incomplete <n>
110
+ unknown <n>
111
+ Reintentos consecutivos
112
+ Total ............... <n> (mismo agente, misma task, ≤30 min)
113
+ Top agente .......... <agente>: <n>
114
+ Latencia top agente
115
+ Mediana ............. <ms>
116
+ p95 ................. <ms>
117
+ Sin instrumentación todavía:
118
+ • precision_routing_fase_dominio
119
+ • utilidad_memoria_recuperada
120
+ • violaciones_formato_post_ejecucion
77
121
 
78
122
  ALERTAS PERSISTENTES
79
123
  <si alertasActivas.length === 0>
@@ -82,6 +126,11 @@ Template de salida:
82
126
  [!] kind=X target=Y count=Z (primera: fecha)
83
127
  ...
84
128
 
129
+ KERNEL (gobernanza)
130
+ Política evolvable .... .planning/evolucion/politica-evolvable.md
131
+ Último ADR kernel ..... <ADR-NNNN — título — fecha>
132
+ Agentes evolvable=false <n>/<total>
133
+
85
134
  ═══════════════════════════════════════════════════════════════
86
135
  ```
87
136
 
@@ -122,6 +122,21 @@ Para proyectos no-SWL: actualiza los archivos detectados en Paso 1 (package.json
122
122
 
123
123
  En ambos casos, verificar consistencia después de actualizar con `grep -r "versión-anterior" .` para detectar referencias olvidadas.
124
124
 
125
+ ## Paso 6.5 — Regenerar skills-lock.json
126
+
127
+ Antes del CHANGELOG, regenerar el lock de skills para capturar el estado de
128
+ los 151 SKILL.md de la release:
129
+
130
+ ```bash
131
+ node scripts/generar-skills-lock.js
132
+ git add manifiestos/skills-lock.json
133
+ ```
134
+
135
+ El lock contiene SHA256 de cada SKILL.md y permite que `/swl:salud` detecte
136
+ drift silencioso entre releases. Si el lock no cambió respecto al anterior,
137
+ el commit lo refleja como no-op (idempotente). El archivo es pequeño (~37KB)
138
+ y debe versionarse.
139
+
125
140
  ## Paso 7 — Generar CHANGELOG
126
141
 
127
142
  Lee CHANGELOG.md existente (o créalo). Agrega entrada al inicio con formato Keep a Changelog:
@@ -178,6 +193,32 @@ Resultado: 13/15 OK, 2 FALLA(S) obligatoria(s)
178
193
 
179
194
  Esta gate existe porque el agente `release-manager-swl` ha omitido archivos en 3 releases consecutivos (5.10.3, 5.10.4, 5.10.5 — ver APRENDIZAJES.md) pese a tener la checklist documentada en su frontmatter. Un checklist textual no basta: se necesita ejecución.
180
195
 
196
+ ### 10.1.1 Gate de cobertura por lenguaje (regeneración obligatoria)
197
+
198
+ Tras el verificar-release.js, regenerar la matriz lenguaje × cobertura SWL:
199
+
200
+ ```bash
201
+ node scripts/generar-matriz-lenguajes.js
202
+ ```
203
+
204
+ El script reescribe `.planning/cobertura-lenguajes.md` con el estado actual.
205
+ Comparar contra el commit anterior:
206
+
207
+ ```bash
208
+ git diff .planning/cobertura-lenguajes.md
209
+ ```
210
+
211
+ Si algún lenguaje **bajó de status** (completo → parcial, parcial → faltante)
212
+ sin ADR documentado en `.planning/adrs/` justificando la regresión, **NO hacer
213
+ release**. La regresión silenciosa de cobertura erosiona la promesa "polyglot"
214
+ del repo.
215
+
216
+ Si el cambio es esperado (eliminación deliberada de un componente), el ADR
217
+ debe existir con número y fecha y referenciarse en el commit del release.
218
+
219
+ Lo mismo aplica para releases con incremento de cobertura: la nueva matriz se
220
+ commitea junto al release y se menciona en RELEASE-NOTES.
221
+
181
222
  ### 10.2 Verificación manual post-gate
182
223
 
183
224
  ```bash
@@ -286,6 +286,29 @@ Si `SWL_AUDIT_AGENTES` no está definida, este paso se omite — los reportes
286
286
  son opt-in por diseño (CLAUDE.md: "Variables de entorno opt-in para
287
287
  integraciones enterprise").
288
288
 
289
+ ## Paso 5e — Verificación de drift de skills (skills-lock)
290
+
291
+ Si existe `manifiestos/skills-lock.json`, comparar el hash actual de cada
292
+ `habilidades/*/SKILL.md` contra el lock. Detecta ediciones silenciosas de
293
+ skills entre release y release.
294
+
295
+ ```bash
296
+ node scripts/generar-skills-lock.js --check
297
+ ```
298
+
299
+ Estados posibles:
300
+
301
+ | Salida | Significado | Acción |
302
+ |--------|-------------|--------|
303
+ | `✓ skills-lock OK: N skills sin drift` | Todos los hashes coinciden | Continuar |
304
+ | `✗ skills-lock DRIFT detectado` | Skills modificados, nuevos o faltantes | Listar diferencias y proponer regenerar |
305
+
306
+ Si NO existe el lock (proyecto fresco o pre-1.1.0), este paso se omite y
307
+ emite advertencia sugerente: `node scripts/generar-skills-lock.js`.
308
+
309
+ El lock se regenera automáticamente como parte de `/swl:release`. Drift
310
+ fuera de release indica ediciones manuales no committed.
311
+
289
312
  ## Paso 6b — Formato de salida enriquecido (HealthRow)
290
313
 
291
314
  El módulo `scripts/lib/health-row.js` genera filas de salud con formato visual enriquecido
@@ -42,6 +42,15 @@
42
42
  const fs = require('fs');
43
43
  const path = require('path');
44
44
 
45
+ // Escritura atómica obligatoria para estado mutable (regla CLAUDE.md).
46
+ // Fallback defensivo si el módulo no existe en el destino.
47
+ let atomicWriteJSON;
48
+ try {
49
+ ({ atomicWriteJSON } = require('./lib/atomic-write'));
50
+ } catch {
51
+ atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
52
+ }
53
+
45
54
  let nudgeTracker;
46
55
  try {
47
56
  nudgeTracker = require('./lib/nudge-tracker');
@@ -70,6 +79,13 @@ try {
70
79
  driftDetector = null;
71
80
  }
72
81
 
82
+ let agentRouting;
83
+ try {
84
+ agentRouting = require('./lib/agent-routing');
85
+ } catch {
86
+ agentRouting = null;
87
+ }
88
+
73
89
  let singletonGuard;
74
90
  try {
75
91
  singletonGuard = require('./lib/singleton-guard');
@@ -120,7 +136,7 @@ function leerNudges() {
120
136
  function escribirNudges(obj) {
121
137
  ensureDir(DIR_AUTOEVOL);
122
138
  try {
123
- fs.writeFileSync(NUDGES_PATH, JSON.stringify(obj, null, 2), 'utf8');
139
+ atomicWriteJSON(NUDGES_PATH, obj);
124
140
  } catch { /* ignore */ }
125
141
  }
126
142
 
@@ -196,6 +212,21 @@ function analizarPayload(data) {
196
212
  ? clasificarTipoFallo(data, response)
197
213
  : null;
198
214
 
215
+ // Inferir fase/dominio del frontmatter del agente para routing precision
216
+ // (ADR 0012). Si el agente no tiene frontmatter o no se encuentra el archivo,
217
+ // los campos quedan null y la métrica simplemente no cuenta esa entrada.
218
+ let routedPhase = null;
219
+ let routedDomain = null;
220
+ let routingSource = 'unknown';
221
+ if (agentRouting) {
222
+ try {
223
+ const r = agentRouting.getRouting(subagentType);
224
+ routedPhase = r.fase;
225
+ routedDomain = r.dominio;
226
+ routingSource = r.source;
227
+ } catch { /* no bloquear por fallo en routing inference */ }
228
+ }
229
+
199
230
  return {
200
231
  ts: new Date().toISOString(),
201
232
  sessionId: String(data.session_id || 'default'),
@@ -205,6 +236,9 @@ function analizarPayload(data) {
205
236
  trivial: toolCalls > 0 && toolCalls < MIN_TOOL_CALLS,
206
237
  duracionMs,
207
238
  tipo_fallo: tipoFallo,
239
+ routed_phase: routedPhase,
240
+ routed_domain: routedDomain,
241
+ routing_source: routingSource,
208
242
  };
209
243
  }
210
244
 
@@ -11,14 +11,31 @@
11
11
  * arquitectura, seguridad, performance, refactor, documentación.
12
12
  *
13
13
  * Inspirado en obsidian-mind classify-message.py.
14
- * Zero-dependencies: regex puro en JS.
14
+ * Zero-dependencies: regex puro en JS + lib propia text-similarity.js.
15
15
  *
16
16
  * Resultado:
17
17
  * - Señales detectadas → hookSpecificOutput con hints de routing
18
18
  * - Sin señales → exit 0 silencioso
19
19
  * - Error interno → exit 0 silencioso (nunca bloquear)
20
+ *
21
+ * Opt-in opcional:
22
+ * - SWL_FUZZY_CLASIFICADOR=1 activa fuzzy matching (Levenshtein + stem ES)
23
+ * como SEGUNDA pasada cuando regex no detectó señales. NUNCA degrada
24
+ * señales del regex original. Útil para typos y variantes morfológicas
25
+ * ("documentar" → matches "documentación"). Costo: ~5-15ms por prompt.
20
26
  */
21
27
 
28
+ const path = require('path');
29
+ const FUZZY_HABILITADO = process.env.SWL_FUZZY_CLASIFICADOR === '1';
30
+ let textSim = null;
31
+ if (FUZZY_HABILITADO) {
32
+ try {
33
+ textSim = require(path.join(__dirname, 'lib', 'text-similarity'));
34
+ } catch (_) {
35
+ // Si la lib no existe, fuzzy se desactiva silenciosamente
36
+ }
37
+ }
38
+
22
39
  // ---------------------------------------------------------------------------
23
40
  // Mapa de complejidad de tarea → modelo recomendado
24
41
  //
@@ -212,22 +229,52 @@ function anyMatch(patterns, text) {
212
229
  return false;
213
230
  }
214
231
 
232
+ /**
233
+ * Verifica si algún patrón aparece de forma aproximada (fuzzy) en el texto.
234
+ * Usa Levenshtein + stem español. Solo se invoca si SWL_FUZZY_CLASIFICADOR=1.
235
+ * NO se llama si `anyMatch` ya devolvió true para esa señal — fuzzy es
236
+ * fallback puro, no reemplazo.
237
+ *
238
+ * @param {string[]} patterns
239
+ * @param {string} text - texto original (no lowercase necesariamente)
240
+ * @returns {boolean}
241
+ */
242
+ function anyMatchFuzzy(patterns, text) {
243
+ if (!textSim) return false;
244
+ for (const phrase of patterns) {
245
+ // Solo aplicar fuzzy a patterns de palabra única o frase corta.
246
+ // Patterns de >3 palabras tienden a generar falsos positivos.
247
+ const tokens = phrase.split(/\s+/).filter(Boolean);
248
+ if (tokens.length > 3) continue;
249
+ if (textSim.fuzzyContains(text, phrase)) return true;
250
+ }
251
+ return false;
252
+ }
253
+
215
254
  /**
216
255
  * Clasifica el mensaje del usuario.
217
256
  * @param {string} prompt
218
- * @returns {{ hints: string[], signalNames: string[] }}
257
+ * @returns {{ hints: string[], signalNames: string[], fuzzyAdded: number }}
219
258
  */
220
259
  function classify(prompt) {
221
260
  const text = (prompt || '').toLowerCase();
222
261
  const hints = [];
223
262
  const signalNames = [];
263
+ let fuzzyAdded = 0;
264
+
224
265
  for (const sig of SIGNALS) {
225
266
  if (anyMatch(sig.patterns, text)) {
226
267
  hints.push(sig.hint);
227
268
  signalNames.push(sig.name);
269
+ } else if (FUZZY_HABILITADO && anyMatchFuzzy(sig.patterns, text)) {
270
+ // Fuzzy detectó algo que el regex perdió — marca con [fuzzy] para
271
+ // observabilidad
272
+ hints.push(sig.hint + ' [match aproximado]');
273
+ signalNames.push(sig.name);
274
+ fuzzyAdded++;
228
275
  }
229
276
  }
230
- return { hints, signalNames };
277
+ return { hints, signalNames, fuzzyAdded };
231
278
  }
232
279
 
233
280
  // ---------------------------------------------------------------------------
@@ -0,0 +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 };