@gethmy/harness 1.2.1 → 1.3.0

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