@codemoot/cli 0.2.4 → 0.2.5
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 +235 -73
- 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
|
|
|
@@ -103,13 +103,25 @@ function getDbPath(projectDir) {
|
|
|
103
103
|
}
|
|
104
104
|
async function withDatabase(fn) {
|
|
105
105
|
const db = openDatabase(getDbPath());
|
|
106
|
+
const originalExit = process.exit;
|
|
107
|
+
let requestedExitCode;
|
|
108
|
+
process.exit = ((code) => {
|
|
109
|
+
requestedExitCode = typeof code === "number" ? code : 1;
|
|
110
|
+
throw new Error("__WITH_DATABASE_EXIT__");
|
|
111
|
+
});
|
|
106
112
|
try {
|
|
107
113
|
return await fn(db);
|
|
108
114
|
} catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
if (requestedExitCode === void 0) {
|
|
116
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
111
119
|
} finally {
|
|
120
|
+
process.exit = originalExit;
|
|
112
121
|
db.close();
|
|
122
|
+
if (requestedExitCode !== void 0) {
|
|
123
|
+
originalExit(requestedExitCode);
|
|
124
|
+
}
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
|
@@ -367,7 +379,7 @@ async function buildReviewCommand(buildId) {
|
|
|
367
379
|
try {
|
|
368
380
|
execSync(`git read-tree HEAD`, { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
369
381
|
execSync("git add -A", { cwd: projectDir, encoding: "utf-8", env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
370
|
-
diff =
|
|
382
|
+
diff = execFileSync("git", ["diff", "--cached", "--", run.baselineRef], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
|
|
371
383
|
} finally {
|
|
372
384
|
try {
|
|
373
385
|
unlinkSync(tmpIndex);
|
|
@@ -963,8 +975,8 @@ import { readFileSync } from "fs";
|
|
|
963
975
|
async function cleanupCommand(path, options) {
|
|
964
976
|
let db;
|
|
965
977
|
try {
|
|
966
|
-
const { resolve:
|
|
967
|
-
const projectDir =
|
|
978
|
+
const { resolve: resolve3 } = await import("path");
|
|
979
|
+
const projectDir = resolve3(path);
|
|
968
980
|
if (options.background) {
|
|
969
981
|
const bgDb = openDatabase4(getDbPath());
|
|
970
982
|
const jobStore = new JobStore(bgDb);
|
|
@@ -1169,8 +1181,8 @@ Scan complete in ${(durationMs / 1e3).toFixed(1)}s`));
|
|
|
1169
1181
|
durationMs
|
|
1170
1182
|
});
|
|
1171
1183
|
if (options.output) {
|
|
1172
|
-
const { writeFileSync:
|
|
1173
|
-
|
|
1184
|
+
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
1185
|
+
writeFileSync4(options.output, JSON.stringify(report, null, 2), "utf-8");
|
|
1174
1186
|
console.error(chalk5.green(` Findings written to ${options.output}`));
|
|
1175
1187
|
}
|
|
1176
1188
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -1585,7 +1597,9 @@ async function eventsCommand(options) {
|
|
|
1585
1597
|
}
|
|
1586
1598
|
|
|
1587
1599
|
// src/commands/fix.ts
|
|
1588
|
-
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
1600
|
+
import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
|
|
1601
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1602
|
+
import { resolve } from "path";
|
|
1589
1603
|
import {
|
|
1590
1604
|
DEFAULT_RULES,
|
|
1591
1605
|
ModelRegistry as ModelRegistry3,
|
|
@@ -1597,6 +1611,54 @@ import {
|
|
|
1597
1611
|
REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS2
|
|
1598
1612
|
} from "@codemoot/core";
|
|
1599
1613
|
import chalk9 from "chalk";
|
|
1614
|
+
function parseFixes(text) {
|
|
1615
|
+
const fixes = [];
|
|
1616
|
+
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;
|
|
1617
|
+
let match;
|
|
1618
|
+
match = fixPattern.exec(text);
|
|
1619
|
+
while (match !== null) {
|
|
1620
|
+
const file = match[1];
|
|
1621
|
+
const line = Number.parseInt(match[2], 10);
|
|
1622
|
+
const description = match[3].trim();
|
|
1623
|
+
if (match[4] !== void 0 && match[5] !== void 0) {
|
|
1624
|
+
fixes.push({ file, line, description, oldCode: match[4], newCode: match[5] });
|
|
1625
|
+
} else if (match[6] !== void 0) {
|
|
1626
|
+
fixes.push({ file, line, description, oldCode: "", newCode: match[6] });
|
|
1627
|
+
}
|
|
1628
|
+
match = fixPattern.exec(text);
|
|
1629
|
+
}
|
|
1630
|
+
return fixes;
|
|
1631
|
+
}
|
|
1632
|
+
function applyFix(fix, projectDir) {
|
|
1633
|
+
const filePath = resolve(projectDir, fix.file);
|
|
1634
|
+
const normalizedProject = resolve(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
1635
|
+
if (!resolve(filePath).startsWith(normalizedProject)) {
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
let content;
|
|
1639
|
+
try {
|
|
1640
|
+
content = readFileSync2(filePath, "utf-8");
|
|
1641
|
+
} catch {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
const lines = content.split("\n");
|
|
1645
|
+
if (fix.oldCode) {
|
|
1646
|
+
const trimmedOld = fix.oldCode.trim();
|
|
1647
|
+
if (content.includes(trimmedOld)) {
|
|
1648
|
+
const updated = content.replace(trimmedOld, fix.newCode.trim());
|
|
1649
|
+
writeFileSync(filePath, updated, "utf-8");
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
const lineIdx = fix.line - 1;
|
|
1655
|
+
if (lineIdx < 0 || lineIdx >= lines.length) return false;
|
|
1656
|
+
const newLines = fix.newCode.trim().split("\n");
|
|
1657
|
+
const oldLineCount = Math.max(1, newLines.length);
|
|
1658
|
+
lines.splice(lineIdx, oldLineCount, ...newLines);
|
|
1659
|
+
writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
1660
|
+
return true;
|
|
1661
|
+
}
|
|
1600
1662
|
async function fixCommand(fileOrGlob, options) {
|
|
1601
1663
|
const projectDir = process.cwd();
|
|
1602
1664
|
const db = openDatabase7(getDbPath());
|
|
@@ -1627,6 +1689,8 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1627
1689
|
let threadId = currentSession?.codexThreadId ?? void 0;
|
|
1628
1690
|
const rounds = [];
|
|
1629
1691
|
let converged = false;
|
|
1692
|
+
const stuckFingerprints = /* @__PURE__ */ new Set();
|
|
1693
|
+
let prevFingerprints = /* @__PURE__ */ new Set();
|
|
1630
1694
|
console.error(
|
|
1631
1695
|
chalk9.cyan(
|
|
1632
1696
|
`Autofix loop: ${fileOrGlob} (max ${options.maxRounds} rounds, focus: ${options.focus})`
|
|
@@ -1644,7 +1708,7 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1644
1708
|
let diffContent = "";
|
|
1645
1709
|
if (options.diff) {
|
|
1646
1710
|
try {
|
|
1647
|
-
diffContent =
|
|
1711
|
+
diffContent = execFileSync2("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
1648
1712
|
cwd: projectDir,
|
|
1649
1713
|
encoding: "utf-8",
|
|
1650
1714
|
maxBuffer: 1024 * 1024
|
|
@@ -1653,23 +1717,46 @@ async function fixCommand(fileOrGlob, options) {
|
|
|
1653
1717
|
diffContent = "";
|
|
1654
1718
|
}
|
|
1655
1719
|
}
|
|
1720
|
+
const fixOutputContract = [
|
|
1721
|
+
"You are an autofix engine. For EVERY fixable issue, you MUST output this EXACT format:",
|
|
1722
|
+
"",
|
|
1723
|
+
"FIX: path/to/file.ts:42 Description of the bug",
|
|
1724
|
+
"```old",
|
|
1725
|
+
"exact old code copied from the file",
|
|
1726
|
+
"```",
|
|
1727
|
+
"```new",
|
|
1728
|
+
"exact replacement code",
|
|
1729
|
+
"```",
|
|
1730
|
+
"",
|
|
1731
|
+
"Then end with:",
|
|
1732
|
+
"VERDICT: APPROVED or VERDICT: NEEDS_REVISION",
|
|
1733
|
+
"SCORE: X/10",
|
|
1734
|
+
"",
|
|
1735
|
+
"Rules:",
|
|
1736
|
+
"- The ```old block MUST be an exact substring copy from the file (whitespace-sensitive).",
|
|
1737
|
+
"- One FIX block per issue.",
|
|
1738
|
+
"- Issues without a FIX block are IGNORED by the autofix engine.",
|
|
1739
|
+
"- If the code is clean, output VERDICT: APPROVED with no FIX blocks."
|
|
1740
|
+
].join("\n");
|
|
1656
1741
|
const reviewPrompt = buildHandoffEnvelope3({
|
|
1657
|
-
command: "
|
|
1742
|
+
command: "custom",
|
|
1658
1743
|
task: options.diff ? `Review and identify fixable issues in this diff.
|
|
1659
1744
|
|
|
1660
1745
|
GIT DIFF (${options.diff}):
|
|
1661
|
-
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}
|
|
1746
|
+
${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}
|
|
1747
|
+
|
|
1748
|
+
${fixOutputContract}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers and exact fixes.
|
|
1749
|
+
|
|
1750
|
+
${fixOutputContract}`,
|
|
1662
1751
|
constraints: [
|
|
1663
1752
|
`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.` : ""
|
|
1753
|
+
round > 1 ? `This is re-review round ${round}. Previous fixes were applied by the host. Only report REMAINING unfixed issues.` : ""
|
|
1667
1754
|
].filter(Boolean),
|
|
1668
1755
|
resumed: Boolean(threadId)
|
|
1669
1756
|
});
|
|
1670
1757
|
const timeoutMs = options.timeout * 1e3;
|
|
1671
1758
|
const progress = createProgressCallbacks("fix-review");
|
|
1672
|
-
console.error(chalk9.dim("
|
|
1759
|
+
console.error(chalk9.dim(" GPT reviewing..."));
|
|
1673
1760
|
const reviewResult = await adapter.callWithResume(reviewPrompt, {
|
|
1674
1761
|
sessionId: threadId,
|
|
1675
1762
|
timeout: timeoutMs,
|
|
@@ -1680,6 +1767,18 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1680
1767
|
sessionMgr.updateThreadId(session2.id, reviewResult.sessionId);
|
|
1681
1768
|
}
|
|
1682
1769
|
sessionMgr.addUsageFromResult(session2.id, reviewResult.usage, reviewPrompt, reviewResult.text);
|
|
1770
|
+
sessionMgr.recordEvent({
|
|
1771
|
+
sessionId: session2.id,
|
|
1772
|
+
command: "fix",
|
|
1773
|
+
subcommand: `review-round-${round}`,
|
|
1774
|
+
promptPreview: `Fix review round ${round}: ${fileOrGlob}`,
|
|
1775
|
+
responsePreview: reviewResult.text.slice(0, 500),
|
|
1776
|
+
promptFull: reviewPrompt,
|
|
1777
|
+
responseFull: reviewResult.text,
|
|
1778
|
+
usageJson: JSON.stringify(reviewResult.usage),
|
|
1779
|
+
durationMs: reviewResult.durationMs,
|
|
1780
|
+
codexThreadId: reviewResult.sessionId
|
|
1781
|
+
});
|
|
1683
1782
|
const tail = reviewResult.text.slice(-500);
|
|
1684
1783
|
const verdictMatch = tail.match(/VERDICT:\s*(APPROVED|NEEDS_REVISION)/i);
|
|
1685
1784
|
const scoreMatch = tail.match(/SCORE:\s*(\d+)\/10/);
|
|
@@ -1697,66 +1796,113 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1697
1796
|
reviewScore: score,
|
|
1698
1797
|
criticalCount,
|
|
1699
1798
|
warningCount,
|
|
1700
|
-
|
|
1799
|
+
fixesProposed: 0,
|
|
1800
|
+
fixesApplied: 0,
|
|
1801
|
+
fixesFailed: 0,
|
|
1802
|
+
exitReason: "all_resolved",
|
|
1701
1803
|
durationMs: Date.now() - roundStart
|
|
1702
1804
|
});
|
|
1703
1805
|
converged = true;
|
|
1704
|
-
console.error(chalk9.green("
|
|
1806
|
+
console.error(chalk9.green(" APPROVED \u2014 all issues resolved."));
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
const fixes = parseFixes(reviewResult.text);
|
|
1810
|
+
console.error(chalk9.dim(` Found ${fixes.length} fix proposal(s)`));
|
|
1811
|
+
if (fixes.length === 0) {
|
|
1812
|
+
rounds.push({
|
|
1813
|
+
round,
|
|
1814
|
+
reviewVerdict: verdict,
|
|
1815
|
+
reviewScore: score,
|
|
1816
|
+
criticalCount,
|
|
1817
|
+
warningCount,
|
|
1818
|
+
fixesProposed: 0,
|
|
1819
|
+
fixesApplied: 0,
|
|
1820
|
+
fixesFailed: 0,
|
|
1821
|
+
exitReason: "no_fixes_proposed",
|
|
1822
|
+
durationMs: Date.now() - roundStart
|
|
1823
|
+
});
|
|
1824
|
+
console.error(chalk9.yellow(" GPT found issues but proposed no structured fixes. Stopping."));
|
|
1705
1825
|
break;
|
|
1706
1826
|
}
|
|
1707
1827
|
if (options.dryRun) {
|
|
1708
|
-
console.error(chalk9.yellow(" Dry-run:
|
|
1828
|
+
console.error(chalk9.yellow(" Dry-run: showing proposed fixes without applying."));
|
|
1829
|
+
for (const fix of fixes) {
|
|
1830
|
+
console.error(chalk9.dim(` ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1831
|
+
}
|
|
1709
1832
|
rounds.push({
|
|
1710
1833
|
round,
|
|
1711
1834
|
reviewVerdict: verdict,
|
|
1712
1835
|
reviewScore: score,
|
|
1713
1836
|
criticalCount,
|
|
1714
1837
|
warningCount,
|
|
1715
|
-
|
|
1838
|
+
fixesProposed: fixes.length,
|
|
1839
|
+
fixesApplied: 0,
|
|
1840
|
+
fixesFailed: 0,
|
|
1841
|
+
exitReason: "dry_run",
|
|
1716
1842
|
durationMs: Date.now() - roundStart
|
|
1717
1843
|
});
|
|
1718
1844
|
continue;
|
|
1719
1845
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1846
|
+
let applied = 0;
|
|
1847
|
+
let failed = 0;
|
|
1848
|
+
const currentFingerprints = /* @__PURE__ */ new Set();
|
|
1849
|
+
for (const fix of fixes) {
|
|
1850
|
+
const fingerprint = `${fix.file}:${fix.description}`;
|
|
1851
|
+
currentFingerprints.add(fingerprint);
|
|
1852
|
+
if (stuckFingerprints.has(fingerprint)) {
|
|
1853
|
+
console.error(chalk9.dim(` Skip (stuck): ${fix.file}:${fix.line}`));
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (prevFingerprints.has(fingerprint)) {
|
|
1857
|
+
stuckFingerprints.add(fingerprint);
|
|
1858
|
+
console.error(chalk9.yellow(` Stuck (recurring): ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1859
|
+
failed++;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const success = applyFix(fix, projectDir);
|
|
1863
|
+
if (success) {
|
|
1864
|
+
applied++;
|
|
1865
|
+
console.error(chalk9.green(` Fixed: ${fix.file}:${fix.line} \u2014 ${fix.description}`));
|
|
1866
|
+
} else {
|
|
1867
|
+
failed++;
|
|
1868
|
+
console.error(chalk9.red(` Failed: ${fix.file}:${fix.line} \u2014 could not match old code`));
|
|
1869
|
+
}
|
|
1739
1870
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1871
|
+
prevFingerprints = currentFingerprints;
|
|
1872
|
+
if (applied > 0 && !options.noStage) {
|
|
1873
|
+
try {
|
|
1874
|
+
execFileSync2("git", ["add", "-A"], { cwd: projectDir, stdio: "pipe" });
|
|
1875
|
+
console.error(chalk9.dim(" Changes staged."));
|
|
1876
|
+
} catch {
|
|
1877
|
+
console.error(chalk9.yellow(" Could not stage changes (not a git repo?)."));
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const exitReason2 = applied === 0 ? "no_diff" : "continue";
|
|
1745
1881
|
rounds.push({
|
|
1746
1882
|
round,
|
|
1747
1883
|
reviewVerdict: verdict,
|
|
1748
1884
|
reviewScore: score,
|
|
1749
1885
|
criticalCount,
|
|
1750
1886
|
warningCount,
|
|
1751
|
-
|
|
1887
|
+
fixesProposed: fixes.length,
|
|
1888
|
+
fixesApplied: applied,
|
|
1889
|
+
fixesFailed: failed,
|
|
1890
|
+
exitReason: exitReason2,
|
|
1752
1891
|
durationMs: Date.now() - roundStart
|
|
1753
1892
|
});
|
|
1754
|
-
if (
|
|
1755
|
-
console.error(chalk9.yellow(" No
|
|
1893
|
+
if (applied === 0) {
|
|
1894
|
+
console.error(chalk9.yellow(" No fixes applied \u2014 stopping loop."));
|
|
1756
1895
|
break;
|
|
1757
1896
|
}
|
|
1897
|
+
if (stuckFingerprints.size >= fixes.length) {
|
|
1898
|
+
console.error(chalk9.yellow(" All remaining issues are stuck \u2014 stopping."));
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
console.error(chalk9.dim(` Applied ${applied}, failed ${failed}. Continuing to re-review...`));
|
|
1758
1902
|
}
|
|
1759
1903
|
const lastRound = rounds[rounds.length - 1];
|
|
1904
|
+
const totalApplied = rounds.reduce((sum, r) => sum + r.fixesApplied, 0);
|
|
1905
|
+
const totalProposed = rounds.reduce((sum, r) => sum + r.fixesProposed, 0);
|
|
1760
1906
|
const policyCtx = {
|
|
1761
1907
|
criticalCount: lastRound?.criticalCount ?? 0,
|
|
1762
1908
|
warningCount: lastRound?.warningCount ?? 0,
|
|
@@ -1765,17 +1911,31 @@ ${diffContent.slice(0, REVIEW_DIFF_MAX_CHARS2)}` : `Review ${fileOrGlob} and ide
|
|
|
1765
1911
|
cleanupHighCount: 0
|
|
1766
1912
|
};
|
|
1767
1913
|
const policy = evaluatePolicy("review.completed", policyCtx, DEFAULT_RULES);
|
|
1914
|
+
const exitReason = converged ? "all_resolved" : stuckFingerprints.size > 0 ? "all_stuck" : lastRound?.exitReason ?? "max_iterations";
|
|
1768
1915
|
const output = {
|
|
1769
1916
|
target: fileOrGlob,
|
|
1770
1917
|
converged,
|
|
1918
|
+
exitReason,
|
|
1771
1919
|
rounds,
|
|
1772
1920
|
totalRounds: rounds.length,
|
|
1921
|
+
totalFixesProposed: totalProposed,
|
|
1922
|
+
totalFixesApplied: totalApplied,
|
|
1923
|
+
stuckCount: stuckFingerprints.size,
|
|
1773
1924
|
finalVerdict: lastRound?.reviewVerdict ?? "unknown",
|
|
1774
1925
|
finalScore: lastRound?.reviewScore ?? null,
|
|
1775
1926
|
policy,
|
|
1776
1927
|
sessionId: session2.id,
|
|
1777
1928
|
codexThreadId: threadId
|
|
1778
1929
|
};
|
|
1930
|
+
const color = converged ? chalk9.green : chalk9.red;
|
|
1931
|
+
console.error(color(`
|
|
1932
|
+
Result: ${converged ? "CONVERGED" : "NOT CONVERGED"} (${exitReason})`));
|
|
1933
|
+
console.error(` Rounds: ${rounds.length}/${options.maxRounds}`);
|
|
1934
|
+
console.error(` Fixes: ${totalApplied} applied, ${totalProposed - totalApplied} failed/skipped`);
|
|
1935
|
+
if (stuckFingerprints.size > 0) {
|
|
1936
|
+
console.error(chalk9.yellow(` Stuck issues: ${stuckFingerprints.size}`));
|
|
1937
|
+
}
|
|
1938
|
+
console.error(` Final: ${lastRound?.reviewVerdict ?? "?"} (${lastRound?.reviewScore ?? "?"}/10)`);
|
|
1779
1939
|
console.log(JSON.stringify(output, null, 2));
|
|
1780
1940
|
db.close();
|
|
1781
1941
|
}
|
|
@@ -1819,7 +1979,7 @@ Initialized with '${presetName}' preset`));
|
|
|
1819
1979
|
}
|
|
1820
1980
|
|
|
1821
1981
|
// src/commands/install-skills.ts
|
|
1822
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as
|
|
1982
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1823
1983
|
import { dirname, join as join5 } from "path";
|
|
1824
1984
|
import chalk11 from "chalk";
|
|
1825
1985
|
var SKILLS = [
|
|
@@ -2197,7 +2357,7 @@ async function installSkillsCommand(options) {
|
|
|
2197
2357
|
continue;
|
|
2198
2358
|
}
|
|
2199
2359
|
mkdirSync2(dir, { recursive: true });
|
|
2200
|
-
|
|
2360
|
+
writeFileSync2(fullPath, skill.content, "utf-8");
|
|
2201
2361
|
console.error(chalk11.green(` OK ${skill.path}`));
|
|
2202
2362
|
installed++;
|
|
2203
2363
|
}
|
|
@@ -2206,7 +2366,7 @@ async function installSkillsCommand(options) {
|
|
|
2206
2366
|
const claudeMdPath = join5(cwd, "CLAUDE.md");
|
|
2207
2367
|
const marker = "## CodeMoot \u2014 Multi-Model Collaboration";
|
|
2208
2368
|
if (existsSync3(claudeMdPath)) {
|
|
2209
|
-
const existing =
|
|
2369
|
+
const existing = readFileSync3(claudeMdPath, "utf-8");
|
|
2210
2370
|
if (existing.includes(marker)) {
|
|
2211
2371
|
if (options.force) {
|
|
2212
2372
|
const markerIdx = existing.indexOf(marker);
|
|
@@ -2214,7 +2374,7 @@ async function installSkillsCommand(options) {
|
|
|
2214
2374
|
const afterMarker = existing.slice(markerIdx + marker.length);
|
|
2215
2375
|
const nextHeadingMatch = afterMarker.match(/\n#{1,2} (?!#)(?!CodeMoot)/);
|
|
2216
2376
|
const after = nextHeadingMatch ? afterMarker.slice(nextHeadingMatch.index) : "";
|
|
2217
|
-
|
|
2377
|
+
writeFileSync2(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
|
|
2218
2378
|
console.error(chalk11.green(" OK CLAUDE.md (updated CodeMoot section)"));
|
|
2219
2379
|
installed++;
|
|
2220
2380
|
} else {
|
|
@@ -2222,12 +2382,12 @@ async function installSkillsCommand(options) {
|
|
|
2222
2382
|
skipped++;
|
|
2223
2383
|
}
|
|
2224
2384
|
} else {
|
|
2225
|
-
|
|
2385
|
+
writeFileSync2(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
|
|
2226
2386
|
console.error(chalk11.green(" OK CLAUDE.md (appended CodeMoot section)"));
|
|
2227
2387
|
installed++;
|
|
2228
2388
|
}
|
|
2229
2389
|
} else {
|
|
2230
|
-
|
|
2390
|
+
writeFileSync2(claudeMdPath, `# Project Instructions
|
|
2231
2391
|
${CLAUDE_MD_SECTION}`, "utf-8");
|
|
2232
2392
|
console.error(chalk11.green(" OK CLAUDE.md (created with CodeMoot section)"));
|
|
2233
2393
|
installed++;
|
|
@@ -2238,7 +2398,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2238
2398
|
const settingsPath = join5(settingsDir, "settings.json");
|
|
2239
2399
|
if (existsSync3(settingsPath)) {
|
|
2240
2400
|
try {
|
|
2241
|
-
const existing = JSON.parse(
|
|
2401
|
+
const existing = JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
2242
2402
|
const hasCodemootHook = Array.isArray(existing.hooks?.PostToolUse) && existing.hooks.PostToolUse.some((h) => h.command?.includes("codemoot"));
|
|
2243
2403
|
if (hasCodemootHook && !options.force) {
|
|
2244
2404
|
console.error(chalk11.dim(" SKIP .claude/settings.json (codemoot hook exists)"));
|
|
@@ -2249,7 +2409,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2249
2409
|
...existing.hooks,
|
|
2250
2410
|
PostToolUse: [...otherHooks, ...HOOKS_CONFIG.hooks.PostToolUse]
|
|
2251
2411
|
};
|
|
2252
|
-
|
|
2412
|
+
writeFileSync2(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
2253
2413
|
console.error(chalk11.green(" OK .claude/settings.json (added post-commit hint hook)"));
|
|
2254
2414
|
installed++;
|
|
2255
2415
|
}
|
|
@@ -2260,7 +2420,7 @@ ${CLAUDE_MD_SECTION}`, "utf-8");
|
|
|
2260
2420
|
}
|
|
2261
2421
|
} else {
|
|
2262
2422
|
mkdirSync2(settingsDir, { recursive: true });
|
|
2263
|
-
|
|
2423
|
+
writeFileSync2(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
|
|
2264
2424
|
console.error(chalk11.green(" OK .claude/settings.json (created with post-commit hook)"));
|
|
2265
2425
|
installed++;
|
|
2266
2426
|
}
|
|
@@ -2558,7 +2718,7 @@ async function jobsStatusCommand(jobId) {
|
|
|
2558
2718
|
}
|
|
2559
2719
|
|
|
2560
2720
|
// src/commands/plan.ts
|
|
2561
|
-
import { writeFileSync as
|
|
2721
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2562
2722
|
import { ModelRegistry as ModelRegistry4, Orchestrator, loadConfig as loadConfig6, openDatabase as openDatabase9 } from "@codemoot/core";
|
|
2563
2723
|
import chalk15 from "chalk";
|
|
2564
2724
|
|
|
@@ -2673,7 +2833,7 @@ async function planCommand(task, options) {
|
|
|
2673
2833
|
maxRounds: options.rounds
|
|
2674
2834
|
});
|
|
2675
2835
|
if (options.output) {
|
|
2676
|
-
|
|
2836
|
+
writeFileSync3(options.output, result.finalOutput, "utf-8");
|
|
2677
2837
|
console.log(chalk15.green(`Plan saved to ${options.output}`));
|
|
2678
2838
|
}
|
|
2679
2839
|
printSessionSummary(result);
|
|
@@ -2688,9 +2848,9 @@ async function planCommand(task, options) {
|
|
|
2688
2848
|
// src/commands/review.ts
|
|
2689
2849
|
import { loadConfig as loadConfig7, ModelRegistry as ModelRegistry5, BINARY_SNIFF_BYTES, REVIEW_DIFF_MAX_CHARS as REVIEW_DIFF_MAX_CHARS3, SessionManager as SessionManager7, JobStore as JobStore3, openDatabase as openDatabase10, buildHandoffEnvelope as buildHandoffEnvelope4, getReviewPreset } from "@codemoot/core";
|
|
2690
2850
|
import chalk16 from "chalk";
|
|
2691
|
-
import { execFileSync as
|
|
2692
|
-
import { closeSync, globSync, openSync, readFileSync as
|
|
2693
|
-
import { resolve } from "path";
|
|
2851
|
+
import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
|
|
2852
|
+
import { closeSync, globSync, openSync, readFileSync as readFileSync4, readSync, statSync, existsSync as existsSync4 } from "fs";
|
|
2853
|
+
import { resolve as resolve2 } from "path";
|
|
2694
2854
|
var MAX_FILE_SIZE = 100 * 1024;
|
|
2695
2855
|
var MAX_TOTAL_SIZE = 200 * 1024;
|
|
2696
2856
|
async function reviewCommand(fileOrGlob, options) {
|
|
@@ -2805,7 +2965,7 @@ Start by listing candidate files, then inspect them thoroughly.`,
|
|
|
2805
2965
|
} else if (mode === "diff") {
|
|
2806
2966
|
let diff;
|
|
2807
2967
|
try {
|
|
2808
|
-
diff =
|
|
2968
|
+
diff = execFileSync3("git", ["diff", "--", ...options.diff.split(/\s+/)], {
|
|
2809
2969
|
cwd: projectDir,
|
|
2810
2970
|
encoding: "utf-8",
|
|
2811
2971
|
maxBuffer: 1024 * 1024
|
|
@@ -2833,12 +2993,13 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
|
2833
2993
|
console.error(chalk16.cyan(`Reviewing diff ${options.diff} (session: ${session2.id.slice(0, 8)}...)...`));
|
|
2834
2994
|
} else {
|
|
2835
2995
|
let globPattern = fileOrGlob;
|
|
2836
|
-
const resolvedInput =
|
|
2996
|
+
const resolvedInput = resolve2(projectDir, globPattern);
|
|
2837
2997
|
if (existsSync4(resolvedInput) && statSync(resolvedInput).isDirectory()) {
|
|
2838
2998
|
globPattern = `${globPattern}/**/*`;
|
|
2839
2999
|
console.error(chalk16.dim(` Expanding directory to: ${globPattern}`));
|
|
2840
3000
|
}
|
|
2841
|
-
const
|
|
3001
|
+
const projectRoot = resolve2(projectDir) + (process.platform === "win32" ? "\\" : "/");
|
|
3002
|
+
const paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve2(projectDir, p)).filter((p) => p.startsWith(projectRoot) || p === resolve2(projectDir));
|
|
2842
3003
|
if (paths.length === 0) {
|
|
2843
3004
|
console.error(chalk16.red(`No files matched: ${fileOrGlob}`));
|
|
2844
3005
|
db.close();
|
|
@@ -2865,7 +3026,7 @@ ${diff.slice(0, REVIEW_DIFF_MAX_CHARS3)}`,
|
|
|
2865
3026
|
console.error(chalk16.yellow(`Skipping ${filePath} (binary file)`));
|
|
2866
3027
|
continue;
|
|
2867
3028
|
}
|
|
2868
|
-
const content =
|
|
3029
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
2869
3030
|
const relativePath = filePath.replace(projectDir, "").replace(/\\/g, "/").replace(/^\//, "");
|
|
2870
3031
|
files.push({ path: relativePath, content });
|
|
2871
3032
|
totalSize += stat.size;
|
|
@@ -3114,7 +3275,7 @@ async function shipitCommand(options) {
|
|
|
3114
3275
|
|
|
3115
3276
|
// src/commands/start.ts
|
|
3116
3277
|
import { existsSync as existsSync5 } from "fs";
|
|
3117
|
-
import { execFileSync as
|
|
3278
|
+
import { execFileSync as execFileSync4, execSync as execSync6 } from "child_process";
|
|
3118
3279
|
import { join as join6, basename as basename2 } from "path";
|
|
3119
3280
|
import chalk19 from "chalk";
|
|
3120
3281
|
import { loadConfig as loadConfig9, writeConfig as writeConfig2 } from "@codemoot/core";
|
|
@@ -3179,7 +3340,7 @@ async function startCommand() {
|
|
|
3179
3340
|
codemoot review ${reviewTarget} --preset quick-scan
|
|
3180
3341
|
`));
|
|
3181
3342
|
try {
|
|
3182
|
-
const output =
|
|
3343
|
+
const output = execFileSync4("codemoot", ["review", reviewTarget, "--preset", "quick-scan"], {
|
|
3183
3344
|
cwd,
|
|
3184
3345
|
encoding: "utf-8",
|
|
3185
3346
|
timeout: 3e5,
|
|
@@ -3450,11 +3611,12 @@ async function workerCommand(options) {
|
|
|
3450
3611
|
console.error(chalk21.cyan(`Processing job ${job.id} (type: ${job.type})`));
|
|
3451
3612
|
jobStore.appendLog(job.id, "info", "job_started", `Worker ${workerId} claimed job`);
|
|
3452
3613
|
try {
|
|
3453
|
-
const { resolve:
|
|
3614
|
+
const { resolve: resolve3, normalize } = await import("path");
|
|
3454
3615
|
const payload = JSON.parse(job.payloadJson);
|
|
3455
|
-
const rawCwd =
|
|
3616
|
+
const rawCwd = resolve3(payload.path ?? payload.cwd ?? projectDir);
|
|
3456
3617
|
const cwd = normalize(rawCwd);
|
|
3457
|
-
|
|
3618
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
3619
|
+
if (cwd !== normalize(projectDir) && !cwd.startsWith(normalize(projectDir) + sep)) {
|
|
3458
3620
|
throw new Error(`Path traversal blocked: "${cwd}" is outside project directory "${projectDir}"`);
|
|
3459
3621
|
}
|
|
3460
3622
|
const timeout = (payload.timeout ?? 600) * 1e3;
|
|
@@ -3472,14 +3634,14 @@ Start by listing candidate files, then inspect them thoroughly.`,
|
|
|
3472
3634
|
resumed: false
|
|
3473
3635
|
});
|
|
3474
3636
|
} else if (payload.diff) {
|
|
3475
|
-
const { execFileSync:
|
|
3637
|
+
const { execFileSync: execFileSync5 } = await import("child_process");
|
|
3476
3638
|
const diffArgs = payload.diff.split(/\s+/).filter((a) => a.length > 0);
|
|
3477
3639
|
for (const arg of diffArgs) {
|
|
3478
3640
|
if (arg.startsWith("-") || !/^[a-zA-Z0-9_.~^:\/\\@{}]+$/.test(arg)) {
|
|
3479
3641
|
throw new Error(`Invalid diff argument: "${arg}" \u2014 only git refs and paths allowed`);
|
|
3480
3642
|
}
|
|
3481
3643
|
}
|
|
3482
|
-
const diff =
|
|
3644
|
+
const diff = execFileSync5("git", ["diff", ...diffArgs], {
|
|
3483
3645
|
cwd,
|
|
3484
3646
|
encoding: "utf-8",
|
|
3485
3647
|
maxBuffer: 1024 * 1024
|
|
@@ -3600,7 +3762,7 @@ jobs.command("status").description("Show job details with recent logs").argument
|
|
|
3600
3762
|
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
3763
|
jobs.command("cancel").description("Cancel a queued or running job").argument("<job-id>", "Job ID").action(jobsCancelCommand);
|
|
3602
3764
|
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);
|
|
3765
|
+
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
3766
|
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
3767
|
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
3768
|
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);
|