@codemoot/cli 0.2.6 → 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,11 +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 = "";
20
21
  let carryOver = "";
22
+ let droppedEvents = 0;
21
23
  function printActivity(msg) {
22
24
  const now = Date.now();
23
25
  if (msg === lastMessage && now - lastActivityAt < THROTTLE_MS) return;
@@ -25,9 +27,20 @@ function createProgressCallbacks(label = "codex") {
25
27
  lastMessage = msg;
26
28
  console.error(chalk.dim(` [${label}] ${msg}`));
27
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
+ }
28
40
  return {
29
41
  onSpawn(pid, command) {
30
- 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})`));
31
44
  },
32
45
  onStderr(_chunk) {
33
46
  },
@@ -35,18 +48,26 @@ function createProgressCallbacks(label = "codex") {
35
48
  const data = carryOver + chunk;
36
49
  const lines = data.split("\n");
37
50
  carryOver = lines.pop() ?? "";
51
+ if (carryOver.length > MAX_CARRY_OVER) {
52
+ carryOver = "";
53
+ droppedEvents++;
54
+ }
38
55
  for (const line of lines) {
39
- const trimmed = line.trim();
40
- if (!trimmed) continue;
41
- try {
42
- const event = JSON.parse(trimmed);
43
- formatEvent(event, printActivity);
44
- } catch {
45
- }
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;
46
67
  }
47
68
  },
48
69
  onHeartbeat(elapsedSec) {
49
- if (elapsedSec % 30 === 0) {
70
+ if (elapsedSec % 60 === 0) {
50
71
  printActivity(`${elapsedSec}s elapsed...`);
51
72
  }
52
73
  }
@@ -74,15 +95,6 @@ function formatEvent(event, print) {
74
95
  }
75
96
  return;
76
97
  }
77
- if (item.type === "agent_message") {
78
- const text = String(item.text ?? "");
79
- const firstLine = text.split("\n").find((l) => l.trim().length > 10);
80
- if (firstLine) {
81
- const preview = firstLine.trim().slice(0, 80);
82
- print(`Response: ${preview}${firstLine.trim().length > 80 ? "..." : ""}`);
83
- }
84
- return;
85
- }
86
98
  }
87
99
  if (type === "turn.completed") {
88
100
  const usage = event.usage;
@@ -382,9 +394,9 @@ async function buildReviewCommand(buildId) {
382
394
  try {
383
395
  const tmpIndex = join2(projectDir, ".git", "codemoot-review-index");
384
396
  try {
385
- execSync(`git read-tree HEAD`, { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
386
- execSync("git add -A", { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
387
- 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 } });
388
400
  } finally {
389
401
  try {
390
402
  unlinkSync(tmpIndex);
@@ -495,7 +507,7 @@ Review for:
495
507
  }
496
508
  const output = {
497
509
  buildId,
498
- review: result.text,
510
+ review: result.text.slice(0, 2e3),
499
511
  verdict: approved ? "approved" : "needs_revision",
500
512
  sessionId: result.sessionId,
501
513
  resumed: existingSession ? result.sessionId === existingSession : false,
@@ -617,7 +629,7 @@ async function debateTurnCommand(debateId, prompt, options) {
617
629
  const output = {
618
630
  debateId,
619
631
  round: newRound,
620
- response: existing.responseText,
632
+ response: existing.responseText?.slice(0, 2e3) ?? "",
621
633
  sessionId: existing.sessionId,
622
634
  resumed: false,
623
635
  cached: true,
@@ -662,7 +674,7 @@ async function debateTurnCommand(debateId, prompt, options) {
662
674
  const output = {
663
675
  debateId,
664
676
  round: newRound,
665
- response: recheckRow.responseText,
677
+ response: recheckRow.responseText?.slice(0, 2e3) ?? "",
666
678
  sessionId: recheckRow.sessionId,
667
679
  resumed: false,
668
680
  cached: true,
@@ -772,7 +784,8 @@ async function debateTurnCommand(debateId, prompt, options) {
772
784
  const output = {
773
785
  debateId,
774
786
  round: newRound,
775
- response: result.text,
787
+ response: result.text.slice(0, 2e3),
788
+ responseTruncated: result.text.length > 2e3,
776
789
  sessionId: result.sessionId,
777
790
  resumed,
778
791
  cached: false,
@@ -1190,7 +1203,9 @@ Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
1190
1203
  writeFileSync4(options.output, JSON.stringify(report, null, 2), "utf-8");
1191
1204
  console.error(chalk5.green(` Findings written to ${options.output}`));
1192
1205
  }
1193
- 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);
1194
1209
  db.close();
1195
1210
  } catch (error) {
1196
1211
  db?.close();
@@ -2078,6 +2093,59 @@ Ask if user wants to fix and re-review. If yes:
2078
2093
  - **No arg size limits**: Content is piped via stdin, not passed as CLI args.
2079
2094
  - **Presets**: Use --preset for specialized reviews (security-audit, performance, quick-scan, pre-commit, api-review).
2080
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
2081
2149
  `
2082
2150
  },
