replume-cli 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1107,22 +1107,33 @@ var PATTERNS2 = [
1107
1107
  // go test
1108
1108
  /^test (\S+) \.\.\. FAILED$/,
1109
1109
  // cargo test
1110
- /^[ \t]*[×✕] (.+?)(?:[ \t]+\(?\d+(?:\.\d+)?[ \t]?ms\)?)?[ \t]*$/
1110
+ /^[ \t]*[×✕] (.+?)(?:[ \t]+\(?\d+(?:\.\d+)?[ \t]?ms\)?)?[ \t]*$/,
1111
1111
  // vitest / jest
1112
+ // jest without --verbose (more than one test file): only the failure header `● describe › name` names the test
1113
+ // (captured from jest 29.7.0 on 30.10.2026); `● Console` and `● Test suite failed to run` are not tests
1114
+ /^[ \t]*● (?!Console[ \t]*$|Test suite failed to run[ \t]*$)(.+?)[ \t]*$/,
1115
+ // vitest's "Failed Tests" block: ` FAIL test/cart.test.js > name` (captured from vitest 3.2.4); jest's `FAIL file` suite
1116
+ // line has no ` > ` and is not a test name
1117
+ /^[ \t]*FAIL[ \t]+(\S.*? > .+?)[ \t]*$/
1112
1118
  ];
1113
1119
  var MAX_NAMES = 50;
