@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 +314 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 %
|
|
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
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
diff = execFileSync("git", ["diff", "--cached", "--"
|
|
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
|
-
|
|
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
|
|
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 \`
|
|
2273
|
-
2. Parse
|
|
2274
|
-
3. If
|
|
2275
|
-
4. Loop until
|
|
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
|
-
##
|
|
2361
|
+
## Available Commands
|
|
2279
2362
|
\`\`\`bash
|
|
2280
|
-
|
|
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
|
-
-
|
|
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
|
|
2375
|
-
const
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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);
|