@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 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: `result ${r.subtype}: ${joined || "(no detail)"}`,
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
- var TAG4 = "oracle-collector";
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
- class OracleCollector {
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 oracle = await this.deps.fetchOracle(context.cardId, context.stageId, this.deps.sessionId);
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(TAG4, `No oracle held for stage ${context.stageId} — blocked`);
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 ${context.stageId}.`
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(TAG4, logLine);
1930
+ log.info(TAG5, logLine);
1583
1931
  } else {
1584
- log.warn(TAG4, logLine);
1932
+ log.warn(TAG5, logLine);
1585
1933
  }
1586
- return {
1587
- result: exitCode === 0 ? "passed" : "failed",
1588
- structured: {
1589
- oracle: {
1590
- exitCode,
1591
- path: oracle.path,
1592
- ...identity,
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(TAG4, `Oracle run threw: ${message} — blocked`);
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(TAG4, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
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(TAG4, `Held test at ${oracle.path} removed on the second attempt`);
1964
+ log.info(TAG5, `Held test at ${oracle.path} removed on the second attempt`);
1621
1965
  } catch (err) {
1622
- 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.");
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 TAG5 = "pm";
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(TAG5, `Detected package manager: ${cached}`);
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 "bun install --frozen-lockfile";
2070
+ return `bun install --frozen-lockfile${skip}`;
1668
2071
  case "pnpm":
1669
- return "pnpm install --frozen-lockfile";
2072
+ return `pnpm install --frozen-lockfile${skip}`;
1670
2073
  case "yarn":
1671
- return "yarn install --frozen-lockfile";
2074
+ return `yarn install --frozen-lockfile${skip}`;
1672
2075
  case "npm":
1673
- return "npm ci";
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 TAG6 = "project-type";
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(TAG6, `Detected project type in ${dir}: ${result.kind}`);
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(readFileSync(`${dir}/package.json`, "utf-8"));
2193
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
1791
2194
  script = pkg.scripts?.test;
1792
2195
  } catch (err) {
1793
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG6, `package.json 'test' is the npm placeholder — skipping tests`);
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(readFileSync(`${dir}/package.json`, "utf-8"));
2210
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
1808
2211
  scripts = pkg.scripts ?? {};
1809
2212
  } catch (err) {
1810
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
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(TAG6, "Could not resolve an Xcode scheme — skipping build (best-effort)");
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(TAG6, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
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 TAG7 = "revert-guard";
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(TAG7, "Failed to re-fetch base for revert guard — using last fetch");
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(TAG7, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
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 TAG8 = "verification";
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(TAG8, `[worker:${workerId}] Checking for reverted merged work...`);
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(TAG8, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
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(TAG8, `[worker:${workerId}] Revert guard passed`);
2321
+ log.info(TAG9, `[worker:${workerId}] Revert guard passed`);
1919
2322
  }
1920
2323
  }
1921
2324
  if (config.verification.build) {
1922
- log.info(TAG8, `[worker:${workerId}] Running build...`);
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(TAG8, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
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(TAG8, `[worker:${workerId}] Build passed`);
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(TAG8, `[worker:${workerId}] Running tests...`);
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(TAG8, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
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(TAG8, `[worker:${workerId}] Tests passed`);
2341
+ log.info(TAG9, `[worker:${workerId}] Tests passed`);
1939
2342
  }
1940
2343
  }
1941
2344
  if (config.verification.lint) {
1942
- log.info(TAG8, `[worker:${workerId}] Running lint...`);
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(TAG8, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2348
+ log.warn(TAG9, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
1946
2349
  } else {
1947
- log.info(TAG8, `[worker:${workerId}] Lint passed`);
2350
+ log.info(TAG9, `[worker:${workerId}] Lint passed`);
1948
2351
  }
1949
2352
  }
1950
2353
  if (config.verification.deepReview) {
1951
- log.info(TAG8, `[worker:${workerId}] Running deep review...`);
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(TAG8, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2357
+ log.warn(TAG9, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
1955
2358
  } else {
1956
- log.info(TAG8, `[worker:${workerId}] Deep review passed`);
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(TAG8, `No known build toolchain for ${worktreePath} — skipping build`);
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(TAG8, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
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(TAG8, `Test run failed:
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(TAG8, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2414
+ log.info(TAG9, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2012
2415
  } catch (err) {
2013
- log.warn(TAG8, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
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(TAG8, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
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(TAG8, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
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(TAG8, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
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(TAG8, `Deep review failed: ${err instanceof Error ? err.message : err}`);
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(TAG8, "Spawning Claude for auto-fix...");
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(TAG8, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
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(TAG8, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
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((resolve, reject) => {
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
- resolve();
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 TAG9 = "gate-collectors";
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(TAG9, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
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(TAG9, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
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(TAG9, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
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 TAG10 = "harmony-client";
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(TAG10, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
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 TAG11 = "cli";
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(TAG11, `Running stage ${request.stageId} as role ${launch.role ?? "(none — fail-closed)"}`);
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(TAG11, `stage ${request.stageId} exceeded ${timeoutMs}ms — stopping the subagent`);
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(TAG11, `subagent error: ${event.payload.message}`);
3310
+ log.warn(TAG12, `subagent error: ${event.payload.message}`);
3041
3311
  } else {
3042
- log.event(TAG11, `subagent ${event.kind}`);
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: readFileSync2 } = await import("node:fs");
3074
- driverMetrics = parseMetricsAllowlist(readFileSync2(metricsPath, "utf8"), metricsPath);
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: findOracleTargetStage(version, stageId)
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,