@saulwade/swl-ses 1.0.1 → 1.1.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 (113) hide show
  1. package/CLAUDE.md +8 -5
  2. package/README.md +3 -3
  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 +90 -4
  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 +77 -1
  65. package/comandos/swl/salud.md +23 -0
  66. package/habilidades/checklist-seguridad/SKILL.md +57 -1
  67. package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
  68. package/habilidades/fastapi-experto/SKILL.md +10 -1
  69. package/habilidades/manejo-errores/.evolved.json +8 -8
  70. package/habilidades/manejo-errores/SKILL.md +63 -4
  71. package/habilidades/patrones-python/SKILL.md +5 -4
  72. package/habilidades/release-semver/.evolved.json +8 -8
  73. package/habilidades/release-semver/SKILL.md +85 -1
  74. package/hooks/auto-evolucion.js +35 -1
  75. package/hooks/clasificador-mensajes.js +50 -3
  76. package/hooks/lib/agent-routing.js +107 -0
  77. package/hooks/lib/delegation-tracker.js +162 -44
  78. package/hooks/lib/evolution-tracker.js +12 -3
  79. package/hooks/lib/memory-search.js +59 -1
  80. package/hooks/lib/nudge-tracker.js +10 -1
  81. package/hooks/lib/provenance-tracker.js +11 -3
  82. package/hooks/lib/text-similarity.js +241 -0
  83. package/hooks/metricas-evolucion.js +168 -1
  84. package/hooks/monitor-contexto.js +54 -6
  85. package/hooks/preservar-estado-pre-compact.js +11 -1
  86. package/hooks/risk-scoring.js +10 -1
  87. package/hooks/tracking-costos.js +10 -1
  88. package/hooks/validar-formato-post-subagente.js +140 -0
  89. package/hooks/validar-memoria-hook.js +218 -0
  90. package/manifiestos/agent-output-schemas.json +57 -0
  91. package/manifiestos/hooks-config.json +18 -0
  92. package/manifiestos/modulos.json +3 -0
  93. package/manifiestos/skills-lock.json +1065 -0
  94. package/package.json +1 -1
  95. package/plugin.json +1 -1
  96. package/reglas/arquitectura.md +20 -0
  97. package/reglas/fragmentos-compartidos.md +152 -0
  98. package/reglas/gobernanza.md +10 -1
  99. package/reglas/seguridad-agentes.md +12 -0
  100. package/reglas/skills-estandar.md +19 -0
  101. package/schemas/agent-frontmatter.schema.json +18 -0
  102. package/scripts/auditar-agentes-gaps.js +9 -1
  103. package/scripts/auditar-cobertura-frameworks.js +9 -1
  104. package/scripts/auditar-skills-gaps.js +9 -1
  105. package/scripts/bootstrap-instintos.js +11 -1
  106. package/scripts/generar-inventario.js +112 -9
  107. package/scripts/generar-matriz-lenguajes.js +271 -0
  108. package/scripts/generar-skills-lock.js +190 -0
  109. package/scripts/lib/estado.js +12 -2
  110. package/scripts/lib/gitignore-manifest.js +32 -2
  111. package/scripts/migrar-csv-a-array.js +168 -0
  112. package/scripts/migrar-fase-dominio.js +201 -0
  113. package/scripts/publicar.js +88 -18
