@gethmy/harness 1.2.0 → 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 +495 -225
- package/dist/index.js +1242 -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/src/worktree.ts +30 -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)));
|
|
@@ -81,6 +84,11 @@ var init_constants = __esm(() => {
|
|
|
81
84
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
82
85
|
};
|
|
83
86
|
});
|
|
87
|
+
// ../harmony-shared/dist/fanoutSource.js
|
|
88
|
+
var FANOUT_KEY_MARKER = "harmony:fanout-item", FANOUT_KEY_RE;
|
|
89
|
+
var init_fanoutSource = __esm(() => {
|
|
90
|
+
FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
|
|
91
|
+
});
|
|
84
92
|
// ../harmony-shared/dist/gateConfigError.js
|
|
85
93
|
function gateConfigErrorReason(evaluation) {
|
|
86
94
|
if (!evaluation || evaluation.passed)
|
|
@@ -388,7 +396,8 @@ var init_gateEvaluate = __esm(() => {
|
|
|
388
396
|
"artifact",
|
|
389
397
|
"label",
|
|
390
398
|
"custom",
|
|
391
|
-
"oracle_passed"
|
|
399
|
+
"oracle_passed",
|
|
400
|
+
"oracle_red"
|
|
392
401
|
];
|
|
393
402
|
GATE_OPERATORS = [
|
|
394
403
|
"eq",
|
|
@@ -491,11 +500,13 @@ var init_dist = __esm(() => {
|
|
|
491
500
|
init_agentStaleness();
|
|
492
501
|
init_branchRef();
|
|
493
502
|
init_cardLinks();
|
|
503
|
+
init_cardModelStat();
|
|
494
504
|
init_classification();
|
|
495
505
|
init_columnCardSplit();
|
|
496
506
|
init_columnSort();
|
|
497
507
|
init_commentSerializer();
|
|
498
508
|
init_constants();
|
|
509
|
+
init_fanoutSource();
|
|
499
510
|
init_gateConfigError();
|
|
500
511
|
init_gateEvaluate();
|
|
501
512
|
init_logger();
|
|
@@ -646,6 +657,8 @@ var AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth to
|
|
|
646
657
|
var OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
|
|
647
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;
|
|
648
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;
|
|
649
662
|
function parseRetryAfterMs(message) {
|
|
650
663
|
const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
|
|
651
664
|
if (!match)
|
|
@@ -661,12 +674,16 @@ function classifyRunError(message) {
|
|
|
661
674
|
const retryAfterMs = parseRetryAfterMs(message);
|
|
662
675
|
if (AUTH.test(message))
|
|
663
676
|
return { kind: "auth", retryAfterMs };
|
|
677
|
+
if (SPEND_LIMIT.test(message))
|
|
678
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
664
679
|
if (OUT_OF_CREDITS.test(message))
|
|
665
680
|
return { kind: "out_of_credits", retryAfterMs };
|
|
666
681
|
if (USAGE_LIMIT.test(message))
|
|
667
682
|
return { kind: "usage_limit", retryAfterMs };
|
|
668
683
|
if (RATE_LIMIT.test(message))
|
|
669
684
|
return { kind: "rate_limit", retryAfterMs };
|
|
685
|
+
if (SPEND_LIMIT_REMEDY.test(message))
|
|
686
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
670
687
|
return { kind: null };
|
|
671
688
|
}
|
|
672
689
|
function describeApiError(kind) {
|
|
@@ -679,6 +696,8 @@ function describeApiError(kind) {
|
|
|
679
696
|
return "Anthropic usage limit reached — retrying after reset";
|
|
680
697
|
case "rate_limit":
|
|
681
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`";
|
|
682
701
|
}
|
|
683
702
|
}
|
|
684
703
|
function cooldownMsFor(kind) {
|
|
@@ -691,6 +710,8 @@ function cooldownMsFor(kind) {
|
|
|
691
710
|
return 30 * 60000;
|
|
692
711
|
case "auth":
|
|
693
712
|
return 30 * 60000;
|
|
713
|
+
case "spend_limit":
|
|
714
|
+
return 30 * 60000;
|
|
694
715
|
}
|
|
695
716
|
}
|
|
696
717
|
|
|
@@ -1044,7 +1065,7 @@ ${this.capturedStderr}`);
|
|
|
1044
1065
|
kind: "error",
|
|
1045
1066
|
source: "system",
|
|
1046
1067
|
payload: {
|
|
1047
|
-
message:
|
|
1068
|
+
message: resultErrorMessage(r.subtype, joined),
|
|
1048
1069
|
errorKind: cls.kind,
|
|
1049
1070
|
retryable: cls.kind !== "auth" && cls.kind !== null
|
|
1050
1071
|
}
|
|
@@ -1055,6 +1076,13 @@ ${this.capturedStderr}`);
|
|
|
1055
1076
|
}
|
|
1056
1077
|
}
|
|
1057
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
|
+
}
|
|
1058
1086
|
function normalize(raw) {
|
|
1059
1087
|
if (raw == null)
|
|
1060
1088
|
return;
|
|
@@ -1539,24 +1567,342 @@ function truncate(value, max) {
|
|
|
1539
1567
|
init_log();
|
|
1540
1568
|
|
|
1541
1569
|
// src/oracle-collector.ts
|
|
1542
|
-
init_log();
|
|
1543
1570
|
import { createHash } from "node:crypto";
|
|
1544
|
-
|
|
1571
|
+
init_log();
|
|
1572
|
+
|
|
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
|
+
}
|
|
1545
1880
|
|
|
1546
|
-
|
|
1881
|
+
// src/oracle-collector.ts
|
|
1882
|
+
var TAG5 = "oracle-collector";
|
|
1883
|
+
|
|
1884
|
+
class HeldOracleCollector {
|
|
1547
1885
|
deps;
|
|
1548
|
-
kind = "oracle_passed";
|
|
1549
1886
|
constructor(deps) {
|
|
1550
1887
|
this.deps = deps;
|
|
1551
1888
|
}
|
|
1552
1889
|
async collect(context) {
|
|
1553
|
-
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);
|
|
1554
1900
|
if (!oracle) {
|
|
1555
|
-
log.info(
|
|
1901
|
+
log.info(TAG5, `No oracle held for stage ${stageId} — blocked`);
|
|
1556
1902
|
return {
|
|
1557
1903
|
result: "blocked",
|
|
1558
1904
|
structured: {
|
|
1559
|
-
reason: `No oracle is held for stage ${
|
|
1905
|
+
reason: `No oracle is held for stage ${stageId}.`
|
|
1560
1906
|
}
|
|
1561
1907
|
};
|
|
1562
1908
|
}
|
|
@@ -1569,28 +1915,24 @@ class OracleCollector {
|
|
|
1569
1915
|
};
|
|
1570
1916
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1571
1917
|
try {
|
|
1572
|
-
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1918
|
+
const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1573
1919
|
const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
|
|
1574
1920
|
${output}`;
|
|
1575
1921
|
if (exitCode === 0) {
|
|
1576
|
-
log.info(
|
|
1922
|
+
log.info(TAG5, logLine);
|
|
1577
1923
|
} else {
|
|
1578
|
-
log.warn(
|
|
1924
|
+
log.warn(TAG5, logLine);
|
|
1579
1925
|
}
|
|
1580
|
-
return {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1926
|
+
return this.verdict({
|
|
1927
|
+
oracle,
|
|
1928
|
+
identity,
|
|
1929
|
+
exitCode,
|
|
1930
|
+
output,
|
|
1931
|
+
report: report ?? null
|
|
1932
|
+
});
|
|
1591
1933
|
} catch (err) {
|
|
1592
1934
|
const message = errText(err);
|
|
1593
|
-
log.warn(
|
|
1935
|
+
log.warn(TAG5, `Oracle run threw: ${message} — blocked`);
|
|
1594
1936
|
return {
|
|
1595
1937
|
result: "blocked",
|
|
1596
1938
|
structured: {
|
|
@@ -1607,16 +1949,74 @@ ${output}`;
|
|
|
1607
1949
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1608
1950
|
return;
|
|
1609
1951
|
} catch (err) {
|
|
1610
|
-
log.warn(
|
|
1952
|
+
log.warn(TAG5, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
|
|
1611
1953
|
}
|
|
1612
1954
|
try {
|
|
1613
1955
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1614
|
-
log.info(
|
|
1956
|
+
log.info(TAG5, `Held test at ${oracle.path} removed on the second attempt`);
|
|
1615
1957
|
} catch (err) {
|
|
1616
|
-
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.");
|
|
1617
1959
|
}
|
|
1618
1960
|
}
|
|
1619
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
|
+
}
|
|
1620
2020
|
function errText(err) {
|
|
1621
2021
|
return err instanceof Error ? err.message : String(err);
|
|
1622
2022
|
}
|
|
@@ -1629,7 +2029,7 @@ import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_proce
|
|
|
1629
2029
|
init_log();
|
|
1630
2030
|
import { execFileSync } from "node:child_process";
|
|
1631
2031
|
import { existsSync } from "node:fs";
|
|
1632
|
-
var
|
|
2032
|
+
var TAG6 = "pm";
|
|
1633
2033
|
var cached = null;
|
|
1634
2034
|
function detectPackageManager() {
|
|
1635
2035
|
if (cached)
|
|
@@ -1651,20 +2051,21 @@ function detectPackageManager() {
|
|
|
1651
2051
|
} else {
|
|
1652
2052
|
cached = "npm";
|
|
1653
2053
|
}
|
|
1654
|
-
log.info(
|
|
2054
|
+
log.info(TAG6, `Detected package manager: ${cached}`);
|
|
1655
2055
|
return cached;
|
|
1656
2056
|
}
|
|
1657
|
-
function installCommand() {
|
|
2057
|
+
function installCommand(ignoreScripts = false) {
|
|
1658
2058
|
const pm = detectPackageManager();
|
|
2059
|
+
const skip = ignoreScripts ? " --ignore-scripts" : "";
|
|
1659
2060
|
switch (pm) {
|
|
1660
2061
|
case "bun":
|
|
1661
|
-
return
|
|
2062
|
+
return `bun install --frozen-lockfile${skip}`;
|
|
1662
2063
|
case "pnpm":
|
|
1663
|
-
return
|
|
2064
|
+
return `pnpm install --frozen-lockfile${skip}`;
|
|
1664
2065
|
case "yarn":
|
|
1665
|
-
return
|
|
2066
|
+
return `yarn install --frozen-lockfile${skip}`;
|
|
1666
2067
|
case "npm":
|
|
1667
|
-
return
|
|
2068
|
+
return `npm ci${skip}`;
|
|
1668
2069
|
}
|
|
1669
2070
|
}
|
|
1670
2071
|
function spawnRunArgs(script, ...extra) {
|
|
@@ -1678,8 +2079,8 @@ function spawnRunArgs(script, ...extra) {
|
|
|
1678
2079
|
// src/project-type.ts
|
|
1679
2080
|
init_log();
|
|
1680
2081
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1681
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
1682
|
-
var
|
|
2082
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2083
|
+
var TAG7 = "project-type";
|
|
1683
2084
|
var _cache = new Map;
|
|
1684
2085
|
function _resetCache() {
|
|
1685
2086
|
_cache.clear();
|
|
@@ -1690,7 +2091,7 @@ function detect(dir) {
|
|
|
1690
2091
|
return cached2;
|
|
1691
2092
|
const result = detectUncached(dir);
|
|
1692
2093
|
_cache.set(dir, result);
|
|
1693
|
-
log.info(
|
|
2094
|
+
log.info(TAG7, `Detected project type in ${dir}: ${result.kind}`);
|
|
1694
2095
|
return result;
|
|
1695
2096
|
}
|
|
1696
2097
|
function detectUncached(dir) {
|
|
@@ -1781,16 +2182,16 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
1781
2182
|
function hasNodeTestScript(dir) {
|
|
1782
2183
|
let script;
|
|
1783
2184
|
try {
|
|
1784
|
-
const pkg = JSON.parse(
|
|
2185
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1785
2186
|
script = pkg.scripts?.test;
|
|
1786
2187
|
} catch (err) {
|
|
1787
|
-
log.warn(
|
|
2188
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1788
2189
|
return false;
|
|
1789
2190
|
}
|
|
1790
2191
|
if (typeof script !== "string" || script.trim().length === 0)
|
|
1791
2192
|
return false;
|
|
1792
2193
|
if (NPM_PLACEHOLDER_TEST.test(script)) {
|
|
1793
|
-
log.info(
|
|
2194
|
+
log.info(TAG7, `package.json 'test' is the npm placeholder — skipping tests`);
|
|
1794
2195
|
return false;
|
|
1795
2196
|
}
|
|
1796
2197
|
return true;
|
|
@@ -1798,10 +2199,10 @@ function hasNodeTestScript(dir) {
|
|
|
1798
2199
|
function firstNodeScript(dir, candidates) {
|
|
1799
2200
|
let scripts;
|
|
1800
2201
|
try {
|
|
1801
|
-
const pkg = JSON.parse(
|
|
2202
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1802
2203
|
scripts = pkg.scripts ?? {};
|
|
1803
2204
|
} catch (err) {
|
|
1804
|
-
log.warn(
|
|
2205
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1805
2206
|
return null;
|
|
1806
2207
|
}
|
|
1807
2208
|
for (const name of candidates) {
|
|
@@ -1820,7 +2221,7 @@ function xcodeBuildCommand(pt) {
|
|
|
1820
2221
|
return null;
|
|
1821
2222
|
const scheme = resolveXcodeScheme(pt);
|
|
1822
2223
|
if (!scheme) {
|
|
1823
|
-
log.warn(
|
|
2224
|
+
log.warn(TAG7, "Could not resolve an Xcode scheme — skipping build (best-effort)");
|
|
1824
2225
|
return null;
|
|
1825
2226
|
}
|
|
1826
2227
|
const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
|
|
@@ -1848,7 +2249,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1848
2249
|
const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
|
|
1849
2250
|
return schemes[0] ?? null;
|
|
1850
2251
|
} catch (err) {
|
|
1851
|
-
log.warn(
|
|
2252
|
+
log.warn(TAG7, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
|
|
1852
2253
|
return null;
|
|
1853
2254
|
}
|
|
1854
2255
|
}
|
|
@@ -1856,7 +2257,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1856
2257
|
// src/revert-guard.ts
|
|
1857
2258
|
init_log();
|
|
1858
2259
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1859
|
-
var
|
|
2260
|
+
var TAG8 = "revert-guard";
|
|
1860
2261
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
1861
2262
|
function isTestFile(path) {
|
|
1862
2263
|
return TEST_FILE.test(path);
|
|
@@ -1871,7 +2272,7 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
1871
2272
|
stdio: "pipe"
|
|
1872
2273
|
});
|
|
1873
2274
|
} catch {
|
|
1874
|
-
log.warn(
|
|
2275
|
+
log.warn(TAG8, "Failed to re-fetch base for revert guard — using last fetch");
|
|
1875
2276
|
}
|
|
1876
2277
|
}
|
|
1877
2278
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
@@ -1880,7 +2281,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
|
1880
2281
|
return out.split(`
|
|
1881
2282
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1882
2283
|
} catch (err) {
|
|
1883
|
-
log.warn(
|
|
2284
|
+
log.warn(TAG8, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
1884
2285
|
return [];
|
|
1885
2286
|
}
|
|
1886
2287
|
}
|
|
@@ -1890,7 +2291,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
1890
2291
|
}
|
|
1891
2292
|
|
|
1892
2293
|
// src/verification.ts
|
|
1893
|
-
var
|
|
2294
|
+
var TAG9 = "verification";
|
|
1894
2295
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
1895
2296
|
async function runVerification(worktreePath, config, workerId) {
|
|
1896
2297
|
const result = {
|
|
@@ -1902,52 +2303,52 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1902
2303
|
revertWarnings: []
|
|
1903
2304
|
};
|
|
1904
2305
|
if (config.verification.revertGuard) {
|
|
1905
|
-
log.info(
|
|
2306
|
+
log.info(TAG9, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
1906
2307
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
1907
2308
|
if (deletedTests.length > 0) {
|
|
1908
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.");
|
|
1909
|
-
log.warn(
|
|
2310
|
+
log.warn(TAG9, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
1910
2311
|
result.passed = false;
|
|
1911
2312
|
} else {
|
|
1912
|
-
log.info(
|
|
2313
|
+
log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
|
|
1913
2314
|
}
|
|
1914
2315
|
}
|
|
1915
2316
|
if (config.verification.build) {
|
|
1916
|
-
log.info(
|
|
2317
|
+
log.info(TAG9, `[worker:${workerId}] Running build...`);
|
|
1917
2318
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
1918
2319
|
if (result.buildErrors.length > 0) {
|
|
1919
|
-
log.warn(
|
|
2320
|
+
log.warn(TAG9, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
1920
2321
|
result.passed = false;
|
|
1921
2322
|
} else {
|
|
1922
|
-
log.info(
|
|
2323
|
+
log.info(TAG9, `[worker:${workerId}] Build passed`);
|
|
1923
2324
|
}
|
|
1924
2325
|
}
|
|
1925
2326
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
1926
|
-
log.info(
|
|
2327
|
+
log.info(TAG9, `[worker:${workerId}] Running tests...`);
|
|
1927
2328
|
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
1928
2329
|
if (result.testFailures.length > 0) {
|
|
1929
|
-
log.warn(
|
|
2330
|
+
log.warn(TAG9, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
1930
2331
|
result.passed = false;
|
|
1931
2332
|
} else {
|
|
1932
|
-
log.info(
|
|
2333
|
+
log.info(TAG9, `[worker:${workerId}] Tests passed`);
|
|
1933
2334
|
}
|
|
1934
2335
|
}
|
|
1935
2336
|
if (config.verification.lint) {
|
|
1936
|
-
log.info(
|
|
2337
|
+
log.info(TAG9, `[worker:${workerId}] Running lint...`);
|
|
1937
2338
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
1938
2339
|
if (result.lintWarnings.length > 0) {
|
|
1939
|
-
log.warn(
|
|
2340
|
+
log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
1940
2341
|
} else {
|
|
1941
|
-
log.info(
|
|
2342
|
+
log.info(TAG9, `[worker:${workerId}] Lint passed`);
|
|
1942
2343
|
}
|
|
1943
2344
|
}
|
|
1944
2345
|
if (config.verification.deepReview) {
|
|
1945
|
-
log.info(
|
|
2346
|
+
log.info(TAG9, `[worker:${workerId}] Running deep review...`);
|
|
1946
2347
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
1947
2348
|
if (result.reviewFindings.length > 0) {
|
|
1948
|
-
log.warn(
|
|
2349
|
+
log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
1949
2350
|
} else {
|
|
1950
|
-
log.info(
|
|
2351
|
+
log.info(TAG9, `[worker:${workerId}] Deep review passed`);
|
|
1951
2352
|
}
|
|
1952
2353
|
}
|
|
1953
2354
|
return result;
|
|
@@ -1955,7 +2356,7 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1955
2356
|
function runBuild(worktreePath, timeout) {
|
|
1956
2357
|
const command = buildCommand(worktreePath);
|
|
1957
2358
|
if (!command) {
|
|
1958
|
-
log.warn(
|
|
2359
|
+
log.warn(TAG9, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
1959
2360
|
return [];
|
|
1960
2361
|
}
|
|
1961
2362
|
try {
|
|
@@ -1973,7 +2374,7 @@ function runBuild(worktreePath, timeout) {
|
|
|
1973
2374
|
function runTests(worktreePath, timeout) {
|
|
1974
2375
|
const command = testCommand(worktreePath);
|
|
1975
2376
|
if (!command) {
|
|
1976
|
-
log.warn(
|
|
2377
|
+
log.warn(TAG9, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
1977
2378
|
return [];
|
|
1978
2379
|
}
|
|
1979
2380
|
try {
|
|
@@ -1986,7 +2387,7 @@ function runTests(worktreePath, timeout) {
|
|
|
1986
2387
|
return [];
|
|
1987
2388
|
} catch (err) {
|
|
1988
2389
|
const output = combineOutput(err);
|
|
1989
|
-
log.warn(
|
|
2390
|
+
log.warn(TAG9, `Test run failed:
|
|
1990
2391
|
${output.slice(-4000) || "(no output captured)"}`);
|
|
1991
2392
|
return parseTestFailures(err, timeout);
|
|
1992
2393
|
}
|
|
@@ -2002,15 +2403,15 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
2002
2403
|
stdio: "pipe",
|
|
2003
2404
|
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2004
2405
|
});
|
|
2005
|
-
log.info(
|
|
2406
|
+
log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2006
2407
|
} catch (err) {
|
|
2007
|
-
log.warn(
|
|
2408
|
+
log.warn(TAG9, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2008
2409
|
}
|
|
2009
2410
|
}
|
|
2010
2411
|
function runLint(worktreePath, timeout) {
|
|
2011
2412
|
const command = lintCommand(worktreePath);
|
|
2012
2413
|
if (!command) {
|
|
2013
|
-
log.info(
|
|
2414
|
+
log.info(TAG9, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2014
2415
|
return [];
|
|
2015
2416
|
}
|
|
2016
2417
|
try {
|
|
@@ -2027,7 +2428,7 @@ function runLint(worktreePath, timeout) {
|
|
|
2027
2428
|
}
|
|
2028
2429
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2029
2430
|
if (!supportsDevServer(worktreePath)) {
|
|
2030
|
-
log.info(
|
|
2431
|
+
log.info(TAG9, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2031
2432
|
return [];
|
|
2032
2433
|
}
|
|
2033
2434
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2042,7 +2443,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2042
2443
|
await waitForDevServer(devServer, 30000);
|
|
2043
2444
|
await probeDevServer(port);
|
|
2044
2445
|
} catch (err) {
|
|
2045
|
-
log.error(
|
|
2446
|
+
log.error(TAG9, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2046
2447
|
return [];
|
|
2047
2448
|
}
|
|
2048
2449
|
let diff = "";
|
|
@@ -2087,7 +2488,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2087
2488
|
});
|
|
2088
2489
|
return parseReviewFindings(output);
|
|
2089
2490
|
} catch (err) {
|
|
2090
|
-
log.error(
|
|
2491
|
+
log.error(TAG9, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2091
2492
|
return [];
|
|
2092
2493
|
} finally {
|
|
2093
2494
|
if (devServer && !devServer.killed) {
|
|
@@ -2126,7 +2527,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2126
2527
|
"--",
|
|
2127
2528
|
fixPrompt
|
|
2128
2529
|
];
|
|
2129
|
-
log.info(
|
|
2530
|
+
log.info(TAG9, "Spawning Claude for auto-fix...");
|
|
2130
2531
|
execFileSync4("claude", args, {
|
|
2131
2532
|
cwd: worktreePath,
|
|
2132
2533
|
timeout: config.verification.timeout,
|
|
@@ -2164,7 +2565,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2164
2565
|
try {
|
|
2165
2566
|
await client.createSubtask(cardId, title);
|
|
2166
2567
|
} catch (err) {
|
|
2167
|
-
log.error(
|
|
2568
|
+
log.error(TAG9, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2168
2569
|
}
|
|
2169
2570
|
}));
|
|
2170
2571
|
if (overflow > 0) {
|
|
@@ -2172,7 +2573,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2172
2573
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2173
2574
|
} catch {}
|
|
2174
2575
|
}
|
|
2175
|
-
log.info(
|
|
2576
|
+
log.info(TAG9, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2176
2577
|
}
|
|
2177
2578
|
function combineOutput(err) {
|
|
2178
2579
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2232,7 +2633,7 @@ class DevServerReadinessError extends Error {
|
|
|
2232
2633
|
}
|
|
2233
2634
|
}
|
|
2234
2635
|
function waitForDevServer(proc, timeout) {
|
|
2235
|
-
return new Promise((
|
|
2636
|
+
return new Promise((resolve2, reject) => {
|
|
2236
2637
|
let settled = false;
|
|
2237
2638
|
const cleanup = () => {
|
|
2238
2639
|
proc.stdout?.off("data", onData);
|
|
@@ -2246,7 +2647,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
2246
2647
|
return;
|
|
2247
2648
|
settled = true;
|
|
2248
2649
|
cleanup();
|
|
2249
|
-
|
|
2650
|
+
resolve2();
|
|
2250
2651
|
};
|
|
2251
2652
|
const settleReject = (err) => {
|
|
2252
2653
|
if (settled)
|
|
@@ -2296,7 +2697,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2296
2697
|
}
|
|
2297
2698
|
|
|
2298
2699
|
// src/gate-collectors.ts
|
|
2299
|
-
var
|
|
2700
|
+
var TAG10 = "gate-collectors";
|
|
2300
2701
|
async function resolveStageGate(client, card) {
|
|
2301
2702
|
const currentStage = card.current_stage;
|
|
2302
2703
|
const playbookId = card.playbook_id;
|
|
@@ -2314,7 +2715,7 @@ async function resolveStageGate(client, card) {
|
|
|
2314
2715
|
return null;
|
|
2315
2716
|
return { stage: resolution.stage, gate };
|
|
2316
2717
|
} catch (err) {
|
|
2317
|
-
log.warn(
|
|
2718
|
+
log.warn(TAG10, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
2318
2719
|
return null;
|
|
2319
2720
|
}
|
|
2320
2721
|
}
|
|
@@ -2439,13 +2840,14 @@ function buildGateCollectorRegistry(deps) {
|
|
|
2439
2840
|
}
|
|
2440
2841
|
if (deps.oracle) {
|
|
2441
2842
|
registry.oracle_passed = new OracleCollector(deps.oracle);
|
|
2843
|
+
registry.oracle_red = new OracleRedCollector(deps.oracle);
|
|
2442
2844
|
}
|
|
2443
2845
|
return registry;
|
|
2444
2846
|
}
|
|
2445
2847
|
async function collectGateEvidence(registry, context) {
|
|
2446
2848
|
const collector = registry[context.gate.kind];
|
|
2447
2849
|
if (!collector) {
|
|
2448
|
-
log.info(
|
|
2850
|
+
log.info(TAG10, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
2449
2851
|
return {
|
|
2450
2852
|
result: "blocked",
|
|
2451
2853
|
structured: {
|
|
@@ -2457,14 +2859,14 @@ async function collectGateEvidence(registry, context) {
|
|
|
2457
2859
|
return await collector.collect(context);
|
|
2458
2860
|
} catch (err) {
|
|
2459
2861
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2460
|
-
log.warn(
|
|
2862
|
+
log.warn(TAG10, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
2461
2863
|
return { result: "blocked", structured: { error: msg } };
|
|
2462
2864
|
}
|
|
2463
2865
|
}
|
|
2464
2866
|
|
|
2465
2867
|
// src/harmony-client.ts
|
|
2466
2868
|
init_log();
|
|
2467
|
-
var
|
|
2869
|
+
var TAG11 = "harmony-client";
|
|
2468
2870
|
function readClientConfig(env) {
|
|
2469
2871
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
2470
2872
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -2516,7 +2918,7 @@ class HarmonyClient {
|
|
|
2516
2918
|
purpose: "gate_evaluation"
|
|
2517
2919
|
});
|
|
2518
2920
|
if (!response.ok) {
|
|
2519
|
-
log.warn(
|
|
2921
|
+
log.warn(TAG11, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
2520
2922
|
return null;
|
|
2521
2923
|
}
|
|
2522
2924
|
const body = await response.json();
|
|
@@ -2597,140 +2999,6 @@ function relayAgentEvent(draft) {
|
|
|
2597
2999
|
return { type: "agent_event", event: draft };
|
|
2598
3000
|
}
|
|
2599
3001
|
|
|
2600
|
-
// src/oracle.ts
|
|
2601
|
-
import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
2602
|
-
import { dirname, isAbsolute, resolve, sep } from "node:path";
|
|
2603
|
-
async function resolveContained(repoPath, relativePath) {
|
|
2604
|
-
if (isAbsolute(relativePath)) {
|
|
2605
|
-
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
2606
|
-
}
|
|
2607
|
-
if (relativePath === "" || relativePath === ".") {
|
|
2608
|
-
throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
|
|
2609
|
-
}
|
|
2610
|
-
const root = await realpath(repoPath);
|
|
2611
|
-
const target = resolve(root, relativePath);
|
|
2612
|
-
if (target !== root && !target.startsWith(root + sep)) {
|
|
2613
|
-
throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
|
|
2614
|
-
}
|
|
2615
|
-
let cursor = root;
|
|
2616
|
-
for (const segment of relativePath.split("/")) {
|
|
2617
|
-
cursor = resolve(cursor, segment);
|
|
2618
|
-
const stat = await lstat(cursor).catch(() => null);
|
|
2619
|
-
if (stat?.isSymbolicLink()) {
|
|
2620
|
-
throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
return target;
|
|
2624
|
-
}
|
|
2625
|
-
async function place(repoPath, oracle) {
|
|
2626
|
-
const target = await resolveContained(repoPath, oracle.path);
|
|
2627
|
-
await mkdir(dirname(target), { recursive: true });
|
|
2628
|
-
await writeFile(target, oracle.content, "utf8");
|
|
2629
|
-
}
|
|
2630
|
-
async function remove(repoPath, oracle) {
|
|
2631
|
-
const target = await resolveContained(repoPath, oracle.path);
|
|
2632
|
-
await rm(target, { force: true });
|
|
2633
|
-
}
|
|
2634
|
-
var ORACLE_RUNNERS = {
|
|
2635
|
-
vitest: (path) => ({
|
|
2636
|
-
command: "npx",
|
|
2637
|
-
args: ["--no-install", "vitest", "run", path]
|
|
2638
|
-
}),
|
|
2639
|
-
bun: (path) => ({ command: "bun", args: ["test", path] })
|
|
2640
|
-
};
|
|
2641
|
-
var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
|
|
2642
|
-
var ORACLE_OUTPUT_LIMIT = 64 * 1024;
|
|
2643
|
-
var ORACLE_SIGINT_GRACE_MS = 2000;
|
|
2644
|
-
var ORACLE_SIGTERM_GRACE_MS = 3000;
|
|
2645
|
-
var ORACLE_DRAIN_GRACE_MS = 500;
|
|
2646
|
-
function resolveOracleRunner(oracle) {
|
|
2647
|
-
const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
|
|
2648
|
-
const build = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
|
|
2649
|
-
if (!build) {
|
|
2650
|
-
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(", ")})`);
|
|
2651
|
-
}
|
|
2652
|
-
return build(argvPath(oracle.path));
|
|
2653
|
-
}
|
|
2654
|
-
function argvPath(path) {
|
|
2655
|
-
return path.startsWith("./") ? path : `./${path}`;
|
|
2656
|
-
}
|
|
2657
|
-
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
2658
|
-
const { command, args } = resolveOracleRunner(oracle);
|
|
2659
|
-
return await new Promise((settleOk, settleErr) => {
|
|
2660
|
-
let child;
|
|
2661
|
-
try {
|
|
2662
|
-
child = spawnInGroup(command, args, {
|
|
2663
|
-
cwd: repoPath,
|
|
2664
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2665
|
-
});
|
|
2666
|
-
} catch (err) {
|
|
2667
|
-
settleErr(err);
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
const pgid = child.pid;
|
|
2671
|
-
let output = "";
|
|
2672
|
-
let settled = false;
|
|
2673
|
-
let killing = false;
|
|
2674
|
-
let timer;
|
|
2675
|
-
let drainTimer;
|
|
2676
|
-
const settle = (failure, result) => {
|
|
2677
|
-
if (settled)
|
|
2678
|
-
return;
|
|
2679
|
-
settled = true;
|
|
2680
|
-
if (timer)
|
|
2681
|
-
clearTimeout(timer);
|
|
2682
|
-
if (drainTimer)
|
|
2683
|
-
clearTimeout(drainTimer);
|
|
2684
|
-
reapGroup(pgid);
|
|
2685
|
-
if (failure)
|
|
2686
|
-
settleErr(failure);
|
|
2687
|
-
else
|
|
2688
|
-
settleOk(result);
|
|
2689
|
-
};
|
|
2690
|
-
const append = (chunk) => {
|
|
2691
|
-
if (output.length >= ORACLE_OUTPUT_LIMIT)
|
|
2692
|
-
return;
|
|
2693
|
-
output += chunk.toString("utf8");
|
|
2694
|
-
if (output.length > ORACLE_OUTPUT_LIMIT) {
|
|
2695
|
-
output = `${output.slice(0, ORACLE_OUTPUT_LIMIT)}
|
|
2696
|
-
… output truncated at ${ORACLE_OUTPUT_LIMIT} characters`;
|
|
2697
|
-
}
|
|
2698
|
-
};
|
|
2699
|
-
child.stdout?.on("data", append);
|
|
2700
|
-
child.stderr?.on("data", append);
|
|
2701
|
-
child.once("error", (err) => settle(err));
|
|
2702
|
-
const settleFromExit = (code, signal) => {
|
|
2703
|
-
if (drainTimer)
|
|
2704
|
-
clearTimeout(drainTimer);
|
|
2705
|
-
if (code === null) {
|
|
2706
|
-
settle(new Error(`the held test was terminated by signal ${signal}`));
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
settle(null, { exitCode: code, output });
|
|
2710
|
-
};
|
|
2711
|
-
child.once("exit", (code, signal) => {
|
|
2712
|
-
if (killing)
|
|
2713
|
-
return;
|
|
2714
|
-
if (timer)
|
|
2715
|
-
clearTimeout(timer);
|
|
2716
|
-
reapGroup(pgid);
|
|
2717
|
-
drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
|
|
2718
|
-
child.once("close", () => settleFromExit(code, signal));
|
|
2719
|
-
});
|
|
2720
|
-
timer = setTimeout(() => {
|
|
2721
|
-
if (settled)
|
|
2722
|
-
return;
|
|
2723
|
-
killing = true;
|
|
2724
|
-
terminateGroup(child, {
|
|
2725
|
-
sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
|
|
2726
|
-
sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
|
|
2727
|
-
}).catch(() => {}).then(() => {
|
|
2728
|
-
settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
|
|
2729
|
-
});
|
|
2730
|
-
}, timeoutMs);
|
|
2731
|
-
});
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
3002
|
// src/stage-cli.ts
|
|
2735
3003
|
init_dist();
|
|
2736
3004
|
|
|
@@ -2979,7 +3247,7 @@ async function runStage(request, deps) {
|
|
|
2979
3247
|
}
|
|
2980
3248
|
|
|
2981
3249
|
// src/cli.ts
|
|
2982
|
-
var
|
|
3250
|
+
var TAG12 = "cli";
|
|
2983
3251
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2984
3252
|
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
2985
3253
|
var activeRunner = null;
|
|
@@ -3010,12 +3278,12 @@ async function runRole(request, prompt, emit2) {
|
|
|
3010
3278
|
});
|
|
3011
3279
|
const runner = new SdkAgentRunner(launch.config);
|
|
3012
3280
|
activeRunner = runner;
|
|
3013
|
-
log.info(
|
|
3281
|
+
log.info(TAG12, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
3014
3282
|
const timeoutMs = stageTimeoutMs(process.env);
|
|
3015
3283
|
let timedOut = false;
|
|
3016
3284
|
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3017
3285
|
timedOut = true;
|
|
3018
|
-
log.warn(
|
|
3286
|
+
log.warn(TAG12, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3019
3287
|
runner.stop("timeout");
|
|
3020
3288
|
}, timeoutMs) : null;
|
|
3021
3289
|
clock?.unref?.();
|
|
@@ -3031,9 +3299,9 @@ async function runRole(request, prompt, emit2) {
|
|
|
3031
3299
|
if (relayed)
|
|
3032
3300
|
emit2(relayed);
|
|
3033
3301
|
if (event.kind === "error") {
|
|
3034
|
-
log.warn(
|
|
3302
|
+
log.warn(TAG12, `subagent error: ${event.payload.message}`);
|
|
3035
3303
|
} else {
|
|
3036
|
-
log.event(
|
|
3304
|
+
log.event(TAG12, `subagent ${event.kind}`);
|
|
3037
3305
|
}
|
|
3038
3306
|
}
|
|
3039
3307
|
} finally {
|
|
@@ -3064,8 +3332,8 @@ ${STAGE_RUN_USAGE}
|
|
|
3064
3332
|
const { cardId, stageId, workspaceId, repoPath, sessionId, metricsPath } = parsed.args;
|
|
3065
3333
|
let driverMetrics = {};
|
|
3066
3334
|
if (metricsPath !== null) {
|
|
3067
|
-
const { readFileSync:
|
|
3068
|
-
driverMetrics = parseMetricsAllowlist(
|
|
3335
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
3336
|
+
driverMetrics = parseMetricsAllowlist(readFileSync3(metricsPath, "utf8"), metricsPath);
|
|
3069
3337
|
}
|
|
3070
3338
|
const client = new HarmonyClient(readClientConfig(process.env));
|
|
3071
3339
|
const card = await client.fetchStageCard(cardId);
|
|
@@ -3079,12 +3347,13 @@ ${STAGE_RUN_USAGE}
|
|
|
3079
3347
|
throw new Error(`refusing to run stage "${stageId}" for card ${cardId}: ${pinned.reason}`);
|
|
3080
3348
|
}
|
|
3081
3349
|
assertStageIsAgentRunnable(pinned.stage);
|
|
3350
|
+
const oracleTargetStageId = findOracleTargetStage(version, stageId);
|
|
3082
3351
|
const prompt = buildStagePrompt({
|
|
3083
3352
|
cardId,
|
|
3084
3353
|
stageId,
|
|
3085
3354
|
stage: pinned.stage,
|
|
3086
3355
|
sessionId,
|
|
3087
|
-
oracleTargetStageId
|
|
3356
|
+
oracleTargetStageId
|
|
3088
3357
|
});
|
|
3089
3358
|
const request = {
|
|
3090
3359
|
cardId,
|
|
@@ -3112,6 +3381,7 @@ ${STAGE_RUN_USAGE}
|
|
|
3112
3381
|
oracle: {
|
|
3113
3382
|
repoPath: req.repoPath,
|
|
3114
3383
|
sessionId: req.sessionId,
|
|
3384
|
+
targetStageId: oracleTargetStageId,
|
|
3115
3385
|
fetchOracle: (oracleCardId, oracleStageId, oracleSessionId) => client.fetchOracle(oracleCardId, oracleStageId, oracleSessionId),
|
|
3116
3386
|
place,
|
|
3117
3387
|
remove,
|