@codemoot/cli 0.2.4 → 0.2.6
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 +242 -75
- package/dist/index.js.map +1 -1
- package/package.json +13 -29
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { CLEANUP_TIMEOUT_SEC, VERSION as VERSION2 } from "@codemoot/core";
|
|
|
7
7
|
// src/commands/build.ts
|
|
8
8
|
import { BuildStore, DebateStore, REVIEW_DIFF_MAX_CHARS, REVIEW_TEXT_MAX_CHARS, SessionManager, buildHandoffEnvelope, generateId, loadConfig, openDatabase as openDatabase2 } from "@codemoot/core";
|
|
9
9
|
import chalk3 from "chalk";
|
|
10
|
-
import { execSync } from "child_process";
|
|
10
|
+
import { execFileSync, execSync } from "child_process";
|
|
11
11
|
import { unlinkSync } from "fs";
|
|
12
12
|
import { join as join2 } from "path";
|
|
13
13
|
|
|
@@ -17,6 +17,7 @@ var THROTTLE_MS = 3e3;
|
|
|
17
17
|
function createProgressCallbacks(label = "codex") {
|
|
18
18
|
let lastActivityAt = 0;
|
|
19
19
|
let lastMessage = "";
|
|
20
|
+
let carryOver = "";
|
|
20
21
|
function printActivity(msg) {
|
|
21
22
|
const now = Date.now();
|
|
22
23
|
if (msg === lastMessage && now - lastActivityAt < THROTTLE_MS) return;
|
|
@@ -31,7 +32,10 @@ function createProgressCallbacks(label = "codex") {
|
|
|
31
32
|
onStderr(_chunk) {
|
|
32
33
|
},
|
|
33
34
|
onProgress(chunk) {
|
|
34
|
-
|
|
35
|
+
const data = carryOver + chunk;
|
|
36
|
+
const lines = data.split("\n");
|
|
37
|
+
carryOver = lines.pop() ?? "";
|
|
38
|
+
for (const line of lines) {
|
|
35
39
|
const trimmed = line.trim();
|
|
36
40
|
if (!trimmed) continue;
|
|
37
41
|
try {
|
|
@@ -60,7 +64,8 @@ function formatEvent(event, print) {
|
|
|
60
64
|
if (!item) return;
|
|
61
65
|
if (item.type === "tool_call" || item.type === "function_call") {
|
|
62
66
|
const name = item.name ?? item.function ?? "tool";
|
|
63
|
-
const
|
|
67
|
+
const rawArgs = item.arguments ?? item.input ?? "";
|
|
68
|
+
const args = (typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs)).slice(0, 80);
|
|
64
69
|
const pathMatch = args.match(/["']([^"']*\.[a-z]{1,4})["']/i);
|
|
65
70
|
if (pathMatch) {
|
|
66
71
|
print(`${name}: ${pathMatch[1]}`);
|
|
@@ -103,13 +108,25 @@ function getDbPath(projectDir) {
|
|
|
103
108
|
}
|
|
104
109
|
async function withDatabase(fn) {
|
|
105
110
|
const db = openDatabase(getDbPath());
|
|
111
|
+
const originalExit = process.exit;
|
|
112
|
+
let requestedExitCode;
|
|
113
|
+
process.exit = ((code) => {
|
|
114
|
+
requestedExitCode = typeof code === "number" ? code : 1;
|
|
115
|
+
throw new Error("__WITH_DATABASE_EXIT__");
|
|
116
|
+
});
|
|
106
117
|
try {
|
|
107
118
|
return await fn(db);
|
|
108
119
|
} catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
if (requestedExitCode === void 0) {
|
|
121
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
111
124
|
} finally {
|
|
125
|
+
process.exit = originalExit;
|
|
112
126
|
db.close();
|
|
127
|
+
if (requestedExitCode !== void 0) {
|
|
128
|
+
originalExit(requestedExitCode);
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
@@ -367,7 +384,7 @@ async function buildReviewCommand(buildId) {
|
|
|
367
384
|
try {
|
|
368
385
|
execSync(`git read-tree HEAD`, { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
369
386
|
execSync("git add -A", { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
370
|
-
diff =
|
|
387
|
+
diff = execFileSync("git", ["diff", "--cached", "--", run.baselineRef], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
371
388
|
} finally {
|
|
372
389
|
try {
|
|
373
390
|
unlinkSync(tmpIndex);
|
|
@@ -963,8 +980,8 @@ import { readFileSync } from "fs";
|
|
|
963
980
|
async function cleanupCommand(path, options) {
|
|
964
981
|
let db;
|
|
965
982
|
try {
|
|
966
|
-
const { resolve:
|
|
967
|
-
const projectDir =
|
|
983
|
+
const { resolve: resolve3 } = await import("path");
|
|
984
|
+
const projectDir = resolve3(path);
|
|
968
985
|
if (options.background) {
|
|
969
986
|
const bgDb = openDatabase4(getDbPath());
|
|
970
987
|
const jobStore = new JobStore(bgDb);
|
|
@@ -1169,8 +1186,8 @@ Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
|
|
|
1169
1186
|
durationMs
|
|
1170
1187
|
});
|
|
1171
1188
|
if (options.output) {
|
|
1172
|
-
const { writeFileSync:
|
|
1173
|
-
|
|
1189
|
+
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
1190
|
+
writeFileSync4(options.output, JSON.stringify(report, null, 2), "utf-8");
|
|
1174
1191
|
console.error(chalk5.green(` Findings written to ${options.output}`));
|
|
1175
1192
|
}
|
|
1176
1193
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -1585,7 +1602,9 @@ async function eventsCommand(options) {
|
|
|
1585
1602
|
}
|
|
1586
1603
|
|
|
1587
1604
|
// src/commands/fix.ts
|
|
1588
|
-
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
1605
|
+
import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
|
|
1606
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1607
|
+
import { resolve } from "path";
|
|
1589
1608
|
import {
|
|
1590
1609
|
DEFAULT_RULES,
|
|
1591
1610
|
ModelRegistry as ModelRegistry3,
|
|
@@ -1597,6 +1616,54 @@ import {
|
|
|
1597
1616
|
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS2
|
|
1598
1617
|
} from "@codemoot/core";
|
|
1599
1618
|
import chalk9 from "chalk";
|
|
1619
|
+
function parseFixes(text) {
|
|
1620
|
+
const fixes = [];
|
|
1621
|
+
const fixPattern = /FIX:\s*(\S+?):(\d+)\s+(.+?)(?:\n```old\n([\s\S]*?)\n```\s*\n```new\n([\s\S]*?)\n```|(?:\n```\n([\s\S]*?)\n```))/g;
|
|
1622
|
+
let match;
|
|
1623
|
+
match = fixPattern.exec(text);
|
|
1624
|
+
while (match !== null) {
|
|
1625
|
+
const file = match[1];
|
|
1626
|
+
const line = Number.parseInt(match[2], 10);
|
|
1627
|
+
const description = match[3].trim();
|
|
1628
|
+
if (match[4] !== void 0 && match[5] !== void 0) {
|
|
1629
|
+
fixes.push({ file, line, description, oldCode: match[4], newCode: match[5] });
|
|
1630
|
+
} else if (match[6] !== void 0) {
|
|
1631
|
+
fixes.push({ file, line, description, oldCode: "", newCode: match[6] });
|
|
1632
|
+
}
|
|
1633
|
+
match = fixPattern.exec(text);
|
|
1634
|
+
}
|
|
1635
|
+
return fixes;
|
|
1636
|
+
}
|
|
1637
|
+
function applyFix(fix, projectDir) {
|
|
1638
|
+
const filePath = resolve(projectDir, fix.file);
|
|
1639
|
+
const normalizedProject = resolve(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
1640
|
+
if (!resolve(filePath).startsWith(normalizedProject)) {
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
let content;
|
|
1644
|
+
try {
|
|
1645
|
+
content = readFileSync2(filePath, "utf-8");
|
|
1646
|
+
} catch {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
const lines = content.split("\n");
|
|
1650
|
+
if (fix.oldCode) {
|
|
1651
|
+
const trimmedOld = fix.oldCode.trim();
|
|
1652
|
+
if (content.includes(trimmedOld)) {
|
|
1653
|
+
const updated = content.replace(trimmedOld, fix.newCode.trim());
|
|
1654
|
+
writeFileSync(filePath, updated, "utf-8");
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
const lineIdx = fix.line - 1;
|
|
1660
|
+
if (lineIdx < 0 || lineIdx >= lines.length) return false;
|
|
1661
|
+
const newLines = fix.newCode.trim().split("\n");
|
|
1662
|
+
const oldLineCount = Math.max(1, newLines.length);
|
|
1663
|
+
lines.splice(lineIdx, oldLineCount, ...newLines);
|
|
1664
|
+
writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1600
1667
|
async function fixCommand(fileOrGlob, options) {
|
|
1601
1668
|
const projectDir = process.cwd();
|
|
1602
1669
|
const db = openDatabase7(getDbPath());
|
|
@@ -1627,6 +1694,8 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1627
1694
|
let threadId = currentSession?.codexThreadId ?? void 0;
|
|
1628
1695
|
const rounds = [];
|
|
1629
1696
|
let converged = false;
|
|
1697
|
+
const stuckFingerprints = /* @__PURE__ */ new Set();
|
|
1698
|
+
let prevFingerprints = /* @__PURE__ */ new Set();
|
|
1630
1699
|
console.error(
|
|
1631
1700
|
chalk9.cyan(
|
|
1632
1701
|
`Autofix loop: ${fileOrGlob} (max ${options.maxRounds} rounds, focus: ${options.focus})`
|
|
@@ -1644,7 +1713,7 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1644
1713
|
let diffContent = "";
|
|
1645
1714
|
if (options.diff) {
|
|
1646
1715
|
try {
|
|
1647
|
-
diffContent =
|
|
1716
|
+
diffContent = execFileSync2("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
1648
1717
|
cwd: projectDir,
|
|
1649
1718
|
encoding: "utf-8",
|
|
1650
1719
|
maxBuffer: 1024 * 1024
|
|
@@ -1653,23 +1722,46 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1653
1722
|
diffContent = "";
|
|
1654
1723
|
}
|
|
1655
1724
|
}
|
|
1725
|
+
const fixOutputContract = [
|
|
1726
|
+
"You are an autofix engine. For EVERY fixable issue, you MUST output this EXACT format:",
|
|
1727
|
+
"",
|
|
1728
|
+
"FIX: path/to/file.ts:42 Description of the bug",
|
|
1729
|
+
"```old",
|
|
1730
|
+
"exact old code copied from the file",
|
|
1731
|
+
"```",
|
|
1732
|
+
"```new",
|
|
1733
|
+
"exact replacement code",
|
|
1734
|
+
"```",
|
|
1735
|
+
"",
|
|
1736
|
+
"Then end with:",
|
|
1737
|
+
"VERDICT: APPROVED or VERDICT: NEEDS_REVISION",
|
|
1738
|
+
"SCORE: X/10",
|
|
1739
|
+
"",
|
|
1740
|
+
"Rules:",
|
|
1741
|
+
"- The ```old block MUST be an exact substring copy from the file (whitespace-sensitive).",
|
|
1742
|
+
"- One FIX block per issue.",
|
|
1743
|
+
"- Issues without a FIX block are IGNORED by the autofix engine.",
|
|
1744
|
+
"- If the code is clean, output VERDICT: APPROVED with no FIX blocks."
|
|
1745
|
+
].join("\n");
|
|
1656
1746
|
const reviewPrompt = buildHandoffEnvelope3({
|
|
1657
|
-
command: "
|
|
1747
|
+
command: "custom",
|
|
1658
1748
|
task: options.diff ? `Review and identify fixable issues in this diff.
|
|
1659
1749
|
|
|
1660
1750
|
GIT DIFF (${options.diff}):
|
|
1661
|
-
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}
|
|
1751
|
+
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}
|
|
1752
|
+
|
|
1753
|
+
${fixOutputContract}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers and exact fixes.
|
|
1754
|
+
|
|
1755
|
+
${fixOutputContract}`,
|
|
1662
1756
|
constraints: [
|
|
1663
1757
|
`Focus: ${options.focus}`,
|
|
1664
|
-
|
|
1665
|
-
"Format fixes as: FIX: <file>:<line> <description>\n```\n<fixed code>\n```",
|
|
1666
|
-
round > 1 ? `This is re-review round ${round}. Previous fixes were applied. Check if issues are resolved.` : ""
|
|
1758
|
+
round > 1 ? `This is re-review round ${round}. Previous fixes were applied by the host. Only report REMAINING unfixed issues.` : ""
|
|
1667
1759
|
].filter(Boolean),
|
|
1668
1760
|
resumed: Boolean(threadId)
|
|
1669
1761
|
});
|
|
1670
1762
|
const timeoutMs = options.timeout * 1e3;
|
|
1671
1763
|
const progress = createProgressCallbacks("fix-review");
|
|
1672
|
-
console.error(chalk9.dim("
|
|
1764
|
+
console.error(chalk9.dim(" GPT reviewing..."));
|
|
1673
1765
|
const reviewResult = await adapter.callWithResume(reviewPrompt, {
|
|
1674
1766
|
sessionId: threadId,
|
|
1675
1767
|
timeout: timeoutMs,
|
|
@@ -1680,6 +1772,18 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1680
1772
|
sessionMgr.updateThreadId(session2.id, reviewResult.sessionId);
|
|
1681
1773
|
}
|
|
1682
1774
|
sessionMgr.addUsageFromResult(session2.id, reviewResult.usage, reviewPrompt, reviewResult.text);
|
|
1775
|
+
sessionMgr.recordEvent({
|
|
1776
|
+
sessionId: session2.id,
|
|
1777
|
+
command: "fix",
|
|
1778
|
+
subcommand: `review-round-${round}`,
|
|
1779
|
+
promptPreview: `Fix review round ${round}: ${fileOrGlob}`,
|
|
1780
|
+
responsePreview: reviewResult.text.slice(0, 500),
|
|
1781
|
+
promptFull: reviewPrompt,
|
|
1782
|
+
responseFull: reviewResult.text,
|
|
1783
|
+
usageJson: JSON.stringify(reviewResult.usage),
|
|
1784
|
+
durationMs: reviewResult.durationMs,
|
|
1785
|
+
codexThreadId: reviewResult.sessionId
|
|
1786
|
+
});
|
|
1683
1787
|
const tail = reviewResult.text.slice(-500);
|
|
1684
1788
|
const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
1685
1789
|
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
@@ -1697,66 +1801,113 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1697
1801
|
reviewScore: score,
|
|
1698
1802
|
criticalCount,
|
|
1699
1803
|
warningCount,
|
|
1700
|
-
|
|
1804
|
+
fixesProposed: 0,
|
|
1805
|
+
fixesApplied: 0,
|
|
1806
|
+
fixesFailed: 0,
|
|
1807
|
+
exitReason: "all_resolved",
|
|
1701
1808
|
durationMs: Date.now() - roundStart
|
|
1702
1809
|
});
|
|
1703
1810
|
converged = true;
|
|
1704
|
-
console.error(chalk9.green("
|
|
1811
|
+
console.error(chalk9.green(" APPROVED \u2014 all issues resolved."));
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
const fixes = parseFixes(reviewResult.text);
|
|
1815
|
+
console.error(chalk9.dim(` Found ${fixes.length} fix proposal(s)`));
|
|
1816
|
+
if (fixes.length === 0) {
|
|
1817
|
+
rounds.push({
|
|
1818
|
+
round,
|
|
1819
|
+
reviewVerdict: verdict,
|
|
1820
|
+
reviewScore: score,
|
|
1821
|
+
criticalCount,
|
|
1822
|
+
warningCount,
|
|
1823
|
+
fixesProposed: 0,
|
|
1824
|
+
fixesApplied: 0,
|
|
1825
|
+
fixesFailed: 0,
|
|
1826
|
+
exitReason: "no_fixes_proposed",
|
|
1827
|
+
durationMs: Date.now() - roundStart
|
|
1828
|
+
});
|
|
1829
|
+
console.error(chalk9.yellow(" GPT found issues but proposed no structured fixes. Stopping."));
|
|
1705
1830
|
break;
|
|
1706
1831
|
}
|
|
1707
1832
|
if (options.dryRun) {
|
|
1708
|
-
console.error(chalk9.yellow(" Dry-run:
|
|
1833
|
+
console.error(chalk9.yellow(" Dry-run: showing proposed fixes without applying."));
|
|
1834
|
+
for (const fix of fixes) {
|
|
1835
|
+
console.error(chalk9.dim(` ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1836
|
+
}
|
|
1709
1837
|
rounds.push({
|
|
1710
1838
|
round,
|
|
1711
1839
|
reviewVerdict: verdict,
|
|
1712
1840
|
reviewScore: score,
|
|
1713
1841
|
criticalCount,
|
|
1714
1842
|
warningCount,
|
|
1715
|
-
|
|
1843
|
+
fixesProposed: fixes.length,
|
|
1844
|
+
fixesApplied: 0,
|
|
1845
|
+
fixesFailed: 0,
|
|
1846
|
+
exitReason: "dry_run",
|
|
1716
1847
|
durationMs: Date.now() - roundStart
|
|
1717
1848
|
});
|
|
1718
1849
|
continue;
|
|
1719
1850
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1851
|
+
let applied = 0;
|
|
1852
|
+
let failed = 0;
|
|
1853
|
+
const currentFingerprints = /* @__PURE__ */ new Set();
|
|
1854
|
+
for (const fix of fixes) {
|
|
1855
|
+
const fingerprint = `${fix.file}:${fix.description}`;
|
|
1856
|
+
currentFingerprints.add(fingerprint);
|
|
1857
|
+
if (stuckFingerprints.has(fingerprint)) {
|
|
1858
|
+
console.error(chalk9.dim(` Skip (stuck): ${fix.file}:${fix.line}`));
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (prevFingerprints.has(fingerprint)) {
|
|
1862
|
+
stuckFingerprints.add(fingerprint);
|
|
1863
|
+
console.error(chalk9.yellow(` Stuck (recurring): ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1864
|
+
failed++;
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
const success = applyFix(fix, projectDir);
|
|
1868
|
+
if (success) {
|
|
1869
|
+
applied++;
|
|
1870
|
+
console.error(chalk9.green(` Fixed: ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1871
|
+
} else {
|
|
1872
|
+
failed++;
|
|
1873
|
+
console.error(chalk9.red(` Failed: ${fix.file}:${fix.line} \u2014 could not match old code`));
|
|
1874
|
+
}
|
|
1739
1875
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1876
|
+
prevFingerprints = currentFingerprints;
|
|
1877
|
+
if (applied > 0 && !options.noStage) {
|
|
1878
|
+
try {
|
|
1879
|
+
execFileSync2("git", ["add", "-A"], { cwd: projectDir, stdio: "pipe" });
|
|
1880
|
+
console.error(chalk9.dim(" Changes staged."));
|
|
1881
|
+
} catch {
|
|
1882
|
+
console.error(chalk9.yellow(" Could not stage changes (not a git repo?)."));
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const exitReason2 = applied === 0 ? "no_diff" : "continue";
|
|
1745
1886
|
rounds.push({
|
|
1746
1887
|
round,
|
|
1747
1888
|
reviewVerdict: verdict,
|
|
1748
1889
|
reviewScore: score,
|
|
1749
1890
|
criticalCount,
|
|
1750
1891
|
warningCount,
|
|
1751
|
-
|
|
1892
|
+
fixesProposed: fixes.length,
|
|
1893
|
+
fixesApplied: applied,
|
|
1894
|
+
fixesFailed: failed,
|
|
1895
|
+
exitReason: exitReason2,
|
|
1752
1896
|
durationMs: Date.now() - roundStart
|
|
1753
1897
|
});
|
|
1754
|
-
if (
|
|
1755
|
-
console.error(chalk9.yellow(" No
|
|
1898
|
+
if (applied === 0) {
|
|
1899
|
+
console.error(chalk9.yellow(" No fixes applied \u2014 stopping loop."));
|
|
1756
1900
|
break;
|
|
1757
1901
|
}
|
|
1902
|
+
if (stuckFingerprints.size >= fixes.length) {
|
|
1903
|
+
console.error(chalk9.yellow(" All remaining issues are stuck \u2014 stopping."));
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
console.error(chalk9.dim(` Applied ${applied}, failed ${failed}. Continuing to re-review...`));
|
|
1758
1907
|
}
|
|
1759
1908
|
const lastRound = rounds[rounds.length - 1];
|
|
1909
|
+
const totalApplied = rounds.reduce((sum, r) => sum + r.fixesApplied, 0);
|
|
1910
|
+
const totalProposed = rounds.reduce((sum, r) => sum + r.fixesProposed, 0);
|
|
1760
1911
|
const policyCtx = {
|
|
1761
1912
|
criticalCount: lastRound?.criticalCount ?? 0,
|
|
1762
1913
|
warningCount: lastRound?.warningCount ?? 0,
|
|
@@ -1765,17 +1916,31 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1765
1916
|
cleanupHighCount: 0
|
|
1766
1917
|
};
|
|
1767
1918
|
const policy = evaluatePolicy("review.completed", policyCtx, DEFAULT_RULES);
|
|
1919
|
+
const exitReason = converged ? "all_resolved" : stuckFingerprints.size > 0 ? "all_stuck" : lastRound?.exitReason ?? "max_iterations";
|
|
1768
1920
|
const output = {
|
|
1769
1921
|
target: fileOrGlob,
|
|
1770
1922
|
converged,
|
|
1923
|
+
exitReason,
|
|
1771
1924
|
rounds,
|
|
1772
1925
|
totalRounds: rounds.length,
|
|
1926
|
+
totalFixesProposed: totalProposed,
|
|
1927
|
+
totalFixesApplied: totalApplied,
|
|
1928
|
+
stuckCount: stuckFingerprints.size,
|
|
1773
1929
|
finalVerdict: lastRound?.reviewVerdict ?? "unknown",
|
|
1774
1930
|
finalScore: lastRound?.reviewScore ?? null,
|
|
1775
1931
|
policy,
|
|
1776
1932
|
sessionId: session2.id,
|
|
1777
1933
|
codexThreadId: threadId
|
|
1778
1934
|
};
|
|
1935
|
+
const color = converged ? chalk9.green : chalk9.red;
|
|
1936
|
+
console.error(color(`
|
|
1937
|
+
Result: ${converged ? "CONVERGED" : "NOT CONVERGED"} (${exitReason})`));
|
|
1938
|
+
console.error(` Rounds: ${rounds.length}/${options.maxRounds}`);
|
|
1939
|
+
console.error(` Fixes: ${totalApplied} applied, ${totalProposed - totalApplied} failed/skipped`);
|
|
1940
|
+
if (stuckFingerprints.size > 0) {
|
|
1941
|
+
console.error(chalk9.yellow(` Stuck issues: ${stuckFingerprints.size}`));
|
|
1942
|
+
}
|
|
1943
|
+
console.error(` Final: ${lastRound?.reviewVerdict ?? "?"} (${lastRound?.reviewScore ?? "?"}/10)`);
|
|
1779
1944
|
console.log(JSON.stringify(output, null, 2));
|
|
1780
1945
|
db.close();
|
|
1781
1946
|
}
|
|
@@ -1819,7 +1984,7 @@ Initialized with '${presetName}' preset`));
|
|
|
1819
1984
|
}
|
|
1820
1985
|
|
|
1821
1986
|
// src/commands/install-skills.ts
|
|
1822
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as
|
|
1987
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1823
1988
|
import { dirname, join as join5 } from "path";
|
|
1824
1989
|
import chalk11 from "chalk";
|
|
1825
1990
|
var SKILLS = [
|
|
@@ -2197,7 +2362,7 @@ async function installSkillsCommand(options) {
|
|
|
2197
2362
|
continue;
|
|
2198
2363
|
}
|
|
2199
2364
|
mkdirSync2(dir, { recursive: true });
|
|
2200
|
-
|
|
2365
|
+
writeFileSync2(fullPath, skill.content, "utf-8");
|
|
2201
2366
|
console.error(chalk11.green(` OK ${skill.path}`));
|
|
2202
2367
|
installed++;
|
|
2203
2368
|
}
|
|
@@ -2206,7 +2371,7 @@ async function installSkillsCommand(options) {
|
|
|
2206
2371
|
const claudeMdPath = join5(cwd, "CLAUDE.md");
|
|
2207
2372
|
const marker = "## CodeMoot \u2014 Multi-Model Collaboration";
|
|
2208
2373
|
if (existsSync3(claudeMdPath)) {
|
|
2209
|
-
const existing =
|
|
2374
|
+
const existing = readFileSync3(claudeMdPath, "utf-8");
|
|
2210
2375
|
if (existing.includes(marker)) {
|
|
2211
2376
|
if (options.force) {
|
|
2212
2377
|
const markerIdx = existing.indexOf(marker);
|
|
@@ -2214,7 +2379,7 @@ async function installSkillsCommand(options) {
|
|
|
2214
2379
|
const afterMarker = existing.slice(markerIdx + marker.length);
|
|
2215
2380
|
const nextHeadingMatch = afterMarker.match(/\n#{1,2} (?!#)(?!CodeMoot)/);
|
|
2216
2381
|
const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
|
|
2217
|
-
|
|
2382
|
+
writeFileSync2(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
|
|
2218
2383
|
console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
|
|
2219
2384
|
installed++;
|
|
2220
2385
|
} else {
|
|
@@ -2222,12 +2387,12 @@ async function installSkillsCommand(options) {
|
|
|
2222
2387
|
skipped++;
|
|
2223
2388
|
}
|
|
2224
2389
|
} else {
|
|
2225
|
-
|
|
2390
|
+
writeFileSync2(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
|
|
2226
2391
|
console.error(chalk11.green(" OK CLAUDE.md (appended CodeMoot section)"));
|
|
2227
2392
|
installed++;
|
|
2228
2393
|
}
|
|
2229
2394
|
} else {
|
|
2230
|
-
|
|
2395
|
+
writeFileSync2(claudeMdPath, `# Project Instructions
|
|
2231
2396
|
${CLAUDE_MD_SECTION}`, "utf-8");
|
|
2232
2397
|
console.error(chalk11.green(" OK CLAUDE.md (created with CodeMoot section)"));
|
|
2233
2398
|
installed++;
|
|
@@ -2238,7 +2403,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2238
2403
|
const settingsPath = join5(settingsDir, "settings.json");
|
|
2239
2404
|
if (existsSync3(settingsPath)) {
|
|
2240
2405
|
try {
|
|
2241
|
-
const existing = JSON.parse(
|
|
2406
|
+
const existing = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
2242
2407
|
const hasCodemootHook = Array.isArray(existing.hooks?.PostToolUse) && existing.hooks.PostToolUse.some((h) => h.command?.includes("codemoot"));
|
|
2243
2408
|
if (hasCodemootHook && !options.force) {
|
|
2244
2409
|
console.error(chalk11.dim(" SKIP .claude/settings.json (codemoot hook exists)"));
|
|
@@ -2249,7 +2414,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2249
2414
|
...existing.hooks,
|
|
2250
2415
|
PostToolUse: [...otherHooks, ...HOOKS_CONFIG.hooks.PostToolUse]
|
|
2251
2416
|
};
|
|
2252
|
-
|
|
2417
|
+
writeFileSync2(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
2253
2418
|
console.error(chalk11.green(" OK .claude/settings.json (added post-commit hint hook)"));
|
|
2254
2419
|
installed++;
|
|
2255
2420
|
}
|
|
@@ -2260,7 +2425,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2260
2425
|
}
|
|
2261
2426
|
} else {
|
|
2262
2427
|
mkdirSync2(settingsDir, { recursive: true });
|
|
2263
|
-
|
|
2428
|
+
writeFileSync2(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
|
|
2264
2429
|
console.error(chalk11.green(" OK .claude/settings.json (created with post-commit hook)"));
|
|
2265
2430
|
installed++;
|
|
2266
2431
|
}
|
|
@@ -2558,7 +2723,7 @@ async function jobsStatusCommand(jobId) {
|
|
|
2558
2723
|
}
|
|
2559
2724
|
|
|
2560
2725
|
// src/commands/plan.ts
|
|
2561
|
-
import { writeFileSync as
|
|
2726
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2562
2727
|
import { ModelRegistry as ModelRegistry4, Orchestrator, loadConfig as loadConfig6, openDatabase as openDatabase9 } from "@codemoot/core";
|
|
2563
2728
|
import chalk15 from "chalk";
|
|
2564
2729
|
|
|
@@ -2673,7 +2838,7 @@ async function planCommand(task, options) {
|
|
|
2673
2838
|
maxRounds: options.rounds
|
|
2674
2839
|
});
|
|
2675
2840
|
if (options.output) {
|
|
2676
|
-
|
|
2841
|
+
writeFileSync3(options.output, result.finalOutput, "utf-8");
|
|
2677
2842
|
console.log(chalk15.green(`Plan saved to ${options.output}`));
|
|
2678
2843
|
}
|
|
2679
2844
|
printSessionSummary(result);
|
|
@@ -2688,9 +2853,9 @@ async function planCommand(task, options) {
|
|
|
2688
2853
|
// src/commands/review.ts
|
|
2689
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";
|
|
2690
2855
|
import chalk16 from "chalk";
|
|
2691
|
-
import { execFileSync as
|
|
2692
|
-
import { closeSync, globSync, openSync, readFileSync as
|
|
2693
|
-
import { resolve } from "path";
|
|
2856
|
+
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";
|
|
2858
|
+
import { resolve as resolve2 } from "path";
|
|
2694
2859
|
var MAX_FILE_SIZE = 100 * 1024;
|
|
2695
2860
|
var MAX_TOTAL_SIZE = 200 * 1024;
|
|
2696
2861
|
async function reviewCommand(fileOrGlob, options) {
|
|
@@ -2805,7 +2970,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
|
|
|
2805
2970
|
} else if (mode === "diff") {
|
|
2806
2971
|
let diff;
|
|
2807
2972
|
try {
|
|
2808
|
-
diff =
|
|
2973
|
+
diff = execFileSync3("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
2809
2974
|
cwd: projectDir,
|
|
2810
2975
|
encoding: "utf-8",
|
|
2811
2976
|
maxBuffer: 1024 * 1024
|
|
@@ -2833,12 +2998,13 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
|
2833
2998
|
console.error(chalk16.cyan(`Reviewing diff ${options.diff} (session: ${session2.id.slice(0, 8)}...)...`));
|
|
2834
2999
|
} else {
|
|
2835
3000
|
let globPattern = fileOrGlob;
|
|
2836
|
-
const resolvedInput =
|
|
3001
|
+
const resolvedInput = resolve2(projectDir, globPattern);
|
|
2837
3002
|
if (existsSync4(resolvedInput) && statSync(resolvedInput).isDirectory()) {
|
|
2838
3003
|
globPattern = `${globPattern}/**/*`;
|
|
2839
3004
|
console.error(chalk16.dim(` Expanding directory to: ${globPattern}`));
|
|
2840
3005
|
}
|
|
2841
|
-
const
|
|
3006
|
+
const projectRoot = resolve2(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
3007
|
+
const paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve2(projectDir, p)).filter((p) => p.startsWith(projectRoot) || p === resolve2(projectDir));
|
|
2842
3008
|
if (paths.length === 0) {
|
|
2843
3009
|
console.error(chalk16.red(`No files matched: ${fileOrGlob}`));
|
|
2844
3010
|
db.close();
|
|
@@ -2865,7 +3031,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
|
2865
3031
|
console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
|
|
2866
3032
|
continue;
|
|
2867
3033
|
}
|
|
2868
|
-
const content =
|
|
3034
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
2869
3035
|
const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
2870
3036
|
files.push({ path: relativePath, content });
|
|
2871
3037
|
totalSize += stat.size;
|
|
@@ -3114,7 +3280,7 @@ async function shipitCommand(options) {
|
|
|
3114
3280
|
|
|
3115
3281
|
// src/commands/start.ts
|
|
3116
3282
|
import { existsSync as existsSync5 } from "fs";
|
|
3117
|
-
import { execFileSync as
|
|
3283
|
+
import { execFileSync as execFileSync4, execSync as execSync6 } from "child_process";
|
|
3118
3284
|
import { join as join6, basename as basename2 } from "path";
|
|
3119
3285
|
import chalk19 from "chalk";
|
|
3120
3286
|
import { loadConfig as loadConfig9, writeConfig as writeConfig2 } from "@codemoot/core";
|
|
@@ -3179,7 +3345,7 @@ async function startCommand() {
|
|
|
3179
3345
|
codemoot review ${reviewTarget} --preset quick-scan
|
|
3180
3346
|
`));
|
|
3181
3347
|
try {
|
|
3182
|
-
const output =
|
|
3348
|
+
const output = execFileSync4("codemoot", ["review", reviewTarget, "--preset", "quick-scan"], {
|
|
3183
3349
|
cwd,
|
|
3184
3350
|
encoding: "utf-8",
|
|
3185
3351
|
timeout: 3e5,
|
|
@@ -3450,11 +3616,12 @@ async function workerCommand(options) {
|
|
|
3450
3616
|
console.error(chalk21.cyan(`Processing job ${job.id} (type: ${job.type})`));
|
|
3451
3617
|
jobStore.appendLog(job.id, "info", "job_started", `Worker ${workerId} claimed job`);
|
|
3452
3618
|
try {
|
|
3453
|
-
const { resolve:
|
|
3619
|
+
const { resolve: resolve3, normalize } = await import("path");
|
|
3454
3620
|
const payload = JSON.parse(job.payloadJson);
|
|
3455
|
-
const rawCwd =
|
|
3621
|
+
const rawCwd = resolve3(payload.path ?? payload.cwd ?? projectDir);
|
|
3456
3622
|
const cwd = normalize(rawCwd);
|
|
3457
|
-
|
|
3623
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
3624
|
+
if (cwd !== normalize(projectDir) && !cwd.startsWith(normalize(projectDir) + sep)) {
|
|
3458
3625
|
throw new Error(`Path traversal blocked: "${cwd}" is outside project directory "${projectDir}"`);
|
|
3459
3626
|
}
|
|
3460
3627
|
const timeout = (payload.timeout ?? 600) * 1e3;
|
|
@@ -3472,14 +3639,14 @@ Start by listing candidate files, then inspect them thoroughly.`,
|
|
|
3472
3639
|
resumed: false
|
|
3473
3640
|
});
|
|
3474
3641
|
} else if (payload.diff) {
|
|
3475
|
-
const { execFileSync:
|
|
3642
|
+
const { execFileSync: execFileSync5 } = await import("child_process");
|
|
3476
3643
|
const diffArgs = payload.diff.split(/\s+/).filter((a) => a.length > 0);
|
|
3477
3644
|
for (const arg of diffArgs) {
|
|
3478
3645
|
if (arg.startsWith("-") || !/^[a-zA-Z0-9_.~^:\/\\@{}]+$/.test(arg)) {
|
|
3479
3646
|
throw new Error(`Invalid diff argument: "${arg}" \u2014 only git refs and paths allowed`);
|
|
3480
3647
|
}
|
|
3481
3648
|
}
|
|
3482
|
-
const diff =
|
|
3649
|
+
const diff = execFileSync5("git", ["diff", ...diffArgs], {
|
|
3483
3650
|
cwd,
|
|
3484
3651
|
encoding: "utf-8",
|
|
3485
3652
|
maxBuffer: 1024 * 1024
|
|
@@ -3600,7 +3767,7 @@ jobs.command("status").description("Show job details with recent logs").argument
|
|
|
3600
3767
|
jobs.command("logs").description("Show job logs").argument("<job-id>", "Job ID").option("--from-seq <n>", "Start from log sequence number", (v) => Number.parseInt(v, 10), 0).option("--limit <n>", "Max log entries", (v) => Number.parseInt(v, 10), 100).action(jobsLogsCommand);
|
|
3601
3768
|
jobs.command("cancel").description("Cancel a queued or running job").argument("<job-id>", "Job ID").action(jobsCancelCommand);
|
|
3602
3769
|
jobs.command("retry").description("Retry a failed job").argument("<job-id>", "Job ID").action(jobsRetryCommand);
|
|
3603
|
-
program.command("fix").description("Autofix loop: review code, apply fixes, re-review until approved").argument("<file-or-glob>", "File path or glob pattern to fix").option("--max-rounds <n>", "Max review-fix rounds", (v) => Number.parseInt(v, 10), 3).addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("bugs")).option("--timeout <seconds>", "Timeout per round", (v) => Number.parseInt(v, 10), 600).option("--dry-run", "Review only, do not apply fixes", false).option("--diff <revspec>", "Fix issues in a git diff").option("--session <id>", "Use specific session").action(fixCommand);
|
|
3770
|
+
program.command("fix").description("Autofix loop: review code, apply fixes, re-review until approved").argument("<file-or-glob>", "File path or glob pattern to fix").option("--max-rounds <n>", "Max review-fix rounds", (v) => Number.parseInt(v, 10), 3).addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("bugs")).option("--timeout <seconds>", "Timeout per round", (v) => Number.parseInt(v, 10), 600).option("--dry-run", "Review only, do not apply fixes", false).option("--no-stage", "Do not git-stage applied fixes").option("--diff <revspec>", "Fix issues in a git diff").option("--session <id>", "Use specific session").action(fixCommand);
|
|
3604
3771
|
program.command("shipit").description("Run composite workflow: lint \u2192 test \u2192 review \u2192 cleanup \u2192 commit").addOption(new Option("--profile <profile>", "Workflow profile").choices(["fast", "safe", "full"]).default("safe")).option("--dry-run", "Print planned steps without executing", false).option("--no-commit", "Run checks but skip commit step").option("--json", "Machine-readable JSON output", false).option("--strict-output", "Strict model output parsing", false).action(shipitCommand);
|
|
3605
3772
|
program.command("cost").description("Token usage and cost dashboard").addOption(new Option("--scope <scope>", "Time scope").choices(["session", "daily", "all"]).default("daily")).option("--days <n>", "Number of days for daily scope", (v) => Number.parseInt(v, 10), 30).option("--session <id>", "Session ID for session scope").action(costCommand);
|
|
3606
3773
|
program.command("watch").description("Watch files and enqueue reviews on change").option("--glob <pattern>", "Glob pattern to watch", "**/*.{ts,tsx,js,jsx}").addOption(new Option("--focus <area>", "Focus area").choices(["security", "performance", "bugs", "all"]).default("all")).option("--timeout <seconds>", "Review timeout", (v) => Number.parseInt(v, 10), 600).option("--quiet-ms <ms>", "Quiet period before flush", (v) => Number.parseInt(v, 10), 800).option("--max-wait-ms <ms>", "Max wait before forced flush", (v) => Number.parseInt(v, 10), 5e3).option("--cooldown-ms <ms>", "Cooldown after flush", (v) => Number.parseInt(v, 10), 1500).action(watchCommand);
|