@codemoot/cli 0.2.1
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/LICENSE +21 -0
- package/dist/index.js +3613 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts-NYEQYOVN.js +22 -0
- package/dist/prompts-NYEQYOVN.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3613 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5
|
+
import { CLEANUP_TIMEOUT_SEC, VERSION as VERSION2 } from "@codemoot/core";
|
|
6
|
+
|
|
7
|
+
// src/commands/build.ts
|
|
8
|
+
import { BuildStore, DebateStore, REVIEW_DIFF_MAX_CHARS, REVIEW_TEXT_MAX_CHARS, SessionManager, buildHandoffEnvelope, generateId, loadConfig, openDatabase as openDatabase2 } from "@codemoot/core";
|
|
9
|
+
import chalk3 from "chalk";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import { unlinkSync } from "fs";
|
|
12
|
+
import { join as join2 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/progress.ts
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
var THROTTLE_MS = 3e3;
|
|
17
|
+
function createProgressCallbacks(label = "codex") {
|
|
18
|
+
let lastActivityAt = 0;
|
|
19
|
+
let lastMessage = "";
|
|
20
|
+
function printActivity(msg) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (msg === lastMessage && now - lastActivityAt < THROTTLE_MS) return;
|
|
23
|
+
lastActivityAt = now;
|
|
24
|
+
lastMessage = msg;
|
|
25
|
+
console.error(chalk.dim(` [${label}] ${msg}`));
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
onSpawn(pid, command) {
|
|
29
|
+
console.error(chalk.dim(` [${label}] Started (PID: ${pid}, cmd: ${command})`));
|
|
30
|
+
},
|
|
31
|
+
onStderr(_chunk) {
|
|
32
|
+
},
|
|
33
|
+
onProgress(chunk) {
|
|
34
|
+
for (const line of chunk.split("\n")) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed) continue;
|
|
37
|
+
try {
|
|
38
|
+
const event = JSON.parse(trimmed);
|
|
39
|
+
formatEvent(event, printActivity);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onHeartbeat(elapsedSec) {
|
|
45
|
+
if (elapsedSec % 30 === 0) {
|
|
46
|
+
printActivity(`${elapsedSec}s elapsed...`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function formatEvent(event, print) {
|
|
52
|
+
const type = event.type;
|
|
53
|
+
if (type === "thread.started") {
|
|
54
|
+
const tid = event.thread_id ?? "";
|
|
55
|
+
print(`Thread: ${tid.slice(0, 12)}...`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (type === "item.completed") {
|
|
59
|
+
const item = event.item;
|
|
60
|
+
if (!item) return;
|
|
61
|
+
if (item.type === "tool_call" || item.type === "function_call") {
|
|
62
|
+
const name = item.name ?? item.function ?? "tool";
|
|
63
|
+
const args = String(item.arguments ?? item.input ?? "").slice(0, 80);
|
|
64
|
+
const pathMatch = args.match(/["']([^"']*\.[a-z]{1,4})["']/i);
|
|
65
|
+
if (pathMatch) {
|
|
66
|
+
print(`${name}: ${pathMatch[1]}`);
|
|
67
|
+
} else {
|
|
68
|
+
print(`${name}${args ? `: ${args.slice(0, 60)}` : ""}`);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (item.type === "agent_message") {
|
|
73
|
+
const text = String(item.text ?? "");
|
|
74
|
+
const firstLine = text.split("\n").find((l) => l.trim().length > 10);
|
|
75
|
+
if (firstLine) {
|
|
76
|
+
const preview = firstLine.trim().slice(0, 80);
|
|
77
|
+
print(`Response: ${preview}${firstLine.trim().length > 80 ? "..." : ""}`);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (type === "turn.completed") {
|
|
83
|
+
const usage = event.usage;
|
|
84
|
+
if (usage) {
|
|
85
|
+
const input = (usage.input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
86
|
+
const output = usage.output_tokens ?? 0;
|
|
87
|
+
print(`Turn done (${input} in / ${output} out tokens)`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/utils.ts
|
|
94
|
+
import { openDatabase } from "@codemoot/core";
|
|
95
|
+
import chalk2 from "chalk";
|
|
96
|
+
import { mkdirSync } from "fs";
|
|
97
|
+
import { join } from "path";
|
|
98
|
+
function getDbPath(projectDir) {
|
|
99
|
+
const base = projectDir ?? process.cwd();
|
|
100
|
+
const dbDir = join(base, ".cowork", "db");
|
|
101
|
+
mkdirSync(dbDir, { recursive: true });
|
|
102
|
+
return join(dbDir, "cowork.db");
|
|
103
|
+
}
|
|
104
|
+
async function withDatabase(fn) {
|
|
105
|
+
const db = openDatabase(getDbPath());
|
|
106
|
+
try {
|
|
107
|
+
return await fn(db);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
} finally {
|
|
112
|
+
db.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/build.ts
|
|
117
|
+
async function buildStartCommand(task, options) {
|
|
118
|
+
let db;
|
|
119
|
+
try {
|
|
120
|
+
const buildId = generateId();
|
|
121
|
+
const dbPath = getDbPath();
|
|
122
|
+
db = openDatabase2(dbPath);
|
|
123
|
+
const buildStore = new BuildStore(db);
|
|
124
|
+
const debateStore = new DebateStore(db);
|
|
125
|
+
const projectDir = process.cwd();
|
|
126
|
+
let baselineRef = null;
|
|
127
|
+
try {
|
|
128
|
+
const dirty = execSync("git status --porcelain", { cwd: projectDir, encoding: "utf-8" }).trim();
|
|
129
|
+
if (dirty && !options.allowDirty) {
|
|
130
|
+
db.close();
|
|
131
|
+
console.error(chalk3.red("Working tree is dirty. Commit or stash your changes first."));
|
|
132
|
+
console.error(chalk3.yellow("Use --allow-dirty to auto-stash."));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (dirty && options.allowDirty) {
|
|
136
|
+
execSync('git stash push -u -m "codemoot-build-baseline"', { cwd: projectDir, encoding: "utf-8" });
|
|
137
|
+
console.error(chalk3.yellow('Auto-stashed dirty changes with marker "codemoot-build-baseline"'));
|
|
138
|
+
}
|
|
139
|
+
baselineRef = execSync("git rev-parse HEAD", { cwd: projectDir, encoding: "utf-8" }).trim();
|
|
140
|
+
} catch {
|
|
141
|
+
console.error(chalk3.yellow("Warning: Not a git repository. No baseline tracking."));
|
|
142
|
+
}
|
|
143
|
+
const debateId = generateId();
|
|
144
|
+
debateStore.upsert({ debateId, role: "proposer", status: "active" });
|
|
145
|
+
debateStore.upsert({ debateId, role: "critic", status: "active" });
|
|
146
|
+
debateStore.saveState(debateId, "proposer", {
|
|
147
|
+
debateId,
|
|
148
|
+
question: task,
|
|
149
|
+
models: ["codex-architect", "codex-reviewer"],
|
|
150
|
+
round: 0,
|
|
151
|
+
turn: 0,
|
|
152
|
+
thread: [],
|
|
153
|
+
runningSummary: "",
|
|
154
|
+
stanceHistory: [],
|
|
155
|
+
usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalCalls: 0, startedAt: Date.now() },
|
|
156
|
+
status: "running",
|
|
157
|
+
sessionIds: {},
|
|
158
|
+
resumeStats: { attempted: 0, succeeded: 0, fallbacks: 0 }
|
|
159
|
+
});
|
|
160
|
+
const sessionMgr = new SessionManager(db);
|
|
161
|
+
const session2 = sessionMgr.resolveActive("build");
|
|
162
|
+
sessionMgr.recordEvent({
|
|
163
|
+
sessionId: session2.id,
|
|
164
|
+
command: "build",
|
|
165
|
+
subcommand: "start",
|
|
166
|
+
promptPreview: `Build started: ${task}`
|
|
167
|
+
});
|
|
168
|
+
buildStore.create({ buildId, task, debateId, baselineRef: baselineRef ?? void 0 });
|
|
169
|
+
buildStore.updateWithEvent(
|
|
170
|
+
buildId,
|
|
171
|
+
{ debateId },
|
|
172
|
+
{ eventType: "debate_started", actor: "system", phase: "debate", payload: { task, debateId, baselineRef } }
|
|
173
|
+
);
|
|
174
|
+
const output = {
|
|
175
|
+
buildId,
|
|
176
|
+
debateId,
|
|
177
|
+
task,
|
|
178
|
+
baselineRef,
|
|
179
|
+
sessionId: session2.id,
|
|
180
|
+
maxRounds: options.maxRounds ?? 5,
|
|
181
|
+
status: "planning",
|
|
182
|
+
phase: "debate"
|
|
183
|
+
};
|
|
184
|
+
console.log(JSON.stringify(output, null, 2));
|
|
185
|
+
db.close();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
db?.close();
|
|
188
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function buildStatusCommand(buildId) {
|
|
193
|
+
let db;
|
|
194
|
+
try {
|
|
195
|
+
const dbPath = getDbPath();
|
|
196
|
+
db = openDatabase2(dbPath);
|
|
197
|
+
const store = new BuildStore(db);
|
|
198
|
+
const run = store.get(buildId);
|
|
199
|
+
if (!run) {
|
|
200
|
+
db.close();
|
|
201
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const events = store.getEvents(buildId);
|
|
205
|
+
const bugsFound = store.countEventsByType(buildId, "bug_found");
|
|
206
|
+
const fixesApplied = store.countEventsByType(buildId, "fix_completed");
|
|
207
|
+
const output = {
|
|
208
|
+
buildId: run.buildId,
|
|
209
|
+
task: run.task,
|
|
210
|
+
status: run.status,
|
|
211
|
+
phase: run.currentPhase,
|
|
212
|
+
loop: run.currentLoop,
|
|
213
|
+
debateId: run.debateId,
|
|
214
|
+
baselineRef: run.baselineRef,
|
|
215
|
+
planCodexSession: run.planCodexSession,
|
|
216
|
+
reviewCodexSession: run.reviewCodexSession,
|
|
217
|
+
planVersion: run.planVersion,
|
|
218
|
+
reviewCycles: run.reviewCycles,
|
|
219
|
+
bugsFound,
|
|
220
|
+
bugsFixed: fixesApplied,
|
|
221
|
+
totalEvents: events.length,
|
|
222
|
+
createdAt: new Date(run.createdAt).toISOString(),
|
|
223
|
+
updatedAt: new Date(run.updatedAt).toISOString(),
|
|
224
|
+
completedAt: run.completedAt ? new Date(run.completedAt).toISOString() : null,
|
|
225
|
+
recentEvents: events.slice(-10).map((e) => ({
|
|
226
|
+
seq: e.seq,
|
|
227
|
+
type: e.eventType,
|
|
228
|
+
actor: e.actor,
|
|
229
|
+
phase: e.phase,
|
|
230
|
+
loop: e.loopIndex,
|
|
231
|
+
tokens: e.tokensUsed,
|
|
232
|
+
time: new Date(e.createdAt).toISOString()
|
|
233
|
+
}))
|
|
234
|
+
};
|
|
235
|
+
console.log(JSON.stringify(output, null, 2));
|
|
236
|
+
db.close();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
db?.close();
|
|
239
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function buildListCommand(options) {
|
|
244
|
+
let db;
|
|
245
|
+
try {
|
|
246
|
+
const dbPath = getDbPath();
|
|
247
|
+
db = openDatabase2(dbPath);
|
|
248
|
+
const store = new BuildStore(db);
|
|
249
|
+
const builds = store.list({
|
|
250
|
+
status: options.status,
|
|
251
|
+
limit: options.limit ?? 20
|
|
252
|
+
});
|
|
253
|
+
const output = builds.map((b) => ({
|
|
254
|
+
buildId: b.buildId,
|
|
255
|
+
task: b.task,
|
|
256
|
+
status: b.status,
|
|
257
|
+
phase: b.phase,
|
|
258
|
+
loop: b.loop,
|
|
259
|
+
reviewCycles: b.reviewCycles,
|
|
260
|
+
createdAt: new Date(b.createdAt).toISOString(),
|
|
261
|
+
updatedAt: new Date(b.updatedAt).toISOString()
|
|
262
|
+
}));
|
|
263
|
+
console.log(JSON.stringify(output, null, 2));
|
|
264
|
+
db.close();
|
|
265
|
+
} catch (error) {
|
|
266
|
+
db?.close();
|
|
267
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function buildEventCommand(buildId, eventType, options) {
|
|
272
|
+
let db;
|
|
273
|
+
try {
|
|
274
|
+
const dbPath = getDbPath();
|
|
275
|
+
db = openDatabase2(dbPath);
|
|
276
|
+
const store = new BuildStore(db);
|
|
277
|
+
const run = store.get(buildId);
|
|
278
|
+
if (!run) {
|
|
279
|
+
db.close();
|
|
280
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
let payload;
|
|
284
|
+
if (!process.stdin.isTTY) {
|
|
285
|
+
const chunks = [];
|
|
286
|
+
for await (const chunk of process.stdin) {
|
|
287
|
+
chunks.push(chunk);
|
|
288
|
+
}
|
|
289
|
+
const input = Buffer.concat(chunks).toString("utf-8").trim();
|
|
290
|
+
if (input) {
|
|
291
|
+
try {
|
|
292
|
+
payload = JSON.parse(input);
|
|
293
|
+
} catch {
|
|
294
|
+
payload = { text: input };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const updates = {};
|
|
299
|
+
if (eventType === "plan_approved") {
|
|
300
|
+
updates.currentPhase = "plan_approved";
|
|
301
|
+
updates.status = "implementing";
|
|
302
|
+
updates.planVersion = run.planVersion + 1;
|
|
303
|
+
} else if (eventType === "impl_completed") {
|
|
304
|
+
updates.currentPhase = "review";
|
|
305
|
+
updates.status = "reviewing";
|
|
306
|
+
} else if (eventType === "review_verdict") {
|
|
307
|
+
const verdict = payload?.verdict;
|
|
308
|
+
if (verdict === "approved") {
|
|
309
|
+
updates.currentPhase = "done";
|
|
310
|
+
updates.status = "completed";
|
|
311
|
+
updates.completedAt = Date.now();
|
|
312
|
+
} else {
|
|
313
|
+
updates.currentPhase = "fix";
|
|
314
|
+
updates.status = "fixing";
|
|
315
|
+
updates.reviewCycles = run.reviewCycles + 1;
|
|
316
|
+
}
|
|
317
|
+
} else if (eventType === "fix_completed") {
|
|
318
|
+
updates.currentPhase = "review";
|
|
319
|
+
updates.status = "reviewing";
|
|
320
|
+
updates.currentLoop = run.currentLoop + 1;
|
|
321
|
+
}
|
|
322
|
+
store.updateWithEvent(
|
|
323
|
+
buildId,
|
|
324
|
+
updates,
|
|
325
|
+
{
|
|
326
|
+
eventType,
|
|
327
|
+
actor: "system",
|
|
328
|
+
phase: updates.currentPhase ?? run.currentPhase,
|
|
329
|
+
loopIndex: options.loop ?? run.currentLoop,
|
|
330
|
+
payload,
|
|
331
|
+
tokensUsed: options.tokens ?? 0
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
const updated = store.get(buildId);
|
|
335
|
+
console.log(JSON.stringify({
|
|
336
|
+
buildId,
|
|
337
|
+
eventType,
|
|
338
|
+
newStatus: updated?.status,
|
|
339
|
+
newPhase: updated?.currentPhase,
|
|
340
|
+
seq: updated?.lastEventSeq
|
|
341
|
+
}));
|
|
342
|
+
db.close();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
db?.close();
|
|
345
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function buildReviewCommand(buildId) {
|
|
350
|
+
let db;
|
|
351
|
+
try {
|
|
352
|
+
const dbPath = getDbPath();
|
|
353
|
+
db = openDatabase2(dbPath);
|
|
354
|
+
const buildStore = new BuildStore(db);
|
|
355
|
+
const config = loadConfig();
|
|
356
|
+
const projectDir = process.cwd();
|
|
357
|
+
const run = buildStore.get(buildId);
|
|
358
|
+
if (!run) {
|
|
359
|
+
db.close();
|
|
360
|
+
console.error(chalk3.red(`No build found with ID: ${buildId}`));
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
let diff = "";
|
|
364
|
+
if (run.baselineRef) {
|
|
365
|
+
try {
|
|
366
|
+
const tmpIndex = join2(projectDir, ".git", "codemoot-review-index");
|
|
367
|
+
try {
|
|
368
|
+
execSync(`git read-tree HEAD`, { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
369
|
+
execSync("git add -A", { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
370
|
+
diff = execSync(`git diff --cached ${run.baselineRef}`, { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
371
|
+
} finally {
|
|
372
|
+
try {
|
|
373
|
+
unlinkSync(tmpIndex);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(chalk3.red(`Failed to generate diff: ${err instanceof Error ? err.message : String(err)}`));
|
|
379
|
+
db.close();
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!diff.trim()) {
|
|
384
|
+
db.close();
|
|
385
|
+
console.error(chalk3.yellow("No changes detected against baseline."));
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
const { ModelRegistry: ModelRegistry8, CliAdapter: CliAdapterClass } = await import("@codemoot/core");
|
|
389
|
+
const registry = ModelRegistry8.fromConfig(config, projectDir);
|
|
390
|
+
const adapter = registry.getAdapter("codex-reviewer") ?? registry.getAdapter("codex-architect");
|
|
391
|
+
if (!adapter) {
|
|
392
|
+
db.close();
|
|
393
|
+
console.error(chalk3.red("No codex adapter found in config"));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const sessionMgr = new SessionManager(db);
|
|
397
|
+
const session2 = sessionMgr.resolveActive("build-review");
|
|
398
|
+
const overflowCheck = sessionMgr.preCallOverflowCheck(session2.id);
|
|
399
|
+
if (overflowCheck.rolled) {
|
|
400
|
+
console.error(chalk3.yellow(` ${overflowCheck.message}`));
|
|
401
|
+
}
|
|
402
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
403
|
+
const existingSession = overflowCheck.rolled ? void 0 : currentSession?.codexThreadId ?? run.reviewCodexSession ?? void 0;
|
|
404
|
+
const prompt = buildHandoffEnvelope({
|
|
405
|
+
command: "build-review",
|
|
406
|
+
task: `Review code changes for the task: "${run.task}"
|
|
407
|
+
|
|
408
|
+
GIT DIFF (against baseline ${run.baselineRef}):
|
|
409
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS)}
|
|
410
|
+
|
|
411
|
+
Review for:
|
|
412
|
+
1. Correctness \u2014 does the code work as intended?
|
|
413
|
+
2. Bugs \u2014 any logic errors, edge cases, or crashes?
|
|
414
|
+
3. Security \u2014 any vulnerabilities introduced?
|
|
415
|
+
4. Code quality \u2014 naming, structure, patterns
|
|
416
|
+
5. Completeness \u2014 does it fully implement the task?`,
|
|
417
|
+
resumed: Boolean(existingSession),
|
|
418
|
+
constraints: run.reviewCycles > 0 ? [`This is review cycle ${run.reviewCycles + 1}. Focus on whether prior issues were addressed.`] : void 0
|
|
419
|
+
});
|
|
420
|
+
const progress = createProgressCallbacks("build-review");
|
|
421
|
+
const result = await adapter.callWithResume(prompt, {
|
|
422
|
+
sessionId: existingSession,
|
|
423
|
+
timeout: 6e5,
|
|
424
|
+
...progress
|
|
425
|
+
});
|
|
426
|
+
if (result.sessionId) {
|
|
427
|
+
sessionMgr.updateThreadId(session2.id, result.sessionId);
|
|
428
|
+
}
|
|
429
|
+
sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
|
|
430
|
+
sessionMgr.recordEvent({
|
|
431
|
+
sessionId: session2.id,
|
|
432
|
+
command: "build",
|
|
433
|
+
subcommand: "review",
|
|
434
|
+
promptPreview: `Build review for ${buildId}: ${run.task}`,
|
|
435
|
+
responsePreview: result.text.slice(0, 500),
|
|
436
|
+
promptFull: prompt,
|
|
437
|
+
responseFull: result.text,
|
|
438
|
+
usageJson: JSON.stringify(result.usage),
|
|
439
|
+
durationMs: result.durationMs,
|
|
440
|
+
codexThreadId: result.sessionId
|
|
441
|
+
});
|
|
442
|
+
const tail = result.text.slice(-500);
|
|
443
|
+
const verdictMatch = tail.match(/^(?:-\s*)?VERDICT:\s*(APPROVED|NEEDS_REVISION)/m);
|
|
444
|
+
const approved = verdictMatch?.[1] === "APPROVED";
|
|
445
|
+
buildStore.updateWithEvent(
|
|
446
|
+
buildId,
|
|
447
|
+
{
|
|
448
|
+
reviewCodexSession: result.sessionId ?? existingSession,
|
|
449
|
+
reviewCycles: run.reviewCycles + 1
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
eventType: "review_verdict",
|
|
453
|
+
actor: "codex",
|
|
454
|
+
phase: "review",
|
|
455
|
+
loopIndex: run.currentLoop,
|
|
456
|
+
payload: {
|
|
457
|
+
verdict: approved ? "approved" : "needs_revision",
|
|
458
|
+
response: result.text.slice(0, REVIEW_TEXT_MAX_CHARS),
|
|
459
|
+
sessionId: result.sessionId,
|
|
460
|
+
resumed: existingSession ? result.sessionId === existingSession : false
|
|
461
|
+
},
|
|
462
|
+
codexThreadId: result.sessionId,
|
|
463
|
+
tokensUsed: result.usage.totalTokens
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
if (approved) {
|
|
467
|
+
buildStore.updateWithEvent(
|
|
468
|
+
buildId,
|
|
469
|
+
{ currentPhase: "done", status: "completed", completedAt: Date.now() },
|
|
470
|
+
{ eventType: "phase_transition", actor: "system", phase: "done", payload: { reason: "review_approved" } }
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
buildStore.updateWithEvent(
|
|
474
|
+
buildId,
|
|
475
|
+
{ currentPhase: "fix", status: "fixing" },
|
|
476
|
+
{ eventType: "phase_transition", actor: "system", phase: "fix", payload: { reason: "review_needs_revision" } }
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const output = {
|
|
480
|
+
buildId,
|
|
481
|
+
review: result.text,
|
|
482
|
+
verdict: approved ? "approved" : "needs_revision",
|
|
483
|
+
sessionId: result.sessionId,
|
|
484
|
+
resumed: existingSession ? result.sessionId === existingSession : false,
|
|
485
|
+
tokens: result.usage,
|
|
486
|
+
durationMs: result.durationMs
|
|
487
|
+
};
|
|
488
|
+
console.log(JSON.stringify(output, null, 2));
|
|
489
|
+
db.close();
|
|
490
|
+
} catch (error) {
|
|
491
|
+
db?.close();
|
|
492
|
+
console.error(chalk3.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/debate.ts
|
|
498
|
+
import {
|
|
499
|
+
DebateStore as DebateStore2,
|
|
500
|
+
MessageStore,
|
|
501
|
+
ModelRegistry,
|
|
502
|
+
SessionManager as SessionManager2,
|
|
503
|
+
buildReconstructionPrompt,
|
|
504
|
+
generateId as generateId2,
|
|
505
|
+
getTokenBudgetStatus,
|
|
506
|
+
loadConfig as loadConfig2,
|
|
507
|
+
openDatabase as openDatabase3,
|
|
508
|
+
parseDebateVerdict,
|
|
509
|
+
preflightTokenCheck
|
|
510
|
+
} from "@codemoot/core";
|
|
511
|
+
import chalk4 from "chalk";
|
|
512
|
+
async function debateStartCommand(topic, options) {
|
|
513
|
+
let db;
|
|
514
|
+
try {
|
|
515
|
+
const debateId = generateId2();
|
|
516
|
+
const dbPath = getDbPath();
|
|
517
|
+
db = openDatabase3(dbPath);
|
|
518
|
+
const store = new DebateStore2(db);
|
|
519
|
+
store.upsert({ debateId, role: "proposer", status: "active" });
|
|
520
|
+
store.upsert({ debateId, role: "critic", status: "active" });
|
|
521
|
+
store.saveState(debateId, "proposer", {
|
|
522
|
+
debateId,
|
|
523
|
+
question: topic,
|
|
524
|
+
models: ["codex-architect", "codex-reviewer"],
|
|
525
|
+
round: 0,
|
|
526
|
+
turn: 0,
|
|
527
|
+
thread: [],
|
|
528
|
+
runningSummary: "",
|
|
529
|
+
stanceHistory: [],
|
|
530
|
+
usage: { totalPromptTokens: 0, totalCompletionTokens: 0, totalCalls: 0, startedAt: Date.now() },
|
|
531
|
+
status: "running",
|
|
532
|
+
sessionIds: {},
|
|
533
|
+
resumeStats: { attempted: 0, succeeded: 0, fallbacks: 0 },
|
|
534
|
+
maxRounds: options.maxRounds ?? 5
|
|
535
|
+
});
|
|
536
|
+
const output = {
|
|
537
|
+
debateId,
|
|
538
|
+
topic,
|
|
539
|
+
maxRounds: options.maxRounds ?? 5,
|
|
540
|
+
status: "started"
|
|
541
|
+
};
|
|
542
|
+
console.log(JSON.stringify(output, null, 2));
|
|
543
|
+
db.close();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
db?.close();
|
|
546
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function debateTurnCommand(debateId, prompt, options) {
|
|
551
|
+
let db;
|
|
552
|
+
try {
|
|
553
|
+
const dbPath = getDbPath();
|
|
554
|
+
db = openDatabase3(dbPath);
|
|
555
|
+
const store = new DebateStore2(db);
|
|
556
|
+
const msgStore = new MessageStore(db);
|
|
557
|
+
const config = loadConfig2();
|
|
558
|
+
const projectDir = process.cwd();
|
|
559
|
+
const registry = ModelRegistry.fromConfig(config, projectDir);
|
|
560
|
+
const criticRow = store.get(debateId, "critic");
|
|
561
|
+
if (!criticRow) {
|
|
562
|
+
db.close();
|
|
563
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
if (criticRow.status === "completed") {
|
|
567
|
+
db.close();
|
|
568
|
+
console.error(chalk4.red(`Debate ${debateId} is already completed. Start a new debate to continue discussion.`));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
572
|
+
if (!adapter) {
|
|
573
|
+
db.close();
|
|
574
|
+
console.error(chalk4.red("No codex adapter found in config. Available: codex-reviewer or codex-architect"));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const rawRound = options.round ?? criticRow.round + 1;
|
|
578
|
+
const newRound = Number.isFinite(rawRound) && rawRound > 0 ? rawRound : criticRow.round + 1;
|
|
579
|
+
const rawTimeout = options.timeout ?? 600;
|
|
580
|
+
const timeout = (Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 600) * 1e3;
|
|
581
|
+
const proposerStateForLimit = store.loadState(debateId, "proposer");
|
|
582
|
+
const rawMax = proposerStateForLimit?.maxRounds ?? 5;
|
|
583
|
+
const maxRounds = Number.isFinite(rawMax) && rawMax > 0 ? rawMax : 5;
|
|
584
|
+
if (newRound > maxRounds) {
|
|
585
|
+
console.error(chalk4.red(`Round ${newRound} exceeds max rounds (${maxRounds}). Complete or increase limit.`));
|
|
586
|
+
db.close();
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
const existing = msgStore.getByRound(debateId, newRound, "critic");
|
|
590
|
+
if (existing?.status === "completed") {
|
|
591
|
+
if (criticRow.round < newRound) {
|
|
592
|
+
store.upsert({
|
|
593
|
+
debateId,
|
|
594
|
+
role: "critic",
|
|
595
|
+
codexSessionId: existing.sessionId ?? criticRow.codexSessionId ?? void 0,
|
|
596
|
+
round: newRound,
|
|
597
|
+
status: "active"
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const output = {
|
|
601
|
+
debateId,
|
|
602
|
+
round: newRound,
|
|
603
|
+
response: existing.responseText,
|
|
604
|
+
sessionId: existing.sessionId,
|
|
605
|
+
resumed: false,
|
|
606
|
+
cached: true,
|
|
607
|
+
usage: existing.usageJson ? (() => {
|
|
608
|
+
try {
|
|
609
|
+
return JSON.parse(existing.usageJson);
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
})() : null,
|
|
614
|
+
durationMs: existing.durationMs
|
|
615
|
+
};
|
|
616
|
+
console.log(JSON.stringify(output, null, 2));
|
|
617
|
+
db.close();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const staleThreshold = timeout + 6e4;
|
|
621
|
+
const recovered = msgStore.recoverStaleForDebate(debateId, staleThreshold);
|
|
622
|
+
if (recovered > 0) {
|
|
623
|
+
console.error(chalk4.yellow(` Recovered ${recovered} stale message(s) from prior crash.`));
|
|
624
|
+
}
|
|
625
|
+
const current = msgStore.getByRound(debateId, newRound, "critic");
|
|
626
|
+
let msgId;
|
|
627
|
+
if (current) {
|
|
628
|
+
msgId = current.id;
|
|
629
|
+
if (current.status === "failed" || current.status === "queued") {
|
|
630
|
+
msgStore.updatePrompt(msgId, prompt);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
msgId = msgStore.insertQueued({
|
|
634
|
+
debateId,
|
|
635
|
+
round: newRound,
|
|
636
|
+
role: "critic",
|
|
637
|
+
bridge: "codex",
|
|
638
|
+
model: adapter.modelId ?? "codex",
|
|
639
|
+
promptText: prompt
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (!msgStore.markRunning(msgId)) {
|
|
643
|
+
const recheckRow = msgStore.getByRound(debateId, newRound, "critic");
|
|
644
|
+
if (recheckRow?.status === "completed") {
|
|
645
|
+
const output = {
|
|
646
|
+
debateId,
|
|
647
|
+
round: newRound,
|
|
648
|
+
response: recheckRow.responseText,
|
|
649
|
+
sessionId: recheckRow.sessionId,
|
|
650
|
+
resumed: false,
|
|
651
|
+
cached: true,
|
|
652
|
+
usage: recheckRow.usageJson ? (() => {
|
|
653
|
+
try {
|
|
654
|
+
return JSON.parse(recheckRow.usageJson);
|
|
655
|
+
} catch {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
})() : null,
|
|
659
|
+
durationMs: recheckRow.durationMs
|
|
660
|
+
};
|
|
661
|
+
console.log(JSON.stringify(output, null, 2));
|
|
662
|
+
db.close();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
db.close();
|
|
666
|
+
console.error(chalk4.red(`Cannot transition message ${msgId} to running (current status: ${recheckRow?.status})`));
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
const sessionMgr = new SessionManager2(db);
|
|
670
|
+
const unifiedSession = sessionMgr.resolveActive("debate");
|
|
671
|
+
let existingSessionId = unifiedSession.codexThreadId ?? criticRow.codexSessionId ?? void 0;
|
|
672
|
+
const attemptedResume = existingSessionId != null;
|
|
673
|
+
const completedHistory = msgStore.getHistory(debateId).filter((m) => m.status === "completed");
|
|
674
|
+
const maxContext = adapter.capabilities.maxContextTokens;
|
|
675
|
+
const preflight = preflightTokenCheck(completedHistory, prompt, maxContext);
|
|
676
|
+
if (preflight.shouldStop) {
|
|
677
|
+
console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}% (${preflight.totalTokensUsed}/${maxContext}). Consider completing this debate.`));
|
|
678
|
+
} else if (preflight.shouldSummarize) {
|
|
679
|
+
console.error(chalk4.yellow(` Token budget at ${Math.round(preflight.utilizationRatio * 100)}%. Older rounds will be summarized on resume failure.`));
|
|
680
|
+
}
|
|
681
|
+
const overflowCheck = sessionMgr.preCallOverflowCheck(unifiedSession.id);
|
|
682
|
+
if (overflowCheck.rolled) {
|
|
683
|
+
console.error(chalk4.yellow(` ${overflowCheck.message}`));
|
|
684
|
+
existingSessionId = void 0;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const progress = createProgressCallbacks("debate");
|
|
688
|
+
let result = await adapter.callWithResume(prompt, {
|
|
689
|
+
sessionId: existingSessionId,
|
|
690
|
+
timeout,
|
|
691
|
+
...progress
|
|
692
|
+
});
|
|
693
|
+
const resumed = attemptedResume && result.sessionId === existingSessionId;
|
|
694
|
+
const resumeFailed = attemptedResume && !resumed;
|
|
695
|
+
if (resumeFailed && result.text.length < 50) {
|
|
696
|
+
console.error(chalk4.yellow(" Resume failed with minimal response. Reconstructing from ledger..."));
|
|
697
|
+
const history = msgStore.getHistory(debateId);
|
|
698
|
+
const reconstructed = buildReconstructionPrompt(history, prompt);
|
|
699
|
+
result = await adapter.callWithResume(reconstructed, {
|
|
700
|
+
timeout,
|
|
701
|
+
...progress
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
if (result.text.length < 200 && (result.durationMs ?? 0) > 6e4) {
|
|
705
|
+
console.error(chalk4.yellow(` Warning: GPT response is only ${result.text.length} chars after ${Math.round((result.durationMs ?? 0) / 1e3)}s \u2014 possible output truncation (codex may have spent its turn on tool calls).`));
|
|
706
|
+
}
|
|
707
|
+
const verdict = parseDebateVerdict(result.text);
|
|
708
|
+
const completed = msgStore.markCompleted(msgId, {
|
|
709
|
+
responseText: result.text,
|
|
710
|
+
verdict,
|
|
711
|
+
usageJson: JSON.stringify(result.usage),
|
|
712
|
+
durationMs: result.durationMs ?? 0,
|
|
713
|
+
sessionId: result.sessionId ?? null
|
|
714
|
+
});
|
|
715
|
+
if (!completed) {
|
|
716
|
+
console.error(chalk4.red(`Message ${msgId} ledger transition to completed failed (possible concurrent invocation or state drift).`));
|
|
717
|
+
db.close();
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
if (resumeFailed) {
|
|
721
|
+
store.incrementResumeFailCount(debateId, "critic");
|
|
722
|
+
}
|
|
723
|
+
if (result.sessionId) {
|
|
724
|
+
sessionMgr.updateThreadId(unifiedSession.id, result.sessionId);
|
|
725
|
+
}
|
|
726
|
+
sessionMgr.addUsageFromResult(unifiedSession.id, result.usage, prompt, result.text);
|
|
727
|
+
sessionMgr.recordEvent({
|
|
728
|
+
sessionId: unifiedSession.id,
|
|
729
|
+
command: "debate",
|
|
730
|
+
subcommand: "turn",
|
|
731
|
+
promptPreview: prompt.slice(0, 500),
|
|
732
|
+
responsePreview: result.text.slice(0, 500),
|
|
733
|
+
promptFull: prompt,
|
|
734
|
+
responseFull: result.text,
|
|
735
|
+
usageJson: JSON.stringify(result.usage),
|
|
736
|
+
durationMs: result.durationMs,
|
|
737
|
+
codexThreadId: result.sessionId
|
|
738
|
+
});
|
|
739
|
+
store.upsert({
|
|
740
|
+
debateId,
|
|
741
|
+
role: "critic",
|
|
742
|
+
codexSessionId: result.sessionId ?? existingSessionId,
|
|
743
|
+
round: newRound,
|
|
744
|
+
status: "active"
|
|
745
|
+
});
|
|
746
|
+
const proposerState = store.loadState(debateId, "proposer");
|
|
747
|
+
if (proposerState) {
|
|
748
|
+
const stats = proposerState.resumeStats ?? { attempted: 0, succeeded: 0, fallbacks: 0 };
|
|
749
|
+
if (attemptedResume) stats.attempted++;
|
|
750
|
+
if (resumed) stats.succeeded++;
|
|
751
|
+
if (resumeFailed) stats.fallbacks++;
|
|
752
|
+
proposerState.resumeStats = stats;
|
|
753
|
+
store.saveState(debateId, "proposer", proposerState);
|
|
754
|
+
}
|
|
755
|
+
const output = {
|
|
756
|
+
debateId,
|
|
757
|
+
round: newRound,
|
|
758
|
+
response: result.text,
|
|
759
|
+
sessionId: result.sessionId,
|
|
760
|
+
resumed,
|
|
761
|
+
cached: false,
|
|
762
|
+
stance: verdict.stance,
|
|
763
|
+
usage: result.usage,
|
|
764
|
+
durationMs: result.durationMs
|
|
765
|
+
};
|
|
766
|
+
const stanceColor = verdict.stance === "SUPPORT" ? chalk4.green : verdict.stance === "OPPOSE" ? chalk4.red : chalk4.yellow;
|
|
767
|
+
console.error(stanceColor(`
|
|
768
|
+
Round ${newRound} \u2014 Stance: ${verdict.stance}`));
|
|
769
|
+
const previewLines = result.text.split("\n").filter((l) => l.trim().length > 10).slice(0, 3);
|
|
770
|
+
for (const line of previewLines) {
|
|
771
|
+
console.error(chalk4.dim(` ${line.trim().slice(0, 120)}`));
|
|
772
|
+
}
|
|
773
|
+
console.error(chalk4.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Tokens: ${result.usage?.totalTokens ?? "?"} | Resumed: ${resumed}`));
|
|
774
|
+
console.log(JSON.stringify(output, null, 2));
|
|
775
|
+
} catch (error) {
|
|
776
|
+
msgStore.markFailed(msgId, error instanceof Error ? error.message : String(error));
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
db.close();
|
|
780
|
+
} catch (error) {
|
|
781
|
+
db?.close();
|
|
782
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function debateStatusCommand(debateId) {
|
|
787
|
+
let db;
|
|
788
|
+
try {
|
|
789
|
+
const dbPath = getDbPath();
|
|
790
|
+
db = openDatabase3(dbPath);
|
|
791
|
+
const store = new DebateStore2(db);
|
|
792
|
+
const turns = store.getByDebateId(debateId);
|
|
793
|
+
if (turns.length === 0) {
|
|
794
|
+
db.close();
|
|
795
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const state = store.loadState(debateId, "proposer");
|
|
799
|
+
const msgStore = new MessageStore(db);
|
|
800
|
+
const msgHistory = msgStore.getHistory(debateId);
|
|
801
|
+
const tokenStatus = getTokenBudgetStatus(msgHistory, 4e5);
|
|
802
|
+
const output = {
|
|
803
|
+
debateId,
|
|
804
|
+
topic: state?.question ?? "unknown",
|
|
805
|
+
status: turns.some((t) => t.status === "active") ? "active" : turns[0].status,
|
|
806
|
+
round: Math.max(...turns.map((t) => t.round)),
|
|
807
|
+
participants: turns.map((t) => ({
|
|
808
|
+
role: t.role,
|
|
809
|
+
codexSessionId: t.codexSessionId,
|
|
810
|
+
round: t.round,
|
|
811
|
+
status: t.status,
|
|
812
|
+
resumeFailCount: t.resumeFailCount,
|
|
813
|
+
lastActivity: new Date(t.lastActivityAt).toISOString()
|
|
814
|
+
})),
|
|
815
|
+
resumeStats: state?.resumeStats ?? null,
|
|
816
|
+
tokenBudget: {
|
|
817
|
+
used: tokenStatus.totalTokensUsed,
|
|
818
|
+
max: tokenStatus.maxContextTokens,
|
|
819
|
+
utilization: `${Math.round(tokenStatus.utilizationRatio * 100)}%`,
|
|
820
|
+
messages: msgHistory.length
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
console.log(JSON.stringify(output, null, 2));
|
|
824
|
+
db.close();
|
|
825
|
+
} catch (error) {
|
|
826
|
+
db?.close();
|
|
827
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function debateListCommand(options) {
|
|
832
|
+
let db;
|
|
833
|
+
try {
|
|
834
|
+
const dbPath = getDbPath();
|
|
835
|
+
db = openDatabase3(dbPath);
|
|
836
|
+
const store = new DebateStore2(db);
|
|
837
|
+
const desiredLimit = options.limit ?? 20;
|
|
838
|
+
const rows = store.list({
|
|
839
|
+
status: options.status,
|
|
840
|
+
limit: desiredLimit * 3
|
|
841
|
+
});
|
|
842
|
+
const debates = /* @__PURE__ */ new Map();
|
|
843
|
+
for (const row of rows) {
|
|
844
|
+
const existing = debates.get(row.debateId) ?? [];
|
|
845
|
+
existing.push(row);
|
|
846
|
+
debates.set(row.debateId, existing);
|
|
847
|
+
}
|
|
848
|
+
const output = Array.from(debates.entries()).slice(0, desiredLimit).map(([id, turns]) => {
|
|
849
|
+
const state = store.loadState(id, "proposer");
|
|
850
|
+
return {
|
|
851
|
+
debateId: id,
|
|
852
|
+
topic: state?.question ?? "unknown",
|
|
853
|
+
status: turns.some((t) => t.status === "active") ? "active" : turns[0].status,
|
|
854
|
+
round: Math.max(...turns.map((t) => t.round)),
|
|
855
|
+
lastActivity: new Date(Math.max(...turns.map((t) => t.lastActivityAt))).toISOString()
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
console.log(JSON.stringify(output, null, 2));
|
|
859
|
+
db.close();
|
|
860
|
+
} catch (error) {
|
|
861
|
+
db?.close();
|
|
862
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function debateHistoryCommand(debateId) {
|
|
867
|
+
let db;
|
|
868
|
+
try {
|
|
869
|
+
const dbPath = getDbPath();
|
|
870
|
+
db = openDatabase3(dbPath);
|
|
871
|
+
const msgStore = new MessageStore(db);
|
|
872
|
+
const history = msgStore.getHistory(debateId);
|
|
873
|
+
if (history.length === 0) {
|
|
874
|
+
const debateStore = new DebateStore2(db);
|
|
875
|
+
const turns = debateStore.getByDebateId(debateId);
|
|
876
|
+
if (turns.length > 0) {
|
|
877
|
+
console.error(chalk4.yellow(`No messages stored for debate ${debateId} \u2014 this debate predates message persistence (schema v4). Only metadata is available via "debate status".`));
|
|
878
|
+
} else {
|
|
879
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
880
|
+
}
|
|
881
|
+
db.close();
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
const tokenStatus = getTokenBudgetStatus(history, 4e5);
|
|
885
|
+
const output = {
|
|
886
|
+
debateId,
|
|
887
|
+
messageCount: history.length,
|
|
888
|
+
tokenBudget: {
|
|
889
|
+
used: tokenStatus.totalTokensUsed,
|
|
890
|
+
max: tokenStatus.maxContextTokens,
|
|
891
|
+
utilization: `${Math.round(tokenStatus.utilizationRatio * 100)}%`
|
|
892
|
+
},
|
|
893
|
+
messages: history.map((m) => ({
|
|
894
|
+
round: m.round,
|
|
895
|
+
role: m.role,
|
|
896
|
+
bridge: m.bridge,
|
|
897
|
+
model: m.model,
|
|
898
|
+
status: m.status,
|
|
899
|
+
stance: m.stance,
|
|
900
|
+
confidence: m.confidence,
|
|
901
|
+
durationMs: m.durationMs,
|
|
902
|
+
sessionId: m.sessionId,
|
|
903
|
+
promptPreview: m.promptText.slice(0, 200),
|
|
904
|
+
responsePreview: m.responseText?.slice(0, 200) ?? null,
|
|
905
|
+
error: m.error,
|
|
906
|
+
createdAt: new Date(m.createdAt).toISOString(),
|
|
907
|
+
completedAt: m.completedAt ? new Date(m.completedAt).toISOString() : null
|
|
908
|
+
}))
|
|
909
|
+
};
|
|
910
|
+
console.log(JSON.stringify(output, null, 2));
|
|
911
|
+
db.close();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
db?.close();
|
|
914
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function debateCompleteCommand(debateId) {
|
|
919
|
+
let db;
|
|
920
|
+
try {
|
|
921
|
+
const dbPath = getDbPath();
|
|
922
|
+
db = openDatabase3(dbPath);
|
|
923
|
+
const store = new DebateStore2(db);
|
|
924
|
+
const turns = store.getByDebateId(debateId);
|
|
925
|
+
if (turns.length === 0) {
|
|
926
|
+
console.error(chalk4.red(`No debate found with ID: ${debateId}`));
|
|
927
|
+
db.close();
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
const completeTransaction = db.transaction(() => {
|
|
931
|
+
store.updateStatus(debateId, "proposer", "completed");
|
|
932
|
+
store.updateStatus(debateId, "critic", "completed");
|
|
933
|
+
});
|
|
934
|
+
completeTransaction();
|
|
935
|
+
console.log(JSON.stringify({ debateId, status: "completed" }));
|
|
936
|
+
db.close();
|
|
937
|
+
} catch (error) {
|
|
938
|
+
db?.close();
|
|
939
|
+
console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/commands/cleanup.ts
|
|
945
|
+
import {
|
|
946
|
+
BuildStore as BuildStore2,
|
|
947
|
+
JobStore,
|
|
948
|
+
ModelRegistry as ModelRegistry2,
|
|
949
|
+
SessionManager as SessionManager3,
|
|
950
|
+
buildHandoffEnvelope as buildHandoffEnvelope2,
|
|
951
|
+
computeThreeWayStats,
|
|
952
|
+
createIgnoreFilter,
|
|
953
|
+
generateId as generateId3,
|
|
954
|
+
hostFindingsSchema,
|
|
955
|
+
loadConfig as loadConfig3,
|
|
956
|
+
mergeThreeWay,
|
|
957
|
+
openDatabase as openDatabase4,
|
|
958
|
+
recalculateConfidenceStats,
|
|
959
|
+
runAllScanners
|
|
960
|
+
} from "@codemoot/core";
|
|
961
|
+
import chalk5 from "chalk";
|
|
962
|
+
import { readFileSync } from "fs";
|
|
963
|
+
async function cleanupCommand(path, options) {
|
|
964
|
+
let db;
|
|
965
|
+
try {
|
|
966
|
+
const { resolve: resolve2 } = await import("path");
|
|
967
|
+
const projectDir = resolve2(path);
|
|
968
|
+
if (options.background) {
|
|
969
|
+
const bgDb = openDatabase4(getDbPath());
|
|
970
|
+
const jobStore = new JobStore(bgDb);
|
|
971
|
+
const jobId = jobStore.enqueue({
|
|
972
|
+
type: "cleanup",
|
|
973
|
+
payload: { path: projectDir, scope: options.scope, timeout: options.timeout, maxDisputes: options.maxDisputes, hostFindings: options.hostFindings, output: options.output }
|
|
974
|
+
});
|
|
975
|
+
console.log(JSON.stringify({ jobId, status: "queued", message: "Cleanup enqueued. Check with: codemoot jobs status " + jobId }));
|
|
976
|
+
bgDb.close();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const scopes = options.scope === "all" ? ["deps", "unused-exports", "hardcoded", "duplicates", "deadcode", "security", "near-duplicates", "anti-patterns"] : [options.scope];
|
|
980
|
+
const dbPath = getDbPath();
|
|
981
|
+
db = openDatabase4(dbPath);
|
|
982
|
+
const buildStore = new BuildStore2(db);
|
|
983
|
+
const buildId = generateId3();
|
|
984
|
+
const startTime = Date.now();
|
|
985
|
+
buildStore.create({ buildId, task: `cleanup:${options.scope}` });
|
|
986
|
+
console.error(chalk5.cyan(`Cleanup scan started (ID: ${buildId})`));
|
|
987
|
+
console.error(chalk5.cyan(`Scopes: ${scopes.join(", ")}`));
|
|
988
|
+
console.error(chalk5.cyan(`Project: ${projectDir}`));
|
|
989
|
+
let hostFindings = [];
|
|
990
|
+
if (options.hostFindings) {
|
|
991
|
+
console.error(chalk5.cyan(`Host findings: ${options.hostFindings}`));
|
|
992
|
+
try {
|
|
993
|
+
const raw = readFileSync(options.hostFindings, "utf-8");
|
|
994
|
+
const parsed = JSON.parse(raw);
|
|
995
|
+
const validated = hostFindingsSchema.parse(parsed);
|
|
996
|
+
hostFindings = validated.map((f) => ({
|
|
997
|
+
key: `${f.scope}:${f.file}:${f.symbol}`,
|
|
998
|
+
scope: f.scope,
|
|
999
|
+
confidence: f.confidence,
|
|
1000
|
+
file: f.file,
|
|
1001
|
+
line: f.line,
|
|
1002
|
+
description: f.description,
|
|
1003
|
+
recommendation: f.recommendation,
|
|
1004
|
+
deterministicEvidence: [],
|
|
1005
|
+
semanticEvidence: [],
|
|
1006
|
+
hostEvidence: [`Host: ${f.description}`],
|
|
1007
|
+
sources: ["host"],
|
|
1008
|
+
disputed: false
|
|
1009
|
+
}));
|
|
1010
|
+
console.error(chalk5.dim(` [host] Loaded ${hostFindings.length} findings`));
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
console.error(chalk5.red(`Failed to load host findings: ${err instanceof Error ? err.message : String(err)}`));
|
|
1013
|
+
db.close();
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const ig = createIgnoreFilter(projectDir, { skipGitignore: options.noGitignore });
|
|
1018
|
+
console.error(chalk5.yellow("\nPhase 1: Scanning (parallel)..."));
|
|
1019
|
+
let codexAdapter = null;
|
|
1020
|
+
try {
|
|
1021
|
+
const config = loadConfig3();
|
|
1022
|
+
const registry = ModelRegistry2.fromConfig(config, projectDir);
|
|
1023
|
+
try {
|
|
1024
|
+
codexAdapter = registry.getAdapter("codex-reviewer");
|
|
1025
|
+
} catch {
|
|
1026
|
+
try {
|
|
1027
|
+
codexAdapter = registry.getAdapter("codex-architect");
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
const sessionMgr = new SessionManager3(db);
|
|
1034
|
+
const session2 = sessionMgr.resolveActive("cleanup");
|
|
1035
|
+
const overflowCheck = sessionMgr.preCallOverflowCheck(session2.id);
|
|
1036
|
+
if (overflowCheck.rolled) {
|
|
1037
|
+
console.error(chalk5.yellow(` ${overflowCheck.message}`));
|
|
1038
|
+
}
|
|
1039
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
1040
|
+
const sessionThreadId = currentSession?.codexThreadId ?? void 0;
|
|
1041
|
+
const [deterministicFindings, semanticFindings] = await Promise.all([
|
|
1042
|
+
Promise.resolve().then(() => {
|
|
1043
|
+
console.error(chalk5.dim(" [deterministic] Starting..."));
|
|
1044
|
+
const findings = runAllScanners(projectDir, scopes, ig);
|
|
1045
|
+
console.error(chalk5.dim(` [deterministic] Done: ${findings.length} findings`));
|
|
1046
|
+
return findings;
|
|
1047
|
+
}),
|
|
1048
|
+
runCodexScan(codexAdapter, projectDir, scopes, options.timeout, sessionMgr, session2.id, sessionThreadId)
|
|
1049
|
+
]);
|
|
1050
|
+
buildStore.updateWithEvent(
|
|
1051
|
+
buildId,
|
|
1052
|
+
{ status: "reviewing", currentPhase: "review", metadata: { cleanupPhase: "scan" } },
|
|
1053
|
+
{
|
|
1054
|
+
eventType: "scan_completed",
|
|
1055
|
+
actor: "system",
|
|
1056
|
+
phase: "review",
|
|
1057
|
+
payload: {
|
|
1058
|
+
deterministicCount: deterministicFindings.length,
|
|
1059
|
+
semanticCount: semanticFindings.length,
|
|
1060
|
+
hostCount: hostFindings.length
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
console.error(chalk5.yellow("\nPhase 2: Merging findings (3-way)..."));
|
|
1065
|
+
const mergedFindings = mergeThreeWay(deterministicFindings, semanticFindings, hostFindings);
|
|
1066
|
+
const stats = computeThreeWayStats(deterministicFindings, semanticFindings, hostFindings, mergedFindings);
|
|
1067
|
+
console.error(chalk5.dim(` Merged: ${mergedFindings.length} total, ${stats.agreed} agreed, ${stats.disputed} disputed`));
|
|
1068
|
+
if (hostFindings.length > 0) {
|
|
1069
|
+
console.error(chalk5.dim(` Sources: deterministic=${stats.deterministic}, codex=${stats.semantic}, host=${stats.host}`));
|
|
1070
|
+
}
|
|
1071
|
+
buildStore.updateWithEvent(
|
|
1072
|
+
buildId,
|
|
1073
|
+
{ metadata: { cleanupPhase: "merge" } },
|
|
1074
|
+
{
|
|
1075
|
+
eventType: "merge_completed",
|
|
1076
|
+
actor: "system",
|
|
1077
|
+
phase: "review",
|
|
1078
|
+
payload: { totalFindings: mergedFindings.length, ...stats }
|
|
1079
|
+
}
|
|
1080
|
+
);
|
|
1081
|
+
const hasAdjudicatable = stats.disputed > 0 || mergedFindings.some((f) => f.confidence === "medium");
|
|
1082
|
+
if (codexAdapter && hasAdjudicatable && options.maxDisputes > 0) {
|
|
1083
|
+
console.error(chalk5.yellow(`
|
|
1084
|
+
Phase 2.5: Adjudicating up to ${options.maxDisputes} disputed findings...`));
|
|
1085
|
+
await adjudicateFindings(codexAdapter, mergedFindings, options.maxDisputes, stats);
|
|
1086
|
+
buildStore.updateWithEvent(
|
|
1087
|
+
buildId,
|
|
1088
|
+
{ metadata: { cleanupPhase: "adjudicate" } },
|
|
1089
|
+
{
|
|
1090
|
+
eventType: "adjudicated",
|
|
1091
|
+
actor: "codex",
|
|
1092
|
+
phase: "review",
|
|
1093
|
+
payload: { adjudicated: stats.adjudicated }
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
const durationMs = Date.now() - startTime;
|
|
1098
|
+
const actionableScopes = /* @__PURE__ */ new Set(["deps", "unused-exports", "hardcoded"]);
|
|
1099
|
+
const actionableCount = mergedFindings.filter(
|
|
1100
|
+
(f) => actionableScopes.has(f.scope) && (f.confidence === "high" || f.confidence === "medium")
|
|
1101
|
+
).length;
|
|
1102
|
+
const report = {
|
|
1103
|
+
scopes,
|
|
1104
|
+
findings: mergedFindings.sort((a, b) => {
|
|
1105
|
+
const confOrder = { high: 0, medium: 1, low: 2 };
|
|
1106
|
+
const confDiff = confOrder[a.confidence] - confOrder[b.confidence];
|
|
1107
|
+
if (confDiff !== 0) return confDiff;
|
|
1108
|
+
return a.key.localeCompare(b.key);
|
|
1109
|
+
}),
|
|
1110
|
+
stats,
|
|
1111
|
+
durationMs
|
|
1112
|
+
};
|
|
1113
|
+
buildStore.updateWithEvent(
|
|
1114
|
+
buildId,
|
|
1115
|
+
{ status: "completed", currentPhase: "done", completedAt: Date.now() },
|
|
1116
|
+
{
|
|
1117
|
+
eventType: "phase_transition",
|
|
1118
|
+
actor: "system",
|
|
1119
|
+
phase: "done",
|
|
1120
|
+
payload: {
|
|
1121
|
+
totalFindings: mergedFindings.length,
|
|
1122
|
+
actionable: actionableCount,
|
|
1123
|
+
reportOnly: mergedFindings.length - actionableCount
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
console.error(chalk5.green(`
|
|
1128
|
+
Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
|
|
1129
|
+
console.error(chalk5.green(`Build ID: ${buildId}`));
|
|
1130
|
+
console.error(` Actionable: ${chalk5.red(String(actionableCount))}`);
|
|
1131
|
+
console.error(` Report-only: ${chalk5.dim(String(mergedFindings.length - actionableCount))}`);
|
|
1132
|
+
console.error(` High: ${stats.highConfidence} | Medium: ${stats.mediumConfidence} | Low: ${stats.lowConfidence}`);
|
|
1133
|
+
if (stats.adjudicated > 0) console.error(` Adjudicated: ${stats.adjudicated}`);
|
|
1134
|
+
if (!options.quiet && mergedFindings.length > 0) {
|
|
1135
|
+
console.error(chalk5.yellow("\n\u2500\u2500 Findings Summary \u2500\u2500"));
|
|
1136
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1137
|
+
for (const f of report.findings) {
|
|
1138
|
+
const arr = byScope.get(f.scope) ?? [];
|
|
1139
|
+
arr.push(f);
|
|
1140
|
+
byScope.set(f.scope, arr);
|
|
1141
|
+
}
|
|
1142
|
+
for (const [scope, items] of byScope) {
|
|
1143
|
+
const high = items.filter((f) => f.confidence === "high").length;
|
|
1144
|
+
const med = items.filter((f) => f.confidence === "medium").length;
|
|
1145
|
+
const low = items.filter((f) => f.confidence === "low").length;
|
|
1146
|
+
console.error(chalk5.cyan(`
|
|
1147
|
+
${scope} (${items.length})`));
|
|
1148
|
+
for (const f of items.filter((i) => i.confidence !== "low").slice(0, 5)) {
|
|
1149
|
+
const conf = f.confidence === "high" ? chalk5.red("HIGH") : chalk5.yellow("MED");
|
|
1150
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
1151
|
+
console.error(` ${conf} ${loc} \u2014 ${f.description}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (high + med > 5) {
|
|
1154
|
+
console.error(chalk5.dim(` ... and ${high + med - 5} more`));
|
|
1155
|
+
}
|
|
1156
|
+
if (low > 0) {
|
|
1157
|
+
console.error(chalk5.dim(` + ${low} low-confidence (report-only)`));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
console.error("");
|
|
1161
|
+
}
|
|
1162
|
+
sessionMgr.recordEvent({
|
|
1163
|
+
sessionId: session2.id,
|
|
1164
|
+
command: "cleanup",
|
|
1165
|
+
subcommand: "report",
|
|
1166
|
+
promptPreview: `Cleanup: ${scopes.join(", ")} on ${projectDir}`,
|
|
1167
|
+
responsePreview: `${mergedFindings.length} findings (${stats.highConfidence} high, ${stats.mediumConfidence} med, ${stats.lowConfidence} low)`,
|
|
1168
|
+
responseFull: JSON.stringify(report),
|
|
1169
|
+
durationMs
|
|
1170
|
+
});
|
|
1171
|
+
if (options.output) {
|
|
1172
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
1173
|
+
writeFileSync3(options.output, JSON.stringify(report, null, 2), "utf-8");
|
|
1174
|
+
console.error(chalk5.green(` Findings written to ${options.output}`));
|
|
1175
|
+
}
|
|
1176
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1177
|
+
db.close();
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
db?.close();
|
|
1180
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function runCodexScan(adapter, _projectDir, scopes, timeoutSec, sessionMgr, sessionId, sessionThreadId) {
|
|
1185
|
+
if (!adapter) {
|
|
1186
|
+
console.error(chalk5.yellow(" [codex] No adapter available \u2014 skipping semantic scan"));
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1189
|
+
console.error(chalk5.dim(" [codex] Starting semantic scan..."));
|
|
1190
|
+
const scopeDescriptions = scopes.map((s) => {
|
|
1191
|
+
if (s === "deps") return "unused dependencies (check each package.json dep, including dynamic imports)";
|
|
1192
|
+
if (s === "unused-exports") return "unused exports (exported but never imported anywhere)";
|
|
1193
|
+
if (s === "hardcoded") return "hardcoded values (magic numbers, URLs, credentials)";
|
|
1194
|
+
if (s === "duplicates") return "duplicate logic (similar function bodies across files)";
|
|
1195
|
+
if (s === "deadcode") return "dead code (unreachable or unused internal code)";
|
|
1196
|
+
return s;
|
|
1197
|
+
}).join(", ");
|
|
1198
|
+
const prompt = buildHandoffEnvelope2({
|
|
1199
|
+
command: "cleanup",
|
|
1200
|
+
task: `Scan this codebase for AI slop and code quality issues.
|
|
1201
|
+
|
|
1202
|
+
SCAN FOR: ${scopeDescriptions}
|
|
1203
|
+
|
|
1204
|
+
Where scope/confidence/file/line/symbol fields are:
|
|
1205
|
+
- scope: deps, unused-exports, hardcoded, duplicates, or deadcode
|
|
1206
|
+
- confidence: high, medium, or low
|
|
1207
|
+
- file: relative path from project root (forward slashes)
|
|
1208
|
+
- line: line number (or 0 if N/A)
|
|
1209
|
+
- symbol: the specific identifier (dep name, export name, variable name, or content hash)
|
|
1210
|
+
|
|
1211
|
+
IMPORTANT KEY FORMAT: The key will be built as scope:file:symbol \u2014 use the SAME symbol that a static scanner would use:
|
|
1212
|
+
- deps: the package name (e.g. "lodash")
|
|
1213
|
+
- unused-exports: the export name (e.g. "myFunction")
|
|
1214
|
+
- hardcoded: for numbers use "num:VALUE:LLINE" (e.g. "num:42:L15"), for URLs use "url:HOSTNAME:LLINE" (e.g. "url:api.example.com:L20"), for credentials use "cred:LLINE" (e.g. "cred:L15")
|
|
1215
|
+
- duplicates: "HASH:FUNCNAME" where HASH is first 8 chars of md5 of normalized body (e.g. "a1b2c3d4:myFunction")
|
|
1216
|
+
- deadcode: the function/variable name`,
|
|
1217
|
+
constraints: [
|
|
1218
|
+
"Be thorough but precise. Only report real issues you can verify.",
|
|
1219
|
+
"Check for dynamic imports (import()) before flagging unused deps",
|
|
1220
|
+
"Check barrel re-exports and index files before flagging unused exports",
|
|
1221
|
+
"Check type-only imports (import type)",
|
|
1222
|
+
"Check framework conventions and cross-package monorepo dependencies"
|
|
1223
|
+
],
|
|
1224
|
+
resumed: Boolean(sessionThreadId)
|
|
1225
|
+
});
|
|
1226
|
+
try {
|
|
1227
|
+
const progress = createProgressCallbacks("cleanup-scan");
|
|
1228
|
+
const result = await adapter.callWithResume(prompt, { sessionId: sessionThreadId, timeout: timeoutSec * 1e3, ...progress });
|
|
1229
|
+
if (sessionMgr && sessionId) {
|
|
1230
|
+
if (result.sessionId) {
|
|
1231
|
+
sessionMgr.updateThreadId(sessionId, result.sessionId);
|
|
1232
|
+
}
|
|
1233
|
+
sessionMgr.addUsageFromResult(sessionId, result.usage, prompt, result.text);
|
|
1234
|
+
sessionMgr.recordEvent({
|
|
1235
|
+
sessionId,
|
|
1236
|
+
command: "cleanup",
|
|
1237
|
+
subcommand: "scan",
|
|
1238
|
+
promptPreview: `Cleanup scan: ${scopes.join(", ")}`,
|
|
1239
|
+
responsePreview: result.text.slice(0, 500),
|
|
1240
|
+
usageJson: JSON.stringify(result.usage),
|
|
1241
|
+
durationMs: result.durationMs,
|
|
1242
|
+
codexThreadId: result.sessionId
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const findings = [];
|
|
1246
|
+
for (const line of result.text.split("\n")) {
|
|
1247
|
+
const match = line.match(/^FINDING:\s*([^|]+)\|([^|]+)\|([^|]+)\|(\d+)\|([^|]+)\|([^|]+)\|(.+)/);
|
|
1248
|
+
if (match) {
|
|
1249
|
+
const scope = match[1].trim();
|
|
1250
|
+
if (!scopes.includes(scope)) continue;
|
|
1251
|
+
const file = match[3].trim();
|
|
1252
|
+
const symbol = match[5].trim();
|
|
1253
|
+
findings.push({
|
|
1254
|
+
key: `${scope}:${file}:${symbol}`,
|
|
1255
|
+
scope,
|
|
1256
|
+
confidence: match[2].trim(),
|
|
1257
|
+
file,
|
|
1258
|
+
line: Number.parseInt(match[4], 10) || void 0,
|
|
1259
|
+
description: match[6].trim(),
|
|
1260
|
+
recommendation: match[7].trim(),
|
|
1261
|
+
deterministicEvidence: [],
|
|
1262
|
+
semanticEvidence: [`Codex: ${match[6].trim()}`],
|
|
1263
|
+
hostEvidence: [],
|
|
1264
|
+
sources: ["semantic"],
|
|
1265
|
+
disputed: false
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
console.error(chalk5.dim(` [codex] Done: ${findings.length} findings`));
|
|
1270
|
+
return findings;
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
console.error(chalk5.yellow(` [codex] Scan failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1273
|
+
return [];
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async function adjudicateFindings(adapter, findings, maxDisputes, stats) {
|
|
1277
|
+
const toAdjudicate = findings.filter((f) => f.disputed || f.confidence === "medium").slice(0, maxDisputes);
|
|
1278
|
+
for (const finding of toAdjudicate) {
|
|
1279
|
+
try {
|
|
1280
|
+
const allEvidence = [...finding.deterministicEvidence, ...finding.semanticEvidence, ...finding.hostEvidence];
|
|
1281
|
+
const prompt = buildHandoffEnvelope2({
|
|
1282
|
+
command: "adjudicate",
|
|
1283
|
+
task: `Verify this finding.
|
|
1284
|
+
|
|
1285
|
+
FINDING: ${finding.description}
|
|
1286
|
+
FILE: ${finding.file}${finding.line ? `:${finding.line}` : ""}
|
|
1287
|
+
SCOPE: ${finding.scope}
|
|
1288
|
+
SOURCES: ${finding.sources.join(", ")}
|
|
1289
|
+
EVIDENCE: ${allEvidence.join("; ")}`,
|
|
1290
|
+
constraints: ["Check for dynamic imports, barrel re-exports, type-only usage, runtime/indirect usage"],
|
|
1291
|
+
resumed: false
|
|
1292
|
+
});
|
|
1293
|
+
const adjProgress = createProgressCallbacks("adjudicate");
|
|
1294
|
+
const result = await adapter.callWithResume(prompt, { timeout: 6e4, ...adjProgress });
|
|
1295
|
+
const match = result.text.match(/ADJUDICATE:\s*(CONFIRMED|DISMISSED|UNCERTAIN)\s+(.*)/);
|
|
1296
|
+
if (match) {
|
|
1297
|
+
const verdict = match[1];
|
|
1298
|
+
if (verdict === "CONFIRMED") {
|
|
1299
|
+
finding.confidence = "high";
|
|
1300
|
+
finding.semanticEvidence.push(`Adjudicated: CONFIRMED \u2014 ${match[2]}`);
|
|
1301
|
+
} else if (verdict === "DISMISSED") {
|
|
1302
|
+
finding.confidence = "low";
|
|
1303
|
+
finding.semanticEvidence.push(`Adjudicated: DISMISSED \u2014 ${match[2]}`);
|
|
1304
|
+
} else {
|
|
1305
|
+
finding.semanticEvidence.push(`Adjudicated: UNCERTAIN \u2014 ${match[2]}`);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
finding.disputed = false;
|
|
1309
|
+
stats.adjudicated++;
|
|
1310
|
+
}
|
|
1311
|
+
} catch {
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
recalculateConfidenceStats(findings, stats);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/commands/cost.ts
|
|
1318
|
+
import { openDatabase as openDatabase5, SessionManager as SessionManager4 } from "@codemoot/core";
|
|
1319
|
+
import chalk6 from "chalk";
|
|
1320
|
+
function parseUsage(usageJson) {
|
|
1321
|
+
try {
|
|
1322
|
+
const u = JSON.parse(usageJson);
|
|
1323
|
+
const input = u.inputTokens ?? u.input_tokens ?? 0;
|
|
1324
|
+
const output = u.outputTokens ?? u.output_tokens ?? 0;
|
|
1325
|
+
const total = u.totalTokens ?? u.total_tokens ?? input + output;
|
|
1326
|
+
return { input, output, total };
|
|
1327
|
+
} catch {
|
|
1328
|
+
return { input: 0, output: 0, total: 0 };
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async function costCommand(options) {
|
|
1332
|
+
const db = openDatabase5(getDbPath());
|
|
1333
|
+
try {
|
|
1334
|
+
let rows;
|
|
1335
|
+
let scopeLabel;
|
|
1336
|
+
if (options.scope === "session") {
|
|
1337
|
+
const sessionMgr = new SessionManager4(db);
|
|
1338
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("cost");
|
|
1339
|
+
if (!session2) {
|
|
1340
|
+
console.error(chalk6.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: codemoot init"));
|
|
1341
|
+
db.close();
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
rows = db.prepare(
|
|
1345
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events WHERE session_id = ? ORDER BY created_at ASC"
|
|
1346
|
+
).all(session2.id);
|
|
1347
|
+
scopeLabel = `session ${session2.id.slice(0, 8)}`;
|
|
1348
|
+
} else if (options.scope === "all") {
|
|
1349
|
+
rows = db.prepare(
|
|
1350
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events ORDER BY created_at ASC"
|
|
1351
|
+
).all();
|
|
1352
|
+
scopeLabel = "all-time";
|
|
1353
|
+
} else {
|
|
1354
|
+
const cutoff = Date.now() - options.days * 24 * 60 * 60 * 1e3;
|
|
1355
|
+
rows = db.prepare(
|
|
1356
|
+
"SELECT command, subcommand, usage_json, duration_ms, created_at FROM session_events WHERE created_at > ? ORDER BY created_at ASC"
|
|
1357
|
+
).all(cutoff);
|
|
1358
|
+
scopeLabel = `last ${options.days} days`;
|
|
1359
|
+
}
|
|
1360
|
+
let totalInput = 0;
|
|
1361
|
+
let totalOutput = 0;
|
|
1362
|
+
let totalTokens = 0;
|
|
1363
|
+
let totalDuration = 0;
|
|
1364
|
+
const byCommand = {};
|
|
1365
|
+
const byDay = {};
|
|
1366
|
+
for (const row of rows) {
|
|
1367
|
+
const usage = parseUsage(row.usage_json);
|
|
1368
|
+
totalInput += usage.input;
|
|
1369
|
+
totalOutput += usage.output;
|
|
1370
|
+
totalTokens += usage.total;
|
|
1371
|
+
totalDuration += row.duration_ms ?? 0;
|
|
1372
|
+
const cmd = row.command ?? "unknown";
|
|
1373
|
+
if (!byCommand[cmd]) byCommand[cmd] = { calls: 0, tokens: 0, durationMs: 0 };
|
|
1374
|
+
byCommand[cmd].calls++;
|
|
1375
|
+
byCommand[cmd].tokens += usage.total;
|
|
1376
|
+
byCommand[cmd].durationMs += row.duration_ms ?? 0;
|
|
1377
|
+
const day = new Date(row.created_at).toISOString().slice(0, 10);
|
|
1378
|
+
if (!byDay[day]) byDay[day] = { calls: 0, tokens: 0 };
|
|
1379
|
+
byDay[day].calls++;
|
|
1380
|
+
byDay[day].tokens += usage.total;
|
|
1381
|
+
}
|
|
1382
|
+
const output = {
|
|
1383
|
+
scope: scopeLabel,
|
|
1384
|
+
totalCalls: rows.length,
|
|
1385
|
+
totalTokens,
|
|
1386
|
+
totalInputTokens: totalInput,
|
|
1387
|
+
totalOutputTokens: totalOutput,
|
|
1388
|
+
totalDurationMs: totalDuration,
|
|
1389
|
+
avgTokensPerCall: rows.length > 0 ? Math.round(totalTokens / rows.length) : 0,
|
|
1390
|
+
avgDurationMs: rows.length > 0 ? Math.round(totalDuration / rows.length) : 0,
|
|
1391
|
+
byCommand,
|
|
1392
|
+
byDay
|
|
1393
|
+
};
|
|
1394
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1395
|
+
db.close();
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
db.close();
|
|
1398
|
+
console.error(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/commands/doctor.ts
|
|
1404
|
+
import { existsSync, accessSync, constants } from "fs";
|
|
1405
|
+
import { execSync as execSync2 } from "child_process";
|
|
1406
|
+
import { join as join3 } from "path";
|
|
1407
|
+
import chalk7 from "chalk";
|
|
1408
|
+
import { VERSION } from "@codemoot/core";
|
|
1409
|
+
async function doctorCommand() {
|
|
1410
|
+
const cwd = process.cwd();
|
|
1411
|
+
const checks = [];
|
|
1412
|
+
console.error(chalk7.cyan(`
|
|
1413
|
+
CodeMoot Doctor v${VERSION}
|
|
1414
|
+
`));
|
|
1415
|
+
try {
|
|
1416
|
+
const version = execSync2("codex --version", { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
1417
|
+
checks.push({ name: "codex-cli", status: "pass", message: `Codex CLI ${version}` });
|
|
1418
|
+
} catch {
|
|
1419
|
+
checks.push({
|
|
1420
|
+
name: "codex-cli",
|
|
1421
|
+
status: "fail",
|
|
1422
|
+
message: "Codex CLI not found in PATH",
|
|
1423
|
+
fix: "npm install -g @openai/codex"
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
const configPath = join3(cwd, ".cowork.yml");
|
|
1427
|
+
if (existsSync(configPath)) {
|
|
1428
|
+
checks.push({ name: "config", status: "pass", message: ".cowork.yml found" });
|
|
1429
|
+
} else {
|
|
1430
|
+
checks.push({
|
|
1431
|
+
name: "config",
|
|
1432
|
+
status: "fail",
|
|
1433
|
+
message: ".cowork.yml not found",
|
|
1434
|
+
fix: "codemoot init"
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
const dbDir = join3(cwd, ".cowork", "db");
|
|
1438
|
+
const dbPath = join3(dbDir, "cowork.db");
|
|
1439
|
+
if (existsSync(dbDir)) {
|
|
1440
|
+
try {
|
|
1441
|
+
accessSync(dbDir, constants.W_OK);
|
|
1442
|
+
checks.push({
|
|
1443
|
+
name: "database",
|
|
1444
|
+
status: existsSync(dbPath) ? "pass" : "warn",
|
|
1445
|
+
message: existsSync(dbPath) ? "Database exists and writable" : "Database directory exists, DB will be created on first use"
|
|
1446
|
+
});
|
|
1447
|
+
} catch {
|
|
1448
|
+
checks.push({
|
|
1449
|
+
name: "database",
|
|
1450
|
+
status: "fail",
|
|
1451
|
+
message: ".cowork/db/ is not writable",
|
|
1452
|
+
fix: "Check file permissions on .cowork/db/"
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
} else {
|
|
1456
|
+
checks.push({
|
|
1457
|
+
name: "database",
|
|
1458
|
+
status: "warn",
|
|
1459
|
+
message: ".cowork/db/ not found \u2014 will be created by codemoot init",
|
|
1460
|
+
fix: "codemoot init"
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
let gitFound = false;
|
|
1464
|
+
let searchDir = cwd;
|
|
1465
|
+
while (searchDir) {
|
|
1466
|
+
if (existsSync(join3(searchDir, ".git"))) {
|
|
1467
|
+
gitFound = true;
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
const parent = join3(searchDir, "..");
|
|
1471
|
+
if (parent === searchDir) break;
|
|
1472
|
+
searchDir = parent;
|
|
1473
|
+
}
|
|
1474
|
+
if (gitFound) {
|
|
1475
|
+
checks.push({ name: "git", status: "pass", message: "Git repository detected" });
|
|
1476
|
+
} else {
|
|
1477
|
+
checks.push({
|
|
1478
|
+
name: "git",
|
|
1479
|
+
status: "warn",
|
|
1480
|
+
message: "Not a git repository \u2014 diff/shipit/watch features limited"
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
const nodeVersion = process.version;
|
|
1484
|
+
const major = Number.parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
1485
|
+
if (major >= 18) {
|
|
1486
|
+
checks.push({ name: "node", status: "pass", message: `Node.js ${nodeVersion}` });
|
|
1487
|
+
} else {
|
|
1488
|
+
checks.push({
|
|
1489
|
+
name: "node",
|
|
1490
|
+
status: "fail",
|
|
1491
|
+
message: `Node.js ${nodeVersion} \u2014 requires >= 18`,
|
|
1492
|
+
fix: "Install Node.js 18+"
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
if (existsSync(dbPath)) {
|
|
1496
|
+
try {
|
|
1497
|
+
const { openDatabase: openDatabase14 } = await import("@codemoot/core");
|
|
1498
|
+
const db = openDatabase14(dbPath);
|
|
1499
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
1500
|
+
const version = row?.user_version ?? 0;
|
|
1501
|
+
if (version >= 7) {
|
|
1502
|
+
checks.push({ name: "schema", status: "pass", message: `Schema version ${version}` });
|
|
1503
|
+
} else {
|
|
1504
|
+
checks.push({
|
|
1505
|
+
name: "schema",
|
|
1506
|
+
status: "warn",
|
|
1507
|
+
message: `Schema version ${version} \u2014 will auto-migrate on next command`
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
db.close();
|
|
1511
|
+
} catch {
|
|
1512
|
+
checks.push({ name: "schema", status: "warn", message: "Could not read schema version" });
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
let hasFailure = false;
|
|
1516
|
+
for (const check of checks) {
|
|
1517
|
+
const icon = check.status === "pass" ? chalk7.green("PASS") : check.status === "warn" ? chalk7.yellow("WARN") : chalk7.red("FAIL");
|
|
1518
|
+
console.error(` ${icon} ${check.name}: ${check.message}`);
|
|
1519
|
+
if (check.fix) {
|
|
1520
|
+
console.error(chalk7.dim(` \u2192 ${check.fix}`));
|
|
1521
|
+
}
|
|
1522
|
+
if (check.status === "fail") hasFailure = true;
|
|
1523
|
+
}
|
|
1524
|
+
console.error("");
|
|
1525
|
+
const output = {
|
|
1526
|
+
version: VERSION,
|
|
1527
|
+
checks,
|
|
1528
|
+
healthy: !hasFailure
|
|
1529
|
+
};
|
|
1530
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1531
|
+
if (hasFailure) {
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/commands/events.ts
|
|
1537
|
+
import { openDatabase as openDatabase6 } from "@codemoot/core";
|
|
1538
|
+
import chalk8 from "chalk";
|
|
1539
|
+
async function eventsCommand(options) {
|
|
1540
|
+
const db = openDatabase6(getDbPath());
|
|
1541
|
+
const query = options.type === "all" ? db.prepare(`
|
|
1542
|
+
SELECT 'session_event' as source, id, session_id, command, subcommand, prompt_preview, response_preview, usage_json, duration_ms, created_at
|
|
1543
|
+
FROM session_events
|
|
1544
|
+
WHERE id > ?
|
|
1545
|
+
ORDER BY id ASC
|
|
1546
|
+
LIMIT ?
|
|
1547
|
+
`) : options.type === "jobs" ? db.prepare(`
|
|
1548
|
+
SELECT 'job_log' as source, id, job_id, seq, level, event_type, message, payload_json, created_at
|
|
1549
|
+
FROM job_logs
|
|
1550
|
+
WHERE id > ?
|
|
1551
|
+
ORDER BY id ASC
|
|
1552
|
+
LIMIT ?
|
|
1553
|
+
`) : db.prepare(`
|
|
1554
|
+
SELECT 'session_event' as source, id, session_id, command, subcommand, prompt_preview, response_preview, usage_json, duration_ms, created_at
|
|
1555
|
+
FROM session_events
|
|
1556
|
+
WHERE id > ?
|
|
1557
|
+
ORDER BY id ASC
|
|
1558
|
+
LIMIT ?
|
|
1559
|
+
`);
|
|
1560
|
+
let cursor = options.sinceSeq;
|
|
1561
|
+
const poll = () => {
|
|
1562
|
+
const rows = query.all(cursor, options.limit);
|
|
1563
|
+
for (const row of rows) {
|
|
1564
|
+
console.log(JSON.stringify(row));
|
|
1565
|
+
cursor = row.id;
|
|
1566
|
+
}
|
|
1567
|
+
return rows.length;
|
|
1568
|
+
};
|
|
1569
|
+
poll();
|
|
1570
|
+
if (!options.follow) {
|
|
1571
|
+
db.close();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
console.error(chalk8.dim("Following events... (Ctrl+C to stop)"));
|
|
1575
|
+
const interval = setInterval(() => {
|
|
1576
|
+
poll();
|
|
1577
|
+
}, 1e3);
|
|
1578
|
+
const shutdown = () => {
|
|
1579
|
+
clearInterval(interval);
|
|
1580
|
+
db.close();
|
|
1581
|
+
process.exit(0);
|
|
1582
|
+
};
|
|
1583
|
+
process.on("SIGINT", shutdown);
|
|
1584
|
+
process.on("SIGTERM", shutdown);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/commands/fix.ts
|
|
1588
|
+
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
1589
|
+
import {
|
|
1590
|
+
DEFAULT_RULES,
|
|
1591
|
+
ModelRegistry as ModelRegistry3,
|
|
1592
|
+
SessionManager as SessionManager5,
|
|
1593
|
+
buildHandoffEnvelope as buildHandoffEnvelope3,
|
|
1594
|
+
evaluatePolicy,
|
|
1595
|
+
loadConfig as loadConfig4,
|
|
1596
|
+
openDatabase as openDatabase7,
|
|
1597
|
+
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS2
|
|
1598
|
+
} from "@codemoot/core";
|
|
1599
|
+
import chalk9 from "chalk";
|
|
1600
|
+
async function fixCommand(fileOrGlob, options) {
|
|
1601
|
+
const projectDir = process.cwd();
|
|
1602
|
+
const db = openDatabase7(getDbPath());
|
|
1603
|
+
const config = loadConfig4();
|
|
1604
|
+
const registry = ModelRegistry3.fromConfig(config, projectDir);
|
|
1605
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
1606
|
+
if (!adapter) {
|
|
1607
|
+
try {
|
|
1608
|
+
execSync3("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
1609
|
+
} catch {
|
|
1610
|
+
console.error(chalk9.red("Codex CLI is not installed or not in PATH."));
|
|
1611
|
+
console.error(chalk9.yellow("Install it: npm install -g @openai/codex"));
|
|
1612
|
+
db.close();
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
}
|
|
1615
|
+
console.error(chalk9.red("No codex adapter found in config. Run: codemoot init"));
|
|
1616
|
+
db.close();
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
const sessionMgr = new SessionManager5(db);
|
|
1620
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("fix");
|
|
1621
|
+
if (!session2) {
|
|
1622
|
+
console.error(chalk9.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: codemoot init"));
|
|
1623
|
+
db.close();
|
|
1624
|
+
process.exit(1);
|
|
1625
|
+
}
|
|
1626
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
1627
|
+
let threadId = currentSession?.codexThreadId ?? void 0;
|
|
1628
|
+
const rounds = [];
|
|
1629
|
+
let converged = false;
|
|
1630
|
+
console.error(
|
|
1631
|
+
chalk9.cyan(
|
|
1632
|
+
`Autofix loop: ${fileOrGlob} (max ${options.maxRounds} rounds, focus: ${options.focus})`
|
|
1633
|
+
)
|
|
1634
|
+
);
|
|
1635
|
+
for (let round = 1; round <= options.maxRounds; round++) {
|
|
1636
|
+
const overflowCheck = sessionMgr.preCallOverflowCheck(session2.id);
|
|
1637
|
+
if (overflowCheck.rolled) {
|
|
1638
|
+
console.error(chalk9.yellow(` ${overflowCheck.message}`));
|
|
1639
|
+
threadId = void 0;
|
|
1640
|
+
}
|
|
1641
|
+
const roundStart = Date.now();
|
|
1642
|
+
console.error(chalk9.dim(`
|
|
1643
|
+
\u2500\u2500 Round ${round}/${options.maxRounds} \u2500\u2500`));
|
|
1644
|
+
let diffContent = "";
|
|
1645
|
+
if (options.diff) {
|
|
1646
|
+
try {
|
|
1647
|
+
diffContent = execFileSync("git", ["diff", ...options.diff.split(/\s+/)], {
|
|
1648
|
+
cwd: projectDir,
|
|
1649
|
+
encoding: "utf-8",
|
|
1650
|
+
maxBuffer: 1024 * 1024
|
|
1651
|
+
});
|
|
1652
|
+
} catch {
|
|
1653
|
+
diffContent = "";
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const reviewPrompt = buildHandoffEnvelope3({
|
|
1657
|
+
command: "review",
|
|
1658
|
+
task: options.diff ? `Review and identify fixable issues in this diff.
|
|
1659
|
+
|
|
1660
|
+
GIT DIFF (${options.diff}):
|
|
1661
|
+
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers.`,
|
|
1662
|
+
constraints: [
|
|
1663
|
+
`Focus: ${options.focus}`,
|
|
1664
|
+
"For each issue, provide the EXACT fix as a code snippet.",
|
|
1665
|
+
"Format fixes as: FIX: <file>:<line> <description>\n```\n<fixed code>\n```",
|
|
1666
|
+
round > 1 ? `This is re-review round ${round}. Previous fixes were applied. Check if issues are resolved.` : ""
|
|
1667
|
+
].filter(Boolean),
|
|
1668
|
+
resumed: Boolean(threadId)
|
|
1669
|
+
});
|
|
1670
|
+
const timeoutMs = options.timeout * 1e3;
|
|
1671
|
+
const progress = createProgressCallbacks("fix-review");
|
|
1672
|
+
console.error(chalk9.dim(" Reviewing..."));
|
|
1673
|
+
const reviewResult = await adapter.callWithResume(reviewPrompt, {
|
|
1674
|
+
sessionId: threadId,
|
|
1675
|
+
timeout: timeoutMs,
|
|
1676
|
+
...progress
|
|
1677
|
+
});
|
|
1678
|
+
if (reviewResult.sessionId) {
|
|
1679
|
+
threadId = reviewResult.sessionId;
|
|
1680
|
+
sessionMgr.updateThreadId(session2.id, reviewResult.sessionId);
|
|
1681
|
+
}
|
|
1682
|
+
sessionMgr.addUsageFromResult(session2.id, reviewResult.usage, reviewPrompt, reviewResult.text);
|
|
1683
|
+
const tail = reviewResult.text.slice(-500);
|
|
1684
|
+
const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
1685
|
+
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
1686
|
+
const verdict = verdictMatch ? verdictMatch[1].toLowerCase() : "unknown";
|
|
1687
|
+
const score = scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null;
|
|
1688
|
+
const criticalCount = (reviewResult.text.match(/CRITICAL/gi) ?? []).length;
|
|
1689
|
+
const warningCount = (reviewResult.text.match(/WARNING/gi) ?? []).length;
|
|
1690
|
+
console.error(
|
|
1691
|
+
` Verdict: ${verdict}, Score: ${score ?? "?"}/10, Critical: ${criticalCount}, Warning: ${warningCount}`
|
|
1692
|
+
);
|
|
1693
|
+
if (verdict === "approved" && criticalCount === 0) {
|
|
1694
|
+
rounds.push({
|
|
1695
|
+
round,
|
|
1696
|
+
reviewVerdict: verdict,
|
|
1697
|
+
reviewScore: score,
|
|
1698
|
+
criticalCount,
|
|
1699
|
+
warningCount,
|
|
1700
|
+
fixApplied: false,
|
|
1701
|
+
durationMs: Date.now() - roundStart
|
|
1702
|
+
});
|
|
1703
|
+
converged = true;
|
|
1704
|
+
console.error(chalk9.green(" Review APPROVED \u2014 no fixes needed."));
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
if (options.dryRun) {
|
|
1708
|
+
console.error(chalk9.yellow(" Dry-run: skipping fix application."));
|
|
1709
|
+
rounds.push({
|
|
1710
|
+
round,
|
|
1711
|
+
reviewVerdict: verdict,
|
|
1712
|
+
reviewScore: score,
|
|
1713
|
+
criticalCount,
|
|
1714
|
+
warningCount,
|
|
1715
|
+
fixApplied: false,
|
|
1716
|
+
durationMs: Date.now() - roundStart
|
|
1717
|
+
});
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
console.error(chalk9.dim(" Applying fixes..."));
|
|
1721
|
+
const fixPrompt = buildHandoffEnvelope3({
|
|
1722
|
+
command: "custom",
|
|
1723
|
+
task: `Based on the review above, apply ALL suggested fixes to the codebase. Use your file editing tools to make the changes. After applying, verify the changes compile correctly. Only fix issues that were identified \u2014 do not refactor or change unrelated code.`,
|
|
1724
|
+
constraints: [
|
|
1725
|
+
"Make minimal, targeted changes only.",
|
|
1726
|
+
"Do not add comments, docstrings, or formatting changes.",
|
|
1727
|
+
"If a fix is ambiguous, skip it rather than guess."
|
|
1728
|
+
],
|
|
1729
|
+
resumed: true
|
|
1730
|
+
});
|
|
1731
|
+
const fixResult = await adapter.callWithResume(fixPrompt, {
|
|
1732
|
+
sessionId: threadId,
|
|
1733
|
+
timeout: timeoutMs,
|
|
1734
|
+
...progress
|
|
1735
|
+
});
|
|
1736
|
+
if (fixResult.sessionId) {
|
|
1737
|
+
threadId = fixResult.sessionId;
|
|
1738
|
+
sessionMgr.updateThreadId(session2.id, fixResult.sessionId);
|
|
1739
|
+
}
|
|
1740
|
+
sessionMgr.addUsageFromResult(session2.id, fixResult.usage, fixPrompt, fixResult.text);
|
|
1741
|
+
const fixApplied = !fixResult.text.includes("no changes") && !fixResult.text.includes("No fixes");
|
|
1742
|
+
console.error(
|
|
1743
|
+
fixApplied ? chalk9.green(" Fixes applied.") : chalk9.yellow(" No fixes applied.")
|
|
1744
|
+
);
|
|
1745
|
+
rounds.push({
|
|
1746
|
+
round,
|
|
1747
|
+
reviewVerdict: verdict,
|
|
1748
|
+
reviewScore: score,
|
|
1749
|
+
criticalCount,
|
|
1750
|
+
warningCount,
|
|
1751
|
+
fixApplied,
|
|
1752
|
+
durationMs: Date.now() - roundStart
|
|
1753
|
+
});
|
|
1754
|
+
if (!fixApplied) {
|
|
1755
|
+
console.error(chalk9.yellow(" No changes made \u2014 stopping loop."));
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const lastRound = rounds[rounds.length - 1];
|
|
1760
|
+
const policyCtx = {
|
|
1761
|
+
criticalCount: lastRound?.criticalCount ?? 0,
|
|
1762
|
+
warningCount: lastRound?.warningCount ?? 0,
|
|
1763
|
+
verdict: lastRound?.reviewVerdict ?? "unknown",
|
|
1764
|
+
stepsCompleted: { fix: converged ? "passed" : "failed" },
|
|
1765
|
+
cleanupHighCount: 0
|
|
1766
|
+
};
|
|
1767
|
+
const policy = evaluatePolicy("review.completed", policyCtx, DEFAULT_RULES);
|
|
1768
|
+
const output = {
|
|
1769
|
+
target: fileOrGlob,
|
|
1770
|
+
converged,
|
|
1771
|
+
rounds,
|
|
1772
|
+
totalRounds: rounds.length,
|
|
1773
|
+
finalVerdict: lastRound?.reviewVerdict ?? "unknown",
|
|
1774
|
+
finalScore: lastRound?.reviewScore ?? null,
|
|
1775
|
+
policy,
|
|
1776
|
+
sessionId: session2.id,
|
|
1777
|
+
codexThreadId: threadId
|
|
1778
|
+
};
|
|
1779
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1780
|
+
db.close();
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/commands/init.ts
|
|
1784
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1785
|
+
import { basename, join as join4 } from "path";
|
|
1786
|
+
import { loadConfig as loadConfig5, writeConfig } from "@codemoot/core";
|
|
1787
|
+
import chalk10 from "chalk";
|
|
1788
|
+
async function initCommand(options) {
|
|
1789
|
+
const cwd = process.cwd();
|
|
1790
|
+
const configPath = join4(cwd, ".cowork.yml");
|
|
1791
|
+
if (existsSync2(configPath) && !options.force) {
|
|
1792
|
+
console.error(chalk10.red("Already initialized. Use --force to overwrite."));
|
|
1793
|
+
process.exit(1);
|
|
1794
|
+
}
|
|
1795
|
+
const validPresets = ["cli-first"];
|
|
1796
|
+
let presetName = "cli-first";
|
|
1797
|
+
if (options.preset) {
|
|
1798
|
+
if (!validPresets.includes(options.preset)) {
|
|
1799
|
+
console.error(chalk10.red(`Unknown preset: ${options.preset}. Available: ${validPresets.join(", ")}`));
|
|
1800
|
+
process.exit(1);
|
|
1801
|
+
}
|
|
1802
|
+
presetName = options.preset;
|
|
1803
|
+
} else if (options.nonInteractive) {
|
|
1804
|
+
presetName = "cli-first";
|
|
1805
|
+
} else {
|
|
1806
|
+
const { selectPreset } = await import("./prompts-NYEQYOVN.js");
|
|
1807
|
+
presetName = await selectPreset();
|
|
1808
|
+
}
|
|
1809
|
+
const config = loadConfig5({ preset: presetName, skipFile: options.force });
|
|
1810
|
+
config.project.name = basename(cwd);
|
|
1811
|
+
writeConfig(config, cwd);
|
|
1812
|
+
console.log(chalk10.green(`
|
|
1813
|
+
Initialized with '${presetName}' preset`));
|
|
1814
|
+
const modelEntries = Object.entries(config.models);
|
|
1815
|
+
for (const [alias, modelConfig] of modelEntries) {
|
|
1816
|
+
console.log(chalk10.gray(` ${alias}: ${modelConfig.model}`));
|
|
1817
|
+
}
|
|
1818
|
+
console.log(chalk10.gray('\nNext: codemoot plan "describe your task"'));
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/commands/install-skills.ts
|
|
1822
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1823
|
+
import { dirname, join as join5 } from "path";
|
|
1824
|
+
import chalk11 from "chalk";
|
|
1825
|
+
var SKILLS = [
|
|
1826
|
+
{
|
|
1827
|
+
path: ".claude/skills/codex-review/SKILL.md",
|
|
1828
|
+
description: "/codex-review \u2014 Get GPT review via Codex CLI",
|
|
1829
|
+
content: `---
|
|
1830
|
+
name: codex-review
|
|
1831
|
+
description: Get an independent GPT review via Codex CLI. Use when you want a second opinion on code, plans, or architecture from a different AI model.
|
|
1832
|
+
user-invocable: true
|
|
1833
|
+
---
|
|
1834
|
+
|
|
1835
|
+
# /codex-review \u2014 Get GPT Review via Codex CLI
|
|
1836
|
+
|
|
1837
|
+
## Usage
|
|
1838
|
+
\`/codex-review <file, glob, or description of what to review>\`
|
|
1839
|
+
|
|
1840
|
+
## Description
|
|
1841
|
+
Sends content to GPT via \`codemoot review\` for an independent review with session persistence. GPT has full codebase access and reviews are tracked in SQLite. Uses your ChatGPT subscription \u2014 zero API cost.
|
|
1842
|
+
|
|
1843
|
+
## Instructions
|
|
1844
|
+
|
|
1845
|
+
When the user invokes \`/codex-review\`, follow these steps:
|
|
1846
|
+
|
|
1847
|
+
### Step 1: Gather project context
|
|
1848
|
+
Before sending to GPT, gather relevant context so GPT understands the project:
|
|
1849
|
+
|
|
1850
|
+
1. Check if \`CLAUDE.md\` or \`README.md\` exists \u2014 read the first 200 lines for project overview
|
|
1851
|
+
2. Check if \`.claude/settings.json\` or similar config exists
|
|
1852
|
+
3. Note the project language, framework, and key patterns
|
|
1853
|
+
|
|
1854
|
+
This context will be included in the prompt to reduce false positives.
|
|
1855
|
+
|
|
1856
|
+
### Step 2: Determine review mode
|
|
1857
|
+
|
|
1858
|
+
**If the user specifies a file or glob:**
|
|
1859
|
+
\`\`\`bash
|
|
1860
|
+
codemoot review <file-or-glob> --focus all
|
|
1861
|
+
\`\`\`
|
|
1862
|
+
|
|
1863
|
+
**If the user specifies a diff:**
|
|
1864
|
+
\`\`\`bash
|
|
1865
|
+
codemoot review --diff HEAD~3..HEAD
|
|
1866
|
+
\`\`\`
|
|
1867
|
+
|
|
1868
|
+
**If the user gives a freeform description:**
|
|
1869
|
+
\`\`\`bash
|
|
1870
|
+
codemoot review --prompt "PROJECT CONTEXT: <context from step 1>
|
|
1871
|
+
|
|
1872
|
+
REVIEW TASK: <user's description>
|
|
1873
|
+
|
|
1874
|
+
Evaluate on: Correctness, Completeness, Quality, Security, Feasibility.
|
|
1875
|
+
For security findings, verify by reading the actual code before flagging.
|
|
1876
|
+
Provide SCORE: X/10 and VERDICT: APPROVED or NEEDS_REVISION"
|
|
1877
|
+
\`\`\`
|
|
1878
|
+
|
|
1879
|
+
**For presets:**
|
|
1880
|
+
\`\`\`bash
|
|
1881
|
+
codemoot review <target> --preset security-audit
|
|
1882
|
+
codemoot review <target> --preset quick-scan
|
|
1883
|
+
codemoot review <target> --preset performance
|
|
1884
|
+
\`\`\`
|
|
1885
|
+
|
|
1886
|
+
### Step 3: Parse and present the output
|
|
1887
|
+
The command outputs JSON to stdout. Parse it and present as clean markdown:
|
|
1888
|
+
|
|
1889
|
+
\`\`\`
|
|
1890
|
+
## GPT Review Results
|
|
1891
|
+
|
|
1892
|
+
**Score**: X/10 | **Verdict**: APPROVED/NEEDS_REVISION
|
|
1893
|
+
|
|
1894
|
+
### Findings
|
|
1895
|
+
- [CRITICAL] file:line \u2014 description
|
|
1896
|
+
- [WARNING] file:line \u2014 description
|
|
1897
|
+
|
|
1898
|
+
### GPT's Full Analysis
|
|
1899
|
+
<review text>
|
|
1900
|
+
|
|
1901
|
+
Session: <sessionId> | Tokens: <usage> | Duration: <durationMs>ms
|
|
1902
|
+
\`\`\`
|
|
1903
|
+
|
|
1904
|
+
### Step 4: If NEEDS_REVISION
|
|
1905
|
+
Ask if user wants to fix and re-review. If yes:
|
|
1906
|
+
1. Fix the issues
|
|
1907
|
+
2. Run \`codemoot review\` again \u2014 session resume gives GPT context of prior review
|
|
1908
|
+
3. GPT will check if previous issues were addressed
|
|
1909
|
+
|
|
1910
|
+
### Important Notes
|
|
1911
|
+
- **Session resume**: Each review builds on prior context. GPT remembers what it reviewed before.
|
|
1912
|
+
- **Codebase access**: GPT can read project files during review via codex tools.
|
|
1913
|
+
- **No arg size limits**: Content is piped via stdin, not passed as CLI args.
|
|
1914
|
+
- **Presets**: Use --preset for specialized reviews (security-audit, performance, quick-scan, pre-commit, api-review).
|
|
1915
|
+
- **Background mode**: Add --background to enqueue and continue working.
|
|
1916
|
+
`
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
path: ".claude/skills/debate/SKILL.md",
|
|
1920
|
+
description: "/debate \u2014 Claude vs GPT multi-round debate",
|
|
1921
|
+
content: `---
|
|
1922
|
+
name: debate
|
|
1923
|
+
description: Real Claude vs GPT multi-round debate. Use when you need a second opinion, want to debate architecture decisions, or evaluate competing approaches.
|
|
1924
|
+
user-invocable: true
|
|
1925
|
+
---
|
|
1926
|
+
|
|
1927
|
+
# /debate \u2014 Real Claude vs GPT Multi-Round Debate
|
|
1928
|
+
|
|
1929
|
+
## Usage
|
|
1930
|
+
\`/debate <topic or question>\`
|
|
1931
|
+
|
|
1932
|
+
## Description
|
|
1933
|
+
Structured debate: Claude proposes, GPT critiques, Claude revises, GPT re-evaluates \u2014 looping until convergence or max rounds. Real multi-model collaboration via codemoot CLI with session persistence.
|
|
1934
|
+
|
|
1935
|
+
## Instructions
|
|
1936
|
+
|
|
1937
|
+
### Phase 0: Setup
|
|
1938
|
+
1. Parse topic from user's message
|
|
1939
|
+
2. Start debate:
|
|
1940
|
+
\`\`\`bash
|
|
1941
|
+
codemoot debate start "TOPIC_HERE"
|
|
1942
|
+
\`\`\`
|
|
1943
|
+
3. Save the \`debateId\` from JSON output
|
|
1944
|
+
4. Announce: "Entering debate mode: Claude vs GPT"
|
|
1945
|
+
|
|
1946
|
+
### Phase 1: Claude's Opening Proposal
|
|
1947
|
+
Think deeply. Generate your genuine proposal. Be thorough and specific.
|
|
1948
|
+
|
|
1949
|
+
### Phase 1.5: Gather Codebase Context
|
|
1950
|
+
If topic relates to code, use Grep/Glob/Read to find relevant files. Summarize for GPT.
|
|
1951
|
+
|
|
1952
|
+
### Phase 2: Send to GPT
|
|
1953
|
+
\`\`\`bash
|
|
1954
|
+
codemoot debate turn DEBATE_ID "You are a senior technical reviewer debating with Claude about a codebase. You have full access to project files.
|
|
1955
|
+
|
|
1956
|
+
DEBATE TOPIC: <topic>
|
|
1957
|
+
CODEBASE CONTEXT: <summary>
|
|
1958
|
+
CLAUDE'S PROPOSAL: <proposal>
|
|
1959
|
+
|
|
1960
|
+
Respond with:
|
|
1961
|
+
1. What you agree with
|
|
1962
|
+
2. What you disagree with
|
|
1963
|
+
3. Suggested improvements
|
|
1964
|
+
4. STANCE: SUPPORT, OPPOSE, or UNCERTAIN" --round N
|
|
1965
|
+
\`\`\`
|
|
1966
|
+
|
|
1967
|
+
### Phase 3: Check Convergence
|
|
1968
|
+
- STANCE: SUPPORT \u2192 go to Phase 5
|
|
1969
|
+
- Max rounds reached \u2192 go to Phase 5
|
|
1970
|
+
- Otherwise \u2192 Phase 4
|
|
1971
|
+
|
|
1972
|
+
### Phase 4: Claude's Revision
|
|
1973
|
+
Read GPT's critique. Revise genuinely. Send back to GPT.
|
|
1974
|
+
|
|
1975
|
+
### Phase 5: Final Synthesis
|
|
1976
|
+
\`\`\`bash
|
|
1977
|
+
codemoot debate complete DEBATE_ID
|
|
1978
|
+
\`\`\`
|
|
1979
|
+
Present: final position, agreements, disagreements, stats.
|
|
1980
|
+
|
|
1981
|
+
### Rules
|
|
1982
|
+
1. Be genuine \u2014 don't just agree to end the debate
|
|
1983
|
+
2. Session resume is automatic via callWithResume()
|
|
1984
|
+
3. State persisted to SQLite
|
|
1985
|
+
4. Zero API cost (ChatGPT subscription)
|
|
1986
|
+
5. 600s default timeout per turn
|
|
1987
|
+
`
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
path: ".claude/skills/build/SKILL.md",
|
|
1991
|
+
description: "/build \u2014 Autonomous build loop with GPT review",
|
|
1992
|
+
content: `---
|
|
1993
|
+
name: build
|
|
1994
|
+
description: Autonomous build loop \u2014 debate, plan, implement, review, fix \u2014 all in one session with GPT review.
|
|
1995
|
+
user-invocable: true
|
|
1996
|
+
---
|
|
1997
|
+
|
|
1998
|
+
# /build \u2014 Autonomous Build Loop
|
|
1999
|
+
|
|
2000
|
+
## Usage
|
|
2001
|
+
\`/build <task description>\`
|
|
2002
|
+
|
|
2003
|
+
## Description
|
|
2004
|
+
Full pipeline: debate approach with GPT \u2192 user approval \u2192 implement \u2192 GPT review \u2192 fix \u2192 re-review until approved. SQLite tracking throughout.
|
|
2005
|
+
|
|
2006
|
+
## Instructions
|
|
2007
|
+
|
|
2008
|
+
### Phase 0: Initialize
|
|
2009
|
+
1. Record user's exact request (acceptance criteria)
|
|
2010
|
+
2. \`codemoot build start "TASK"\`
|
|
2011
|
+
3. Save buildId and debateId
|
|
2012
|
+
|
|
2013
|
+
### Phase 1: Debate the Approach (MANDATORY)
|
|
2014
|
+
Use /debate protocol. Loop until GPT says STANCE: SUPPORT.
|
|
2015
|
+
- Gather codebase context first
|
|
2016
|
+
- Send detailed implementation plan to GPT
|
|
2017
|
+
- Revise on OPPOSE/UNCERTAIN \u2014 never skip
|
|
2018
|
+
|
|
2019
|
+
### Phase 1.5: User Approval Gate
|
|
2020
|
+
Present agreed plan. Wait for explicit approval via AskUserQuestion.
|
|
2021
|
+
\`\`\`bash
|
|
2022
|
+
codemoot build event BUILD_ID plan_approved
|
|
2023
|
+
codemoot debate complete DEBATE_ID
|
|
2024
|
+
\`\`\`
|
|
2025
|
+
|
|
2026
|
+
### Phase 2: Implement
|
|
2027
|
+
Write code. Run tests: \`pnpm run test\`
|
|
2028
|
+
Never send broken code to review.
|
|
2029
|
+
\`\`\`bash
|
|
2030
|
+
codemoot build event BUILD_ID impl_completed
|
|
2031
|
+
\`\`\`
|
|
2032
|
+
|
|
2033
|
+
### Phase 3: GPT Review
|
|
2034
|
+
\`\`\`bash
|
|
2035
|
+
codemoot build review BUILD_ID
|
|
2036
|
+
\`\`\`
|
|
2037
|
+
Parse verdict: approved \u2192 Phase 4.5, needs_revision \u2192 Phase 4
|
|
2038
|
+
|
|
2039
|
+
### Phase 4: Fix Issues
|
|
2040
|
+
Fix every CRITICAL and BUG. Run tests. Back to Phase 3.
|
|
2041
|
+
\`\`\`bash
|
|
2042
|
+
codemoot build event BUILD_ID fix_completed
|
|
2043
|
+
\`\`\`
|
|
2044
|
+
|
|
2045
|
+
### Phase 4.5: Completeness Check
|
|
2046
|
+
Compare deliverables against original request. Every requirement must be met.
|
|
2047
|
+
|
|
2048
|
+
### Phase 5: Done
|
|
2049
|
+
\`\`\`bash
|
|
2050
|
+
codemoot build status BUILD_ID
|
|
2051
|
+
\`\`\`
|
|
2052
|
+
Present summary with metrics, requirements checklist, GPT verdict.
|
|
2053
|
+
|
|
2054
|
+
### Rules
|
|
2055
|
+
1. NEVER skip debate rounds
|
|
2056
|
+
2. NEVER skip user approval
|
|
2057
|
+
3. NEVER declare done without completeness check
|
|
2058
|
+
4. Run tests after every implementation/fix
|
|
2059
|
+
5. Zero API cost (ChatGPT subscription)
|
|
2060
|
+
`
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
path: ".claude/skills/cleanup/SKILL.md",
|
|
2064
|
+
description: "/cleanup \u2014 Bidirectional AI slop scanner",
|
|
2065
|
+
content: `---
|
|
2066
|
+
name: cleanup
|
|
2067
|
+
description: Bidirectional AI slop scanner \u2014 Claude + GPT independently analyze, then debate disagreements.
|
|
2068
|
+
user-invocable: true
|
|
2069
|
+
---
|
|
2070
|
+
|
|
2071
|
+
# /cleanup \u2014 Bidirectional AI Slop Scanner
|
|
2072
|
+
|
|
2073
|
+
## Usage
|
|
2074
|
+
\`/cleanup [scope]\` where scope is: deps, unused-exports, hardcoded, duplicates, deadcode, or all
|
|
2075
|
+
|
|
2076
|
+
## Description
|
|
2077
|
+
Claude analyzes independently, then codemoot cleanup runs deterministic regex + GPT scans. 3-way merge with majority-vote confidence.
|
|
2078
|
+
|
|
2079
|
+
## Instructions
|
|
2080
|
+
|
|
2081
|
+
### Phase 1: Claude Independent Analysis
|
|
2082
|
+
Scan the codebase yourself using Grep/Glob/Read. For each scope:
|
|
2083
|
+
- **deps**: Check package.json deps against actual imports
|
|
2084
|
+
- **unused-exports**: Find exported symbols not imported elsewhere
|
|
2085
|
+
- **hardcoded**: Magic numbers, URLs, credentials
|
|
2086
|
+
- **duplicates**: Similar function logic across files
|
|
2087
|
+
- **deadcode**: Declared but never referenced
|
|
2088
|
+
|
|
2089
|
+
Save findings as JSON to a temp file.
|
|
2090
|
+
|
|
2091
|
+
### Phase 2: Run codemoot cleanup
|
|
2092
|
+
\`\`\`bash
|
|
2093
|
+
codemoot cleanup --scope SCOPE --host-findings /path/to/claude-findings.json
|
|
2094
|
+
\`\`\`
|
|
2095
|
+
|
|
2096
|
+
### Phase 3: Present merged results
|
|
2097
|
+
Show summary: total, high confidence, disputed, adjudicated, by source.
|
|
2098
|
+
|
|
2099
|
+
### Phase 4: Rebuttal Round
|
|
2100
|
+
For Claude/GPT disagreements, optionally debate via \`codemoot debate turn\`.
|
|
2101
|
+
`
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
path: ".claude/agents/codex-liaison.md",
|
|
2105
|
+
description: "Codex Liaison agent \u2014 iterates with GPT until 9.5/10",
|
|
2106
|
+
content: `# Codex Liaison Agent
|
|
2107
|
+
|
|
2108
|
+
## Role
|
|
2109
|
+
Specialized teammate that communicates with GPT via Codex CLI to get independent reviews and iterate until quality reaches 9.5/10.
|
|
2110
|
+
|
|
2111
|
+
## How You Work
|
|
2112
|
+
1. Send content to GPT via \`codex exec\` for review
|
|
2113
|
+
2. Parse feedback and score
|
|
2114
|
+
3. If score < 9.5: revise and re-submit
|
|
2115
|
+
4. Loop until 9.5/10 or max 7 iterations
|
|
2116
|
+
5. Report final version back to team lead
|
|
2117
|
+
|
|
2118
|
+
## Calling Codex CLI
|
|
2119
|
+
\`\`\`bash
|
|
2120
|
+
codex exec --skip-git-repo-check -o ".codex-liaison-output.txt" "PROMPT_HERE"
|
|
2121
|
+
\`\`\`
|
|
2122
|
+
|
|
2123
|
+
## Important Rules
|
|
2124
|
+
- NEVER fabricate GPT's responses
|
|
2125
|
+
- NEVER skip iterations if GPT says NEEDS_REVISION
|
|
2126
|
+
- Use your own judgment when GPT's feedback conflicts with project requirements
|
|
2127
|
+
- 9.5/10 threshold is strict
|
|
2128
|
+
`
|
|
2129
|
+
}
|
|
2130
|
+
];
|
|
2131
|
+
var CLAUDE_MD_SECTION = `
|
|
2132
|
+
## CodeMoot \u2014 Multi-Model Collaboration
|
|
2133
|
+
|
|
2134
|
+
This project uses [CodeMoot](https://github.com/katarmal-ram/codemoot) for Claude + GPT collaboration. CodeMoot bridges Claude Code and Codex CLI so they work as partners \u2014 one plans, the other reviews.
|
|
2135
|
+
|
|
2136
|
+
### How Sessions Work
|
|
2137
|
+
- Every \`codemoot\` command uses a **unified session** with GPT via Codex CLI
|
|
2138
|
+
- Sessions persist across commands \u2014 GPT remembers prior reviews, debates, and fixes
|
|
2139
|
+
- Sessions are stored in \`.cowork/db/cowork.db\` (SQLite)
|
|
2140
|
+
- When a session's token budget fills up, it auto-rolls to a new thread
|
|
2141
|
+
- Run \`codemoot session current\` to see the active session
|
|
2142
|
+
|
|
2143
|
+
### Available Commands (use these, not raw codex)
|
|
2144
|
+
- \`codemoot review <file-or-dir>\` \u2014 GPT reviews code with codebase access
|
|
2145
|
+
- \`codemoot review --prompt "question"\` \u2014 GPT explores codebase to answer
|
|
2146
|
+
- \`codemoot review --diff HEAD~3..HEAD\` \u2014 Review git changes
|
|
2147
|
+
- \`codemoot review --preset security-audit\` \u2014 Specialized review presets
|
|
2148
|
+
- \`codemoot fix <file>\` \u2014 Autofix loop: review \u2192 apply fixes \u2192 re-review
|
|
2149
|
+
- \`codemoot debate start "topic"\` \u2014 Multi-round Claude vs GPT debate
|
|
2150
|
+
- \`codemoot cleanup\` \u2014 Scan for unused deps, dead code, duplicates
|
|
2151
|
+
- \`codemoot shipit --profile safe\` \u2014 Composite workflow (lint+test+review)
|
|
2152
|
+
- \`codemoot cost\` \u2014 Token usage dashboard
|
|
2153
|
+
- \`codemoot doctor\` \u2014 Check prerequisites
|
|
2154
|
+
|
|
2155
|
+
### Slash Commands
|
|
2156
|
+
- \`/codex-review\` \u2014 Quick GPT review (uses codemoot review internally)
|
|
2157
|
+
- \`/debate\` \u2014 Start a Claude vs GPT debate
|
|
2158
|
+
- \`/build\` \u2014 Full build loop: debate \u2192 plan \u2192 implement \u2192 GPT review \u2192 fix
|
|
2159
|
+
- \`/cleanup\` \u2014 Bidirectional AI slop scanner
|
|
2160
|
+
|
|
2161
|
+
### When to Use CodeMoot
|
|
2162
|
+
- After implementing a feature \u2192 \`codemoot review src/\`
|
|
2163
|
+
- Before committing \u2192 \`codemoot review --diff HEAD --preset pre-commit\`
|
|
2164
|
+
- Architecture decisions \u2192 \`/debate "REST vs GraphQL?"\`
|
|
2165
|
+
- Full feature build \u2192 \`/build "add user authentication"\`
|
|
2166
|
+
- After shipping \u2192 \`codemoot shipit --profile safe\`
|
|
2167
|
+
|
|
2168
|
+
### Session Tips
|
|
2169
|
+
- Sessions auto-resume \u2014 GPT retains context from prior commands
|
|
2170
|
+
- \`codemoot session list\` shows all sessions with token usage
|
|
2171
|
+
- \`codemoot cost --scope session\` shows current session spend
|
|
2172
|
+
- Start fresh with \`codemoot session start --name "new-feature"\`
|
|
2173
|
+
`;
|
|
2174
|
+
var HOOKS_CONFIG = {
|
|
2175
|
+
hooks: {
|
|
2176
|
+
PostToolUse: [
|
|
2177
|
+
{
|
|
2178
|
+
matcher: "Bash",
|
|
2179
|
+
pattern: "git commit",
|
|
2180
|
+
command: 'echo "Tip: Run codemoot review --diff HEAD~1 for a GPT review of this commit"'
|
|
2181
|
+
}
|
|
2182
|
+
]
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
async function installSkillsCommand(options) {
|
|
2186
|
+
const cwd = process.cwd();
|
|
2187
|
+
let installed = 0;
|
|
2188
|
+
let skipped = 0;
|
|
2189
|
+
console.error(chalk11.cyan("\n Installing CodeMoot integration for Claude Code\n"));
|
|
2190
|
+
console.error(chalk11.dim(" Skills & Agents:"));
|
|
2191
|
+
for (const skill of SKILLS) {
|
|
2192
|
+
const fullPath = join5(cwd, skill.path);
|
|
2193
|
+
const dir = dirname(fullPath);
|
|
2194
|
+
if (existsSync3(fullPath) && !options.force) {
|
|
2195
|
+
console.error(chalk11.dim(` SKIP ${skill.path} (exists)`));
|
|
2196
|
+
skipped++;
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
mkdirSync2(dir, { recursive: true });
|
|
2200
|
+
writeFileSync(fullPath, skill.content, "utf-8");
|
|
2201
|
+
console.error(chalk11.green(` OK ${skill.path}`));
|
|
2202
|
+
installed++;
|
|
2203
|
+
}
|
|
2204
|
+
console.error("");
|
|
2205
|
+
console.error(chalk11.dim(" CLAUDE.md:"));
|
|
2206
|
+
const claudeMdPath = join5(cwd, "CLAUDE.md");
|
|
2207
|
+
const marker = "## CodeMoot \u2014 Multi-Model Collaboration";
|
|
2208
|
+
if (existsSync3(claudeMdPath)) {
|
|
2209
|
+
const existing = readFileSync2(claudeMdPath, "utf-8");
|
|
2210
|
+
if (existing.includes(marker)) {
|
|
2211
|
+
if (options.force) {
|
|
2212
|
+
const markerIdx = existing.indexOf(marker);
|
|
2213
|
+
const before = existing.slice(0, markerIdx);
|
|
2214
|
+
const afterMarker = existing.slice(markerIdx + marker.length);
|
|
2215
|
+
const nextHeadingMatch = afterMarker.match(/\n#{1,2} (?!#)(?!CodeMoot)/);
|
|
2216
|
+
const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
|
|
2217
|
+
writeFileSync(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
|
|
2218
|
+
console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
|
|
2219
|
+
installed++;
|
|
2220
|
+
} else {
|
|
2221
|
+
console.error(chalk11.dim(" SKIP CLAUDE.md (CodeMoot section exists)"));
|
|
2222
|
+
skipped++;
|
|
2223
|
+
}
|
|
2224
|
+
} else {
|
|
2225
|
+
writeFileSync(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
|
|
2226
|
+
console.error(chalk11.green(" OK CLAUDE.md (appended CodeMoot section)"));
|
|
2227
|
+
installed++;
|
|
2228
|
+
}
|
|
2229
|
+
} else {
|
|
2230
|
+
writeFileSync(claudeMdPath, `# Project Instructions
|
|
2231
|
+
${CLAUDE_MD_SECTION}`, "utf-8");
|
|
2232
|
+
console.error(chalk11.green(" OK CLAUDE.md (created with CodeMoot section)"));
|
|
2233
|
+
installed++;
|
|
2234
|
+
}
|
|
2235
|
+
console.error("");
|
|
2236
|
+
console.error(chalk11.dim(" Hooks:"));
|
|
2237
|
+
const settingsDir = join5(cwd, ".claude");
|
|
2238
|
+
const settingsPath = join5(settingsDir, "settings.json");
|
|
2239
|
+
if (existsSync3(settingsPath)) {
|
|
2240
|
+
try {
|
|
2241
|
+
const existing = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
2242
|
+
const hasCodemootHook = Array.isArray(existing.hooks?.PostToolUse) && existing.hooks.PostToolUse.some((h) => h.command?.includes("codemoot"));
|
|
2243
|
+
if (hasCodemootHook && !options.force) {
|
|
2244
|
+
console.error(chalk11.dim(" SKIP .claude/settings.json (codemoot hook exists)"));
|
|
2245
|
+
skipped++;
|
|
2246
|
+
} else {
|
|
2247
|
+
const otherHooks = Array.isArray(existing.hooks?.PostToolUse) ? existing.hooks.PostToolUse.filter((h) => !h.command?.includes("codemoot")) : [];
|
|
2248
|
+
existing.hooks = {
|
|
2249
|
+
...existing.hooks,
|
|
2250
|
+
PostToolUse: [...otherHooks, ...HOOKS_CONFIG.hooks.PostToolUse]
|
|
2251
|
+
};
|
|
2252
|
+
writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
2253
|
+
console.error(chalk11.green(" OK .claude/settings.json (added post-commit hint hook)"));
|
|
2254
|
+
installed++;
|
|
2255
|
+
}
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
console.error(chalk11.yellow(` WARN .claude/settings.json parse error: ${err.message}`));
|
|
2258
|
+
console.error(chalk11.yellow(" Back up and delete the file, then re-run install-skills"));
|
|
2259
|
+
skipped++;
|
|
2260
|
+
}
|
|
2261
|
+
} else {
|
|
2262
|
+
mkdirSync2(settingsDir, { recursive: true });
|
|
2263
|
+
writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
|
|
2264
|
+
console.error(chalk11.green(" OK .claude/settings.json (created with post-commit hook)"));
|
|
2265
|
+
installed++;
|
|
2266
|
+
}
|
|
2267
|
+
console.error("");
|
|
2268
|
+
console.error(chalk11.cyan(` Installed: ${installed}, Skipped: ${skipped}`));
|
|
2269
|
+
console.error("");
|
|
2270
|
+
console.error(chalk11.dim(" Slash commands: /codex-review, /debate, /build, /cleanup"));
|
|
2271
|
+
console.error(chalk11.dim(" CLAUDE.md: Claude now knows about codemoot commands & sessions"));
|
|
2272
|
+
console.error(chalk11.dim(" Hook: Post-commit hint to run codemoot review"));
|
|
2273
|
+
console.error("");
|
|
2274
|
+
const output = {
|
|
2275
|
+
installed,
|
|
2276
|
+
skipped,
|
|
2277
|
+
total: SKILLS.length + 2,
|
|
2278
|
+
// +CLAUDE.md +hooks
|
|
2279
|
+
skills: SKILLS.map((s) => ({ path: s.path, description: s.description }))
|
|
2280
|
+
};
|
|
2281
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/commands/session.ts
|
|
2285
|
+
import { SessionManager as SessionManager6 } from "@codemoot/core";
|
|
2286
|
+
import chalk12 from "chalk";
|
|
2287
|
+
async function sessionStartCommand(options) {
|
|
2288
|
+
await withDatabase(async (db) => {
|
|
2289
|
+
const mgr = new SessionManager6(db);
|
|
2290
|
+
const id = mgr.create(options.name);
|
|
2291
|
+
const session2 = mgr.get(id);
|
|
2292
|
+
mgr.recordEvent({
|
|
2293
|
+
sessionId: id,
|
|
2294
|
+
command: "session",
|
|
2295
|
+
subcommand: "start",
|
|
2296
|
+
promptPreview: `Session started: ${session2?.name ?? id}`
|
|
2297
|
+
});
|
|
2298
|
+
console.log(JSON.stringify({
|
|
2299
|
+
sessionId: id,
|
|
2300
|
+
name: session2?.name ?? null,
|
|
2301
|
+
status: "active",
|
|
2302
|
+
message: "Session created. All GPT commands will now use this session."
|
|
2303
|
+
}, null, 2));
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
async function sessionCurrentCommand() {
|
|
2307
|
+
await withDatabase(async (db) => {
|
|
2308
|
+
const mgr = new SessionManager6(db);
|
|
2309
|
+
const session2 = mgr.getActive();
|
|
2310
|
+
if (!session2) {
|
|
2311
|
+
console.log(JSON.stringify({ active: false, message: 'No active session. Run "codemoot session start" to create one.' }));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
const events = mgr.getEvents(session2.id, 5);
|
|
2315
|
+
const overflow = mgr.getOverflowStatus(session2.id);
|
|
2316
|
+
console.log(JSON.stringify({
|
|
2317
|
+
sessionId: session2.id,
|
|
2318
|
+
name: session2.name,
|
|
2319
|
+
codexThreadId: session2.codexThreadId,
|
|
2320
|
+
status: session2.status,
|
|
2321
|
+
tokenBudget: {
|
|
2322
|
+
used: overflow.cumulativeTokens,
|
|
2323
|
+
lastTurnInput: overflow.lastTurnInputTokens,
|
|
2324
|
+
max: overflow.maxContext,
|
|
2325
|
+
utilization: `${Math.round(overflow.utilizationRatio * 100)}%`
|
|
2326
|
+
},
|
|
2327
|
+
recentEvents: events.map((e) => ({
|
|
2328
|
+
command: e.command,
|
|
2329
|
+
subcommand: e.subcommand,
|
|
2330
|
+
durationMs: e.durationMs,
|
|
2331
|
+
createdAt: new Date(e.createdAt).toISOString()
|
|
2332
|
+
})),
|
|
2333
|
+
createdAt: new Date(session2.createdAt).toISOString(),
|
|
2334
|
+
updatedAt: new Date(session2.updatedAt).toISOString()
|
|
2335
|
+
}, null, 2));
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
async function sessionListCommand(options) {
|
|
2339
|
+
await withDatabase(async (db) => {
|
|
2340
|
+
const mgr = new SessionManager6(db);
|
|
2341
|
+
const sessions = mgr.list({
|
|
2342
|
+
status: options.status,
|
|
2343
|
+
limit: options.limit ?? 20
|
|
2344
|
+
});
|
|
2345
|
+
const output = sessions.map((s) => ({
|
|
2346
|
+
sessionId: s.id,
|
|
2347
|
+
name: s.name,
|
|
2348
|
+
status: s.status,
|
|
2349
|
+
codexThreadId: s.codexThreadId ? `${s.codexThreadId.slice(0, 12)}...` : null,
|
|
2350
|
+
tokenUsage: s.tokenUsage,
|
|
2351
|
+
createdAt: new Date(s.createdAt).toISOString(),
|
|
2352
|
+
updatedAt: new Date(s.updatedAt).toISOString()
|
|
2353
|
+
}));
|
|
2354
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
async function sessionStatusCommand(sessionId) {
|
|
2358
|
+
await withDatabase(async (db) => {
|
|
2359
|
+
const mgr = new SessionManager6(db);
|
|
2360
|
+
const session2 = mgr.get(sessionId);
|
|
2361
|
+
if (!session2) {
|
|
2362
|
+
console.error(chalk12.red(`No session found with ID: ${sessionId}`));
|
|
2363
|
+
process.exit(1);
|
|
2364
|
+
}
|
|
2365
|
+
const events = mgr.getEvents(sessionId, 20);
|
|
2366
|
+
const overflow = mgr.getOverflowStatus(sessionId);
|
|
2367
|
+
console.log(JSON.stringify({
|
|
2368
|
+
sessionId: session2.id,
|
|
2369
|
+
name: session2.name,
|
|
2370
|
+
codexThreadId: session2.codexThreadId,
|
|
2371
|
+
status: session2.status,
|
|
2372
|
+
tokenBudget: {
|
|
2373
|
+
used: overflow.cumulativeTokens,
|
|
2374
|
+
lastTurnInput: overflow.lastTurnInputTokens,
|
|
2375
|
+
max: overflow.maxContext,
|
|
2376
|
+
utilization: `${Math.round(overflow.utilizationRatio * 100)}%`,
|
|
2377
|
+
shouldWarn: overflow.shouldWarn,
|
|
2378
|
+
shouldReconstruct: overflow.shouldReconstruct
|
|
2379
|
+
},
|
|
2380
|
+
eventCount: events.length,
|
|
2381
|
+
events: events.map((e) => ({
|
|
2382
|
+
command: e.command,
|
|
2383
|
+
subcommand: e.subcommand,
|
|
2384
|
+
promptPreview: e.promptPreview,
|
|
2385
|
+
responsePreview: e.responsePreview,
|
|
2386
|
+
durationMs: e.durationMs,
|
|
2387
|
+
createdAt: new Date(e.createdAt).toISOString()
|
|
2388
|
+
})),
|
|
2389
|
+
createdAt: new Date(session2.createdAt).toISOString(),
|
|
2390
|
+
updatedAt: new Date(session2.updatedAt).toISOString(),
|
|
2391
|
+
completedAt: session2.completedAt ? new Date(session2.completedAt).toISOString() : null
|
|
2392
|
+
}, null, 2));
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
async function sessionCloseCommand(sessionId) {
|
|
2396
|
+
await withDatabase(async (db) => {
|
|
2397
|
+
const mgr = new SessionManager6(db);
|
|
2398
|
+
const session2 = mgr.get(sessionId);
|
|
2399
|
+
if (!session2) {
|
|
2400
|
+
console.error(chalk12.red(`No session found with ID: ${sessionId}`));
|
|
2401
|
+
process.exit(1);
|
|
2402
|
+
}
|
|
2403
|
+
mgr.complete(sessionId);
|
|
2404
|
+
console.log(JSON.stringify({ sessionId, status: "completed" }));
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// src/commands/jobs.ts
|
|
2409
|
+
import { JobStore as JobStore2, openDatabase as openDatabase8 } from "@codemoot/core";
|
|
2410
|
+
import chalk13 from "chalk";
|
|
2411
|
+
async function jobsListCommand(options) {
|
|
2412
|
+
let db;
|
|
2413
|
+
try {
|
|
2414
|
+
db = openDatabase8(getDbPath());
|
|
2415
|
+
const store = new JobStore2(db);
|
|
2416
|
+
const jobs2 = store.list({
|
|
2417
|
+
status: options.status,
|
|
2418
|
+
type: options.type,
|
|
2419
|
+
limit: options.limit ?? 20
|
|
2420
|
+
});
|
|
2421
|
+
const output = jobs2.map((j) => ({
|
|
2422
|
+
id: j.id,
|
|
2423
|
+
type: j.type,
|
|
2424
|
+
status: j.status,
|
|
2425
|
+
priority: j.priority,
|
|
2426
|
+
retryCount: j.retryCount,
|
|
2427
|
+
workerId: j.workerId,
|
|
2428
|
+
createdAt: new Date(j.createdAt).toISOString(),
|
|
2429
|
+
startedAt: j.startedAt ? new Date(j.startedAt).toISOString() : null,
|
|
2430
|
+
finishedAt: j.finishedAt ? new Date(j.finishedAt).toISOString() : null
|
|
2431
|
+
}));
|
|
2432
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2433
|
+
db.close();
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
db?.close();
|
|
2436
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
async function jobsLogsCommand(jobId, options) {
|
|
2441
|
+
let db;
|
|
2442
|
+
try {
|
|
2443
|
+
db = openDatabase8(getDbPath());
|
|
2444
|
+
const store = new JobStore2(db);
|
|
2445
|
+
const job = store.get(jobId);
|
|
2446
|
+
if (!job) {
|
|
2447
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2448
|
+
db.close();
|
|
2449
|
+
process.exit(1);
|
|
2450
|
+
}
|
|
2451
|
+
const logs = store.getLogs(jobId, options.fromSeq ?? 0, options.limit ?? 100);
|
|
2452
|
+
const output = {
|
|
2453
|
+
jobId: job.id,
|
|
2454
|
+
type: job.type,
|
|
2455
|
+
status: job.status,
|
|
2456
|
+
logs: logs.map((l) => ({
|
|
2457
|
+
seq: l.seq,
|
|
2458
|
+
level: l.level,
|
|
2459
|
+
event: l.eventType,
|
|
2460
|
+
message: l.message,
|
|
2461
|
+
time: new Date(l.createdAt).toISOString()
|
|
2462
|
+
}))
|
|
2463
|
+
};
|
|
2464
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2465
|
+
db.close();
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
db?.close();
|
|
2468
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2469
|
+
process.exit(1);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async function jobsCancelCommand(jobId) {
|
|
2473
|
+
let db;
|
|
2474
|
+
try {
|
|
2475
|
+
db = openDatabase8(getDbPath());
|
|
2476
|
+
const store = new JobStore2(db);
|
|
2477
|
+
const job = store.get(jobId);
|
|
2478
|
+
if (!job) {
|
|
2479
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2480
|
+
db.close();
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
store.cancel(jobId);
|
|
2484
|
+
console.log(JSON.stringify({ jobId, status: "canceled" }));
|
|
2485
|
+
db.close();
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
db?.close();
|
|
2488
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
async function jobsRetryCommand(jobId) {
|
|
2493
|
+
let db;
|
|
2494
|
+
try {
|
|
2495
|
+
db = openDatabase8(getDbPath());
|
|
2496
|
+
const store = new JobStore2(db);
|
|
2497
|
+
const job = store.get(jobId);
|
|
2498
|
+
if (!job) {
|
|
2499
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2500
|
+
db.close();
|
|
2501
|
+
process.exit(1);
|
|
2502
|
+
}
|
|
2503
|
+
const retried = store.retry(jobId);
|
|
2504
|
+
if (!retried) {
|
|
2505
|
+
console.error(chalk13.red(`Cannot retry job ${jobId}: status=${job.status}, retries=${job.retryCount}/${job.maxRetries}`));
|
|
2506
|
+
db.close();
|
|
2507
|
+
process.exit(1);
|
|
2508
|
+
}
|
|
2509
|
+
console.log(JSON.stringify({ jobId, status: "queued", retryCount: job.retryCount + 1 }));
|
|
2510
|
+
db.close();
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
db?.close();
|
|
2513
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2514
|
+
process.exit(1);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
async function jobsStatusCommand(jobId) {
|
|
2518
|
+
let db;
|
|
2519
|
+
try {
|
|
2520
|
+
db = openDatabase8(getDbPath());
|
|
2521
|
+
const store = new JobStore2(db);
|
|
2522
|
+
const job = store.get(jobId);
|
|
2523
|
+
if (!job) {
|
|
2524
|
+
console.error(chalk13.red(`No job found with ID: ${jobId}`));
|
|
2525
|
+
db.close();
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
}
|
|
2528
|
+
const logs = store.getLogs(jobId, 0, 5);
|
|
2529
|
+
const output = {
|
|
2530
|
+
id: job.id,
|
|
2531
|
+
type: job.type,
|
|
2532
|
+
status: job.status,
|
|
2533
|
+
priority: job.priority,
|
|
2534
|
+
retryCount: job.retryCount,
|
|
2535
|
+
maxRetries: job.maxRetries,
|
|
2536
|
+
workerId: job.workerId,
|
|
2537
|
+
sessionId: job.sessionId,
|
|
2538
|
+
payload: JSON.parse(job.payloadJson),
|
|
2539
|
+
result: job.resultJson ? JSON.parse(job.resultJson) : null,
|
|
2540
|
+
error: job.errorText,
|
|
2541
|
+
recentLogs: logs.map((l) => ({
|
|
2542
|
+
seq: l.seq,
|
|
2543
|
+
level: l.level,
|
|
2544
|
+
event: l.eventType,
|
|
2545
|
+
message: l.message
|
|
2546
|
+
})),
|
|
2547
|
+
createdAt: new Date(job.createdAt).toISOString(),
|
|
2548
|
+
startedAt: job.startedAt ? new Date(job.startedAt).toISOString() : null,
|
|
2549
|
+
finishedAt: job.finishedAt ? new Date(job.finishedAt).toISOString() : null
|
|
2550
|
+
};
|
|
2551
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2552
|
+
db.close();
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
db?.close();
|
|
2555
|
+
console.error(chalk13.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2556
|
+
process.exit(1);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/commands/plan.ts
|
|
2561
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2562
|
+
import { ModelRegistry as ModelRegistry4, Orchestrator, loadConfig as loadConfig6, openDatabase as openDatabase9 } from "@codemoot/core";
|
|
2563
|
+
import chalk15 from "chalk";
|
|
2564
|
+
|
|
2565
|
+
// src/render.ts
|
|
2566
|
+
import chalk14 from "chalk";
|
|
2567
|
+
import ora from "ora";
|
|
2568
|
+
var roleColors = {
|
|
2569
|
+
architect: chalk14.blue,
|
|
2570
|
+
reviewer: chalk14.yellow,
|
|
2571
|
+
implementer: chalk14.green
|
|
2572
|
+
};
|
|
2573
|
+
function getRoleColor(role) {
|
|
2574
|
+
return roleColors[role] ?? chalk14.white;
|
|
2575
|
+
}
|
|
2576
|
+
function renderEvent(event, _config) {
|
|
2577
|
+
switch (event.type) {
|
|
2578
|
+
case "session.started":
|
|
2579
|
+
console.log(chalk14.gray(`
|
|
2580
|
+
\u2501\u2501\u2501 Session ${event.sessionId} \u2501\u2501\u2501`));
|
|
2581
|
+
console.log(chalk14.gray(`Workflow: ${event.workflow}`));
|
|
2582
|
+
console.log(chalk14.gray(`Task: ${event.task}
|
|
2583
|
+
`));
|
|
2584
|
+
break;
|
|
2585
|
+
case "session.completed":
|
|
2586
|
+
console.log(chalk14.green("\n\u2501\u2501\u2501 Session Complete \u2501\u2501\u2501"));
|
|
2587
|
+
console.log(chalk14.cyan(` Cost: $${event.totalCost.toFixed(4)}`));
|
|
2588
|
+
console.log(chalk14.cyan(` Tokens: ${event.totalTokens.toLocaleString()}`));
|
|
2589
|
+
console.log(chalk14.cyan(` Duration: ${(event.durationMs / 1e3).toFixed(1)}s`));
|
|
2590
|
+
break;
|
|
2591
|
+
case "session.failed":
|
|
2592
|
+
console.log(chalk14.red("\n\u2501\u2501\u2501 Session Failed \u2501\u2501\u2501"));
|
|
2593
|
+
console.log(chalk14.red(` Error: ${event.error}`));
|
|
2594
|
+
console.log(chalk14.red(` Last step: ${event.lastStep}`));
|
|
2595
|
+
break;
|
|
2596
|
+
case "step.started": {
|
|
2597
|
+
const color = getRoleColor(event.role);
|
|
2598
|
+
console.log(
|
|
2599
|
+
color(`
|
|
2600
|
+
\u25B6 [${event.role}] ${event.stepId} (${event.model}, iter ${event.iteration})`)
|
|
2601
|
+
);
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
case "step.completed":
|
|
2605
|
+
console.log(
|
|
2606
|
+
chalk14.gray(
|
|
2607
|
+
` \u2713 ${event.stepId} (${(event.durationMs / 1e3).toFixed(1)}s, ${event.tokenUsage.totalTokens} tokens)`
|
|
2608
|
+
)
|
|
2609
|
+
);
|
|
2610
|
+
break;
|
|
2611
|
+
case "step.failed":
|
|
2612
|
+
console.log(chalk14.red(` \u2717 ${event.stepId}: ${event.error}`));
|
|
2613
|
+
break;
|
|
2614
|
+
case "text.delta": {
|
|
2615
|
+
const deltaColor = getRoleColor(event.role);
|
|
2616
|
+
process.stdout.write(deltaColor(event.delta));
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
case "text.done":
|
|
2620
|
+
process.stdout.write("\n");
|
|
2621
|
+
break;
|
|
2622
|
+
case "loop.iteration": {
|
|
2623
|
+
const verdictColor = event.verdict === "approved" ? chalk14.green : chalk14.yellow;
|
|
2624
|
+
console.log(
|
|
2625
|
+
verdictColor(`
|
|
2626
|
+
\u21BB Loop ${event.iteration}/${event.maxIterations}: ${event.verdict}`)
|
|
2627
|
+
);
|
|
2628
|
+
if (event.feedback) {
|
|
2629
|
+
console.log(chalk14.gray(` Feedback: ${event.feedback.slice(0, 200)}...`));
|
|
2630
|
+
}
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
case "cost.update":
|
|
2634
|
+
console.log(
|
|
2635
|
+
chalk14.cyan(
|
|
2636
|
+
` $${event.costUsd.toFixed(4)} (cumulative: $${event.cumulativeSessionCost.toFixed(4)})`
|
|
2637
|
+
)
|
|
2638
|
+
);
|
|
2639
|
+
break;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
function printSessionSummary(result) {
|
|
2643
|
+
console.log(chalk14.bold("\nSession Summary"));
|
|
2644
|
+
console.log(chalk14.gray("-".repeat(40)));
|
|
2645
|
+
console.log(` Session: ${chalk14.white(result.sessionId)}`);
|
|
2646
|
+
console.log(
|
|
2647
|
+
` Status: ${result.status === "completed" ? chalk14.green("completed") : chalk14.red(result.status)}`
|
|
2648
|
+
);
|
|
2649
|
+
console.log(` Cost: ${chalk14.cyan(`$${result.totalCost.toFixed(4)}`)}`);
|
|
2650
|
+
console.log(` Tokens: ${chalk14.cyan(result.totalTokens.toLocaleString())}`);
|
|
2651
|
+
console.log(` Duration: ${chalk14.cyan(`${(result.durationMs / 1e3).toFixed(1)}s`)}`);
|
|
2652
|
+
console.log(` Iterations: ${chalk14.cyan(String(result.iterations))}`);
|
|
2653
|
+
console.log(chalk14.gray("-".repeat(40)));
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// src/commands/plan.ts
|
|
2657
|
+
async function planCommand(task, options) {
|
|
2658
|
+
try {
|
|
2659
|
+
const config = loadConfig6();
|
|
2660
|
+
const projectDir = process.cwd();
|
|
2661
|
+
const registry = ModelRegistry4.fromConfig(config, projectDir);
|
|
2662
|
+
const health = await registry.healthCheckAll();
|
|
2663
|
+
for (const [alias, hasKey] of health) {
|
|
2664
|
+
if (!hasKey) {
|
|
2665
|
+
console.warn(chalk15.yellow(`Warning: No API key for model "${alias}"`));
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
const dbPath = getDbPath();
|
|
2669
|
+
const db = openDatabase9(dbPath);
|
|
2670
|
+
const orchestrator = new Orchestrator({ registry, db, config });
|
|
2671
|
+
orchestrator.on("event", (event) => renderEvent(event, config));
|
|
2672
|
+
const result = await orchestrator.plan(task, {
|
|
2673
|
+
maxRounds: options.rounds
|
|
2674
|
+
});
|
|
2675
|
+
if (options.output) {
|
|
2676
|
+
writeFileSync2(options.output, result.finalOutput, "utf-8");
|
|
2677
|
+
console.log(chalk15.green(`Plan saved to ${options.output}`));
|
|
2678
|
+
}
|
|
2679
|
+
printSessionSummary(result);
|
|
2680
|
+
db.close();
|
|
2681
|
+
process.exit(result.status === "completed" ? 0 : 2);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// src/commands/review.ts
|
|
2689
|
+
import { loadConfig as loadConfig7, ModelRegistry as ModelRegistry5, BINARY_SNIFF_BYTES, REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS3, SessionManager as SessionManager7, JobStore as JobStore3, openDatabase as openDatabase10, buildHandoffEnvelope as buildHandoffEnvelope4, getReviewPreset } from "@codemoot/core";
|
|
2690
|
+
import chalk16 from "chalk";
|
|
2691
|
+
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
2692
|
+
import { closeSync, globSync, openSync, readFileSync as readFileSync3, readSync, statSync, existsSync as existsSync4 } from "fs";
|
|
2693
|
+
import { resolve } from "path";
|
|
2694
|
+
var MAX_FILE_SIZE = 100 * 1024;
|
|
2695
|
+
var MAX_TOTAL_SIZE = 200 * 1024;
|
|
2696
|
+
async function reviewCommand(fileOrGlob, options) {
|
|
2697
|
+
try {
|
|
2698
|
+
const projectDir = process.cwd();
|
|
2699
|
+
const modes = [
|
|
2700
|
+
fileOrGlob ? "file" : "",
|
|
2701
|
+
options.prompt ? "prompt" : "",
|
|
2702
|
+
options.stdin ? "stdin" : "",
|
|
2703
|
+
options.diff ? "diff" : ""
|
|
2704
|
+
].filter(Boolean);
|
|
2705
|
+
if (modes.length === 0) {
|
|
2706
|
+
console.error(chalk16.red("No input specified. Use: <file-or-glob>, --prompt, --stdin, or --diff"));
|
|
2707
|
+
process.exit(1);
|
|
2708
|
+
}
|
|
2709
|
+
if (modes.length > 1) {
|
|
2710
|
+
console.error(chalk16.red(`Conflicting input modes: ${modes.join(", ")}. Use exactly one.`));
|
|
2711
|
+
process.exit(1);
|
|
2712
|
+
}
|
|
2713
|
+
if (options.scope && !options.prompt && !options.stdin) {
|
|
2714
|
+
console.error(chalk16.red("--scope can only be used with --prompt or --stdin"));
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
const config = loadConfig7();
|
|
2718
|
+
const registry = ModelRegistry5.fromConfig(config, projectDir);
|
|
2719
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
2720
|
+
if (!adapter) {
|
|
2721
|
+
try {
|
|
2722
|
+
execSync4("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
2723
|
+
} catch {
|
|
2724
|
+
console.error(chalk16.red("Codex CLI is not installed or not in PATH."));
|
|
2725
|
+
console.error(chalk16.yellow("Install it: npm install -g @openai/codex"));
|
|
2726
|
+
console.error(chalk16.yellow("Then run: codemoot init"));
|
|
2727
|
+
process.exit(1);
|
|
2728
|
+
}
|
|
2729
|
+
console.error(chalk16.red("No codex adapter found in config. Run: codemoot init"));
|
|
2730
|
+
console.error(chalk16.dim("Diagnose: codemoot doctor"));
|
|
2731
|
+
process.exit(1);
|
|
2732
|
+
}
|
|
2733
|
+
if (options.background) {
|
|
2734
|
+
const db2 = openDatabase10(getDbPath());
|
|
2735
|
+
const jobStore = new JobStore3(db2);
|
|
2736
|
+
const jobId = jobStore.enqueue({
|
|
2737
|
+
type: "review",
|
|
2738
|
+
payload: {
|
|
2739
|
+
fileOrGlob: fileOrGlob ?? null,
|
|
2740
|
+
focus: options.focus,
|
|
2741
|
+
timeout: options.timeout,
|
|
2742
|
+
prompt: options.prompt,
|
|
2743
|
+
stdin: options.stdin,
|
|
2744
|
+
diff: options.diff,
|
|
2745
|
+
scope: options.scope,
|
|
2746
|
+
cwd: projectDir
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
console.log(JSON.stringify({ jobId, status: "queued", message: "Review enqueued. Check with: codemoot jobs status " + jobId }));
|
|
2750
|
+
db2.close();
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const db = openDatabase10(getDbPath());
|
|
2754
|
+
const sessionMgr = new SessionManager7(db);
|
|
2755
|
+
const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("review");
|
|
2756
|
+
if (!session2) {
|
|
2757
|
+
console.error(chalk16.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: codemoot init"));
|
|
2758
|
+
db.close();
|
|
2759
|
+
process.exit(1);
|
|
2760
|
+
}
|
|
2761
|
+
const overflowCheck = sessionMgr.preCallOverflowCheck(session2.id);
|
|
2762
|
+
if (overflowCheck.rolled) {
|
|
2763
|
+
console.error(chalk16.yellow(` ${overflowCheck.message}`));
|
|
2764
|
+
}
|
|
2765
|
+
const preset = options.preset ? getReviewPreset(options.preset) : void 0;
|
|
2766
|
+
if (options.preset && !preset) {
|
|
2767
|
+
console.error(chalk16.red(`Unknown preset: ${options.preset}. Use: security-audit, performance, quick-scan, pre-commit, api-review`));
|
|
2768
|
+
db.close();
|
|
2769
|
+
process.exit(1);
|
|
2770
|
+
}
|
|
2771
|
+
const focusArea = preset?.focus ?? options.focus ?? "all";
|
|
2772
|
+
const focusConstraint = focusArea === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focusArea}`;
|
|
2773
|
+
const presetConstraints = preset?.constraints ?? [];
|
|
2774
|
+
const currentSession = sessionMgr.get(session2.id);
|
|
2775
|
+
const sessionThreadId = currentSession?.codexThreadId ?? void 0;
|
|
2776
|
+
const isResumed = Boolean(sessionThreadId);
|
|
2777
|
+
let prompt;
|
|
2778
|
+
let promptPreview;
|
|
2779
|
+
const mode = modes[0];
|
|
2780
|
+
if (mode === "prompt" || mode === "stdin") {
|
|
2781
|
+
let instruction = options.prompt ?? "";
|
|
2782
|
+
if (mode === "stdin") {
|
|
2783
|
+
const chunks = [];
|
|
2784
|
+
for await (const chunk of process.stdin) {
|
|
2785
|
+
chunks.push(chunk);
|
|
2786
|
+
}
|
|
2787
|
+
instruction = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2788
|
+
if (!instruction) {
|
|
2789
|
+
console.error(chalk16.red("No input received from stdin"));
|
|
2790
|
+
db.close();
|
|
2791
|
+
process.exit(1);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
prompt = buildHandoffEnvelope4({
|
|
2795
|
+
command: "review",
|
|
2796
|
+
task: `TASK: ${instruction}
|
|
2797
|
+
|
|
2798
|
+
Start by listing candidate files, then inspect them thoroughly.`,
|
|
2799
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
2800
|
+
scope: options.scope,
|
|
2801
|
+
resumed: isResumed
|
|
2802
|
+
});
|
|
2803
|
+
promptPreview = `Prompt review: ${instruction.slice(0, 100)}`;
|
|
2804
|
+
console.error(chalk16.cyan(`Reviewing via prompt (session: ${session2.id.slice(0, 8)}...)...`));
|
|
2805
|
+
} else if (mode === "diff") {
|
|
2806
|
+
let diff;
|
|
2807
|
+
try {
|
|
2808
|
+
diff = execFileSync2("git", ["diff", ...options.diff.split(/\s+/)], {
|
|
2809
|
+
cwd: projectDir,
|
|
2810
|
+
encoding: "utf-8",
|
|
2811
|
+
maxBuffer: 1024 * 1024
|
|
2812
|
+
});
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
console.error(chalk16.red(`Failed to get diff for ${options.diff}: ${err instanceof Error ? err.message : String(err)}`));
|
|
2815
|
+
db.close();
|
|
2816
|
+
process.exit(1);
|
|
2817
|
+
}
|
|
2818
|
+
if (!diff.trim()) {
|
|
2819
|
+
console.error(chalk16.yellow(`No changes in diff: ${options.diff}`));
|
|
2820
|
+
db.close();
|
|
2821
|
+
process.exit(0);
|
|
2822
|
+
}
|
|
2823
|
+
prompt = buildHandoffEnvelope4({
|
|
2824
|
+
command: "review",
|
|
2825
|
+
task: `Review the following code changes.
|
|
2826
|
+
|
|
2827
|
+
GIT DIFF (${options.diff}):
|
|
2828
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
2829
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
2830
|
+
resumed: isResumed
|
|
2831
|
+
});
|
|
2832
|
+
promptPreview = `Diff review: ${options.diff}`;
|
|
2833
|
+
console.error(chalk16.cyan(`Reviewing diff ${options.diff} (session: ${session2.id.slice(0, 8)}...)...`));
|
|
2834
|
+
} else {
|
|
2835
|
+
let globPattern = fileOrGlob;
|
|
2836
|
+
const resolvedInput = resolve(projectDir, globPattern);
|
|
2837
|
+
if (existsSync4(resolvedInput) && statSync(resolvedInput).isDirectory()) {
|
|
2838
|
+
globPattern = `${globPattern}/**/*`;
|
|
2839
|
+
console.error(chalk16.dim(` Expanding directory to: ${globPattern}`));
|
|
2840
|
+
}
|
|
2841
|
+
const paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve(projectDir, p));
|
|
2842
|
+
if (paths.length === 0) {
|
|
2843
|
+
console.error(chalk16.red(`No files matched: ${fileOrGlob}`));
|
|
2844
|
+
db.close();
|
|
2845
|
+
process.exit(1);
|
|
2846
|
+
}
|
|
2847
|
+
const files = [];
|
|
2848
|
+
let totalSize = 0;
|
|
2849
|
+
for (const filePath of paths) {
|
|
2850
|
+
const stat = statSync(filePath);
|
|
2851
|
+
if (!stat.isFile()) continue;
|
|
2852
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
2853
|
+
console.error(chalk16.yellow(`Skipping ${filePath} (${(stat.size / 1024).toFixed(0)}KB > 100KB limit)`));
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
if (totalSize + stat.size > MAX_TOTAL_SIZE) {
|
|
2857
|
+
console.error(chalk16.yellow(`Skipping remaining files (total would exceed 200KB)`));
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
const buf = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
2861
|
+
const fd = openSync(filePath, "r");
|
|
2862
|
+
const bytesRead = readSync(fd, buf, 0, BINARY_SNIFF_BYTES, 0);
|
|
2863
|
+
closeSync(fd);
|
|
2864
|
+
if (buf.subarray(0, bytesRead).includes(0)) {
|
|
2865
|
+
console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
2869
|
+
const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
2870
|
+
files.push({ path: relativePath, content });
|
|
2871
|
+
totalSize += stat.size;
|
|
2872
|
+
}
|
|
2873
|
+
if (files.length === 0) {
|
|
2874
|
+
console.error(chalk16.red("No readable files to review"));
|
|
2875
|
+
db.close();
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
}
|
|
2878
|
+
const fileContents = files.map((f) => `--- ${f.path} ---
|
|
2879
|
+
${f.content}`).join("\n\n");
|
|
2880
|
+
prompt = buildHandoffEnvelope4({
|
|
2881
|
+
command: "review",
|
|
2882
|
+
task: `Review the following code files.
|
|
2883
|
+
|
|
2884
|
+
FILES TO REVIEW:
|
|
2885
|
+
${fileContents}`,
|
|
2886
|
+
constraints: [focusConstraint, ...presetConstraints],
|
|
2887
|
+
resumed: isResumed
|
|
2888
|
+
});
|
|
2889
|
+
promptPreview = `Review ${files.length} file(s): ${files.map((f) => f.path).join(", ")}`;
|
|
2890
|
+
console.error(chalk16.cyan(`Reviewing ${files.length} file(s) via codex (session: ${session2.id.slice(0, 8)}...)...`));
|
|
2891
|
+
}
|
|
2892
|
+
const timeoutMs = (options.timeout ?? 600) * 1e3;
|
|
2893
|
+
const progress = createProgressCallbacks("review");
|
|
2894
|
+
const result = await adapter.callWithResume(prompt, {
|
|
2895
|
+
sessionId: sessionThreadId,
|
|
2896
|
+
timeout: timeoutMs,
|
|
2897
|
+
...progress
|
|
2898
|
+
});
|
|
2899
|
+
if (result.sessionId) {
|
|
2900
|
+
sessionMgr.updateThreadId(session2.id, result.sessionId);
|
|
2901
|
+
}
|
|
2902
|
+
sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
|
|
2903
|
+
sessionMgr.recordEvent({
|
|
2904
|
+
sessionId: session2.id,
|
|
2905
|
+
command: "review",
|
|
2906
|
+
subcommand: mode,
|
|
2907
|
+
promptPreview: promptPreview.slice(0, 500),
|
|
2908
|
+
responsePreview: result.text.slice(0, 500),
|
|
2909
|
+
promptFull: prompt,
|
|
2910
|
+
responseFull: result.text,
|
|
2911
|
+
usageJson: JSON.stringify(result.usage),
|
|
2912
|
+
durationMs: result.durationMs,
|
|
2913
|
+
codexThreadId: result.sessionId
|
|
2914
|
+
});
|
|
2915
|
+
const findings = [];
|
|
2916
|
+
for (const line of result.text.split("\n")) {
|
|
2917
|
+
const match = line.match(/^-\s*(CRITICAL|WARNING|INFO):\s*(\S+?)(?::(\d+))?\s+(.+)/);
|
|
2918
|
+
if (match) {
|
|
2919
|
+
findings.push({
|
|
2920
|
+
severity: match[1].toLowerCase(),
|
|
2921
|
+
file: match[2],
|
|
2922
|
+
line: match[3] ?? "?",
|
|
2923
|
+
message: match[4]
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const tail = result.text.slice(-500);
|
|
2928
|
+
const verdictMatch = tail.match(/^(?:-\s*)?VERDICT:\s*(APPROVED|NEEDS_REVISION)/m);
|
|
2929
|
+
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
2930
|
+
const output = {
|
|
2931
|
+
mode,
|
|
2932
|
+
findings,
|
|
2933
|
+
verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
|
|
2934
|
+
score: scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null,
|
|
2935
|
+
review: result.text,
|
|
2936
|
+
sessionId: session2.id,
|
|
2937
|
+
codexThreadId: result.sessionId,
|
|
2938
|
+
resumed: sessionThreadId ? result.sessionId === sessionThreadId : false,
|
|
2939
|
+
usage: result.usage,
|
|
2940
|
+
durationMs: result.durationMs
|
|
2941
|
+
};
|
|
2942
|
+
const verdictColor = output.verdict === "approved" ? chalk16.green : chalk16.red;
|
|
2943
|
+
console.error(verdictColor(`
|
|
2944
|
+
Verdict: ${output.verdict.toUpperCase()} (${output.score ?? "?"}/10)`));
|
|
2945
|
+
if (findings.length > 0) {
|
|
2946
|
+
console.error(chalk16.yellow(`Findings (${findings.length}):`));
|
|
2947
|
+
for (const f of findings) {
|
|
2948
|
+
const sev = f.severity === "critical" ? chalk16.red("CRITICAL") : f.severity === "warning" ? chalk16.yellow("WARNING") : chalk16.dim("INFO");
|
|
2949
|
+
console.error(` ${sev} ${f.file}:${f.line} \u2014 ${f.message}`);
|
|
2950
|
+
}
|
|
2951
|
+
} else {
|
|
2952
|
+
console.error(chalk16.green("No issues found."));
|
|
2953
|
+
}
|
|
2954
|
+
console.error(chalk16.dim(`Duration: ${(output.durationMs / 1e3).toFixed(1)}s | Tokens: ${output.usage?.totalTokens ?? "?"}`));
|
|
2955
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2956
|
+
db.close();
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
console.error(chalk16.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2959
|
+
process.exit(1);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/commands/run.ts
|
|
2964
|
+
import { ModelRegistry as ModelRegistry6, Orchestrator as Orchestrator2, loadConfig as loadConfig8, openDatabase as openDatabase11 } from "@codemoot/core";
|
|
2965
|
+
import chalk17 from "chalk";
|
|
2966
|
+
async function runCommand(task, options) {
|
|
2967
|
+
try {
|
|
2968
|
+
const config = loadConfig8();
|
|
2969
|
+
const projectDir = process.cwd();
|
|
2970
|
+
const registry = ModelRegistry6.fromConfig(config, projectDir);
|
|
2971
|
+
const health = await registry.healthCheckAll();
|
|
2972
|
+
for (const [alias, hasKey] of health) {
|
|
2973
|
+
if (!hasKey) {
|
|
2974
|
+
console.warn(chalk17.yellow(`Warning: No API key for model "${alias}"`));
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
const dbPath = getDbPath();
|
|
2978
|
+
const db = openDatabase11(dbPath);
|
|
2979
|
+
const orchestrator = new Orchestrator2({ registry, db, config });
|
|
2980
|
+
orchestrator.on("event", (event) => renderEvent(event, config));
|
|
2981
|
+
const result = await orchestrator.run(task, {
|
|
2982
|
+
mode: options.mode ?? config.mode,
|
|
2983
|
+
maxIterations: options.maxIterations,
|
|
2984
|
+
stream: options.stream
|
|
2985
|
+
});
|
|
2986
|
+
printSessionSummary(result);
|
|
2987
|
+
db.close();
|
|
2988
|
+
process.exit(result.status === "completed" ? 0 : 2);
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
console.error(chalk17.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
2991
|
+
process.exit(1);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// src/commands/shipit.ts
|
|
2996
|
+
import { execSync as execSync5 } from "child_process";
|
|
2997
|
+
import { DEFAULT_RULES as DEFAULT_RULES2, evaluatePolicy as evaluatePolicy2 } from "@codemoot/core";
|
|
2998
|
+
import chalk18 from "chalk";
|
|
2999
|
+
var PROFILES = {
|
|
3000
|
+
fast: ["review"],
|
|
3001
|
+
safe: ["lint", "test", "review", "cleanup"],
|
|
3002
|
+
full: ["lint", "test", "review", "cleanup", "commit"]
|
|
3003
|
+
};
|
|
3004
|
+
function runStep(name, dryRun) {
|
|
3005
|
+
const start = Date.now();
|
|
3006
|
+
if (dryRun) {
|
|
3007
|
+
return { name, status: "skipped", output: "dry-run", durationMs: 0 };
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
let cmd;
|
|
3011
|
+
switch (name) {
|
|
3012
|
+
case "lint":
|
|
3013
|
+
cmd = "npx biome check .";
|
|
3014
|
+
break;
|
|
3015
|
+
case "test":
|
|
3016
|
+
cmd = "pnpm run test";
|
|
3017
|
+
break;
|
|
3018
|
+
case "review":
|
|
3019
|
+
cmd = "codemoot review --preset pre-commit --diff HEAD";
|
|
3020
|
+
break;
|
|
3021
|
+
case "cleanup":
|
|
3022
|
+
cmd = "codemoot cleanup --scope deps";
|
|
3023
|
+
break;
|
|
3024
|
+
case "commit":
|
|
3025
|
+
return { name, status: "skipped", output: "handled by shipit", durationMs: 0 };
|
|
3026
|
+
default:
|
|
3027
|
+
return { name, status: "skipped", output: `unknown step: ${name}`, durationMs: 0 };
|
|
3028
|
+
}
|
|
3029
|
+
const output = execSync5(cmd, {
|
|
3030
|
+
encoding: "utf-8",
|
|
3031
|
+
timeout: 3e5,
|
|
3032
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3033
|
+
});
|
|
3034
|
+
return {
|
|
3035
|
+
name,
|
|
3036
|
+
status: "passed",
|
|
3037
|
+
output: output.slice(0, 2e3),
|
|
3038
|
+
durationMs: Date.now() - start
|
|
3039
|
+
};
|
|
3040
|
+
} catch (err) {
|
|
3041
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3042
|
+
return { name, status: "failed", output: msg.slice(0, 2e3), durationMs: Date.now() - start };
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
async function shipitCommand(options) {
|
|
3046
|
+
const profile = options.profile;
|
|
3047
|
+
const steps = PROFILES[profile];
|
|
3048
|
+
if (!steps) {
|
|
3049
|
+
console.error(chalk18.red(`Unknown profile: ${profile}. Use: fast, safe, full`));
|
|
3050
|
+
process.exit(1);
|
|
3051
|
+
}
|
|
3052
|
+
if (options.dryRun) {
|
|
3053
|
+
console.error(chalk18.cyan(`Shipit dry-run (profile: ${profile})`));
|
|
3054
|
+
console.error(chalk18.dim(`Steps: ${steps.join(" \u2192 ")}`));
|
|
3055
|
+
} else {
|
|
3056
|
+
console.error(chalk18.cyan(`Shipit (profile: ${profile}): ${steps.join(" \u2192 ")}`));
|
|
3057
|
+
}
|
|
3058
|
+
const results = [];
|
|
3059
|
+
let shouldStop = false;
|
|
3060
|
+
for (const step of steps) {
|
|
3061
|
+
if (shouldStop) {
|
|
3062
|
+
results.push({ name: step, status: "skipped", durationMs: 0 });
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
const result = runStep(step, options.dryRun);
|
|
3066
|
+
results.push(result);
|
|
3067
|
+
if (!options.dryRun) {
|
|
3068
|
+
const icon = result.status === "passed" ? chalk18.green("OK") : result.status === "failed" ? chalk18.red("FAIL") : chalk18.dim("SKIP");
|
|
3069
|
+
console.error(` ${icon} ${result.name} (${result.durationMs}ms)`);
|
|
3070
|
+
}
|
|
3071
|
+
if (result.status === "failed" && (step === "lint" || step === "test")) {
|
|
3072
|
+
shouldStop = true;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
const reviewResult = results.find((r) => r.name === "review");
|
|
3076
|
+
const criticalCount = reviewResult?.output?.match(/CRITICAL/gi)?.length ?? 0;
|
|
3077
|
+
const warningCount = reviewResult?.output?.match(/WARNING/gi)?.length ?? 0;
|
|
3078
|
+
const verdictMatch = reviewResult?.output?.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
3079
|
+
const policyCtx = {
|
|
3080
|
+
criticalCount,
|
|
3081
|
+
warningCount,
|
|
3082
|
+
verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
|
|
3083
|
+
stepsCompleted: Object.fromEntries(results.map((r) => [r.name, r.status])),
|
|
3084
|
+
cleanupHighCount: 0
|
|
3085
|
+
};
|
|
3086
|
+
const policyResult = evaluatePolicy2("review.completed", policyCtx, DEFAULT_RULES2);
|
|
3087
|
+
if (policyResult.decision === "block") {
|
|
3088
|
+
console.error(chalk18.red("Policy BLOCKED:"));
|
|
3089
|
+
for (const v of policyResult.violations) {
|
|
3090
|
+
console.error(chalk18.red(` - ${v.message}`));
|
|
3091
|
+
}
|
|
3092
|
+
} else if (policyResult.decision === "warn") {
|
|
3093
|
+
for (const v of policyResult.violations) {
|
|
3094
|
+
console.error(chalk18.yellow(` Warning: ${v.message}`));
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
const output = {
|
|
3098
|
+
profile,
|
|
3099
|
+
steps: results,
|
|
3100
|
+
policy: policyResult,
|
|
3101
|
+
canCommit: policyResult.decision !== "block" && !shouldStop && !options.noCommit
|
|
3102
|
+
};
|
|
3103
|
+
if (options.json) {
|
|
3104
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3105
|
+
} else {
|
|
3106
|
+
const allPassed = results.every((r) => r.status === "passed" || r.status === "skipped");
|
|
3107
|
+
if (allPassed && policyResult.decision !== "block") {
|
|
3108
|
+
console.error(chalk18.green("\nAll checks passed. Ready to commit."));
|
|
3109
|
+
} else {
|
|
3110
|
+
console.error(chalk18.red("\nSome checks failed or policy blocked."));
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/commands/start.ts
|
|
3116
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3117
|
+
import { execFileSync as execFileSync3, execSync as execSync6 } from "child_process";
|
|
3118
|
+
import { join as join6, basename as basename2 } from "path";
|
|
3119
|
+
import chalk19 from "chalk";
|
|
3120
|
+
import { loadConfig as loadConfig9, writeConfig as writeConfig2 } from "@codemoot/core";
|
|
3121
|
+
async function startCommand() {
|
|
3122
|
+
const cwd = process.cwd();
|
|
3123
|
+
console.error(chalk19.cyan("\n CodeMoot \u2014 First Run Setup\n"));
|
|
3124
|
+
console.error(chalk19.dim(" [1/4] Checking Codex CLI..."));
|
|
3125
|
+
let codexVersion = null;
|
|
3126
|
+
try {
|
|
3127
|
+
codexVersion = execSync6("codex --version", { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
3128
|
+
} catch {
|
|
3129
|
+
}
|
|
3130
|
+
if (!codexVersion) {
|
|
3131
|
+
console.error(chalk19.red(" Codex CLI is not installed."));
|
|
3132
|
+
console.error(chalk19.yellow(" Install it: npm install -g @openai/codex"));
|
|
3133
|
+
console.error(chalk19.yellow(" Then run: codemoot start"));
|
|
3134
|
+
process.exit(1);
|
|
3135
|
+
}
|
|
3136
|
+
console.error(chalk19.green(` Codex CLI ${codexVersion} found.`));
|
|
3137
|
+
console.error(chalk19.dim(" [2/4] Checking project config..."));
|
|
3138
|
+
const configPath = join6(cwd, ".cowork.yml");
|
|
3139
|
+
if (existsSync5(configPath)) {
|
|
3140
|
+
console.error(chalk19.green(" .cowork.yml exists \u2014 using it."));
|
|
3141
|
+
} else {
|
|
3142
|
+
const config = loadConfig9({ preset: "cli-first", skipFile: true });
|
|
3143
|
+
config.project.name = basename2(cwd);
|
|
3144
|
+
writeConfig2(config, cwd);
|
|
3145
|
+
console.error(chalk19.green(" Created .cowork.yml with cli-first preset."));
|
|
3146
|
+
}
|
|
3147
|
+
console.error(chalk19.dim(" [3/4] Detecting project..."));
|
|
3148
|
+
const hasGit = existsSync5(join6(cwd, ".git"));
|
|
3149
|
+
const hasSrc = existsSync5(join6(cwd, "src"));
|
|
3150
|
+
const hasPackageJson = existsSync5(join6(cwd, "package.json"));
|
|
3151
|
+
let reviewTarget = "";
|
|
3152
|
+
if (hasGit) {
|
|
3153
|
+
try {
|
|
3154
|
+
const diff = execSync6("git diff --name-only HEAD", { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
|
3155
|
+
if (diff) {
|
|
3156
|
+
const files = diff.split("\n").filter((f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".py"));
|
|
3157
|
+
if (files.length > 0) {
|
|
3158
|
+
reviewTarget = files.slice(0, 10).join(" ");
|
|
3159
|
+
console.error(chalk19.green(` Found ${files.length} changed file(s) \u2014 reviewing those.`));
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
} catch {
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (!reviewTarget) {
|
|
3166
|
+
if (hasSrc) {
|
|
3167
|
+
reviewTarget = "src/";
|
|
3168
|
+
console.error(chalk19.green(" Found src/ directory \u2014 reviewing it."));
|
|
3169
|
+
} else if (hasPackageJson) {
|
|
3170
|
+
reviewTarget = "**/*.ts";
|
|
3171
|
+
console.error(chalk19.green(" TypeScript project \u2014 reviewing *.ts files."));
|
|
3172
|
+
} else {
|
|
3173
|
+
console.error(chalk19.yellow(" No src/ or package.json found. Try: codemoot review <path>"));
|
|
3174
|
+
process.exit(0);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
console.error(chalk19.dim(" [4/4] Running quick review..."));
|
|
3178
|
+
console.error(chalk19.cyan(`
|
|
3179
|
+
codemoot review ${reviewTarget} --preset quick-scan
|
|
3180
|
+
`));
|
|
3181
|
+
try {
|
|
3182
|
+
const output = execFileSync3("codemoot", ["review", reviewTarget, "--preset", "quick-scan"], {
|
|
3183
|
+
cwd,
|
|
3184
|
+
encoding: "utf-8",
|
|
3185
|
+
timeout: 3e5,
|
|
3186
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
3187
|
+
shell: process.platform === "win32"
|
|
3188
|
+
});
|
|
3189
|
+
try {
|
|
3190
|
+
const result = JSON.parse(output);
|
|
3191
|
+
const findingCount = result.findings?.length ?? 0;
|
|
3192
|
+
const verdict = result.verdict ?? "unknown";
|
|
3193
|
+
const score = result.score;
|
|
3194
|
+
console.error("");
|
|
3195
|
+
if (findingCount > 0) {
|
|
3196
|
+
console.error(chalk19.yellow(` Found ${findingCount} issue(s). Score: ${score ?? "?"}/10`));
|
|
3197
|
+
console.error("");
|
|
3198
|
+
console.error(chalk19.cyan(" Next steps:"));
|
|
3199
|
+
console.error(chalk19.dim(` codemoot fix ${reviewTarget} --dry-run # preview fixes`));
|
|
3200
|
+
console.error(chalk19.dim(` codemoot fix ${reviewTarget} # apply fixes`));
|
|
3201
|
+
console.error(chalk19.dim(" codemoot review --preset security-audit # deeper scan"));
|
|
3202
|
+
} else if (verdict === "approved") {
|
|
3203
|
+
console.error(chalk19.green(` Code looks good! Score: ${score ?? "?"}/10`));
|
|
3204
|
+
console.error("");
|
|
3205
|
+
console.error(chalk19.cyan(" Next steps:"));
|
|
3206
|
+
console.error(chalk19.dim(" codemoot review --preset security-audit # security scan"));
|
|
3207
|
+
console.error(chalk19.dim(' codemoot debate start "your question" # debate with GPT'));
|
|
3208
|
+
console.error(chalk19.dim(" codemoot watch # watch for changes"));
|
|
3209
|
+
} else {
|
|
3210
|
+
console.error(chalk19.dim(` Review complete. Verdict: ${verdict}, Score: ${score ?? "?"}/10`));
|
|
3211
|
+
}
|
|
3212
|
+
} catch {
|
|
3213
|
+
console.log(output);
|
|
3214
|
+
}
|
|
3215
|
+
} catch (err) {
|
|
3216
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3217
|
+
if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
|
|
3218
|
+
console.error(chalk19.yellow(" Review timed out. Try: codemoot review --preset quick-scan"));
|
|
3219
|
+
} else {
|
|
3220
|
+
console.error(chalk19.red(` Review failed: ${msg.slice(0, 200)}`));
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
console.error(chalk19.dim(" Tip: Run codemoot install-skills to add /debate, /build, /cleanup"));
|
|
3224
|
+
console.error(chalk19.dim(" slash commands to Claude Code in this project."));
|
|
3225
|
+
console.error("");
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// src/commands/watch.ts
|
|
3229
|
+
import { JobStore as JobStore4, openDatabase as openDatabase12 } from "@codemoot/core";
|
|
3230
|
+
import chalk20 from "chalk";
|
|
3231
|
+
import { watch } from "chokidar";
|
|
3232
|
+
|
|
3233
|
+
// src/watch/debouncer.ts
|
|
3234
|
+
var DEFAULT_CONFIG = {
|
|
3235
|
+
quietMs: 800,
|
|
3236
|
+
maxWaitMs: 5e3,
|
|
3237
|
+
cooldownMs: 1500,
|
|
3238
|
+
maxBatchSize: 50
|
|
3239
|
+
};
|
|
3240
|
+
var batchCounter = 0;
|
|
3241
|
+
var Debouncer = class {
|
|
3242
|
+
config;
|
|
3243
|
+
pending = /* @__PURE__ */ new Map();
|
|
3244
|
+
quietTimer = null;
|
|
3245
|
+
maxWaitTimer = null;
|
|
3246
|
+
cooldownUntil = 0;
|
|
3247
|
+
windowStart = 0;
|
|
3248
|
+
onFlush;
|
|
3249
|
+
destroyed = false;
|
|
3250
|
+
constructor(onFlush, config) {
|
|
3251
|
+
this.onFlush = onFlush;
|
|
3252
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3253
|
+
}
|
|
3254
|
+
push(e) {
|
|
3255
|
+
if (this.destroyed) return false;
|
|
3256
|
+
if (e.ts < this.cooldownUntil) return false;
|
|
3257
|
+
if (this.pending.size === 0) {
|
|
3258
|
+
this.windowStart = e.ts;
|
|
3259
|
+
this.startMaxWaitTimer();
|
|
3260
|
+
}
|
|
3261
|
+
this.pending.set(e.path, e);
|
|
3262
|
+
this.restartQuietTimer();
|
|
3263
|
+
if (this.pending.size >= this.config.maxBatchSize) {
|
|
3264
|
+
this.flush("maxBatch");
|
|
3265
|
+
}
|
|
3266
|
+
return true;
|
|
3267
|
+
}
|
|
3268
|
+
flushNow() {
|
|
3269
|
+
this.flush("manual");
|
|
3270
|
+
}
|
|
3271
|
+
cancel() {
|
|
3272
|
+
this.clearTimers();
|
|
3273
|
+
this.pending.clear();
|
|
3274
|
+
}
|
|
3275
|
+
destroy() {
|
|
3276
|
+
this.cancel();
|
|
3277
|
+
this.destroyed = true;
|
|
3278
|
+
}
|
|
3279
|
+
getPendingCount() {
|
|
3280
|
+
return this.pending.size;
|
|
3281
|
+
}
|
|
3282
|
+
flush(reason) {
|
|
3283
|
+
if (this.pending.size === 0) return;
|
|
3284
|
+
this.clearTimers();
|
|
3285
|
+
const now = Date.now();
|
|
3286
|
+
const batch = {
|
|
3287
|
+
files: [...this.pending.keys()],
|
|
3288
|
+
batchId: `wb-${++batchCounter}`,
|
|
3289
|
+
windowStart: this.windowStart,
|
|
3290
|
+
windowEnd: now,
|
|
3291
|
+
reason
|
|
3292
|
+
};
|
|
3293
|
+
this.pending.clear();
|
|
3294
|
+
this.cooldownUntil = now + this.config.cooldownMs;
|
|
3295
|
+
this.onFlush(batch);
|
|
3296
|
+
}
|
|
3297
|
+
restartQuietTimer() {
|
|
3298
|
+
if (this.quietTimer) clearTimeout(this.quietTimer);
|
|
3299
|
+
this.quietTimer = setTimeout(() => this.flush("quiet"), this.config.quietMs);
|
|
3300
|
+
}
|
|
3301
|
+
startMaxWaitTimer() {
|
|
3302
|
+
if (this.maxWaitTimer) return;
|
|
3303
|
+
this.maxWaitTimer = setTimeout(() => this.flush("maxWait"), this.config.maxWaitMs);
|
|
3304
|
+
}
|
|
3305
|
+
clearTimers() {
|
|
3306
|
+
if (this.quietTimer) {
|
|
3307
|
+
clearTimeout(this.quietTimer);
|
|
3308
|
+
this.quietTimer = null;
|
|
3309
|
+
}
|
|
3310
|
+
if (this.maxWaitTimer) {
|
|
3311
|
+
clearTimeout(this.maxWaitTimer);
|
|
3312
|
+
this.maxWaitTimer = null;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
};
|
|
3316
|
+
|
|
3317
|
+
// src/commands/watch.ts
|
|
3318
|
+
async function watchCommand(options) {
|
|
3319
|
+
const projectDir = process.cwd();
|
|
3320
|
+
const db = openDatabase12(getDbPath());
|
|
3321
|
+
const jobStore = new JobStore4(db);
|
|
3322
|
+
const debouncer = new Debouncer(
|
|
3323
|
+
(batch) => {
|
|
3324
|
+
const dedupeKey = `watch-review:${batch.files.sort().join(",")}`.slice(0, 255);
|
|
3325
|
+
if (jobStore.hasActiveByType("watch-review")) {
|
|
3326
|
+
console.error(
|
|
3327
|
+
chalk20.yellow(` Skipping batch ${batch.batchId} \u2014 watch-review already queued/running`)
|
|
3328
|
+
);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const jobId = jobStore.enqueue({
|
|
3332
|
+
type: "watch-review",
|
|
3333
|
+
payload: {
|
|
3334
|
+
files: batch.files,
|
|
3335
|
+
focus: options.focus,
|
|
3336
|
+
timeout: options.timeout,
|
|
3337
|
+
cwd: projectDir,
|
|
3338
|
+
batchId: batch.batchId,
|
|
3339
|
+
reason: batch.reason
|
|
3340
|
+
},
|
|
3341
|
+
dedupeKey
|
|
3342
|
+
});
|
|
3343
|
+
const event = {
|
|
3344
|
+
type: "watch_batch",
|
|
3345
|
+
jobId,
|
|
3346
|
+
batchId: batch.batchId,
|
|
3347
|
+
files: batch.files.length,
|
|
3348
|
+
reason: batch.reason,
|
|
3349
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3350
|
+
};
|
|
3351
|
+
console.log(JSON.stringify(event));
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
quietMs: options.quietMs,
|
|
3355
|
+
maxWaitMs: options.maxWaitMs,
|
|
3356
|
+
cooldownMs: options.cooldownMs
|
|
3357
|
+
}
|
|
3358
|
+
);
|
|
3359
|
+
const ignored = [
|
|
3360
|
+
"**/node_modules/**",
|
|
3361
|
+
"**/.git/**",
|
|
3362
|
+
"**/dist/**",
|
|
3363
|
+
"**/.cowork/**",
|
|
3364
|
+
"**/coverage/**",
|
|
3365
|
+
"**/*.db",
|
|
3366
|
+
"**/*.db-journal",
|
|
3367
|
+
"**/*.db-wal"
|
|
3368
|
+
];
|
|
3369
|
+
console.error(chalk20.cyan(`Watching ${options.glob} for changes...`));
|
|
3370
|
+
console.error(
|
|
3371
|
+
chalk20.dim(
|
|
3372
|
+
` quiet=${options.quietMs}ms, maxWait=${options.maxWaitMs}ms, cooldown=${options.cooldownMs}ms`
|
|
3373
|
+
)
|
|
3374
|
+
);
|
|
3375
|
+
const watcher = watch(options.glob, {
|
|
3376
|
+
cwd: projectDir,
|
|
3377
|
+
ignored,
|
|
3378
|
+
ignoreInitial: true,
|
|
3379
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
3380
|
+
});
|
|
3381
|
+
watcher.on("all", (event, path) => {
|
|
3382
|
+
if (event === "add" || event === "change" || event === "unlink") {
|
|
3383
|
+
debouncer.push({ path, event, ts: Date.now() });
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
const shutdown = () => {
|
|
3387
|
+
console.error(chalk20.dim("\nShutting down watcher..."));
|
|
3388
|
+
debouncer.flushNow();
|
|
3389
|
+
debouncer.destroy();
|
|
3390
|
+
watcher.close();
|
|
3391
|
+
db.close();
|
|
3392
|
+
process.exit(0);
|
|
3393
|
+
};
|
|
3394
|
+
process.on("SIGINT", shutdown);
|
|
3395
|
+
process.on("SIGTERM", shutdown);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// src/commands/worker.ts
|
|
3399
|
+
import {
|
|
3400
|
+
JobStore as JobStore5,
|
|
3401
|
+
ModelRegistry as ModelRegistry7,
|
|
3402
|
+
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS4,
|
|
3403
|
+
buildHandoffEnvelope as buildHandoffEnvelope5,
|
|
3404
|
+
loadConfig as loadConfig10,
|
|
3405
|
+
openDatabase as openDatabase13
|
|
3406
|
+
} from "@codemoot/core";
|
|
3407
|
+
import chalk21 from "chalk";
|
|
3408
|
+
import { execSync as execSync7 } from "child_process";
|
|
3409
|
+
async function workerCommand(options) {
|
|
3410
|
+
const projectDir = process.cwd();
|
|
3411
|
+
const db = openDatabase13(getDbPath());
|
|
3412
|
+
const jobStore = new JobStore5(db);
|
|
3413
|
+
const config = loadConfig10();
|
|
3414
|
+
const registry = ModelRegistry7.fromConfig(config, projectDir);
|
|
3415
|
+
const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
|
|
3416
|
+
if (!adapter) {
|
|
3417
|
+
try {
|
|
3418
|
+
execSync7("codex --version", { stdio: "pipe", encoding: "utf-8" });
|
|
3419
|
+
} catch {
|
|
3420
|
+
console.error(chalk21.red("Codex CLI is not installed or not in PATH."));
|
|
3421
|
+
console.error(chalk21.yellow("Install it: npm install -g @openai/codex"));
|
|
3422
|
+
db.close();
|
|
3423
|
+
process.exit(1);
|
|
3424
|
+
}
|
|
3425
|
+
console.error(chalk21.red("No codex adapter found in config. Run: codemoot init"));
|
|
3426
|
+
db.close();
|
|
3427
|
+
process.exit(1);
|
|
3428
|
+
}
|
|
3429
|
+
const workerId = options.workerId;
|
|
3430
|
+
console.error(
|
|
3431
|
+
chalk21.cyan(`Worker ${workerId} started (poll: ${options.pollMs}ms, once: ${options.once})`)
|
|
3432
|
+
);
|
|
3433
|
+
let running = true;
|
|
3434
|
+
const shutdown = () => {
|
|
3435
|
+
running = false;
|
|
3436
|
+
console.error(chalk21.dim("\nWorker shutting down..."));
|
|
3437
|
+
};
|
|
3438
|
+
process.on("SIGINT", shutdown);
|
|
3439
|
+
process.on("SIGTERM", shutdown);
|
|
3440
|
+
while (running) {
|
|
3441
|
+
const job = jobStore.claimNext(workerId)[0];
|
|
3442
|
+
if (!job) {
|
|
3443
|
+
if (options.once) {
|
|
3444
|
+
console.error(chalk21.dim("No jobs in queue. Exiting (--once mode)."));
|
|
3445
|
+
break;
|
|
3446
|
+
}
|
|
3447
|
+
await new Promise((r) => setTimeout(r, options.pollMs));
|
|
3448
|
+
continue;
|
|
3449
|
+
}
|
|
3450
|
+
console.error(chalk21.cyan(`Processing job ${job.id} (type: ${job.type})`));
|
|
3451
|
+
jobStore.appendLog(job.id, "info", "job_started", `Worker ${workerId} claimed job`);
|
|
3452
|
+
try {
|
|
3453
|
+
const { resolve: resolve2, normalize } = await import("path");
|
|
3454
|
+
const payload = JSON.parse(job.payloadJson);
|
|
3455
|
+
const rawCwd = resolve2(payload.path ?? payload.cwd ?? projectDir);
|
|
3456
|
+
const cwd = normalize(rawCwd);
|
|
3457
|
+
if (!cwd.startsWith(normalize(projectDir))) {
|
|
3458
|
+
throw new Error(`Path traversal blocked: "${cwd}" is outside project directory "${projectDir}"`);
|
|
3459
|
+
}
|
|
3460
|
+
const timeout = (payload.timeout ?? 600) * 1e3;
|
|
3461
|
+
let prompt;
|
|
3462
|
+
if (job.type === "review" || job.type === "watch-review") {
|
|
3463
|
+
const focus = payload.focus ?? "all";
|
|
3464
|
+
const focusConstraint = focus === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focus}`;
|
|
3465
|
+
if (payload.prompt) {
|
|
3466
|
+
prompt = buildHandoffEnvelope5({
|
|
3467
|
+
command: "review",
|
|
3468
|
+
task: `TASK: ${payload.prompt}
|
|
3469
|
+
|
|
3470
|
+
Start by listing candidate files, then inspect them thoroughly.`,
|
|
3471
|
+
constraints: [focusConstraint],
|
|
3472
|
+
resumed: false
|
|
3473
|
+
});
|
|
3474
|
+
} else if (payload.diff) {
|
|
3475
|
+
const { execFileSync: execFileSync4 } = await import("child_process");
|
|
3476
|
+
const diffArgs = payload.diff.split(/\s+/).filter((a) => a.length > 0);
|
|
3477
|
+
for (const arg of diffArgs) {
|
|
3478
|
+
if (arg.startsWith("-") || !/^[a-zA-Z0-9_.~^:\/\\@{}]+$/.test(arg)) {
|
|
3479
|
+
throw new Error(`Invalid diff argument: "${arg}" \u2014 only git refs and paths allowed`);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
const diff = execFileSync4("git", ["diff", ...diffArgs], {
|
|
3483
|
+
cwd,
|
|
3484
|
+
encoding: "utf-8",
|
|
3485
|
+
maxBuffer: 1024 * 1024
|
|
3486
|
+
});
|
|
3487
|
+
prompt = buildHandoffEnvelope5({
|
|
3488
|
+
command: "review",
|
|
3489
|
+
task: `Review these code changes.
|
|
3490
|
+
|
|
3491
|
+
GIT DIFF:
|
|
3492
|
+
${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
|
|
3493
|
+
constraints: [focusConstraint],
|
|
3494
|
+
resumed: false
|
|
3495
|
+
});
|
|
3496
|
+
} else if (payload.files && Array.isArray(payload.files)) {
|
|
3497
|
+
prompt = buildHandoffEnvelope5({
|
|
3498
|
+
command: "review",
|
|
3499
|
+
task: `Review these files: ${payload.files.join(", ")}. Read each file and report issues.`,
|
|
3500
|
+
constraints: [focusConstraint],
|
|
3501
|
+
resumed: false
|
|
3502
|
+
});
|
|
3503
|
+
} else {
|
|
3504
|
+
prompt = buildHandoffEnvelope5({
|
|
3505
|
+
command: "review",
|
|
3506
|
+
task: "Review the codebase for issues. Start by listing key files.",
|
|
3507
|
+
constraints: [focusConstraint],
|
|
3508
|
+
resumed: false
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
} else if (job.type === "cleanup") {
|
|
3512
|
+
prompt = buildHandoffEnvelope5({
|
|
3513
|
+
command: "cleanup",
|
|
3514
|
+
task: `Scan ${cwd} for: unused dependencies, dead code, duplicates, hardcoded values. Report findings with confidence levels.`,
|
|
3515
|
+
constraints: [`Scope: ${payload.scope ?? "all"}`],
|
|
3516
|
+
resumed: false
|
|
3517
|
+
});
|
|
3518
|
+
} else {
|
|
3519
|
+
jobStore.fail(job.id, `Unsupported job type: ${job.type}`);
|
|
3520
|
+
continue;
|
|
3521
|
+
}
|
|
3522
|
+
jobStore.appendLog(job.id, "info", "codex_started", "Sending to codex...");
|
|
3523
|
+
const progress = createProgressCallbacks("worker");
|
|
3524
|
+
const result = await adapter.callWithResume(prompt, {
|
|
3525
|
+
timeout,
|
|
3526
|
+
...progress
|
|
3527
|
+
});
|
|
3528
|
+
jobStore.appendLog(
|
|
3529
|
+
job.id,
|
|
3530
|
+
"info",
|
|
3531
|
+
"codex_completed",
|
|
3532
|
+
`Received ${result.text.length} chars in ${result.durationMs}ms`
|
|
3533
|
+
);
|
|
3534
|
+
const resultData = {
|
|
3535
|
+
text: result.text,
|
|
3536
|
+
usage: result.usage,
|
|
3537
|
+
durationMs: result.durationMs,
|
|
3538
|
+
sessionId: result.sessionId
|
|
3539
|
+
};
|
|
3540
|
+
jobStore.succeed(job.id, resultData);
|
|
3541
|
+
console.error(chalk21.green(`Job ${job.id} completed (${result.durationMs}ms)`));
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3544
|
+
jobStore.appendLog(job.id, "error", "job_failed", errMsg);
|
|
3545
|
+
jobStore.fail(job.id, errMsg);
|
|
3546
|
+
console.error(chalk21.red(`Job ${job.id} failed: ${errMsg}`));
|
|
3547
|
+
}
|
|
3548
|
+
if (options.once) break;
|
|
3549
|
+
}
|
|
3550
|
+
db.close();
|
|
3551
|
+
console.error(chalk21.dim("Worker stopped."));
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
// src/index.ts
|
|
3555
|
+
var program = new Command();
|
|
3556
|
+
program.name("codemoot").description("Multi-model collaborative AI development tool").version(VERSION2).option("--verbose", "Enable debug logging");
|
|
3557
|
+
program.command("start").description("First-run setup: verify codex, init config, run quick review").action(startCommand);
|
|
3558
|
+
program.command("doctor").description("Preflight diagnostics: check codex, config, database, git, node").action(doctorCommand);
|
|
3559
|
+
program.command("install-skills").description("Install Claude Code slash commands (/debate, /build, /codex-review, /cleanup)").option("--force", "Overwrite existing skill files", false).action(installSkillsCommand);
|
|
3560
|
+
program.command("init").description("Initialize CodeMoot in the current project").option("--preset <name>", "Use preset (balanced|budget)").option("--non-interactive", "Skip prompts, use defaults").option("--force", "Overwrite existing .cowork.yml").action(initCommand);
|
|
3561
|
+
program.command("run").description("Run a task through the full workflow").argument("<task>", "Task description (natural language)").option("--mode <mode>", "Execution mode (autonomous|interactive)", "autonomous").option("--max-iterations <n>", "Max review loop iterations", (v) => Number.parseInt(v, 10), 3).option("--no-stream", "Disable streaming output").action(runCommand);
|
|
3562
|
+
program.command("review").description("Review code via codex \u2014 files, prompts, or diffs").argument("[file-or-glob]", "File path or glob pattern to review").option("--prompt <instruction>", "Freeform prompt \u2014 codex explores codebase via tools").option("--stdin", "Read prompt from stdin").option("--diff <revspec>", "Review a git diff (e.g., HEAD~3..HEAD, origin/main...HEAD)").option("--scope <glob>", "Restrict codex exploration to matching files (only with --prompt/--stdin)").addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("all")).option("--preset <name>", "Use named preset (security-audit|performance|quick-scan|pre-commit|api-review)").option("--session <id>", "Use specific session (default: active session)").option("--background", "Enqueue review and return immediately").option("--timeout <seconds>", "Timeout in seconds", (v) => {
|
|
3563
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
3564
|
+
const n = Number.parseInt(v, 10);
|
|
3565
|
+
if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
3566
|
+
return n;
|
|
3567
|
+
}, 600).action(reviewCommand);
|
|
3568
|
+
program.command("cleanup").description("Scan codebase for AI slop: security vulns, anti-patterns, near-duplicates, dead code, and more").argument("[path]", "Project path to scan", ".").addOption(new Option("--scope <scope>", "What to scan for").choices(["deps", "unused-exports", "hardcoded", "duplicates", "deadcode", "security", "near-duplicates", "anti-patterns", "all"]).default("all")).option("--timeout <seconds>", "Codex scan timeout in seconds", (v) => {
|
|
3569
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
3570
|
+
const n = Number.parseInt(v, 10);
|
|
3571
|
+
if (n <= 0) throw new InvalidArgumentError("Timeout must be a positive integer");
|
|
3572
|
+
return n;
|
|
3573
|
+
}, CLEANUP_TIMEOUT_SEC).option("--max-disputes <n>", "Max findings to adjudicate", (v) => {
|
|
3574
|
+
if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Must be a non-negative integer");
|
|
3575
|
+
return Number.parseInt(v, 10);
|
|
3576
|
+
}, 10).option("--host-findings <path>", "JSON file with host AI findings for 3-way merge").option("--output <path>", "Write findings report to JSON file").option("--background", "Enqueue cleanup and return immediately").option("--no-gitignore", "Skip .gitignore rules (scan everything)").option("--quiet", "Suppress human-readable summary").action(cleanupCommand);
|
|
3577
|
+
program.command("plan").description("Generate a plan using architect + reviewer loop").argument("<task>", "Task to plan").option("--rounds <n>", "Max plan-review rounds", (v) => Number.parseInt(v, 10), 3).option("--output <file>", "Save plan to file").action(planCommand);
|
|
3578
|
+
var debate = program.command("debate").description("Multi-model debate with session persistence");
|
|
3579
|
+
debate.command("start").description("Start a new debate").argument("<topic>", "Debate topic or question").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).action(debateStartCommand);
|
|
3580
|
+
debate.command("turn").description("Send a prompt to GPT and get critique (with session resume)").argument("<debate-id>", "Debate ID from start command").argument("<prompt>", "Prompt to send to GPT").option("--round <n>", "Round number", (v) => Number.parseInt(v, 10)).option("--timeout <seconds>", "Timeout in seconds", (v) => Number.parseInt(v, 10), 600).action(debateTurnCommand);
|
|
3581
|
+
debate.command("status").description("Show debate status and session info").argument("<debate-id>", "Debate ID").action(debateStatusCommand);
|
|
3582
|
+
debate.command("list").description("List all debates").option("--status <status>", "Filter by status (active|completed|stale)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(debateListCommand);
|
|
3583
|
+
debate.command("history").description("Show full message history with token budget").argument("<debate-id>", "Debate ID").action(debateHistoryCommand);
|
|
3584
|
+
debate.command("complete").description("Mark a debate as completed").argument("<debate-id>", "Debate ID").action(debateCompleteCommand);
|
|
3585
|
+
var build = program.command("build").description("Automated build loop: debate \u2192 plan \u2192 implement \u2192 review \u2192 fix");
|
|
3586
|
+
build.command("start").description("Start a new build session").argument("<task>", "Task description").option("--max-rounds <n>", "Max debate rounds", (v) => Number.parseInt(v, 10), 5).option("--allow-dirty", "Allow starting with dirty working tree (auto-stashes)").action(buildStartCommand);
|
|
3587
|
+
build.command("status").description("Show build status and event log").argument("<build-id>", "Build ID").action(buildStatusCommand);
|
|
3588
|
+
build.command("list").description("List all builds").option("--status <status>", "Filter by status").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(buildListCommand);
|
|
3589
|
+
build.command("event").description("Record a build event (phase transition)").argument("<build-id>", "Build ID").argument("<event-type>", "Event type (plan_approved|impl_completed|fix_completed|etc)").option("--loop <n>", "Loop index", (v) => Number.parseInt(v, 10)).option("--tokens <n>", "Tokens used", (v) => Number.parseInt(v, 10)).action(buildEventCommand);
|
|
3590
|
+
build.command("review").description("Send implementation to codex for review (with codebase access)").argument("<build-id>", "Build ID").action(buildReviewCommand);
|
|
3591
|
+
var session = program.command("session").description("Unified session management \u2014 persistent GPT context across commands");
|
|
3592
|
+
session.command("start").description("Start a new session").option("--name <name>", "Session name").action(sessionStartCommand);
|
|
3593
|
+
session.command("current").description("Show the active session").action(sessionCurrentCommand);
|
|
3594
|
+
session.command("list").description("List all sessions").option("--status <status>", "Filter by status (active|completed|stale)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(sessionListCommand);
|
|
3595
|
+
session.command("status").description("Show detailed session info with events").argument("<session-id>", "Session ID").action(sessionStatusCommand);
|
|
3596
|
+
session.command("close").description("Mark a session as completed").argument("<session-id>", "Session ID").action(sessionCloseCommand);
|
|
3597
|
+
var jobs = program.command("jobs").description("Background job queue \u2014 async reviews, cleanups, and more");
|
|
3598
|
+
jobs.command("list").description("List jobs").option("--status <status>", "Filter by status (queued|running|succeeded|failed|canceled)").option("--type <type>", "Filter by type (review|cleanup|build-review|composite|watch-review)").option("--limit <n>", "Max results", (v) => Number.parseInt(v, 10), 20).action(jobsListCommand);
|
|
3599
|
+
jobs.command("status").description("Show job details with recent logs").argument("<job-id>", "Job ID").action(jobsStatusCommand);
|
|
3600
|
+
jobs.command("logs").description("Show job logs").argument("<job-id>", "Job ID").option("--from-seq <n>", "Start from log sequence number", (v) => Number.parseInt(v, 10), 0).option("--limit <n>", "Max log entries", (v) => Number.parseInt(v, 10), 100).action(jobsLogsCommand);
|
|
3601
|
+
jobs.command("cancel").description("Cancel a queued or running job").argument("<job-id>", "Job ID").action(jobsCancelCommand);
|
|
3602
|
+
jobs.command("retry").description("Retry a failed job").argument("<job-id>", "Job ID").action(jobsRetryCommand);
|
|
3603
|
+
program.command("fix").description("Autofix loop: review code, apply fixes, re-review until approved").argument("<file-or-glob>", "File path or glob pattern to fix").option("--max-rounds <n>", "Max review-fix rounds", (v) => Number.parseInt(v, 10), 3).addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("bugs")).option("--timeout <seconds>", "Timeout per round", (v) => Number.parseInt(v, 10), 600).option("--dry-run", "Review only, do not apply fixes", false).option("--diff <revspec>", "Fix issues in a git diff").option("--session <id>", "Use specific session").action(fixCommand);
|
|
3604
|
+
program.command("shipit").description("Run composite workflow: lint \u2192 test \u2192 review \u2192 cleanup \u2192 commit").addOption(new Option("--profile <profile>", "Workflow profile").choices(["fast", "safe", "full"]).default("safe")).option("--dry-run", "Print planned steps without executing", false).option("--no-commit", "Run checks but skip commit step").option("--json", "Machine-readable JSON output", false).option("--strict-output", "Strict model output parsing", false).action(shipitCommand);
|
|
3605
|
+
program.command("cost").description("Token usage and cost dashboard").addOption(new Option("--scope <scope>", "Time scope").choices(["session", "daily", "all"]).default("daily")).option("--days <n>", "Number of days for daily scope", (v) => Number.parseInt(v, 10), 30).option("--session <id>", "Session ID for session scope").action(costCommand);
|
|
3606
|
+
program.command("watch").description("Watch files and enqueue reviews on change").option("--glob <pattern>", "Glob pattern to watch", "**/*.{ts,tsx,js,jsx}").addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("all")).option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 600).option("--quiet-ms <ms>", "Quiet period before flush", (v) => Number.parseInt(v, 10), 800).option("--max-wait-ms <ms>", "Max wait before forced flush", (v) => Number.parseInt(v, 10), 5e3).option("--cooldown-ms <ms>", "Cooldown after flush", (v) => Number.parseInt(v, 10), 1500).action(watchCommand);
|
|
3607
|
+
program.command("events").description("Stream session events and job logs as JSONL").option("--follow", "Follow mode \u2014 poll for new events", false).option("--since-seq <n>", "Start from sequence number", (v) => Number.parseInt(v, 10), 0).option("--limit <n>", "Max events per poll", (v) => Number.parseInt(v, 10), 100).addOption(new Option("--type <type>", "Event source filter").choices(["all", "sessions", "jobs"]).default("all")).action(eventsCommand);
|
|
3608
|
+
jobs.command("worker").description("Start background job worker (processes queued jobs)").option("--once", "Process one job and exit", false).option("--poll-ms <ms>", "Poll interval in milliseconds", (v) => Number.parseInt(v, 10), 2e3).option("--worker-id <id>", "Worker identifier", `w-${Date.now()}`).action(workerCommand);
|
|
3609
|
+
program.parse();
|
|
3610
|
+
export {
|
|
3611
|
+
program
|
|
3612
|
+
};
|
|
3613
|
+
//# sourceMappingURL=index.js.map
|