@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/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