@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 +9 -1
- package/context/agent-registry.test.ts +61 -0
- package/context/agent-registry.ts +19 -2
- package/gates/typecheck-validator.ts +28 -22
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
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": {
|