@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 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
- for (const line of chunk.split("\n")) {
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 args = String(item.arguments ?? item.input ?? "").slice(0, 80);
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
- console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
110
- process.exit(1);
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 = execSync(`git diff --cached ${run.baselineRef}`, { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
387
+ diff = execFileSync("git", ["diff", "--cached", "--", run.baselineRef], { cwd: projectDir, encoding: "utf-8", maxBuffer: 1024 * 1024, env: { ...process.env, GIT_INDEX_FILE: tmpIndex } });
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: resolve2 } = await import("path");
967
- const projectDir = resolve2(path);
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: writeFileSync3 } = await import("fs");
1173
- writeFileSync3(options.output, JSON.stringify(report, null, 2), "utf-8");
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 = execFileSync("git", ["diff", ...options.diff.split(/\s+/)], {
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: "review",
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)}` : `Review ${fileOrGlob} and identify fixable issues. Read the file(s) first, then report issues with exact line numbers.`,
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
- "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.` : ""
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(" Reviewing..."));
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
- fixApplied: false,
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(" Review APPROVED \u2014 no fixes needed."));
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: skipping fix application."));
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
- fixApplied: false,
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
- 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);
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
- 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
- );
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
- fixApplied,
1892
+ fixesProposed: fixes.length,
1893
+ fixesApplied: applied,
1894
+ fixesFailed: failed,
1895
+ exitReason: exitReason2,
1752
1896
  durationMs: Date.now() - roundStart
1753
1897
  });
1754
- if (!fixApplied) {
1755
- console.error(chalk9.yellow(" No changes made \u2014 stopping loop."));
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 readFileSync2, writeFileSync } from "fs";
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
- writeFileSync(fullPath, skill.content, "utf-8");
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 = readFileSync2(claudeMdPath, "utf-8");
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
- writeFileSync(claudeMdPath, before.trimEnd() + "\n" + CLAUDE_MD_SECTION + after, "utf-8");
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
- writeFileSync(claudeMdPath, existing.trimEnd() + "\n" + CLAUDE_MD_SECTION, "utf-8");
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
- writeFileSync(claudeMdPath, `# Project Instructions
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(readFileSync2(settingsPath, "utf-8"));
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
- writeFileSync(settingsPath, JSON.stringify(existing, null, 2), "utf-8");
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
- writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2), "utf-8");
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 writeFileSync2 } from "fs";
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
- writeFileSync2(options.output, result.finalOutput, "utf-8");
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 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";
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 = execFileSync2("git", ["diff", ...options.diff.split(/\s+/)], {
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 = resolve(projectDir, globPattern);
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 paths = globSync(globPattern, { cwd: projectDir }).map((p) => resolve(projectDir, p));
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 = readFileSync3(filePath, "utf-8");
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 execFileSync3, execSync as execSync6 } from "child_process";
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 = execFileSync3("codemoot", ["review", reviewTarget, "--preset", "quick-scan"], {
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: resolve2, normalize } = await import("path");
3619
+ const { resolve: resolve3, normalize } = await import("path");
3454
3620
  const payload = JSON.parse(job.payloadJson);
3455
- const rawCwd = resolve2(payload.path ?? payload.cwd ?? projectDir);
3621
+ const rawCwd = resolve3(payload.path ?? payload.cwd ?? projectDir);
3456
3622
  const cwd = normalize(rawCwd);
3457
- if (!cwd.startsWith(normalize(projectDir))) {
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: execFileSync4 } = await import("child_process");
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 = execFileSync4("git", ["diff", ...diffArgs], {
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);