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/README.md +2 -1
- package/THIRD-PARTY-NOTICES.md +28 -0
- package/dist/cli.js +862 -307
- package/package.json +1 -1
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.
|
|
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
|
|
52
|
-
import { existsSync as
|
|
53
|
-
import { join as
|
|
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
|
|
355
|
+
const str4 = (s) => redactLedgerText(s, repoRoot2);
|
|
355
356
|
const deep = (v) => {
|
|
356
|
-
if (typeof v === "string") return
|
|
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]) => [
|
|
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
|
|
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
|
|
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(
|
|
1054
|
+
this.entry(obj3, m[1], m[2], line, indent);
|
|
1054
1055
|
}
|
|
1055
|
-
return
|
|
1056
|
+
return obj3;
|
|
1056
1057
|
}
|
|
1057
|
-
entry(
|
|
1058
|
-
if (key in
|
|
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
|
-
|
|
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)
|
|
1065
|
-
else if (next && next.indent === indent && ITEM.test(next.body))
|
|
1066
|
-
else
|
|
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
|
|
1086
|
-
this.entry(
|
|
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:
|
|
1089
|
-
items.push(
|
|
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(
|
|
1117
|
+
function stringifyYaml(obj3, comments = {}) {
|
|
1117
1118
|
const lines = [];
|
|
1118
|
-
for (const [k, v] of Object.entries(
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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 <
|
|
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 (
|
|
1592
|
-
|
|
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
|
|
1597
|
-
if (SHELLS.has(verb) && depth <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1623
|
-
|
|
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]
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
if (
|
|
1633
|
-
if (
|
|
1634
|
-
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
if (
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
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)
|
|
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 <
|
|
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,
|
|
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(
|
|
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
|
|
1869
|
-
for (
|
|
1870
|
-
|
|
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
|
|
1874
|
-
if (
|
|
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
|
-
|
|
2118
|
+
result2.push({ targets: splitFlags(words.slice(1)).operands, gitClean: false });
|
|
1878
2119
|
} else if (verb === "find" && words.includes("-delete")) {
|
|
1879
|
-
|
|
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
|
-
|
|
2127
|
+
result2.push({ targets: [...cwd ? [cwd] : [], ...paths], gitClean: true });
|
|
1887
2128
|
}
|
|
1888
2129
|
}
|
|
1889
|
-
return
|
|
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
|
-
|
|
1893
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
3092
|
+
const file2 = join9(".replume", "scenarios", `${d.scenario}.yaml`);
|
|
2796
3093
|
let run = null;
|
|
2797
3094
|
try {
|
|
2798
|
-
if (existsSync7(
|
|
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.@+-]*)
|
|
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) =>
|
|
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((
|
|
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 =
|
|
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(
|
|
3032
|
-
message:
|
|
3334
|
+
action: ledgerAction(result2.decision),
|
|
3335
|
+
message: result2.message,
|
|
3033
3336
|
raw: {
|
|
3034
3337
|
fingerprint: action.fingerprint,
|
|
3035
|
-
decision:
|
|
3036
|
-
layer:
|
|
3037
|
-
layer2Mode:
|
|
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,
|
|
3045
|
-
recordLayer2(rec,
|
|
3046
|
-
return { exitCode: 0, stdout: decisionOutput("PreToolUse",
|
|
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 =
|
|
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 =
|
|
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" :
|
|
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 ??
|
|
3397
|
+
message: message ?? result2.message,
|
|
3095
3398
|
raw: {
|
|
3096
3399
|
decision: stopDecision,
|
|
3097
|
-
layer:
|
|
3098
|
-
layer2Mode:
|
|
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,
|
|
3106
|
-
recordLayer2(rec,
|
|
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 =
|
|
3472
|
+
const dir = join10(repoRoot2 ?? env.CLAUDE_PROJECT_DIR ?? process.cwd(), REPLUME_DIR);
|
|
3170
3473
|
mkdirSync6(dir, { recursive: true });
|
|
3171
|
-
appendFileSync3(
|
|
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 = (
|
|
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:
|
|
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
|
|
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
|
|
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) =>
|
|
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/
|
|
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,
|
|
3552
|
-
import {
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
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 (
|
|
4117
|
+
if (existsSync11(statePath)) {
|
|
3600
4118
|
let state;
|
|
3601
4119
|
try {
|
|
3602
|
-
state = JSON.parse(
|
|
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 =
|
|
4132
|
+
const hadOriginal = existsSync11(path);
|
|
3615
4133
|
const command = opts.command ?? CURSOR_HOOK_COMMAND;
|
|
3616
|
-
const original = hadOriginal ?
|
|
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:
|
|
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
|
-
|
|
3631
|
-
if (!
|
|
3632
|
-
if (original !== null)
|
|
3633
|
-
|
|
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(
|
|
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
|
-
|
|
4157
|
+
writeFileSync6(statePath, JSON.stringify(state) + "\n");
|
|
3640
4158
|
}
|
|
3641
4159
|
const temp = `${path}.replume-tmp`;
|
|
3642
|
-
|
|
4160
|
+
writeFileSync6(temp, updated);
|
|
3643
4161
|
renameSync(temp, path);
|
|
3644
|
-
return { path, backupPath:
|
|
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 (!
|
|
3651
|
-
const state = JSON.parse(
|
|
3652
|
-
const current =
|
|
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 (!
|
|
3657
|
-
|
|
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
|
-
|
|
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 &&
|
|
4195
|
+
return { restored: true, path, backupRetained: !unchanged && existsSync11(backupPath) };
|
|
3678
4196
|
}
|
|
3679
4197
|
|
|
3680
4198
|
// src/agents/claude/install.ts
|
|
3681
|
-
import { copyFileSync as
|
|
3682
|
-
import { dirname as
|
|
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
|
|
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 (
|
|
3716
|
-
original =
|
|
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
|
-
|
|
4249
|
+
copyFileSync3(settingsPath, backupPath);
|
|
3732
4250
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
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
|
|
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
|
|
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(
|
|
4263
|
+
const repo = mkdtempSync(join15(tmpdir2(), "replume-replay-hooks-"));
|
|
3746
4264
|
try {
|
|
3747
|
-
|
|
3748
|
-
if (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
|
|
3779
|
-
import { existsSync as
|
|
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
|
|
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: "
|
|
3804
|
-
block: "
|
|
3805
|
-
steer: "
|
|
4321
|
+
observe: "verified live (CLI)",
|
|
4322
|
+
block: "verified live (CLI)",
|
|
4323
|
+
steer: "documented",
|
|
3806
4324
|
installedByInit: false,
|
|
3807
|
-
notes: "
|
|
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
|
|
3837
|
-
import { execFileSync as
|
|
3838
|
-
import { appendFileSync as appendFileSync4, existsSync as
|
|
3839
|
-
import { dirname as
|
|
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) =>
|
|
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 =
|
|
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(
|
|
4381
|
+
else if (st.isFile()) out[r] = sha256(readFileSync14(p));
|
|
3864
4382
|
}
|
|
3865
4383
|
};
|
|
3866
|
-
if (
|
|
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
|
|
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) =>
|
|
4399
|
+
var baselinePath = (contractPath2) => join16(dirname8(contractPath2), BASELINE_FILE);
|
|
3882
4400
|
function ensureBaselineIgnored(replumeDir) {
|
|
3883
|
-
const gi =
|
|
3884
|
-
if (!
|
|
3885
|
-
|
|
4401
|
+
const gi = join16(replumeDir, ".gitignore");
|
|
4402
|
+
if (!existsSync13(gi)) {
|
|
4403
|
+
writeFileSync9(gi, DEFAULT_REPLUME_GITIGNORE);
|
|
3886
4404
|
return;
|
|
3887
4405
|
}
|
|
3888
|
-
const text =
|
|
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(
|
|
3901
|
-
scenario_hashes: hashTree(opts.scenariosDir ??
|
|
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
|
-
|
|
3905
|
-
ensureBaselineIgnored(
|
|
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 (!
|
|
4444
|
+
if (!existsSync13(path)) return { status: "absent" };
|
|
3927
4445
|
let raw3;
|
|
3928
4446
|
try {
|
|
3929
|
-
raw3 = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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") ||
|
|
3985
|
-
Cursor: onPath("cursor") ||
|
|
3986
|
-
Codex: onPath("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 =
|
|
3992
|
-
|
|
3993
|
-
const gi =
|
|
3994
|
-
if (!
|
|
3995
|
-
|
|
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
|
|
4032
|
-
import { existsSync as
|
|
4033
|
-
import { join as
|
|
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
|
|
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
|
|
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) ||
|
|
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 =
|
|
4612
|
+
const path = join18(input.repoRoot, f);
|
|
4095
4613
|
try {
|
|
4096
4614
|
if (!lstatSync2(path).isFile() || lstatSync2(path).isSymbolicLink()) return [];
|
|
4097
|
-
const s = parseScenario(
|
|
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 =
|
|
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
|
|
4182
|
-
import { join as
|
|
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
|
|
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 =
|
|
4194
|
-
|
|
4195
|
-
|
|
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
|
|
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
|
|
4210
|
-
const r = await
|
|
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
|
|
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});
|
|
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
|
|
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);
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
4373
|
-
const staged =
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
4439
|
-
import { execFileSync as
|
|
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
|
|
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
|
|
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((
|
|
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 ?
|
|
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(
|
|
4466
|
-
const root =
|
|
4467
|
-
const 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((
|
|
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
|
-
|
|
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
|
|
5239
|
+
const obj3 = j;
|
|
4712
5240
|
for (const s of NPM_SECTIONS) {
|
|
4713
|
-
if (!(s in
|
|
4714
|
-
const sec =
|
|
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 =
|
|
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
|
|
4933
|
-
import { join as
|
|
4934
|
-
var file = (root) =>
|
|
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 =
|
|
4941
|
-
|
|
4942
|
-
const ignore =
|
|
4943
|
-
const prior =
|
|
4944
|
-
if (!prior.split(/\r?\n/).includes("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 (!
|
|
5479
|
+
if (!existsSync16(file(repoRoot2))) return null;
|
|
4952
5480
|
let content;
|
|
4953
5481
|
try {
|
|
4954
|
-
content =
|
|
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 =
|
|
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(
|
|
5091
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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>"` + (
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
5929
|
+
const reportsDir = join22(repo, ".replume/reports");
|
|
5402
5930
|
await mkdir(reportsDir, { recursive: true });
|
|
5403
|
-
await writeFile2(
|
|
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 =
|
|
5537
|
-
const scenarios =
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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] =
|
|
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] =
|
|
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 :
|
|
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((
|
|
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", () =>
|
|
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
|
|
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
|
|
5828
|
-
console.log(
|
|
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 =
|
|
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 (
|
|
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(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6024
|
-
if (!
|
|
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(
|
|
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 =
|
|
6052
|
-
if (!
|
|
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 ?
|
|
6604
|
+
const contractFile = opts.contract ? resolve8(opts.contract) : existsSync20(sibling) ? sibling : contractPath(repoRoot());
|
|
6056
6605
|
const contract = loadContract(contractFile);
|
|
6057
|
-
console.log(renderReplay(replay(
|
|
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 =
|
|
6064
|
-
if (!
|
|
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 ?
|
|
6615
|
+
const contractFile = contractOpt ? resolve8(contractOpt) : existsSync20(sibling) ? sibling : null;
|
|
6067
6616
|
try {
|
|
6068
|
-
const r = await replayHooks(
|
|
6069
|
-
contractYaml: contractFile ?
|
|
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 ??
|
|
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 =
|
|
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);
|