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.
Files changed (2) hide show
  1. package/dist/cli.js +1173 -573
  2. 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((resolve17) => {
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
- resolve17({ code: code2, stdout, stderr, timedOut: timedOut2, notFound });
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((resolve17) => {
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
- resolve17({ stdout, stderr, exitCode, timedOut: timedOut2, durationMs: Math.round(performance2.now() - started), oomKills });
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 resolve2, sep } from "path";
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 requestText = (s) => s.replace(NEW_TEST, " ").replace(RUNS_TESTS, " ").replace(NOUN_CHANGE, " ");
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 opaque = [...new Set(shell.filter((e) => unobservable(e.command)).map((e) => e.command))];
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 (opaque.length)
2078
+ if (opaque2.length)
1987
2079
  notSeen.push(
1988
- `${opaque.length} shell command${opaque.length === 1 ? "" : "s"} ran a script file or wrote to a path built at run time (e.g. \`${opaque[0].slice(0, 80)}\`); what ${opaque.length === 1 ? "it" : "they"} wrote is not observable by Replume's shell rules`
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: opaque.length + unparsed.length,
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 = resolve2(repoRoot2);
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) ? resolve2(p) : resolve2(root, 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 closing = (from) => {
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 = closing(i + 1);
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 = resolve2(repoRoot2, rel3);
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 = resolve2(repoRoot2);
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 = resolve2(repoRoot2);
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, resolve2(root, found));
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(resolve2(root, found)));
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 byContent = [...heredocTestWrites(a, ctx), ...sedTestWrites(a, ctx)].map((w) => w.path);
3904
- const hit = [.../* @__PURE__ */ new Set([...redirected, ...operands, ...inline])].filter((p) => !moved.includes(p) && !byContent.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 removedExisting = !!ch.before && removed > 0 && (start === void 0 || gone.some(existing));
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 && askedTestPaths(ctx).includes(ch.path)) {
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 = resolve2(env);
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) => resolve2(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, resolve2(ctx.repoRoot, prefix))) return "outside";
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 = resolve2(ctx.repoRoot, expanded);
4252
- const root = resolve2(ctx.repoRoot);
4253
- const home = resolve2(homedir());
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 resolve2(ctx.repoRoot, t);
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) => resolve2(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 = resolve2(homedir());
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 WHOLE_SUITE = [
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) => WHOLE_SUITE.some((re) => re.test(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 resolve4, sep as sep4 } from "path";
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 LITERAL = /-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`[^`\n]*`|\b(?:true|false|True|False|None|nil|null|undefined)\b/g;
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(LITERAL) ?? [];
5959
+ return line2.match(LITERAL2) ?? [];
5554
5960
  }
5555
5961
  function shapeOf(line2) {
5556
- return line2.replace(LITERAL, "\0").replace(/\s+/g, " ").trim();
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(norm(m[0]), (before.get(norm(m[0])) ?? 0) + 1);
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 = norm(m[0]);
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 norm = (s) => s.replace(/\s+/g, " ").trim();
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 IDENT = /[A-Za-z_]\w*(?=\s*\()/g;
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(IDENT) ?? []).filter((f) => !FRAMEWORK_CALL.test(f));
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 resolve3 } from "path";
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 = resolve3(repoRoot2, git4(repoRoot2, ["rev-parse", "--git-path", "info/exclude"]));
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 norm3 = (s) => s.trim().replace(/\s+/g, " ").toLowerCase();
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) => norm3(s.description) === norm3(text));
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((resolve17, reject) => {
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) resolve17(out.trim());
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((resolve17) => {
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
- resolve17({ stdout, stderr, exitCode, durationMs: Math.round(performance3.now() - started), timedOut: timedOut2 });
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 w2 = weakening(await patch2(...e.status === "R" ? [e.oldPath, e.path] : [e.path]), await atBase(e.oldPath ?? e.path)).filter((x) => !/assertions? changed/.test(x));
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((resolve17, reject) => {
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
- resolve17(parseJudgeResponse(JSON.parse(out)));
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 = resolve4(root, path);
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
- namedPaths: changedPaths.filter((p) => named.has(p) || byFailed.has(p)),
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 = resolve4(opts.repoPath);
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
- if (so_far >= max) {
9334
- const reason = `the agent stopped with \`${command}\` ${what} after ${max} steer${max === 1 ? "" : "s"} (auto.max_retries ${max})`;
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 message = snap ? `Replume ran \`${command}\` before letting you stop: it ${what}. ${run.summary ? `${run.summary}
9349
- ` : ""}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})` : `Replume ran \`${command}\` before letting you stop: it ${what}. ${run.summary ? `${run.summary}
9350
- ` : ""}${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. 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. Otherwise: 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})`;
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 norm2 = (s) => s.replace(/\s+/g, " ").trim();
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 = norm2(quote4);
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) => norm2(l).includes(x)));
9477
- return found(q) || /^[+-]/.test(q) && found(norm2(q.slice(1)));
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 hit = rowFor(e);
11017
- if (!hit) continue;
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.6",
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 resolve16 } from "path";
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 resolve5 } from "path";
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 = resolve5(repoDir, gitPath);
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 readFileSync41 } from "fs";
11867
- import { join as join49 } from "path";
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/confirm-edits.ts
12358
+ // src/core/shell-observe.ts
11927
12359
  import { execFileSync as execFileSync26 } from "child_process";
