@codemoot/cli 0.2.5 → 0.2.8

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 CHANGED
@@ -13,10 +13,13 @@ import { join as join2 } from "path";
13
13
 
14
14
  // src/progress.ts
15
15
  import chalk from "chalk";
16
- var THROTTLE_MS = 3e3;
16
+ var THROTTLE_MS = 3e4;
17
+ var MAX_CARRY_OVER = 64 * 1024;
17
18
  function createProgressCallbacks(label = "codex") {
18
19
  let lastActivityAt = 0;
19
20
  let lastMessage = "";
21
+ let carryOver = "";
22
+ let droppedEvents = 0;
20
23
  function printActivity(msg) {
21
24
  const now = Date.now();
22
25
  if (msg === lastMessage && now - lastActivityAt < THROTTLE_MS) return;
@@ -24,25 +27,47 @@ function createProgressCallbacks(label = "codex") {
24
27
  lastMessage = msg;
25
28
  console.error(chalk.dim(` [${label}] ${msg}`));
26
29
  }
30
+ function parseLine(line) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed) return;
33
+ try {
34
+ const event = JSON.parse(trimmed);
35
+ formatEvent(event, printActivity);
36
+ } catch {
37
+ droppedEvents++;
38
+ }
39
+ }
27
40
  return {
28
41
  onSpawn(pid, command) {
29
- console.error(chalk.dim(` [${label}] Started (PID: ${pid}, cmd: ${command})`));
42
+ const exe = command.replace(/^"([^"]+)".*/, "$1").split(/[\s/\\]+/).pop() ?? command;
43
+ console.error(chalk.dim(` [${label}] Started (PID: ${pid}, cmd: ${exe})`));
30
44
  },
31
45
  onStderr(_chunk) {
32
46
  },
33
47
  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
- }
48
+ const data = carryOver + chunk;
49
+ const lines = data.split("\n");
50
+ carryOver = lines.pop() ?? "";
51
+ if (carryOver.length > MAX_CARRY_OVER) {
52
+ carryOver = "";
53
+ droppedEvents++;
54
+ }
55
+ for (const line of lines) {
56
+ parseLine(line);
57
+ }
58
+ },
59
+ onClose() {
60
+ if (carryOver.trim()) {
61
+ parseLine(carryOver);
62
+ carryOver = "";
63
+ }
64
+ if (droppedEvents > 0) {
65
+ console.error(chalk.dim(` [${label}] ${droppedEvents} event(s) dropped (parse errors or buffer overflow)`));
66
+ droppedEvents = 0;
42
67
  }
43
68
  },
