@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.
- package/commands/architect.test.ts +531 -0
- package/commands/architect.ts +75 -17
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -1
- package/commands/decide.ts +37 -49
- package/commands/discover.ts +136 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +192 -102
- package/commands/patterns.test.ts +169 -0
- package/commands/patterns.ts +6 -13
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +18 -66
- 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 +89 -0
- package/commands/task.ts +72 -167
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -706
- package/db/schema.test.ts +760 -0
- package/db/schema.ts +284 -130
- package/gates/validator.test.ts +675 -0
- package/gates/validator.ts +112 -27
- 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 +19 -1
- package/workflow.ts +176 -67
|
@@ -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
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { CodexaError } from "../errors";
|
|
5
|
+
|
|
6
|
+
// Agents que sao referencias (nao spawnables como subagent_type)
|
|
7
|
+
const REFERENCE_FILES = new Set([
|
|
8
|
+
"common-directives.md",
|
|
9
|
+
"subagent-return-protocol.md",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function findPluginAgentsDir(): string | null {
|
|
13
|
+
// 1. Dev repo: git root + plugins/codexa-workflow/agents/
|
|
14
|
+
try {
|
|
15
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
16
|
+
const pluginDir = join(gitRoot, "plugins", "codexa-workflow", "agents");
|
|
17
|
+
if (existsSync(pluginDir)) return pluginDir;
|
|
18
|
+
} catch {
|
|
19
|
+
// Not in git repo
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Global install: resolve from @codexa/cli package location
|
|
23
|
+
try {
|
|
24
|
+
const cliPath = execSync("bun pm ls -g 2>/dev/null || npm list -g @codexa/cli --parseable 2>/dev/null", {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
}).trim();
|
|
27
|
+
if (cliPath) {
|
|
28
|
+
// Navigate from cli package to sibling plugin
|
|
29
|
+
const lines = cliPath.split("\n").filter(l => l.includes("codexa"));
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
// Try parent paths to find the monorepo root
|
|
33
|
+
const parts = trimmed.split(/[/\\]/);
|
|
34
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
35
|
+
const candidate = join(parts.slice(0, i + 1).join("/"), "plugins", "codexa-workflow", "agents");
|
|
36
|
+
if (existsSync(candidate)) return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Package manager not available
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function syncAgents(options: { force?: boolean } = {}): void {
|
|
48
|
+
const pluginDir = findPluginAgentsDir();
|
|
49
|
+
|
|
50
|
+
if (!pluginDir) {
|
|
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.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const targetDir = join(process.cwd(), ".claude", "agents");
|
|
55
|
+
mkdirSync(targetDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const agentFiles = readdirSync(pluginDir).filter(
|
|
58
|
+
(f) => f.endsWith(".md") && !REFERENCE_FILES.has(f)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let copied = 0;
|
|
62
|
+
let skipped = 0;
|
|
63
|
+
|
|
64
|
+
for (const file of agentFiles) {
|
|
65
|
+
const src = join(pluginDir, file);
|
|
66
|
+
const dst = join(targetDir, file);
|
|
67
|
+
|
|
68
|
+
if (existsSync(dst) && !options.force) {
|
|
69
|
+
skipped++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
copyFileSync(src, dst);
|
|
74
|
+
copied++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\n[OK] Agents sincronizados para .claude/agents/`);
|
|
78
|
+
console.log(` Copiados: ${copied}`);
|
|
79
|
+
if (skipped > 0) {
|
|
80
|
+
console.log(` Ignorados (ja existem): ${skipped} — use --force para sobrescrever`);
|
|
81
|
+
}
|
|
82
|
+
console.log(` Total disponivel: ${agentFiles.length}`);
|
|
83
|
+
console.log(`\nAgents disponiveis como subagent_type:`);
|
|
84
|
+
for (const file of agentFiles) {
|
|
85
|
+
const name = file.replace(".md", "");
|
|
86
|
+
console.log(` - ${name}`);
|
|
87
|
+
}
|
|
88
|
+
console.log();
|
|
89
|
+
}
|