@codexa/cli 9.0.13 → 9.0.15

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,7 +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
+ import { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames, getAgentsByDomain } from "../context/agent-registry";
6
6
 
7
7
  export function generateSpecId(name: string): string {
8
8
  const date = new Date().toISOString().split("T")[0];
@@ -273,6 +273,14 @@ export function planTaskAdd(options: {
273
273
  if (normalizedAgent) {
274
274
  const entry = resolveAgent(normalizedAgent);
275
275
  if (!entry) {
276
+ // Check if it's an ambiguous domain name (multiple agents)
277
+ const domainAgents = getAgentsByDomain(normalizedAgent.toLowerCase());
278
+ if (domainAgents.length > 1) {
279
+ const agentList = domainAgents.map(a => `"${a.canonical}"`).join(", ");
280
+ throw new ValidationError(
281
+ `"${normalizedAgent}" e um dominio com ${domainAgents.length} agentes: ${agentList}.\nEspecifique qual agente usar.`
282
+ );
283
+ }
276
284
  const suggestion = suggestAgent(normalizedAgent);
277
285
  const hint = suggestion
278
286
  ? `\nVoce quis dizer: "${suggestion}"?`
@@ -7,6 +7,7 @@ import {
7
7
  buildAgentDomainMap,
8
8
  getAllAgentNames,
9
9
  getCanonicalAgentNames,
10
+ getAgentsByDomain,
10
11
  } from "./agent-registry";
11
12
 
12
13
  // ── AGENT_REGISTRY structure ─────────────────────────────────
@@ -90,6 +91,27 @@ describe("resolveAgent", () => {
90
91
  it("returns null for empty/null-like input", () => {
91
92
  expect(resolveAgent("")).toBeNull();
92
93
  });
94
+
95
+ // Domain-based resolution (v9.5)
96
+ it("resolves unambiguous domain names (1 agent in domain)", () => {
97
+ expect(resolveAgent("database")?.canonical).toBe("expert-postgres-developer");
98
+ expect(resolveAgent("testing")?.canonical).toBe("testing-unit");
99
+ expect(resolveAgent("review")?.canonical).toBe("expert-code-reviewer");
100
+ expect(resolveAgent("security")?.canonical).toBe("security-specialist");
101
+ expect(resolveAgent("explore")?.canonical).toBe("deep-explore");
102
+ expect(resolveAgent("architecture")?.canonical).toBe("architect");
103
+ });
104
+
105
+ it("returns null for ambiguous domain names (multiple agents)", () => {
106
+ // frontend has nextjs + flutter, backend has go + csharp + js
107
+ expect(resolveAgent("frontend")).toBeNull();
108
+ expect(resolveAgent("backend")).toBeNull();
109
+ });
110
+
111
+ it("domain resolution is case-insensitive", () => {
112
+ expect(resolveAgent("DATABASE")?.canonical).toBe("expert-postgres-developer");
113
+ expect(resolveAgent("Testing")?.canonical).toBe("testing-unit");
114
+ });
93
115
  });
94
116
 
95
117
  // ── resolveAgentName ─────────────────────────────────────────
@@ -111,6 +133,16 @@ describe("resolveAgentName", () => {
111
133
  expect(resolveAgentName("custom-agent")).toBe("custom-agent");
112
134
  expect(resolveAgentName("my-special-agent")).toBe("my-special-agent");
113
135
  });
136
+
137
+ it("resolves unambiguous domain names to canonical", () => {
138
+ expect(resolveAgentName("database")).toBe("expert-postgres-developer");
139
+ expect(resolveAgentName("testing")).toBe("testing-unit");
140
+ });
141
+
142
+ it("returns ambiguous domain names unchanged (backward compat)", () => {
143
+ expect(resolveAgentName("frontend")).toBe("frontend");
144
+ expect(resolveAgentName("backend")).toBe("backend");
145
+ });
114
146
  });
115
147
 
116
148
  // ── suggestAgent ─────────────────────────────────────────────
@@ -163,6 +195,35 @@ describe("buildAgentDomainMap", () => {
163
195
  });
164
196
  });
165
197
 
198
+ // ── getAgentsByDomain ────────────────────────────────────────
199
+
200
+ describe("getAgentsByDomain", () => {
201
+ it("returns agents in multi-agent domains", () => {
202
+ const frontend = getAgentsByDomain("frontend");
203
+ expect(frontend).toHaveLength(2);
204
+ expect(frontend.map(a => a.canonical)).toContain("expert-nextjs-developer");
205
+ expect(frontend.map(a => a.canonical)).toContain("frontend-flutter");
206
+
207
+ const backend = getAgentsByDomain("backend");
208
+ expect(backend).toHaveLength(3);
209
+ });
210
+
211
+ it("returns agents in single-agent domains", () => {
212
+ const database = getAgentsByDomain("database");
213
+ expect(database).toHaveLength(1);
214
+ expect(database[0].canonical).toBe("expert-postgres-developer");
215
+
216
+ const testing = getAgentsByDomain("testing");
217
+ expect(testing).toHaveLength(1);
218
+ expect(testing[0].canonical).toBe("testing-unit");
219
+ });
220
+
221
+ it("returns empty array for unknown domains", () => {
222
+ expect(getAgentsByDomain("unknown")).toHaveLength(0);
223
+ expect(getAgentsByDomain("general")).toHaveLength(0);
224
+ });
225
+ });
226
+
166
227
  // ── getAllAgentNames / getCanonicalAgentNames ─────────────────
167
228
 
168
229
  describe("getAllAgentNames", () => {
@@ -115,12 +115,21 @@ for (const entry of AGENT_REGISTRY) {
115
115
 
116
116
  /**
117
117
  * Resolve any agent name variant (canonical, filename, alias) to registry entry.
118
- * Case-insensitive. Returns null if no match found.
118
+ * Case-insensitive. Falls back to domain-based resolution when exactly 1 agent
119
+ * exists in the matching domain (e.g., "database" → expert-postgres-developer).
120
+ * Returns null if no match found or if domain has multiple agents (ambiguous).
119
121
  */
120
122
  export function resolveAgent(name: string): AgentEntry | null {
121
123
  if (!name) return null;
122
124
  const lower = name.toLowerCase();
123
- return byCanonical.get(lower) ?? byAlias.get(lower) ?? null;
125
+ const direct = byCanonical.get(lower) ?? byAlias.get(lower);
126
+ if (direct) return direct;
127
+
128
+ // Domain fallback: resolve "frontend", "backend", etc. when unambiguous
129
+ const domainAgents = getAgentsByDomain(lower);
130
+ if (domainAgents.length === 1) return domainAgents[0];
131
+
132
+ return null;
124
133
  }
125
134
 
126
135
  /**
@@ -171,6 +180,14 @@ export function suggestAgent(input: string): string | null {
171
180
  return null;
172
181
  }
173
182
 
183
+ /**
184
+ * Get all agents belonging to a specific domain.
185
+ * Used for domain-based resolution (e.g., "frontend" → agents in frontend domain).
186
+ */
187
+ export function getAgentsByDomain(domain: string): AgentEntry[] {
188
+ return AGENT_REGISTRY.filter(e => e.domain === domain);
189
+ }
190
+
174
191
  /**
175
192
  * Derive AGENT_DOMAIN map from registry (backward compat with domains.ts).
176
193
  */
@@ -137,32 +137,38 @@ function detectCommand(candidates: string[]): string | null {
137
137
  }
138
138
 
139
139
  function findConfigFile(files: string[], pattern: string): string | null {
140
- // Buscar no cwd primeiro
141
- const cwdCandidate = resolve(process.cwd(), pattern.replace("*.", ""));
142
- if (!pattern.includes("*") && existsSync(cwdCandidate)) return cwdCandidate;
143
-
144
- // Buscar subindo diretorios a partir dos arquivos
145
140
  const existingFiles = files.filter(f => existsSync(f));
146
- if (existingFiles.length === 0) return null;
147
-
148
- let dir = dirname(resolve(existingFiles[0]));
149
141
  const root = resolve("/");
150
-
151
- while (dir !== root) {
152
- if (pattern.includes("*")) {
153
- // Glob simples: *.csproj
154
- try {
155
- const entries = require("fs").readdirSync(dir) as string[];
156
- const ext = pattern.replace("*", "");
157
- const match = entries.find((e: string) => e.endsWith(ext));
158
- if (match) return resolve(dir, match);
159
- } catch { /* continue */ }
160
- } else {
161
- const candidate = resolve(dir, pattern);
162
- if (existsSync(candidate)) return candidate;
142
+ const cwd = process.cwd();
143
+
144
+ // Monorepo-aware: buscar subindo a partir dos arquivos primeiro,
145
+ // parando no cwd (raiz do projeto). Isso encontra o tsconfig mais
146
+ // proximo (ex: apps/frontend/tsconfig.json) antes do tsconfig raiz.
147
+ if (existingFiles.length > 0) {
148
+ let dir = dirname(resolve(existingFiles[0]));
149
+
150
+ while (dir !== root) {
151
+ if (pattern.includes("*")) {
152
+ try {
153
+ const entries = require("fs").readdirSync(dir) as string[];
154
+ const ext = pattern.replace("*", "");
155
+ const match = entries.find((e: string) => e.endsWith(ext));
156
+ if (match) return resolve(dir, match);
157
+ } catch { /* continue */ }
158
+ } else {
159
+ const candidate = resolve(dir, pattern);
160
+ if (existsSync(candidate)) return candidate;
161
+ }
162
+ dir = dirname(dir);
163
163
  }
164
- dir = dirname(dir);
165
164
  }
165
+
166
+ // Fallback: cwd (so chega aqui se nenhum config foi encontrado subindo)
167
+ if (!pattern.includes("*")) {
168
+ const cwdCandidate = resolve(cwd, pattern.replace("*.", ""));
169
+ if (existsSync(cwdCandidate)) return cwdCandidate;
170
+ }
171
+
166
172
  return null;
167
173
  }
168
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.13",
3
+ "version": "9.0.15",
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": {