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.
Files changed (117) hide show
  1. package/DESIGN.md +145 -0
  2. package/LICENSE +21 -0
  3. package/README.md +670 -2
  4. package/agents/oficina-architect.md +72 -0
  5. package/agents/oficina-commander.md +108 -0
  6. package/agents/oficina-critico.md +148 -0
  7. package/agents/oficina-director.md +160 -0
  8. package/agents/oficina-guardian.md +128 -0
  9. package/agents/oficina-integrator.md +98 -0
  10. package/agents/oficina-verificador.md +88 -0
  11. package/commands/oficina.md +36 -0
  12. package/dist/cli.d.ts +12 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +394 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/core/briefing.d.ts +115 -0
  17. package/dist/core/briefing.d.ts.map +1 -0
  18. package/dist/core/briefing.js +410 -0
  19. package/dist/core/briefing.js.map +1 -0
  20. package/dist/core/bus.d.ts +132 -0
  21. package/dist/core/bus.d.ts.map +1 -0
  22. package/dist/core/bus.js +250 -0
  23. package/dist/core/bus.js.map +1 -0
  24. package/dist/core/catalog.d.ts +303 -0
  25. package/dist/core/catalog.d.ts.map +1 -0
  26. package/dist/core/catalog.js +816 -0
  27. package/dist/core/catalog.js.map +1 -0
  28. package/dist/core/dispatch.d.ts +110 -0
  29. package/dist/core/dispatch.d.ts.map +1 -0
  30. package/dist/core/dispatch.js +301 -0
  31. package/dist/core/dispatch.js.map +1 -0
  32. package/dist/core/factory.d.ts +74 -0
  33. package/dist/core/factory.d.ts.map +1 -0
  34. package/dist/core/factory.js +243 -0
  35. package/dist/core/factory.js.map +1 -0
  36. package/dist/core/fsx.d.ts +39 -0
  37. package/dist/core/fsx.d.ts.map +1 -0
  38. package/dist/core/fsx.js +113 -0
  39. package/dist/core/fsx.js.map +1 -0
  40. package/dist/core/gates.d.ts +97 -0
  41. package/dist/core/gates.d.ts.map +1 -0
  42. package/dist/core/gates.js +467 -0
  43. package/dist/core/gates.js.map +1 -0
  44. package/dist/core/leases.d.ts +46 -0
  45. package/dist/core/leases.d.ts.map +1 -0
  46. package/dist/core/leases.js +94 -0
  47. package/dist/core/leases.js.map +1 -0
  48. package/dist/core/log.d.ts +30 -0
  49. package/dist/core/log.d.ts.map +1 -0
  50. package/dist/core/log.js +43 -0
  51. package/dist/core/log.js.map +1 -0
  52. package/dist/core/mesa.d.ts +82 -0
  53. package/dist/core/mesa.d.ts.map +1 -0
  54. package/dist/core/mesa.js +178 -0
  55. package/dist/core/mesa.js.map +1 -0
  56. package/dist/core/office.d.ts +208 -0
  57. package/dist/core/office.d.ts.map +1 -0
  58. package/dist/core/office.js +618 -0
  59. package/dist/core/office.js.map +1 -0
  60. package/dist/core/paths.d.ts +67 -0
  61. package/dist/core/paths.d.ts.map +1 -0
  62. package/dist/core/paths.js +136 -0
  63. package/dist/core/paths.js.map +1 -0
  64. package/dist/core/retry.d.ts +41 -0
  65. package/dist/core/retry.d.ts.map +1 -0
  66. package/dist/core/retry.js +86 -0
  67. package/dist/core/retry.js.map +1 -0
  68. package/dist/core/roster.d.ts +47 -0
  69. package/dist/core/roster.d.ts.map +1 -0
  70. package/dist/core/roster.js +146 -0
  71. package/dist/core/roster.js.map +1 -0
  72. package/dist/core/router.d.ts +67 -0
  73. package/dist/core/router.d.ts.map +1 -0
  74. package/dist/core/router.js +186 -0
  75. package/dist/core/router.js.map +1 -0
  76. package/dist/core/state.d.ts +89 -0
  77. package/dist/core/state.d.ts.map +1 -0
  78. package/dist/core/state.js +303 -0
  79. package/dist/core/state.js.map +1 -0
  80. package/dist/core/types.d.ts +333 -0
  81. package/dist/core/types.d.ts.map +1 -0
  82. package/dist/core/types.js +27 -0
  83. package/dist/core/types.js.map +1 -0
  84. package/dist/hooks/compacting.d.ts +18 -0
  85. package/dist/hooks/compacting.d.ts.map +1 -0
  86. package/dist/hooks/compacting.js +72 -0
  87. package/dist/hooks/compacting.js.map +1 -0
  88. package/dist/hooks/scopeGuard.d.ts +39 -0
  89. package/dist/hooks/scopeGuard.d.ts.map +1 -0
  90. package/dist/hooks/scopeGuard.js +114 -0
  91. package/dist/hooks/scopeGuard.js.map +1 -0
  92. package/dist/hooks/sessionIdle.d.ts +35 -0
  93. package/dist/hooks/sessionIdle.d.ts.map +1 -0
  94. package/dist/hooks/sessionIdle.js +110 -0
  95. package/dist/hooks/sessionIdle.js.map +1 -0
  96. package/dist/hooks/systemTransform.d.ts +18 -0
  97. package/dist/hooks/systemTransform.d.ts.map +1 -0
  98. package/dist/hooks/systemTransform.js +63 -0
  99. package/dist/hooks/systemTransform.js.map +1 -0
  100. package/dist/index.d.ts +36 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +103 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/tools/index.d.ts +22 -0
  105. package/dist/tools/index.d.ts.map +1 -0
  106. package/dist/tools/index.js +854 -0
  107. package/dist/tools/index.js.map +1 -0
  108. package/docs/ARCHITECTURE.md +258 -0
  109. package/docs/QUE-ES.md +337 -0
  110. package/examples/opencode.json +7 -0
  111. package/package.json +81 -4
  112. package/skills/oficina/README.md +120 -0
  113. package/skills/oficina/SKILL.md +251 -0
  114. package/skills/oficina/reference/mesh.md +345 -0
  115. package/skills/oficina/reference/protocol.md +342 -0
  116. package/skills/oficina/reference/roles.md +171 -0
  117. 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