replume-cli 0.2.6 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1173 -573
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -358,14 +358,14 @@ function cliEnv() {
|
|
|
358
358
|
return { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: process.env.HOME, DOCKER_CONFIG: process.env.DOCKER_CONFIG };
|
|
359
359
|
}
|
|
360
360
|
function dockerCli(binary, args, timeoutMs) {
|
|
361
|
-
return new Promise((
|
|
361
|
+
return new Promise((resolve18) => {
|
|
362
362
|
const child = spawn(binary, args, { env: cliEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
363
363
|
let stdout = "", stderr = "", settled = false, timedOut2 = false, notFound = false;
|
|
364
364
|
const done = (code2) => {
|
|
365
365
|
if (settled) return;
|
|
366
366
|
settled = true;
|
|
367
367
|
clearTimeout(timer);
|
|
368
|
-
|
|
368
|
+
resolve18({ code: code2, stdout, stderr, timedOut: timedOut2, notFound });
|
|
369
369
|
};
|
|
370
370
|
child.stdout.on("data", (chunk) => {
|
|
371
371
|
stdout += chunk.toString();
|
|
@@ -503,7 +503,7 @@ async function runDockerScenario(input) {
|
|
|
503
503
|
await cp(input.root, staged, { recursive: true, filter: (path) => basename(path) !== ".git" });
|
|
504
504
|
if (process.getuid?.() === 0) await chmod(temp, 493);
|
|
505
505
|
const args = buildDockerRunArgs({ name, image: input.image ?? DEFAULT_DOCKER_IMAGE, source: staged, command: input.command, labels: input.labels, limits: input.limits });
|
|
506
|
-
return await new Promise((
|
|
506
|
+
return await new Promise((resolve18) => {
|
|
507
507
|
const child = spawn(binary, args, { env: cliEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
508
508
|
const marker = `${commandStartMarker(name)}
|
|
509
509
|
`;
|
|
@@ -516,7 +516,7 @@ async function runDockerScenario(input) {
|
|
|
516
516
|
const endLine = END_LINE(name).exec(stderr);
|
|
517
517
|
if (endLine) stderr = stderr.replace(endLine[0], "");
|
|
518
518
|
const oomKills = endLine ? Number(endLine[1] ?? 0) : null;
|
|
519
|
-
|
|
519
|
+
resolve18({ stdout, stderr, exitCode, timedOut: timedOut2, durationMs: Math.round(performance2.now() - started), oomKills });
|
|
520
520
|
};
|
|
521
521
|
const expire = (note) => async () => {
|
|
522
522
|
timedOut2 = true;
|
|
@@ -1397,7 +1397,7 @@ function commitGateMessage(s, testCommand, verbs) {
|
|
|
1397
1397
|
import { createHash as createHash3 } from "crypto";
|
|
1398
1398
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1399
1399
|
import { lstatSync as lstatSync3, readFileSync as readFileSync10, realpathSync, statSync } from "fs";
|
|
1400
|
-
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as
|
|
1400
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep } from "path";
|
|
1401
1401
|
import { homedir, tmpdir as tmpdir3 } from "os";
|
|
1402
1402
|
|
|
1403
1403
|
// src/core/glob.ts
|
|
@@ -1442,16 +1442,22 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync2, mkdtempSync, readFi
|
|
|
1442
1442
|
import { tmpdir as tmpdir2 } from "os";
|
|
1443
1443
|
import { join as join4, posix } from "path";
|
|
1444
1444
|
var MAX_ASKED = 5;
|
|
1445
|
-
var VERB = "rename[sd]?|renaming|move[sd]?|moving|update[sd]?|updating|change[sd]?|changing|edit(?:s|ed)?|modify|modif(?:ies|ied)|split|add(?:s|ed)?|replace[sd]?|rewrite|adjust|switch|reorgani[sz]e|regenerate|refresh|convert|extend|delete|remove";
|
|
1446
|
-
var OBJECT = "tests?|test files?|test cases?|snapshots?|specs?|assertions?|expectations?";
|
|
1447
|
-
var ASKS = new RegExp(`\\b(?:${VERB})\\b(?:[^.!?\\n]|\\.(?=\\S)){0,80}?\\b(?:${OBJECT})\\b(?!\\s+(?:pass|passes|passing|green|run|runs|succeed|succeeds|work))`, "i");
|
|
1445
|
+
var VERB = "rename[sd]?|renaming|move[sd]?|moving|update[sd]?|updating|change[sd]?|changing|edit(?:s|ed)?|modify|modif(?:ies|ied)|split|add(?:s|ed)?|replace[sd]?|replacing|rewrite[sn]?|rewrote|rewriting|adjust|switch|reorgani[sz]e|regenerate|refresh|convert|extend|delete|remove|revise[sd]?|revising|refactor(?:s|ed|ing)?|migrate[sd]?|migrating|port";
|
|
1446
|
+
var OBJECT = "tests?|test files?|test cases?|test suites?|snapshots?|specs?|assertions?|expectations?";
|
|
1447
|
+
var ASKS = new RegExp(`\\b(?:${VERB})\\b(?:[^.!?\\n]|\\.(?=\\S)){0,80}?\\b(?:${OBJECT})\\b(?!\\s+(?:(?:must|should|still|all|will|can|to|keep|keeps|continue|continues|need|needs)\\s+){0,3}(?:pass|passes|passing|green|run|runs|succeed|succeeds|work))`, "i");
|
|
1448
|
+
var DONE_FORM = "updated|updating|rewritten|rewriting|replaced|replacing|changed|changing|edited|editing|renamed|renaming|moved|moving|adjusted|adjusting|modified|modifying|revised|revising|migrated|migrating|change";
|
|
1449
|
+
var ASKS_PASSIVE = new RegExp(`\\b(?:${OBJECT})\\b(?:[^.!?\\n,;]){0,40}?\\b(?:must|should|need|needs|has to|have to|ought to|will|to)\\s+(?:also\\s+)?(?:be\\s+)?(?:${DONE_FORM})\\b`, "i");
|
|
1450
|
+
var CHANGE_VERB = "update[sd]?|updating|rewrite[sn]?|rewrote|rewriting|replace[sd]?|replacing|change[sd]?|changing|edit(?:s|ed)?|modify|adjust|revise[sd]?|revising|refactor(?:s|ed|ing)?|migrate[sd]?|migrating|port";
|
|
1451
|
+
var WHOLE_SUITE = new RegExp(`\\b(?:${CHANGE_VERB})\\b(?:\\s+(?:all|every|each|the|of|existing|its|their|our|affected|relevant|these|those|remaining))*\\s+(?:tests|test files|test suite)\\b(?!\\s+(?:that|which|for|of|in)\\b)|\\b(?:${CHANGE_VERB})\\b[^.!?\\n;]{0,60}?\\bin\\s+(?:all\\s+|every\\s+|each\\s+)?(?:of\\s+)?(?:the\\s+)?tests\\b|\\b(?:the\\s+|all\\s+|every\\s+)?tests\\s+(?:must|should|need|needs|have to)\\s+(?:also\\s+)?(?:be\\s+)?(?:${DONE_FORM})\\b`, "i");
|
|
1452
|
+
var FOLDER_TOKEN = /(?:^|[\s"'`(<[])((?:\.\/)?[\w@.-]+(?:\/[\w@.-]+)*\/?)(?=$|[\s"'`)>\],;:!?]|\.(?:\s|$))/g;
|
|
1448
1453
|
var VERB_ONLY = new RegExp(`\\b(?:${VERB})\\b`, "i");
|
|
1449
1454
|
var PATH = /(?:^|[\s"'`(<[])((?:\.{0,2}\/)?(?:[\w@.-]+\/)*[\w@.-]+\.[A-Za-z0-9]+)(?=$|[\s"'`)>\],;:!?]|\.(?:\s|$))/g;
|
|
1450
1455
|
var STOP = /* @__PURE__ */ new Set(["test", "tests", "file", "files", "case", "cases", "snapshot", "snapshots", "that", "this", "with", "from", "into", "each", "every", "where", "which", "should", "would", "rule", "both", "them", "they", "their", "what", "when", "make", "pass", "update", "change", "rename", "move", "split", "add", "and", "the", "for"]);
|
|
1451
1456
|
var RUNS_TESTS = /\b(?:run|runs|running|rerun|re-run|execute|executing)\s+(?:(?!and\b|then\b)\w+\s+){0,3}?(?:tests?|specs?)\b|\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?test\b/gi;
|
|
1452
1457
|
var NOUN_CHANGE = /\b(?:that|this|the|a|an|each|every|your|my|these|those|one|other)\s+(?:changes?|updates?|edits?|moves?|split|rename|replacement|switch|refresh|rewrite)\b(?!\s+(?:to|in|on|of)\b)/gi;
|
|
1453
1458
|
var NEW_TEST = /\b(?:add|adds|adding|create|creates|creating|write|writes|writing)\s+(?:a\s+|an\s+|some\s+)?new\s+(?:test|spec)(?:s|\s+files?|\s+cases?)?\b/gi;
|
|
1454
|
-
var
|
|
1459
|
+
var LEFT_ALONE = new RegExp(`\\b(?:leave|leaves|leaving|keep|keeps|keeping|without\\s+(?:changing|touching|updating|editing)|(?:do\\s+not|don['\u2019]?t|never|not)\\s+(?:\\w+\\s+){0,2}?(?:change|touch|edit|update|modify))\\b[^.!?\\n;,]{0,30}?\\b(?:${OBJECT})\\b`, "gi");
|
|
1460
|
+
var requestText = (s) => s.replace(LEFT_ALONE, " ").replace(NEW_TEST, " ").replace(RUNS_TESTS, " ").replace(NOUN_CHANGE, " ");
|
|
1455
1461
|
var CLAUSE_SPLIT = /(?:[;,:]|\s+but\s+|\s+and\s+then\s+|\s+then\s+|\s+instead\s+)/i;
|
|
1456
1462
|
var NEG = /\b(?:do\s+not|don['’]?t|dont|never|not|no\s+need\s+to|without|avoid|leave|keep)\b(?:\s+\w+){0,3}\s*$/i;
|
|
1457
1463
|
var FIRST_VERB = new RegExp(`\\b(?:${VERB})\\b`, "i");
|
|
@@ -1505,7 +1511,7 @@ function testEditsAskedFor(task, repoRoot2, files, rev) {
|
|
|
1505
1511
|
const before = prev;
|
|
1506
1512
|
prev = { named: pathsIn2(s).filter((p) => tests.includes(p)), negated };
|
|
1507
1513
|
const refersBack = !named.length && !!before && !before.negated && before.named.length > 0 && before.named.length <= MAX_ASKED && REFERS_BACK.test(request) && VERB_ONLY.test(request);
|
|
1508
|
-
const asks = ASKS.test(request) || named.length > 0 && VERB_ONLY.test(s) || refersBack;
|
|
1514
|
+
const asks = ASKS.test(request) || ASKS_PASSIVE.test(request) || named.length > 0 && VERB_ONLY.test(s) || refersBack;
|
|
1509
1515
|
if (!asks) continue;
|
|
1510
1516
|
const had = out.paths.length;
|
|
1511
1517
|
for (const p of named) add2(p, "the task names it");
|
|
@@ -1522,6 +1528,11 @@ function testEditsAskedFor(task, repoRoot2, files, rev) {
|
|
|
1522
1528
|
const src = imports(f).find((x) => sources2.includes(x));
|
|
1523
1529
|
if (src) add2(f, `it tests ${src}, which the task names`);
|
|
1524
1530
|
}
|
|
1531
|
+
if (new RegExp(`\\b(?:${OBJECT})\\b`, "i").test(request))
|
|
1532
|
+
for (const d of foldersNamedIn(task, all)) {
|
|
1533
|
+
const ref = new RegExp(`(?:^|[^\\w.-])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/|\\b${d.replace(/\//g, ".").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\w|\\bfrom\\s+${d.replace(/\//g, ".").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+import\\b|/${posix.basename(d).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`);
|
|
1534
|
+
for (const f of tests) if (ref.test(text(f))) add2(f, `it names ${d}, which the task names`);
|
|
1535
|
+
}
|
|
1525
1536
|
const words6 = (s.toLowerCase().match(/[a-z]{5,}/g) ?? []).filter((w) => !STOP.has(w));
|
|
1526
1537
|
if (/\btests?\b/i.test(request) && words6.length)
|
|
1527
1538
|
for (const f of tests) {
|
|
@@ -1537,10 +1548,89 @@ function testEditsAskedFor(task, repoRoot2, files, rev) {
|
|
|
1537
1548
|
}
|
|
1538
1549
|
if (out.paths.length === had && refersBack)
|
|
1539
1550
|
for (const p of before.named) add2(p, `the task names it in the sentence before "${whole2.slice(0, 40)}"`);
|
|
1551
|
+
if (!out.paths.length && WHOLE_SUITE.test(request) && tests.length <= MAX_ASKED)
|
|
1552
|
+
for (const f of tests) add2(f, WHOLE_SUITE_WHY);
|
|
1540
1553
|
}
|
|
1541
1554
|
if (out.paths.length > MAX_ASKED) return { paths: [], why: {} };
|
|
1542
1555
|
return out;
|
|
1543
1556
|
}
|
|
1557
|
+
var WHOLE_SUITE_WHY = "the task asks to change the tests as a whole";
|
|
1558
|
+
function foldersNamedIn(task, all) {
|
|
1559
|
+
const out = /* @__PURE__ */ new Set();
|
|
1560
|
+
const add2 = (name) => {
|
|
1561
|
+
const d = posix.normalize(name.replace(/^\.\//, "")).replace(/\/+$/, "");
|
|
1562
|
+
if (d && d !== "." && !d.startsWith("..") && all.some((f) => f.startsWith(`${d}/`))) out.add(d);
|
|
1563
|
+
};
|
|
1564
|
+
for (const m of task.matchAll(FOLDER_TOKEN)) if (m[1].includes("/")) add2(m[1]);
|
|
1565
|
+
for (const m of task.matchAll(/\b([\w@.-]+)`?\s+(?:package|folder|directory)\b/gi)) add2(m[1]);
|
|
1566
|
+
for (const m of task.matchAll(/\b(?:package|folder|directory)\s+`?([\w@./-]+)/gi)) add2(m[1]);
|
|
1567
|
+
return [...out];
|
|
1568
|
+
}
|
|
1569
|
+
var DELETE_VERB = /\b(?:delete[sd]?|deleting|remove[sd]?|removing)\s+/gi;
|
|
1570
|
+
var LEAD_WORDS = /^(?:the|this|that|these|those|old|legacy|whole|entire|empty|original|now-empty|file|files|folder|folders|directory|directories|dir|dirs|package|packages|module|modules)\s+/i;
|
|
1571
|
+
var TAIL_WORDS = /\s+(?:package|packages|folder|folders|directory|directories|dir|dirs|module|modules|file|files|too|also|afterwards|entirely|completely|as well)$/i;
|
|
1572
|
+
var CODE_EXT = /\.(?:py|js|mjs|cjs|jsx|ts|tsx|rb|go|rs|java|kt|php)$/;
|
|
1573
|
+
function deletionsAskedFor(task, repoRoot2, tracked_, rev) {
|
|
1574
|
+
const all = tracked_ ?? tracked(repoRoot2, rev);
|
|
1575
|
+
const files = /* @__PURE__ */ new Set();
|
|
1576
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1577
|
+
const deletable = (f) => !isTestFile(f) && !isTestSupportFile(f) && !isSnapshot(f);
|
|
1578
|
+
const resolve18 = (name) => {
|
|
1579
|
+
const p = posix.normalize(name.replace(/^\.\//, "")).replace(/\/+$/, "");
|
|
1580
|
+
if (!p || p === "." || p.startsWith("..") || p.startsWith("/") || /\s/.test(p)) return false;
|
|
1581
|
+
if (all.includes(p)) {
|
|
1582
|
+
if (deletable(p)) files.add(p);
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
const under = all.filter((f) => f.startsWith(`${p}/`));
|
|
1586
|
+
if (under.length) {
|
|
1587
|
+
const kept = under.filter(deletable);
|
|
1588
|
+
if (kept.length) {
|
|
1589
|
+
kept.forEach((f) => files.add(f));
|
|
1590
|
+
if (kept.length === under.length) dirs.add(p);
|
|
1591
|
+
}
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
const one = all.filter((f) => {
|
|
1595
|
+
const b = posix.basename(f);
|
|
1596
|
+
return b === p || b.replace(CODE_EXT, "") === p && CODE_EXT.test(b);
|
|
1597
|
+
});
|
|
1598
|
+
if (one.length === 1) {
|
|
1599
|
+
if (deletable(one[0])) files.add(one[0]);
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
return false;
|
|
1603
|
+
};
|
|
1604
|
+
for (const whole2 of task.split(/(?<=[.!?])\s+|\n+/)) {
|
|
1605
|
+
const s = withoutNegatedClauses(whole2).text;
|
|
1606
|
+
for (const m of s.matchAll(DELETE_VERB)) {
|
|
1607
|
+
const rest = s.slice(m.index + m[0].length).split(/[;:!?]|\.(?:\s|$)|\s+then\s+/)[0];
|
|
1608
|
+
for (const raw6 of rest.split(/,|\s+and\s+/)) {
|
|
1609
|
+
let t = raw6.replace(/[`"'*]/g, "").trim();
|
|
1610
|
+
for (let prev = ""; prev !== t; ) {
|
|
1611
|
+
prev = t;
|
|
1612
|
+
t = t.replace(LEAD_WORDS, "").replace(TAIL_WORDS, "").trim();
|
|
1613
|
+
}
|
|
1614
|
+
if (!resolve18(t)) break;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return { files: [...files], dirs: [...dirs] };
|
|
1619
|
+
}
|
|
1620
|
+
var DELETIONS_FILE = "contracts/asked-deletions.json";
|
|
1621
|
+
function recordAskedDeletions(repoRoot2, contractId, asked) {
|
|
1622
|
+
mkdirSync2(join4(repoRoot2, ".replume", "contracts"), { recursive: true });
|
|
1623
|
+
writeFileSync(join4(repoRoot2, ".replume", DELETIONS_FILE), JSON.stringify({ contract_id: contractId, ...asked }));
|
|
1624
|
+
}
|
|
1625
|
+
function recordedAskedDeletions(repoRoot2, contractId) {
|
|
1626
|
+
try {
|
|
1627
|
+
const r = JSON.parse(readFileSync3(join4(repoRoot2, ".replume", DELETIONS_FILE), "utf8"));
|
|
1628
|
+
const strings2 = (v2) => Array.isArray(v2) ? v2.filter((p) => typeof p === "string") : [];
|
|
1629
|
+
return r.contract_id === contractId ? { files: strings2(r.files), dirs: strings2(r.dirs) } : null;
|
|
1630
|
+
} catch {
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1544
1634
|
var ASKED_FILE = "contracts/asked-tests.json";
|
|
1545
1635
|
var FILE_TYPES = new Set("js jsx mjs cjs ts tsx py rb go rs java kt c h cc cpp hpp cs php swift scala sh bash zsh sql json jsonl yaml yml toml ini cfg conf env xml html htm css scss less md mdx rst txt csv tsv lock proto graphql vue svelte".split(" "));
|
|
1546
1636
|
function recordAskedTests(repoRoot2, contractId, paths) {
|
|
@@ -1636,6 +1726,8 @@ function testFileOfFailing(repoRoot2, names3) {
|
|
|
1636
1726
|
function pathsNamedInTask(task, repoRoot2) {
|
|
1637
1727
|
const out = /* @__PURE__ */ new Set();
|
|
1638
1728
|
for (const whole2 of task.split(/(?<=[.!?])\s+|\n+/)) {
|
|
1729
|
+
for (const m of withoutNegatedClauses(whole2).text.matchAll(/\b(?:every|all|each)\b[^.!?\n]{0,60}?\bin\s+(?:the\s+)?([\w@.-]+)(?=[\s,;]|$)/gi))
|
|
1730
|
+
if (!m[1].includes(".") && existsSync4(join4(repoRoot2, m[1]))) out.add(`${m[1]}/**`);
|
|
1639
1731
|
for (const raw6 of withoutNegatedClauses(whole2).text.match(/[\w@.\-/*?]+/g) ?? []) {
|
|
1640
1732
|
const token = raw6.replace(/^\.\//, "").replace(/[.,:;?]+$/, "");
|
|
1641
1733
|
const dirLike = token.endsWith("/");
|
|
@@ -1981,11 +2073,11 @@ function captureCoverage(eventsFile, changedPaths, hooksConfigured = false) {
|
|
|
1981
2073
|
}
|
|
1982
2074
|
const compared = new Set(events.filter((e) => e.type === "command" && e.command && e.raw?.treeCompared === true).map((e) => e.command));
|
|
1983
2075
|
const shell = events.filter((e) => (e.type === "command" || e.type === "tool_pre") && e.command && !compared.has(e.command));
|
|
1984
|
-
const
|
|
2076
|
+
const opaque2 = [...new Set(shell.filter((e) => unobservable(e.command)).map((e) => e.command))];
|
|
1985
2077
|
const unparsed = [...new Set(shell.filter((e) => !unobservable(e.command) && e.raw?.writeNote === "write target not parsed").map((e) => e.command))];
|
|
1986
|
-
if (
|
|
2078
|
+
if (opaque2.length)
|
|
1987
2079
|
notSeen.push(
|
|
1988
|
-
`${
|
|
2080
|
+
`${opaque2.length} shell command${opaque2.length === 1 ? "" : "s"} ran a script file or wrote to a path built at run time (e.g. \`${opaque2[0].slice(0, 80)}\`); what ${opaque2.length === 1 ? "it" : "they"} wrote is not observable by Replume's shell rules`
|
|
1989
2081
|
);
|
|
1990
2082
|
if (unparsed.length)
|
|
1991
2083
|
notSeen.push(
|
|
@@ -2009,7 +2101,7 @@ function captureCoverage(eventsFile, changedPaths, hooksConfigured = false) {
|
|
|
2009
2101
|
changedFilesLanded: changedPaths.filter((p) => landedPaths.has(p)).length,
|
|
2010
2102
|
changedFilesAttemptedOnly: attemptedOnly.slice(0, SHOWN_PATHS),
|
|
2011
2103
|
changedFilesNotSeen: unseen.slice(0, SHOWN_PATHS),
|
|
2012
|
-
unobservableCommands:
|
|
2104
|
+
unobservableCommands: opaque2.length + unparsed.length,
|
|
2013
2105
|
notSeen
|
|
2014
2106
|
};
|
|
2015
2107
|
}
|
|
@@ -2688,6 +2780,8 @@ var ERROR_CODES = {
|
|
|
2688
2780
|
NO_COMMIT: "NO_COMMIT",
|
|
2689
2781
|
STALE_REPORT: "STALE_REPORT",
|
|
2690
2782
|
TASK_TEST_RED: "TASK_TEST_RED",
|
|
2783
|
+
ASKED_DELETE_NOT_DONE: "ASKED_DELETE_NOT_DONE",
|
|
2784
|
+
EXPECTATION_CHANGED: "EXPECTATION_CHANGED",
|
|
2691
2785
|
SUITE_SHRANK: "SUITE_SHRANK",
|
|
2692
2786
|
REQUIRED_CHECK_MISSING: "REQUIRED_CHECK_MISSING",
|
|
2693
2787
|
JIRA_NOT_DONE: "JIRA_NOT_DONE",
|
|
@@ -2716,6 +2810,7 @@ var ERROR_CODES = {
|
|
|
2716
2810
|
CHECK_FAILED: "CHECK_FAILED",
|
|
2717
2811
|
CHECK_NOT_RUN: "CHECK_NOT_RUN"
|
|
2718
2812
|
};
|
|
2813
|
+
var ASKED_SHELL_REWRITE = "a shell command that rewrites a test cannot be checked before it runs";
|
|
2719
2814
|
function codeForReportReason(subject, reason, status) {
|
|
2720
2815
|
const embedded = /^\[([A-Z][A-Z0-9_]+)\]\s/.exec(reason)?.[1];
|
|
2721
2816
|
if (embedded && embedded in ERROR_CODES) return embedded;
|
|
@@ -2729,6 +2824,8 @@ function codeForReportReason(subject, reason, status) {
|
|
|
2729
2824
|
if (/runner not found|test runner was not there|vitest: not found|jest: not found|command not found/.test(s)) return ERROR_CODES.RUNNER_NOT_FOUND;
|
|
2730
2825
|
if (/zero tests|no tests collected|no tests ran/.test(s)) return ERROR_CODES.ZERO_TESTS;
|
|
2731
2826
|
if (/task test still red/.test(s)) return ERROR_CODES.TASK_TEST_RED;
|
|
2827
|
+
if (/the task asks to delete .* still there/.test(s)) return ERROR_CODES.ASKED_DELETE_NOT_DONE;
|
|
2828
|
+
if (/an expected value (?:may have )?changed in /.test(s)) return ERROR_CODES.EXPECTATION_CHANGED;
|
|
2732
2829
|
if (/still failed|stopped with .* after \d+ steers?/.test(s)) return ERROR_CODES.STOP_RETRIES_EXHAUSTED;
|
|
2733
2830
|
if (/test.bypass|special.case/.test(s)) return ERROR_CODES.TEST_BYPASS;
|
|
2734
2831
|
if (/bent to|states the bug|expected.*bug/.test(s)) return ERROR_CODES.TEST_BENT;
|
|
@@ -2747,7 +2844,7 @@ function codeForReportReason(subject, reason, status) {
|
|
|
2747
2844
|
if (/runtime/.test(subject)) return status === "failed" ? ERROR_CODES.RUNTIME_FAILED : ERROR_CODES.RUNTIME_NOT_CHECKED;
|
|
2748
2845
|
if (/not observed|no agent session/.test(s)) return /no agent session/.test(s) ? ERROR_CODES.NO_AGENT_SESSION : ERROR_CODES.CHANGE_NOT_OBSERVED;
|
|
2749
2846
|
if (/no allow globs/.test(s)) return ERROR_CODES.SCOPE_UNSET;
|
|
2750
|
-
if (/outside allow|new dependency|lockfile adds/.test(s)) return ERROR_CODES.SCOPE_EXTRA;
|
|
2847
|
+
if (/outside allow|changed outside the task|new dependency|lockfile adds/.test(s)) return ERROR_CODES.SCOPE_EXTRA;
|
|
2751
2848
|
if (/not pass with at least one test|output does not show|unconfirmed/.test(s)) return ERROR_CODES.TEST_UNCONFIRMED;
|
|
2752
2849
|
if (/no commit/.test(s)) return ERROR_CODES.NO_COMMIT;
|
|
2753
2850
|
if (/working tree changed after this report/.test(s)) return ERROR_CODES.STALE_REPORT;
|
|
@@ -2804,6 +2901,390 @@ function prefixCode(code2, message) {
|
|
|
2804
2901
|
return /^\[[A-Z][A-Z0-9_]+\]\s/.test(message) ? message : `[${code2}] ${message}`;
|
|
2805
2902
|
}
|
|
2806
2903
|
|
|
2904
|
+
// src/core/test-cases.ts
|
|
2905
|
+
function assertAliases(text) {
|
|
2906
|
+
const out = /* @__PURE__ */ new Set();
|
|
2907
|
+
const mod = String.raw`['"](?:node:)?assert(?:/strict)?['"]`;
|
|
2908
|
+
for (const m of text.matchAll(new RegExp(String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*${mod}\s*\)(?:\.strict)?`, "g"))) out.add(m[1]);
|
|
2909
|
+
for (const m of text.matchAll(new RegExp(String.raw`\bimport\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\}\s*)?from\s*${mod}`, "g"))) out.add(m[1]);
|
|
2910
|
+
for (const m of text.matchAll(new RegExp(String.raw`(?:\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*${mod}\s*\)|\bimport\s*\{([^}]*)\}\s*from\s*${mod})`, "g")))
|
|
2911
|
+
for (const part of (m[1] ?? m[2] ?? "").split(",")) {
|
|
2912
|
+
const name = part.split(/\s*(?::|\bas\b)\s*/).pop()?.trim();
|
|
2913
|
+
if (name && /^[A-Za-z_$][\w$]*$/.test(name)) out.add(name);
|
|
2914
|
+
}
|
|
2915
|
+
out.delete("assert");
|
|
2916
|
+
return [...out];
|
|
2917
|
+
}
|
|
2918
|
+
var isPython = (path) => /\.py$/.test(path);
|
|
2919
|
+
function closing(s, open) {
|
|
2920
|
+
const pairs = { "(": ")", "[": "]", "{": "}" };
|
|
2921
|
+
const stack = [];
|
|
2922
|
+
for (let i = open; i < s.length; i++) {
|
|
2923
|
+
const c = s[i];
|
|
2924
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2925
|
+
for (i++; i < s.length && s[i] !== c; i++) if (s[i] === "\\") i++;
|
|
2926
|
+
continue;
|
|
2927
|
+
}
|
|
2928
|
+
if (pairs[c]) stack.push(pairs[c]);
|
|
2929
|
+
else if (c === stack.at(-1)) {
|
|
2930
|
+
stack.pop();
|
|
2931
|
+
if (!stack.length) return i;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return -1;
|
|
2935
|
+
}
|
|
2936
|
+
function splitTop(s, sep6) {
|
|
2937
|
+
const out = [];
|
|
2938
|
+
let at = 0;
|
|
2939
|
+
for (let i = 0; i < s.length; i++) {
|
|
2940
|
+
const c = s[i];
|
|
2941
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2942
|
+
for (i++; i < s.length && s[i] !== c; i++) if (s[i] === "\\") i++;
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
if ("([{".includes(c)) {
|
|
2946
|
+
const end = closing(s, i);
|
|
2947
|
+
if (end < 0) break;
|
|
2948
|
+
i = end;
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
if (s.startsWith(sep6, i)) {
|
|
2952
|
+
out.push(s.slice(at, i));
|
|
2953
|
+
at = i + sep6.length;
|
|
2954
|
+
i += sep6.length - 1;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
out.push(s.slice(at));
|
|
2958
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
2959
|
+
}
|
|
2960
|
+
function norm(s) {
|
|
2961
|
+
let out = "";
|
|
2962
|
+
for (let i = 0; i < s.length; i++) {
|
|
2963
|
+
const c = s[i];
|
|
2964
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2965
|
+
let j = i + 1;
|
|
2966
|
+
for (; j < s.length && s[j] !== c; j++) if (s[j] === "\\") j++;
|
|
2967
|
+
out += `"${s.slice(i + 1, j)}"`;
|
|
2968
|
+
i = j;
|
|
2969
|
+
} else if (!/\s/.test(c)) out += c;
|
|
2970
|
+
}
|
|
2971
|
+
return out.replace(/,([)\]}])/g, "$1").replace(/;$/, "");
|
|
2972
|
+
}
|
|
2973
|
+
var inputKey = (s) => norm(s).replace(/^(?:[A-Za-z_$][\w$]*\.)+(?=[A-Za-z_$][\w$]*\()/, "");
|
|
2974
|
+
var substitute = (text, bind) => {
|
|
2975
|
+
const names3 = Object.keys(bind);
|
|
2976
|
+
if (!names3.length) return text;
|
|
2977
|
+
return text.replace(new RegExp(`(?<![\\w$."'])(?:${names3.map((n3) => n3.replace(/\$/g, "\\$")).join("|")})(?![\\w$"'])`, "g"), (m) => bind[m]);
|
|
2978
|
+
};
|
|
2979
|
+
function rowsOf(list5) {
|
|
2980
|
+
const body = list5.trim().replace(/^[[(]/, "").replace(/[\])]$/, "");
|
|
2981
|
+
return splitTop(body, ",").map((row) => {
|
|
2982
|
+
const r = row.replace(/^pytest\.param\(/, "(");
|
|
2983
|
+
return /^[[(]/.test(r) ? splitTop(r.slice(1, closing(r, 0)), ",").filter((x) => !/^\w+\s*=/.test(x)) : [r];
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
function bindAll(params, rows2) {
|
|
2987
|
+
return rows2.filter((r) => r.length >= params.length).map((r) => Object.fromEntries(params.map((n3, i) => [n3, r[i]])));
|
|
2988
|
+
}
|
|
2989
|
+
var LITERAL = /^(?:["'`]|-?\d|\[|\{|\(|True\b|False\b|None\b|true\b|false\b|null\b|undefined\b)/;
|
|
2990
|
+
var ordered = (a, b) => LITERAL.test(a.trim()) && !LITERAL.test(b.trim()) ? [b, a] : [a, b];
|
|
2991
|
+
var opaque = (s) => ({ input: `?${norm(s)}`, expected: "" });
|
|
2992
|
+
var resolve2 = (text, vars) => {
|
|
2993
|
+
let out = text;
|
|
2994
|
+
for (let n3 = 0; n3 < 4; n3++) {
|
|
2995
|
+
const next = substitute(out, vars);
|
|
2996
|
+
if (next === out) break;
|
|
2997
|
+
out = next;
|
|
2998
|
+
}
|
|
2999
|
+
return out;
|
|
3000
|
+
};
|
|
3001
|
+
var IDENT = /(?<![\w$."'])[A-Za-z_$][\w$]*(?![\w$"'(])/g;
|
|
3002
|
+
var unbound = (text, params) => [...text.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, "").matchAll(IDENT)].some((m) => params.has(m[0]));
|
|
3003
|
+
function dictRows(list5) {
|
|
3004
|
+
return splitTop(list5.trim().slice(1, -1), ",").map((p) => splitTop(p, ":")).filter((p) => p.length === 2);
|
|
3005
|
+
}
|
|
3006
|
+
function pythonCases(text) {
|
|
3007
|
+
const cases = [];
|
|
3008
|
+
let read4 = true;
|
|
3009
|
+
const lines = text.split("\n");
|
|
3010
|
+
const moduleVars = {};
|
|
3011
|
+
let localVars = {};
|
|
3012
|
+
let pending = null;
|
|
3013
|
+
let scope = null;
|
|
3014
|
+
let fn = null;
|
|
3015
|
+
const vars = () => ({ ...moduleVars, ...localVars });
|
|
3016
|
+
const rowsFrom = (expr) => {
|
|
3017
|
+
const items = /\.items\(\)$/.test(expr.trim());
|
|
3018
|
+
const value = resolve2(expr.trim().replace(/\.items\(\)$/, ""), vars());
|
|
3019
|
+
if (/^\{/.test(value)) return items ? dictRows(value) : null;
|
|
3020
|
+
return /^[[(]/.test(value) ? rowsOf(value) : null;
|
|
3021
|
+
};
|
|
3022
|
+
const statement = (i) => {
|
|
3023
|
+
let s = lines[i];
|
|
3024
|
+
let last = i;
|
|
3025
|
+
const open = (x) => {
|
|
3026
|
+
let n3 = 0;
|
|
3027
|
+
for (let k = 0; k < x.length; k++) {
|
|
3028
|
+
const c = x[k];
|
|
3029
|
+
if (c === '"' || c === "'") {
|
|
3030
|
+
for (k++; k < x.length && x[k] !== c; k++) if (x[k] === "\\") k++;
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
if ("([{".includes(c)) n3++;
|
|
3034
|
+
if (")]}".includes(c)) n3--;
|
|
3035
|
+
}
|
|
3036
|
+
return n3;
|
|
3037
|
+
};
|
|
3038
|
+
while (open(s) > 0 && last + 1 < lines.length) s += `
|
|
3039
|
+
${lines[++last]}`;
|
|
3040
|
+
return { text: s, last };
|
|
3041
|
+
};
|
|
3042
|
+
const add2 = (left, right, raw6) => {
|
|
3043
|
+
const binds = scope?.binds ?? [{}];
|
|
3044
|
+
for (const b of binds) {
|
|
3045
|
+
const v2 = (x) => resolve2(substitute(x, b), vars());
|
|
3046
|
+
const whole2 = right === null ? v2(raw6) : `${v2(left)} ${v2(right)}`;
|
|
3047
|
+
if (fn && unbound(whole2, new Set([...fn.params].filter((p) => !(p in b))))) {
|
|
3048
|
+
read4 = false;
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
if (right === null) cases.push(opaque(whole2));
|
|
3052
|
+
else {
|
|
3053
|
+
const [a, e] = ordered(v2(left), v2(right));
|
|
3054
|
+
cases.push({ input: inputKey(a), expected: norm(e) });
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3059
|
+
const line2 = lines[i];
|
|
3060
|
+
if (!line2.trim() || line2.trim().startsWith("#")) continue;
|
|
3061
|
+
const indent = line2.length - line2.trimStart().length;
|
|
3062
|
+
if (scope && indent <= scope.indent) scope = null;
|
|
3063
|
+
if (fn && indent <= fn.indent) {
|
|
3064
|
+
fn = null;
|
|
3065
|
+
localVars = {};
|
|
3066
|
+
}
|
|
3067
|
+
const st = statement(i);
|
|
3068
|
+
const body = st.text.trim();
|
|
3069
|
+
const param = /^@pytest\.mark\.parametrize\s*\(/.exec(body);
|
|
3070
|
+
if (param) {
|
|
3071
|
+
i = st.last;
|
|
3072
|
+
const parts2 = splitTop(body.slice(param[0].length, closing(body, param[0].length - 1)), ",");
|
|
3073
|
+
if (parts2.length < 2) {
|
|
3074
|
+
read4 = false;
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
const names3 = /^[[(]/.test(parts2[0]) ? splitTop(parts2[0].slice(1, -1), ",").map((n3) => n3.replace(/["']/g, "")) : parts2[0].replace(/["']/g, "").split(",").map((n3) => n3.trim());
|
|
3078
|
+
const rows2 = rowsFrom(parts2[1]);
|
|
3079
|
+
if (!rows2) {
|
|
3080
|
+
read4 = false;
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
pending = { names: names3, rows: rows2 };
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
const def = /^(?:async\s+)?def\s+\w+\s*\(/.exec(body);
|
|
3087
|
+
if (def) {
|
|
3088
|
+
i = st.last;
|
|
3089
|
+
const params = splitTop(body.slice(def[0].length, closing(body, def[0].length - 1)), ",").map((p) => p.split(/[:=]/)[0].trim().replace(/^\*+/, "")).filter((p) => p && p !== "self" && p !== "cls");
|
|
3090
|
+
fn = { indent, params: new Set(params) };
|
|
3091
|
+
localVars = {};
|
|
3092
|
+
if (pending) {
|
|
3093
|
+
scope = { indent, binds: bindAll(pending.names, pending.rows) };
|
|
3094
|
+
if (!scope.binds.length) read4 = false;
|
|
3095
|
+
}
|
|
3096
|
+
pending = null;
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
const loop2 = /^for\s+(.+?)\s+in\s+(.+?):\s*$/s.exec(body);
|
|
3100
|
+
if (loop2) {
|
|
3101
|
+
i = st.last;
|
|
3102
|
+
const rows2 = rowsFrom(loop2[2]);
|
|
3103
|
+
if (rows2) scope = { indent, binds: bindAll(loop2[1].replace(/[()]/g, "").split(",").map((n3) => n3.trim()), rows2) };
|
|
3104
|
+
else if (/\bassert\b/.test(lines.slice(i + 1, i + 8).join("\n"))) read4 = false;
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
const assign = /^([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/s.exec(body);
|
|
3108
|
+
if (assign) {
|
|
3109
|
+
i = st.last;
|
|
3110
|
+
(fn ? localVars : moduleVars)[assign[1]] = assign[2].trim();
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
const a = /^assert\s+(.*)$/s.exec(body);
|
|
3114
|
+
if (a) {
|
|
3115
|
+
i = st.last;
|
|
3116
|
+
const expr = splitTop(a[1], ",")[0] ?? "";
|
|
3117
|
+
const sides = splitTop(expr, "==");
|
|
3118
|
+
if (sides.length === 2) add2(sides[0], sides[1], expr);
|
|
3119
|
+
else add2("", null, expr);
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
const unit = /^self\.(assert\w+|fail\w*)\s*\(/.exec(body);
|
|
3123
|
+
if (unit) {
|
|
3124
|
+
i = st.last;
|
|
3125
|
+
const args = splitTop(body.slice(unit[0].length, closing(body, unit[0].length - 1)), ",");
|
|
3126
|
+
if (unit[1] === "assertEqual" && args.length >= 2) add2(args[0], args[1], "");
|
|
3127
|
+
else add2("", null, body);
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
if (/\braises\s*\(|\bpytest\.(?:fail|approx|warns)\b/.test(body)) {
|
|
3131
|
+
i = st.last;
|
|
3132
|
+
add2("", null, body);
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
i = st.last;
|
|
3136
|
+
}
|
|
3137
|
+
return { cases, read: read4 && cases.length > 0 };
|
|
3138
|
+
}
|
|
3139
|
+
function jsCases(text, aliases) {
|
|
3140
|
+
const cases = [];
|
|
3141
|
+
let read4 = true;
|
|
3142
|
+
if (/\.should\b|\bshould\(/.test(text)) read4 = false;
|
|
3143
|
+
const helperParams = /* @__PURE__ */ new Set();
|
|
3144
|
+
for (const m of text.matchAll(/\bfunction\s+[\w$]*\s*\(([^)]*)\)|\b(?:const|let|var)\s+[\w$]+\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>/g)) for (const p of (m[1] ?? m[2] ?? "").split(",")) {
|
|
3145
|
+
const n3 = p.split("=")[0].trim();
|
|
3146
|
+
if (n3) helperParams.add(n3);
|
|
3147
|
+
}
|
|
3148
|
+
const nameAlt = ["assert", ...aliases].map((n3) => n3.replace(/\$/g, "\\$")).join("|");
|
|
3149
|
+
const CALL = new RegExp(`(?<![\\w$.])(?:expect|(${nameAlt})(?:\\.(\\w+))?)\\(`, "g");
|
|
3150
|
+
const varsBefore = (at, offset) => {
|
|
3151
|
+
const out = {};
|
|
3152
|
+
for (const m of text.slice(0, at + offset).matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?!require\b|async\b|function\b|\()/g)) {
|
|
3153
|
+
const start = m.index + m[0].length;
|
|
3154
|
+
const rest = text.slice(start);
|
|
3155
|
+
const stop2 = splitTop(rest.split("\n")[0], ";")[0];
|
|
3156
|
+
if (stop2 && !/=>/.test(stop2)) out[m[1]] = /^[[{(]/.test(stop2) && closing(text, start) > 0 ? text.slice(start, closing(text, start) + 1) : stop2;
|
|
3157
|
+
}
|
|
3158
|
+
return out;
|
|
3159
|
+
};
|
|
3160
|
+
const scan = (body, binds, offset) => {
|
|
3161
|
+
for (const m of body.matchAll(CALL)) {
|
|
3162
|
+
const open = m.index + m[0].length - 1;
|
|
3163
|
+
const end = closing(body, open);
|
|
3164
|
+
if (end < 0) {
|
|
3165
|
+
read4 = false;
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
const args = splitTop(body.slice(open + 1, end), ",");
|
|
3169
|
+
let input = null, expected = null, whole2 = body.slice(m.index, end + 1);
|
|
3170
|
+
if (m[0].startsWith("expect")) {
|
|
3171
|
+
const chain = /^(?:\s*\.\s*\w+)*\s*\(/.exec(body.slice(end + 1));
|
|
3172
|
+
const o = chain ? end + chain[0].length : -1;
|
|
3173
|
+
const c = o > 0 ? closing(body, o) : -1;
|
|
3174
|
+
if (c > 0) whole2 = body.slice(m.index, c + 1);
|
|
3175
|
+
const matcher = chain?.[0].replace(/\s/g, "");
|
|
3176
|
+
if (chain && c > 0 && args.length === 1 && /^\.(?:toBe|toEqual|toStrictEqual)\($/.test(matcher ?? "")) {
|
|
3177
|
+
input = args[0];
|
|
3178
|
+
expected = body.slice(o + 1, c);
|
|
3179
|
+
}
|
|
3180
|
+
} else if (m[2] && /^(?:equal|strictEqual|deepEqual|deepStrictEqual)$/.test(m[2]) && args.length >= 2) {
|
|
3181
|
+
input = args[0];
|
|
3182
|
+
expected = args[1];
|
|
3183
|
+
}
|
|
3184
|
+
const vars = varsBefore(m.index, offset);
|
|
3185
|
+
for (const b of binds) {
|
|
3186
|
+
const v2 = (x) => resolve2(substitute(x, b), vars);
|
|
3187
|
+
const seen = input !== null && expected !== null ? `${v2(input)} ${v2(expected)}` : v2(whole2);
|
|
3188
|
+
if (unbound(seen, new Set([...helperParams].filter((p) => !(p in b))))) {
|
|
3189
|
+
read4 = false;
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
if (input !== null && expected !== null) {
|
|
3193
|
+
const [a, e] = ordered(v2(input), v2(expected));
|
|
3194
|
+
cases.push({ input: inputKey(a), expected: norm(e) });
|
|
3195
|
+
} else cases.push(opaque(v2(whole2)));
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
const parametrized = [];
|
|
3200
|
+
const rowsOfJs = (s) => /^\[/.test(s.trim()) ? rowsOf(s) : null;
|
|
3201
|
+
for (const m of text.matchAll(/\b(?:test|it|describe)\.each\(/g)) {
|
|
3202
|
+
const open = m.index + m[0].length - 1;
|
|
3203
|
+
const end = closing(text, open);
|
|
3204
|
+
const call2 = end > 0 ? /^\s*\(/.exec(text.slice(end + 1)) : null;
|
|
3205
|
+
if (!call2) {
|
|
3206
|
+
read4 = false;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
const callOpen = end + 1 + call2[0].length - 1;
|
|
3210
|
+
const callEnd = closing(text, callOpen);
|
|
3211
|
+
const fn = callEnd > 0 ? /,\s*(?:async\s*)?\(\s*\[?([^)]*?)\]?\s*\)\s*=>\s*/.exec(text.slice(callOpen, callEnd)) : null;
|
|
3212
|
+
const rows2 = fn ? rowsOfJs(resolve2(text.slice(open + 1, end), varsBefore(m.index, 0))) : null;
|
|
3213
|
+
if (!fn || !rows2) {
|
|
3214
|
+
read4 = false;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
parametrized.push([m.index, callEnd]);
|
|
3218
|
+
scan(text.slice(callOpen, callEnd), bindAll(fn[1].split(",").map((n3) => n3.trim()).filter(Boolean), rows2), callOpen);
|
|
3219
|
+
}
|
|
3220
|
+
for (const m of text.matchAll(/\bfor\s*\(\s*(?:const|let|var)\s*\[([^\]]+)\]\s+of\s+([^)]+?)\s*\)\s*\{/g)) {
|
|
3221
|
+
const bodyOpen = m.index + m[0].length - 1;
|
|
3222
|
+
const bodyEnd = closing(text, bodyOpen);
|
|
3223
|
+
const entries = /^Object\.entries\((.+)\)$/.exec(m[2].trim());
|
|
3224
|
+
const source = resolve2((entries?.[1] ?? m[2]).trim(), varsBefore(m.index, 0));
|
|
3225
|
+
const rows2 = entries && /^\{/.test(source) ? dictRows(source) : rowsOfJs(source);
|
|
3226
|
+
if (bodyEnd < 0 || !rows2) {
|
|
3227
|
+
read4 = false;
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
parametrized.push([m.index, bodyEnd]);
|
|
3231
|
+
scan(text.slice(bodyOpen, bodyEnd), bindAll(m[1].split(",").map((n3) => n3.trim()), rows2), bodyOpen);
|
|
3232
|
+
}
|
|
3233
|
+
let plain = text;
|
|
3234
|
+
for (const [a, b] of parametrized) plain = plain.slice(0, a) + " ".repeat(b + 1 - a) + plain.slice(b + 1);
|
|
3235
|
+
scan(plain, [{}], 0);
|
|
3236
|
+
return { cases, read: read4 && cases.length > 0 };
|
|
3237
|
+
}
|
|
3238
|
+
function testCases(text, path) {
|
|
3239
|
+
try {
|
|
3240
|
+
return isPython(path) ? pythonCases(text) : /\.[cm]?[jt]sx?$/.test(path) ? jsCases(text, assertAliases(text)) : { cases: [], read: false };
|
|
3241
|
+
} catch {
|
|
3242
|
+
return { cases: [], read: false };
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
var CALLEE = /^([A-Za-z_$][\w$]*)(\(.*)$/s;
|
|
3246
|
+
function withRenamedCallees(before, after) {
|
|
3247
|
+
const inputs = new Set(after.map((c) => c.input));
|
|
3248
|
+
const map = /* @__PURE__ */ new Map();
|
|
3249
|
+
for (const c of before) {
|
|
3250
|
+
if (inputs.has(c.input)) continue;
|
|
3251
|
+
const m = CALLEE.exec(c.input);
|
|
3252
|
+
if (!m) continue;
|
|
3253
|
+
const to = after.filter((x) => {
|
|
3254
|
+
const n3 = CALLEE.exec(x.input);
|
|
3255
|
+
return !!n3 && n3[2] === m[2] && n3[1] !== m[1];
|
|
3256
|
+
}).map((x) => CALLEE.exec(x.input)[1]);
|
|
3257
|
+
const names3 = [...new Set(to)];
|
|
3258
|
+
if (names3.length !== 1 || after.some((x) => CALLEE.exec(x.input)?.[1] === m[1]) || before.some((x) => CALLEE.exec(x.input)?.[1] === names3[0])) continue;
|
|
3259
|
+
if (map.has(m[1]) && map.get(m[1]) !== names3[0]) return before;
|
|
3260
|
+
map.set(m[1], names3[0]);
|
|
3261
|
+
}
|
|
3262
|
+
if (!map.size) return before;
|
|
3263
|
+
return before.map((c) => {
|
|
3264
|
+
const m = CALLEE.exec(c.input);
|
|
3265
|
+
return m && map.has(m[1]) ? { ...c, input: `${map.get(m[1])}${m[2]}` } : c;
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
function compareCases(before, after, path) {
|
|
3269
|
+
const a = testCases(before, path), b = testCases(after, path);
|
|
3270
|
+
if (!a.read || !b.read) return { kind: "other", changes: [] };
|
|
3271
|
+
const was = withRenamedCallees(a.cases, b.cases);
|
|
3272
|
+
if (!was.every((c) => b.cases.some((x) => x.input === c.input))) return { kind: "other", changes: [] };
|
|
3273
|
+
const changes = [];
|
|
3274
|
+
for (const c of was) {
|
|
3275
|
+
const now = b.cases.filter((x) => x.input === c.input);
|
|
3276
|
+
if (!now.some((x) => x.expected === c.expected)) changes.push({ input: c.input, from: c.expected, to: now[0].expected });
|
|
3277
|
+
}
|
|
3278
|
+
return { kind: changes.length ? "values" : "same", changes };
|
|
3279
|
+
}
|
|
3280
|
+
var sameInputsKept = (before, after, path) => compareCases(before, after, path).kind !== "other";
|
|
3281
|
+
function taskNamesValue(task, value) {
|
|
3282
|
+
const bare = value.replace(/^"(.*)"$/, "$1");
|
|
3283
|
+
if (!bare) return false;
|
|
3284
|
+
const quoted = bare.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3285
|
+
return new RegExp(`\\bto\\s+[\`'"]?${quoted}[\`'"]?(?!\\w|\\.\\d)`, "i").test(task);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
2807
3288
|
// src/core/rules.ts
|
|
2808
3289
|
var PREFIX = "Replume:";
|
|
2809
3290
|
var pick = (ctx, approve, auto) => ctx?.mode === "auto" ? auto : approve;
|
|
@@ -2820,7 +3301,7 @@ function expandHome(p) {
|
|
|
2820
3301
|
function splitPaths(repoRoot2, paths) {
|
|
2821
3302
|
const inside = [];
|
|
2822
3303
|
const outside = [];
|
|
2823
|
-
const root =
|
|
3304
|
+
const root = resolve3(repoRoot2);
|
|
2824
3305
|
let realRoot;
|
|
2825
3306
|
const isOut = (rel3) => rel3 === "" || rel3.startsWith("..") || isAbsolute2(rel3);
|
|
2826
3307
|
for (const raw6 of paths) {
|
|
@@ -2829,7 +3310,7 @@ function splitPaths(repoRoot2, paths) {
|
|
|
2829
3310
|
outside.push(p);
|
|
2830
3311
|
continue;
|
|
2831
3312
|
}
|
|
2832
|
-
const abs = isAbsolute2(p) ?
|
|
3313
|
+
const abs = isAbsolute2(p) ? resolve3(p) : resolve3(root, p);
|
|
2833
3314
|
let rel3 = relative2(root, abs);
|
|
2834
3315
|
if (isOut(rel3)) {
|
|
2835
3316
|
if (realRoot === void 0) realRoot = realPathOf(root);
|
|
@@ -3085,7 +3566,7 @@ var isBlockDevice = (p) => /^\/dev\/(?:(?:s|h|v|xv)d[a-z]+[0-9]*|nvme[0-9]+n[0-9
|
|
|
3085
3566
|
function substitutionBodies(src) {
|
|
3086
3567
|
const text = src.replace(/<<(-?)[ \t]*(['"]?)([A-Za-z_][\w-]*)\2[^\n]*\n[\s\S]*?\n[ \t]*\3(?=\n|$)/g, "");
|
|
3087
3568
|
const out = [];
|
|
3088
|
-
const
|
|
3569
|
+
const closing2 = (from) => {
|
|
3089
3570
|
let depth = 0;
|
|
3090
3571
|
for (let j = from; j < text.length; j++) {
|
|
3091
3572
|
const c = text[j];
|
|
@@ -3125,7 +3606,7 @@ function substitutionBodies(src) {
|
|
|
3125
3606
|
continue;
|
|
3126
3607
|
}
|
|
3127
3608
|
if (text[i + 1] === "(" && (c === "$" && text[i + 2] !== "(" || (c === "<" || c === ">") && !dq)) {
|
|
3128
|
-
const end =
|
|
3609
|
+
const end = closing2(i + 1);
|
|
3129
3610
|
out.push(text.slice(i + 2, end));
|
|
3130
3611
|
i = end;
|
|
3131
3612
|
} else if (c === "`") {
|
|
@@ -3557,44 +4038,6 @@ function inlineWriteTargets(command, repoRoot2) {
|
|
|
3557
4038
|
};
|
|
3558
4039
|
return splitPaths(repoRoot2, [...literals2].filter((l) => !/^[a-z]+:\/\//i.test(l) && !l.endsWith("/")).map((l) => isAbsolute2(l) ? l : join12(repoRoot2, l))).inside.filter((p) => isFile(p) || /\/[^/]+\.[A-Za-z0-9]+$/.test(p));
|
|
3559
4040
|
}
|
|
3560
|
-
function heredocFileWrites(command, repoRoot2) {
|
|
3561
|
-
const lines = command.split("\n");
|
|
3562
|
-
const out = [];
|
|
3563
|
-
const word = String.raw`(?:"[^"\n]+"|'[^'\n]+'|[^\s<>;&|'"()$` + "`" + String.raw`]+)`;
|
|
3564
|
-
const unq = (w) => w.replace(/^(['"])(.*)\1$/, "$2");
|
|
3565
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3566
|
-
const m = /<<(-?)[ \t]*(['"]?)([A-Za-z_][\w-]*)\2/.exec(lines[i]);
|
|
3567
|
-
if (!m) continue;
|
|
3568
|
-
const strip = m[1] === "-";
|
|
3569
|
-
const quoted = !!m[2];
|
|
3570
|
-
const body = [];
|
|
3571
|
-
let j = i + 1;
|
|
3572
|
-
for (; j < lines.length; j++) {
|
|
3573
|
-
if ((strip ? lines[j].replace(/^\t+/, "") : lines[j]) === m[3]) break;
|
|
3574
|
-
body.push(strip ? lines[j].replace(/^\t+/, "") : lines[j]);
|
|
3575
|
-
}
|
|
3576
|
-
const header = lines[i];
|
|
3577
|
-
const closed = j < lines.length;
|
|
3578
|
-
const skipTo = j;
|
|
3579
|
-
const pre = header.slice(0, m.index).split(/&&|\|\||;/).pop();
|
|
3580
|
-
const post = header.slice(m.index + m[0].length).split(/&&|\|\||;/)[0];
|
|
3581
|
-
i = skipTo;
|
|
3582
|
-
if (!closed || /<<|\|/.test(pre + post)) continue;
|
|
3583
|
-
if (!quoted && /[$`\\]/.test(body.join("\n"))) continue;
|
|
3584
|
-
const seg = `${pre} ${post}`.trim();
|
|
3585
|
-
let target = null;
|
|
3586
|
-
let cm;
|
|
3587
|
-
if ((cm = new RegExp(String.raw`^cat\s*>\s*(${word})\s*$|^cat\s*$`).exec(pre.trim())) && (cm[1] || (cm = new RegExp(String.raw`^>\s*(${word})\s*$`).exec(post.trim())))) target = unq(cm[1]);
|
|
3588
|
-
else if ((cm = new RegExp(String.raw`^tee\s+(${word})\s*$`).exec(seg)) && !cm[1].startsWith("-")) target = unq(cm[1]);
|
|
3589
|
-
if (!target || /[*?[\]{}~]/.test(target)) continue;
|
|
3590
|
-
const abs = resolve2(repoRoot2, target);
|
|
3591
|
-
const rel3 = relative2(resolve2(repoRoot2), abs);
|
|
3592
|
-
if (!rel3 || rel3.startsWith("..") || isAbsolute2(rel3)) continue;
|
|
3593
|
-
out.push({ path: normalizeRel(rel3.split(sep).join("/")), content: body.length ? `${body.join("\n")}
|
|
3594
|
-
` : "" });
|
|
3595
|
-
}
|
|
3596
|
-
return out.filter((w) => out.filter((x) => x.path === w.path).length === 1);
|
|
3597
|
-
}
|
|
3598
4041
|
function npmPkgKeys(command) {
|
|
3599
4042
|
const keys2 = [];
|
|
3600
4043
|
for (const { words: w } of commandSegments(command)) {
|
|
@@ -3618,7 +4061,7 @@ var outOfScopeEdit = (a, ctx) => {
|
|
|
3618
4061
|
}
|
|
3619
4062
|
const allow = ctx.contract?.allow ?? [];
|
|
3620
4063
|
if (!ctx.contract || allow.length === 0) return null;
|
|
3621
|
-
const outside = touched.inside.filter((p) => !matchesAny(p, allow) && !p.startsWith(".replume/") && !judgeRunnerConfigEdit(a, ctx, p)?.pass && !askedTestPaths(ctx).includes(p));
|
|
4064
|
+
const outside = touched.inside.filter((p) => !matchesAny(p, allow) && !p.startsWith(".replume/") && !judgeRunnerConfigEdit(a, ctx, p)?.pass && !askedTestPaths(ctx).includes(p) && !askedToDelete(ctx, p));
|
|
3622
4065
|
if (!outside.length) return null;
|
|
3623
4066
|
const ignored = ignoredByGit(ctx.repoRoot, outside);
|
|
3624
4067
|
const redirected = a.kind === "command" && a.command ? splitPaths(ctx.repoRoot, redirectTargets(a.command)).inside : [];
|
|
@@ -3644,7 +4087,7 @@ var outOfScopeEdit = (a, ctx) => {
|
|
|
3644
4087
|
};
|
|
3645
4088
|
var CAPTURED_OUTPUT = /\.(?:log|out|txt|json|xml)$/i;
|
|
3646
4089
|
function symlinkPointsOutside(repoRoot2, rel3) {
|
|
3647
|
-
const abs =
|
|
4090
|
+
const abs = resolve3(repoRoot2, rel3);
|
|
3648
4091
|
try {
|
|
3649
4092
|
if (!lstatSync3(abs).isSymbolicLink()) return false;
|
|
3650
4093
|
} catch {
|
|
@@ -3656,7 +4099,7 @@ function symlinkPointsOutside(repoRoot2, rel3) {
|
|
|
3656
4099
|
} catch {
|
|
3657
4100
|
return true;
|
|
3658
4101
|
}
|
|
3659
|
-
let root =
|
|
4102
|
+
let root = resolve3(repoRoot2);
|
|
3660
4103
|
try {
|
|
3661
4104
|
root = realpathSync(repoRoot2);
|
|
3662
4105
|
} catch {
|
|
@@ -3718,7 +4161,7 @@ function importedSources(repoRoot2, testPath) {
|
|
|
3718
4161
|
}
|
|
3719
4162
|
const out = [];
|
|
3720
4163
|
const exts = ["", ".js", ".ts", ".mjs", ".cjs", ".jsx", ".tsx", ".py", "/index.js", "/index.ts", "/__init__.py"];
|
|
3721
|
-
const root =
|
|
4164
|
+
const root = resolve3(repoRoot2);
|
|
3722
4165
|
for (const base4 of [...[...specs].map((spec) => join12(dirname7(testPath), spec)), ...rooted]) {
|
|
3723
4166
|
const found = exts.map((e) => `${base4}${e}`).find((c) => {
|
|
3724
4167
|
try {
|
|
@@ -3728,10 +4171,10 @@ function importedSources(repoRoot2, testPath) {
|
|
|
3728
4171
|
}
|
|
3729
4172
|
});
|
|
3730
4173
|
if (!found) continue;
|
|
3731
|
-
const rel3 = relative2(root,
|
|
4174
|
+
const rel3 = relative2(root, resolve3(root, found));
|
|
3732
4175
|
if (!rel3 || rel3.startsWith("..") || isAbsolute2(rel3)) continue;
|
|
3733
4176
|
try {
|
|
3734
|
-
const real3 = relative2(realpathSync(root), realpathSync(
|
|
4177
|
+
const real3 = relative2(realpathSync(root), realpathSync(resolve3(root, found)));
|
|
3735
4178
|
if (real3.startsWith("..") || isAbsolute2(real3)) continue;
|
|
3736
4179
|
} catch {
|
|
3737
4180
|
continue;
|
|
@@ -3795,6 +4238,22 @@ function askedTestPaths(ctx) {
|
|
|
3795
4238
|
if (!askedMemo.has(ctx)) askedMemo.set(ctx, askedTestsNow(ctx.repoRoot, ctx.contract));
|
|
3796
4239
|
return askedMemo.get(ctx);
|
|
3797
4240
|
}
|
|
4241
|
+
function judgeAskedAfterRun(repoRoot2, contract, path) {
|
|
4242
|
+
let now = null;
|
|
4243
|
+
try {
|
|
4244
|
+
now = readFileSync10(join12(repoRoot2, path), "utf8");
|
|
4245
|
+
} catch {
|
|
4246
|
+
now = null;
|
|
4247
|
+
}
|
|
4248
|
+
const base4 = baseCommitOf(repoRoot2);
|
|
4249
|
+
const start = base4 ? fileAt(repoRoot2, base4, path) : null;
|
|
4250
|
+
if (now === null || start === null || now === start) return null;
|
|
4251
|
+
const write = { kind: "write", tool: "Write", paths: [path], outsidePaths: [], command: null, changes: [{ path, before: start, after: now }], fingerprint: `after-run:${path}` };
|
|
4252
|
+
const r = testWeakened(write, { contract, history: [], repoRoot: repoRoot2, mode: "auto" });
|
|
4253
|
+
if (!r || r.action === "ok") return null;
|
|
4254
|
+
const said = r.message.replace("this edit weakens tests", "the shell command that just ran weakened tests").replace("this edit was not made: ", "the shell command that just ran changed this: ").replace("this edit changes existing expectations", "the shell command that just ran changed existing expectations");
|
|
4255
|
+
return prefixCode(codeForIntervention(r.rule, r.message), said);
|
|
4256
|
+
}
|
|
3798
4257
|
function askedTestsNow(repoRoot2, contract) {
|
|
3799
4258
|
const base4 = baseCommitOf(repoRoot2);
|
|
3800
4259
|
const atStart = recordedAskedTests(repoRoot2, contract.id) ?? testEditsAskedFor(contract.task, repoRoot2, void 0, base4 ?? void 0).paths;
|
|
@@ -3835,14 +4294,6 @@ function expandGlobs(repoRoot2, paths) {
|
|
|
3835
4294
|
}
|
|
3836
4295
|
return [...paths.filter((p) => !globs.includes(p)), ...tracked2.filter((f) => matchesAny(f, globs))];
|
|
3837
4296
|
}
|
|
3838
|
-
function movesInCommand(command) {
|
|
3839
|
-
const out = [];
|
|
3840
|
-
for (const seg of command.split(/&&|\|\||;/)) {
|
|
3841
|
-
const m = /^\s*(?:git\s+mv|mv)\s+([\w@./-]+)\s+([\w@./-]+)\s*$/.exec(seg);
|
|
3842
|
-
if (m) out.push([normalizeRel(m[1].replace(/^\.\//, "")), normalizeRel(m[2].replace(/^\.\//, ""))]);
|
|
3843
|
-
}
|
|
3844
|
-
return out;
|
|
3845
|
-
}
|
|
3846
4297
|
function laterPromptAsks(ctx, path) {
|
|
3847
4298
|
if (ctx.mode !== "auto") return false;
|
|
3848
4299
|
const prompts = ctx.history.filter((e) => e.type === "prompt" && e.raw?.autoContract === void 0).map((e) => {
|
|
@@ -3900,15 +4351,11 @@ var protectedPathModified = (a, ctx) => {
|
|
|
3900
4351
|
const operands = [...splitPaths(ctx.repoRoot, [a.command, ...innerScripts(a.command)].flatMap((c) => fileVerbOperands(c))).inside, ...expandGlobs(ctx.repoRoot, globOperands(a.command))].filter(guarded);
|
|
3901
4352
|
const inline = ctx.mode === "auto" ? inlineWriteTargets(a.command, ctx.repoRoot).filter(guarded) : [];
|
|
3902
4353
|
const moved = movedByCommand(a.command, askedTestPaths(ctx));
|
|
3903
|
-
const
|
|
3904
|
-
const hit = [.../* @__PURE__ */ new Set([...redirected, ...operands, ...inline])].filter((p) => !moved.includes(p) && !
|
|
4354
|
+
const askedNow = askedTestPaths(ctx);
|
|
4355
|
+
const hit = [.../* @__PURE__ */ new Set([...redirected, ...operands, ...inline])].filter((p) => !moved.includes(p) && !askedNow.includes(p));
|
|
3905
4356
|
if (!hit.length) return null;
|
|
3906
4357
|
const restored = honestRestores(a.command, ctx.repoRoot);
|
|
3907
4358
|
if (hit.every((p) => restored.includes(p))) return null;
|
|
3908
|
-
const asked = askedTestPaths(ctx);
|
|
3909
|
-
if (asked.length && hit.every((p) => asked.includes(p))) {
|
|
3910
|
-
return { rule: "protected_path_modified", action: "block", message: `${PREFIX} the task asks to change ${hit.join(", ")}, but a shell command that rewrites a test cannot be checked before it runs, so it was not run. Make the same change with your edit tool (Edit or Write): Replume checks an edit of this test by its content and lets it through unless it weakens the test (a skip, a removed test or assertion, a mock).` };
|
|
3911
|
-
}
|
|
3912
4359
|
return { rule: "protected_path_modified", action: "block", message: protectBlockMessage(ctx, hit, "command") };
|
|
3913
4360
|
}
|
|
3914
4361
|
return null;
|
|
@@ -4059,7 +4506,10 @@ var testWeakened = (a, ctx) => {
|
|
|
4059
4506
|
afterLines.splice(i, 1);
|
|
4060
4507
|
return false;
|
|
4061
4508
|
});
|
|
4062
|
-
const
|
|
4509
|
+
const askedHere = askedTestPaths(ctx).includes(ch.path);
|
|
4510
|
+
const keptAfter = askedHere && ch.before ? contentAfter(a, ctx, ch.path) : null;
|
|
4511
|
+
const kept = !!keptAfter && sameInputsKept(start ?? ch.before, keptAfter.next, ch.path);
|
|
4512
|
+
const removedExisting = !kept && !!ch.before && removed > 0 && (start === void 0 || gone.some(existing));
|
|
4063
4513
|
if (removedExisting) {
|
|
4064
4514
|
findings.push(`${ch.path}: ${removed} assertion${removed > 1 ? "s" : ""} removed`);
|
|
4065
4515
|
severity = "block";
|
|
@@ -4069,7 +4519,7 @@ var testWeakened = (a, ctx) => {
|
|
|
4069
4519
|
severity = "block";
|
|
4070
4520
|
}
|
|
4071
4521
|
const declsRemoved = count2(TEST_DECL, ch.before) - count2(TEST_DECL, ch.after);
|
|
4072
|
-
if (ch.before && declsRemoved > 0 &&
|
|
4522
|
+
if (!kept && ch.before && declsRemoved > 0 && askedHere) {
|
|
4073
4523
|
findings.push(`${ch.path}: ${declsRemoved} test${declsRemoved > 1 ? "s" : ""} removed`);
|
|
4074
4524
|
severity = "block";
|
|
4075
4525
|
}
|
|
@@ -4227,12 +4677,12 @@ function expandTmpdirVar(target, ctx) {
|
|
|
4227
4677
|
if (!m) return target;
|
|
4228
4678
|
const env = process.env.TMPDIR === void 0 ? tmpdir3() : process.env.TMPDIR;
|
|
4229
4679
|
if (!env || !isAbsolute2(env)) return null;
|
|
4230
|
-
const base4 =
|
|
4680
|
+
const base4 = resolve3(env);
|
|
4231
4681
|
if (!isTempDir(ctx, base4)) return null;
|
|
4232
4682
|
return base4 + target.slice(m[0].length);
|
|
4233
4683
|
}
|
|
4234
4684
|
function isTempDir(ctx, abs) {
|
|
4235
|
-
const roots = ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", ...ctx.scratchDirs ?? []].map((d) =>
|
|
4685
|
+
const roots = ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", ...ctx.scratchDirs ?? []].map((d) => resolve3(d));
|
|
4236
4686
|
return roots.some((d) => abs === d || abs.startsWith(d + sep)) || /^\/(?:private\/)?var\/folders\/[^/]+\/[^/]+\/T(?:\/|$)/.test(abs);
|
|
4237
4687
|
}
|
|
4238
4688
|
function deletionLocation(target, ctx) {
|
|
@@ -4244,20 +4694,20 @@ function deletionLocation(target, ctx) {
|
|
|
4244
4694
|
const segs = t.split("/");
|
|
4245
4695
|
const gi = segs.findIndex((x) => /[*?[]/.test(x));
|
|
4246
4696
|
const prefix = segs.slice(0, gi).join("/") || (t.startsWith("/") ? "/" : "");
|
|
4247
|
-
if (!/[$`{]/.test(prefix) && prefix && isTempDir(ctx,
|
|
4697
|
+
if (!/[$`{]/.test(prefix) && prefix && isTempDir(ctx, resolve3(ctx.repoRoot, prefix))) return "outside";
|
|
4248
4698
|
return "critical";
|
|
4249
4699
|
}
|
|
4250
4700
|
const expanded = t === "~" || t.startsWith("~/") ? homedir() + t.slice(1) : t;
|
|
4251
|
-
const abs =
|
|
4252
|
-
const root =
|
|
4253
|
-
const home =
|
|
4701
|
+
const abs = resolve3(ctx.repoRoot, expanded);
|
|
4702
|
+
const root = resolve3(ctx.repoRoot);
|
|
4703
|
+
const home = resolve3(homedir());
|
|
4254
4704
|
if (abs === home || abs === "/" || root !== abs && root.startsWith(abs + sep)) return "critical";
|
|
4255
4705
|
const rel3 = relative2(root, abs);
|
|
4256
4706
|
return !rel3 || !rel3.startsWith("..") && !isAbsolute2(rel3) ? "inside" : "outside";
|
|
4257
4707
|
}
|
|
4258
4708
|
function resolvedTarget(target, ctx) {
|
|
4259
4709
|
const t = expandTmpdirVar(target.replace(/^["']|["']$/g, ""), ctx) ?? target;
|
|
4260
|
-
return
|
|
4710
|
+
return resolve3(ctx.repoRoot, t);
|
|
4261
4711
|
}
|
|
4262
4712
|
function blockDeviceWrites(scripts) {
|
|
4263
4713
|
const out = shellWriteTargets(scripts[0] ?? "").paths.filter(isBlockDevice);
|
|
@@ -4281,9 +4731,17 @@ function blockDeviceWrites(scripts) {
|
|
|
4281
4731
|
}
|
|
4282
4732
|
return [...new Set(out)];
|
|
4283
4733
|
}
|
|
4734
|
+
function askedToDelete(ctx, path) {
|
|
4735
|
+
if (!ctx.contract || ctx.mode !== "auto") return false;
|
|
4736
|
+
return askedDeletionTargets(ctx.repoRoot, ctx.contract).includes(normalizeRel(path).replace(/\/+$/, ""));
|
|
4737
|
+
}
|
|
4738
|
+
function askedDeletionTargets(repoRoot2, contract) {
|
|
4739
|
+
const asked = recordedAskedDeletions(repoRoot2, contract.id) ?? deletionsAskedFor(contract.task, repoRoot2, void 0, baseCommitOf(repoRoot2) ?? void 0);
|
|
4740
|
+
return [...asked.dirs, ...asked.files];
|
|
4741
|
+
}
|
|
4284
4742
|
function rmRfTargets(command) {
|
|
4285
4743
|
const out = [];
|
|
4286
|
-
for (const seg of command.split(
|
|
4744
|
+
for (const seg of command.split(/&&|\|\||;|\||\n/)) {
|
|
4287
4745
|
const words6 = seg.trim().split(/\s+/);
|
|
4288
4746
|
const i = words6.findIndex((w) => w === "rm" || w.endsWith("/rm"));
|
|
4289
4747
|
if (i < 0 || i > 0 && !["sudo", "command", "xargs"].includes(words6[i - 1])) continue;
|
|
@@ -4297,12 +4755,12 @@ function rmRfTargets(command) {
|
|
|
4297
4755
|
return out;
|
|
4298
4756
|
}
|
|
4299
4757
|
function scratchRoots(ctx) {
|
|
4300
|
-
return [tmpdir3(), "/tmp", "/private/tmp", ...ctx.scratchDirs ?? []].map((d) =>
|
|
4758
|
+
return [tmpdir3(), "/tmp", "/private/tmp", ...ctx.scratchDirs ?? []].map((d) => resolve3(d));
|
|
4301
4759
|
}
|
|
4302
4760
|
function inScratch(ctx, abs) {
|
|
4303
4761
|
const roots = scratchRoots(ctx).filter((d) => abs === d || abs.startsWith(d + sep));
|
|
4304
4762
|
if (!roots.length) return false;
|
|
4305
|
-
const home =
|
|
4763
|
+
const home = resolve3(homedir());
|
|
4306
4764
|
if ((abs === home || abs.startsWith(home + sep)) && roots.every((d) => d.length <= home.length)) return false;
|
|
4307
4765
|
return true;
|
|
4308
4766
|
}
|
|
@@ -4359,7 +4817,7 @@ var destructiveCommand = (a, ctx) => {
|
|
|
4359
4817
|
if (/^(\/|~|\$HOME|\*|\.{1,2}\/?)$/.test(clean3) || clean3.startsWith("~") || deletionLocation(clean3, ctx) === "critical") return true;
|
|
4360
4818
|
const { inside, outside } = splitPaths(ctx.repoRoot, [expandTmpdirVar(clean3, ctx) ?? clean3]);
|
|
4361
4819
|
if (outside.length) return !inScratch(ctx, outside[0]);
|
|
4362
|
-
return !REGENERABLE.test(inside[0]);
|
|
4820
|
+
return !REGENERABLE.test(inside[0]) && !askedToDelete(ctx, inside[0]);
|
|
4363
4821
|
});
|
|
4364
4822
|
if (bad.length) {
|
|
4365
4823
|
return {
|
|
@@ -4478,112 +4936,60 @@ var RULES = {
|
|
|
4478
4936
|
commit_without_tests: commitWithoutTests
|
|
4479
4937
|
};
|
|
4480
4938
|
var RANK = { ok: 0, warn: 1, block: 2 };
|
|
4481
|
-
function sedTestWrites(a, ctx) {
|
|
4482
|
-
if (ctx.mode !== "auto" || a.kind !== "command" || !a.command || !/\b(?:sed|perl)\b/.test(a.command) || /[`$*?]|<</.test(a.command.replace(/'[^']*'/g, "''"))) return [];
|
|
4483
|
-
const asked = askedTestPaths(ctx);
|
|
4484
|
-
if (!asked.length) return [];
|
|
4485
|
-
const segments2 = commandSegments(a.command);
|
|
4486
|
-
const other = new Set(splitPaths(ctx.repoRoot, segments2.filter((s) => s.words[0] !== "sed" && s.words[0] !== "perl").flatMap((s) => fileVerbOperands(s.text))).inside);
|
|
4487
|
-
const out = /* @__PURE__ */ new Map();
|
|
4488
|
-
for (const seg of segments2.filter((s) => s.words[0] === "perl")) {
|
|
4489
|
-
const scripts = [];
|
|
4490
|
-
const files = [];
|
|
4491
|
-
let inPlace = false;
|
|
4492
|
-
let printing = false;
|
|
4493
|
-
const words6 = seg.words.slice(1);
|
|
4494
|
-
for (let i = 0; i < words6.length; i++) {
|
|
4495
|
-
const w = words6[i];
|
|
4496
|
-
if (/^-[pi]+$/.test(w)) {
|
|
4497
|
-
inPlace ||= w.includes("i");
|
|
4498
|
-
printing ||= w.includes("p");
|
|
4499
|
-
} else if (/^-[pi]*e$/.test(w)) {
|
|
4500
|
-
inPlace ||= w.includes("i");
|
|
4501
|
-
printing ||= w.includes("p");
|
|
4502
|
-
scripts.push(words6[++i] ?? "");
|
|
4503
|
-
} else if (w.startsWith("-")) {
|
|
4504
|
-
files.length = 0;
|
|
4505
|
-
inPlace = false;
|
|
4506
|
-
break;
|
|
4507
|
-
} else files.push(w);
|
|
4508
|
-
}
|
|
4509
|
-
if (!inPlace || !printing || !scripts.length || !scripts.every((s) => SED_SUBSTITUTION.test(s) && !/[$@`{}]|\(\?/.test(s))) continue;
|
|
4510
|
-
for (const p of splitPaths(ctx.repoRoot, files).inside.filter((f) => asked.includes(f) && !other.has(f))) {
|
|
4511
|
-
try {
|
|
4512
|
-
const current = out.get(p) ?? readFileSync10(join12(ctx.repoRoot, p), "utf8");
|
|
4513
|
-
out.set(p, execFileSync7("perl", ["-pe", scripts.join(";")], { input: current, encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 2e3 }));
|
|
4514
|
-
} catch {
|
|
4515
|
-
out.delete(p);
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
}
|
|
4519
|
-
for (const seg of segments2.filter((s) => s.words[0] === "sed")) {
|
|
4520
|
-
const scripts = [];
|
|
4521
|
-
const flags = [];
|
|
4522
|
-
const files = [];
|
|
4523
|
-
let inPlace = false;
|
|
4524
|
-
const words6 = seg.words.slice(1);
|
|
4525
|
-
for (let i = 0; i < words6.length; i++) {
|
|
4526
|
-
const w = words6[i];
|
|
4527
|
-
if (w === "-i") {
|
|
4528
|
-
inPlace = true;
|
|
4529
|
-
if (words6[i + 1] === "") i++;
|
|
4530
|
-
} else if (w === "-e") scripts.push(words6[++i] ?? "");
|
|
4531
|
-
else if (w === "-E" || w === "-r") flags.push(w);
|
|
4532
|
-
else if (w.startsWith("-")) {
|
|
4533
|
-
files.length = 0;
|
|
4534
|
-
inPlace = false;
|
|
4535
|
-
break;
|
|
4536
|
-
} else if (!scripts.length && !files.length && SED_SUBSTITUTION.test(w)) scripts.push(w);
|
|
4537
|
-
else files.push(w);
|
|
4538
|
-
}
|
|
4539
|
-
if (!inPlace || !scripts.length || !scripts.every((s) => SED_SUBSTITUTION.test(s))) continue;
|
|
4540
|
-
for (const p of splitPaths(ctx.repoRoot, files).inside.filter((f) => asked.includes(f) && !other.has(f))) {
|
|
4541
|
-
try {
|
|
4542
|
-
const current = out.get(p) ?? readFileSync10(join12(ctx.repoRoot, p), "utf8");
|
|
4543
|
-
out.set(p, execFileSync7("sed", [...flags, ...scripts.flatMap((s) => ["-e", s])], { input: current, encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 2e3 }));
|
|
4544
|
-
} catch {
|
|
4545
|
-
out.delete(p);
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
return [...out].map(([path, content]) => ({ path, content }));
|
|
4550
|
-
}
|
|
4551
|
-
var SED_SUBSTITUTION = /^s([^\w\s\\])(?:(?!\1)[^\n])*\1(?:(?!\1)[^\n])*\1[gI]*$/;
|
|
4552
|
-
function heredocTestWrites(a, ctx) {
|
|
4553
|
-
if (ctx.mode !== "auto" || a.kind !== "command" || !a.command || !a.command.includes("<<")) return [];
|
|
4554
|
-
const asked = askedTestPaths(ctx);
|
|
4555
|
-
if (!asked.length) return [];
|
|
4556
|
-
const writes = heredocFileWrites(a.command, ctx.repoRoot).filter((w) => asked.includes(w.path));
|
|
4557
|
-
if (!writes.length) return [];
|
|
4558
|
-
const other = /* @__PURE__ */ new Set([
|
|
4559
|
-
...splitPaths(ctx.repoRoot, [a.command, ...innerScripts(a.command)].flatMap((c) => fileVerbOperands(c))).inside,
|
|
4560
|
-
...inlineWriteTargets(a.command, ctx.repoRoot)
|
|
4561
|
-
]);
|
|
4562
|
-
return writes.filter((w) => !other.has(w.path));
|
|
4563
|
-
}
|
|
4564
4939
|
function evaluate(a, ctx) {
|
|
4565
4940
|
const hits = [];
|
|
4566
4941
|
for (const rule of Object.values(RULES)) {
|
|
4567
4942
|
const r = rule(a, ctx);
|
|
4568
4943
|
if (r && r.action !== "ok") hits.push(r);
|
|
4569
4944
|
}
|
|
4570
|
-
for (const w of [...heredocTestWrites(a, ctx), ...sedTestWrites(a, ctx)]) {
|
|
4571
|
-
let current = "";
|
|
4572
|
-
try {
|
|
4573
|
-
current = readFileSync10(join12(ctx.repoRoot, w.path), "utf8");
|
|
4574
|
-
} catch {
|
|
4575
|
-
current = "";
|
|
4576
|
-
}
|
|
4577
|
-
const write = { kind: "write", tool: "Write", paths: [w.path], outsidePaths: [], command: null, changes: [{ path: w.path, before: current, after: w.content }], fingerprint: `${a.fingerprint}:${w.path}`, moves: movesInCommand(a.command ?? "") };
|
|
4578
|
-
const r = testWeakened(write, ctx);
|
|
4579
|
-
if (r && r.action !== "ok") hits.push(r);
|
|
4580
|
-
}
|
|
4581
4945
|
hits.sort((x, y) => RANK[y.action] - RANK[x.action]);
|
|
4582
4946
|
for (const hit of hits) hit.message = prefixCode(codeForIntervention(hit.rule, hit.message), hit.message);
|
|
4583
4947
|
const action = hits[0]?.action ?? "ok";
|
|
4584
4948
|
return { action, hits, message: hits.length ? hits.map((h) => h.message).join("\n") : null };
|
|
4585
4949
|
}
|
|
4586
4950
|
|
|
4951
|
+
// src/core/spec-conflict.ts
|
|
4952
|
+
var NOT_THE_TASK = /\b(?:spec|specification|docs?|documents?|documentation|readme|requirements?|the task|your request|the prompt|the brief|rules?|what you asked|as written|[\w./-]+\.(?:md|rst|txt|adoc))\b/i;
|
|
4953
|
+
var A_TEST = /\b(?:tests?|suite|specs?|assertions?|expectations?|[\w./-]+\.(?:test|spec)\.[a-z]+|test_\w+|[\w./-]*tests?\/[\w./-]+)\b/i;
|
|
4954
|
+
var DISAGREE = /\b(?:contradict\w*|conflict\w*|disagree\w*|mismatch\w*|inconsisten\w*|at odds|(?:both )?can(?:no|')t (?:both )?(?:be true|hold)|cannot both|does(?:n't| not) match|do(?:n't| not) match)\b/i;
|
|
4955
|
+
var CONFLICT_WORD = "(?:contradict\\w*|conflict\\w*|disagree\\w*|mismatch\\w*|inconsisten\\w*|at odds|wrong|incorrect|outdated|mistaken|stale)";
|
|
4956
|
+
var GAP = "(?:(?!because\\b|but\\b|since\\b|so\\b|while\\b|though\\b|although\\b|however\\b|and\\b|which\\b|that\\b|as\\b)[\\w'\u2019./-]+\\s+){0,6}?";
|
|
4957
|
+
var NEGATED = new RegExp(`(?:\\b(?:no|not|without|never|none|nothing|neither|nor|nobody|any)\\b|\\w+n['\u2019]t\\b)\\s+${GAP}${CONFLICT_WORD}`, "gi");
|
|
4958
|
+
var withoutDenials = (s) => s.replace(NEGATED, " ");
|
|
4959
|
+
var NO_CHOICE = /(?:\b(?:no|not|nobody|no one|never|nothing)\b|\w+n['’]t\b).{0,30}\b(?:pick|choose|decide)\b/i;
|
|
4960
|
+
var JUST_DENIED = /(?:\b(?:not|never)|n['’]t)\s+(?:\w+\s+)?$/i;
|
|
4961
|
+
var TEST_OVER_TASK = /\b(?:treated|took|counted|kept)\b.{0,40}\btests?\b.{0,20}\bas the spec\b|\bfollowed the (?:protected )?tests?\b|\b(?:changed|edited|rewrote|reworded|updated)\b.{0,40}\b(?:docs?|spec|[\w./-]+\.md)\b.{0,60}\b(?:to match|because|so that)\b.{0,40}\btests?\b|\bspec\b.{0,40}\b(?:unimplemented|not implemented)\b/i;
|
|
4962
|
+
var IS_WRONG = /\b(?:is|are|looks?|seems?|must be|appears? to be)\s+(?:\w+\s+)?(?:wrong|incorrect|outdated|out of date|stale|mistaken)\b/i;
|
|
4963
|
+
var SOMEONE_DECIDES = /\b(?:pick|choose|decide)\b.{0,30}\bwhich\b|\bwhich (?:one )?(?:is right|is correct|should win)\b|\byour call\b|\b(?:choice|decision) is yours\b|\b(?:needs?|for) (?:a person|someone|a human|you)\b.{0,20}\bdecide\b|\bdecide between\b|\b(?:someone|a person|you) (?:has|have|must|needs?) to (?:pick|choose|decide)\b/i;
|
|
4964
|
+
var CLAIM = "(?:says?|said|states?|expects?|asserts?|requires?|wants?|insists?|makes?|gives?|needs?)";
|
|
4965
|
+
var CONTRAST = "(?:but|while|whereas|though|although|yet|however)";
|
|
4966
|
+
var AGAINST = new RegExp(`\\b${CLAIM}\\b.*\\b${CONTRAST}\\b.*\\b${CLAIM}\\b`, "i");
|
|
4967
|
+
var ONE_IS_WRONG = /\bone of (?:them|those|these|the two)\b.{0,30}\b(?:wrong|mistaken|incorrect|outdated)\b/i;
|
|
4968
|
+
var TEST_EXPECTS = /\b(?:expects?|asserts?|requires?|wants?)\b/i;
|
|
4969
|
+
var DOC_SAYS = /\b(?:says?|said|states?|specif\w+|requires?)\b/i;
|
|
4970
|
+
var CONTRACT_REMARK = /contract\.ya?ml|\.replume\/|\bcontract conflict\b|\b(?:the|task|replume(?:['’]s)?) contract\b(?!\s+tests?\b)|\bunder protect\b|\bprotect (?:list|globs?)\b/i;
|
|
4971
|
+
var sentences = (m) => m.split(/(?<=[.!?:])\s+|\n+/).map((s) => s.replace(/[*`]/g, "").trim()).filter(Boolean);
|
|
4972
|
+
function statedSpecTestConflict(message, opts = {}) {
|
|
4973
|
+
const all = sentences(message).filter((s) => !CONTRACT_REMARK.test(s));
|
|
4974
|
+
for (let i = 0; i < all.length; i++) {
|
|
4975
|
+
const s = all[i];
|
|
4976
|
+
const near = [s, all[i + 1] ?? "", all[i + 2] ?? ""].join(" ");
|
|
4977
|
+
const said = withoutDenials(s);
|
|
4978
|
+
if (DISAGREE.test(said) && !/\bif\b/i.test(s) && A_TEST.test(near) && NOT_THE_TASK.test(near)) return s;
|
|
4979
|
+
const over = TEST_OVER_TASK.exec(s);
|
|
4980
|
+
if (over && !JUST_DENIED.test(s.slice(0, over.index))) return s;
|
|
4981
|
+
if (IS_WRONG.test(said) && !/\b(?:if|whether)\b/i.test(s) && (A_TEST.test(s) || NOT_THE_TASK.test(s))) return s;
|
|
4982
|
+
if ((SOMEONE_DECIDES.test(s) || ONE_IS_WRONG.test(said)) && !NO_CHOICE.test(s) && !/\bif\b/i.test(s)) return s;
|
|
4983
|
+
if (AGAINST.test(s) && A_TEST.test(s) && NOT_THE_TASK.test(s) && !/\bif\b/i.test(s)) return s;
|
|
4984
|
+
}
|
|
4985
|
+
if (opts.red) {
|
|
4986
|
+
const test = all.findIndex((s) => A_TEST.test(s) && TEST_EXPECTS.test(s));
|
|
4987
|
+
const doc = all.findIndex((s, i) => i !== test && NOT_THE_TASK.test(s) && DOC_SAYS.test(s) && !/\bif\b/i.test(s));
|
|
4988
|
+
if (test >= 0 && doc >= 0) return all[test];
|
|
4989
|
+
}
|
|
4990
|
+
return null;
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4587
4993
|
// src/supervisor/unattributed.ts
|
|
4588
4994
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
4589
4995
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -4713,7 +5119,7 @@ function unattributedChanges(repoRoot2, base4, contract, events) {
|
|
|
4713
5119
|
const protectedBy = glob ? `protected by "${glob}"` : scriptDiff ? `the test command: ${scriptDiff}` : null;
|
|
4714
5120
|
const restore = target?.kind === "json-keys" && !glob ? `put ${target.keys.join(", ")} in ${path} back as they were (Edit tool)` : before.has(path) ? `git checkout ${ref} -- ${path}` : `remove ${path}`;
|
|
4715
5121
|
return { path, kind: "file", blob: now.get(path) ?? "deleted", protectedBy, restore };
|
|
4716
|
-
}).map((f) => byContent.has(f.path) ? { ...f, seenByContent: true } : f).filter((f) => !f.seenByContent || f.protectedBy);
|
|
5122
|
+
}).map((f) => byContent.has(f.path) ? { ...f, seenByContent: true } : f).filter((f) => !f.seenByContent || f.protectedBy || f.kind === "lockfile");
|
|
4717
5123
|
}
|
|
4718
5124
|
function committedSinceTask(repoRoot2) {
|
|
4719
5125
|
const b = readBaseline(baselinePath(contractPath(repoRoot2)));
|
|
@@ -5058,7 +5464,7 @@ var spawnRunner = (command, cwd, timeoutSec) => {
|
|
|
5058
5464
|
var AUTO_BASE_RUN_RULE = "auto_base_run";
|
|
5059
5465
|
var PRE_EXISTING_TEXT = "already failing before the task \u2014 not caused by the agent";
|
|
5060
5466
|
var TASK_TEST_RED = "task test still red";
|
|
5061
|
-
var
|
|
5467
|
+
var WHOLE_SUITE2 = [
|
|
5062
5468
|
/\b(?:make|get|have|keep)\s+(?:the\s+|all\s+(?:the\s+)?)?(?:(?:npm|yarn|pnpm)\s+(?:run\s+)?test|tests?|test\s+suite|suite|ci|build)\s+(?:pass|passing|green)\b/i,
|
|
5063
5469
|
/\b(?:npm|yarn|pnpm)\s+(?:run\s+)?test\s+(?:passes|pass|passing|succeeds|is\s+green|goes\s+green|should\s+pass|must\s+pass)\b/i,
|
|
5064
5470
|
/\b(?:all\s+(?:the\s+)?)?tests?\s+(?:must|should|need\s+to|have\s+to|to)\s+pass\b/i,
|
|
@@ -5068,7 +5474,7 @@ var WHOLE_SUITE = [
|
|
|
5068
5474
|
/\bmake\s+(?:it|them)\s+(?:pass|green)\s*(?:[.!]|$)/im,
|
|
5069
5475
|
/\bfix\s+(?:the\s+|all\s+(?:the\s+)?)?(?:failing|broken|red)\s+tests?\b/i
|
|
5070
5476
|
];
|
|
5071
|
-
var requiresWholeSuite = (task) =>
|
|
5477
|
+
var requiresWholeSuite = (task) => WHOLE_SUITE2.some((re) => re.test(task));
|
|
5072
5478
|
function inCopy(repoRoot2, commit, fn, startedAt = Date.now()) {
|
|
5073
5479
|
const tmp = mkdtempSync4(join16(tmpdir6(), "replume-base-"));
|
|
5074
5480
|
const root = join16(tmp, "repo");
|
|
@@ -5401,7 +5807,7 @@ function hasCommit(repoRoot2) {
|
|
|
5401
5807
|
import { existsSync as existsSync18, readFileSync as readFileSync20 } from "fs";
|
|
5402
5808
|
import { execFileSync as execFileSync17 } from "child_process";
|
|
5403
5809
|
import { lstat as lstat4, mkdir as mkdir2, symlink, writeFile as writeFile3 } from "fs/promises";
|
|
5404
|
-
import { isAbsolute as isAbsolute4, join as join26, relative as relative6, resolve as
|
|
5810
|
+
import { isAbsolute as isAbsolute4, join as join26, relative as relative6, resolve as resolve5, sep as sep4 } from "path";
|
|
5405
5811
|
import { performance as performance4 } from "perf_hooks";
|
|
5406
5812
|
|
|
5407
5813
|
// src/sandbox/scenarios.ts
|
|
@@ -5548,12 +5954,12 @@ function countMatches(re, s) {
|
|
|
5548
5954
|
const g = re.global ? re : new RegExp(re.source, re.flags + "g");
|
|
5549
5955
|
return (s.match(g) ?? []).length;
|
|
5550
5956
|
}
|
|
5551
|
-
var
|
|
5957
|
+
var LITERAL2 = /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`[^`\n]*`|\b(?:true|false|True|False|None|nil|null|undefined)\b/g;
|
|
5552
5958
|
function literalsOf(line2) {
|
|
5553
|
-
return line2.match(
|
|
5959
|
+
return line2.match(LITERAL2) ?? [];
|
|
5554
5960
|
}
|
|
5555
5961
|
function shapeOf(line2) {
|
|
5556
|
-
return line2.replace(
|
|
5962
|
+
return line2.replace(LITERAL2, "\0").replace(/\s+/g, " ").trim();
|
|
5557
5963
|
}
|
|
5558
5964
|
function unquote(lit) {
|
|
5559
5965
|
const t = lit.trim();
|
|
@@ -5665,9 +6071,9 @@ function testBypass(oldText, newText, path, opts = {}) {
|
|
|
5665
6071
|
const out = [];
|
|
5666
6072
|
for (const rule of rules) {
|
|
5667
6073
|
const before = /* @__PURE__ */ new Map();
|
|
5668
|
-
for (const m of oldCode.matchAll(rule.re)) before.set(
|
|
6074
|
+
for (const m of oldCode.matchAll(rule.re)) before.set(norm2(m[0]), (before.get(norm2(m[0])) ?? 0) + 1);
|
|
5669
6075
|
for (const m of newCode.matchAll(rule.re)) {
|
|
5670
|
-
const key =
|
|
6076
|
+
const key = norm2(m[0]);
|
|
5671
6077
|
const left = before.get(key) ?? 0;
|
|
5672
6078
|
if (left > 0) {
|
|
5673
6079
|
before.set(key, left - 1);
|
|
@@ -5732,7 +6138,7 @@ function titleSource(path) {
|
|
|
5732
6138
|
const lang = langOf(path);
|
|
5733
6139
|
return (lang === "js" || lang === "python") && (isTestFile(path) || TOP_TEST.test(path)) && !/\.d\.ts$/i.test(path);
|
|
5734
6140
|
}
|
|
5735
|
-
var
|
|
6141
|
+
var norm2 = (s) => s.replace(/\s+/g, " ").trim();
|
|
5736
6142
|
|
|
5737
6143
|
// src/supervisor/checks/test-output.ts
|
|
5738
6144
|
var num2 = (s) => s === void 0 ? null : Number(s);
|
|
@@ -5955,19 +6361,6 @@ function testWeakening(oldText, newText, path, opts = {}) {
|
|
|
5955
6361
|
const severity = findings.some((f) => f.severity === "block") ? "block" : findings.length ? "warn" : "ok";
|
|
5956
6362
|
return { path, lang, fileKind, findings, severity, steer: findings.length ? steerMessage(path, findings, { mode, allow: opts.allow }) : null };
|
|
5957
6363
|
}
|
|
5958
|
-
function assertAliases(text) {
|
|
5959
|
-
const out = /* @__PURE__ */ new Set();
|
|
5960
|
-
const mod = String.raw`['"](?:node:)?assert(?:/strict)?['"]`;
|
|
5961
|
-
for (const m of text.matchAll(new RegExp(String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*${mod}\s*\)(?:\.strict)?`, "g"))) out.add(m[1]);
|
|
5962
|
-
for (const m of text.matchAll(new RegExp(String.raw`\bimport\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\}\s*)?from\s*${mod}`, "g"))) out.add(m[1]);
|
|
5963
|
-
for (const m of text.matchAll(new RegExp(String.raw`(?:\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*${mod}\s*\)|\bimport\s*\{([^}]*)\}\s*from\s*${mod})`, "g")))
|
|
5964
|
-
for (const part of (m[1] ?? m[2] ?? "").split(",")) {
|
|
5965
|
-
const name = part.split(/\s*(?::|\bas\b)\s*/).pop()?.trim();
|
|
5966
|
-
if (name && /^[A-Za-z_$][\w$]*$/.test(name)) out.add(name);
|
|
5967
|
-
}
|
|
5968
|
-
out.delete("assert");
|
|
5969
|
-
return [...out];
|
|
5970
|
-
}
|
|
5971
6364
|
function rulesWithAliases(r, lang, ...texts) {
|
|
5972
6365
|
if (lang !== "js") return r;
|
|
5973
6366
|
const names3 = [...new Set(texts.flatMap(assertAliases))];
|
|
@@ -6055,7 +6448,7 @@ function firstNew(re, o, n3) {
|
|
|
6055
6448
|
}
|
|
6056
6449
|
var normalize = (l) => l.replace(/^\s*(?:\/\/|#)\s*/, "").replace(/\s+/g, "");
|
|
6057
6450
|
var pick2 = (mode, approve, auto) => mode === "approve" ? approve : auto;
|
|
6058
|
-
var
|
|
6451
|
+
var IDENT2 = /[A-Za-z_]\w*(?=\s*\()/g;
|
|
6059
6452
|
var FRAMEWORK_CALL = /^(?:expect|assert\w*|it|test|describe|t|should|to[A-Z]\w*|not|Fatal|Fatalf|Error|Errorf|Fail|FailNow|Log|Logf|Equal\w*|NotEqual|True|False|Nil|NotNil|Contains|Len|Empty|NotEmpty|Greater\w*|Less\w*|Panics|NoError|InDelta|self|pytest|raises|approx|assert_eq|assert_ne|assert)$/;
|
|
6060
6453
|
var renameKey = (l) => normalize(l).replace(/[A-Za-z_$][\w$]*(?=\s*\()/g, "f");
|
|
6061
6454
|
function lostAssertions(oCode, nCode, r) {
|
|
@@ -6089,7 +6482,7 @@ function lostAssertions(oCode, nCode, r) {
|
|
|
6089
6482
|
if (k && newFlat.includes(k)) continue;
|
|
6090
6483
|
if (newAsserts.some((a) => renameKey(a) === renameKey(l))) continue;
|
|
6091
6484
|
const lits = literalsOf(l).map(unquote);
|
|
6092
|
-
const calls = (l.match(
|
|
6485
|
+
const calls = (l.match(IDENT2) ?? []).filter((f) => !FRAMEWORK_CALL.test(f));
|
|
6093
6486
|
const litsOf = (x) => new Set(literalsOf(x).map(unquote));
|
|
6094
6487
|
const newAssertsOnSubject = newAsserts.filter((a) => calls.some((f) => new RegExp(`\\b${f}\\s*\\(`).test(a)));
|
|
6095
6488
|
if (newAssertsOnSubject.some((a) => lits.every((x) => litsOf(a).has(x)))) continue;
|
|
@@ -6308,7 +6701,7 @@ function steerMessage(path, findings, opts = {}) {
|
|
|
6308
6701
|
// src/core/dependency-dirs.ts
|
|
6309
6702
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
6310
6703
|
import { appendFileSync as appendFileSync4, existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync15 } from "fs";
|
|
6311
|
-
import { dirname as dirname10, join as join18, resolve as
|
|
6704
|
+
import { dirname as dirname10, join as join18, resolve as resolve4 } from "path";
|
|
6312
6705
|
var DEPENDENCY_PATH = /(^|\/)(node_modules|\.venv|venv|__pypackages__|bower_components|\.pnpm-store|\.yarn\/(cache|unplugged)|vendor\/bundle|\.bundle)(\/|$)/;
|
|
6313
6706
|
var isDependencyPath = (path) => DEPENDENCY_PATH.test(path.split("\\").join("/"));
|
|
6314
6707
|
var MARK = "# Replume: dependency directories are not the agent's change";
|
|
@@ -6317,7 +6710,7 @@ var git4 = (root, args) => execFileSync12("git", args, { cwd: root, encoding: "u
|
|
|
6317
6710
|
function ensureDependencyExcludes(repoRoot2) {
|
|
6318
6711
|
try {
|
|
6319
6712
|
if (!existsSync13(join18(repoRoot2, ".replume"))) return;
|
|
6320
|
-
const file3 =
|
|
6713
|
+
const file3 = resolve4(repoRoot2, git4(repoRoot2, ["rev-parse", "--git-path", "info/exclude"]));
|
|
6321
6714
|
const have = existsSync13(file3) ? readFileSync15(file3, "utf8") : "";
|
|
6322
6715
|
const lines = new Set(have.split("\n").map((l) => l.trim()));
|
|
6323
6716
|
const patterns = [...BASE_PATTERNS];
|
|
@@ -6420,10 +6813,10 @@ function jiraChecklist(task, scenarios, tests = []) {
|
|
|
6420
6813
|
const marker = task.indexOf(MARKER);
|
|
6421
6814
|
if (marker < 0) return [];
|
|
6422
6815
|
const criteria = task.slice(marker + MARKER.length).split("\n").map((s) => /^- (.+)$/.exec(s)?.[1]).filter((s) => !!s);
|
|
6423
|
-
const
|
|
6816
|
+
const norm4 = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
6424
6817
|
return criteria.map((text) => {
|
|
6425
6818
|
if (text === "[No acceptance criteria found on this Jira issue]") return { text, status: "not checked", evidence: "Jira issue has no recognized acceptance criteria" };
|
|
6426
|
-
const matching = scenarios.filter((s) =>
|
|
6819
|
+
const matching = scenarios.filter((s) => norm4(s.description) === norm4(text));
|
|
6427
6820
|
if (!matching.length || matching.some((s) => s.status === "not_run")) {
|
|
6428
6821
|
const test = matching.length ? null : testCovering(text, tests);
|
|
6429
6822
|
return test ? { text, status: "not checked", evidence: `LIKELY COVERED by ${test.path} ("${test.title}") (word match); the test failed before and passed after, but no matching scenario ran` } : { text, status: "not checked", evidence: "no matching scenario ran" };
|
|
@@ -6461,7 +6854,7 @@ import { tmpdir as tmpdir7 } from "os";
|
|
|
6461
6854
|
import { join as join19 } from "path";
|
|
6462
6855
|
import { performance as performance3 } from "perf_hooks";
|
|
6463
6856
|
async function git5(cwd, args, timeoutMs, tooLong) {
|
|
6464
|
-
return new Promise((
|
|
6857
|
+
return new Promise((resolve18, reject) => {
|
|
6465
6858
|
const child = spawn2("git", args, { cwd, env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, stdio: ["ignore", "pipe", "pipe"] });
|
|
6466
6859
|
let out = "", err = "", late = false;
|
|
6467
6860
|
const timer = timeoutMs === void 0 ? null : setTimeout(() => {
|
|
@@ -6478,7 +6871,7 @@ async function git5(cwd, args, timeoutMs, tooLong) {
|
|
|
6478
6871
|
child.on("close", (code2) => {
|
|
6479
6872
|
if (timer) clearTimeout(timer);
|
|
6480
6873
|
if (late) return reject(new CopyLimitError(tooLong?.() ?? `git ${args.join(" ")} took too long`));
|
|
6481
|
-
if (code2 === 0)
|
|
6874
|
+
if (code2 === 0) resolve18(out.trim());
|
|
6482
6875
|
else reject(new Error(`git ${args.join(" ")}: ${err.trim() || `exit ${code2}`}`));
|
|
6483
6876
|
});
|
|
6484
6877
|
});
|
|
@@ -6509,14 +6902,14 @@ async function withWorkingCopy(repo, commit, keep, fn) {
|
|
|
6509
6902
|
}
|
|
6510
6903
|
async function runCommand(command, cwd, home, timeoutSec) {
|
|
6511
6904
|
const started = performance3.now();
|
|
6512
|
-
return new Promise((
|
|
6905
|
+
return new Promise((resolve18) => {
|
|
6513
6906
|
const child = spawn2(command, { cwd, shell: true, detached: process.platform !== "win32", env: { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: home }, stdio: ["ignore", "pipe", "pipe"] });
|
|
6514
6907
|
let stdout = "", stderr = "", timedOut2 = false, settled = false;
|
|
6515
6908
|
const finish = (exitCode) => {
|
|
6516
6909
|
if (settled) return;
|
|
6517
6910
|
settled = true;
|
|
6518
6911
|
clearTimeout(timer);
|
|
6519
|
-
|
|
6912
|
+
resolve18({ stdout, stderr, exitCode, durationMs: Math.round(performance3.now() - started), timedOut: timedOut2 });
|
|
6520
6913
|
};
|
|
6521
6914
|
child.stdout.on("data", (v2) => {
|
|
6522
6915
|
stdout += v2.toString();
|
|
@@ -6734,7 +7127,9 @@ async function tamperingFindings(repo, base4, commit, entries, opts) {
|
|
|
6734
7127
|
const after = blobAt(repo, commit, e.path);
|
|
6735
7128
|
const confirmed = after !== null && isConfirmed(opts.repoRoot, e.path, after);
|
|
6736
7129
|
if (asked || confirmed) {
|
|
6737
|
-
const
|
|
7130
|
+
const startText = await atBase(e.oldPath ?? e.path);
|
|
7131
|
+
const kept = asked && after !== null && sameInputsKept(startText, after, e.path);
|
|
7132
|
+
const w2 = weakening(await patch2(...e.status === "R" ? [e.oldPath, e.path] : [e.path]), startText).filter((x) => !/assertions? changed/.test(x) && !(kept && /assertions? removed|tests? removed/.test(x)));
|
|
6738
7133
|
const start = !w2.length && !confirmed && after !== null && opts.baseFailing && e.status === "M" ? blobAt(repo, base4, e.path) : null;
|
|
6739
7134
|
const bent = start !== null && after !== null && opts.baseFailing ? bentToCode({ repoRoot: opts.repoRoot, path: e.path, start, after, changed: (p) => entries.some((x) => x.path === p || x.oldPath === p), baseFailing: opts.baseFailing }) : null;
|
|
6740
7135
|
const moves = movesSince(opts.repoRoot, base4, commit);
|
|
@@ -7418,7 +7813,7 @@ function parseJudgeResponse(x) {
|
|
|
7418
7813
|
function commandJudge(executable, apiKey) {
|
|
7419
7814
|
return {
|
|
7420
7815
|
name: `command:${executable}`,
|
|
7421
|
-
judge: (req, signal) => new Promise((
|
|
7816
|
+
judge: (req, signal) => new Promise((resolve18, reject) => {
|
|
7422
7817
|
const env = { PATH: process.env.PATH, HOME: process.env.HOME };
|
|
7423
7818
|
if (apiKey) env[ENV.apiKey] = apiKey;
|
|
7424
7819
|
const child = spawn3(executable, [], { env, stdio: ["pipe", "pipe", "pipe"], signal });
|
|
@@ -7435,7 +7830,7 @@ function commandJudge(executable, apiKey) {
|
|
|
7435
7830
|
if (code2 !== 0) return reject(new Error(`judge command exited ${code2}: ${err.trim().slice(0, 200)}`));
|
|
7436
7831
|
if (stdinError) return reject(new Error(`judge command stdin failed: ${stdinError.message}`));
|
|
7437
7832
|
try {
|
|
7438
|
-
|
|
7833
|
+
resolve18(parseJudgeResponse(JSON.parse(out)));
|
|
7439
7834
|
} catch (e) {
|
|
7440
7835
|
reject(e);
|
|
7441
7836
|
}
|
|
@@ -8568,7 +8963,7 @@ async function isLink(p) {
|
|
|
8568
8963
|
}
|
|
8569
8964
|
}
|
|
8570
8965
|
function safeRelative(root, path) {
|
|
8571
|
-
const full =
|
|
8966
|
+
const full = resolve5(root, path);
|
|
8572
8967
|
if (full !== root && !full.startsWith(root + "/")) throw new Error(`path outside repository: ${path}`);
|
|
8573
8968
|
return full;
|
|
8574
8969
|
}
|
|
@@ -8715,7 +9110,8 @@ function observedChanges(events, changedPaths, since) {
|
|
|
8715
9110
|
}
|
|
8716
9111
|
return {
|
|
8717
9112
|
sessions: new Set(inTask.map((e) => e.sessionId)).size,
|
|
8718
|
-
|
|
9113
|
+
// (0.2.7) `rm -rf units` names the folder: the hooks saw the files in it go
|
|
9114
|
+
namedPaths: changedPaths.filter((p) => named.has(p) || byFailed.has(p) || [...named].some((n3) => p.startsWith(`${n3}/`))),
|
|
8719
9115
|
byFailedCommand: changedPaths.filter((p) => byFailed.has(p) && !named.has(p))
|
|
8720
9116
|
};
|
|
8721
9117
|
}
|
|
@@ -8734,7 +9130,7 @@ ${r.stderrTail}`)) return r;
|
|
|
8734
9130
|
return { ...r, status: "unconfirmed", expectationFailures: ["a dependency this change adds is not installed in the verification copy (no network there, and this machine has no node_modules): the tests could not run; install the dependencies and run replume verify again"] };
|
|
8735
9131
|
}
|
|
8736
9132
|
async function runVerify(opts) {
|
|
8737
|
-
const repo =
|
|
9133
|
+
const repo = resolve5(opts.repoPath);
|
|
8738
9134
|
if (!isAbsolute4(opts.repoPath)) throw new Error("repoPath must be absolute");
|
|
8739
9135
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString(), started = performance4.now();
|
|
8740
9136
|
const image = opts.dockerImage ?? DEFAULT_DOCKER_IMAGE;
|
|
@@ -9244,6 +9640,13 @@ function taskClosed(session) {
|
|
|
9244
9640
|
const closedAt = session.map((e) => e.type === "rule_hit" && e.rule === AUTO_INCOMPLETE_RULE).lastIndexOf(true);
|
|
9245
9641
|
return closedAt >= 0 && !session.slice(closedAt + 1).some(isWriteEvent);
|
|
9246
9642
|
}
|
|
9643
|
+
function reportedConflict(session) {
|
|
9644
|
+
for (const e of [...session].reverse()) {
|
|
9645
|
+
const q = isGate(e, "block") ? e.raw?.conflictReported : null;
|
|
9646
|
+
if (typeof q === "string") return q;
|
|
9647
|
+
}
|
|
9648
|
+
return null;
|
|
9649
|
+
}
|
|
9247
9650
|
function gateBlocksSoFar(session) {
|
|
9248
9651
|
return session.filter((e) => isGate(e, "block")).length;
|
|
9249
9652
|
}
|
|
@@ -9330,12 +9733,15 @@ ${r0.stderr}`.replace(ANSI5, "");
|
|
|
9330
9733
|
}
|
|
9331
9734
|
}
|
|
9332
9735
|
const what = run.status === "zero_tests" ? `ran zero tests (exit code 0)` : `failed (exit code ${run.exitCode ?? "unknown"})`;
|
|
9333
|
-
|
|
9334
|
-
|
|
9736
|
+
const conflictNow = run.status === "failed" ? statedSpecTestConflict(c.lastAssistantMessage, { red: true }) : null;
|
|
9737
|
+
const conflictBefore = reportedConflict(c.session);
|
|
9738
|
+
const conflictClosed = !!(conflictBefore || conflictNow) && so_far >= 1;
|
|
9739
|
+
if (so_far >= max || conflictClosed) {
|
|
9740
|
+
const reason = conflictClosed ? `the agent reported that the task and a test disagree, with \`${command}\` ${what}: no second retry` : `the agent stopped with \`${command}\` ${what} after ${max} steer${max === 1 ? "" : "s"} (auto.max_retries ${max})`;
|
|
9335
9741
|
return {
|
|
9336
9742
|
rule: AUTO_INCOMPLETE_RULE,
|
|
9337
9743
|
outcome: "allow",
|
|
9338
|
-
message: `Replume: \`${command}\` still ${what}. Replume is not blocking again (auto.max_retries ${max} reached); the report gives the result: ${run.summary || what}
|
|
9744
|
+
message: `Replume: \`${command}\` still ${what}. Replume is not blocking again (${conflictClosed ? "the agent reported a conflict" : `auto.max_retries ${max} reached`}); the report gives the result: ${run.summary || what}
|
|
9339
9745
|
Next, for the person: read the report. If the test is right, the code still needs fixing. If the test is wrong, correct it and run \`replume confirm-config ${testFileOfFailing(c.repoRoot, failingTestNames(`${r0.stdout}
|
|
9340
9746
|
${r0.stderr}`.replace(ANSI5, ""))) ?? "<test file>"}\`, then \`replume verify\`.`,
|
|
9341
9747
|
summary: `${AUTO_INCOMPLETE_RULE}: ${reason}`,
|
|
@@ -9345,11 +9751,13 @@ ${r0.stderr}`.replace(ANSI5, ""))) ?? "<test file>"}\`, then \`replume verify\`.
|
|
|
9345
9751
|
};
|
|
9346
9752
|
}
|
|
9347
9753
|
const snap = snapshotUnavailable(c.repoRoot);
|
|
9348
|
-
const
|
|
9349
|
-
|
|
9350
|
-
` : ""}
|
|
9754
|
+
const conflictRule = `If the task, or a document it names, and this test disagree, stop here and say so in your final message, naming the test: leave the test and the document as they are, and do not make the code match the test.`;
|
|
9755
|
+
const message = conflictNow ? `You reported that the task and a test disagree. That is the right stop: leave the test and the document as they are, and do not make the code match the test. A person decides which is right. Replume ran \`${command}\`: it ${what}. ${run.summary ? `${run.summary}
|
|
9756
|
+
` : ""}Finish anything else the task asks, then end with a final message that names the test. (auto steer 1 of 1)` : snap ? `${conflictRule} Replume ran \`${command}\` before letting you stop: it ${what}. ${run.summary ? `${run.summary}
|
|
9757
|
+
` : ""}Replume could not check which failures existed before the task (the task-start snapshot failed${snap.files.length ? `; these files were already modified when the task started: ${snap.files.slice(0, 8).join(", ")}${snap.files.length > 8 ? ", \u2026" : ""}` : ""}), so it cannot tell whether this failure is yours. Fix only what you changed in this task; do not edit files you did not change. If the failure comes from files you did not change, say so in your final message. (auto steer ${so_far + 1} of ${max})` : `${conflictRule} Otherwise: Replume ran \`${command}\` before letting you stop: it ${what}. ${run.summary ? `${run.summary}
|
|
9758
|
+
` : ""}${whyYours ? `Why this counts as yours: ${whyYours}. ` : ""}The agreed task is "${contract.task.slice(0, 200)}"; this failure belongs to it, whatever the last request said. Do not stop yet.${stopFixText(c.repoRoot, contract)} Run \`${command}\` yourself until it passes with at least one test. (auto steer ${so_far + 1} of ${max})`;
|
|
9351
9759
|
return { failureOutput: `${r0.stdout}
|
|
9352
|
-
${r0.stderr}`.slice(-5e4), rule: AUTO_GATE_RULE, outcome: "block", message, summary: `${AUTO_GATE_RULE}: block on stop (\`${command}\` ${what}, steer ${so_far + 1} of ${max})`, raw: { ...raw6, outcome: "block", ...whyYours ? { whyYours } : {} }, ran };
|
|
9760
|
+
${r0.stderr}`.slice(-5e4), rule: AUTO_GATE_RULE, outcome: "block", message, summary: `${AUTO_GATE_RULE}: block on stop (\`${command}\` ${what}, steer ${so_far + 1} of ${conflictNow ? 1 : max})`, raw: { ...raw6, outcome: "block", ...whyYours ? { whyYours } : {}, ...conflictNow ? { conflictReported: conflictNow, maxRetries: 1 } : {} }, ran };
|
|
9353
9761
|
};
|
|
9354
9762
|
}
|
|
9355
9763
|
|
|
@@ -9440,7 +9848,7 @@ function passedTests(output) {
|
|
|
9440
9848
|
}
|
|
9441
9849
|
return out;
|
|
9442
9850
|
}
|
|
9443
|
-
var
|
|
9851
|
+
var norm3 = (s) => s.replace(/\s+/g, " ").trim();
|
|
9444
9852
|
function changedLinesByFile(diff) {
|
|
9445
9853
|
const out = /* @__PURE__ */ new Map();
|
|
9446
9854
|
let file3 = "";
|
|
@@ -9469,12 +9877,12 @@ function changedLinesByFile(diff) {
|
|
|
9469
9877
|
return out;
|
|
9470
9878
|
}
|
|
9471
9879
|
function quoteInDiff(diff, file3, quote4) {
|
|
9472
|
-
const q =
|
|
9880
|
+
const q = norm3(quote4);
|
|
9473
9881
|
if (!/[A-Za-z0-9]{3}/.test(q)) return false;
|
|
9474
9882
|
const lines = changedLinesByFile(diff);
|
|
9475
9883
|
const pool = file3 && lines.has(file3) ? [file3] : [...lines.keys()];
|
|
9476
|
-
const found = (x) => pool.some((f) => lines.get(f).some((l) =>
|
|
9477
|
-
return found(q) || /^[+-]/.test(q) && found(
|
|
9884
|
+
const found = (x) => pool.some((f) => lines.get(f).some((l) => norm3(l).includes(x)));
|
|
9885
|
+
return found(q) || /^[+-]/.test(q) && found(norm3(q.slice(1)));
|
|
9478
9886
|
}
|
|
9479
9887
|
var asList = (x) => Array.isArray(x) ? x.filter((y) => !!y && typeof y === "object" && !Array.isArray(y)) : [];
|
|
9480
9888
|
var str = (x, n3 = 300) => typeof x === "string" ? redact(x).slice(0, n3) : "";
|
|
@@ -10486,6 +10894,8 @@ function classify4(i) {
|
|
|
10486
10894
|
if (i.run?.status === "timeout") return { failure_class: "timeout", reason: "the test command did not finish in time", failing_tests: [], recovery: { text: "raise auto.test_timeout_sec in .replume/config, or make the tests faster", command: null } };
|
|
10487
10895
|
if (i.run?.expectationFailures.some((f) => f.startsWith("zero_tests_ran"))) return { failure_class: "zero_tests", reason: "the test command ran zero tests", failing_tests: [], recovery: { text: "make the test command run at least one test", command: i.testCommand } };
|
|
10488
10896
|
if (i.specConflict && i.status === "INCOMPLETE") return { failure_class: "spec_test_conflict", reason: i.specConflict, failing_tests: failing, recovery: { text: "a person decides whether the task or the test is right", command: /`(replume confirm-config [^`]+)`/.exec(i.specConflict)?.[1] ?? null } };
|
|
10897
|
+
if (i.expectationConfirm && i.status === "INCOMPLETE") return { failure_class: "incomplete", reason: i.incompleteWhy ?? "an expected value changed in an asked test", failing_tests: failing, recovery: { text: "a person decides: if the new value is right, confirm it, then run replume verify", command: i.expectationConfirm } };
|
|
10898
|
+
if (i.askedLeft?.length && i.status === "INCOMPLETE") return { failure_class: "incomplete", reason: i.incompleteWhy ?? "what the task asks to delete is still there", failing_tests: failing, recovery: { text: `delete ${i.askedLeft.slice(0, 5).join(", ")}${i.askedLeft.length > 5 ? ` and ${i.askedLeft.length - 5} more` : ""}, then run replume verify`, command: "replume verify" } };
|
|
10489
10899
|
if (i.status === "FAIL" && (i.run?.status === "failed" || i.failedIds.length)) {
|
|
10490
10900
|
const why = i.run?.taskTestRed?.length ? `task test still red: ${i.run.taskTestRed.slice(0, 3).join(", ")}` : `failed: ${i.failedIds.slice(0, 3).join(", ") || "test_command"}`;
|
|
10491
10901
|
return { failure_class: "test_failed", reason: why, failing_tests: failing, recovery: i.testCommand ? { text: `fix the code until \`${i.testCommand}\` passes, then run replume verify`, command: i.testCommand } : verify };
|
|
@@ -10919,15 +11329,17 @@ function timeAndTokensLines(repoRoot2, sessionId2, agent, events, opts = {}) {
|
|
|
10919
11329
|
// src/core/stats.ts
|
|
10920
11330
|
var STATS_ROWS = {
|
|
10921
11331
|
protected_test_edits_blocked: "Protected-test edits blocked",
|
|
11332
|
+
asked_edits_redirected: "Asked edits redirected",
|
|
10922
11333
|
bent_tests_caught: "Tests bent to the code caught",
|
|
10923
11334
|
weakened_tests_blocked: "Weakened tests or test scripts blocked",
|
|
10924
11335
|
protected_config_edits_blocked: "Protected config edits blocked",
|
|
10925
11336
|
done_with_red_tests_caught: '"Done" with red tests caught',
|
|
10926
11337
|
commit_gate_stops: "Commit-gate stops",
|
|
11338
|
+
caught_at_verify: "Weakened or bent tests caught at verify",
|
|
10927
11339
|
tests_rerun: "Test runs by Replume",
|
|
10928
11340
|
other_interventions: "Other interventions"
|
|
10929
11341
|
};
|
|
10930
|
-
var CATCHES = ["protected_test_edits_blocked", "bent_tests_caught", "weakened_tests_blocked", "protected_config_edits_blocked", "done_with_red_tests_caught", "commit_gate_stops"];
|
|
11342
|
+
var CATCHES = ["protected_test_edits_blocked", "bent_tests_caught", "weakened_tests_blocked", "protected_config_edits_blocked", "done_with_red_tests_caught", "commit_gate_stops", "caught_at_verify"];
|
|
10931
11343
|
var REPLUME_RUNS = /* @__PURE__ */ new Set(["auto_stop_gate", "auto_incomplete", "auto_base_run", "auto_bent_run"]);
|
|
10932
11344
|
var WEAKENED = /* @__PURE__ */ new Set([ERROR_CODES.TEST_WEAKENED, ERROR_CODES.TEST_BYPASS, ERROR_CODES.TEST_SCRIPT_CHANGED]);
|
|
10933
11345
|
var AGENT_STEPS2 = /* @__PURE__ */ new Set(["tool_pre", "tool_post", "file_edit", "command"]);
|
|
@@ -10943,12 +11355,24 @@ function rowFor(e) {
|
|
|
10943
11355
|
if (e.rule === "auto_stop_gate") return raw3(e).outcome === "block" ? { row: "done_with_red_tests_caught", code: code2 } : null;
|
|
10944
11356
|
const blocked = e.action === "block";
|
|
10945
11357
|
if (code2 === ERROR_CODES.TEST_BENT && blocked) return { row: "bent_tests_caught", code: code2 };
|
|
11358
|
+
if (code2 === ERROR_CODES.PROTECTED_TEST_EDIT && blocked && (e.message ?? "").includes(ASKED_SHELL_REWRITE)) return { row: "asked_edits_redirected", code: code2 };
|
|
10946
11359
|
if (code2 === ERROR_CODES.PROTECTED_TEST_EDIT && blocked) return { row: "protected_test_edits_blocked", code: code2 };
|
|
10947
11360
|
if (code2 === ERROR_CODES.PROTECTED_CONFIG_EDIT && blocked) return { row: "protected_config_edits_blocked", code: code2 };
|
|
10948
11361
|
if (WEAKENED.has(code2) && blocked) return { row: "weakened_tests_blocked", code: code2 };
|
|
10949
11362
|
if (code2 === ERROR_CODES.COMMIT_BEFORE_GREEN && blocked) return { row: "commit_gate_stops", code: code2 };
|
|
10950
11363
|
return { row: "other_interventions", code: code2 };
|
|
10951
11364
|
}
|
|
11365
|
+
function blockMattered(e, verdict, bent, confirmedPaths) {
|
|
11366
|
+
if (e.paths.some((p) => confirmedPaths.has(p) || bent.has(p))) return true;
|
|
11367
|
+
if (!verdict || raw3(verdict).status === "PASS") return false;
|
|
11368
|
+
const text = String(verdict.raw?.visibleSummary ?? verdict.summary ?? "");
|
|
11369
|
+
return e.paths.some((p) => text.includes(p));
|
|
11370
|
+
}
|
|
11371
|
+
function caughtAtVerify(verdict) {
|
|
11372
|
+
if (!verdict || raw3(verdict).status !== "FAIL") return null;
|
|
11373
|
+
const code2 = /\[([A-Z][A-Z0-9_]+)\]/.exec(String(verdict.raw?.visibleSummary ?? ""))?.[1];
|
|
11374
|
+
return code2 && (code2 === ERROR_CODES.TEST_BENT || WEAKENED.has(code2)) ? code2 : null;
|
|
11375
|
+
}
|
|
10952
11376
|
function ranTests(e) {
|
|
10953
11377
|
if (e.type !== "rule_hit" || !e.rule || !REPLUME_RUNS.has(e.rule)) return false;
|
|
10954
11378
|
return e.rule !== "auto_stop_gate" || raw3(e).outcome === "block" || raw3(e).outcome === "passed";
|
|
@@ -10982,6 +11406,7 @@ function computeStats(repoRoot2, opts = {}) {
|
|
|
10982
11406
|
const total = emptyAgent();
|
|
10983
11407
|
const byAgent = /* @__PURE__ */ Object.create(null);
|
|
10984
11408
|
const saved = { tokens: 0, ms: 0, episodes: 0, skippedSessions: 0, notCounted: 0, withTokens: 0, withTime: 0 };
|
|
11409
|
+
const confirmedPaths = new Set(readConfirmations(repoRoot2).map((c) => c.path));
|
|
10985
11410
|
for (const [sessionId2, list5] of bySession) {
|
|
10986
11411
|
if (!list5.some((e) => STEP_TYPES.has(e.type))) continue;
|
|
10987
11412
|
const agent = list5[0].agent;
|
|
@@ -10997,7 +11422,7 @@ function computeStats(repoRoot2, opts = {}) {
|
|
|
10997
11422
|
const endedPass = !!verdict && raw3(verdict).status === "PASS";
|
|
10998
11423
|
let lastStep = -1;
|
|
10999
11424
|
list5.forEach((e, i) => {
|
|
11000
|
-
if (AGENT_STEPS2.has(e.type)) lastStep = i;
|
|
11425
|
+
if (AGENT_STEPS2.has(e.type) && e.raw?.treeSweep !== true) lastStep = i;
|
|
11001
11426
|
});
|
|
11002
11427
|
if (verdict && typeof raw3(verdict).status === "string") both((x) => {
|
|
11003
11428
|
const s = raw3(verdict).status;
|
|
@@ -11013,8 +11438,9 @@ function computeStats(repoRoot2, opts = {}) {
|
|
|
11013
11438
|
x.interventions.tests_rerun++;
|
|
11014
11439
|
});
|
|
11015
11440
|
if (e.type !== "rule_hit" && e.type !== "tool_pre" && e.type !== "stop" && e.type !== "command") continue;
|
|
11016
|
-
const
|
|
11017
|
-
if (!
|
|
11441
|
+
const first = rowFor(e);
|
|
11442
|
+
if (!first) continue;
|
|
11443
|
+
const hit = first.row === "protected_test_edits_blocked" && !blockMattered(e, verdict, bent, confirmedPaths) ? { ...first, row: "asked_edits_redirected" } : first;
|
|
11018
11444
|
if (hit.row === "done_with_red_tests_caught" && !(endedPass && i < lastStep)) continue;
|
|
11019
11445
|
const call2 = typeof raw3(e).tool_use_id === "string" && raw3(e).tool_use_id ? String(raw3(e).tool_use_id) : `${e.rule}\0${e.message ?? e.summary}`;
|
|
11020
11446
|
const key = `${hit.row}\0${hit.code}\0${call2}`;
|
|
@@ -11032,6 +11458,12 @@ function computeStats(repoRoot2, opts = {}) {
|
|
|
11032
11458
|
}
|
|
11033
11459
|
if (hit.row === "done_with_red_tests_caught" || hit.row === "bent_tests_caught" && e.paths.some((p) => bent.has(p))) confirmed.add(call2);
|
|
11034
11460
|
}
|
|
11461
|
+
if (caughtAtVerify(verdict) && ![...seen].some((k) => k.startsWith("weakened_tests_blocked\0") || k.startsWith("bent_tests_caught\0"))) {
|
|
11462
|
+
both((x) => {
|
|
11463
|
+
x.interventions.caught_at_verify++;
|
|
11464
|
+
});
|
|
11465
|
+
catches.add("verify");
|
|
11466
|
+
}
|
|
11035
11467
|
if (!catches.size) continue;
|
|
11036
11468
|
both((x) => {
|
|
11037
11469
|
x.catches += catches.size;
|
|
@@ -11228,7 +11660,7 @@ import { Command, Option } from "commander";
|
|
|
11228
11660
|
// package.json
|
|
11229
11661
|
var package_default = {
|
|
11230
11662
|
name: "replume-cli",
|
|
11231
|
-
version: "0.2.
|
|
11663
|
+
version: "0.2.10",
|
|
11232
11664
|
type: "module",
|
|
11233
11665
|
bin: {
|
|
11234
11666
|
replume: "dist/cli.js"
|
|
@@ -11280,7 +11712,7 @@ var package_default = {
|
|
|
11280
11712
|
import { execFileSync as execFileSync37, spawn as spawn7 } from "child_process";
|
|
11281
11713
|
import { existsSync as existsSync53, lstatSync as lstatSync8, mkdtempSync as mkdtempSync8, readdirSync as readdirSync15, readFileSync as readFileSync59, statSync as statSync15, unlinkSync } from "fs";
|
|
11282
11714
|
import { homedir as homedir10, tmpdir as tmpdir15 } from "os";
|
|
11283
|
-
import { join as join69, resolve as
|
|
11715
|
+
import { join as join69, resolve as resolve17 } from "path";
|
|
11284
11716
|
|
|
11285
11717
|
// src/hooks/session-start.ts
|
|
11286
11718
|
import { execFileSync as execFileSync23 } from "child_process";
|
|
@@ -11288,12 +11720,12 @@ import { execFileSync as execFileSync23 } from "child_process";
|
|
|
11288
11720
|
// src/session-state.ts
|
|
11289
11721
|
import { execFileSync as execFileSync22 } from "child_process";
|
|
11290
11722
|
import { existsSync as existsSync32, mkdirSync as mkdirSync18, readFileSync as readFileSync35, rmSync as rmSync11, writeFileSync as writeFileSync19 } from "fs";
|
|
11291
|
-
import { dirname as dirname15, join as join42, resolve as
|
|
11723
|
+
import { dirname as dirname15, join as join42, resolve as resolve6 } from "path";
|
|
11292
11724
|
function sessionStatePath(repoDir, sessionId2) {
|
|
11293
11725
|
const gitPath = execFileSync22("git", ["rev-parse", "--git-path", "replume"], {
|
|
11294
11726
|
cwd: repoDir
|
|
11295
11727
|
}).toString().trim();
|
|
11296
|
-
const dir2 =
|
|
11728
|
+
const dir2 = resolve6(repoDir, gitPath);
|
|
11297
11729
|
return join42(dir2, `${sessionId2}.start-sha`);
|
|
11298
11730
|
}
|
|
11299
11731
|
function writeStartSha(repoDir, sessionId2, sha2) {
|
|
@@ -11863,8 +12295,8 @@ async function runVerify2(opts) {
|
|
|
11863
12295
|
}
|
|
11864
12296
|
|
|
11865
12297
|
// src/supervisor/review-at-stop.ts
|
|
11866
|
-
import { readFileSync as
|
|
11867
|
-
import { join as
|
|
12298
|
+
import { readFileSync as readFileSync42 } from "fs";
|
|
12299
|
+
import { join as join50 } from "path";
|
|
11868
12300
|
|
|
11869
12301
|
// src/core/removed-tests.ts
|
|
11870
12302
|
var TEST_FILE2 = /(^|\/)(tests?|__tests__|specs?)\/|[._-](test|spec)s?\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$/i;
|
|
@@ -11923,19 +12355,225 @@ function removedTestLine(r) {
|
|
|
11923
12355
|
return r.name ? `test removed: ${r.name} in ${r.file}` : `test file removed: ${r.file}${r.tests ? ` (${r.tests} test${r.tests === 1 ? "" : "s"})` : ""}`;
|
|
11924
12356
|
}
|
|
11925
12357
|
|
|
11926
|
-
// src/core/
|
|
12358
|
+
// src/core/shell-observe.ts
|
|
11927
12359
|
import { execFileSync as execFileSync26 } from "child_process";
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
12360
|
+
import { createHash as createHash6 } from "crypto";
|
|
12361
|
+
import { existsSync as existsSync35, lstatSync as lstatSync5, mkdirSync as mkdirSync21, readFileSync as readFileSync37, renameSync, rmSync as rmSync12, writeFileSync as writeFileSync21 } from "fs";
|
|
12362
|
+
import { tmpdir as tmpdir9 } from "os";
|
|
12363
|
+
import { join as join45, resolve as resolve7 } from "path";
|
|
12364
|
+
var MAX_TRACKED = 5e3;
|
|
12365
|
+
var SKIP2 = /^(\.claude|\.cursor|\.codex|\.git)\/|^\.replume\/(?!config$)/;
|
|
12366
|
+
function needsTreeCompare(a) {
|
|
12367
|
+
if (a.kind !== "command" || !a.command) return false;
|
|
12368
|
+
if (a.tool === "PowerShell") return true;
|
|
12369
|
+
if (a.writeTargetNotParsed) return true;
|
|
12370
|
+
const code2 = stripShellComments(a.command);
|
|
12371
|
+
return INLINE_CODE.test(code2) || unobservable(code2);
|
|
12372
|
+
}
|
|
12373
|
+
var git8 = (cwd, args, input) => execFileSync26("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", input, stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
|
|
12374
|
+
function blobsOnDisk(top, paths) {
|
|
12375
|
+
const out = /* @__PURE__ */ new Map();
|
|
12376
|
+
const files = paths.filter((p) => {
|
|
12377
|
+
try {
|
|
12378
|
+
return lstatSync5(join45(top, p)).isFile();
|
|
12379
|
+
} catch {
|
|
12380
|
+
return false;
|
|
12381
|
+
}
|
|
12382
|
+
});
|
|
12383
|
+
for (const p of paths) out.set(p, null);
|
|
12384
|
+
if (!files.length) return out;
|
|
12385
|
+
const ids = git8(top, ["hash-object", "--stdin-paths"], files.join("\n") + "\n").trim().split("\n");
|
|
12386
|
+
files.forEach((p, i) => out.set(p, ids[i] ?? null));
|
|
12387
|
+
return out;
|
|
12388
|
+
}
|
|
12389
|
+
function where(repoRoot2) {
|
|
12390
|
+
const top = git8(repoRoot2, ["rev-parse", "--show-toplevel"]).trim();
|
|
12391
|
+
const prefix = git8(repoRoot2, ["rev-parse", "--show-prefix"]).trim();
|
|
12392
|
+
return { top, prefix };
|
|
12393
|
+
}
|
|
12394
|
+
function changedNow(repoRoot2) {
|
|
12395
|
+
const { top, prefix } = where(repoRoot2);
|
|
12396
|
+
let head = null;
|
|
12397
|
+
try {
|
|
12398
|
+
head = git8(top, ["rev-parse", "--verify", "-q", "HEAD"]).trim() || null;
|
|
12399
|
+
} catch {
|
|
12400
|
+
head = null;
|
|
12401
|
+
}
|
|
12402
|
+
const recs = git8(top, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames", "--", prefix ? prefix : "."]).split("\0").filter(Boolean);
|
|
12403
|
+
if (recs.length > MAX_TRACKED) return null;
|
|
12404
|
+
const paths = recs.map((r) => r.slice(3)).filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)).filter((p) => !SKIP2.test(p));
|
|
12405
|
+
return { top, prefix, head, paths };
|
|
12406
|
+
}
|
|
12407
|
+
function treeState(repoRoot2) {
|
|
12408
|
+
try {
|
|
12409
|
+
const now = changedNow(repoRoot2);
|
|
12410
|
+
if (!now) return null;
|
|
12411
|
+
const blobs = blobsOnDisk(now.top, now.paths.map((p) => now.prefix + p));
|
|
12412
|
+
return { head: now.head, files: Object.fromEntries(now.paths.map((p) => [p, blobs.get(now.prefix + p) ?? null])) };
|
|
12413
|
+
} catch {
|
|
12414
|
+
return null;
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
function changedSince2(repoRoot2, before) {
|
|
12418
|
+
try {
|
|
12419
|
+
const now = changedNow(repoRoot2);
|
|
12420
|
+
if (!now) return [];
|
|
12421
|
+
const all = [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...now.paths])].filter((p) => !SKIP2.test(p)).sort();
|
|
12422
|
+
const after = blobsOnDisk(now.top, all.map((p) => now.prefix + p));
|
|
12423
|
+
const unlisted = all.filter((p) => !(p in before.files));
|
|
12424
|
+
const inHead = /* @__PURE__ */ new Map();
|
|
12425
|
+
if (before.head && unlisted.length) {
|
|
12426
|
+
const text = git8(now.top, ["ls-tree", "-z", before.head, "--", ...unlisted.map((p) => now.prefix + p)]);
|
|
12427
|
+
for (const rec of text.split("\0").filter(Boolean)) {
|
|
12428
|
+
const m = /^\d+ blob ([0-9a-f]+)\t(.+)$/.exec(rec);
|
|
12429
|
+
if (m && m[2].startsWith(now.prefix)) inHead.set(m[2].slice(now.prefix.length), m[1]);
|
|
12430
|
+
}
|
|
12431
|
+
}
|
|
12432
|
+
const out = [];
|
|
12433
|
+
for (const p of all) {
|
|
12434
|
+
const b = p in before.files ? before.files[p] : inHead.get(p) ?? null;
|
|
12435
|
+
const a = after.get(now.prefix + p) ?? null;
|
|
12436
|
+
if (a !== b) out.push({ path: p, before: b, after: a });
|
|
12437
|
+
}
|
|
12438
|
+
return out;
|
|
12439
|
+
} catch {
|
|
12440
|
+
return [];
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12443
|
+
var sha = (s, n3 = 16) => createHash6("sha1").update(s).digest("hex").slice(0, n3);
|
|
12444
|
+
var stateFile = (repoRoot2, sessionId2, toolUseId) => join45(tmpdir9(), "replume-tree", sha(resolve7(repoRoot2), 12), sha(sessionId2, 12), `${sha(toolUseId, 20)}.json`);
|
|
12445
|
+
function rememberTree(repoRoot2, sessionId2, toolUseId) {
|
|
12446
|
+
if (!toolUseId) return false;
|
|
12447
|
+
const state = treeState(repoRoot2);
|
|
12448
|
+
if (!state) return false;
|
|
12449
|
+
try {
|
|
12450
|
+
const f = stateFile(repoRoot2, sessionId2, toolUseId);
|
|
12451
|
+
mkdirSync21(join45(f, ".."), { recursive: true });
|
|
12452
|
+
writeFileSync21(f, JSON.stringify(state));
|
|
12453
|
+
return true;
|
|
12454
|
+
} catch {
|
|
12455
|
+
return false;
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
function takeTreeChanges(repoRoot2, sessionId2, toolUseId) {
|
|
12459
|
+
if (!toolUseId) return null;
|
|
12460
|
+
const f = stateFile(repoRoot2, sessionId2, toolUseId);
|
|
12461
|
+
let state;
|
|
12462
|
+
try {
|
|
12463
|
+
if (!existsSync35(f)) return null;
|
|
12464
|
+
state = JSON.parse(readFileSync37(f, "utf-8"));
|
|
12465
|
+
} catch {
|
|
12466
|
+
return null;
|
|
12467
|
+
} finally {
|
|
12468
|
+
rmSync12(f, { force: true });
|
|
12469
|
+
}
|
|
12470
|
+
if (!state || typeof state.files !== "object" || state.files === null) return null;
|
|
12471
|
+
return changedSince2(repoRoot2, state);
|
|
12472
|
+
}
|
|
12473
|
+
function forgetTree(repoRoot2, sessionId2, toolUseId) {
|
|
12474
|
+
if (toolUseId) rmSync12(stateFile(repoRoot2, sessionId2, toolUseId), { force: true });
|
|
12475
|
+
}
|
|
12476
|
+
function changedFromBase(repoRoot2, base4) {
|
|
12477
|
+
try {
|
|
12478
|
+
const { top, prefix } = where(repoRoot2);
|
|
12479
|
+
const list5 = (args) => git8(repoRoot2, args).split("\0").filter(Boolean);
|
|
12480
|
+
let third = false;
|
|
12481
|
+
try {
|
|
12482
|
+
git8(repoRoot2, ["rev-parse", "--verify", "-q", `${base4}^3`]);
|
|
12483
|
+
third = true;
|
|
12484
|
+
} catch {
|
|
12485
|
+
third = false;
|
|
12486
|
+
}
|
|
12487
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
12488
|
+
...list5(["diff", "--name-only", "-z", "--no-renames", "--relative", base4]),
|
|
12489
|
+
...list5(["ls-files", "-o", "--exclude-standard", "-z"]),
|
|
12490
|
+
...third ? list5(["ls-tree", "-r", "-z", "--name-only", `${base4}^3`]) : []
|
|
12491
|
+
]);
|
|
12492
|
+
const paths = [...candidates].filter((p) => !SKIP2.test(p));
|
|
12493
|
+
if (paths.length > MAX_TRACKED) return null;
|
|
12494
|
+
const inBase = /* @__PURE__ */ new Map();
|
|
12495
|
+
const scope = paths.length && paths.length <= 500 ? ["--", ...paths] : [];
|
|
12496
|
+
for (const ref of [...third ? [`${base4}^3`] : [], base4]) {
|
|
12497
|
+
for (const rec of paths.length ? list5(["ls-tree", "-r", "-z", ref, ...scope]) : []) {
|
|
12498
|
+
const m = /^\d+ blob ([0-9a-f]+)\t(.+)$/.exec(rec);
|
|
12499
|
+
if (m) inBase.set(m[2], m[1]);
|
|
12500
|
+
}
|
|
12501
|
+
}
|
|
12502
|
+
const now = blobsOnDisk(top, paths.map((p) => prefix + p));
|
|
12503
|
+
const out = /* @__PURE__ */ new Map();
|
|
12504
|
+
for (const p of paths) {
|
|
12505
|
+
const before = inBase.get(p) ?? null;
|
|
12506
|
+
const after = now.get(prefix + p) ?? null;
|
|
12507
|
+
if (before !== after) out.set(p, { before, after });
|
|
12508
|
+
}
|
|
12509
|
+
return out;
|
|
12510
|
+
} catch {
|
|
12511
|
+
return null;
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12514
|
+
var seenFile = (repoRoot2, base4) => join45(tmpdir9(), "replume-tree", sha(resolve7(repoRoot2), 12), `seen-${base4.slice(0, 16)}.json`);
|
|
12515
|
+
function newTreeChanges(repoRoot2, base4) {
|
|
12516
|
+
if (!base4) return null;
|
|
12517
|
+
const now = changedFromBase(repoRoot2, base4);
|
|
12518
|
+
if (!now) return null;
|
|
12519
|
+
const file3 = seenFile(repoRoot2, base4);
|
|
12520
|
+
let seen = {};
|
|
12521
|
+
try {
|
|
12522
|
+
seen = JSON.parse(readFileSync37(file3, "utf-8"));
|
|
12523
|
+
} catch {
|
|
12524
|
+
seen = {};
|
|
12525
|
+
}
|
|
12526
|
+
const out = [];
|
|
12527
|
+
for (const [path, c] of now) if (!(path in seen) || seen[path].after !== c.after) out.push({ path, before: path in seen ? seen[path].after : c.before, after: c.after });
|
|
12528
|
+
for (const [path, c] of Object.entries(seen)) if (!now.has(path)) out.push({ path, before: c.after, after: c.before });
|
|
12529
|
+
try {
|
|
12530
|
+
mkdirSync21(join45(file3, ".."), { recursive: true });
|
|
12531
|
+
const tmp = `${file3}.${process.pid}`;
|
|
12532
|
+
writeFileSync21(tmp, JSON.stringify(Object.fromEntries(now)));
|
|
12533
|
+
renameSync(tmp, file3);
|
|
12534
|
+
} catch {
|
|
12535
|
+
}
|
|
12536
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
12537
|
+
}
|
|
12538
|
+
function writesFromTree(changes, namedInCommand) {
|
|
12539
|
+
const named = namedInCommand.map((p) => p.replace(/\/+$/, ""));
|
|
12540
|
+
const read4 = (c) => named.includes(c.path) || c.after === null && named.some((n3) => c.path.startsWith(`${n3}/`));
|
|
12541
|
+
return { writes: changes.map((c) => c.path), hashWrites: changes.filter((c) => !read4(c)), treeCompared: true };
|
|
12542
|
+
}
|
|
12543
|
+
function landTreeChanges(repoRoot2, base4, event) {
|
|
12544
|
+
const changes = newTreeChanges(repoRoot2, base4);
|
|
12545
|
+
if (!changes?.length) return [];
|
|
12546
|
+
const files = changes.map((c) => c.path);
|
|
12547
|
+
appendEvent(ledgerPath(repoRoot2), {
|
|
12548
|
+
agent: event.agent,
|
|
12549
|
+
agentVersion: null,
|
|
12550
|
+
sessionId: event.sessionId,
|
|
12551
|
+
type: "command",
|
|
12552
|
+
tool: null,
|
|
12553
|
+
paths: files,
|
|
12554
|
+
command: null,
|
|
12555
|
+
summary: `changed outside a recorded step: ${files.join(", ")}`,
|
|
12556
|
+
rule: null,
|
|
12557
|
+
action: "ok",
|
|
12558
|
+
message: null,
|
|
12559
|
+
raw: { ok: true, treeSweep: true, ...writesFromTree(changes, []) }
|
|
12560
|
+
});
|
|
12561
|
+
return files;
|
|
12562
|
+
}
|
|
12563
|
+
|
|
12564
|
+
// src/core/confirm-edits.ts
|
|
12565
|
+
import { execFileSync as execFileSync27 } from "child_process";
|
|
12566
|
+
var NOT_EDITS = /^(?:bash|shell|terminal|unknown)$|read|view|search|grep|glob|list|fetch|web/i;
|
|
12567
|
+
function confirmEdits(root, sid, agent) {
|
|
12568
|
+
const ledger = ledgerPath(root);
|
|
12569
|
+
const mine = readEvents(ledger).events.filter((e) => e.sessionId === sid);
|
|
12570
|
+
const confirmed = new Set(mine.filter((e) => e.type === "file_edit" && e.raw?.ok !== false).flatMap((e) => e.paths));
|
|
12571
|
+
const b = readBaseline(baselinePath(contractPath(root)));
|
|
12572
|
+
const base4 = b.status === "ok" ? b.baseline.base_commit : null;
|
|
12573
|
+
const git10 = (args) => execFileSync27("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
12574
|
+
const changed = (path) => {
|
|
12575
|
+
try {
|
|
12576
|
+
if (git10(["status", "--porcelain", "--", path]).trim()) return true;
|
|
11939
12577
|
if (base4) {
|
|
11940
12578
|
try {
|
|
11941
12579
|
git10(["diff", "--quiet", base4, "--", path]);
|
|
@@ -11964,16 +12602,16 @@ var confirmCopilotEdits = (root, sid) => confirmEdits(root, sid, "copilot");
|
|
|
11964
12602
|
var confirmHermesEdits = (root, sid) => confirmEdits(root, sid, "hermes");
|
|
11965
12603
|
|
|
11966
12604
|
// src/core/auto-report.ts
|
|
11967
|
-
import { execFileSync as
|
|
11968
|
-
import { existsSync as
|
|
11969
|
-
import { tmpdir as
|
|
11970
|
-
import { join as
|
|
12605
|
+
import { execFileSync as execFileSync30 } from "child_process";
|
|
12606
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync6, readFileSync as readFileSync41, rmSync as rmSync14, statSync as statSync8, writeFileSync as writeFileSync25 } from "fs";
|
|
12607
|
+
import { tmpdir as tmpdir11 } from "os";
|
|
12608
|
+
import { join as join49 } from "path";
|
|
11971
12609
|
|
|
11972
12610
|
// src/core/ignored-state.ts
|
|
11973
|
-
import { execFileSync as
|
|
11974
|
-
import { createHash as
|
|
11975
|
-
import { lstatSync as
|
|
11976
|
-
import { join as
|
|
12611
|
+
import { execFileSync as execFileSync28 } from "child_process";
|
|
12612
|
+
import { createHash as createHash7 } from "crypto";
|
|
12613
|
+
import { lstatSync as lstatSync6, mkdirSync as mkdirSync22, readdirSync as readdirSync10, readFileSync as readFileSync38, writeFileSync as writeFileSync22 } from "fs";
|
|
12614
|
+
import { join as join46 } from "path";
|
|
11977
12615
|
var FILE2 = "contracts/ignored-start.json";
|
|
11978
12616
|
var MAX_FILES2 = 2e4;
|
|
11979
12617
|
var HASH_FILE_BYTES = 16 * 1024 ** 2;
|
|
@@ -11983,7 +12621,7 @@ var COMPARE_MS = 15e3;
|
|
|
11983
12621
|
var NOISE = /(^|\/)(__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.tox|\.nox|\.cache|\.turbo|\.parcel-cache|\.nyc_output|coverage)(\/|$)|\.(pyc|pyo|log|tsbuildinfo)$|(^|\/)(\.DS_Store|\.coverage)$/;
|
|
11984
12622
|
function hashOf(path) {
|
|
11985
12623
|
try {
|
|
11986
|
-
return
|
|
12624
|
+
return createHash7("sha256").update(readFileSync38(path)).digest("hex");
|
|
11987
12625
|
} catch {
|
|
11988
12626
|
return null;
|
|
11989
12627
|
}
|
|
@@ -11993,7 +12631,7 @@ function walk(repoRoot2, ms, hashAll) {
|
|
|
11993
12631
|
const seen = { files: /* @__PURE__ */ Object.create(null), partial: false };
|
|
11994
12632
|
let list5;
|
|
11995
12633
|
try {
|
|
11996
|
-
list5 =
|
|
12634
|
+
list5 = execFileSync28("git", ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 256 * 1024 ** 2, timeout: ms });
|
|
11997
12635
|
} catch {
|
|
11998
12636
|
seen.partial = true;
|
|
11999
12637
|
return seen;
|
|
@@ -12006,12 +12644,12 @@ function walk(repoRoot2, ms, hashAll) {
|
|
|
12006
12644
|
seen.partial = true;
|
|
12007
12645
|
return;
|
|
12008
12646
|
}
|
|
12009
|
-
const st =
|
|
12647
|
+
const st = lstatSync6(join46(repoRoot2, rel3), { throwIfNoEntry: false });
|
|
12010
12648
|
if (!st) return;
|
|
12011
12649
|
if (st.isDirectory()) {
|
|
12012
12650
|
let names3 = [];
|
|
12013
12651
|
try {
|
|
12014
|
-
names3 = readdirSync10(
|
|
12652
|
+
names3 = readdirSync10(join46(repoRoot2, rel3));
|
|
12015
12653
|
} catch {
|
|
12016
12654
|
}
|
|
12017
12655
|
for (const name of names3) if (!skippedInCopy(name)) one(`${rel3}/${name}`);
|
|
@@ -12022,7 +12660,7 @@ function walk(repoRoot2, ms, hashAll) {
|
|
|
12022
12660
|
count6++;
|
|
12023
12661
|
const hashIt = st.isFile() && hashAll && st.size <= HASH_FILE_BYTES && hashed + st.size <= HASH_TOTAL_BYTES;
|
|
12024
12662
|
if (hashIt) hashed += st.size;
|
|
12025
|
-
seen.files[rel3] = [st.size, st.mtimeMs, hashIt ? hashOf(
|
|
12663
|
+
seen.files[rel3] = [st.size, st.mtimeMs, hashIt ? hashOf(join46(repoRoot2, rel3)) : null];
|
|
12026
12664
|
};
|
|
12027
12665
|
for (const entry of list5.split("\0").filter(Boolean)) {
|
|
12028
12666
|
const rel3 = entry.replace(/\/$/, "");
|
|
@@ -12037,15 +12675,15 @@ function ignoredWriter(path, events) {
|
|
|
12037
12675
|
return "build (unattributed)";
|
|
12038
12676
|
}
|
|
12039
12677
|
function recordIgnoredState(repoRoot2, contractId, opts = {}) {
|
|
12040
|
-
const dir2 =
|
|
12041
|
-
|
|
12678
|
+
const dir2 = join46(repoRoot2, REPLUME_DIR, "contracts");
|
|
12679
|
+
mkdirSync22(dir2, { recursive: true });
|
|
12042
12680
|
const seen = walk(repoRoot2, opts.ms ?? RECORD_MS, true);
|
|
12043
|
-
|
|
12681
|
+
writeFileSync22(join46(repoRoot2, REPLUME_DIR, FILE2), JSON.stringify({ v: 1, contract_id: contractId, partial: seen.partial, files: seen.files }), { mode: 384 });
|
|
12044
12682
|
}
|
|
12045
12683
|
function ignoredChangesSinceStart(repoRoot2, contractId, opts = {}) {
|
|
12046
12684
|
let start;
|
|
12047
12685
|
try {
|
|
12048
|
-
start = JSON.parse(
|
|
12686
|
+
start = JSON.parse(readFileSync38(join46(repoRoot2, REPLUME_DIR, FILE2), "utf8"));
|
|
12049
12687
|
} catch {
|
|
12050
12688
|
return null;
|
|
12051
12689
|
}
|
|
@@ -12060,7 +12698,7 @@ function ignoredChangesSinceStart(repoRoot2, contractId, opts = {}) {
|
|
|
12060
12698
|
continue;
|
|
12061
12699
|
}
|
|
12062
12700
|
if (is[0] === was[0] && is[1] === was[1]) continue;
|
|
12063
|
-
if (was[2] && is[0] === was[0] && hashOf(
|
|
12701
|
+
if (was[2] && is[0] === was[0] && hashOf(join46(repoRoot2, path)) === was[2]) continue;
|
|
12064
12702
|
changes.push({ path, kind: "modified" });
|
|
12065
12703
|
}
|
|
12066
12704
|
if (!start.partial) {
|
|
@@ -12069,40 +12707,6 @@ function ignoredChangesSinceStart(repoRoot2, contractId, opts = {}) {
|
|
|
12069
12707
|
return { changes: changes.sort((a, b) => a.path < b.path ? -1 : 1), partial };
|
|
12070
12708
|
}
|
|
12071
12709
|
|
|
12072
|
-
// src/core/spec-conflict.ts
|
|
12073
|
-
var NOT_THE_TASK = /\b(?:spec|specification|docs?|documents?|documentation|readme|requirements?|the task|your request|the prompt|the brief|rules?|what you asked|as written|[\w./-]+\.(?:md|rst|txt|adoc))\b/i;
|
|
12074
|
-
var A_TEST = /\b(?:tests?|suite|specs?|assertions?|expectations?|[\w./-]+\.(?:test|spec)\.[a-z]+|test_\w+|[\w./-]*tests?\/[\w./-]+)\b/i;
|
|
12075
|
-
var DISAGREE = /\b(?:contradict\w*|conflict\w*|disagree\w*|mismatch\w*|inconsisten\w*|at odds|(?:both )?can(?:no|')t (?:both )?(?:be true|hold)|cannot both|does(?:n't| not) match|do(?:n't| not) match)\b/i;
|
|
12076
|
-
var NEGATED = /\b(?:no|not|without|never|none|nothing|any|n't)\s+(?:\w+\s+){0,2}(?:contradict|conflict|disagree|mismatch|inconsisten|wrong)/i;
|
|
12077
|
-
var TEST_OVER_TASK = /\b(?:treated|took|counted|kept)\b.{0,40}\btests?\b.{0,20}\bas the spec\b|\bfollowed the (?:protected )?tests?\b|\b(?:changed|edited|rewrote|reworded|updated)\b.{0,40}\b(?:docs?|spec|[\w./-]+\.md)\b.{0,60}\b(?:to match|because|so that)\b.{0,40}\btests?\b|\bspec\b.{0,40}\b(?:unimplemented|not implemented)\b/i;
|
|
12078
|
-
var IS_WRONG = /\b(?:is|are|looks?|seems?|must be|appears? to be)\s+(?:\w+\s+)?(?:wrong|incorrect|outdated|out of date|stale|mistaken)\b/i;
|
|
12079
|
-
var SOMEONE_DECIDES = /\b(?:pick|choose|decide)\b.{0,30}\bwhich\b|\bwhich (?:one )?(?:is right|is correct|should win)\b|\byour call\b|\b(?:choice|decision) is yours\b|\b(?:needs?|for) (?:a person|someone|a human|you)\b.{0,20}\bdecide\b|\bdecide between\b|\b(?:someone|a person|you) (?:has|have|must|needs?) to (?:pick|choose|decide)\b/i;
|
|
12080
|
-
var CLAIM = "(?:says?|said|states?|expects?|asserts?|requires?|wants?|insists?|makes?|gives?|needs?)";
|
|
12081
|
-
var CONTRAST = "(?:but|while|whereas|though|although|yet|however)";
|
|
12082
|
-
var AGAINST = new RegExp(`\\b${CLAIM}\\b.*\\b${CONTRAST}\\b.*\\b${CLAIM}\\b`, "i");
|
|
12083
|
-
var ONE_IS_WRONG = /\bone of (?:them|those|these|the two)\b.{0,30}\b(?:wrong|mistaken|incorrect|outdated)\b/i;
|
|
12084
|
-
var TEST_EXPECTS = /\b(?:expects?|asserts?|requires?|wants?)\b/i;
|
|
12085
|
-
var DOC_SAYS = /\b(?:says?|said|states?|specif\w+|requires?)\b/i;
|
|
12086
|
-
var sentences = (m) => m.split(/(?<=[.!?:])\s+|\n+/).map((s) => s.replace(/[*`]/g, "").trim()).filter(Boolean);
|
|
12087
|
-
function statedSpecTestConflict(message, opts = {}) {
|
|
12088
|
-
const all = sentences(message);
|
|
12089
|
-
for (let i = 0; i < all.length; i++) {
|
|
12090
|
-
const s = all[i];
|
|
12091
|
-
const near = [s, all[i + 1] ?? "", all[i + 2] ?? ""].join(" ");
|
|
12092
|
-
if (DISAGREE.test(s) && !NEGATED.test(s) && !/\bif\b/i.test(s) && A_TEST.test(near) && NOT_THE_TASK.test(near)) return s;
|
|
12093
|
-
if (TEST_OVER_TASK.test(s)) return s;
|
|
12094
|
-
if (IS_WRONG.test(s) && !NEGATED.test(s) && !/\b(?:if|whether)\b/i.test(s) && (A_TEST.test(s) || NOT_THE_TASK.test(s))) return s;
|
|
12095
|
-
if ((SOMEONE_DECIDES.test(s) || ONE_IS_WRONG.test(s)) && !/\bif\b/i.test(s)) return s;
|
|
12096
|
-
if (AGAINST.test(s) && A_TEST.test(s) && NOT_THE_TASK.test(s) && !/\bif\b/i.test(s)) return s;
|
|
12097
|
-
}
|
|
12098
|
-
if (opts.red) {
|
|
12099
|
-
const test = all.findIndex((s) => A_TEST.test(s) && TEST_EXPECTS.test(s));
|
|
12100
|
-
const doc = all.findIndex((s, i) => i !== test && NOT_THE_TASK.test(s) && DOC_SAYS.test(s) && !/\bif\b/i.test(s));
|
|
12101
|
-
if (test >= 0 && doc >= 0) return all[test];
|
|
12102
|
-
}
|
|
12103
|
-
return null;
|
|
12104
|
-
}
|
|
12105
|
-
|
|
12106
12710
|
// src/report/session-report.ts
|
|
12107
12711
|
function raw4(event) {
|
|
12108
12712
|
return event.raw && typeof event.raw === "object" && !Array.isArray(event.raw) ? event.raw : {};
|
|
@@ -12344,6 +12948,7 @@ function renderSessionReport(ledgerEvents, extras = {}) {
|
|
|
12344
12948
|
{ title: "What was asked", lines: asked },
|
|
12345
12949
|
{ title: "Project memory", lines: memoryLines },
|
|
12346
12950
|
{ title: "What changed", lines: changed },
|
|
12951
|
+
...extras.askedTestChanges?.length ? [{ title: "Asked tests for a person to confirm", lines: extras.askedTestChanges }] : [],
|
|
12347
12952
|
...extras.ignoredChanged ? [{ title: IGNORED_CHANGED_TITLE, lines: ignoredChangedLines(extras.ignoredChanged) }] : [],
|
|
12348
12953
|
{ title: "Where Replume steered", lines: interventionLines(events, ledgerRuns) },
|
|
12349
12954
|
{ title: "Tests", lines: testLines },
|
|
@@ -12405,11 +13010,11 @@ ${reportTest.stderrTail}` : details.outputTail;
|
|
|
12405
13010
|
|
|
12406
13011
|
// src/supervisor/judge-wire.ts
|
|
12407
13012
|
import { spawn as spawn4 } from "child_process";
|
|
12408
|
-
import { execFileSync as
|
|
12409
|
-
import { createHash as
|
|
12410
|
-
import { mkdirSync as
|
|
12411
|
-
import { tmpdir as
|
|
12412
|
-
import { join as
|
|
13013
|
+
import { execFileSync as execFileSync29 } from "child_process";
|
|
13014
|
+
import { createHash as createHash8 } from "crypto";
|
|
13015
|
+
import { mkdirSync as mkdirSync23, readFileSync as readFileSync39, writeFileSync as writeFileSync23 } from "fs";
|
|
13016
|
+
import { tmpdir as tmpdir10 } from "os";
|
|
13017
|
+
import { join as join47 } from "path";
|
|
12413
13018
|
|
|
12414
13019
|
// src/model-supervisor/index.ts
|
|
12415
13020
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -12949,6 +13554,7 @@ function claimCheck(lastAgentMessage2, ledgerEvents, opts = {}) {
|
|
|
12949
13554
|
if (c.kind !== "file_added" && c.kind !== "file_changed" || !c.file) continue;
|
|
12950
13555
|
const hit = [...writes.written.keys()].some((p) => samePath(c.file, p));
|
|
12951
13556
|
if (hit) continue;
|
|
13557
|
+
if ((opts.changedFromBase ?? []).some((p) => samePath(c.file, p))) continue;
|
|
12952
13558
|
const blocked = [...writes.blocked].some((p) => samePath(c.file, p));
|
|
12953
13559
|
mismatches.push({
|
|
12954
13560
|
kind: blocked ? "file_write_blocked" : "file_not_written",
|
|
@@ -13071,7 +13677,8 @@ function classify5(s, ctx) {
|
|
|
13071
13677
|
if (writes.length) {
|
|
13072
13678
|
if (s.type === "tool_pre") return neutral("write attempt; judged when it lands");
|
|
13073
13679
|
if (allow.length) {
|
|
13074
|
-
const
|
|
13680
|
+
const asked = ctx.scope.askedDeletions ?? [];
|
|
13681
|
+
const out = writes.filter((p) => !matchesAny(normalizeRel(p), allow) && !RUNNER_CONFIG_FILE.test(normalizeRel(p)) && !asked.includes(normalizeRel(p).replace(/\/+$/, "")));
|
|
13075
13682
|
if (out.length) return { relation: "unrelated", why: `writes ${out.join(", ")} outside the task's allowed files`, outOfScope: out, label: label(s), weight: 2 };
|
|
13076
13683
|
remember(writes, ctx);
|
|
13077
13684
|
ctx.started = true;
|
|
@@ -13236,10 +13843,10 @@ function enqueueJudgeStep(config, job, deps = {}) {
|
|
|
13236
13843
|
const cli = process.argv[1];
|
|
13237
13844
|
if (!cli || !/(?:^|[\\/])(?:cli\.[cm]?[jt]s|replume(?:\.js)?)$/.test(cli)) return false;
|
|
13238
13845
|
try {
|
|
13239
|
-
const dir2 =
|
|
13240
|
-
|
|
13241
|
-
const file3 =
|
|
13242
|
-
|
|
13846
|
+
const dir2 = join47(tmpdir10(), "replume-judge-jobs");
|
|
13847
|
+
mkdirSync23(dir2, { recursive: true });
|
|
13848
|
+
const file3 = join47(dir2, `${createHash8("sha1").update(`${job.sessionId}\0${job.forEventId}`).digest("hex").slice(0, 20)}.json`);
|
|
13849
|
+
writeFileSync23(file3, JSON.stringify(job));
|
|
13243
13850
|
const child = spawn4(process.execPath, [...process.execArgv, cli, "hook", "judge-step", "--job", file3], { detached: true, stdio: "ignore", cwd: job.repoRoot });
|
|
13244
13851
|
child.on("error", () => void 0);
|
|
13245
13852
|
child.unref();
|
|
@@ -13318,7 +13925,7 @@ function pendingJudgeCandidate(events) {
|
|
|
13318
13925
|
function fullDiff(repoRoot2, maxChars = 6e4) {
|
|
13319
13926
|
const git10 = (args) => {
|
|
13320
13927
|
try {
|
|
13321
|
-
return
|
|
13928
|
+
return execFileSync29("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 8 * 1024 * 1024 });
|
|
13322
13929
|
} catch {
|
|
13323
13930
|
return "";
|
|
13324
13931
|
}
|
|
@@ -13326,7 +13933,7 @@ function fullDiff(repoRoot2, maxChars = 6e4) {
|
|
|
13326
13933
|
let out = git10(["diff", "HEAD", "--", ".", ":(exclude).replume", ":(exclude).claude", ":(exclude).cursor", ":(exclude).codex"]);
|
|
13327
13934
|
for (const f of git10(["ls-files", "--others", "--exclude-standard"]).split("\n").filter((f2) => f2 && !/^\.(?:replume|claude|cursor|codex)\//.test(f2)).slice(0, 20)) {
|
|
13328
13935
|
try {
|
|
13329
|
-
const text =
|
|
13936
|
+
const text = readFileSync39(join47(repoRoot2, f), "utf8");
|
|
13330
13937
|
out += `
|
|
13331
13938
|
${diffText("", text.slice(0, 2e4), f).replace("@@ -1,0 ", "@@ -0,0 ")}`;
|
|
13332
13939
|
} catch {
|
|
@@ -13381,19 +13988,19 @@ function modelJudgeExtras(events, fresh) {
|
|
|
13381
13988
|
}
|
|
13382
13989
|
|
|
13383
13990
|
// src/core/project-memory.ts
|
|
13384
|
-
import { createHash as
|
|
13385
|
-
import { appendFileSync as appendFileSync7, existsSync as
|
|
13386
|
-
import { join as
|
|
13387
|
-
var dirOf = (root) =>
|
|
13991
|
+
import { createHash as createHash9 } from "crypto";
|
|
13992
|
+
import { appendFileSync as appendFileSync7, existsSync as existsSync36, lstatSync as lstatSync7, mkdirSync as mkdirSync24, readFileSync as readFileSync40, readdirSync as readdirSync11, rmSync as rmSync13, writeFileSync as writeFileSync24 } from "fs";
|
|
13993
|
+
import { join as join48 } from "path";
|
|
13994
|
+
var dirOf = (root) => join48(root, ".replume", "memory");
|
|
13388
13995
|
var clip2 = (value, max, root) => redactLedgerText(value.replace(/\s+/g, " ").trim(), root).slice(0, max);
|
|
13389
13996
|
function ensureIgnored(root) {
|
|
13390
|
-
const gi =
|
|
13391
|
-
|
|
13392
|
-
if (!
|
|
13393
|
-
|
|
13997
|
+
const gi = join48(root, ".replume", ".gitignore");
|
|
13998
|
+
mkdirSync24(join48(root, ".replume"), { recursive: true });
|
|
13999
|
+
if (!existsSync36(gi)) {
|
|
14000
|
+
writeFileSync24(gi, DEFAULT_REPLUME_GITIGNORE + "memory/\n");
|
|
13394
14001
|
return;
|
|
13395
14002
|
}
|
|
13396
|
-
const current =
|
|
14003
|
+
const current = readFileSync40(gi, "utf8");
|
|
13397
14004
|
if (!current.split(/\r?\n/).some((line2) => line2.trim() === "memory/"))
|
|
13398
14005
|
appendFileSync7(gi, `${current.endsWith("\n") || !current ? "" : "\n"}memory/
|
|
13399
14006
|
`);
|
|
@@ -13401,8 +14008,8 @@ function ensureIgnored(root) {
|
|
|
13401
14008
|
function rememberSession(root, input) {
|
|
13402
14009
|
ensureIgnored(root);
|
|
13403
14010
|
const dir2 = dirOf(root);
|
|
13404
|
-
|
|
13405
|
-
const name =
|
|
14011
|
+
mkdirSync24(dir2, { recursive: true });
|
|
14012
|
+
const name = createHash9("sha256").update(input.sessionId).digest("hex").slice(0, 24) + ".json";
|
|
13406
14013
|
const entry = {
|
|
13407
14014
|
v: 1,
|
|
13408
14015
|
id: clip2(input.sessionId, 80, root),
|
|
@@ -13413,7 +14020,7 @@ function rememberSession(root, input) {
|
|
|
13413
14020
|
verdict: clip2(input.verdict, 24, root)
|
|
13414
14021
|
};
|
|
13415
14022
|
try {
|
|
13416
|
-
|
|
14023
|
+
writeFileSync24(join48(dir2, name), JSON.stringify(entry) + "\n", { flag: "wx", mode: 384 });
|
|
13417
14024
|
return true;
|
|
13418
14025
|
} catch (error) {
|
|
13419
14026
|
if (error.code === "EEXIST") return false;
|
|
@@ -13434,21 +14041,21 @@ function rememberUnverifiedSession(root, sessionId2, task, events) {
|
|
|
13434
14041
|
}
|
|
13435
14042
|
function listProjectMemory(root) {
|
|
13436
14043
|
const dir2 = dirOf(root);
|
|
13437
|
-
if (!
|
|
14044
|
+
if (!existsSync36(dir2)) return [];
|
|
13438
14045
|
const out = [];
|
|
13439
14046
|
const names3 = readdirSync11(dir2).filter((n3) => /^[a-f0-9]{24}\.json$/.test(n3)).map((name) => {
|
|
13440
14047
|
try {
|
|
13441
|
-
return { name, time:
|
|
14048
|
+
return { name, time: lstatSync7(join48(dir2, name)).mtimeMs };
|
|
13442
14049
|
} catch {
|
|
13443
14050
|
return { name, time: 0 };
|
|
13444
14051
|
}
|
|
13445
14052
|
}).sort((a, b) => b.time - a.time).slice(0, 200);
|
|
13446
14053
|
for (const { name } of names3) {
|
|
13447
14054
|
try {
|
|
13448
|
-
const path =
|
|
13449
|
-
const st =
|
|
14055
|
+
const path = join48(dir2, name);
|
|
14056
|
+
const st = lstatSync7(path);
|
|
13450
14057
|
if (!st.isFile() || st.size > 8192) continue;
|
|
13451
|
-
const e = JSON.parse(
|
|
14058
|
+
const e = JSON.parse(readFileSync40(path, "utf8"));
|
|
13452
14059
|
if (e.v === 1 && typeof e.id === "string" && typeof e.task === "string" && typeof e.at === "string" && Array.isArray(e.files) && e.files.every((f) => typeof f === "string") && Array.isArray(e.steers) && e.steers.every((s) => s && typeof s.kind === "string" && typeof s.reason === "string") && typeof e.verdict === "string")
|
|
13453
14060
|
out.push({ v: 1, id: clip2(e.id, 80, root), at: e.at, task: clip2(e.task, 200, root), files: e.files.slice(0, 16).map((f) => clip2(f, 120, root)), steers: e.steers.slice(0, 4).map((s) => ({ kind: clip2(s.kind, 32, root), reason: clip2(s.reason, 150, root) })), verdict: clip2(e.verdict, 24, root) });
|
|
13454
14061
|
} catch {
|
|
@@ -13458,12 +14065,12 @@ function listProjectMemory(root) {
|
|
|
13458
14065
|
}
|
|
13459
14066
|
function clearProjectMemory(root) {
|
|
13460
14067
|
const dir2 = dirOf(root);
|
|
13461
|
-
if (!
|
|
14068
|
+
if (!existsSync36(dir2)) return 0;
|
|
13462
14069
|
let count6 = 0;
|
|
13463
14070
|
for (const name of readdirSync11(dir2).filter((n3) => /^[a-f0-9]{24}\.json$/.test(n3))) {
|
|
13464
|
-
const path =
|
|
13465
|
-
if (
|
|
13466
|
-
|
|
14071
|
+
const path = join48(dir2, name);
|
|
14072
|
+
if (lstatSync7(path).isFile()) {
|
|
14073
|
+
rmSync13(path);
|
|
13467
14074
|
count6++;
|
|
13468
14075
|
}
|
|
13469
14076
|
}
|
|
@@ -13492,30 +14099,30 @@ function selectProjectMemory(root, task, files = []) {
|
|
|
13492
14099
|
}
|
|
13493
14100
|
|
|
13494
14101
|
// src/core/auto-report.ts
|
|
13495
|
-
var
|
|
14102
|
+
var git9 = (cwd, args, env = process.env) => execFileSync30("git", args, { cwd, encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
13496
14103
|
function snapshotWorkingTree(repoRoot2, sessionId2) {
|
|
13497
|
-
const head =
|
|
13498
|
-
if (!
|
|
13499
|
-
const dir2 = mkdtempSync6(
|
|
14104
|
+
const head = git9(repoRoot2, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
14105
|
+
if (!git9(repoRoot2, ["status", "--porcelain"])) return { commit: head, snapshot: false, ref: null };
|
|
14106
|
+
const dir2 = mkdtempSync6(join49(tmpdir11(), "replume-auto-"));
|
|
13500
14107
|
try {
|
|
13501
|
-
const env = { ...process.env, GIT_INDEX_FILE:
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
const tree =
|
|
14108
|
+
const env = { ...process.env, GIT_INDEX_FILE: join49(dir2, "index") };
|
|
14109
|
+
git9(repoRoot2, ["read-tree", "HEAD"], env);
|
|
14110
|
+
git9(repoRoot2, ["add", "-A"], env);
|
|
14111
|
+
const tree = git9(repoRoot2, ["write-tree"], env);
|
|
13505
14112
|
const ident = (key) => {
|
|
13506
14113
|
try {
|
|
13507
|
-
return
|
|
14114
|
+
return git9(repoRoot2, ["config", "--get", key]);
|
|
13508
14115
|
} catch {
|
|
13509
14116
|
return "";
|
|
13510
14117
|
}
|
|
13511
14118
|
};
|
|
13512
14119
|
const withIdent = { ...process.env, ...ident("user.name") ? {} : { GIT_AUTHOR_NAME: "Replume", GIT_COMMITTER_NAME: "Replume" }, ...ident("user.email") ? {} : { GIT_AUTHOR_EMAIL: "replume@localhost", GIT_COMMITTER_EMAIL: "replume@localhost" } };
|
|
13513
|
-
const commit =
|
|
14120
|
+
const commit = git9(repoRoot2, ["commit-tree", tree, "-p", head, "-m", `Replume auto snapshot of the working tree (session ${sessionId2.slice(0, 8)})`], withIdent);
|
|
13514
14121
|
const ref = `refs/replume/auto/${sessionId2.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 40) || "session"}`;
|
|
13515
|
-
|
|
14122
|
+
git9(repoRoot2, ["update-ref", ref, commit]);
|
|
13516
14123
|
return { commit, snapshot: true, ref };
|
|
13517
14124
|
} finally {
|
|
13518
|
-
|
|
14125
|
+
rmSync14(dir2, { recursive: true, force: true });
|
|
13519
14126
|
}
|
|
13520
14127
|
}
|
|
13521
14128
|
function agentOfSession(repoRoot2, sessionId2) {
|
|
@@ -13543,16 +14150,65 @@ ${e.summary}`;
|
|
|
13543
14150
|
var shortCommand = (c) => c.length > 240 ? `${c.slice(0, 237)}\u2026` : c;
|
|
13544
14151
|
var blindLine = (s) => `${UNPARSED_OUTSIDE_LINE}: \`${shortCommand(s.command)}\` wrote to a path computed when it ran, and no file git tracks changed with it`;
|
|
13545
14152
|
var seenLine = (s) => `\`${shortCommand(s.command)}\` wrote to a path computed when it ran; comparing the tree before and after it shows it changed ${s.files.join(", ")} in the repository (checked above). A write it made outside the repository was not seen`;
|
|
14153
|
+
function askedTestChanges(repoRoot2, base4, contract, changed, commit) {
|
|
14154
|
+
const asked = askedTestsNow(repoRoot2, contract);
|
|
14155
|
+
const moves = movesSince(repoRoot2, base4, commit);
|
|
14156
|
+
const out = [];
|
|
14157
|
+
for (const path of changed.filter((p) => asked.includes(p))) {
|
|
14158
|
+
let now = null;
|
|
14159
|
+
try {
|
|
14160
|
+
now = readFileSync41(join49(repoRoot2, path), "utf8");
|
|
14161
|
+
} catch {
|
|
14162
|
+
now = null;
|
|
14163
|
+
}
|
|
14164
|
+
const from = moves.find(([, to]) => to === path)?.[0] ?? path;
|
|
14165
|
+
const start = fileAt(repoRoot2, base4, from);
|
|
14166
|
+
if (now === null || start === null || start === now || isConfirmed(repoRoot2, path, now)) continue;
|
|
14167
|
+
const c = compareCases(start, now, path);
|
|
14168
|
+
const left = c.changes.filter((x) => !taskNamesValue(contract.task, x.to));
|
|
14169
|
+
if (c.kind === "same" || c.kind === "values" && !left.length) continue;
|
|
14170
|
+
let diff = [];
|
|
14171
|
+
if (c.kind === "other") {
|
|
14172
|
+
try {
|
|
14173
|
+
diff = execFileSync30("git", ["diff", "--no-color", "--unified=1", base4, "--", from, path], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 8 * 1024 * 1024 }).split("\n").filter((l) => l && !/^(?:diff --git|index )/.test(l)).slice(0, 60);
|
|
14174
|
+
} catch {
|
|
14175
|
+
diff = [];
|
|
14176
|
+
}
|
|
14177
|
+
}
|
|
14178
|
+
out.push({ path, kind: c.kind, changes: left, confirm: confirmCommand(path, now), diff });
|
|
14179
|
+
}
|
|
14180
|
+
return out;
|
|
14181
|
+
}
|
|
14182
|
+
var TEST_DECL_LINE = /^\s*(?:async\s+)?def\s+test_\w*|\b(?:test|it)\s*\(/gm;
|
|
14183
|
+
function casesExplainDrop(repoRoot2, base4, contract, changed, drop, commit) {
|
|
14184
|
+
const asked = askedTestsNow(repoRoot2, contract);
|
|
14185
|
+
const moves = movesSince(repoRoot2, base4, commit);
|
|
14186
|
+
const tests = changed.filter(isTestFile);
|
|
14187
|
+
if (!tests.length || tests.some((p) => !asked.includes(p))) return false;
|
|
14188
|
+
let fewer = 0;
|
|
14189
|
+
for (const path of tests) {
|
|
14190
|
+
let now = null;
|
|
14191
|
+
try {
|
|
14192
|
+
now = readFileSync41(join49(repoRoot2, path), "utf8");
|
|
14193
|
+
} catch {
|
|
14194
|
+
now = null;
|
|
14195
|
+
}
|
|
14196
|
+
const start = fileAt(repoRoot2, base4, moves.find(([, to]) => to === path)?.[0] ?? path);
|
|
14197
|
+
if (now === null || start === null || compareCases(start, now, path).kind === "other") return false;
|
|
14198
|
+
fewer += (start.match(TEST_DECL_LINE) ?? []).length - (now.match(TEST_DECL_LINE) ?? []).length;
|
|
14199
|
+
}
|
|
14200
|
+
return fewer >= drop;
|
|
14201
|
+
}
|
|
13546
14202
|
function specConflictWhy(repoRoot2, quote4, message, failing) {
|
|
13547
14203
|
let tests = [];
|
|
13548
14204
|
try {
|
|
13549
|
-
tests =
|
|
14205
|
+
tests = execFileSync30("git", ["ls-files"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).split("\n").filter((f) => f && isTestFile(f));
|
|
13550
14206
|
} catch {
|
|
13551
14207
|
}
|
|
13552
14208
|
const titles = [...message.matchAll(/[`'"]([^`'"\n]{3,80})[`'"]/g)].map((m) => m[1]);
|
|
13553
14209
|
const holdsTitle = (f) => {
|
|
13554
14210
|
try {
|
|
13555
|
-
const t =
|
|
14211
|
+
const t = readFileSync41(join49(repoRoot2, f), "utf8");
|
|
13556
14212
|
return titles.some((q) => t.includes(q));
|
|
13557
14213
|
} catch {
|
|
13558
14214
|
return false;
|
|
@@ -13563,11 +14219,11 @@ function specConflictWhy(repoRoot2, quote4, message, failing) {
|
|
|
13563
14219
|
}
|
|
13564
14220
|
function buildCommand(repoRoot2) {
|
|
13565
14221
|
try {
|
|
13566
|
-
if (JSON.parse(
|
|
14222
|
+
if (JSON.parse(readFileSync41(join49(repoRoot2, "package.json"), "utf8"))?.scripts?.build) return "npm run build";
|
|
13567
14223
|
} catch {
|
|
13568
14224
|
}
|
|
13569
14225
|
try {
|
|
13570
|
-
if (/^build\s*:/m.test(
|
|
14226
|
+
if (/^build\s*:/m.test(readFileSync41(join49(repoRoot2, "Makefile"), "utf8"))) return "make build";
|
|
13571
14227
|
} catch {
|
|
13572
14228
|
}
|
|
13573
14229
|
return null;
|
|
@@ -13582,7 +14238,7 @@ function rewrittenByBuild(repoRoot2, events, path) {
|
|
|
13582
14238
|
const built = events.slice(lastWrite + 1).some((e) => e.type === "command" && e.raw?.ok !== false && !!e.command && BUILD_RUN.test(e.command));
|
|
13583
14239
|
if (!built) return false;
|
|
13584
14240
|
try {
|
|
13585
|
-
return statSync8(
|
|
14241
|
+
return statSync8(join49(repoRoot2, path)).mtimeMs > Date.parse(events[lastWrite].ts);
|
|
13586
14242
|
} catch {
|
|
13587
14243
|
return false;
|
|
13588
14244
|
}
|
|
@@ -13602,21 +14258,21 @@ function lastAgentMessage(events) {
|
|
|
13602
14258
|
}
|
|
13603
14259
|
function refreshReportTime(repoRoot2, sessionId2) {
|
|
13604
14260
|
const stem2 = sessionId2.replace(/[^A-Za-z0-9_-]/g, "") || "session";
|
|
13605
|
-
const mdPath =
|
|
13606
|
-
const htmlPath =
|
|
13607
|
-
if (!
|
|
14261
|
+
const mdPath = join49(repoRoot2, REPLUME_DIR, "reports", `${stem2}.md`);
|
|
14262
|
+
const htmlPath = join49(repoRoot2, REPLUME_DIR, "reports", `${stem2}.html`);
|
|
14263
|
+
if (!existsSync37(mdPath)) return false;
|
|
13608
14264
|
try {
|
|
13609
14265
|
const lines = timeAndTokensLines(repoRoot2, sessionId2, agentOfSession(repoRoot2, sessionId2), sessionEvents(repoRoot2, sessionId2));
|
|
13610
14266
|
const esc = (t) => t.replace(/([\\`*_[])/g, "\\$1");
|
|
13611
14267
|
const htmlEsc = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
13612
|
-
const md2 =
|
|
14268
|
+
const md2 = readFileSync41(mdPath, "utf8");
|
|
13613
14269
|
const mdNew = md2.replace(/(## Time and tokens\n\n)((?:- .*\n?)+)/, (_m, head) => `${head}${lines.map((l) => `- ${esc(l)}`).join("\n")}
|
|
13614
14270
|
`);
|
|
13615
|
-
if (mdNew !== md2)
|
|
13616
|
-
if (
|
|
13617
|
-
const html2 =
|
|
14271
|
+
if (mdNew !== md2) writeFileSync25(mdPath, mdNew);
|
|
14272
|
+
if (existsSync37(htmlPath)) {
|
|
14273
|
+
const html2 = readFileSync41(htmlPath, "utf8");
|
|
13618
14274
|
const htmlNew = html2.replace(/(<h2>Time and tokens<\/h2>\s*<ul>)([\s\S]*?)(<\/ul>)/, (_m, a, _b, c) => `${a}${lines.map((l) => `<li>${htmlEsc(l)}</li>`).join("")}${c}`);
|
|
13619
|
-
if (htmlNew !== html2)
|
|
14275
|
+
if (htmlNew !== html2) writeFileSync25(htmlPath, htmlNew);
|
|
13620
14276
|
}
|
|
13621
14277
|
return mdNew !== md2;
|
|
13622
14278
|
} catch {
|
|
@@ -13640,7 +14296,7 @@ function stepsSection(events, repoRoot2) {
|
|
|
13640
14296
|
}
|
|
13641
14297
|
function assumptions(f) {
|
|
13642
14298
|
const out = [];
|
|
13643
|
-
out.push(`Mode: ${f.config.mode}${f.config.modeSource === "default" ? ` (the default; ${
|
|
14299
|
+
out.push(`Mode: ${f.config.mode}${f.config.modeSource === "default" ? ` (the default; ${existsSync37(configPath(f.repoRoot)) ? `\`${REPLUME_DIR}/config\` has no valid mode` : `no \`${REPLUME_DIR}/config\``})` : ` (from \`${REPLUME_DIR}/config\`)`}.`);
|
|
13644
14300
|
for (const p of f.config.problems) out.push(`Config problem: ${p}.`);
|
|
13645
14301
|
const contract = (() => {
|
|
13646
14302
|
try {
|
|
@@ -13676,7 +14332,7 @@ function assumptions(f) {
|
|
|
13676
14332
|
const tree = (c) => {
|
|
13677
14333
|
if (!c) return null;
|
|
13678
14334
|
try {
|
|
13679
|
-
return
|
|
14335
|
+
return execFileSync30("git", ["rev-parse", `${c}^{tree}`], { cwd: f.repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
13680
14336
|
} catch {
|
|
13681
14337
|
return null;
|
|
13682
14338
|
}
|
|
@@ -13706,14 +14362,14 @@ function headline(status, report, steps, incomplete, extra) {
|
|
|
13706
14362
|
}
|
|
13707
14363
|
var revParse = (repoRoot2, ref) => {
|
|
13708
14364
|
try {
|
|
13709
|
-
return
|
|
14365
|
+
return git9(repoRoot2, ["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
13710
14366
|
} catch {
|
|
13711
14367
|
return null;
|
|
13712
14368
|
}
|
|
13713
14369
|
};
|
|
13714
14370
|
function removedTestsBetween(repoRoot2, base4, commit) {
|
|
13715
14371
|
try {
|
|
13716
|
-
return removedTests(
|
|
14372
|
+
return removedTests(execFileSync30(
|
|
13717
14373
|
"git",
|
|
13718
14374
|
["diff", "--no-color", "-U0", base4, commit, "--", ".", `:(exclude)${REPLUME_DIR}`],
|
|
13719
14375
|
{ cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }
|
|
@@ -13724,7 +14380,7 @@ function removedTestsBetween(repoRoot2, base4, commit) {
|
|
|
13724
14380
|
}
|
|
13725
14381
|
function numstat(repoRoot2, base4, commit) {
|
|
13726
14382
|
try {
|
|
13727
|
-
return
|
|
14383
|
+
return git9(repoRoot2, ["diff", "--numstat", base4, commit]).split("\n").filter(Boolean).flatMap((line2) => {
|
|
13728
14384
|
const [a, r, ...rest] = line2.split(" ");
|
|
13729
14385
|
const path = rest.join(" ");
|
|
13730
14386
|
if (!path || path.startsWith(`${REPLUME_DIR}/`)) return [];
|
|
@@ -13780,6 +14436,10 @@ async function autoFinish(repoRoot2, sessionId2, opts = {}) {
|
|
|
13780
14436
|
ensureDependencyExcludes(repoRoot2);
|
|
13781
14437
|
confirmEdits(repoRoot2, sessionId2, "copilot");
|
|
13782
14438
|
confirmEdits(repoRoot2, sessionId2, "hermes");
|
|
14439
|
+
{
|
|
14440
|
+
const agentOf = readEvents(ledgerPath(repoRoot2)).events.find((e) => e.sessionId === sessionId2)?.agent;
|
|
14441
|
+
if (agentOf) landTreeChanges(repoRoot2, baseCommitOf(repoRoot2), { agent: agentOf, sessionId: sessionId2 });
|
|
14442
|
+
}
|
|
13783
14443
|
const config = loadConfig(repoRoot2);
|
|
13784
14444
|
const events = sessionEvents(repoRoot2, sessionId2);
|
|
13785
14445
|
const incompleteHit = [...events].reverse().find((e) => e.type === "rule_hit" && e.rule === AUTO_INCOMPLETE_RULE);
|
|
@@ -13846,6 +14506,7 @@ async function autoFinish(repoRoot2, sessionId2, opts = {}) {
|
|
|
13846
14506
|
const now = run && !run.runnerMissing ? parseTestCounts(`${run.stdoutTail}
|
|
13847
14507
|
${run.stderrTail}`) : null;
|
|
13848
14508
|
const after = now ? now.passed + now.failed + now.skipped : null;
|
|
14509
|
+
if (typeof before === "number" && after !== null && after < before && contract && base4 && casesExplainDrop(repoRoot2, base4, contract, changesList.map((c) => c.path), before - after, snap?.commit)) return null;
|
|
13849
14510
|
return typeof before === "number" && after !== null && after < before ? `fewer tests ran than at the task start (${before} \u2192 ${after}): a test may have been removed, skipped or deselected` : null;
|
|
13850
14511
|
})();
|
|
13851
14512
|
const ignoredWritten = (report && snap && !fail2 ? ignoredAmong(repoRoot2, events.flatMap((e) => e.type === "file_edit" && e.raw?.ok !== false ? e.paths : commandWrites(e))) : []).filter((p) => !rewrittenByBuild(repoRoot2, events, p));
|
|
@@ -13855,12 +14516,19 @@ ${run.stderrTail}`) : null;
|
|
|
13855
14516
|
const cleanRed = report?.verdict === "fail" && !report.tampering.length && !report.extra.some((x) => !x.informational);
|
|
13856
14517
|
const gateSawRed = events.some((e) => e.type === "rule_hit" && (e.rule === AUTO_INCOMPLETE_RULE || e.rule === "auto_stop_gate" && e.raw?.outcome === "block"));
|
|
13857
14518
|
const failingNames = report?.scenarios.find((r) => r.id === "test_command")?.failingTests ?? [];
|
|
13858
|
-
const stated = report?.verdict === "pass" ? statedSpecTestConflict(lastAgentMessage(events)) : cleanRed ? statedSpecTestConflict(lastAgentMessage(events), { red: gateSawRed }) : null;
|
|
14519
|
+
const stated = report?.verdict === "pass" ? statedSpecTestConflict(lastAgentMessage(events)) : cleanRed ? statedSpecTestConflict(lastAgentMessage(events), { red: gateSawRed }) ?? reportedConflict(events) : null;
|
|
13859
14520
|
const conflictWhy = stated ? specConflictWhy(repoRoot2, stated, lastAgentMessage(events), failingNames) : null;
|
|
13860
14521
|
const inCommit = contract && snap?.snapshot ? committedProtected(repoRoot2, contract) : null;
|
|
13861
14522
|
const restoredInCommit = inCommit ? inCommit.paths.filter((p) => !changesList.some((c) => c.path === p)) : [];
|
|
13862
14523
|
const committedWhy = restoredInCommit.length ? `commit ${inCommit.commit} holds a change to protected ${restoredInCommit.join(", ")} that the working tree no longer has, and Replume checked the working tree, not that commit. Next: \`git revert ${inCommit.commit}\`` : null;
|
|
13863
|
-
const
|
|
14524
|
+
const askedDeletions = contract ? recordedAskedDeletions(repoRoot2, contract.id) ?? deletionsAskedFor(contract.task, repoRoot2, void 0, report?.baseCommit ?? void 0) : null;
|
|
14525
|
+
const leftBehind = report && report.verdict !== "fail" && askedDeletions ? askedDeletions.files.filter((f) => existsSync37(join49(repoRoot2, f))) : [];
|
|
14526
|
+
const leftWhy = leftBehind.length ? `the task asks to delete ${leftBehind.slice(0, 3).join(", ")}${leftBehind.length > 3 ? ` and ${leftBehind.length - 3} more` : ""}, which ${leftBehind.length === 1 ? "is" : "are"} still there` : null;
|
|
14527
|
+
const changedValues = report?.verdict === "pass" && contract && since.base ? askedTestChanges(repoRoot2, since.base, contract, changesList.map((c) => c.path), snap?.commit) : [];
|
|
14528
|
+
const valueLine = (c) => c.kind === "values" ? `${c.path} (${c.changes.slice(0, 3).map((x) => `${x.input}: ${x.from} -> ${x.to}`).join("; ")}${c.changes.length > 3 ? `; and ${c.changes.length - 3} more` : ""})` : `${c.path} (Replume cannot read this test as cases, or a case went away: read its diff)`;
|
|
14529
|
+
const valueWhy = changedValues.length ? `an expected value ${changedValues.every((c) => c.kind === "values") ? "" : "may have "}changed in ${changedValues.map(valueLine).join(", ")}` : null;
|
|
14530
|
+
const valueConfirm = changedValues.length ? changedValues.map((c) => c.confirm).join(" && ") : void 0;
|
|
14531
|
+
const incompleteWhy = (copyLimit ? problem : null) ?? committedWhy ?? (outsideReason ? null : conflictWhy) ?? valueWhy ?? leftWhy ?? (fail2 ? null : incomplete) ?? outsideReason ?? ignoredWhy ?? (scriptBlind && !fail2 ? blindLine(blindScripts[0]) : null) ?? (shrink && !fail2 ? shrink : null);
|
|
13864
14532
|
const testRun = report?.scenarios.find((r) => r.id === "test_command");
|
|
13865
14533
|
const preCount = report?.preExistingFailures?.length ?? 0;
|
|
13866
14534
|
const preScenarioCount = report?.preExistingScenarios?.length ?? 0;
|
|
@@ -13875,18 +14543,19 @@ ${run.stderrTail}`) : null;
|
|
|
13875
14543
|
const unchangedSince = (p) => {
|
|
13876
14544
|
if (!base4 || !snap) return false;
|
|
13877
14545
|
try {
|
|
13878
|
-
|
|
14546
|
+
execFileSync30("git", ["diff", "--quiet", base4, snap.commit, "--", p], { cwd: repoRoot2, stdio: "ignore" });
|
|
13879
14547
|
return true;
|
|
13880
14548
|
} catch {
|
|
13881
14549
|
return false;
|
|
13882
14550
|
}
|
|
13883
14551
|
};
|
|
13884
|
-
const
|
|
14552
|
+
const wholeSuite = contract ? testEditsAskedFor(contract.task, repoRoot2, void 0, report?.baseCommit ?? void 0).why : {};
|
|
14553
|
+
const askedUnchanged = report && !noChanges ? asked.filter((p) => wholeSuite[p] !== WHOLE_SUITE_WHY).filter(unchangedSince) : [];
|
|
13885
14554
|
const notDone = status === "PASS" && askedUnchanged.length ? `PASS - not done: the task asks to change ${askedUnchanged.slice(0, 3).join(", ")}, which ${askedUnchanged.length === 1 ? "is" : "are"} unchanged` : null;
|
|
13886
14555
|
const verdictText = status === "NOT VERIFIED" ? "NOT VERIFIED" : taskRed ? taskRed : notDone ? notDone : status === "PASS" && elsewhere ? `PASS - ${elsewhere.text}` : status === "NO CHANGES" ? `NO CHANGES \u2014 ${noChangesWhy}` : status === "PASS" && previous.length ? `PASS (${previous.join("; ")})` : void 0;
|
|
13887
14556
|
const stem2 = sessionId2.replace(/[^A-Za-z0-9_-]/g, "") || "session";
|
|
13888
|
-
const reportPath =
|
|
13889
|
-
const htmlPath =
|
|
14557
|
+
const reportPath = join49(REPLUME_DIR, "reports", `${stem2}.md`);
|
|
14558
|
+
const htmlPath = join49(REPLUME_DIR, "reports", `${stem2}.html`);
|
|
13890
14559
|
const head0 = headline(status, report, steps, status === "NO CHANGES" ? noChangesWhy : noCommit ? NO_COMMIT : incompleteWhy, problem && !noCommit && !copyLimit ? `could not verify: ${cell(problem, 160)}` : copyLimit ? "nothing verified" : void 0);
|
|
13891
14560
|
const head = elsewhere && status === "PASS" ? head0.replace(/^PASS/, `PASS - ${elsewhere.text}`) : head0;
|
|
13892
14561
|
const facts2 = { config, repoRoot: repoRoot2, sessionId: sessionId2, events, snap, report };
|
|
@@ -13902,7 +14571,9 @@ ${run.stderrTail}`) : null;
|
|
|
13902
14571
|
noCommit,
|
|
13903
14572
|
snapshotFailed: since.unavailable ?? null,
|
|
13904
14573
|
unseenChanges: outside.length > 0 || !!scriptBlind,
|
|
13905
|
-
shrink
|
|
14574
|
+
shrink,
|
|
14575
|
+
askedLeft: leftWhy && incompleteWhy === leftWhy ? leftBehind : void 0,
|
|
14576
|
+
expectationConfirm: valueWhy && incompleteWhy === valueWhy ? valueConfirm : void 0
|
|
13906
14577
|
});
|
|
13907
14578
|
const next = nextLine({ status, recovery: nextClass.recovery }, { uncommitted: !!snap?.snapshot });
|
|
13908
14579
|
const rt = runtime ? {
|
|
@@ -13912,6 +14583,7 @@ ${run.stderrTail}`) : null;
|
|
|
13912
14583
|
...runtime.evidencePaths.length ? { screenshots: runtime.evidencePaths } : {}
|
|
13913
14584
|
} : { status: "not_checked", reason: config.auto.runtime === "off" ? "the runtime check is off (auto.runtime: off)" : report ? "the runtime check did not run" : "nothing was verified, so no runtime check ran" };
|
|
13914
14585
|
const extras = {
|
|
14586
|
+
...changedValues.length ? { askedTestChanges: changedValues.flatMap((c) => [...c.changes.map((x) => `${c.path}: ${x.input}: was ${x.from}, now ${x.to}`), ...c.kind === "other" ? [`${c.path}: cannot be read as cases, or a case went away. Its diff:`, ...c.diff.map((l) => `\`${l}\``)] : []]) } : {},
|
|
13915
14587
|
verdict: status === "PASS" ? "pass" : status === "FAIL" ? "fail" : "incomplete",
|
|
13916
14588
|
...status === "INCOMPLETE" && incompleteWhy ? { verdictReason: incompleteWhy } : status === "FAIL" && report ? { verdictReason: visibleFailureReason(report) } : {},
|
|
13917
14589
|
// CHECK_NOT_RUN is only for a check that did not run
|
|
@@ -13963,14 +14635,14 @@ ${differs}\`\`\`
|
|
|
13963
14635
|
${renderReport(report).replace(/^Verdict: /, "Check run on the snapshot: ")}
|
|
13964
14636
|
\`\`\`
|
|
13965
14637
|
` : rendered.markdown;
|
|
13966
|
-
|
|
13967
|
-
|
|
13968
|
-
|
|
14638
|
+
mkdirSync25(join49(repoRoot2, REPLUME_DIR, "reports"), { recursive: true });
|
|
14639
|
+
writeFileSync25(join49(repoRoot2, reportPath), redactLedgerText(full, repoRoot2));
|
|
14640
|
+
writeFileSync25(join49(repoRoot2, htmlPath), redactLedgerText(rendered.html, repoRoot2));
|
|
13969
14641
|
try {
|
|
13970
14642
|
const treeOf = (c) => {
|
|
13971
14643
|
if (!c) return null;
|
|
13972
14644
|
try {
|
|
13973
|
-
return
|
|
14645
|
+
return execFileSync30("git", ["rev-parse", `${c}^{tree}`], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
13974
14646
|
} catch {
|
|
13975
14647
|
return null;
|
|
13976
14648
|
}
|
|
@@ -13988,7 +14660,9 @@ ${renderReport(report).replace(/^Verdict: /, "Check run on the snapshot: ")}
|
|
|
13988
14660
|
noCommit,
|
|
13989
14661
|
snapshotFailed: since.unavailable ?? null,
|
|
13990
14662
|
unseenChanges: unseen,
|
|
13991
|
-
shrink
|
|
14663
|
+
shrink,
|
|
14664
|
+
askedLeft: leftWhy && incompleteWhy === leftWhy ? leftBehind : void 0,
|
|
14665
|
+
expectationConfirm: valueWhy && incompleteWhy === valueWhy ? valueConfirm : void 0
|
|
13992
14666
|
});
|
|
13993
14667
|
writeSummary(repoRoot2, stem2, { v: 1, session: sessionId2, status, verdict_line: (redactLedgerText(full, repoRoot2).split("\n")[0] ?? "").replace(/^Verdict:\s*/, ""), ...cls, ...ignoredNow?.changes.length ? { ignored_changed: ignoredNow.changes.map((c) => c.path) } : {}, tree: { verified: treeOf(snap?.commit), task_start: treeOf(base4) }, report: reportPath, generated_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13994
14668
|
} catch {
|
|
@@ -14084,7 +14758,7 @@ function reviewAtStop(i) {
|
|
|
14084
14758
|
...withRenames(i.repoRoot, testEditsAskedFor(i.contract.task, i.repoRoot, void 0, base4).paths, base4),
|
|
14085
14759
|
...changedFiles.filter((p) => {
|
|
14086
14760
|
try {
|
|
14087
|
-
return isConfirmed(i.repoRoot, p,
|
|
14761
|
+
return isConfirmed(i.repoRoot, p, readFileSync42(join50(i.repoRoot, p), "utf8"));
|
|
14088
14762
|
} catch {
|
|
14089
14763
|
return false;
|
|
14090
14764
|
}
|
|
@@ -14109,15 +14783,15 @@ import { join as join54 } from "path";
|
|
|
14109
14783
|
import { homedir as homedir5 } from "os";
|
|
14110
14784
|
|
|
14111
14785
|
// src/core/auto-contract.ts
|
|
14112
|
-
import { execFileSync as
|
|
14113
|
-
import { existsSync as
|
|
14114
|
-
import { join as
|
|
14115
|
-
import { readFileSync as
|
|
14786
|
+
import { execFileSync as execFileSync31 } from "child_process";
|
|
14787
|
+
import { existsSync as existsSync38 } from "fs";
|
|
14788
|
+
import { join as join51 } from "path";
|
|
14789
|
+
import { readFileSync as readFileSync43 } from "fs";
|
|
14116
14790
|
var MAX_AUTO_DIRS = 12;
|
|
14117
14791
|
var MAX_IMPORT_ALLOW = 6;
|
|
14118
14792
|
function ensureAutoContract(input) {
|
|
14119
14793
|
const mode = readMode(input.repoRoot);
|
|
14120
|
-
const existing =
|
|
14794
|
+
const existing = existsSync38(contractPath(input.repoRoot)) ? loadContract(contractPath(input.repoRoot)) : null;
|
|
14121
14795
|
if (mode !== "auto" || !input.prompt.trim()) return { created: false, contractId: existing?.id ?? null, assumptions: [] };
|
|
14122
14796
|
const events = readEvents(ledgerPath(input.repoRoot)).events;
|
|
14123
14797
|
const autoRaw = (e) => e.raw ?? {};
|
|
@@ -14137,7 +14811,9 @@ function ensureAutoContract(input) {
|
|
|
14137
14811
|
for (const [p, why] of Object.entries(testEditsAskedFor(input.prompt.trim(), input.repoRoot).why))
|
|
14138
14812
|
notes.push(`allow change "${p}": ${why}; the task asks to change it (skip, removed tests or assertions and other weakening are still refused)`);
|
|
14139
14813
|
const allow = plan.allow;
|
|
14140
|
-
const
|
|
14814
|
+
const asked = testEditsAskedFor(input.prompt.trim(), input.repoRoot).paths;
|
|
14815
|
+
const tracked2 = trackedFiles(input.repoRoot);
|
|
14816
|
+
const protect = [...new Set(plan.protect.flatMap((g) => asked.includes(g) ? [] : /[*?[]/.test(g) && asked.some((p) => matchesAny(p, [g])) ? tracked2.filter((f) => matchesAny(f, [g]) && !asked.includes(f)) : [g]))];
|
|
14141
14817
|
const confirmed = { ...contract, allow, protect, status: "confirmed" };
|
|
14142
14818
|
const path = contractPath(input.repoRoot);
|
|
14143
14819
|
writeContract(path, confirmed);
|
|
@@ -14155,6 +14831,10 @@ function ensureAutoContract(input) {
|
|
|
14155
14831
|
recordAskedTests(input.repoRoot, confirmed.id, testEditsAskedFor(input.prompt.trim(), input.repoRoot).paths);
|
|
14156
14832
|
} catch {
|
|
14157
14833
|
}
|
|
14834
|
+
try {
|
|
14835
|
+
recordAskedDeletions(input.repoRoot, confirmed.id, deletionsAskedFor(input.prompt.trim(), input.repoRoot));
|
|
14836
|
+
} catch {
|
|
14837
|
+
}
|
|
14158
14838
|
try {
|
|
14159
14839
|
recordIgnoredState(input.repoRoot, confirmed.id);
|
|
14160
14840
|
} catch {
|
|
@@ -14164,7 +14844,7 @@ function ensureAutoContract(input) {
|
|
|
14164
14844
|
snap.unavailable ? `snapshot_unavailable: ${snap.unavailable}; changes outside captured tools will NOT be checked, and Replume will not tell the agent to restore files to HEAD` : `worktree snapshot ${snap.commit ?? "none"}${snap.dirty ? " (dirty tree)" : " (clean tree)"}`,
|
|
14165
14845
|
...notes
|
|
14166
14846
|
];
|
|
14167
|
-
if (
|
|
14847
|
+
if (existsSync38(join51(input.repoRoot, REPLUME_DIR))) {
|
|
14168
14848
|
appendEvent(ledgerPath(input.repoRoot), {
|
|
14169
14849
|
agent: input.agent ?? "claude-code",
|
|
14170
14850
|
agentVersion: null,
|
|
@@ -14182,7 +14862,7 @@ function ensureAutoContract(input) {
|
|
|
14182
14862
|
}
|
|
14183
14863
|
const askedTests = testEditsAskedFor(confirmed.task, input.repoRoot).paths;
|
|
14184
14864
|
const named = protectedTestsNamedInTask(confirmed.task, confirmed.protect).filter((p) => !askedTests.includes(p));
|
|
14185
|
-
if (named.length &&
|
|
14865
|
+
if (named.length && existsSync38(join51(input.repoRoot, REPLUME_DIR))) {
|
|
14186
14866
|
appendEvent(ledgerPath(input.repoRoot), {
|
|
14187
14867
|
agent: input.agent ?? "claude-code",
|
|
14188
14868
|
agentVersion: null,
|
|
@@ -14216,14 +14896,14 @@ function ensureAutoContractLate(input) {
|
|
|
14216
14896
|
const events = readEvents(ledgerPath(input.repoRoot)).events;
|
|
14217
14897
|
const mine = events.filter((e) => e.sessionId === input.sessionId);
|
|
14218
14898
|
if (mine.some((e) => e.raw?.autoContract === true)) return "have";
|
|
14219
|
-
const existing =
|
|
14899
|
+
const existing = existsSync38(contractPath(input.repoRoot)) ? loadContract(contractPath(input.repoRoot)) : null;
|
|
14220
14900
|
const madeByAuto = existing && events.some((e) => e.raw?.autoContract === true && e.raw.contractId === existing.id);
|
|
14221
14901
|
if (existing && !madeByAuto) return "have";
|
|
14222
14902
|
const prompt2 = firstPromptOf(mine, input.sessionId);
|
|
14223
14903
|
if (prompt2) return ensureAutoContract({ repoRoot: input.repoRoot, sessionId: input.sessionId, prompt: prompt2, agent: input.agent, late: input.hook }).created ? "created" : "have";
|
|
14224
14904
|
const fromAgent = input.fallbackPrompt?.() ?? null;
|
|
14225
14905
|
if (fromAgent) return ensureAutoContract({ repoRoot: input.repoRoot, sessionId: input.sessionId, prompt: fromAgent, agent: input.agent, late: `${input.hook}, prompt read from the agent's transcript` }).created ? "created" : "have";
|
|
14226
|
-
if (!mine.some((e) => e.rule === AUTO_CONTRACT_UNAVAILABLE_RULE) &&
|
|
14906
|
+
if (!mine.some((e) => e.rule === AUTO_CONTRACT_UNAVAILABLE_RULE) && existsSync38(join51(input.repoRoot, REPLUME_DIR))) {
|
|
14227
14907
|
appendEvent(ledgerPath(input.repoRoot), {
|
|
14228
14908
|
agent: input.agent,
|
|
14229
14909
|
agentVersion: null,
|
|
@@ -14272,9 +14952,20 @@ function autoScope(i) {
|
|
|
14272
14952
|
if (matchesAny(probe, allow) || allow.includes(p) || matchesAny(probe, protect) || isNeverAllowed(probe) || isTestFile(probe) || isTestSupportFile(probe)) continue;
|
|
14273
14953
|
allow.push(p);
|
|
14274
14954
|
notes.push(`allow "${p}": the task names it`);
|
|
14955
|
+
const dir2 = p.endsWith(".py") && p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : null;
|
|
14956
|
+
const init = dir2 ? `${dir2}/__init__.py` : null;
|
|
14957
|
+
if (dir2 && init && !i.files.some((f) => f.startsWith(`${dir2}/`)) && !matchesAny(init, allow) && !matchesAny(init, protect) && !allow.includes(init)) {
|
|
14958
|
+
allow.push(init);
|
|
14959
|
+
notes.push(`allow "${init}": the package file of ${p}, which the task names, in a folder that does not exist yet`);
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
for (const f of deletionsAskedFor(i.task, i.repoRoot, i.files).files) {
|
|
14963
|
+
if (matchesAny(f, allow) || matchesAny(f, protect) || isNeverAllowed(f)) continue;
|
|
14964
|
+
allow.push(f);
|
|
14965
|
+
notes.push(`allow "${f}": the task asks to delete it`);
|
|
14275
14966
|
}
|
|
14276
14967
|
const taskWords = words4(i.task);
|
|
14277
|
-
const tests = protect.filter((p) => !/[*?[]/.test(p) && isTestFile(p) &&
|
|
14968
|
+
const tests = protect.filter((p) => !/[*?[]/.test(p) && isTestFile(p) && existsSync38(join51(i.repoRoot, p)));
|
|
14278
14969
|
const failing = new Set((i.failingTests ?? []).map(normalizeRel));
|
|
14279
14970
|
const relevant = tests.filter((t) => named(t) || failing.has(t) || taskWords.has(stemOf(t)) || importedSources(i.repoRoot, t).some(named));
|
|
14280
14971
|
const reasonFor = (t) => named(t) ? "the task names it" : failing.has(t) ? "it fails now" : taskWords.has(stemOf(t)) ? `its name matches the task word "${stemOf(t)}"` : "it imports a file the task names";
|
|
@@ -14318,13 +15009,13 @@ function autoScope(i) {
|
|
|
14318
15009
|
}
|
|
14319
15010
|
var AUTO_ALLOW_EXTENDED_RULE = "auto_allow_extended";
|
|
14320
15011
|
function failingProtectedTests(repoRoot2, protect, output) {
|
|
14321
|
-
const tests = protect.filter((p) => !/[*?[]/.test(p) && isTestFile(p) &&
|
|
15012
|
+
const tests = protect.filter((p) => !/[*?[]/.test(p) && isTestFile(p) && existsSync38(join51(repoRoot2, p)));
|
|
14322
15013
|
const names3 = failingTestNames(output);
|
|
14323
15014
|
return tests.filter((t) => {
|
|
14324
|
-
if (output.includes(t) || output.includes(
|
|
15015
|
+
if (output.includes(t) || output.includes(join51(repoRoot2, t))) return true;
|
|
14325
15016
|
let text = "";
|
|
14326
15017
|
try {
|
|
14327
|
-
text =
|
|
15018
|
+
text = readFileSync43(join51(repoRoot2, t), "utf8");
|
|
14328
15019
|
} catch {
|
|
14329
15020
|
return false;
|
|
14330
15021
|
}
|
|
@@ -14333,7 +15024,7 @@ function failingProtectedTests(repoRoot2, protect, output) {
|
|
|
14333
15024
|
}
|
|
14334
15025
|
function extendAutoAllowForFailures(i) {
|
|
14335
15026
|
const path = contractPath(i.repoRoot);
|
|
14336
|
-
const c =
|
|
15027
|
+
const c = existsSync38(path) ? loadContract(path) : null;
|
|
14337
15028
|
if (!c || c.status === "draft") return { added: [], notes: [] };
|
|
14338
15029
|
const events = readEvents(ledgerPath(i.repoRoot)).events;
|
|
14339
15030
|
const madeByAuto = events.some((e) => (e.raw ?? {}).autoContract === true && (e.raw ?? {}).contractId === c.id);
|
|
@@ -14376,7 +15067,7 @@ function extendAutoAllowForFailures(i) {
|
|
|
14376
15067
|
}
|
|
14377
15068
|
function modifiedNow(repoRoot2) {
|
|
14378
15069
|
try {
|
|
14379
|
-
const out =
|
|
15070
|
+
const out = execFileSync31("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
14380
15071
|
const recs = out.split("\0").filter(Boolean);
|
|
14381
15072
|
const paths = [];
|
|
14382
15073
|
for (let i = 0; i < recs.length; i++) {
|
|
@@ -14390,125 +15081,6 @@ function modifiedNow(repoRoot2) {
|
|
|
14390
15081
|
}
|
|
14391
15082
|
}
|
|
14392
15083
|
|
|
14393
|
-
// src/core/shell-observe.ts
|
|
14394
|
-
import { execFileSync as execFileSync31 } from "child_process";
|
|
14395
|
-
import { createHash as createHash9 } from "crypto";
|
|
14396
|
-
import { existsSync as existsSync38, lstatSync as lstatSync7, mkdirSync as mkdirSync25, readFileSync as readFileSync43, rmSync as rmSync14, writeFileSync as writeFileSync25 } from "fs";
|
|
14397
|
-
import { tmpdir as tmpdir11 } from "os";
|
|
14398
|
-
import { join as join51, resolve as resolve6 } from "path";
|
|
14399
|
-
var MAX_TRACKED = 5e3;
|
|
14400
|
-
var SKIP2 = /^(\.claude|\.cursor|\.codex|\.git)\/|^\.replume\/(?!config$)/;
|
|
14401
|
-
function needsTreeCompare(a) {
|
|
14402
|
-
if (a.kind !== "command" || !a.command) return false;
|
|
14403
|
-
if (a.tool === "PowerShell") return true;
|
|
14404
|
-
if (a.writeTargetNotParsed) return true;
|
|
14405
|
-
const code2 = stripShellComments(a.command);
|
|
14406
|
-
return INLINE_CODE.test(code2) || unobservable(code2);
|
|
14407
|
-
}
|
|
14408
|
-
var git9 = (cwd, args, input) => execFileSync31("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", input, stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
|
|
14409
|
-
function blobsOnDisk(top, paths) {
|
|
14410
|
-
const out = /* @__PURE__ */ new Map();
|
|
14411
|
-
const files = paths.filter((p) => {
|
|
14412
|
-
try {
|
|
14413
|
-
return lstatSync7(join51(top, p)).isFile();
|
|
14414
|
-
} catch {
|
|
14415
|
-
return false;
|
|
14416
|
-
}
|
|
14417
|
-
});
|
|
14418
|
-
for (const p of paths) out.set(p, null);
|
|
14419
|
-
if (!files.length) return out;
|
|
14420
|
-
const ids = git9(top, ["hash-object", "--stdin-paths"], files.join("\n") + "\n").trim().split("\n");
|
|
14421
|
-
files.forEach((p, i) => out.set(p, ids[i] ?? null));
|
|
14422
|
-
return out;
|
|
14423
|
-
}
|
|
14424
|
-
function where(repoRoot2) {
|
|
14425
|
-
const top = git9(repoRoot2, ["rev-parse", "--show-toplevel"]).trim();
|
|
14426
|
-
const prefix = git9(repoRoot2, ["rev-parse", "--show-prefix"]).trim();
|
|
14427
|
-
return { top, prefix };
|
|
14428
|
-
}
|
|
14429
|
-
function changedNow(repoRoot2) {
|
|
14430
|
-
const { top, prefix } = where(repoRoot2);
|
|
14431
|
-
let head = null;
|
|
14432
|
-
try {
|
|
14433
|
-
head = git9(top, ["rev-parse", "--verify", "-q", "HEAD"]).trim() || null;
|
|
14434
|
-
} catch {
|
|
14435
|
-
head = null;
|
|
14436
|
-
}
|
|
14437
|
-
const recs = git9(top, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--no-renames", "--", prefix ? prefix : "."]).split("\0").filter(Boolean);
|
|
14438
|
-
if (recs.length > MAX_TRACKED) return null;
|
|
14439
|
-
const paths = recs.map((r) => r.slice(3)).filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length)).filter((p) => !SKIP2.test(p));
|
|
14440
|
-
return { top, prefix, head, paths };
|
|
14441
|
-
}
|
|
14442
|
-
function treeState(repoRoot2) {
|
|
14443
|
-
try {
|
|
14444
|
-
const now = changedNow(repoRoot2);
|
|
14445
|
-
if (!now) return null;
|
|
14446
|
-
const blobs = blobsOnDisk(now.top, now.paths.map((p) => now.prefix + p));
|
|
14447
|
-
return { head: now.head, files: Object.fromEntries(now.paths.map((p) => [p, blobs.get(now.prefix + p) ?? null])) };
|
|
14448
|
-
} catch {
|
|
14449
|
-
return null;
|
|
14450
|
-
}
|
|
14451
|
-
}
|
|
14452
|
-
function changedSince2(repoRoot2, before) {
|
|
14453
|
-
try {
|
|
14454
|
-
const now = changedNow(repoRoot2);
|
|
14455
|
-
if (!now) return [];
|
|
14456
|
-
const all = [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...now.paths])].filter((p) => !SKIP2.test(p)).sort();
|
|
14457
|
-
const after = blobsOnDisk(now.top, all.map((p) => now.prefix + p));
|
|
14458
|
-
const unlisted = all.filter((p) => !(p in before.files));
|
|
14459
|
-
const inHead = /* @__PURE__ */ new Map();
|
|
14460
|
-
if (before.head && unlisted.length) {
|
|
14461
|
-
const text = git9(now.top, ["ls-tree", "-z", before.head, "--", ...unlisted.map((p) => now.prefix + p)]);
|
|
14462
|
-
for (const rec of text.split("\0").filter(Boolean)) {
|
|
14463
|
-
const m = /^\d+ blob ([0-9a-f]+)\t(.+)$/.exec(rec);
|
|
14464
|
-
if (m && m[2].startsWith(now.prefix)) inHead.set(m[2].slice(now.prefix.length), m[1]);
|
|
14465
|
-
}
|
|
14466
|
-
}
|
|
14467
|
-
const out = [];
|
|
14468
|
-
for (const p of all) {
|
|
14469
|
-
const b = p in before.files ? before.files[p] : inHead.get(p) ?? null;
|
|
14470
|
-
const a = after.get(now.prefix + p) ?? null;
|
|
14471
|
-
if (a !== b) out.push({ path: p, before: b, after: a });
|
|
14472
|
-
}
|
|
14473
|
-
return out;
|
|
14474
|
-
} catch {
|
|
14475
|
-
return [];
|
|
14476
|
-
}
|
|
14477
|
-
}
|
|
14478
|
-
var sha = (s, n3 = 16) => createHash9("sha1").update(s).digest("hex").slice(0, n3);
|
|
14479
|
-
var stateFile = (repoRoot2, sessionId2, toolUseId) => join51(tmpdir11(), "replume-tree", sha(resolve6(repoRoot2), 12), sha(sessionId2, 12), `${sha(toolUseId, 20)}.json`);
|
|
14480
|
-
function rememberTree(repoRoot2, sessionId2, toolUseId) {
|
|
14481
|
-
if (!toolUseId) return false;
|
|
14482
|
-
const state = treeState(repoRoot2);
|
|
14483
|
-
if (!state) return false;
|
|
14484
|
-
try {
|
|
14485
|
-
const f = stateFile(repoRoot2, sessionId2, toolUseId);
|
|
14486
|
-
mkdirSync25(join51(f, ".."), { recursive: true });
|
|
14487
|
-
writeFileSync25(f, JSON.stringify(state));
|
|
14488
|
-
return true;
|
|
14489
|
-
} catch {
|
|
14490
|
-
return false;
|
|
14491
|
-
}
|
|
14492
|
-
}
|
|
14493
|
-
function takeTreeChanges(repoRoot2, sessionId2, toolUseId) {
|
|
14494
|
-
if (!toolUseId) return null;
|
|
14495
|
-
const f = stateFile(repoRoot2, sessionId2, toolUseId);
|
|
14496
|
-
let state;
|
|
14497
|
-
try {
|
|
14498
|
-
if (!existsSync38(f)) return null;
|
|
14499
|
-
state = JSON.parse(readFileSync43(f, "utf-8"));
|
|
14500
|
-
} catch {
|
|
14501
|
-
return null;
|
|
14502
|
-
} finally {
|
|
14503
|
-
rmSync14(f, { force: true });
|
|
14504
|
-
}
|
|
14505
|
-
if (!state || typeof state.files !== "object" || state.files === null) return null;
|
|
14506
|
-
return changedSince2(repoRoot2, state);
|
|
14507
|
-
}
|
|
14508
|
-
function forgetTree(repoRoot2, sessionId2, toolUseId) {
|
|
14509
|
-
if (toolUseId) rmSync14(stateFile(repoRoot2, sessionId2, toolUseId), { force: true });
|
|
14510
|
-
}
|
|
14511
|
-
|
|
14512
15084
|
// src/supervisor/types.ts
|
|
14513
15085
|
var RANK2 = { ok: 0, steer: 1, block: 2 };
|
|
14514
15086
|
var MAX_MESSAGE_CHARS = 1e3;
|
|
@@ -14664,11 +15236,11 @@ function doneMessage(s, allow = [], mode = "approve") {
|
|
|
14664
15236
|
import { createHash as createHash10 } from "crypto";
|
|
14665
15237
|
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync45, rmSync as rmSync15, statSync as statSync9, writeFileSync as writeFileSync26 } from "fs";
|
|
14666
15238
|
import { tmpdir as tmpdir12 } from "os";
|
|
14667
|
-
import { dirname as dirname17, join as join53, resolve as
|
|
15239
|
+
import { dirname as dirname17, join as join53, resolve as resolve8 } from "path";
|
|
14668
15240
|
var STEER_RULE_PREFIX = "supervisor_";
|
|
14669
15241
|
var steerRule = (source) => `${STEER_RULE_PREFIX}${source}`;
|
|
14670
15242
|
var MAX_FILE_BYTES = 3e5;
|
|
14671
|
-
var stashDir = (repoRoot2, sessionId2) => join53(tmpdir12(), "replume-before", createHash10("sha1").update(
|
|
15243
|
+
var stashDir = (repoRoot2, sessionId2) => join53(tmpdir12(), "replume-before", createHash10("sha1").update(resolve8(repoRoot2)).digest("hex").slice(0, 12), createHash10("sha1").update(sessionId2).digest("hex").slice(0, 12));
|
|
14672
15244
|
var stashFile = (repoRoot2, sessionId2, rel3, toolUseId) => join53(stashDir(repoRoot2, sessionId2), createHash10("sha1").update(`${toolUseId ?? ""}\0${rel3}`).digest("hex").slice(0, 20) + ".json");
|
|
14673
15245
|
function readText(abs) {
|
|
14674
15246
|
try {
|
|
@@ -14781,6 +15353,7 @@ function afterWrite(ctx, input, events, extraCandidates = []) {
|
|
|
14781
15353
|
if (before === null || after === null || before === after) continue;
|
|
14782
15354
|
const base4 = baseCommitOf(ctx.repoRoot);
|
|
14783
15355
|
if (base4 && fileAt(ctx.repoRoot, base4, rel3) === after) continue;
|
|
15356
|
+
if (mode === "auto" && ctx.contract && askedTestsNow(ctx.repoRoot, ctx.contract).includes(rel3) && sameInputsKept(before, after, rel3)) continue;
|
|
14784
15357
|
if (bypassApplies(rel3)) titles ??= workingTreeTestTitles(ctx.repoRoot);
|
|
14785
15358
|
const w = testWeakening(before, after, rel3, { mode, allow: ctx.contract?.allow, taskText: anchor.text || void 0, recentTestOutput: recent, testTitles: titles });
|
|
14786
15359
|
if (w.severity !== "ok" && w.steer) candidates.push({ source: "weakening", severity: w.severity, message: w.steer, detail: rel3 });
|
|
@@ -14788,7 +15361,8 @@ function afterWrite(ctx, input, events, extraCandidates = []) {
|
|
|
14788
15361
|
}
|
|
14789
15362
|
const stepIndex = events.map((e) => e.type === "file_edit" || e.type === "command" && (e.raw?.writes?.length ?? 0) > 0).lastIndexOf(true);
|
|
14790
15363
|
if (stepIndex >= 0) {
|
|
14791
|
-
const
|
|
15364
|
+
const askedDeletions = ctx.contract && mode === "auto" ? askedDeletionTargets(ctx.repoRoot, ctx.contract) : void 0;
|
|
15365
|
+
const d = driftScore(events[stepIndex], { task: anchor.text, allow: ctx.contract?.allow, testCommand, mode, askedDeletions }, events.slice(0, stepIndex));
|
|
14792
15366
|
if (d.nudge) candidates.push({ source: "drift", severity: "warn", message: d.nudge, detail: d.step.label });
|
|
14793
15367
|
if (d.nudge) driftExtra.outOfScope = d.outOfScopeWrites;
|
|
14794
15368
|
}
|
|
@@ -14814,7 +15388,9 @@ function deliverPendingJudge(ctx, events) {
|
|
|
14814
15388
|
function claimCandidate(ctx, events, lastAssistantMessage, stopHookActive) {
|
|
14815
15389
|
if (!ctx.config.supervisor.checks || stopHookActive || !lastAssistantMessage.trim()) return null;
|
|
14816
15390
|
if (taskClosed(events)) return null;
|
|
14817
|
-
const
|
|
15391
|
+
const base4 = baseCommitOf(ctx.repoRoot);
|
|
15392
|
+
const changedFromBaseNow = base4 ? [...changedFromBase(ctx.repoRoot, base4)?.keys() ?? []] : [];
|
|
15393
|
+
const r = claimCheck(lastAssistantMessage, events, { testCommand: ctx.contract?.test_command, changedFromBase: changedFromBaseNow });
|
|
14818
15394
|
return r.message ? { source: "claim", severity: "warn", message: r.message, detail: r.mismatches[0]?.kind } : null;
|
|
14819
15395
|
}
|
|
14820
15396
|
|
|
@@ -14857,7 +15433,7 @@ function ranToEvent(ran, session) {
|
|
|
14857
15433
|
|
|
14858
15434
|
// src/agents/claude/payload.ts
|
|
14859
15435
|
import { existsSync as existsSync41, readFileSync as readFileSync46 } from "fs";
|
|
14860
|
-
import { isAbsolute as isAbsolute5, resolve as
|
|
15436
|
+
import { isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
14861
15437
|
function parsePayload(text) {
|
|
14862
15438
|
const x = JSON.parse(text);
|
|
14863
15439
|
if (typeof x !== "object" || x === null) throw new Error("hook payload is not a JSON object");
|
|
@@ -14876,7 +15452,7 @@ function readIfExists(path) {
|
|
|
14876
15452
|
}
|
|
14877
15453
|
function absolutePathsIn(command) {
|
|
14878
15454
|
return (command.match(/(?<![\w.:/])(?:\/|~\/)[\w.@+-]+(?:\/[\w.@+-]*)*(?![\w.@+(/-])/g) ?? []).map(
|
|
14879
|
-
(p) => p.startsWith("~/") ?
|
|
15455
|
+
(p) => p.startsWith("~/") ? resolve9(process.env.HOME ?? "/", p.slice(2)) : p
|
|
14880
15456
|
);
|
|
14881
15457
|
}
|
|
14882
15458
|
function toProposedAction(p, repoRoot2) {
|
|
@@ -14892,7 +15468,7 @@ function toProposedAction(p, repoRoot2) {
|
|
|
14892
15468
|
const pending = [];
|
|
14893
15469
|
const abs = (fp) => {
|
|
14894
15470
|
const h = expandHome(fp);
|
|
14895
|
-
return isAbsolute5(h) || h.startsWith("~") ? h :
|
|
15471
|
+
return isAbsolute5(h) || h.startsWith("~") ? h : resolve9(p.cwd, h);
|
|
14896
15472
|
};
|
|
14897
15473
|
switch (tool) {
|
|
14898
15474
|
case "Bash":
|
|
@@ -15110,7 +15686,8 @@ function preToolUse(p, repoRoot2, env, deps) {
|
|
|
15110
15686
|
if (action.kind === "edit" || action.kind === "write" || action.kind === "command" && action.paths.length) {
|
|
15111
15687
|
if (loadConfig(repoRoot2).supervisor.checks) stashBefore(repoRoot2, p.session_id, action.paths, p.tool_use_id);
|
|
15112
15688
|
}
|
|
15113
|
-
|
|
15689
|
+
if (action.kind === "command") landTreeChanges(repoRoot2, baseCommitOf(repoRoot2), { agent: "claude-code", sessionId: p.session_id });
|
|
15690
|
+
const comparing = !baseCommitOf(repoRoot2) && needsTreeCompare(action) && rememberTree(repoRoot2, p.session_id, p.tool_use_id);
|
|
15114
15691
|
let session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
|
|
15115
15692
|
if (contractFromTranscript(p, repoRoot2, session, "PreToolUse")) session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
|
|
15116
15693
|
return andThen(decide2(p, repoRoot2, { kind: "action", action }, session, env, deps), ({ result: result2, contract }) => {
|
|
@@ -15169,7 +15746,7 @@ async function waitForAutoVerdict(repoRoot2, sessionId2) {
|
|
|
15169
15746
|
while (Date.now() < deadline) {
|
|
15170
15747
|
const verdict = currentVerdict(readEvents(ledgerPath(repoRoot2)).events.filter((e) => e.sessionId === sessionId2));
|
|
15171
15748
|
if (verdict) return verdict;
|
|
15172
|
-
await new Promise((
|
|
15749
|
+
await new Promise((resolve18) => setTimeout(resolve18, 100));
|
|
15173
15750
|
}
|
|
15174
15751
|
return null;
|
|
15175
15752
|
}
|
|
@@ -15230,6 +15807,7 @@ function recordStopChecks(p, ledger, results, contractId, lastAssistantMessage,
|
|
|
15230
15807
|
function stop(p, repoRoot2, env, deps) {
|
|
15231
15808
|
const startedAtMs = Date.now();
|
|
15232
15809
|
const ledger = ledgerPath(repoRoot2);
|
|
15810
|
+
landTreeChanges(repoRoot2, baseCommitOf(repoRoot2), { agent: "claude-code", sessionId: p.session_id });
|
|
15233
15811
|
let session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
|
|
15234
15812
|
if (contractFromTranscript(p, repoRoot2, session, "Stop")) session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
|
|
15235
15813
|
const lastAssistantMessage = typeof p.last_assistant_message === "string" ? p.last_assistant_message : "";
|
|
@@ -15361,8 +15939,10 @@ function postToolUse(p, repoRoot2, failed) {
|
|
|
15361
15939
|
const exitMatch = error ? /^Exit code (\d+)/.exec(error) : null;
|
|
15362
15940
|
const wctx = wireContext(p, repoRoot2);
|
|
15363
15941
|
const tail = action.kind === "command" && action.command && isTestCommand(action.command, wctx.contract?.test_command) ? outputTailOf(failed ? error : p.tool_response, failed ? null : error) : null;
|
|
15364
|
-
const
|
|
15365
|
-
const
|
|
15942
|
+
const fromBase = newTreeChanges(repoRoot2, baseCommitOf(repoRoot2));
|
|
15943
|
+
const changes = action.kind === "command" ? fromBase ?? takeTreeChanges(repoRoot2, p.session_id, p.tool_use_id) : null;
|
|
15944
|
+
const observed = action.kind !== "command" ? null : fromBase ? writesFromTree(fromBase, [...action.paths, ...action.certainPaths ?? [], ...action.managedWrites ?? []]) : observedWrites(action, changes);
|
|
15945
|
+
const alsoChanged = action.kind !== "command" && fromBase ? fromBase.filter((c) => !action.paths.includes(c.path)) : [];
|
|
15366
15946
|
const confirmed = failed && changes ? changes.map((c) => c.path) : null;
|
|
15367
15947
|
appendEvent(ledgerPath(repoRoot2), {
|
|
15368
15948
|
...base3(p, type),
|
|
@@ -15389,8 +15969,17 @@ function postToolUse(p, repoRoot2, failed) {
|
|
|
15389
15969
|
} : {}
|
|
15390
15970
|
}
|
|
15391
15971
|
});
|
|
15972
|
+
if (alsoChanged.length) {
|
|
15973
|
+
const files = alsoChanged.map((c) => c.path);
|
|
15974
|
+
appendEvent(ledgerPath(repoRoot2), { ...base3(p, "command"), tool: action.tool, paths: files, command: null, summary: `${action.tool} also changed ${files.join(", ")}`, rule: null, action: "ok", message: null, raw: { tool_use_id: p.tool_use_id ?? null, ok: true, treeSweep: true, ...writesFromTree(alsoChanged, []) } });
|
|
15975
|
+
}
|
|
15392
15976
|
const commandPaths = action.kind !== "command" ? [] : failed ? (action.certainPaths ?? []).filter((path) => confirmed?.includes(path)) : action.certainPaths ?? [];
|
|
15393
|
-
const
|
|
15977
|
+
const askedInRun = action.kind === "command" && wctx.contract && wctx.config.mode === "auto" && fromBase ? fromBase.map((c) => c.path).filter((f) => askedTestsNow(repoRoot2, wctx.contract).includes(f)) : [];
|
|
15978
|
+
const afterRun = askedInRun.flatMap((f) => {
|
|
15979
|
+
const m = judgeAskedAfterRun(repoRoot2, wctx.contract, f);
|
|
15980
|
+
return m ? [{ source: "weakening", severity: "warn", message: m, detail: f }] : [];
|
|
15981
|
+
});
|
|
15982
|
+
const wrote = action.kind === "command" ? commandPaths.length > 0 || afterRun.length > 0 : !failed && (action.kind === "edit" || action.kind === "write");
|
|
15394
15983
|
if (!wctx.config.supervisor.checks) return QUIET;
|
|
15395
15984
|
if (!wrote) {
|
|
15396
15985
|
if (!wctx.config.judge.enabled) return QUIET;
|
|
@@ -15400,7 +15989,7 @@ function postToolUse(p, repoRoot2, failed) {
|
|
|
15400
15989
|
}
|
|
15401
15990
|
const paths = action.kind === "command" ? commandPaths : action.paths;
|
|
15402
15991
|
const events = readEvents(ledgerPath(repoRoot2)).events.filter((e) => e.sessionId === p.session_id);
|
|
15403
|
-
const d = afterWrite(wctx, { paths, toolUseId: p.tool_use_id }, events);
|
|
15992
|
+
const d = afterWrite(wctx, { paths, toolUseId: p.tool_use_id }, events, afterRun);
|
|
15404
15993
|
if (!d.message) return QUIET;
|
|
15405
15994
|
return { exitCode: 0, stdout: JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: d.message } }), stderr: "" };
|
|
15406
15995
|
}
|
|
@@ -15541,7 +16130,7 @@ function handleClaudeHook(stdinText, env = process.env, deps = {}) {
|
|
|
15541
16130
|
}
|
|
15542
16131
|
|
|
15543
16132
|
// src/agents/cursor/handler.ts
|
|
15544
|
-
import { isAbsolute as isAbsolute8, relative as relative8, resolve as
|
|
16133
|
+
import { isAbsolute as isAbsolute8, relative as relative8, resolve as resolve10, sep as sep5 } from "path";
|
|
15545
16134
|
|
|
15546
16135
|
// src/agents/cursor/adapter.ts
|
|
15547
16136
|
import { relative as relative7, isAbsolute as isAbsolute6 } from "path";
|
|
@@ -16031,7 +16620,7 @@ function autoSessionEnd(i) {
|
|
|
16031
16620
|
|
|
16032
16621
|
// src/agents/cursor/install.ts
|
|
16033
16622
|
import { createHash as createHash11 } from "crypto";
|
|
16034
|
-
import { existsSync as existsSync43, mkdirSync as mkdirSync28, readFileSync as readFileSync48, renameSync, rmSync as rmSync16, writeFileSync as writeFileSync27 } from "fs";
|
|
16623
|
+
import { existsSync as existsSync43, mkdirSync as mkdirSync28, readFileSync as readFileSync48, renameSync as renameSync2, rmSync as rmSync16, writeFileSync as writeFileSync27 } from "fs";
|
|
16035
16624
|
import { dirname as dirname18, join as join57 } from "path";
|
|
16036
16625
|
var CURSOR_HOOK_COMMAND = "replume hook cursor";
|
|
16037
16626
|
var CURSOR_HOOK_EVENTS = [
|
|
@@ -16142,7 +16731,7 @@ function installCursorHooks(root, opts = {}) {
|
|
|
16142
16731
|
}
|
|
16143
16732
|
const temp = `${path}.replume-tmp`;
|
|
16144
16733
|
writeFileSync27(temp, updated);
|
|
16145
|
-
|
|
16734
|
+
renameSync2(temp, path);
|
|
16146
16735
|
return { path, backupPath: existsSync43(backupPath) ? backupPath : null, added, updated: upgraded };
|
|
16147
16736
|
}
|
|
16148
16737
|
function uninstallCursorHooks(root) {
|
|
@@ -16204,7 +16793,7 @@ function handleCursorHook(stdinText, env = process.env, deps = {}) {
|
|
|
16204
16793
|
const payload = JSON.parse(stdinText);
|
|
16205
16794
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid payload");
|
|
16206
16795
|
const raw6 = payload;
|
|
16207
|
-
const root =
|
|
16796
|
+
const root = resolve10(env.CURSOR_PROJECT_DIR ?? process.cwd());
|
|
16208
16797
|
ensureDependencyExcludes(root);
|
|
16209
16798
|
const event = cursorHookToEvent(raw6, { id: newEventId(), ts: (/* @__PURE__ */ new Date()).toISOString(), repoRoot: root });
|
|
16210
16799
|
if (!event) return { stdout: "{}", stderr: "", exitCode: 0 };
|
|
@@ -16273,6 +16862,7 @@ function handleCursorHook(stdinText, env = process.env, deps = {}) {
|
|
|
16273
16862
|
for (const hit of evaluation?.hits ?? []) {
|
|
16274
16863
|
appendEvent(ledger, sanitizeCursorLedgerEvent(ruleHitEvent(event, hit, { id: newEventId(), ts: (/* @__PURE__ */ new Date()).toISOString() }), root));
|
|
16275
16864
|
}
|
|
16865
|
+
if (["postToolUse", "afterShellExecution", "afterFileEdit"].includes(hookName)) landTreeChanges(root, baseCommitOf(root), { agent: "cursor", sessionId: event.sessionId });
|
|
16276
16866
|
if (hookName === "sessionEnd") {
|
|
16277
16867
|
const settings = cursorStopSettings(root);
|
|
16278
16868
|
autoSessionEnd({ agent: "cursor", repoRoot: root, sessionId: event.sessionId, hookTimeoutSec: settings.timeoutSec, followupLimit: settings.loopLimit, base: { ...event, id: void 0, ts: void 0 }, sanitize, deps });
|
|
@@ -16296,10 +16886,10 @@ function handleCursorHook(stdinText, env = process.env, deps = {}) {
|
|
|
16296
16886
|
import { execFileSync as execFileSync33 } from "child_process";
|
|
16297
16887
|
import { createHash as createHash12 } from "crypto";
|
|
16298
16888
|
import { existsSync as existsSync45, readFileSync as readFileSync50 } from "fs";
|
|
16299
|
-
import { resolve as
|
|
16889
|
+
import { resolve as resolve12 } from "path";
|
|
16300
16890
|
|
|
16301
16891
|
// src/agents/codex/action.ts
|
|
16302
|
-
import { resolve as
|
|
16892
|
+
import { resolve as resolve11 } from "path";
|
|
16303
16893
|
function codexAction(payload, root) {
|
|
16304
16894
|
const tool = typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
|
|
16305
16895
|
const input = payload.tool_input && typeof payload.tool_input === "object" && !Array.isArray(payload.tool_input) ? payload.tool_input : {};
|
|
@@ -16309,7 +16899,7 @@ function codexAction(payload, root) {
|
|
|
16309
16899
|
}
|
|
16310
16900
|
if (tool === "apply_patch") {
|
|
16311
16901
|
const rawPaths = [...command.matchAll(/^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/gm)].map((m) => m[1].trim());
|
|
16312
|
-
const { inside, outside } = splitPaths(root, rawPaths.map((p) =>
|
|
16902
|
+
const { inside, outside } = splitPaths(root, rawPaths.map((p) => resolve11(String(payload.cwd), p)));
|
|
16313
16903
|
return {
|
|
16314
16904
|
kind: "edit",
|
|
16315
16905
|
tool,
|
|
@@ -16422,13 +17012,13 @@ function handleCodexHook(stdinText, deps = {}) {
|
|
|
16422
17012
|
if (stdinText.length > 1024 * 1024) throw new Error("oversized hook input");
|
|
16423
17013
|
const p = JSON.parse(stdinText);
|
|
16424
17014
|
if (!object3(p) || !str4(p.session_id) || !str4(p.cwd) || !str4(p.hook_event_name)) throw new Error("invalid hook input");
|
|
16425
|
-
const cwd =
|
|
17015
|
+
const cwd = resolve12(str4(p.cwd));
|
|
16426
17016
|
let root = cwd;
|
|
16427
17017
|
try {
|
|
16428
17018
|
root = execFileSync33("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
16429
17019
|
} catch {
|
|
16430
17020
|
}
|
|
16431
|
-
if (!existsSync45(
|
|
17021
|
+
if (!existsSync45(resolve12(root, ".replume"))) return QUIET2;
|
|
16432
17022
|
ensureDependencyExcludes(root);
|
|
16433
17023
|
const ledger = ledgerPath(root);
|
|
16434
17024
|
const base4 = (type) => ({
|
|
@@ -16520,7 +17110,7 @@ ${stop2.message}${note}` }));
|
|
|
16520
17110
|
rule: first?.rule ?? null,
|
|
16521
17111
|
action: verdict.action,
|
|
16522
17112
|
message: verdict.message,
|
|
16523
|
-
raw: { ...raw6, beforeHashes: a.kind === "edit" || a.kind === "command" ? Object.fromEntries((a.kind === "edit" ? a.paths : raw6.writes ?? []).map((path) => [path, fileHash(
|
|
17113
|
+
raw: { ...raw6, beforeHashes: a.kind === "edit" || a.kind === "command" ? Object.fromEntries((a.kind === "edit" ? a.paths : raw6.writes ?? []).map((path) => [path, fileHash(resolve12(root, path))])) : void 0 }
|
|
16524
17114
|
});
|
|
16525
17115
|
for (const hit of verdict.hits) appendEvent(ledger, {
|
|
16526
17116
|
...base4("rule_hit"),
|
|
@@ -16554,10 +17144,10 @@ ${stop2.message}${note}` }));
|
|
|
16554
17144
|
const result_ = exitKnown ? { ok: success } : fromOutput ? { ok: fromOutput.ok, okFrom: "output", exitUnknown: true } : { exitUnknown: true };
|
|
16555
17145
|
const prior = readEvents(ledger).events.reverse().find((e) => e.type === "tool_pre" && e.raw?.tool_use_id === str4(p.tool_use_id));
|
|
16556
17146
|
const before = prior?.raw?.beforeHashes;
|
|
16557
|
-
const hashChanged = () => (raw6.writes ?? []).filter((path) => before && path in before && fileHash(
|
|
17147
|
+
const hashChanged = () => (raw6.writes ?? []).filter((path) => before && path in before && fileHash(resolve12(root, path)) !== before[path]);
|
|
16558
17148
|
const writes = a.kind === "command" && !exitKnown ? { writes: hashChanged() } : a.kind === "command" && !success ? { contentConfirmed: hashChanged() } : {};
|
|
16559
17149
|
const confirmed = writes.contentConfirmed ?? [];
|
|
16560
|
-
const changed = a.paths.filter((path) => before && path in before && fileHash(
|
|
17150
|
+
const changed = a.paths.filter((path) => before && path in before && fileHash(resolve12(root, path)) !== before[path]);
|
|
16561
17151
|
appendEvent(ledger, {
|
|
16562
17152
|
...base4("tool_post"),
|
|
16563
17153
|
tool: a.tool,
|
|
@@ -16566,6 +17156,7 @@ ${stop2.message}${note}` }));
|
|
|
16566
17156
|
summary: `${a.tool} completed`,
|
|
16567
17157
|
raw: { ...raw6, ...result_ }
|
|
16568
17158
|
});
|
|
17159
|
+
landTreeChanges(root, baseCommitOf(root), { agent: "codex", sessionId: str4(p.session_id) });
|
|
16569
17160
|
if (a.kind === "edit" && changed.length && (success || responseText.includes("Success."))) {
|
|
16570
17161
|
for (const path of changed)
|
|
16571
17162
|
appendEvent(ledger, {
|
|
@@ -16602,11 +17193,11 @@ ${stop2.message}${note}` }));
|
|
|
16602
17193
|
// src/agents/copilot/handler.ts
|
|
16603
17194
|
import { execFileSync as execFileSync34 } from "child_process";
|
|
16604
17195
|
import { existsSync as existsSync46 } from "fs";
|
|
16605
|
-
import { resolve as
|
|
17196
|
+
import { resolve as resolve14 } from "path";
|
|
16606
17197
|
|
|
16607
17198
|
// src/agents/copilot/action.ts
|
|
16608
17199
|
import { realpathSync as realpathSync3 } from "fs";
|
|
16609
|
-
import { basename as basename3, dirname as dirname20, isAbsolute as isAbsolute9, join as join59, relative as relative9, resolve as
|
|
17200
|
+
import { basename as basename3, dirname as dirname20, isAbsolute as isAbsolute9, join as join59, relative as relative9, resolve as resolve13 } from "path";
|
|
16610
17201
|
var object4 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
16611
17202
|
function parseToolArgs(toolArgs) {
|
|
16612
17203
|
if (object4(toolArgs)) return toolArgs;
|
|
@@ -16636,7 +17227,7 @@ function real(path) {
|
|
|
16636
17227
|
}
|
|
16637
17228
|
function rel(repoRoot2, path) {
|
|
16638
17229
|
const root = real(repoRoot2);
|
|
16639
|
-
const abs = isAbsolute9(path) ? path :
|
|
17230
|
+
const abs = isAbsolute9(path) ? path : resolve13(root, path);
|
|
16640
17231
|
const parent = real(dirname20(abs));
|
|
16641
17232
|
const file3 = join59(parent, basename3(abs));
|
|
16642
17233
|
const out = relative9(root, file3);
|
|
@@ -16717,13 +17308,13 @@ function handleCopilotHook(stdinText, deps = {}) {
|
|
|
16717
17308
|
if (stdinText.length > 1024 * 1024) throw new Error("oversized hook input");
|
|
16718
17309
|
const raw6 = JSON.parse(stdinText);
|
|
16719
17310
|
if (!object5(raw6)) throw new Error("invalid hook input");
|
|
16720
|
-
const cwd =
|
|
17311
|
+
const cwd = resolve14(str5(raw6.cwd) || process.cwd());
|
|
16721
17312
|
let root = cwd;
|
|
16722
17313
|
try {
|
|
16723
17314
|
root = execFileSync34("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
16724
17315
|
} catch {
|
|
16725
17316
|
}
|
|
16726
|
-
if (!existsSync46(
|
|
17317
|
+
if (!existsSync46(resolve14(root, ".replume"))) return QUIET3;
|
|
16727
17318
|
ensureDependencyExcludes(root);
|
|
16728
17319
|
const sid = sessionId(raw6) || "copilot";
|
|
16729
17320
|
const name = eventName(raw6);
|
|
@@ -16856,7 +17447,7 @@ import { execFileSync as execFileSync35 } from "child_process";
|
|
|
16856
17447
|
import { existsSync as existsSync48, readFileSync as readFileSync52, rmSync as rmSync17, statSync as statSync10, writeFileSync as writeFileSync30 } from "fs";
|
|
16857
17448
|
import { realpathSync as realpathSync4 } from "fs";
|
|
16858
17449
|
import { tmpdir as tmpdir13 } from "os";
|
|
16859
|
-
import { basename as basename4, dirname as dirname22, isAbsolute as isAbsolute10, join as join61, relative as relative10, resolve as
|
|
17450
|
+
import { basename as basename4, dirname as dirname22, isAbsolute as isAbsolute10, join as join61, relative as relative10, resolve as resolve15 } from "path";
|
|
16860
17451
|
var QUIET4 = { stdout: "", stderr: "", exitCode: 0 };
|
|
16861
17452
|
var object6 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
16862
17453
|
var str6 = (x) => typeof x === "string" ? x : "";
|
|
@@ -16870,7 +17461,7 @@ function real2(path) {
|
|
|
16870
17461
|
}
|
|
16871
17462
|
function rel2(repoRoot2, path) {
|
|
16872
17463
|
const root = real2(repoRoot2);
|
|
16873
|
-
const abs = isAbsolute10(path) ? path :
|
|
17464
|
+
const abs = isAbsolute10(path) ? path : resolve15(root, path);
|
|
16874
17465
|
const parent = real2(dirname22(abs));
|
|
16875
17466
|
const file3 = join61(parent, basename4(abs));
|
|
16876
17467
|
const out = relative10(root, file3);
|
|
@@ -16911,11 +17502,11 @@ function resolveRoot(cwd, args, extra) {
|
|
|
16911
17502
|
const top = gitTop(cwd);
|
|
16912
17503
|
if (top) starts.push(top);
|
|
16913
17504
|
const path = fileArg(args);
|
|
16914
|
-
if (path) starts.push(isAbsolute10(path) ? path :
|
|
17505
|
+
if (path) starts.push(isAbsolute10(path) ? path : resolve15(cwd, path));
|
|
16915
17506
|
const changed = extra.changed_paths;
|
|
16916
17507
|
if (Array.isArray(changed)) {
|
|
16917
17508
|
for (const item of changed) {
|
|
16918
|
-
if (typeof item === "string" && item.trim()) starts.push(isAbsolute10(item) ? item :
|
|
17509
|
+
if (typeof item === "string" && item.trim()) starts.push(isAbsolute10(item) ? item : resolve15(cwd, item));
|
|
16919
17510
|
}
|
|
16920
17511
|
}
|
|
16921
17512
|
for (const start of starts) {
|
|
@@ -16982,7 +17573,7 @@ function handleHermesHook(stdinText, deps = {}) {
|
|
|
16982
17573
|
const raw6 = JSON.parse(stdinText);
|
|
16983
17574
|
if (!object6(raw6)) throw new Error("invalid hook input");
|
|
16984
17575
|
const name = str6(raw6.hook_event_name);
|
|
16985
|
-
const cwd =
|
|
17576
|
+
const cwd = resolve15(str6(raw6.cwd) || process.cwd());
|
|
16986
17577
|
const sid = str6(raw6.session_id) || "hermes";
|
|
16987
17578
|
const extra = object6(raw6.extra) ? raw6.extra : {};
|
|
16988
17579
|
const args = object6(raw6.tool_input) ? raw6.tool_input : {};
|
|
@@ -17194,7 +17785,7 @@ function writeBugBundle(root, version, now = /* @__PURE__ */ new Date()) {
|
|
|
17194
17785
|
// src/core/share.ts
|
|
17195
17786
|
import { appendFileSync as appendFileSync9, existsSync as existsSync49, mkdirSync as mkdirSync33, readFileSync as readFileSync54, readdirSync as readdirSync13, statSync as statSync12, writeFileSync as writeFileSync33 } from "fs";
|
|
17196
17787
|
import { homedir as homedir7 } from "os";
|
|
17197
|
-
import { join as join64, resolve as
|
|
17788
|
+
import { join as join64, resolve as resolve16 } from "path";
|
|
17198
17789
|
var SHARE_ENDPOINT = "https://replume.dev";
|
|
17199
17790
|
var SHARED_SECTIONS = [
|
|
17200
17791
|
"What was asked",
|
|
@@ -17213,7 +17804,7 @@ var MAX_LINE = 600;
|
|
|
17213
17804
|
function findReport(repoRoot2, arg) {
|
|
17214
17805
|
const dir2 = join64(repoRoot2, ".replume", "reports");
|
|
17215
17806
|
if (arg) {
|
|
17216
|
-
const asPath =
|
|
17807
|
+
const asPath = resolve16(repoRoot2, arg);
|
|
17217
17808
|
if (existsSync49(asPath) && statSync12(asPath).isFile()) return asPath;
|
|
17218
17809
|
const byId = join64(dir2, `${arg.replace(/[^A-Za-z0-9_-]/g, "")}.md`);
|
|
17219
17810
|
if (existsSync49(byId)) return byId;
|
|
@@ -17226,7 +17817,7 @@ function findReport(repoRoot2, arg) {
|
|
|
17226
17817
|
var unescape2 = (s) => s.replace(/\\([\\`*_{}[\]()#+\-.!|>~])/g, "$1");
|
|
17227
17818
|
function scrub2(text, repoRoot2) {
|
|
17228
17819
|
let t = text;
|
|
17229
|
-
if (repoRoot2) t = t.split(`${
|
|
17820
|
+
if (repoRoot2) t = t.split(`${resolve16(repoRoot2)}/`).join("").split(resolve16(repoRoot2)).join(".");
|
|
17230
17821
|
const home = homedir7();
|
|
17231
17822
|
if (home && home.length > 1) t = t.split(home).join("~");
|
|
17232
17823
|
t = t.replace(/(^|[\s(`"'=:])((?:~|\/(?:Users|home|private|tmp|var|opt|etc|root|Volumes|mnt))(?:\/[^\s`"')\]]*)?)/g, (_m, pre) => `${pre}<path>`).replace(/\b[A-Za-z]:\\[^\s`"')\]]*/g, "<path>");
|
|
@@ -17422,14 +18013,20 @@ function short(value, max) {
|
|
|
17422
18013
|
}
|
|
17423
18014
|
return `${out.replace(/\s+$/, "")}\u2026`;
|
|
17424
18015
|
}
|
|
18016
|
+
function pathOfIntervention(e) {
|
|
18017
|
+
const text = e.message ?? "";
|
|
18018
|
+
const at = (p) => text.search(new RegExp(`(?<![\\w./-])${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w/-]|\\.\\w)`));
|
|
18019
|
+
return e.paths.map((p) => [p, at(p)]).filter(([, i]) => i >= 0).sort((a, b) => a[1] - b[1])[0]?.[0] ?? e.paths[0] ?? "";
|
|
18020
|
+
}
|
|
17425
18021
|
function line(e) {
|
|
17426
18022
|
const raw6 = rawOf(e);
|
|
17427
|
-
const path = e.paths[0] ?? "";
|
|
18023
|
+
const path = e.action === "block" || e.action === "warn" ? pathOfIntervention(e) : e.paths[0] ?? "";
|
|
17428
18024
|
const tool = clean2(e.tool ?? e.type);
|
|
17429
18025
|
const target = clean2([tool, path || e.command || ""].filter(Boolean).join(" "));
|
|
17430
18026
|
const summary = clean2(e.summary);
|
|
17431
18027
|
const reason = clean2(e.message?.split(/\r?\n/)[0]).replace(/^\[[A-Z][A-Z0-9_]+\]\s*/, "").replace(/^Replume:\s*/i, "");
|
|
17432
|
-
if (raw6.watchWrite === true) return { label: "WRITE", detail: summary, color: 36 };
|
|
18028
|
+
if (raw6.watchWrite === true || raw6.treeSweep === true) return { label: "WRITE", detail: summary, color: 36 };
|
|
18029
|
+
if (raw6.noResult === true) return { label: "NO RESULT", detail: `${target} (the agent reported no result; it may not have run)`, color: 33 };
|
|
17433
18030
|
if (e.type === "rule_hit" && e.rule === "auto_verdict") {
|
|
17434
18031
|
const verdict = clean2(raw6.status).toUpperCase() || "RESULT";
|
|
17435
18032
|
const report = clean2(raw6.reportPath);
|
|
@@ -17507,11 +18104,14 @@ var callId = (e) => {
|
|
|
17507
18104
|
var callKey = (e) => callId(e) ?? `k:${e.sessionId}:${e.tool ?? ""}|${e.command ?? ""}|${e.paths.join(",")}`;
|
|
17508
18105
|
var isHeldPre = (e) => e.type === "tool_pre" && (e.action === "ok" || e.action === null || e.action === void 0);
|
|
17509
18106
|
var isResult = (e) => (e.type === "command" || e.type === "file_edit" || e.type === "tool_post") && rawOf(e).by !== "replume";
|
|
17510
|
-
var
|
|
18107
|
+
var STOP_RULES = /* @__PURE__ */ new Set(["auto_verdict", "auto_stop_gate", "auto_incomplete", "changed_outside_tools"]);
|
|
18108
|
+
var flushes = (e) => e.type === "prompt" || e.type === "stop" || e.type === "rule_hit" && !!e.rule && STOP_RULES.has(e.rule);
|
|
18109
|
+
var NO_POST_HOOK = /* @__PURE__ */ new Set(["copilot", "hermes"]);
|
|
18110
|
+
var heldOut = (p) => NO_POST_HOOK.has(p.agent) ? p : { ...p, raw: { ...rawOf(p), noResult: true } };
|
|
17511
18111
|
function formatEvents(events, state, opts = {}) {
|
|
17512
18112
|
const out = [];
|
|
17513
18113
|
const flushAll = () => {
|
|
17514
|
-
for (const list5 of state.pending.values()) for (const p of list5) out.push(formatEvent(p, opts));
|
|
18114
|
+
for (const list5 of state.pending.values()) for (const p of list5) out.push(formatEvent(heldOut(p), opts));
|
|
17515
18115
|
state.pending.clear();
|
|
17516
18116
|
};
|
|
17517
18117
|
for (const e of events) {
|
|
@@ -18039,7 +18639,7 @@ function otherHookWarnings(home = process.env.HOME || homedir8()) {
|
|
|
18039
18639
|
}
|
|
18040
18640
|
|
|
18041
18641
|
// src/core/telemetry.ts
|
|
18042
|
-
import { appendFileSync as appendFileSync10, existsSync as existsSync52, mkdirSync as mkdirSync34, readdirSync as readdirSync14, readFileSync as readFileSync57, renameSync as
|
|
18642
|
+
import { appendFileSync as appendFileSync10, existsSync as existsSync52, mkdirSync as mkdirSync34, readdirSync as readdirSync14, readFileSync as readFileSync57, renameSync as renameSync3, rmSync as rmSync19, statSync as statSync14, writeFileSync as writeFileSync34 } from "fs";
|
|
18043
18643
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
18044
18644
|
import { arch as osArch, homedir as homedir9, platform as osPlatform } from "os";
|
|
18045
18645
|
import { dirname as dirname23, join as join67 } from "path";
|
|
@@ -18182,7 +18782,7 @@ function writeState(c, s) {
|
|
|
18182
18782
|
const tmp = `${p}.${process.pid}.tmp`;
|
|
18183
18783
|
writeFileSync34(tmp, `${JSON.stringify(s, null, 1)}
|
|
18184
18784
|
`, { mode: 384 });
|
|
18185
|
-
|
|
18785
|
+
renameSync3(tmp, p);
|
|
18186
18786
|
}
|
|
18187
18787
|
function enqueue(c, p) {
|
|
18188
18788
|
if (!p || !isClosedPayload(p)) return;
|
|
@@ -18288,7 +18888,7 @@ async function flushTelemetry(c, fetchImpl = fetch) {
|
|
|
18288
18888
|
if (sink(c)) return;
|
|
18289
18889
|
const q = queuePath(c), claim = `${q}.${process.pid}.${c.now.getTime()}`;
|
|
18290
18890
|
const stale = claimedFiles(c);
|
|
18291
|
-
if (existsSync52(q))
|
|
18891
|
+
if (existsSync52(q)) renameSync3(q, claim);
|
|
18292
18892
|
const files = [...stale, ...existsSync52(claim) ? [claim] : []];
|
|
18293
18893
|
const oldest = c.now.getTime() - QUEUE_MAX_AGE_MS;
|
|
18294
18894
|
const batch = files.flatMap(readQueue).filter((p) => Date.parse(p.ts) >= oldest);
|
|
@@ -18439,11 +19039,11 @@ var summaryFor = (root, session) => {
|
|
|
18439
19039
|
// src/cli.ts
|
|
18440
19040
|
var CODEX_RETRUST_LINE = "Codex: Replume's hooks in .codex/hooks.json are new or changed; run /hooks in Codex to trust them (until then Codex skips them).";
|
|
18441
19041
|
function readStdin() {
|
|
18442
|
-
return new Promise((
|
|
19042
|
+
return new Promise((resolve18, reject) => {
|
|
18443
19043
|
let data = "";
|
|
18444
19044
|
process.stdin.setEncoding("utf-8");
|
|
18445
19045
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
18446
|
-
process.stdin.on("end", () =>
|
|
19046
|
+
process.stdin.on("end", () => resolve18(data));
|
|
18447
19047
|
process.stdin.on("error", reject);
|
|
18448
19048
|
});
|
|
18449
19049
|
}
|
|
@@ -18892,7 +19492,7 @@ async function executeInit(opts) {
|
|
|
18892
19492
|
}
|
|
18893
19493
|
}
|
|
18894
19494
|
function boundedInitCheck(root, isolation) {
|
|
18895
|
-
return new Promise((
|
|
19495
|
+
return new Promise((resolve18) => {
|
|
18896
19496
|
const child = spawn7(process.execPath, [...process.execArgv, process.argv[1], "__init-check-worker", ...isolation ? ["--isolation", isolation] : []], {
|
|
18897
19497
|
cwd: root,
|
|
18898
19498
|
env: process.env,
|
|
@@ -18910,7 +19510,7 @@ function boundedInitCheck(root, isolation) {
|
|
|
18910
19510
|
if (settled) return;
|
|
18911
19511
|
settled = true;
|
|
18912
19512
|
clearTimeout(timer);
|
|
18913
|
-
|
|
19513
|
+
resolve18(timedOut2 ? ["NOT CHECKED \u2014 init --check exceeded its 55s limit; the base is neither green nor red by this check."] : code2 === 0 ? stdout.trimEnd().split("\n") : [`NOT CHECKED \u2014 init --check could not finish: ${redact(stderr.split("\n")[0] || `exit ${code2}`)}.`]);
|
|
18914
19514
|
};
|
|
18915
19515
|
child.on("error", (e) => {
|
|
18916
19516
|
stderr += e.message;
|
|
@@ -19169,11 +19769,11 @@ demo.option("--replay <file>", "run a recorded events.jsonl session through the
|
|
|
19169
19769
|
console.log(renderReplay(replay(builtInEvents, builtInContract), builtInContract));
|
|
19170
19770
|
return;
|
|
19171
19771
|
}
|
|
19172
|
-
const file3 =
|
|
19772
|
+
const file3 = resolve17(opts.replay);
|
|
19173
19773
|
if (!existsSync53(file3)) fail(`replay file not found: ${file3}`);
|
|
19174
19774
|
try {
|
|
19175
19775
|
const sibling = file3.replace(/\.jsonl?$/, "") + ".contract.yaml";
|
|
19176
|
-
const contractFile = opts.contract ?
|
|
19776
|
+
const contractFile = opts.contract ? resolve17(opts.contract) : existsSync53(sibling) ? sibling : contractPath(repoRoot());
|
|
19177
19777
|
const contract = loadContract(contractFile);
|
|
19178
19778
|
console.log(renderReplay(replay(readFileSync59(file3, "utf-8"), contract), contract, contract ? contractFile : void 0));
|
|
19179
19779
|
} catch (e) {
|
|
@@ -19181,10 +19781,10 @@ demo.option("--replay <file>", "run a recorded events.jsonl session through the
|
|
|
19181
19781
|
}
|
|
19182
19782
|
});
|
|
19183
19783
|
async function demoReplayHooks(path, contractOpt) {
|
|
19184
|
-
const file3 =
|
|
19784
|
+
const file3 = resolve17(path);
|
|
19185
19785
|
if (!existsSync53(file3)) fail(`replay file not found: ${file3}`);
|
|
19186
19786
|
const sibling = file3.replace(/\.jsonl?$/, "") + ".contract.yaml";
|
|
19187
|
-
const contractFile = contractOpt ?
|
|
19787
|
+
const contractFile = contractOpt ? resolve17(contractOpt) : existsSync53(sibling) ? sibling : null;
|
|
19188
19788
|
try {
|
|
19189
19789
|
const r = await replayHooks(readFileSync59(file3, "utf-8"), {
|
|
19190
19790
|
contractYaml: contractFile ? readFileSync59(contractFile, "utf-8") : void 0,
|
|
@@ -19252,7 +19852,7 @@ hook.command("judge-step").description("Internal: run one model-judge step judgm
|
|
|
19252
19852
|
}
|
|
19253
19853
|
});
|
|
19254
19854
|
hook.command("auto-finish").description("Internal, AUTO mode: verify the session's work (snapshot of the working tree if nothing was committed) and write .replume/reports/<session>.md. Started by the SessionEnd hook.").requiredOption("--session <id>", "the agent session id").option("--root <path>", "repository root (default: the current repository)").option("--limits <json>", "JSON array of 'Agent hook limits' lines for the report (Codex and Cursor)").action(async (opts) => {
|
|
19255
|
-
const root =
|
|
19855
|
+
const root = resolve17(opts.root ?? repoRoot());
|
|
19256
19856
|
let limits;
|
|
19257
19857
|
try {
|
|
19258
19858
|
const parsed = opts.limits ? JSON.parse(opts.limits) : void 0;
|