@codexa/cli 9.0.9 → 9.0.11

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.
@@ -12,6 +12,7 @@ import {
12
12
  type UnifiedDetectionResult,
13
13
  } from "../detectors/loader";
14
14
  import { CodexaError } from "../errors";
15
+ import { getGrepaiWorkspace } from "./patterns";
15
16
 
16
17
  interface StackDetection {
17
18
  frontend?: string;
@@ -1123,19 +1124,36 @@ export function ensureDeepExploreAgent(): void {
1123
1124
  console.warn(" Instale com: go install github.com/your-org/grepai@latest");
1124
1125
  console.warn(" Sem grepai, o deep-explore usara apenas Grep (menos eficaz).\n");
1125
1126
  } else {
1126
- // Check if grepai index exists for this project
1127
+ // Check if grepai index exists for this project (local GOB or workspace)
1127
1128
  const grepaiDir = join(process.cwd(), ".grepai");
1128
- if (!existsSync(grepaiDir)) {
1129
+ const workspace = getGrepaiWorkspace();
1130
+ if (!existsSync(grepaiDir) && !workspace) {
1129
1131
  console.warn("\n⚠ grepai index nao encontrado neste projeto.");
1130
- console.warn(" Execute: grepai index");
1132
+ console.warn(" Execute: grepai init && grepai watch");
1133
+ console.warn(" Ou configure um workspace: codexa discover set-stack --grepai-workspace <nome>");
1131
1134
  console.warn(" Sem o index, grepai search nao retornara resultados.\n");
1135
+ } else if (workspace) {
1136
+ console.log(`✓ grepai workspace configurado: ${workspace}`);
1132
1137
  }
1133
1138
  }
1134
1139
 
1135
1140
  // Ensure grepai permissions in target project settings
1136
1141
  ensureGrepaiPermissions();
1137
1142
 
1138
- if (existsSync(agentPath)) return;
1143
+ // Check if workspace config changed — regenerate if needed
1144
+ const currentWorkspace = getGrepaiWorkspace();
1145
+ const wsFlag = currentWorkspace ? ` --workspace ${currentWorkspace}` : "";
1146
+
1147
+ if (existsSync(agentPath)) {
1148
+ const existing = readFileSync(agentPath, "utf-8");
1149
+ const hasWorkspace = existing.includes("--workspace ");
1150
+ if ((currentWorkspace && !hasWorkspace) || (!currentWorkspace && hasWorkspace)) {
1151
+ console.log("⟳ Atualizando deep-explore.md com config de workspace...");
1152
+ // Fall through to regenerate
1153
+ } else {
1154
+ return; // Already up to date
1155
+ }
1156
+ }
1139
1157
 
1140
1158
  const agentsDir = join(process.cwd(), ".claude", "agents");
1141
1159
  if (!existsSync(agentsDir)) {
@@ -1165,7 +1183,7 @@ You are a semantic code exploration agent. Your primary tool is \\\`grepai\\\`
1165
1183
  **IMPORTANT: Execute ONE grepai search per Bash call. Do NOT run multiple grepai commands in parallel — this causes "Sibling tool call errored". Run them SEQUENTIALLY, one at a time.**
1166
1184
 
1167
1185
  \\\`\\\`\\\`bash
1168
- grepai search "your query here" --json --compact
1186
+ grepai search "your query here" --json --compact${wsFlag}
1169
1187
  \\\`\\\`\\\`
1170
1188
 
1171
1189
  - Queries MUST be in English
@@ -1176,28 +1194,28 @@ grepai search "your query here" --json --compact
1176
1194
  Examples (run each one SEPARATELY):
1177
1195
 
1178
1196
  \\\`\\\`\\\`bash
1179
- grepai search "authentication flow" --json --compact
1197
+ grepai search "authentication flow" --json --compact${wsFlag}
1180
1198
  \\\`\\\`\\\`
1181
1199
 
1182
1200
  \\\`\\\`\\\`bash
1183
- grepai search "error handling middleware" --json --compact
1201
+ grepai search "error handling middleware" --json --compact${wsFlag}
1184
1202
  \\\`\\\`\\\`
1185
1203
 
1186
1204
  \\\`\\\`\\\`bash
1187
- grepai search "database connection management" --json --compact
1205
+ grepai search "database connection management" --json --compact${wsFlag}
1188
1206
  \\\`\\\`\\\`
1189
1207
 
1190
1208
  ### Step 2: Use grepai trace for relationships
1191
1209
 
1192
1210
  \\\`\\\`\\\`bash
1193
1211
  # Find all functions that call a symbol
1194
- grepai trace callers "HandleRequest" --json
1212
+ grepai trace callers "HandleRequest" --json${wsFlag}
1195
1213
 
1196
1214
  # Find all functions called by a symbol
1197
- grepai trace callees "ProcessOrder" --json
1215
+ grepai trace callees "ProcessOrder" --json${wsFlag}
1198
1216
 
1199
1217
  # Build complete call graph
1200
- grepai trace graph "ValidateToken" --depth 3 --json
1218
+ grepai trace graph "ValidateToken" --depth 3 --json${wsFlag}
1201
1219
  \\\`\\\`\\\`
1202
1220
 
1203
1221
  ### Step 3: Read files identified by grepai
@@ -1210,6 +1228,15 @@ Use Grep ONLY if you need exact string match (variable name, import path). Never
1210
1228
 
1211
1229
  ---
1212
1230
 
1231
+ ### Workspace (cross-project search)
1232
+
1233
+ If \\\`--workspace\\\` flag is included in the commands above, this project uses a grepai workspace
1234
+ for cross-project semantic search. The flag was injected automatically by the Codexa CLI.
1235
+
1236
+ - With workspace: searches across ALL projects in the workspace
1237
+ - Without workspace: searches only the local project index
1238
+ - To filter by project: add \\\`--project <name>\\\`
1239
+
1213
1240
  ### FORBIDDEN
1214
1241
 
1215
1242
  - **DO NOT** use \\\`find\\\` or \\\`ls\\\` to explore code — use \\\`grepai search\\\`
@@ -33,15 +33,42 @@ export function isGrepaiAvailable(): boolean {
33
33
  }
34
34
  }
35
35
 
36
+ // ═══════════════════════════════════════════════════════════════
37
+ // WORKSPACE CONFIG
38
+ // ═══════════════════════════════════════════════════════════════
39
+
40
+ const WORKSPACE_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
41
+
42
+ export function isValidWorkspaceName(name: string): boolean {
43
+ return WORKSPACE_NAME_REGEX.test(name);
44
+ }
45
+
46
+ export function getGrepaiWorkspace(): string | null {
47
+ try {
48
+ initSchema();
49
+ const db = getDb();
50
+ const project = db.query(
51
+ "SELECT grepai_workspace FROM project WHERE id = 'default'"
52
+ ).get() as any;
53
+ return project?.grepai_workspace || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
36
59
  // ═══════════════════════════════════════════════════════════════
37
60
  // BUSCA SEMÂNTICA
38
61
  // ═══════════════════════════════════════════════════════════════
39
62
 
40
- export function searchWithGrepai(query: string, topK: number = 10): GrepaiResult[] {
63
+ export function searchWithGrepai(query: string, topK: number = 10, workspace?: string): GrepaiResult[] {
41
64
  try {
65
+ const args = ["search", query, "--top", topK.toString(), "--format", "json"];
66
+ if (workspace) {
67
+ args.push("--workspace", workspace);
68
+ }
42
69
  const result = spawnSync(
43
70
  "grepai",
44
- ["search", query, "--top", topK.toString(), "--format", "json"],
71
+ args,
45
72
  { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
46
73
  );
47
74
 
package/commands/sync.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { execSync } from "child_process";
2
- import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
2
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { CodexaError } from "../errors";
5
+ import { getGrepaiWorkspace } from "./patterns";
5
6
 
6
7
  // Agents que sao referencias (nao spawnables como subagent_type)
7
8
  const REFERENCE_FILES = new Set([
@@ -71,6 +72,16 @@ export function syncAgents(options: { force?: boolean } = {}): void {
71
72
  }
72
73
 
73
74
  copyFileSync(src, dst);
75
+
76
+ // Inject workspace flag in deep-explore if configured
77
+ if (file === "deep-explore.md") {
78
+ const workspace = getGrepaiWorkspace();
79
+ const wsFlag = workspace ? ` --workspace ${workspace}` : "";
80
+ let agentContent = readFileSync(dst, "utf-8");
81
+ agentContent = agentContent.replace(/\{\{GREPAI_WS_FLAG\}\}/g, wsFlag);
82
+ writeFileSync(dst, agentContent);
83
+ }
84
+
74
85
  copied++;
75
86
  }
76
87
 
@@ -0,0 +1,253 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import {
4
+ detectPhase,
5
+ buildLeadSpawnInstruction,
6
+ buildImpSpawnPrompt,
7
+ buildRevSpawnPrompt,
8
+ buildArchitectSpawnPrompt,
9
+ buildDevilAdvocatePrompt,
10
+ type TeammateConfig,
11
+ } from "./team";
12
+
13
+ // ─────────────────────────────────────────────────────────────────
14
+ // Helpers
15
+ // ─────────────────────────────────────────────────────────────────
16
+
17
+ function createTestDb(): Database {
18
+ const db = new Database(":memory:");
19
+
20
+ db.run(`CREATE TABLE specs (
21
+ id TEXT PRIMARY KEY, name TEXT, phase TEXT, approved_at TEXT,
22
+ analysis_id TEXT, created_at TEXT, updated_at TEXT
23
+ )`);
24
+
25
+ db.run(`CREATE TABLE tasks (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT, spec_id TEXT, number INTEGER,
27
+ name TEXT, depends_on TEXT, can_parallel INTEGER DEFAULT 1,
28
+ agent TEXT, files TEXT, status TEXT DEFAULT 'pending',
29
+ checkpoint TEXT, started_at TEXT, completed_at TEXT
30
+ )`);
31
+
32
+ db.run(`CREATE TABLE artifacts (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT, spec_id TEXT,
34
+ task_ref INTEGER, path TEXT, action TEXT, created_at TEXT
35
+ )`);
36
+
37
+ db.run(`CREATE TABLE architectural_analyses (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, status TEXT DEFAULT 'pending'
39
+ )`);
40
+
41
+ db.run(`CREATE TABLE project (
42
+ id TEXT PRIMARY KEY DEFAULT 'default', stack TEXT,
43
+ cli_version TEXT, typecheck_command TEXT, grepai_workspace TEXT
44
+ )`);
45
+
46
+ db.run("INSERT INTO project (id) VALUES ('default')");
47
+
48
+ return db;
49
+ }
50
+
51
+ function insertSpec(db: Database, id: string, name: string, phase: string): void {
52
+ db.run("INSERT INTO specs (id, name, phase, created_at) VALUES (?, ?, ?, datetime('now'))", [id, name, phase]);
53
+ }
54
+
55
+ function insertTask(
56
+ db: Database,
57
+ specId: string,
58
+ number: number,
59
+ name: string,
60
+ options: { agent?: string; canParallel?: boolean; dependsOn?: number[]; status?: string } = {}
61
+ ): number {
62
+ const result = db.run(
63
+ `INSERT INTO tasks (spec_id, number, name, agent, can_parallel, depends_on, status)
64
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
65
+ [
66
+ specId,
67
+ number,
68
+ name,
69
+ options.agent || "general",
70
+ options.canParallel !== false ? 1 : 0,
71
+ options.dependsOn ? JSON.stringify(options.dependsOn) : null,
72
+ options.status || "pending",
73
+ ]
74
+ );
75
+ return Number(result.lastInsertRowid);
76
+ }
77
+
78
+ function insertArtifact(db: Database, specId: string, taskRef: number, path: string): void {
79
+ db.run("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, ?, ?, 'created')", [specId, taskRef, path]);
80
+ }
81
+
82
+ // ─────────────────────────────────────────────────────────────────
83
+ // Tests
84
+ // ─────────────────────────────────────────────────────────────────
85
+
86
+ describe("detectPhase", () => {
87
+ it("detecta imp quando spec.phase = implementing", () => {
88
+ const db = createTestDb();
89
+ const spec = { phase: "implementing" };
90
+ expect(detectPhase(db, spec)).toBe("imp");
91
+ });
92
+
93
+ it("detecta rev quando spec.phase = reviewing", () => {
94
+ const db = createTestDb();
95
+ const spec = { phase: "reviewing" };
96
+ expect(detectPhase(db, spec)).toBe("rev");
97
+ });
98
+
99
+ it("detecta architect quando ha analise pendente", () => {
100
+ const db = createTestDb();
101
+ db.run("INSERT INTO architectural_analyses (name, status) VALUES ('test', 'pending')");
102
+ const spec = { phase: "planning" };
103
+ expect(detectPhase(db, spec)).toBe("architect");
104
+ });
105
+
106
+ it("retorna phase original se nao ha analise pendente", () => {
107
+ const db = createTestDb();
108
+ const spec = { phase: "planning" };
109
+ expect(detectPhase(db, spec)).toBe("planning");
110
+ });
111
+ });
112
+
113
+ describe("buildLeadSpawnInstruction", () => {
114
+ it("gera instrucao em linguagem natural", () => {
115
+ const teammates: TeammateConfig[] = [
116
+ { role: "backend-implementer", domain: "backend", taskIds: [1], spawnPrompt: "test prompt" },
117
+ ];
118
+ const instruction = buildLeadSpawnInstruction(teammates, "imp");
119
+ expect(instruction).toContain("Create an agent team");
120
+ expect(instruction).toContain("delegate mode");
121
+ });
122
+
123
+ it("inclui nome de todos teammates", () => {
124
+ const teammates: TeammateConfig[] = [
125
+ { role: "backend-implementer", domain: "backend", taskIds: [1], spawnPrompt: "p1" },
126
+ { role: "frontend-implementer", domain: "frontend", taskIds: [2], spawnPrompt: "p2" },
127
+ ];
128
+ const instruction = buildLeadSpawnInstruction(teammates, "imp");
129
+ expect(instruction).toContain("backend-implementer");
130
+ expect(instruction).toContain("frontend-implementer");
131
+ });
132
+
133
+ it("inclui instrucao de delegate mode", () => {
134
+ const teammates: TeammateConfig[] = [
135
+ { role: "test", domain: "test", taskIds: [], spawnPrompt: "p" },
136
+ ];
137
+ const instruction = buildLeadSpawnInstruction(teammates, "rev");
138
+ expect(instruction).toContain("do NOT implement anything yourself");
139
+ });
140
+
141
+ it("usa descricao correta por fase", () => {
142
+ const tm: TeammateConfig[] = [{ role: "r", domain: "d", taskIds: [], spawnPrompt: "p" }];
143
+ expect(buildLeadSpawnInstruction(tm, "imp")).toContain("implement parallel tasks");
144
+ expect(buildLeadSpawnInstruction(tm, "rev")).toContain("review artifacts");
145
+ expect(buildLeadSpawnInstruction(tm, "architect")).toContain("competing architectural");
146
+ });
147
+ });
148
+
149
+ describe("buildImpSpawnPrompt", () => {
150
+ it("inclui identidade de teammate", () => {
151
+ const prompt = buildImpSpawnPrompt({ name: "Test Feature" }, "backend", [
152
+ { number: 1, name: "Task 1", id: 10, files: '["src/a.ts"]' },
153
+ ]);
154
+ expect(prompt).toContain("TEAMMATE de implementacao");
155
+ expect(prompt).toContain("Dominio: backend");
156
+ });
157
+
158
+ it("inclui secao TASK LIST com instrucao de ignorar nativa", () => {
159
+ const prompt = buildImpSpawnPrompt({ name: "F" }, "backend", [
160
+ { number: 1, name: "T1", id: 1, files: null },
161
+ ]);
162
+ expect(prompt).toContain("TASK LIST");
163
+ expect(prompt).toContain("IGNORE");
164
+ expect(prompt).toContain("Codexa tasks");
165
+ });
166
+
167
+ it("inclui instrucao de messaging nativo", () => {
168
+ const prompt = buildImpSpawnPrompt({ name: "F" }, "backend", [
169
+ { number: 1, name: "T1", id: 1, files: null },
170
+ ]);
171
+ expect(prompt).toContain("ferramenta `message`");
172
+ expect(prompt).toContain("codexa knowledge add");
173
+ });
174
+
175
+ it("lista todas tasks atribuidas", () => {
176
+ const prompt = buildImpSpawnPrompt({ name: "F" }, "backend", [
177
+ { number: 1, name: "Create schema", id: 10, files: '["db/schema.ts"]' },
178
+ { number: 2, name: "Add migration", id: 11, files: null },
179
+ ]);
180
+ expect(prompt).toContain("Task #1: Create schema");
181
+ expect(prompt).toContain("Task #2: Add migration");
182
+ });
183
+ });
184
+
185
+ describe("buildRevSpawnPrompt", () => {
186
+ it("inclui lente correta", () => {
187
+ const prompt = buildRevSpawnPrompt({ name: "F" }, "security", ["src/a.ts"]);
188
+ expect(prompt).toContain("**SECURITY**");
189
+ expect(prompt).toContain("OWASP");
190
+ });
191
+
192
+ it("inclui instrucao de message para enviar findings", () => {
193
+ const prompt = buildRevSpawnPrompt({ name: "F" }, "performance", ["src/a.ts"]);
194
+ expect(prompt).toContain("ferramenta `message`");
195
+ });
196
+
197
+ it("inclui instrucao de ignorar task list nativa", () => {
198
+ const prompt = buildRevSpawnPrompt({ name: "F" }, "standards", ["src/a.ts"]);
199
+ expect(prompt).toContain("IGNORE a task list nativa");
200
+ });
201
+
202
+ it("lista artefatos para revisao", () => {
203
+ const prompt = buildRevSpawnPrompt({ name: "F" }, "security", ["src/a.ts", "src/b.ts"]);
204
+ expect(prompt).toContain("- src/a.ts");
205
+ expect(prompt).toContain("- src/b.ts");
206
+ });
207
+ });
208
+
209
+ describe("buildArchitectSpawnPrompt", () => {
210
+ it("inclui identidade de architect", () => {
211
+ const prompt = buildArchitectSpawnPrompt({ name: "Add auth" }, {}, "alpha");
212
+ expect(prompt).toContain("ARCHITECT ALPHA");
213
+ });
214
+
215
+ it("inclui instrucao de message para proposta", () => {
216
+ const prompt = buildArchitectSpawnPrompt({ name: "Add auth" }, {}, "beta");
217
+ expect(prompt).toContain("ferramenta `message`");
218
+ expect(prompt).toContain("Enviar ao Lead");
219
+ });
220
+
221
+ it("inclui 8 headers obrigatorios", () => {
222
+ const prompt = buildArchitectSpawnPrompt({ name: "Add auth" }, {}, "alpha");
223
+ expect(prompt).toContain("Contexto e Entendimento");
224
+ expect(prompt).toContain("Baby Steps");
225
+ expect(prompt).toContain("Decisoes Arquiteturais");
226
+ });
227
+ });
228
+
229
+ describe("buildDevilAdvocatePrompt", () => {
230
+ it("inclui explicacao de entrega automatica de mensagens", () => {
231
+ const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
232
+ expect(prompt).toContain("entregues automaticamente");
233
+ expect(prompt).toContain("sem precisar");
234
+ expect(prompt).toContain("polling");
235
+ });
236
+
237
+ it("instrui a aguardar AMBAS propostas", () => {
238
+ const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
239
+ expect(prompt).toContain("AMBAS as propostas");
240
+ });
241
+
242
+ it("inclui formato estruturado de critica", () => {
243
+ const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
244
+ expect(prompt).toContain("Pontos Fortes");
245
+ expect(prompt).toContain("Pontos Fracos");
246
+ expect(prompt).toContain("FORTE | VIAVEL | FRACO");
247
+ });
248
+
249
+ it("inclui instrucao de IGNORE task list nativa", () => {
250
+ const prompt = buildDevilAdvocatePrompt({ name: "Add auth" });
251
+ expect(prompt).toContain("IGNORE a task list nativa");
252
+ });
253
+ });