@codexa/cli 9.0.7 → 9.0.8

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.
@@ -1,14 +1,20 @@
1
1
  /**
2
- * Gate 4.5: Typecheck Validator
2
+ * Gate 4.5: Typecheck Validator (Stack-Agnostic)
3
3
  *
4
4
  * Executa verificacao de tipos nos arquivos criados/modificados pelo subagent.
5
- * Requer tsconfig.json no projeto.
5
+ * Suporta multiplas stacks via presets automaticos + comando custom configuravel.
6
+ *
7
+ * Prioridade: custom_command > preset da stack > skip
6
8
  */
7
9
 
8
10
  import { existsSync } from "fs";
9
11
  import { spawnSync } from "child_process";
10
12
  import { resolve, dirname } from "path";
11
13
 
14
+ // ═══════════════════════════════════════════════════════════════
15
+ // INTERFACES
16
+ // ═══════════════════════════════════════════════════════════════
17
+
12
18
  export interface TypecheckResult {
13
19
  passed: boolean;
14
20
  errors: TypecheckError[];
@@ -24,136 +30,334 @@ export interface TypecheckError {
24
30
  code: string;
25
31
  }
26
32
 
27
- /**
28
- * Encontra o tsconfig.json mais proximo subindo diretorios
29
- */
30
- function findTsConfig(startDir: string): string | null {
31
- let dir = startDir;
33
+ interface TypecheckPreset {
34
+ fileFilter: RegExp;
35
+ setup: (files: string[]) => string[] | null; // Returns command args or null if unavailable
36
+ errorParser: (output: string, files: string[]) => TypecheckError[];
37
+ }
38
+
39
+ // ═══════════════════════════════════════════════════════════════
40
+ // PRESETS POR STACK
41
+ // ═══════════════════════════════════════════════════════════════
42
+
43
+ const TYPECHECK_PRESETS: Record<string, TypecheckPreset> = {
44
+ node: {
45
+ fileFilter: /\.(ts|tsx)$/,
46
+ setup: (files) => {
47
+ const tsconfig = findConfigFile(files, "tsconfig.json");
48
+ if (!tsconfig) return null;
49
+ const tscCmd = detectCommand(["bunx tsc", "npx tsc", "tsc"]);
50
+ if (!tscCmd) return null;
51
+ return [...tscCmd.split(" "), "--noEmit", "--project", tsconfig];
52
+ },
53
+ errorParser: parseColonSeparatedErrors(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/),
54
+ },
55
+
56
+ go: {
57
+ fileFilter: /\.go$/,
58
+ setup: () => {
59
+ if (!isCommandAvailable("go")) return null;
60
+ return ["go", "vet", "./..."];
61
+ },
62
+ errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(\d+):\s+(.+)$/),
63
+ },
64
+
65
+ dotnet: {
66
+ fileFilter: /\.cs$/,
67
+ setup: (files) => {
68
+ const csproj = findConfigFile(files, "*.csproj");
69
+ if (!csproj && !isCommandAvailable("dotnet")) return null;
70
+ return ["dotnet", "build", "--no-restore", "--nologo", "-v", "q"];
71
+ },
72
+ errorParser: parseColonSeparatedErrors(/^(.+)\((\d+),(\d+)\):\s+error\s+(CS\d+):\s+(.+)$/),
73
+ },
74
+
75
+ rust: {
76
+ fileFilter: /\.rs$/,
77
+ setup: () => {
78
+ if (!isCommandAvailable("cargo")) return null;
79
+ return ["cargo", "check", "--message-format", "short"];
80
+ },
81
+ errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(\d+):\s+error(?:\[(\w+)\])?:\s+(.+)$/),
82
+ },
83
+
84
+ python: {
85
+ fileFilter: /\.py$/,
86
+ setup: () => {
87
+ // Tentar mypy primeiro, depois pyright
88
+ const checker = detectCommand(["mypy", "pyright"]);
89
+ if (!checker) return null;
90
+ if (checker === "mypy") return ["mypy", "--no-error-summary"];
91
+ return ["pyright"];
92
+ },
93
+ errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(?:(\d+):)?\s+error:\s+(.+)$/),
94
+ },
95
+
96
+ flutter: {
97
+ fileFilter: /\.dart$/,
98
+ setup: () => {
99
+ if (!isCommandAvailable("dart")) return null;
100
+ return ["dart", "analyze", "--no-preamble"];
101
+ },
102
+ errorParser: parseColonSeparatedErrors(/^\s*(.+):(\d+):(\d+)\s+.*error.*[-•]\s+(.+)$/),
103
+ },
104
+
105
+ jvm: {
106
+ fileFilter: /\.(java|kt|kts)$/,
107
+ setup: () => {
108
+ // Gradle ou Maven
109
+ if (existsSync("gradlew") || existsSync("gradlew.bat")) {
110
+ const cmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
111
+ return [cmd, "compileJava", "-q"];
112
+ }
113
+ if (existsSync("pom.xml") && isCommandAvailable("mvn")) {
114
+ return ["mvn", "compile", "-q"];
115
+ }
116
+ return null;
117
+ },
118
+ errorParser: parseColonSeparatedErrors(/^(.+):(\d+):\s+error:\s+(.+)$/),
119
+ },
120
+ };
121
+
122
+ // ═══════════════════════════════════════════════════════════════
123
+ // HELPERS
124
+ // ═══════════════════════════════════════════════════════════════
125
+
126
+ function isCommandAvailable(cmd: string): boolean {
127
+ const result = spawnSync(cmd, ["--version"], { encoding: "utf-8", timeout: 3000 });
128
+ return !result.error;
129
+ }
130
+
131
+ function detectCommand(candidates: string[]): string | null {
132
+ for (const cmd of candidates) {
133
+ const bin = cmd.split(" ")[0];
134
+ if (isCommandAvailable(bin)) return cmd;
135
+ }
136
+ return null;
137
+ }
138
+
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
+ const existingFiles = files.filter(f => existsSync(f));
146
+ if (existingFiles.length === 0) return null;
147
+
148
+ let dir = dirname(resolve(existingFiles[0]));
32
149
  const root = resolve("/");
