@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.
- package/commands/architect.test.ts +531 -0
- package/commands/architect.ts +68 -11
- package/commands/clear.ts +0 -1
- package/commands/decide.ts +28 -28
- package/commands/discover.ts +128 -3
- package/commands/knowledge.ts +2 -27
- package/commands/patterns.test.ts +169 -0
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +4 -2
- package/commands/sync.ts +90 -0
- package/commands/task.ts +43 -159
- package/commands/utils.ts +251 -249
- package/db/schema.test.ts +333 -0
- package/db/schema.ts +160 -130
- package/gates/validator.test.ts +617 -0
- package/gates/validator.ts +42 -10
- package/package.json +3 -1
- package/protocol/process-return.ts +25 -93
- package/protocol/subagent-protocol.test.ts +936 -0
- package/protocol/subagent-protocol.ts +25 -1
- package/workflow.ts +102 -21
package/gates/validator.ts
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 (
|
|
335
|
-
if (
|
|
336
|
-
for (const
|
|
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",
|