1120
+ var ANSI3 = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
1121
+ var VITEST_FAIL = PATTERNS2[PATTERNS2.length - 1];
1114
1122
  function failingTestNames(output) {
1115
1123
  const names3 = [];
1116
- for (const line2 of output.split(/\r?\n/)) {
1124
+ const fullNames = [];
1125
+ for (const line2 of output.replace(ANSI3, "").split(/\r?\n/)) {
1117
1126
  for (const re of PATTERNS2) {
1118
1127
  const m = re.exec(line2);
1119
- if (m && !names3.includes(m[1])) {
1120
- names3.push(m[1]);
1121
- break;
1122
- }
1128
+ if (!m) continue;
1129
+ if (re === VITEST_FAIL) {
1130
+ if (!fullNames.includes(m[1])) fullNames.push(m[1]);
1131
+ } else if (!names3.includes(m[1])) names3.push(m[1]);
1132
+ break;
1123
1133
  }
1124
1134
  if (names3.length >= MAX_NAMES) break;
1125
1135
  }
1136
+ for (const f of fullNames) if (names3.length < MAX_NAMES && !names3.some((n3) => f === n3 || f.endsWith(` > ${n3}`))) names3.push(f);
1126
1137
  return names3;
1127
1138
  }
1128
1139
 
@@ -1314,6 +1325,300 @@ function matchesAny(path, patterns) {
1314
1325
  return null;
1315
1326
  }
1316
1327
 
1328
+ // src/supervisor/layer1.ts
1329
+ var TEST_COMMAND = /(?:^|[\s;&|(])(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b|npm\s+t\b|npx\s+(?:vitest|jest|mocha)\b|vitest\b|jest\b|mocha\b|pytest\b|python3?\s+-m\s+(?:pytest|unittest)\b|go\s+test\b|cargo\s+test\b|node\s+--test\b|mvn\s+(?:-\S+\s+)*test\b|gradle\w*\s+test\b|make\s+test\b|rspec\b|phpunit\b|dotnet\s+test\b)/;
1330
+ function isTestCommand(command, contractTestCommand) {
1331
+ if (!command) return false;
1332
+ if (contractTestCommand && command.trim() === contractTestCommand.trim()) return true;
1333
+ return TEST_COMMAND.test(command);
1334
+ }
1335
+ function commandRuns(events) {
1336
+ const runs = [];
1337
+ events.forEach((e, index) => {
1338
+ if (e.type !== "command" || !e.command) return;
1339
+ const raw5 = e.raw ?? {};
1340
+ if (raw5.exitUnknown === true) {
1341
+ runs.push({ index, command: e.command, ok: raw5.ok !== false, exitCode: null, exitUnknown: true });
1342
+ return;
1343
+ }
1344
+ runs.push({ index, command: e.command, ok: raw5.ok !== false, exitCode: raw5.exitCode ?? (raw5.ok === false ? null : 0) });
1345
+ });
1346
+ return runs;
1347
+ }
1348
+ var lastIndex = (xs, pred) => {
1349
+ for (let i = xs.length - 1; i >= 0; i--) if (pred(xs[i])) return i;
1350
+ return -1;
1351
+ };
1352
+ var TESTS_PASS = /\b(?:all\s+)?(?:\d+\s+|the\s+)?(?:unit\s+)?tests?\s+(?:now\s+|all\s+|still\s+)?(?:are\s+|is\s+)?(?:now\s+)?(?:pass(?:es|ed|ing)?|green|succeed(?:s|ed)?)\b|\btest\s+suite\s+(?:now\s+)?(?:passes|passed|is\s+green)\b|\bpass(?:es|ing)?\s+all\s+(?:the\s+)?tests\b|\bnpm\s+test\s+(?:now\s+)?(?:passes|passed|succeeds)\b/i;
1353
+ var DONE = /\b(?:is|are|now|everything(?:'s| is))\s+(?:done|complete|completed|fixed|working)\b|\b(?:I(?:'ve|\s+have)|has\s+been|have\s+been)\s+(?:implemented|fixed|completed|finished|resolved)\b|\ball\s+done\b|\btask\s+(?:is\s+)?(?:complete|completed|done)\b/i;
1354
+ var NEGATION = /\b(?:not|never|no|cannot|can't|couldn't|didn't|don't|doesn't|haven't|hasn't|isn't|aren't|wasn't|weren't|unable|fail(?:s|ed|ing)?|without)\b|n't\b/i;
1355
+ var NOTHING_WRONG = /\b(?:0|zero|no|none|without\s+(?:any\s+)?)\s*(?:\w+\s+)?(?:fail(?:s|ed|ures?|ing)?|errors?|regressions?|broken)\b|\bnothing\s+(?:failed|fails|is\s+failing|broke|broken)\b|\b(?:no|without)\s+(?:test\s+)?failures?\b/gi;
1356
+ function findClaims(message) {
1357
+ const sentences2 = message.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter(Boolean);
1358
+ const denies = (s) => NEGATION.test(s.replace(NOTHING_WRONG, " "));
1359
+ const pick3 = (re) => sentences2.find((s) => re.test(s) && !denies(s)) ?? null;
1360
+ return { testsPass: pick3(TESTS_PASS), done: pick3(DONE) };
1361
+ }
1362
+ var quote = (s) => {
1363
+ const t = s.replace(/[.!?]+$/, "");
1364
+ return t.length > 160 ? `${t.slice(0, 159)}\u2026` : t;
1365
+ };
1366
+ function claimVsEvidence(input) {
1367
+ if (input.proposal.kind !== "stop") return null;
1368
+ const { lastAssistantMessage } = input.proposal;
1369
+ const claims = findClaims(lastAssistantMessage);
1370
+ if (!claims.testsPass && !claims.done) return null;
1371
+ const events = input.recent;
1372
+ const contractTest = input.anchor.contract?.test_command;
1373
+ const runs = commandRuns(events).filter((r) => isTestCommand(r.command, contractTest));
1374
+ const lastEdit = lastIndex(events, (e) => e.type === "file_edit");
1375
+ const lastRun = runs.at(-1) ?? null;
1376
+ const lastOk = [...runs].reverse().find((r) => r.ok) ?? null;
1377
+ const editPath = lastEdit >= 0 ? events[lastEdit].paths.join(", ") || "a file" : null;
1378
+ const finding = (rule, claim, fact, reason) => ({
1379
+ layer: "layer1",
1380
+ rule,
1381
+ decision: "steer",
1382
+ message: `Replume: the final message says "${quote(claim)}". ${fact}`,
1383
+ reason
1384
+ });
1385
+ if (claims.testsPass) {
1386
+ if (!runs.length) {
1387
+ return finding("claim_without_evidence", claims.testsPass, "The ledger for this session has no test command that ran.", "tests-pass claim, no test run in session");
1388
+ }
1389
+ if (lastRun && !lastRun.ok) {
1390
+ return finding(
1391
+ "claim_without_evidence",
1392
+ claims.testsPass,
1393
+ `The last test command in this session, \`${lastRun.command}\`, ended with exit code ${lastRun.exitCode ?? "unknown"}.`,
1394
+ "tests-pass claim, last test run failed"
1395
+ );
1396
+ }
1397
+ if (lastOk && lastEdit > lastOk.index) {
1398
+ return finding(
1399
+ "claim_without_evidence",
1400
+ claims.testsPass,
1401
+ `The last successful test run (\`${lastOk.command}\`) was before the last edit (${editPath}).`,
1402
+ "tests-pass claim, edits after the last successful run"
1403
+ );
1404
+ }
1405
+ return null;
1406
+ }
1407
+ if (lastEdit < 0) return null;
1408
+ if (lastRun && lastRun.index > lastEdit && !lastRun.ok) {
1409
+ return finding(
1410
+ "claim_without_evidence",
1411
+ claims.done,
1412
+ `The last test command after the last edit, \`${lastRun.command}\`, ended with exit code ${lastRun.exitCode ?? "unknown"}.`,
1413
+ "done claim, last test run failed"
1414
+ );
1415
+ }
1416
+ const okAfterEdit = runs.some((r) => r.ok && r.index > lastEdit);
1417
+ if (okAfterEdit) return null;
1418
+ if (runs.length) {
1419
+ return finding(
1420
+ "claim_without_evidence",
1421
+ claims.done,
1422
+ `Tests ran earlier in this session (\`${runs.at(-1).command}\`), but no test command succeeded after the last edit (${editPath}).`,
1423
+ "done claim, no successful test run after the last edit"
1424
+ );
1425
+ }
1426
+ if (contractTest) {
1427
+ return finding(
1428
+ "claim_without_evidence",
1429
+ claims.done,
1430
+ `The task contract names the test command \`${contractTest}\`; it did not run in this session.`,
1431
+ "done claim, contract test_command never ran"
1432
+ );
1433
+ }
1434
+ return null;
1435
+ }
1436
+ function existingRules(input) {
1437
+ if (input.proposal.kind !== "action") return [];
1438
+ const ev = evaluate(input.proposal.action, {
1439
+ contract: input.anchor.contract,
1440
+ history: input.recent,
1441
+ repoRoot: input.repoRoot,
1442
+ scratchDirs: input.scratchDirs,
1443
+ mode: input.mode,
1444
+ baseFailing: input.baseFailing
1445
+ });
1446
+ return ev.hits.map((h) => ({
1447
+ layer: "layer1",
1448
+ rule: h.rule,
1449
+ decision: h.action === "block" ? "block" : "steer",
1450
+ message: h.message,
1451
+ reason: `rule ${h.rule} returned ${h.action}`
1452
+ }));
1453
+ }
1454
+ function layer1(input) {
1455
+ if (input.proposal.kind === "stop") {
1456
+ if (input.proposal.stopHookActive) return [];
1457
+ if (input.legacyClaim === false) return [];
1458
+ const f = claimVsEvidence(input);
1459
+ return f ? [f] : [];
1460
+ }
1461
+ return existingRules(input);
1462
+ }
1463
+
1464
+ // src/core/test-counts.ts
1465
+ var num = (m) => m ? Number(m[1]) : 0;
1466
+ function parseTestCounts(output) {
1467
+ const text = output.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "");
1468
+ const tests = /^\s*(?:ℹ|#)\s*tests\s+(\d+)\s*$/m.exec(text);
1469
+ if (tests) {
1470
+ const pass = num(/^\s*(?:ℹ|#)\s*pass\s+(\d+)\s*$/m.exec(text));
1471
+ const fail2 = num(/^\s*(?:ℹ|#)\s*fail\s+(\d+)\s*$/m.exec(text));
1472
+ const skipped = num(/^\s*(?:ℹ|#)\s*(?:skipped|skip)\s+(\d+)\s*$/m.exec(text));
1473
+ return { passed: pass, failed: fail2, skipped };
1474
+ }
1475
+ const jest = /^\s*Tests:\s+(.*?)\s+\d+\s+total/m.exec(text);
1476
+ if (jest) return { passed: num(/(\d+)\s+passed/.exec(jest[1])), failed: num(/(\d+)\s+failed/.exec(jest[1])), skipped: num(/(\d+)\s+(?:skipped|todo)/.exec(jest[1])) };
1477
+ const vitest = /^\s*Tests\s{2,}(.*?)\s*\(\d+\)/m.exec(text);
1478
+ if (vitest) return { passed: num(/(\d+)\s+passed/.exec(vitest[1])), failed: num(/(\d+)\s+failed/.exec(vitest[1])), skipped: num(/(\d+)\s+(?:skipped|todo)/.exec(vitest[1])) };
1479
+ const pytest = /^[=\s]*((?:\d+\s+(?:passed|failed|skipped|error|errors|xfailed|xpassed|deselected)(?:,\s*)?)+)\s+in\s+[\d.]+s/m.exec(text);
1480
+ if (pytest) return { passed: num(/(\d+)\s+passed/.exec(pytest[1])), failed: num(/(\d+)\s+(?:failed|errors?)/.exec(pytest[1])), skipped: num(/(\d+)\s+skipped/.exec(pytest[1])) };
1481
+ const cargo = /test result: \w+\.\s+(\d+) passed;\s+(\d+) failed;\s+(\d+) ignored/.exec(text);
1482
+ if (cargo) return { passed: Number(cargo[1]), failed: Number(cargo[2]), skipped: Number(cargo[3]) };
1483
+ return null;
1484
+ }
1485
+
1486
+ // src/core/commit-gate.ts
1487
+ var SEPARATORS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
1488
+ var GIT_OPTS_WITH_VALUE = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env", "--super-prefix"]);
1489
+ function simpleCommands(command) {
1490
+ const groups = [[]];
1491
+ const toks = shellTokens(command);
1492
+ for (let i = 0; i < toks.length; i++) {
1493
+ const t = toks[i];
1494
+ if ("op" in t) {
1495
+ if (SEPARATORS.has(t.op)) groups.push([]);
1496
+ else if (toks[i + 1] && "word" in toks[i + 1]) i++;
1497
+ } else groups[groups.length - 1].push(t.word);
1498
+ }
1499
+ return groups.map((g) => unwrapCommand(g)).filter((g) => g.length);
1500
+ }
1501
+ var OUTPUT_ONLY = /^(?:--|-q{1,3}|-v{1,3}|-s|--quiet|--verbose|--silent|--no-header|--ci|--colou?rs?|--no-colou?rs?|--colou?rs?=\w+|--tb=\w+|-rA)$/;
1502
+ var CWD_COMMANDS = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
1503
+ var isTestRun = (words6, want) => want.length > 0 && words6.length >= want.length && want.every((w, k) => words6[k] === w) && words6.slice(want.length).every((w) => OUTPUT_ONLY.test(w));
1504
+ function segments(command) {
1505
+ const out = [];
1506
+ let words6 = [];
1507
+ const toks = shellTokens(command);
1508
+ for (let i = 0; i < toks.length; i++) {
1509
+ const t = toks[i];
1510
+ if (!("op" in t)) {
1511
+ words6.push(t.word);
1512
+ continue;
1513
+ }
1514
+ if (!SEPARATORS.has(t.op)) {
1515
+ if (toks[i + 1] && "word" in toks[i + 1]) i++;
1516
+ continue;
1517
+ }
1518
+ out.push({ words: unwrapCommand(words6), after: t.op });
1519
+ words6 = [];
1520
+ }
1521
+ out.push({ words: unwrapCommand(words6), after: null });
1522
+ return out.filter((s) => s.words.length || s.after);
1523
+ }
1524
+ function gateBinding(ran, testCommand) {
1525
+ const want = simpleCommands(testCommand)[0] ?? [];
1526
+ const segs = segments(ran);
1527
+ const hit = segs.findIndex((s) => isTestRun(s.words, want));
1528
+ if (hit < 0 || segs.slice(0, hit).some((s) => CWD_COMMANDS.has(s.words[0] ?? ""))) return "none";
1529
+ const before = segs.slice(0, hit).every((s) => s.after === ";" || s.after === "&&");
1530
+ const after = segs.slice(hit).every((s) => s.after === null || s.after === "&&");
1531
+ return before && after ? "bound" : "masked";
1532
+ }
1533
+ function gitSubcommand(words6) {
1534
+ if (words6[0]?.split("/").pop() !== "git") return null;
1535
+ let i = 1;
1536
+ while (i < words6.length && words6[i].startsWith("-")) i += GIT_OPTS_WITH_VALUE.has(words6[i]) ? 2 : 1;
1537
+ return words6[i] ?? null;
1538
+ }
1539
+ function gitPublishVerbs(command) {
1540
+ const out = /* @__PURE__ */ new Set();
1541
+ for (const c of [command, ...innerScripts(command)]) {
1542
+ for (const words6 of simpleCommands(c)) {
1543
+ const sub = gitSubcommand(words6);
1544
+ if (sub === "commit" || sub === "push") out.add(sub);
1545
+ }
1546
+ }
1547
+ return [...out];
1548
+ }
1549
+ var cursorShell = (e, type) => {
1550
+ const raw5 = e.raw ?? {};
1551
+ return e.agent === "cursor" && e.type === type && raw5.tool_name === "Shell" ? raw5 : null;
1552
+ };
1553
+ var UNPARSED_SHELL_WRITE = "(a shell command whose write target could not be parsed)";
1554
+ function eventWrites(e) {
1555
+ const raw5 = e.raw ?? {};
1556
+ let paths;
1557
+ if (e.type === "file_edit") paths = raw5.ok !== false ? e.paths : [];
1558
+ else if (cursorShell(e, "tool_pre")) {
1559
+ paths = e.action === "block" ? [] : [...Array.isArray(raw5.writes) ? raw5.writes.filter((x) => typeof x === "string") : [], ...raw5.writeNote ? [UNPARSED_SHELL_WRITE] : []];
1560
+ } else paths = commandWrites(e);
1561
+ return paths.filter((p) => !p.startsWith(".replume/"));
1562
+ }
1563
+ function testRuns(history) {
1564
+ const runs = commandRuns(history).filter((r) => !(history[r.index].agent === "cursor" && history[r.index].raw?.hook_event_name === "beforeShellExecution")).map((r) => ({ ...r, tail: history[r.index].raw?.outputTail }));
1565
+ history.forEach((e, index) => {
1566
+ const raw5 = cursorShell(e, "tool_post");
1567
+ const command = raw5?.tool_input?.command;
1568
+ if (!raw5 || typeof command !== "string") return;
1569
+ runs.push({ index, command, ok: raw5.hook_event_name === "postToolUse", exitCode: null });
1570
+ });
1571
+ return runs.sort((x, y) => x.index - y.index);
1572
+ }
1573
+ function pipesTestCommand(command, testCommand) {
1574
+ const want = simpleCommands(testCommand)[0] ?? [];
1575
+ return segments(command).some((s) => isTestRun(s.words, want) && (s.after === "|" || s.after === "|&"));
1576
+ }
1577
+ function testsGateTheCommit(command, testCommand) {
1578
+ if (gateBinding(command, testCommand) !== "bound") return false;
1579
+ const want = simpleCommands(testCommand)[0] ?? [];
1580
+ const segs = segments(command).map((s) => s.words);
1581
+ const t = segs.findIndex((w) => isTestRun(w, want));
1582
+ const c = segs.findIndex((w) => {
1583
+ const s = gitSubcommand(w);
1584
+ return s === "commit" || s === "push";
1585
+ });
1586
+ return t >= 0 && c > t;
1587
+ }
1588
+ function commitGateStatus(history, testCommand, command, ownWrites = []) {
1589
+ let lastWrite = ownWrites.length ? history.length : -1;
1590
+ if (lastWrite < 0) history.forEach((e, i) => {
1591
+ if (eventWrites(e).length) lastWrite = i;
1592
+ });
1593
+ if (lastWrite < 0) return { ok: true, why: "no_changes", changed: [] };
1594
+ if (testsGateTheCommit(command, testCommand)) return { ok: true, why: "tests_first_in_command", changed: [] };
1595
+ const all = testRuns(history);
1596
+ const runs = all.filter((r) => r.index > lastWrite);
1597
+ const bound = runs.filter((r) => gateBinding(r.command, testCommand) === "bound");
1598
+ const last = bound[bound.length - 1];
1599
+ const counts = typeof last?.tail === "string" ? parseTestCounts(last.tail) : null;
1600
+ const zero = counts !== null && counts.passed + counts.failed === 0;
1601
+ if (last?.ok && !zero) return { ok: true, why: "passed_after_last_change", changed: [] };
1602
+ const passes = all.filter((r) => r.ok && gateBinding(r.command, testCommand) === "bound");
1603
+ const since = passes.length ? passes[passes.length - 1].index : -1;
1604
+ const changed = [.../* @__PURE__ */ new Set([...history.slice(since + 1).flatMap((e) => eventWrites(e)), ...ownWrites])];
1605
+ if (last && zero) return { ok: false, why: "zero_tests", changed };
1606
+ const pipedHere = pipesTestCommand(command, testCommand);
1607
+ const here = pipedHere ? { pipedHere } : {};
1608
+ if (last) return { ok: false, why: "failed", changed, exitCode: last.exitCode, ...here };
1609
+ const masked = runs.filter((r) => gateBinding(r.command, testCommand) === "masked");
1610
+ if (masked.length) return { ok: false, why: "masked", changed, ...here, ...masked.some((r) => pipesTestCommand(r.command, testCommand)) ? { pipedBefore: true } : {} };
1611
+ return { ok: false, why: "not_run", changed, ...here };
1612
+ }
1613
+ var list = (xs) => xs.length > 5 ? `${xs.slice(0, 5).join(", ")} and ${xs.length - 5} more` : xs.join(", ");
1614
+ function commitGateMessage(s, testCommand, verbs) {
1615
+ const what = verbs.includes("commit") ? "commit" : "push";
1616
+ const files = s.changed.length ? `you changed ${list(s.changed)}` : "files changed";
1617
+ const state = s.why === "failed" ? `\`${testCommand}\` failed on its last run after that${s.exitCode !== null && s.exitCode !== void 0 ? ` (exit code ${s.exitCode})` : ""}` : s.why === "zero_tests" ? `the last run of \`${testCommand}\` after that ran zero tests` : s.why === "masked" && s.pipedBefore ? `\`${testCommand}\` ran after that only piped into another command (as in \`${testCommand} | tail\`)` : s.why === "masked" ? `\`${testCommand}\` ran after that only in a form whose exit status does not show the result (e.g. \`${testCommand} || true\`)` : `\`${testCommand}\` has not run after that`;
1618
+ const pipe = s.pipedBefore || s.pipedHere ? ` A run piped into another command does not count: its exit status is the pipe's last command's, not the tests'${s.pipedHere ? ` (this command pipes \`${testCommand}\` too)` : ""}.` : "";
1619
+ return `Replume: no ${what} yet: ${files} after the last passing run of the tests, and ${state}.${pipe} Run \`${testCommand}\`, fix what fails, then ${what} (or run \`${testCommand} && git ${what} \u2026\`).`;
1620
+ }
1621
+
1317
1622
  // src/core/rules.ts
1318
1623
  import { createHash as createHash3 } from "crypto";
1319
1624
  import { execFileSync as execFileSync6 } from "child_process";
@@ -1441,14 +1746,14 @@ var unobservable = (command) => SCRIPT_RUN.test(command) || RUNTIME_TARGET.test(
1441
1746
  function hookErrors(eventsFile) {
1442
1747
  return existsSync5(join5(dirname3(eventsFile), "hook-errors.log"));
1443
1748
  }
1444
- var list = (m) => Object.entries(m).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([k, n3]) => `${k} ${n3}`).join(", ");
1749
+ var list2 = (m) => Object.entries(m).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([k, n3]) => `${k} ${n3}`).join(", ");
1445
1750
  function renderCapture(c) {
1446
1751
  const out = ["CAPTURE COVERAGE"];
1447
1752
  if (c.ledger !== "present") {
1448
1753
  out.push(` seen nothing (ledger ${c.ledger === "absent" ? "file not found" : "empty"})`);
1449
1754
  } else {
1450
- out.push(` seen ${c.events} events from ${c.agents.join(", ")} in ${c.sessions} session${c.sessions === 1 ? "" : "s"} (${c.firstTs} to ${c.lastTs}): ${list(c.byType)}`);
1451
- if (Object.keys(c.tools).length) out.push(` tools ${list(c.tools)}`);
1755
+ out.push(` seen ${c.events} events from ${c.agents.join(", ")} in ${c.sessions} session${c.sessions === 1 ? "" : "s"} (${c.firstTs} to ${c.lastTs}): ${list2(c.byType)}`);
1756
+ if (Object.keys(c.tools).length) out.push(` tools ${list2(c.tools)}`);
1452
1757
  if (c.changedFilesLanded !== void 0) {
1453
1758
  const a = c.changedFilesAttemptedOnly ?? [];
1454
1759
  out.push(
@@ -2300,7 +2605,7 @@ function redirectTargets(command, depth = 0) {
2300
2605
  return out.filter((p) => p && !DEVICE_SINK.test(p) && !/[$*?{}`]/.test(p) && !/^\d+$/.test(p));
2301
2606
  }
2302
2607
  var FILE_VERBS = /* @__PURE__ */ new Set(["rm", "mv", "cp", "truncate", "tee", "chmod", "chown", "ln", "unlink", "patch", "dd", "install", "touch"]);
2303
- var SEPARATORS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
2608
+ var SEPARATORS2 = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
2304
2609
  function fileVerbOperands(command, depth = 0) {
2305
2610
  const out = [];
2306
2611
  const simple = [[]];
@@ -2308,7 +2613,7 @@ function fileVerbOperands(command, depth = 0) {
2308
2613
  for (let i = 0; i < toks.length; i++) {
2309
2614
  const t = toks[i];
2310
2615
  if ("op" in t) {
2311
- if (SEPARATORS.has(t.op)) simple.push([]);
2616
+ if (SEPARATORS2.has(t.op)) simple.push([]);
2312
2617
  else if (WRITE_OPS.has(t.op) || t.op === "<" || t.op === "<<<" || t.op === "<<") i++;
2313
2618
  continue;
2314
2619
  }
@@ -2342,19 +2647,19 @@ function shellWriteTargets(command, depth = 0) {
2342
2647
  let cond = false;
2343
2648
  let base4 = false;
2344
2649
  const orLists = /* @__PURE__ */ new Set();
2345
- let list4 = 0;
2650
+ let list5 = 0;
2346
2651
  let construct = 0;
2347
2652
  let pendingFunc = false;
2348
2653
  const stack = [];
2349
2654
  const simple = [{ words: [], redirects: [], conditional: false, list: 0, depth: 0, construct: 0, after: ";" }];
2350
- const next = (after) => simple.push({ words: [], redirects: [], conditional: cond, list: list4, depth: stack.length, construct, after });
2655
+ const next = (after) => simple.push({ words: [], redirects: [], conditional: cond, list: list5, depth: stack.length, construct, after });
2351
2656
  for (let i = 0; i < toks.length; i++) {
2352
2657
  const t = toks[i];
2353
2658
  if ("op" in t) {
2354
2659
  const cur2 = simple[simple.length - 1];
2355
2660
  if (t.op === ";") {
2356
2661
  cond = base4;
2357
- list4++;
2662
+ list5++;
2358
2663
  pendingFunc = false;
2359
2664
  next(";");
2360
2665
  } else if (t.op === "&&") {
@@ -2362,7 +2667,7 @@ function shellWriteTargets(command, depth = 0) {
2362
2667
  next("&&");
2363
2668
  } else if (t.op === "||") {
2364
2669
  cond = true;
2365
- orLists.add(list4);
2670
+ orLists.add(list5);
2366
2671
  next("||");
2367
2672
  } else if (t.op === "(") {
2368
2673
  if (cur2.words.length === 1 && /^[A-Za-z_][\w.-]*$/.test(cur2.words[0])) pendingFunc = true;
@@ -2374,7 +2679,7 @@ function shellWriteTargets(command, depth = 0) {
2374
2679
  cond = top?.cond ?? false;
2375
2680
  base4 = top?.base ?? false;
2376
2681
  next(")");
2377
- } else if (SEPARATORS.has(t.op)) next(t.op);
2682
+ } else if (SEPARATORS2.has(t.op)) next(t.op);
2378
2683
  else if (WRITE_OPS.has(t.op)) {
2379
2684
  const target = toks[i + 1];
2380
2685
  const w = target && "word" in target ? target.word : null;
@@ -2529,7 +2834,7 @@ function packageManagerWrites(verb, args) {
2529
2834
  }
2530
2835
  function deletedOperands(command, depth = 0) {
2531
2836
  const out = [];
2532
- for (const t of shellTokens(command).reduce((acc, x) => ("op" in x && SEPARATORS.has(x.op) ? acc.push([]) : "word" in x && acc[acc.length - 1].push(x.word), acc), [[]])) {
2837
+ for (const t of shellTokens(command).reduce((acc, x) => ("op" in x && SEPARATORS2.has(x.op) ? acc.push([]) : "word" in x && acc[acc.length - 1].push(x.word), acc), [[]])) {
2533
2838
  const words6 = unwrapCommand(t);
2534
2839
  const verb = words6[0]?.split("/").pop();
2535
2840
  const ops = splitFlags(words6.slice(1)).operands;
@@ -2969,6 +3274,63 @@ var TEST_DECL = /\b(?:test|it|t)(?:\.\w+)?\s*\(\s*['"`]|^\s*(?:async\s+)?def\s+t
2969
3274
  function assertionLiterals(s) {
2970
3275
  return s.split("\n").filter((l) => new RegExp(ASSERTION.source).test(l)).map((l) => l.trim()).sort();
2971
3276
  }
3277
+ var TEST_TITLE = /\b(?:test|it|t)(?:\.\w+)?\s*\(\s*(['"`])((?:(?!\1).){1,200})\1|^\s*(?:async\s+)?def\s+(test_\w*)|^\s*func\s+(Test\w*)/;
3278
+ function changedSince(repoRoot2, base4, path) {
3279
+ let now = null;
3280
+ try {
3281
+ now = readFileSync9(join10(repoRoot2, path), "utf8");
3282
+ } catch {
3283
+ now = null;
3284
+ }
3285
+ return fileAt(repoRoot2, base4, path) !== now;
3286
+ }
3287
+ function reachableSources(repoRoot2, testPath) {
3288
+ const seen = /* @__PURE__ */ new Set([testPath]);
3289
+ const queue = [testPath];
3290
+ const out = [];
3291
+ while (queue.length && seen.size < 400) {
3292
+ for (const next of importedSources(repoRoot2, queue.shift())) {
3293
+ if (seen.has(next)) continue;
3294
+ seen.add(next);
3295
+ out.push(next);
3296
+ queue.push(next);
3297
+ }
3298
+ }
3299
+ return out;
3300
+ }
3301
+ function bentToCode(i) {
3302
+ const afterLines = assertionLiterals(i.after);
3303
+ const gone = assertionLiterals(i.start).filter((l) => {
3304
+ const k = afterLines.indexOf(l);
3305
+ if (k < 0) return true;
3306
+ afterLines.splice(k, 1);
3307
+ return false;
3308
+ });
3309
+ if (!gone.length) return null;
3310
+ const lines = i.start.split("\n");
3311
+ const titles = /* @__PURE__ */ new Set();
3312
+ lines.forEach((l, n3) => {
3313
+ if (!gone.includes(l.trim())) return;
3314
+ for (let k = n3; k >= 0; k--) {
3315
+ const m = TEST_TITLE.exec(lines[k]);
3316
+ if (m) {
3317
+ titles.add(m[2] ?? m[3] ?? m[4]);
3318
+ break;
3319
+ }
3320
+ }
3321
+ });
3322
+ if (!titles.size) return null;
3323
+ const imports = reachableSources(i.repoRoot, i.path);
3324
+ if (!imports.length || imports.some(i.changed)) return null;
3325
+ const failing = i.baseFailing();
3326
+ if (!failing?.length) return null;
3327
+ const tests = [...titles].filter((t) => failing.some((n3) => n3 === t || n3.endsWith(t)));
3328
+ return tests.length ? { tests, imports } : null;
3329
+ }
3330
+ var quoteList = (xs) => xs.slice(0, 3).map((x) => `"${x}"`).join(", ") + (xs.length > 3 ? ` and ${xs.length - 3} more` : "");
3331
+ function bentDetail(path, b) {
3332
+ return `${path}: the expected value of ${quoteList(b.tests)} changed while ${b.tests.length > 1 ? "those tests" : "that test"} failed at the task start, and the code it tests (${b.imports.slice(0, 3).join(", ")}${b.imports.length > 3 ? ", \u2026" : ""}) did not change, so the edit only makes the test agree with the code`;
3333
+ }
2972
3334
  var testWeakened = (a, ctx) => {
2973
3335
  if (a.kind === "command" && a.command) {
2974
3336
  if (FORCED_SUCCESS.test(a.command)) {
@@ -2991,6 +3353,7 @@ var testWeakened = (a, ctx) => {
2991
3353
  if (!isWrite(a)) return null;
2992
3354
  const findings = [];
2993
3355
  let severity = "ok";
3356
+ let bent = null;
2994
3357
  const base4 = baseCommitOf(ctx.repoRoot);
2995
3358
  const restoresStart = (ch) => {
2996
3359
  if (!base4) return false;
@@ -3038,6 +3401,15 @@ var testWeakened = (a, ctx) => {
3038
3401
  findings.push(`${ch.path}: ${declsRemoved} test${declsRemoved > 1 ? "s" : ""} removed`);
3039
3402
  severity = "block";
3040
3403
  }
3404
+ if (ch.before && !removedExisting && skipsAdded <= 0 && !bent && base4 && start && ctx.baseFailing && askedTestPaths(ctx).includes(ch.path)) {
3405
+ const after = contentAfter(a, ctx, ch.path);
3406
+ const b = after && !isConfirmed(ctx.repoRoot, ch.path, after.next) ? bentToCode({ repoRoot: ctx.repoRoot, path: ch.path, start, after: after.next, changed: (p) => changedSince(ctx.repoRoot, base4, p), baseFailing: ctx.baseFailing }) : null;
3407
+ if (b && after) {
3408
+ bent = { path: ch.path, next: after.next };
3409
+ findings.push(bentDetail(ch.path, b));
3410
+ severity = "block";
3411
+ }
3412
+ }
3041
3413
  if (ch.before && !removedExisting && skipsAdded <= 0 && !askedTestPaths(ctx).includes(ch.path)) {
3042
3414
  const changed = gone.filter(existing).length;
3043
3415
  if (changed > 0) {
@@ -3047,6 +3419,13 @@ var testWeakened = (a, ctx) => {
3047
3419
  }
3048
3420
  }
3049
3421
  if (!findings.length) return null;
3422
+ if (bent && findings.length === 1) {
3423
+ return {
3424
+ rule: "test_weakened",
3425
+ action: "block",
3426
+ message: `${PREFIX} this edit was not made: ${findings[0]}. Keep the expected value and fix the code instead. If the user asked for exactly this change, do not make it another way: say in your final message that it needs their confirmation, with this command for them to run: \`${confirmCommand(bent.path, bent.next)}\`. After they run it, the same edit goes through.`
3427
+ };
3428
+ }
3050
3429
  return {
3051
3430
  rule: "test_weakened",
3052
3431
  action: severity,
@@ -3116,7 +3495,7 @@ function shellScripts(command, depth = 0) {
3116
3495
  const groups = [[]];
3117
3496
  for (const t of shellTokens(command)) {
3118
3497
  if ("op" in t) {
3119
- if (SEPARATORS.has(t.op)) groups.push([]);
3498
+ if (SEPARATORS2.has(t.op)) groups.push([]);
3120
3499
  } else groups[groups.length - 1].push(t.word);
3121
3500
  }
3122
3501
  for (const g of groups) {
@@ -3137,7 +3516,7 @@ function deletionTargets(command) {
3137
3516
  const groups = [[]];
3138
3517
  for (const token of shellTokens(command)) {
3139
3518
  if ("op" in token) {
3140
- if (SEPARATORS.has(token.op)) groups.push([]);
3519
+ if (SEPARATORS2.has(token.op)) groups.push([]);
3141
3520
  } else groups[groups.length - 1].push(token.word);
3142
3521
  }
3143
3522
  const result2 = [];
@@ -3207,7 +3586,7 @@ function blockDeviceWrites(scripts) {
3207
3586
  for (let i = 0; i < toks.length; i++) {
3208
3587
  const t = toks[i];
3209
3588
  if ("op" in t) {
3210
- if (SEPARATORS.has(t.op)) groups.push([]);
3589
+ if (SEPARATORS2.has(t.op)) groups.push([]);
3211
3590
  else if (WRITE_OPS.has(t.op) || t.op === "<" || t.op === "<<<" || t.op === "<<") i++;
3212
3591
  continue;
3213
3592
  }
@@ -3393,6 +3772,17 @@ var replumeStateWrite = (a, ctx) => {
3393
3772
  message: `${PREFIX} this command appears to write into .replume/, where Replume keeps the contract, scenarios and ledger. Agents must not change them. ${pick(ctx, "If the contract is wrong, ask the user to run \\`replume task\\` again.", "Keep working inside the contract; if you think it is wrong, finish the task within it and write in your final message what you would change.")}`
3394
3773
  };
3395
3774
  };
3775
+ var commitWithoutTests = (a, ctx) => {
3776
+ if (ctx.mode !== "auto" || a.kind !== "command" || !a.command) return null;
3777
+ const testCommand = ctx.contract?.test_command;
3778
+ if (!testCommand) return null;
3779
+ const verbs = gitPublishVerbs(a.command);
3780
+ if (!verbs.length) return null;
3781
+ const own = [...a.paths, ...a.writeTargetNotParsed ? [UNPARSED_SHELL_WRITE] : []].filter((p) => !p.startsWith(".replume/"));
3782
+ const s = commitGateStatus(ctx.history, testCommand, a.command, own);
3783
+ if (s.ok) return null;
3784
+ return { rule: "commit_without_tests", action: "block", message: commitGateMessage(s, testCommand, verbs) };
3785
+ };
3396
3786
  var RULES = {
3397
3787
  out_of_scope_edit: outOfScopeEdit,
3398
3788
  protected_path_modified: protectedPathModified,
@@ -3401,7 +3791,8 @@ var RULES = {
3401
3791
  destructive_command: destructiveCommand,
3402
3792
  loop,
3403
3793
  secrets_touch: secretsTouch,
3404
- replume_state_write: replumeStateWrite
3794
+ replume_state_write: replumeStateWrite,
3795
+ commit_without_tests: commitWithoutTests
3405
3796
  };
3406
3797
  var RANK = { ok: 0, warn: 1, block: 2 };
3407
3798
  function evaluate(a, ctx) {
@@ -3419,7 +3810,7 @@ function evaluate(a, ctx) {
3419
3810
  var PATH2 = /(?:^|[\s"'`(<[])((?:\.{0,2}\/)?(?:[\w@.-]+\/)*[\w@.-]+\.[A-Za-z0-9]+)(?=$|[\s"'`)>\],;:!?]|\.(?:\s|$))/g;
3420
3811
  var CHANGE = new RegExp(`\\b(?:${VERB}|fix(?:e[sd])?|fixing|write|writes|wrote|put|insert|implement(?:s|ed|ing)?|belongs?\\s+in|go(?:es)?\\s+(?:in|into)|should\\s+(?:be|go)\\s+in)\\b`, "gi");
3421
3812
  var READ = /\b(?:read|reads|reading|look(?:ing)?\s+at|see|open|inspect|review|check|study|consult|refer\s+to|based\s+on|like)\b/gi;
3422
- var lastIndex = (re, text) => {
3813
+ var lastIndex2 = (re, text) => {
3423
3814
  let at = -1;
3424
3815
  for (const m of text.matchAll(re)) at = m.index ?? at;
3425
3816
  return at;
@@ -3432,16 +3823,16 @@ function protectedTestsNamedInTask(task, protect, opts = {}) {
3432
3823
  if (!isTestFile(p) || !matchesAny(p, protect) || out.includes(p)) continue;
3433
3824
  if (opts.askedOnly) {
3434
3825
  const before = sentence.slice(0, m.index ?? 0);
3435
- if (lastIndex(CHANGE, before) <= lastIndex(READ, before)) continue;
3826
+ if (lastIndex2(CHANGE, before) <= lastIndex2(READ, before)) continue;
3436
3827
  }
3437
3828
  out.push(p);
3438
3829
  }
3439
3830
  }
3440
3831
  return out;
3441
3832
  }
3442
- var list2 = (xs) => xs.length <= 3 ? xs.join(", ") : `${xs.slice(0, 3).join(", ")} and ${xs.length - 3} more`;
3833
+ var list3 = (xs) => xs.length <= 3 ? xs.join(", ") : `${xs.slice(0, 3).join(", ")} and ${xs.length - 3} more`;
3443
3834
  function protectedNamedNotice(named) {
3444
- return `The task names ${list2(named)}, which ${named.length === 1 ? "is" : "are"} protected (existing test files are protected in auto mode): ${named.length === 1 ? "it stays" : "they stay"} blocked. Add new tests in a new test file and say so in your final message.`;
3835
+ return `The task names ${list3(named)}, which ${named.length === 1 ? "is" : "are"} protected (existing test files are protected in auto mode): ${named.length === 1 ? "it stays" : "they stay"} blocked. Add new tests in a new test file and say so in your final message.`;
3445
3836
  }
3446
3837
  function notWhereAsked(named, changed) {
3447
3838
  const missed = named.filter((n3) => !changed.includes(n3));
@@ -3449,7 +3840,7 @@ function notWhereAsked(named, changed) {
3449
3840
  if (!missed.length || !others.length) return null;
3450
3841
  return {
3451
3842
  named: missed,
3452
- text: `not where you asked: the task names ${list2(missed)}, which ${missed.length === 1 ? "is" : "are"} protected (existing test files are protected in auto mode); the agent wrote ${list2(others)} instead`
3843
+ text: `not where you asked: the task names ${list3(missed)}, which ${missed.length === 1 ? "is" : "are"} protected (existing test files are protected in auto mode); the agent wrote ${list3(others)} instead`
3453
3844
  };
3454
3845
  }
3455
3846
 
@@ -3519,8 +3910,8 @@ function contractBrief(c, mode = "approve", opts = {}) {
3519
3910
  const auto = mode === "auto";
3520
3911
  const task = c.task.length > 400 ? `${c.task.slice(0, 399)}\u2026` : c.task;
3521
3912
  const head = `Replume task contract ${c.id}. Task: "${task}".`;
3522
- const list4 = groupedList(protectedFiles, 500);
3523
- const protect = protectedFiles.length ? auto ? `Protected, do not change: ${list4}. A protected test is the spec: when it fails, fix the code, not the test. This contract was agreed before your request and takes priority over it: if a request asks for a change to a protected file, reach the goal in the allowed files and say in your final message what you did instead. Do not ask the user.` : `Protected, do not change: ${list4}. A protected test is the spec: when it fails, fix the code, not the test. A block is not a dead end. This contract was agreed before your request and takes priority over it: if a request asks for a change to a protected file, reach the goal inside the allowed files, say what you did instead, and ask only if no allowed file can reach it.` : "";
3913
+ const list5 = groupedList(protectedFiles, 500);
3914
+ const protect = protectedFiles.length ? auto ? `Protected, do not change: ${list5}. A protected test is the spec: when it fails, fix the code, not the test. This contract was agreed before your request and takes priority over it: if a request asks for a change to a protected file, reach the goal in the allowed files and say in your final message what you did instead. Do not ask the user.` : `Protected, do not change: ${list5}. A protected test is the spec: when it fails, fix the code, not the test. A block is not a dead end. This contract was agreed before your request and takes priority over it: if a request asks for a change to a protected file, reach the goal inside the allowed files, say what you did instead, and ask only if no allowed file can reach it.` : "";
3524
3915
  const guard = auto && c.test_command && opts.testScript?.length ? `The test command is protected too: do not change ${opts.testScript.join("; ")}.` : "";
3525
3916
  const done = c.test_command ? `${guard ? `${guard} ` : ""}Done means \`${c.test_command}\` passes after your last edit.` : "";
3526
3917
  const tail = "Replume checks all of this when the change is committed.";
@@ -3632,28 +4023,6 @@ function buildContract(input) {
3632
4023
  };
3633
4024
  }
3634
4025
 
3635
- // src/core/test-counts.ts
3636
- var num = (m) => m ? Number(m[1]) : 0;
3637
- function parseTestCounts(output) {
3638
- const text = output.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "");
3639
- const tests = /^\s*(?:ℹ|#)\s*tests\s+(\d+)\s*$/m.exec(text);
3640
- if (tests) {
3641
- const pass = num(/^\s*(?:ℹ|#)\s*pass\s+(\d+)\s*$/m.exec(text));
3642
- const fail2 = num(/^\s*(?:ℹ|#)\s*fail\s+(\d+)\s*$/m.exec(text));
3643
- const skipped = num(/^\s*(?:ℹ|#)\s*(?:skipped|skip)\s+(\d+)\s*$/m.exec(text));
3644
- return { passed: pass, failed: fail2, skipped };
3645
- }
3646
- const jest = /^\s*Tests:\s+(.*?)\s+\d+\s+total/m.exec(text);
3647
- if (jest) return { passed: num(/(\d+)\s+passed/.exec(jest[1])), failed: num(/(\d+)\s+failed/.exec(jest[1])), skipped: num(/(\d+)\s+(?:skipped|todo)/.exec(jest[1])) };
3648
- const vitest = /^\s*Tests\s{2,}(.*?)\s*\(\d+\)/m.exec(text);
3649
- if (vitest) return { passed: num(/(\d+)\s+passed/.exec(vitest[1])), failed: num(/(\d+)\s+failed/.exec(vitest[1])), skipped: num(/(\d+)\s+(?:skipped|todo)/.exec(vitest[1])) };
3650
- const pytest = /^[=\s]*((?:\d+\s+(?:passed|failed|skipped|error|errors|xfailed|xpassed|deselected)(?:,\s*)?)+)\s+in\s+[\d.]+s/m.exec(text);
3651
- if (pytest) return { passed: num(/(\d+)\s+passed/.exec(pytest[1])), failed: num(/(\d+)\s+(?:failed|errors?)/.exec(pytest[1])), skipped: num(/(\d+)\s+skipped/.exec(pytest[1])) };
3652
- const cargo = /test result: \w+\.\s+(\d+) passed;\s+(\d+) failed;\s+(\d+) ignored/.exec(text);
3653
- if (cargo) return { passed: Number(cargo[1]), failed: Number(cargo[2]), skipped: Number(cargo[3]) };
3654
- return null;
3655
- }
3656
-
3657
4026
  // src/supervisor/base-run.ts
3658
4027
  import { readFileSync as readFileSync11 } from "fs";
3659
4028
  var spawnRunner = (command, cwd, timeoutSec) => {
@@ -3740,6 +4109,15 @@ function baseRunFor(i) {
3740
4109
  }
3741
4110
  return run;
3742
4111
  }
4112
+ function baseFailingFor(i) {
4113
+ let memo;
4114
+ return () => {
4115
+ if (memo !== void 0) return memo;
4116
+ const command = i.command?.trim();
4117
+ const run = command ? baseRunFor({ repoRoot: i.repoRoot, sessionId: i.session[0]?.sessionId ?? "", session: i.session, command, timeoutSec: Math.min(i.timeoutSec, 20), runner: i.runner ?? spawnRunner }) : null;
4118
+ return memo = run?.ok ? run.failing : null;
4119
+ };
4120
+ }
3743
4121
  function splitFailures(i) {
3744
4122
  if (requiresWholeSuite(i.task)) return { blocking: i.nowFailing, preExisting: [] };
3745
4123
  const task = i.task.toLowerCase();
@@ -3810,7 +4188,7 @@ function makeTouchesChangedCode(repoRoot2, changedSources) {
3810
4188
  };
3811
4189
  return Object.assign(touches, { detail });
3812
4190
  }
3813
- var names = (list4) => list4.slice(0, 3).map((n3) => `"${n3}"`).join(", ") + (list4.length > 3 ? ` and ${list4.length - 3} more` : "");
4191
+ var names = (list5) => list5.slice(0, 3).map((n3) => `"${n3}"`).join(", ") + (list5.length > 3 ? ` and ${list5.length - 3} more` : "");
3814
4192
  function whyBlocking(blocking, i) {
3815
4193
  if (!blocking.length) return "no failing test was there at the task start";
3816
4194
  const base4 = new Set(i.base.failing);
@@ -3972,141 +4350,6 @@ function preExistingScenario(scenario, before, after, contract, changedPaths) {
3972
4350
  return true;
3973
4351
  }
3974
4352
 
3975
- // src/supervisor/layer1.ts
3976
- var TEST_COMMAND = /(?:^|[\s;&|(])(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b|npm\s+t\b|npx\s+(?:vitest|jest|mocha)\b|vitest\b|jest\b|mocha\b|pytest\b|python3?\s+-m\s+(?:pytest|unittest)\b|go\s+test\b|cargo\s+test\b|node\s+--test\b|mvn\s+(?:-\S+\s+)*test\b|gradle\w*\s+test\b|make\s+test\b|rspec\b|phpunit\b|dotnet\s+test\b)/;
3977
- function isTestCommand(command, contractTestCommand) {
3978
- if (!command) return false;
3979
- if (contractTestCommand && command.trim() === contractTestCommand.trim()) return true;
3980
- return TEST_COMMAND.test(command);
3981
- }
3982
- function commandRuns(events) {
3983
- const runs = [];
3984
- events.forEach((e, index) => {
3985
- if (e.type !== "command" || !e.command) return;
3986
- const raw5 = e.raw ?? {};
3987
- if (raw5.exitUnknown === true) {
3988
- runs.push({ index, command: e.command, ok: raw5.ok !== false, exitCode: null, exitUnknown: true });
3989
- return;
3990
- }
3991
- runs.push({ index, command: e.command, ok: raw5.ok !== false, exitCode: raw5.exitCode ?? (raw5.ok === false ? null : 0) });
3992
- });
3993
- return runs;
3994
- }
3995
- var lastIndex2 = (xs, pred) => {
3996
- for (let i = xs.length - 1; i >= 0; i--) if (pred(xs[i])) return i;
3997
- return -1;
3998
- };
3999
- var TESTS_PASS = /\b(?:all\s+)?(?:\d+\s+|the\s+)?(?:unit\s+)?tests?\s+(?:now\s+|all\s+|still\s+)?(?:are\s+|is\s+)?(?:now\s+)?(?:pass(?:es|ed|ing)?|green|succeed(?:s|ed)?)\b|\btest\s+suite\s+(?:now\s+)?(?:passes|passed|is\s+green)\b|\bpass(?:es|ing)?\s+all\s+(?:the\s+)?tests\b|\bnpm\s+test\s+(?:now\s+)?(?:passes|passed|succeeds)\b/i;
4000
- var DONE = /\b(?:is|are|now|everything(?:'s| is))\s+(?:done|complete|completed|fixed|working)\b|\b(?:I(?:'ve|\s+have)|has\s+been|have\s+been)\s+(?:implemented|fixed|completed|finished|resolved)\b|\ball\s+done\b|\btask\s+(?:is\s+)?(?:complete|completed|done)\b/i;
4001
- var NEGATION = /\b(?:not|never|no|cannot|can't|couldn't|didn't|don't|doesn't|haven't|hasn't|isn't|aren't|wasn't|weren't|unable|fail(?:s|ed|ing)?|without)\b|n't\b/i;
4002
- var NOTHING_WRONG = /\b(?:0|zero|no|none|without\s+(?:any\s+)?)\s*(?:\w+\s+)?(?:fail(?:s|ed|ures?|ing)?|errors?|regressions?|broken)\b|\bnothing\s+(?:failed|fails|is\s+failing|broke|broken)\b|\b(?:no|without)\s+(?:test\s+)?failures?\b/gi;
4003
- function findClaims(message) {
4004
- const sentences2 = message.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter(Boolean);
4005
- const denies = (s) => NEGATION.test(s.replace(NOTHING_WRONG, " "));
4006
- const pick3 = (re) => sentences2.find((s) => re.test(s) && !denies(s)) ?? null;
4007
- return { testsPass: pick3(TESTS_PASS), done: pick3(DONE) };
4008
- }
4009
- var quote = (s) => {
4010
- const t = s.replace(/[.!?]+$/, "");
4011
- return t.length > 160 ? `${t.slice(0, 159)}\u2026` : t;
4012
- };
4013
- function claimVsEvidence(input) {
4014
- if (input.proposal.kind !== "stop") return null;
4015
- const { lastAssistantMessage } = input.proposal;
4016
- const claims = findClaims(lastAssistantMessage);
4017
- if (!claims.testsPass && !claims.done) return null;
4018
- const events = input.recent;
4019
- const contractTest = input.anchor.contract?.test_command;
4020
- const runs = commandRuns(events).filter((r) => isTestCommand(r.command, contractTest));
4021
- const lastEdit = lastIndex2(events, (e) => e.type === "file_edit");
4022
- const lastRun = runs.at(-1) ?? null;
4023
- const lastOk = [...runs].reverse().find((r) => r.ok) ?? null;
4024
- const editPath = lastEdit >= 0 ? events[lastEdit].paths.join(", ") || "a file" : null;
4025
- const finding = (rule, claim, fact, reason) => ({
4026
- layer: "layer1",
4027
- rule,
4028
- decision: "steer",
4029
- message: `Replume: the final message says "${quote(claim)}". ${fact}`,
4030
- reason
4031
- });
4032
- if (claims.testsPass) {
4033
- if (!runs.length) {
4034
- return finding("claim_without_evidence", claims.testsPass, "The ledger for this session has no test command that ran.", "tests-pass claim, no test run in session");
4035
- }
4036
- if (lastRun && !lastRun.ok) {
4037
- return finding(
4038
- "claim_without_evidence",
4039
- claims.testsPass,
4040
- `The last test command in this session, \`${lastRun.command}\`, ended with exit code ${lastRun.exitCode ?? "unknown"}.`,
4041
- "tests-pass claim, last test run failed"
4042
- );
4043
- }
4044
- if (lastOk && lastEdit > lastOk.index) {
4045
- return finding(
4046
- "claim_without_evidence",
4047
- claims.testsPass,
4048
- `The last successful test run (\`${lastOk.command}\`) was before the last edit (${editPath}).`,
4049
- "tests-pass claim, edits after the last successful run"
4050
- );
4051
- }
4052
- return null;
4053
- }
4054
- if (lastEdit < 0) return null;
4055
- if (lastRun && lastRun.index > lastEdit && !lastRun.ok) {
4056
- return finding(
4057
- "claim_without_evidence",
4058
- claims.done,
4059
- `The last test command after the last edit, \`${lastRun.command}\`, ended with exit code ${lastRun.exitCode ?? "unknown"}.`,
4060
- "done claim, last test run failed"
4061
- );
4062
- }
4063
- const okAfterEdit = runs.some((r) => r.ok && r.index > lastEdit);
4064
- if (okAfterEdit) return null;
4065
- if (runs.length) {
4066
- return finding(
4067
- "claim_without_evidence",
4068
- claims.done,
4069
- `Tests ran earlier in this session (\`${runs.at(-1).command}\`), but no test command succeeded after the last edit (${editPath}).`,
4070
- "done claim, no successful test run after the last edit"
4071
- );
4072
- }
4073
- if (contractTest) {
4074
- return finding(
4075
- "claim_without_evidence",
4076
- claims.done,
4077
- `The task contract names the test command \`${contractTest}\`; it did not run in this session.`,
4078
- "done claim, contract test_command never ran"
4079
- );
4080
- }
4081
- return null;
4082
- }
4083
- function existingRules(input) {
4084
- if (input.proposal.kind !== "action") return [];
4085
- const ev = evaluate(input.proposal.action, {
4086
- contract: input.anchor.contract,
4087
- history: input.recent,
4088
- repoRoot: input.repoRoot,
4089
- scratchDirs: input.scratchDirs,
4090
- mode: input.mode
4091
- });
4092
- return ev.hits.map((h) => ({
4093
- layer: "layer1",
4094
- rule: h.rule,
4095
- decision: h.action === "block" ? "block" : "steer",
4096
- message: h.message,
4097
- reason: `rule ${h.rule} returned ${h.action}`
4098
- }));
4099
- }
4100
- function layer1(input) {
4101
- if (input.proposal.kind === "stop") {
4102
- if (input.proposal.stopHookActive) return [];
4103
- if (input.legacyClaim === false) return [];
4104
- const f = claimVsEvidence(input);
4105
- return f ? [f] : [];
4106
- }
4107
- return existingRules(input);
4108
- }
4109
-
4110
4353
  // src/supervisor/checks/text.ts
4111
4354
  function langOf(path) {
4112
4355
  const p = path.toLowerCase();
@@ -5170,7 +5413,7 @@ var git4 = (root, args) => {
5170
5413
  return null;
5171
5414
  }
5172
5415
  };
5173
- var sources = (list4) => (list4 ?? "").split("\0").filter((p) => p && titleSource(p) && !isDependencyPath(p)).slice(0, MAX_FILES);
5416
+ var sources = (list5) => (list5 ?? "").split("\0").filter((p) => p && titleSource(p) && !isDependencyPath(p)).slice(0, MAX_FILES);
5174
5417
  function workingTreeTestTitles(repoRoot2) {
5175
5418
  const out = /* @__PURE__ */ new Set();
5176
5419
  for (const p of sources(git4(repoRoot2, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]))) {
@@ -5352,7 +5595,10 @@ async function tamperingFindings(repo, base4, commit, entries, opts) {
5352
5595
  const confirmed = after !== null && isConfirmed(opts.repoRoot, e.path, after);
5353
5596
  if (asked || confirmed) {
5354
5597
  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));
5598
+ const start = !w2.length && !confirmed && after !== null && opts.baseFailing && e.status === "M" ? blobAt(repo, base4, e.path) : null;
5599
+ 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;
5355
5600
  if (w2.length) findings.push({ rule: "test_weakened", path: e.path, detail: `${w2.join(", ")} (the task${confirmed ? " or a person" : ""} allowed a change to this test, not a weaker test)` });
5601
+ else if (bent) findings.push({ rule: "test_weakened", path: e.path, detail: `${bentDetail(e.path, bent).slice(e.path.length + 2)} (the task allowed a change to this test, not a test that states the bug)` });
5356
5602
  else {
5357
5603
  const rule = confirmed ? TEST_CHANGE_CONFIRMED : TEST_CHANGE_ASKED;
5358
5604
  const what2 = confirmed ? "this content was confirmed by a person (replume confirm-config; informational)" : "changed as the task asks (informational; review it)";
@@ -7226,8 +7472,8 @@ function projectHooksConfigured(root) {
7226
7472
  var TEST_RUNNER = /\b(?:npm\s+(?:run\s+)?test|npm\s+t|vitest|jest|pytest|cargo\s+test|node\s+--test|go\s+test)\b/;
7227
7473
  var ZERO_TESTS = /^\s*(?:ℹ|#)?\s*tests\s+0\s*$|^\s*1\.\.0\b|<!--\s*tests\s+0\s*-->|^[ \t]*(?:running[ \t]+)?0 tests?\b|\b(?:ran|executed|found|collected)[ \t]+0[ \t]+tests?\b|\b0 passing\b|collected 0 items|no tests? ran|no tests found|no test files|no tests to run/im;
7228
7474
  var TEST_EVIDENCE = /^[ \t]*(?:ℹ|#)[ \t]*tests[ \t]+[1-9]\d*[ \t]*$|^[ \t]*1\.\.[1-9]\d*\b|^ok[ \t]+[1-9]\d*\b|<!--[ \t]*tests[ \t]+[1-9]\d*[ \t]*-->|<testcase\b|^[.]+$|\b[1-9]\d*[ \t]+passing\b|\b[1-9]\d*[ \t]+passed\b|\brunning[ \t]+[1-9]\d*[ \t]+tests?\b|^ok[ \t]+\S+[ \t]|^--- PASS\b/m;
7229
- var ANSI3 = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
7230
- var stripAnsi = (s) => s.replace(ANSI3, "");
7475
+ var ANSI4 = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
7476
+ var stripAnsi = (s) => s.replace(ANSI4, "");
7231
7477
  var UNRECOGNISED = "test output not recognised: cannot confirm that any test ran";
7232
7478
  async function execute(s, run, defaultTimeout, requireEvidence = false) {
7233
7479
  const timeout = s.timeout_sec ?? defaultTimeout;
@@ -7479,7 +7725,12 @@ async function runVerify(opts) {
7479
7725
  const agentEntries = entries.filter((e) => !setup.has(e.path));
7480
7726
  const paths = entryPaths(agentEntries);
7481
7727
  const auto = loadConfig(repo).mode === "auto";
7482
- const tampering = await tamperingFindings(repo, base4, commit, agentEntries, { auto, repoRoot: repo, askedTests: auto ? testEditsAskedFor(contract.task, repo, void 0, base4).paths : [] });
7728
+ const baseFailing = () => {
7729
+ if (!contract.test_command?.trim()) return null;
7730
+ const run = baseRunFor({ repoRoot: repo, sessionId: "verify", session: readEvents(safeRelative(repo, opts.eventsPath ?? ".replume/events.jsonl")).events, command: contract.test_command.trim(), timeoutSec: opts.timeoutSecDefault ?? 120, runner: spawnRunner, record: false });
7731
+ return run?.ok ? run.failing : null;
7732
+ };
7733
+ const tampering = await tamperingFindings(repo, base4, commit, agentEntries, { auto, repoRoot: repo, askedTests: auto ? testEditsAskedFor(contract.task, repo, void 0, base4).paths : [], ...auto ? { baseFailing } : {} });
7483
7734
  const newTests = agentEntries.filter((e) => e.status === "A" && TEST_PATH.test(e.path) && !tampering.some((t) => t.path === e.path)).map((e) => e.path);
7484
7735
  const dependencies = await dependencySection(repo, base4, commit, paths, contract, opts);
7485
7736
  const eventsFile = safeRelative(repo, opts.eventsPath ?? ".replume/events.jsonl");
@@ -7819,9 +8070,9 @@ var AUTO_NOT_CHECKED_RULE = "auto_not_checked";
7819
8070
  var AUTO_SKIPPED_RULE = "auto_gate_skipped";
7820
8071
  var NO_CHANGES_SKIP = "no changes, gate skipped";
7821
8072
  var MAX_SUMMARY = 1200;
7822
- var ANSI4 = /\u001b\[[0-9;?]*[A-Za-z]/g;
8073
+ var ANSI5 = /\u001b\[[0-9;?]*[A-Za-z]/g;
7823
8074
  function summarizeFailure(output) {
7824
- const text = output.replace(ANSI4, "");
8075
+ const text = output.replace(ANSI5, "");
7825
8076
  const names3 = failingTestNames(text).slice(0, 5);
7826
8077
  const lines = text.split(/\r?\n/).map((l) => l.trimEnd()).filter((l) => l.trim());
7827
8078
  const interesting = lines.filter((l) => /\b(fail|error|expected|actual|assert|not ok|✗|✖)/i.test(l)).slice(-10);
@@ -7835,7 +8086,7 @@ function classify3(r) {
7835
8086
  const output = `${r.stdout}
7836
8087
  ${r.stderr}`;
7837
8088
  const tail = outputTailOf(r.stdout, r.stderr);
7838
- const run = classifyRun(r, output.replace(ANSI4, ""));
8089
+ const run = classifyRun(r, output.replace(ANSI5, ""));
7839
8090
  return tail ? { ...run, ...tail } : run;
7840
8091
  }
7841
8092
  function classifyRun(r, output) {
@@ -7909,7 +8160,7 @@ function autoStopGate(opts = {}) {
7909
8160
  let whyYours = run.status === "failed" && requiresWholeSuite(contract.task) ? "the task asks for a green suite" : null;
7910
8161
  if (run.status === "failed" && !requiresWholeSuite(contract.task)) {
7911
8162
  const output = `${r0.stdout}
7912
- ${r0.stderr}`.replace(ANSI4, "");
8163
+ ${r0.stderr}`.replace(ANSI5, "");
7913
8164
  const nowFailing = failingTestNames(output);
7914
8165
  const left = Number.isFinite(budget) ? budget - run.durationMs / 1e3 : c.config.auto.test_timeout_sec;
7915
8166
  if (!nowFailing.length) whyYours = "Replume could not read the failing test names from this run, so it cannot compare with the task start";
@@ -7929,13 +8180,13 @@ ${r0.stderr}`.replace(ANSI4, "");
7929
8180
  const verdict = preExistingOnly({ output, nowFailing, base: base4, task: contract.task, changedTestSources: sources2, touchesChangedCode: makeTouchesChangedCode(c.repoRoot, netChanged(c.repoRoot, sessionSourceWrites(c.session))) });
7930
8181
  if (!verdict.ok && base4.ok) whyYours = verdict.why;
7931
8182
  if (verdict.ok) {
7932
- const list4 = verdict.preExisting.slice(0, 10);
8183
+ const list5 = verdict.preExisting.slice(0, 10);
7933
8184
  return {
7934
8185
  rule: AUTO_GATE_RULE,
7935
8186
  outcome: "pass",
7936
8187
  message: null,
7937
8188
  summary: `${AUTO_GATE_RULE}: \`${command}\` fails only in ${verdict.preExisting.length} test(s) that were already failing before the task, not sent back`,
7938
- raw: { ...raw5, outcome: "passed", preExisting: list4, preExistingCount: verdict.preExisting.length },
8189
+ raw: { ...raw5, outcome: "passed", preExisting: list5, preExistingCount: verdict.preExisting.length },
7939
8190
  ran: { ...ran, ok: true, preExistingOnly: true }
7940
8191
  };
7941
8192
  }
@@ -8555,14 +8806,14 @@ function stripReplumeHooks(config, ours) {
8555
8806
  const isOurs = (h) => obj2(h) && [h.command, h.bash].some((c) => typeof c === "string" && ours.test(c.trim()));
8556
8807
  const hooks = {};
8557
8808
  const events = [];
8558
- for (const [event, list4] of Object.entries(config.hooks)) {
8559
- if (!Array.isArray(list4)) {
8560
- hooks[event] = list4;
8809
+ for (const [event, list5] of Object.entries(config.hooks)) {
8810
+ if (!Array.isArray(list5)) {
8811
+ hooks[event] = list5;
8561
8812
  continue;
8562
8813
  }
8563
8814
  let removed = false;
8564
8815
  const kept = [];
8565
- for (const item of list4) {
8816
+ for (const item of list5) {
8566
8817
  if (obj2(item) && Array.isArray(item.hooks)) {
8567
8818
  const inner = item.hooks.filter((h) => !isOurs(h));
8568
8819
  if (inner.length === item.hooks.length) {
@@ -8749,7 +9000,7 @@ import { Command, Option } from "commander";
8749
9000
  // package.json
8750
9001
  var package_default = {
8751
9002
  name: "replume-cli",
8752
- version: "0.2.3",
9003
+ version: "0.2.4",
8753
9004
  type: "module",
8754
9005
  bin: {
8755
9006
  replume: "dist/cli.js"
@@ -8798,7 +9049,7 @@ var package_default = {
8798
9049
 
8799
9050
  // src/cli.ts
8800
9051
  import { execFileSync as execFileSync34 } from "child_process";
8801
- import { existsSync as existsSync47, lstatSync as lstatSync6, mkdtempSync as mkdtempSync8, readFileSync as readFileSync52, unlinkSync } from "fs";
9052
+ import { existsSync as existsSync47, lstatSync as lstatSync6, mkdtempSync as mkdtempSync8, readFileSync as readFileSync51, unlinkSync } from "fs";
8802
9053
  import { homedir as homedir8, tmpdir as tmpdir16 } from "os";
8803
9054
  import { join as join59, resolve as resolve16 } from "path";
8804
9055
 
@@ -8837,9 +9088,6 @@ function runSessionStart(input) {
8837
9088
  writeStartSha(input.repoDir, input.sessionId, sha2);
8838
9089
  }
8839
9090
 
8840
- // src/hooks/session-end.ts
8841
- import { readFileSync as readFileSync26 } from "fs";
8842
-
8843
9091
  // src/commit-range.ts
8844
9092
  import { execFileSync as execFileSync20 } from "child_process";
8845
9093
  function resolveCommitRange(repoDir, startSha) {
@@ -8860,42 +9108,135 @@ function resolveCommitRange(repoDir, startSha) {
8860
9108
  return { commits, ok: true };
8861
9109
  }
8862
9110
 
9111
+ // src/core/transcript-read.ts
9112
+ import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
9113
+ var TRANSCRIPT_MAX_BYTES = 32 * 1024 * 1024;
9114
+ var TRANSCRIPT_MAX_LINES = 2e5;
9115
+ var TRANSCRIPT_MAX_LINE_BYTES = 4 * 1024 * 1024;
9116
+ var TRANSCRIPT_HEAD_BYTES = 256 * 1024;
9117
+ function transcriptEntries(text, limits = {}, cut = false) {
9118
+ const maxLines = limits.maxLines ?? TRANSCRIPT_MAX_LINES;
9119
+ const maxLine = limits.maxLineBytes ?? TRANSCRIPT_MAX_LINE_BYTES;
9120
+ const lines = text.split("\n");
9121
+ if (cut) lines.pop();
9122
+ const entries = [];
9123
+ let skipped = 0;
9124
+ let truncated = cut;
9125
+ for (let i = 0; i < lines.length; i++) {
9126
+ if (i >= maxLines) {
9127
+ truncated = true;
9128
+ break;
9129
+ }
9130
+ const line2 = lines[i].trim();
9131
+ if (!line2) continue;
9132
+ if (line2.length > maxLine) {
9133
+ skipped++;
9134
+ continue;
9135
+ }
9136
+ let v2;
9137
+ try {
9138
+ v2 = JSON.parse(line2);
9139
+ } catch {
9140
+ skipped++;
9141
+ continue;
9142
+ }
9143
+ if (v2 && typeof v2 === "object" && !Array.isArray(v2)) entries.push(v2);
9144
+ else skipped++;
9145
+ }
9146
+ return { entries, truncated, skipped };
9147
+ }
9148
+ function readTranscript(path, limits = {}) {
9149
+ if (typeof path !== "string" || !path.endsWith(".jsonl")) return null;
9150
+ const maxBytes = limits.maxBytes ?? TRANSCRIPT_MAX_BYTES;
9151
+ let fd;
9152
+ try {
9153
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
9154
+ } catch {
9155
+ return null;
9156
+ }
9157
+ try {
9158
+ const st = fstatSync(fd);
9159
+ if (!st.isFile()) return null;
9160
+ const size = Math.min(st.size, maxBytes);
9161
+ const buf = Buffer.alloc(size);
9162
+ let got = 0;
9163
+ while (got < size) {
9164
+ const n3 = readSync(fd, buf, got, size - got, got);
9165
+ if (n3 <= 0) break;
9166
+ got += n3;
9167
+ }
9168
+ return transcriptEntries(buf.subarray(0, got).toString("utf8"), limits, st.size > got);
9169
+ } catch {
9170
+ return null;
9171
+ } finally {
9172
+ closeSync(fd);
9173
+ }
9174
+ }
9175
+ function contentText(content) {
9176
+ if (typeof content === "string") return content;
9177
+ if (!Array.isArray(content)) return null;
9178
+ const texts = content.filter((c) => !!c && typeof c === "object" && c.type === "text" && typeof c.text === "string").map((c) => c.text);
9179
+ return texts.length ? texts.join("\n") : null;
9180
+ }
9181
+ var INJECTED = /^\s*(?:<command-name>|<command-message>|<local-command-|<system-reminder>|<task-notification>|\[Request interrupted)/;
9182
+ function claudeTypedPrompt(e) {
9183
+ if (e.type !== "user") return null;
9184
+ if (e.isMeta === true || e.isCompactSummary === true || e.isVisibleInTranscriptOnly === true || e.isSidechain === true) return null;
9185
+ if (e.promptSource === "system" || e.turnOrigin === "task_notification") return null;
9186
+ const origin = e.origin;
9187
+ if (origin && typeof origin === "object" && origin.kind !== void 0 && origin.kind !== "human") return null;
9188
+ const msg = e.message;
9189
+ if (!msg || typeof msg !== "object" || msg.role !== void 0 && msg.role !== "user") return null;
9190
+ const text = contentText(msg.content);
9191
+ if (!text || !text.trim() || INJECTED.test(text)) return null;
9192
+ return text;
9193
+ }
9194
+ function cursorUserQuery(e) {
9195
+ if (e.role !== "user") return null;
9196
+ const text = contentText(e.message?.content);
9197
+ if (!text) return null;
9198
+ const q = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(text)?.[1] ?? text.replace(/<timestamp>[\s\S]*?<\/timestamp>/g, "").trim();
9199
+ return q || null;
9200
+ }
9201
+ var MAX_PROMPT_CHARS = 4e3;
9202
+ function firstPromptIn(path, pick3) {
9203
+ const t = readTranscript(path, { maxBytes: TRANSCRIPT_HEAD_BYTES });
9204
+ if (!t) return null;
9205
+ for (const e of t.entries) {
9206
+ const p = pick3(e);
9207
+ if (p) return p.trim().slice(0, MAX_PROMPT_CHARS);
9208
+ }
9209
+ return null;
9210
+ }
9211
+
8863
9212
  // src/transcript-parser.ts
8864
- function parseTranscript(jsonlContent) {
9213
+ function parseTranscriptFile(path) {
9214
+ const t = readTranscript(path);
9215
+ return t ? parseEntries(t) : null;
9216
+ }
9217
+ function parseEntries(t) {
8865
9218
  const userTexts = [];
8866
9219
  const toolCalls = [];
8867
- for (const line2 of jsonlContent.split("\n")) {
8868
- const trimmed = line2.trim();
8869
- if (!trimmed) continue;
8870
- let parsed;
8871
- try {
8872
- parsed = JSON.parse(trimmed);
8873
- } catch {
9220
+ for (const e of t.entries) {
9221
+ if (e.type === "user") {
9222
+ const text = claudeTypedPrompt(e);
9223
+ if (text) userTexts.push(text);
8874
9224
  continue;
8875
9225
  }
8876
- if (parsed.type !== "user" && parsed.type !== "assistant") continue;
8877
- const content = parsed.message?.content;
9226
+ if (e.type !== "assistant") continue;
9227
+ const content = e.message?.content;
8878
9228
  if (!Array.isArray(content)) continue;
8879
- if (parsed.type === "user") {
8880
- for (const block of content) {
8881
- if (block.type === "text" && typeof block.text === "string") {
8882
- userTexts.push(block.text);
8883
- }
8884
- }
8885
- }
8886
- if (parsed.type === "assistant") {
8887
- for (const block of content) {
8888
- if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string" && typeof block.input === "object" && block.input !== null) {
8889
- toolCalls.push({
8890
- id: block.id,
8891
- name: block.name,
8892
- input: block.input
8893
- });
8894
- }
9229
+ for (const block of content) {
9230
+ if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string" && typeof block.input === "object" && block.input !== null) {
9231
+ toolCalls.push({
9232
+ id: block.id,
9233
+ name: block.name,
9234
+ input: block.input
9235
+ });
8895
9236
  }
8896
9237
  }
8897
9238
  }
8898
- return { prompt: userTexts.join("\n\n"), toolCalls };
9239
+ return { prompt: userTexts.join("\n\n"), toolCalls, truncated: t.truncated };
8899
9240
  }
8900
9241
 
8901
9242
  // src/git-notes.ts
@@ -8932,8 +9273,7 @@ function runSessionEnd(input) {
8932
9273
  clearStartSha(input.repoDir, input.sessionId);
8933
9274
  return;
8934
9275
  }
8935
- const transcriptRaw = readFileSync26(input.transcriptPath, "utf-8");
8936
- const { prompt: prompt2, toolCalls } = parseTranscript(transcriptRaw);
9276
+ const { prompt: prompt2, toolCalls } = parseTranscriptFile(input.transcriptPath) ?? { prompt: "", toolCalls: [] };
8937
9277
  const now = (/* @__PURE__ */ new Date()).toISOString();
8938
9278
  for (const sha2 of commits) {
8939
9279
  const note = {
@@ -8958,7 +9298,7 @@ function runSessionEnd(input) {
8958
9298
  }
8959
9299
 
8960
9300
  // src/install.ts
8961
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync27, writeFileSync as writeFileSync13 } from "fs";
9301
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "fs";
8962
9302
  import { join as join32 } from "path";
8963
9303
 
8964
9304
  // templates/workflow.yml.ts
@@ -9003,7 +9343,7 @@ function writeSettingsHooks(claudeSettingsPath2) {
9003
9343
  mkdirSync12(join32(claudeSettingsPath2, ".."), { recursive: true });
9004
9344
  let settings = {};
9005
9345
  if (existsSync23(claudeSettingsPath2)) {
9006
- settings = JSON.parse(readFileSync27(claudeSettingsPath2, "utf-8"));
9346
+ settings = JSON.parse(readFileSync26(claudeSettingsPath2, "utf-8"));
9007
9347
  }
9008
9348
  settings.hooks = {
9009
9349
  ...settings.hooks,
@@ -9294,7 +9634,7 @@ async function runVerify2(opts) {
9294
9634
  }
9295
9635
 
9296
9636
  // src/supervisor/review-at-stop.ts
9297
- import { readFileSync as readFileSync33 } from "fs";
9637
+ import { readFileSync as readFileSync32 } from "fs";
9298
9638
  import { join as join39 } from "path";
9299
9639
 
9300
9640
  // src/core/removed-tests.ts
@@ -9396,7 +9736,7 @@ var confirmHermesEdits = (root, sid) => confirmEdits(root, sid, "hermes");
9396
9736
 
9397
9737
  // src/core/auto-report.ts
9398
9738
  import { execFileSync as execFileSync25 } from "child_process";
9399
- import { existsSync as existsSync27, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync6, readFileSync as readFileSync32, rmSync as rmSync11, writeFileSync as writeFileSync17 } from "fs";
9739
+ import { existsSync as existsSync27, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync6, readFileSync as readFileSync31, rmSync as rmSync11, writeFileSync as writeFileSync17 } from "fs";
9400
9740
  import { tmpdir as tmpdir11 } from "os";
9401
9741
  import { join as join38 } from "path";
9402
9742
 
@@ -9647,7 +9987,7 @@ function renderSessionReport(ledgerEvents, extras = {}) {
9647
9987
  }
9648
9988
 
9649
9989
  // src/core/time-tokens.ts
9650
- import { existsSync as existsSync25, readdirSync as readdirSync7, readFileSync as readFileSync28, statSync as statSync7 } from "fs";
9990
+ import { existsSync as existsSync25, readdirSync as readdirSync7, readFileSync as readFileSync27, statSync as statSync7 } from "fs";
9651
9991
  import { homedir as homedir3 } from "os";
9652
9992
  import { join as join34 } from "path";
9653
9993
  var AGENT_STEPS = /* @__PURE__ */ new Set(["prompt", "tool_pre", "tool_post", "file_edit", "command", "stop"]);
@@ -9720,7 +10060,7 @@ function codexTokens(sessionId2, codexHome) {
9720
10060
  if (!file3) return null;
9721
10061
  try {
9722
10062
  if (statSync7(file3).size > 5e7) return null;
9723
- const counts = codexUsage(readFileSync28(file3, "utf8"));
10063
+ const counts = codexUsage(readFileSync27(file3, "utf8"));
9724
10064
  return counts ? { counts, file: file3 } : null;
9725
10065
  } catch {
9726
10066
  return null;
@@ -9781,7 +10121,7 @@ ${reportTest.stderrTail}` : details.outputTail;
9781
10121
  import { spawn as spawn4 } from "child_process";
9782
10122
  import { execFileSync as execFileSync23 } from "child_process";
9783
10123
  import { createHash as createHash4 } from "crypto";
9784
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync29, writeFileSync as writeFileSync14 } from "fs";
10124
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync28, writeFileSync as writeFileSync14 } from "fs";
9785
10125
  import { tmpdir as tmpdir9 } from "os";
9786
10126
  import { join as join35 } from "path";
9787
10127
 
@@ -10700,7 +11040,7 @@ function fullDiff(repoRoot2, maxChars = 6e4) {
10700
11040
  let out = git10(["diff", "HEAD", "--", ".", ":(exclude).replume", ":(exclude).claude", ":(exclude).cursor", ":(exclude).codex"]);
10701
11041
  for (const f of git10(["ls-files", "--others", "--exclude-standard"]).split("\n").filter((f2) => f2 && !/^\.(?:replume|claude|cursor|codex)\//.test(f2)).slice(0, 20)) {
10702
11042
  try {
10703
- const text = readFileSync29(join35(repoRoot2, f), "utf8");
11043
+ const text = readFileSync28(join35(repoRoot2, f), "utf8");
10704
11044
  out += `
10705
11045
  ${diffText("", text.slice(0, 2e4), f).replace("@@ -1,0 ", "@@ -0,0 ")}`;
10706
11046
  } catch {
@@ -10757,7 +11097,7 @@ function modelJudgeExtras(events, fresh) {
10757
11097
  // src/supervisor/unattributed.ts
10758
11098
  import { execFileSync as execFileSync24 } from "child_process";
10759
11099
  import { createHash as createHash5 } from "crypto";
10760
- import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as writeFileSync15 } from "fs";
11100
+ import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync29, rmSync as rmSync9, writeFileSync as writeFileSync15 } from "fs";
10761
11101
  import { tmpdir as tmpdir10 } from "os";
10762
11102
  import { join as join36 } from "path";
10763
11103
  var UNATTRIBUTED_RULE = "changed_outside_tools";
@@ -10882,7 +11222,7 @@ function contractTampered(repoRoot2, now = /* @__PURE__ */ new Date()) {
10882
11222
  if (b.status !== "ok") return null;
10883
11223
  let current = null;
10884
11224
  try {
10885
- current = readFileSync30(path);
11225
+ current = readFileSync29(path);
10886
11226
  } catch {
10887
11227
  current = null;
10888
11228
  }
@@ -10899,7 +11239,7 @@ function contractTampered(repoRoot2, now = /* @__PURE__ */ new Date()) {
10899
11239
  }
10900
11240
  const copy = currentContractCopy(path);
10901
11241
  try {
10902
- const saved = readFileSync30(copy);
11242
+ const saved = readFileSync29(copy);
10903
11243
  if (hash2(saved) === b.baseline.policy_hash && (current === null || savedTo !== null)) {
10904
11244
  writeFileSync15(path, saved);
10905
11245
  return { restored: true, savedTo };
@@ -10928,9 +11268,9 @@ function unattributedCheck() {
10928
11268
  const prot = found.filter((f) => f.protectedBy);
10929
11269
  const outsideFingerprint = createHash5("sha256").update(found.map((f) => `${f.path}:${f.blob}`).join("\n")).digest("hex").slice(0, 16);
10930
11270
  const raw5 = { files: found.map((f) => ({ path: f.path, protectedBy: f.protectedBy })), base: base4, outsideFingerprint };
10931
- const list4 = found.map((f) => f.path).join(", ");
11271
+ const list5 = found.map((f) => f.path).join(", ");
10932
11272
  if (!prot.length || !auto) {
10933
- return { rule: UNATTRIBUTED_RULE, outcome: "pass", message: null, summary: `${UNATTRIBUTED_RULE}: ${list4} changed outside captured tools`, raw: { ...raw5, outcome: "listed" }, outsideFingerprint };
11273
+ return { rule: UNATTRIBUTED_RULE, outcome: "pass", message: null, summary: `${UNATTRIBUTED_RULE}: ${list5} changed outside captured tools`, raw: { ...raw5, outcome: "listed" }, outsideFingerprint };
10934
11274
  }
10935
11275
  const max = c.config.auto.max_retries;
10936
11276
  const sofar = c.session.filter(isBlock).length;
@@ -10946,7 +11286,7 @@ function unattributedCheck() {
10946
11286
 
10947
11287
  // src/core/project-memory.ts
10948
11288
  import { createHash as createHash6 } from "crypto";
10949
- import { appendFileSync as appendFileSync7, existsSync as existsSync26, lstatSync as lstatSync3, mkdirSync as mkdirSync15, readFileSync as readFileSync31, readdirSync as readdirSync8, rmSync as rmSync10, writeFileSync as writeFileSync16 } from "fs";
11289
+ import { appendFileSync as appendFileSync7, existsSync as existsSync26, lstatSync as lstatSync3, mkdirSync as mkdirSync15, readFileSync as readFileSync30, readdirSync as readdirSync8, rmSync as rmSync10, writeFileSync as writeFileSync16 } from "fs";
10950
11290
  import { join as join37 } from "path";
10951
11291
  var dirOf = (root) => join37(root, ".replume", "memory");
10952
11292
  var clip2 = (value, max, root) => redactLedgerText(value.replace(/\s+/g, " ").trim(), root).slice(0, max);
@@ -10957,7 +11297,7 @@ function ensureIgnored(root) {
10957
11297
  writeFileSync16(gi, DEFAULT_REPLUME_GITIGNORE + "memory/\n");
10958
11298
  return;
10959
11299
  }
10960
- const current = readFileSync31(gi, "utf8");
11300
+ const current = readFileSync30(gi, "utf8");
10961
11301
  if (!current.split(/\r?\n/).some((line2) => line2.trim() === "memory/"))
10962
11302
  appendFileSync7(gi, `${current.endsWith("\n") || !current ? "" : "\n"}memory/
10963
11303
  `);
@@ -11012,7 +11352,7 @@ function listProjectMemory(root) {
11012
11352
  const path = join37(dir2, name);
11013
11353
  const st = lstatSync3(path);
11014
11354
  if (!st.isFile() || st.size > 8192) continue;
11015
- const e = JSON.parse(readFileSync31(path, "utf8"));
11355
+ const e = JSON.parse(readFileSync30(path, "utf8"));
11016
11356
  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")
11017
11357
  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) });
11018
11358
  } catch {
@@ -11122,12 +11462,12 @@ function refreshReportTime(repoRoot2, sessionId2) {
11122
11462
  const lines = timeAndTokensLines(repoRoot2, sessionId2, agentOfSession(repoRoot2, sessionId2), sessionEvents(repoRoot2, sessionId2));
11123
11463
  const esc = (t) => t.replace(/([\\`*_[])/g, "\\$1");
11124
11464
  const htmlEsc = (t) => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11125
- const md2 = readFileSync32(mdPath, "utf8");
11465
+ const md2 = readFileSync31(mdPath, "utf8");
11126
11466
  const mdNew = md2.replace(/(## Time and tokens\n\n)((?:- .*\n?)+)/, (_m, head) => `${head}${lines.map((l) => `- ${esc(l)}`).join("\n")}
11127
11467
  `);
11128
11468
  if (mdNew !== md2) writeFileSync17(mdPath, mdNew);
11129
11469
  if (existsSync27(htmlPath)) {
11130
- const html2 = readFileSync32(htmlPath, "utf8");
11470
+ const html2 = readFileSync31(htmlPath, "utf8");
11131
11471
  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}`);
11132
11472
  if (htmlNew !== html2) writeFileSync17(htmlPath, htmlNew);
11133
11473
  }
@@ -11546,7 +11886,7 @@ function reviewAtStop(i) {
11546
11886
  ...testEditsAskedFor(i.contract.task, i.repoRoot, void 0, base4).paths,
11547
11887
  ...changedFiles.filter((p) => {
11548
11888
  try {
11549
- return isConfirmed(i.repoRoot, p, readFileSync33(join39(i.repoRoot, p), "utf8"));
11889
+ return isConfirmed(i.repoRoot, p, readFileSync32(join39(i.repoRoot, p), "utf8"));
11550
11890
  } catch {
11551
11891
  return false;
11552
11892
  }
@@ -11574,11 +11914,11 @@ import { homedir as homedir4 } from "os";
11574
11914
  import { execFileSync as execFileSync27 } from "child_process";
11575
11915
  import { existsSync as existsSync30 } from "fs";
11576
11916
  import { join as join41 } from "path";
11577
- import { readFileSync as readFileSync36 } from "fs";
11917
+ import { readFileSync as readFileSync35 } from "fs";
11578
11918
 
11579
11919
  // src/core/draft.ts
11580
11920
  import { execFileSync as execFileSync26 } from "child_process";
11581
- import { existsSync as existsSync28, lstatSync as lstatSync4, readFileSync as readFileSync34 } from "fs";
11921
+ import { existsSync as existsSync28, lstatSync as lstatSync4, readFileSync as readFileSync33 } from "fs";
11582
11922
  import { join as join40, posix as posix2 } from "path";
11583
11923
  var LOCKFILES = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "poetry.lock", "Pipfile.lock", "uv.lock", "Cargo.lock", "go.sum", "Gemfile.lock", "composer.lock"];
11584
11924
  var CI_CONFIG = [
@@ -11604,7 +11944,7 @@ function trackedFiles(repoRoot2) {
11604
11944
  }
11605
11945
  var read3 = (repoRoot2, rel3) => {
11606
11946
  try {
11607
- return readFileSync34(join40(repoRoot2, rel3), "utf-8");
11947
+ return readFileSync33(join40(repoRoot2, rel3), "utf-8");
11608
11948
  } catch {
11609
11949
  return "";
11610
11950
  }
@@ -11643,8 +11983,8 @@ function draftContract(input) {
11643
11983
  const notes = [];
11644
11984
  const allow = [];
11645
11985
  const protect = [];
11646
- const add2 = (list4, glob, note) => {
11647
- if (!list4.includes(glob)) list4.push(glob);
11986
+ const add2 = (list5, glob, note) => {
11987
+ if (!list5.includes(glob)) list5.push(glob);
11648
11988
  notes.push(note);
11649
11989
  };
11650
11990
  const t = detectTestCommand(input.repoRoot, files);
@@ -11653,7 +11993,7 @@ function draftContract(input) {
11653
11993
  const path = join40(input.repoRoot, f);
11654
11994
  try {
11655
11995
  if (!lstatSync4(path).isFile() || lstatSync4(path).isSymbolicLink()) return [];
11656
- const s = parseScenario(readFileSync34(path, "utf8"), f);
11996
+ const s = parseScenario(readFileSync33(path, "utf8"), f);
11657
11997
  return [s.id === posix2.basename(f).replace(/\.ya?ml$/, "") ? s : null].filter((x) => x !== null);
11658
11998
  } catch {
11659
11999
  notes.push(`must_check: skipped invalid scenario ${f}; fix it before verification`);
@@ -11750,7 +12090,7 @@ function allowHasNoSource(allow) {
11750
12090
  var ALLOW_NO_SOURCE_WARNING = "allow contains no source files; the agent may have no allowed way to finish this task. Name the files in --task or edit allow.";
11751
12091
 
11752
12092
  // src/core/mode.ts
11753
- import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync35, writeFileSync as writeFileSync18 } from "fs";
12093
+ import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync34, writeFileSync as writeFileSync18 } from "fs";
11754
12094
  import { dirname as dirname11 } from "path";
11755
12095
  function readMode(repoRoot2) {
11756
12096
  return loadConfig(repoRoot2).mode;
@@ -11759,7 +12099,7 @@ function writeMode(repoRoot2, mode) {
11759
12099
  const path = configPath(repoRoot2);
11760
12100
  mkdirSync17(dirname11(path), { recursive: true });
11761
12101
  const line2 = `mode: ${mode}`;
11762
- const current = existsSync29(path) ? readFileSync35(path, "utf8") : "";
12102
+ const current = existsSync29(path) ? readFileSync34(path, "utf8") : "";
11763
12103
  if (/^mode:.*$/m.test(current)) writeFileSync18(path, current.replace(/^mode:.*$/m, line2));
11764
12104
  else writeFileSync18(path, `${line2}
11765
12105
  ${current}`);
@@ -11963,7 +12303,7 @@ function failingProtectedTests(repoRoot2, protect, output) {
11963
12303
  if (output.includes(t) || output.includes(join41(repoRoot2, t))) return true;
11964
12304
  let text = "";
11965
12305
  try {
11966
- text = readFileSync36(join41(repoRoot2, t), "utf8");
12306
+ text = readFileSync35(join41(repoRoot2, t), "utf8");
11967
12307
  } catch {
11968
12308
  return false;
11969
12309
  }
@@ -12030,7 +12370,7 @@ function modifiedNow(repoRoot2) {
12030
12370
  }
12031
12371
 
12032
12372
  // src/agents/claude/install.ts
12033
- import { copyFileSync as copyFileSync5, existsSync as existsSync31, mkdirSync as mkdirSync18, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "fs";
12373
+ import { copyFileSync as copyFileSync5, existsSync as existsSync31, mkdirSync as mkdirSync18, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "fs";
12034
12374
  import { dirname as dirname12, join as join42 } from "path";
12035
12375
  var DEFAULT_HOOK_COMMAND = "replume hook claude";
12036
12376
  var HOOK_EVENTS = ["PreToolUse", "PostToolUse", "PostToolUseFailure", "UserPromptSubmit", "Stop", "SessionEnd"];
@@ -12073,7 +12413,7 @@ function installClaudeHooks(repoRoot2, opts = {}) {
12073
12413
  let current = {};
12074
12414
  let original = null;
12075
12415
  if (existsSync31(settingsPath)) {
12076
- original = readFileSync37(settingsPath, "utf-8");
12416
+ original = readFileSync36(settingsPath, "utf-8");
12077
12417
  try {
12078
12418
  const parsed = original.trim() ? JSON.parse(original) : {};
12079
12419
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("not a JSON object");
@@ -12097,7 +12437,7 @@ function installClaudeHooks(repoRoot2, opts = {}) {
12097
12437
  }
12098
12438
  function replumeStopTimeoutSec(repoRoot2) {
12099
12439
  try {
12100
- const settings = JSON.parse(readFileSync37(claudeSettingsPath(repoRoot2), "utf-8"));
12440
+ const settings = JSON.parse(readFileSync36(claudeSettingsPath(repoRoot2), "utf-8"));
12101
12441
  for (const g of settings.hooks?.Stop ?? []) for (const h of g.hooks ?? []) if (isReplume2(h, DEFAULT_HOOK_COMMAND) && typeof h.timeout === "number") return h.timeout;
12102
12442
  } catch {
12103
12443
  }
@@ -12107,7 +12447,7 @@ function replumeStopTimeoutSec(repoRoot2) {
12107
12447
  // src/core/shell-observe.ts
12108
12448
  import { execFileSync as execFileSync28 } from "child_process";
12109
12449
  import { createHash as createHash7 } from "crypto";
12110
- import { existsSync as existsSync32, lstatSync as lstatSync5, mkdirSync as mkdirSync19, readFileSync as readFileSync38, rmSync as rmSync12, writeFileSync as writeFileSync20 } from "fs";
12450
+ import { existsSync as existsSync32, lstatSync as lstatSync5, mkdirSync as mkdirSync19, readFileSync as readFileSync37, rmSync as rmSync12, writeFileSync as writeFileSync20 } from "fs";
12111
12451
  import { tmpdir as tmpdir12 } from "os";
12112
12452
  import { join as join43, resolve as resolve6 } from "path";
12113
12453
  var MAX_TRACKED = 5e3;
@@ -12163,7 +12503,7 @@ function treeState(repoRoot2) {
12163
12503
  return null;
12164
12504
  }
12165
12505
  }
12166
- function changedSince(repoRoot2, before) {
12506
+ function changedSince2(repoRoot2, before) {
12167
12507
  try {
12168
12508
  const now = changedNow(repoRoot2);
12169
12509
  if (!now) return [];
@@ -12210,14 +12550,14 @@ function takeTreeChanges(repoRoot2, sessionId2, toolUseId) {
12210
12550
  let state;
12211
12551
  try {
12212
12552
  if (!existsSync32(f)) return null;
12213
- state = JSON.parse(readFileSync38(f, "utf-8"));
12553
+ state = JSON.parse(readFileSync37(f, "utf-8"));
12214
12554
  } catch {
12215
12555
  return null;
12216
12556
  } finally {
12217
12557
  rmSync12(f, { force: true });
12218
12558
  }
12219
12559
  if (!state || typeof state.files !== "object" || state.files === null) return null;
12220
- return changedSince(repoRoot2, state);
12560
+ return changedSince2(repoRoot2, state);
12221
12561
  }
12222
12562
  function forgetTree(repoRoot2, sessionId2, toolUseId) {
12223
12563
  if (toolUseId) rmSync12(stateFile(repoRoot2, sessionId2, toolUseId), { force: true });
@@ -12278,14 +12618,14 @@ async function supervise(input, opts) {
12278
12618
  }
12279
12619
 
12280
12620
  // src/supervisor/done.ts
12281
- import { existsSync as existsSync33, readFileSync as readFileSync39 } from "fs";
12621
+ import { existsSync as existsSync33, readFileSync as readFileSync38 } from "fs";
12282
12622
  import { join as join44 } from "path";
12283
12623
  var DONE_RULE = "done_unmet";
12284
12624
  var DEFAULT_MAX_STOP_BLOCKS = 2;
12285
12625
  var LIST_OPS2 = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
12286
12626
  var REDIRECT_OPS2 = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>", ">&", "<", "<<", "<<<", "<&"]);
12287
12627
  var HELP_ONLY = /* @__PURE__ */ new Set(["--help", "-h", "--version"]);
12288
- var CWD_COMMANDS = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
12628
+ var CWD_COMMANDS2 = /* @__PURE__ */ new Set(["cd", "pushd", "popd"]);
12289
12629
  var ENV_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=/;
12290
12630
  function stripEnv(words6) {
12291
12631
  let w = words6;
@@ -12303,7 +12643,7 @@ function runBinding(ran, wanted) {
12303
12643
  const helpish = (a) => HELP_ONLY.has(a) || a === "--";
12304
12644
  const isHelp = extra.length > 0 && extra.every(helpish) && extra.some((a) => HELP_ONLY.has(a));
12305
12645
  if (extra.length > 0 && !isHelp) return;
12306
- if (segs.slice(0, i).some((earlier) => CWD_COMMANDS.has(earlier[0] ?? ""))) return;
12646
+ if (segs.slice(0, i).some((earlier) => CWD_COMMANDS2.has(earlier[0] ?? ""))) return;
12307
12647
  hit = i;
12308
12648
  help = isHelp;
12309
12649
  });
@@ -12356,7 +12696,7 @@ function doneStatus(contract, sessionEvents2, repoRoot2) {
12356
12696
  const file3 = join44(".replume", "scenarios", `${d.scenario}.yaml`);
12357
12697
  let run = null;
12358
12698
  try {
12359
- if (existsSync33(join44(repoRoot2, file3))) run = parseScenario(readFileSync39(join44(repoRoot2, file3), "utf-8"), file3).run;
12699
+ if (existsSync33(join44(repoRoot2, file3))) run = parseScenario(readFileSync38(join44(repoRoot2, file3), "utf-8"), file3).run;
12360
12700
  } catch {
12361
12701
  run = null;
12362
12702
  }
@@ -12376,7 +12716,7 @@ function doneMessage(s, allow = [], mode = "approve") {
12376
12716
 
12377
12717
  // src/supervisor/wire.ts
12378
12718
  import { createHash as createHash8 } from "crypto";
12379
- import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync40, rmSync as rmSync13, statSync as statSync8, writeFileSync as writeFileSync21 } from "fs";
12719
+ import { existsSync as existsSync34, mkdirSync as mkdirSync20, readFileSync as readFileSync39, rmSync as rmSync13, statSync as statSync8, writeFileSync as writeFileSync21 } from "fs";
12380
12720
  import { tmpdir as tmpdir13 } from "os";
12381
12721
  import { dirname as dirname13, join as join45, resolve as resolve7 } from "path";
12382
12722
  var STEER_RULE_PREFIX = "supervisor_";
@@ -12387,7 +12727,7 @@ var stashFile = (repoRoot2, sessionId2, rel3, toolUseId) => join45(stashDir(repo
12387
12727
  function readText(abs) {
12388
12728
  try {
12389
12729
  if (!existsSync34(abs) || statSync8(abs).size > MAX_FILE_BYTES) return null;
12390
- return readFileSync40(abs, "utf-8");
12730
+ return readFileSync39(abs, "utf-8");
12391
12731
  } catch {
12392
12732
  return null;
12393
12733
  }
@@ -12408,7 +12748,7 @@ function takeBefore(repoRoot2, sessionId2, rel3, toolUseId) {
12408
12748
  const f = stashFile(repoRoot2, sessionId2, rel3, toolUseId);
12409
12749
  try {
12410
12750
  if (!existsSync34(f)) return null;
12411
- const v2 = JSON.parse(readFileSync40(f, "utf-8"));
12751
+ const v2 = JSON.parse(readFileSync39(f, "utf-8"));
12412
12752
  rmSync13(f, { force: true });
12413
12753
  return typeof v2.text === "string" ? v2.text : null;
12414
12754
  } catch {
@@ -12567,7 +12907,7 @@ function ranToEvent(ran, session) {
12567
12907
  }
12568
12908
 
12569
12909
  // src/agents/claude/payload.ts
12570
- import { existsSync as existsSync35, readFileSync as readFileSync41 } from "fs";
12910
+ import { existsSync as existsSync35, readFileSync as readFileSync40 } from "fs";
12571
12911
  import { isAbsolute as isAbsolute5, resolve as resolve8 } from "path";
12572
12912
  function parsePayload(text) {
12573
12913
  const x = JSON.parse(text);
@@ -12580,7 +12920,7 @@ function parsePayload(text) {
12580
12920
  var str2 = (v2) => typeof v2 === "string" ? v2 : "";
12581
12921
  function readIfExists(path) {
12582
12922
  try {
12583
- return existsSync35(path) ? readFileSync41(path, "utf-8") : "";
12923
+ return existsSync35(path) ? readFileSync40(path, "utf-8") : "";
12584
12924
  } catch {
12585
12925
  return "";
12586
12926
  }
@@ -12763,7 +13103,16 @@ function decide2(p, repoRoot2, proposal, sessionEvents2, env, deps) {
12763
13103
  const contract = loaded && !isDraft(loaded) ? effectivePolicy(loaded) : null;
12764
13104
  const layer2 = layer2ConfigFromEnv(env);
12765
13105
  const judge = deps.judge !== void 0 ? layer2.mode === "off" ? null : deps.judge : judgeFromConfig(layer2);
12766
- const input = { anchor: taskAnchor(contract, sessionEvents2), recent: sessionEvents2, proposal, repoRoot: repoRoot2, scratchDirs: p.scratchpad_dir ? [p.scratchpad_dir] : [], mode: loadConfig(repoRoot2).mode, legacyClaim: !loadConfig(repoRoot2).supervisor.checks };
13106
+ const input = {
13107
+ anchor: taskAnchor(contract, sessionEvents2),
13108
+ recent: sessionEvents2,
13109
+ proposal,
13110
+ repoRoot: repoRoot2,
13111
+ scratchDirs: p.scratchpad_dir ? [p.scratchpad_dir] : [],
13112
+ mode: loadConfig(repoRoot2).mode,
13113
+ legacyClaim: !loadConfig(repoRoot2).supervisor.checks,
13114
+ baseFailing: baseFailingFor({ repoRoot: repoRoot2, session: sessionEvents2, command: contract?.test_command, timeoutSec: loadConfig(repoRoot2).auto.test_timeout_sec, runner: deps.testRunner })
13115
+ };
12767
13116
  const ledger = ledgerPath(repoRoot2);
12768
13117
  const slot = {
12769
13118
  reserve: () => {
@@ -12795,6 +13144,17 @@ function wireContext(p, repoRoot2) {
12795
13144
  }
12796
13145
  return { repoRoot: repoRoot2, sessionId: p.session_id, agent: "claude-code", config: loadConfig(repoRoot2), contract };
12797
13146
  }
13147
+ function contractFromTranscript(p, repoRoot2, session, hook2) {
13148
+ if (!p.transcript_path || session.some((e) => e.type === "prompt")) return false;
13149
+ try {
13150
+ if (loadConfig(repoRoot2).mode !== "auto") return false;
13151
+ const prompt2 = firstPromptIn(p.transcript_path, claudeTypedPrompt);
13152
+ if (!prompt2) return false;
13153
+ return ensureAutoContractLate({ repoRoot: repoRoot2, sessionId: p.session_id, agent: "claude-code", hook: hook2, fallbackPrompt: () => prompt2 }) === "created";
13154
+ } catch {
13155
+ return false;
13156
+ }
13157
+ }
12798
13158
  function preToolUse(p, repoRoot2, env, deps) {
12799
13159
  const ledger = ledgerPath(repoRoot2);
12800
13160
  const action = toProposedAction(p, repoRoot2);
@@ -12802,7 +13162,8 @@ function preToolUse(p, repoRoot2, env, deps) {
12802
13162
  if (loadConfig(repoRoot2).supervisor.checks) stashBefore(repoRoot2, p.session_id, action.paths, p.tool_use_id);
12803
13163
  }
12804
13164
  const comparing = needsTreeCompare(action) && rememberTree(repoRoot2, p.session_id, p.tool_use_id);
12805
- const session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
13165
+ let session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
13166
+ if (contractFromTranscript(p, repoRoot2, session, "PreToolUse")) session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
12806
13167
  return andThen(decide2(p, repoRoot2, { kind: "action", action }, session, env, deps), ({ result: result2, contract }) => {
12807
13168
  const deciding = result2.findings.find((f) => f.decision === result2.decision);
12808
13169
  if (comparing && result2.decision === "block") forgetTree(repoRoot2, p.session_id, p.tool_use_id);
@@ -12921,6 +13282,7 @@ function stop(p, repoRoot2, env, deps) {
12921
13282
  const startedAtMs = Date.now();
12922
13283
  const ledger = ledgerPath(repoRoot2);
12923
13284
  let session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
13285
+ if (contractFromTranscript(p, repoRoot2, session, "Stop")) session = readEvents(ledger).events.filter((e) => e.sessionId === p.session_id);
12924
13286
  const lastAssistantMessage = typeof p.last_assistant_message === "string" ? p.last_assistant_message : "";
12925
13287
  const stopHookActive = p.stop_hook_active === true;
12926
13288
  return Promise.resolve(decide2(p, repoRoot2, { kind: "stop", lastAssistantMessage, stopHookActive }, session, env, deps)).then(async ({ result: result2, contract }) => {
@@ -13230,7 +13592,6 @@ function handleClaudeHook(stdinText, env = process.env, deps = {}) {
13230
13592
  }
13231
13593
 
13232
13594
  // src/agents/cursor/handler.ts
13233
- import { closeSync, openSync, readSync } from "fs";
13234
13595
  import { isAbsolute as isAbsolute8, relative as relative8, resolve as resolve9, sep as sep5 } from "path";
13235
13596
 
13236
13597
  // src/agents/cursor/adapter.ts
@@ -13526,7 +13887,7 @@ function cursorPayloadToAction(raw5, repoRoot2, fallback = {}) {
13526
13887
 
13527
13888
  // src/agents/codex/auto.ts
13528
13889
  import { spawn as spawn6 } from "child_process";
13529
- import { existsSync as existsSync36, readFileSync as readFileSync42 } from "fs";
13890
+ import { existsSync as existsSync36, readFileSync as readFileSync41 } from "fs";
13530
13891
  import { join as join48 } from "path";
13531
13892
  function policy(repoRoot2) {
13532
13893
  try {
@@ -13721,7 +14082,7 @@ function autoSessionEnd(i) {
13721
14082
 
13722
14083
  // src/agents/cursor/install.ts
13723
14084
  import { createHash as createHash9 } from "crypto";
13724
- import { existsSync as existsSync37, mkdirSync as mkdirSync22, readFileSync as readFileSync43, renameSync, rmSync as rmSync14, writeFileSync as writeFileSync22 } from "fs";
14085
+ import { existsSync as existsSync37, mkdirSync as mkdirSync22, readFileSync as readFileSync42, renameSync, rmSync as rmSync14, writeFileSync as writeFileSync22 } from "fs";
13725
14086
  import { dirname as dirname14, join as join49 } from "path";
13726
14087
  var CURSOR_HOOK_COMMAND = "replume hook cursor";
13727
14088
  var CURSOR_HOOK_EVENTS = [
@@ -13769,7 +14130,7 @@ function exactInstalledEntry(x, command, event) {
13769
14130
  function validateCursorHooksConfig(root, opts = {}) {
13770
14131
  const path = cursorHooksPath(root);
13771
14132
  const command = opts.command ?? CURSOR_HOOK_COMMAND;
13772
- const config = existsSync37(path) ? parseConfig2(readFileSync43(path, "utf8"), path) : {};
14133
+ const config = existsSync37(path) ? parseConfig2(readFileSync42(path, "utf8"), path) : {};
13773
14134
  const hooks = config.hooks ?? {};
13774
14135
  for (const event of CURSOR_HOOK_EVENTS) {
13775
14136
  if (hooks[event] !== void 0 && !Array.isArray(hooks[event]))
@@ -13779,7 +14140,7 @@ function validateCursorHooksConfig(root, opts = {}) {
13779
14140
  if (existsSync37(statePath)) {
13780
14141
  let state;
13781
14142
  try {
13782
- state = JSON.parse(readFileSync43(statePath, "utf8"));
14143
+ state = JSON.parse(readFileSync42(statePath, "utf8"));
13783
14144
  } catch {
13784
14145
  throw new Error(`${statePath} is not valid JSON; nothing was changed. Fix it and run replume init again.`);
13785
14146
  }
@@ -13793,7 +14154,7 @@ function installCursorHooks(root, opts = {}) {
13793
14154
  const statePath = statePathFor(path);
13794
14155
  const hadOriginal = existsSync37(path);
13795
14156
  const command = opts.command ?? CURSOR_HOOK_COMMAND;
13796
- const original = hadOriginal ? readFileSync43(path, "utf8") : null;
14157
+ const original = hadOriginal ? readFileSync42(path, "utf8") : null;
13797
14158
  const config = original === null ? {} : parseConfig2(original, path);
13798
14159
  const hooks = { ...config.hooks ?? {} };
13799
14160
  const added = [];
@@ -13824,7 +14185,7 @@ function installCursorHooks(root, opts = {}) {
13824
14185
  if (original !== null) writeFileSync22(backupPath, original, { flag: "wx" });
13825
14186
  writeFileSync22(statePath, JSON.stringify({ hadOriginal, installedHash: hash(updated), added, command }) + "\n", { flag: "wx" });
13826
14187
  } else {
13827
- const state = JSON.parse(readFileSync43(statePath, "utf8"));
14188
+ const state = JSON.parse(readFileSync42(statePath, "utf8"));
13828
14189
  if (state.command !== command) throw new Error(`Cursor hooks already installed with a different command; uninstall first`);
13829
14190
  state.added = [.../* @__PURE__ */ new Set([...state.added, ...added])];
13830
14191
  state.installedHash = hash(updated);
@@ -13840,13 +14201,13 @@ function uninstallCursorHooks(root) {
13840
14201
  const backupPath = backupPathFor(path);
13841
14202
  const statePath = statePathFor(path);
13842
14203
  if (!existsSync37(statePath)) return { restored: false, path };
13843
- const state = JSON.parse(readFileSync43(statePath, "utf8"));
13844
- const current = existsSync37(path) ? readFileSync43(path, "utf8") : null;
14204
+ const state = JSON.parse(readFileSync42(statePath, "utf8"));
14205
+ const current = existsSync37(path) ? readFileSync42(path, "utf8") : null;
13845
14206
  const unchanged = current !== null && hash(current) === state.installedHash;
13846
14207
  if (unchanged) {
13847
14208
  if (state.hadOriginal) {
13848
14209
  if (!existsSync37(backupPath)) throw new Error(`Cursor hook backup missing: ${backupPath}`);
13849
- writeFileSync22(path, readFileSync43(backupPath));
14210
+ writeFileSync22(path, readFileSync42(backupPath));
13850
14211
  } else rmSync14(path);
13851
14212
  } else if (current !== null) {
13852
14213
  const config = parseConfig2(current, path);
@@ -13871,7 +14232,7 @@ function uninstallCursorHooks(root) {
13871
14232
  function cursorStopSettings(root, command = CURSOR_HOOK_COMMAND) {
13872
14233
  const fallback = { timeoutSec: null, loopLimit: 5 };
13873
14234
  try {
13874
- const config = parseConfig2(readFileSync43(cursorHooksPath(root), "utf8"), cursorHooksPath(root));
14235
+ const config = parseConfig2(readFileSync42(cursorHooksPath(root), "utf8"), cursorHooksPath(root));
13875
14236
  const entries = config.hooks?.stop;
13876
14237
  const ours = (x) => managedEntry(x, command) || isObject(x) && typeof x.command === "string" && /\bhook cursor\s*$/.test(x.command);
13877
14238
  const entry = Array.isArray(entries) ? entries.find(ours) : void 0;
@@ -13887,33 +14248,7 @@ function cursorStopSettings(root, command = CURSOR_HOOK_COMMAND) {
13887
14248
  // src/agents/cursor/handler.ts
13888
14249
  var relPath = (root, p) => (isAbsolute8(p) ? relative8(root, p) : p).split(sep5).join("/");
13889
14250
  function firstPromptFromTranscript(path) {
13890
- if (typeof path !== "string" || !path.endsWith(".jsonl")) return null;
13891
- let head = "";
13892
- try {
13893
- const fd = openSync(path, "r");
13894
- try {
13895
- const buf = Buffer.alloc(64 * 1024);
13896
- head = buf.subarray(0, readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
13897
- } finally {
13898
- closeSync(fd);
13899
- }
13900
- } catch {
13901
- return null;
13902
- }
13903
- for (const line2 of head.split("\n")) {
13904
- let m;
13905
- try {
13906
- m = JSON.parse(line2);
13907
- } catch {
13908
- continue;
13909
- }
13910
- const msg = m;
13911
- if (msg?.role !== "user" || !Array.isArray(msg.message?.content)) continue;
13912
- const text = msg.message.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
13913
- const q = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(text)?.[1] ?? text.replace(/<timestamp>[\s\S]*?<\/timestamp>/g, "").trim();
13914
- if (q) return q.slice(0, 4e3);
13915
- }
13916
- return null;
14251
+ return firstPromptIn(path, cursorUserQuery);
13917
14252
  }
13918
14253
  function handleCursorHook(stdinText, env = process.env, deps = {}) {
13919
14254
  try {
@@ -13965,7 +14300,7 @@ function handleCursorHook(stdinText, env = process.env, deps = {}) {
13965
14300
  const contract = isDraft(loaded) ? null : loaded;
13966
14301
  const action = cursorPayloadToAction(raw5, root, { tool: event.tool, paths: event.paths, command: event.command });
13967
14302
  const canEvaluate = event.type === "tool_pre" || event.type === "command" && raw5.hook_event_name === "beforeShellExecution" || event.type === "file_edit";
13968
- const evaluation = canEvaluate ? evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode }) : null;
14303
+ const evaluation = canEvaluate ? evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode, baseFailing: baseFailingFor({ repoRoot: root, session: history, command: contract?.test_command, timeoutSec: loadConfig(root).auto.test_timeout_sec }) }) : null;
13969
14304
  const decision2 = {
13970
14305
  rule: evaluation?.hits[0]?.rule ?? null,
13971
14306
  action: evaluation?.action ?? "ok",
@@ -14011,7 +14346,7 @@ function handleCursorHook(stdinText, env = process.env, deps = {}) {
14011
14346
  // src/agents/codex/handler.ts
14012
14347
  import { execFileSync as execFileSync30 } from "child_process";
14013
14348
  import { createHash as createHash10 } from "crypto";
14014
- import { existsSync as existsSync39, readFileSync as readFileSync45 } from "fs";
14349
+ import { existsSync as existsSync39, readFileSync as readFileSync44 } from "fs";
14015
14350
  import { resolve as resolve11 } from "path";
14016
14351
 
14017
14352
  // src/agents/codex/action.ts
@@ -14040,7 +14375,7 @@ function codexAction(payload, root) {
14040
14375
  }
14041
14376
 
14042
14377
  // src/agents/codex/install.ts
14043
- import { copyFileSync as copyFileSync6, existsSync as existsSync38, mkdirSync as mkdirSync23, readFileSync as readFileSync44, writeFileSync as writeFileSync23 } from "fs";
14378
+ import { copyFileSync as copyFileSync6, existsSync as existsSync38, mkdirSync as mkdirSync23, readFileSync as readFileSync43, writeFileSync as writeFileSync23 } from "fs";
14044
14379
  import { dirname as dirname15, join as join50 } from "path";
14045
14380
  var CODEX_HOOK_COMMAND = "replume hook codex";
14046
14381
  var CODEX_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "SessionEnd"];
@@ -14078,7 +14413,7 @@ function validateCodexHooks(root) {
14078
14413
  const path = codexHooksPath(root);
14079
14414
  if (!existsSync38(path)) return;
14080
14415
  try {
14081
- const parsed = JSON.parse(readFileSync44(path, "utf8"));
14416
+ const parsed = JSON.parse(readFileSync43(path, "utf8"));
14082
14417
  if (!obj3(parsed)) throw new Error("expected an object");
14083
14418
  mergeCodexHooks(parsed);
14084
14419
  } catch (e) {
@@ -14088,7 +14423,7 @@ function validateCodexHooks(root) {
14088
14423
  function installCodexHooks(root) {
14089
14424
  validateCodexHooks(root);
14090
14425
  const path = codexHooksPath(root);
14091
- const original = existsSync38(path) ? readFileSync44(path, "utf8") : null;
14426
+ const original = existsSync38(path) ? readFileSync43(path, "utf8") : null;
14092
14427
  const parsed = original ? JSON.parse(original) : {};
14093
14428
  const { config, added, updated } = mergeCodexHooks(parsed);
14094
14429
  if (added.length || updated.length) {
@@ -14100,7 +14435,7 @@ function installCodexHooks(root) {
14100
14435
  }
14101
14436
  function codexStopTimeoutSec(root, command = CODEX_HOOK_COMMAND) {
14102
14437
  try {
14103
- const parsed = JSON.parse(readFileSync44(codexHooksPath(root), "utf8"));
14438
+ const parsed = JSON.parse(readFileSync43(codexHooksPath(root), "utf8"));
14104
14439
  const groups = obj3(parsed) && obj3(parsed.hooks) ? parsed.hooks.Stop : void 0;
14105
14440
  if (!Array.isArray(groups)) return null;
14106
14441
  for (const g of groups) {
@@ -14121,7 +14456,7 @@ var str4 = (x) => typeof x === "string" ? x : "";
14121
14456
  var result = (stdout) => ({ ...QUIET2, stdout });
14122
14457
  var fileHash = (path) => {
14123
14458
  try {
14124
- return createHash10("sha256").update(readFileSync45(path)).digest("hex");
14459
+ return createHash10("sha256").update(readFileSync44(path)).digest("hex");
14125
14460
  } catch {
14126
14461
  return null;
14127
14462
  }
@@ -14225,7 +14560,7 @@ ${stop2.message}${note}` }));
14225
14560
  if (event === "PreToolUse") {
14226
14561
  if (a.kind === "edit" && a.paths.length && loadConfig(root).supervisor.checks) stashBefore(root, str4(p.session_id), a.paths, str4(p.tool_use_id) || void 0);
14227
14562
  const history = readEvents(ledger).events.filter((e) => e.sessionId === str4(p.session_id));
14228
- const verdict = evaluate(a, { contract, history, repoRoot: root, mode: loadConfig(root).mode });
14563
+ const verdict = evaluate(a, { contract, history, repoRoot: root, mode: loadConfig(root).mode, baseFailing: baseFailingFor({ repoRoot: root, session: history, command: contract?.test_command, timeoutSec: loadConfig(root).auto.test_timeout_sec }) });
14229
14564
  const first = verdict.hits[0];
14230
14565
  appendEvent(ledger, {
14231
14566
  ...base4("tool_pre"),
@@ -14473,7 +14808,7 @@ function handleCopilotHook(stdinText, deps = {}) {
14473
14808
  const loaded = loadContract(contractPath(root));
14474
14809
  const contract = isDraft(loaded) ? null : loaded;
14475
14810
  const history = readEvents(ledgerPath(root)).events.filter((e) => e.sessionId === sid);
14476
- const verdict = evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode });
14811
+ const verdict = evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode, baseFailing: baseFailingFor({ repoRoot: root, session: history, command: contract?.test_command, timeoutSec: loadConfig(root).auto.test_timeout_sec }) });
14477
14812
  appendEvent(ledgerPath(root), {
14478
14813
  ...base4("tool_pre"),
14479
14814
  tool: toolName,
@@ -14520,7 +14855,7 @@ function handleCopilotHook(stdinText, deps = {}) {
14520
14855
  }
14521
14856
 
14522
14857
  // src/agents/copilot/install.ts
14523
- import { copyFileSync as copyFileSync7, existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync46, writeFileSync as writeFileSync24 } from "fs";
14858
+ import { copyFileSync as copyFileSync7, existsSync as existsSync41, mkdirSync as mkdirSync24, readFileSync as readFileSync45, writeFileSync as writeFileSync24 } from "fs";
14524
14859
  import { dirname as dirname17, join as join52 } from "path";
14525
14860
  var COPILOT_HOOK_COMMAND = "replume hook copilot";
14526
14861
  var COPILOT_HOOK_FILE = join52(".github", "hooks", "replume.json");
@@ -14536,18 +14871,18 @@ function mergeCopilotHooks(config, command = COPILOT_HOOK_COMMAND) {
14536
14871
  const added = [];
14537
14872
  const ours = (h) => obj4(h) && (h.bash === command || h.command === command);
14538
14873
  for (const event of EVENTS) {
14539
- const list4 = hooks[event] ?? [];
14540
- if (!Array.isArray(list4)) throw new Error(`hooks.${event} must be an array`);
14541
- if (list4.some(ours)) continue;
14874
+ const list5 = hooks[event] ?? [];
14875
+ if (!Array.isArray(list5)) throw new Error(`hooks.${event} must be an array`);
14876
+ if (list5.some(ours)) continue;
14542
14877
  const timeoutSec = event === "agentStop" ? COPILOT_STOP_TIMEOUT_SEC : 15;
14543
- hooks[event] = [...list4, { type: "command", bash: command, command, timeoutSec }];
14878
+ hooks[event] = [...list5, { type: "command", bash: command, command, timeoutSec }];
14544
14879
  added.push(event);
14545
14880
  }
14546
14881
  return { config: { ...config, version: 1, hooks }, added };
14547
14882
  }
14548
14883
  function installCopilotHooks(root) {
14549
14884
  const path = copilotHooksPath(root);
14550
- const original = existsSync41(path) ? readFileSync46(path, "utf8") : null;
14885
+ const original = existsSync41(path) ? readFileSync45(path, "utf8") : null;
14551
14886
  let parsed = {};
14552
14887
  if (original !== null) {
14553
14888
  try {
@@ -14569,7 +14904,7 @@ function installCopilotHooks(root) {
14569
14904
 
14570
14905
  // src/agents/hermes/handler.ts
14571
14906
  import { execFileSync as execFileSync32 } from "child_process";
14572
- import { existsSync as existsSync42, readFileSync as readFileSync47, rmSync as rmSync15, statSync as statSync9, writeFileSync as writeFileSync25 } from "fs";
14907
+ import { existsSync as existsSync42, readFileSync as readFileSync46, rmSync as rmSync15, statSync as statSync9, writeFileSync as writeFileSync25 } from "fs";
14573
14908
  import { realpathSync as realpathSync4 } from "fs";
14574
14909
  import { tmpdir as tmpdir14 } from "os";
14575
14910
  import { basename as basename4, dirname as dirname18, isAbsolute as isAbsolute10, join as join53, relative as relative10, resolve as resolve14 } from "path";
@@ -14654,7 +14989,7 @@ function rememberPrompt(sessionId2, text) {
14654
14989
  function takePrompt(sessionId2) {
14655
14990
  const file3 = promptFile(sessionId2);
14656
14991
  try {
14657
- const text = readFileSync47(file3, "utf8");
14992
+ const text = readFileSync46(file3, "utf8");
14658
14993
  rmSync15(file3, { force: true });
14659
14994
  return text;
14660
14995
  } catch {
@@ -14750,7 +15085,7 @@ ${memory.text}` : memory.text;
14750
15085
  const loaded = loadContract(contractPath(root));
14751
15086
  const contract = isDraft(loaded) ? null : loaded;
14752
15087
  const history = readEvents(ledgerPath(root)).events.filter((e) => e.sessionId === sid);
14753
- const verdict = evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode });
15088
+ const verdict = evaluate(action, { contract, history, repoRoot: root, mode: loadConfig(root).mode, baseFailing: baseFailingFor({ repoRoot: root, session: history, command: contract?.test_command, timeoutSec: loadConfig(root).auto.test_timeout_sec }) });
14754
15089
  appendEvent(ledgerPath(root), {
14755
15090
  ...base4("tool_pre"),
14756
15091
  tool,
@@ -14835,7 +15170,7 @@ async function replayHooks(content, opts = {}) {
14835
15170
 
14836
15171
  // src/core/init.ts
14837
15172
  import { execFileSync as execFileSync33 } from "child_process";
14838
- import { existsSync as existsSync43, mkdirSync as mkdirSync26, readFileSync as readFileSync48, writeFileSync as writeFileSync27 } from "fs";
15173
+ import { existsSync as existsSync43, mkdirSync as mkdirSync26, readFileSync as readFileSync47, writeFileSync as writeFileSync27 } from "fs";
14839
15174
  import { homedir as homedir5 } from "os";
14840
15175
  import { delimiter, join as join55 } from "path";
14841
15176
 
@@ -14979,7 +15314,7 @@ function uncommittedSetup(repoRoot2, files) {
14979
15314
  function runnerNotInstalled(repoRoot2) {
14980
15315
  let pkg;
14981
15316
  try {
14982
- pkg = JSON.parse(readFileSync48(join55(repoRoot2, "package.json"), "utf8"));
15317
+ pkg = JSON.parse(readFileSync47(join55(repoRoot2, "package.json"), "utf8"));
14983
15318
  } catch {
14984
15319
  return null;
14985
15320
  }
@@ -14994,7 +15329,7 @@ function runnerNotInstalled(repoRoot2) {
14994
15329
  }
14995
15330
 
14996
15331
  // src/core/share.ts
14997
- import { appendFileSync as appendFileSync9, existsSync as existsSync44, mkdirSync as mkdirSync27, readFileSync as readFileSync49, readdirSync as readdirSync9, statSync as statSync10, writeFileSync as writeFileSync28 } from "fs";
15332
+ import { appendFileSync as appendFileSync9, existsSync as existsSync44, mkdirSync as mkdirSync27, readFileSync as readFileSync48, readdirSync as readdirSync9, statSync as statSync10, writeFileSync as writeFileSync28 } from "fs";
14998
15333
  import { homedir as homedir6 } from "os";
14999
15334
  import { join as join56, resolve as resolve15 } from "path";
15000
15335
  var SHARE_ENDPOINT = "https://replume.dev";
@@ -15082,14 +15417,14 @@ function saveShare(repoRoot2, r) {
15082
15417
  const dir2 = join56(repoRoot2, ".replume");
15083
15418
  mkdirSync27(dir2, { recursive: true });
15084
15419
  const ignore = join56(dir2, ".gitignore");
15085
- const prior = existsSync44(ignore) ? readFileSync49(ignore, "utf8") : "";
15420
+ const prior = existsSync44(ignore) ? readFileSync48(ignore, "utf8") : "";
15086
15421
  if (!prior.split(/\r?\n/).includes("shares.jsonl")) writeFileSync28(ignore, `${prior}${prior && !prior.endsWith("\n") ? "\n" : ""}shares.jsonl
15087
15422
  `);
15088
15423
  appendFileSync9(sharesFile(repoRoot2), JSON.stringify(r) + "\n", { mode: 384 });
15089
15424
  }
15090
15425
  function readShares(repoRoot2) {
15091
15426
  if (!existsSync44(sharesFile(repoRoot2))) return [];
15092
- return readFileSync49(sharesFile(repoRoot2), "utf8").split(/\r?\n/).flatMap((l) => {
15427
+ return readFileSync48(sharesFile(repoRoot2), "utf8").split(/\r?\n/).flatMap((l) => {
15093
15428
  try {
15094
15429
  const r = JSON.parse(l);
15095
15430
  return typeof r.id === "string" && typeof r.deleteToken === "string" ? [r] : [];
@@ -15120,7 +15455,7 @@ function destination(repo, pr) {
15120
15455
  if (!Number.isSafeInteger(pr) || pr < 1) throw new Error("--pr must be a positive PR number");
15121
15456
  return `https://github.com/${repo}/pull/${pr}`;
15122
15457
  }
15123
- var list3 = (items) => items.length ? items.slice(0, 100).map((x) => `- ${x}`).join("\n") : "- none";
15458
+ var list4 = (items) => items.length ? items.slice(0, 100).map((x) => `- ${x}`).join("\n") : "- none";
15124
15459
  function verifyReportToPayload(r, client) {
15125
15460
  const verdict = r.verdict === "pass" ? "PASS" : r.verdict === "fail" ? "FAIL" : "INCOMPLETE";
15126
15461
  const markdown = [
@@ -15129,14 +15464,14 @@ function verifyReportToPayload(r, client) {
15129
15464
  "## What was asked",
15130
15465
  "- Task text is not stored in the verify JSON; see the local contract.",
15131
15466
  "## Tests",
15132
- list3([
15467
+ list4([
15133
15468
  ...r.done.map((x) => `DONE: ${x.id}`),
15134
15469
  ...r.failed.map((x) => `FAILED: ${x.id}`),
15135
15470
  ...r.scenarios.map((x) => `${x.id}: ${x.status}`),
15136
15471
  ...(r.jiraCriteria ?? []).map((x) => `Jira criterion ${x.status}: ${x.text}`)
15137
15472
  ]),
15138
15473
  "## What was NOT checked",
15139
- list3(r.notChecked.map((x) => `${x.path}: ${x.reason}`)),
15474
+ list4(r.notChecked.map((x) => `${x.path}: ${x.reason}`)),
15140
15475
  "## Agent hook limits",
15141
15476
  "- This saved verify report does not prove agent steps were captured in CI."
15142
15477
  ].join("\n");
@@ -15173,7 +15508,7 @@ async function postComment2(opts) {
15173
15508
  }
15174
15509
 
15175
15510
  // src/core/watch.ts
15176
- import { existsSync as existsSync45, readFileSync as readFileSync50, statSync as statSync11, watchFile, unwatchFile } from "fs";
15511
+ import { existsSync as existsSync45, readFileSync as readFileSync49, statSync as statSync11, watchFile, unwatchFile } from "fs";
15177
15512
  import { basename as basename5 } from "path";
15178
15513
  var rawOf = (e) => e.raw && typeof e.raw === "object" && !Array.isArray(e.raw) ? e.raw : {};
15179
15514
  function clean(value) {
@@ -15287,7 +15622,7 @@ var flushes = (e) => e.type === "prompt" || e.type === "stop" || e.type === "rul
15287
15622
  function formatEvents(events, state, opts = {}) {
15288
15623
  const out = [];
15289
15624
  const flushAll = () => {
15290
- for (const list4 of state.pending.values()) for (const p of list4) out.push(formatEvent(p, opts));
15625
+ for (const list5 of state.pending.values()) for (const p of list5) out.push(formatEvent(p, opts));
15291
15626
  state.pending.clear();
15292
15627
  };
15293
15628
  for (const e of events) {
@@ -15323,7 +15658,7 @@ function readNew(path, offset, opts = {}, state) {
15323
15658
  const size = statSync11(path).size;
15324
15659
  if (size < offset) offset = 0;
15325
15660
  if (size === offset) return { lines: [], offset, bad: 0 };
15326
- const buf = readFileSync50(path);
15661
+ const buf = readFileSync49(path);
15327
15662
  const chunk = buf.subarray(offset, size).toString("utf-8");
15328
15663
  const lastNl = chunk.lastIndexOf("\n");
15329
15664
  if (lastNl < 0) return { lines: [], offset, bad: 0 };
@@ -15755,7 +16090,7 @@ function formatEvalReport(report) {
15755
16090
  }
15756
16091
 
15757
16092
  // src/core/doctor.ts
15758
- import { existsSync as existsSync46, readFileSync as readFileSync51 } from "fs";
16093
+ import { existsSync as existsSync46, readFileSync as readFileSync50 } from "fs";
15759
16094
  import { homedir as homedir7 } from "os";
15760
16095
  import { isAbsolute as isAbsolute12, join as join58 } from "path";
15761
16096
  var object7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -15799,7 +16134,7 @@ function otherHookWarnings(home = process.env.HOME || homedir7()) {
15799
16134
  if (!existsSync46(path)) continue;
15800
16135
  let config;
15801
16136
  try {
15802
- config = JSON.parse(readFileSync51(path, "utf8"));
16137
+ config = JSON.parse(readFileSync50(path, "utf8"));
15803
16138
  } catch {
15804
16139
  warnings.push(`${agent} hook settings could not be read: ${path}`);
15805
16140
  continue;
@@ -15858,7 +16193,7 @@ program.command("github").description("Explicit GitHub PR integrations").command
15858
16193
  const pr = Number(opts.pr);
15859
16194
  const url = destination(opts.repo, pr);
15860
16195
  const file3 = findReport(root, opts.report);
15861
- const source = readFileSync52(file3, "utf8");
16196
+ const source = readFileSync51(file3, "utf8");
15862
16197
  const client = `replume-cli ${package_default.version}`;
15863
16198
  let payload;
15864
16199
  if (file3.endsWith(".json")) {
@@ -15906,7 +16241,7 @@ program.command("share").description("Upload one session report to replume.dev a
15906
16241
  return;
15907
16242
  }
15908
16243
  const file3 = findReport(root, reportArg);
15909
- const payload = reportToPayload(readFileSync52(file3, "utf8"), { repoRoot: root, noDiff: opts.diff === false, client: `replume-cli ${package_default.version}` });
16244
+ const payload = reportToPayload(readFileSync51(file3, "utf8"), { repoRoot: root, noDiff: opts.diff === false, client: `replume-cli ${package_default.version}` });
15910
16245
  console.log(renderPreview(payload, endpoint2));
15911
16246
  if (opts.dryRun) return;
15912
16247
  if (!opts.yes) {
@@ -16094,7 +16429,7 @@ function executeInit(opts) {
16094
16429
  const path = claudeSettingsPath(root);
16095
16430
  let parsed;
16096
16431
  try {
16097
- parsed = JSON.parse(readFileSync52(path, "utf8"));
16432
+ parsed = JSON.parse(readFileSync51(path, "utf8"));
16098
16433
  } catch {
16099
16434
  throw new Error(`${path} is not valid JSON; nothing was changed. Fix it and run replume init again.`);
16100
16435
  }
@@ -16336,7 +16671,7 @@ program.command("confirm-config").description("Confirm one change to an existing
16336
16671
  let sha2 = opts.sha?.toLowerCase();
16337
16672
  if (!sha2) {
16338
16673
  try {
16339
- sha2 = contentSha(readFileSync52(join59(root, rel3)));
16674
+ sha2 = contentSha(readFileSync51(join59(root, rel3)));
16340
16675
  } catch {
16341
16676
  fail(`${rel3} does not exist; pass --sha with the hash from Replume's message`);
16342
16677
  }
@@ -16386,7 +16721,7 @@ program.command("report").description("Re-print a saved report. Session report (
16386
16721
  if (!existsSync47(file3)) fail(`no report for ${commit.slice(0, 7)} at ${file3}; run replume verify --commit ${ref}`);
16387
16722
  let report;
16388
16723
  try {
16389
- report = JSON.parse(readFileSync52(file3, "utf-8"));
16724
+ report = JSON.parse(readFileSync51(file3, "utf-8"));
16390
16725
  } catch (e) {
16391
16726
  fail(`${file3} is not valid JSON: ${e.message}`);
16392
16727
  }
@@ -16418,7 +16753,7 @@ demo.option("--replay <file>", "run a recorded events.jsonl session through the
16418
16753
  const sibling = file3.replace(/\.jsonl?$/, "") + ".contract.yaml";
16419
16754
  const contractFile = opts.contract ? resolve16(opts.contract) : existsSync47(sibling) ? sibling : contractPath(repoRoot());
16420
16755
  const contract = loadContract(contractFile);
16421
- console.log(renderReplay(replay(readFileSync52(file3, "utf-8"), contract), contract, contract ? contractFile : void 0));
16756
+ console.log(renderReplay(replay(readFileSync51(file3, "utf-8"), contract), contract, contract ? contractFile : void 0));
16422
16757
  } catch (e) {
16423
16758
  fail(e.message);
16424
16759
  }
@@ -16429,8 +16764,8 @@ async function demoReplayHooks(path, contractOpt) {
16429
16764
  const sibling = file3.replace(/\.jsonl?$/, "") + ".contract.yaml";
16430
16765
  const contractFile = contractOpt ? resolve16(contractOpt) : existsSync47(sibling) ? sibling : null;
16431
16766
  try {
16432
- const r = await replayHooks(readFileSync52(file3, "utf-8"), {
16433
- contractYaml: contractFile ? readFileSync52(contractFile, "utf-8") : void 0,
16767
+ const r = await replayHooks(readFileSync51(file3, "utf-8"), {
16768
+ contractYaml: contractFile ? readFileSync51(contractFile, "utf-8") : void 0,
16434
16769
  env: process.env
16435
16770
  });
16436
16771
  console.log(REPLAY_HOOKS_BANNER);
@@ -16484,7 +16819,7 @@ hook.hook("preAction", () => {
16484
16819
  hook.command("judge-step").description("Internal: run one model-judge step judgment in the background (started by the supervisor when judge.enabled)").requiredOption("--job <file>", "JSON job file written by the hook").action(async (opts) => {
16485
16820
  let job = null;
16486
16821
  try {
16487
- job = JSON.parse(readFileSync52(opts.job, "utf8"));
16822
+ job = JSON.parse(readFileSync51(opts.job, "utf8"));
16488
16823
  } catch {
16489
16824
  job = null;
16490
16825
  }