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 +693 -358
- package/package.json +1 -1
- package/templates/pr-check.yml +1 -1
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
|
-
|
|
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 (
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
|
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}): ${
|
|
1451
|
-
if (Object.keys(c.tools).length) out.push(` 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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
3523
|
-
const protect = protectedFiles.length ? auto ? `Protected, do not change: ${
|
|
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 = (
|
|
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 = (
|
|
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
|
|
7230
|
-
var stripAnsi = (s) => s.replace(
|
|
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
|
|
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
|
|
8073
|
+
var ANSI5 = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
7823
8074
|
function summarizeFailure(output) {
|
|
7824
|
-
const text = output.replace(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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,
|
|
8559
|
-
if (!Array.isArray(
|
|
8560
|
-
hooks[event] =
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
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 (
|
|
8877
|
-
const content =
|
|
9226
|
+
if (e.type !== "assistant") continue;
|
|
9227
|
+
const content = e.message?.content;
|
|
8878
9228
|
if (!Array.isArray(content)) continue;
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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}: ${
|
|
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
|
|
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 =
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
11125
|
-
const md2 =
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
11647
|
-
if (!
|
|
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(
|
|
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
|
|
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) ?
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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) ?
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
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(
|
|
13844
|
-
const current = existsSync37(path) ?
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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) ?
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
14540
|
-
if (!Array.isArray(
|
|
14541
|
-
if (
|
|
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] = [...
|
|
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) ?
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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) ?
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
16433
|
-
contractYaml: contractFile ?
|
|
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(
|
|
16822
|
+
job = JSON.parse(readFileSync51(opts.job, "utf8"));
|
|
16488
16823
|
} catch {
|
|
16489
16824
|
job = null;
|
|
16490
16825
|
}
|