replume-cli 0.1.1 → 0.1.2

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/cli.js CHANGED
@@ -6,7 +6,7 @@ import { Command, Option } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "replume-cli",
9
- version: "0.1.1",
9
+ version: "0.1.2",
10
10
  type: "module",
11
11
  bin: {
12
12
  replume: "dist/cli.js"
@@ -48,9 +48,9 @@ var package_default = {
48
48
  };
49
49
 
50
50
  // src/cli.ts
51
- import { execFileSync as execFileSync10 } from "child_process";
52
- import { existsSync as existsSync18, lstatSync as lstatSync3, readFileSync as readFileSync17 } from "fs";
53
- import { join as join23, resolve as resolve6 } from "path";
51
+ import { execFileSync as execFileSync11 } from "child_process";
52
+ import { existsSync as existsSync20, lstatSync as lstatSync3, readFileSync as readFileSync19 } from "fs";
53
+ import { join as join25, resolve as resolve8 } from "path";
54
54
 
55
55
  // src/hooks/session-start.ts
56
56
  import { execFileSync as execFileSync2 } from "child_process";
@@ -342,20 +342,21 @@ function markPathsOutsideRepo(text, repoRoot2) {
342
342
  const path = (value) => {
343
343
  const rel = relative(repoRoot2, value);
344
344
  if (!rel) return "[REPO_ROOT]";
345
+ if (/^\/dev\/[\w./-]+$/.test(value)) return value;
345
346
  return rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel) ? rel.replace(/\\/g, "/") : "[OUTSIDE_REPO]";
346
347
  };
347
- return text.replace(/(^|[\s"'=(,:])(\/(?!\/)[^\s"'`,;)]+)/g, (_m, prefix, absolute) => `${prefix}${path(absolute)}`);
348
+ return text.replace(/(^|[\s"'=(,:])(\/(?!\/)[^\s"'`,;)(]+)(?![^\s"'`,;)])/g, (_m, prefix, absolute) => `${prefix}${path(absolute)}`);
348
349
  }
349
350
  function redactLedgerText(text, repoRoot2) {
350
351
  const out = redact(text).replace(SECRET_ENV_ANYCASE, `$1${REDACTED}`).replace(URL_USER, `$1${REDACTED}:${REDACTED}@`);
351
352
  return markPathsOutsideRepo(out, repoRoot2);
352
353
  }
353
354
  function redactForLedger(value, repoRoot2) {
354
- const str3 = (s) => redactLedgerText(s, repoRoot2);
355
+ const str4 = (s) => redactLedgerText(s, repoRoot2);
355
356
  const deep = (v) => {
356
- if (typeof v === "string") return str3(v);
357
+ if (typeof v === "string") return str4(v);
357
358
  if (Array.isArray(v)) return v.map(deep);
358
- if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, c]) => [str3(k), deep(c)]));
359
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, c]) => [str4(k), deep(c)]));
359
360
  return v;
360
361
  };
361
362
  return deep(value);
@@ -601,7 +602,7 @@ async function runVerify(opts) {
601
602
  // src/agents/claude/handler.ts
602
603
  import { execFileSync as execFileSync5 } from "child_process";
603
604
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync6 } from "fs";
604
- import { join as join9 } from "path";
605
+ import { join as join10 } from "path";
605
606
  import { homedir as homedir2 } from "os";
606
607
 
607
608
  // src/core/usage-store.ts
@@ -1041,7 +1042,7 @@ var Parser = class {
1041
1042
  return ITEM.test(first.body) ? this.sequence(indent) : this.mapping(indent);
1042
1043
  }
1043
1044
  mapping(indent, seed) {
1044
- const obj2 = seed?.obj ?? {};
1045
+ const obj3 = seed?.obj ?? {};
1045
1046
  while (this.peek()) {
1046
1047
  const line = this.peek();
1047
1048
  if (line.indent < indent) break;
@@ -1050,20 +1051,20 @@ var Parser = class {
1050
1051
  const m = KEY.exec(line.body);
1051
1052
  if (!m) throw new Error(`line ${line.n}: expected "key: value"`);
1052
1053
  this.i++;
1053
- this.entry(obj2, m[1], m[2], line, indent);
1054
+ this.entry(obj3, m[1], m[2], line, indent);
1054
1055
  }
1055
- return obj2;
1056
+ return obj3;
1056
1057
  }
1057
- entry(obj2, key, rest, line, indent) {
1058
- if (key in obj2) throw new Error(`line ${line.n}: duplicate key ${key}`);
1058
+ entry(obj3, key, rest, line, indent) {
1059
+ if (key in obj3) throw new Error(`line ${line.n}: duplicate key ${key}`);
1059
1060
  if (rest !== void 0 && rest.trim() !== "") {
1060
- obj2[key] = parseInline(rest, `line ${line.n}`);
1061
+ obj3[key] = parseInline(rest, `line ${line.n}`);
1061
1062
  return;
1062
1063
  }
1063
1064
  const next = this.peek();
1064
- if (next && next.indent > indent) obj2[key] = this.block(next.indent);
1065
- else if (next && next.indent === indent && ITEM.test(next.body)) obj2[key] = this.sequence(indent);
1066
- else obj2[key] = null;
1065
+ if (next && next.indent > indent) obj3[key] = this.block(next.indent);
1066
+ else if (next && next.indent === indent && ITEM.test(next.body)) obj3[key] = this.sequence(indent);
1067
+ else obj3[key] = null;
1067
1068
  }
1068
1069
  sequence(indent) {
1069
1070
  const items = [];
@@ -1082,11 +1083,11 @@ var Parser = class {
1082
1083
  continue;
1083
1084
  }
1084
1085
  const column = indent + (line.body.length - body.length);
1085
- const obj2 = {};
1086
- this.entry(obj2, km[1], km[2], line, column);
1086
+ const obj3 = {};
1087
+ this.entry(obj3, km[1], km[2], line, column);
1087
1088
  const next = this.peek();
1088
- if (next && next.indent === column && !ITEM.test(next.body)) this.mapping(column, { obj: obj2 });
1089
- items.push(obj2);
1089
+ if (next && next.indent === column && !ITEM.test(next.body)) this.mapping(column, { obj: obj3 });
1090
+ items.push(obj3);
1090
1091
  }
1091
1092
  return items;
1092
1093
  }
@@ -1113,9 +1114,9 @@ function emitScalar(v) {
1113
1114
  if (typeof v === "string") return JSON.stringify(v);
1114
1115
  return String(v);
1115
1116
  }
1116
- function stringifyYaml(obj2, comments = {}) {
1117
+ function stringifyYaml(obj3, comments = {}) {
1117
1118
  const lines = [];
1118
- for (const [k, v] of Object.entries(obj2)) {
1119
+ for (const [k, v] of Object.entries(obj3)) {
1119
1120
  if (v === void 0) continue;
1120
1121
  const c = comments[k] ? ` # ${comments[k]}` : "";
1121
1122
  if (Array.isArray(v)) {
@@ -1260,7 +1261,7 @@ function buildContract(input) {
1260
1261
 
1261
1262
  // src/core/rules.ts
1262
1263
  import { createHash } from "crypto";
1263
- import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve2, sep } from "path";
1264
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve2, sep } from "path";
1264
1265
  import { homedir, tmpdir } from "os";
1265
1266
 
1266
1267
  // src/core/glob.ts
@@ -1351,7 +1352,8 @@ function captureCoverage(eventsFile, changedPaths, hooksConfigured = false) {
1351
1352
  for (const p of e.paths) {
1352
1353
  if (blocked) blockedPaths.add(p);
1353
1354
  else seenPaths.add(p);
1354
- if (landed) landedPaths.add(p);
1355
+ const writes = e.type === "command" ? e.raw?.writes : void 0;
1356
+ if (landed && (!Array.isArray(writes) || writes.includes(p))) landedPaths.add(p);
1355
1357
  }
1356
1358
  }
1357
1359
  const ts = events.map((e) => e.ts).sort();
@@ -1434,11 +1436,21 @@ var PREFIX = "Replume:";
1434
1436
  function fingerprint(tool, input) {
1435
1437
  return createHash("sha1").update(tool).update("\0").update(JSON.stringify(input ?? null)).digest("hex").slice(0, 16);
1436
1438
  }
1439
+ function expandHome(p) {
1440
+ if (p === "~") return homedir();
1441
+ if (p.startsWith("~/")) return join7(homedir(), p.slice(2));
1442
+ return p;
1443
+ }
1437
1444
  function splitPaths(repoRoot2, paths) {
1438
1445
  const inside = [];
1439
1446
  const outside = [];
1440
1447
  const root = resolve2(repoRoot2);
1441
- for (const p of paths) {
1448
+ for (const raw3 of paths) {
1449
+ const p = expandHome(raw3);
1450
+ if (p.startsWith("~")) {
1451
+ outside.push(p);
1452
+ continue;
1453
+ }
1442
1454
  const abs = isAbsolute3(p) ? resolve2(p) : resolve2(root, p);
1443
1455
  const rel = relative3(root, abs);
1444
1456
  if (rel === "" || rel.startsWith("..") || isAbsolute3(rel)) outside.push(abs);
@@ -1529,11 +1541,13 @@ function shellTokens(src) {
1529
1541
  i++;
1530
1542
  } else if (">|<&;()".includes(c)) {
1531
1543
  const op = /^(?:&>>|>>|>\||>&|&>|&&|\|\||\|&|<<<|<<|<&|[>|<&;()])/.exec(src.slice(i))[0];
1544
+ let fd;
1532
1545
  if ((op[0] === ">" || op[0] === "<") && has && /^\d+$/.test(cur)) {
1546
+ fd = Number(cur);
1533
1547
  cur = "";
1534
1548
  has = false;
1535
1549
  } else flush();
1536
- toks.push({ op });
1550
+ toks.push(fd === void 0 ? { op } : { op, fd });
1537
1551
  i += op.length;
1538
1552
  } else {
1539
1553
  cur += c;
@@ -1545,8 +1559,122 @@ function shellTokens(src) {
1545
1559
  return toks;
1546
1560
  }
1547
1561
  var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
1548
- var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "xargs", "env", "nohup"]);
1549
1562
  var WRITE_OPS = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>"]);
1563
+ var MAX_NEST = 8;
1564
+ var SHELL_KEYWORDS = /* @__PURE__ */ new Set(["if", "then", "else", "elif", "fi", "while", "until", "do", "done", "esac", "{", "}", "!"]);
1565
+ var HEADER_KEYWORDS = /* @__PURE__ */ new Set(["for", "select", "case"]);
1566
+ var WRAPPER_SPECS = {
1567
+ sudo: { withArg: ["-u", "-g", "-h", "-p", "-C", "-T", "-r", "-t", "-U", "-D", "-R", "--user", "--group", "--host", "--prompt"] },
1568
+ doas: { withArg: ["-u", "-C"] },
1569
+ env: { withArg: ["-u", "-C", "--unset", "--chdir"] },
1570
+ command: { withArg: [] },
1571
+ exec: { withArg: ["-a"] },
1572
+ time: { withArg: ["-f", "-o", "--format", "--output"] },
1573
+ nohup: { withArg: [] },
1574
+ nice: { withArg: ["-n", "--adjustment"] },
1575
+ ionice: { withArg: ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata"] },
1576
+ stdbuf: { withArg: ["-i", "-o", "-e"] },
1577
+ timeout: { withArg: ["-s", "-k", "--signal", "--kill-after"], positional: 1 },
1578
+ xargs: { withArg: ["-I", "-n", "-P", "-L", "-s", "-d", "-E", "-a"] }
1579
+ };
1580
+ function unwrapCommand(input) {
1581
+ let words = input;
1582
+ for (let guard = 0; guard < 16 && words.length; guard++) {
1583
+ const w = words[0];
1584
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w) || SHELL_KEYWORDS.has(w)) {
1585
+ words = words.slice(1);
1586
+ continue;
1587
+ }
1588
+ if (HEADER_KEYWORDS.has(w)) return [];
1589
+ if (w === "function") {
1590
+ words = words.slice(2);
1591
+ continue;
1592
+ }
1593
+ const spec = WRAPPER_SPECS[w.split("/").pop()];
1594
+ if (!spec) break;
1595
+ let i = 1;
1596
+ while (i < words.length) {
1597
+ const a = words[i];
1598
+ if (a === "--") {
1599
+ i++;
1600
+ break;
1601
+ }
1602
+ if (a.startsWith("-") && a.length > 1) i += spec.withArg.includes(a) ? 2 : 1;
1603
+ else break;
1604
+ }
1605
+ if (spec.positional && /^\d+(?:\.\d+)?[smhd]?$/.test(words[i] ?? "")) i += spec.positional;
1606
+ words = words.slice(i);
1607
+ }
1608
+ return words;
1609
+ }
1610
+ function splitFlags(args) {
1611
+ const flags = [];
1612
+ const operands = [];
1613
+ let done = false;
1614
+ for (const a of args) {
1615
+ if (done) operands.push(a);
1616
+ else if (a === "--") done = true;
1617
+ else if (a.startsWith("-") && a.length > 1) flags.push(a);
1618
+ else operands.push(a);
1619
+ }
1620
+ return { flags, operands };
1621
+ }
1622
+ var isBlockDevice = (p) => /^\/dev\/(?:(?:s|h|v|xv)d[a-z]+[0-9]*|nvme[0-9]+n[0-9]+(?:p[0-9]+)?|mmcblk[0-9]+(?:p[0-9]+)?|r?disk[0-9]+(?:s[0-9]+)*)$/.test(p.trim());
1623
+ function substitutionBodies(src) {
1624
+ const text = src.replace(/<<(-?)[ \t]*(['"]?)([A-Za-z_][\w-]*)\2[^\n]*\n[\s\S]*?\n[ \t]*\3(?=\n|$)/g, "");
1625
+ const out = [];
1626
+ const closing = (from) => {
1627
+ let depth = 0;
1628
+ for (let j = from; j < text.length; j++) {
1629
+ const c = text[j];
1630
+ if (c === "\\") {
1631
+ j++;
1632
+ continue;
1633
+ }
1634
+ if (c === "'") {
1635
+ j = text.indexOf("'", j + 1);
1636
+ if (j < 0) return text.length;
1637
+ continue;
1638
+ }
1639
+ if (c === '"') {
1640
+ for (j++; j < text.length && text[j] !== '"'; j++) if (text[j] === "\\") j++;
1641
+ continue;
1642
+ }
1643
+ if (c === "(") depth++;
1644
+ else if (c === ")" && --depth === 0) return j;
1645
+ }
1646
+ return text.length;
1647
+ };
1648
+ let dq = false;
1649
+ for (let i = 0; i < text.length; i++) {
1650
+ const c = text[i];
1651
+ if (c === "\\") {
1652
+ i++;
1653
+ continue;
1654
+ }
1655
+ if (c === "'" && !dq) {
1656
+ const j = text.indexOf("'", i + 1);
1657
+ if (j < 0) break;
1658
+ i = j;
1659
+ continue;
1660
+ }
1661
+ if (c === '"') {
1662
+ dq = !dq;
1663
+ continue;
1664
+ }
1665
+ if (text[i + 1] === "(" && (c === "$" && text[i + 2] !== "(" || (c === "<" || c === ">") && !dq)) {
1666
+ const end = closing(i + 1);
1667
+ out.push(text.slice(i + 2, end));
1668
+ i = end;
1669
+ } else if (c === "`") {
1670
+ let j = i + 1;
1671
+ while (j < text.length && text[j] !== "`") j += text[j] === "\\" ? 2 : 1;
1672
+ out.push(text.slice(i + 1, j));
1673
+ i = j;
1674
+ }
1675
+ }
1676
+ return out;
1677
+ }
1550
1678
  function redirectTargets(command, depth = 0) {
1551
1679
  const toks = shellTokens(command);
1552
1680
  const out = [];
@@ -1559,13 +1687,13 @@ function redirectTargets(command, depth = 0) {
1559
1687
  }
1560
1688
  if (!("word" in t)) return;
1561
1689
  const prev = toks[i - 1];
1562
- const atCommand = !prev || "op" in prev || WRAPPERS.has(word(prev) ?? "");
1690
+ const atCommand = !prev || "op" in prev || (word(prev) ?? "") in WRAPPER_SPECS;
1563
1691
  if (t.word === "tee" && atCommand) {
1564
1692
  for (let j = i + 1; j < toks.length && "word" in toks[j]; j++) {
1565
1693
  const w = word(toks[j]);
1566
1694
  if (!w.startsWith("-")) out.push(w);
1567
1695
  }
1568
- } else if (depth < 2 && SHELLS.has(t.word.split("/").pop())) {
1696
+ } else if (depth < MAX_NEST && SHELLS.has(t.word.split("/").pop())) {
1569
1697
  const k = toks.findIndex((x, j) => j > i && "word" in x && /^-[a-z]*c$/.test(x.word));
1570
1698
  const script = k >= 0 ? word(toks[k + 1]) : null;
1571
1699
  if (script) out.push(...redirectTargets(script, depth + 1));
@@ -1588,13 +1716,13 @@ function fileVerbOperands(command, depth = 0) {
1588
1716
  }
1589
1717
  simple[simple.length - 1].push(t.word);
1590
1718
  }
1591
- for (let words of simple) {
1592
- while (words.length && (WRAPPERS.has(words[0]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))) words = words.slice(1);
1719
+ for (const sw of simple) {
1720
+ const words = unwrapCommand(sw);
1593
1721
  if (!words.length) continue;
1594
1722
  const verb = words[0].split("/").pop();
1595
1723
  const args = words.slice(1);
1596
- const operands = args.filter((w) => !w.startsWith("-")).map((w) => w.replace(/^of=/, ""));
1597
- if (SHELLS.has(verb) && depth < 2) {
1724
+ const operands = splitFlags(args).operands.map((w) => w.replace(/^of=/, ""));
1725
+ if (SHELLS.has(verb) && depth < MAX_NEST) {
1598
1726
  const k = args.findIndex((w) => /^-[a-z]*c$/.test(w));
1599
1727
  if (k >= 0 && args[k + 1]) out.push(...fileVerbOperands(args[k + 1], depth + 1));
1600
1728
  } else if (FILE_VERBS.has(verb)) out.push(...operands);
@@ -1609,51 +1737,130 @@ var INLINE_WRITE = /writeFile|appendFile|createWriteStream|copyFile|rename|unlin
1609
1737
  var GIT_TREE_CHANGERS = /* @__PURE__ */ new Set(["switch", "reset", "stash", "apply", "am", "merge", "pull", "rebase", "cherry-pick", "revert", "clean"]);
1610
1738
  function shellWriteTargets(command, depth = 0) {
1611
1739
  const out = [];
1740
+ const certain = [];
1612
1741
  let unparsed = false;
1613
1742
  const toks = shellTokens(command);
1614
- const simple = [[]];
1743
+ let cond = false;
1744
+ let base3 = false;
1745
+ const orLists = /* @__PURE__ */ new Set();
1746
+ let list2 = 0;
1747
+ let construct = 0;
1748
+ let pendingFunc = false;
1749
+ const stack = [];
1750
+ const simple = [{ words: [], redirects: [], conditional: false, list: 0, depth: 0, construct: 0, after: ";" }];
1751
+ const next = (after) => simple.push({ words: [], redirects: [], conditional: cond, list: list2, depth: stack.length, construct, after });
1615
1752
  for (let i = 0; i < toks.length; i++) {
1616
1753
  const t = toks[i];
1617
1754
  if ("op" in t) {
1618
- if (SEPARATORS.has(t.op)) simple.push([]);
1755
+ const cur2 = simple[simple.length - 1];
1756
+ if (t.op === ";") {
1757
+ cond = base3;
1758
+ list2++;
1759
+ pendingFunc = false;
1760
+ next(";");
1761
+ } else if (t.op === "&&") {
1762
+ cond = true;
1763
+ next("&&");
1764
+ } else if (t.op === "||") {
1765
+ cond = true;
1766
+ orLists.add(list2);
1767
+ next("||");
1768
+ } else if (t.op === "(") {
1769
+ if (cur2.words.length === 1 && /^[A-Za-z_][\w.-]*$/.test(cur2.words[0])) pendingFunc = true;
1770
+ stack.push({ cond, base: base3, func: false });
1771
+ base3 = cond;
1772
+ next("(");
1773
+ } else if (t.op === ")") {
1774
+ const top = stack.pop();
1775
+ cond = top?.cond ?? false;
1776
+ base3 = top?.base ?? false;
1777
+ next(")");
1778
+ } else if (SEPARATORS.has(t.op)) next(t.op);
1619
1779
  else if (WRITE_OPS.has(t.op)) {
1620
1780
  const target = toks[i + 1];
1621
1781
  const w = target && "word" in target ? target.word : null;
1622
- if (!w || DYNAMIC.test(w)) unparsed = true;
1623
- else if (!DEVICE_SINK.test(w)) out.push(w);
1782
+ const fds = t.fd !== void 0 ? [t.fd] : t.op === "&>" || t.op === "&>>" ? [1, 2] : [1];
1783
+ cur2.redirects.push({ fds, target: w });
1624
1784
  i++;
1625
- } else if (t.op === "<" || t.op === "<<<" || t.op === "<<") i++;
1785
+ } else if (t.op === "<" || t.op === "<<<" || t.op === "<<" || t.op === ">&" || t.op === "<&") i++;
1626
1786
  continue;
1627
1787
  }
1628
- simple[simple.length - 1].push(t.word);
1629
- }
1630
- const add = (ws) => {
1631
- for (const w of ws) {
1632
- if (!w) continue;
1633
- if (DYNAMIC.test(w)) unparsed = true;
1634
- else if (!DEVICE_SINK.test(w)) out.push(w);
1788
+ const cur = simple[simple.length - 1];
1789
+ if (cur.words.length === 0) {
1790
+ if (["if", "while", "until", "for", "select", "case"].includes(t.word)) construct++;
1791
+ else if (["fi", "done", "esac"].includes(t.word)) construct = Math.max(0, construct - 1);
1792
+ else if (t.word === "function") pendingFunc = true;
1793
+ else if (t.word === "{") {
1794
+ stack.push({ cond, base: base3, func: pendingFunc });
1795
+ base3 = cond;
1796
+ if (pendingFunc) {
1797
+ construct++;
1798
+ cur.construct = construct;
1799
+ }
1800
+ pendingFunc = false;
1801
+ } else if (t.word === "}") {
1802
+ const top = stack.pop();
1803
+ cond = top?.cond ?? false;
1804
+ base3 = top?.base ?? false;
1805
+ if (top?.func) construct = Math.max(0, construct - 1);
1806
+ }
1635
1807
  }
1808
+ cur.words.push(t.word);
1809
+ }
1810
+ const fails = (sc) => {
1811
+ const w = unwrapCommand(sc.words);
1812
+ return w[0] === "false" || (w[0] === "exit" || w[0] === "return") && /^[1-9][0-9]*$/.test(w[1] ?? "");
1636
1813
  };
1637
- for (let words of simple) {
1638
- while (words.length && (["sudo", "command", "exec", "time", "env", "nohup"].includes(words[0]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))) words = words.slice(1);
1639
- if (!words.length) continue;
1814
+ const skipped = simple.map(() => false);
1815
+ simple.forEach((sc, i) => {
1816
+ if (i > 0 && sc.after === "&&" && (fails(simple[i - 1]) || skipped[i - 1])) skipped[i] = true;
1817
+ });
1818
+ const lastList = [...simple].reverse().find((x) => x.words.length || x.redirects.length)?.list ?? 0;
1819
+ simple.forEach((sc, idx) => {
1820
+ const sure = !skipped[idx] && sc.construct === 0 && (!sc.conditional || sc.depth === 0 && !orLists.has(sc.list) && sc.list === lastList);
1821
+ const last = /* @__PURE__ */ new Map();
1822
+ sc.redirects.forEach((r, j) => r.fds.forEach((fd) => last.set(fd, j)));
1823
+ sc.redirects.forEach((r, j) => {
1824
+ if (!r.target || DYNAMIC.test(r.target)) {
1825
+ unparsed = true;
1826
+ return;
1827
+ }
1828
+ if (DEVICE_SINK.test(r.target)) return;
1829
+ out.push(r.target);
1830
+ if (sure && r.fds.some((fd) => last.get(fd) === j)) certain.push(r.target);
1831
+ });
1832
+ const add = (ws, content = true) => {
1833
+ for (const w of ws) {
1834
+ if (!w) continue;
1835
+ if (DYNAMIC.test(w)) unparsed = true;
1836
+ else if (!DEVICE_SINK.test(w)) {
1837
+ out.push(w);
1838
+ if (content && sure) certain.push(w);
1839
+ }
1840
+ }
1841
+ };
1842
+ const words = unwrapCommand(sc.words);
1843
+ if (!words.length) return;
1640
1844
  const verb = words[0].split("/").pop();
1641
1845
  const args = words.slice(1);
1642
- const operands = args.filter((w) => !w.startsWith("-"));
1846
+ const operands = splitFlags(args).operands;
1847
+ if (sc.words.slice(0, sc.words.length - words.length).some((x) => x.split("/").pop() === "xargs")) {
1848
+ if (FILE_VERBS.has(verb) || verb === "sed" || verb === "perl") unparsed = true;
1849
+ return;
1850
+ }
1643
1851
  if (SHELLS.has(verb)) {
1644
1852
  const k = args.findIndex((w) => /^-[a-z]*c$/.test(w));
1645
1853
  if (k >= 0 && args[k + 1] !== void 0) {
1646
- if (depth >= 2) unparsed = true;
1854
+ if (depth >= MAX_NEST) unparsed = true;
1647
1855
  else {
1648
1856
  const r = shellWriteTargets(args[k + 1], depth + 1);
1649
1857
  out.push(...r.paths);
1858
+ if (sure) certain.push(...r.certain);
1650
1859
  unparsed ||= r.unparsed;
1651
1860
  }
1652
1861
  }
1653
- } else if (verb === "xargs") {
1654
- if (args.some((w) => FILE_VERBS.has(w) || w === "sed" || w === "perl")) unparsed = true;
1655
1862
  } else if (["tee", "touch", "truncate", "rm", "unlink", "chmod", "chown"].includes(verb)) {
1656
- add(verb === "chmod" || verb === "chown" ? operands.slice(1) : verb === "truncate" ? args.filter((w, j) => !w.startsWith("-") && !/^-s$|^--size$/.test(args[j - 1] ?? "")) : operands);
1863
+ add(verb === "chmod" || verb === "chown" ? operands.slice(1) : verb === "truncate" ? args.filter((w, j) => !w.startsWith("-") && !/^-s$|^--size$/.test(args[j - 1] ?? "")) : operands, !["touch", "chmod", "chown"].includes(verb));
1657
1864
  } else if (verb === "cp" || verb === "install" || verb === "ln") {
1658
1865
  const t = args.findIndex((w) => w === "-t" || w === "--target-directory");
1659
1866
  if (t >= 0 || args.some((w) => w.startsWith("--target-directory="))) unparsed = true;
@@ -1677,20 +1884,26 @@ function shellWriteTargets(command, depth = 0) {
1677
1884
  else unparsed = true;
1678
1885
  } else if (GIT_TREE_CHANGERS.has(sub)) unparsed = true;
1679
1886
  } else if (INLINE_INTERP.has(verb.replace(/[0-9.]+$/, "")) && args.some((w) => /^-[a-zA-Z]*[ec]$|^eval$/.test(w)) && INLINE_WRITE.test(args.join(" "))) unparsed = true;
1887
+ });
1888
+ const bodies = substitutionBodies(command);
1889
+ if (bodies.length && depth >= MAX_NEST) unparsed = true;
1890
+ else for (const body of bodies) {
1891
+ const r = shellWriteTargets(body, depth + 1);
1892
+ out.push(...r.paths);
1893
+ unparsed ||= r.unparsed;
1680
1894
  }
1681
1895
  if (depth === 0 && unobservable(command)) unparsed = true;
1682
- return { paths: [...new Set(out)], unparsed };
1896
+ return { paths: [...new Set(out)], unparsed, certain: [...new Set(certain)] };
1683
1897
  }
1684
1898
  function deletedOperands(command, depth = 0) {
1685
1899
  const out = [];
1686
1900
  for (const t of shellTokens(command).reduce((acc, x) => ("op" in x && SEPARATORS.has(x.op) ? acc.push([]) : "word" in x && acc[acc.length - 1].push(x.word), acc), [[]])) {
1687
- let words = t;
1688
- while (words.length && (WRAPPERS.has(words[0]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))) words = words.slice(1);
1901
+ const words = unwrapCommand(t);
1689
1902
  const verb = words[0]?.split("/").pop();
1690
- const ops = words.slice(1).filter((w) => !w.startsWith("-"));
1903
+ const ops = splitFlags(words.slice(1)).operands;
1691
1904
  if (verb === "rm" || verb === "unlink") out.push(...ops);
1692
1905
  else if (verb === "git" && words[1] === "rm") out.push(...ops.slice(1));
1693
- else if (verb && SHELLS.has(verb) && depth < 2) {
1906
+ else if (verb && SHELLS.has(verb) && depth < MAX_NEST) {
1694
1907
  const k = words.findIndex((w) => /^-[a-z]*c$/.test(w));
1695
1908
  if (k >= 0 && words[k + 1]) out.push(...deletedOperands(words[k + 1], depth + 1));
1696
1909
  }
@@ -1752,6 +1965,11 @@ var MUTATING_VERBS = /(^|[\s;&|(])(rm|mv|cp|truncate|tee|chmod|chown|ln|unlink|p
1752
1965
  function commandPathTokens(command) {
1753
1966
  return (command.match(/[A-Za-z0-9_./@+-]+/g) ?? []).filter((t) => t.includes("/") || t.includes("."));
1754
1967
  }
1968
+ function allowedForTask(ctx, protectedPaths) {
1969
+ const allow = ctx.contract?.allow ?? [];
1970
+ if (!allow.length) return null;
1971
+ return allow.map((g) => protectedPaths.some((p) => matchesAny(p, [g])) ? `${g} (except protected files)` : g).join(", ");
1972
+ }
1755
1973
  var protectedPathModified = (a, ctx) => {
1756
1974
  const protect = ctx.contract?.protect ?? [];
1757
1975
  if (isWrite(a)) {
@@ -1765,22 +1983,24 @@ var protectedPathModified = (a, ctx) => {
1765
1983
  }
1766
1984
  const hit = a.paths.filter((p) => matchesAny(p, protect));
1767
1985
  if (!hit.length) return null;
1986
+ const allowed = allowedForTask(ctx, hit);
1768
1987
  return {
1769
1988
  rule: "protected_path_modified",
1770
1989
  action: "block",
1771
- message: `${PREFIX} ${hit.join(", ")} is protected by the task contract and must not change. Leave it as it is and make the implementation satisfy it. If the task truly requires changing it, stop and ask the user.`
1990
+ message: allowed ? `${PREFIX} ${hit.join(", ")} is protected. Allowed for this task: ${allowed}. Change those instead; if the protected file must change, stop and ask the user.` : `${PREFIX} ${hit.join(", ")} is protected by the task contract and must not change. Leave it as it is and make the implementation satisfy it. If the task truly requires changing it, stop and ask the user.`
1772
1991
  };
1773
1992
  }
1774
1993
  if (a.kind === "command" && a.command) {
1775
1994
  const guarded = (p) => matchesAny(p, protect) !== null || p === ".replume" || p.startsWith(".replume/");
1776
1995
  const redirected = writeTargets(a, ctx.repoRoot).inside.filter(guarded);
1777
- const operands = splitPaths(ctx.repoRoot, fileVerbOperands(a.command)).inside.filter(guarded);
1996
+ const operands = splitPaths(ctx.repoRoot, [a.command, ...innerScripts(a.command)].flatMap((c) => fileVerbOperands(c))).inside.filter(guarded);
1778
1997
  const hit = [.../* @__PURE__ */ new Set([...redirected, ...operands])];
1779
1998
  if (!hit.length) return null;
1999
+ const allowed = allowedForTask(ctx, hit);
1780
2000
  return {
1781
2001
  rule: "protected_path_modified",
1782
2002
  action: "block",
1783
- message: `${PREFIX} this command would modify ${hit.join(", ")}, which the task contract protects. Do not change protected files through the shell either. If it is required, stop and ask the user.`
2003
+ message: allowed ? `${PREFIX} this command would modify ${hit.join(", ")}, which is protected. Allowed for this task: ${allowed}. Change those instead; if the protected file must change, stop and ask the user.` : `${PREFIX} this command would modify ${hit.join(", ")}, which the task contract protects. Do not change protected files through the shell either. If it is required, stop and ask the user.`
1784
2004
  };
1785
2005
  }
1786
2006
  return null;
@@ -1801,7 +2021,7 @@ var testWeakened = (a) => {
1801
2021
  message: `${PREFIX} this command forces the test run to succeed (\`|| true\` / \`exit 0\`), which hides failures. Run the tests plainly and fix what fails.`
1802
2022
  };
1803
2023
  }
1804
- const rmTests = [...new Set(deletedOperands(a.command).filter((t) => isTestFile(t)))];
2024
+ const rmTests = [...new Set([a.command, ...innerScripts(a.command)].flatMap((c) => deletedOperands(c)).filter((t) => isTestFile(t)))];
1805
2025
  if (rmTests.length) {
1806
2026
  return {
1807
2027
  rule: "test_weakened",
@@ -1854,10 +2074,32 @@ var ALWAYS_DESTRUCTIVE = [
1854
2074
  { re: /\bgit\s+branch\s+-D\b/, why: "force-deletes a branch" },
1855
2075
  { re: /\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE\s+TABLE)\b/i, why: "drops or truncates database data" },
1856
2076
  { re: /\bDELETE\s+FROM\s+\w+\s*(;|$|")/i, why: "deletes every row of a table" },
1857
- { re: /\bmkfs(\.\w+)?\b|\bdd\s+[^\n]*of=\/dev\//, why: "overwrites a disk" },
1858
2077
  { re: /\bchmod\s+-R\s+777\s+\//, why: "makes the filesystem world-writable" },
1859
2078
  { re: /:\(\)\s*\{\s*:\|:&\s*\};:/, why: "is a fork bomb" }
1860
2079
  ];
2080
+ function shellScripts(command, depth = 0) {
2081
+ if (depth >= MAX_NEST) return [];
2082
+ const out = [];
2083
+ const groups = [[]];
2084
+ for (const t of shellTokens(command)) {
2085
+ if ("op" in t) {
2086
+ if (SEPARATORS.has(t.op)) groups.push([]);
2087
+ } else groups[groups.length - 1].push(t.word);
2088
+ }
2089
+ for (const g of groups) {
2090
+ const words = unwrapCommand(g);
2091
+ if (!words.length || !SHELLS.has(words[0].split("/").pop())) continue;
2092
+ const k = words.findIndex((w, i) => i > 0 && /^-[a-z]*c$/.test(w));
2093
+ const script = k >= 0 ? words[k + 1] : void 0;
2094
+ if (script) out.push(script, ...shellScripts(script, depth + 1));
2095
+ }
2096
+ return out;
2097
+ }
2098
+ function innerScripts(command, depth = 0) {
2099
+ if (depth >= MAX_NEST) return [];
2100
+ const direct = [...shellScripts(command, MAX_NEST - 1), ...substitutionBodies(command)];
2101
+ return [...new Set(direct)].flatMap((x) => [x, ...innerScripts(x, depth + 1)]);
2102
+ }
1861
2103
  function deletionTargets(command) {
1862
2104
  const groups = [[]];
1863
2105
  for (const token of shellTokens(command)) {
@@ -1865,32 +2107,54 @@ function deletionTargets(command) {
1865
2107
  if (SEPARATORS.has(token.op)) groups.push([]);
1866
2108
  } else groups[groups.length - 1].push(token.word);
1867
2109
  }
1868
- const result = [];
1869
- for (let words of groups) {
1870
- while (words.length && (WRAPPERS.has(words[0]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))) words = words.slice(1);
2110
+ const result2 = [];
2111
+ for (const g of groups) {
2112
+ const words = unwrapCommand(g);
1871
2113
  const verb = words[0]?.split("/").pop();
1872
2114
  if (verb === "rm") {
1873
- const args = words.slice(1);
1874
- if (args.some((x) => /^-[^-]*[rR]/.test(x) || x === "--recursive"))
1875
- result.push({ targets: args.filter((x) => x !== "--" && !x.startsWith("-")), gitClean: false });
2115
+ const { flags, operands } = splitFlags(words.slice(1));
2116
+ if (flags.some((x) => /^-[^-]*[rR]/.test(x) || x === "--recursive")) result2.push({ targets: operands, gitClean: false });
1876
2117
  } else if (verb === "rmdir") {
1877
- result.push({ targets: words.slice(1).filter((x) => x !== "--" && !x.startsWith("-")), gitClean: false });
2118
+ result2.push({ targets: splitFlags(words.slice(1)).operands, gitClean: false });
1878
2119
  } else if (verb === "find" && words.includes("-delete")) {
1879
- result.push({ targets: words.slice(1, words.indexOf("-delete")).filter((x) => x !== "--" && !x.startsWith("-") && x !== "-depth"), gitClean: false });
2120
+ result2.push({ targets: words.slice(1, words.indexOf("-delete")).filter((x) => x !== "--" && !x.startsWith("-") && x !== "-depth"), gitClean: false });
1880
2121
  } else if (verb === "git") {
1881
2122
  const clean = words.indexOf("clean");
1882
2123
  if (clean < 0 || !words.slice(clean + 1).some((x) => /^-[^-]*f/.test(x) || x === "--force")) continue;
1883
2124
  const c = words.indexOf("-C");
1884
2125
  const cwd = c >= 0 && c < clean ? words[c + 1] : null;
1885
2126
  const paths = words.slice(clean + 1).filter((x) => x !== "--" && !x.startsWith("-"));
1886
- result.push({ targets: [...cwd ? [cwd] : [], ...paths], gitClean: true });
2127
+ result2.push({ targets: [...cwd ? [cwd] : [], ...paths], gitClean: true });
1887
2128
  }
1888
2129
  }
1889
- return result;
2130
+ return result2;
2131
+ }
2132
+ function expandTmpdirVar(target, ctx) {
2133
+ const m = /^\$\{?TMPDIR\}?(?=\/|$)/.exec(target);
2134
+ if (!m) return target;
2135
+ const env = process.env.TMPDIR === void 0 ? tmpdir() : process.env.TMPDIR;
2136
+ if (!env || !isAbsolute3(env)) return null;
2137
+ const base3 = resolve2(env);
2138
+ if (!isTempDir(ctx, base3)) return null;
2139
+ return base3 + target.slice(m[0].length);
2140
+ }
2141
+ function isTempDir(ctx, abs) {
2142
+ const roots = ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", ...ctx.scratchDirs ?? []].map((d) => resolve2(d));
2143
+ return roots.some((d) => abs === d || abs.startsWith(d + sep)) || /^\/(?:private\/)?var\/folders\/[^/]+\/[^/]+\/T(?:\/|$)/.test(abs);
1890
2144
  }
1891
2145
  function deletionLocation(target, ctx) {
1892
- if (target.startsWith("~") || /[$*?{}`]/.test(target)) return "critical";
1893
- const expanded = target === "~" || target.startsWith("~/") ? homedir() + target.slice(1) : target;
2146
+ const bare = target.replace(/^["']|["']$/g, "");
2147
+ const t = expandTmpdirVar(bare, ctx);
2148
+ if (t === null) return "critical";
2149
+ if (t.startsWith("~") || /[$?{}`]/.test(t) && !/[*?[]/.test(t)) return "critical";
2150
+ if (/[*?[]/.test(t)) {
2151
+ const segs = t.split("/");
2152
+ const gi = segs.findIndex((x) => /[*?[]/.test(x));
2153
+ const prefix = segs.slice(0, gi).join("/") || (t.startsWith("/") ? "/" : "");
2154
+ if (!/[$`{]/.test(prefix) && prefix && isTempDir(ctx, resolve2(ctx.repoRoot, prefix))) return "outside";
2155
+ return "critical";
2156
+ }
2157
+ const expanded = t === "~" || t.startsWith("~/") ? homedir() + t.slice(1) : t;
1894
2158
  const abs = resolve2(ctx.repoRoot, expanded);
1895
2159
  const root = resolve2(ctx.repoRoot);
1896
2160
  const home = resolve2(homedir());
@@ -1898,6 +2162,32 @@ function deletionLocation(target, ctx) {
1898
2162
  const rel = relative3(root, abs);
1899
2163
  return !rel || !rel.startsWith("..") && !isAbsolute3(rel) ? "inside" : "outside";
1900
2164
  }
2165
+ function resolvedTarget(target, ctx) {
2166
+ const t = expandTmpdirVar(target.replace(/^["']|["']$/g, ""), ctx) ?? target;
2167
+ return resolve2(ctx.repoRoot, t);
2168
+ }
2169
+ function blockDeviceWrites(scripts) {
2170
+ const out = shellWriteTargets(scripts[0] ?? "").paths.filter(isBlockDevice);
2171
+ for (const script of scripts) {
2172
+ const groups = [[]];
2173
+ const toks = shellTokens(script);
2174
+ for (let i = 0; i < toks.length; i++) {
2175
+ const t = toks[i];
2176
+ if ("op" in t) {
2177
+ if (SEPARATORS.has(t.op)) groups.push([]);
2178
+ else if (WRITE_OPS.has(t.op) || t.op === "<" || t.op === "<<<" || t.op === "<<") i++;
2179
+ continue;
2180
+ }
2181
+ groups[groups.length - 1].push(t.word);
2182
+ }
2183
+ for (const g of groups) {
2184
+ const words = unwrapCommand(g);
2185
+ const verb = words[0]?.split("/").pop() ?? "";
2186
+ if (verb === "mkfs" || verb.startsWith("mkfs.") || verb === "mkswap") out.push(...splitFlags(words.slice(1)).operands.filter(isBlockDevice));
2187
+ }
2188
+ }
2189
+ return [...new Set(out)];
2190
+ }
1901
2191
  function rmRfTargets(command) {
1902
2192
  const out = [];
1903
2193
  for (const seg of command.split(/&&|\|\||;|\|/)) {
@@ -1931,7 +2221,14 @@ var destructiveCommand = (a, ctx) => {
1931
2221
  };
1932
2222
  }
1933
2223
  }
1934
- const deletions = deletionTargets(cmd);
2224
+ const scripts = [cmd, ...innerScripts(cmd)];
2225
+ const devices = blockDeviceWrites(scripts);
2226
+ if (devices.length) return {
2227
+ rule: "destructive_command",
2228
+ action: "block",
2229
+ message: `${PREFIX} this command writes to a raw disk or block device (${devices.join(", ")}), which overwrites what is on it. It is blocked. If this is really intended, ask the user to run it themselves.`
2230
+ };
2231
+ const deletions = scripts.flatMap(deletionTargets);
1935
2232
  const deletionPaths = deletions.flatMap((d) => d.targets);
1936
2233
  const critical = deletionPaths.filter((p) => deletionLocation(p, ctx) === "critical");
1937
2234
  if (critical.length) return {
@@ -1939,7 +2236,7 @@ var destructiveCommand = (a, ctx) => {
1939
2236
  action: "block",
1940
2237
  message: `${PREFIX} this command can delete the home directory, filesystem root, or a parent of this repository (${critical.join(", ")}). It is blocked.`
1941
2238
  };
1942
- const foreignDeletion = deletionPaths.filter((p) => deletionLocation(p, ctx) === "outside" && !matchesAny(resolve2(ctx.repoRoot, p), ctx.contract?.allow ?? []));
2239
+ const foreignDeletion = deletionPaths.filter((p) => deletionLocation(p, ctx) === "outside" && !matchesAny(resolvedTarget(p, ctx), ctx.contract?.allow ?? []));
1943
2240
  if (foreignDeletion.length) return {
1944
2241
  rule: "destructive_command",
1945
2242
  action: "warn",
@@ -1959,11 +2256,11 @@ var destructiveCommand = (a, ctx) => {
1959
2256
  };
1960
2257
  }
1961
2258
  }
1962
- for (const targets of rmRfTargets(cmd)) {
2259
+ for (const targets of scripts.flatMap(rmRfTargets)) {
1963
2260
  const bad = targets.filter((t) => {
1964
2261
  const clean = t.replace(/^["']|["']$/g, "");
1965
- if (/^(\/|~|\$HOME|\*|\.{1,2}\/?)$/.test(clean) || clean.startsWith("~") || clean.includes("$")) return true;
1966
- const { inside, outside } = splitPaths(ctx.repoRoot, [clean]);
2262
+ if (/^(\/|~|\$HOME|\*|\.{1,2}\/?)$/.test(clean) || clean.startsWith("~") || deletionLocation(clean, ctx) === "critical") return true;
2263
+ const { inside, outside } = splitPaths(ctx.repoRoot, [expandTmpdirVar(clean, ctx) ?? clean]);
1967
2264
  if (outside.length) return !inScratch(ctx, outside[0]);
1968
2265
  return !REGENERABLE.test(inside[0]);
1969
2266
  });
@@ -1983,7 +2280,7 @@ var destructiveCommand = (a, ctx) => {
1983
2280
  message: `${PREFIX} this command deletes backup files. It is blocked; ask the user before removing backups.`
1984
2281
  };
1985
2282
  }
1986
- const verbTargets = MUTATING_VERBS.test(cmd) ? a.outsidePaths : [];
2283
+ const verbTargets = MUTATING_VERBS.test(cmd) ? a.outsidePaths.filter((p) => !p.startsWith("/dev/")) : [];
1987
2284
  const foreign = [.../* @__PURE__ */ new Set([...verbTargets, ...writeTargets(a, ctx.repoRoot).outside])].filter((p) => !inScratch(ctx, p) && !DEVICE_SINK.test(p));
1988
2285
  if (foreign.length) {
1989
2286
  return {
@@ -2274,7 +2571,7 @@ function parseJudgeResponse(x) {
2274
2571
  function commandJudge(executable, apiKey) {
2275
2572
  return {
2276
2573
  name: `command:${executable}`,
2277
- judge: (req, signal) => new Promise((resolve7, reject) => {
2574
+ judge: (req, signal) => new Promise((resolve9, reject) => {
2278
2575
  const env = { PATH: process.env.PATH, HOME: process.env.HOME };
2279
2576
  if (apiKey) env[ENV.apiKey] = apiKey;
2280
2577
  const child = spawn(executable, [], { env, stdio: ["pipe", "pipe", "pipe"], signal });
@@ -2291,7 +2588,7 @@ function commandJudge(executable, apiKey) {
2291
2588
  if (code !== 0) return reject(new Error(`judge command exited ${code}: ${err.trim().slice(0, 200)}`));
2292
2589
  if (stdinError) return reject(new Error(`judge command stdin failed: ${stdinError.message}`));
2293
2590
  try {
2294
- resolve7(parseJudgeResponse(JSON.parse(out)));
2591
+ resolve9(parseJudgeResponse(JSON.parse(out)));
2295
2592
  } catch (e) {
2296
2593
  reject(e);
2297
2594
  }
@@ -2656,11 +2953,11 @@ async function supervise(input, opts) {
2656
2953
 
2657
2954
  // src/supervisor/done.ts
2658
2955
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
2659
- import { join as join8 } from "path";
2956
+ import { join as join9 } from "path";
2660
2957
 
2661
2958
  // src/sandbox/scenarios.ts
2662
2959
  import { lstat, readdir, readFile, realpath } from "fs/promises";
2663
- import { join as join7, relative as relative4, sep as sep2 } from "path";
2960
+ import { join as join8, relative as relative4, sep as sep2 } from "path";
2664
2961
  var required = (ok, file2, field) => {
2665
2962
  if (!ok) throw new Error(`${file2}: invalid or missing ${field}`);
2666
2963
  };
@@ -2703,14 +3000,14 @@ async function loadScenarios(dir, root = dir) {
2703
3000
  if (!inside(await realpath(dir))) throw new Error(`scenario directory ${rel(dir)} resolves outside the commit; ${NOT_REGULAR}`);
2704
3001
  const files = (await readdir(dir)).filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).sort();
2705
3002
  for (const n of files) {
2706
- const p = join7(dir, n);
3003
+ const p = join8(dir, n);
2707
3004
  const st = await lstat(p);
2708
3005
  if (st.isSymbolicLink()) throw new Error(`scenario file ${rel(p)} is a symbolic link; ${NOT_REGULAR}`);
2709
3006
  if (!st.isFile()) throw new Error(`scenario file ${rel(p)} is not a regular file; ${NOT_REGULAR}`);
2710
3007
  if (!inside(await realpath(p))) throw new Error(`scenario file ${rel(p)} resolves outside the commit; ${NOT_REGULAR}`);
2711
3008
  }
2712
3009
  const scenarios = await Promise.all(files.map(async (n) => {
2713
- const s = parseScenario(await readFile(join7(dir, n), "utf8"), n);
3010
+ const s = parseScenario(await readFile(join8(dir, n), "utf8"), n);
2714
3011
  required(n.replace(/\.ya?ml$/, "") === s.id, n, "id (must match filename)");
2715
3012
  return s;
2716
3013
  }));
@@ -2792,10 +3089,10 @@ function doneStatus(contract, sessionEvents, repoRoot2) {
2792
3089
  for (const d of entries) {
2793
3090
  if ("run" in d) need(d.run, `\`${d.run}\``, d.run);
2794
3091
  else {
2795
- const file2 = join8(".replume", "scenarios", `${d.scenario}.yaml`);
3092
+ const file2 = join9(".replume", "scenarios", `${d.scenario}.yaml`);
2796
3093
  let run = null;
2797
3094
  try {
2798
- if (existsSync7(join8(repoRoot2, file2))) run = parseScenario(readFileSync8(join8(repoRoot2, file2), "utf-8"), file2).run;
3095
+ if (existsSync7(join9(repoRoot2, file2))) run = parseScenario(readFileSync8(join9(repoRoot2, file2), "utf-8"), file2).run;
2799
3096
  } catch {
2800
3097
  run = null;
2801
3098
  }
@@ -2831,7 +3128,7 @@ function readIfExists(path) {
2831
3128
  }
2832
3129
  }
2833
3130
  function absolutePathsIn(command) {
2834
- return (command.match(/(?<![\w.:/])(?:\/|~\/)[\w.@+-]+(?:\/[\w.@+-]*)*/g) ?? []).map(
3131
+ return (command.match(/(?<![\w.:/])(?:\/|~\/)[\w.@+-]+(?:\/[\w.@+-]*)*(?![\w.@+(/-])/g) ?? []).map(
2835
3132
  (p) => p.startsWith("~/") ? resolve3(process.env.HOME ?? "/", p.slice(2)) : p
2836
3133
  );
2837
3134
  }
@@ -2843,8 +3140,12 @@ function toProposedAction(p, repoRoot2) {
2843
3140
  let command = null;
2844
3141
  let writeTargetNotParsed = false;
2845
3142
  let mentioned = [];
3143
+ let certainRaw = [];
2846
3144
  const pending = [];
2847
- const abs = (fp) => isAbsolute4(fp) ? fp : resolve3(p.cwd, fp);
3145
+ const abs = (fp) => {
3146
+ const h = expandHome(fp);
3147
+ return isAbsolute4(h) || h.startsWith("~") ? h : resolve3(p.cwd, h);
3148
+ };
2848
3149
  switch (tool) {
2849
3150
  case "Bash":
2850
3151
  case "PowerShell": {
@@ -2853,6 +3154,7 @@ function toProposedAction(p, repoRoot2) {
2853
3154
  const w = tool === "Bash" ? shellWriteTargets(command) : { paths: [], unparsed: false };
2854
3155
  writeTargetNotParsed = w.unparsed;
2855
3156
  rawPaths = w.paths.map(abs);
3157
+ certainRaw = "certain" in w ? w.certain.map(abs) : [];
2856
3158
  mentioned = absolutePathsIn(command);
2857
3159
  break;
2858
3160
  }
@@ -2901,12 +3203,13 @@ function toProposedAction(p, repoRoot2) {
2901
3203
  }
2902
3204
  const { inside, outside: writtenOutside } = splitPaths(repoRoot2, rawPaths.filter(Boolean));
2903
3205
  const outside = [.../* @__PURE__ */ new Set([...writtenOutside, ...splitPaths(repoRoot2, mentioned).outside])];
3206
+ const certainPaths = [...new Set(splitPaths(repoRoot2, certainRaw).inside)];
2904
3207
  const changes = [];
2905
3208
  for (const c of pending) {
2906
3209
  const rel = splitPaths(repoRoot2, [c.abs]).inside[0];
2907
3210
  if (rel !== void 0) changes.push({ path: rel, before: c.before, after: c.after });
2908
3211
  }
2909
- return { kind, tool, paths: [...new Set(inside)], outsidePaths: outside, command, changes, fingerprint: fingerprint(tool, input), ...writeTargetNotParsed ? { writeTargetNotParsed } : {} };
3212
+ return { kind, tool, paths: [...new Set(inside)], outsidePaths: outside, command, changes, fingerprint: fingerprint(tool, input), ...writeTargetNotParsed ? { writeTargetNotParsed } : {}, ...kind === "command" ? { certainPaths } : {} };
2910
3213
  }
2911
3214
 
2912
3215
  // src/agents/claude/handler.ts
@@ -2922,7 +3225,7 @@ function resolveRepoRoot(p, env = process.env) {
2922
3225
  var WRITE_NOT_PARSED = "write target not parsed";
2923
3226
  function writeFields(a) {
2924
3227
  if (a.kind !== "command") return {};
2925
- return { writes: a.paths, ...a.writeTargetNotParsed ? { writeNote: WRITE_NOT_PARSED } : {} };
3228
+ return { writes: a.certainPaths ?? a.paths, ...a.writeTargetNotParsed ? { writeNote: WRITE_NOT_PARSED } : {} };
2926
3229
  }
2927
3230
  function summarize(a) {
2928
3231
  if (a.command !== null) return `${a.tool}: ${a.command}${a.writeTargetNotParsed ? ` (${WRITE_NOT_PARSED})` : ""}`;
@@ -3013,14 +3316,14 @@ function decide(p, repoRoot2, proposal, sessionEvents, env, deps) {
3013
3316
  };
3014
3317
  const opts = { layer2, judge, sessionEvents, slot };
3015
3318
  if (layer2Inactive(opts)) return { result: superviseSync(input, opts), contract };
3016
- return supervise(input, opts).then((result) => ({ result, contract }));
3319
+ return supervise(input, opts).then((result2) => ({ result: result2, contract }));
3017
3320
  }
3018
3321
  function preToolUse(p, repoRoot2, env, deps) {
3019
3322
  const ledger = ledgerPath(repoRoot2);
3020
3323
  const action = toProposedAction(p, repoRoot2);
3021
3324
  const session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
3022
- return andThen(decide(p, repoRoot2, { kind: "action", action }, session, env, deps), ({ result, contract }) => {
3023
- const deciding = result.findings.find((f) => f.decision === result.decision);
3325
+ return andThen(decide(p, repoRoot2, { kind: "action", action }, session, env, deps), ({ result: result2, contract }) => {
3326
+ const deciding = result2.findings.find((f) => f.decision === result2.decision);
3024
3327
  appendEvent(ledger, {
3025
3328
  ...base(p, "tool_pre"),
3026
3329
  tool: action.tool,
@@ -3028,22 +3331,22 @@ function preToolUse(p, repoRoot2, env, deps) {
3028
3331
  command: action.command,
3029
3332
  summary: summarize(action),
3030
3333
  rule: deciding?.rule ?? null,
3031
- action: ledgerAction(result.decision),
3032
- message: result.message,
3334
+ action: ledgerAction(result2.decision),
3335
+ message: result2.message,
3033
3336
  raw: {
3034
3337
  fingerprint: action.fingerprint,
3035
- decision: result.decision,
3036
- layer: result.layer,
3037
- layer2Mode: result.layer2.mode,
3338
+ decision: result2.decision,
3339
+ layer: result2.layer,
3340
+ layer2Mode: result2.layer2.mode,
3038
3341
  tool_use_id: p.tool_use_id ?? null,
3039
3342
  tool_input: p.tool_input ?? null,
3040
3343
  ...writeFields(action)
3041
3344
  }
3042
3345
  });
3043
3346
  const rec = { p, ledger, tool: action.tool, paths: action.paths, command: action.command, target: summarize(action), contractId: contract?.id ?? null };
3044
- recordFindings(rec, result.findings);
3045
- recordLayer2(rec, result.layer2Run, result.layer2.mode);
3046
- return { exitCode: 0, stdout: decisionOutput("PreToolUse", result), stderr: "" };
3347
+ recordFindings(rec, result2.findings);
3348
+ recordLayer2(rec, result2.layer2Run, result2.layer2.mode);
3349
+ return { exitCode: 0, stdout: decisionOutput("PreToolUse", result2), stderr: "" };
3047
3350
  });
3048
3351
  }
3049
3352
  var DONE_SLOT_RULE = "done_stop_slot";
@@ -3071,18 +3374,18 @@ function stop(p, repoRoot2, env, deps) {
3071
3374
  const session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
3072
3375
  const lastAssistantMessage = typeof p.last_assistant_message === "string" ? p.last_assistant_message : "";
3073
3376
  const stopHookActive = p.stop_hook_active === true;
3074
- return andThen(decide(p, repoRoot2, { kind: "stop", lastAssistantMessage, stopHookActive }, session, env, deps), ({ result, contract }) => {
3075
- const deciding = result.findings.find((f) => f.decision === result.decision);
3377
+ return andThen(decide(p, repoRoot2, { kind: "stop", lastAssistantMessage, stopHookActive }, session, env, deps), ({ result: result2, contract }) => {
3378
+ const deciding = result2.findings.find((f) => f.decision === result2.decision);
3076
3379
  const done = contract?.done?.length && !stopHookActive ? doneStatus(contract, session, repoRoot2) : null;
3077
3380
  const cap = contract?.max_stop_blocks ?? DEFAULT_MAX_STOP_BLOCKS;
3078
3381
  const unmet = done !== null && done.missing.length > 0;
3079
3382
  const slot = unmet ? reserveStopSlot(p, ledger) : null;
3080
3383
  const blocksSoFar = slot?.ahead ?? 0;
3081
3384
  const blockStop = unmet && blocksSoFar < cap;
3082
- const steer = result.decision === "block" ? { decision: "steer", message: result.message } : result;
3385
+ const steer = result2.decision === "block" ? { decision: "steer", message: result2.message } : result2;
3083
3386
  const doneText = !unmet ? null : blockStop ? `${doneMessage(done)} (stop block ${blocksSoFar + 1} of ${cap})` : `${doneMessage(done)} Replume is not blocking again (max_stop_blocks ${cap} reached).`;
3084
3387
  const message = doneText ? [doneText, steer.decision === "ok" ? "" : steer.message ?? ""].filter(Boolean).join("\n") : null;
3085
- const stopDecision = doneText ? blockStop ? "block" : "steer" : result.decision;
3388
+ const stopDecision = doneText ? blockStop ? "block" : "steer" : result2.decision;
3086
3389
  appendEvent(ledger, {
3087
3390
  ...base(p, "stop"),
3088
3391
  tool: null,
@@ -3091,19 +3394,19 @@ function stop(p, repoRoot2, env, deps) {
3091
3394
  summary: stopHookActive ? "agent finished its turn (continuing after stop-hook feedback)" : "agent finished its turn",
3092
3395
  rule: doneText ? DONE_RULE : deciding?.rule ?? null,
3093
3396
  action: ledgerAction(stopDecision),
3094
- message: message ?? result.message,
3397
+ message: message ?? result2.message,
3095
3398
  raw: {
3096
3399
  decision: stopDecision,
3097
- layer: result.layer,
3098
- layer2Mode: result.layer2.mode,
3400
+ layer: result2.layer,
3401
+ layer2Mode: result2.layer2.mode,
3099
3402
  stopHookActive,
3100
3403
  lastAssistantMessage: lastAssistantMessage.slice(0, 2e3),
3101
3404
  ...done ? { done: { checked: done.checked, missing: done.missing, blocked: blockStop } } : {}
3102
3405
  }
3103
3406
  });
3104
3407
  const rec = { p, ledger, tool: null, paths: [], command: null, target: "stop", contractId: contract?.id ?? null };
3105
- recordFindings(rec, result.findings);
3106
- recordLayer2(rec, result.layer2Run, result.layer2.mode);
3408
+ recordFindings(rec, result2.findings);
3409
+ recordLayer2(rec, result2.layer2Run, result2.layer2.mode);
3107
3410
  if (!unmet || !doneText || !message) return { exitCode: 0, stdout: decisionOutput("Stop", steer), stderr: "" };
3108
3411
  appendEvent(ledger, {
3109
3412
  ...base(p, "rule_hit"),
@@ -3166,9 +3469,9 @@ function prompt(p, repoRoot2) {
3166
3469
  }
3167
3470
  function failOpen(err, repoRoot2, env) {
3168
3471
  try {
3169
- const dir = join9(repoRoot2 ?? env.CLAUDE_PROJECT_DIR ?? process.cwd(), REPLUME_DIR);
3472
+ const dir = join10(repoRoot2 ?? env.CLAUDE_PROJECT_DIR ?? process.cwd(), REPLUME_DIR);
3170
3473
  mkdirSync6(dir, { recursive: true });
3171
- appendFileSync3(join9(dir, "hook-errors.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${err.stack ?? err}
3474
+ appendFileSync3(join10(dir, "hook-errors.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${err.stack ?? err}
3172
3475
  `);
3173
3476
  } catch {
3174
3477
  }
@@ -3203,19 +3506,19 @@ function handleClaudeHook(stdinText, env = process.env, deps = {}) {
3203
3506
  out = QUIET;
3204
3507
  }
3205
3508
  const root = repoRoot2;
3206
- const finish = (result) => {
3509
+ const finish = (result2) => {
3207
3510
  if ((p.hook_event_name === "Stop" || p.hook_event_name === "SessionEnd") && p.transcript_path) {
3208
3511
  const contract = loadContract(contractPath(root));
3209
3512
  if (contract && contract.status !== "draft") captureClaudeUsage({
3210
3513
  repoRoot: root,
3211
- allowedRoot: join9(env.HOME ?? homedir2(), ".claude", "projects"),
3514
+ allowedRoot: join10(env.HOME ?? homedir2(), ".claude", "projects"),
3212
3515
  transcriptPath: p.transcript_path,
3213
3516
  taskId: contract.id,
3214
3517
  sessionId: p.session_id,
3215
3518
  final: p.hook_event_name === "SessionEnd"
3216
3519
  });
3217
3520
  }
3218
- return result;
3521
+ return result2;
3219
3522
  };
3220
3523
  return out instanceof Promise ? out.then(finish).catch((err) => failOpen(err, root, env)) : Promise.resolve(finish(out));
3221
3524
  } catch (err) {
@@ -3470,13 +3773,16 @@ function ruleDecisionToHookResponse(hookEventName, decision2) {
3470
3773
  }
3471
3774
 
3472
3775
  // src/agents/cursor/action.ts
3473
- import { isAbsolute as isAbsolute6, join as join10 } from "path";
3776
+ import { isAbsolute as isAbsolute6, join as join11 } from "path";
3474
3777
  var str2 = (v) => typeof v === "string" && v.length > 0 ? v : null;
3475
3778
  function cursorPayloadToAction(raw3, repoRoot2, fallback = {}) {
3476
3779
  const hook2 = str2(raw3.hook_event_name);
3477
3780
  const toolInput = raw3.tool_input && typeof raw3.tool_input === "object" ? raw3.tool_input : {};
3478
3781
  const command = str2(raw3.command) && hook2 !== "beforeMCPExecution" ? str2(raw3.command) : str2(raw3.tool_name) === "Shell" ? str2(toolInput.command) : fallback.command ?? null;
3479
- const abs = (p) => isAbsolute6(p) ? p : join10(repoRoot2, p);
3782
+ const abs = (p) => {
3783
+ const h = expandHome(p);
3784
+ return isAbsolute6(h) || h.startsWith("~") ? h : join11(repoRoot2, h);
3785
+ };
3480
3786
  const proposedPath = hook2 === "preToolUse" && /write|edit|delete/i.test(str2(raw3.tool_name) ?? "") ? str2(toolInput.file_path) : null;
3481
3787
  const files = [str2(raw3.file_path), proposedPath, ...fallback.paths ?? []].filter((p) => !!p).map(abs);
3482
3788
  const edits = Array.isArray(raw3.edits) ? raw3.edits : [];
@@ -3502,7 +3808,8 @@ function cursorPayloadToAction(raw3, repoRoot2, fallback = {}) {
3502
3808
  command,
3503
3809
  changes,
3504
3810
  fingerprint: fingerprint(tool, { files: inside, command, edits }),
3505
- ...w?.unparsed ? { writeTargetNotParsed: true } : {}
3811
+ ...w?.unparsed ? { writeTargetNotParsed: true } : {},
3812
+ ...w ? { certainPaths: [...new Set(splitPaths(repoRoot2, w.certain.map(abs)).inside)] } : {}
3506
3813
  };
3507
3814
  }
3508
3815
 
@@ -3527,7 +3834,7 @@ function handleCursorHook(stdinText, env = process.env) {
3527
3834
  action: evaluation?.action ?? "ok",
3528
3835
  message: evaluation?.message ?? null
3529
3836
  };
3530
- const writes = action.kind === "command" ? { writes: action.paths, ...action.writeTargetNotParsed ? { writeNote: "write target not parsed" } : {} } : {};
3837
+ const writes = action.kind === "command" ? { writes: action.certainPaths ?? action.paths, ...action.writeTargetNotParsed ? { writeNote: "write target not parsed" } : {} } : {};
3531
3838
  appendEvent(ledger, sanitizeCursorLedgerEvent({
3532
3839
  ...event,
3533
3840
  ...action.kind === "command" ? { paths: action.paths } : {},
@@ -3546,10 +3853,221 @@ function handleCursorHook(stdinText, env = process.env) {
3546
3853
  }
3547
3854
  }
3548
3855
 
3549
- // src/agents/cursor/install.ts
3856
+ // src/agents/codex/handler.ts
3857
+ import { execFileSync as execFileSync6 } from "child_process";
3550
3858
  import { createHash as createHash2 } from "crypto";
3551
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
3552
- import { dirname as dirname5, join as join11 } from "path";
3859
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
3860
+ import { resolve as resolve6 } from "path";
3861
+
3862
+ // src/agents/codex/action.ts
3863
+ import { resolve as resolve5 } from "path";
3864
+ function codexAction(payload, root) {
3865
+ const tool = typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
3866
+ const input = payload.tool_input && typeof payload.tool_input === "object" && !Array.isArray(payload.tool_input) ? payload.tool_input : {};
3867
+ const command = typeof input.command === "string" ? input.command : "";
3868
+ if (tool === "Bash") {
3869
+ return toProposedAction({ session_id: "", cwd: String(payload.cwd), hook_event_name: "PreToolUse", tool_name: tool, tool_input: input }, root);
3870
+ }
3871
+ if (tool === "apply_patch") {
3872
+ const rawPaths = [...command.matchAll(/^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/gm)].map((m) => m[1].trim());
3873
+ const { inside, outside } = splitPaths(root, rawPaths.map((p) => resolve5(String(payload.cwd), p)));
3874
+ return {
3875
+ kind: "edit",
3876
+ tool,
3877
+ paths: [...new Set(inside)],
3878
+ outsidePaths: outside,
3879
+ command: null,
3880
+ changes: [],
3881
+ fingerprint: fingerprint(tool, input)
3882
+ };
3883
+ }
3884
+ return { kind: "other", tool, paths: [], outsidePaths: [], command: null, changes: [], fingerprint: fingerprint(tool, input) };
3885
+ }
3886
+
3887
+ // src/agents/codex/handler.ts
3888
+ var QUIET2 = { stdout: "", stderr: "", exitCode: 0 };
3889
+ var object = (x) => !!x && typeof x === "object" && !Array.isArray(x);
3890
+ var str3 = (x) => typeof x === "string" ? x : "";
3891
+ var result = (stdout) => ({ ...QUIET2, stdout });
3892
+ var fileHash = (path) => {
3893
+ try {
3894
+ return createHash2("sha256").update(readFileSync10(path)).digest("hex");
3895
+ } catch {
3896
+ return null;
3897
+ }
3898
+ };
3899
+ function handleCodexHook(stdinText) {
3900
+ try {
3901
+ if (stdinText.length > 1024 * 1024) throw new Error("oversized hook input");
3902
+ const p = JSON.parse(stdinText);
3903
+ if (!object(p) || !str3(p.session_id) || !str3(p.cwd) || !str3(p.hook_event_name)) throw new Error("invalid hook input");
3904
+ const cwd = resolve6(str3(p.cwd));
3905
+ let root = cwd;
3906
+ try {
3907
+ root = execFileSync6("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
3908
+ } catch {
3909
+ }
3910
+ if (!existsSync9(resolve6(root, ".replume"))) return QUIET2;
3911
+ const ledger = ledgerPath(root);
3912
+ const base3 = (type) => ({
3913
+ agent: "codex",
3914
+ agentVersion: null,
3915
+ sessionId: str3(p.session_id),
3916
+ type,
3917
+ tool: null,
3918
+ paths: [],
3919
+ command: null,
3920
+ summary: str3(p.hook_event_name),
3921
+ rule: null,
3922
+ action: null,
3923
+ message: null
3924
+ });
3925
+ const event = str3(p.hook_event_name);
3926
+ if (event === "UserPromptSubmit") {
3927
+ appendEvent(ledger, { ...base3("prompt"), summary: "Codex prompt received" });
3928
+ return QUIET2;
3929
+ }
3930
+ if (event === "Stop" || event === "SessionEnd") {
3931
+ appendEvent(ledger, { ...base3("stop"), summary: `Codex ${event}` });
3932
+ return QUIET2;
3933
+ }
3934
+ if (event !== "PreToolUse" && event !== "PostToolUse") return QUIET2;
3935
+ const a = codexAction(p, root);
3936
+ const raw3 = {
3937
+ tool_use_id: str3(p.tool_use_id),
3938
+ fingerprint: a.fingerprint,
3939
+ ...a.kind === "command" ? { writes: a.certainPaths ?? a.paths, ...a.writeTargetNotParsed ? { writeNote: "write target not parsed" } : {} } : {}
3940
+ };
3941
+ const loaded = loadContract(contractPath(root));
3942
+ const contract = isDraft(loaded) ? null : loaded;
3943
+ if (event === "PreToolUse") {
3944
+ const history = readEvents(ledger).events.filter((e) => e.sessionId === str3(p.session_id));
3945
+ const verdict = evaluate(a, { contract, history, repoRoot: root });
3946
+ const first = verdict.hits[0];
3947
+ appendEvent(ledger, {
3948
+ ...base3("tool_pre"),
3949
+ tool: a.tool,
3950
+ paths: a.paths,
3951
+ command: a.command,
3952
+ summary: a.command ? `${a.tool}: ${a.command}` : `${a.tool} ${a.paths.join(", ")}`,
3953
+ rule: first?.rule ?? null,
3954
+ action: verdict.action,
3955
+ message: verdict.message,
3956
+ raw: { ...raw3, beforeHashes: a.kind === "edit" ? Object.fromEntries(a.paths.map((path) => [path, fileHash(resolve6(root, path))])) : void 0 }
3957
+ });
3958
+ for (const hit of verdict.hits) appendEvent(ledger, {
3959
+ ...base3("rule_hit"),
3960
+ tool: a.tool,
3961
+ paths: a.paths,
3962
+ command: a.command,
3963
+ summary: `${hit.rule}: ${hit.action}`,
3964
+ rule: hit.rule,
3965
+ action: hit.action,
3966
+ message: hit.message,
3967
+ raw: { tool_use_id: str3(p.tool_use_id), layer: "layer1", sentToAgent: true }
3968
+ });
3969
+ if (verdict.action === "block") return result(JSON.stringify({ hookSpecificOutput: {
3970
+ hookEventName: "PreToolUse",
3971
+ permissionDecision: "deny",
3972
+ permissionDecisionReason: verdict.message
3973
+ } }));
3974
+ if (verdict.action === "warn") return result(JSON.stringify({ hookSpecificOutput: {
3975
+ hookEventName: "PreToolUse",
3976
+ additionalContext: verdict.message
3977
+ } }));
3978
+ return QUIET2;
3979
+ }
3980
+ const response = object(p.tool_response) ? p.tool_response : {};
3981
+ const responseText = str3(p.tool_response);
3982
+ const exit = typeof response.exit_code === "number" ? response.exit_code : typeof response.exitCode === "number" ? response.exitCode : /^Exit code: (\d+)\b/m.exec(responseText)?.[1];
3983
+ const success = exit === 0 || exit === "0";
3984
+ const prior = readEvents(ledger).events.reverse().find((e) => e.type === "tool_pre" && e.raw?.tool_use_id === str3(p.tool_use_id));
3985
+ const before = prior?.raw?.beforeHashes;
3986
+ const changed = a.paths.filter((path) => before && path in before && fileHash(resolve6(root, path)) !== before[path]);
3987
+ appendEvent(ledger, {
3988
+ ...base3("tool_post"),
3989
+ tool: a.tool,
3990
+ paths: a.paths,
3991
+ command: a.command,
3992
+ summary: `${a.tool} completed`,
3993
+ raw: { ...raw3, ok: success }
3994
+ });
3995
+ if (a.kind === "edit" && changed.length && (success || responseText.includes("Success."))) {
3996
+ for (const path of changed)
3997
+ appendEvent(ledger, {
3998
+ ...base3("file_edit"),
3999
+ tool: a.tool,
4000
+ paths: [path],
4001
+ summary: `${a.tool} changed ${path}`,
4002
+ action: "ok",
4003
+ raw: raw3
4004
+ });
4005
+ } else if (a.kind === "command") {
4006
+ appendEvent(ledger, {
4007
+ ...base3("command"),
4008
+ tool: a.tool,
4009
+ paths: a.paths,
4010
+ command: a.command,
4011
+ summary: `${a.tool}: ${a.command}`,
4012
+ raw: { ...raw3, ok: success }
4013
+ });
4014
+ }
4015
+ return QUIET2;
4016
+ } catch {
4017
+ return { stdout: "", stderr: "replume codex hook: failed to process payload\n", exitCode: 0 };
4018
+ }
4019
+ }
4020
+
4021
+ // src/agents/codex/install.ts
4022
+ import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
4023
+ import { dirname as dirname5, join as join12 } from "path";
4024
+ var CODEX_HOOK_COMMAND = "replume hook codex";
4025
+ var CODEX_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionEnd"];
4026
+ var codexHooksPath = (root) => join12(root, ".codex", "hooks.json");
4027
+ var obj2 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
4028
+ function mergeCodexHooks(config, command = CODEX_HOOK_COMMAND) {
4029
+ if (config.hooks !== void 0 && !obj2(config.hooks)) throw new Error("hooks must be an object");
4030
+ const hooks = { ...config.hooks ?? {} };
4031
+ const added = [];
4032
+ for (const event of CODEX_HOOK_EVENTS) {
4033
+ const groups = hooks[event] ?? [];
4034
+ if (!Array.isArray(groups)) throw new Error(`hooks.${event} must be an array`);
4035
+ if (groups.some((g) => obj2(g) && Array.isArray(g.hooks) && g.hooks.some((h) => obj2(h) && h.command === command))) continue;
4036
+ const entry = { hooks: [{ type: "command", command, timeout: event === "SessionEnd" ? 2 : 10 }] };
4037
+ hooks[event] = [...groups, event === "PreToolUse" || event === "PostToolUse" ? { matcher: "^(Bash|apply_patch)$", ...entry } : entry];
4038
+ added.push(event);
4039
+ }
4040
+ return { config: { ...config, hooks }, added };
4041
+ }
4042
+ function validateCodexHooks(root) {
4043
+ const path = codexHooksPath(root);
4044
+ if (!existsSync10(path)) return;
4045
+ try {
4046
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
4047
+ if (!obj2(parsed)) throw new Error("expected an object");
4048
+ mergeCodexHooks(parsed);
4049
+ } catch (e) {
4050
+ throw new Error(`${path} is invalid (${e.message}); nothing was changed. Fix it and run replume init again.`);
4051
+ }
4052
+ }
4053
+ function installCodexHooks(root) {
4054
+ validateCodexHooks(root);
4055
+ const path = codexHooksPath(root);
4056
+ const original = existsSync10(path) ? readFileSync11(path, "utf8") : null;
4057
+ const parsed = original ? JSON.parse(original) : {};
4058
+ const { config, added } = mergeCodexHooks(parsed);
4059
+ if (added.length) {
4060
+ mkdirSync7(dirname5(path), { recursive: true });
4061
+ if (original !== null && !existsSync10(`${path}.replume-backup`)) copyFileSync2(path, `${path}.replume-backup`);
4062
+ writeFileSync5(path, JSON.stringify(config, null, 2) + "\n");
4063
+ }
4064
+ return { path, added, backupPath: original === null ? null : `${path}.replume-backup` };
4065
+ }
4066
+
4067
+ // src/agents/cursor/install.ts
4068
+ import { createHash as createHash3 } from "crypto";
4069
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync12, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
4070
+ import { dirname as dirname6, join as join13 } from "path";
3553
4071
  var CURSOR_HOOK_COMMAND = "replume hook cursor";
3554
4072
  var CURSOR_HOOK_EVENTS = [
3555
4073
  "preToolUse",
@@ -3563,9 +4081,9 @@ var CURSOR_HOOK_EVENTS = [
3563
4081
  "stop",
3564
4082
  "sessionEnd"
3565
4083
  ];
3566
- var hash = (text) => createHash2("sha256").update(text).digest("hex");
4084
+ var hash = (text) => createHash3("sha256").update(text).digest("hex");
3567
4085
  var isObject = (x) => !!x && typeof x === "object" && !Array.isArray(x);
3568
- var cursorHooksPath = (root) => join11(root, ".cursor", "hooks.json");
4086
+ var cursorHooksPath = (root) => join13(root, ".cursor", "hooks.json");
3569
4087
  var backupPathFor = (path) => `${path}.replume-backup`;
3570
4088
  var statePathFor = (path) => `${path}.replume-state.json`;
3571
4089
  function parseConfig(text, path) {
@@ -3589,17 +4107,17 @@ function exactInstalledEntry(x, command) {
3589
4107
  function validateCursorHooksConfig(root, opts = {}) {
3590
4108
  const path = cursorHooksPath(root);
3591
4109
  const command = opts.command ?? CURSOR_HOOK_COMMAND;
3592
- const config = existsSync9(path) ? parseConfig(readFileSync10(path, "utf8"), path) : {};
4110
+ const config = existsSync11(path) ? parseConfig(readFileSync12(path, "utf8"), path) : {};
3593
4111
  const hooks = config.hooks ?? {};
3594
4112
  for (const event of CURSOR_HOOK_EVENTS) {
3595
4113
  if (hooks[event] !== void 0 && !Array.isArray(hooks[event]))
3596
4114
  throw new Error(`${path}.hooks.${event} is not an array; nothing was changed. Fix it and run replume init again.`);
3597
4115
  }
3598
4116
  const statePath = statePathFor(path);
3599
- if (existsSync9(statePath)) {
4117
+ if (existsSync11(statePath)) {
3600
4118
  let state;
3601
4119
  try {
3602
- state = JSON.parse(readFileSync10(statePath, "utf8"));
4120
+ state = JSON.parse(readFileSync12(statePath, "utf8"));
3603
4121
  } catch {
3604
4122
  throw new Error(`${statePath} is not valid JSON; nothing was changed. Fix it and run replume init again.`);
3605
4123
  }
@@ -3611,9 +4129,9 @@ function installCursorHooks(root, opts = {}) {
3611
4129
  const path = cursorHooksPath(root);
3612
4130
  const backupPath = backupPathFor(path);
3613
4131
  const statePath = statePathFor(path);
3614
- const hadOriginal = existsSync9(path);
4132
+ const hadOriginal = existsSync11(path);
3615
4133
  const command = opts.command ?? CURSOR_HOOK_COMMAND;
3616
- const original = hadOriginal ? readFileSync10(path, "utf8") : null;
4134
+ const original = hadOriginal ? readFileSync12(path, "utf8") : null;
3617
4135
  const config = original === null ? {} : parseConfig(original, path);
3618
4136
  const hooks = { ...config.hooks ?? {} };
3619
4137
  const added = [];
@@ -3625,36 +4143,36 @@ function installCursorHooks(root, opts = {}) {
3625
4143
  added.push(event);
3626
4144
  }
3627
4145
  }
3628
- if (!added.length) return { path, backupPath: existsSync9(backupPath) ? backupPath : null, added };
4146
+ if (!added.length) return { path, backupPath: existsSync11(backupPath) ? backupPath : null, added };
3629
4147
  const updated = JSON.stringify({ ...config, version: 1, hooks }, null, 2) + "\n";
3630
- mkdirSync7(dirname5(path), { recursive: true });
3631
- if (!existsSync9(statePath)) {
3632
- if (original !== null) writeFileSync5(backupPath, original, { flag: "wx" });
3633
- writeFileSync5(statePath, JSON.stringify({ hadOriginal, installedHash: hash(updated), added, command }) + "\n", { flag: "wx" });
4148
+ mkdirSync8(dirname6(path), { recursive: true });
4149
+ if (!existsSync11(statePath)) {
4150
+ if (original !== null) writeFileSync6(backupPath, original, { flag: "wx" });
4151
+ writeFileSync6(statePath, JSON.stringify({ hadOriginal, installedHash: hash(updated), added, command }) + "\n", { flag: "wx" });
3634
4152
  } else {
3635
- const state = JSON.parse(readFileSync10(statePath, "utf8"));
4153
+ const state = JSON.parse(readFileSync12(statePath, "utf8"));
3636
4154
  if (state.command !== command) throw new Error(`Cursor hooks already installed with a different command; uninstall first`);
3637
4155
  state.added = [.../* @__PURE__ */ new Set([...state.added, ...added])];
3638
4156
  state.installedHash = hash(updated);
3639
- writeFileSync5(statePath, JSON.stringify(state) + "\n");
4157
+ writeFileSync6(statePath, JSON.stringify(state) + "\n");
3640
4158
  }
3641
4159
  const temp = `${path}.replume-tmp`;
3642
- writeFileSync5(temp, updated);
4160
+ writeFileSync6(temp, updated);
3643
4161
  renameSync(temp, path);
3644
- return { path, backupPath: existsSync9(backupPath) ? backupPath : null, added };
4162
+ return { path, backupPath: existsSync11(backupPath) ? backupPath : null, added };
3645
4163
  }
3646
4164
  function uninstallCursorHooks(root) {
3647
4165
  const path = cursorHooksPath(root);
3648
4166
  const backupPath = backupPathFor(path);
3649
4167
  const statePath = statePathFor(path);
3650
- if (!existsSync9(statePath)) return { restored: false, path };
3651
- const state = JSON.parse(readFileSync10(statePath, "utf8"));
3652
- const current = existsSync9(path) ? readFileSync10(path, "utf8") : null;
4168
+ if (!existsSync11(statePath)) return { restored: false, path };
4169
+ const state = JSON.parse(readFileSync12(statePath, "utf8"));
4170
+ const current = existsSync11(path) ? readFileSync12(path, "utf8") : null;
3653
4171
  const unchanged = current !== null && hash(current) === state.installedHash;
3654
4172
  if (unchanged) {
3655
4173
  if (state.hadOriginal) {
3656
- if (!existsSync9(backupPath)) throw new Error(`Cursor hook backup missing: ${backupPath}`);
3657
- writeFileSync5(path, readFileSync10(backupPath));
4174
+ if (!existsSync11(backupPath)) throw new Error(`Cursor hook backup missing: ${backupPath}`);
4175
+ writeFileSync6(path, readFileSync12(backupPath));
3658
4176
  } else rmSync2(path);
3659
4177
  } else if (current !== null) {
3660
4178
  const config = parseConfig(current, path);
@@ -3670,20 +4188,20 @@ function uninstallCursorHooks(root) {
3670
4188
  if (kept.length) hooks[event] = kept;
3671
4189
  else delete hooks[event];
3672
4190
  }
3673
- writeFileSync5(path, JSON.stringify({ ...config, hooks }, null, 2) + "\n");
4191
+ writeFileSync6(path, JSON.stringify({ ...config, hooks }, null, 2) + "\n");
3674
4192
  }
3675
4193
  rmSync2(statePath);
3676
4194
  if (unchanged) rmSync2(backupPath, { force: true });
3677
- return { restored: true, path, backupRetained: !unchanged && existsSync9(backupPath) };
4195
+ return { restored: true, path, backupRetained: !unchanged && existsSync11(backupPath) };
3678
4196
  }
3679
4197
 
3680
4198
  // src/agents/claude/install.ts
3681
- import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3682
- import { dirname as dirname6, join as join12 } from "path";
4199
+ import { copyFileSync as copyFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
4200
+ import { dirname as dirname7, join as join14 } from "path";
3683
4201
  var DEFAULT_HOOK_COMMAND = "replume hook claude";
3684
4202
  var HOOK_EVENTS = ["PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "Stop", "SessionEnd"];
3685
4203
  function claudeSettingsPath(repoRoot2) {
3686
- return join12(repoRoot2, ".claude", "settings.json");
4204
+ return join14(repoRoot2, ".claude", "settings.json");
3687
4205
  }
3688
4206
  var isReplume = (h, command) => typeof h.command === "string" && (h.command === command || /(^|\s)hook claude\s*$/.test(h.command));
3689
4207
  function mergeHooks(settings, command = DEFAULT_HOOK_COMMAND) {
@@ -3712,8 +4230,8 @@ function installClaudeHooks(repoRoot2, opts = {}) {
3712
4230
  const settingsPath = claudeSettingsPath(repoRoot2);
3713
4231
  let current = {};
3714
4232
  let original = null;
3715
- if (existsSync10(settingsPath)) {
3716
- original = readFileSync11(settingsPath, "utf-8");
4233
+ if (existsSync12(settingsPath)) {
4234
+ original = readFileSync13(settingsPath, "utf-8");
3717
4235
  try {
3718
4236
  const parsed = original.trim() ? JSON.parse(original) : {};
3719
4237
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("not a JSON object");
@@ -3728,24 +4246,24 @@ function installClaudeHooks(repoRoot2, opts = {}) {
3728
4246
  if (original !== null) {
3729
4247
  const stamp = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3730
4248
  backupPath = `${settingsPath}.replume-backup-${stamp}`;
3731
- copyFileSync2(settingsPath, backupPath);
4249
+ copyFileSync3(settingsPath, backupPath);
3732
4250
  }
3733
- mkdirSync8(dirname6(settingsPath), { recursive: true });
3734
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4251
+ mkdirSync9(dirname7(settingsPath), { recursive: true });
4252
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3735
4253
  }
3736
4254
  return { settingsPath, backupPath, added, alreadyPresent };
3737
4255
  }
3738
4256
 
3739
4257
  // src/agents/claude/replay-hooks.ts
3740
- import { mkdtempSync, mkdirSync as mkdirSync9, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
4258
+ import { mkdtempSync, mkdirSync as mkdirSync10, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
3741
4259
  import { tmpdir as tmpdir2 } from "os";
3742
- import { join as join13 } from "path";
4260
+ import { join as join15 } from "path";
3743
4261
  var REPLAY_HOOKS_BANNER = "HOOK PAYLOAD REPLAY. Each line is a Claude Code hook payload run through Replume's live hook handler in a temporary project.\nThis shows Replume's answers to these payloads; it does not show a live agent receiving or following them.";
3744
4262
  async function replayHooks(content, opts = {}) {
3745
- const repo = mkdtempSync(join13(tmpdir2(), "replume-replay-hooks-"));
4263
+ const repo = mkdtempSync(join15(tmpdir2(), "replume-replay-hooks-"));
3746
4264
  try {
3747
- mkdirSync9(join13(repo, ".replume"), { recursive: true });
3748
- if (opts.contractYaml) writeFileSync7(contractPath(repo), opts.contractYaml);
4265
+ mkdirSync10(join15(repo, ".replume"), { recursive: true });
4266
+ if (opts.contractYaml) writeFileSync8(contractPath(repo), opts.contractYaml);
3749
4267
  const env = { REPLUME_JUDGE: "off", ...opts.env, CLAUDE_PROJECT_DIR: repo };
3750
4268
  const steps = [];
3751
4269
  const lines = content.split("\n").filter((l) => l.trim());
@@ -3775,10 +4293,10 @@ async function replayHooks(content, opts = {}) {
3775
4293
  }
3776
4294
 
3777
4295
  // src/core/init.ts
3778
- import { execFileSync as execFileSync7 } from "child_process";
3779
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
4296
+ import { execFileSync as execFileSync8 } from "child_process";
4297
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
3780
4298
  import { homedir as homedir3 } from "os";
3781
- import { delimiter, join as join15 } from "path";
4299
+ import { delimiter, join as join17 } from "path";
3782
4300
 
3783
4301
  // src/core/capabilities.ts
3784
4302
  var CAPABILITIES = [
@@ -3800,11 +4318,11 @@ var CAPABILITIES = [
3800
4318
  },
3801
4319
  {
3802
4320
  agent: "Codex",
3803
- observe: "unverified",
3804
- block: "unverified",
3805
- steer: "unverified",
4321
+ observe: "verified live (CLI)",
4322
+ block: "verified live (CLI)",
4323
+ steer: "documented",
3806
4324
  installedByInit: false,
3807
- notes: "partial at best; live hook delivery has not been verified"
4325
+ notes: "Codex CLI 0.144.4: project hooks delivered PreToolUse and PostToolUse with trust enabled; Replume blocked one protected apply_patch and observed one allowed edit leading to a no-scenario PASS on 20.10.2026. Warning delivery and Stop continuation not tested; hook trust and disabled hooks limit capture; cloud project hooks unsupported."
3808
4326
  }
3809
4327
  ];
3810
4328
  function renderCapabilities(caps = CAPABILITIES, detected = {}) {
@@ -3833,10 +4351,10 @@ function renderCapabilities(caps = CAPABILITIES, detected = {}) {
3833
4351
  }
3834
4352
 
3835
4353
  // src/core/baseline.ts
3836
- import { createHash as createHash3 } from "crypto";
3837
- import { execFileSync as execFileSync6 } from "child_process";
3838
- import { appendFileSync as appendFileSync4, existsSync as existsSync11, lstatSync, readFileSync as readFileSync12, readdirSync, readlinkSync, writeFileSync as writeFileSync8 } from "fs";
3839
- import { dirname as dirname7, join as join14 } from "path";
4354
+ import { createHash as createHash4 } from "crypto";
4355
+ import { execFileSync as execFileSync7 } from "child_process";
4356
+ import { appendFileSync as appendFileSync4, existsSync as existsSync13, lstatSync, readFileSync as readFileSync14, readdirSync, readlinkSync, writeFileSync as writeFileSync9 } from "fs";
4357
+ import { dirname as dirname8, join as join16 } from "path";
3840
4358
  var BASELINE_FILE = "baseline.json";
3841
4359
  var DEFAULT_REPLUME_GITIGNORE = `events.jsonl
3842
4360
  reports/
@@ -3848,22 +4366,22 @@ acceptance.jsonl
3848
4366
  `;
3849
4367
  var SCENARIOS_SUBDIR = "scenarios";
3850
4368
  var PREFLIGHT_STATUS = ["works", "fails_before", "not_runnable", "skipped"];
3851
- var sha256 = (data) => createHash3("sha256").update(data).digest("hex");
4369
+ var sha256 = (data) => createHash4("sha256").update(data).digest("hex");
3852
4370
  var IGNORED_NAMES = /* @__PURE__ */ new Set([".DS_Store"]);
3853
4371
  function hashTree(dir) {
3854
4372
  const out = {};
3855
4373
  const walk = (abs, rel) => {
3856
4374
  for (const name of readdirSync(abs).sort()) {
3857
4375
  if (IGNORED_NAMES.has(name)) continue;
3858
- const p = join14(abs, name);
4376
+ const p = join16(abs, name);
3859
4377
  const r = rel ? `${rel}/${name}` : name;
3860
4378
  const st = lstatSync(p);
3861
4379
  if (st.isSymbolicLink()) out[r] = sha256(`symlink:${readlinkSync(p)}`);
3862
4380
  else if (st.isDirectory()) walk(p, r);
3863
- else if (st.isFile()) out[r] = sha256(readFileSync12(p));
4381
+ else if (st.isFile()) out[r] = sha256(readFileSync14(p));
3864
4382
  }
3865
4383
  };
3866
- if (existsSync11(dir)) {
4384
+ if (existsSync13(dir)) {
3867
4385
  try {
3868
4386
  if (lstatSync(dir).isDirectory()) walk(dir, "");
3869
4387
  } catch {
@@ -3873,19 +4391,19 @@ function hashTree(dir) {
3873
4391
  }
3874
4392
  function headCommit(repoRoot2) {
3875
4393
  try {
3876
- return execFileSync6("git", ["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: repoRoot2, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4394
+ return execFileSync7("git", ["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: repoRoot2, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
3877
4395
  } catch {
3878
4396
  return null;
3879
4397
  }
3880
4398
  }
3881
- var baselinePath = (contractPath2) => join14(dirname7(contractPath2), BASELINE_FILE);
4399
+ var baselinePath = (contractPath2) => join16(dirname8(contractPath2), BASELINE_FILE);
3882
4400
  function ensureBaselineIgnored(replumeDir) {
3883
- const gi = join14(replumeDir, ".gitignore");
3884
- if (!existsSync11(gi)) {
3885
- writeFileSync8(gi, DEFAULT_REPLUME_GITIGNORE);
4401
+ const gi = join16(replumeDir, ".gitignore");
4402
+ if (!existsSync13(gi)) {
4403
+ writeFileSync9(gi, DEFAULT_REPLUME_GITIGNORE);
3886
4404
  return;
3887
4405
  }
3888
- const text = readFileSync12(gi, "utf-8");
4406
+ const text = readFileSync14(gi, "utf-8");
3889
4407
  if (text.split(/\r?\n/).some((l) => l.trim() === BASELINE_FILE)) return;
3890
4408
  appendFileSync4(gi, `${text === "" || text.endsWith("\n") ? "" : "\n"}${BASELINE_FILE}
3891
4409
  `);
@@ -3897,12 +4415,12 @@ function recordBaseline(opts) {
3897
4415
  contract_id: opts.contractId,
3898
4416
  recorded_at: (opts.now ?? /* @__PURE__ */ new Date()).toISOString(),
3899
4417
  base_commit: headCommit(opts.repoRoot),
3900
- policy_hash: sha256(readFileSync12(opts.contractPath)),
3901
- scenario_hashes: hashTree(opts.scenariosDir ?? join14(dirname7(opts.contractPath), SCENARIOS_SUBDIR)),
4418
+ policy_hash: sha256(readFileSync14(opts.contractPath)),
4419
+ scenario_hashes: hashTree(opts.scenariosDir ?? join16(dirname8(opts.contractPath), SCENARIOS_SUBDIR)),
3902
4420
  ...opts.preflight ? { preflight: opts.preflight } : {}
3903
4421
  };
3904
- writeFileSync8(path, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
3905
- ensureBaselineIgnored(dirname7(opts.contractPath));
4422
+ writeFileSync9(path, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
4423
+ ensureBaselineIgnored(dirname8(opts.contractPath));
3906
4424
  return { path, baseline };
3907
4425
  }
3908
4426
  var isHash = (x) => typeof x === "string" && /^[0-9a-f]{64}$/.test(x);
@@ -3923,10 +4441,10 @@ function validateBaseline(x) {
3923
4441
  return p;
3924
4442
  }
3925
4443
  function readBaseline(path) {
3926
- if (!existsSync11(path)) return { status: "absent" };
4444
+ if (!existsSync13(path)) return { status: "absent" };
3927
4445
  let raw3;
3928
4446
  try {
3929
- raw3 = JSON.parse(readFileSync12(path, "utf-8"));
4447
+ raw3 = JSON.parse(readFileSync14(path, "utf-8"));
3930
4448
  } catch (e) {
3931
4449
  return { status: "unreadable", detail: e.message };
3932
4450
  }
@@ -3960,7 +4478,7 @@ function compareToBaseline(baseline, current, displayPaths) {
3960
4478
  // src/core/init.ts
3961
4479
  function onPath(bin) {
3962
4480
  try {
3963
- execFileSync7(process.platform === "win32" ? "where" : "which", [bin], { stdio: "ignore" });
4481
+ execFileSync8(process.platform === "win32" ? "where" : "which", [bin], { stdio: "ignore" });
3964
4482
  return true;
3965
4483
  } catch {
3966
4484
  return false;
@@ -3971,7 +4489,7 @@ function launchedThroughNpx() {
3971
4489
  }
3972
4490
  function gitIdentity(repoRoot2, key) {
3973
4491
  try {
3974
- return execFileSync7("git", ["-C", repoRoot2, "config", "--get", key], {
4492
+ return execFileSync8("git", ["-C", repoRoot2, "config", "--get", key], {
3975
4493
  encoding: "utf8",
3976
4494
  stdio: ["ignore", "pipe", "ignore"]
3977
4495
  }).trim().length > 0;
@@ -3981,18 +4499,18 @@ function gitIdentity(repoRoot2, key) {
3981
4499
  }
3982
4500
  function detectAgents(repoRoot2, home = homedir3()) {
3983
4501
  return {
3984
- "Claude Code": onPath("claude") || existsSync12(join15(repoRoot2, ".claude")) || existsSync12(join15(home, ".claude")),
3985
- Cursor: onPath("cursor") || existsSync12(join15(repoRoot2, ".cursor")) || existsSync12(join15(home, ".cursor")),
3986
- Codex: onPath("codex") || existsSync12(join15(home, ".codex"))
4502
+ "Claude Code": onPath("claude") || existsSync14(join17(repoRoot2, ".claude")) || existsSync14(join17(home, ".claude")),
4503
+ Cursor: onPath("cursor") || existsSync14(join17(repoRoot2, ".cursor")) || existsSync14(join17(home, ".cursor")),
4504
+ Codex: onPath("codex") || existsSync14(join17(home, ".codex"))
3987
4505
  };
3988
4506
  }
3989
4507
  function runInit(repoRoot2, opts = {}) {
3990
4508
  const lines = [];
3991
- const dir = join15(repoRoot2, REPLUME_DIR);
3992
- mkdirSync10(join15(dir, "scenarios"), { recursive: true });
3993
- const gi = join15(dir, ".gitignore");
3994
- if (!existsSync12(gi)) {
3995
- writeFileSync9(gi, DEFAULT_REPLUME_GITIGNORE);
4509
+ const dir = join17(repoRoot2, REPLUME_DIR);
4510
+ mkdirSync11(join17(dir, "scenarios"), { recursive: true });
4511
+ const gi = join17(dir, ".gitignore");
4512
+ if (!existsSync14(gi)) {
4513
+ writeFileSync10(gi, DEFAULT_REPLUME_GITIGNORE);
3996
4514
  }
3997
4515
  lines.push(`\u2713 ${REPLUME_DIR}/ ready (scenarios/ for team scenarios; ledger and reports are git-ignored)`);
3998
4516
  const detected = opts.detected ?? detectAgents(repoRoot2);
@@ -4007,7 +4525,7 @@ function runInit(repoRoot2, opts = {}) {
4007
4525
  } else lines.push("- Claude Code not found: no hooks written");
4008
4526
  const npx = launchedThroughNpx();
4009
4527
  const persistentCommand = !npx && onPath("replume");
4010
- const defaultHooks = detected["Claude Code"] && !opts.hookCommand || opts.cursorHooks && !opts.cursorHookCommand;
4528
+ const defaultHooks = detected["Claude Code"] && !opts.hookCommand || opts.cursorHooks && !opts.cursorHookCommand || opts.codexHooks;
4011
4529
  if (defaultHooks && !persistentCommand) {
4012
4530
  lines.push(npx ? "! `replume` is not guaranteed on PATH for later agent hooks: npx's temporary PATH entry does not persist, so those hooks may fail." : "! `replume` is not on PATH, so the hooks will fail (the agent then continues without them).");
4013
4531
  lines.push(" Install the persistent command with `npm install -g replume-cli`, then re-run `replume init`.");
@@ -4028,9 +4546,9 @@ function runInit(repoRoot2, opts = {}) {
4028
4546
  }
4029
4547
 
4030
4548
  // src/core/draft.ts
4031
- import { execFileSync as execFileSync8 } from "child_process";
4032
- import { existsSync as existsSync13, lstatSync as lstatSync2, readFileSync as readFileSync13 } from "fs";
4033
- import { join as join16, posix } from "path";
4549
+ import { execFileSync as execFileSync9 } from "child_process";
4550
+ import { existsSync as existsSync15, lstatSync as lstatSync2, readFileSync as readFileSync15 } from "fs";
4551
+ import { join as join18, posix } from "path";
4034
4552
  var LOCKFILES = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "poetry.lock", "Pipfile.lock", "uv.lock", "Cargo.lock", "go.sum", "Gemfile.lock", "composer.lock"];
4035
4553
  var CI_CONFIG = [
4036
4554
  [".github/workflows/**", /^\.github\/workflows\//],
@@ -4048,20 +4566,20 @@ var STOPWORDS = new Set(
4048
4566
  var NPM_PLACEHOLDER = /no test specified/;
4049
4567
  function trackedFiles(repoRoot2) {
4050
4568
  try {
4051
- return execFileSync8("git", ["ls-files", "-z"], { cwd: repoRoot2, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).toString().split("\0").filter(Boolean);
4569
+ return execFileSync9("git", ["ls-files", "-z"], { cwd: repoRoot2, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).toString().split("\0").filter(Boolean);
4052
4570
  } catch {
4053
4571
  return [];
4054
4572
  }
4055
4573
  }
4056
4574
  var read = (repoRoot2, rel) => {
4057
4575
  try {
4058
- return readFileSync13(join16(repoRoot2, rel), "utf-8");
4576
+ return readFileSync15(join18(repoRoot2, rel), "utf-8");
4059
4577
  } catch {
4060
4578
  return "";
4061
4579
  }
4062
4580
  };
4063
4581
  function detectTestCommand(repoRoot2, files) {
4064
- const has = (f) => files.includes(f) || existsSync13(join16(repoRoot2, f));
4582
+ const has = (f) => files.includes(f) || existsSync15(join18(repoRoot2, f));
4065
4583
  if (has("package.json")) {
4066
4584
  let script;
4067
4585
  try {
@@ -4091,10 +4609,10 @@ function draftContract(input) {
4091
4609
  const t = detectTestCommand(input.repoRoot, files);
4092
4610
  notes.push(t.command ? `test_command "${t.command}": ${t.reason}` : `test_command: none (${t.reason})`);
4093
4611
  const scenarios = files.filter((f) => /^\.replume\/scenarios\/[^/]+\.ya?ml$/.test(f)).flatMap((f) => {
4094
- const path = join16(input.repoRoot, f);
4612
+ const path = join18(input.repoRoot, f);
4095
4613
  try {
4096
4614
  if (!lstatSync2(path).isFile() || lstatSync2(path).isSymbolicLink()) return [];
4097
- const s = parseScenario(readFileSync13(path, "utf8"), f);
4615
+ const s = parseScenario(readFileSync15(path, "utf8"), f);
4098
4616
  return [s.id === posix.basename(f).replace(/\.ya?ml$/, "") ? s : null].filter((x) => x !== null);
4099
4617
  } catch {
4100
4618
  notes.push(`must_check: skipped invalid scenario ${f}; fix it before verification`);
@@ -4161,7 +4679,7 @@ function draftContract(input) {
4161
4679
  for (const p of namedCandidates) notes.push(`candidate: ${p} (named in the task; review before allowing)`);
4162
4680
  let recent = [];
4163
4681
  try {
4164
- recent = execFileSync8("git", ["log", "-5", "--name-only", "--pretty=format:"], { cwd: input.repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
4682
+ recent = execFileSync9("git", ["log", "-5", "--name-only", "--pretty=format:"], { cwd: input.repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
4165
4683
  } catch {
4166
4684
  }
4167
4685
  const recentCandidates = [...new Set(recent.filter((f) => source.includes(f) && !namedCandidates.includes(f) && !["README.md", "package.json"].includes(f)))].slice(0, Math.max(0, 5 - namedCandidates.length));
@@ -4176,10 +4694,14 @@ function draftContract(input) {
4176
4694
  };
4177
4695
  return { contract, notes, broad: broad.filter((g) => allow.includes(g)) };
4178
4696
  }
4697
+ function allowHasNoSource(allow) {
4698
+ return allow.every((g) => isTestFile(g.replace(/\*+/g, "x")));
4699
+ }
4700
+ var ALLOW_NO_SOURCE_WARNING = "allow contains no source files; the agent may have no allowed way to finish this task. Name the files in --task or edit allow.";
4179
4701
 
4180
4702
  // src/sandbox/preflight.ts
4181
- import { existsSync as existsSync16 } from "fs";
4182
- import { join as join21 } from "path";
4703
+ import { existsSync as existsSync18 } from "fs";
4704
+ import { join as join23 } from "path";
4183
4705
 
4184
4706
  // src/sandbox/docker.ts
4185
4707
  import { spawn as spawn2, spawnSync } from "child_process";
@@ -4187,12 +4709,17 @@ import { randomUUID } from "crypto";
4187
4709
  import { rmSync as rmSync4 } from "fs";
4188
4710
  import { chmod, cp, lstat as lstat2, mkdtemp, readdir as readdir2, rm, writeFile } from "fs/promises";
4189
4711
  import { tmpdir as tmpdir3 } from "os";
4190
- import { basename as basename2, join as join17 } from "path";
4712
+ import { basename as basename2, join as join19 } from "path";
4191
4713
  import { performance } from "perf_hooks";
4192
4714
  var DEFAULT_DOCKER_IMAGE = "node:20-bookworm-slim";
4193
- var DOCKER_PROBE_TIMEOUT_MS = 2e4;
4194
- async function probeDaemon(binary = "docker", timeoutMs = DOCKER_PROBE_TIMEOUT_MS) {
4195
- const context = await dockerCli(binary, ["context", "inspect"], timeoutMs);
4715
+ var DOCKER_PROBE_TIMEOUT_MS = 15e3;
4716
+ var remaining = (deadline) => Math.max(0, Math.ceil(deadline - performance.now()));
4717
+ var probeCli = (binary, args, deadline) => {
4718
+ const left = remaining(deadline);
4719
+ return left > 0 ? dockerCli(binary, args, left) : Promise.resolve({ code: null, stdout: "", stderr: "", timedOut: true, notFound: false });
4720
+ };
4721
+ async function probeDaemonUntil(binary, deadline) {
4722
+ const context = await probeCli(binary, ["context", "inspect"], deadline);
4196
4723
  if (context.timedOut) return { status: "timeout", step: "docker context inspect" };
4197
4724
  if (context.notFound) return { status: "unavailable", detail: "docker CLI not found" };
4198
4725
  if (context.code !== 0) return { status: "unavailable" };
@@ -4202,12 +4729,12 @@ async function probeDaemon(binary = "docker", timeoutMs = DOCKER_PROBE_TIMEOUT_M
4202
4729
  } catch {
4203
4730
  return { status: "unavailable" };
4204
4731
  }
4205
- const info = await dockerCli(binary, ["info", "--format", "{{.ServerVersion}}"], timeoutMs);
4732
+ const info = await probeCli(binary, ["info", "--format", "{{.ServerVersion}}"], deadline);
4206
4733
  if (info.timedOut) return { status: "timeout", step: "docker info" };
4207
4734
  return info.code === 0 ? { status: "ok" } : { status: "unavailable" };
4208
4735
  }
4209
- async function probeImage(image, binary = "docker", timeoutMs = DOCKER_PROBE_TIMEOUT_MS) {
4210
- const r = await dockerCli(binary, ["image", "inspect", image], timeoutMs);
4736
+ async function probeImageUntil(image, binary, deadline) {
4737
+ const r = await probeCli(binary, ["image", "inspect", image], deadline);
4211
4738
  return r.timedOut ? "timeout" : r.code === 0 ? "present" : "missing";
4212
4739
  }
4213
4740
  var seconds = (ms) => `${ms / 1e3} s`;
@@ -4215,19 +4742,20 @@ async function resolveIsolation(choice, image, probe = {}) {
4215
4742
  if (choice === "worktree") return { mode: "worktree", reason: "requested: worktree" };
4216
4743
  const legacy = "daemon" in probe;
4217
4744
  const timeoutMs = legacy ? DOCKER_PROBE_TIMEOUT_MS : probe.timeoutMs ?? DOCKER_PROBE_TIMEOUT_MS;
4218
- const daemon = legacy ? await probe.daemon() ? { status: "ok" } : { status: "unavailable" } : await probeDaemon(probe.binary, timeoutMs);
4745
+ const deadline = performance.now() + timeoutMs;
4746
+ const daemon = legacy ? await probe.daemon() ? { status: "ok" } : { status: "unavailable" } : await probeDaemonUntil(probe.binary ?? "docker", deadline);
4219
4747
  if (daemon.status === "timeout") {
4220
4748
  if (choice === "docker") throw new Error(`Docker was requested but did not answer within ${seconds(timeoutMs)} (${daemon.step})`);
4221
- return { mode: "worktree", reason: `auto: Docker did not answer within ${seconds(timeoutMs)} (${daemon.step}); used the worktree` };
4749
+ return { mode: "worktree", reason: `auto: Docker did not answer within ${seconds(timeoutMs)} (${daemon.step}); using the isolated working copy instead (not a security sandbox)` };
4222
4750
  }
4223
4751
  if (daemon.status === "unavailable") {
4224
4752
  if (choice === "docker") throw new Error(`Docker was requested but the Docker CLI or a local daemon is unavailable${daemon.detail ? ` (${daemon.detail})` : ""}`);
4225
4753
  return { mode: "worktree", reason: `auto: no local Docker daemon${daemon.detail ? ` (${daemon.detail})` : ""}` };
4226
4754
  }
4227
- const img = legacy ? await probe.image(image) ? "present" : "missing" : await probeImage(image, probe.binary, timeoutMs);
4755
+ const img = legacy ? await probe.image(image) ? "present" : "missing" : await probeImageUntil(image, probe.binary ?? "docker", deadline);
4228
4756
  if (img === "timeout") {
4229
4757
  if (choice === "docker") throw new Error(`Docker was requested but did not answer within ${seconds(timeoutMs)} (docker image inspect)`);
4230
- return { mode: "worktree", reason: `auto: Docker did not answer within ${seconds(timeoutMs)} (docker image inspect); used the worktree` };
4758
+ return { mode: "worktree", reason: `auto: Docker did not answer within ${seconds(timeoutMs)} (docker image inspect); using the isolated working copy instead (not a security sandbox)` };
4231
4759
  }
4232
4760
  if (img === "missing") {
4233
4761
  if (choice === "docker") throw new Error(`Docker was requested but image ${image} is not present locally; run: docker pull ${image}`);
@@ -4239,14 +4767,14 @@ function cliEnv() {
4239
4767
  return { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: process.env.HOME, DOCKER_CONFIG: process.env.DOCKER_CONFIG };
4240
4768
  }
4241
4769
  function dockerCli(binary, args, timeoutMs) {
4242
- return new Promise((resolve7) => {
4770
+ return new Promise((resolve9) => {
4243
4771
  const child = spawn2(binary, args, { env: cliEnv(), stdio: ["ignore", "pipe", "pipe"] });
4244
4772
  let stdout = "", stderr = "", settled = false, timedOut = false, notFound = false;
4245
4773
  const done = (code) => {
4246
4774
  if (settled) return;
4247
4775
  settled = true;
4248
4776
  clearTimeout(timer);
4249
- resolve7({ code, stdout, stderr, timedOut, notFound });
4777
+ resolve9({ code, stdout, stderr, timedOut, notFound });
4250
4778
  };
4251
4779
  child.stdout.on("data", (chunk) => {
4252
4780
  stdout += chunk.toString();
@@ -4328,11 +4856,11 @@ async function sweepStaleStaging(tmpRoot = tmpdir3(), now = Date.now()) {
4328
4856
  }
4329
4857
  for (const n of names) {
4330
4858
  if (!n.startsWith(STAGING_PREFIX)) continue;
4331
- const dir = join17(tmpRoot, n);
4859
+ const dir = join19(tmpRoot, n);
4332
4860
  try {
4333
4861
  const st = await lstat2(dir);
4334
4862
  if (st.isSymbolicLink() || !st.isDirectory()) continue;
4335
- const marker = await lstat2(join17(dir, STAGING_MARKER));
4863
+ const marker = await lstat2(join19(dir, STAGING_MARKER));
4336
4864
  if (!marker.isFile() || now - marker.mtimeMs < STALE_AFTER_MS) continue;
4337
4865
  await rm(dir, { recursive: true, force: true });
4338
4866
  removed.push(dir);
@@ -4369,17 +4897,17 @@ async function runDockerScenario(input) {
4369
4897
  const binary = input.dockerBinary ?? "docker";
4370
4898
  const tmpRoot = input.tmpRoot ?? tmpdir3();
4371
4899
  await sweepStaleStaging(tmpRoot);
4372
- const temp = await mkdtemp(join17(tmpRoot, STAGING_PREFIX));
4373
- const staged = join17(temp, "repo");
4900
+ const temp = await mkdtemp(join19(tmpRoot, STAGING_PREFIX));
4901
+ const staged = join19(temp, "repo");
4374
4902
  const name = `replume-${randomUUID()}`;
4375
4903
  active.set(temp, { binary, name });
4376
4904
  attachSignals();
4377
4905
  try {
4378
- await writeFile(join17(temp, STAGING_MARKER), JSON.stringify({ by: "replume", pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() }) + "\n");
4906
+ await writeFile(join19(temp, STAGING_MARKER), JSON.stringify({ by: "replume", pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() }) + "\n");
4379
4907
  await cp(input.root, staged, { recursive: true, filter: (path) => basename2(path) !== ".git" });
4380
4908
  if (process.getuid?.() === 0) await chmod(temp, 493);
4381
4909
  const args = buildDockerRunArgs({ name, image: input.image ?? DEFAULT_DOCKER_IMAGE, source: staged, command: input.command, labels: input.labels });
4382
- return await new Promise((resolve7) => {
4910
+ return await new Promise((resolve9) => {
4383
4911
  const child = spawn2(binary, args, { env: cliEnv(), stdio: ["ignore", "pipe", "pipe"] });
4384
4912
  const marker = `${commandStartMarker(name)}
4385
4913
  `;
@@ -4389,7 +4917,7 @@ async function runDockerScenario(input) {
4389
4917
  if (settled) return;
4390
4918
  settled = true;
4391
4919
  clearTimeout(timer);
4392
- resolve7({ stdout, stderr, exitCode, timedOut, durationMs: Math.round(performance.now() - started) });
4920
+ resolve9({ stdout, stderr, exitCode, timedOut, durationMs: Math.round(performance.now() - started) });
4393
4921
  };
4394
4922
  const expire = (note) => async () => {
4395
4923
  timedOut = true;
@@ -4435,20 +4963,20 @@ Docker container did not start the scenario command within ${startup}s (start-up
4435
4963
  }
4436
4964
 
4437
4965
  // src/sandbox/index.ts
4438
- import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
4439
- import { execFileSync as execFileSync9 } from "child_process";
4966
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
4967
+ import { execFileSync as execFileSync10 } from "child_process";
4440
4968
  import { lstat as lstat3, mkdir, writeFile as writeFile2 } from "fs/promises";
4441
- import { isAbsolute as isAbsolute7, join as join20, relative as relative6, resolve as resolve5, sep as sep3 } from "path";
4969
+ import { isAbsolute as isAbsolute7, join as join22, relative as relative6, resolve as resolve7, sep as sep3 } from "path";
4442
4970
  import { performance as performance3 } from "perf_hooks";
4443
4971
 
4444
4972
  // src/sandbox/working-copy.ts
4445
4973
  import { spawn as spawn3 } from "child_process";
4446
4974
  import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
4447
4975
  import { tmpdir as tmpdir4 } from "os";
4448
- import { join as join18 } from "path";
4976
+ import { join as join20 } from "path";
4449
4977
  import { performance as performance2 } from "perf_hooks";
4450
4978
  async function git(cwd, args) {
4451
- return new Promise((resolve7, reject) => {
4979
+ return new Promise((resolve9, reject) => {
4452
4980
  const child = spawn3("git", args, { cwd, env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, stdio: ["ignore", "pipe", "pipe"] });
4453
4981
  let out = "", err = "";
4454
4982
  child.stdout.on("data", (v) => {
@@ -4458,13 +4986,13 @@ async function git(cwd, args) {
4458
4986
  err += v.toString();
4459
4987
  });
4460
4988
  child.on("error", reject);
4461
- child.on("close", (code) => code === 0 ? resolve7(out.trim()) : reject(new Error(`git ${args.join(" ")}: ${err.trim() || `exit ${code}`}`)));
4989
+ child.on("close", (code) => code === 0 ? resolve9(out.trim()) : reject(new Error(`git ${args.join(" ")}: ${err.trim() || `exit ${code}`}`)));
4462
4990
  });
4463
4991
  }
4464
4992
  async function withWorkingCopy(repo, commit, keep, fn) {
4465
- const temp = await mkdtemp2(join18(tmpdir4(), "replume-copy-"));
4466
- const root = join18(temp, "repo");
4467
- const home = join18(temp, "home");
4993
+ const temp = await mkdtemp2(join20(tmpdir4(), "replume-copy-"));
4994
+ const root = join20(temp, "repo");
4995
+ const home = join20(temp, "home");
4468
4996
  let added = false;
4469
4997
  try {
4470
4998
  await git(repo, ["worktree", "add", "--detach", root, commit]);
@@ -4484,14 +5012,14 @@ async function withWorkingCopy(repo, commit, keep, fn) {
4484
5012
  }
4485
5013
  async function runCommand(command, cwd, home, timeoutSec) {
4486
5014
  const started = performance2.now();
4487
- return new Promise((resolve7) => {
5015
+ return new Promise((resolve9) => {
4488
5016
  const child = spawn3(command, { cwd, shell: true, detached: process.platform !== "win32", env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: home }, stdio: ["ignore", "pipe", "pipe"] });
4489
5017
  let stdout = "", stderr = "", timedOut = false, settled = false;
4490
5018
  const finish = (exitCode) => {
4491
5019
  if (settled) return;
4492
5020
  settled = true;
4493
5021
  clearTimeout(timer);
4494
- resolve7({ stdout, stderr, exitCode, durationMs: Math.round(performance2.now() - started), timedOut });
5022
+ resolve9({ stdout, stderr, exitCode, durationMs: Math.round(performance2.now() - started), timedOut });
4495
5023
  };
4496
5024
  child.stdout.on("data", (v) => {
4497
5025
  stdout += v.toString();
@@ -4708,15 +5236,15 @@ function parsePackageJson(text) {
4708
5236
  return { error: "not valid JSON" };
4709
5237
  }
4710
5238
  if (!j || typeof j !== "object" || Array.isArray(j)) return { error: "not a package manifest" };
4711
- const obj2 = j;
5239
+ const obj3 = j;
4712
5240
  for (const s of NPM_SECTIONS) {
4713
- if (!(s in obj2) || obj2[s] == null) continue;
4714
- const sec = obj2[s];
5241
+ if (!(s in obj3) || obj3[s] == null) continue;
5242
+ const sec = obj3[s];
4715
5243
  if (typeof sec !== "object" || Array.isArray(sec)) return { error: `${s} is not an object` };
4716
5244
  }
4717
5245
  const entries = [];
4718
5246
  for (const s of NPM_SECTIONS) {
4719
- const sec = obj2[s];
5247
+ const sec = obj3[s];
4720
5248
  if (sec && typeof sec === "object" && !Array.isArray(sec)) {
4721
5249
  for (const name of Object.keys(sec)) entries.push({ name, section: s });
4722
5250
  }
@@ -4929,29 +5457,29 @@ function failingTestNames(output) {
4929
5457
  }
4930
5458
 
4931
5459
  // src/core/acceptance.ts
4932
- import { appendFileSync as appendFileSync5, existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
4933
- import { join as join19 } from "path";
4934
- var file = (root) => join19(root, ".replume", "acceptance.jsonl");
5460
+ import { appendFileSync as appendFileSync5, existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
5461
+ import { join as join21 } from "path";
5462
+ var file = (root) => join21(root, ".replume", "acceptance.jsonl");
4935
5463
  function recordAcceptance(repoRoot2, opts) {
4936
5464
  if (opts.verdict !== "pass") throw new Error("accept requires a fresh verify PASS");
4937
5465
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(opts.taskId) || !/^[0-9a-f]{40}$/.test(opts.commit)) throw new Error("invalid task ID or commit");
4938
5466
  const existing = readAcceptance(repoRoot2, opts.taskId, opts.commit);
4939
5467
  if (existing) return { ...existing, alreadyAccepted: true };
4940
- const dir = join19(repoRoot2, ".replume");
4941
- mkdirSync11(dir, { recursive: true });
4942
- const ignore = join19(dir, ".gitignore");
4943
- const prior = existsSync14(ignore) ? readFileSync14(ignore, "utf8") : "";
4944
- if (!prior.split(/\r?\n/).includes("acceptance.jsonl")) writeFileSync10(ignore, `${prior}${prior && !prior.endsWith("\n") ? "\n" : ""}acceptance.jsonl
5468
+ const dir = join21(repoRoot2, ".replume");
5469
+ mkdirSync12(dir, { recursive: true });
5470
+ const ignore = join21(dir, ".gitignore");
5471
+ const prior = existsSync16(ignore) ? readFileSync16(ignore, "utf8") : "";
5472
+ if (!prior.split(/\r?\n/).includes("acceptance.jsonl")) writeFileSync11(ignore, `${prior}${prior && !prior.endsWith("\n") ? "\n" : ""}acceptance.jsonl
4945
5473
  `);
4946
5474
  const entry = { taskId: opts.taskId, commit: opts.commit, acceptedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString() };
4947
5475
  appendFileSync5(file(repoRoot2), JSON.stringify(entry) + "\n");
4948
5476
  return { ...entry, alreadyAccepted: false };
4949
5477
  }
4950
5478
  function readAcceptance(repoRoot2, taskId, commit) {
4951
- if (!existsSync14(file(repoRoot2))) return null;
5479
+ if (!existsSync16(file(repoRoot2))) return null;
4952
5480
  let content;
4953
5481
  try {
4954
- content = readFileSync14(file(repoRoot2), "utf8");
5482
+ content = readFileSync16(file(repoRoot2), "utf8");
4955
5483
  } catch {
4956
5484
  return null;
4957
5485
  }
@@ -5080,15 +5608,15 @@ async function isLink(p) {
5080
5608
  }
5081
5609
  }
5082
5610
  function safeRelative(root, path) {
5083
- const full = resolve5(root, path);
5611
+ const full = resolve7(root, path);
5084
5612
  if (full !== root && !full.startsWith(root + "/")) throw new Error(`path outside repository: ${path}`);
5085
5613
  return full;
5086
5614
  }
5087
5615
  function projectHooksConfigured(root) {
5088
- for (const rel of [".claude/settings.json", ".cursor/hooks.json"]) {
5616
+ for (const rel of [".claude/settings.json", ".cursor/hooks.json", ".codex/hooks.json"]) {
5089
5617
  try {
5090
- const settings = JSON.parse(readFileSync15(join20(root, rel), "utf8"));
5091
- if (JSON.stringify(settings.hooks ?? {}).includes("replume hook claude") || JSON.stringify(settings.hooks ?? {}).includes("replume hook cursor")) return true;
5618
+ const settings = JSON.parse(readFileSync17(join22(root, rel), "utf8"));
5619
+ if (["claude", "cursor", "codex"].some((agent) => JSON.stringify(settings.hooks ?? {}).includes(`replume hook ${agent}`))) return true;
5092
5620
  } catch {
5093
5621
  }
5094
5622
  }
@@ -5177,7 +5705,7 @@ var showRef = (t) => `${/^[0-9a-f]{40}$/.test(t.ref) ? t.ref.slice(0, 7) : t.ref
5177
5705
  async function compareToTrustedBase(repo, t, commit, contractRel, scenariosRel, contractFile) {
5178
5706
  const where = `the trusted base ${showRef(t)}`;
5179
5707
  const out = [];
5180
- const local = existsSync15(contractFile) ? readFileSync15(contractFile, "utf-8").trim() : null;
5708
+ const local = existsSync17(contractFile) ? readFileSync17(contractFile, "utf-8").trim() : null;
5181
5709
  if (local !== t.contractText.trim())
5182
5710
  out.push({ rule: "policy_differs_from_trusted_base", path: contractRel, detail: local === null ? `contract missing in the working tree; ${where} has one` : `working-tree contract differs from the one at ${where}; the contract at the ref was used as the policy` });
5183
5711
  const blobs = async (rev) => new Map(
@@ -5209,7 +5737,7 @@ function observedChanges(events, changedPaths, since) {
5209
5737
  return { sessions: new Set(inTask.map((e) => e.sessionId)).size, namedPaths: changedPaths.filter((p) => named.has(p)) };
5210
5738
  }
5211
5739
  async function runVerify2(opts) {
5212
- const repo = resolve5(opts.repoPath);
5740
+ const repo = resolve7(opts.repoPath);
5213
5741
  if (!isAbsolute7(opts.repoPath)) throw new Error("repoPath must be absolute");
5214
5742
  const startedAt = (/* @__PURE__ */ new Date()).toISOString(), started = performance3.now();
5215
5743
  const image = opts.dockerImage ?? DEFAULT_DOCKER_IMAGE;
@@ -5232,9 +5760,9 @@ async function runVerify2(opts) {
5232
5760
  }
5233
5761
  const loaded = trusted ? parseContract(trusted.contractText, `${trusted.ref}:${contractRel}`) : loadContract(contractFile);
5234
5762
  if (!loaded) {
5235
- const example = join20(repo, ".replume/contract.example.yaml");
5763
+ const example = join22(repo, ".replume/contract.example.yaml");
5236
5764
  throw new Error(
5237
- `no task contract at ${contractFile}; write one with: replume task "<what you asked the agent>" --allow "<glob>"` + (existsSync15(example) ? ", or copy .replume/contract.example.yaml to .replume/contract.yaml" : "")
5765
+ `no task contract at ${contractFile}; write one with: replume task "<what you asked the agent>" --allow "<glob>"` + (existsSync17(example) ? ", or copy .replume/contract.example.yaml to .replume/contract.yaml" : "")
5238
5766
  );
5239
5767
  }
5240
5768
  const draft = isDraft(loaded);
@@ -5289,7 +5817,7 @@ async function runVerify2(opts) {
5289
5817
  const entries = await diffEntries(repo, base3, commit);
5290
5818
  const setup = /* @__PURE__ */ new Set();
5291
5819
  if (baseline?.base_commit === base3) {
5292
- const committed = (p) => execFileSync9("git", ["show", `${commit}:${p}`], { cwd: repo });
5820
+ const committed = (p) => execFileSync10("git", ["show", `${commit}:${p}`], { cwd: repo });
5293
5821
  for (const e of entries) {
5294
5822
  if (e.status !== "A" && e.status !== "M" || e.oldPath) continue;
5295
5823
  if (e.path === contractRel && sha256(committed(e.path)) === baseline.policy_hash) setup.add(e.path);
@@ -5308,7 +5836,7 @@ async function runVerify2(opts) {
5308
5836
  const report = await withWorkingCopy(repo, commit, mode === "worktree" && (opts.keepWorkdir ?? false), async (root, home) => {
5309
5837
  const run = (command, timeoutSec) => mode === "docker" ? runDockerScenario({ root, command, timeoutSec, image }) : runCommand(command, root, home, timeoutSec);
5310
5838
  const dir = safeRelative(root, opts.scenariosDir ?? ".replume/scenarios");
5311
- const scenarios = existsSync15(dir) || await isLink(dir) ? await loadScenarios(dir, root) : [];
5839
+ const scenarios = existsSync17(dir) || await isLink(dir) ? await loadScenarios(dir, root) : [];
5312
5840
  if (scenarios.some((s) => s.id === TEST_COMMAND_ID)) throw new Error(`scenario id "${TEST_COMMAND_ID}" is reserved for the contract's test_command`);
5313
5841
  const results = [];
5314
5842
  for (const scenario of scenarios) results.push(await execute(scenario, run, opts.timeoutSecDefault ?? 60));
@@ -5336,7 +5864,7 @@ async function runVerify2(opts) {
5336
5864
  }
5337
5865
  const baselineFindings = trusted ? await compareToTrustedBase(repo, trusted, commit, contractRel, scenariosRel, contractFile) : baseline ? compareToBaseline(
5338
5866
  baseline,
5339
- { policyHash: sha256(readFileSync15(contractFile)), scenarioHashes: hashTree(dir) },
5867
+ { policyHash: sha256(readFileSync17(contractFile)), scenarioHashes: hashTree(dir) },
5340
5868
  { contract: opts.contractPath ?? ".replume/contract.yaml", scenariosDir: opts.scenariosDir ?? ".replume/scenarios" }
5341
5869
  ) : [];
5342
5870
  return buildReport({
@@ -5398,9 +5926,9 @@ async function runVerify2(opts) {
5398
5926
  }
5399
5927
  }
5400
5928
  const safe = redactDeep(report, Number.POSITIVE_INFINITY);
5401
- const reportsDir = join20(repo, ".replume/reports");
5929
+ const reportsDir = join22(repo, ".replume/reports");
5402
5930
  await mkdir(reportsDir, { recursive: true });
5403
- await writeFile2(join20(reportsDir, `${commit}.json`), JSON.stringify(safe, null, 2) + "\n");
5931
+ await writeFile2(join22(reportsDir, `${commit}.json`), JSON.stringify(safe, null, 2) + "\n");
5404
5932
  return safe;
5405
5933
  }
5406
5934
  function isolationFooter(report) {
@@ -5533,8 +6061,8 @@ async function runPreflight(opts) {
5533
6061
  const { mode, reason } = await resolveIsolation(opts.isolation ?? "auto", image);
5534
6062
  const results = await withWorkingCopy(opts.repoPath, opts.commit, false, async (root, home) => {
5535
6063
  const run = (command, t) => mode === "docker" ? runDockerScenario({ root, command, timeoutSec: t, image }) : runCommand(command, root, home, t);
5536
- const dir = join21(root, opts.scenariosDir ?? ".replume/scenarios");
5537
- const scenarios = existsSync16(dir) ? await loadScenarios(dir, root).catch(() => []) : [];
6064
+ const dir = join23(root, opts.scenariosDir ?? ".replume/scenarios");
6065
+ const scenarios = existsSync18(dir) ? await loadScenarios(dir, root).catch(() => []) : [];
5538
6066
  const out = [];
5539
6067
  for (const id of c.must_check) {
5540
6068
  const s = scenarios.find((x) => x.id === id);
@@ -5590,7 +6118,7 @@ function comparePreflight(p, now) {
5590
6118
  }
5591
6119
 
5592
6120
  // src/core/watch.ts
5593
- import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync2, watchFile, unwatchFile } from "fs";
6121
+ import { existsSync as existsSync19, readFileSync as readFileSync18, statSync as statSync2, watchFile, unwatchFile } from "fs";
5594
6122
  function formatEvent(e) {
5595
6123
  const time = e.ts.slice(11, 19);
5596
6124
  const action = e.action && e.action !== "ok" ? e.action.toUpperCase() : "";
@@ -5601,11 +6129,11 @@ ${" ".repeat(19)}${e.message.split("\n")[0]}`;
5601
6129
  return head;
5602
6130
  }
5603
6131
  function readNew(path, offset) {
5604
- if (!existsSync17(path)) return { lines: [], offset, bad: 0 };
6132
+ if (!existsSync19(path)) return { lines: [], offset, bad: 0 };
5605
6133
  const size = statSync2(path).size;
5606
6134
  if (size < offset) offset = 0;
5607
6135
  if (size === offset) return { lines: [], offset, bad: 0 };
5608
- const buf = readFileSync16(path);
6136
+ const buf = readFileSync18(path);
5609
6137
  const chunk = buf.subarray(offset, size).toString("utf-8");
5610
6138
  const lastNl = chunk.lastIndexOf("\n");
5611
6139
  if (lastNl < 0) return { lines: [], offset, bad: 0 };
@@ -5629,7 +6157,7 @@ function watchLedger(path, print, opts = {}) {
5629
6157
  }
5630
6158
 
5631
6159
  // src/core/replay.ts
5632
- import { isAbsolute as isAbsolute8, join as join22 } from "path";
6160
+ import { isAbsolute as isAbsolute8, join as join24 } from "path";
5633
6161
  var REPLAY_ROOT = "/replay-repo";
5634
6162
  var REPLAY_BANNER = "RECORDED SESSION (replay). These are recorded events run through Replume's rules now.\nThis shows what the rules decide on these events; it does not show a live agent being steered.";
5635
6163
  function kindOf(e) {
@@ -5647,10 +6175,10 @@ function actionFromEvent(e) {
5647
6175
  for (const k of ["file_path", "notebook_path", "path"]) {
5648
6176
  const v = input[k];
5649
6177
  if (typeof v !== "string") continue;
5650
- if (!isAbsolute8(v)) input[k] = join22(REPLAY_ROOT, v);
6178
+ if (!isAbsolute8(v)) input[k] = join24(REPLAY_ROOT, v);
5651
6179
  else {
5652
6180
  const rel = e.paths.find((p) => v.endsWith(`/${p}`));
5653
- if (rel) input[k] = join22(REPLAY_ROOT, rel);
6181
+ if (rel) input[k] = join24(REPLAY_ROOT, rel);
5654
6182
  }
5655
6183
  }
5656
6184
  return toProposedAction(
@@ -5666,7 +6194,7 @@ function actionFromEvent(e) {
5666
6194
  });
5667
6195
  }
5668
6196
  const kind = kindOf(e);
5669
- const { inside, outside } = splitPaths(REPLAY_ROOT, e.paths.map((p) => isAbsolute8(p) ? p : join22(REPLAY_ROOT, p)));
6197
+ const { inside, outside } = splitPaths(REPLAY_ROOT, e.paths.map((p) => isAbsolute8(p) ? p : join24(REPLAY_ROOT, p)));
5670
6198
  const rawEdit = raw3;
5671
6199
  const before = rawEdit.before ?? rawEdit.old_string;
5672
6200
  const after = rawEdit.after ?? rawEdit.new_string;
@@ -5764,17 +6292,17 @@ var builtInEvents = JSON.stringify({
5764
6292
 
5765
6293
  // src/cli.ts
5766
6294
  function readStdin() {
5767
- return new Promise((resolve7, reject) => {
6295
+ return new Promise((resolve9, reject) => {
5768
6296
  let data = "";
5769
6297
  process.stdin.setEncoding("utf-8");
5770
6298
  process.stdin.on("data", (chunk) => data += chunk);
5771
- process.stdin.on("end", () => resolve7(data));
6299
+ process.stdin.on("end", () => resolve9(data));
5772
6300
  process.stdin.on("error", reject);
5773
6301
  });
5774
6302
  }
5775
6303
  function repoRoot() {
5776
6304
  try {
5777
- return execFileSync10("git", ["rev-parse", "--show-toplevel"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6305
+ return execFileSync11("git", ["rev-parse", "--show-toplevel"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
5778
6306
  } catch {
5779
6307
  return process.cwd();
5780
6308
  }
@@ -5820,16 +6348,16 @@ usageCommand.command("import-cursor").requiredOption("--result <path>", "local C
5820
6348
  });
5821
6349
  program.name("replume").description("Verify what your AI coding agent actually did").version(package_default.version);
5822
6350
  program.addHelpText("after", "\nOff-machine options: verify --legacy-judge sends the full git diff, task text, and agent action summary to api.openai.com; --post-github-comment posts task text to the PR on api.github.com; --check-registry sends package names to npm/PyPI. HTTP layer 2 is off unless REPLUME_JUDGE=shadow|on and REPLUME_JUDGE_PROVIDER=http with REPLUME_JUDGE_URL; HTTP needs REPLUME_JUDGE_ALLOW_INSECURE_HTTP=1.\n");
5823
- program.command("init").description("Create .replume/, install agent hooks where supported, print the capability matrix").option("--hook-command <cmd>", "command Claude Code should run for hooks", void 0).option("--cursor", "install Cursor project hooks even when .cursor/ is absent").option("--cursor-hook-command <cmd>", "command Cursor should run for hooks").option("--remove-cursor-hooks", "restore the Cursor hook config from before Replume init").action((opts) => {
6351
+ program.command("init").description("Create .replume/, install agent hooks where supported, print the capability matrix").option("--hook-command <cmd>", "command Claude Code should run for hooks", void 0).option("--cursor", "install Cursor project hooks even when .cursor/ is absent").option("--codex", "install Codex project hooks even when .codex/ is absent").option("--cursor-hook-command <cmd>", "command Cursor should run for hooks").option("--remove-cursor-hooks", "restore the Cursor hook config from before Replume init").action((opts) => {
5824
6352
  try {
5825
6353
  const root = repoRoot();
5826
6354
  if (opts.removeCursorHooks) {
5827
- const result = uninstallCursorHooks(root);
5828
- console.log(result.restored ? result.backupRetained ? `Cursor hooks removed where still identifiable; edited settings kept at ${result.path}; original backup retained beside it` : `Cursor hooks removed; previous configuration restored at ${result.path}` : "No Replume Cursor installation found");
6355
+ const result2 = uninstallCursorHooks(root);
6356
+ console.log(result2.restored ? result2.backupRetained ? `Cursor hooks removed where still identifiable; edited settings kept at ${result2.path}; original backup retained beside it` : `Cursor hooks removed; previous configuration restored at ${result2.path}` : "No Replume Cursor installation found");
5829
6357
  return;
5830
6358
  }
5831
6359
  const detected = detectAgents(root);
5832
- const cursorDir = join23(root, ".cursor");
6360
+ const cursorDir = join25(root, ".cursor");
5833
6361
  let cursorEntry = null;
5834
6362
  try {
5835
6363
  cursorEntry = lstatSync3(cursorDir);
@@ -5839,12 +6367,23 @@ program.command("init").description("Create .replume/, install agent hooks where
5839
6367
  if (opts.cursor && cursorEntry && !cursorEntry.isDirectory())
5840
6368
  throw new Error(`${cursorDir} must be a directory; nothing was changed.`);
5841
6369
  const wantCursor = Boolean(opts.cursor || cursorEntry?.isDirectory());
6370
+ const codexDir = join25(root, ".codex");
6371
+ let codexEntry = null;
6372
+ try {
6373
+ codexEntry = lstatSync3(codexDir);
6374
+ } catch (e) {
6375
+ if (e.code !== "ENOENT") throw e;
6376
+ }
6377
+ if (opts.codex && codexEntry && !codexEntry.isDirectory())
6378
+ throw new Error(`${codexDir} must be a directory; nothing was changed.`);
6379
+ const wantCodex = Boolean(opts.codex || codexEntry?.isDirectory());
5842
6380
  if (wantCursor) validateCursorHooksConfig(root, { command: opts.cursorHookCommand });
5843
- if (detected["Claude Code"] && existsSync18(claudeSettingsPath(root))) {
6381
+ if (wantCodex) validateCodexHooks(root);
6382
+ if (detected["Claude Code"] && existsSync20(claudeSettingsPath(root))) {
5844
6383
  const path = claudeSettingsPath(root);
5845
6384
  let parsed;
5846
6385
  try {
5847
- parsed = JSON.parse(readFileSync17(path, "utf8"));
6386
+ parsed = JSON.parse(readFileSync19(path, "utf8"));
5848
6387
  } catch {
5849
6388
  throw new Error(`${path} is not valid JSON; nothing was changed. Fix it and run replume init again.`);
5850
6389
  }
@@ -5856,15 +6395,24 @@ program.command("init").description("Create .replume/, install agent hooks where
5856
6395
  hookCommand: opts.hookCommand,
5857
6396
  cursorHookCommand: opts.cursorHookCommand,
5858
6397
  cursorHooks: wantCursor,
6398
+ codexHooks: wantCodex,
5859
6399
  detected
5860
6400
  });
5861
6401
  const oldNotice = lines.findIndex((l) => l.startsWith("- Cursor found, but"));
5862
6402
  if (oldNotice >= 0) lines.splice(oldNotice, 1);
6403
+ const codexNotice = lines.findIndex((l) => l.startsWith("- Codex found, but"));
6404
+ if (codexNotice >= 0) lines.splice(codexNotice, 1);
5863
6405
  const matrixAt = lines.indexOf("Capability matrix");
5864
6406
  lines[matrixAt + 1] = renderCapabilities(CAPABILITIES.map((c) => c.agent === "Cursor" ? {
5865
6407
  ...c,
5866
6408
  installedByInit: wantCursor
5867
- } : c), detected);
6409
+ } : c.agent === "Codex" ? { ...c, installedByInit: wantCodex } : c), detected);
6410
+ if (wantCodex) {
6411
+ const codex = installCodexHooks(root);
6412
+ lines.splice(matrixAt, 0, codex.added.length ? `\u2713 Codex hooks added to .codex/hooks.json: ${codex.added.join(", ")}; run /hooks in Codex to trust this definition` : "\u2713 Codex hooks already present; run /hooks in Codex if the definition changed");
6413
+ } else {
6414
+ lines.splice(matrixAt, 0, "- Codex project hooks not installed: run replume init --codex; otherwise Codex agent steps will not be recorded in the ledger");
6415
+ }
5868
6416
  if (wantCursor) {
5869
6417
  const cursor = installCursorHooks(root, { command: opts.cursorHookCommand });
5870
6418
  lines.splice(matrixAt, 0, cursor.added.length ? `\u2713 Cursor hooks added to .cursor/hooks.json: ${cursor.added.join(", ")}${cursor.backupPath ? ` (backup: ${cursor.backupPath})` : ""}` : "\u2713 Cursor hooks already present (nothing changed)");
@@ -5940,6 +6488,7 @@ addPreflightOptions(program.command("draft")).description("Propose a Task Contra
5940
6488
  console.log(serializeContract(c).trimEnd());
5941
6489
  console.log("");
5942
6490
  console.log(finalAllow);
6491
+ if (allowHasNoSource(c.allow)) console.log(` WARNING: ${ALLOW_NO_SOURCE_WARNING}`);
5943
6492
  if (opts.yes) await preflightAndRecord(root, path, c, opts);
5944
6493
  else console.log(" This draft is NOT policy yet: verify reports the contract as NOT CHECKED and the hooks ignore it. Review the file, then run `replume confirm`.");
5945
6494
  console.log(passPathHint(c));
@@ -5968,7 +6517,7 @@ addPreflightOptions(program.command("confirm")).description("Confirm the draft c
5968
6517
  });
5969
6518
  program.command("watch").description("Live view of .replume/events.jsonl with rule hits").option("--once", "print what is there and exit").action((opts) => {
5970
6519
  const path = ledgerPath(repoRoot());
5971
- if (!existsSync18(path)) console.log(`(no events yet at ${path}; waiting)`);
6520
+ if (!existsSync20(path)) console.log(`(no events yet at ${path}; waiting)`);
5972
6521
  watchLedger(path, (l) => console.log(l), { once: opts.once });
5973
6522
  });
5974
6523
  program.command("verify").description("Run the contract and scenarios on a commit in an isolated working copy (exit 0 pass, 1 fail, 2 incomplete)").option("--commit <sha>", "commit to verify", "HEAD").option("--base <sha>", "commit to diff against").option("--trusted-base <ref>", "git ref (commit or protected branch) whose contract and scenarios are the policy; also the diff base unless --base is given").option("--json", "print the report as JSON").option("--keep-workdir", "keep the isolated working copy for debugging (worktree mode only)").addOption(
@@ -5982,7 +6531,7 @@ program.command("verify").description("Run the contract and scenarios on a commi
5982
6531
  if (opts.checkRegistry) console.error("replume: opt-in registry check sends package names from dependency changes to registry.npmjs.org or pypi.org; no source file contents are sent by this lookup.");
5983
6532
  try {
5984
6533
  const root = repoRoot();
5985
- const commit = execFileSync10("git", ["rev-parse", "--verify", `${opts.commit}^{commit}`], { cwd: root }).toString().trim();
6534
+ const commit = execFileSync11("git", ["rev-parse", "--verify", `${opts.commit}^{commit}`], { cwd: root }).toString().trim();
5986
6535
  const report = await runVerify2({
5987
6536
  repoPath: root,
5988
6537
  commit,
@@ -6002,7 +6551,7 @@ program.command("verify").description("Run the contract and scenarios on a commi
6002
6551
  program.command("accept").description("Explicitly accept HEAD after a fresh verification PASS; only then count cost per accepted change").option("--base <sha>", "commit to diff against").addOption(new Option("--isolation <mode>", "verification mode").choices(["auto", "docker", "worktree"]).default("auto")).action(async (opts) => {
6003
6552
  const root = repoRoot();
6004
6553
  try {
6005
- const commit = execFileSync10("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: root, encoding: "utf8" }).trim();
6554
+ const commit = execFileSync11("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: root, encoding: "utf8" }).trim();
6006
6555
  const report = await runVerify2({ repoPath: root, commit, baseCommit: opts.base, isolation: opts.isolation });
6007
6556
  if (report.isolationMode === "worktree") console.error(WORKTREE_WARNING);
6008
6557
  if (report.verdict !== "pass") fail(`accept requires a fresh verify PASS; this commit is ${report.verdict.toUpperCase()}`);
@@ -6016,15 +6565,15 @@ program.command("report").description("Re-print the report saved for a commit (.
6016
6565
  const root = repoRoot();
6017
6566
  let commit;
6018
6567
  try {
6019
- commit = execFileSync10("git", ["rev-parse", "--verify", `${opts.commit}^{commit}`], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
6568
+ commit = execFileSync11("git", ["rev-parse", "--verify", `${opts.commit}^{commit}`], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
6020
6569
  } catch {
6021
6570
  fail(`not a commit: ${opts.commit}`);
6022
6571
  }
6023
- const file2 = join23(root, REPLUME_DIR, "reports", `${commit}.json`);
6024
- if (!existsSync18(file2)) fail(`no report for ${commit.slice(0, 7)} at ${file2}; run replume verify --commit ${opts.commit}`);
6572
+ const file2 = join25(root, REPLUME_DIR, "reports", `${commit}.json`);
6573
+ if (!existsSync20(file2)) fail(`no report for ${commit.slice(0, 7)} at ${file2}; run replume verify --commit ${opts.commit}`);
6025
6574
  let report;
6026
6575
  try {
6027
- report = JSON.parse(readFileSync17(file2, "utf-8"));
6576
+ report = JSON.parse(readFileSync19(file2, "utf-8"));
6028
6577
  } catch (e) {
6029
6578
  fail(`${file2} is not valid JSON: ${e.message}`);
6030
6579
  }
@@ -6048,25 +6597,25 @@ demo.option("--replay <file>", "run a recorded events.jsonl session through the
6048
6597
  console.log(renderReplay(replay(builtInEvents, builtInContract), builtInContract));
6049
6598
  return;
6050
6599
  }
6051
- const file2 = resolve6(opts.replay);
6052
- if (!existsSync18(file2)) fail(`replay file not found: ${file2}`);
6600
+ const file2 = resolve8(opts.replay);
6601
+ if (!existsSync20(file2)) fail(`replay file not found: ${file2}`);
6053
6602
  try {
6054
6603
  const sibling = file2.replace(/\.jsonl?$/, "") + ".contract.yaml";
6055
- const contractFile = opts.contract ? resolve6(opts.contract) : existsSync18(sibling) ? sibling : contractPath(repoRoot());
6604
+ const contractFile = opts.contract ? resolve8(opts.contract) : existsSync20(sibling) ? sibling : contractPath(repoRoot());
6056
6605
  const contract = loadContract(contractFile);
6057
- console.log(renderReplay(replay(readFileSync17(file2, "utf-8"), contract), contract, contract ? contractFile : void 0));
6606
+ console.log(renderReplay(replay(readFileSync19(file2, "utf-8"), contract), contract, contract ? contractFile : void 0));
6058
6607
  } catch (e) {
6059
6608
  fail(e.message);
6060
6609
  }
6061
6610
  });
6062
6611
  async function demoReplayHooks(path, contractOpt) {
6063
- const file2 = resolve6(path);
6064
- if (!existsSync18(file2)) fail(`replay file not found: ${file2}`);
6612
+ const file2 = resolve8(path);
6613
+ if (!existsSync20(file2)) fail(`replay file not found: ${file2}`);
6065
6614
  const sibling = file2.replace(/\.jsonl?$/, "") + ".contract.yaml";
6066
- const contractFile = contractOpt ? resolve6(contractOpt) : existsSync18(sibling) ? sibling : null;
6615
+ const contractFile = contractOpt ? resolve8(contractOpt) : existsSync20(sibling) ? sibling : null;
6067
6616
  try {
6068
- const r = await replayHooks(readFileSync17(file2, "utf-8"), {
6069
- contractYaml: contractFile ? readFileSync17(contractFile, "utf-8") : void 0,
6617
+ const r = await replayHooks(readFileSync19(file2, "utf-8"), {
6618
+ contractYaml: contractFile ? readFileSync19(contractFile, "utf-8") : void 0,
6070
6619
  env: process.env
6071
6620
  });
6072
6621
  console.log(REPLAY_HOOKS_BANNER);
@@ -6089,13 +6638,13 @@ program.command("install").description("[legacy, August MVP] Register SessionSta
6089
6638
  });
6090
6639
  async function legacyVerify(postGithubComment) {
6091
6640
  const repoDir = process.cwd();
6092
- const headSha = process.env.REPLUME_HEAD_SHA ?? execFileSync10("git", ["rev-parse", "HEAD"], { cwd: repoDir }).toString().trim();
6641
+ const headSha = process.env.REPLUME_HEAD_SHA ?? execFileSync11("git", ["rev-parse", "HEAD"], { cwd: repoDir }).toString().trim();
6093
6642
  const [owner, repo] = (process.env.GITHUB_REPOSITORY ?? "/").split("/");
6094
6643
  const prNumber = Number(process.env.REPLUME_PR_NUMBER ?? "0");
6095
6644
  const baseRef = process.env.REPLUME_BASE_REF ?? "main";
6096
6645
  let diff = "";
6097
6646
  try {
6098
- diff = execFileSync10("git", ["diff", `origin/${baseRef}...HEAD`], { cwd: repoDir }).toString();
6647
+ diff = execFileSync11("git", ["diff", `origin/${baseRef}...HEAD`], { cwd: repoDir }).toString();
6099
6648
  } catch (err) {
6100
6649
  console.warn(`replume: could not compute diff against origin/${baseRef}, continuing with empty diff:`, err);
6101
6650
  }
@@ -6114,6 +6663,12 @@ async function legacyVerify(postGithubComment) {
6114
6663
  console.log(`replume verdict: ${verdict}`);
6115
6664
  }
6116
6665
  var hook = program.command("hook").description("Internal: agent hook entry points");
6666
+ hook.command("codex").description("Codex CLI hook handler").action(async () => {
6667
+ const out = handleCodexHook(await readStdin());
6668
+ if (out.stdout) process.stdout.write(out.stdout);
6669
+ if (out.stderr) process.stderr.write(out.stderr);
6670
+ process.exitCode = out.exitCode;
6671
+ });
6117
6672
  hook.command("cursor").description("Cursor hook handler").action(async () => {
6118
6673
  const out = handleCursorHook(await readStdin());
6119
6674
  process.stdout.write(out.stdout);