@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.
@@ -1,6 +1,7 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema } from "../db/schema";
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
4
+ import { CodexaError } from "../errors";
4
5
  import { join } from "path";
5
6
 
6
7
  // ═══════════════════════════════════════════════════════════════
@@ -364,7 +365,7 @@ export function architectStart(description: string, options: { json?: boolean }
364
365
  console.log(` Criada: ${pending.created_at}`);
365
366
  console.log("\nUse 'architect show' para ver detalhes ou 'architect cancel' para cancelar.\n");
366
367
  }
367
- process.exit(1);
368
+ throw new CodexaError("Ja existe uma analise pendente. Use 'architect show' ou 'architect cancel'.");
368
369
  }
369
370
 
370
371
  // Criar nova analise
@@ -566,7 +567,7 @@ export function architectSave(options: { file?: string; json?: boolean }): void
566
567
  } else {
567
568
  console.log("\n[ERRO] Nenhuma analise pendente encontrada.\n");
568
569
  }
569
- process.exit(1);
570
+ throw new CodexaError("Nenhuma analise pendente encontrada.");
570
571
  }
571
572
 
572
573
  // v8.4: Resolver caminho do arquivo .md
@@ -587,7 +588,7 @@ export function architectSave(options: { file?: string; json?: boolean }): void
587
588
  console.log(`\n[ERRO] Arquivo nao encontrado: ${filePath}`);
588
589
  console.log("O agente architect deve escrever o .md antes de chamar 'architect save'.\n");
589
590
  }
590
- process.exit(1);
591
+ throw new CodexaError(`Arquivo nao encontrado: ${filePath}`);
591
592
  }
592
593
 
593
594
  const content = readFileSync(filePath, "utf-8");
@@ -697,7 +698,7 @@ export function architectApprove(options: { id?: string; json?: boolean }): void
697
698
  } else {
698
699
  console.log("\n[ERRO] Analise nao encontrada.\n");
699
700
  }
700
- process.exit(1);
701
+ throw new CodexaError("Analise nao encontrada.");
701
702
  }
702
703
 
703
704
  if (analysis.status !== "pending") {
@@ -706,7 +707,7 @@ export function architectApprove(options: { id?: string; json?: boolean }): void
706
707
  } else {
707
708
  console.log(`\n[ERRO] Analise ja esta com status '${analysis.status}'.\n`);
708
709
  }
709
- process.exit(1);
710
+ throw new CodexaError(`Analise ja esta com status '${analysis.status}'.`);
710
711
  }
711
712
 
712
713
  db.run(
@@ -750,7 +751,7 @@ export function architectExport(options: { id?: string; json?: boolean }): void
750
751
  console.log("\n[ERRO] Nenhuma analise aprovada encontrada.");
751
752
  console.log("Use 'architect approve' primeiro.\n");
752
753
  }
753
- process.exit(1);
754
+ throw new CodexaError("Nenhuma analise aprovada encontrada. Use 'architect approve' primeiro.");
754
755
  }
755
756
 
756
757
  const babySteps: BabyStep[] = analysis.baby_steps ? JSON.parse(analysis.baby_steps) : [];
package/commands/check.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema } from "../db/schema";
3
3
  import { enforceGate } from "../gates/validator";
4
+ import { resolveSpec } from "./spec-resolver";
4
5
 