2083
2151
  {
@@ -2149,6 +2217,8 @@ Present: final position, agreements, disagreements, stats.
2149
2217
  3. State persisted to SQLite
2150
2218
  4. Zero API cost (ChatGPT subscription)
2151
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
2152
2222
  `
2153
2223
  },
2154
2224
  {
@@ -2181,6 +2251,14 @@ Use /debate protocol. Loop until GPT says STANCE: SUPPORT.
2181
2251
  - Send detailed implementation plan to GPT
2182
2252
  - Revise on OPPOSE/UNCERTAIN \u2014 never skip
2183
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
+
2184
2262
  ### Phase 1.5: User Approval Gate
2185
2263
  Present agreed plan. Wait for explicit approval via AskUserQuestion.
2186
2264
  \`\`\`bash
@@ -2271,25 +2349,31 @@ For Claude/GPT disagreements, optionally debate via \`codemoot debate turn\`.
2271
2349
  content: `# Codex Liaison Agent
2272
2350
 
2273
2351
  ## Role
2274
- 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.
2275
2353
 
2276
2354
  ## How You Work
2277
- 1. Send content to GPT via \`codex exec\` for review
2278
- 2. Parse feedback and score
2279
- 3. If score < 9.5: revise and re-submit
2280
- 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
2281
2359
  5. Report final version back to team lead
2282
2360
 
2283
- ## Calling Codex CLI
2361
+ ## Available Commands
2284
2362
  \`\`\`bash
2285
- 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
2286
2368
  \`\`\`
2287
2369
 
2288
2370
  ## Important Rules
2289
- - NEVER fabricate GPT's responses
2371
+ - NEVER fabricate GPT's responses \u2014 always parse actual JSON output
2290
2372
  - NEVER skip iterations if GPT says NEEDS_REVISION
2291
2373
  - Use your own judgment when GPT's feedback conflicts with project requirements
2292
- - 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)
2293
2377
  `
2294
2378
  }
2295
2379
  ];