11928
- var NOT_EDITS = /^(?:bash|shell|terminal|unknown)$|read|view|search|grep|glob|list|fetch|web/i;
11929
- function confirmEdits(root, sid, agent) {
11930
- const ledger = ledgerPath(root);
11931
- const mine = readEvents(ledger).events.filter((e) => e.sessionId === sid);
11932
- const confirmed = new Set(mine.filter((e) => e.type === "file_edit" && e.raw?.ok !== false).flatMap((e) => e.paths));
11933
- const b = readBaseline(baselinePath(contractPath(root)));
11934
- const base4 = b.status === "ok" ? b.baseline.base_commit : null;
11935
- const git10 = (args) => execFileSync26("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
11936
- const changed = (path) => {
11937
- try {
11938
- if (git10(["status", "--porcelain", "--", path]).trim()) return true;
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 execFileSync29 } from "child_process";
11968
- import { existsSync as existsSync36, mkdirSync as mkdirSync24, mkdtempSync as mkdtempSync6, readFileSync as readFileSync40, rmSync as rmSync13, statSync as statSync8, writeFileSync as writeFileSync24 } from "fs";
11969
- import { tmpdir as tmpdir10 } from "os";
11970
- import { join as join48 } from "path";
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 execFileSync27 } from "child_process";
11974
- import { createHash as createHash6 } from "crypto";
11975
- import { lstatSync as lstatSync5, mkdirSync as mkdirSync21, readdirSync as readdirSync10, readFileSync as readFileSync37, writeFileSync as writeFileSync21 } from "fs";
11976
- import { join as join45 } from "path";
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 createHash6("sha256").update(readFileSync37(path)).digest("hex");
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 = execFileSync27("git", ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 256 * 1024 ** 2, timeout: ms });
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 = lstatSync5(join45(repoRoot2, rel3), { throwIfNoEntry: false });
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(join45(repoRoot2, rel3));
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(join45(repoRoot2, rel3)) : null];
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 = join45(repoRoot2, REPLUME_DIR, "contracts");
12041
- mkdirSync21(dir2, { recursive: true });
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
- writeFileSync21(join45(repoRoot2, REPLUME_DIR, FILE2), JSON.stringify({ v: 1, contract_id: contractId, partial: seen.partial, files: seen.files }), { mode: 384 });
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(readFileSync37(join45(repoRoot2, REPLUME_DIR, FILE2), "utf8"));
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(join45(repoRoot2, path)) === was[2]) continue;
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 execFileSync28 } from "child_process";
12409
- import { createHash as createHash7 } from "crypto";
12410
- import { mkdirSync as mkdirSync22, readFileSync as readFileSync38, writeFileSync as writeFileSync22 } from "fs";
12411
- import { tmpdir as tmpdir9 } from "os";
12412
- import { join as join46 } from "path";
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 out = writes.filter((p) => !matchesAny(normalizeRel(p), allow) && !RUNNER_CONFIG_FILE.test(normalizeRel(p)));
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 = join46(tmpdir9(), "replume-judge-jobs");
13240
- mkdirSync22(dir2, { recursive: true });
13241
- const file3 = join46(dir2, `${createHash7("sha1").update(`${job.sessionId}\0${job.forEventId}`).digest("hex").slice(0, 20)}.json`);
13242
- writeFileSync22(file3, JSON.stringify(job));
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 execFileSync28("git", args, { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 8 * 1024 * 1024 });
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 = readFileSync38(join46(repoRoot2, f), "utf8");
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 createHash8 } from "crypto";
13385
- import { appendFileSync as appendFileSync7, existsSync as existsSync35, lstatSync as lstatSync6, mkdirSync as mkdirSync23, readFileSync as readFileSync39, readdirSync as readdirSync11, rmSync as rmSync12, writeFileSync as writeFileSync23 } from "fs";
13386
- import { join as join47 } from "path";
13387
- var dirOf = (root) => join47(root, ".replume", "memory");
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 = join47(root, ".replume", ".gitignore");
13391
- mkdirSync23(join47(root, ".replume"), { recursive: true });
13392
- if (!existsSync35(gi)) {
13393
- writeFileSync23(gi, DEFAULT_REPLUME_GITIGNORE + "memory/\n");
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 = readFileSync39(gi, "utf8");
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
- mkdirSync23(dir2, { recursive: true });
13405
- const name = createHash8("sha256").update(input.sessionId).digest("hex").slice(0, 24) + ".json";
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
- writeFileSync23(join47(dir2, name), JSON.stringify(entry) + "\n", { flag: "wx", mode: 384 });
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 (!existsSync35(dir2)) return [];
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: lstatSync6(join47(dir2, name)).mtimeMs };
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 = join47(dir2, name);
13449
- const st = lstatSync6(path);
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(readFileSync39(path, "utf8"));
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 (!existsSync35(dir2)) return 0;
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 = join47(dir2, name);
13465
- if (lstatSync6(path).isFile()) {
13466
- rmSync12(path);
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 git8 = (cwd, args, env = process.env) => execFileSync29("git", args, { cwd, encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] }).trim();
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 = git8(repoRoot2, ["rev-parse", "--verify", "HEAD^{commit}"]);
13498
- if (!git8(repoRoot2, ["status", "--porcelain"])) return { commit: head, snapshot: false, ref: null };
13499
- const dir2 = mkdtempSync6(join48(tmpdir10(), "replume-auto-"));
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: join48(dir2, "index") };
13502
- git8(repoRoot2, ["read-tree", "HEAD"], env);
13503
- git8(repoRoot2, ["add", "-A"], env);
13504
- const tree = git8(repoRoot2, ["write-tree"], env);
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 git8(repoRoot2, ["config", "--get", key]);
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 = git8(repoRoot2, ["commit-tree", tree, "-p", head, "-m", `Replume auto snapshot of the working tree (session ${sessionId2.slice(0, 8)})`], withIdent);
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
- git8(repoRoot2, ["update-ref", ref, commit]);
14122
+ git9(repoRoot2, ["update-ref", ref, commit]);
13516
14123
  return { commit, snapshot: true, ref };
