@codexa/cli 9.0.2 → 9.0.4

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,11 +5,14 @@ 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, RecoverySuggestion } from "../errors";
9
+ import { resolveSpecOrNull } from "../commands/spec-resolver";
8
10
 
9
11
  export interface GateResult {
10
12
  passed: boolean;
11
13
  reason?: string;
12
14
  resolution?: string;
15
+ recovery?: RecoverySuggestion;
13
16
  }
14
17
 
15
18
  interface GateCheck {
@@ -63,8 +66,8 @@ const GATES: Record<string, GateCheck[]> = {
63
66
  },
64
67
  {
65
68
  check: "checkpoint-filled",
66
- message: "Checkpoint obrigatorio",
67
- resolution: "Forneca --checkpoint 'resumo do que foi feito'",
69
+ message: "Checkpoint obrigatorio (min 10 chars)",
70
+ resolution: "Forneca --checkpoint 'resumo do que foi feito' (min 10 caracteres)",
68
71
  },
69
72
  {
70
73
  check: "files-exist",
@@ -108,9 +111,59 @@ const GATES: Record<string, GateCheck[]> = {
108
111
  ],
109
112
  };
110
113
 
111
- function getActiveSpec(): any {
112
- const db = getDb();
113
- return db.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1").get();
114
+ // v9.3: Estrategias de recuperacao por gate — diagnostico + passos concretos
115
+ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggestion> = {
116
+ "standards-follow": (details) => ({
117
+ diagnostic: `Violacoes de standards detectadas:\n${details || "Detalhes nao disponiveis"}`,
118
+ steps: [
119
+ "Revise as violacoes listadas acima",
120
+ "Corrija o codigo para seguir os standards obrigatorios",
121
+ "Ou use --force --force-reason 'motivo' para bypass (auditado no review)",
122
+ ],
123
+ command: "codexa context detail standards",
124
+ }),
125
+ "dry-check": (details) => ({
126
+ diagnostic: `Utilities duplicadas encontradas:\n${details || "Detalhes nao disponiveis"}`,
127
+ steps: [
128
+ "Verifique as utilities existentes",
129
+ "Importe do arquivo existente em vez de recriar",
130
+ "Se intencional, use --force --force-reason 'motivo'",
131
+ ],
132
+ }),
133
+ "typecheck-pass": (details) => ({
134
+ diagnostic: `Erros TypeScript encontrados:\n${details || "Detalhes nao disponiveis"}`,
135
+ steps: [
136
+ "Corrija os erros de tipo listados",
137
+ "Verifique imports e definicoes de tipo",
138
+ "Se erros em deps externas, use --force --force-reason 'motivo'",
139
+ ],
140
+ }),
141
+ "files-exist": (details) => ({
142
+ diagnostic: `Arquivos esperados nao encontrados:\n${details || "Detalhes nao disponiveis"}`,
143
+ steps: [
144
+ "Verifique se o subagent usou Write/Edit para criar os arquivos",
145
+ "Confirme que os caminhos em --files correspondem aos arquivos reais",
146
+ "Verifique conteudo valido (nao vazio, estrutura correta)",
147
+ ],
148
+ }),
149
+ "checkpoint-filled": (details) => ({
150
+ diagnostic: `Checkpoint invalido: ${details || "muito curto ou incompleto"}`,
151
+ steps: [
152
+ "Forneca --checkpoint com resumo do que foi feito (min 10 chars)",
153
+ "Descreva O QUE foi feito, nao apenas 'feito' ou 'ok'",
154
+ ],
155
+ }),
156
+ "reasoning-provided": () => ({
157
+ diagnostic: "Subagent retornou sem reasoning.approach adequado",
158
+ steps: [
159
+ "Inclua 'reasoning.approach' no retorno JSON do subagent (min 20 chars)",
160
+ "Descreva COMO o problema foi abordado",
161
+ ],
162
+ }),
163
+ };
164
+
165
+ function getActiveSpec(specId?: string): any {
166
+ return resolveSpecOrNull(specId);
114
167
  }
115
168
 
116
169
  function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
@@ -118,24 +171,24 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
118
171
 
119
172
  switch (check) {
120
173
  case "plan-exists": {
121
- const spec = getActiveSpec();
174
+ const spec = getActiveSpec(context.specId);
122
175
  return { passed: spec !== null };
123
176
  }
124
177
 
125
178
  case "has-tasks": {
126
- const spec = getActiveSpec();
179
+ const spec = getActiveSpec(context.specId);
127
180
  if (!spec) return { passed: false };
128
181
  const count = db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(spec.id) as any;
129
182
  return { passed: count.c > 0 };
130
183
  }
131
184
 
132
185
  case "phase-is-checking": {
133
- const spec = getActiveSpec();
186
+ const spec = getActiveSpec(context.specId);
134
187
  return { passed: spec?.phase === "checking" };
135
188
  }
136
189
 
137
190
  case "spec-approved": {
138
- const spec = getActiveSpec();
191
+ const spec = getActiveSpec(context.specId);
139
192
  return { passed: spec?.approved_at !== null };
140
193
  }
141
194
 
@@ -164,13 +217,15 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
164
217
  }
165
218
 
166
219
  case "checkpoint-filled": {
167
- return {
168
- passed: !!context.checkpoint && context.checkpoint.length >= 10,
169
- };
220
+ const cp = context.checkpoint?.trim() || "";
221
+ if (cp.length < 10) {
222
+ return { passed: false, details: "Checkpoint deve ter pelo menos 10 caracteres" };
223
+ }
224
+ return { passed: true };
170
225
  }
171
226
 
172
227
  case "all-tasks-done": {
173
- const spec = getActiveSpec();
228
+ const spec = getActiveSpec(context.specId);
174
229
  if (!spec) return { passed: false };
175
230
  const pending = db.query(
176
231
  "SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
@@ -182,7 +237,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
182
237
  }
183
238
 
184
239
  case "review-exists": {
185
- const spec = getActiveSpec();
240
+ const spec = getActiveSpec(context.specId);
186
241
  if (!spec) return { passed: false };
187
242
  const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id);
188
243
  return { passed: review !== null };
@@ -192,12 +247,38 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
192
247
  if (!context.files || context.files.length === 0) return { passed: true };
193
248
 
194
249
  // v8.0: Validar não apenas existência, mas conteúdo mínimo
250
+ // v9.2: Validar que arquivo foi modificado DURANTE a task
251
+ // v9.3: Tolerancia de 5s para clock skew em sandbox
252
+ const MTIME_TOLERANCE_MS = 5000;
195
253
  const issues: string[] = [];
196
254
 
255
+ // Buscar started_at da task para comparacao temporal
256
+ let taskStartTime: number | null = null;
257
+ if (context.taskId) {
258
+ const taskRow = db.query("SELECT started_at FROM tasks WHERE id = ?").get(context.taskId) as any;
259
+ if (taskRow?.started_at) {
260
+ taskStartTime = new Date(taskRow.started_at).getTime();
261
+ }
262
+ }
263
+
197
264
  for (const file of context.files) {
198
265
  const validation = validateFileContent(file);
199
266
  if (!validation.valid) {
200
267
  issues.push(`${file}: ${validation.reason}`);
268
+ continue;
269
+ }
270
+ // Verificar que arquivo foi tocado durante a task (com tolerancia)
271
+ if (taskStartTime) {
272
+ try {
273
+ const stat = statSync(file);
274
+ const mtime = stat.mtimeMs;
275
+ if (mtime < (taskStartTime - MTIME_TOLERANCE_MS)) {
276
+ const diffSec = Math.round((taskStartTime - mtime) / 1000);
277
+ issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
278
+ }
279
+ } catch {
280
+ // statSync falhou — arquivo pode nao existir (ja reportado por validateFileContent)
281
+ }
201
282
  }
202
283
  }
203
284
 
@@ -312,7 +393,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
312
393
  }
313
394
 
314
395
  case "no-critical-blockers": {
315
- const spec = getActiveSpec();
396
+ const spec = getActiveSpec(context.specId);
316
397
  if (!spec) return { passed: true };
317
398
 
318
399
  const allKnowledge = db
@@ -326,13 +407,10 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
326
407
  .all(spec.id) as any[];
327
408
 
328
409
  const unresolved = allKnowledge.filter((k: any) => {
329
- if (!k.acknowledged_by) return true;
330
- try {
331
- const acked = JSON.parse(k.acknowledged_by) as number[];
332
- return acked.length === 0;
333
- } catch {
334
- return true;
335
- }
410
+ const hasAck = db.query(
411
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ?"
412
+ ).get(k.id);
413
+ return !hasAck;
336
414
  });
337
415
 
338
416
  if (unresolved.length === 0) return { passed: true };
@@ -392,12 +470,16 @@ export function validateGate(command: string, context: any = {}): GateResult {
392
470
  const result = executeCheck(gate.check, context);
393
471
 
394
472
  if (!result.passed) {
473
+ const recoveryFn = RECOVERY_STRATEGIES[gate.check];
474
+ const recovery = recoveryFn ? recoveryFn(result.details) : undefined;
475
+
395
476
  return {
396
477
  passed: false,
397
478
  reason: result.details
398
479
  ? `${gate.message}: ${result.details}`
399
480
  : gate.message,
400
481
  resolution: gate.resolution,
482
+ recovery,
401
483
  };
402
484
  }
403
485
  }
@@ -409,14 +491,17 @@ export function enforceGate(command: string, context: any = {}): void {
409
491
  const result = validateGate(command, context);
410
492
 
411
493
  if (!result.passed) {
412
- console.error(`\nBLOQUEADO: ${result.reason}`);
413
- console.error(`Resolva: ${result.resolution}\n`);
414
- process.exit(1);
494
+ throw new GateError(
495
+ result.reason || "Gate falhou",
496
+ result.resolution || "Verifique o estado atual",
497
+ command,
498
+ result.recovery
499
+ );
415
500
  }
416
501
  }
417
502
 
418
503
  // v8.0: Validar conteúdo de arquivos criados (não apenas existência)
419
- interface FileValidationResult {
504
+ export interface FileValidationResult {
420
505
  valid: boolean;
421
506
  reason?: string;
422
507
  }
@@ -456,7 +541,7 @@ function validateFileContent(filePath: string): FileValidationResult {
456
541
  }
457
542
  }
458
543
 
459
- function validateByExtension(ext: string, content: string): FileValidationResult {
544
+ export function validateByExtension(ext: string, content: string): FileValidationResult {
460
545
  const trimmed = content.trim();
461
546
 
462
547
  switch (ext) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.2",
3
+ "version": "9.0.4",
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",