33
150
 
34
151
  while (dir !== root) {
35
- const candidate = resolve(dir, "tsconfig.json");
36
- if (existsSync(candidate)) return candidate;
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;
163
+ }
37
164
  dir = dirname(dir);
38
165
  }
39
166
  return null;
40
167
  }
41
168
 
169
+ function isFileRelevant(errorFile: string, relevantFiles: string[]): boolean {
170
+ const normalized = errorFile.replace(/\\/g, "/");
171
+ return relevantFiles.some(rf => {
172
+ const normalizedRf = rf.replace(/\\/g, "/");
173
+ return (
174
+ normalized.endsWith(normalizedRf) ||
175
+ normalized.includes(normalizedRf) ||
176
+ normalizedRf.endsWith(normalized) ||
177
+ normalizedRf.includes(normalized)
178
+ );
179
+ });
180
+ }
181
+
42
182
  /**
43
- * Detecta o runtime do projeto para escolher o comando tsc
183
+ * Factory para parsers de erro com formato "file:line:col: message"
184
+ * Aceita regex com 4 ou 5 capture groups:
185
+ * 5 groups: (file, line, col, code, message)
186
+ * 4 groups: (file, line, col, message) — code sera "ERROR"
44
187
  */
45
- function detectTscCommand(): string[] {
46
- const bunCheck = spawnSync("bun", ["--version"], { encoding: "utf-8", timeout: 3000 });
47
- if (!bunCheck.error) return ["bunx", "tsc"];
188
+ function parseColonSeparatedErrors(regex: RegExp) {
189
+ return (output: string, relevantFiles: string[]): TypecheckError[] => {
190
+ const errors: TypecheckError[] = [];
191
+ const lines = output.split(/\r?\n/);
48
192
 
49
- const npxCheck = spawnSync("npx", ["--version"], { encoding: "utf-8", timeout: 3000 });
50
- if (!npxCheck.error) return ["npx", "tsc"];
193
+ for (const line of lines) {
194
+ const match = line.match(regex);
195
+ if (!match) continue;
51
196
 
52
- const tscCheck = spawnSync("tsc", ["--version"], { encoding: "utf-8", timeout: 3000 });
53
- if (!tscCheck.error) return ["tsc"];
197
+ let file: string, lineNum: string, col: string, code: string, message: string;
54
198
 
55
- return [];
56
- }
199
+ if (match.length >= 6) {
200
+ [, file, lineNum, col, code, message] = match;
201
+ } else if (match.length >= 5) {
202
+ [, file, lineNum, col, message] = match;
203
+ code = "ERROR";
204
+ } else {
205
+ continue;
206
+ }
57
207
 
58
- /**
59
- * Parseia output do tsc em erros estruturados
60
- */
61
- function parseTscOutput(output: string, relevantFiles: string[]): TypecheckError[] {
62
- const errors: TypecheckError[] = [];
63
- const lines = output.split(/\r?\n/);
64
-
65
- // Formato: file(line,col): error TSxxxx: message
66
- const errorRegex = /^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
67
-
68
- for (const line of lines) {
69
- const match = line.match(errorRegex);
70
- if (!match) continue;
71
-
72
- const [, file, lineNum, col, code, message] = match;
73
- const normalizedFile = file.replace(/\\/g, "/");
74
-
75
- // Filtrar: apenas erros nos arquivos relevantes
76
- const isRelevant = relevantFiles.some(rf => {
77
- const normalizedRf = rf.replace(/\\/g, "/");
78
- return (
79
- normalizedFile.endsWith(normalizedRf) ||
80
- normalizedFile.includes(normalizedRf) ||
81
- normalizedRf.endsWith(normalizedFile) ||
82
- normalizedRf.includes(normalizedFile)
83
- );
84
- });
208
+ if (!isFileRelevant(file, relevantFiles)) continue;
85
209
 
86
- if (isRelevant) {
87
210
  errors.push({
88
- file: normalizedFile,
89
- line: parseInt(lineNum),
90
- column: parseInt(col),
91
- message,
92
- code,
211
+ file: file.replace(/\\/g, "/"),
212
+ line: parseInt(lineNum) || 0,
213
+ column: parseInt(col) || 0,
214
+ message: message?.trim() || "",
215
+ code: code || "ERROR",
93
216
  });
94
217
  }
95
- }
96
218
 
97
- return errors;
219
+ return errors;
220
+ };
98
221
  }
99
222
 
100
223
  /**
101
- * Executa typecheck nos arquivos especificados
224
+ * Resolve ecosystems da stack JSON do projeto
102
225
  */
103
- export function runTypecheck(files: string[]): TypecheckResult {
104
- // Filtrar apenas arquivos TS/TSX
105
- const tsFiles = files.filter(f => /\.(ts|tsx)$/.test(f) && existsSync(f));
106
-
107
- if (tsFiles.length === 0) {
108
- return { passed: true, errors: [], skipped: true, reason: "Nenhum arquivo TypeScript" };
226
+ function resolveEcosystems(stackJson: string | null): string[] {
227
+ if (!stackJson) return [];
228
+ try {
229
+ const stack = JSON.parse(stackJson);
230
+ // Formato novo (detectUniversal): tem ecosystems array
231
+ if (Array.isArray(stack.ecosystems)) return stack.ecosystems;
232
+ // Formato legado: tem chaves frontend/backend
233
+ if (stack.frontend || stack.backend) {
234
+ const ecosystems: string[] = [];
235
+ const stackStr = JSON.stringify(stack).toLowerCase();
236
+ if (stackStr.includes("typescript") || stackStr.includes("next") || stackStr.includes("react") || stackStr.includes("node") || stackStr.includes("bun")) ecosystems.push("node");
237
+ if (stackStr.includes("go") || stackStr.includes("golang")) ecosystems.push("go");
238
+ if (stackStr.includes("dotnet") || stackStr.includes("c#") || stackStr.includes("csharp")) ecosystems.push("dotnet");
239
+ if (stackStr.includes("python") || stackStr.includes("django") || stackStr.includes("flask") || stackStr.includes("fastapi")) ecosystems.push("python");
240
+ if (stackStr.includes("rust") || stackStr.includes("cargo")) ecosystems.push("rust");
241
+ if (stackStr.includes("flutter") || stackStr.includes("dart")) ecosystems.push("flutter");
242
+ if (stackStr.includes("java") || stackStr.includes("kotlin") || stackStr.includes("spring") || stackStr.includes("gradle")) ecosystems.push("jvm");
243
+ return ecosystems;
244
+ }
245
+ return [];
246
+ } catch {
247
+ return [];
109
248
  }
249
+ }
250
+
251
+ // ═══════════════════════════════════════════════════════════════
252
+ // EXECUCAO DO TYPECHECK
253
+ // ═══════════════════════════════════════════════════════════════
254
+
255
+ function runCustomCommand(customCommand: string, files: string[]): TypecheckResult {
256
+ const parts = customCommand.trim().split(/\s+/);
257
+ const [cmd, ...args] = parts;
110
258
 
111
- // Encontrar tsconfig.json (primeiro no cwd, depois no diretorio dos arquivos)
112
- let tsconfig = findTsConfig(process.cwd());
113
- if (!tsconfig) {
114
- const firstFileDir = dirname(resolve(tsFiles[0]));
115
- tsconfig = findTsConfig(firstFileDir);
259
+ if (!isCommandAvailable(cmd)) {
260
+ return { passed: true, errors: [], skipped: true, reason: `Comando custom '${cmd}' nao encontrado no PATH` };
116
261
  }
117
- if (!tsconfig) {
118
- return { passed: true, errors: [], skipped: true, reason: "tsconfig.json nao encontrado" };
262
+
263
+ const result = spawnSync(cmd, args, {
264
+ encoding: "utf-8",
265
+ timeout: 60000,
266
+ maxBuffer: 5 * 1024 * 1024,
267
+ cwd: process.cwd(),
268
+ });
269
+
270
+ if (result.error) {
271
+ return { passed: true, errors: [], skipped: true, reason: `Erro ao executar '${customCommand}': ${result.error.message}` };
119
272
  }
120
273
 
121
- // Detectar comando tsc
122
- const tscCmd = detectTscCommand();
123
- if (tscCmd.length === 0) {
124
- return { passed: true, errors: [], skipped: true, reason: "tsc nao encontrado (instale typescript)" };
274
+ const output = (result.stdout || "") + (result.stderr || "");
275
+ // Comando custom: verificar exit code (0 = pass, non-zero = fail)
276
+ // Parsear erros com formato generico file:line:col
277
+ const genericParser = parseColonSeparatedErrors(/^(.+):(\d+):(?:(\d+):?)?\s+(?:error|Error):\s+(.+)$/);
278
+ const errors = genericParser(output, files);
279
+
280
+ // Se nao encontrou erros parseados mas exit code != 0, reportar output bruto
281
+ if (result.status !== 0 && errors.length === 0) {
282
+ const trimmedOutput = output.trim().slice(0, 500);
283
+ return {
284
+ passed: false,
285
+ errors: [{ file: "typecheck", line: 0, column: 0, message: trimmedOutput, code: `EXIT_${result.status}` }],
286
+ skipped: false,
287
+ };
125
288
  }
126
289
 
127
- // Executar tsc --noEmit
128
- const result = spawnSync(
129
- tscCmd[0],
130
- [...tscCmd.slice(1), "--noEmit", "--project", tsconfig],
131
- {
290
+ return { passed: errors.length === 0, errors, skipped: false };
291
+ }
292
+
293
+ function runPresetTypecheck(ecosystems: string[], files: string[]): TypecheckResult {
294
+ // Tentar cada ecosystem detectado ate encontrar um que funcione
295
+ for (const eco of ecosystems) {
296
+ const preset = TYPECHECK_PRESETS[eco];
297
+ if (!preset) continue;
298
+
299
+ const relevantFiles = files.filter(f => preset.fileFilter.test(f) && existsSync(f));
300
+ if (relevantFiles.length === 0) continue;
301
+
302
+ const cmdArgs = preset.setup(relevantFiles);
303
+ if (!cmdArgs) continue;
304
+
305
+ const result = spawnSync(cmdArgs[0], cmdArgs.slice(1), {
132
306
  encoding: "utf-8",
133
307
  timeout: 60000,
134
308
  maxBuffer: 5 * 1024 * 1024,
135
- cwd: dirname(tsconfig),
309
+ cwd: process.cwd(),
310
+ });
311
+
312
+ if (result.error) {
313
+ return { passed: true, errors: [], skipped: true, reason: `Erro ao executar ${cmdArgs[0]}: ${result.error.message}` };
136
314
  }
137
- );
138
315
 
139
- if (result.error) {
140
- return { passed: true, errors: [], skipped: true, reason: `Erro ao executar tsc: ${result.error.message}` };
316
+ const output = (result.stdout || "") + (result.stderr || "");
317
+ const errors = preset.errorParser(output, relevantFiles);
318
+
319
+ return { passed: errors.length === 0, errors, skipped: false };
141
320
  }
142
321
 
143
- // Parsear erros, filtrando apenas os dos arquivos relevantes
144
- const output = (result.stdout || "") + (result.stderr || "");
145
- const errors = parseTscOutput(output, tsFiles);
322
+ return { passed: true, errors: [], skipped: true, reason: "Nenhum typechecker disponivel para a stack detectada" };
323
+ }
146
324
 
147
- return {
148
- passed: errors.length === 0,
149
- errors,
150
- skipped: false,
151
- };
325
+ // ═══════════════════════════════════════════════════════════════
326
+ // API PUBLICA
327
+ // ═══════════════════════════════════════════════════════════════
328
+
329
+ export function runTypecheck(files: string[], options?: { customCommand?: string | null; stackJson?: string | null }): TypecheckResult {
330
+ const existingFiles = files.filter(f => existsSync(f));
331
+ if (existingFiles.length === 0) {
332
+ return { passed: true, errors: [], skipped: true, reason: "Nenhum arquivo encontrado no disco" };
333
+ }
334
+
335
+ // Prioridade 1: Comando custom configurado pelo usuario
336
+ if (options?.customCommand) {
337
+ return runCustomCommand(options.customCommand, existingFiles);
338
+ }
339
+
340
+ // Prioridade 2: Preset automatico baseado na stack detectada
341
+ const ecosystems = resolveEcosystems(options?.stackJson || null);
342
+ if (ecosystems.length > 0) {
343
+ return runPresetTypecheck(ecosystems, existingFiles);
344
+ }
345
+
346
+ // Prioridade 3: Fallback — tentar detectar stack pelos arquivos
347
+ const fallbackEcosystems: string[] = [];
348
+ for (const [eco, preset] of Object.entries(TYPECHECK_PRESETS)) {
349
+ if (existingFiles.some(f => preset.fileFilter.test(f))) {
350
+ fallbackEcosystems.push(eco);
351
+ }
352
+ }
353
+
354
+ if (fallbackEcosystems.length > 0) {
355
+ return runPresetTypecheck(fallbackEcosystems, existingFiles);
356
+ }
357
+
358
+ return { passed: true, errors: [], skipped: true, reason: "Stack nao detectada e nenhum typechecker configurado" };
152
359
  }
153
360
 
154
- /**
155
- * Formata resultado para exibicao no CLI
156
- */
157
361
  export function printTypecheckResult(result: TypecheckResult): void {
158
362
  if (result.skipped) {
159
363
  console.log(`\n[i] Typecheck pulado: ${result.reason}`);
@@ -3,10 +3,12 @@ import { existsSync, readFileSync, statSync } from "fs";
3
3
  import { extname } from "path";
4
4
  import { validateAgainstStandards, printValidationResult } from "./standards-validator";
5
5
  import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
6
- import { extractUtilitiesFromFile } from "../commands/patterns";
6
+ import { extractUtilitiesFromFile, inferScopeFromPath } from "../commands/patterns";
7
7
  import { findDuplicateUtilities } from "../db/schema";
8
8
  import { GateError, RecoverySuggestion } from "../errors";
9
9
  import { resolveSpecOrNull } from "../commands/spec-resolver";
10
+ import { detectConflicts } from "../commands/decide";
11
+ import { getAgentDomain, domainToScope } from "../context/domains";
10
12
 
11
13
  export interface GateResult {
12
14
  passed: boolean;
@@ -92,7 +94,12 @@ const GATES: Record<string, GateCheck[]> = {
92
94
  {
93
95
  check: "typecheck-pass",
94
96
  message: "Erros de tipo encontrados nos arquivos da task",
95
- resolution: "Corrija os erros TypeScript ou use --force --force-reason para bypass",
97
+ resolution: "Corrija os erros de tipo ou use --force --force-reason para bypass",
98
+ },
99
+ {
100
+ check: "decision-conflicts",
101
+ message: "Conflito entre decisoes do subagent e decisoes ativas",
102
+ resolution: "Revise com: decisions. Revogue: decide:revoke <id>. Substitua: decide:supersede <id> <title> <decision>. Ou: --force --force-reason",
96
103
  },
97
104
  ],
98
105
  "review-start": [
@@ -131,11 +138,12 @@ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggesti
131
138
  ],
132
139
  }),
133
140
  "typecheck-pass": (details) => ({
134
- diagnostic: `Erros TypeScript encontrados:\n${details || "Detalhes nao disponiveis"}`,
141
+ diagnostic: `Erros de tipo encontrados:\n${details || "Detalhes nao disponiveis"}`,
135
142
  steps: [
136
143
  "Corrija os erros de tipo listados",
137
144
  "Verifique imports e definicoes de tipo",
138
145
  "Se erros em deps externas, use --force --force-reason 'motivo'",
146
+ "Para configurar typechecker custom: codexa project set --typecheck-command '<comando>'",
139
147
  ],
140
148
  }),
141
149
  "files-exist": (details) => ({
@@ -153,6 +161,16 @@ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggesti
153
161
  "Descreva O QUE foi feito, nao apenas 'feito' ou 'ok'",
154
162
  ],
155
163
  }),
164
+ "decision-conflicts": (details) => ({
165
+ diagnostic: `Conflitos de decisao:\n${details || "Detalhes nao disponiveis"}`,
166
+ steps: [
167
+ "Revise decisoes existentes: decisions",
168
+ "Revogue a antiga: decide:revoke <id> --reason 'motivo'",
169
+ "Ou substitua: decide:supersede <id> <title> <decision>",
170
+ "Se intencional: --force --force-reason 'motivo'",
171
+ ],
172
+ command: "codexa decisions",
173
+ }),
156
174
  "reasoning-provided": () => ({
157
175
  diagnostic: "Subagent retornou sem reasoning.approach adequado",
158
176
  steps: [
@@ -249,9 +267,17 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
249
267
  // v8.0: Validar não apenas existência, mas conteúdo mínimo
250
268
  // v9.2: Validar que arquivo foi modificado DURANTE a task
251
269
  // v9.3: Tolerancia de 5s para clock skew em sandbox
270
+ // v9.6: Confiar no protocolo quando subagent reporta arquivos (sandbox)
252
271
  const MTIME_TOLERANCE_MS = 5000;
253
272
  const issues: string[] = [];
254
273
 
274
+ // Construir set de arquivos reportados pelo subagent (podem estar em sandbox)
275
+ const subagentReportedFiles = new Set<string>();
276
+ if (context.subagentData) {
277
+ for (const f of context.subagentData.files_created || []) subagentReportedFiles.add(f);
278
+ for (const f of context.subagentData.files_modified || []) subagentReportedFiles.add(f);
279
+ }
280
+
255
281
  // Buscar started_at da task para comparacao temporal
256
282
  let taskStartTime: number | null = null;
257
283
  if (context.taskId) {
@@ -262,6 +288,17 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
262
288
  }
263
289
 
264
290
  for (const file of context.files) {
291
+ if (!existsSync(file)) {
292
+ // Arquivo nao existe no disco — verificar se veio de subagent
293
+ if (subagentReportedFiles.has(file)) {
294
+ // Subagent reportou o arquivo via protocolo → confiar
295
+ // (arquivo pode estar em sandbox, sera materializado no commit)
296
+ continue;
297
+ }
298
+ issues.push(`${file}: arquivo nao encontrado no disco`);
299
+ continue;
300
+ }
301
+
265
302
  const validation = validateFileContent(file);
266
303
  if (!validation.valid) {
267
304
  issues.push(`${file}: ${validation.reason}`);
@@ -277,7 +314,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
277
314
  issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
278
315
  }
279
316
  } catch {
280
- // statSync falhou — arquivo pode nao existir (ja reportado por validateFileContent)
317
+ // statSync falhou — arquivo pode nao existir (ja reportado acima)
281
318
  }
282
319
  }
283
320
  }
@@ -300,7 +337,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
300
337
 
301
338
  // Obter agente da task
302
339
  const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
303
- const agentDomain = task?.agent?.split("-")[0] || "all";
340
+ const agentDomain = domainToScope(getAgentDomain(task?.agent));
304
341
 
305
342
  // Validar arquivos contra standards
306
343
  const result = validateAgainstStandards(context.files, agentDomain);
@@ -331,13 +368,22 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
331
368
 
332
369
  for (const file of context.files) {
333
370
  if (!existsSync(file)) continue;
371
+
372
+ const fileScope = inferScopeFromPath(file);
373
+ // Arquivos de teste nao conflitam com production code
374
+ if (fileScope === "testing") continue;
375
+
334
376
  const utilities = extractUtilitiesFromFile(file);
335
377
 
336
378
  for (const util of utilities) {
337
379
  const existing = findDuplicateUtilities(util.name, file);
338
- if (existing.length > 0) {
380
+ // Filtrar: duplicatas em scope "testing" nao contam
381
+ const realDuplicates = existing.filter(
382
+ (e: any) => inferScopeFromPath(e.file_path) !== "testing"
383
+ );
384
+ if (realDuplicates.length > 0) {
339
385
  duplicates.push(
340
- `"${util.name}" (${util.type}) em ${file} -- ja existe em: ${existing.map((e: any) => e.file_path).join(", ")}`
386
+ `"${util.name}" (${util.type}) em ${file} -- ja existe em: ${realDuplicates.map((e: any) => e.file_path).join(", ")}`
341
387
  );
342
388
  }
343
389
  }
@@ -380,7 +426,12 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
380
426
 
381
427
  if (!context.files || context.files.length === 0) return { passed: true };
382
428
 
383
- const typecheckResult = runTypecheck(context.files);
429
+ // Buscar stack e custom command do projeto
430
+ const projectRow = db.query("SELECT stack, typecheck_command FROM project WHERE id = 'default'").get() as any;
431
+ const typecheckResult = runTypecheck(context.files, {
432
+ customCommand: projectRow?.typecheck_command || null,
433
+ stackJson: projectRow?.stack || null,
434
+ });
384
435
 
385
436
  printTypecheckResult(typecheckResult);
386
437
 
@@ -426,6 +477,40 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
426
477
  };
427
478
  }
428
479
 
480
+ case "decision-conflicts": {
481
+ if (context.force) {
482
+ logGateBypass(context.taskId, "decision-conflicts", context.forceReason);
483
+ return { passed: true };
484
+ }
485
+
486
+ if (!context.subagentData?.decisions_made?.length) return { passed: true };
487
+
488
+ const dcTask = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
489
+ if (!dcTask) return { passed: true };
490
+
491
+ const existingDecisions = db
492
+ .query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active'")
493
+ .all(dcTask.spec_id) as any[];
494
+ if (existingDecisions.length === 0) return { passed: true };
495
+
496
+ const allConflicts: string[] = [];
497
+ for (const dec of context.subagentData.decisions_made) {
498
+ const analysis = detectConflicts(dec.title, dec.decision, existingDecisions);
499
+ if (analysis.hasConflict) {
500
+ for (const conflict of analysis.conflictingDecisions) {
501
+ allConflicts.push(
502
+ `"${dec.title}" conflita com ${conflict.id} ("${conflict.title}"): ${conflict.reason}`
503
+ );
504
+ }
505
+ }
506
+ }
507
+
508
+ if (allConflicts.length > 0) {
509
+ return { passed: false, details: allConflicts.join("\n") };
510
+ }
511
+ return { passed: true };
512
+ }
513
+
429
514
  case "reasoning-provided": {
430
515
  if (!context.subagentData) return { passed: true };
431
516
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.7",
3
+ "version": "9.0.8",
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": {