@codemoot/cli 0.2.3 → 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 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
- console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
110
- process.exit(1);
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 = execSync(`git diff --cached ${run.baselineRef}`, { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
382
+ diff = execFileSync("git", ["diff", "--cached", "--", run.baselineRef], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
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: resolve2 } = await import("path");
967
- const projectDir = resolve2(path);
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: writeFileSync3 } = await import("fs");
1173
- writeFileSync3(options.output, JSON.stringify(report, null, 2), "utf-8");
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 = execFileSync("git", ["diff", ...options.diff.split(/\s+/)], {
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: "review",
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)}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers.`,
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
- "For each issue, provide the EXACT fix as a code snippet.",
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(" Reviewing..."));
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
- fixApplied: false,
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(" Review APPROVED \u2014 no fixes needed."));
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: skipping fix application."));
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
- fixApplied: false,
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
- console.error(chalk9.dim(" Applying fixes..."));
1721
- const fixPrompt = buildHandoffEnvelope3({
1722
- command: "custom",
1723
- task: `Based on the review above, apply ALL suggested fixes to the codebase. Use your file editing tools to make the changes. After applying, verify the changes compile correctly. Only fix issues that were identified \u2014 do not refactor or change unrelated code.`,
1724
- constraints: [
1725
- "Make minimal, targeted changes only.",
1726
- "Do not add comments, docstrings, or formatting changes.",
1727
- "If a fix is ambiguous, skip it rather than guess."
1728
- ],
1729
- resumed: true
1730
- });
1731
- const fixResult = await adapter.callWithResume(fixPrompt, {
1732
- sessionId: threadId,
1733
- timeout: timeoutMs,
1734
- ...progress
1735
- });
1736
- if (fixResult.sessionId) {
1737
- threadId = fixResult.sessionId;
1738
- sessionMgr.updateThreadId(session2.id, fixResult.sessionId);
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
- sessionMgr.addUsageFromResult(session2.id, fixResult.usage, fixPrompt, fixResult.text);
1741
- const fixApplied = !fixResult.text.includes("no changes") && !fixResult.text.includes("No fixes");
1742
- console.error(
1743
- fixApplied ? chalk9.green(" Fixes applied.") : chalk9.yellow(" No fixes applied.")
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
- fixApplied,
1887
+ fixesProposed: fixes.length,
1888
+ fixesApplied: applied,
1889
+ fixesFailed: failed,
1890
+ exitReason: exitReason2,
1752
1891
  durationMs: Date.now() - roundStart
1753
1892
  });
1754
- if (!fixApplied) {
1755
- console.error(chalk9.yellow(" No changes made \u2014 stopping loop."));
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 readFileSync2, writeFileSync } from "fs";
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
- writeFileSync(fullPath, skill.content, "utf-8");
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 = readFileSync2(claudeMdPath, "utf-8");
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
- writeFileSync(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
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
- writeFileSync(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
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
- writeFileSync(claudeMdPath, `# Project Instructions
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(readFileSync2(settingsPath, "utf-8"));
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
- writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
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
- writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
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 writeFileSync2 } from "fs";
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
- writeFileSync2(options.output, result.finalOutput, "utf-8");
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 execFileSync2, execSync as execSync4 } from "child_process";
2692
- import { closeSync, globSync, openSync, readFileSync as readFileSync3, readSync, statSync, existsSync as existsSync4 } from "fs";
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 = execFileSync2("git", ["diff", ...options.diff.split(/\s+/)], {
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 = resolve(projectDir, globPattern);
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 paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve(projectDir, p));
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 = readFileSync3(filePath, "utf-8");
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 execFileSync3, execSync as execSync6 } from "child_process";
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 = execFileSync3("codemoot", ["review", reviewTarget, "--preset", "quick-scan"], {
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: resolve2, normalize } = await import("path");
3614
+ const { resolve: resolve3, normalize } = await import("path");
3454
3615
  const payload = JSON.parse(job.payloadJson);
3455
- const rawCwd = resolve2(payload.path ?? payload.cwd ?? projectDir);
3616
+ const rawCwd = resolve3(payload.path ?? payload.cwd ?? projectDir);
3456
3617
  const cwd = normalize(rawCwd);
3457
- if (!cwd.startsWith(normalize(projectDir))) {
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: execFileSync4 } = await import("child_process");
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 = execFileSync4("git", ["diff", ...diffArgs], {
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);