@gethmy/harness 1.2.1 → 1.3.0
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 +489 -225
- package/dist/index.js +1222 -401
- package/package.json +2 -2
- package/src/ci-failure.ts +465 -0
- package/src/cli.ts +11 -1
- package/src/confine-to-repo.test.ts +324 -1
- package/src/confine-to-repo.ts +274 -22
- package/src/error-classifier.ts +52 -1
- package/src/gate-collectors.ts +11 -3
- package/src/git-pr.ts +461 -8
- package/src/index.ts +2 -0
- package/src/model-tier.test.ts +11 -6
- package/src/model-tier.ts +4 -4
- package/src/oracle-collector.ts +244 -23
- package/src/oracle.ts +856 -108
- package/src/pm.ts +15 -5
- package/src/repair-sandbox.test.ts +116 -0
- package/src/repair-sandbox.ts +303 -0
- package/src/run-sizing.test.ts +264 -66
- package/src/run-sizing.ts +146 -26
- package/src/sdk-agent-runner.ts +22 -1
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,9 @@ var init_branchRef = __esm(() => {
|
|
|
35
35
|
|
|
36
36
|
// ../harmony-shared/dist/cardLinks.js
|
|
37
37
|
var init_cardLinks = () => {};
|
|
38
|
+
|
|
39
|
+
// ../harmony-shared/dist/cardModelStat.js
|
|
40
|
+
var init_cardModelStat = () => {};
|
|
38
41
|
// ../harmony-shared/dist/classification.js
|
|
39
42
|
function tierFromScore(score) {
|
|
40
43
|
const s = Math.max(0, Math.min(10, Math.round(score)));
|
|
@@ -393,7 +396,8 @@ var init_gateEvaluate = __esm(() => {
|
|
|
393
396
|
"artifact",
|
|
394
397
|
"label",
|
|
395
398
|
"custom",
|
|
396
|
-
"oracle_passed"
|
|
399
|
+
"oracle_passed",
|
|
400
|
+
"oracle_red"
|
|
397
401
|
];
|
|
398
402
|
GATE_OPERATORS = [
|
|
399
403
|
"eq",
|
|
@@ -496,6 +500,7 @@ var init_dist = __esm(() => {
|
|
|
496
500
|
init_agentStaleness();
|
|
497
501
|
init_branchRef();
|
|
498
502
|
init_cardLinks();
|
|
503
|
+
init_cardModelStat();
|
|
499
504
|
init_classification();
|
|
500
505
|
init_columnCardSplit();
|
|
501
506
|
init_columnSort();
|
|
@@ -652,6 +657,8 @@ var AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth to
|
|
|
652
657
|
var OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
|
|
653
658
|
var USAGE_LIMIT = /usage limit|daily limit|monthly limit|quota (?:exceeded|reached)|reached your .{0,20}limit|usage_limit_reached|limit will reset/i;
|
|
654
659
|
var RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
|
|
660
|
+
var SPEND_LIMIT = /spend(?:ing)? (?:limit|cap)|monthly spend/i;
|
|
661
|
+
var SPEND_LIMIT_REMEDY = /admin-settings\/usage/i;
|
|
655
662
|
function parseRetryAfterMs(message) {
|
|
656
663
|
const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
|
|
657
664
|
if (!match)
|
|
@@ -667,12 +674,16 @@ function classifyRunError(message) {
|
|
|
667
674
|
const retryAfterMs = parseRetryAfterMs(message);
|
|
668
675
|
if (AUTH.test(message))
|
|
669
676
|
return { kind: "auth", retryAfterMs };
|
|
677
|
+
if (SPEND_LIMIT.test(message))
|
|
678
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
670
679
|
if (OUT_OF_CREDITS.test(message))
|
|
671
680
|
return { kind: "out_of_credits", retryAfterMs };
|
|
672
681
|
if (USAGE_LIMIT.test(message))
|
|
673
682
|
return { kind: "usage_limit", retryAfterMs };
|
|
674
683
|
if (RATE_LIMIT.test(message))
|
|
675
684
|
return { kind: "rate_limit", retryAfterMs };
|
|
685
|
+
if (SPEND_LIMIT_REMEDY.test(message))
|
|
686
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
676
687
|
return { kind: null };
|
|
677
688
|
}
|
|
678
689
|
function describeApiError(kind) {
|
|
@@ -685,6 +696,8 @@ function describeApiError(kind) {
|
|
|
685
696
|
return "Anthropic usage limit reached — retrying after reset";
|
|
686
697
|
case "rate_limit":
|
|
687
698
|
return "Anthropic rate limit hit — retrying shortly";
|
|
699
|
+
case "spend_limit":
|
|
700
|
+
return "Account spend limit reached — raise it at claude.ai/admin-settings/usage, then run `harmony-agent resume`";
|
|
688
701
|
}
|
|
689
702
|
}
|
|
690
703
|
function cooldownMsFor(kind) {
|
|
@@ -697,6 +710,8 @@ function cooldownMsFor(kind) {
|
|
|
697
710
|
return 30 * 60000;
|
|
698
711
|
case "auth":
|
|
699
712
|
return 30 * 60000;
|
|
713
|
+
case "spend_limit":
|
|
714
|
+
return 30 * 60000;
|
|
700
715
|
}
|
|
701
716
|
}
|
|
702
717
|
|
|
@@ -1050,7 +1065,7 @@ ${this.capturedStderr}`);
|
|
|
1050
1065
|
kind: "error",
|
|
1051
1066
|
source: "system",
|
|
1052
1067
|
payload: {
|
|
1053
|
-
message:
|
|
1068
|
+
message: resultErrorMessage(r.subtype, joined),
|
|
1054
1069
|
errorKind: cls.kind,
|
|
1055
1070
|
retryable: cls.kind !== "auth" && cls.kind !== null
|
|
1056
1071
|
}
|
|
@@ -1061,6 +1076,13 @@ ${this.capturedStderr}`);
|
|
|
1061
1076
|
}
|
|
1062
1077
|
}
|
|
1063
1078
|
}
|
|
1079
|
+
function resultErrorMessage(subtype, detail) {
|
|
1080
|
+
return `result ${subtype}: ${detail || "(no detail)"}`;
|
|
1081
|
+
}
|
|
1082
|
+
function resultErrorSubtype(message) {
|
|
1083
|
+
const match = message.match(/^result ([A-Za-z0-9_]+):/);
|
|
1084
|
+
return match ? match[1] : null;
|
|
1085
|
+
}
|
|
1064
1086
|
function normalize(raw) {
|
|
1065
1087
|
if (raw == null)
|
|
1066
1088
|
return;
|
|
@@ -1545,24 +1567,342 @@ function truncate(value, max) {
|
|
|
1545
1567
|
init_log();
|
|
1546
1568
|
|
|
1547
1569
|
// src/oracle-collector.ts
|
|
1548
|
-
init_log();
|
|
1549
1570
|
import { createHash } from "node:crypto";
|
|
1550
|
-
|
|
1571
|
+
init_log();
|
|
1551
1572
|
|
|
1552
|
-
|
|
1573
|
+
// src/oracle.ts
|
|
1574
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
1575
|
+
import {
|
|
1576
|
+
chmod,
|
|
1577
|
+
lstat,
|
|
1578
|
+
mkdir,
|
|
1579
|
+
mkdtemp,
|
|
1580
|
+
realpath,
|
|
1581
|
+
rm,
|
|
1582
|
+
writeFile
|
|
1583
|
+
} from "node:fs/promises";
|
|
1584
|
+
import { tmpdir } from "node:os";
|
|
1585
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
1586
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1587
|
+
init_log();
|
|
1588
|
+
var TAG4 = "oracle";
|
|
1589
|
+
async function resolveContained(repoPath, relativePath) {
|
|
1590
|
+
if (isAbsolute(relativePath)) {
|
|
1591
|
+
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
1592
|
+
}
|
|
1593
|
+
if (relativePath === "" || relativePath === ".") {
|
|
1594
|
+
throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
|
|
1595
|
+
}
|
|
1596
|
+
const root = await realpath(repoPath);
|
|
1597
|
+
const target = resolve(root, relativePath);
|
|
1598
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
1599
|
+
throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
|
|
1600
|
+
}
|
|
1601
|
+
let cursor = root;
|
|
1602
|
+
for (const segment of relativePath.split("/")) {
|
|
1603
|
+
cursor = resolve(cursor, segment);
|
|
1604
|
+
const stat = await lstat(cursor).catch(() => null);
|
|
1605
|
+
if (stat?.isSymbolicLink()) {
|
|
1606
|
+
throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return target;
|
|
1610
|
+
}
|
|
1611
|
+
async function place(repoPath, oracle) {
|
|
1612
|
+
const target = await resolveContained(repoPath, oracle.path);
|
|
1613
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1614
|
+
await writeFile(target, oracle.content, "utf8");
|
|
1615
|
+
}
|
|
1616
|
+
async function remove(repoPath, oracle) {
|
|
1617
|
+
const target = await resolveContained(repoPath, oracle.path);
|
|
1618
|
+
await rm(target, { force: true });
|
|
1619
|
+
}
|
|
1620
|
+
var ORACLE_RUNNERS = {
|
|
1621
|
+
vitest: {
|
|
1622
|
+
argv: (path) => ({
|
|
1623
|
+
command: "npx",
|
|
1624
|
+
args: ["--no-install", "vitest", "run", path]
|
|
1625
|
+
}),
|
|
1626
|
+
verdictStream: "stdout",
|
|
1627
|
+
report: {
|
|
1628
|
+
file: "report.json",
|
|
1629
|
+
flags: (reportPath) => [
|
|
1630
|
+
"--reporter=default",
|
|
1631
|
+
"--reporter=json",
|
|
1632
|
+
`--outputFile=${reportPath}`
|
|
1633
|
+
],
|
|
1634
|
+
parse: (content) => {
|
|
1635
|
+
const parsed = JSON.parse(content);
|
|
1636
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1637
|
+
return null;
|
|
1638
|
+
const { numTotalTests, numFailedTests } = parsed;
|
|
1639
|
+
if (typeof numTotalTests !== "number" || typeof numFailedTests !== "number" || !Number.isFinite(numTotalTests) || !Number.isFinite(numFailedTests)) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
return { total: numTotalTests, failed: numFailedTests };
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
bun: {
|
|
1647
|
+
argv: (path) => ({ command: "bun", args: ["test", path] }),
|
|
1648
|
+
verdictStream: "stderr",
|
|
1649
|
+
report: {
|
|
1650
|
+
file: "report.xml",
|
|
1651
|
+
flags: (reportPath) => [
|
|
1652
|
+
"--reporter=junit",
|
|
1653
|
+
`--reporter-outfile=${reportPath}`
|
|
1654
|
+
],
|
|
1655
|
+
parse: (content) => {
|
|
1656
|
+
const root = /<testsuites\b[^>]*>/.exec(content);
|
|
1657
|
+
if (!root)
|
|
1658
|
+
return null;
|
|
1659
|
+
const total = /\btests="(\d+)"/.exec(root[0]);
|
|
1660
|
+
const failed = /\bfailures="(\d+)"/.exec(root[0]);
|
|
1661
|
+
if (!total || !failed)
|
|
1662
|
+
return null;
|
|
1663
|
+
return { total: Number(total[1]), failed: Number(failed[1]) };
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
|
|
1669
|
+
var ORACLE_OUTPUT_LIMIT = 64 * 1024;
|
|
1670
|
+
var ORACLE_SIGINT_GRACE_MS = 2000;
|
|
1671
|
+
var ORACLE_SIGTERM_GRACE_MS = 3000;
|
|
1672
|
+
var ORACLE_DRAIN_GRACE_MS = 500;
|
|
1673
|
+
function resolveOracleRunner(oracle) {
|
|
1674
|
+
return resolveOracleRunnerSpec(oracle).argv(argvPath(oracle.path));
|
|
1675
|
+
}
|
|
1676
|
+
function resolveOracleRunnerSpec(oracle) {
|
|
1677
|
+
const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
|
|
1678
|
+
const spec = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
|
|
1679
|
+
if (!spec) {
|
|
1680
|
+
throw new Error(`refusing to run the held test: runner_hint ${JSON.stringify(oracle.runnerHint)} is not in the motor's allow-list (${ORACLE_RUNNER_HINTS.join(", ")})`);
|
|
1681
|
+
}
|
|
1682
|
+
return spec;
|
|
1683
|
+
}
|
|
1684
|
+
function summarizeOracleReport(oracle, content) {
|
|
1685
|
+
const spec = resolveOracleRunnerSpec(oracle);
|
|
1686
|
+
try {
|
|
1687
|
+
return spec.report.parse(content) ?? null;
|
|
1688
|
+
} catch {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function gradeOracleRed(summary, exitCode) {
|
|
1693
|
+
if (!summary) {
|
|
1694
|
+
return {
|
|
1695
|
+
outcome: "no_verdict",
|
|
1696
|
+
reason: `the runner exited ${exitCode} but the motor has no report it could read — absent, unreadable, or refused, ` + "so it cannot be shown to have run the held test (an absent runner and a failing test share this exit code). " + "The report is a file the runner writes outside the worktree; the motor's local log says which of the three it was"
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
if (summary.total === 0) {
|
|
1700
|
+
return {
|
|
1701
|
+
outcome: "no_verdict",
|
|
1702
|
+
reason: `the runner started but executed no tests (exit ${exitCode}), so the held test produced no verdict`
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
if (summary.failed > 0) {
|
|
1706
|
+
return { outcome: "reproduced", summary };
|
|
1707
|
+
}
|
|
1708
|
+
if (exitCode === 0) {
|
|
1709
|
+
return { outcome: "not_reproduced", summary };
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
outcome: "no_verdict",
|
|
1713
|
+
reason: `all ${summary.total} test(s) passed but the runner exited ${exitCode}, so the failure is not the held test's`
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function argvPath(path) {
|
|
1717
|
+
return path.startsWith("./") ? path : `./${path}`;
|
|
1718
|
+
}
|
|
1719
|
+
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
1720
|
+
const spec = resolveOracleRunnerSpec(oracle);
|
|
1721
|
+
const reportDir = await mkdtemp(join(tmpdir(), reportDirPrefix()));
|
|
1722
|
+
const reportPath = join(reportDir, spec.report.file);
|
|
1723
|
+
try {
|
|
1724
|
+
return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
|
|
1725
|
+
} finally {
|
|
1726
|
+
await removeReportDir(reportDir);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function reportDirPrefix() {
|
|
1730
|
+
return "harmony-oracle-report-";
|
|
1731
|
+
}
|
|
1732
|
+
function assertUntampered(reportPath) {
|
|
1733
|
+
const dir = statSync(dirname(reportPath));
|
|
1734
|
+
if ((dir.mode & 511) !== 448) {
|
|
1735
|
+
throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
|
|
1736
|
+
}
|
|
1737
|
+
const file = lstatSync(reportPath);
|
|
1738
|
+
if (!file.isFile()) {
|
|
1739
|
+
throw new Error("the oracle report is not a regular file — refusing it");
|
|
1740
|
+
}
|
|
1741
|
+
if ((file.mode & 128) === 0) {
|
|
1742
|
+
throw new Error(`the oracle report is not owner-writable (mode ${(file.mode & 511).toString(8)}), so the runner could not have written it last — refusing it`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function removeReportDir(reportDir) {
|
|
1746
|
+
try {
|
|
1747
|
+
await chmod(reportDir, 448).catch(() => {});
|
|
1748
|
+
await rm(reportDir, { recursive: true, force: true });
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
log.warn(TAG4, `Could not remove the oracle report directory ${reportDir} (${err instanceof Error ? err.message : String(err)}) — it may still hold the runner's report, which carries assertion text for vitest. Remove it by hand.`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
|
|
1754
|
+
const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
|
|
1755
|
+
const args = [...baseArgs, ...spec.report.flags(reportPath)];
|
|
1756
|
+
return await new Promise((settleOk, settleErr) => {
|
|
1757
|
+
let child;
|
|
1758
|
+
try {
|
|
1759
|
+
child = spawnInGroup(command, args, {
|
|
1760
|
+
cwd: repoPath,
|
|
1761
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1762
|
+
});
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
settleErr(err);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const pgid = child.pid;
|
|
1768
|
+
let output = "";
|
|
1769
|
+
let truncated = false;
|
|
1770
|
+
const outDecoder = new StringDecoder("utf8");
|
|
1771
|
+
const errDecoder = new StringDecoder("utf8");
|
|
1772
|
+
let verdict = "";
|
|
1773
|
+
let flushed = false;
|
|
1774
|
+
const flush = () => {
|
|
1775
|
+
if (flushed)
|
|
1776
|
+
return;
|
|
1777
|
+
flushed = true;
|
|
1778
|
+
const outTail = outDecoder.end();
|
|
1779
|
+
const errTail = errDecoder.end();
|
|
1780
|
+
output += outTail + errTail;
|
|
1781
|
+
verdict += spec.verdictStream === "stdout" ? outTail : errTail;
|
|
1782
|
+
};
|
|
1783
|
+
const finalOutput = () => {
|
|
1784
|
+
flush();
|
|
1785
|
+
return truncated ? `… earlier output dropped; kept the last ${ORACLE_OUTPUT_LIMIT} characters
|
|
1786
|
+
${output}` : output;
|
|
1787
|
+
};
|
|
1788
|
+
const finalVerdict = () => {
|
|
1789
|
+
flush();
|
|
1790
|
+
return verdict;
|
|
1791
|
+
};
|
|
1792
|
+
let report = null;
|
|
1793
|
+
const captureReport = () => {
|
|
1794
|
+
try {
|
|
1795
|
+
assertUntampered(reportPath);
|
|
1796
|
+
report = spec.report.parse(readFileSync(reportPath, "utf8"));
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
report = null;
|
|
1799
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1800
|
+
const absent = err instanceof Error && err.code === "ENOENT";
|
|
1801
|
+
if (absent) {
|
|
1802
|
+
log.info(TAG4, `No oracle report at ${reportPath} — no verdict.`);
|
|
1803
|
+
} else {
|
|
1804
|
+
log.warn(TAG4, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
let settled = false;
|
|
1809
|
+
let killing = false;
|
|
1810
|
+
let timer;
|
|
1811
|
+
let drainTimer;
|
|
1812
|
+
const settle = (failure, result) => {
|
|
1813
|
+
if (settled)
|
|
1814
|
+
return;
|
|
1815
|
+
settled = true;
|
|
1816
|
+
if (timer)
|
|
1817
|
+
clearTimeout(timer);
|
|
1818
|
+
if (drainTimer)
|
|
1819
|
+
clearTimeout(drainTimer);
|
|
1820
|
+
reapGroup(pgid);
|
|
1821
|
+
if (failure)
|
|
1822
|
+
settleErr(failure);
|
|
1823
|
+
else
|
|
1824
|
+
settleOk(result);
|
|
1825
|
+
};
|
|
1826
|
+
const append = (stream, chunk) => {
|
|
1827
|
+
const text = (stream === "stdout" ? outDecoder : errDecoder).write(chunk);
|
|
1828
|
+
output += text;
|
|
1829
|
+
if (output.length > ORACLE_OUTPUT_LIMIT) {
|
|
1830
|
+
output = output.slice(-ORACLE_OUTPUT_LIMIT);
|
|
1831
|
+
truncated = true;
|
|
1832
|
+
}
|
|
1833
|
+
if (stream === spec.verdictStream) {
|
|
1834
|
+
verdict += text;
|
|
1835
|
+
if (verdict.length > ORACLE_OUTPUT_LIMIT) {
|
|
1836
|
+
verdict = verdict.slice(-ORACLE_OUTPUT_LIMIT);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
child.stdout?.on("data", (chunk) => append("stdout", chunk));
|
|
1841
|
+
child.stderr?.on("data", (chunk) => append("stderr", chunk));
|
|
1842
|
+
child.once("error", (err) => settle(err));
|
|
1843
|
+
const settleFromExit = (code, signal) => {
|
|
1844
|
+
if (drainTimer)
|
|
1845
|
+
clearTimeout(drainTimer);
|
|
1846
|
+
if (code === null) {
|
|
1847
|
+
settle(new Error(`the held test was terminated by signal ${signal}`));
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
settle(null, {
|
|
1851
|
+
exitCode: code,
|
|
1852
|
+
output: finalOutput(),
|
|
1853
|
+
verdict: finalVerdict(),
|
|
1854
|
+
report
|
|
1855
|
+
});
|
|
1856
|
+
};
|
|
1857
|
+
child.once("exit", (code, signal) => {
|
|
1858
|
+
if (killing)
|
|
1859
|
+
return;
|
|
1860
|
+
captureReport();
|
|
1861
|
+
if (timer)
|
|
1862
|
+
clearTimeout(timer);
|
|
1863
|
+
reapGroup(pgid);
|
|
1864
|
+
drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
|
|
1865
|
+
child.once("close", () => settleFromExit(code, signal));
|
|
1866
|
+
});
|
|
1867
|
+
timer = setTimeout(() => {
|
|
1868
|
+
if (settled)
|
|
1869
|
+
return;
|
|
1870
|
+
killing = true;
|
|
1871
|
+
terminateGroup(child, {
|
|
1872
|
+
sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
|
|
1873
|
+
sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
|
|
1874
|
+
}).catch(() => {}).then(() => {
|
|
1875
|
+
settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
|
|
1876
|
+
});
|
|
1877
|
+
}, timeoutMs);
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// src/oracle-collector.ts
|
|
1882
|
+
var TAG5 = "oracle-collector";
|
|
1883
|
+
|
|
1884
|
+
class HeldOracleCollector {
|
|
1553
1885
|
deps;
|
|
1554
|
-
kind = "oracle_passed";
|
|
1555
1886
|
constructor(deps) {
|
|
1556
1887
|
this.deps = deps;
|
|
1557
1888
|
}
|
|
1558
1889
|
async collect(context) {
|
|
1559
|
-
const
|
|
1890
|
+
const stageId = this.oracleStageId(context);
|
|
1891
|
+
if (!stageId) {
|
|
1892
|
+
const reason = `No stage downstream of ${context.stageId} declares an \`oracle_passed\` gate, ` + "so there is no held test for this gate to grade. A red gate needs a later stage that runs the same test green.";
|
|
1893
|
+
log.warn(TAG5, `${reason} — blocked (config)`);
|
|
1894
|
+
return {
|
|
1895
|
+
result: "blocked",
|
|
1896
|
+
structured: { reason, ...GATE_CONFIG_ERROR_MARK }
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
|
|
1560
1900
|
if (!oracle) {
|
|
1561
|
-
log.info(
|
|
1901
|
+
log.info(TAG5, `No oracle held for stage ${stageId} — blocked`);
|
|
1562
1902
|
return {
|
|
1563
1903
|
result: "blocked",
|
|
1564
1904
|
structured: {
|
|
1565
|
-
reason: `No oracle is held for stage ${
|
|
1905
|
+
reason: `No oracle is held for stage ${stageId}.`
|
|
1566
1906
|
}
|
|
1567
1907
|
};
|
|
1568
1908
|
}
|
|
@@ -1575,28 +1915,24 @@ class OracleCollector {
|
|
|
1575
1915
|
};
|
|
1576
1916
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1577
1917
|
try {
|
|
1578
|
-
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1918
|
+
const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1579
1919
|
const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
|
|
1580
1920
|
${output}`;
|
|
1581
1921
|
if (exitCode === 0) {
|
|
1582
|
-
log.info(
|
|
1922
|
+
log.info(TAG5, logLine);
|
|
1583
1923
|
} else {
|
|
1584
|
-
log.warn(
|
|
1924
|
+
log.warn(TAG5, logLine);
|
|
1585
1925
|
}
|
|
1586
|
-
return {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
};
|
|
1926
|
+
return this.verdict({
|
|
1927
|
+
oracle,
|
|
1928
|
+
identity,
|
|
1929
|
+
exitCode,
|
|
1930
|
+
output,
|
|
1931
|
+
report: report ?? null
|
|
1932
|
+
});
|
|
1597
1933
|
} catch (err) {
|
|
1598
1934
|
const message = errText(err);
|
|
1599
|
-
log.warn(
|
|
1935
|
+
log.warn(TAG5, `Oracle run threw: ${message} — blocked`);
|
|
1600
1936
|
return {
|
|
1601
1937
|
result: "blocked",
|
|
1602
1938
|
structured: {
|
|
@@ -1613,16 +1949,74 @@ ${output}`;
|
|
|
1613
1949
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1614
1950
|
return;
|
|
1615
1951
|
} catch (err) {
|
|
1616
|
-
log.warn(
|
|
1952
|
+
log.warn(TAG5, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
|
|
1617
1953
|
}
|
|
1618
1954
|
try {
|
|
1619
1955
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1620
|
-
log.info(
|
|
1956
|
+
log.info(TAG5, `Held test at ${oracle.path} removed on the second attempt`);
|
|
1621
1957
|
} catch (err) {
|
|
1622
|
-
log.error(
|
|
1958
|
+
log.error(TAG5, `HELD TEST NOT REMOVED: ${oracle.path} is still in ${this.deps.repoPath} after two attempts (${errText(err)}). ` + "It will be auto-committed by the completion path if it is left there — delete it by hand and check whether it reached a commit.");
|
|
1623
1959
|
}
|
|
1624
1960
|
}
|
|
1625
1961
|
}
|
|
1962
|
+
|
|
1963
|
+
class OracleCollector extends HeldOracleCollector {
|
|
1964
|
+
kind = "oracle_passed";
|
|
1965
|
+
oracleStageId(context) {
|
|
1966
|
+
return context.stageId;
|
|
1967
|
+
}
|
|
1968
|
+
verdict({
|
|
1969
|
+
oracle,
|
|
1970
|
+
identity,
|
|
1971
|
+
exitCode
|
|
1972
|
+
}) {
|
|
1973
|
+
return {
|
|
1974
|
+
result: exitCode === 0 ? "passed" : "failed",
|
|
1975
|
+
structured: {
|
|
1976
|
+
oracle: {
|
|
1977
|
+
exitCode,
|
|
1978
|
+
path: oracle.path,
|
|
1979
|
+
...identity,
|
|
1980
|
+
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
class OracleRedCollector extends HeldOracleCollector {
|
|
1988
|
+
kind = "oracle_red";
|
|
1989
|
+
oracleStageId() {
|
|
1990
|
+
return this.deps.targetStageId ?? null;
|
|
1991
|
+
}
|
|
1992
|
+
verdict({
|
|
1993
|
+
oracle,
|
|
1994
|
+
identity,
|
|
1995
|
+
exitCode,
|
|
1996
|
+
report
|
|
1997
|
+
}) {
|
|
1998
|
+
const graded = gradeOracleRed(report, exitCode);
|
|
1999
|
+
const base = { exitCode, path: oracle.path, ...identity };
|
|
2000
|
+
if (graded.outcome === "no_verdict") {
|
|
2001
|
+
log.warn(TAG5, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
|
|
2002
|
+
return {
|
|
2003
|
+
result: "blocked",
|
|
2004
|
+
structured: { oracle: base, reason: graded.reason }
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
return {
|
|
2008
|
+
result: graded.outcome === "reproduced" ? "passed" : "failed",
|
|
2009
|
+
structured: {
|
|
2010
|
+
oracle: {
|
|
2011
|
+
...base,
|
|
2012
|
+
testsRun: graded.summary.total,
|
|
2013
|
+
testsFailed: graded.summary.failed,
|
|
2014
|
+
output: "withheld — oracle_red is a secrecy gate; see the motor's local log"
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1626
2020
|
function errText(err) {
|
|
1627
2021
|
return err instanceof Error ? err.message : String(err);
|
|
1628
2022
|
}
|
|
@@ -1635,7 +2029,7 @@ import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_proce
|
|
|
1635
2029
|
init_log();
|
|
1636
2030
|
import { execFileSync } from "node:child_process";
|
|
1637
2031
|
import { existsSync } from "node:fs";
|
|
1638
|
-
var
|
|
2032
|
+
var TAG6 = "pm";
|
|
1639
2033
|
var cached = null;
|
|
1640
2034
|
function detectPackageManager() {
|
|
1641
2035
|
if (cached)
|
|
@@ -1657,20 +2051,21 @@ function detectPackageManager() {
|
|
|
1657
2051
|
} else {
|
|
1658
2052
|
cached = "npm";
|
|
1659
2053
|
}
|
|
1660
|
-
log.info(
|
|
2054
|
+
log.info(TAG6, `Detected package manager: ${cached}`);
|
|
1661
2055
|
return cached;
|
|
1662
2056
|
}
|
|
1663
|
-
function installCommand() {
|
|
2057
|
+
function installCommand(ignoreScripts = false) {
|
|
1664
2058
|
const pm = detectPackageManager();
|
|
2059
|
+
const skip = ignoreScripts ? " --ignore-scripts" : "";
|
|
1665
2060
|
switch (pm) {
|
|
1666
2061
|
case "bun":
|
|
1667
|
-
return
|
|
2062
|
+
return `bun install --frozen-lockfile${skip}`;
|
|
1668
2063
|
case "pnpm":
|
|
1669
|
-
return
|
|
2064
|
+
return `pnpm install --frozen-lockfile${skip}`;
|
|
1670
2065
|
case "yarn":
|
|
1671
|
-
return
|
|
2066
|
+
return `yarn install --frozen-lockfile${skip}`;
|
|
1672
2067
|
case "npm":
|
|
1673
|
-
return
|
|
2068
|
+
return `npm ci${skip}`;
|
|
1674
2069
|
}
|
|
1675
2070
|
}
|
|
1676
2071
|
function spawnRunArgs(script, ...extra) {
|
|
@@ -1684,8 +2079,8 @@ function spawnRunArgs(script, ...extra) {
|
|
|
1684
2079
|
// src/project-type.ts
|
|
1685
2080
|
init_log();
|
|
1686
2081
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1687
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
1688
|
-
var
|
|
2082
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2083
|
+
var TAG7 = "project-type";
|
|
1689
2084
|
var _cache = new Map;
|
|
1690
2085
|
function _resetCache() {
|
|
1691
2086
|
_cache.clear();
|
|
@@ -1696,7 +2091,7 @@ function detect(dir) {
|
|
|
1696
2091
|
return cached2;
|
|
1697
2092
|
const result = detectUncached(dir);
|
|
1698
2093
|
_cache.set(dir, result);
|
|
1699
|
-
log.info(
|
|
2094
|
+
log.info(TAG7, `Detected project type in ${dir}: ${result.kind}`);
|
|
1700
2095
|
return result;
|
|
1701
2096
|
}
|
|
1702
2097
|
function detectUncached(dir) {
|
|
@@ -1787,16 +2182,16 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
1787
2182
|
function hasNodeTestScript(dir) {
|
|
1788
2183
|
let script;
|
|
1789
2184
|
try {
|
|
1790
|
-
const pkg = JSON.parse(
|
|
2185
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1791
2186
|
script = pkg.scripts?.test;
|
|
1792
2187
|
} catch (err) {
|
|
1793
|
-
log.warn(
|
|
2188
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1794
2189
|
return false;
|
|
1795
2190
|
}
|
|
1796
2191
|
if (typeof script !== "string" || script.trim().length === 0)
|
|
1797
2192
|
return false;
|
|
1798
2193
|
if (NPM_PLACEHOLDER_TEST.test(script)) {
|
|
1799
|
-
log.info(
|
|
2194
|
+
log.info(TAG7, `package.json 'test' is the npm placeholder — skipping tests`);
|
|
1800
2195
|
return false;
|
|
1801
2196
|
}
|
|
1802
2197
|
return true;
|
|
@@ -1804,10 +2199,10 @@ function hasNodeTestScript(dir) {
|
|
|
1804
2199
|
function firstNodeScript(dir, candidates) {
|
|
1805
2200
|
let scripts;
|
|
1806
2201
|
try {
|
|
1807
|
-
const pkg = JSON.parse(
|
|
2202
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1808
2203
|
scripts = pkg.scripts ?? {};
|
|
1809
2204
|
} catch (err) {
|
|
1810
|
-
log.warn(
|
|
2205
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1811
2206
|
return null;
|
|
1812
2207
|
}
|
|
1813
2208
|
for (const name of candidates) {
|
|
@@ -1826,7 +2221,7 @@ function xcodeBuildCommand(pt) {
|
|
|
1826
2221
|
return null;
|
|
1827
2222
|
const scheme = resolveXcodeScheme(pt);
|
|
1828
2223
|
if (!scheme) {
|
|
1829
|
-
log.warn(
|
|
2224
|
+
log.warn(TAG7, "Could not resolve an Xcode scheme — skipping build (best-effort)");
|
|
1830
2225
|
return null;
|
|
1831
2226
|
}
|
|
1832
2227
|
const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
|
|
@@ -1854,7 +2249,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1854
2249
|
const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
|
|
1855
2250
|
return schemes[0] ?? null;
|
|
1856
2251
|
} catch (err) {
|
|
1857
|
-
log.warn(
|
|
2252
|
+
log.warn(TAG7, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
|
|
1858
2253
|
return null;
|
|
1859
2254
|
}
|
|
1860
2255
|
}
|
|
@@ -1862,7 +2257,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1862
2257
|
// src/revert-guard.ts
|
|
1863
2258
|
init_log();
|
|
1864
2259
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1865
|
-
var
|
|
2260
|
+
var TAG8 = "revert-guard";
|
|
1866
2261
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
1867
2262
|
function isTestFile(path) {
|
|
1868
2263
|
return TEST_FILE.test(path);
|
|
@@ -1877,7 +2272,7 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
1877
2272
|
stdio: "pipe"
|
|
1878
2273
|
});
|
|
1879
2274
|
} catch {
|
|
1880
|
-
log.warn(
|
|
2275
|
+
log.warn(TAG8, "Failed to re-fetch base for revert guard — using last fetch");
|
|
1881
2276
|
}
|
|
1882
2277
|
}
|
|
1883
2278
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
@@ -1886,7 +2281,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
|
1886
2281
|
return out.split(`
|
|
1887
2282
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1888
2283
|
} catch (err) {
|
|
1889
|
-
log.warn(
|
|
2284
|
+
log.warn(TAG8, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
1890
2285
|
return [];
|
|
1891
2286
|
}
|
|
1892
2287
|
}
|
|
@@ -1896,7 +2291,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
1896
2291
|
}
|
|
1897
2292
|
|
|
1898
2293
|
// src/verification.ts
|
|
1899
|
-
var
|
|
2294
|
+
var TAG9 = "verification";
|
|
1900
2295
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
1901
2296
|
async function runVerification(worktreePath, config, workerId) {
|
|
1902
2297
|
const result = {
|
|
@@ -1908,52 +2303,52 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1908
2303
|
revertWarnings: []
|
|
1909
2304
|
};
|
|
1910
2305
|
if (config.verification.revertGuard) {
|
|
1911
|
-
log.info(
|
|
2306
|
+
log.info(TAG9, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
1912
2307
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
1913
2308
|
if (deletedTests.length > 0) {
|
|
1914
2309
|
result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
|
|
1915
|
-
log.warn(
|
|
2310
|
+
log.warn(TAG9, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
1916
2311
|
result.passed = false;
|
|
1917
2312
|
} else {
|
|
1918
|
-
log.info(
|
|
2313
|
+
log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
|
|
1919
2314
|
}
|
|
1920
2315
|
}
|
|
1921
2316
|
if (config.verification.build) {
|
|
1922
|
-
log.info(
|
|
2317
|
+
log.info(TAG9, `[worker:${workerId}] Running build...`);
|
|
1923
2318
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
1924
2319
|
if (result.buildErrors.length > 0) {
|
|
1925
|
-
log.warn(
|
|
2320
|
+
log.warn(TAG9, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
1926
2321
|
result.passed = false;
|
|
1927
2322
|
} else {
|
|
1928
|
-
log.info(
|
|
2323
|
+
log.info(TAG9, `[worker:${workerId}] Build passed`);
|
|
1929
2324
|
}
|
|
1930
2325
|
}
|
|
1931
2326
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
1932
|
-
log.info(
|
|
2327
|
+
log.info(TAG9, `[worker:${workerId}] Running tests...`);
|
|
1933
2328
|
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
1934
2329
|
if (result.testFailures.length > 0) {
|
|
1935
|
-
log.warn(
|
|
2330
|
+
log.warn(TAG9, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
1936
2331
|
result.passed = false;
|
|
1937
2332
|
} else {
|
|
1938
|
-
log.info(
|
|
2333
|
+
log.info(TAG9, `[worker:${workerId}] Tests passed`);
|
|
1939
2334
|
}
|
|
1940
2335
|
}
|
|
1941
2336
|
if (config.verification.lint) {
|
|
1942
|
-
log.info(
|
|
2337
|
+
log.info(TAG9, `[worker:${workerId}] Running lint...`);
|
|
1943
2338
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
1944
2339
|
if (result.lintWarnings.length > 0) {
|
|
1945
|
-
log.warn(
|
|
2340
|
+
log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
1946
2341
|
} else {
|
|
1947
|
-
log.info(
|
|
2342
|
+
log.info(TAG9, `[worker:${workerId}] Lint passed`);
|
|
1948
2343
|
}
|
|
1949
2344
|
}
|
|
1950
2345
|
if (config.verification.deepReview) {
|
|
1951
|
-
log.info(
|
|
2346
|
+
log.info(TAG9, `[worker:${workerId}] Running deep review...`);
|
|
1952
2347
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
1953
2348
|
if (result.reviewFindings.length > 0) {
|
|
1954
|
-
log.warn(
|
|
2349
|
+
log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
1955
2350
|
} else {
|
|
1956
|
-
log.info(
|
|
2351
|
+
log.info(TAG9, `[worker:${workerId}] Deep review passed`);
|
|
1957
2352
|
}
|
|
1958
2353
|
}
|
|
1959
2354
|
return result;
|
|
@@ -1961,7 +2356,7 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1961
2356
|
function runBuild(worktreePath, timeout) {
|
|
1962
2357
|
const command = buildCommand(worktreePath);
|
|
1963
2358
|
if (!command) {
|
|
1964
|
-
log.warn(
|
|
2359
|
+
log.warn(TAG9, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
1965
2360
|
return [];
|
|
1966
2361
|
}
|
|
1967
2362
|
try {
|
|
@@ -1979,7 +2374,7 @@ function runBuild(worktreePath, timeout) {
|
|
|
1979
2374
|
function runTests(worktreePath, timeout) {
|
|
1980
2375
|
const command = testCommand(worktreePath);
|
|
1981
2376
|
if (!command) {
|
|
1982
|
-
log.warn(
|
|
2377
|
+
log.warn(TAG9, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
1983
2378
|
return [];
|
|
1984
2379
|
}
|
|
1985
2380
|
try {
|
|
@@ -1992,7 +2387,7 @@ function runTests(worktreePath, timeout) {
|
|
|
1992
2387
|
return [];
|
|
1993
2388
|
} catch (err) {
|
|
1994
2389
|
const output = combineOutput(err);
|
|
1995
|
-
log.warn(
|
|
2390
|
+
log.warn(TAG9, `Test run failed:
|
|
1996
2391
|
${output.slice(-4000) || "(no output captured)"}`);
|
|
1997
2392
|
return parseTestFailures(err, timeout);
|
|
1998
2393
|
}
|
|
@@ -2008,15 +2403,15 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
2008
2403
|
stdio: "pipe",
|
|
2009
2404
|
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2010
2405
|
});
|
|
2011
|
-
log.info(
|
|
2406
|
+
log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2012
2407
|
} catch (err) {
|
|
2013
|
-
log.warn(
|
|
2408
|
+
log.warn(TAG9, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2014
2409
|
}
|
|
2015
2410
|
}
|
|
2016
2411
|
function runLint(worktreePath, timeout) {
|
|
2017
2412
|
const command = lintCommand(worktreePath);
|
|
2018
2413
|
if (!command) {
|
|
2019
|
-
log.info(
|
|
2414
|
+
log.info(TAG9, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2020
2415
|
return [];
|
|
2021
2416
|
}
|
|
2022
2417
|
try {
|
|
@@ -2033,7 +2428,7 @@ function runLint(worktreePath, timeout) {
|
|
|
2033
2428
|
}
|
|
2034
2429
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2035
2430
|
if (!supportsDevServer(worktreePath)) {
|
|
2036
|
-
log.info(
|
|
2431
|
+
log.info(TAG9, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2037
2432
|
return [];
|
|
2038
2433
|
}
|
|
2039
2434
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2048,7 +2443,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2048
2443
|
await waitForDevServer(devServer, 30000);
|
|
2049
2444
|
await probeDevServer(port);
|
|
2050
2445
|
} catch (err) {
|
|
2051
|
-
log.error(
|
|
2446
|
+
log.error(TAG9, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2052
2447
|
return [];
|
|
2053
2448
|
}
|
|
2054
2449
|
let diff = "";
|
|
@@ -2093,7 +2488,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2093
2488
|
});
|
|
2094
2489
|
return parseReviewFindings(output);
|
|
2095
2490
|
} catch (err) {
|
|
2096
|
-
log.error(
|
|
2491
|
+
log.error(TAG9, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2097
2492
|
return [];
|
|
2098
2493
|
} finally {
|
|
2099
2494
|
if (devServer && !devServer.killed) {
|
|
@@ -2132,7 +2527,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2132
2527
|
"--",
|
|
2133
2528
|
fixPrompt
|
|
2134
2529
|
];
|
|
2135
|
-
log.info(
|
|
2530
|
+
log.info(TAG9, "Spawning Claude for auto-fix...");
|
|
2136
2531
|
execFileSync4("claude", args, {
|
|
2137
2532
|
cwd: worktreePath,
|
|
2138
2533
|
timeout: config.verification.timeout,
|
|
@@ -2170,7 +2565,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2170
2565
|
try {
|
|
2171
2566
|
await client.createSubtask(cardId, title);
|
|
2172
2567
|
} catch (err) {
|
|
2173
|
-
log.error(
|
|
2568
|
+
log.error(TAG9, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2174
2569
|
}
|
|
2175
2570
|
}));
|
|
2176
2571
|
if (overflow > 0) {
|
|
@@ -2178,7 +2573,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2178
2573
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2179
2574
|
} catch {}
|
|
2180
2575
|
}
|
|
2181
|
-
log.info(
|
|
2576
|
+
log.info(TAG9, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2182
2577
|
}
|
|
2183
2578
|
function combineOutput(err) {
|
|
2184
2579
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2238,7 +2633,7 @@ class DevServerReadinessError extends Error {
|
|
|
2238
2633
|
}
|
|
2239
2634
|
}
|
|
2240
2635
|
function waitForDevServer(proc, timeout) {
|
|
2241
|
-
return new Promise((
|
|
2636
|
+
return new Promise((resolve2, reject) => {
|
|
2242
2637
|
let settled = false;
|
|
2243
2638
|
const cleanup = () => {
|
|
2244
2639
|
proc.stdout?.off("data", onData);
|
|
@@ -2252,7 +2647,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
2252
2647
|
return;
|
|
2253
2648
|
settled = true;
|
|
2254
2649
|
cleanup();
|
|
2255
|
-
|
|
2650
|
+
resolve2();
|
|
2256
2651
|
};
|
|
2257
2652
|
const settleReject = (err) => {
|
|
2258
2653
|
if (settled)
|
|
@@ -2302,7 +2697,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2302
2697
|
}
|
|
2303
2698
|
|
|
2304
2699
|
// src/gate-collectors.ts
|
|
2305
|
-
var
|
|
2700
|
+
var TAG10 = "gate-collectors";
|
|
2306
2701
|
async function resolveStageGate(client, card) {
|
|
2307
2702
|
const currentStage = card.current_stage;
|
|
2308
2703
|
const playbookId = card.playbook_id;
|
|
@@ -2320,7 +2715,7 @@ async function resolveStageGate(client, card) {
|
|
|
2320
2715
|
return null;
|
|
2321
2716
|
return { stage: resolution.stage, gate };
|
|
2322
2717
|
} catch (err) {
|
|
2323
|
-
log.warn(
|
|
2718
|
+
log.warn(TAG10, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
2324
2719
|
return null;
|
|
2325
2720
|
}
|
|
2326
2721
|
}
|
|
@@ -2445,13 +2840,14 @@ function buildGateCollectorRegistry(deps) {
|
|
|
2445
2840
|
}
|
|
2446
2841
|
if (deps.oracle) {
|
|
2447
2842
|
registry.oracle_passed = new OracleCollector(deps.oracle);
|
|
2843
|
+
registry.oracle_red = new OracleRedCollector(deps.oracle);
|
|
2448
2844
|
}
|
|
2449
2845
|
return registry;
|
|
2450
2846
|
}
|
|
2451
2847
|
async function collectGateEvidence(registry, context) {
|
|
2452
2848
|
const collector = registry[context.gate.kind];
|
|
2453
2849
|
if (!collector) {
|
|
2454
|
-
log.info(
|
|
2850
|
+
log.info(TAG10, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
2455
2851
|
return {
|
|
2456
2852
|
result: "blocked",
|
|
2457
2853
|
structured: {
|
|
@@ -2463,14 +2859,14 @@ async function collectGateEvidence(registry, context) {
|
|
|
2463
2859
|
return await collector.collect(context);
|
|
2464
2860
|
} catch (err) {
|
|
2465
2861
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2466
|
-
log.warn(
|
|
2862
|
+
log.warn(TAG10, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
2467
2863
|
return { result: "blocked", structured: { error: msg } };
|
|
2468
2864
|
}
|
|
2469
2865
|
}
|
|
2470
2866
|
|
|
2471
2867
|
// src/harmony-client.ts
|
|
2472
2868
|
init_log();
|
|
2473
|
-
var
|
|
2869
|
+
var TAG11 = "harmony-client";
|
|
2474
2870
|
function readClientConfig(env) {
|
|
2475
2871
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
2476
2872
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -2522,7 +2918,7 @@ class HarmonyClient {
|
|
|
2522
2918
|
purpose: "gate_evaluation"
|
|
2523
2919
|
});
|
|
2524
2920
|
if (!response.ok) {
|
|
2525
|
-
log.warn(
|
|
2921
|
+
log.warn(TAG11, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
2526
2922
|
return null;
|
|
2527
2923
|
}
|
|
2528
2924
|
const body = await response.json();
|
|
@@ -2603,140 +2999,6 @@ function relayAgentEvent(draft) {
|
|
|
2603
2999
|
return { type: "agent_event", event: draft };
|
|
2604
3000
|
}
|
|
2605
3001
|
|
|
2606
|
-
// src/oracle.ts
|
|
2607
|
-
import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2608
|
-
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
2609
|
-
async function resolveContained(repoPath, relativePath) {
|
|
2610
|
-
if (isAbsolute(relativePath)) {
|
|
2611
|
-
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
2612
|
-
}
|
|
2613
|
-
if (relativePath === "" || relativePath === ".") {
|
|
2614
|
-
throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
|
|
2615
|
-
}
|
|
2616
|
-
const root = await realpath(repoPath);
|
|
2617
|
-
const target = resolve(root, relativePath);
|
|
2618
|
-
if (target !== root && !target.startsWith(root + sep)) {
|
|
2619
|
-
throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
|
|
2620
|
-
}
|
|
2621
|
-
let cursor = root;
|
|
2622
|
-
for (const segment of relativePath.split("/")) {
|
|
2623
|
-
cursor = resolve(cursor, segment);
|
|
2624
|
-
const stat = await lstat(cursor).catch(() => null);
|
|
2625
|
-
if (stat?.isSymbolicLink()) {
|
|
2626
|
-
throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
return target;
|
|
2630
|
-
}
|
|
2631
|
-
async function place(repoPath, oracle) {
|
|
2632
|
-
const target = await resolveContained(repoPath, oracle.path);
|
|
2633
|
-
await mkdir(dirname(target), { recursive: true });
|
|
2634
|
-
await writeFile(target, oracle.content, "utf8");
|
|
2635
|
-
}
|
|
2636
|
-
async function remove(repoPath, oracle) {
|
|
2637
|
-
const target = await resolveContained(repoPath, oracle.path);
|
|
2638
|
-
await rm(target, { force: true });
|
|
2639
|
-
}
|
|
2640
|
-
var ORACLE_RUNNERS = {
|
|
2641
|
-
vitest: (path) => ({
|
|
2642
|
-
command: "npx",
|
|
2643
|
-
args: ["--no-install", "vitest", "run", path]
|
|
2644
|
-
}),
|
|
2645
|
-
bun: (path) => ({ command: "bun", args: ["test", path] })
|
|
2646
|
-
};
|
|
2647
|
-
var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
|
|
2648
|
-
var ORACLE_OUTPUT_LIMIT = 64 * 1024;
|
|
2649
|
-
var ORACLE_SIGINT_GRACE_MS = 2000;
|
|
2650
|
-
var ORACLE_SIGTERM_GRACE_MS = 3000;
|
|
2651
|
-
var ORACLE_DRAIN_GRACE_MS = 500;
|
|
2652
|
-
function resolveOracleRunner(oracle) {
|
|
2653
|
-
const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
|
|
2654
|
-
const build = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
|
|
2655
|
-
if (!build) {
|
|
2656
|
-
throw new Error(`refusing to run the held test: runner_hint ${JSON.stringify(oracle.runnerHint)} is not in the motor's allow-list (${ORACLE_RUNNER_HINTS.join(", ")})`);
|
|
2657
|
-
}
|
|
2658
|
-
return build(argvPath(oracle.path));
|
|
2659
|
-
}
|
|
2660
|
-
function argvPath(path) {
|
|
2661
|
-
return path.startsWith("./") ? path : `./${path}`;
|
|
2662
|
-
}
|
|
2663
|
-
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
2664
|
-
const { command, args } = resolveOracleRunner(oracle);
|
|
2665
|
-
return await new Promise((settleOk, settleErr) => {
|
|
2666
|
-
let child;
|
|
2667
|
-
try {
|
|
2668
|
-
child = spawnInGroup(command, args, {
|
|
2669
|
-
cwd: repoPath,
|
|
2670
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2671
|
-
});
|
|
2672
|
-
} catch (err) {
|
|
2673
|
-
settleErr(err);
|
|
2674
|
-
return;
|
|
2675
|
-
}
|
|
2676
|
-
const pgid = child.pid;
|
|
2677
|
-
let output = "";
|
|
2678
|
-
let settled = false;
|
|
2679
|
-
let killing = false;
|
|
2680
|
-
let timer;
|
|
2681
|
-
let drainTimer;
|
|
2682
|
-
const settle = (failure, result) => {
|
|
2683
|
-
if (settled)
|
|
2684
|
-
return;
|
|
2685
|
-
settled = true;
|
|
2686
|
-
if (timer)
|
|
2687
|
-
clearTimeout(timer);
|
|
2688
|
-
if (drainTimer)
|
|
2689
|
-
clearTimeout(drainTimer);
|
|
2690
|
-
reapGroup(pgid);
|
|
2691
|
-
if (failure)
|
|
2692
|
-
settleErr(failure);
|
|
2693
|
-
else
|
|
2694
|
-
settleOk(result);
|
|
2695
|
-
};
|
|
2696
|
-
const append = (chunk) => {
|
|
2697
|
-
if (output.length >= ORACLE_OUTPUT_LIMIT)
|
|
2698
|
-
return;
|
|
2699
|
-
output += chunk.toString("utf8");
|
|
2700
|
-
if (output.length > ORACLE_OUTPUT_LIMIT) {
|
|
2701
|
-
output = `${output.slice(0, ORACLE_OUTPUT_LIMIT)}
|
|
2702
|
-
… output truncated at ${ORACLE_OUTPUT_LIMIT} characters`;
|
|
2703
|
-
}
|
|
2704
|
-
};
|
|
2705
|
-
child.stdout?.on("data", append);
|
|
2706
|
-
child.stderr?.on("data", append);
|
|
2707
|
-
child.once("error", (err) => settle(err));
|
|
2708
|
-
const settleFromExit = (code, signal) => {
|
|
2709
|
-
if (drainTimer)
|
|
2710
|
-
clearTimeout(drainTimer);
|
|
2711
|
-
if (code === null) {
|
|
2712
|
-
settle(new Error(`the held test was terminated by signal ${signal}`));
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
settle(null, { exitCode: code, output });
|
|
2716
|
-
};
|
|
2717
|
-
child.once("exit", (code, signal) => {
|
|
2718
|
-
if (killing)
|
|
2719
|
-
return;
|
|
2720
|
-
if (timer)
|
|
2721
|
-
clearTimeout(timer);
|
|
2722
|
-
reapGroup(pgid);
|
|
2723
|
-
drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
|
|
2724
|
-
child.once("close", () => settleFromExit(code, signal));
|
|
2725
|
-
});
|
|
2726
|
-
timer = setTimeout(() => {
|
|
2727
|
-
if (settled)
|
|
2728
|
-
return;
|
|
2729
|
-
killing = true;
|
|
2730
|
-
terminateGroup(child, {
|
|
2731
|
-
sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
|
|
2732
|
-
sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
|
|
2733
|
-
}).catch(() => {}).then(() => {
|
|
2734
|
-
settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
|
|
2735
|
-
});
|
|
2736
|
-
}, timeoutMs);
|
|
2737
|
-
});
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
3002
|
// src/stage-cli.ts
|
|
2741
3003
|
init_dist();
|
|
2742
3004
|
|
|
@@ -2985,7 +3247,7 @@ async function runStage(request, deps) {
|
|
|
2985
3247
|
}
|
|
2986
3248
|
|
|
2987
3249
|
// src/cli.ts
|
|
2988
|
-
var
|
|
3250
|
+
var TAG12 = "cli";
|
|
2989
3251
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2990
3252
|
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
2991
3253
|
var activeRunner = null;
|
|
@@ -3016,12 +3278,12 @@ async function runRole(request, prompt, emit2) {
|
|
|
3016
3278
|
});
|
|
3017
3279
|
const runner = new SdkAgentRunner(launch.config);
|
|
3018
3280
|
activeRunner = runner;
|
|
3019
|
-
log.info(
|
|
3281
|
+
log.info(TAG12, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
3020
3282
|
const timeoutMs = stageTimeoutMs(process.env);
|
|
3021
3283
|
let timedOut = false;
|
|
3022
3284
|
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3023
3285
|
timedOut = true;
|
|
3024
|
-
log.warn(
|
|
3286
|
+
log.warn(TAG12, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3025
3287
|
runner.stop("timeout");
|
|
3026
3288
|
}, timeoutMs) : null;
|
|
3027
3289
|
clock?.unref?.();
|
|
@@ -3037,9 +3299,9 @@ async function runRole(request, prompt, emit2) {
|
|
|
3037
3299
|
if (relayed)
|
|
3038
3300
|
emit2(relayed);
|
|
3039
3301
|
if (event.kind === "error") {
|
|
3040
|
-
log.warn(
|
|
3302
|
+
log.warn(TAG12, `subagent error: ${event.payload.message}`);
|
|
3041
3303
|
} else {
|
|
3042
|
-
log.event(
|
|
3304
|
+
log.event(TAG12, `subagent ${event.kind}`);
|
|
3043
3305
|
}
|
|
3044
3306
|
}
|
|
3045
3307
|
} finally {
|
|
@@ -3070,8 +3332,8 @@ ${STAGE_RUN_USAGE}
|
|
|
3070
3332
|
const { cardId, stageId, workspaceId, repoPath, sessionId, metricsPath } = parsed.args;
|
|
3071
3333
|
let driverMetrics = {};
|
|
3072
3334
|
if (metricsPath !== null) {
|
|
3073
|
-
const { readFileSync:
|
|
3074
|
-
driverMetrics = parseMetricsAllowlist(
|
|
3335
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
3336
|
+
driverMetrics = parseMetricsAllowlist(readFileSync3(metricsPath, "utf8"), metricsPath);
|
|
3075
3337
|
}
|
|
3076
3338
|
const client = new HarmonyClient(readClientConfig(process.env));
|
|
3077
3339
|
const card = await client.fetchStageCard(cardId);
|
|
@@ -3085,12 +3347,13 @@ ${STAGE_RUN_USAGE}
|
|
|
3085
3347
|
throw new Error(`refusing to run stage "${stageId}" for card ${cardId}: ${pinned.reason}`);
|
|
3086
3348
|
}
|
|
3087
3349
|
assertStageIsAgentRunnable(pinned.stage);
|
|
3350
|
+
const oracleTargetStageId = findOracleTargetStage(version, stageId);
|
|
3088
3351
|
const prompt = buildStagePrompt({
|
|
3089
3352
|
cardId,
|
|
3090
3353
|
stageId,
|
|
3091
3354
|
stage: pinned.stage,
|
|
3092
3355
|
sessionId,
|
|
3093
|
-
oracleTargetStageId
|
|
3356
|
+
oracleTargetStageId
|
|
3094
3357
|
});
|
|
3095
3358
|
const request = {
|
|
3096
3359
|
cardId,
|
|
@@ -3118,6 +3381,7 @@ ${STAGE_RUN_USAGE}
|
|
|
3118
3381
|
oracle: {
|
|
3119
3382
|
repoPath: req.repoPath,
|
|
3120
3383
|
sessionId: req.sessionId,
|
|
3384
|
+
targetStageId: oracleTargetStageId,
|
|
3121
3385
|
fetchOracle: (oracleCardId, oracleStageId, oracleSessionId) => client.fetchOracle(oracleCardId, oracleStageId, oracleSessionId),
|
|
3122
3386
|
place,
|
|
3123
3387
|
remove,
|