@codexa/cli 9.0.3 → 9.0.5
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.ts +7 -6
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -0
- package/commands/decide.ts +9 -21
- package/commands/discover.ts +11 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +190 -75
- package/commands/patterns.ts +6 -13
- package/commands/plan.ts +14 -64
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +2 -3
- package/commands/task.ts +30 -9
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -708
- package/db/schema.test.ts +475 -48
- package/db/schema.ts +136 -12
- package/gates/validator.test.ts +58 -0
- package/gates/validator.ts +83 -30
- package/package.json +1 -1
- package/workflow.ts +113 -61
package/commands/review.ts
CHANGED
|
@@ -1,17 +1,135 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
|
+
import { resolveSpec } from "./spec-resolver";
|
|
5
|
+
import { CodexaError, GateError } from "../errors";
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
// P1-2: Review Score — Threshold Automatico
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
export interface ReviewScore {
|
|
12
|
+
total: number; // 0-100
|
|
13
|
+
breakdown: {
|
|
14
|
+
tasksCompleted: number; // % tasks done vs planned (25 pts)
|
|
15
|
+
gatesPassedClean: number; // % gates sem bypass (25 pts)
|
|
16
|
+
filesDelivered: number; // % arquivos planejados vs criados (25 pts)
|
|
17
|
+
standardsFollowed: number; // % standards sem violacao (25 pts)
|
|
18
|
+
};
|
|
19
|
+
autoApproveEligible: boolean; // score >= 80 AND zero critical bypasses
|
|
20
|
+
mustReviewItems: string[]; // Items que exigem atencao humana
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function calculateReviewScore(specId: string): ReviewScore {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
|
|
26
|
+
const mustReviewItems: string[] = [];
|
|
27
|
+
|
|
28
|
+
// 1. Tasks: (completed / total) * 25
|
|
29
|
+
const totalTasks = (db.query(
|
|
30
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?"
|
|
31
|
+
).get(specId) as any).c;
|
|
32
|
+
const completedTasks = (db.query(
|
|
33
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'"
|
|
34
|
+
).get(specId) as any).c;
|
|
35
|
+
const tasksCompleted = totalTasks > 0
|
|
36
|
+
? Math.round((completedTasks / totalTasks) * 25)
|
|
37
|
+
: 25;
|
|
38
|
+
|
|
39
|
+
if (completedTasks < totalTasks) {
|
|
40
|
+
mustReviewItems.push(`${totalTasks - completedTasks} task(s) nao concluida(s)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Gates: (clean passes / total gate events) * 25
|
|
44
|
+
// Total gate events = tasks * 7 gates per task
|
|
45
|
+
const totalGateEvents = totalTasks * 7;
|
|
46
|
+
const bypassCount = (db.query(
|
|
47
|
+
"SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ?"
|
|
48
|
+
).get(specId) as any).c;
|
|
49
|
+
const cleanGateEvents = Math.max(0, totalGateEvents - bypassCount);
|
|
50
|
+
const gatesPassedClean = totalGateEvents > 0
|
|
51
|
+
? Math.round((cleanGateEvents / totalGateEvents) * 25)
|
|
52
|
+
: 25;
|
|
53
|
+
|
|
54
|
+
// Check for critical bypasses
|
|
55
|
+
const criticalBypasses = db.query(
|
|
56
|
+
"SELECT * FROM gate_bypasses WHERE spec_id = ? AND gate_name IN ('standards-follow', 'dry-check', 'typecheck-pass')"
|
|
57
|
+
).all(specId) as any[];
|
|
58
|
+
if (criticalBypasses.length > 0) {
|
|
59
|
+
mustReviewItems.push(`${criticalBypasses.length} bypass(es) de gates criticos`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Files: (delivered / planned) * 25
|
|
63
|
+
const plannedFiles = db.query(
|
|
64
|
+
"SELECT files FROM tasks WHERE spec_id = ? AND files IS NOT NULL"
|
|
65
|
+
).all(specId) as any[];
|
|
66
|
+
const allPlannedFiles = new Set<string>();
|
|
67
|
+
for (const t of plannedFiles) {
|
|
68
|
+
try {
|
|
69
|
+
const files = JSON.parse(t.files) as string[];
|
|
70
|
+
for (const f of files) allPlannedFiles.add(f);
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const deliveredFiles = new Set(
|
|
75
|
+
(db.query(
|
|
76
|
+
"SELECT DISTINCT path FROM artifacts WHERE spec_id = ?"
|
|
77
|
+
).all(specId) as any[]).map(a => a.path)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let filesDelivered: number;
|
|
81
|
+
if (allPlannedFiles.size === 0) {
|
|
82
|
+
filesDelivered = deliveredFiles.size > 0 ? 25 : 0;
|
|
83
|
+
} else {
|
|
84
|
+
let matchCount = 0;
|
|
85
|
+
for (const f of allPlannedFiles) {
|
|
86
|
+
if (deliveredFiles.has(f)) matchCount++;
|
|
87
|
+
}
|
|
88
|
+
filesDelivered = Math.round((matchCount / allPlannedFiles.size) * 25);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const missingFiles = [...allPlannedFiles].filter(f => !deliveredFiles.has(f));
|
|
92
|
+
if (missingFiles.length > 0) {
|
|
93
|
+
mustReviewItems.push(`${missingFiles.length} arquivo(s) planejado(s) nao entregue(s)`);
|
|
94
|
+
}
|
|
4
95
|
|
|
5
|
-
|
|
96
|
+
// 4. Standards: (followed / total) * 25
|
|
97
|
+
// Inverse of standards-follow bypasses relative to total tasks
|
|
98
|
+
const standardsBypasses = (db.query(
|
|
99
|
+
"SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ? AND gate_name = 'standards-follow'"
|
|
100
|
+
).get(specId) as any).c;
|
|
101
|
+
const standardsFollowed = totalTasks > 0
|
|
102
|
+
? Math.round(((totalTasks - standardsBypasses) / totalTasks) * 25)
|
|
103
|
+
: 25;
|
|
104
|
+
|
|
105
|
+
if (standardsBypasses > 0) {
|
|
106
|
+
mustReviewItems.push(`${standardsBypasses} bypass(es) de standards`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const total = tasksCompleted + gatesPassedClean + filesDelivered + standardsFollowed;
|
|
110
|
+
const autoApproveEligible = total >= 80 && criticalBypasses.length === 0;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
total,
|
|
114
|
+
breakdown: {
|
|
115
|
+
tasksCompleted,
|
|
116
|
+
gatesPassedClean,
|
|
117
|
+
filesDelivered,
|
|
118
|
+
standardsFollowed,
|
|
119
|
+
},
|
|
120
|
+
autoApproveEligible,
|
|
121
|
+
mustReviewItems,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function reviewStart(json: boolean = false, specId?: string): void {
|
|
6
126
|
initSchema();
|
|
7
127
|
enforceGate("review-start");
|
|
8
128
|
|
|
9
129
|
const db = getDb();
|
|
10
130
|
const now = new Date().toISOString();
|
|
11
131
|
|
|
12
|
-
const spec =
|
|
13
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
14
|
-
.get() as any;
|
|
132
|
+
const spec = resolveSpec(specId, ["implementing"]);
|
|
15
133
|
|
|
16
134
|
const tasks = db
|
|
17
135
|
.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
|
|
@@ -87,8 +205,11 @@ export function reviewStart(json: boolean = false): void {
|
|
|
87
205
|
// Atualizar fase
|
|
88
206
|
db.run("UPDATE specs SET phase = 'reviewing', updated_at = ? WHERE id = ?", [now, spec.id]);
|
|
89
207
|
|
|
208
|
+
// P1-2: Calcular score automatico
|
|
209
|
+
const score = calculateReviewScore(spec.id);
|
|
210
|
+
|
|
90
211
|
if (json) {
|
|
91
|
-
console.log(JSON.stringify({ spec, reviewData, deviations }));
|
|
212
|
+
console.log(JSON.stringify({ spec, reviewData, deviations, score }));
|
|
92
213
|
return;
|
|
93
214
|
}
|
|
94
215
|
|
|
@@ -96,6 +217,25 @@ export function reviewStart(json: boolean = false): void {
|
|
|
96
217
|
console.log(`REVIEW: ${spec.name}`);
|
|
97
218
|
console.log(`${"=".repeat(60)}`);
|
|
98
219
|
|
|
220
|
+
// P1-2: Mostrar score
|
|
221
|
+
const scoreIcon = score.total >= 80 ? "[OK]" : score.total >= 50 ? "[!]" : "[X]";
|
|
222
|
+
console.log(`\n${scoreIcon} REVIEW SCORE: ${score.total}/100`);
|
|
223
|
+
console.log(` Tasks concluidas: ${score.breakdown.tasksCompleted}/25`);
|
|
224
|
+
console.log(` Gates limpos: ${score.breakdown.gatesPassedClean}/25`);
|
|
225
|
+
console.log(` Arquivos entregues: ${score.breakdown.filesDelivered}/25`);
|
|
226
|
+
console.log(` Standards seguidos: ${score.breakdown.standardsFollowed}/25`);
|
|
227
|
+
|
|
228
|
+
if (score.autoApproveEligible) {
|
|
229
|
+
console.log(`\n Elegivel para aprovacao automatica.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (score.mustReviewItems.length > 0) {
|
|
233
|
+
console.log(`\n Items que exigem atencao:`);
|
|
234
|
+
for (const item of score.mustReviewItems) {
|
|
235
|
+
console.log(` - ${item}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
99
239
|
console.log(`\nResumo da implementacao:`);
|
|
100
240
|
console.log(` Tasks concluidas: ${tasks.length}`);
|
|
101
241
|
console.log(` Artefatos criados: ${artifacts.length}`);
|
|
@@ -123,24 +263,48 @@ export function reviewStart(json: boolean = false): void {
|
|
|
123
263
|
console.log(`\nNenhum desvio encontrado.`);
|
|
124
264
|
}
|
|
125
265
|
|
|
126
|
-
|
|
266
|
+
if (score.total < 50) {
|
|
267
|
+
console.log(`\n[!] Score abaixo de 50. Aprovacao requer: review approve --force --force-reason "motivo"`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(`\nPara aprovar: review approve`);
|
|
270
|
+
}
|
|
127
271
|
console.log(`Para ver status: status\n`);
|
|
128
272
|
}
|
|
129
273
|
|
|
130
|
-
export function reviewApprove(): void {
|
|
274
|
+
export function reviewApprove(options?: { specId?: string; force?: boolean; forceReason?: string } | string): void {
|
|
131
275
|
initSchema();
|
|
132
276
|
enforceGate("review-approve");
|
|
133
277
|
|
|
134
278
|
const db = getDb();
|
|
135
279
|
const now = new Date().toISOString();
|
|
136
280
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
// Backward compat: accept string (old API) or options object
|
|
282
|
+
const opts = typeof options === "string"
|
|
283
|
+
? { specId: options }
|
|
284
|
+
: (options || {});
|
|
285
|
+
|
|
286
|
+
const spec = resolveSpec(opts.specId, ["reviewing"]);
|
|
287
|
+
|
|
288
|
+
// P1-2: Enforce minimum score
|
|
289
|
+
const score = calculateReviewScore(spec.id);
|
|
290
|
+
if (score.total < 50 && !opts.force) {
|
|
291
|
+
throw new GateError(
|
|
292
|
+
`Review score muito baixo: ${score.total}/100.\n` +
|
|
293
|
+
` Items criticos:\n` +
|
|
294
|
+
score.mustReviewItems.map(i => ` - ${i}`).join("\n"),
|
|
295
|
+
`Use: review approve --force --force-reason "motivo" para aprovar mesmo assim`,
|
|
296
|
+
"review-approve"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
140
299
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
300
|
+
if (score.total < 50 && opts.force) {
|
|
301
|
+
// Log the forced approval
|
|
302
|
+
db.run(
|
|
303
|
+
`INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason, created_at)
|
|
304
|
+
VALUES (?, 0, 'review-low-score', ?, ?)`,
|
|
305
|
+
[spec.id, opts.forceReason || `Score ${score.total}/100 - forced approval`, now]
|
|
306
|
+
);
|
|
307
|
+
console.log(`\n[!] Review aprovado com score baixo (${score.total}/100). Bypass registrado.`);
|
|
144
308
|
}
|
|
145
309
|
|
|
146
310
|
// Atualizar review
|
|
@@ -175,27 +339,19 @@ export function reviewApprove(): void {
|
|
|
175
339
|
]);
|
|
176
340
|
|
|
177
341
|
// Mostrar relatorio final completo
|
|
178
|
-
showReviewReport(spec, tasks, artifacts, decisions, knowledge, review);
|
|
342
|
+
showReviewReport(spec, tasks, artifacts, decisions, knowledge, review, score);
|
|
179
343
|
}
|
|
180
344
|
|
|
181
345
|
/**
|
|
182
346
|
* Pula o review e finaliza a feature diretamente
|
|
183
347
|
*/
|
|
184
|
-
export function reviewSkip(): void {
|
|
348
|
+
export function reviewSkip(specId?: string): void {
|
|
185
349
|
initSchema();
|
|
186
350
|
|
|
187
351
|
const db = getDb();
|
|
188
352
|
const now = new Date().toISOString();
|
|
189
353
|
|
|
190
|
-
|
|
191
|
-
const spec = db
|
|
192
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
193
|
-
.get() as any;
|
|
194
|
-
|
|
195
|
-
if (!spec) {
|
|
196
|
-
console.error("\nNenhuma feature em fase de implementacao.\n");
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
354
|
+
const spec = resolveSpec(specId, ["implementing"]);
|
|
199
355
|
|
|
200
356
|
// Verificar se todas tasks estao done
|
|
201
357
|
const pending = db
|
|
@@ -203,9 +359,7 @@ export function reviewSkip(): void {
|
|
|
203
359
|
.get(spec.id) as any;
|
|
204
360
|
|
|
205
361
|
if (pending.c > 0) {
|
|
206
|
-
|
|
207
|
-
console.error("Complete todas as tasks antes de pular o review.\n");
|
|
208
|
-
process.exit(1);
|
|
362
|
+
throw new CodexaError(`Ainda existem ${pending.c} tasks pendentes.\nComplete todas as tasks antes de pular o review.`);
|
|
209
363
|
}
|
|
210
364
|
|
|
211
365
|
// Buscar dados
|
|
@@ -265,7 +419,8 @@ function showReviewReport(
|
|
|
265
419
|
artifacts: any[],
|
|
266
420
|
decisions: any[],
|
|
267
421
|
knowledge: any[],
|
|
268
|
-
review: any
|
|
422
|
+
review: any,
|
|
423
|
+
score?: ReviewScore
|
|
269
424
|
): void {
|
|
270
425
|
const reviewData = review.planned_vs_done ? JSON.parse(review.planned_vs_done) : {};
|
|
271
426
|
const deviations = review.deviations ? JSON.parse(review.deviations) : [];
|
|
@@ -278,6 +433,13 @@ function showReviewReport(
|
|
|
278
433
|
console.log(` Status: ✅ COMPLETA E APROVADA`);
|
|
279
434
|
console.log(` ID: ${spec.id}`);
|
|
280
435
|
|
|
436
|
+
// P1-2: Score no relatorio final
|
|
437
|
+
if (score) {
|
|
438
|
+
const scoreIcon = score.total >= 80 ? "✅" : score.total >= 50 ? "⚠️" : "❌";
|
|
439
|
+
console.log(` Score: ${scoreIcon} ${score.total}/100`);
|
|
440
|
+
console.log(` Tasks: ${score.breakdown.tasksCompleted}/25 | Gates: ${score.breakdown.gatesPassedClean}/25 | Files: ${score.breakdown.filesDelivered}/25 | Standards: ${score.breakdown.standardsFollowed}/25`);
|
|
441
|
+
}
|
|
442
|
+
|
|
281
443
|
// Resumo de execucao
|
|
282
444
|
console.log(`\n${"─".repeat(60)}`);
|
|
283
445
|
console.log(`📊 RESUMO DA EXECUCAO`);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { resolveSpec, getAllActiveSpecs } from "./spec-resolver";
|
|
3
|
+
import { getDb } from "../db/connection";
|
|
4
|
+
import { initSchema } from "../db/schema";
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
initSchema();
|
|
8
|
+
const db = getDb();
|
|
9
|
+
// Limpar em ordem que respeita foreign keys
|
|
10
|
+
db.run("DELETE FROM reasoning_log");
|
|
11
|
+
db.run("DELETE FROM knowledge_graph");
|
|
12
|
+
db.run("DELETE FROM knowledge");
|
|
13
|
+
db.run("DELETE FROM gate_bypasses");
|
|
14
|
+
db.run("DELETE FROM snapshots");
|
|
15
|
+
db.run("DELETE FROM review");
|
|
16
|
+
db.run("DELETE FROM artifacts");
|
|
17
|
+
db.run("DELETE FROM decisions");
|
|
18
|
+
db.run("DELETE FROM tasks");
|
|
19
|
+
db.run("DELETE FROM context");
|
|
20
|
+
db.run("DELETE FROM specs");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function createSpec(id: string, name: string, phase: string, createdAt?: string) {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
const now = createdAt || new Date().toISOString();
|
|
26
|
+
db.run(
|
|
27
|
+
"INSERT INTO specs (id, name, phase, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
28
|
+
[id, name, phase, now, now]
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("resolveSpec", () => {
|
|
33
|
+
it("returns spec by ID when specId provided", () => {
|
|
34
|
+
createSpec("spec-a", "Feature A", "planning");
|
|
35
|
+
createSpec("spec-b", "Feature B", "implementing");
|
|
36
|
+
|
|
37
|
+
const result = resolveSpec("spec-a");
|
|
38
|
+
expect(result.id).toBe("spec-a");
|
|
39
|
+
expect(result.name).toBe("Feature A");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns most recent active spec when no specId", () => {
|
|
43
|
+
createSpec("spec-old", "Old Feature", "planning", "2026-01-01T00:00:00Z");
|
|
44
|
+
createSpec("spec-new", "New Feature", "implementing", "2026-02-01T00:00:00Z");
|
|
45
|
+
|
|
46
|
+
const result = resolveSpec();
|
|
47
|
+
expect(result.id).toBe("spec-new");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("validates requiredPhases", () => {
|
|
51
|
+
createSpec("spec-a", "Feature A", "planning");
|
|
52
|
+
|
|
53
|
+
// Should work with matching phase
|
|
54
|
+
const result = resolveSpec("spec-a", ["planning"]);
|
|
55
|
+
expect(result.phase).toBe("planning");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("accepts multiple requiredPhases", () => {
|
|
59
|
+
createSpec("spec-a", "Feature A", "checking");
|
|
60
|
+
const result = resolveSpec("spec-a", ["planning", "checking"]);
|
|
61
|
+
expect(result.phase).toBe("checking");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("skips completed specs when no specId", () => {
|
|
65
|
+
createSpec("spec-done", "Done Feature", "completed", "2026-02-01T00:00:00Z");
|
|
66
|
+
createSpec("spec-active", "Active Feature", "planning", "2026-01-01T00:00:00Z");
|
|
67
|
+
|
|
68
|
+
const result = resolveSpec();
|
|
69
|
+
expect(result.id).toBe("spec-active");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("skips cancelled specs when no specId", () => {
|
|
73
|
+
createSpec("spec-cancelled", "Cancelled Feature", "cancelled", "2026-02-01T00:00:00Z");
|
|
74
|
+
createSpec("spec-active", "Active Feature", "implementing", "2026-01-01T00:00:00Z");
|
|
75
|
+
|
|
76
|
+
const result = resolveSpec();
|
|
77
|
+
expect(result.id).toBe("spec-active");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("getAllActiveSpecs", () => {
|
|
82
|
+
it("returns empty array when no active specs", () => {
|
|
83
|
+
const result = getAllActiveSpecs();
|
|
84
|
+
expect(result).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns all active specs ordered by created_at DESC", () => {
|
|
88
|
+
createSpec("spec-old", "Old Feature", "planning", "2026-01-01T00:00:00Z");
|
|
89
|
+
createSpec("spec-new", "New Feature", "implementing", "2026-02-01T00:00:00Z");
|
|
90
|
+
|
|
91
|
+
const result = getAllActiveSpecs();
|
|
92
|
+
expect(result.length).toBe(2);
|
|
93
|
+
expect(result[0].id).toBe("spec-new");
|
|
94
|
+
expect(result[1].id).toBe("spec-old");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("excludes completed and cancelled specs", () => {
|
|
98
|
+
createSpec("spec-active", "Active", "planning");
|
|
99
|
+
createSpec("spec-done", "Done", "completed");
|
|
100
|
+
createSpec("spec-cancelled", "Cancelled", "cancelled");
|
|
101
|
+
|
|
102
|
+
const result = getAllActiveSpecs();
|
|
103
|
+
expect(result.length).toBe(1);
|
|
104
|
+
expect(result[0].id).toBe("spec-active");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns multiple specs in different phases", () => {
|
|
108
|
+
createSpec("spec-plan", "Planning Feature", "planning", "2026-01-01T00:00:00Z");
|
|
109
|
+
createSpec("spec-impl", "Implementing Feature", "implementing", "2026-01-02T00:00:00Z");
|
|
110
|
+
createSpec("spec-rev", "Reviewing Feature", "reviewing", "2026-01-03T00:00:00Z");
|
|
111
|
+
|
|
112
|
+
const result = getAllActiveSpecs();
|
|
113
|
+
expect(result.length).toBe(3);
|
|
114
|
+
// Ordered by created_at DESC
|
|
115
|
+
expect(result[0].id).toBe("spec-rev");
|
|
116
|
+
expect(result[1].id).toBe("spec-impl");
|
|
117
|
+
expect(result[2].id).toBe("spec-plan");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getDb } from "../db/connection";
|
|
2
|
+
import { initSchema } from "../db/schema";
|
|
3
|
+
import { CodexaError } from "../errors";
|
|
4
|
+
|
|
5
|
+
export interface ResolvedSpec {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
phase: string;
|
|
9
|
+
approved_at: string | null;
|
|
10
|
+
analysis_id: string | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve qual spec usar para um comando.
|
|
17
|
+
* - Se specId fornecido: busca esse spec (erro se nao encontrado ou finalizado)
|
|
18
|
+
* - Se nao fornecido: busca o mais recente ativo (ORDER BY created_at DESC LIMIT 1)
|
|
19
|
+
* - Se requiredPhases: valida que spec.phase esta na lista
|
|
20
|
+
*/
|
|
21
|
+
export function resolveSpec(specId?: string, requiredPhases?: string[]): ResolvedSpec {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
|
|
24
|
+
let spec: any;
|
|
25
|
+
|
|
26
|
+
if (specId) {
|
|
27
|
+
spec = db
|
|
28
|
+
.query("SELECT * FROM specs WHERE id = ?")
|
|
29
|
+
.get(specId);
|
|
30
|
+
|
|
31
|
+
if (!spec) {
|
|
32
|
+
throw new CodexaError(`Spec '${specId}' nao encontrado.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (spec.phase === "completed" || spec.phase === "cancelled") {
|
|
36
|
+
throw new CodexaError(`Spec '${specId}' ja esta ${spec.phase}.`);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
spec = db
|
|
40
|
+
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
41
|
+
.get();
|
|
42
|
+
|
|
43
|
+
if (!spec) {
|
|
44
|
+
throw new CodexaError("Nenhuma feature ativa.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requiredPhases && !requiredPhases.includes(spec.phase)) {
|
|
49
|
+
throw new CodexaError(
|
|
50
|
+
`Spec '${spec.name}' esta na fase '${spec.phase}', esperado: ${requiredPhases.join(" ou ")}.\nSpec ID: ${spec.id}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return spec as ResolvedSpec;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Versao silenciosa de resolveSpec que retorna null em vez de process.exit(1).
|
|
59
|
+
* Util para comandos que tem tratamento especial (ex: json output, throw customizado).
|
|
60
|
+
*/
|
|
61
|
+
export function resolveSpecOrNull(specId?: string, requiredPhases?: string[]): ResolvedSpec | null {
|
|
62
|
+
const db = getDb();
|
|
63
|
+
|
|
64
|
+
let spec: any;
|
|
65
|
+
|
|
66
|
+
if (specId) {
|
|
67
|
+
spec = db.query("SELECT * FROM specs WHERE id = ?").get(specId);
|
|
68
|
+
if (!spec) return null;
|
|
69
|
+
if (spec.phase === "completed" || spec.phase === "cancelled") return null;
|
|
70
|
+
} else {
|
|
71
|
+
spec = db
|
|
72
|
+
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
73
|
+
.get();
|
|
74
|
+
if (!spec) return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (requiredPhases && !requiredPhases.includes(spec.phase)) return null;
|
|
78
|
+
|
|
79
|
+
return spec as ResolvedSpec;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retorna todos os specs ativos (nao completed/cancelled).
|
|
84
|
+
*/
|
|
85
|
+
export function getAllActiveSpecs(): ResolvedSpec[] {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
return db
|
|
88
|
+
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC")
|
|
89
|
+
.all() as ResolvedSpec[];
|
|
90
|
+
}
|
package/commands/standards.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema } from "../db/schema";
|
|
3
3
|
import { generateStandardsMarkdown } from "./discover";
|
|
4
|
+
import { CodexaError, ValidationError } from "../errors";
|
|
4
5
|
|
|
5
6
|
export function standardsList(options: { category?: string; scope?: string; json?: boolean }): void {
|
|
6
7
|
initSchema();
|
|
@@ -69,22 +70,16 @@ export function standardsAdd(options: {
|
|
|
69
70
|
const validEnforcement = ["required", "recommended"];
|
|
70
71
|
|
|
71
72
|
if (!validCategories.includes(options.category)) {
|
|
72
|
-
|
|
73
|
-
console.error(`Validas: ${validCategories.join(", ")}\n`);
|
|
74
|
-
process.exit(2);
|
|
73
|
+
throw new ValidationError(`Categoria invalida: ${options.category}\nValidas: ${validCategories.join(", ")}`);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
if (!validScopes.includes(options.scope)) {
|
|
78
|
-
|
|
79
|
-
console.error(`Validos: ${validScopes.join(", ")}\n`);
|
|
80
|
-
process.exit(2);
|
|
77
|
+
throw new ValidationError(`Escopo invalido: ${options.scope}\nValidos: ${validScopes.join(", ")}`);
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
const enforcement = options.enforcement || "required";
|
|
84
81
|
if (!validEnforcement.includes(enforcement)) {
|
|
85
|
-
|
|
86
|
-
console.error(`Validos: ${validEnforcement.join(", ")}\n`);
|
|
87
|
-
process.exit(2);
|
|
82
|
+
throw new ValidationError(`Enforcement invalido: ${enforcement}\nValidos: ${validEnforcement.join(", ")}`);
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
let examples = null;
|
|
@@ -131,8 +126,7 @@ export function standardsEdit(
|
|
|
131
126
|
const existing = db.query("SELECT * FROM standards WHERE id = ?").get(standardId) as any;
|
|
132
127
|
|
|
133
128
|
if (!existing) {
|
|
134
|
-
|
|
135
|
-
process.exit(1);
|
|
129
|
+
throw new CodexaError(`Standard #${id} nao encontrado.`);
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
const updates: string[] = [];
|
|
@@ -159,8 +153,7 @@ export function standardsEdit(
|
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
if (updates.length === 0) {
|
|
162
|
-
|
|
163
|
-
process.exit(2);
|
|
156
|
+
throw new ValidationError("Nenhuma alteracao fornecida.");
|
|
164
157
|
}
|
|
165
158
|
|
|
166
159
|
params.push(standardId);
|
|
@@ -181,8 +174,7 @@ export function standardsRemove(id: string): void {
|
|
|
181
174
|
const existing = db.query("SELECT * FROM standards WHERE id = ?").get(standardId) as any;
|
|
182
175
|
|
|
183
176
|
if (!existing) {
|
|
184
|
-
|
|
185
|
-
process.exit(1);
|
|
177
|
+
throw new CodexaError(`Standard #${id} nao encontrado.`);
|
|
186
178
|
}
|
|
187
179
|
|
|
188
180
|
db.run("DELETE FROM standards WHERE id = ?", [standardId]);
|
package/commands/sync.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { CodexaError } from "../errors";
|
|
4
5
|
|
|
5
6
|
// Agents que sao referencias (nao spawnables como subagent_type)
|
|
6
7
|
const REFERENCE_FILES = new Set([
|
|
@@ -47,9 +48,7 @@ export function syncAgents(options: { force?: boolean } = {}): void {
|
|
|
47
48
|
const pluginDir = findPluginAgentsDir();
|
|
48
49
|
|
|
49
50
|
if (!pluginDir) {
|
|
50
|
-
|
|
51
|
-
console.error(" Verifique se voce esta no repositorio codexa ou se @codexa/cli esta instalado globalmente.\n");
|
|
52
|
-
process.exit(1);
|
|
51
|
+
throw new CodexaError("Diretorio de agents do plugin nao encontrado.\nVerifique se voce esta no repositorio codexa ou se @codexa/cli esta instalado globalmente.");
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
const targetDir = join(process.cwd(), ".claude", "agents");
|
package/commands/task.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
|
-
import { initSchema, getPatternsForFiles, getPatternsByScope, getRecentReasoning, claimTask } from "../db/schema";
|
|
2
|
+
import { initSchema, getPatternsForFiles, getPatternsByScope, getRecentReasoning, claimTask, recordAgentPerformance } from "../db/schema";
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
4
|
import { parseSubagentReturn, formatValidationErrors } from "../protocol/subagent-protocol";
|
|
5
5
|
import { processSubagentReturn, formatProcessResult } from "../protocol/process-return";
|
|
@@ -7,14 +7,13 @@ import { getContextForSubagent, getMinimalContextForSubagent } from "./utils";
|
|
|
7
7
|
import { getUnreadKnowledgeForTask } from "./knowledge";
|
|
8
8
|
import { loadTemplate } from "../templates/loader";
|
|
9
9
|
import { TaskStateError, ValidationError, KnowledgeBlockError } from "../errors";
|
|
10
|
+
import { resolveSpec, resolveSpecOrNull } from "./spec-resolver";
|
|
10
11
|
|
|
11
|
-
export function taskNext(json: boolean = false): void {
|
|
12
|
+
export function taskNext(json: boolean = false, specId?: string): void {
|
|
12
13
|
initSchema();
|
|
13
14
|
|
|
14
15
|
const db = getDb();
|
|
15
|
-
const spec =
|
|
16
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
17
|
-
.get() as any;
|
|
16
|
+
const spec = resolveSpecOrNull(specId, ["implementing"]);
|
|
18
17
|
|
|
19
18
|
if (!spec) {
|
|
20
19
|
if (json) {
|
|
@@ -101,16 +100,14 @@ export function taskNext(json: boolean = false): void {
|
|
|
101
100
|
console.log(`Use: task start <id> ou task start <id1>,<id2>,...\n`);
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
export function taskStart(ids: string, json: boolean = false, fullContext: boolean = false): void {
|
|
103
|
+
export function taskStart(ids: string, json: boolean = false, fullContext: boolean = false, specId?: string): void {
|
|
105
104
|
initSchema();
|
|
106
105
|
enforceGate("task-start");
|
|
107
106
|
|
|
108
107
|
const db = getDb();
|
|
109
108
|
const now = new Date().toISOString();
|
|
110
109
|
|
|
111
|
-
const spec =
|
|
112
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
113
|
-
.get() as any;
|
|
110
|
+
const spec = resolveSpec(specId, ["implementing"]);
|
|
114
111
|
|
|
115
112
|
const taskIds = ids.split(",").map((s) => parseInt(s.trim()));
|
|
116
113
|
const startedTasks: any[] = [];
|
|
@@ -411,6 +408,30 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
|
|
|
411
408
|
[checkpoint, now, taskId]
|
|
412
409
|
);
|
|
413
410
|
|
|
411
|
+
// v9.3: Registrar performance do agente
|
|
412
|
+
try {
|
|
413
|
+
const agentType = task.agent || "general-purpose";
|
|
414
|
+
const startedAt = task.started_at ? new Date(task.started_at).getTime() : Date.now();
|
|
415
|
+
const duration = Date.now() - startedAt;
|
|
416
|
+
const bypassCount = (db.query(
|
|
417
|
+
"SELECT COUNT(*) as c FROM gate_bypasses WHERE task_id = ?"
|
|
418
|
+
).get(taskId) as any)?.c || 0;
|
|
419
|
+
const totalGates = 7;
|
|
420
|
+
|
|
421
|
+
recordAgentPerformance({
|
|
422
|
+
agentType,
|
|
423
|
+
specId: spec.id,
|
|
424
|
+
taskId,
|
|
425
|
+
gatesPassedFirstTry: Math.max(0, totalGates - bypassCount),
|
|
426
|
+
gatesTotal: totalGates,
|
|
427
|
+
bypassesUsed: bypassCount,
|
|
428
|
+
filesCreated: subagentData?.files_created?.length || 0,
|
|
429
|
+
filesModified: subagentData?.files_modified?.length || 0,
|
|
430
|
+
contextSizeBytes: 0,
|
|
431
|
+
executionDurationMs: duration,
|
|
432
|
+
});
|
|
433
|
+
} catch { /* nao-critico: nao falhar task done por tracking de performance */ }
|
|
434
|
+
|
|
414
435
|
// Registrar artefatos se NAO veio do subagent (ja foi processado acima)
|
|
415
436
|
if (!subagentData && options.files) {
|
|
416
437
|
const files = options.files.split(",").map((s) => s.trim());
|