@codexa/cli 9.0.1 → 9.0.3

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.
@@ -5,6 +5,7 @@ import { validateAgainstStandards, printValidationResult } from "./standards-val
5
5
  import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
6
6
  import { extractUtilitiesFromFile } from "../commands/patterns";
7
7
  import { findDuplicateUtilities } from "../db/schema";
8
+ import { GateError } from "../errors";
8
9
 
9
10
  export interface GateResult {
10
11
  passed: boolean;
@@ -63,8 +64,8 @@ const GATES: Record<string, GateCheck[]> = {
63
64
  },
64
65
  {
65
66
  check: "checkpoint-filled",
66
- message: "Checkpoint obrigatorio",
67
- resolution: "Forneca --checkpoint 'resumo do que foi feito'",
67
+ message: "Checkpoint obrigatorio (min 30 chars, 5 palavras)",
68
+ resolution: "Forneca --checkpoint 'resumo detalhado do que foi feito' (min 30 caracteres e 5 palavras)",
68
69
  },
69
70
  {
70
71
  check: "files-exist",
@@ -164,9 +165,15 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
164
165
  }
165
166
 
166
167
  case "checkpoint-filled": {
167
- return {
168
- passed: !!context.checkpoint && context.checkpoint.length >= 10,
169
- };
168
+ const cp = context.checkpoint?.trim() || "";
169
+ if (cp.length < 30) {
170
+ return { passed: false, details: "Checkpoint deve ter pelo menos 30 caracteres" };
171
+ }
172
+ const wordCount = cp.split(/\s+/).filter((w: string) => w.length > 1).length;
173
+ if (wordCount < 5) {
174
+ return { passed: false, details: "Checkpoint deve ter pelo menos 5 palavras" };
175
+ }
176
+ return { passed: true };
170
177
  }
171
178
 
172
179
  case "all-tasks-done": {
@@ -192,12 +199,35 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
192
199
  if (!context.files || context.files.length === 0) return { passed: true };
193
200
 
194
201
  // v8.0: Validar não apenas existência, mas conteúdo mínimo
202
+ // v9.2: Validar que arquivo foi modificado DURANTE a task
195
203
  const issues: string[] = [];
196
204
 
205
+ // Buscar started_at da task para comparacao temporal
206
+ let taskStartTime: number | null = null;
207
+ if (context.taskId) {
208
+ const taskRow = db.query("SELECT started_at FROM tasks WHERE id = ?").get(context.taskId) as any;
209
+ if (taskRow?.started_at) {
210
+ taskStartTime = new Date(taskRow.started_at).getTime();
211
+ }
212
+ }
213
+
197
214
  for (const file of context.files) {
198
215
  const validation = validateFileContent(file);
199
216
  if (!validation.valid) {
200
217
  issues.push(`${file}: ${validation.reason}`);
218
+ continue;
219
+ }
220
+ // Verificar que arquivo foi tocado durante a task
221
+ if (taskStartTime) {
222
+ try {
223
+ const stat = statSync(file);
224
+ const mtime = stat.mtimeMs;
225
+ if (mtime < taskStartTime) {
226
+ issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime anterior ao start)`);
227
+ }
228
+ } catch {
229
+ // statSync falhou — arquivo pode nao existir (ja reportado por validateFileContent)
230
+ }
201
231
  }
202
232
  }
203
233
 
@@ -409,14 +439,16 @@ export function enforceGate(command: string, context: any = {}): void {
409
439
  const result = validateGate(command, context);
410
440
 
411
441
  if (!result.passed) {
412
- console.error(`\nBLOQUEADO: ${result.reason}`);
413
- console.error(`Resolva: ${result.resolution}\n`);
414
- process.exit(1);
442
+ throw new GateError(
443
+ result.reason || "Gate falhou",
444
+ result.resolution || "Verifique o estado atual",
445
+ command
446
+ );
415
447
  }
416
448
  }
417
449
 
418
450
  // v8.0: Validar conteúdo de arquivos criados (não apenas existência)
419
- interface FileValidationResult {
451
+ export interface FileValidationResult {
420
452
  valid: boolean;
421
453
  reason?: string;
422
454
  }
@@ -456,7 +488,7 @@ function validateFileContent(filePath: string): FileValidationResult {
456
488
  }
457
489
  }
458
490
 
459
- function validateByExtension(ext: string, content: string): FileValidationResult {
491
+ export function validateByExtension(ext: string, content: string): FileValidationResult {
460
492
  const trimmed = content.trim();
461
493
 
462
494
  switch (ext) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.1",
3
+ "version": "9.0.3",
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": {
@@ -16,6 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "cli": "bun run workflow.ts",
19
+ "test": "bun test",
20
+ "test:watch": "bun test --watch",
19
21
  "install": "npm publish --access public"
20
22
  },
21
23
  "engines": {
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { getDb } from "../db/connection";
13
13
  import { SubagentReturn, Knowledge } from "./subagent-protocol";
14
- import { addReasoning, addGraphRelation, getRelatedFiles, upsertUtility } from "../db/schema";
14
+ import { addReasoning, addGraphRelation, upsertUtility, getNextDecisionId } from "../db/schema";
15
15
  import { extractUtilitiesFromFile, inferScopeFromPath } from "../commands/patterns";
16
16
 
17
17
  interface ProcessResult {
@@ -26,18 +26,6 @@ interface ProcessResult {
26
26
  errors: string[];
27
27
  }
28
28
 
29
- function getNextDecisionId(specId: string): string {
30
- const db = getDb();
31
- const last = db
32
- .query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
33
- .get(specId) as any;
34
-
35
- if (!last) return "DEC-001";
36
-
37
- const num = parseInt(last.id.replace("DEC-", "")) + 1;
38
- return `DEC-${num.toString().padStart(3, "0")}`;
39
- }
40
-
41
29
  /**
42
30
  * Processa o retorno de um subagent e registra automaticamente
43
31
  * todos os dados extraidos no banco
@@ -85,87 +73,32 @@ export function processSubagentReturn(
85
73
  }
86
74
  }
87
75
 
88
- // 2. Registrar Decisions
76
+ // 2. Registrar Decisions (com retry para collision de IDs concorrentes)
77
+ const savedDecisionIds: string[] = [];
89
78
  if (data.decisions_made && data.decisions_made.length > 0) {
90
79
  for (const dec of data.decisions_made) {
91
- try {
92
- const decisionId = getNextDecisionId(specId);
93
- db.run(
94
- `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
95
- VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
96
- [decisionId, specId, taskNumber, dec.title, dec.decision, dec.rationale || null, now]
97
- );
98
- result.decisionsAdded++;
99
- } catch (e) {
100
- result.errors.push(`Erro ao registrar decision: ${(e as Error).message}`);
101
- }
102
- }
103
- }
104
-
105
- // 2b. v8.3: Deteccao automatica de contradicoes entre decisoes
106
- if (data.decisions_made && data.decisions_made.length > 0) {
107
- try {
108
- const existingDecisions = db.query(
109
- "SELECT id, title, decision FROM decisions WHERE spec_id = ? AND status = 'active'"
110
- ).all(specId) as any[];
111
-
112
- const allTaskFiles = [...data.files_created, ...data.files_modified];
113
-
114
- for (const newDec of data.decisions_made) {
115
- for (const existingDec of existingDecisions) {
116
- // Skip se mesma decisao (recem adicionada)
117
- if (existingDec.title === newDec.title) continue;
118
-
119
- const newDecLower = `${newDec.title} ${newDec.decision}`.toLowerCase();
120
- const existDecLower = `${existingDec.title} ${existingDec.decision}`.toLowerCase();
121
-
122
- // Verificar overlap de arquivos
123
- const existingDecFiles = getRelatedFiles(existingDec.id, "decision");
124
- const fileOverlap = allTaskFiles.some(f => existingDecFiles.includes(f));
125
-
126
- if (fileOverlap) {
127
- // Heuristica de keywords opostos
128
- const opposites = [
129
- ['usar', 'nao usar'], ['usar', 'evitar'],
130
- ['use', 'avoid'], ['add', 'remove'],
131
- ['incluir', 'excluir'], ['habilitar', 'desabilitar'],
132
- ['enable', 'disable'], ['create', 'delete'],
133
- ['client', 'server'], ['sync', 'async'],
134
- ];
135
-
136
- let isContradiction = false;
137
- for (const [word1, word2] of opposites) {
138
- if ((newDecLower.includes(word1) && existDecLower.includes(word2)) ||
139
- (newDecLower.includes(word2) && existDecLower.includes(word1))) {
140
- isContradiction = true;
141
- break;
142
- }
143
- }
144
-
145
- if (isContradiction) {
146
- const newDecId = db.query(
147
- "SELECT id FROM decisions WHERE spec_id = ? AND title = ? ORDER BY created_at DESC LIMIT 1"
148
- ).get(specId, newDec.title) as any;
149
-
150
- if (newDecId) {
151
- addGraphRelation(specId, {
152
- sourceType: "decision",
153
- sourceId: newDecId.id,
154
- targetType: "decision",
155
- targetId: existingDec.id,
156
- relation: "contradicts",
157
- strength: 0.7,
158
- metadata: { reason: "Arquivos sobrepostos com semantica oposta" },
159
- });
160
- result.relationsAdded++;
161
- }
162
- }
80
+ let retries = 3;
81
+ let inserted = false;
82
+ while (retries > 0 && !inserted) {
83
+ try {
84
+ const decisionId = getNextDecisionId(specId);
85
+ db.run(
86
+ `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
87
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
88
+ [decisionId, specId, taskNumber, dec.title, dec.decision, dec.rationale || null, now]
89
+ );
90
+ savedDecisionIds.push(decisionId);
91
+ result.decisionsAdded++;
92
+ inserted = true;
93
+ } catch (e: any) {
94
+ if (e.message?.includes("UNIQUE constraint") && retries > 1) {
95
+ retries--;
96
+ continue;
163
97
  }
98
+ result.errors.push(`Erro ao registrar decision: ${(e as Error).message}`);
99
+ break;
164
100
  }
165
101
  }
166
- } catch (e) {
167
- // Nao-critico: nao falhar processamento por deteccao de contradicoes
168
- result.errors.push(`Aviso contradicoes: ${(e as Error).message}`);
169
102
  }
170
103
  }
171
104
 
@@ -331,10 +264,9 @@ export function processSubagentReturn(
331
264
  result.relationsAdded++;
332
265
  }
333
266
 
334
- // Relação: decision -> arquivos (se houver decisions)
335
- if (data.decisions_made) {
336
- for (const dec of data.decisions_made) {
337
- const decisionId = getNextDecisionId(specId);
267
+ // Relação: decision -> arquivos (usa IDs salvos na seção 2, não gera novos)
268
+ if (savedDecisionIds.length > 0) {
269
+ for (const decisionId of savedDecisionIds) {
338
270
  for (const file of [...data.files_created, ...data.files_modified]) {
339
271
  addGraphRelation(specId, {
340
272
  sourceType: "decision",