@codexa/cli 9.0.11 → 9.0.13

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/commands/plan.ts CHANGED
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
2
2
  import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
3
3
  import { resolveSpec } from "./spec-resolver";
4
4
  import { CodexaError, ValidationError } from "../errors";
5
+ import { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames } from "../context/agent-registry";
5
6
 
6
7
  export function generateSpecId(name: string): string {
7
8
  const date = new Date().toISOString().split("T")[0];
@@ -87,10 +88,11 @@ export function planStart(description: string, options: { fromAnalysis?: string;
87
88
  const files = step.files && step.files.length > 0 ? JSON.stringify(step.files) : null;
88
89
  const dependsOn = step.dependsOn && step.dependsOn.length > 0 ? JSON.stringify(step.dependsOn) : null;
89
90
 
91
+ const normalizedStepAgent = step.agent ? resolveAgentName(step.agent) : null;
90
92
  db.run(
91
93
  `INSERT INTO tasks (spec_id, number, name, agent, depends_on, can_parallel, files)
92
94
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
93
- [specId, step.number, step.name, step.agent || null, dependsOn, 1, files]
95
+ [specId, step.number, step.name, normalizedStepAgent, dependsOn, 1, files]
94
96
  );
95
97
  tasksCreated++;
96
98
  }
@@ -266,6 +268,22 @@ export function planTaskAdd(options: {
266
268
  }
267
269
  }
268
270
 
271
+ // Validar e normalizar agent name (R3)
272
+ let normalizedAgent: string | null = options.agent || null;
273
+ if (normalizedAgent) {
274
+ const entry = resolveAgent(normalizedAgent);
275
+ if (!entry) {
276
+ const suggestion = suggestAgent(normalizedAgent);
277
+ const hint = suggestion
278
+ ? `\nVoce quis dizer: "${suggestion}"?`
279
+ : `\nAgentes validos: ${getCanonicalAgentNames().join(", ")}`;
280
+ throw new ValidationError(
281
+ `Agente desconhecido: "${normalizedAgent}".${hint}`
282
+ );
283
+ }
284
+ normalizedAgent = entry.canonical;
285
+ }
286
+
269
287
  // Parsear arquivos
270
288
  let files: string[] = [];
271
289
  if (options.files) {
@@ -279,7 +297,7 @@ export function planTaskAdd(options: {
279
297
  spec.id,
280
298
  nextNumber,
281
299
  options.name,
282
- options.agent || null,
300
+ normalizedAgent,
283
301
  dependsOn.length > 0 ? JSON.stringify(dependsOn) : null,
284
302
  options.sequential ? 0 : 1,
285
303
  files.length > 0 ? JSON.stringify(files) : null,
@@ -295,7 +313,7 @@ export function planTaskAdd(options: {
295
313
  ]);
296
314
 
297
315
  console.log(`\nTask #${nextNumber} adicionada: ${options.name}`);
298
- if (options.agent) console.log(` Agente: ${options.agent}`);
316
+ if (normalizedAgent) console.log(` Agente: ${normalizedAgent}${normalizedAgent !== options.agent ? ` (normalizado de "${options.agent}")` : ""}`);
299
317
  if (dependsOn.length > 0) console.log(` Depende de: ${dependsOn.join(", ")}`);
300
318
  if (files.length > 0) console.log(` Arquivos: ${files.join(", ")}`);
301
319
  console.log(` Paralelizavel: ${options.sequential ? "Nao" : "Sim"}\n`);
package/commands/task.ts CHANGED
@@ -9,6 +9,8 @@ import { loadTemplate } from "../templates/loader";
9
9
  import { TaskStateError, ValidationError, KnowledgeBlockError } from "../errors";
10
10
  import { resolveSpec, resolveSpecOrNull } from "./spec-resolver";
11
11
  import { getAgentDomain, domainToScope } from "../context/domains";
12
+ import { resolveAgent } from "../context/agent-registry";
13
+ import { loadAgentExpertise, getAgentDescription } from "../context/agent-expertise";
12
14
 
13
15
  export function taskNext(json: boolean = false, specId?: string): void {
14
16
  initSchema();
@@ -224,11 +226,24 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
224
226
  confidence: p.confidence,
225
227
  }));
226
228
 
229
+ // R1: Load condensed expertise from agent .md
230
+ const agentExpertise = task.agent ? loadAgentExpertise(task.agent) : "";
231
+
232
+ // R4: Build agent identity string
233
+ const agentEntry = task.agent ? resolveAgent(task.agent) : null;
234
+ const agentIdentity = agentEntry
235
+ ? `## IDENTIDADE DO AGENTE\n\nVoce e um ${agentEntry.description}.\nSiga os principios e padroes descritos abaixo.\n`
236
+ : "";
237
+
238
+ // R5: Get description for orchestrator use
239
+ const agentDescription = task.agent ? getAgentDescription(task.agent) : null;
240
+
227
241
  return {
228
242
  taskId: task.id,
229
243
  number: task.number,
230
244
  name: task.name,
231
245
  agent: task.agent,
246
+ agentDescription,
232
247
  files: taskFiles,
233
248
  // AVISO PARA O ORQUESTRADOR - NAO EXECUTE, DELEGUE
234
249
  _orchestratorWarning: "NAO execute esta task diretamente. Use Task tool com subagent_type='general-purpose' para delegar. O campo 'subagentContext' abaixo e o prompt para o SUBAGENT.",
@@ -248,6 +263,10 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
248
263
  // Contexto para o SUBAGENT (o orquestrador deve passar isso via Task tool)
249
264
  subagentContext: loadTemplate("subagent-context", {
250
265
  filesList: taskFiles.map(f => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado - analise o contexto)',
266
+ agentIdentity,
267
+ agentExpertise: agentExpertise
268
+ ? `\n## EXPERTISE DO AGENTE\n\n${agentExpertise}\n\n`
269
+ : '',
251
270
  }),
252
271
  // Instrucoes de retorno para o SUBAGENT
253
272
  subagentReturnProtocol: loadTemplate("subagent-return-protocol", {
@@ -6,7 +6,7 @@ import {
6
6
  buildImpSpawnPrompt,
7
7
  buildRevSpawnPrompt,
8
8
  buildArchitectSpawnPrompt,
9
- buildDevilAdvocatePrompt,
9
+ buildArchitectReviewerPrompt,
10
10
  type TeammateConfig,
11
11
  } from "./team";
12
12
 
@@ -226,28 +226,28 @@ describe("buildArchitectSpawnPrompt", () => {
226
226
  });
227
227
  });
228
228
 
229
- describe("buildDevilAdvocatePrompt", () => {
229
+ describe("buildArchitectReviewerPrompt", () => {
230
230
  it("inclui explicacao de entrega automatica de mensagens", () => {
231
- const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
231
+ const prompt = buildArchitectReviewerPrompt({ name: "Add auth" });
232
232
  expect(prompt).toContain("entregues automaticamente");
233
233
  expect(prompt).toContain("sem precisar");
234
234
  expect(prompt).toContain("polling");
235
235
  });
236
236
 
237
237
  it("instrui a aguardar AMBAS propostas", () => {
238
- const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
238
+ const prompt = buildArchitectReviewerPrompt({ name: "Add auth" });
239
239
  expect(prompt).toContain("AMBAS as propostas");
240
240
  });
241
241
 
242
242
  it("inclui formato estruturado de critica", () => {
243
- const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
243
+ const prompt = buildArchitectReviewerPrompt({ name: "Add auth" });
244
244
  expect(prompt).toContain("Pontos Fortes");
245
245
  expect(prompt).toContain("Pontos Fracos");
246
246
  expect(prompt).toContain("FORTE | VIAVEL | FRACO");
247
247
  });
248
248
 
249
249
  it("inclui instrucao de IGNORE task list nativa", () => {
250
- const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
250
+ const prompt = buildArchitectReviewerPrompt({ name: "Add auth" });
251
251
  expect(prompt).toContain("IGNORE a task list nativa");
252
252
  });
253
253
  });
package/commands/team.ts CHANGED
@@ -272,10 +272,10 @@ function suggestForArchitect(db: any, spec: any, envVarSet: boolean): TeamSugges
272
272
  spawnPrompt: buildArchitectSpawnPrompt(analysis, stackSummary, "beta"),
273
273
  },
274
274
  {
275
- role: "devil-advocate",
275
+ role: "architect-reviewer",
276
276
  domain: "architecture",
277
277
  taskIds: [],
278
- spawnPrompt: buildDevilAdvocatePrompt(analysis),
278
+ spawnPrompt: buildArchitectReviewerPrompt(analysis),
279
279
  },
280
280
  ];
281
281
 
@@ -564,8 +564,8 @@ Crie sua proposta seguindo os 8 headers OBRIGATORIOS:
564
564
  Use a ferramenta \`message\` (nativa Agent Teams) para enviar proposta completa ao lead.
565
565
 
566
566
  ### 5. Responder Criticas
567
- Voce recebera criticas do devil's advocate via \`message\` (entrega automatica).
568
- - Responda via \`message\` ao devil's advocate com justificativas tecnicas
567
+ Voce recebera criticas do Architect Reviewer via \`message\` (entrega automatica).
568
+ - Responda via \`message\` ao Architect Reviewer com justificativas tecnicas
569
569
  - Reconheca pontos fracos legitimos
570
570
  - Proponha ajustes se necessario
571
571
  - Envie versao revisada ao lead via \`message\`
@@ -573,7 +573,7 @@ Voce recebera criticas do devil's advocate via \`message\` (entrega automatica).
573
573
  ## COMUNICACAO
574
574
 
575
575
  - **Proposta ao lead**: ferramenta \`message\` (nativa Agent Teams)
576
- - **Resposta a criticas**: ferramenta \`message\` ao devil's advocate
576
+ - **Resposta a criticas**: ferramenta \`message\` ao Architect Reviewer
577
577
  - **NAO** leia arquivos criados por outros architects (independencia)
578
578
  - **NAO** use a task list nativa do Agent Teams
579
579
 
@@ -589,13 +589,13 @@ Voce recebera criticas do devil's advocate via \`message\` (entrega automatica).
589
589
  }
590
590
 
591
591
  // ─────────────────────────────────────────────────────────────────
592
- // Spawn Prompts: Devil's Advocate
592
+ // Spawn Prompts: Architect Reviewer
593
593
  // ─────────────────────────────────────────────────────────────────
594
594
 
595
- export function buildDevilAdvocatePrompt(analysis: any): string {
595
+ export function buildArchitectReviewerPrompt(analysis: any): string {
596
596
  return `## IDENTIDADE
597
597
 
598
- Voce e o DEVIL'S ADVOCATE do Codexa Workflow.
598
+ Voce e o ARCHITECT REVIEWER do Codexa Workflow.
599
599
  Seu papel e DESAFIAR propostas arquiteturais, encontrando pontos fracos,
600
600
  riscos nao considerados, e alternativas melhores.
601
601
 
@@ -29,10 +29,14 @@ describe("Domain exports from utils (backward compatibility)", () => {
29
29
  expect(domainToScope(domain)).toBe("backend");
30
30
  });
31
31
 
32
+ it("aliases now resolve via registry", () => {
33
+ expect(getAgentDomain("frontend-next")).toBe("frontend");
34
+ expect(getAgentDomain("database-postgres")).toBe("database");
35
+ });
36
+
32
37
  it("unknown agents return null domain and 'all' scope", () => {
33
- expect(getAgentDomain("frontend-next")).toBeNull();
34
- expect(getAgentDomain("database-postgres")).toBeNull();
35
38
  expect(getAgentDomain("general-purpose")).toBeNull();
39
+ expect(getAgentDomain("some-unknown-agent")).toBeNull();
36
40
  expect(domainToScope(getAgentDomain("general-purpose"))).toBe("all");
37
41
  });
38
42
  });
package/commands/utils.ts CHANGED
@@ -11,6 +11,8 @@ import pkg from "../package.json";
11
11
  export { getContextForSubagent, getMinimalContextForSubagent } from "../context/generator";
12
12
  export { MAX_CONTEXT_SIZE } from "../context/assembly";
13
13
  export { AGENT_DOMAIN, getAgentDomain, domainToScope } from "../context/domains";
14
+ export { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames } from "../context/agent-registry";
15
+ export { loadAgentExpertise, getAgentDescription } from "../context/agent-expertise";
14
16
 
15
17
  export function status(json: boolean = false, specId?: string): void {
16
18
  initSchema();
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { loadAgentExpertise, getAgentDescription, clearExpertiseCache } from "./agent-expertise";
3
+
4
+ beforeEach(() => {
5
+ clearExpertiseCache();
6
+ });
7
+
8
+ // ── loadAgentExpertise ───────────────────────────────────────
9
+
10
+ describe("loadAgentExpertise", () => {
11
+ it("returns non-empty string for known agents", () => {
12
+ const expertise = loadAgentExpertise("golang-pro");
13
+ expect(expertise.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it("resolves aliases before loading", () => {
17
+ const byCanonical = loadAgentExpertise("golang-pro");
18
+ clearExpertiseCache();
19
+ const byAlias = loadAgentExpertise("backend-go");
20
+ expect(byCanonical).toBe(byAlias);
21
+ });
22
+
23
+ it("loads different expertise for different agents", () => {
24
+ const goExpertise = loadAgentExpertise("golang-pro");
25
+ clearExpertiseCache();
26
+ const flutterExpertise = loadAgentExpertise("frontend-flutter");
27
+ expect(goExpertise).not.toBe(flutterExpertise);
28
+ });
29
+
30
+ it("returns empty for unknown agents", () => {
31
+ expect(loadAgentExpertise("nonexistent-agent")).toBe("");
32
+ });
33
+
34
+ it("returns empty for empty input", () => {
35
+ expect(loadAgentExpertise("")).toBe("");
36
+ });
37
+
38
+ it("respects 2.5KB budget", () => {
39
+ // Check all known agents
40
+ const agents = ["golang-pro", "expert-csharp-developer", "backend-javascript",
41
+ "expert-nextjs-developer", "frontend-flutter", "expert-postgres-developer",
42
+ "testing-unit", "security-specialist"];
43
+
44
+ for (const agent of agents) {
45
+ clearExpertiseCache();
46
+ const expertise = loadAgentExpertise(agent);
47
+ // Allow small margin for truncation message
48
+ expect(expertise.length).toBeLessThanOrEqual(2560 + 100);
49
+ }
50
+ });
51
+
52
+ it("strips YAML frontmatter", () => {
53
+ const expertise = loadAgentExpertise("golang-pro");
54
+ expect(expertise).not.toContain("---\nname:");
55
+ expect(expertise).not.toContain("invocation:");
56
+ });
57
+
58
+ it("strips redundant Retorno section", () => {
59
+ const expertise = loadAgentExpertise("golang-pro");
60
+ expect(expertise).not.toContain("**Retorno**: Siga");
61
+ });
62
+
63
+ it("contains agent-specific content", () => {
64
+ const goExpertise = loadAgentExpertise("golang-pro");
65
+ expect(goExpertise).toContain("Go");
66
+
67
+ clearExpertiseCache();
68
+ const flutterExpertise = loadAgentExpertise("frontend-flutter");
69
+ expect(flutterExpertise).toContain("Flutter");
70
+ });
71
+
72
+ it("caches results across calls", () => {
73
+ const first = loadAgentExpertise("golang-pro");
74
+ const second = loadAgentExpertise("golang-pro");
75
+ expect(first).toBe(second);
76
+ });
77
+ });
78
+
79
+ // ── getAgentDescription ──────────────────────────────────────
80
+
81
+ describe("getAgentDescription", () => {
82
+ it("returns description for known agent by canonical name", () => {
83
+ const desc = getAgentDescription("golang-pro");
84
+ expect(desc).not.toBeNull();
85
+ expect(desc).toContain("Go");
86
+ });
87
+
88
+ it("returns description for known agent by alias", () => {
89
+ const desc = getAgentDescription("backend-go");
90
+ expect(desc).not.toBeNull();
91
+ expect(desc).toContain("Go");
92
+ });
93
+
94
+ it("returns null for unknown agent", () => {
95
+ expect(getAgentDescription("nonexistent")).toBeNull();
96
+ });
97
+
98
+ it("returns null for empty input", () => {
99
+ expect(getAgentDescription("")).toBeNull();
100
+ });
101
+ });
@@ -0,0 +1,134 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // AGENT EXPERTISE LOADER (v9.5)
3
+ // Loads and condenses agent .md expertise for injection into
4
+ // the subagent prompt. Budget: max 2.5KB per agent.
5
+ // ═══════════════════════════════════════════════════════════════
6
+
7
+ import { readFileSync, existsSync } from "fs";
8
+ import { join } from "path";
9
+ import { execSync } from "child_process";
10
+ import { resolveAgent } from "./agent-registry";
11
+
12
+ const MAX_EXPERTISE_SIZE = 2560; // 2.5KB budget
13
+
14
+ const expertiseCache = new Map<string, string>();
15
+
16
+ /**
17
+ * Find the agents directory.
18
+ * Checks: 1) .claude/agents/ in cwd 2) plugins dir via git root
19
+ */
20
+ export function findAgentsDir(): string | null {
21
+ // 1. Check synced agents in project
22
+ const projectAgents = join(process.cwd(), ".claude", "agents");
23
+ if (existsSync(projectAgents)) return projectAgents;
24
+
25
+ // 2. Dev repo: git root + plugins/codexa-workflow/agents/
26
+ try {
27
+ const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
28
+ const pluginDir = join(gitRoot, "plugins", "codexa-workflow", "agents");
29
+ if (existsSync(pluginDir)) return pluginDir;
30
+ } catch {
31
+ // Not in git repo
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Load and condense expertise from an agent .md file.
39
+ * Strips frontmatter, condenses code blocks, truncates to budget.
40
+ * Returns empty string if agent not found or has no file.
41
+ */
42
+ export function loadAgentExpertise(agentName: string): string {
43
+ if (!agentName) return "";
44
+
45
+ const cached = expertiseCache.get(agentName);
46
+ if (cached !== undefined) return cached;
47
+
48
+ const entry = resolveAgent(agentName);
49
+ if (!entry) {
50
+ expertiseCache.set(agentName, "");
51
+ return "";
52
+ }
53
+
54
+ const agentsDir = findAgentsDir();
55
+ if (!agentsDir) {
56
+ expertiseCache.set(agentName, "");
57
+ return "";
58
+ }
59
+
60
+ const filePath = join(agentsDir, `${entry.filename}.md`);
61
+ if (!existsSync(filePath)) {
62
+ expertiseCache.set(agentName, "");
63
+ return "";
64
+ }
65
+
66
+ try {
67
+ const raw = readFileSync(filePath, "utf-8");
68
+ const expertise = extractAndCondense(raw);
69
+ expertiseCache.set(agentName, expertise);
70
+ return expertise;
71
+ } catch {
72
+ expertiseCache.set(agentName, "");
73
+ return "";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get agent description from registry (R5).
79
+ */
80
+ export function getAgentDescription(agentName: string): string | null {
81
+ const entry = resolveAgent(agentName);
82
+ return entry?.description ?? null;
83
+ }
84
+
85
+ /**
86
+ * Clear the expertise cache (for tests).
87
+ */
88
+ export function clearExpertiseCache(): void {
89
+ expertiseCache.clear();
90
+ }
91
+
92
+ // ── Internal helpers ─────────────────────────────────────────
93
+
94
+ function extractAndCondense(raw: string): string {
95
+ // Strip YAML frontmatter (--- ... ---)
96
+ const match = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
97
+ if (!match) return "";
98
+ let body = match[1].trim();
99
+
100
+ // Remove redundant "Retorno" footer (already in return protocol template)
101
+ body = body.replace(/---\s*\n+\*\*Retorno\*\*:.*$/s, "").trim();
102
+
103
+ // Condense code blocks: keep ≤8 lines as-is, >8 keeps first 6
104
+ body = condenseCodeBlocks(body);
105
+
106
+ // Truncate to budget at section boundary
107
+ if (body.length > MAX_EXPERTISE_SIZE) {
108
+ const truncPoint = findSectionBoundary(body, MAX_EXPERTISE_SIZE - 80);
109
+ body = body.substring(0, truncPoint).trim() +
110
+ "\n\n[Expertise truncada — use agent .md completo se necessario]";
111
+ }
112
+
113
+ return body;
114
+ }
115
+
116
+ function condenseCodeBlocks(text: string): string {
117
+ return text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang: string, code: string) => {
118
+ const lines = code.split("\n");
119
+ if (lines.length <= 8) return _match;
120
+ const kept = lines.slice(0, 6).join("\n");
121
+ return `\`\`\`${lang}\n${kept}\n// ... (${lines.length - 6} linhas omitidas)\n\`\`\``;
122
+ });
123
+ }
124
+
125
+ function findSectionBoundary(text: string, targetLen: number): number {
126
+ let i = Math.min(targetLen, text.length);
127
+ const floor = Math.max(0, targetLen - 500);
128
+ while (i > floor) {
129
+ if (text[i] === "\n" && text.substring(i + 1, i + 4) === "## ") return i;
130
+ if (text[i] === "\n" && text.substring(i + 1, i + 5) === "### ") return i;
131
+ i--;
132
+ }
133
+ return targetLen;
134
+ }
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ AGENT_REGISTRY,
4
+ resolveAgent,
5
+ resolveAgentName,
6
+ suggestAgent,
7
+ buildAgentDomainMap,
8
+ getAllAgentNames,
9
+ getCanonicalAgentNames,
10
+ } from "./agent-registry";
11
+
12
+ // ── AGENT_REGISTRY structure ─────────────────────────────────
13
+
14
+ describe("AGENT_REGISTRY", () => {
15
+ it("contains 11 agents", () => {
16
+ expect(AGENT_REGISTRY).toHaveLength(11);
17
+ });
18
+
19
+ it("every entry has required fields", () => {
20
+ for (const entry of AGENT_REGISTRY) {
21
+ expect(entry.canonical).toBeTruthy();
22
+ expect(entry.filename).toBeTruthy();
23
+ expect(entry.domain).toBeTruthy();
24
+ expect(entry.description).toBeTruthy();
25
+ expect(Array.isArray(entry.aliases)).toBe(true);
26
+ expect(entry.aliases.length).toBeGreaterThan(0);
27
+ }
28
+ });
29
+
30
+ it("filename is always present as alias", () => {
31
+ for (const entry of AGENT_REGISTRY) {
32
+ // filename should be resolvable (either as alias or as canonical)
33
+ const resolved = resolveAgent(entry.filename);
34
+ expect(resolved).not.toBeNull();
35
+ expect(resolved!.canonical).toBe(entry.canonical);
36
+ }
37
+ });
38
+
39
+ it("canonical names are unique", () => {
40
+ const canonicals = AGENT_REGISTRY.map(e => e.canonical);
41
+ expect(new Set(canonicals).size).toBe(canonicals.length);
42
+ });
43
+ });
44
+
45
+ // ── resolveAgent ─────────────────────────────────────────────
46
+
47
+ describe("resolveAgent", () => {
48
+ it("resolves by canonical name", () => {
49
+ expect(resolveAgent("golang-pro")?.canonical).toBe("golang-pro");
50
+ expect(resolveAgent("architect")?.canonical).toBe("architect");
51
+ expect(resolveAgent("backend-javascript")?.canonical).toBe("backend-javascript");
52
+ expect(resolveAgent("frontend-flutter")?.canonical).toBe("frontend-flutter");
53
+ });
54
+
55
+ it("resolves by filename (alias)", () => {
56
+ expect(resolveAgent("backend-go")?.canonical).toBe("golang-pro");
57
+ expect(resolveAgent("backend-csharp")?.canonical).toBe("expert-csharp-developer");
58
+ expect(resolveAgent("frontend-next")?.canonical).toBe("expert-nextjs-developer");
59
+ expect(resolveAgent("database-postgres")?.canonical).toBe("expert-postgres-developer");
60
+ expect(resolveAgent("codexa-architect")?.canonical).toBe("architect");
61
+ });
62
+
63
+ it("resolves by short aliases", () => {
64
+ expect(resolveAgent("go")?.canonical).toBe("golang-pro");
65
+ expect(resolveAgent("golang")?.canonical).toBe("golang-pro");
66
+ expect(resolveAgent("csharp")?.canonical).toBe("expert-csharp-developer");
67
+ expect(resolveAgent("dotnet")?.canonical).toBe("expert-csharp-developer");
68
+ expect(resolveAgent("nextjs")?.canonical).toBe("expert-nextjs-developer");
69
+ expect(resolveAgent("next")?.canonical).toBe("expert-nextjs-developer");
70
+ expect(resolveAgent("flutter")?.canonical).toBe("frontend-flutter");
71
+ expect(resolveAgent("dart")?.canonical).toBe("frontend-flutter");
72
+ expect(resolveAgent("postgres")?.canonical).toBe("expert-postgres-developer");
73
+ expect(resolveAgent("supabase")?.canonical).toBe("expert-postgres-developer");
74
+ expect(resolveAgent("security")?.canonical).toBe("security-specialist");
75
+ expect(resolveAgent("explore")?.canonical).toBe("deep-explore");
76
+ expect(resolveAgent("arch")?.canonical).toBe("architect");
77
+ });
78
+
79
+ it("is case-insensitive", () => {
80
+ expect(resolveAgent("GOLANG-PRO")?.canonical).toBe("golang-pro");
81
+ expect(resolveAgent("Backend-Go")?.canonical).toBe("golang-pro");
82
+ expect(resolveAgent("FLUTTER")?.canonical).toBe("frontend-flutter");
83
+ });
84
+
85
+ it("returns null for unknown agents", () => {
86
+ expect(resolveAgent("unknown-agent")).toBeNull();
87
+ expect(resolveAgent("general-purpose")).toBeNull();
88
+ });
89
+
90
+ it("returns null for empty/null-like input", () => {
91
+ expect(resolveAgent("")).toBeNull();
92
+ });
93
+ });
94
+
95
+ // ── resolveAgentName ─────────────────────────────────────────
96
+
97
+ describe("resolveAgentName", () => {
98
+ it("normalizes aliases to canonical", () => {
99
+ expect(resolveAgentName("backend-go")).toBe("golang-pro");
100
+ expect(resolveAgentName("nextjs")).toBe("expert-nextjs-developer");
101
+ expect(resolveAgentName("postgres")).toBe("expert-postgres-developer");
102
+ expect(resolveAgentName("codexa-architect")).toBe("architect");
103
+ });
104
+
105
+ it("keeps canonical names unchanged", () => {
106
+ expect(resolveAgentName("golang-pro")).toBe("golang-pro");
107
+ expect(resolveAgentName("architect")).toBe("architect");
108
+ });
109
+
110
+ it("returns input unchanged if not found (backward compat)", () => {
111
+ expect(resolveAgentName("custom-agent")).toBe("custom-agent");
112
+ expect(resolveAgentName("my-special-agent")).toBe("my-special-agent");
113
+ });
114
+ });
115
+
116
+ // ── suggestAgent ─────────────────────────────────────────────
117
+
118
+ describe("suggestAgent", () => {
119
+ it("suggests for close typos", () => {
120
+ expect(suggestAgent("golng-pro")).toBe("golang-pro");
121
+ expect(suggestAgent("backend-goo")).toBe("golang-pro");
122
+ expect(suggestAgent("testing-unti")).toBe("testing-unit");
123
+ });
124
+
125
+ it("suggests for minor misspellings of aliases", () => {
126
+ expect(suggestAgent("fluter")).toBe("frontend-flutter");
127
+ expect(suggestAgent("postgre")).toBe("expert-postgres-developer");
128
+ });
129
+
130
+ it("returns null for very different strings", () => {
131
+ expect(suggestAgent("xyzxyzxyzxyz")).toBeNull();
132
+ expect(suggestAgent("completely-unrelated-name")).toBeNull();
133
+ });
134
+ });
135
+
136
+ // ── buildAgentDomainMap ──────────────────────────────────────
137
+
138
+ describe("buildAgentDomainMap", () => {
139
+ const derived = buildAgentDomainMap();
140
+
141
+ it("contains all 11 canonical agents", () => {
142
+ expect(Object.keys(derived)).toHaveLength(11);
143
+ });
144
+
145
+ it("maps backend agents correctly", () => {
146
+ expect(derived["golang-pro"]).toBe("backend");
147
+ expect(derived["expert-csharp-developer"]).toBe("backend");
148
+ expect(derived["backend-javascript"]).toBe("backend");
149
+ });
150
+
151
+ it("maps frontend agents correctly", () => {
152
+ expect(derived["expert-nextjs-developer"]).toBe("frontend");
153
+ expect(derived["frontend-flutter"]).toBe("frontend");
154
+ });
155
+
156
+ it("maps other domains correctly", () => {
157
+ expect(derived["expert-postgres-developer"]).toBe("database");
158
+ expect(derived["testing-unit"]).toBe("testing");
159
+ expect(derived["expert-code-reviewer"]).toBe("review");
160
+ expect(derived["security-specialist"]).toBe("security");
161
+ expect(derived["deep-explore"]).toBe("explore");
162
+ expect(derived["architect"]).toBe("architecture");
163
+ });
164
+ });
165
+
166
+ // ── getAllAgentNames / getCanonicalAgentNames ─────────────────
167
+
168
+ describe("getAllAgentNames", () => {
169
+ it("includes canonical names", () => {
170
+ const all = getAllAgentNames();
171
+ expect(all).toContain("golang-pro");
172
+ expect(all).toContain("architect");
173
+ });
174
+
175
+ it("includes aliases", () => {
176
+ const all = getAllAgentNames();
177
+ expect(all).toContain("backend-go");
178
+ expect(all).toContain("go");
179
+ expect(all).toContain("nextjs");
180
+ expect(all).toContain("flutter");
181
+ });
182
+ });
183
+
184
+ describe("getCanonicalAgentNames", () => {
185
+ it("returns exactly 11 names", () => {
186
+ expect(getCanonicalAgentNames()).toHaveLength(11);
187
+ });
188
+
189
+ it("contains only canonical names", () => {
190
+ const canonical = getCanonicalAgentNames();
191
+ expect(canonical).toContain("golang-pro");
192
+ expect(canonical).not.toContain("backend-go"); // alias, not canonical
193
+ expect(canonical).not.toContain("go"); // alias, not canonical
194
+ });
195
+ });
@@ -0,0 +1,207 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // AGENT REGISTRY (v9.5)
3
+ // Single source of truth for all agent metadata.
4
+ // Replaces hardcoded AGENT_DOMAIN in domains.ts.
5
+ // ═══════════════════════════════════════════════════════════════
6
+
7
+ import type { AgentDomain } from "./domains";
8
+
9
+ export interface AgentEntry {
10
+ /** Canonical name (= YAML `name` field in agent .md) */
11
+ canonical: string;
12
+ /** Filename without .md extension */
13
+ filename: string;
14
+ /** Domain for context priority */
15
+ domain: AgentDomain;
16
+ /** Human-readable description from YAML frontmatter */
17
+ description: string;
18
+ /** Aliases: filename-without-ext, abbreviations */
19
+ aliases: string[];
20
+ }
21
+
22
+ export const AGENT_REGISTRY: AgentEntry[] = [
23
+ {
24
+ canonical: "golang-pro",
25
+ filename: "backend-go",
26
+ domain: "backend",
27
+ description: "Go senior developer — goroutines, channels, interfaces, net/http 1.22+, error handling with wrapping",
28
+ aliases: ["backend-go", "go", "golang"],
29
+ },
30
+ {
31
+ canonical: "expert-csharp-developer",
32
+ filename: "backend-csharp",
33
+ domain: "backend",
34
+ description: "C#/.NET senior developer — ASP.NET Core 9+, Entity Framework Core, Minimal APIs, DI, modern C# 12+",
35
+ aliases: ["backend-csharp", "csharp", "dotnet", "c#"],
36
+ },
37
+ {
38
+ canonical: "backend-javascript",
39
+ filename: "backend-javascript",
40
+ domain: "backend",
41
+ description: "Backend JavaScript/TypeScript developer — Node.js (Express/Fastify) e Bun (Hono/Elysia), APIs REST, middleware",
42
+ aliases: ["backend-js", "nodejs", "bun-backend"],
43
+ },
44
+ {
45
+ canonical: "expert-nextjs-developer",
46
+ filename: "frontend-next",
47
+ domain: "frontend",
48
+ description: "Next.js 16 developer — App Router, Server Components, Cache Components, Turbopack, React 19.2",
49
+ aliases: ["frontend-next", "nextjs", "next"],
50
+ },
51
+ {
52
+ canonical: "frontend-flutter",
53
+ filename: "frontend-flutter",
54
+ domain: "frontend",
55
+ description: "Flutter/Dart senior developer — Riverpod, GoRouter, widget composition, platform-specific code",
56
+ aliases: ["flutter", "dart"],
57
+ },
58
+ {
59
+ canonical: "expert-postgres-developer",
60
+ filename: "database-postgres",
61
+ domain: "database",
62
+ description: "PostgreSQL/Supabase developer — schema design, query optimization, RLS policies, Drizzle ORM",
63
+ aliases: ["database-postgres", "postgres", "postgresql", "supabase"],
64
+ },
65
+ {
66
+ canonical: "testing-unit",
67
+ filename: "testing-unit",
68
+ domain: "testing",
69
+ description: "Unit testing specialist — Vitest/Jest, Testing Library, mocks, coverage",
70
+ aliases: ["testing", "unit-test", "vitest", "jest"],
71
+ },
72
+ {
73
+ canonical: "expert-code-reviewer",
74
+ filename: "expert-code-reviewer",
75
+ domain: "review",
76
+ description: "Code reviewer — quality assurance, security analysis, standards compliance, architectural validation",
77
+ aliases: ["reviewer", "code-reviewer", "review"],
78
+ },
79
+ {
80
+ canonical: "security-specialist",
81
+ filename: "security-specialist",
82
+ domain: "security",
83
+ description: "Security auditor — OWASP Top 10, secrets detection, dependency vulnerabilities, auth/authz",
84
+ aliases: ["security", "owasp"],
85
+ },
86
+ {
87
+ canonical: "deep-explore",
88
+ filename: "deep-explore",
89
+ domain: "explore",
90
+ description: "Codebase explorer — grepai semantic search, call graph tracing, architecture understanding",
91
+ aliases: ["explore", "grepai", "search"],
92
+ },
93
+ {
94
+ canonical: "architect",
95
+ filename: "codexa-architect",
96
+ domain: "architecture",
97
+ description: "Software architect — analise, design, decomposicao em baby steps, diagramas Mermaid, ADRs",
98
+ aliases: ["codexa-architect", "arch"],
99
+ },
100
+ ];
101
+
102
+ // ── Lookup indexes (built once, O(1) lookups) ────────────────
103
+
104
+ const byCanonical = new Map<string, AgentEntry>();
105
+ const byAlias = new Map<string, AgentEntry>();
106
+
107
+ for (const entry of AGENT_REGISTRY) {
108
+ byCanonical.set(entry.canonical, entry);
109
+ byAlias.set(entry.canonical, entry);
110
+ byAlias.set(entry.filename, entry);
111
+ for (const alias of entry.aliases) {
112
+ byAlias.set(alias.toLowerCase(), entry);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Resolve any agent name variant (canonical, filename, alias) to registry entry.
118
+ * Case-insensitive. Returns null if no match found.
119
+ */
120
+ export function resolveAgent(name: string): AgentEntry | null {
121
+ if (!name) return null;
122
+ const lower = name.toLowerCase();
123
+ return byCanonical.get(lower) ?? byAlias.get(lower) ?? null;
124
+ }
125
+
126
+ /**
127
+ * Resolve agent name to canonical name.
128
+ * Returns the input unchanged if not found (backward compat).
129
+ */
130
+ export function resolveAgentName(name: string): string {
131
+ const entry = resolveAgent(name);
132
+ return entry ? entry.canonical : name;
133
+ }
134
+
135
+ /**
136
+ * Get all valid agent names (canonical + aliases) for validation/suggestions.
137
+ */
138
+ export function getAllAgentNames(): string[] {
139
+ return Array.from(byAlias.keys());
140
+ }
141
+
142
+ /**
143
+ * Get all canonical agent names.
144
+ */
145
+ export function getCanonicalAgentNames(): string[] {
146
+ return AGENT_REGISTRY.map(e => e.canonical);
147
+ }
148
+
149
+ /**
150
+ * Did-you-mean suggestion using Levenshtein distance.
151
+ * Returns the closest canonical name if distance <= 4, else null.
152
+ */
153
+ export function suggestAgent(input: string): string | null {
154
+ const lower = input.toLowerCase();
155
+ let bestMatch: string | null = null;
156
+ let bestDistance = Infinity;
157
+
158
+ for (const name of getAllAgentNames()) {
159
+ const d = levenshtein(lower, name);
160
+ if (d < bestDistance) {
161
+ bestDistance = d;
162
+ bestMatch = name;
163
+ }
164
+ }
165
+
166
+ if (bestDistance <= 4 && bestMatch) {
167
+ const entry = resolveAgent(bestMatch);
168
+ return entry?.canonical ?? bestMatch;
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Derive AGENT_DOMAIN map from registry (backward compat with domains.ts).
176
+ */
177
+ export function buildAgentDomainMap(): Record<string, AgentDomain> {
178
+ const map: Record<string, AgentDomain> = {};
179
+ for (const entry of AGENT_REGISTRY) {
180
+ map[entry.canonical] = entry.domain;
181
+ }
182
+ return map;
183
+ }
184
+
185
+ // ── Levenshtein (minimal, no deps) ──────────────────────────
186
+
187
+ function levenshtein(a: string, b: string): number {
188
+ const m = a.length;
189
+ const n = b.length;
190
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
191
+
192
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
193
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
194
+
195
+ for (let i = 1; i <= m; i++) {
196
+ for (let j = 1; j <= n; j++) {
197
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
198
+ dp[i][j] = Math.min(
199
+ dp[i - 1][j] + 1,
200
+ dp[i][j - 1] + 1,
201
+ dp[i - 1][j - 1] + cost,
202
+ );
203
+ }
204
+ }
205
+
206
+ return dp[m][n];
207
+ }
@@ -298,3 +298,36 @@ describe("AGENT_DOMAIN completeness", () => {
298
298
  }
299
299
  });
300
300
  });
301
+
302
+ // ── getAgentDomain with aliases (via registry) ───────────────
303
+
304
+ describe("getAgentDomain with aliases", () => {
305
+ it("resolves filename aliases to correct domain", () => {
306
+ expect(getAgentDomain("backend-go")).toBe("backend");
307
+ expect(getAgentDomain("backend-csharp")).toBe("backend");
308
+ expect(getAgentDomain("frontend-next")).toBe("frontend");
309
+ expect(getAgentDomain("database-postgres")).toBe("database");
310
+ expect(getAgentDomain("codexa-architect")).toBe("architecture");
311
+ });
312
+
313
+ it("resolves short aliases to correct domain", () => {
314
+ expect(getAgentDomain("go")).toBe("backend");
315
+ expect(getAgentDomain("golang")).toBe("backend");
316
+ expect(getAgentDomain("csharp")).toBe("backend");
317
+ expect(getAgentDomain("dotnet")).toBe("backend");
318
+ expect(getAgentDomain("nextjs")).toBe("frontend");
319
+ expect(getAgentDomain("next")).toBe("frontend");
320
+ expect(getAgentDomain("flutter")).toBe("frontend");
321
+ expect(getAgentDomain("dart")).toBe("frontend");
322
+ expect(getAgentDomain("postgres")).toBe("database");
323
+ expect(getAgentDomain("supabase")).toBe("database");
324
+ expect(getAgentDomain("security")).toBe("security");
325
+ expect(getAgentDomain("explore")).toBe("explore");
326
+ expect(getAgentDomain("arch")).toBe("architecture");
327
+ });
328
+
329
+ it("still returns null for truly unknown names", () => {
330
+ expect(getAgentDomain("xyz-unknown")).toBeNull();
331
+ expect(getAgentDomain("general-purpose")).toBeNull();
332
+ });
333
+ });
@@ -4,6 +4,7 @@
4
4
  // ═══════════════════════════════════════════════════════════════
5
5
 
6
6
  import type { ContextSection } from "./assembly";
7
+ import { buildAgentDomainMap, resolveAgent } from "./agent-registry";
7
8
 
8
9
  export type AgentDomain =
9
10
  | "backend"
@@ -34,21 +35,9 @@ export type SectionName =
34
35
 
35
36
  export type DomainProfile = Record<SectionName, Relevance>;
36
37
 
37
- // ── Agent → Domain mapping ───────────────────────────────────
38
-
39
- export const AGENT_DOMAIN: Record<string, AgentDomain> = {
40
- "backend-javascript": "backend",
41
- "expert-csharp-developer": "backend",
42
- "golang-pro": "backend",
43
- "expert-nextjs-developer": "frontend",
44
- "frontend-flutter": "frontend",
45
- "expert-postgres-developer": "database",
46
- "testing-unit": "testing",
47
- "expert-code-reviewer": "review",
48
- "security-specialist": "security",
49
- "deep-explore": "explore",
50
- "architect": "architecture",
51
- };
38
+ // ── Agent → Domain mapping (derived from AGENT_REGISTRY) ─────
39
+
40
+ export const AGENT_DOMAIN: Record<string, AgentDomain> = buildAgentDomainMap();
52
41
 
53
42
  // ── Domain → Section relevance profiles ──────────────────────
54
43
  //
@@ -113,7 +102,11 @@ const RELEVANCE_OFFSET: Record<Relevance, number> = {
113
102
 
114
103
  export function getAgentDomain(agentName: string | null | undefined): AgentDomain | null {
115
104
  if (!agentName) return null;
116
- return AGENT_DOMAIN[agentName] ?? null;
105
+ // Fast path: direct canonical lookup
106
+ if (AGENT_DOMAIN[agentName]) return AGENT_DOMAIN[agentName];
107
+ // Resolve alias via registry
108
+ const entry = resolveAgent(agentName);
109
+ return entry?.domain ?? null;
117
110
  }
118
111
 
119
112
  export function domainToScope(domain: AgentDomain | null): string {
package/context/index.ts CHANGED
@@ -22,3 +22,6 @@ export {
22
22
  } from "./sections";
23
23
  export { findReferenceFiles } from "./references";
24
24
  export type { ReferenceFile } from "./references";
25
+ export { AGENT_REGISTRY, resolveAgent, resolveAgentName, suggestAgent, buildAgentDomainMap, getAllAgentNames, getCanonicalAgentNames } from "./agent-registry";
26
+ export type { AgentEntry } from "./agent-registry";
27
+ export { loadAgentExpertise, getAgentDescription, findAgentsDir, clearExpertiseCache } from "./agent-expertise";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.11",
3
+ "version": "9.0.13",
4
4
  "description": "Orchestrated workflow system for Claude Code - manages feature development through parallel subagents with structured phases, gates, and quality enforcement.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,11 @@
1
+ {{agentIdentity}}
2
+
1
3
  ╔══════════════════════════════════════════════════════════════════════════════╗
2
4
  ║ DIRETIVA CRITICA: USE Write/Edit PARA CRIAR OS ARQUIVOS ║
3
5
  ║ NAO descreva. NAO planeje. NAO simule. EXECUTE AGORA. ║
4
6
  ║ Se retornar sem usar Write/Edit, a task FALHA. ║
5
7
  ╚══════════════════════════════════════════════════════════════════════════════╝
6
-
8
+ {{agentExpertise}}
7
9
  ARQUIVOS QUE VOCE DEVE CRIAR (use Write para cada um):
8
10
  {{filesList}}
9
11