oficina-opencode 0.0.0-stage → 2.0.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.
- package/DESIGN.md +145 -0
- package/LICENSE +21 -0
- package/README.md +670 -2
- package/agents/oficina-architect.md +72 -0
- package/agents/oficina-commander.md +108 -0
- package/agents/oficina-critico.md +148 -0
- package/agents/oficina-director.md +160 -0
- package/agents/oficina-guardian.md +128 -0
- package/agents/oficina-integrator.md +98 -0
- package/agents/oficina-verificador.md +88 -0
- package/commands/oficina.md +36 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +394 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/briefing.d.ts +115 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +410 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/bus.d.ts +132 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +250 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/catalog.d.ts +303 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +816 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/dispatch.d.ts +110 -0
- package/dist/core/dispatch.d.ts.map +1 -0
- package/dist/core/dispatch.js +301 -0
- package/dist/core/dispatch.js.map +1 -0
- package/dist/core/factory.d.ts +74 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +243 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/fsx.d.ts +39 -0
- package/dist/core/fsx.d.ts.map +1 -0
- package/dist/core/fsx.js +113 -0
- package/dist/core/fsx.js.map +1 -0
- package/dist/core/gates.d.ts +97 -0
- package/dist/core/gates.d.ts.map +1 -0
- package/dist/core/gates.js +467 -0
- package/dist/core/gates.js.map +1 -0
- package/dist/core/leases.d.ts +46 -0
- package/dist/core/leases.d.ts.map +1 -0
- package/dist/core/leases.js +94 -0
- package/dist/core/leases.js.map +1 -0
- package/dist/core/log.d.ts +30 -0
- package/dist/core/log.d.ts.map +1 -0
- package/dist/core/log.js +43 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/mesa.d.ts +82 -0
- package/dist/core/mesa.d.ts.map +1 -0
- package/dist/core/mesa.js +178 -0
- package/dist/core/mesa.js.map +1 -0
- package/dist/core/office.d.ts +208 -0
- package/dist/core/office.d.ts.map +1 -0
- package/dist/core/office.js +618 -0
- package/dist/core/office.js.map +1 -0
- package/dist/core/paths.d.ts +67 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +136 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/retry.d.ts +41 -0
- package/dist/core/retry.d.ts.map +1 -0
- package/dist/core/retry.js +86 -0
- package/dist/core/retry.js.map +1 -0
- package/dist/core/roster.d.ts +47 -0
- package/dist/core/roster.d.ts.map +1 -0
- package/dist/core/roster.js +146 -0
- package/dist/core/roster.js.map +1 -0
- package/dist/core/router.d.ts +67 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +186 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/state.d.ts +89 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +303 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/types.d.ts +333 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +27 -0
- package/dist/core/types.js.map +1 -0
- package/dist/hooks/compacting.d.ts +18 -0
- package/dist/hooks/compacting.d.ts.map +1 -0
- package/dist/hooks/compacting.js +72 -0
- package/dist/hooks/compacting.js.map +1 -0
- package/dist/hooks/scopeGuard.d.ts +39 -0
- package/dist/hooks/scopeGuard.d.ts.map +1 -0
- package/dist/hooks/scopeGuard.js +114 -0
- package/dist/hooks/scopeGuard.js.map +1 -0
- package/dist/hooks/sessionIdle.d.ts +35 -0
- package/dist/hooks/sessionIdle.d.ts.map +1 -0
- package/dist/hooks/sessionIdle.js +110 -0
- package/dist/hooks/sessionIdle.js.map +1 -0
- package/dist/hooks/systemTransform.d.ts +18 -0
- package/dist/hooks/systemTransform.d.ts.map +1 -0
- package/dist/hooks/systemTransform.js +63 -0
- package/dist/hooks/systemTransform.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +854 -0
- package/dist/tools/index.js.map +1 -0
- package/docs/ARCHITECTURE.md +258 -0
- package/docs/QUE-ES.md +337 -0
- package/examples/opencode.json +7 -0
- package/package.json +81 -4
- package/skills/oficina/README.md +120 -0
- package/skills/oficina/SKILL.md +251 -0
- package/skills/oficina/reference/mesh.md +345 -0
- package/skills/oficina/reference/protocol.md +342 -0
- package/skills/oficina/reference/roles.md +171 -0
- package/skills/oficina/reference/templates.md +278 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo: resolución de roles desde agency-agents.
|
|
3
|
+
*
|
|
4
|
+
* ## Por qué esto no es un `grep` de descripciones
|
|
5
|
+
*
|
|
6
|
+
* El repo de agency-agents tiene 306 agentes en 25 divisiones. Cada uno trae
|
|
7
|
+
* un *briefing* de 10 secciones, y la especialidad real NO está en el
|
|
8
|
+
* `description` del frontmatter: está en el cuerpo.
|
|
9
|
+
*
|
|
10
|
+
* Un agente de statistics, por ejemplo, declara:
|
|
11
|
+
* - description: "Expert in quantitative research methodology..."
|
|
12
|
+
* - Core Mission → "Pressure-Test Quantitative Claims", "Design Sound Studies"
|
|
13
|
+
* - Technical Deliverables → "Claim Interrogation Framework", "Effect Size Report"
|
|
14
|
+
* - Advanced Capabilities → "Causal Inference", "Experimental Design"
|
|
15
|
+
*
|
|
16
|
+
* La `description` dice "de qué se ocupa". El cuerpo dice **en qué es
|
|
17
|
+
* exactamente bueno**. Un matcher que solo mira la description no puede
|
|
18
|
+
* distinguir a un metodólogo de un analista de datos, aunque uno de los dos
|
|
19
|
+
* sea exactamente lo que la card necesita.
|
|
20
|
+
*
|
|
21
|
+
* Por eso el parser extrae las secciones del cuerpo y el matching pondera
|
|
22
|
+
* cada campo por lo que aporta.
|
|
23
|
+
*/
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { listDirs, listFiles, readText } from "./fsx.js";
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Parser de frontmatter y de secciones
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
/** Parsea frontmatter YAML simple (clave: valor, sin anidación). */
|
|
30
|
+
export function parseFrontmatter(content) {
|
|
31
|
+
if (!content.startsWith("---"))
|
|
32
|
+
return { data: {}, body: content };
|
|
33
|
+
const end = content.indexOf("\n---", 3);
|
|
34
|
+
if (end === -1)
|
|
35
|
+
return { data: {}, body: content };
|
|
36
|
+
const block = content.slice(3, end);
|
|
37
|
+
const body = content.slice(end + 4);
|
|
38
|
+
const data = {};
|
|
39
|
+
for (const line of block.split("\n")) {
|
|
40
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
41
|
+
if (m)
|
|
42
|
+
data[m[1].toLowerCase()] = m[2].trim().replace(/^["']|["']$/g, "");
|
|
43
|
+
}
|
|
44
|
+
return { data, body };
|
|
45
|
+
}
|
|
46
|
+
/** Limpia un encabezado de sección de los emoji y espacios que agency-agents usa. */
|
|
47
|
+
export function normalizeHeading(raw) {
|
|
48
|
+
return raw
|
|
49
|
+
.replace(/^#+\s*/, "")
|
|
50
|
+
.replace(/[^\p{L}\p{N}\s&-]/gu, "")
|
|
51
|
+
.replace(/\s+/g, " ")
|
|
52
|
+
.trim();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Divide el cuerpo en secciones H2.
|
|
56
|
+
*
|
|
57
|
+
* Los encabezados de agency-agents vienen con emoji (`## 🎯 Your Identity &
|
|
58
|
+
* Memory`) o lisos (`## Core Mission`). Se normalizan para que el matching
|
|
59
|
+
* sea estable.
|
|
60
|
+
*/
|
|
61
|
+
export function parseSections(body) {
|
|
62
|
+
const sections = {};
|
|
63
|
+
const lines = body.split("\n");
|
|
64
|
+
let currentKey = "";
|
|
65
|
+
let buffer = [];
|
|
66
|
+
const flush = () => {
|
|
67
|
+
if (currentKey)
|
|
68
|
+
sections[currentKey] = buffer.join("\n").trim();
|
|
69
|
+
};
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (/^##\s/.test(line)) {
|
|
72
|
+
flush();
|
|
73
|
+
currentKey = normalizeHeading(line);
|
|
74
|
+
buffer = [];
|
|
75
|
+
}
|
|
76
|
+
else if (currentKey) {
|
|
77
|
+
buffer.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
flush();
|
|
81
|
+
return sections;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Busca una sección por cualquiera de sus nombres posibles.
|
|
85
|
+
*
|
|
86
|
+
* La estructura de agency-agents tiene variantes con y sin el "Your", y
|
|
87
|
+
* con y sin emoji. En vez de hardcodear 20 nombres, se busca por coincidencia
|
|
88
|
+
* de palabra clave.
|
|
89
|
+
*/
|
|
90
|
+
export function findSection(sections, ...keywords) {
|
|
91
|
+
const keys = Object.keys(sections);
|
|
92
|
+
for (const keyword of keywords) {
|
|
93
|
+
const match = keys.find((k) => k.toLowerCase().includes(keyword.toLowerCase()));
|
|
94
|
+
if (match)
|
|
95
|
+
return sections[match];
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/** Extrae los subtítulos H3 de un bloque de texto. */
|
|
100
|
+
export function extractSubheadings(block) {
|
|
101
|
+
if (!block)
|
|
102
|
+
return [];
|
|
103
|
+
return block
|
|
104
|
+
.split("\n")
|
|
105
|
+
.filter((line) => /^###\s/.test(line))
|
|
106
|
+
.map((line) => normalizeHeading(line))
|
|
107
|
+
.filter((s) => s.length > 2);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Extrae las etiquetas de una sección "Core Mission".
|
|
111
|
+
*
|
|
112
|
+
* La sección viene en dos formatos y ambos son válidos en el repo real:
|
|
113
|
+
*
|
|
114
|
+
* 1. **Subtítulos H3** — `### Pressure-Test Quantitative Claims`
|
|
115
|
+
* 2. **Lista de bullets** — `- **Subvert Default Biases**: Ensure generated...`
|
|
116
|
+
*
|
|
117
|
+
* Se intentan los H3 primero. Si no hay, se cae a los bullets, y de cada
|
|
118
|
+
* bullet se extrae la etiqueta: la parte en negrita si la hay, o las
|
|
119
|
+
* primeras palabras antes del primer `:`.
|
|
120
|
+
*
|
|
121
|
+
* Sin este fallback, la mitad del catálogo queda sin misión declarada y el
|
|
122
|
+
* matching pierde la señal más fuerte que tiene.
|
|
123
|
+
*/
|
|
124
|
+
export function extractMissionLabels(block) {
|
|
125
|
+
const headings = extractSubheadings(block);
|
|
126
|
+
if (headings.length > 0)
|
|
127
|
+
return headings;
|
|
128
|
+
const items = extractListItems(block);
|
|
129
|
+
if (items.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
return items
|
|
132
|
+
.map((item) => {
|
|
133
|
+
// `**Label**: resto` → `Label`
|
|
134
|
+
const bold = /^\*\*(.+?)\*\*/.exec(item);
|
|
135
|
+
if (bold)
|
|
136
|
+
return bold[1].trim();
|
|
137
|
+
// `Label: resto` → `Label`
|
|
138
|
+
const colon = item.split(":")[0];
|
|
139
|
+
if (colon && colon.length <= 80 && colon.trim().length > 2)
|
|
140
|
+
return colon.trim();
|
|
141
|
+
// Si no, las primeras palabras.
|
|
142
|
+
return item.split(/\s+/).slice(0, 6).join(" ");
|
|
143
|
+
})
|
|
144
|
+
.filter((label) => label.length > 2);
|
|
145
|
+
}
|
|
146
|
+
/** Extrae los items numerados o con guion de un bloque de texto. */
|
|
147
|
+
export function extractListItems(block) {
|
|
148
|
+
if (!block)
|
|
149
|
+
return [];
|
|
150
|
+
return block
|
|
151
|
+
.split("\n")
|
|
152
|
+
.map((line) => line.trim())
|
|
153
|
+
.filter((line) => /^(\d+[.)]|[-*])\s+/.test(line))
|
|
154
|
+
.map((line) => line.replace(/^(\d+[.)]|[-*])\s+/, "").trim())
|
|
155
|
+
.filter((s) => s.length > 4);
|
|
156
|
+
}
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Taxonomía
|
|
159
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Mapa de tokens a dominios.
|
|
162
|
+
*
|
|
163
|
+
* Solo se usa para el fallback por tokens (cuando no hay match estructural).
|
|
164
|
+
* El matching principal es por secciones.
|
|
165
|
+
*/
|
|
166
|
+
export const CAPABILITY_TAXONOMY = {
|
|
167
|
+
react: ["react", "component", "jsx", "hooks", "frontend", "ui", "web", "spa", "dom", "view", "modern"],
|
|
168
|
+
css: ["css", "styling", "layout", "tailwind", "design-system", "animation", "motion", "theme", "token"],
|
|
169
|
+
accessibility: ["accessibility", "a11y", "aria", "wcag", "screen-reader", "keyboard", "semantic", "section-508", "conformance"],
|
|
170
|
+
typescript: ["typescript", "ts", "type-safety", "typecheck", "generics", "type"],
|
|
171
|
+
backend: ["backend", "server", "api", "endpoint", "service", "rest", "graphql", "microservice", "gateway", "contract"],
|
|
172
|
+
database: ["database", "sql", "postgres", "postgresql", "prisma", "orm", "schema", "migration", "index", "query", "reliability", "availability", "tuning", "partitioning"],
|
|
173
|
+
security: ["security", "auth", "authentication", "authorization", "vulnerability", "owasp", "encryption", "threat", "vuln", "pentest", "xss", "csrf", "injection", "sanitize", "hardening", "audit", "compliance"],
|
|
174
|
+
testing: ["test", "testing", "qa", "e2e", "end-to-end", "end2end", "unit-test", "coverage", "playwright", "vitest", "fixture", "assertion", "browser", "checkout", "integration-test", "acceptance-test", "smoke-test"],
|
|
175
|
+
performance: ["performance", "optimization", "profiling", "caching", "latency", "benchmark", "throughput", "slowdown", "speed"],
|
|
176
|
+
architecture: ["architecture", "design", "pattern", "scalability", "system-design", "refactor", "modular", "blueprint", "blueprints"],
|
|
177
|
+
devops: ["devops", "ci", "cd", "deployment", "docker", "kubernetes", "infrastructure", "pipeline", "terraform", "automator", "release"],
|
|
178
|
+
documentation: ["documentation", "docs", "readme", "changelog", "api-docs", "technical-writing", "writer"],
|
|
179
|
+
data: ["data", "analytics", "etl", "dataset", "machine-learning", "ml", "model", "ingest", "aggregate", "warehouse", "stream"],
|
|
180
|
+
game: ["game", "gamedev", "unity", "unreal", "godot", "gameplay", "roblox", "physics", "blender"],
|
|
181
|
+
finance: ["finance", "financial", "billing", "accounting", "trading", "payment", "revenue", "pricing", "cost"],
|
|
182
|
+
gis: ["gis", "geospatial", "mapping", "spatial", "geography", "cartography", "tile", "shapefile", "coordinate"],
|
|
183
|
+
academic: ["academic", "research", "paper", "citation", "literature", "thesis", "peer-review", "statistic", "methodology", "experiment", "inference", "study"],
|
|
184
|
+
mobile: ["mobile", "ios", "android", "flutter", "react-native", "swift", "kotlin", "desktop"],
|
|
185
|
+
marketing: ["marketing", "seo", "campaign", "copywriting", "brand", "social", "email", "growth", "content", "copy", "viral"],
|
|
186
|
+
sales: ["sales", "crm", "lead", "pipeline", "prospect", "negotiation", "outreach", "closing", "demo"],
|
|
187
|
+
support: ["support", "helpdesk", "ticket", "customer", "faq", "troubleshoot", "onboarding"],
|
|
188
|
+
product: ["product", "roadmap", "prd", "requirement", "backlog", "user-story", "feature", "discovery", "prioritiz"],
|
|
189
|
+
strategy: ["strategy", "positioning", "market", "competitive", "portfolio", "moat"],
|
|
190
|
+
project: ["project", "milestone", "timeline", "gantt", "sprint", "kanban", "resource", "estimation", "plan"],
|
|
191
|
+
spatial: ["spatial", "3d", "vr", "ar", "webgl", "threejs", "unity", "shader", "rendering", "material", "mesh"],
|
|
192
|
+
healthcare: ["health", "clinical", "patient", "medical", "hipaa", "ehr", "diagnosis"],
|
|
193
|
+
research: ["research", "analyze", "synthesis", "review", "meta-analysis", "intelligence", "find-bugs"],
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Afinidad de división.
|
|
197
|
+
*
|
|
198
|
+
* Si la card pide capacidades de un dominio, la división homónima del
|
|
199
|
+
* catálogo es una señal fuerte. "gis" → división `gis`. "game" →
|
|
200
|
+
* `game-development`. Es una señal barata y muy precisa.
|
|
201
|
+
*/
|
|
202
|
+
export const DIVISION_AFFINITY = {
|
|
203
|
+
gis: ["gis", "spatial", "geospatial", "map", "cartography", "tile"],
|
|
204
|
+
"game-development": ["game", "unity", "unreal", "godot", "roblox", "gameplay", "physics"],
|
|
205
|
+
"spatial-computing": ["spatial", "3d", "vr", "ar", "webgl", "shader", "blender", "rendering", "mesh"],
|
|
206
|
+
security: ["security", "auth", "vulnerability", "owasp", "threat", "pentest", "audit", "compliance", "hardening"],
|
|
207
|
+
testing: ["test", "qa", "e2e", "coverage", "fixture", "assertion", "browser"],
|
|
208
|
+
marketing: ["marketing", "seo", "campaign", "copywriting", "brand", "growth", "content", "viral"],
|
|
209
|
+
"paid-media": ["ads", "ppc", "campaign", "spend", "media"],
|
|
210
|
+
sales: ["sales", "crm", "lead", "pipeline", "prospect", "outreach", "closing", "demo"],
|
|
211
|
+
product: ["product", "roadmap", "prd", "requirement", "backlog", "user-story", "discovery", "prioritiz"],
|
|
212
|
+
support: ["support", "helpdesk", "ticket", "customer", "faq", "troubleshoot", "onboarding"],
|
|
213
|
+
strategy: ["strategy", "positioning", "market", "competitive", "portfolio", "pricing", "moat"],
|
|
214
|
+
"project-management": ["project", "milestone", "timeline", "sprint", "kanban", "resource", "estimation", "plan"],
|
|
215
|
+
academic: ["academic", "research", "paper", "citation", "thesis", "statistic", "methodology", "experiment", "inference"],
|
|
216
|
+
research: ["research", "analyze", "synthesis", "review", "meta-analysis", "intelligence", "find-bugs"],
|
|
217
|
+
healthcare: ["health", "clinical", "patient", "medical", "hipaa", "ehr", "diagnosis"],
|
|
218
|
+
finance: ["finance", "billing", "accounting", "payment", "revenue", "cost", "pricing"],
|
|
219
|
+
design: ["design", "ux", "ui", "visual", "brand", "typography", "layout", "motion", "css", "theme", "token", "copy"],
|
|
220
|
+
engineering: ["code", "develop", "build", "implement", "refactor", "api", "service", "codebase", "backend", "frontend", "data", "type", "test", "architecture", "performance", "mobile", "security", "database", "devops", "deployment", "docker", "kubernetes", "cicd", "infrastructure", "pipeline", "release", "automation", "automator", "sre", "platform"],
|
|
221
|
+
specialized: ["specialist", "optimizer", "remediation", "automation", "automator", "intelligence"],
|
|
222
|
+
};
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
// Extracción de capacidades
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
/** Stopwords que no aportan señal al matching. */
|
|
227
|
+
const STOPWORDS = new Set([
|
|
228
|
+
"the", "and", "for", "with", "that", "this", "from", "into", "your", "you",
|
|
229
|
+
"are", "not", "but", "can", "will", "was", "has", "have", "its", "our",
|
|
230
|
+
"using", "use", "used", "make", "make", "them", "then", "than", "when",
|
|
231
|
+
"what", "which", "while", "who", "how", "all", "any", "one", "two", "new",
|
|
232
|
+
"own", "same", "also", "more", "most", "other", "some", "such", "only",
|
|
233
|
+
]);
|
|
234
|
+
/**
|
|
235
|
+
* Convierte una frase en un conjunto de tokens con señal.
|
|
236
|
+
*
|
|
237
|
+
* Se filtran stopwords y tokens de menos de 3 caracteres. Los tokens de
|
|
238
|
+
* más de 3 caracteres se consideran "concepts" para el matching.
|
|
239
|
+
*/
|
|
240
|
+
export function capabilityTokens(phrase) {
|
|
241
|
+
const tokens = new Set();
|
|
242
|
+
for (const raw of phrase.toLowerCase().split(/[^a-z0-9+#.-]+/)) {
|
|
243
|
+
const token = raw.replace(/^[.-]+|[.-]+$/g, "");
|
|
244
|
+
if (token.length < 3)
|
|
245
|
+
continue;
|
|
246
|
+
if (STOPWORDS.has(token))
|
|
247
|
+
continue;
|
|
248
|
+
tokens.add(token);
|
|
249
|
+
}
|
|
250
|
+
return tokens;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Infiere capabilities de un agente.
|
|
254
|
+
*
|
|
255
|
+
* Tres fuentes, en orden de valor:
|
|
256
|
+
* 1. Los subtítulos de "Core Mission" — **la** especialidad del agente.
|
|
257
|
+
* 2. Los subtítulos de "Advanced Capabilities" — lo que maneja mejor.
|
|
258
|
+
* 3. Los subtítulos de "Technical Deliverables" — lo que produce.
|
|
259
|
+
*
|
|
260
|
+
* Más los tokens de la descripción y del nombre, como respaldo.
|
|
261
|
+
*/
|
|
262
|
+
export function extractCapabilities(agent) {
|
|
263
|
+
const found = new Set();
|
|
264
|
+
for (const phrase of [...agent.missions, ...agent.advanced, ...agent.deliverables]) {
|
|
265
|
+
for (const token of capabilityTokens(phrase))
|
|
266
|
+
found.add(token);
|
|
267
|
+
}
|
|
268
|
+
for (const token of capabilityTokens(agent.description))
|
|
269
|
+
found.add(token);
|
|
270
|
+
for (const token of capabilityTokens(agent.displayName))
|
|
271
|
+
found.add(token);
|
|
272
|
+
return [...found].sort();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Convierte los subtítulos de misión en etiquetas legibles.
|
|
276
|
+
*
|
|
277
|
+
* "Pressure-Test Quantitative Claims" → "pressure-test quantitative claims".
|
|
278
|
+
* Se usan como capabilitiesdirectly comparables con lo que pide la card.
|
|
279
|
+
*/
|
|
280
|
+
export function missionTags(missions) {
|
|
281
|
+
return missions
|
|
282
|
+
.map((m) => m.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").trim())
|
|
283
|
+
.filter((m) => m.length > 2);
|
|
284
|
+
}
|
|
285
|
+
/** Divisiones que no aportan agentes de código. */
|
|
286
|
+
const NON_CODE_DIVISIONS = new Set([
|
|
287
|
+
"examples", "playbooks", "runbooks", "scripts", ".github", "integrations",
|
|
288
|
+
]);
|
|
289
|
+
export async function scanCatalog(options) {
|
|
290
|
+
const allDivisions = await listDirs(options.root);
|
|
291
|
+
const divisions = options.divisions?.length
|
|
292
|
+
? options.divisions
|
|
293
|
+
: allDivisions.filter((d) => !NON_CODE_DIVISIONS.has(d) && !d.startsWith("."));
|
|
294
|
+
const out = [];
|
|
295
|
+
for (const division of divisions) {
|
|
296
|
+
const dir = path.join(options.root, division);
|
|
297
|
+
const files = await listFiles(dir);
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
if (!file.endsWith(".md"))
|
|
300
|
+
continue;
|
|
301
|
+
if (/^(readme|changelog|contributing|license|security|pull_request)/i.test(file))
|
|
302
|
+
continue;
|
|
303
|
+
const full = path.join(dir, file);
|
|
304
|
+
const content = await readText(full);
|
|
305
|
+
if (!content)
|
|
306
|
+
continue;
|
|
307
|
+
const { data, body } = parseFrontmatter(content);
|
|
308
|
+
const sections = parseSections(body);
|
|
309
|
+
const slug = file.replace(/\.md$/, "");
|
|
310
|
+
const displayName = data.name || titleize(slug);
|
|
311
|
+
const description = data.description || firstParagraph(body);
|
|
312
|
+
const missions = extractMissionLabels(findSection(sections, "Core Mission", "Mission"));
|
|
313
|
+
const deliverables = extractMissionLabels(findSection(sections, "Technical Deliverables", "Deliverables"));
|
|
314
|
+
const advanced = extractMissionLabels(findSection(sections, "Advanced Capabilities", "Advanced"));
|
|
315
|
+
const rules = extractListItems(findSection(sections, "Critical Rules", "Rules"));
|
|
316
|
+
const partial = { displayName, description, missions, deliverables, advanced };
|
|
317
|
+
out.push({
|
|
318
|
+
slug,
|
|
319
|
+
displayName,
|
|
320
|
+
description,
|
|
321
|
+
vibe: data.vibe ?? "",
|
|
322
|
+
division,
|
|
323
|
+
path: path.join(division, file),
|
|
324
|
+
capabilities: extractCapabilities(partial),
|
|
325
|
+
missions,
|
|
326
|
+
deliverables,
|
|
327
|
+
advanced,
|
|
328
|
+
rules,
|
|
329
|
+
sections,
|
|
330
|
+
sourcePrompt: body.trim(),
|
|
331
|
+
searchText: `${displayName} ${description} ${body}`.toLowerCase(),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/** Primer párrafo no vacío del body, como descripción de respaldo. */
|
|
338
|
+
function firstParagraph(body) {
|
|
339
|
+
for (const para of body.split(/\n\s*\n/)) {
|
|
340
|
+
const trimmed = para.trim();
|
|
341
|
+
if (trimmed.length > 20 && !trimmed.startsWith("#"))
|
|
342
|
+
return trimmed.replace(/\s+/g, " ").slice(0, 400);
|
|
343
|
+
}
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
/** "frontend-developer" → "Frontend Developer". */
|
|
347
|
+
export function titleize(slug) {
|
|
348
|
+
return slug
|
|
349
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
|
|
352
|
+
.join(" ");
|
|
353
|
+
}
|
|
354
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
+
// Matching
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
/**
|
|
358
|
+
* Pesos de cada señal en el score.
|
|
359
|
+
*
|
|
360
|
+
* El orden reflects dónde está la información real:
|
|
361
|
+
*
|
|
362
|
+
* 1. **El nombre del rol** (`engineering-frontend-developer`) es la etiqueta
|
|
363
|
+
* más literal de la especialidad que existe. ElCommander también pide
|
|
364
|
+
* roles por nombre, así que es la señal más alineada entre las dos
|
|
365
|
+
* listas.
|
|
366
|
+
*
|
|
367
|
+
* 2. **La misión declarada** ("Create Modern Web Applications") es lo que el
|
|
368
|
+
* agente dice que sabe hacer. El overlap literal es preciso; el
|
|
369
|
+
* conceptual (con taxonomía) bridgea sinónimos.
|
|
370
|
+
*
|
|
371
|
+
* 3. **El cuerpo completo** es un pajaro: 300 líneas donde cualquier
|
|
372
|
+
* palabra genérica ("review", "design", "audit") aparece en todos. Por
|
|
373
|
+
* eso casi no pesa. Con peso alto, un "Legal Document Review" ganaba el
|
|
374
|
+
* match de "revisar la seguridad de una API" por la palabra "review".
|
|
375
|
+
*/
|
|
376
|
+
export const MATCH_WEIGHTS = {
|
|
377
|
+
/** El nombre del rol y su display name. */
|
|
378
|
+
name: 0.35,
|
|
379
|
+
/** La misión, overlap literal. */
|
|
380
|
+
missionLiteral: 0.28,
|
|
381
|
+
/** La misión, overlap conceptual (taxonomía + lematización). */
|
|
382
|
+
missionConcept: 0.18,
|
|
383
|
+
/** Capabilities inferidas del cuerpo. */
|
|
384
|
+
capabilities: 0.05,
|
|
385
|
+
/** Lo que el agente sabe producir. */
|
|
386
|
+
deliverables: 0.03,
|
|
387
|
+
/**
|
|
388
|
+
* La división: contexto, no especialidad — pero señal fuerte. Una card de
|
|
389
|
+
* e2e testing tiene que ir a la división `testing`, no a `engineering`.
|
|
390
|
+
*/
|
|
391
|
+
division: 0.1,
|
|
392
|
+
/** El texto completo: ruido puro. */
|
|
393
|
+
text: 0.01,
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Convierte una frase en un conjunto de tokens **y conceptos**.
|
|
397
|
+
*
|
|
398
|
+
* ## Por qué se conservan los tokens crudos
|
|
399
|
+
*
|
|
400
|
+
* La primera versión de esta función reemplazaba cada token por su grupo
|
|
401
|
+
* completo de alias. El efecto fue contraproducente: el objetivo
|
|
402
|
+
* `{playwright, e2e, testing, checkout, browser}` colapsaba a un solo
|
|
403
|
+
* concepto `testing`, y cualquier agente que mencionara "Testing" una vez
|
|
404
|
+
* (un code reviewer, por ejemplo)得分 100% de cobertura conceptual.
|
|
405
|
+
*
|
|
406
|
+
* La granularidad se pierde al colapsar. Por eso los tokens crudos se
|
|
407
|
+
* **conservan** y el concepto se **agrega**:
|
|
408
|
+
*
|
|
409
|
+
* - "web" → {web, react} (puentea con "frontend" y "react")
|
|
410
|
+
* - "Testing"→ {testing} (no colapsa a sus 13 alias)
|
|
411
|
+
*
|
|
412
|
+
* Ahora un agente tiene que coincidir en **varias** de las cosas pedidas,
|
|
413
|
+
* no en una sola.
|
|
414
|
+
*/
|
|
415
|
+
export function canonicalConcepts(phrase) {
|
|
416
|
+
const tokens = capabilityTokens(phrase);
|
|
417
|
+
const concepts = new Set(tokens);
|
|
418
|
+
for (const token of tokens) {
|
|
419
|
+
for (const [concept, aliases] of Object.entries(CAPABILITY_TAXONOMY)) {
|
|
420
|
+
if (aliases.includes(token)) {
|
|
421
|
+
concepts.add(concept);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return concepts;
|
|
427
|
+
}
|
|
428
|
+
/** Conceptos canónicos de un conjunto de tokens. */
|
|
429
|
+
function canonicalizeTokens(tokens) {
|
|
430
|
+
const concepts = new Set(tokens);
|
|
431
|
+
for (const token of tokens) {
|
|
432
|
+
for (const [concept, aliases] of Object.entries(CAPABILITY_TAXONOMY)) {
|
|
433
|
+
if (aliases.includes(token)) {
|
|
434
|
+
concepts.add(concept);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return concepts;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Sinónimos **por capability**.
|
|
443
|
+
*
|
|
444
|
+
* ## Por qué no alcanza con la taxonomía
|
|
445
|
+
*
|
|
446
|
+
* La taxonomía agrupa por *concepto*: "e2e" y "testing" caen en el mismo
|
|
447
|
+
* grupo. Con eso, un agente que dice "testing" matcheaba "e2e", y un PDF
|
|
448
|
+
* engine que menciona "Playwright" le ganaba a un Test Automation Engineer
|
|
449
|
+
* que decía "end-to-end" — que es la misma palabra escrita larga.
|
|
450
|
+
*
|
|
451
|
+
* Los sinónimos van **por capability**: "e2e" es "end-to-end", pero
|
|
452
|
+
* "e2e" NO es "playwright". Esa distinción es la que separa a un
|
|
453
|
+
* especialista de un generalista que menciona la tool de pasada.
|
|
454
|
+
*/
|
|
455
|
+
export const CAPABILITY_SYNONYMS = {
|
|
456
|
+
e2e: ["e2e", "end-to-end", "end2end", "end to end", "integration test", "acceptance test", "browser test"],
|
|
457
|
+
testing: ["test", "tests", "testing", "qa", "suite", "suites", "test suite", "spec", "specs"],
|
|
458
|
+
playwright: ["playwright"],
|
|
459
|
+
vitest: ["vitest"],
|
|
460
|
+
jest: ["jest"],
|
|
461
|
+
cypress: ["cypress"],
|
|
462
|
+
react: ["react", "jsx", "component", "components", "hook", "hooks"],
|
|
463
|
+
css: ["css", "style", "styles", "styling", "tailwind", "layout", "theme", "token", "tokens"],
|
|
464
|
+
accessibility: ["accessibility", "a11y", "aria", "wcag", "section 508", "assistive", "screen reader", "keyboard"],
|
|
465
|
+
security: ["security", "vulnerability", "vulnerabilities", "auth", "authentication", "authorization", "owasp", "threat", "injection", "xss", "csrf", "pentest", "hardening"],
|
|
466
|
+
api: ["api", "endpoint", "endpoints", "rest", "graphql", "openapi", "contract"],
|
|
467
|
+
database: ["database", "sql", "postgres", "postgresql", "mysql", "sqlite", "prisma", "orm", "schema", "migration", "query", "index", "partitioning"],
|
|
468
|
+
backend: ["backend", "server", "service", "services", "microservice", "worker"],
|
|
469
|
+
frontend: ["frontend", "ui", "web", "spa", "browser"],
|
|
470
|
+
typescript: ["typescript", "ts", "type", "types", "generic", "generics", "typecheck"],
|
|
471
|
+
performance: ["performance", "optimize", "optimization", "profiling", "caching", "latency", "benchmark", "throughput"],
|
|
472
|
+
architecture: ["architecture", "pattern", "patterns", "design", "scalability", "system design", "refactor", "modular"],
|
|
473
|
+
devops: ["devops", "ci", "cd", "deployment", "deploy", "docker", "kubernetes", "infrastructure", "pipeline", "terraform", "release", "operations", "sre", "automation"],
|
|
474
|
+
documentation: ["documentation", "docs", "readme", "changelog", "api docs", "technical writing", "writer"],
|
|
475
|
+
cicd: ["cicd", "ci", "cd", "continuous integration", "continuous delivery", "continuous deployment", "pipeline", "release"],
|
|
476
|
+
data: ["data", "analytics", "etl", "dataset", "ingest", "aggregate", "warehouse", "pipeline", "stream", "streaming", "ml", "machine learning"],
|
|
477
|
+
accessibility_audit: ["audit", "auditor", "auditing", "audit", "review", "reviewer", "conformance", "compliance"],
|
|
478
|
+
gis: ["gis", "geospatial", "mapping", "map", "cartography", "tile", "shapefile", "coordinate", "spatial"],
|
|
479
|
+
design: ["design", "designer", "visual", "brand", "typography", "mockup"],
|
|
480
|
+
qa: ["qa", "quality assurance", "test", "testing", "review"],
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Sinónimos de una capability.
|
|
484
|
+
*
|
|
485
|
+
* Si la capability no está en la tabla, sus sinónimos son ella misma: el
|
|
486
|
+
* match es literal o lematizado.
|
|
487
|
+
*/
|
|
488
|
+
export function synonymsOf(capability) {
|
|
489
|
+
const lower = capability.toLowerCase().trim();
|
|
490
|
+
const explicit = CAPABILITY_SYNONYMS[lower];
|
|
491
|
+
if (explicit)
|
|
492
|
+
return new Set([lower, ...explicit]);
|
|
493
|
+
return new Set([lower]);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* ¿El agente menciona esta capability, por sinonimia o lematización?
|
|
497
|
+
*
|
|
498
|
+
* La capability puede ser una **frase** ("react components"), no un token
|
|
499
|
+
* suelto: se tokeniza y se busca cualquiera de sus partes. Un token solo
|
|
500
|
+
* ("react") se busca como synonym group.
|
|
501
|
+
*/
|
|
502
|
+
export function mentionsCapability(agentTokens, capability) {
|
|
503
|
+
const tokens = capabilityTokens(capability);
|
|
504
|
+
for (const token of tokens) {
|
|
505
|
+
for (const alias of synonymsOf(token)) {
|
|
506
|
+
for (const agentToken of agentTokens) {
|
|
507
|
+
if (agentToken === alias)
|
|
508
|
+
return true;
|
|
509
|
+
if (stem(agentToken) === stem(alias))
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Fracción de las capabilities objetivo que el agente menciona.
|
|
518
|
+
*
|
|
519
|
+
* Una capability puede ser una **frase** ("react component accessibility").
|
|
520
|
+
* En ese caso cuenta como varias: cada palabra se evalúa por separado. Si no,
|
|
521
|
+
* "react component accessibility" valdría lo mismo que "react", y un agente
|
|
522
|
+
* que solo menciona accesibilidad ganaría una card de frontend.
|
|
523
|
+
*/
|
|
524
|
+
export function capabilityCoverage(agentTokens, target) {
|
|
525
|
+
if (target.length === 0)
|
|
526
|
+
return 0;
|
|
527
|
+
let hits = 0;
|
|
528
|
+
let total = 0;
|
|
529
|
+
for (const capability of target) {
|
|
530
|
+
const tokens = capabilityTokens(capability);
|
|
531
|
+
if (tokens.size === 0)
|
|
532
|
+
continue;
|
|
533
|
+
for (const token of tokens) {
|
|
534
|
+
total += 1;
|
|
535
|
+
if (mentionsCapability(agentTokens, token))
|
|
536
|
+
hits += 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return total === 0 ? 0 : hits / total;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Lematiza un token: "testing" → "test", "validation" → "validat".
|
|
543
|
+
*
|
|
544
|
+
* ## Por qué hace falta
|
|
545
|
+
*
|
|
546
|
+
* El Commander pide capabilities en sus palabras ("testing", "validation") y
|
|
547
|
+
* los agentes de agency-agents usan las suyas ("test", "validate"). Sin
|
|
548
|
+
* lematizar, dos palabras que significan lo mismo no se ven nunca.
|
|
549
|
+
*
|
|
550
|
+
* Es la diferencia entre un matcher de palabras y uno de conceptos.
|
|
551
|
+
*/
|
|
552
|
+
export function stem(token) {
|
|
553
|
+
if (token.length <= 4)
|
|
554
|
+
return token;
|
|
555
|
+
for (const suffix of ["ations", "ation", "ings", "ing", "ies", "ied", "ers", "er", "es", "ed", "s"]) {
|
|
556
|
+
if (token.endsWith(suffix) && token.length - suffix.length >= 3) {
|
|
557
|
+
return token.slice(0, token.length - suffix.length);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return token;
|
|
561
|
+
}
|
|
562
|
+
/** Fracción de tokens de `want` que aparecen en `have`. */
|
|
563
|
+
function tokenOverlap(want, have) {
|
|
564
|
+
if (want.size === 0)
|
|
565
|
+
return 0;
|
|
566
|
+
const haveStems = new Set([...have].map(stem));
|
|
567
|
+
let hits = 0;
|
|
568
|
+
for (const token of want) {
|
|
569
|
+
if (have.has(token)) {
|
|
570
|
+
hits += 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
// Match por lematización: "testing" ~ "test".
|
|
574
|
+
if (haveStems.has(stem(token))) {
|
|
575
|
+
hits += 1;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
// Match parcial por prefijo: "streaming" ~ "stream".
|
|
579
|
+
for (const candidate of have) {
|
|
580
|
+
if (candidate.length > 3 && token.length > 3 && (candidate.startsWith(token) || token.startsWith(candidate))) {
|
|
581
|
+
hits += 0.6;
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return Math.min(1, hits / want.size);
|
|
587
|
+
}
|
|
588
|
+
/** Afinidad de la división con las capabilities pedidas. */
|
|
589
|
+
export function divisionAffinity(division, target) {
|
|
590
|
+
const aliases = DIVISION_AFFINITY[division];
|
|
591
|
+
if (!aliases || aliases.length === 0)
|
|
592
|
+
return 0;
|
|
593
|
+
// El objetivo se canónica y se lematiza.
|
|
594
|
+
const targetConcepts = canonicalConcepts(target.join(" "));
|
|
595
|
+
const targetStems = new Set([...targetConcepts].map(stem));
|
|
596
|
+
// La división se expresa en conceptos, no en palabras sueltas.
|
|
597
|
+
// El nombre de la división cuenta: una card de testing tiene que ir a la
|
|
598
|
+
// división `testing`, y esa es la señal más obvia de todas.
|
|
599
|
+
const divisionConcepts = new Set();
|
|
600
|
+
for (const alias of [...aliases, division]) {
|
|
601
|
+
for (const concept of canonicalConcepts(alias))
|
|
602
|
+
divisionConcepts.add(concept);
|
|
603
|
+
}
|
|
604
|
+
const divisionStems = new Set([...divisionConcepts].map(stem));
|
|
605
|
+
let hits = 0;
|
|
606
|
+
for (const concept of targetConcepts) {
|
|
607
|
+
if (divisionConcepts.has(concept)) {
|
|
608
|
+
hits += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (divisionStems.has(stem(concept)))
|
|
612
|
+
hits += 1;
|
|
613
|
+
}
|
|
614
|
+
if (hits === 0)
|
|
615
|
+
return 0;
|
|
616
|
+
// Normalizado contra el tamaño de la división, no del objetivo: una
|
|
617
|
+
// división chica que matchea todo vale más que una grande que matchea poco.
|
|
618
|
+
return Math.min(1, hits / Math.max(1, Math.min(divisionConcepts.size, targetConcepts.size)));
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Matchea un objetivo de capabilities contra el catálogo.
|
|
622
|
+
*
|
|
623
|
+
* El score es ponderado por campo, no una bolsa de tokens. Un agente cuyo
|
|
624
|
+
* "Core Mission" menciona literalmente lo que la card necesita gana sobre
|
|
625
|
+
* uno que menciona la palabra de pasada en el cuerpo.
|
|
626
|
+
*/
|
|
627
|
+
export function matchCapabilities(catalog, target, options = {}) {
|
|
628
|
+
if (target.length === 0)
|
|
629
|
+
return undefined;
|
|
630
|
+
const pool = options.division
|
|
631
|
+
? catalog.filter((a) => a.division === options.division)
|
|
632
|
+
: catalog;
|
|
633
|
+
const targetRaw = capabilityTokens(target.join(" "));
|
|
634
|
+
const targetTokens = canonicalConcepts(target.join(" "));
|
|
635
|
+
let best;
|
|
636
|
+
for (const agent of pool) {
|
|
637
|
+
// Tokens crudos de la misión: overlap literal, preciso.
|
|
638
|
+
const agentMissionRaw = new Set();
|
|
639
|
+
for (const mission of agent.missions) {
|
|
640
|
+
for (const token of capabilityTokens(mission))
|
|
641
|
+
agentMissionRaw.add(token);
|
|
642
|
+
}
|
|
643
|
+
// Tokens canónicos de la misión: bridgea sinónimos.
|
|
644
|
+
const agentMissions = new Set();
|
|
645
|
+
for (const mission of agent.missions) {
|
|
646
|
+
for (const token of canonicalConcepts(mission))
|
|
647
|
+
agentMissions.add(token);
|
|
648
|
+
}
|
|
649
|
+
const agentDeliverables = new Set();
|
|
650
|
+
for (const deliverable of agent.deliverables) {
|
|
651
|
+
for (const token of canonicalConcepts(deliverable))
|
|
652
|
+
agentDeliverables.add(token);
|
|
653
|
+
}
|
|
654
|
+
// La identidad del agente: nombre, slug, **description** y **vibe**.
|
|
655
|
+
//
|
|
656
|
+
// Los dos últimos son la señal curada — líneas escritas a mano que dicen
|
|
657
|
+
// exactamente de qué se ocupa el agente. El cuerpo son 300 líneas de
|
|
658
|
+
// contexto donde cualquier palabra genérica aparece; la description y el
|
|
659
|
+
// vibe, no. Excluirlos era dejar la mejor señal afuera.
|
|
660
|
+
const agentName = capabilityTokens(`${agent.slug} ${agent.displayName} ${agent.description} ${agent.vibe}`);
|
|
661
|
+
const agentCapabilities = canonicalizeTokens(new Set(agent.capabilities));
|
|
662
|
+
const agentText = canonicalizeTokens(capabilityTokens(agent.searchText));
|
|
663
|
+
const nameScore = capabilityCoverage(agentName, target);
|
|
664
|
+
const missionLiteral = capabilityCoverage(agentMissionRaw, target);
|
|
665
|
+
const missionConcept = tokenOverlap(targetTokens, agentMissions);
|
|
666
|
+
const capabilityScore = tokenOverlap(targetTokens, agentCapabilities);
|
|
667
|
+
const deliverableScore = tokenOverlap(targetTokens, agentDeliverables);
|
|
668
|
+
const divisionScore = divisionAffinity(agent.division, target);
|
|
669
|
+
const textScore = tokenOverlap(targetTokens, agentText);
|
|
670
|
+
const coverage = nameScore * MATCH_WEIGHTS.name +
|
|
671
|
+
missionLiteral * MATCH_WEIGHTS.missionLiteral +
|
|
672
|
+
missionConcept * MATCH_WEIGHTS.missionConcept +
|
|
673
|
+
capabilityScore * MATCH_WEIGHTS.capabilities +
|
|
674
|
+
deliverableScore * MATCH_WEIGHTS.deliverables +
|
|
675
|
+
divisionScore * MATCH_WEIGHTS.division +
|
|
676
|
+
textScore * MATCH_WEIGHTS.text;
|
|
677
|
+
// **Recall**: qué parte del objetivo cubre este agente, con las señales
|
|
678
|
+
// confiables (identidad curada + misión declarada).
|
|
679
|
+
//
|
|
680
|
+
// El conteo es POR TOKEN, igual que `capabilityCoverage`. Si `coverage`
|
|
681
|
+
// cuenta tokens y `reliable` cuenta frases, el panel miente: un agente
|
|
682
|
+
// que menciona "mapping" de pasada se contaba como si cubriera "gis
|
|
683
|
+
// geospatial mapping cartography" entero.
|
|
684
|
+
const reliableFraction = (capability) => {
|
|
685
|
+
const tokens = capabilityTokens(capability);
|
|
686
|
+
if (tokens.size === 0)
|
|
687
|
+
return 0;
|
|
688
|
+
let hits = 0;
|
|
689
|
+
for (const token of tokens) {
|
|
690
|
+
if (mentionsCapability(agentName, token) || mentionsCapability(agentMissionRaw, token)) {
|
|
691
|
+
hits += 1;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return hits / tokens.size;
|
|
695
|
+
};
|
|
696
|
+
const reliableHits = target.filter((t) => reliableFraction(t) >= 0.5);
|
|
697
|
+
const recall = target.length > 0 ? reliableHits.length / target.length : 0;
|
|
698
|
+
// **Focus de misión**: qué fracción de las misiones declaradas del agente
|
|
699
|
+
// tienen que ver con el objetivo.
|
|
700
|
+
//
|
|
701
|
+
// Este es el discriminante entre un especialista y un generalista que
|
|
702
|
+
// menciona la tool de pasada. Un Test Automation Engineer tiene las 5
|
|
703
|
+
// misiones en el dominio del testing → focus ~1. Un PDF engine con UNA
|
|
704
|
+
// misión sobre "Playwright Browser Context Pools" y las otras 7 sobre
|
|
705
|
+
// PDF → focus ~0.12.
|
|
706
|
+
const matchingMissions = agent.missions.filter((m) => {
|
|
707
|
+
const raw = new Set(capabilityTokens(m));
|
|
708
|
+
return target.some((t) => mentionsCapability(raw, t));
|
|
709
|
+
});
|
|
710
|
+
const focus = agent.missions.length > 0 ? matchingMissions.length / agent.missions.length : 0;
|
|
711
|
+
// **Precisión**: qué fracción de lo que el agente es realmente va en la
|
|
712
|
+
// dirección del objetivo.
|
|
713
|
+
const hits = [...targetTokens].filter((token) => agentMissions.has(token)).length;
|
|
714
|
+
const precision = agentMissions.size > 0 ? Math.min(1, hits / agentMissions.size) : 0;
|
|
715
|
+
const f1 = recall + precision > 0 ? (2 * recall * precision) / (recall + precision) : 0;
|
|
716
|
+
// Score final: la señal de match, el alcance sobre el objetivo y la
|
|
717
|
+
// especificidad. El recall mide alcance; el focus mide si el agente es
|
|
718
|
+
// un especialista del dominio o un generalista que pasó por ahí.
|
|
719
|
+
const score = coverage * 0.35 + recall * 0.3 + focus * 0.25 + f1 * 0.1;
|
|
720
|
+
if (!best || score > best.coverage) {
|
|
721
|
+
// `matched` y `missing` se calculan SOLO con las señales confiables:
|
|
722
|
+
// el nombre del rol y la misión declarada.
|
|
723
|
+
//
|
|
724
|
+
// Si se incluyera el cuerpo completo, casi todo agente "cubriría"
|
|
725
|
+
// casi todo: un "Code Reviewer" emparejaría un objetivo de Playwright
|
|
726
|
+
// porque su cuerpo de 300 líneas menciona testing, browser y checkout
|
|
727
|
+
// de pasada. Un `matched` inflado rompe el panel: los complementos
|
|
728
|
+
// dejan de ser complementos y se vuelven duplicados.
|
|
729
|
+
best = {
|
|
730
|
+
agent,
|
|
731
|
+
coverage: score,
|
|
732
|
+
matched: reliableHits,
|
|
733
|
+
missing: target.filter((t) => !reliableHits.includes(t)),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (!best)
|
|
738
|
+
return undefined;
|
|
739
|
+
if (options.minCoverage !== undefined && best.coverage < options.minCoverage)
|
|
740
|
+
return undefined;
|
|
741
|
+
return best;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Resuelve un rol: catálogo primero, factory si la cobertura es baja.
|
|
745
|
+
*
|
|
746
|
+
* ## La cobertura es sobre el PANEL, no sobre el primario
|
|
747
|
+
*
|
|
748
|
+
* Un objetivo casi nunca es de un solo dominio. Si el primario cubre la
|
|
749
|
+
* mitad y un complemento cubre la otra mitad, el objetivo **está cubierto**
|
|
750
|
+
* y no hace falta fabricar un rol.
|
|
751
|
+
*
|
|
752
|
+
* La cobertura se mide como:Concepts cubiertos por el panel / concepts
|
|
753
|
+
* pedidos. El umbral de 0.6 significa "el equipo cubre el 60% de lo que
|
|
754
|
+
* la card pide". Por debajo, se fabrica el rol.
|
|
755
|
+
*/
|
|
756
|
+
export function resolveRole(catalog, target, minCoverage) {
|
|
757
|
+
const panel = buildPanel(catalog, target);
|
|
758
|
+
if (!panel.primary)
|
|
759
|
+
return { match: undefined, needsFactory: true };
|
|
760
|
+
const covered = new Set(panel.primary.matched);
|
|
761
|
+
for (const complement of panel.complements) {
|
|
762
|
+
for (const m of complement.matched)
|
|
763
|
+
covered.add(m);
|
|
764
|
+
}
|
|
765
|
+
const coverageRatio = target.length > 0 ? covered.size / target.length : 0;
|
|
766
|
+
if (coverageRatio < minCoverage) {
|
|
767
|
+
return { match: panel.primary, needsFactory: true, panel };
|
|
768
|
+
}
|
|
769
|
+
return { match: panel.primary, needsFactory: false, panel };
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Devuelve los mejores N agentes para un objetivo, ordenados por cobertura.
|
|
773
|
+
*
|
|
774
|
+
* Útil para que el Comandante vea alternativas: "este es el mejor, pero
|
|
775
|
+
* estos dos también sirven y podrían repartir la carga".
|
|
776
|
+
*/
|
|
777
|
+
export function rankCandidates(catalog, target, limit = 5) {
|
|
778
|
+
return catalog
|
|
779
|
+
.map((agent) => matchCapabilities([agent], target))
|
|
780
|
+
.filter((m) => !!m)
|
|
781
|
+
.sort((a, b) => b.coverage - a.coverage)
|
|
782
|
+
.slice(0, limit);
|
|
783
|
+
}
|
|
784
|
+
export function buildPanel(catalog, target, options = {}) {
|
|
785
|
+
const maxComplements = options.maxComplements ?? 3;
|
|
786
|
+
const minComplementCoverage = options.minComplementCoverage ?? 0.15;
|
|
787
|
+
const ranked = catalog
|
|
788
|
+
.map((agent) => matchCapabilities([agent], target))
|
|
789
|
+
.filter((m) => !!m)
|
|
790
|
+
.sort((a, b) => b.coverage - a.coverage);
|
|
791
|
+
const primary = ranked[0] ?? null;
|
|
792
|
+
if (!primary) {
|
|
793
|
+
return { primary: null, complements: [], uncovered: [...target] };
|
|
794
|
+
}
|
|
795
|
+
const covered = new Set(primary.matched);
|
|
796
|
+
const complements = [];
|
|
797
|
+
for (const candidate of ranked.slice(1)) {
|
|
798
|
+
if (complements.length >= maxComplements)
|
|
799
|
+
break;
|
|
800
|
+
if (candidate.coverage < minComplementCoverage)
|
|
801
|
+
continue;
|
|
802
|
+
// Solo si aporta algo que el primario no cubre.
|
|
803
|
+
const aporta = candidate.matched.filter((m) => !covered.has(m));
|
|
804
|
+
if (aporta.length === 0)
|
|
805
|
+
continue;
|
|
806
|
+
complements.push(candidate);
|
|
807
|
+
for (const m of aporta)
|
|
808
|
+
covered.add(m);
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
primary,
|
|
812
|
+
complements,
|
|
813
|
+
uncovered: target.filter((t) => !covered.has(t)),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
//# sourceMappingURL=catalog.js.map
|