@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.
- package/CLAUDE.md +8 -5
- package/README.md +3 -3
- package/agentes/accesibilidad-wcag-swl.md +5 -7
- package/agentes/arquitecto-swl.md +5 -3
- package/agentes/auto-evolucion-swl.md +42 -12
- package/agentes/backend-api-swl.md +5 -3
- package/agentes/backend-csharp-swl.md +5 -3
- package/agentes/backend-go-swl.md +5 -3
- package/agentes/backend-java-swl.md +5 -3
- package/agentes/backend-node-swl.md +5 -3
- package/agentes/backend-python-swl.md +5 -3
- package/agentes/backend-rust-swl.md +5 -3
- package/agentes/backend-workers-swl.md +5 -3
- package/agentes/cloud-infra-swl.md +5 -6
- package/agentes/consolidador-swl.md +5 -3
- package/agentes/datos-swl.md +5 -7
- package/agentes/depurador-swl.md +6 -3
- package/agentes/devops-ci-swl.md +5 -3
- package/agentes/disenador-ui-swl.md +5 -7
- package/agentes/documentador-swl.md +5 -3
- package/agentes/frontend-angular-swl.md +5 -11
- package/agentes/frontend-css-swl.md +5 -9
- package/agentes/frontend-react-swl.md +5 -9
- package/agentes/frontend-swl.md +5 -9
- package/agentes/frontend-tailwind-swl.md +5 -9
- package/agentes/implementador-swl.md +6 -3
- package/agentes/investigador-swl.md +5 -3
- package/agentes/investigador-ux-swl.md +5 -9
- package/agentes/llm-apps-swl.md +5 -3
- package/agentes/migrador-swl.md +6 -3
- package/agentes/mobile-android-swl.md +5 -3
- package/agentes/mobile-cross-swl.md +5 -3
- package/agentes/mobile-ios-swl.md +5 -3
- package/agentes/mobile-testing-swl.md +5 -3
- package/agentes/notificador-swl.md +5 -3
- package/agentes/observabilidad-swl.md +5 -3
- package/agentes/orquestador-swl.md +29 -8
- package/agentes/pagos-swl.md +5 -3
- package/agentes/perfilador-usuario-swl.md +4 -2
- package/agentes/planificador-swl.md +5 -3
- package/agentes/producto-prd-swl.md +5 -3
- package/agentes/red-team-swl.md +4 -2
- package/agentes/release-manager-swl.md +6 -8
- package/agentes/rendimiento-swl.md +5 -6
- package/agentes/resolutor-build-swl.md +5 -3
- package/agentes/revisor-angular-swl.md +5 -3
- package/agentes/revisor-codigo-swl.md +90 -4
- package/agentes/revisor-csharp-swl.md +5 -3
- package/agentes/revisor-go-swl.md +5 -3
- package/agentes/revisor-java-swl.md +5 -3
- package/agentes/revisor-kotlin-swl.md +5 -3
- package/agentes/revisor-nextjs-swl.md +5 -3
- package/agentes/revisor-php-swl.md +5 -3
- package/agentes/revisor-react-swl.md +5 -3
- package/agentes/revisor-rust-swl.md +5 -3
- package/agentes/revisor-seguridad-swl.md +5 -3
- package/agentes/revisor-swift-swl.md +5 -3
- package/agentes/revisor-typescript-swl.md +5 -3
- package/agentes/sre-swl.md +5 -3
- package/agentes/tdd-qa-swl.md +5 -3
- package/agentes/ux-disenador-swl.md +5 -9
- package/comandos/swl/evaluar-skill.md +18 -0
- package/comandos/swl/evolucion-estado.md +49 -0
- package/comandos/swl/release.md +77 -1
- package/comandos/swl/salud.md +23 -0
- package/habilidades/checklist-seguridad/SKILL.md +57 -1
- package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
- package/habilidades/fastapi-experto/SKILL.md +10 -1
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/manejo-errores/SKILL.md +63 -4
- package/habilidades/patrones-python/SKILL.md +5 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/release-semver/SKILL.md +85 -1
- package/hooks/auto-evolucion.js +35 -1
- package/hooks/clasificador-mensajes.js +50 -3
- package/hooks/lib/agent-routing.js +107 -0
- package/hooks/lib/delegation-tracker.js +162 -44
- package/hooks/lib/evolution-tracker.js +12 -3
- package/hooks/lib/memory-search.js +59 -1
- package/hooks/lib/nudge-tracker.js +10 -1
- package/hooks/lib/provenance-tracker.js +11 -3
- package/hooks/lib/text-similarity.js +241 -0
- package/hooks/metricas-evolucion.js +168 -1
- package/hooks/monitor-contexto.js +54 -6
- package/hooks/preservar-estado-pre-compact.js +11 -1
- package/hooks/risk-scoring.js +10 -1
- package/hooks/tracking-costos.js +10 -1
- package/hooks/validar-formato-post-subagente.js +140 -0
- package/hooks/validar-memoria-hook.js +218 -0
- package/manifiestos/agent-output-schemas.json +57 -0
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +3 -0
- package/manifiestos/skills-lock.json +1065 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/reglas/arquitectura.md +20 -0
- package/reglas/fragmentos-compartidos.md +152 -0
- package/reglas/gobernanza.md +10 -1
- package/reglas/seguridad-agentes.md +12 -0
- package/reglas/skills-estandar.md +19 -0
- package/schemas/agent-frontmatter.schema.json +18 -0
- package/scripts/auditar-agentes-gaps.js +9 -1
- package/scripts/auditar-cobertura-frameworks.js +9 -1
- package/scripts/auditar-skills-gaps.js +9 -1
- package/scripts/bootstrap-instintos.js +11 -1
- package/scripts/generar-inventario.js +112 -9
- package/scripts/generar-matriz-lenguajes.js +271 -0
- package/scripts/generar-skills-lock.js +190 -0
- package/scripts/lib/estado.js +12 -2
- package/scripts/lib/gitignore-manifest.js +32 -2
- package/scripts/migrar-csv-a-array.js +168 -0
- package/scripts/migrar-fase-dominio.js +201 -0
- package/scripts/publicar.js +88 -18
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"SKILL.md": {
|
|
3
|
-
"evolved": true,
|
|
4
|
-
"evolvedFrom": "
|
|
5
|
-
"evolvedAt": "2026-
|
|
6
|
-
"evolvedBy": "aprender",
|
|
7
|
-
"evolvedNote": "
|
|
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.
|
|
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 |
|
package/hooks/auto-evolucion.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|