5
- export function checkRequest(): void {
6
+ export function checkRequest(specId?: string): void {
6
7
  initSchema();
7
8
  enforceGate("check-request");
8
9
 
9
10
  const db = getDb();
10
- const spec = db
11
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
12
- .get() as any;
11
+ const spec = resolveSpec(specId, ["planning"]);
13
12
 
14
13
  const tasks = db
15
14
  .query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
@@ -68,16 +67,14 @@ export function checkRequest(): void {
68
67
  console.log(`Para ajustar: plan task-add (adiciona mais tasks)\n`);
69
68
  }
70
69
 
71
- export function checkApprove(): void {
70
+ export function checkApprove(specId?: string): void {
72
71
  initSchema();
73
72
  enforceGate("check-approve");
74
73
 
75
74
  const db = getDb();
76
75
  const now = new Date().toISOString();
77
76
 
78
- const spec = db
79
- .query("SELECT * FROM specs WHERE phase = 'checking' ORDER BY created_at DESC LIMIT 1")
80
- .get() as any;
77
+ const spec = resolveSpec(specId, ["checking"]);
81
78
 
82
79
  // Atualizar para aprovado
83
80
  db.run(
@@ -100,20 +97,13 @@ export function checkApprove(): void {
100
97
  console.log(`3. Complete uma task com: task done <id> --checkpoint "..."\n`);
101
98
  }
102
99
 
103
- export function checkReject(reason: string): void {
100
+ export function checkReject(reason: string, specId?: string): void {
104
101
  initSchema();
105
102
 
106
103
  const db = getDb();
107
104
  const now = new Date().toISOString();
108
105
 
109
- const spec = db
110
- .query("SELECT * FROM specs WHERE phase = 'checking' ORDER BY created_at DESC LIMIT 1")
111
- .get() as any;
112
-
113
- if (!spec) {
114
- console.error("\nNenhum plano aguardando aprovacao.\n");
115
- process.exit(1);
116
- }
106
+ const spec = resolveSpec(specId, ["checking"]);
117
107
 
118
108
  // Voltar para planning
119
109
  db.run("UPDATE specs SET phase = 'planning', updated_at = ? WHERE id = ?", [now, spec.id]);
package/commands/clear.ts CHANGED
@@ -5,6 +5,7 @@ import { join, dirname } from "path";
5
5
 
6
6
  interface ClearOptions {
7
7
  force?: boolean;
8
+ specId?: string;
8
9
  }
9
10
 
10
11
  /**
@@ -64,6 +65,45 @@ export function clearTasks(options: ClearOptions = {}): void {
64
65
  return;
65
66
  }
66
67
 
68
+ // Per-spec clear or clear all
69
+ if (options.specId) {
70
+ console.log("\n" + "═".repeat(60));
71
+ console.log(`LIMPANDO SPEC: ${options.specId}`);
72
+ console.log("═".repeat(60) + "\n");
73
+
74
+ const tablesWithSpecId = [
75
+ "reasoning_log",
76
+ "knowledge_graph",
77
+ "knowledge",
78
+ "gate_bypasses",
79
+ "snapshots",
80
+ "review",
81
+ "artifacts",
82
+ "decisions",
83
+ "tasks",
84
+ "context",
85
+ "specs",
86
+ ];
87
+
88
+ for (const table of tablesWithSpecId) {
89
+ try {
90
+ const result = db.run(`DELETE FROM ${table} WHERE spec_id = ?`, [options.specId]);
91
+ if (result.changes > 0) {
92
+ console.log(` Limpo: ${table} (${result.changes} registros)`);
93
+ }
94
+ } catch (err: any) {
95
+ if (!err.message.includes("no such table") && !err.message.includes("no such column")) {
96
+ console.error(` Erro ao limpar ${table}: ${err.message}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ console.log("\n" + "═".repeat(60));
102
+ console.log(`SPEC ${options.specId} LIMPO`);
103
+ console.log("═".repeat(60) + "\n");
104
+ return;
105
+ }
106
+
67
107
  console.log("\n" + "═".repeat(60));
68
108
  console.log("LIMPANDO TASKS...");
69
109
  console.log("═".repeat(60) + "\n");
@@ -1,5 +1,7 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema, getNextDecisionId } from "../db/schema";
3
+ import { resolveSpec } from "./spec-resolver";
4
+ import { CodexaError } from "../errors";
3
5
 
4
6
  interface ConflictAnalysis {
5
7
  hasConflict: boolean;
@@ -124,20 +126,13 @@ function detectConflicts(
124
126
  };
125
127
  }
126
128
 
127
- export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean }): void {
129
+ export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean; specId?: string }): void {
128
130
  initSchema();
129
131
 
130
132
  const db = getDb();
131
133
  const now = new Date().toISOString();
132
134
 
133
- const spec = db
134
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
135
- .get() as any;
136
-
137
- if (!spec) {
138
- console.error("\nNenhuma feature ativa.\n");
139
- process.exit(1);
140
- }
135
+ const spec = resolveSpec(options.specId);
141
136
 
142
137
  // Verificar conflitos com decisoes existentes
143
138
  const existingDecisions = db
@@ -159,9 +154,9 @@ export function decide(title: string, decision: string, options: { rationale?: s
159
154
  }
160
155
 
161
156
  console.warn(`${"─".repeat(50)}`);
162
- console.warn(`Para registrar mesmo assim, use: decide "${title}" "${decision}" --force`);
163
- console.warn(`Ou revise as decisoes existentes com: decisions\n`);
164
- process.exit(1);
157
+ throw new CodexaError(
158
+ `Potencial conflito detectado.\nPara registrar mesmo assim, use: decide "${title}" "${decision}" --force\nOu revise as decisoes existentes com: decisions`
159
+ );
165
160
  }
166
161
  }
167
162
 
@@ -207,19 +202,12 @@ export function decide(title: string, decision: string, options: { rationale?: s
207
202
  console.log();
208
203
  }
209
204
 
210
- export function listDecisions(json: boolean = false): void {
205
+ export function listDecisions(json: boolean = false, specId?: string): void {
211
206
  initSchema();
212
207
 
213
208
  const db = getDb();
214
209
 
215
- const spec = db
216
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
217
- .get() as any;
218
-
219
- if (!spec) {
220
- console.error("\nNenhuma feature ativa.\n");
221
- process.exit(1);
222
- }
210
+ const spec = resolveSpec(specId);
223
211
 
224
212
  const decisions = db
225
213
  .query("SELECT * FROM decisions WHERE spec_id = ? ORDER BY created_at")
@@ -11,6 +11,7 @@ import {
11
11
  getDetailedTechnologies,
12
12
  type UnifiedDetectionResult,
13
13
  } from "../detectors/loader";
14
+ import { CodexaError } from "../errors";
14
15
 
15
16
  interface StackDetection {
16
17
  frontend?: string;
@@ -172,9 +173,7 @@ export function discoverConfirm(): void {
172
173
 
173
174
  const pending = db.query("SELECT * FROM project WHERE id = 'pending'").get() as any;
174
175
  if (!pending) {
175
- console.error("\nNenhuma descoberta pendente.");
176
- console.error("Execute: discover start primeiro\n");
177
- process.exit(1);
176
+ throw new CodexaError("Nenhuma descoberta pendente.\nExecute: discover start primeiro");
178
177
  }
179
178
 
180
179
  const data = JSON.parse(pending.stack);
@@ -283,9 +282,7 @@ export function discoverShow(json: boolean = false): void {
283
282
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
284
283
 
285
284
  if (!project) {
286
- console.error("\nProjeto nao descoberto.");
287
- console.error("Execute: discover start\n");
288
- process.exit(1);
285
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start");
289
286
  }
290
287
 
291
288
  const standards = db.query("SELECT * FROM standards ORDER BY category, scope").all() as any[];
@@ -381,9 +378,7 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
381
378
 
382
379
  const currentProject = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
383
380
  if (!currentProject) {
384
- console.error("\nProjeto nao descoberto.");
385
- console.error("Execute: discover start primeiro\n");
386
- process.exit(1);
381
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
387
382
  }
388
383
 
389
384
  const currentStack = JSON.parse(currentProject.stack);
@@ -466,9 +461,7 @@ export async function discoverIncremental(): Promise<void> {
466
461
 
467
462
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
468
463
  if (!project) {
469
- console.error("\nProjeto nao descoberto.");
470
- console.error("Execute: discover start primeiro\n");
471
- process.exit(1);
464
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
472
465
  }
473
466
 
474
467
  // Data de referencia: last_discover_at > discovered_at > fallback 30 dias
@@ -774,9 +767,7 @@ export function discoverPatternsShow(name: string, json: boolean = false): void
774
767
  const pattern = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
775
768
 
776
769
  if (!pattern) {
777
- console.error(`\nPattern '${name}' nao encontrado.`);
778
- console.error("Use: discover patterns para listar todos\n");
779
- process.exit(1);
770
+ throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
780
771
  }
781
772
 
782
773
  const structure = JSON.parse(pattern.structure || "{}");
@@ -876,9 +867,7 @@ export function discoverPatternAdd(options: {
876
867
  // Verificar se pattern ja existe
877
868
  const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(options.name);
878
869
  if (existing) {
879
- console.error(`\nPattern '${options.name}' ja existe.`);
880
- console.error("Use: discover pattern-edit para modificar\n");
881
- process.exit(1);
870
+ throw new CodexaError(`Pattern '${options.name}' ja existe.\nUse: discover pattern-edit para modificar`);
882
871
  }
883
872
 
884
873
  const now = new Date().toISOString();
@@ -925,9 +914,7 @@ export function discoverPatternEdit(name: string, options: {
925
914
 
926
915
  const existing = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
927
916
  if (!existing) {
928
- console.error(`\nPattern '${name}' nao encontrado.`);
929
- console.error("Use: discover patterns para listar todos\n");
930
- process.exit(1);
917
+ throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
931
918
  }
932
919
 
933
920
  const updates: string[] = [];
@@ -967,8 +954,7 @@ export function discoverPatternEdit(name: string, options: {
967
954
  }
968
955
 
969
956
  if (updates.length === 0) {
970
- console.error("\nNenhuma opcao de atualizacao fornecida.\n");
971
- process.exit(1);
957
+ throw new CodexaError("Nenhuma opcao de atualizacao fornecida.");
972
958
  }
973
959
 
974
960
  updates.push("updated_at = ?");
@@ -993,8 +979,7 @@ export function discoverPatternRemove(name: string): void {
993
979
 
994
980
  const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(name);
995
981
  if (!existing) {
996
- console.error(`\nPattern '${name}' nao encontrado.\n`);
997
- process.exit(1);
982
+ throw new CodexaError(`Pattern '${name}' nao encontrado.`);
998
983
  }
999
984
 
1000
985
  db.run("DELETE FROM implementation_patterns WHERE name = ?", [name]);
@@ -1013,9 +998,7 @@ export function discoverRefreshPatterns(): void {
1013
998
  // Verificar se projeto foi descoberto
1014
999
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
1015
1000
  if (!project) {
1016
- console.error("\nProjeto nao descoberto.");
1017
- console.error("Execute: discover start primeiro\n");
1018
- process.exit(1);
1001
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
1019
1002
  }
1020
1003
 
1021
1004
  console.log("\n⚠ Refresh de patterns requer analise manual do codigo.");
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { jaccardSimilarity, compactKnowledge } from "./knowledge";
3
+ import { getDb } from "../db/connection";
4
+ import { initSchema } from "../db/schema";
5
+
6
+ describe("jaccardSimilarity", () => {
7
+ it("returns 1 for identical strings", () => {
8
+ expect(jaccardSimilarity("hello world test", "hello world test")).toBe(1);
9
+ });
10
+
11
+ it("returns 0 for completely different strings", () => {
12
+ expect(jaccardSimilarity("apple banana cherry", "delta echo foxtrot")).toBe(0);
13
+ });
14
+
15
+ it("returns high similarity for near-duplicates", () => {
16
+ const sim = jaccardSimilarity(
17
+ "Implemented user authentication with JWT tokens",
18
+ "Implemented user authentication using JWT tokens"
19
+ );
20
+ expect(sim).toBeGreaterThan(0.7);
21
+ });
22
+
23
+ it("returns 1 for two empty strings", () => {
24
+ expect(jaccardSimilarity("", "")).toBe(1);
25
+ });
26
+
27
+ it("returns 0 when one string is empty", () => {
28
+ expect(jaccardSimilarity("hello world test", "")).toBe(0);
29
+ });
30
+
31
+ it("is case insensitive", () => {
32
+ expect(jaccardSimilarity("Hello World Test", "hello world test")).toBe(1);
33
+ });
34
+
35
+ it("ignores short words (<=2 chars)", () => {
36
+ // "a" and "is" are filtered out, only "cat" and "dog" matter
37
+ const sim = jaccardSimilarity("a cat is here", "a dog is here");
38
+ // "cat" vs "dog" + "here" in common
39
+ expect(sim).toBeGreaterThan(0);
40
+ expect(sim).toBeLessThan(1);
41
+ });
42
+
43
+ it("strips punctuation", () => {
44
+ const sim = jaccardSimilarity("hello, world! test.", "hello world test");
45
+ expect(sim).toBe(1);
46
+ });
47
+ });
48
+
49
+ describe("compactKnowledge", () => {
50
+ beforeEach(() => {
51
+ initSchema();
52
+ const db = getDb();
53
+ db.run("DELETE FROM knowledge_graph");
54
+ db.run("DELETE FROM knowledge");
55
+ db.run("DELETE FROM tasks");
56
+ db.run("DELETE FROM context");
57
+ db.run("DELETE FROM specs");
58
+ });
59
+
60
+ function createSpec(id: string, phase: string) {
61
+ const db = getDb();
62
+ const now = new Date().toISOString();
63
+ db.run(
64
+ "INSERT INTO specs (id, name, phase, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
65
+ [id, `Feature ${id}`, phase, now, now]
66
+ );
67
+ }
68
+
69
+ function addKnowledgeEntry(specId: string, content: string, category: string, severity: string, createdAt?: string) {
70
+ const db = getDb();
71
+ const now = createdAt || new Date().toISOString();
72
+ db.run(
73
+ `INSERT INTO knowledge (spec_id, task_origin, category, content, severity, broadcast_to, created_at)
74
+ VALUES (?, 0, ?, ?, ?, 'all', ?)`,
75
+ [specId, category, content, severity, now]
76
+ );
77
+ }
78
+
79
+ it("merges similar entries in same category and spec", () => {
80
+ createSpec("spec-a", "implementing");
81
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
82
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for login flow", "discovery", "info");
83
+
84
+ // Dry run first
85
+ compactKnowledge({ dryRun: true, json: true });
86
+
87
+ // Actual compact
88
+ compactKnowledge({ json: true });
89
+
90
+ const db = getDb();
91
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
92
+ expect(archived.c).toBe(1);
93
+ });
94
+
95
+ it("keeps higher severity entry when merging", () => {
96
+ createSpec("spec-a", "implementing");
97
+ addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production", "blocker", "critical");
98
+ addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production system", "blocker", "info");
99
+
100
+ compactKnowledge({ json: true });
101
+
102
+ const db = getDb();
103
+ const remaining = db.query("SELECT * FROM knowledge WHERE severity != 'archived'").all() as any[];
104
+ expect(remaining.length).toBe(1);
105
+ expect(remaining[0].severity).toBe("critical");
106
+ });
107
+
108
+ it("archives old info entries (>7 days)", () => {
109
+ createSpec("spec-a", "implementing");
110
+ const oldDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
111
+ addKnowledgeEntry("spec-a", "Some old discovery that is not referenced anywhere in the graph", "discovery", "info", oldDate);
112
+ addKnowledgeEntry("spec-a", "A recent discovery that should not be archived by this compaction", "discovery", "info");
113
+
114
+ compactKnowledge({ json: true });
115
+
116
+ const db = getDb();
117
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
118
+ expect(archived.c).toBe(1);
119
+ });
120
+
121
+ it("archives entries from completed specs", () => {
122
+ createSpec("spec-done", "completed");
123
+ addKnowledgeEntry("spec-done", "Something learned during completed feature implementation", "discovery", "info");
124
+ addKnowledgeEntry("spec-done", "A critical blocker that was found during the implementation phase", "blocker", "critical");
125
+
126
+ compactKnowledge({ json: true });
127
+
128
+ const db = getDb();
129
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
130
+ expect(archived.c).toBe(2);
131
+ });
132
+
133
+ it("dry-run mode does not modify DB", () => {
134
+ createSpec("spec-done", "completed");
135
+ addKnowledgeEntry("spec-done", "Something that would be archived in a real compaction run", "discovery", "info");
136
+
137
+ compactKnowledge({ dryRun: true, json: true });
138
+
139
+ const db = getDb();
140
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
141
+ expect(archived.c).toBe(0);
142
+ });
143
+
144
+ it("respects --spec filter", () => {
145
+ createSpec("spec-a", "implementing");
146
+ createSpec("spec-b", "implementing");
147
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system", "discovery", "info");
148
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system flow", "discovery", "info");
149
+ addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login", "discovery", "info");
150
+ addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
151
+
152
+ compactKnowledge({ specId: "spec-a", json: true });
153
+
154
+ const db = getDb();
155
+ const archivedA = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-a'").get() as any;
156
+ const archivedB = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-b'").get() as any;
157
+ expect(archivedA.c).toBe(1);
158
+ expect(archivedB.c).toBe(0);
159
+ });
160
+ });