@codexa/cli 9.0.3 → 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.
@@ -1,5 +1,7 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
3
+ import { resolveSpec } from "./spec-resolver";
4
+ import { CodexaError, ValidationError } from "../errors";
3
5
 
4
6
  type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
5
7
  type KnowledgeSeverity = "info" | "warning" | "critical";
@@ -9,13 +11,7 @@ interface AddKnowledgeOptions {
9
11
  category: KnowledgeCategory;
10
12
  severity?: KnowledgeSeverity;
11
13
  broadcastTo?: string;
12
- }
13
-
14
- function getActiveSpec(): any {
15
- const db = getDb();
16
- return db
17
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
18
- .get();
14
+ specId?: string;
19
15
  }
20
16
 
21
17
  function getCurrentTask(specId: string): any {
@@ -31,24 +27,16 @@ export function addKnowledge(options: AddKnowledgeOptions): void {
31
27
  const db = getDb();
32
28
  const now = new Date().toISOString();
33
29
 
34
- const spec = getActiveSpec();
35
- if (!spec) {
36
- console.error("\nNenhuma feature ativa.\n");
37
- process.exit(1);
38
- }
30
+ const spec = resolveSpec(options.specId);
39
31
 
40
32
  const currentTask = getCurrentTask(spec.id);
41
33
  if (!currentTask) {
42
- console.error("\nNenhuma task em execucao.");
43
- console.error("Knowledge so pode ser adicionado durante execucao de uma task.\n");
44
- process.exit(1);
34
+ throw new CodexaError("Nenhuma task em execucao.\nKnowledge so pode ser adicionado durante execucao de uma task.");
45
35
  }
46
36
 
47
37
  const validCategories: KnowledgeCategory[] = ["discovery", "decision", "blocker", "pattern", "constraint"];
48
38
  if (!validCategories.includes(options.category)) {
49
- console.error(`\nCategoria invalida: ${options.category}`);
50
- console.error(`Validas: ${validCategories.join(", ")}\n`);
51
- process.exit(1);
39
+ throw new ValidationError(`Categoria invalida: ${options.category}\nValidas: ${validCategories.join(", ")}`);
52
40
  }
53
41
 
54
42
  const severity = options.severity || "info";
@@ -79,20 +67,13 @@ export function listKnowledge(options: {
79
67
  category?: string;
80
68
  severity?: string; // v8.0: Filtro por severidade
81
69
  json?: boolean;
70
+ specId?: string;
82
71
  }): void {
83
72
  initSchema();
84
73
 
85
74
  const db = getDb();
86
75
 
87
- const spec = getActiveSpec();
88
- if (!spec) {
89
- if (options.json) {
90
- console.log(JSON.stringify({ knowledge: [], message: "Nenhuma feature ativa" }));
91
- return;
92
- }
93
- console.error("\nNenhuma feature ativa.\n");
94
- process.exit(1);
95
- }
76
+ const spec = resolveSpec(options.specId);
96
77
 
97
78
  let query = "SELECT * FROM knowledge WHERE spec_id = ?";
98
79
  const params: any[] = [spec.id];
@@ -122,9 +103,10 @@ export function listKnowledge(options: {
122
103
  const currentTask = getCurrentTask(spec.id);
123
104
  if (currentTask) {
124
105
  filtered = knowledge.filter((k) => {
125
- if (!k.acknowledged_by) return true;
126
- const acked = JSON.parse(k.acknowledged_by) as number[];
127
- return !acked.includes(currentTask.id);
106
+ const isAcked = db.query(
107
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
108
+ ).get(k.id, currentTask.id);
109
+ return !isAcked;
128
110
  });
129
111
  }
130
112
  }
@@ -158,42 +140,29 @@ export function listKnowledge(options: {
158
140
  }
159
141
  }
160
142
 
161
- export function acknowledgeKnowledge(knowledgeId: string): void {
143
+ export function acknowledgeKnowledge(knowledgeId: string, specId?: string): void {
162
144
  initSchema();
163
145
 
164
146
  const db = getDb();
165
147
 
166
- const spec = getActiveSpec();
167
- if (!spec) {
168
- console.error("\nNenhuma feature ativa.\n");
169
- process.exit(1);
170
- }
148
+ const spec = resolveSpec(specId);
171
149
 
172
150
  const currentTask = getCurrentTask(spec.id);
173
151
  if (!currentTask) {
174
- console.error("\nNenhuma task em execucao.\n");
175
- process.exit(1);
152
+ throw new CodexaError("Nenhuma task em execucao.");
176
153
  }
177
154
 
178
155
  const kid = parseInt(knowledgeId);
179
156
  const knowledge = db.query("SELECT * FROM knowledge WHERE id = ?").get(kid) as any;
180
157
 
181
158
  if (!knowledge) {
182
- console.error(`\nKnowledge #${kid} nao encontrado.\n`);
183
- process.exit(1);
159
+ throw new CodexaError(`Knowledge #${kid} nao encontrado.`);
184
160
  }
185
161
 
186
- const acknowledged = knowledge.acknowledged_by
187
- ? (JSON.parse(knowledge.acknowledged_by) as number[])
188
- : [];
189
-
190
- if (!acknowledged.includes(currentTask.id)) {
191
- acknowledged.push(currentTask.id);
192
- db.run("UPDATE knowledge SET acknowledged_by = ? WHERE id = ?", [
193
- JSON.stringify(acknowledged),
194
- kid,
195
- ]);
196
- }
162
+ db.run(
163
+ "INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
164
+ [kid, currentTask.id]
165
+ );
197
166
 
198
167
  console.log(`\nKnowledge #${kid} marcado como lido pela Task #${currentTask.number}.\n`);
199
168
  }
@@ -221,11 +190,13 @@ export function getKnowledgeForTask(specId: string, taskId: number): any[] {
221
190
 
222
191
  export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[] {
223
192
  const all = getKnowledgeForTask(specId, taskId);
193
+ const db = getDb();
224
194
 
225
195
  return all.filter((k) => {
226
- if (!k.acknowledged_by) return true;
227
- const acked = JSON.parse(k.acknowledged_by) as number[];
228
- return !acked.includes(taskId);
196
+ const isAcked = db.query(
197
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
198
+ ).get(k.id, taskId);
199
+ return !isAcked;
229
200
  });
230
201
  }
231
202
 
@@ -234,15 +205,12 @@ export function queryGraph(options: {
234
205
  file?: string;
235
206
  decision?: string;
236
207
  json?: boolean;
208
+ specId?: string;
237
209
  }): void {
238
210
  initSchema();
239
211
  const db = getDb();
240
212
 
241
- const spec = getActiveSpec();
242
- if (!spec) {
243
- console.error("\nNenhuma feature ativa.\n");
244
- process.exit(1);
245
- }
213
+ const spec = resolveSpec(options.specId);
246
214
 
247
215
  // Buscar relacoes para um arquivo
248
216
  if (options.file) {
@@ -336,17 +304,172 @@ export function queryGraph(options: {
336
304
  console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
337
305
  }
338
306
 
307
+ // v9.2: Jaccard similarity for knowledge compaction
308
+ export function jaccardSimilarity(a: string, b: string): number {
309
+ const tokenize = (s: string): Set<string> => {
310
+ const words = s
311
+ .toLowerCase()
312
+ .replace(/[^\w\s-]/g, " ")
313
+ .split(/\s+/)
314
+ .filter((w) => w.length > 2);
315
+ return new Set(words);
316
+ };
317
+
318
+ const setA = tokenize(a);
319
+ const setB = tokenize(b);
320
+
321
+ if (setA.size === 0 && setB.size === 0) return 1;
322
+ if (setA.size === 0 || setB.size === 0) return 0;
323
+
324
+ let intersection = 0;
325
+ for (const word of setA) {
326
+ if (setB.has(word)) intersection++;
327
+ }
328
+
329
+ const union = setA.size + setB.size - intersection;
330
+ return union === 0 ? 0 : intersection / union;
331
+ }
332
+
333
+ // v9.2: Knowledge compaction — merge similar, archive old, archive completed
334
+ export function compactKnowledge(options: {
335
+ specId?: string;
336
+ dryRun?: boolean;
337
+ json?: boolean;
338
+ }): void {
339
+ initSchema();
340
+ const db = getDb();
341
+
342
+ let merged = 0;
343
+ let archivedOld = 0;
344
+ let archivedCompleted = 0;
345
+
346
+ // Phase 1: Merge similar entries (same category + spec_id, Jaccard >= 0.8)
347
+ const specFilter = options.specId
348
+ ? "AND spec_id = ?"
349
+ : "";
350
+ const specParams = options.specId ? [options.specId] : [];
351
+
352
+ const entries = db
353
+ .query(
354
+ `SELECT * FROM knowledge WHERE severity != 'archived' ${specFilter} ORDER BY category, spec_id, created_at DESC`
355
+ )
356
+ .all(...specParams) as any[];
357
+
358
+ // Group by category + spec_id
359
+ const groups = new Map<string, any[]>();
360
+ for (const entry of entries) {
361
+ const key = `${entry.category}:${entry.spec_id}`;
362
+ if (!groups.has(key)) groups.set(key, []);
363
+ groups.get(key)!.push(entry);
364
+ }
365
+
366
+ const toArchive = new Set<number>();
367
+
368
+ for (const [, group] of groups) {
369
+ for (let i = 0; i < group.length; i++) {
370
+ if (toArchive.has(group[i].id)) continue;
371
+ for (let j = i + 1; j < group.length; j++) {
372
+ if (toArchive.has(group[j].id)) continue;
373
+
374
+ const sim = jaccardSimilarity(group[i].content, group[j].content);
375
+ if (sim >= 0.8) {
376
+ // Keep higher severity or newer; archive the other
377
+ const severityOrder: Record<string, number> = { critical: 3, warning: 2, info: 1, archived: 0 };
378
+ const keepI = (severityOrder[group[i].severity] || 0) >= (severityOrder[group[j].severity] || 0);
379
+ const archiveId = keepI ? group[j].id : group[i].id;
380
+ toArchive.add(archiveId);
381
+ merged++;
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ // Phase 2: Archive old info entries (>7 days, no graph references)
388
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
389
+ const oldInfoEntries = db
390
+ .query(
391
+ `SELECT k.id FROM knowledge k
392
+ WHERE k.severity = 'info' AND k.created_at < ? ${specFilter}
393
+ AND NOT EXISTS (
394
+ SELECT 1 FROM knowledge_graph kg
395
+ WHERE kg.source_id = CAST(k.id AS TEXT) AND kg.source_type = 'knowledge'
396
+ )
397
+ AND NOT EXISTS (
398
+ SELECT 1 FROM knowledge_graph kg
399
+ WHERE kg.target_id = CAST(k.id AS TEXT) AND kg.target_type = 'knowledge'
400
+ )`
401
+ )
402
+ .all(sevenDaysAgo, ...specParams) as any[];
403
+
404
+ for (const entry of oldInfoEntries) {
405
+ if (!toArchive.has(entry.id)) {
406
+ toArchive.add(entry.id);
407
+ archivedOld++;
408
+ }
409
+ }
410
+
411
+ // Phase 3: Archive all entries from completed/cancelled specs
412
+ const completedEntries = db
413
+ .query(
414
+ `SELECT k.id FROM knowledge k
415
+ JOIN specs s ON k.spec_id = s.id
416
+ WHERE s.phase IN ('completed', 'cancelled')
417
+ AND k.severity != 'archived'
418
+ ${options.specId ? "AND k.spec_id = ?" : ""}`
419
+ )
420
+ .all(...specParams) as any[];
421
+
422
+ for (const entry of completedEntries) {
423
+ if (!toArchive.has(entry.id)) {
424
+ toArchive.add(entry.id);
425
+ archivedCompleted++;
426
+ }
427
+ }
428
+
429
+ // Apply changes
430
+ if (!options.dryRun && toArchive.size > 0) {
431
+ const ids = Array.from(toArchive);
432
+ const placeholders = ids.map(() => "?").join(",");
433
+ db.run(
434
+ `UPDATE knowledge SET severity = 'archived' WHERE id IN (${placeholders})`,
435
+ ids
436
+ );
437
+ }
438
+
439
+ // Output
440
+ const result = {
441
+ merged,
442
+ archivedOld,
443
+ archivedCompleted,
444
+ totalArchived: toArchive.size,
445
+ dryRun: !!options.dryRun,
446
+ };
447
+
448
+ if (options.json) {
449
+ console.log(JSON.stringify(result));
450
+ return;
451
+ }
452
+
453
+ const mode = options.dryRun ? "[DRY RUN] " : "";
454
+ console.log(`\n${mode}Knowledge Compaction:`);
455
+ console.log(`${"─".repeat(50)}`);
456
+ console.log(` Similares mesclados: ${merged}`);
457
+ console.log(` Info antigos (>7d): ${archivedOld}`);
458
+ console.log(` Specs finalizados: ${archivedCompleted}`);
459
+ console.log(` Total arquivado: ${toArchive.size}`);
460
+ if (options.dryRun) {
461
+ console.log(`\nPara aplicar: knowledge compact${options.specId ? ` --spec ${options.specId}` : ""}`);
462
+ }
463
+ console.log();
464
+ }
465
+
339
466
  // v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
340
- export function resolveKnowledge(ids: string, resolution?: string): void {
467
+ export function resolveKnowledge(ids: string, resolution?: string, specId?: string): void {
341
468
  initSchema();
342
469
  const db = getDb();
343
470
  const now = new Date().toISOString();
344
471
 
345
- const spec = getActiveSpec();
346
- if (!spec) {
347
- console.error("\nNenhuma feature ativa.\n");
348
- process.exit(1);
349
- }
472
+ const spec = resolveSpec(specId);
350
473
 
351
474
  const knowledgeIds = ids.split(",").map(s => parseInt(s.trim()));
352
475
 
@@ -358,18 +481,10 @@ export function resolveKnowledge(ids: string, resolution?: string): void {
358
481
  continue;
359
482
  }
360
483
 
361
- const acknowledged = knowledge.acknowledged_by
362
- ? (JSON.parse(knowledge.acknowledged_by) as number[])
363
- : [];
364
-
365
484
  // Usar -1 como marker de "resolvido pelo orquestrador"
366
- if (!acknowledged.includes(-1)) {
367
- acknowledged.push(-1);
368
- }
369
-
370
485
  db.run(
371
- "UPDATE knowledge SET acknowledged_by = ? WHERE id = ?",
372
- [JSON.stringify(acknowledged), kid]
486
+ "INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
487
+ [kid, -1]
373
488
  );
374
489
 
375
490
  console.log(`Knowledge #${kid} resolvido.`);
@@ -8,6 +8,7 @@
8
8
  import { execSync, spawnSync } from "child_process";
9
9
  import { readFileSync, existsSync } from "fs";
10
10
  import { extname, basename, dirname } from "path";
11
+ import { CodexaError, ValidationError } from "../errors";
11
12
  import { getDb } from "../db/connection";
12
13
  import { initSchema } from "../db/schema";
13
14
 
@@ -601,10 +602,7 @@ export function patternsExtract(options: ExtractOptions): void {
601
602
 
602
603
  // Verificar grepai
603
604
  if (!isGrepaiAvailable()) {
604
- console.error("\n[ERRO] grepai nao encontrado.");
605
- console.error("Instale com: go install github.com/your-org/grepai@latest");
606
- console.error("Ou configure no PATH.\n");
607
- process.exit(1);
605
+ throw new CodexaError("grepai nao encontrado.\nInstale com: go install github.com/your-org/grepai@latest\nOu configure no PATH.");
608
606
  }
609
607
 
610
608
  const db = getDb();
@@ -617,9 +615,7 @@ export function patternsExtract(options: ExtractOptions): void {
617
615
  }
618
616
 
619
617
  if (queries.length === 0) {
620
- console.error(`\n[ERRO] Escopo '${options.scope}' nao reconhecido.`);
621
- console.error("Escopos validos: frontend, backend, database, testing\n");
622
- process.exit(1);
618
+ throw new ValidationError("Escopo '" + options.scope + "' nao reconhecido.\nEscopos validos: frontend, backend, database, testing");
623
619
  }
624
620
 
625
621
  console.log(`\n🔍 Extraindo patterns via grepai...`);
@@ -737,15 +733,13 @@ export function patternsExtract(options: ExtractOptions): void {
737
733
 
738
734
  export function patternsAnalyze(filePath: string, json: boolean = false): void {
739
735
  if (!existsSync(filePath)) {
740
- console.error(`\n[ERRO] Arquivo nao encontrado: ${filePath}\n`);
741
- process.exit(1);
736
+ throw new CodexaError("Arquivo nao encontrado: " + filePath);
742
737
  }
743
738
 
744
739
  const analysis = analyzeFile(filePath);
745
740
 
746
741
  if (!analysis) {
747
- console.error(`\n[ERRO] Nao foi possivel analisar o arquivo.\n`);
748
- process.exit(1);
742
+ throw new CodexaError("Nao foi possivel analisar o arquivo.");
749
743
  }
750
744
 
751
745
  if (json) {
@@ -855,8 +849,7 @@ export function patternsAnalyzeDeep(files: string[], json: boolean = false): voi
855
849
  }
856
850
 
857
851
  if (analyses.length === 0) {
858
- console.error("\nNenhum arquivo analisado.\n");
859
- process.exit(1);
852
+ throw new CodexaError("Nenhum arquivo analisado.");
860
853
  }
861
854
 
862
855
  const summary = {
package/commands/plan.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { getDb } from "../db/connection";
2
2
  import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
3
+ import { resolveSpec } from "./spec-resolver";
4
+ import { CodexaError, ValidationError } from "../errors";
3
5
 
4
6
  export function generateSpecId(name: string): string {
5
7
  const date = new Date().toISOString().split("T")[0];
@@ -17,28 +19,6 @@ export function planStart(description: string, options: { fromAnalysis?: string;
17
19
  initSchema();
18
20
  const db = getDb();
19
21
 
20
- // Verificar se ja existe spec ativo (ignorar completed e cancelled)
21
- const existing = db
22
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
23
- .get() as any;
24
-
25
- if (existing) {
26
- if (options.json) {
27
- console.log(JSON.stringify({
28
- error: "FEATURE_ACTIVE",
29
- name: existing.name,
30
- id: existing.id,
31
- phase: existing.phase,
32
- }));
33
- } else {
34
- console.error(`\nJa existe uma feature ativa: ${existing.name} (${existing.id})`);
35
- console.error(`Fase atual: ${existing.phase}`);
36
- console.error(`\nPara continuar, use: status`);
37
- console.error(`Para cancelar a atual, use: plan cancel\n`);
38
- }
39
- process.exit(1);
40
- }
41
-
42
22
  // v8.4: Buscar analise arquitetural se --from-analysis fornecido
43
23
  let analysis: any = null;
44
24
  if (options.fromAnalysis) {
@@ -52,7 +32,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
52
32
  } else {
53
33
  console.error(`\n[ERRO] Analise arquitetural '${options.fromAnalysis}' nao encontrada.\n`);
54
34
  }
55
- process.exit(1);
35
+ throw new CodexaError(`Analise arquitetural '${options.fromAnalysis}' nao encontrada.`);
56
36
  }
57
37
 
58
38
  if (analysis.status !== "approved") {
@@ -62,7 +42,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
62
42
  console.error(`\n[ERRO] Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}).`);
63
43
  console.error("Use 'architect approve' primeiro.\n");
64
44
  }
65
- process.exit(1);
45
+ throw new CodexaError(`Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}). Use 'architect approve' primeiro.`);
66
46
  }
67
47
  } else {
68
48
  // v8.4: Auto-deteccao por nome
@@ -161,19 +141,11 @@ export function planStart(description: string, options: { fromAnalysis?: string;
161
141
  }
162
142
  }
163
143
 
164
- export function planShow(json: boolean = false): void {
144
+ export function planShow(json: boolean = false, specId?: string): void {
165
145
  initSchema();
166
146
  const db = getDb();
167
147
 
168
- const spec = db
169
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
170
- .get() as any;
171
-
172
- if (!spec) {
173
- console.error("\nNenhuma feature ativa.");
174
- console.error("Inicie com: /codexa:feature'\n");
175
- process.exit(1);
176
- }
148
+ const spec = resolveSpec(specId);
177
149
 
178
150
  const tasks = db
179
151
  .query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
@@ -227,25 +199,12 @@ export function planTaskAdd(options: {
227
199
  depends?: string;
228
200
  files?: string;
229
201
  sequential?: boolean;
202
+ specId?: string;
230
203
  }): void {
231
204
  initSchema();
232
205
  const db = getDb();
233
206
 
234
- const spec = db
235
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
236
- .get() as any;
237
-
238
- if (!spec) {
239
- console.error("\nNenhuma feature ativa.");
240
- console.error("Inicie com: /codexa:feature\n");
241
- process.exit(1);
242
- }
243
-
244
- if (spec.phase !== "planning") {
245
- console.error(`\nNao e possivel adicionar tasks na fase '${spec.phase}'.`);
246
- console.error("Tasks so podem ser adicionadas na fase 'planning'.\n");
247
- process.exit(1);
248
- }
207
+ const spec = resolveSpec(options.specId, ["planning"]);
249
208
 
250
209
  // Pegar proximo numero
251
210
  const lastTask = db
@@ -262,8 +221,7 @@ export function planTaskAdd(options: {
262
221
  .query("SELECT id FROM tasks WHERE spec_id = ? AND number = ?")
263
222
  .get(spec.id, depId);
264
223
  if (!exists) {
265
- console.error(`\nDependencia invalida: task #${depId} nao existe.\n`);
266
- process.exit(2);
224
+ throw new ValidationError(`Dependencia invalida: task #${depId} nao existe.`);
267
225
  }
268
226
  }
269
227
 
@@ -302,10 +260,9 @@ export function planTaskAdd(options: {
302
260
  }
303
261
 
304
262
  if (hasCycle(nextNumber)) {
305
- console.error(`\nERRO: Dependencia circular detectada!`);
306
- console.error(`Task #${nextNumber} -> [${dependsOn.join(", ")}] cria um ciclo.`);
307
- console.error(`Corrija as dependencias para evitar deadlocks.\n`);
308
- process.exit(2);
263
+ throw new ValidationError(
264
+ `Dependencia circular detectada! Task #${nextNumber} -> [${dependsOn.join(", ")}] cria um ciclo.\nCorrija as dependencias para evitar deadlocks.`
265
+ );
309
266
  }
310
267
  }
311
268
 
@@ -344,18 +301,11 @@ export function planTaskAdd(options: {
344
301
  console.log(` Paralelizavel: ${options.sequential ? "Nao" : "Sim"}\n`);
345
302
  }
346
303
 
347
- export function planCancel(): void {
304
+ export function planCancel(specId?: string): void {
348
305
  initSchema();
349
306
  const db = getDb();
350
307
 
351
- const spec = db
352
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
353
- .get() as any;
354
-
355
- if (!spec) {
356
- console.error("\nNenhuma feature ativa para cancelar.\n");
357
- process.exit(1);
358
- }
308
+ const spec = resolveSpec(specId);
359
309
 
360
310
  const now = new Date().toISOString();
361
311
 
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
2
2
  import { initSchema } from "../db/schema";
3
3
  import { existsSync, readFileSync, writeFileSync } from "fs";
4
4
  import { join } from "path";
5
+ import { CodexaError } from "../errors";
5
6
 
6
7
  interface ProductContext {
7
8
  name: string;
@@ -93,15 +94,13 @@ export function productImport(options: { file?: string; content?: string }): voi
93
94
 
94
95
  if (options.file) {
95
96
  if (!existsSync(options.file)) {
96
- console.error(`\nArquivo nao encontrado: ${options.file}\n`);
97
- process.exit(1);
97
+ throw new CodexaError("Arquivo nao encontrado: " + options.file);
98
98
  }
99
99
  prdContent = readFileSync(options.file, "utf-8");
100
100
  } else if (options.content) {
101
101
  prdContent = options.content;
102
102
  } else {
103
- console.error("\nForneca --file ou --content\n");
104
- process.exit(1);
103
+ throw new CodexaError("Forneca --file ou --content");
105
104
  }
106
105
 
107
106
  // Salvar como pendente para o agente processar
@@ -286,21 +285,15 @@ export function productConfirm(): void {
286
285
 
287
286
  const pending = db.query("SELECT * FROM product_context WHERE id = 'pending'").get() as any;
288
287
  if (!pending) {
289
- console.error("\nNenhum contexto de produto pendente.");
290
- console.error("Execute: product guide ou product import primeiro\n");
291
- process.exit(1);
288
+ throw new CodexaError("Nenhum contexto de produto pendente.\nExecute: product guide ou product import primeiro");
292
289
  }
293
290
 
294
291
  // Validar campos obrigatorios
295
292
  if (!pending.name || pending.name === "Pendente") {
296
- console.error("\nCampo obrigatorio ausente: name");
297
- console.error("Use: product set --name \"Nome do Produto\"\n");
298
- process.exit(1);
293
+ throw new CodexaError("Campo obrigatorio ausente: name\nUse: product set --name \"Nome do Produto\"");
299
294
  }
300
295
  if (!pending.problem || pending.problem === "") {
301
- console.error("\nCampo obrigatorio ausente: problem");
302
- console.error("Use: product set --problem \"Problema que resolve\"\n");
303
- process.exit(1);
296
+ throw new CodexaError("Campo obrigatorio ausente: problem\nUse: product set --problem \"Problema que resolve\"");
304
297
  }
305
298
 
306
299
  const now = new Date().toISOString();
@@ -358,12 +351,10 @@ export function productShow(options: { json?: boolean; pending?: boolean } = {})
358
351
 
359
352
  if (!product) {
360
353
  if (options.pending) {
361
- console.error("\nNenhum contexto pendente.");
354
+ throw new CodexaError("Nenhum contexto pendente.");
362
355
  } else {
363
- console.error("\nContexto de produto nao definido.");
364
- console.error("Execute: product guide ou product import\n");
356
+ throw new CodexaError("Contexto de produto nao definido.\nExecute: product guide ou product import");
365
357
  }
366
- process.exit(1);
367
358
  }
368
359
 
369
360
  const goals = db.query(`SELECT * FROM product_goals WHERE product_id = ? ORDER BY priority, category`).all(id) as any[];
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
2
2
  import { initSchema } from "../db/schema";
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import { join } from "path";
5
+ import { CodexaError } from "../errors";
5
6
  import { getDetectors } from "../detectors/loader";
6
7
 
7
8
  // ============================================================
@@ -359,7 +360,7 @@ export async function researchStart(options: { json?: boolean } = {}): Promise<v
359
360
  console.error("Certifique-se de estar na raiz do projeto.");
360
361
  console.error("Ecossistemas suportados: Node.js, Python, Go, .NET, Rust, Java/Kotlin, Flutter\n");
361
362
  }
362
- process.exit(1);
363
+ throw new CodexaError("Nenhum ecossistema ou biblioteca detectada.\nCertifique-se de estar na raiz do projeto.");
363
364
  }
364
365
 
365
366
  const libContextDir = ensureLibContextDir();
@@ -517,7 +518,7 @@ export function researchShow(options: { json?: boolean; lib?: string } = {}): vo
517
518
  console.error("\nNenhuma biblioteca registrada.");
518
519
  console.error("Execute: research start\n");
519
520
  }
520
- process.exit(1);
521
+ throw new CodexaError("Nenhuma biblioteca registrada.\nExecute: research start");
521
522
  }
522
523
 
523
524
  if (options.json) {
@@ -590,7 +591,7 @@ export function researchFill(libName: string, options: { json?: boolean } = {}):
590
591
  console.error(`\nBiblioteca '${libName}' nao encontrada.`);
591
592
  console.error("Execute: research show para ver bibliotecas disponiveis.\n");
592
593
  }
593
- process.exit(1);
594
+ throw new CodexaError("Biblioteca '" + libName + "' nao encontrada.\nExecute: research show para ver bibliotecas disponiveis.");
594
595
  }
595
596
 
596
597
  const versionStr = lib.version && lib.version !== "latest" ? lib.version : "latest";