@@ -2313,6 +2397,8 @@ This project uses [CodeMoot](https://github.com/katarmal-ram/codemoot) for Claud
2313
2397
  - \`codemoot fix <file>\` \u2014 Autofix loop: review \u2192 apply fixes \u2192 re-review
2314
2398
  - \`codemoot debate start "topic"\` \u2014 Multi-round Claude vs GPT debate
2315
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
2316
2402
  - \`codemoot shipit --profile safe\` \u2014 Composite workflow (lint+test+review)
2317
2403
  - \`codemoot cost\` \u2014 Token usage dashboard
2318
2404
  - \`codemoot doctor\` \u2014 Check prerequisites
@@ -2320,6 +2406,7 @@ This project uses [CodeMoot](https://github.com/katarmal-ram/codemoot) for Claud
2320
2406
  ### Slash Commands
2321
2407
  - \`/codex-review\` \u2014 Quick GPT review (uses codemoot review internally)
2322
2408
  - \`/debate\` \u2014 Start a Claude vs GPT debate
2409
+ - \`/plan-review\` \u2014 GPT review of execution plans
2323
2410
  - \`/build\` \u2014 Full build loop: debate \u2192 plan \u2192 implement \u2192 GPT review \u2192 fix
2324
2411
  - \`/cleanup\` \u2014 Bidirectional AI slop scanner
2325
2412
 
@@ -2376,8 +2463,9 @@ async function installSkillsCommand(options) {
2376
2463
  if (options.force) {
2377
2464
  const markerIdx = existing.indexOf(marker);
2378
2465
  const before = existing.slice(0, markerIdx);
2379
- const afterMarker = existing.slice(markerIdx + marker.length);
2380
- 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)/);
2381
2469
  const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
2382
2470
  writeFileSync2(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
2383
2471
  console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
@@ -2432,7 +2520,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
2432
2520
  console.error("");
2433
2521
  console.error(chalk11.cyan(` Installed: ${installed}, Skipped: ${skipped}`));
2434
2522
  console.error("");
2435
- 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"));
2436
2524
  console.error(chalk11.dim(" CLAUDE.md: Claude now knows about codemoot commands & sessions"));
2437
2525
  console.error(chalk11.dim(" Hook: Post-commit hint to run codemoot review"));
2438
2526
  console.error("");
@@ -2723,8 +2811,15 @@ async function jobsStatusCommand(jobId) {
2723
2811
  }
2724
2812
 
2725
2813
  // src/commands/plan.ts
2726
- import { writeFileSync as writeFileSync3 } from "fs";
2727
- 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";
2728
2823
  import chalk15 from "chalk";
2729
2824
 
2730
2825
  // src/render.ts
@@ -2819,19 +2914,14 @@ function printSessionSummary(result) {
2819
2914
  }
2820
2915
 
2821
2916
  // src/commands/plan.ts
2822
- async function planCommand(task, options) {
2917
+ async function planGenerateCommand(task, options) {
2918
+ let db;
2823
2919
  try {
2824
2920
  const config = loadConfig6();
2825
2921
  const projectDir = process.cwd();
2826
2922
  const registry = ModelRegistry4.fromConfig(config, projectDir);
2827
- const health = await registry.healthCheckAll();
2828
- for (const [alias, hasKey] of health) {
2829
- if (!hasKey) {
2830
- console.warn(chalk15.yellow(`Warning: No API key for model "${alias}"`));
2831
- }
2832
- }
2833
2923
  const dbPath = getDbPath();
2834
- const db = openDatabase9(dbPath);
2924
+ db = openDatabase9(dbPath);
2835
2925
  const orchestrator = new Orchestrator({ registry, db, config });
2836
2926
  orchestrator.on("event", (event) => renderEvent(event, config));
2837
2927
  const result = await orchestrator.plan(task, {
@@ -2839,22 +2929,172 @@ async function planCommand(task, options) {
2839
2929
  });
2840
2930
  if (options.output) {
2841
2931
  writeFileSync3(options.output, result.finalOutput, "utf-8");
2842
- console.log(chalk15.green(`Plan saved to ${options.output}`));
2932
+ console.error(chalk15.green(`Plan saved to ${options.output}`));
2843
2933
  }
2844
2934
  printSessionSummary(result);
2845
2935
  db.close();
2846
2936
  process.exit(result.status === "completed" ? 0 : 2);
2847
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();
2848
3088
  console.error(chalk15.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
2849
3089
  process.exit(1);
2850
3090
  }
2851
3091
  }
2852
3092
 
2853
3093
  // src/commands/review.ts
2854
- 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";
2855
3095
  import chalk16 from "chalk";
2856
3096
  import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
2857
- 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";
2858
3098
  import { resolve as resolve2 } from "path";
2859
3099
  var MAX_FILE_SIZE = 100 * 1024;
2860
3100
  var MAX_TOTAL_SIZE = 200 * 1024;
@@ -2916,7 +3156,7 @@ async function reviewCommand(fileOrGlob, options) {
2916
3156
  return;
2917
3157
  }
2918
3158
  const db = openDatabase10(getDbPath());
2919
- const sessionMgr = new SessionManager7(db);
3159
+ const sessionMgr = new SessionManager8(db);
2920
3160
  const session2 = options.session ? sessionMgr.get(options.session) : sessionMgr.resolveActive("review");
2921
3161
  if (!session2) {
2922
3162
  console.error(chalk16.red(options.session ? `Session not found: ${options.session}` : "No active session. Run: codemoot init"));
@@ -2956,7 +3196,7 @@ async function reviewCommand(fileOrGlob, options) {
2956
3196
  process.exit(1);
2957
3197
  }
2958
3198
  }
2959
- prompt = buildHandoffEnvelope4({
3199
+ prompt = buildHandoffEnvelope5({
2960
3200
  command: "review",
2961
3201
  task: `TASK: ${instruction}
2962
3202
 
@@ -2985,7 +3225,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
2985
3225
  db.close();
2986
3226
  process.exit(0);
2987
3227
  }
