@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 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: `result ${r.subtype}: ${joined || "(no detail)"}`,
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
- var TAG4 = "oracle-collector";
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
- class OracleCollector {
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 oracle = await this.deps.fetchOracle(context.cardId, context.stageId, this.deps.sessionId);
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(TAG4, `No oracle held for stage ${context.stageId} — blocked`);
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 ${context.stageId}.`
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(TAG4, logLine);
1922
+ log.info(TAG5, logLine);
1577
1923
  } else {
1578
- log.warn(TAG4, logLine);
1924
+ log.warn(TAG5, logLine);
1579
1925
  }
1580
- return {
1581
- result: exitCode === 0 ? "passed" : "failed",
1582
- structured: {
1583
- oracle: {
1584
- exitCode,
1585
- path: oracle.path,
1586
- ...identity,
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(TAG4, `Oracle run threw: ${message} — blocked`);
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(TAG4, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
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(TAG4, `Held test at ${oracle.path} removed on the second attempt`);
1956
+ log.info(TAG5, `Held test at ${oracle.path} removed on the second attempt`);
1615
1957
  } catch (err) {
1616
- log.error(TAG4, `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.");
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 TAG5 = "pm";
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(TAG5, `Detected package manager: ${cached}`);
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 "bun install --frozen-lockfile";
2062
+ return `bun install --frozen-lockfile${skip}`;
1662
2063
  case "pnpm":
1663
- return "pnpm install --frozen-lockfile";
2064
+ return `pnpm install --frozen-lockfile${skip}`;
1664
2065
  case "yarn":
1665
- return "yarn install --frozen-lockfile";
2066
+ return `yarn install --frozen-lockfile${skip}`;
1666
2067
  case "npm":
1667
- return "npm ci";
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 TAG6 = "project-type";
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(TAG6, `Detected project type in ${dir}: ${result.kind}`);
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(readFileSync(`${dir}/package.json`, "utf-8"));
2185
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
1785
2186
  script = pkg.scripts?.test;
1786
2187
  } catch (err) {
1787
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG6, `package.json 'test' is the npm placeholder — skipping tests`);
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(readFileSync(`${dir}/package.json`, "utf-8"));
2202
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
1802
2203
  scripts = pkg.scripts ?? {};
1803
2204
  } catch (err) {
1804
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG6, "Could not resolve an Xcode scheme — skipping build (best-effort)");
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(TAG6, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
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 TAG7 = "revert-guard";
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(TAG7, "Failed to re-fetch base for revert guard — using last fetch");
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(TAG7, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
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 TAG8 = "verification";
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(TAG8, `[worker:${workerId}] Checking for reverted merged work...`);
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(TAG8, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
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(TAG8, `[worker:${workerId}] Revert guard passed`);
2313
+ log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
1913
2314
  }
1914
2315
  }
1915
2316
  if (config.verification.build) {
1916
- log.info(TAG8, `[worker:${workerId}] Running build...`);
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(TAG8, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
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(TAG8, `[worker:${workerId}] Build passed`);
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(TAG8, `[worker:${workerId}] Running tests...`);
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(TAG8, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
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(TAG8, `[worker:${workerId}] Tests passed`);
2333
+ log.info(TAG9, `[worker:${workerId}] Tests passed`);
1933
2334
  }
1934
2335
  }
1935
2336
  if (config.verification.lint) {
1936
- log.info(TAG8, `[worker:${workerId}] Running lint...`);
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(TAG8, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2340
+ log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
1940
2341
  } else {
1941
- log.info(TAG8, `[worker:${workerId}] Lint passed`);
2342
+ log.info(TAG9, `[worker:${workerId}] Lint passed`);
1942
2343
  }
1943
2344
  }
1944
2345
  if (config.verification.deepReview) {
1945
- log.info(TAG8, `[worker:${workerId}] Running deep review...`);
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(TAG8, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2349
+ log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
1949
2350
  } else {
1950
- log.info(TAG8, `[worker:${workerId}] Deep review passed`);
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(TAG8, `No known build toolchain for ${worktreePath} — skipping build`);
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(TAG8, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
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(TAG8, `Test run failed:
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(TAG8, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2406
+ log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2006
2407
  } catch (err) {
2007
- log.warn(TAG8, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
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(TAG8, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
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(TAG8, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
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(TAG8, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
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(TAG8, `Deep review failed: ${err instanceof Error ? err.message : err}`);
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(TAG8, "Spawning Claude for auto-fix...");
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(TAG8, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
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(TAG8, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
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((resolve, reject) => {
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
- resolve();
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 TAG9 = "gate-collectors";
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(TAG9, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
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(TAG9, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
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(TAG9, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
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 TAG10 = "harmony-client";
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(TAG10, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
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 TAG11 = "cli";
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(TAG11, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
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(TAG11, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
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(TAG11, `subagent error: ${event.payload.message}`);
3302
+ log.warn(TAG12, `subagent error: ${event.payload.message}`);
3035
3303
  } else {
3036
- log.event(TAG11, `subagent ${event.kind}`);
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: readFileSync2 } = await import("node:fs");
3068
- driverMetrics = parseMetricsAllowlist(readFileSync2(metricsPath, "utf8"), metricsPath);
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: findOracleTargetStage(version, stageId)
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,