@gethmy/harness 1.2.1 → 1.4.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 +497 -225
- package/dist/index.js +1259 -402
- 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 +114 -2
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",
|
|
@@ -469,6 +473,13 @@ var init_playbookStage = __esm(() => {
|
|
|
469
473
|
|
|
470
474
|
// ../harmony-shared/dist/projectTemplates.js
|
|
471
475
|
var init_projectTemplates = () => {};
|
|
476
|
+
|
|
477
|
+
// ../harmony-shared/dist/realtimeChannel.js
|
|
478
|
+
var inFlightDetach;
|
|
479
|
+
var init_realtimeChannel = __esm(() => {
|
|
480
|
+
init_logger();
|
|
481
|
+
inFlightDetach = new WeakMap;
|
|
482
|
+
});
|
|
472
483
|
// ../harmony-shared/dist/reviewTools.js
|
|
473
484
|
var REVIEW_DISALLOWED_TOOLS;
|
|
474
485
|
var init_reviewTools = __esm(() => {
|
|
@@ -496,6 +507,7 @@ var init_dist = __esm(() => {
|
|
|
496
507
|
init_agentStaleness();
|
|
497
508
|
init_branchRef();
|
|
498
509
|
init_cardLinks();
|
|
510
|
+
init_cardModelStat();
|
|
499
511
|
init_classification();
|
|
500
512
|
init_columnCardSplit();
|
|
501
513
|
init_columnSort();
|
|
@@ -509,6 +521,7 @@ var init_dist = __esm(() => {
|
|
|
509
521
|
init_playbookCatalog();
|
|
510
522
|
init_playbookStage();
|
|
511
523
|
init_projectTemplates();
|
|
524
|
+
init_realtimeChannel();
|
|
512
525
|
init_reviewTools();
|
|
513
526
|
init_stageHandoff();
|
|
514
527
|
init_types();
|
|
@@ -652,6 +665,8 @@ var AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth to
|
|
|
652
665
|
var OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
|
|
653
666
|
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
667
|
var RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
|
|
668
|
+
var SPEND_LIMIT = /spend(?:ing)? (?:limit|cap)|monthly spend/i;
|
|
669
|
+
var SPEND_LIMIT_REMEDY = /admin-settings\/usage/i;
|
|
655
670
|
function parseRetryAfterMs(message) {
|
|
656
671
|
const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
|
|
657
672
|
if (!match)
|
|
@@ -667,12 +682,16 @@ function classifyRunError(message) {
|
|
|
667
682
|
const retryAfterMs = parseRetryAfterMs(message);
|
|
668
683
|
if (AUTH.test(message))
|
|
669
684
|
return { kind: "auth", retryAfterMs };
|
|
685
|
+
if (SPEND_LIMIT.test(message))
|
|
686
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
670
687
|
if (OUT_OF_CREDITS.test(message))
|
|
671
688
|
return { kind: "out_of_credits", retryAfterMs };
|
|
672
689
|
if (USAGE_LIMIT.test(message))
|
|
673
690
|
return { kind: "usage_limit", retryAfterMs };
|
|
674
691
|
if (RATE_LIMIT.test(message))
|
|
675
692
|
return { kind: "rate_limit", retryAfterMs };
|
|
693
|
+
if (SPEND_LIMIT_REMEDY.test(message))
|
|
694
|
+
return { kind: "spend_limit", retryAfterMs };
|
|
676
695
|
return { kind: null };
|
|
677
696
|
}
|
|
678
697
|
function describeApiError(kind) {
|
|
@@ -685,6 +704,8 @@ function describeApiError(kind) {
|
|
|
685
704
|
return "Anthropic usage limit reached — retrying after reset";
|
|
686
705
|
case "rate_limit":
|
|
687
706
|
return "Anthropic rate limit hit — retrying shortly";
|
|
707
|
+
case "spend_limit":
|
|
708
|
+
return "Account spend limit reached — raise it at claude.ai/admin-settings/usage, then run `harmony-agent resume`";
|
|
688
709
|
}
|
|
689
710
|
}
|
|
690
711
|
function cooldownMsFor(kind) {
|
|
@@ -697,6 +718,8 @@ function cooldownMsFor(kind) {
|
|
|
697
718
|
return 30 * 60000;
|
|
698
719
|
case "auth":
|
|
699
720
|
return 30 * 60000;
|
|
721
|
+
case "spend_limit":
|
|
722
|
+
return 30 * 60000;
|
|
700
723
|
}
|
|
701
724
|
}
|
|
702
725
|
|
|
@@ -1050,7 +1073,7 @@ ${this.capturedStderr}`);
|
|
|
1050
1073
|
kind: "error",
|
|
1051
1074
|
source: "system",
|
|
1052
1075
|
payload: {
|
|
1053
|
-
message:
|
|
1076
|
+
message: resultErrorMessage(r.subtype, joined),
|
|
1054
1077
|
errorKind: cls.kind,
|
|
1055
1078
|
retryable: cls.kind !== "auth" && cls.kind !== null
|
|
1056
1079
|
}
|
|
@@ -1061,6 +1084,13 @@ ${this.capturedStderr}`);
|
|
|
1061
1084
|
}
|
|
1062
1085
|
}
|
|
1063
1086
|
}
|
|
1087
|
+
function resultErrorMessage(subtype, detail) {
|
|
1088
|
+
return `result ${subtype}: ${detail || "(no detail)"}`;
|
|
1089
|
+
}
|
|
1090
|
+
function resultErrorSubtype(message) {
|
|
1091
|
+
const match = message.match(/^result ([A-Za-z0-9_]+):/);
|
|
1092
|
+
return match ? match[1] : null;
|
|
1093
|
+
}
|
|
1064
1094
|
function normalize(raw) {
|
|
1065
1095
|
if (raw == null)
|
|
1066
1096
|
return;
|
|
@@ -1545,24 +1575,342 @@ function truncate(value, max) {
|
|
|
1545
1575
|
init_log();
|
|
1546
1576
|
|
|
1547
1577
|
// src/oracle-collector.ts
|
|
1548
|
-
init_log();
|
|
1549
1578
|
import { createHash } from "node:crypto";
|
|
1550
|
-
|
|
1579
|
+
init_log();
|
|
1580
|
+
|
|
1581
|
+
// src/oracle.ts
|
|
1582
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
1583
|
+
import {
|
|
1584
|
+
chmod,
|
|
1585
|
+
lstat,
|
|
1586
|
+
mkdir,
|
|
1587
|
+
mkdtemp,
|
|
1588
|
+
realpath,
|
|
1589
|
+
rm,
|
|
1590
|
+
writeFile
|
|
1591
|
+
} from "node:fs/promises";
|
|
1592
|
+
import { tmpdir } from "node:os";
|
|
1593
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
1594
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1595
|
+
init_log();
|
|
1596
|
+
var TAG4 = "oracle";
|
|
1597
|
+
async function resolveContained(repoPath, relativePath) {
|
|
1598
|
+
if (isAbsolute(relativePath)) {
|
|
1599
|
+
throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
|
|
1600
|
+
}
|
|
1601
|
+
if (relativePath === "" || relativePath === ".") {
|
|
1602
|
+
throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
|
|
1603
|
+
}
|
|
1604
|
+
const root = await realpath(repoPath);
|
|
1605
|
+
const target = resolve(root, relativePath);
|
|
1606
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
1607
|
+
throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
|
|
1608
|
+
}
|
|
1609
|
+
let cursor = root;
|
|
1610
|
+
for (const segment of relativePath.split("/")) {
|
|
1611
|
+
cursor = resolve(cursor, segment);
|
|
1612
|
+
const stat = await lstat(cursor).catch(() => null);
|
|
1613
|
+
if (stat?.isSymbolicLink()) {
|
|
1614
|
+
throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return target;
|
|
1618
|
+
}
|
|
1619
|
+
async function place(repoPath, oracle) {
|
|
1620
|
+
const target = await resolveContained(repoPath, oracle.path);
|
|
1621
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1622
|
+
await writeFile(target, oracle.content, "utf8");
|
|
1623
|
+
}
|
|
1624
|
+
async function remove(repoPath, oracle) {
|
|
1625
|
+
const target = await resolveContained(repoPath, oracle.path);
|
|
1626
|
+
await rm(target, { force: true });
|
|
1627
|
+
}
|
|
1628
|
+
var ORACLE_RUNNERS = {
|
|
1629
|
+
vitest: {
|
|
1630
|
+
argv: (path) => ({
|
|
1631
|
+
command: "npx",
|
|
1632
|
+
args: ["--no-install", "vitest", "run", path]
|
|
1633
|
+
}),
|
|
1634
|
+
verdictStream: "stdout",
|
|
1635
|
+
report: {
|
|
1636
|
+
file: "report.json",
|
|
1637
|
+
flags: (reportPath) => [
|
|
1638
|
+
"--reporter=default",
|
|
1639
|
+
"--reporter=json",
|
|
1640
|
+
`--outputFile=${reportPath}`
|
|
1641
|
+
],
|
|
1642
|
+
parse: (content) => {
|
|
1643
|
+
const parsed = JSON.parse(content);
|
|
1644
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1645
|
+
return null;
|
|
1646
|
+
const { numTotalTests, numFailedTests } = parsed;
|
|
1647
|
+
if (typeof numTotalTests !== "number" || typeof numFailedTests !== "number" || !Number.isFinite(numTotalTests) || !Number.isFinite(numFailedTests)) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
return { total: numTotalTests, failed: numFailedTests };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
bun: {
|
|
1655
|
+
argv: (path) => ({ command: "bun", args: ["test", path] }),
|
|
1656
|
+
verdictStream: "stderr",
|
|
1657
|
+
report: {
|
|
1658
|
+
file: "report.xml",
|
|
1659
|
+
flags: (reportPath) => [
|
|
1660
|
+
"--reporter=junit",
|
|
1661
|
+
`--reporter-outfile=${reportPath}`
|
|
1662
|
+
],
|
|
1663
|
+
parse: (content) => {
|
|
1664
|
+
const root = /<testsuites\b[^>]*>/.exec(content);
|
|
1665
|
+
if (!root)
|
|
1666
|
+
return null;
|
|
1667
|
+
const total = /\btests="(\d+)"/.exec(root[0]);
|
|
1668
|
+
const failed = /\bfailures="(\d+)"/.exec(root[0]);
|
|
1669
|
+
if (!total || !failed)
|
|
1670
|
+
return null;
|
|
1671
|
+
return { total: Number(total[1]), failed: Number(failed[1]) };
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
|
|
1677
|
+
var ORACLE_OUTPUT_LIMIT = 64 * 1024;
|
|
1678
|
+
var ORACLE_SIGINT_GRACE_MS = 2000;
|
|
1679
|
+
var ORACLE_SIGTERM_GRACE_MS = 3000;
|
|
1680
|
+
var ORACLE_DRAIN_GRACE_MS = 500;
|
|
1681
|
+
function resolveOracleRunner(oracle) {
|
|
1682
|
+
return resolveOracleRunnerSpec(oracle).argv(argvPath(oracle.path));
|
|
1683
|
+
}
|
|
1684
|
+
function resolveOracleRunnerSpec(oracle) {
|
|
1685
|
+
const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
|
|
1686
|
+
const spec = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
|
|
1687
|
+
if (!spec) {
|
|
1688
|
+
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(", ")})`);
|
|
1689
|
+
}
|
|
1690
|
+
return spec;
|
|
1691
|
+
}
|
|
1692
|
+
function summarizeOracleReport(oracle, content) {
|
|
1693
|
+
const spec = resolveOracleRunnerSpec(oracle);
|
|
1694
|
+
try {
|
|
1695
|
+
return spec.report.parse(content) ?? null;
|
|
1696
|
+
} catch {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function gradeOracleRed(summary, exitCode) {
|
|
1701
|
+
if (!summary) {
|
|
1702
|
+
return {
|
|
1703
|
+
outcome: "no_verdict",
|
|
1704
|
+
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"
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
if (summary.total === 0) {
|
|
1708
|
+
return {
|
|
1709
|
+
outcome: "no_verdict",
|
|
1710
|
+
reason: `the runner started but executed no tests (exit ${exitCode}), so the held test produced no verdict`
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
if (summary.failed > 0) {
|
|
1714
|
+
return { outcome: "reproduced", summary };
|
|
1715
|
+
}
|
|
1716
|
+
if (exitCode === 0) {
|
|
1717
|
+
return { outcome: "not_reproduced", summary };
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
outcome: "no_verdict",
|
|
1721
|
+
reason: `all ${summary.total} test(s) passed but the runner exited ${exitCode}, so the failure is not the held test's`
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function argvPath(path) {
|
|
1725
|
+
return path.startsWith("./") ? path : `./${path}`;
|
|
1726
|
+
}
|
|
1727
|
+
async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
|
|
1728
|
+
const spec = resolveOracleRunnerSpec(oracle);
|
|
1729
|
+
const reportDir = await mkdtemp(join(tmpdir(), reportDirPrefix()));
|
|
1730
|
+
const reportPath = join(reportDir, spec.report.file);
|
|
1731
|
+
try {
|
|
1732
|
+
return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
|
|
1733
|
+
} finally {
|
|
1734
|
+
await removeReportDir(reportDir);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function reportDirPrefix() {
|
|
1738
|
+
return "harmony-oracle-report-";
|
|
1739
|
+
}
|
|
1740
|
+
function assertUntampered(reportPath) {
|
|
1741
|
+
const dir = statSync(dirname(reportPath));
|
|
1742
|
+
if ((dir.mode & 511) !== 448) {
|
|
1743
|
+
throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
|
|
1744
|
+
}
|
|
1745
|
+
const file = lstatSync(reportPath);
|
|
1746
|
+
if (!file.isFile()) {
|
|
1747
|
+
throw new Error("the oracle report is not a regular file — refusing it");
|
|
1748
|
+
}
|
|
1749
|
+
if ((file.mode & 128) === 0) {
|
|
1750
|
+
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`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function removeReportDir(reportDir) {
|
|
1754
|
+
try {
|
|
1755
|
+
await chmod(reportDir, 448).catch(() => {});
|
|
1756
|
+
await rm(reportDir, { recursive: true, force: true });
|
|
1757
|
+
} catch (err) {
|
|
1758
|
+
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.`);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
|
|
1762
|
+
const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
|
|
1763
|
+
const args = [...baseArgs, ...spec.report.flags(reportPath)];
|
|
1764
|
+
return await new Promise((settleOk, settleErr) => {
|
|
1765
|
+
let child;
|
|
1766
|
+
try {
|
|
1767
|
+
child = spawnInGroup(command, args, {
|
|
1768
|
+
cwd: repoPath,
|
|
1769
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1770
|
+
});
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
settleErr(err);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
const pgid = child.pid;
|
|
1776
|
+
let output = "";
|
|
1777
|
+
let truncated = false;
|
|
1778
|
+
const outDecoder = new StringDecoder("utf8");
|
|
1779
|
+
const errDecoder = new StringDecoder("utf8");
|
|
1780
|
+
let verdict = "";
|
|
1781
|
+
let flushed = false;
|
|
1782
|
+
const flush = () => {
|
|
1783
|
+
if (flushed)
|
|
1784
|
+
return;
|
|
1785
|
+
flushed = true;
|
|
1786
|
+
const outTail = outDecoder.end();
|
|
1787
|
+
const errTail = errDecoder.end();
|
|
1788
|
+
output += outTail + errTail;
|
|
1789
|
+
verdict += spec.verdictStream === "stdout" ? outTail : errTail;
|
|
1790
|
+
};
|
|
1791
|
+
const finalOutput = () => {
|
|
1792
|
+
flush();
|
|
1793
|
+
return truncated ? `… earlier output dropped; kept the last ${ORACLE_OUTPUT_LIMIT} characters
|
|
1794
|
+
${output}` : output;
|
|
1795
|
+
};
|
|
1796
|
+
const finalVerdict = () => {
|
|
1797
|
+
flush();
|
|
1798
|
+
return verdict;
|
|
1799
|
+
};
|
|
1800
|
+
let report = null;
|
|
1801
|
+
const captureReport = () => {
|
|
1802
|
+
try {
|
|
1803
|
+
assertUntampered(reportPath);
|
|
1804
|
+
report = spec.report.parse(readFileSync(reportPath, "utf8"));
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
report = null;
|
|
1807
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1808
|
+
const absent = err instanceof Error && err.code === "ENOENT";
|
|
1809
|
+
if (absent) {
|
|
1810
|
+
log.info(TAG4, `No oracle report at ${reportPath} — no verdict.`);
|
|
1811
|
+
} else {
|
|
1812
|
+
log.warn(TAG4, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
let settled = false;
|
|
1817
|
+
let killing = false;
|
|
1818
|
+
let timer;
|
|
1819
|
+
let drainTimer;
|
|
1820
|
+
const settle = (failure, result) => {
|
|
1821
|
+
if (settled)
|
|
1822
|
+
return;
|
|
1823
|
+
settled = true;
|
|
1824
|
+
if (timer)
|
|
1825
|
+
clearTimeout(timer);
|
|
1826
|
+
if (drainTimer)
|
|
1827
|
+
clearTimeout(drainTimer);
|
|
1828
|
+
reapGroup(pgid);
|
|
1829
|
+
if (failure)
|
|
1830
|
+
settleErr(failure);
|
|
1831
|
+
else
|
|
1832
|
+
settleOk(result);
|
|
1833
|
+
};
|
|
1834
|
+
const append = (stream, chunk) => {
|
|
1835
|
+
const text = (stream === "stdout" ? outDecoder : errDecoder).write(chunk);
|
|
1836
|
+
output += text;
|
|
1837
|
+
if (output.length > ORACLE_OUTPUT_LIMIT) {
|
|
1838
|
+
output = output.slice(-ORACLE_OUTPUT_LIMIT);
|
|
1839
|
+
truncated = true;
|
|
1840
|
+
}
|
|
1841
|
+
if (stream === spec.verdictStream) {
|
|
1842
|
+
verdict += text;
|
|
1843
|
+
if (verdict.length > ORACLE_OUTPUT_LIMIT) {
|
|
1844
|
+
verdict = verdict.slice(-ORACLE_OUTPUT_LIMIT);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
child.stdout?.on("data", (chunk) => append("stdout", chunk));
|
|
1849
|
+
child.stderr?.on("data", (chunk) => append("stderr", chunk));
|
|
1850
|
+
child.once("error", (err) => settle(err));
|
|
1851
|
+
const settleFromExit = (code, signal) => {
|
|
1852
|
+
if (drainTimer)
|
|
1853
|
+
clearTimeout(drainTimer);
|
|
1854
|
+
if (code === null) {
|
|
1855
|
+
settle(new Error(`the held test was terminated by signal ${signal}`));
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
settle(null, {
|
|
1859
|
+
exitCode: code,
|
|
1860
|
+
output: finalOutput(),
|
|
1861
|
+
verdict: finalVerdict(),
|
|
1862
|
+
report
|
|
1863
|
+
});
|
|
1864
|
+
};
|
|
1865
|
+
child.once("exit", (code, signal) => {
|
|
1866
|
+
if (killing)
|
|
1867
|
+
return;
|
|
1868
|
+
captureReport();
|
|
1869
|
+
if (timer)
|
|
1870
|
+
clearTimeout(timer);
|
|
1871
|
+
reapGroup(pgid);
|
|
1872
|
+
drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
|
|
1873
|
+
child.once("close", () => settleFromExit(code, signal));
|
|
1874
|
+
});
|
|
1875
|
+
timer = setTimeout(() => {
|
|
1876
|
+
if (settled)
|
|
1877
|
+
return;
|
|
1878
|
+
killing = true;
|
|
1879
|
+
terminateGroup(child, {
|
|
1880
|
+
sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
|
|
1881
|
+
sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
|
|
1882
|
+
}).catch(() => {}).then(() => {
|
|
1883
|
+
settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
|
|
1884
|
+
});
|
|
1885
|
+
}, timeoutMs);
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1551
1888
|
|
|
1552
|
-
|
|
1889
|
+
// src/oracle-collector.ts
|
|
1890
|
+
var TAG5 = "oracle-collector";
|
|
1891
|
+
|
|
1892
|
+
class HeldOracleCollector {
|
|
1553
1893
|
deps;
|
|
1554
|
-
kind = "oracle_passed";
|
|
1555
1894
|
constructor(deps) {
|
|
1556
1895
|
this.deps = deps;
|
|
1557
1896
|
}
|
|
1558
1897
|
async collect(context) {
|
|
1559
|
-
const
|
|
1898
|
+
const stageId = this.oracleStageId(context);
|
|
1899
|
+
if (!stageId) {
|
|
1900
|
+
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.";
|
|
1901
|
+
log.warn(TAG5, `${reason} — blocked (config)`);
|
|
1902
|
+
return {
|
|
1903
|
+
result: "blocked",
|
|
1904
|
+
structured: { reason, ...GATE_CONFIG_ERROR_MARK }
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
|
|
1560
1908
|
if (!oracle) {
|
|
1561
|
-
log.info(
|
|
1909
|
+
log.info(TAG5, `No oracle held for stage ${stageId} — blocked`);
|
|
1562
1910
|
return {
|
|
1563
1911
|
result: "blocked",
|
|
1564
1912
|
structured: {
|
|
1565
|
-
reason: `No oracle is held for stage ${
|
|
1913
|
+
reason: `No oracle is held for stage ${stageId}.`
|
|
1566
1914
|
}
|
|
1567
1915
|
};
|
|
1568
1916
|
}
|
|
@@ -1575,28 +1923,24 @@ class OracleCollector {
|
|
|
1575
1923
|
};
|
|
1576
1924
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
1577
1925
|
try {
|
|
1578
|
-
const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1926
|
+
const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
|
|
1579
1927
|
const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
|
|
1580
1928
|
${output}`;
|
|
1581
1929
|
if (exitCode === 0) {
|
|
1582
|
-
log.info(
|
|
1930
|
+
log.info(TAG5, logLine);
|
|
1583
1931
|
} else {
|
|
1584
|
-
log.warn(
|
|
1932
|
+
log.warn(TAG5, logLine);
|
|
1585
1933
|
}
|
|
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
|
-
};
|
|
1934
|
+
return this.verdict({
|
|
1935
|
+
oracle,
|
|
1936
|
+
identity,
|
|
1937
|
+
exitCode,
|
|
1938
|
+
output,
|
|
1939
|
+
report: report ?? null
|
|
1940
|
+
});
|
|
1597
1941
|
} catch (err) {
|
|
1598
1942
|
const message = errText(err);
|
|
1599
|
-
log.warn(
|
|
1943
|
+
log.warn(TAG5, `Oracle run threw: ${message} — blocked`);
|
|
1600
1944
|
return {
|
|
1601
1945
|
result: "blocked",
|
|
1602
1946
|
structured: {
|
|
@@ -1613,14 +1957,72 @@ ${output}`;
|
|
|
1613
1957
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1614
1958
|
return;
|
|
1615
1959
|
} catch (err) {
|
|
1616
|
-
log.warn(
|
|
1960
|
+
log.warn(TAG5, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
|
|
1617
1961
|
}
|
|
1618
1962
|
try {
|
|
1619
1963
|
await this.deps.remove(this.deps.repoPath, oracle);
|
|
1620
|
-
log.info(
|
|
1964
|
+
log.info(TAG5, `Held test at ${oracle.path} removed on the second attempt`);
|
|
1621
1965
|
} catch (err) {
|
|
1622
|
-
log.error(
|
|
1966
|
+
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.");
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
class OracleCollector extends HeldOracleCollector {
|
|
1972
|
+
kind = "oracle_passed";
|
|
1973
|
+
oracleStageId(context) {
|
|
1974
|
+
return context.stageId;
|
|
1975
|
+
}
|
|
1976
|
+
verdict({
|
|
1977
|
+
oracle,
|
|
1978
|
+
identity,
|
|
1979
|
+
exitCode
|
|
1980
|
+
}) {
|
|
1981
|
+
return {
|
|
1982
|
+
result: exitCode === 0 ? "passed" : "failed",
|
|
1983
|
+
structured: {
|
|
1984
|
+
oracle: {
|
|
1985
|
+
exitCode,
|
|
1986
|
+
path: oracle.path,
|
|
1987
|
+
...identity,
|
|
1988
|
+
output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
class OracleRedCollector extends HeldOracleCollector {
|
|
1996
|
+
kind = "oracle_red";
|
|
1997
|
+
oracleStageId() {
|
|
1998
|
+
return this.deps.targetStageId ?? null;
|
|
1999
|
+
}
|
|
2000
|
+
verdict({
|
|
2001
|
+
oracle,
|
|
2002
|
+
identity,
|
|
2003
|
+
exitCode,
|
|
2004
|
+
report
|
|
2005
|
+
}) {
|
|
2006
|
+
const graded = gradeOracleRed(report, exitCode);
|
|
2007
|
+
const base = { exitCode, path: oracle.path, ...identity };
|
|
2008
|
+
if (graded.outcome === "no_verdict") {
|
|
2009
|
+
log.warn(TAG5, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
|
|
2010
|
+
return {
|
|
2011
|
+
result: "blocked",
|
|
2012
|
+
structured: { oracle: base, reason: graded.reason }
|
|
2013
|
+
};
|
|
1623
2014
|
}
|
|
2015
|
+
return {
|
|
2016
|
+
result: graded.outcome === "reproduced" ? "passed" : "failed",
|
|
2017
|
+
structured: {
|
|
2018
|
+
oracle: {
|
|
2019
|
+
...base,
|
|
2020
|
+
testsRun: graded.summary.total,
|
|
2021
|
+
testsFailed: graded.summary.failed,
|
|
2022
|
+
output: "withheld — oracle_red is a secrecy gate; see the motor's local log"
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
1624
2026
|
}
|
|
1625
2027
|
}
|
|
1626
2028
|
function errText(err) {
|
|
@@ -1635,7 +2037,7 @@ import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_proce
|
|
|
1635
2037
|
init_log();
|
|
1636
2038
|
import { execFileSync } from "node:child_process";
|
|
1637
2039
|
import { existsSync } from "node:fs";
|
|
1638
|
-
var
|
|
2040
|
+
var TAG6 = "pm";
|
|
1639
2041
|
var cached = null;
|
|
1640
2042
|
function detectPackageManager() {
|
|
1641
2043
|
if (cached)
|
|
@@ -1657,20 +2059,21 @@ function detectPackageManager() {
|
|
|
1657
2059
|
} else {
|
|
1658
2060
|
cached = "npm";
|
|
1659
2061
|
}
|
|
1660
|
-
log.info(
|
|
2062
|
+
log.info(TAG6, `Detected package manager: ${cached}`);
|
|
1661
2063
|
return cached;
|
|
1662
2064
|
}
|
|
1663
|
-
function installCommand() {
|
|
2065
|
+
function installCommand(ignoreScripts = false) {
|
|
1664
2066
|
const pm = detectPackageManager();
|
|
2067
|
+
const skip = ignoreScripts ? " --ignore-scripts" : "";
|
|
1665
2068
|
switch (pm) {
|
|
1666
2069
|
case "bun":
|
|
1667
|
-
return
|
|
2070
|
+
return `bun install --frozen-lockfile${skip}`;
|
|
1668
2071
|
case "pnpm":
|
|
1669
|
-
return
|
|
2072
|
+
return `pnpm install --frozen-lockfile${skip}`;
|
|
1670
2073
|
case "yarn":
|
|
1671
|
-
return
|
|
2074
|
+
return `yarn install --frozen-lockfile${skip}`;
|
|
1672
2075
|
case "npm":
|
|
1673
|
-
return
|
|
2076
|
+
return `npm ci${skip}`;
|
|
1674
2077
|
}
|
|
1675
2078
|
}
|
|
1676
2079
|
function spawnRunArgs(script, ...extra) {
|
|
@@ -1684,8 +2087,8 @@ function spawnRunArgs(script, ...extra) {
|
|
|
1684
2087
|
// src/project-type.ts
|
|
1685
2088
|
init_log();
|
|
1686
2089
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1687
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
1688
|
-
var
|
|
2090
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2091
|
+
var TAG7 = "project-type";
|
|
1689
2092
|
var _cache = new Map;
|
|
1690
2093
|
function _resetCache() {
|
|
1691
2094
|
_cache.clear();
|
|
@@ -1696,7 +2099,7 @@ function detect(dir) {
|
|
|
1696
2099
|
return cached2;
|
|
1697
2100
|
const result = detectUncached(dir);
|
|
1698
2101
|
_cache.set(dir, result);
|
|
1699
|
-
log.info(
|
|
2102
|
+
log.info(TAG7, `Detected project type in ${dir}: ${result.kind}`);
|
|
1700
2103
|
return result;
|
|
1701
2104
|
}
|
|
1702
2105
|
function detectUncached(dir) {
|
|
@@ -1787,16 +2190,16 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
|
|
|
1787
2190
|
function hasNodeTestScript(dir) {
|
|
1788
2191
|
let script;
|
|
1789
2192
|
try {
|
|
1790
|
-
const pkg = JSON.parse(
|
|
2193
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1791
2194
|
script = pkg.scripts?.test;
|
|
1792
2195
|
} catch (err) {
|
|
1793
|
-
log.warn(
|
|
2196
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1794
2197
|
return false;
|
|
1795
2198
|
}
|
|
1796
2199
|
if (typeof script !== "string" || script.trim().length === 0)
|
|
1797
2200
|
return false;
|
|
1798
2201
|
if (NPM_PLACEHOLDER_TEST.test(script)) {
|
|
1799
|
-
log.info(
|
|
2202
|
+
log.info(TAG7, `package.json 'test' is the npm placeholder — skipping tests`);
|
|
1800
2203
|
return false;
|
|
1801
2204
|
}
|
|
1802
2205
|
return true;
|
|
@@ -1804,10 +2207,10 @@ function hasNodeTestScript(dir) {
|
|
|
1804
2207
|
function firstNodeScript(dir, candidates) {
|
|
1805
2208
|
let scripts;
|
|
1806
2209
|
try {
|
|
1807
|
-
const pkg = JSON.parse(
|
|
2210
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
1808
2211
|
scripts = pkg.scripts ?? {};
|
|
1809
2212
|
} catch (err) {
|
|
1810
|
-
log.warn(
|
|
2213
|
+
log.warn(TAG7, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
|
|
1811
2214
|
return null;
|
|
1812
2215
|
}
|
|
1813
2216
|
for (const name of candidates) {
|
|
@@ -1826,7 +2229,7 @@ function xcodeBuildCommand(pt) {
|
|
|
1826
2229
|
return null;
|
|
1827
2230
|
const scheme = resolveXcodeScheme(pt);
|
|
1828
2231
|
if (!scheme) {
|
|
1829
|
-
log.warn(
|
|
2232
|
+
log.warn(TAG7, "Could not resolve an Xcode scheme — skipping build (best-effort)");
|
|
1830
2233
|
return null;
|
|
1831
2234
|
}
|
|
1832
2235
|
const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
|
|
@@ -1854,7 +2257,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1854
2257
|
const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
|
|
1855
2258
|
return schemes[0] ?? null;
|
|
1856
2259
|
} catch (err) {
|
|
1857
|
-
log.warn(
|
|
2260
|
+
log.warn(TAG7, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
|
|
1858
2261
|
return null;
|
|
1859
2262
|
}
|
|
1860
2263
|
}
|
|
@@ -1862,7 +2265,7 @@ function resolveXcodeScheme(pt) {
|
|
|
1862
2265
|
// src/revert-guard.ts
|
|
1863
2266
|
init_log();
|
|
1864
2267
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1865
|
-
var
|
|
2268
|
+
var TAG8 = "revert-guard";
|
|
1866
2269
|
var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
1867
2270
|
function isTestFile(path) {
|
|
1868
2271
|
return TEST_FILE.test(path);
|
|
@@ -1877,7 +2280,7 @@ function refetchBase(worktreePath, baseBranch) {
|
|
|
1877
2280
|
stdio: "pipe"
|
|
1878
2281
|
});
|
|
1879
2282
|
} catch {
|
|
1880
|
-
log.warn(
|
|
2283
|
+
log.warn(TAG8, "Failed to re-fetch base for revert guard — using last fetch");
|
|
1881
2284
|
}
|
|
1882
2285
|
}
|
|
1883
2286
|
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
@@ -1886,7 +2289,7 @@ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
|
1886
2289
|
return out.split(`
|
|
1887
2290
|
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1888
2291
|
} catch (err) {
|
|
1889
|
-
log.warn(
|
|
2292
|
+
log.warn(TAG8, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
1890
2293
|
return [];
|
|
1891
2294
|
}
|
|
1892
2295
|
}
|
|
@@ -1896,7 +2299,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
|
1896
2299
|
}
|
|
1897
2300
|
|
|
1898
2301
|
// src/verification.ts
|
|
1899
|
-
var
|
|
2302
|
+
var TAG9 = "verification";
|
|
1900
2303
|
var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
|
|
1901
2304
|
async function runVerification(worktreePath, config, workerId) {
|
|
1902
2305
|
const result = {
|
|
@@ -1908,52 +2311,52 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1908
2311
|
revertWarnings: []
|
|
1909
2312
|
};
|
|
1910
2313
|
if (config.verification.revertGuard) {
|
|
1911
|
-
log.info(
|
|
2314
|
+
log.info(TAG9, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
1912
2315
|
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
1913
2316
|
if (deletedTests.length > 0) {
|
|
1914
2317
|
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(
|
|
2318
|
+
log.warn(TAG9, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
1916
2319
|
result.passed = false;
|
|
1917
2320
|
} else {
|
|
1918
|
-
log.info(
|
|
2321
|
+
log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
|
|
1919
2322
|
}
|
|
1920
2323
|
}
|
|
1921
2324
|
if (config.verification.build) {
|
|
1922
|
-
log.info(
|
|
2325
|
+
log.info(TAG9, `[worker:${workerId}] Running build...`);
|
|
1923
2326
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
1924
2327
|
if (result.buildErrors.length > 0) {
|
|
1925
|
-
log.warn(
|
|
2328
|
+
log.warn(TAG9, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
1926
2329
|
result.passed = false;
|
|
1927
2330
|
} else {
|
|
1928
|
-
log.info(
|
|
2331
|
+
log.info(TAG9, `[worker:${workerId}] Build passed`);
|
|
1929
2332
|
}
|
|
1930
2333
|
}
|
|
1931
2334
|
if (config.verification.test && result.buildErrors.length === 0) {
|
|
1932
|
-
log.info(
|
|
2335
|
+
log.info(TAG9, `[worker:${workerId}] Running tests...`);
|
|
1933
2336
|
result.testFailures = runTests(worktreePath, config.verification.testTimeout);
|
|
1934
2337
|
if (result.testFailures.length > 0) {
|
|
1935
|
-
log.warn(
|
|
2338
|
+
log.warn(TAG9, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
|
|
1936
2339
|
result.passed = false;
|
|
1937
2340
|
} else {
|
|
1938
|
-
log.info(
|
|
2341
|
+
log.info(TAG9, `[worker:${workerId}] Tests passed`);
|
|
1939
2342
|
}
|
|
1940
2343
|
}
|
|
1941
2344
|
if (config.verification.lint) {
|
|
1942
|
-
log.info(
|
|
2345
|
+
log.info(TAG9, `[worker:${workerId}] Running lint...`);
|
|
1943
2346
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
1944
2347
|
if (result.lintWarnings.length > 0) {
|
|
1945
|
-
log.warn(
|
|
2348
|
+
log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
1946
2349
|
} else {
|
|
1947
|
-
log.info(
|
|
2350
|
+
log.info(TAG9, `[worker:${workerId}] Lint passed`);
|
|
1948
2351
|
}
|
|
1949
2352
|
}
|
|
1950
2353
|
if (config.verification.deepReview) {
|
|
1951
|
-
log.info(
|
|
2354
|
+
log.info(TAG9, `[worker:${workerId}] Running deep review...`);
|
|
1952
2355
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
1953
2356
|
if (result.reviewFindings.length > 0) {
|
|
1954
|
-
log.warn(
|
|
2357
|
+
log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
1955
2358
|
} else {
|
|
1956
|
-
log.info(
|
|
2359
|
+
log.info(TAG9, `[worker:${workerId}] Deep review passed`);
|
|
1957
2360
|
}
|
|
1958
2361
|
}
|
|
1959
2362
|
return result;
|
|
@@ -1961,7 +2364,7 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
1961
2364
|
function runBuild(worktreePath, timeout) {
|
|
1962
2365
|
const command = buildCommand(worktreePath);
|
|
1963
2366
|
if (!command) {
|
|
1964
|
-
log.warn(
|
|
2367
|
+
log.warn(TAG9, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
1965
2368
|
return [];
|
|
1966
2369
|
}
|
|
1967
2370
|
try {
|
|
@@ -1979,7 +2382,7 @@ function runBuild(worktreePath, timeout) {
|
|
|
1979
2382
|
function runTests(worktreePath, timeout) {
|
|
1980
2383
|
const command = testCommand(worktreePath);
|
|
1981
2384
|
if (!command) {
|
|
1982
|
-
log.warn(
|
|
2385
|
+
log.warn(TAG9, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
1983
2386
|
return [];
|
|
1984
2387
|
}
|
|
1985
2388
|
try {
|
|
@@ -1992,7 +2395,7 @@ function runTests(worktreePath, timeout) {
|
|
|
1992
2395
|
return [];
|
|
1993
2396
|
} catch (err) {
|
|
1994
2397
|
const output = combineOutput(err);
|
|
1995
|
-
log.warn(
|
|
2398
|
+
log.warn(TAG9, `Test run failed:
|
|
1996
2399
|
${output.slice(-4000) || "(no output captured)"}`);
|
|
1997
2400
|
return parseTestFailures(err, timeout);
|
|
1998
2401
|
}
|
|
@@ -2008,15 +2411,15 @@ function runFormatFix(worktreePath, timeout, workerId) {
|
|
|
2008
2411
|
stdio: "pipe",
|
|
2009
2412
|
maxBuffer: MAX_OUTPUT_BUFFER2
|
|
2010
2413
|
});
|
|
2011
|
-
log.info(
|
|
2414
|
+
log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2012
2415
|
} catch (err) {
|
|
2013
|
-
log.warn(
|
|
2416
|
+
log.warn(TAG9, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2014
2417
|
}
|
|
2015
2418
|
}
|
|
2016
2419
|
function runLint(worktreePath, timeout) {
|
|
2017
2420
|
const command = lintCommand(worktreePath);
|
|
2018
2421
|
if (!command) {
|
|
2019
|
-
log.info(
|
|
2422
|
+
log.info(TAG9, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2020
2423
|
return [];
|
|
2021
2424
|
}
|
|
2022
2425
|
try {
|
|
@@ -2033,7 +2436,7 @@ function runLint(worktreePath, timeout) {
|
|
|
2033
2436
|
}
|
|
2034
2437
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2035
2438
|
if (!supportsDevServer(worktreePath)) {
|
|
2036
|
-
log.info(
|
|
2439
|
+
log.info(TAG9, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2037
2440
|
return [];
|
|
2038
2441
|
}
|
|
2039
2442
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2048,7 +2451,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2048
2451
|
await waitForDevServer(devServer, 30000);
|
|
2049
2452
|
await probeDevServer(port);
|
|
2050
2453
|
} catch (err) {
|
|
2051
|
-
log.error(
|
|
2454
|
+
log.error(TAG9, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2052
2455
|
return [];
|
|
2053
2456
|
}
|
|
2054
2457
|
let diff = "";
|
|
@@ -2093,7 +2496,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2093
2496
|
});
|
|
2094
2497
|
return parseReviewFindings(output);
|
|
2095
2498
|
} catch (err) {
|
|
2096
|
-
log.error(
|
|
2499
|
+
log.error(TAG9, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2097
2500
|
return [];
|
|
2098
2501
|
} finally {
|
|
2099
2502
|
if (devServer && !devServer.killed) {
|
|
@@ -2132,7 +2535,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2132
2535
|
"--",
|
|
2133
2536
|
fixPrompt
|
|
2134
2537
|
];
|
|
2135
|
-
log.info(
|
|
2538
|
+
log.info(TAG9, "Spawning Claude for auto-fix...");
|
|
2136
2539
|
execFileSync4("claude", args, {
|
|
2137
2540
|
cwd: worktreePath,
|
|
2138
2541
|
timeout: config.verification.timeout,
|
|
@@ -2170,7 +2573,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2170
2573
|
try {
|
|
2171
2574
|
await client.createSubtask(cardId, title);
|
|
2172
2575
|
} catch (err) {
|
|
2173
|
-
log.error(
|
|
2576
|
+
log.error(TAG9, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2174
2577
|
}
|
|
2175
2578
|
}));
|
|
2176
2579
|
if (overflow > 0) {
|
|
@@ -2178,7 +2581,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2178
2581
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2179
2582
|
} catch {}
|
|
2180
2583
|
}
|
|
2181
|
-
log.info(
|
|
2584
|
+
log.info(TAG9, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2182
2585
|
}
|
|
2183
2586
|
function combineOutput(err) {
|
|
2184
2587
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2238,7 +2641,7 @@ class DevServerReadinessError extends Error {
|
|
|
2238
2641
|
}
|
|
2239
2642
|
}
|
|
2240
2643
|
function waitForDevServer(proc, timeout) {
|
|
2241
|
-
return new Promise((
|
|
2644
|
+
return new Promise((resolve2, reject) => {
|
|
2242
2645
|
let settled = false;
|
|
2243
2646
|
const cleanup = () => {
|
|
2244
2647
|
proc.stdout?.off("data", onData);
|
|
@@ -2252,7 +2655,7 @@ function waitForDevServer(proc, timeout) {
|
|
|
2252
2655
|
return;
|
|
2253
2656
|
settled = true;
|
|
2254
2657
|
cleanup();
|
|
2255
|
-
|
|
2658
|
+
resolve2();
|
|
2256
2659
|
};
|
|
2257
2660
|
const settleReject = (err) => {
|
|
2258
2661
|
if (settled)
|
|
@@ -2302,7 +2705,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2302
2705
|
}
|
|
2303
2706
|
|
|
2304
2707
|
// src/gate-collectors.ts
|
|
2305
|
-
var
|
|
2708
|
+
var TAG10 = "gate-collectors";
|
|
2306
2709
|
async function resolveStageGate(client, card) {
|
|
2307
2710
|
const currentStage = card.current_stage;
|
|
2308
2711
|
const playbookId = card.playbook_id;
|
|
@@ -2320,7 +2723,7 @@ async function resolveStageGate(client, card) {
|
|
|
2320
2723
|
return null;
|
|
2321
2724
|
return { stage: resolution.stage, gate };
|
|
2322
2725
|
} catch (err) {
|
|
2323
|
-
log.warn(
|
|
2726
|
+
log.warn(TAG10, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
|
|
2324
2727
|
return null;
|
|
2325
2728
|
}
|
|
2326
2729
|
}
|
|
@@ -2445,13 +2848,14 @@ function buildGateCollectorRegistry(deps) {
|
|
|
2445
2848
|
}
|
|
2446
2849
|
if (deps.oracle) {
|
|
2447
2850
|
registry.oracle_passed = new OracleCollector(deps.oracle);
|
|
2851
|
+
registry.oracle_red = new OracleRedCollector(deps.oracle);
|
|
2448
2852
|
}
|
|
2449
2853
|
return registry;
|
|
2450
2854
|
}
|
|
2451
2855
|
async function collectGateEvidence(registry, context) {
|
|
2452
2856
|
const collector = registry[context.gate.kind];
|
|
2453
2857
|
if (!collector) {
|
|
2454
|
-
log.info(
|
|
2858
|
+
log.info(TAG10, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
|
|
2455
2859
|
return {
|
|
2456
2860
|
result: "blocked",
|
|
2457
2861
|
structured: {
|
|
@@ -2463,14 +2867,14 @@ async function collectGateEvidence(registry, context) {
|
|
|
2463
2867
|
return await collector.collect(context);
|
|
2464
2868
|
} catch (err) {
|
|
2465
2869
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2466
|
-
log.warn(
|
|
2870
|
+
log.warn(TAG10, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
|
|
2467
2871
|
return { result: "blocked", structured: { error: msg } };
|
|
2468
2872
|
}
|
|
2469
2873
|
}
|
|
2470
2874
|
|
|
2471
2875
|
// src/harmony-client.ts
|
|
2472
2876
|
init_log();
|
|
2473
|
-
var
|
|
2877
|
+
var TAG11 = "harmony-client";
|
|
2474
2878
|
function readClientConfig(env) {
|
|
2475
2879
|
const apiUrl = env.HARMONY_API_URL?.trim();
|
|
2476
2880
|
const apiKey = env.HARMONY_API_KEY?.trim();
|
|
@@ -2522,7 +2926,7 @@ class HarmonyClient {
|
|
|
2522
2926
|
purpose: "gate_evaluation"
|
|
2523
2927
|
});
|
|
2524
2928
|
if (!response.ok) {
|
|
2525
|
-
log.warn(
|
|
2929
|
+
log.warn(TAG11, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
|
|
2526
2930
|
return null;
|
|
2527
2931
|
}
|
|
2528
2932
|
const body = await response.json();
|
|
@@ -2603,140 +3007,6 @@ function relayAgentEvent(draft) {
|
|
|
2603
3007
|
return { type: "agent_event", event: draft };
|
|
2604
3008
|
}
|
|
2605
3009
|
|
|
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
3010
|
// src/stage-cli.ts
|
|
2741
3011
|
init_dist();
|
|
2742
3012
|
|
|
@@ -2985,7 +3255,7 @@ async function runStage(request, deps) {
|
|
|
2985
3255
|
}
|
|
2986
3256
|
|
|
2987
3257
|
// src/cli.ts
|
|
2988
|
-
var
|
|
3258
|
+
var TAG12 = "cli";
|
|
2989
3259
|
var GATE_VERIFICATION_TIMEOUT_MS = 600000;
|
|
2990
3260
|
var SHUTDOWN_HARD_EXIT_MS = 15000;
|
|
2991
3261
|
var activeRunner = null;
|
|
@@ -3016,12 +3286,12 @@ async function runRole(request, prompt, emit2) {
|
|
|
3016
3286
|
});
|
|
3017
3287
|
const runner = new SdkAgentRunner(launch.config);
|
|
3018
3288
|
activeRunner = runner;
|
|
3019
|
-
log.info(
|
|
3289
|
+
log.info(TAG12, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
|
|
3020
3290
|
const timeoutMs = stageTimeoutMs(process.env);
|
|
3021
3291
|
let timedOut = false;
|
|
3022
3292
|
const clock = timeoutMs > 0 ? setTimeout(() => {
|
|
3023
3293
|
timedOut = true;
|
|
3024
|
-
log.warn(
|
|
3294
|
+
log.warn(TAG12, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
|
|
3025
3295
|
runner.stop("timeout");
|
|
3026
3296
|
}, timeoutMs) : null;
|
|
3027
3297
|
clock?.unref?.();
|
|
@@ -3037,9 +3307,9 @@ async function runRole(request, prompt, emit2) {
|
|
|
3037
3307
|
if (relayed)
|
|
3038
3308
|
emit2(relayed);
|
|
3039
3309
|
if (event.kind === "error") {
|
|
3040
|
-
log.warn(
|
|
3310
|
+
log.warn(TAG12, `subagent error: ${event.payload.message}`);
|
|
3041
3311
|
} else {
|
|
3042
|
-
log.event(
|
|
3312
|
+
log.event(TAG12, `subagent ${event.kind}`);
|
|
3043
3313
|
}
|
|
3044
3314
|
}
|
|
3045
3315
|
} finally {
|
|
@@ -3070,8 +3340,8 @@ ${STAGE_RUN_USAGE}
|
|
|
3070
3340
|
const { cardId, stageId, workspaceId, repoPath, sessionId, metricsPath } = parsed.args;
|
|
3071
3341
|
let driverMetrics = {};
|
|
3072
3342
|
if (metricsPath !== null) {
|
|
3073
|
-
const { readFileSync:
|
|
3074
|
-
driverMetrics = parseMetricsAllowlist(
|
|
3343
|
+
const { readFileSync: readFileSync3 } = await import("node:fs");
|
|
3344
|
+
driverMetrics = parseMetricsAllowlist(readFileSync3(metricsPath, "utf8"), metricsPath);
|
|
3075
3345
|
}
|
|
3076
3346
|
const client = new HarmonyClient(readClientConfig(process.env));
|
|
3077
3347
|
const card = await client.fetchStageCard(cardId);
|
|
@@ -3085,12 +3355,13 @@ ${STAGE_RUN_USAGE}
|
|
|
3085
3355
|
throw new Error(`refusing to run stage "${stageId}" for card ${cardId}: ${pinned.reason}`);
|
|
3086
3356
|
}
|
|
3087
3357
|
assertStageIsAgentRunnable(pinned.stage);
|
|
3358
|
+
const oracleTargetStageId = findOracleTargetStage(version, stageId);
|
|
3088
3359
|
const prompt = buildStagePrompt({
|
|
3089
3360
|
cardId,
|
|
3090
3361
|
stageId,
|
|
3091
3362
|
stage: pinned.stage,
|
|
3092
3363
|
sessionId,
|
|
3093
|
-
oracleTargetStageId
|
|
3364
|
+
oracleTargetStageId
|
|
3094
3365
|
});
|
|
3095
3366
|
const request = {
|
|
3096
3367
|
cardId,
|
|
@@ -3118,6 +3389,7 @@ ${STAGE_RUN_USAGE}
|
|
|
3118
3389
|
oracle: {
|
|
3119
3390
|
repoPath: req.repoPath,
|
|
3120
3391
|
sessionId: req.sessionId,
|
|
3392
|
+
targetStageId: oracleTargetStageId,
|
|
3121
3393
|
fetchOracle: (oracleCardId, oracleStageId, oracleSessionId) => client.fetchOracle(oracleCardId, oracleStageId, oracleSessionId),
|
|
3122
3394
|
place,
|
|
3123
3395
|
remove,
|