13517
14124
  } finally {
13518
- rmSync13(dir2, { recursive: true, force: true });
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 = execFileSync29("git", ["ls-files"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).split("\n").filter((f) => f && isTestFile(f));
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 = readFileSync40(join48(repoRoot2, f), "utf8");
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(readFileSync40(join48(repoRoot2, "package.json"), "utf8"))?.scripts?.build) return "npm run build";
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(readFileSync40(join48(repoRoot2, "Makefile"), "utf8"))) return "make build";
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(join48(repoRoot2, path)).mtimeMs > Date.parse(events[lastWrite].ts);
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 = join48(repoRoot2, REPLUME_DIR, "reports", `${stem2}.md`);
13606
- const htmlPath = join48(repoRoot2, REPLUME_DIR, "reports", `${stem2}.html`);
13607
- if (!existsSync36(mdPath)) return false;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13612
- const md2 = readFileSync40(mdPath, "utf8");
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) writeFileSync24(mdPath, mdNew);
13616
- if (existsSync36(htmlPath)) {
13617
- const html2 = readFileSync40(htmlPath, "utf8");
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) writeFileSync24(htmlPath, htmlNew);
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; ${existsSync36(configPath(f.repoRoot)) ? `\`${REPLUME_DIR}/config\` has no valid mode` : `no \`${REPLUME_DIR}/config\``})` : ` (from \`${REPLUME_DIR}/config\`)`}.`);
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 execFileSync29("git", ["rev-parse", `${c}^{tree}`], { cwd: f.repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
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 git8(repoRoot2, ["rev-parse", "--verify", `${ref}^{commit}`]);
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(execFileSync29(
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 git8(repoRoot2, ["diff", "--numstat", base4, commit]).split("\n").filter(Boolean).flatMap((line2) => {
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 incompleteWhy = (copyLimit ? problem : null) ?? committedWhy ?? (outsideReason ? null : conflictWhy) ?? (fail2 ? null : incomplete) ?? outsideReason ?? ignoredWhy ?? (scriptBlind && !fail2 ? blindLine(blindScripts[0]) : null) ?? (shrink && !fail2 ? shrink : null);
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
- execFileSync29("git", ["diff", "--quiet", base4, snap.commit, "--", p], { cwd: repoRoot2, stdio: "ignore" });
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 askedUnchanged = report && !noChanges ? asked.filter(unchangedSince) : [];
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 = join48(REPLUME_DIR, "reports", `${stem2}.md`);
13889
- const htmlPath = join48(REPLUME_DIR, "reports", `${stem2}.html`);
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
- mkdirSync24(join48(repoRoot2, REPLUME_DIR, "reports"), { recursive: true });
13967
- writeFileSync24(join48(repoRoot2, reportPath), redactLedgerText(full, repoRoot2));
13968
- writeFileSync24(join48(repoRoot2, htmlPath), redactLedgerText(rendered.html, repoRoot2));
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 execFileSync29("git", ["rev-parse", `${c}^{tree}`], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
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, readFileSync41(join49(i.repoRoot, p), "utf8"));
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 execFileSync30 } from "child_process";
14113
- import { existsSync as existsSync37 } from "fs";
14114
- import { join as join50 } from "path";
14115
- import { readFileSync as readFileSync42 } from "fs";
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 = existsSync37(contractPath(input.repoRoot)) ? loadContract(contractPath(input.repoRoot)) : null;
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 protect = plan.protect;
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 (existsSync37(join50(input.repoRoot, REPLUME_DIR))) {
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 && existsSync37(join50(input.repoRoot, REPLUME_DIR))) {
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 = existsSync37(contractPath(input.repoRoot)) ? loadContract(contractPath(input.repoRoot)) : null;
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) && existsSync37(join50(input.repoRoot, REPLUME_DIR))) {
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) && existsSync37(join50(i.repoRoot, 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) && existsSync37(join50(repoRoot2, 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(join50(repoRoot2, t))) return true;
15015
+ if (output.includes(t) || output.includes(join51(repoRoot2, t))) return true;
14325
15016
  let text = "";
14326
15017
  try {
14327
- text = readFileSync42(join50(repoRoot2, t), "utf8");
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 = existsSync37(path) ? loadContract(path) : null;
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 = execFileSync30("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot2, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
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 resolve7 } from "path";
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(resolve7(repoRoot2)).digest("hex").slice(0, 12), createHash10("sha1").update(sessionId2).digest("hex").slice(0, 12));
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 d = driftScore(events[stepIndex], { task: anchor.text, allow: ctx.contract?.allow, testCommand, mode }, events.slice(0, stepIndex));
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 r = claimCheck(lastAssistantMessage, events, { testCommand: ctx.contract?.test_command });
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 resolve8 } from "path";
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("~/") ? resolve8(process.env.HOME ?? "/", p.slice(2)) : p
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 : resolve8(p.cwd, 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
- const comparing = needsTreeCompare(action) && rememberTree(repoRoot2, p.session_id, p.tool_use_id);
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((resolve17) => setTimeout(resolve17, 100));
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 changes = action.kind === "command" ? takeTreeChanges(repoRoot2, p.session_id, p.tool_use_id) : null;
15365
- const observed = action.kind === "command" ? observedWrites(action, changes) : null;
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 wrote = action.kind === "command" ? commandPaths.length > 0 : !failed && (action.kind === "edit" || action.kind === "write");
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 resolve9, sep as sep5 } from "path";
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
- renameSync(temp, path);
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 = resolve9(env.CURSOR_PROJECT_DIR ?? process.cwd());
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 resolve11 } from "path";
16889
+ import { resolve as resolve12 } from "path";
16300
16890
 
16301
16891
  // src/agents/codex/action.ts
16302
- import { resolve as resolve10 } from "path";
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) => resolve10(String(payload.cwd), 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 = resolve11(str4(p.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(resolve11(root, ".replume"))) return QUIET2;
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(resolve11(root, path))])) : void 0 }
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(resolve11(root, path)) !== before[path]);
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(resolve11(root, path)) !== before[path]);
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 resolve13 } from "path";
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 resolve12 } from "path";
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 : resolve12(root, 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 = resolve13(str5(raw6.cwd) || process.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(resolve13(root, ".replume"))) return QUIET3;
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 resolve14 } from "path";
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 : resolve14(root, 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 : resolve14(cwd, 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 : resolve14(cwd, 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 = resolve14(str6(raw6.cwd) || process.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 resolve15 } from "path";
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 = resolve15(repoRoot2, arg);
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(`${resolve15(repoRoot2)}/`).join("").split(resolve15(repoRoot2)).join(".");
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 flushes = (e) => e.type === "prompt" || e.type === "stop" || e.type === "rule_hit" && e.rule === "auto_verdict";
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 renameSync2, rmSync as rmSync19, statSync as statSync14, writeFileSync as writeFileSync34 } from "fs";
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
- renameSync2(tmp, p);
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)) renameSync2(q, claim);
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((resolve17, reject) => {
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", () => resolve17(data));
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((resolve17) => {
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
- resolve17(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}`)}.`]);
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 = resolve16(opts.replay);
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 ? resolve16(opts.contract) : existsSync53(sibling) ? sibling : contractPath(repoRoot());
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 = resolve16(path);
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 ? resolve16(contractOpt) : existsSync53(sibling) ? sibling : null;
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 = resolve16(opts.root ?? repoRoot());
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;