44
69
  onHeartbeat(elapsedSec) {
45
- if (elapsedSec % 30 === 0) {
70
+ if (elapsedSec % 60 === 0) {
46
71
  printActivity(`${elapsedSec}s elapsed...`);
47
72
  }
48
73
  }
@@ -60,7 +85,8 @@ function formatEvent(event, print) {
60
85
  if (!item) return;
61
86
  if (item.type === "tool_call" || item.type === "function_call") {
62
87
  const name = item.name ?? item.function ?? "tool";
63
- const args = String(item.arguments ?? item.input ?? "").slice(0, 80);
88
+ const rawArgs = item.arguments ?? item.input ?? "";
89
+ const args = (typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs)).slice(0, 80);
64
90
  const pathMatch = args.match(/["']([^"']*\.[a-z]{1,4})["']/i);
65
91
  if (pathMatch) {
66
92
  print(`${name}: ${pathMatch[1]}`);
@@ -69,15 +95,6 @@ function formatEvent(event, print) {
69
95
  }
70
96
  return;
71
97
  }
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
98
  }
82
99
  if (type === "turn.completed") {
83
100
  const usage = event.usage;
@@ -377,9 +394,9 @@ async function buildReviewCommand(buildId) {
377
394
  try {
378
395
  const tmpIndex = join2(projectDir, ".git", "codemoot-review-index");
379
396
  try {
380
- execSync(`git read-tree HEAD`, { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
381
- execSync("git add -A", { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
382
- diff = execFileSync("git", ["diff", "--cached", "--", run.baselineRef], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
397
+ execFileSync("git", ["read-tree", "HEAD"], { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
398
+ execFileSync("git", ["add", "-A"], { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
399
+ diff = execFileSync("git", ["diff", "--cached", run.baselineRef, "--"], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
383
400
  } finally {
384
401
  try {
385
402
  unlinkSync(tmpIndex);
@@ -490,7 +507,7 @@ Review for:
490
507
  }
491
508
  const output = {
492
509
  buildId,
493
- review: result.text,
510
+ review: result.text.slice(0, 2e3),
494
511
  verdict: approved ? "approved" : "needs_revision",
495
512
  sessionId: result.sessionId,
496
513
  resumed: existingSession ? result.sessionId === existingSession : false,
@@ -612,7 +629,7 @@ async function debateTurnCommand(debateId, prompt, options) {
612
629
  const output = {
613
630
  debateId,
614
631
  round: newRound,
615
- response: existing.responseText,
632
+ response: existing.responseText?.slice(0, 2e3) ?? "",
616
633
  sessionId: existing.sessionId,
617
634
  resumed: false,
618
635
  cached: true,
@@ -657,7 +674,7 @@ async function debateTurnCommand(debateId, prompt, options) {
657
674
  const output = {
658
675
  debateId,
659
676
  round: newRound,
660
- response: recheckRow.responseText,
677
+ response: recheckRow.responseText?.slice(0, 2e3) ?? "",
661
678
  sessionId: recheckRow.sessionId,
662
679
  resumed: false,
663
680
  cached: true,
@@ -767,7 +784,8 @@ async function debateTurnCommand(debateId, prompt, options) {
767
784
  const output = {
768
785
  debateId,
769
786
  round: newRound,
770
- response: result.text,
787
+ response: result.text.slice(0, 2e3),
788
+ responseTruncated: result.text.length > 2e3,
771
789
  sessionId: result.sessionId,
772
790
  resumed,
773
791
  cached: false,
@@ -1185,7 +1203,9 @@ Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
1185
1203
  writeFileSync4(options.output, JSON.stringify(report, null, 2), "utf-8");
1186
1204
  console.error(chalk5.green(` Findings written to ${options.output}`));
1187
1205
  }
1188
- console.log(JSON.stringify(report, null, 2));
1206
+ const reportJson = JSON.stringify(report, null, 2);
1207
+ console.log(reportJson.length > 2e3 ? `${reportJson.slice(0, 2e3)}
1208
+ ... (truncated, ${reportJson.length} chars total \u2014 use --output to save full report)` : reportJson);
1189
1209
  db.close();
1190
1210
  } catch (error) {
1191
1211
  db?.close();
@@ -2073,6 +2093,59 @@ Ask if user wants to fix and re-review. If yes:
2073
2093
  - **No arg size limits**: Content is piped via stdin, not passed as CLI args.
2074
2094
  - **Presets**: Use --preset for specialized reviews (security-audit, performance, quick-scan, pre-commit, api-review).
2075
2095
  - **Background mode**: Add --background to enqueue and continue working.
2096
+ - **Output capped**: JSON \`review\` field is capped to 2KB to prevent terminal crashes. Full text is stored in session_events.
2097
+ - **Progress**: Heartbeat every 60s, throttled to 30s intervals. No agent_message dumps.
2098
+ `
2099
+ },
2100
+ {
2101
+ path: ".claude/skills/plan-review/SKILL.md",
2102
+ description: "/plan-review \u2014 GPT review of execution plans",
2103
+ content: `---
2104
+ name: plan-review
2105
+ description: Send execution plans to GPT for structured review via Codex CLI. Use when you have a written plan and want independent validation before implementation.
2106
+ user-invocable: true
2107
+ ---
2108
+
2109
+ # /plan-review \u2014 GPT Review of Execution Plans
2110
+
2111
+ ## Usage
2112
+ \`/plan-review <plan-file-or-description>\`
2113
+
2114
+ ## Description
2115
+ Sends a Claude-authored execution plan to GPT via \`codemoot plan review\` for structured critique. GPT returns ISSUE (HIGH/MEDIUM/LOW) and SUGGEST lines with file-level references.
2116
+
2117
+ ## Instructions
2118
+
2119
+ ### Step 1: Prepare the plan
2120
+ - If the user provides a file path, use it directly
2121
+ - If reviewing the current conversation's plan, write to a temp file first
2122
+
2123
+ ### Step 2: Run plan review
2124
+ \`\`\`bash
2125
+ codemoot plan review <plan-file> [--phase N] [--build BUILD_ID] [--timeout ms] [--output file]
2126
+ \`\`\`
2127
+
2128
+ ### Step 3: Parse and present output
2129
+ JSON output includes: \`verdict\`, \`score\`, \`issues[]\` (severity + message), \`suggestions[]\`.
2130
+
2131
+ Present as:
2132
+ \`\`\`
2133
+ ## GPT Plan Review
2134
+ **Score**: X/10 | **Verdict**: APPROVED/NEEDS_REVISION
2135
+ ### Issues
2136
+ - [HIGH] description
2137
+ ### Suggestions
2138
+ - suggestion text
2139
+ \`\`\`
2140
+
2141
+ ### Step 4: If NEEDS_REVISION
2142
+ Fix HIGH issues, revise the plan, re-run. Session resume gives GPT context of prior review.
2143
+
2144
+ ### Important Notes
2145
+ - **Session resume**: GPT remembers prior reviews via thread resume
2146
+ - **Codebase access**: GPT reads actual project files to verify plan references
2147
+ - **Output capped**: JSON \`review\` field is capped to 2KB. Use \`--output\` for full text
2148
+ - **Zero API cost**: Uses ChatGPT subscription via Codex CLI
2076
2149
  `
2077
2150
  },
2078
2151
  {
@@ -2144,6 +2217,8 @@ Present: final position, agreements, disagreements, stats.
2144
2217
  3. State persisted to SQLite
2145
2218
  4. Zero API cost (ChatGPT subscription)
2146
2219
  5. 600s default timeout per turn
2220
+ 6. JSON \`response\` field capped to 2KB \u2014 check \`responseTruncated\` boolean
2221
+ 7. Progress heartbeat every 60s, throttled to 30s intervals
2147
2222
  `
2148
2223
  },
2149
2224
  {
@@ -2176,6 +2251,14 @@ Use /debate protocol. Loop until GPT says STANCE: SUPPORT.
2176
2251
  - Send detailed implementation plan to GPT
2177
2252
  - Revise on OPPOSE/UNCERTAIN \u2014 never skip
2178
2253
 
2254
+ ### Phase 1.25: Plan Review (Recommended)
2255
+ Before user approval, validate the plan with GPT:
2256
+ \`\`\`bash
2257
+ codemoot plan review /path/to/plan.md --build BUILD_ID
2258
+ \`\`\`
2259
+ GPT reviews the plan against actual codebase, returns ISSUE/SUGGEST lines.
2260
+ Fix HIGH issues before presenting to user.
2261
+
2179
2262
  ### Phase 1.5: User Approval Gate
2180
2263
  Present agreed plan. Wait for explicit approval via AskUserQuestion.
2181
2264
  \`\`\`bash
@@ -2266,25 +2349,31 @@ For Claude/GPT disagreements, optionally debate via \`codemoot debate turn\`.
2266
2349
  content: `# Codex Liaison Agent
2267
2350
 
2268
2351
  ## Role
2269
- Specialized teammate that communicates with GPT via Codex CLI to get independent reviews and iterate until quality reaches 9.5/10.
2352
+ Specialized teammate that communicates with GPT via codemoot CLI to get independent reviews and iterate until quality threshold is met.
2270
2353
 
2271
2354
  ## How You Work
2272
- 1. Send content to GPT via \`codex exec\` for review
2273
- 2. Parse feedback and score
2274
- 3. If score < 9.5: revise and re-submit
2275
- 4. Loop until 9.5/10 or max 7 iterations
2355
+ 1. Send content to GPT via \`codemoot review\` or \`codemoot plan review\`
2356
+ 2. Parse JSON output for score, verdict, findings
2357
+ 3. If NEEDS_REVISION: fix issues and re-submit (session resume retains context)
2358
+ 4. Loop until APPROVED or max iterations
2276
2359
  5. Report final version back to team lead
2277
2360
 
2278
- ## Calling Codex CLI
2361
+ ## Available Commands
2279
2362
  \`\`\`bash
2280
- codex exec --skip-git-repo-check -o ".codex-liaison-output.txt" "PROMPT_HERE"
2363
+ codemoot review <file-or-glob> # Code review
2364
+ codemoot review --diff HEAD~3..HEAD # Diff review
2365
+ codemoot plan review <plan-file> # Plan review
2366
+ codemoot debate turn DEBATE_ID "prompt" # Debate turn
2367
+ codemoot fix <file-or-glob> # Autofix loop
2281
2368
  \`\`\`
2282
2369
 
2283
2370
  ## Important Rules
2284
- - NEVER fabricate GPT's responses
2371
+ - NEVER fabricate GPT's responses \u2014 always parse actual JSON output
2285
2372
  - NEVER skip iterations if GPT says NEEDS_REVISION
2286
2373
  - Use your own judgment when GPT's feedback conflicts with project requirements
2287
- - 9.5/10 threshold is strict
2374
+ - JSON \`review\`/\`response\` fields are capped to 2KB; use \`--output\` for full text
2375
+ - All commands use session resume \u2014 GPT retains context across calls
2376
+ - Zero API cost (ChatGPT subscription via Codex CLI)
2288
2377
  `
2289
2378
  }
2290
2379
  ];
@@ -2308,6 +2397,8 @@ This project uses [CodeMoot](https://github.com/katarmal-ram/codemoot) for Claud
2308
2397
  - \`codemoot fix <file>\` \u2014 Autofix loop: review \u2192 apply fixes \u2192 re-review
2309
2398
  - \`codemoot debate start "topic"\` \u2014 Multi-round Claude vs GPT debate
2310
2399
  - \`codemoot cleanup\` \u2014 Scan for unused deps, dead code, duplicates
2400
+ - \`codemoot plan generate "task"\` \u2014 Generate plans via multi-model loop
2401
+ - \`codemoot plan review <plan-file>\` \u2014 GPT review of execution plans
2311
2402
  - \`codemoot shipit --profile safe\` \u2014 Composite workflow (lint+test+review)
2312
2403
  - \`codemoot cost\` \u2014 Token usage dashboard
2313
2404
  - \`codemoot doctor\` \u2014 Check prerequisites
@@ -2315,6 +2406,7 @@ This project uses [CodeMoot](https://github.com/katarmal-ram/codemoot) for Claud
2315
2406
  ### Slash Commands
2316
2407
  - \`/codex-review\` \u2014 Quick GPT review (uses codemoot review internally)
2317
2408
  - \`/debate\` \u2014 Start a Claude vs GPT debate
2409
+ - \`/plan-review\` \u2014 GPT review of execution plans
2318
2410
  - \`/build\` \u2014 Full build loop: debate \u2192 plan \u2192 implement \u2192 GPT review \u2192 fix
2319
2411
  - \`/cleanup\` \u2014 Bidirectional AI slop scanner
2320
2412
 
@@ -2371,8 +2463,9 @@ async function installSkillsCommand(options) {
2371
2463
  if (options.force) {
2372
2464
  const markerIdx = existing.indexOf(marker);
2373
2465
  const before = existing.slice(0, markerIdx);
2374
- const afterMarker = existing.slice(markerIdx + marker.length);
2375
- const nextHeadingMatch = afterMarker.match(/\n#{1,2} (?!#)(?!CodeMoot)/);
2466
+ const sectionEnd = existing.indexOf(CLAUDE_MD_SECTION.trimEnd(), markerIdx);
2467
+ const afterMarker = sectionEnd >= 0 ? existing.slice(sectionEnd + CLAUDE_MD_SECTION.trimEnd().length) : existing.slice(markerIdx + marker.length);
2468
+ const nextHeadingMatch = sectionEnd >= 0 ? null : afterMarker.match(/\n#{1,6} (?!CodeMoot)/);
2376
2469
  const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
2377
2470
  writeFileSync2(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
2378
2471
  console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
@@ -2427,7 +2520,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
2427
2520
  console.error("");
2428
2521
  console.error(chalk11.cyan(` Installed: ${installed}, Skipped: ${skipped}`));
2429
2522
  console.error("");
2430
- console.error(chalk11.dim(" Slash commands: /codex-review, /debate, /build, /cleanup"));
2523
+ console.error(chalk11.dim(" Slash commands: /codex-review, /debate, /plan-review, /build, /cleanup"));
2431
2524
  console.error(chalk11.dim(" CLAUDE.md: Claude now knows about codemoot commands & sessions"));
2432
2525
  console.error(chalk11.dim(" Hook: Post-commit hint to run codemoot review"));
2433
2526
  console.error("");
@@ -2718,8 +2811,15 @@ async function jobsStatusCommand(jobId) {
2718
2811
  }
2719
2812
 
2720
2813
  // src/commands/plan.ts
2721
- import { writeFileSync as writeFileSync3 } from "fs";
2722
- import { ModelRegistry as ModelRegistry4, Orchestrator, loadConfig as loadConfig6, openDatabase as openDatabase9 } from "@codemoot/core";
2814
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2815
+ import {
2816
+ ModelRegistry as ModelRegistry4,
2817
+ Orchestrator,
2818
+ SessionManager as SessionManager7,
2819
+ buildHandoffEnvelope as buildHandoffEnvelope4,
2820
+ loadConfig as loadConfig6,
2821
+ openDatabase as openDatabase9
2822
+ } from "@codemoot/core";
2723
2823
  import chalk15 from "chalk";
2724
2824
 
2725
2825
  // src/render.ts
@@ -2814,19 +2914,14 @@ function printSessionSummary(result) {
2814
2914
  }
2815
2915
 
2816
2916
  // src/commands/plan.ts
2817
- async function planCommand(task, options) {
2917
+ async function planGenerateCommand(task, options) {
2918
+ let db;
2818
2919
  try {
2819
2920
  const config = loadConfig6();
2820
2921
  const projectDir = process.cwd();
2821
2922
  const registry = ModelRegistry4.fromConfig(config, projectDir);
2822
- const health = await registry.healthCheckAll();
2823
- for (const [alias, hasKey] of health) {
2824
- if (!hasKey) {
2825
- console.warn(chalk15.yellow(`Warning: No API key for model "${alias}"`));
2826
- }
2827
- }
2828
2923
  const dbPath = getDbPath();
2829
- const db = openDatabase9(dbPath);
2924
+ db = openDatabase9(dbPath);
2830
2925
  const orchestrator = new Orchestrator({ registry, db, config });
2831
2926
  orchestrator.on("event", (event) => renderEvent(event, config));
2832
2927
  const result = await orchestrator.plan(task, {
@@ -2834,22 +2929,172 @@ async function planCommand(task, options) {
2834
2929
  });
2835
2930
  if (options.output) {
2836
2931
  writeFileSync3(options.output, result.finalOutput, "utf-8");
2837
- console.log(chalk15.green(`Plan saved to ${options.output}`));
2932
+ console.error(chalk15.green(`Plan saved to ${options.output}`));
2838
2933
  }
2839
2934
  printSessionSummary(result);
2840
2935
  db.close();
2841
2936
  process.exit(result.status === "completed" ? 0 : 2);
2842
2937
  } catch (error) {
2938
+ db?.close();
2939
+ console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
2940
+ process.exit(1);
2941
+ }
2942
+ }
2943
+ async function planReviewCommand(planFile, options) {
2944
+ let db;
2945
+ try {
2946
+ let planContent;
2947
+ if (planFile === "-") {
2948
+ const chunks = [];
2949
+ for await (const chunk of process.stdin) {
2950
+ chunks.push(chunk);
2951
+ }
2952
+ planContent = Buffer.concat(chunks).toString("utf-8");
2953
+ } else {
2954
+ planContent = readFileSync4(planFile, "utf-8");
2955
+ }
2956
+ if (!planContent.trim()) {
2957
+ console.error(chalk15.red("Plan file is empty."));
2958
+ process.exit(1);
2959
+ }
2960
+ const config = loadConfig6();
2961
+ const projectDir = process.cwd();
2962
+ const registry = ModelRegistry4.fromConfig(config, projectDir);
2963
+ const adapter = registry.tryGetAdapter("codex-reviewer") ?? registry.tryGetAdapter("codex-architect");
2964
+ if (!adapter) {
2965
+ console.error(chalk15.red("No codex adapter found in config. Run: codemoot init"));
2966
+ process.exit(1);
2967
+ }
2968
+ const dbPath = getDbPath();
2969
+ db = openDatabase9(dbPath);
2970
+ const sessionMgr = new SessionManager7(db);
2971
+ const session2 = sessionMgr.resolveActive("plan-review");
2972
+ const overflowCheck = sessionMgr.preCallOverflowCheck(session2.id);
2973
+ if (overflowCheck.rolled) {
2974
+ console.error(chalk15.yellow(` ${overflowCheck.message}`));
2975
+ }
2976
+ const currentSession = sessionMgr.get(session2.id);
2977
+ const threadId = overflowCheck.rolled ? void 0 : currentSession?.codexThreadId ?? void 0;
2978
+ const phaseContext = options.phase ? `
2979
+ This is Phase ${options.phase} of a multi-phase plan.` : "";
2980
+ const buildContext = options.build ? `
2981
+ Build ID: ${options.build}` : "";
2982
+ const prompt = buildHandoffEnvelope4({
2983
+ command: "plan-review",
2984
+ task: `Review the following execution plan for completeness, correctness, and feasibility. Read relevant codebase files to verify the plan's assumptions.${phaseContext}${buildContext}
2985
+
2986
+ PLAN:
2987
+ ${planContent.slice(0, 5e4)}
2988
+
2989
+ Review criteria:
2990
+ 1. Are all files/functions mentioned actually present in the codebase?
2991
+ 2. Are there missing steps or dependencies between phases?
2992
+ 3. Are there architectural concerns or better approaches?
2993
+ 4. Is the scope realistic for the described phases?
2994
+ 5. Are there security or performance concerns?
2995
+
2996
+ Output format:
2997
+ - For each issue, output: ISSUE: [HIGH|MEDIUM|LOW] <description>
2998
+ - For each suggestion, output: SUGGEST: <description>
2999
+ - End with: VERDICT: APPROVED or VERDICT: NEEDS_REVISION
3000
+ - End with: SCORE: X/10`,
3001
+ constraints: [
3002
+ "Verify file paths and function names against the actual codebase before flagging issues.",
3003
+ "Be specific \u2014 reference exact files and line numbers when possible.",
3004
+ "Focus on feasibility, not style preferences."
3005
+ ],
3006
+ resumed: Boolean(threadId)
3007
+ });
3008
+ const timeoutMs = (options.timeout ?? 300) * 1e3;
3009
+ const progress = createProgressCallbacks("plan-review");
3010
+ console.error(chalk15.cyan("Sending plan to codex for review..."));
3011
+ const result = await adapter.callWithResume(prompt, {
3012
+ sessionId: threadId,
3013
+ timeout: timeoutMs,
3014
+ ...progress
3015
+ });
3016
+ if (result.sessionId) {
3017
+ sessionMgr.updateThreadId(session2.id, result.sessionId);
3018
+ }
3019
+ sessionMgr.addUsageFromResult(session2.id, result.usage, prompt, result.text);
3020
+ sessionMgr.recordEvent({
3021
+ sessionId: session2.id,
3022
+ command: "plan",
3023
+ subcommand: "review",
3024
+ promptPreview: `Plan review: ${planFile}${options.phase ? ` (phase ${options.phase})` : ""}`,
3025
+ responsePreview: result.text.slice(0, 500),
3026
+ promptFull: prompt,
3027
+ responseFull: result.text,
3028
+ usageJson: JSON.stringify(result.usage),
3029
+ durationMs: result.durationMs,
3030
+ codexThreadId: result.sessionId
3031
+ });
3032
+ const tail = result.text.slice(-500);
3033
+ const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
3034
+ const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
3035
+ const verdict = verdictMatch ? verdictMatch[1].toLowerCase() : "unknown";
3036
+ const score = scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null;
3037
+ const issues = [];
3038
+ const suggestions = [];
3039
+ for (const line of result.text.split("\n")) {
3040
+ const issueMatch = line.match(/^[-*]?\s*ISSUE:\s*\[(HIGH|MEDIUM|LOW)]\s*(.*)/i);
3041
+ if (issueMatch) {
3042
+ issues.push({ severity: issueMatch[1].toLowerCase(), message: issueMatch[2].trim() });
3043
+ }
3044
+ const suggestMatch = line.match(/^[-*]?\s*SUGGEST:\s*(.*)/i);
3045
+ if (suggestMatch) {
3046
+ suggestions.push(suggestMatch[1].trim());
3047
+ }
3048
+ }
3049
+ const verdictColor = verdict === "approved" ? chalk15.green : chalk15.red;
3050
+ console.error(verdictColor(`
3051
+ Verdict: ${verdict.toUpperCase()} (${score ?? "?"}/10)`));
3052
+ if (issues.length > 0) {
3053
+ console.error(chalk15.yellow(`Issues (${issues.length}):`));
3054
+ for (const issue of issues) {
3055
+ const sevColor = issue.severity === "high" ? chalk15.red : issue.severity === "medium" ? chalk15.yellow : chalk15.dim;
3056
+ console.error(` ${sevColor(issue.severity.toUpperCase())} ${issue.message}`);
3057
+ }
3058
+ }
3059
+ if (suggestions.length > 0) {
3060
+ console.error(chalk15.cyan(`Suggestions (${suggestions.length}):`));
3061
+ for (const s of suggestions) {
3062
+ console.error(` ${chalk15.dim("\u2192")} ${s}`);
3063
+ }
3064
+ }
3065
+ console.error(chalk15.dim(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s | Tokens: ${result.usage.totalTokens}`));
3066
+ const output = {
3067
+ planFile,
3068
+ phase: options.phase ?? null,
3069
+ buildId: options.build ?? null,
3070
+ verdict,
3071
+ score,
3072
+ issues,
3073
+ suggestions,
3074
+ review: result.text.slice(0, 2e3),
3075
+ sessionId: result.sessionId,
3076
+ resumed: threadId ? result.sessionId === threadId : false,
3077
+ usage: result.usage,
3078
+ durationMs: result.durationMs
3079
+ };
3080
+ console.log(JSON.stringify(output, null, 2));
3081
+ if (options.output) {
3082
+ writeFileSync3(options.output, JSON.stringify(output, null, 2), "utf-8");
3083
+ console.error(chalk15.green(`Review saved to ${options.output}`));
3084
+ }
3085
+ db.close();
3086
+ } catch (error) {
3087
+ db?.close();
2843
3088
  console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
2844
3089
  process.exit(1);
2845
3090
  }
2846
3091
  }
2847
3092
 
2848
3093
  // src/commands/review.ts
2849
- 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";
3094
+ import { loadConfig as loadConfig7, ModelRegistry as ModelRegistry5, BINARY_SNIFF_BYTES, REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS3, SessionManager as SessionManager8, JobStore as JobStore3, openDatabase as openDatabase10, buildHandoffEnvelope as buildHandoffEnvelope5, getReviewPreset } from "@codemoot/core";
2850
3095
  import chalk16 from "chalk";
2851
3096
  import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
2852
- import { closeSync, globSync, openSync, readFileSync as readFileSync4, readSync, statSync, existsSync as existsSync4 } from "fs";
3097
+ import { closeSync, globSync, openSync, readFileSync as readFileSync5, readSync, statSync, existsSync as existsSync4 } from "fs";
2853
3098
  import { resolve as resolve2 } from "path";
2854
3099
  var MAX_FILE_SIZE = 100 * 1024;
2855
3100
  var MAX_TOTAL_SIZE = 200 * 1024;
@@ -2911,7 +3156,7 @@ async function reviewCommand(fileOrGlob, options) {
2911
3156
  return;
2912
3157
  }
2913
3158
  const db = openDatabase10(getDbPath());
2914
- const sessionMgr = new SessionManager7(db);
3159
+ const sessionMgr = new SessionManager8(db);
2915
3160
  const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("review");
2916
3161
  if (!session2) {
2917
3162
  console.error(chalk16.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: codemoot init"));
@@ -2951,7 +3196,7 @@ async function reviewCommand(fileOrGlob, options) {
2951
3196
  process.exit(1);
2952
3197
  }
2953
3198
  }
2954
- prompt = buildHandoffEnvelope4({
3199
+ prompt = buildHandoffEnvelope5({
2955
3200
  command: "review",
2956
3201
  task: `TASK: ${instruction}
2957
3202
 
@@ -2980,7 +3225,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
2980
3225
  db.close();
2981
3226
  process.exit(0);
2982
3227
  }
2983
- prompt = buildHandoffEnvelope4({
3228
+ prompt = buildHandoffEnvelope5({
2984
3229
  command: "review",
2985
3230
  task: `Review the following code changes.
2986
3231
 
@@ -3026,7 +3271,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
3026
3271
  console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
3027
3272
  continue;
3028
3273
  }
3029
- const content = readFileSync4(filePath, "utf-8");
3274
+ const content = readFileSync5(filePath, "utf-8");
3030
3275
  const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
3031
3276
  files.push({ path: relativePath, content });
3032
3277
  totalSize += stat.size;
@@ -3038,7 +3283,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
3038
3283
  }
3039
3284
  const fileContents = files.map((f) => `--- ${f.path} ---
3040
3285
  ${f.content}`).join("\n\n");
3041
- prompt = buildHandoffEnvelope4({
3286
+ prompt = buildHandoffEnvelope5({
3042
3287
  command: "review",
3043
3288
  task: `Review the following code files.
3044
3289
 
@@ -3093,7 +3338,7 @@ ${fileContents}`,
3093
3338
  findings,
3094
3339
  verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
3095
3340
  score: scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null,
3096
- review: result.text,
3341
+ review: result.text.slice(0, 2e3),
3097
3342
  sessionId: session2.id,
3098
3343
  codexThreadId: result.sessionId,
3099
3344
  resumed: sessionThreadId ? result.sessionId === sessionThreadId : false,
@@ -3561,7 +3806,7 @@ import {
3561
3806
  JobStore as JobStore5,
3562
3807
  ModelRegistry as ModelRegistry7,
3563
3808
  REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS4,
3564
- buildHandoffEnvelope as buildHandoffEnvelope5,
3809
+ buildHandoffEnvelope as buildHandoffEnvelope6,
3565
3810
  loadConfig as loadConfig10,
3566
3811
  openDatabase as openDatabase13
3567
3812
  } from "@codemoot/core";
@@ -3625,7 +3870,7 @@ async function workerCommand(options) {
3625
3870
  const focus = payload.focus ?? "all";
3626
3871
  const focusConstraint = focus === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focus}`;
3627
3872
  if (payload.prompt) {
3628
- prompt = buildHandoffEnvelope5({
3873
+ prompt = buildHandoffEnvelope6({
3629
3874
  command: "review",
3630
3875
  task: `TASK: ${payload.prompt}
3631
3876
 
@@ -3646,7 +3891,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
3646
3891
  encoding: "utf-8",
3647
3892
  maxBuffer: 1024 * 1024
3648
3893
  });
3649
- prompt = buildHandoffEnvelope5({
3894
+ prompt = buildHandoffEnvelope6({
3650
3895
  command: "review",
3651
3896
  task: `Review these code changes.
3652
3897
 
@@ -3656,14 +3901,14 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
3656
3901
  resumed: false
3657
3902
  });
3658
3903
  } else if (payload.files && Array.isArray(payload.files)) {
3659
- prompt = buildHandoffEnvelope5({
3904
+ prompt = buildHandoffEnvelope6({
3660
3905
  command: "review",
3661
3906
  task: `Review these files: ${payload.files.join(", ")}. Read each file and report issues.`,
3662
3907
  constraints: [focusConstraint],
3663
3908
  resumed: false
3664
3909
  });
3665
3910
  } else {
3666
- prompt = buildHandoffEnvelope5({
3911
+ prompt = buildHandoffEnvelope6({
3667
3912
  command: "review",
3668
3913
  task: "Review the codebase for issues. Start by listing key files.",
3669
3914
  constraints: [focusConstraint],
@@ -3671,7 +3916,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
3671
3916
  });
3672
3917
  }
3673
3918
  } else if (job.type === "cleanup") {
3674
- prompt = buildHandoffEnvelope5({
3919
+ prompt = buildHandoffEnvelope6({
3675
3920
  command: "cleanup",
3676
3921
  task: `Scan ${cwd} for: unused dependencies, dead code, duplicates, hardcoded values. Report findings with confidence levels.`,
3677
3922
  constraints: [`Scope: ${payload.scope ?? "all"}`],
@@ -3736,7 +3981,9 @@ program.command("cleanup").description("Scan codebase for AI slop: security vuln
3736
3981
  if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Must be a non-negative integer");
3737
3982
  return Number.parseInt(v, 10);
3738
3983
  }, 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);
3739
- 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);
3984
+ var plan = program.command("plan").description("Plan generation and review \u2014 write plans, get GPT review");
3985
+ plan.command("generate").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(planGenerateCommand);
3986
+ plan.command("review").description("Send a host-authored plan to codex for review").argument("<plan-file>", "Plan file to review (use - for stdin)").option("--build <id>", "Link review to a build ID").option("--phase <id>", 'Phase identifier (e.g. "1", "setup")').option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 300).option("--output <file>", "Save review result to file").action(planReviewCommand);
3740
3987
  var debate = program.command("debate").description("Multi-model debate with session persistence");
3741
3988
  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);
3742
3989
  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);