@@ -1,9 +1,9 @@
1
- {
2
- "SKILL.md": {
3
- "evolved": true,
4
- "evolvedFrom": "5.10.4",
5
- "evolvedAt": "2026-04-20",
6
- "evolvedBy": "aprender",
7
- "evolvedNote": "npx @latest en post-install"
8
- }
1
+ {
2
+ "SKILL.md": {
3
+ "evolved": true,
4
+ "evolvedFrom": "1.0.1",
5
+ "evolvedAt": "2026-05-02",
6
+ "evolvedBy": "aprender",
7
+ "evolvedNote": "Sección nueva: publish a múltiples registries (republish-only + auth GitHub Packages)"
8
+ }
9
9
  }
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: release-semver
3
3
  description: Versionado semántico (SemVer). Cuándo bumpar major/minor/patch, changelogs convencionales, estrategia de tags y proceso de release completo.
4
- version: "1.0.1"
4
+ version: "1.0.2"
5
+ evolved: true
6
+ evolved-from: "1.0.1"
7
+ evolved-at: "2026-05-02"
8
+ evolved-by: "aprender"
9
+ evolved-note: "Sección nueva: publish a múltiples registries (npmjs + GitHub Packages) — republish-only pattern y auth GitHub Packages no soporta npm login"
5
10
  herramientasPermitidas: [Read, Bash]
6
11
  exclusiones:
7
12
  - "No cargar para versionar el sistema SWL — el bump de versión de swl-ses sigue el checklist de 15 ubicaciones documentado en `/swl:release`; este skill cubre SemVer general para proyectos de usuario, no el proceso interno de release del sistema."
@@ -209,6 +214,85 @@ git describe --tags --abbrev=0 # Último tag del commit actual
209
214
 
210
215
  ---
211
216
 
217
+ ## Publish a múltiples registries (mirror dual)
218
+
219
+ Cuando un paquete se publica al mismo tiempo en dos registries (típicamente
220
+ npmjs.org como canónico y GitHub Packages como mirror), la coordinación de
221
+ versiones tiene reglas distintas a un publish simple.
222
+
223
+ ### NUNCA: reintentar la misma versión cuando uno de los registries ya la aceptó
224
+
225
+ **Problema**: el publish dual falló en uno de los dos registries pero el otro
226
+ quedó publicado correctamente. La intuición lleva a "republicar la misma versión"
227
+ después de arreglar el problema. Esto NO funciona: ningún registry permite
228
+ sobreescribir una versión ya publicada (es la garantía de inmutabilidad de
229
+ paquetes). El publish al registry que ya tiene esa versión devuelve:
230
+
231
+ ```
232
+ npm error You cannot publish over the previously published versions: X.Y.Z
233
+ ```
234
+
235
+ ```
236
+ # MAL — reintentar 1.1.0 porque GitHub Packages la tiene pero npmjs no
237
+ npm publish --registry=https://registry.npmjs.org/ # podría funcionar
238
+ npm publish --registry=https://npm.pkg.github.com # FALLA: ya existe 1.1.0
239
+ ```
240
+
241
+ ```
242
+ # BIEN — bumpear PATCH y publicar solo al registry faltante
243
+ # 1.1.0 → 1.1.1 en package.json + plugin.json + lock + headers de docs
244
+ node scripts/publicar.js --solo-npmjs # solo al que falta
245
+ ```
246
+
247
+ **Regla**: si un publish dual falla en el registry A pero queda publicado en B,
248
+ bumpear PATCH inmediatamente y publicar solo a A. Documentar en CHANGELOG que
249
+ es un republish exclusivo de coordinación entre registries (sin cambios funcionales).
250
+
251
+ ### NUNCA: usar `npm login` con GitHub Packages
252
+
253
+ **Problema**: GitHub Packages NO soporta `npm login` (ni el flujo web OAuth ni el
254
+ fallback CouchDB de creación de usuarios). Ejecutar `npm login --registry=https://npm.pkg.github.com`
255
+ devuelve 404 en `/-/v1/login` y luego 403 en el `PUT /-/user/...`. La autenticación
256
+ a GitHub Packages se hace EXCLUSIVAMENTE con un Personal Access Token de GitHub
257
+ configurado como `_authToken` directamente en `~/.npmrc`.
258
+
259
+ ```bash
260
+ # MAL — esto siempre devuelve 403
261
+ npm login --registry=https://npm.pkg.github.com
262
+
263
+ # BIEN — agregar el PAT al ~/.npmrc manualmente
264
+ echo "//npm.pkg.github.com/:_authToken=ghp_xxxxxxxx" >> ~/.npmrc
265
+ npm whoami --registry=https://npm.pkg.github.com # → tu-usuario-github
266
+ ```
267
+
268
+ El token requiere los scopes `read:packages` y `write:packages` en GitHub
269
+ (Settings → Developer settings → Personal access tokens).
270
+
271
+ ### SIEMPRE: diagnosticar auth con `npm whoami` antes de `npm login`
272
+
273
+ **Cuándo aplicar**: cuando un publish falla con "no autenticado" o 401/403.
274
+ **Beneficio**: distingue entre "sin token", "token expirado", "cuenta sin permiso
275
+ al scope" y "registry equivocado" sin abrir el flujo interactivo de login.
276
+
277
+ ```bash
278
+ # Diagnóstico estructurado
279
+ npm whoami --registry=https://registry.npmjs.org/
280
+
281
+ # Resultado posible 1: nombre de usuario → autenticado correctamente
282
+ # Resultado posible 2: 401 Unauthorized → token expirado/inválido
283
+ # → fix: npm login --registry=https://registry.npmjs.org/
284
+ # Resultado posible 3: 404 → registry incorrecto
285
+ # Resultado posible 4: nombre distinto al esperado → cuenta sin permiso
286
+ # → fix: verificar dueño del scope con
287
+ # npm owner ls @scope/paquete --registry=https://registry.npmjs.org/
288
+ ```
289
+
290
+ Ningún publisher debería hacer `npm login` sin antes hacer `npm whoami`. El whoami
291
+ es no-destructivo y revela la causa raíz; el login interactivo solo cubre el caso
292
+ de token inválido.
293
+
294
+ ---
295
+
212
296
  ## Herramientas recomendadas
213
297
 
214
298
  | Herramienta | Uso |
@@ -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 };