@codexa/cli 9.0.2 → 9.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/architect.test.ts +531 -0
- package/commands/architect.ts +68 -11
- package/commands/clear.ts +0 -1
- package/commands/decide.ts +28 -28
- package/commands/discover.ts +128 -3
- package/commands/knowledge.ts +2 -27
- package/commands/patterns.test.ts +169 -0
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +4 -2
- package/commands/sync.ts +90 -0
- package/commands/task.ts +43 -159
- package/commands/utils.ts +251 -249
- package/db/schema.test.ts +333 -0
- package/db/schema.ts +160 -130
- package/gates/validator.test.ts +617 -0
- package/gates/validator.ts +42 -10
- package/package.json +3 -1
- package/protocol/process-return.ts +25 -93
- package/protocol/subagent-protocol.test.ts +936 -0
- package/protocol/subagent-protocol.ts +19 -1
- package/workflow.ts +85 -27
package/commands/decide.ts
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
|
-
import { initSchema } from "../db/schema";
|
|
3
|
-
|
|
4
|
-
function getNextDecisionId(specId: string): string {
|
|
5
|
-
const db = getDb();
|
|
6
|
-
const last = db
|
|
7
|
-
.query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
|
|
8
|
-
.get(specId) as any;
|
|
9
|
-
|
|
10
|
-
if (!last) return "DEC-001";
|
|
11
|
-
|
|
12
|
-
const num = parseInt(last.id.replace("DEC-", "")) + 1;
|
|
13
|
-
return `DEC-${num.toString().padStart(3, "0")}`;
|
|
14
|
-
}
|
|
2
|
+
import { initSchema, getNextDecisionId } from "../db/schema";
|
|
15
3
|
|
|
16
4
|
interface ConflictAnalysis {
|
|
17
5
|
hasConflict: boolean;
|
|
@@ -182,21 +170,33 @@ export function decide(title: string, decision: string, options: { rationale?: s
|
|
|
182
170
|
.query("SELECT * FROM tasks WHERE spec_id = ? AND status = 'running' LIMIT 1")
|
|
183
171
|
.get(spec.id) as any;
|
|
184
172
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
173
|
+
let decisionId = "";
|
|
174
|
+
let retries = 3;
|
|
175
|
+
while (retries > 0) {
|
|
176
|
+
decisionId = getNextDecisionId(spec.id);
|
|
177
|
+
try {
|
|
178
|
+
db.run(
|
|
179
|
+
`INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
|
|
180
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
|
|
181
|
+
[
|
|
182
|
+
decisionId,
|
|
183
|
+
spec.id,
|
|
184
|
+
currentTask?.number || null,
|
|
185
|
+
title,
|
|
186
|
+
decision,
|
|
187
|
+
options.rationale || null,
|
|
188
|
+
now,
|
|
189
|
+
]
|
|
190
|
+
);
|
|
191
|
+
break;
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
if (e.message?.includes("UNIQUE constraint") && retries > 1) {
|
|
194
|
+
retries--;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
200
|
|
|
201
201
|
console.log(`\nDecisao registrada: ${decisionId}`);
|
|
202
202
|
console.log(`Titulo: ${title}`);
|
package/commands/discover.ts
CHANGED
|
@@ -183,9 +183,9 @@ export function discoverConfirm(): void {
|
|
|
183
183
|
// Mover de pending para default
|
|
184
184
|
db.run("DELETE FROM project WHERE id = 'pending'");
|
|
185
185
|
db.run(
|
|
186
|
-
`INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version)
|
|
187
|
-
VALUES ('default', ?, ?, ?, ?, ?)`,
|
|
188
|
-
["Projeto", JSON.stringify(data.stack), now, now, pkg.version]
|
|
186
|
+
`INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version, last_discover_at)
|
|
187
|
+
VALUES ('default', ?, ?, ?, ?, ?, ?)`,
|
|
188
|
+
["Projeto", JSON.stringify(data.stack), now, now, pkg.version, now]
|
|
189
189
|
);
|
|
190
190
|
|
|
191
191
|
// Criar standards baseados na estrutura detectada
|
|
@@ -460,6 +460,131 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
|
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
export async function discoverIncremental(): Promise<void> {
|
|
464
|
+
initSchema();
|
|
465
|
+
const db = getDb();
|
|
466
|
+
|
|
467
|
+
const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
|
468
|
+
if (!project) {
|
|
469
|
+
console.error("\nProjeto nao descoberto.");
|
|
470
|
+
console.error("Execute: discover start primeiro\n");
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Data de referencia: last_discover_at > discovered_at > fallback 30 dias
|
|
475
|
+
const sinceDate = project.last_discover_at || project.discovered_at ||
|
|
476
|
+
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
477
|
+
|
|
478
|
+
console.log(`\nDiscover incremental desde: ${sinceDate}\n`);
|
|
479
|
+
|
|
480
|
+
// Listar arquivos modificados via git
|
|
481
|
+
const gitResult = spawnSync("git", [
|
|
482
|
+
"diff", "--name-only", "--diff-filter=ACMR",
|
|
483
|
+
`--since=${sinceDate}`, "HEAD"
|
|
484
|
+
], { encoding: "utf-8", timeout: 10000 });
|
|
485
|
+
|
|
486
|
+
// Fallback: se --since nao funciona com diff, usar log
|
|
487
|
+
let modifiedFiles: string[] = [];
|
|
488
|
+
if (gitResult.status !== 0 || !gitResult.stdout.trim()) {
|
|
489
|
+
const logResult = spawnSync("git", [
|
|
490
|
+
"log", "--since", sinceDate, "--name-only", "--pretty=format:", "--diff-filter=ACMR"
|
|
491
|
+
], { encoding: "utf-8", timeout: 10000 });
|
|
492
|
+
|
|
493
|
+
if (logResult.status === 0 && logResult.stdout.trim()) {
|
|
494
|
+
modifiedFiles = [...new Set(logResult.stdout.trim().split("\n").filter(f => f.trim()))];
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
modifiedFiles = [...new Set(gitResult.stdout.trim().split("\n").filter(f => f.trim()))];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Filtrar apenas arquivos que ainda existem e sao codigo
|
|
501
|
+
const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".cs", ".dart"];
|
|
502
|
+
const existingFiles = modifiedFiles.filter(f => {
|
|
503
|
+
const fullPath = join(process.cwd(), f);
|
|
504
|
+
return existsSync(fullPath) && codeExtensions.some(ext => f.endsWith(ext));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (existingFiles.length === 0) {
|
|
508
|
+
console.log("Nenhum arquivo de codigo modificado desde ultimo discover.");
|
|
509
|
+
console.log("Stack e utilities estao atualizados.\n");
|
|
510
|
+
|
|
511
|
+
// Atualizar timestamp mesmo sem mudancas
|
|
512
|
+
const now = new Date().toISOString();
|
|
513
|
+
db.run("UPDATE project SET last_discover_at = ? WHERE id = 'default'", [now]);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log(`[1/3] Analisando ${existingFiles.length} arquivo(s) modificado(s)...`);
|
|
518
|
+
|
|
519
|
+
// Importar funcoes de patterns
|
|
520
|
+
const { extractUtilitiesFromFile, inferScopeFromPath } = await import("./patterns");
|
|
521
|
+
const { upsertUtility } = await import("../db/schema");
|
|
522
|
+
|
|
523
|
+
// Extrair utilities dos arquivos modificados
|
|
524
|
+
let utilitiesUpdated = 0;
|
|
525
|
+
for (const file of existingFiles) {
|
|
526
|
+
const fullPath = join(process.cwd(), file);
|
|
527
|
+
const utilities = extractUtilitiesFromFile(fullPath);
|
|
528
|
+
const scope = inferScopeFromPath(file);
|
|
529
|
+
|
|
530
|
+
for (const util of utilities) {
|
|
531
|
+
upsertUtility({
|
|
532
|
+
filePath: file,
|
|
533
|
+
utilityName: util.name,
|
|
534
|
+
utilityType: util.type,
|
|
535
|
+
scope,
|
|
536
|
+
signature: util.signature,
|
|
537
|
+
});
|
|
538
|
+
utilitiesUpdated++;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(` ✓ ${utilitiesUpdated} utilities registradas/atualizadas`);
|
|
543
|
+
|
|
544
|
+
// Re-detectar stack e comparar
|
|
545
|
+
console.log("\n[2/3] Verificando mudancas no stack...");
|
|
546
|
+
const currentStack = JSON.parse(project.stack);
|
|
547
|
+
const newStack = await detectStack();
|
|
548
|
+
|
|
549
|
+
const stackChanges: { key: string; from: string | undefined; to: string | undefined }[] = [];
|
|
550
|
+
const allKeys = new Set([...Object.keys(currentStack), ...Object.keys(newStack)]);
|
|
551
|
+
|
|
552
|
+
for (const key of allKeys) {
|
|
553
|
+
const current = currentStack[key as keyof StackDetection];
|
|
554
|
+
const detected = newStack[key as keyof StackDetection];
|
|
555
|
+
if (current !== detected) {
|
|
556
|
+
stackChanges.push({ key, from: current, to: detected });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (stackChanges.length > 0) {
|
|
561
|
+
console.log(" ⚠ Mudancas detectadas no stack:");
|
|
562
|
+
for (const change of stackChanges) {
|
|
563
|
+
const from = change.from || "(nao definido)";
|
|
564
|
+
const to = change.to || "(removido)";
|
|
565
|
+
console.log(` ${change.key}: ${from} → ${to}`);
|
|
566
|
+
}
|
|
567
|
+
console.log(" Use: discover refresh --force para aplicar");
|
|
568
|
+
} else {
|
|
569
|
+
console.log(" ✓ Stack inalterado");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Atualizar timestamp
|
|
573
|
+
console.log("\n[3/3] Atualizando timestamp...");
|
|
574
|
+
const now = new Date().toISOString();
|
|
575
|
+
db.run("UPDATE project SET last_discover_at = ?, updated_at = ? WHERE id = 'default'", [now, now]);
|
|
576
|
+
|
|
577
|
+
// Resumo
|
|
578
|
+
console.log("\n" + "═".repeat(50));
|
|
579
|
+
console.log("DISCOVER INCREMENTAL CONCLUIDO");
|
|
580
|
+
console.log("═".repeat(50));
|
|
581
|
+
console.log(` Arquivos analisados: ${existingFiles.length}`);
|
|
582
|
+
console.log(` Utilities atualizadas: ${utilitiesUpdated}`);
|
|
583
|
+
console.log(` Mudancas no stack: ${stackChanges.length}`);
|
|
584
|
+
console.log(` Timestamp atualizado: ${now}`);
|
|
585
|
+
console.log("═".repeat(50) + "\n");
|
|
586
|
+
}
|
|
587
|
+
|
|
463
588
|
function generateStandardsMarkdown(): void {
|
|
464
589
|
const db = getDb();
|
|
465
590
|
const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
package/commands/knowledge.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
|
-
import { initSchema, getRelatedDecisions, getRelatedFiles
|
|
2
|
+
import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
|
|
3
3
|
|
|
4
4
|
type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
|
|
5
5
|
type KnowledgeSeverity = "info" | "warning" | "critical";
|
|
@@ -233,7 +233,6 @@ export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[]
|
|
|
233
233
|
export function queryGraph(options: {
|
|
234
234
|
file?: string;
|
|
235
235
|
decision?: string;
|
|
236
|
-
contradictions?: boolean;
|
|
237
236
|
json?: boolean;
|
|
238
237
|
}): void {
|
|
239
238
|
initSchema();
|
|
@@ -245,29 +244,6 @@ export function queryGraph(options: {
|
|
|
245
244
|
process.exit(1);
|
|
246
245
|
}
|
|
247
246
|
|
|
248
|
-
// Buscar contradicoes
|
|
249
|
-
if (options.contradictions) {
|
|
250
|
-
const contradictions = findContradictions(spec.id);
|
|
251
|
-
|
|
252
|
-
if (options.json) {
|
|
253
|
-
console.log(JSON.stringify({ contradictions }));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (contradictions.length === 0) {
|
|
258
|
-
console.log("\nNenhuma contradicao detectada.\n");
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
console.log(`\nContradicoes detectadas (${contradictions.length}):`);
|
|
263
|
-
console.log(`${"─".repeat(50)}`);
|
|
264
|
-
for (const c of contradictions) {
|
|
265
|
-
console.log(` [!] "${c.decision1}" <-> "${c.decision2}"`);
|
|
266
|
-
console.log(` Detectada em: ${c.createdAt}\n`);
|
|
267
|
-
}
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
247
|
// Buscar relacoes para um arquivo
|
|
272
248
|
if (options.file) {
|
|
273
249
|
const decisions = getRelatedDecisions(options.file, "file");
|
|
@@ -357,8 +333,7 @@ export function queryGraph(options: {
|
|
|
357
333
|
}
|
|
358
334
|
console.log(`\nComandos:`);
|
|
359
335
|
console.log(` knowledge graph --file <path> Relacoes de um arquivo`);
|
|
360
|
-
console.log(` knowledge graph --decision <id> Arquivos afetados por decisao`);
|
|
361
|
-
console.log(` knowledge graph --contradictions Detectar contradicoes\n`);
|
|
336
|
+
console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
|
|
362
337
|
}
|
|
363
338
|
|
|
364
339
|
// v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "bun:test";
|
|
2
|
+
import { extractUtilitiesFromFile, inferScopeFromPath } from "./patterns";
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const TMP_DIR = join(import.meta.dir, "__test_tmp__");
|
|
7
|
+
|
|
8
|
+
function setupTmpDir() {
|
|
9
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cleanTmpDir() {
|
|
13
|
+
try { rmSync(TMP_DIR, { recursive: true }); } catch {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeTmpFile(name: string, content: string): string {
|
|
17
|
+
setupTmpDir();
|
|
18
|
+
const path = join(TMP_DIR, name);
|
|
19
|
+
writeFileSync(path, content);
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("extractUtilitiesFromFile", () => {
|
|
24
|
+
afterAll(() => cleanTmpDir());
|
|
25
|
+
|
|
26
|
+
it("extracts exported function", () => {
|
|
27
|
+
const path = writeTmpFile("func.ts", 'export function createUser(name: string): User {\n return { name };\n}');
|
|
28
|
+
const utils = extractUtilitiesFromFile(path);
|
|
29
|
+
expect(utils).toHaveLength(1);
|
|
30
|
+
expect(utils[0].name).toBe("createUser");
|
|
31
|
+
expect(utils[0].type).toBe("function");
|
|
32
|
+
expect(utils[0].signature).toContain("(name: string)");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("extracts exported async function", () => {
|
|
36
|
+
const path = writeTmpFile("async.ts", 'export async function fetchData(url: string): Promise<Data> {\n return await fetch(url);\n}');
|
|
37
|
+
const utils = extractUtilitiesFromFile(path);
|
|
38
|
+
expect(utils).toHaveLength(1);
|
|
39
|
+
expect(utils[0].name).toBe("fetchData");
|
|
40
|
+
expect(utils[0].type).toBe("function");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("extracts exported const", () => {
|
|
44
|
+
const path = writeTmpFile("const.ts", 'export const MAX_RETRIES: number = 3;');
|
|
45
|
+
const utils = extractUtilitiesFromFile(path);
|
|
46
|
+
expect(utils).toHaveLength(1);
|
|
47
|
+
expect(utils[0].name).toBe("MAX_RETRIES");
|
|
48
|
+
expect(utils[0].type).toBe("const");
|
|
49
|
+
expect(utils[0].signature).toBe("number");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("extracts exported const without type annotation", () => {
|
|
53
|
+
const path = writeTmpFile("const2.ts", 'export const config = { port: 3000 };');
|
|
54
|
+
const utils = extractUtilitiesFromFile(path);
|
|
55
|
+
expect(utils).toHaveLength(1);
|
|
56
|
+
expect(utils[0].name).toBe("config");
|
|
57
|
+
expect(utils[0].type).toBe("const");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("extracts exported class", () => {
|
|
61
|
+
const path = writeTmpFile("class.ts", 'export class UserService {\n constructor() {}\n}');
|
|
62
|
+
const utils = extractUtilitiesFromFile(path);
|
|
63
|
+
expect(utils).toHaveLength(1);
|
|
64
|
+
expect(utils[0].name).toBe("UserService");
|
|
65
|
+
expect(utils[0].type).toBe("class");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("extracts exported interface", () => {
|
|
69
|
+
const path = writeTmpFile("iface.ts", 'export interface UserProps {\n name: string;\n}');
|
|
70
|
+
const utils = extractUtilitiesFromFile(path);
|
|
71
|
+
expect(utils).toHaveLength(1);
|
|
72
|
+
expect(utils[0].name).toBe("UserProps");
|
|
73
|
+
expect(utils[0].type).toBe("interface");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("extracts exported type", () => {
|
|
77
|
+
const path = writeTmpFile("type.ts", 'export type Status = "active" | "inactive";');
|
|
78
|
+
const utils = extractUtilitiesFromFile(path);
|
|
79
|
+
expect(utils).toHaveLength(1);
|
|
80
|
+
expect(utils[0].name).toBe("Status");
|
|
81
|
+
expect(utils[0].type).toBe("type");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("extracts multiple exports from one file", () => {
|
|
85
|
+
const path = writeTmpFile("multi.ts", [
|
|
86
|
+
'export function foo() {}',
|
|
87
|
+
'export const BAR = 1;',
|
|
88
|
+
'export class Baz {}',
|
|
89
|
+
'export interface Qux {}',
|
|
90
|
+
'export type Quux = string;',
|
|
91
|
+
].join("\n"));
|
|
92
|
+
const utils = extractUtilitiesFromFile(path);
|
|
93
|
+
expect(utils).toHaveLength(5);
|
|
94
|
+
expect(utils.map(u => u.name)).toEqual(["foo", "BAR", "Baz", "Qux", "Quux"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns empty array for non-existent file", () => {
|
|
98
|
+
const utils = extractUtilitiesFromFile("/nonexistent/path.ts");
|
|
99
|
+
expect(utils).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns empty array for file without exports", () => {
|
|
103
|
+
const path = writeTmpFile("noexport.ts", 'const internal = 1;\nfunction helper() {}');
|
|
104
|
+
const utils = extractUtilitiesFromFile(path);
|
|
105
|
+
expect(utils).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("ignores non-export declarations", () => {
|
|
109
|
+
const path = writeTmpFile("mixed.ts", [
|
|
110
|
+
'const private1 = 1;',
|
|
111
|
+
'function private2() {}',
|
|
112
|
+
'export function public1() {}',
|
|
113
|
+
].join("\n"));
|
|
114
|
+
const utils = extractUtilitiesFromFile(path);
|
|
115
|
+
expect(utils).toHaveLength(1);
|
|
116
|
+
expect(utils[0].name).toBe("public1");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("inferScopeFromPath", () => {
|
|
121
|
+
it("identifies backend paths", () => {
|
|
122
|
+
expect(inferScopeFromPath("src/app/api/users/route.ts")).toBe("backend");
|
|
123
|
+
expect(inferScopeFromPath("src/server/index.ts")).toBe("backend");
|
|
124
|
+
expect(inferScopeFromPath("src/backend/auth.ts")).toBe("backend");
|
|
125
|
+
expect(inferScopeFromPath("src/services/user.ts")).toBe("backend");
|
|
126
|
+
expect(inferScopeFromPath("src/actions/submit.ts")).toBe("backend");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("identifies frontend paths", () => {
|
|
130
|
+
expect(inferScopeFromPath("src/components/Button.tsx")).toBe("frontend");
|
|
131
|
+
expect(inferScopeFromPath("src/hooks/useAuth.ts")).toBe("frontend");
|
|
132
|
+
expect(inferScopeFromPath("src/pages/index.tsx")).toBe("frontend");
|
|
133
|
+
expect(inferScopeFromPath("src/frontend/App.tsx")).toBe("frontend");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("identifies frontend for /app/ paths without /api/", () => {
|
|
137
|
+
expect(inferScopeFromPath("src/app/dashboard/page.tsx")).toBe("frontend");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("identifies database paths", () => {
|
|
141
|
+
expect(inferScopeFromPath("src/db/schema.ts")).toBe("database");
|
|
142
|
+
expect(inferScopeFromPath("src/schema/users.ts")).toBe("database");
|
|
143
|
+
expect(inferScopeFromPath("src/migrations/001.sql")).toBe("database");
|
|
144
|
+
expect(inferScopeFromPath("src/database/connection.ts")).toBe("database");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("identifies testing paths", () => {
|
|
148
|
+
expect(inferScopeFromPath("src/tests/user.test.ts")).toBe("testing");
|
|
149
|
+
// Note: /components/ matches frontend before .test. matches testing
|
|
150
|
+
expect(inferScopeFromPath("src/components/Button.test.tsx")).toBe("frontend");
|
|
151
|
+
expect(inferScopeFromPath("src/lib/auth.spec.ts")).toBe("testing");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns shared for unrecognized paths", () => {
|
|
155
|
+
expect(inferScopeFromPath("src/lib/utils.ts")).toBe("shared");
|
|
156
|
+
expect(inferScopeFromPath("src/config/env.ts")).toBe("shared");
|
|
157
|
+
expect(inferScopeFromPath("package.json")).toBe("shared");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles Windows-style paths (backslashes)", () => {
|
|
161
|
+
expect(inferScopeFromPath("src\\components\\Button.tsx")).toBe("frontend");
|
|
162
|
+
expect(inferScopeFromPath("src\\db\\schema.ts")).toBe("database");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("is case-insensitive", () => {
|
|
166
|
+
expect(inferScopeFromPath("src/Components/Button.tsx")).toBe("frontend");
|
|
167
|
+
expect(inferScopeFromPath("src/DB/Schema.ts")).toBe("database");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { generateSpecId } from "./plan";
|
|
3
|
+
|
|
4
|
+
describe("generateSpecId", () => {
|
|
5
|
+
it("generates ID with date-slug-hash format", () => {
|
|
6
|
+
const id = generateSpecId("Add user authentication");
|
|
7
|
+
// Format: YYYY-MM-DD-slug-hash
|
|
8
|
+
expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+-[a-z0-9]{4}$/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("normalizes slug to lowercase", () => {
|
|
12
|
+
const id = generateSpecId("Add User Auth");
|
|
13
|
+
const parts = id.split("-");
|
|
14
|
+
// After date (3 parts), all slug parts should be lowercase
|
|
15
|
+
const slugAndHash = parts.slice(3).join("-");
|
|
16
|
+
expect(slugAndHash).toBe(slugAndHash.toLowerCase());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("removes special characters from slug", () => {
|
|
20
|
+
const id = generateSpecId("Add auth (v2) @special!");
|
|
21
|
+
// Should not contain (, ), @, !
|
|
22
|
+
expect(id).not.toMatch(/[()@!]/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("truncates slug at 30 characters", () => {
|
|
26
|
+
const longName = "This is a very long feature name that exceeds thirty characters by a lot";
|
|
27
|
+
const id = generateSpecId(longName);
|
|
28
|
+
// Date is YYYY-MM-DD (10 chars) + dash + slug (max 30) + dash + hash (4)
|
|
29
|
+
const parts = id.split("-");
|
|
30
|
+
// date = parts[0]-parts[1]-parts[2], hash = last part
|
|
31
|
+
const datePart = parts.slice(0, 3).join("-"); // YYYY-MM-DD
|
|
32
|
+
const hashPart = parts[parts.length - 1]; // 4 char hash
|
|
33
|
+
const slugPart = parts.slice(3, -1).join("-"); // everything between date and hash
|
|
34
|
+
expect(slugPart.length).toBeLessThanOrEqual(30);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes a 4-character hash suffix", () => {
|
|
38
|
+
const id = generateSpecId("Test feature");
|
|
39
|
+
const parts = id.split("-");
|
|
40
|
+
const hash = parts[parts.length - 1];
|
|
41
|
+
expect(hash.length).toBe(4);
|
|
42
|
+
// Hash should be base36
|
|
43
|
+
expect(hash).toMatch(/^[a-z0-9]+$/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("starts with today's date", () => {
|
|
47
|
+
const id = generateSpecId("Some feature");
|
|
48
|
+
const today = new Date().toISOString().split("T")[0];
|
|
49
|
+
expect(id.startsWith(today)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("generates different IDs for different names", () => {
|
|
53
|
+
const id1 = generateSpecId("Feature A");
|
|
54
|
+
const id2 = generateSpecId("Feature B");
|
|
55
|
+
expect(id1).not.toBe(id2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("removes trailing dashes from slug", () => {
|
|
59
|
+
const id = generateSpecId("test-feature-");
|
|
60
|
+
// Should not have double dashes or trailing dash before hash
|
|
61
|
+
expect(id).not.toMatch(/--/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles empty-ish name gracefully", () => {
|
|
65
|
+
const id = generateSpecId("a");
|
|
66
|
+
expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-a-[a-z0-9]{4}$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("converts spaces and special chars to dashes", () => {
|
|
70
|
+
const id = generateSpecId("add user auth");
|
|
71
|
+
expect(id).toContain("add-user-auth");
|
|
72
|
+
});
|
|
73
|
+
});
|
package/commands/plan.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
3
3
|
|
|
4
|
-
function generateSpecId(name: string): string {
|
|
4
|
+
export function generateSpecId(name: string): string {
|
|
5
5
|
const date = new Date().toISOString().split("T")[0];
|
|
6
6
|
const slug = name
|
|
7
7
|
.toLowerCase()
|
|
8
8
|
.replace(/[^a-z0-9]+/g, "-")
|
|
9
|
+
.replace(/-+$/, "")
|
|
9
10
|
.substring(0, 30);
|
|
10
|
-
|
|
11
|
+
const hash = Bun.hash(name + Date.now()).toString(36).substring(0, 4);
|
|
12
|
+
return `${date}-${slug}-${hash}`;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
// v8.4: Suporte a --from-analysis para import automatico de baby steps
|
package/commands/sync.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
// Agents que sao referencias (nao spawnables como subagent_type)
|
|
6
|
+
const REFERENCE_FILES = new Set([
|
|
7
|
+
"common-directives.md",
|
|
8
|
+
"subagent-return-protocol.md",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function findPluginAgentsDir(): string | null {
|
|
12
|
+
// 1. Dev repo: git root + plugins/codexa-workflow/agents/
|
|
13
|
+
try {
|
|
14
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
15
|
+
const pluginDir = join(gitRoot, "plugins", "codexa-workflow", "agents");
|
|
16
|
+
if (existsSync(pluginDir)) return pluginDir;
|
|
17
|
+
} catch {
|
|
18
|
+
// Not in git repo
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Global install: resolve from @codexa/cli package location
|
|
22
|
+
try {
|
|
23
|
+
const cliPath = execSync("bun pm ls -g 2>/dev/null || npm list -g @codexa/cli --parseable 2>/dev/null", {
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
}).trim();
|
|
26
|
+
if (cliPath) {
|
|
27
|
+
// Navigate from cli package to sibling plugin
|
|
28
|
+
const lines = cliPath.split("\n").filter(l => l.includes("codexa"));
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
// Try parent paths to find the monorepo root
|
|
32
|
+
const parts = trimmed.split(/[/\\]/);
|
|
33
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
34
|
+
const candidate = join(parts.slice(0, i + 1).join("/"), "plugins", "codexa-workflow", "agents");
|
|
35
|
+
if (existsSync(candidate)) return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Package manager not available
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function syncAgents(options: { force?: boolean } = {}): void {
|
|
47
|
+
const pluginDir = findPluginAgentsDir();
|
|
48
|
+
|
|
49
|
+
if (!pluginDir) {
|
|
50
|
+
console.error("\n[ERRO] Diretorio de agents do plugin nao encontrado.");
|
|
51
|
+
console.error(" Verifique se voce esta no repositorio codexa ou se @codexa/cli esta instalado globalmente.\n");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const targetDir = join(process.cwd(), ".claude", "agents");
|
|
56
|
+
mkdirSync(targetDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const agentFiles = readdirSync(pluginDir).filter(
|
|
59
|
+
(f) => f.endsWith(".md") && !REFERENCE_FILES.has(f)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
let copied = 0;
|
|
63
|
+
let skipped = 0;
|
|
64
|
+
|
|
65
|
+
for (const file of agentFiles) {
|
|
66
|
+
const src = join(pluginDir, file);
|
|
67
|
+
const dst = join(targetDir, file);
|
|
68
|
+
|
|
69
|
+
if (existsSync(dst) && !options.force) {
|
|
70
|
+
skipped++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
copyFileSync(src, dst);
|
|
75
|
+
copied++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`\n[OK] Agents sincronizados para .claude/agents/`);
|
|
79
|
+
console.log(` Copiados: ${copied}`);
|
|
80
|
+
if (skipped > 0) {
|
|
81
|
+
console.log(` Ignorados (ja existem): ${skipped} — use --force para sobrescrever`);
|
|
82
|
+
}
|
|
83
|
+
console.log(` Total disponivel: ${agentFiles.length}`);
|
|
84
|
+
console.log(`\nAgents disponiveis como subagent_type:`);
|
|
85
|
+
for (const file of agentFiles) {
|
|
86
|
+
const name = file.replace(".md", "");
|
|
87
|
+
console.log(` - ${name}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|