2988
- prompt = buildHandoffEnvelope4({
3228
+ prompt = buildHandoffEnvelope5({
2989
3229
  command: "review",
2990
3230
  task: `Review the following code changes.
2991
3231
 
@@ -3031,7 +3271,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
3031
3271
  console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
3032
3272
  continue;
3033
3273
  }
3034
- const content = readFileSync4(filePath, "utf-8");
3274
+ const content = readFileSync5(filePath, "utf-8");
3035
3275
  const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
3036
3276
  files.push({ path: relativePath, content });
3037
3277
  totalSize += stat.size;
@@ -3043,7 +3283,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
3043
3283
  }
3044
3284
  const fileContents = files.map((f) => `--- ${f.path} ---
3045
3285
  ${f.content}`).join("\n\n");
3046
- prompt = buildHandoffEnvelope4({
3286
+ prompt = buildHandoffEnvelope5({
3047
3287
  command: "review",
3048
3288
  task: `Review the following code files.
3049
3289
 
@@ -3098,7 +3338,7 @@ ${fileContents}`,
3098
3338
  findings,
3099
3339
  verdict: verdictMatch ? verdictMatch[1].toLowerCase() : "unknown",
3100
3340
  score: scoreMatch ? Number.parseInt(scoreMatch[1], 10) : null,
3101
- review: result.text,
3341
+ review: result.text.slice(0, 2e3),
3102
3342
  sessionId: session2.id,
3103
3343
  codexThreadId: result.sessionId,
3104
3344
  resumed: sessionThreadId ? result.sessionId === sessionThreadId : false,
@@ -3566,7 +3806,7 @@ import {
3566
3806
  JobStore as JobStore5,
3567
3807
  ModelRegistry as ModelRegistry7,
3568
3808
  REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS4,
3569
- buildHandoffEnvelope as buildHandoffEnvelope5,
3809
+ buildHandoffEnvelope as buildHandoffEnvelope6,
3570
3810
  loadConfig as loadConfig10,
3571
3811
  openDatabase as openDatabase13
3572
3812
  } from "@codemoot/core";
@@ -3630,7 +3870,7 @@ async function workerCommand(options) {
3630
3870
  const focus = payload.focus ?? "all";
3631
3871
  const focusConstraint = focus === "all" ? "Review for: correctness, bugs, security, performance, code quality" : `Focus specifically on: ${focus}`;
3632
3872
  if (payload.prompt) {
3633
- prompt = buildHandoffEnvelope5({
3873
+ prompt = buildHandoffEnvelope6({
3634
3874
  command: "review",
3635
3875
  task: `TASK: ${payload.prompt}
3636
3876
 
@@ -3651,7 +3891,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
3651
3891
  encoding: "utf-8",
3652
3892
  maxBuffer: 1024 * 1024
3653
3893
  });
3654
- prompt = buildHandoffEnvelope5({
3894
+ prompt = buildHandoffEnvelope6({
3655
3895
  command: "review",
3656
3896
  task: `Review these code changes.
3657
3897
 
@@ -3661,14 +3901,14 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
3661
3901
  resumed: false
3662
3902
  });
3663
3903
  } else if (payload.files && Array.isArray(payload.files)) {
3664
- prompt = buildHandoffEnvelope5({
3904
+ prompt = buildHandoffEnvelope6({
3665
3905
  command: "review",
3666
3906
  task: `Review these files: ${payload.files.join(", ")}. Read each file and report issues.`,
3667
3907
  constraints: [focusConstraint],
3668
3908
  resumed: false
3669
3909
  });
3670
3910
  } else {
3671
- prompt = buildHandoffEnvelope5({
3911
+ prompt = buildHandoffEnvelope6({
3672
3912
  command: "review",
3673
3913
  task: "Review the codebase for issues. Start by listing key files.",
3674
3914
  constraints: [focusConstraint],
@@ -3676,7 +3916,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS4)}`,
3676
3916
  });
3677
3917
  }
3678
3918
  } else if (job.type === "cleanup") {
3679
- prompt = buildHandoffEnvelope5({
3919
+ prompt = buildHandoffEnvelope6({
3680
3920
  command: "cleanup",
3681
3921
  task: `Scan ${cwd} for: unused dependencies, dead code, duplicates, hardcoded values. Report findings with confidence levels.`,
3682
3922
  constraints: [`Scope: ${payload.scope ?? "all"}`],
@@ -3741,7 +3981,9 @@ program.command("cleanup").description("Scan codebase for AI slop: security vuln
3741
3981
  if (!/^\d+$/.test(v)) throw new InvalidArgumentError("Must be a non-negative integer");
3742
3982
  return Number.parseInt(v, 10);
3743
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);
3744
- 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);
3745
3987
  var debate = program.command("debate").description("Multi-model debate with session persistence");
3746
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);
3747
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);