@gethmy/harness 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -118,6 +118,9 @@ var init_branchRef = __esm(() => {
118
118
 
119
119
  // ../harmony-shared/dist/cardLinks.js
120
120
  var init_cardLinks = () => {};
121
+
122
+ // ../harmony-shared/dist/cardModelStat.js
123
+ var init_cardModelStat = () => {};
121
124
  // ../harmony-shared/dist/classification.js
122
125
  function tierFromScore(score) {
123
126
  const s = Math.max(0, Math.min(10, Math.round(score)));
@@ -164,6 +167,11 @@ var init_constants = __esm(() => {
164
167
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
165
168
  };
166
169
  });
170
+ // ../harmony-shared/dist/fanoutSource.js
171
+ var FANOUT_KEY_MARKER = "harmony:fanout-item", FANOUT_KEY_RE;
172
+ var init_fanoutSource = __esm(() => {
173
+ FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
174
+ });
167
175
  // ../harmony-shared/dist/gateConfigError.js
168
176
  function gateConfigErrorReason(evaluation) {
169
177
  if (!evaluation || evaluation.passed)
@@ -471,7 +479,8 @@ var init_gateEvaluate = __esm(() => {
471
479
  "artifact",
472
480
  "label",
473
481
  "custom",
474
- "oracle_passed"
482
+ "oracle_passed",
483
+ "oracle_red"
475
484
  ];
476
485
  GATE_OPERATORS = [
477
486
  "eq",
@@ -574,11 +583,13 @@ var init_dist = __esm(() => {
574
583
  init_agentStaleness();
575
584
  init_branchRef();
576
585
  init_cardLinks();
586
+ init_cardModelStat();
577
587
  init_classification();
578
588
  init_columnCardSplit();
579
589
  init_columnSort();
580
590
  init_commentSerializer();
581
591
  init_constants();
592
+ init_fanoutSource();
582
593
  init_gateConfigError();
583
594
  init_gateEvaluate();
584
595
  init_logger();
@@ -596,11 +607,14 @@ var exports_git_pr = {};
596
607
  __export(exports_git_pr, {
597
608
  validateGitProviderCli: () => validateGitProviderCli,
598
609
  upsertReviewedSha: () => upsertReviewedSha,
610
+ upsertCiReviewRequestedSha: () => upsertCiReviewRequestedSha,
599
611
  updateExistingPr: () => updateExistingPr,
600
612
  resolvePrUrl: () => resolvePrUrl,
601
613
  resolvePrHeadBranch: () => resolvePrHeadBranch,
614
+ requestIndependentReview: () => requestIndependentReview,
602
615
  renameRemoteBranch: () => renameRemoteBranch,
603
616
  remoteBranchExists: () => remoteBranchExists,
617
+ readPrChecks: () => readPrChecks,
604
618
  pushBranch: () => pushBranch,
605
619
  mergePullRequest: () => mergePullRequest,
606
620
  getPrStatus: () => getPrStatus,
@@ -609,25 +623,28 @@ __export(exports_git_pr, {
609
623
  findExistingPr: () => findExistingPr,
610
624
  extractReviewedSha: () => extractReviewedSha,
611
625
  extractPrUrl: () => extractPrUrl,
626
+ extractCiReviewRequestedSha: () => extractCiReviewRequestedSha,
612
627
  extractAzurePrId: () => extractAzurePrId,
613
628
  detectGitProvider: () => detectGitProvider,
629
+ deriveReviewVerdictState: () => deriveReviewVerdictState,
630
+ deriveIndependentReviewState: () => deriveIndependentReviewState,
614
631
  deriveCiStatus: () => deriveCiStatus,
615
632
  decidePrBranch: () => decidePrBranch,
616
633
  createPullRequest: () => createPullRequest,
617
634
  checkPrMergeStatus: () => checkPrMergeStatus,
618
635
  buildPrBody: () => buildPrBody
619
636
  });
620
- import { execFile, execFileSync as execFileSync6 } from "node:child_process";
621
- import { promisify } from "node:util";
622
- function createExecFileAsync() {
623
- return promisify(execFile);
637
+ import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
638
+ import { promisify as promisify2 } from "node:util";
639
+ function createExecFileAsync2() {
640
+ return promisify2(execFile2);
624
641
  }
625
- function execFileAsync() {
626
- return cachedExecFileAsync ??= createExecFileAsync();
642
+ function execFileAsync2() {
643
+ return cachedExecFileAsync2 ??= createExecFileAsync2();
627
644
  }
628
645
  function detectGitProvider(cwd) {
629
646
  try {
630
- const url = execFileSync6("git", ["remote", "get-url", "origin"], {
647
+ const url = execFileSync7("git", ["remote", "get-url", "origin"], {
631
648
  cwd,
632
649
  encoding: "utf-8"
633
650
  }).trim();
@@ -648,7 +665,7 @@ function validateGitProviderCli(provider, cwd) {
648
665
  switch (provider) {
649
666
  case "github": {
650
667
  try {
651
- execFileSync6("gh", ["auth", "status"], { cwd, stdio: "pipe" });
668
+ execFileSync7("gh", ["auth", "status"], { cwd, stdio: "pipe" });
652
669
  } catch {
653
670
  throw new Error("GitHub CLI (gh) is not authenticated. Run: gh auth login");
654
671
  }
@@ -656,12 +673,12 @@ function validateGitProviderCli(provider, cwd) {
656
673
  }
657
674
  case "azure": {
658
675
  try {
659
- execFileSync6("az", ["--version"], { cwd, stdio: "pipe" });
676
+ execFileSync7("az", ["--version"], { cwd, stdio: "pipe" });
660
677
  } catch {
661
678
  throw new Error("Azure CLI (az) not found. Install it: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli");
662
679
  }
663
680
  try {
664
- execFileSync6("az", ["account", "show"], { cwd, stdio: "pipe" });
681
+ execFileSync7("az", ["account", "show"], { cwd, stdio: "pipe" });
665
682
  } catch {
666
683
  throw new Error("Azure CLI is not authenticated. Run: az login");
667
684
  }
@@ -669,7 +686,7 @@ function validateGitProviderCli(provider, cwd) {
669
686
  }
670
687
  case "gitlab": {
671
688
  try {
672
- execFileSync6("glab", ["auth", "status"], { cwd, stdio: "pipe" });
689
+ execFileSync7("glab", ["auth", "status"], { cwd, stdio: "pipe" });
673
690
  } catch {
674
691
  throw new Error("GitLab CLI (glab) is not installed or not authenticated. Install: https://gitlab.com/gitlab-org/cli — then run: glab auth login");
675
692
  }
@@ -677,7 +694,7 @@ function validateGitProviderCli(provider, cwd) {
677
694
  }
678
695
  case "bitbucket":
679
696
  case "unknown":
680
- log.warn(TAG11, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
697
+ log.warn(TAG13, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
681
698
  break;
682
699
  }
683
700
  }
@@ -695,18 +712,51 @@ function upsertReviewedSha(description, sha) {
695
712
  if (REVIEWED_SHA_RE.test(description)) {
696
713
  return description.replace(REVIEWED_SHA_RE, line);
697
714
  }
698
- const sep2 = description ? `
715
+ const sep3 = description ? `
716
+ ` : "";
717
+ return `${description}${sep3}${line}`;
718
+ }
719
+ function extractCiReviewRequestedSha(description) {
720
+ if (!description)
721
+ return null;
722
+ const m = description.match(CI_REVIEW_REQUESTED_SHA_RE);
723
+ return m ? m[1] : null;
724
+ }
725
+ function upsertCiReviewRequestedSha(description, sha) {
726
+ const line = `CI-Review-Requested-SHA: ${sha}`;
727
+ if (CI_REVIEW_REQUESTED_SHA_RE.test(description)) {
728
+ return description.replace(CI_REVIEW_REQUESTED_SHA_RE, line);
729
+ }
730
+ const sep3 = description ? `
699
731
  ` : "";
700
- return `${description}${sep2}${line}`;
732
+ return `${description}${sep3}${line}`;
733
+ }
734
+ function checkMatchesName(c, wanted) {
735
+ if (!wanted)
736
+ return false;
737
+ const name = typeof c.name === "string" ? c.name.toLowerCase() : "";
738
+ const workflow = typeof c.workflowName === "string" ? c.workflowName.toLowerCase() : "";
739
+ return name === wanted || workflow === wanted;
701
740
  }
702
- function deriveCiStatus(rollup) {
741
+ function isNamedCheck(c, skip) {
742
+ if (!skip)
743
+ return false;
744
+ const wanted = skip.trim().toLowerCase();
745
+ if (!wanted)
746
+ return false;
747
+ return typeof c.name === "string" && c.name.toLowerCase() === wanted;
748
+ }
749
+ function deriveCiStatus(rollup, excludeChecks = []) {
703
750
  if (!Array.isArray(rollup) || rollup.length === 0)
704
751
  return "unknown";
752
+ const excluded = excludeChecks.map((n) => n.trim().toLowerCase()).filter(Boolean);
705
753
  let anyPending = false;
706
754
  for (const check of rollup) {
707
755
  if (typeof check !== "object" || check === null)
708
756
  continue;
709
757
  const c = check;
758
+ if (excluded.some((name) => checkMatchesName(c, name)))
759
+ continue;
710
760
  if (typeof c.status === "string") {
711
761
  if (c.status.toUpperCase() !== "COMPLETED") {
712
762
  anyPending = true;
@@ -730,19 +780,153 @@ function deriveCiStatus(rollup) {
730
780
  }
731
781
  return anyPending ? "pending" : "success";
732
782
  }
733
- async function getPrStatus(prUrl, cwd, provider) {
734
- if (provider !== "github" || !isValidPrUrl(prUrl)) {
735
- return { ciStatus: "unknown", headSha: null };
783
+ function deriveIndependentReviewState(rollup, checkName, skipCheckName) {
784
+ if (!Array.isArray(rollup))
785
+ return "absent";
786
+ const wanted = checkName.trim().toLowerCase();
787
+ if (!wanted)
788
+ return "absent";
789
+ let sawPending = false;
790
+ let sawFailed = false;
791
+ let sawSkipped = false;
792
+ for (const check of rollup) {
793
+ if (typeof check !== "object" || check === null)
794
+ continue;
795
+ const c = check;
796
+ if (isNamedCheck(c, skipCheckName))
797
+ continue;
798
+ if (!checkMatchesName(c, wanted))
799
+ continue;
800
+ if (typeof c.status === "string") {
801
+ if (c.status.toUpperCase() !== "COMPLETED") {
802
+ sawPending = true;
803
+ continue;
804
+ }
805
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
806
+ if (conclusion === "SKIPPED") {
807
+ sawSkipped = true;
808
+ continue;
809
+ }
810
+ if (conclusion === "SUCCESS" || conclusion === "NEUTRAL")
811
+ return "passed";
812
+ sawFailed = true;
813
+ continue;
814
+ }
815
+ if (typeof c.state === "string") {
816
+ const state = c.state.toUpperCase();
817
+ if (state === "SUCCESS")
818
+ return "passed";
819
+ if (state === "PENDING") {
820
+ sawPending = true;
821
+ continue;
822
+ }
823
+ sawFailed = true;
824
+ }
825
+ }
826
+ if (sawPending)
827
+ return "pending";
828
+ if (sawFailed)
829
+ return "failed";
830
+ if (sawSkipped)
831
+ return "skipped";
832
+ return "absent";
833
+ }
834
+ function deriveReviewVerdictState(rollup, checkName, skipCheckName) {
835
+ if (!Array.isArray(rollup))
836
+ return "absent";
837
+ const wanted = checkName.trim().toLowerCase();
838
+ if (!wanted)
839
+ return "absent";
840
+ let sawPending = false;
841
+ let sawUnknown = false;
842
+ let sawClean = false;
843
+ for (const check of rollup) {
844
+ if (typeof check !== "object" || check === null)
845
+ continue;
846
+ const c = check;
847
+ if (isNamedCheck(c, skipCheckName))
848
+ continue;
849
+ if (!checkMatchesName(c, wanted))
850
+ continue;
851
+ if (typeof c.status === "string") {
852
+ if (c.status.toUpperCase() !== "COMPLETED") {
853
+ sawPending = true;
854
+ continue;
855
+ }
856
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
857
+ if (conclusion === "SUCCESS") {
858
+ sawClean = true;
859
+ continue;
860
+ }
861
+ if (conclusion === "NEUTRAL" || conclusion === "SKIPPED") {
862
+ sawUnknown = true;
863
+ continue;
864
+ }
865
+ return "blocking";
866
+ }
867
+ if (typeof c.state === "string") {
868
+ const state = c.state.toUpperCase();
869
+ if (state === "SUCCESS") {
870
+ sawClean = true;
871
+ continue;
872
+ }
873
+ if (state === "PENDING") {
874
+ sawPending = true;
875
+ continue;
876
+ }
877
+ return "blocking";
878
+ }
736
879
  }
880
+ if (sawPending)
881
+ return "pending";
882
+ if (sawUnknown)
883
+ return "unknown";
884
+ if (sawClean)
885
+ return "clean";
886
+ return "absent";
887
+ }
888
+ function readPrChecks(rollup, checks = {}) {
889
+ const { independentReviewCheck, reviewVerdictCheck } = checks;
890
+ return {
891
+ ciStatus: deriveCiStatus(rollup, [independentReviewCheck, reviewVerdictCheck].filter((n) => typeof n === "string")),
892
+ independentReview: independentReviewCheck ? deriveIndependentReviewState(rollup, independentReviewCheck, reviewVerdictCheck) : "absent",
893
+ reviewVerdict: reviewVerdictCheck ? deriveReviewVerdictState(rollup, reviewVerdictCheck, independentReviewCheck) : "absent"
894
+ };
895
+ }
896
+ async function getPrStatus(prUrl, cwd, provider, independentReviewCheck, reviewVerdictCheck) {
897
+ const unreadable = {
898
+ ciStatus: "unknown",
899
+ headSha: null,
900
+ independentReview: "absent",
901
+ reviewVerdict: "absent"
902
+ };
903
+ if (provider !== "github" || !isValidPrUrl(prUrl))
904
+ return { ...unreadable };
737
905
  try {
738
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup,headRefOid"], { cwd, encoding: "utf-8", timeout: 1e4 });
906
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup,headRefOid"], { cwd, encoding: "utf-8", timeout: 1e4 });
739
907
  const parsed = JSON.parse(stdout.trim());
740
908
  const headSha = typeof parsed.headRefOid === "string" ? parsed.headRefOid : null;
741
- return { ciStatus: deriveCiStatus(parsed.statusCheckRollup), headSha };
909
+ return {
910
+ ...readPrChecks(parsed.statusCheckRollup, {
911
+ independentReviewCheck,
912
+ reviewVerdictCheck
913
+ }),
914
+ headSha
915
+ };
742
916
  } catch {
743
- return { ciStatus: "unknown", headSha: null };
917
+ return { ...unreadable };
744
918
  }
745
919
  }
920
+ async function requestIndependentReview(prUrl, cwd, provider, label) {
921
+ if (provider !== "github") {
922
+ throw new Error(`independent review request unsupported for "${provider}"`);
923
+ }
924
+ const opts = { cwd, encoding: "utf-8", timeout: 30000 };
925
+ try {
926
+ await execFileAsync2()("gh", ["pr", "edit", prUrl, "--remove-label", label], opts);
927
+ } catch {}
928
+ await execFileAsync2()("gh", ["pr", "edit", prUrl, "--add-label", label], opts);
929
+ }
746
930
  async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
747
931
  if (provider !== "github") {
748
932
  throw new Error(`auto-merge unsupported for provider "${provider}"`);
@@ -750,7 +934,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
750
934
  const args = ["pr", "merge", prUrl, `--${strategy}`];
751
935
  if (deleteBranch)
752
936
  args.push("--delete-branch");
753
- await execFileAsync()("gh", args, {
937
+ await execFileAsync2()("gh", args, {
754
938
  cwd,
755
939
  encoding: "utf-8",
756
940
  timeout: 30000
@@ -758,7 +942,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
758
942
  }
759
943
  function getHeadSha(cwd) {
760
944
  try {
761
- return execFileSync6("git", ["rev-parse", "HEAD"], {
945
+ return execFileSync7("git", ["rev-parse", "HEAD"], {
762
946
  cwd,
763
947
  encoding: "utf-8"
764
948
  }).trim();
@@ -772,7 +956,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
772
956
  try {
773
957
  switch (provider) {
774
958
  case "github": {
775
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 1e4 });
959
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 1e4 });
776
960
  switch (stdout.trim()) {
777
961
  case "MERGED":
778
962
  return "merged";
@@ -788,12 +972,12 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
788
972
  const mrMatch = prUrl.match(/merge_requests\/(\d+)/);
789
973
  if (!mrMatch)
790
974
  return "unknown";
791
- const { stdout } = await execFileAsync()("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
975
+ const { stdout } = await execFileAsync2()("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
792
976
  let parsed;
793
977
  try {
794
978
  parsed = JSON.parse(stdout.trim());
795
979
  } catch {
796
- log.warn(TAG11, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
980
+ log.warn(TAG13, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
797
981
  return "unknown";
798
982
  }
799
983
  if (typeof parsed !== "object" || parsed === null)
@@ -888,10 +1072,10 @@ function extractAzurePrId(prUrl) {
888
1072
  async function resolvePrHeadBranch(prUrl, cwd, provider) {
889
1073
  if (provider === "github") {
890
1074
  try {
891
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1075
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
892
1076
  return decidePrBranch("github", stdout);
893
1077
  } catch (err) {
894
- log.warn(TAG11, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1078
+ log.warn(TAG13, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
895
1079
  return decidePrBranch("github", null);
896
1080
  }
897
1081
  }
@@ -904,10 +1088,10 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
904
1088
  };
905
1089
  }
906
1090
  try {
907
- const { stdout } = await execFileAsync()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1091
+ const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
908
1092
  return decidePrBranch("azure", stdout);
909
1093
  } catch (err) {
910
- log.warn(TAG11, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1094
+ log.warn(TAG13, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
911
1095
  return decidePrBranch("azure", null);
912
1096
  }
913
1097
  }
@@ -935,7 +1119,7 @@ function resolvePrUrl(description, branchName, cwd, provider) {
935
1119
  }
936
1120
  function remoteBranchExists(branchName, cwd) {
937
1121
  try {
938
- execFileSync6("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
1122
+ execFileSync7("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
939
1123
  return true;
940
1124
  } catch {
941
1125
  return false;
@@ -943,24 +1127,24 @@ function remoteBranchExists(branchName, cwd) {
943
1127
  }
944
1128
  function pushBranch(branchName, cwd) {
945
1129
  if (remoteBranchExists(branchName, cwd)) {
946
- log.info(TAG11, `Remote branch ${branchName} exists (rework), force-pushing`);
1130
+ log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
947
1131
  let expectedSha = null;
948
1132
  try {
949
- execFileSync6("git", ["fetch", "origin", branchName], {
1133
+ execFileSync7("git", ["fetch", "origin", branchName], {
950
1134
  cwd,
951
1135
  stdio: "pipe"
952
1136
  });
953
- expectedSha = execFileSync6("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
1137
+ expectedSha = execFileSync7("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
954
1138
  } catch (err) {
955
- log.warn(TAG11, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1139
+ log.warn(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
956
1140
  }
957
1141
  const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
958
- execFileSync6("git", ["push", lease, "-u", "origin", branchName], {
1142
+ execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
959
1143
  cwd,
960
1144
  stdio: "pipe"
961
1145
  });
962
1146
  } else {
963
- execFileSync6("git", ["push", "-u", "origin", branchName], {
1147
+ execFileSync7("git", ["push", "-u", "origin", branchName], {
964
1148
  cwd,
965
1149
  stdio: "pipe"
966
1150
  });
@@ -971,25 +1155,25 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
971
1155
  return;
972
1156
  let sha;
973
1157
  try {
974
- sha = execFileSync6("git", ["rev-parse", "HEAD"], {
1158
+ sha = execFileSync7("git", ["rev-parse", "HEAD"], {
975
1159
  cwd,
976
1160
  encoding: "utf-8"
977
1161
  }).trim();
978
1162
  } catch (err) {
979
1163
  throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
980
1164
  }
981
- log.info(TAG11, `Renaming remote ${oldRef} → ${newRef}`);
982
- execFileSync6("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
1165
+ log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
1166
+ execFileSync7("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
983
1167
  try {
984
- execFileSync6("git", ["push", "origin", `:refs/heads/${oldRef}`], {
1168
+ execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
985
1169
  cwd,
986
1170
  stdio: "pipe"
987
1171
  });
988
1172
  } catch (err) {
989
- log.warn(TAG11, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1173
+ log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
990
1174
  }
991
1175
  try {
992
- execFileSync6("git", ["branch", "-m", oldRef, newRef], {
1176
+ execFileSync7("git", ["branch", "-m", oldRef, newRef], {
993
1177
  cwd,
994
1178
  stdio: "pipe"
995
1179
  });
@@ -997,7 +1181,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
997
1181
  }
998
1182
  function getBranchWebUrl(branchName, cwd) {
999
1183
  try {
1000
- const remoteUrl = execFileSync6("git", ["remote", "get-url", "origin"], {
1184
+ const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
1001
1185
  cwd,
1002
1186
  encoding: "utf-8"
1003
1187
  }).trim();
@@ -1045,12 +1229,12 @@ function buildPrBody(card, commitLog) {
1045
1229
  }
1046
1230
  function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1047
1231
  if (existingPrUrl) {
1048
- log.info(TAG11, `Reusing existing PR from card description: ${existingPrUrl}`);
1232
+ log.info(TAG13, `Reusing existing PR from card description: ${existingPrUrl}`);
1049
1233
  return existingPrUrl;
1050
1234
  }
1051
1235
  let commitLog = "";
1052
1236
  try {
1053
- commitLog = execFileSync6("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
1237
+ commitLog = execFileSync7("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
1054
1238
  } catch {
1055
1239
  commitLog = "(unable to retrieve commit log)";
1056
1240
  }
@@ -1059,7 +1243,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1059
1243
  const base = config.worktree.baseBranch;
1060
1244
  const existingUrl = findExistingPr(branchName, worktreePath, provider);
1061
1245
  if (existingUrl) {
1062
- log.info(TAG11, `PR already exists for ${branchName}, updating body...`);
1246
+ log.info(TAG13, `PR already exists for ${branchName}, updating body...`);
1063
1247
  updateExistingPr(branchName, body, worktreePath, provider);
1064
1248
  return existingUrl;
1065
1249
  }
@@ -1067,10 +1251,10 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1067
1251
  let result;
1068
1252
  switch (provider) {
1069
1253
  case "github":
1070
- result = execFileSync6("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
1254
+ result = execFileSync7("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
1071
1255
  break;
1072
1256
  case "azure": {
1073
- const azOutput = execFileSync6("az", [
1257
+ const azOutput = execFileSync7("az", [
1074
1258
  "repos",
1075
1259
  "pr",
1076
1260
  "create",
@@ -1094,7 +1278,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1094
1278
  break;
1095
1279
  }
1096
1280
  case "gitlab":
1097
- result = execFileSync6("glab", [
1281
+ result = execFileSync7("glab", [
1098
1282
  "mr",
1099
1283
  "create",
1100
1284
  "--title",
@@ -1109,13 +1293,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1109
1293
  ], { cwd: worktreePath, encoding: "utf-8" }).trim();
1110
1294
  break;
1111
1295
  default:
1112
- log.warn(TAG11, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1296
+ log.warn(TAG13, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1113
1297
  return null;
1114
1298
  }
1115
- log.info(TAG11, `PR created: ${result}`);
1299
+ log.info(TAG13, `PR created: ${result}`);
1116
1300
  return result;
1117
1301
  } catch (err) {
1118
- log.error(TAG11, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1302
+ log.error(TAG13, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1119
1303
  return null;
1120
1304
  }
1121
1305
  }
@@ -1123,9 +1307,9 @@ function findExistingPr(branchName, worktreePath, provider) {
1123
1307
  try {
1124
1308
  switch (provider) {
1125
1309
  case "github":
1126
- return execFileSync6("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1310
+ return execFileSync7("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1127
1311
  case "gitlab": {
1128
- const json = execFileSync6("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1312
+ const json = execFileSync7("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1129
1313
  const parsed = JSON.parse(json);
1130
1314
  return parsed.web_url || null;
1131
1315
  }
@@ -1140,27 +1324,28 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
1140
1324
  try {
1141
1325
  switch (provider) {
1142
1326
  case "github":
1143
- execFileSync6("gh", ["pr", "edit", branchName, "--body", body], {
1327
+ execFileSync7("gh", ["pr", "edit", branchName, "--body", body], {
1144
1328
  cwd: worktreePath,
1145
1329
  stdio: "pipe"
1146
1330
  });
1147
1331
  break;
1148
1332
  case "gitlab":
1149
- execFileSync6("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1333
+ execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1150
1334
  break;
1151
1335
  }
1152
- log.info(TAG11, `Updated existing PR body for ${branchName}`);
1336
+ log.info(TAG13, `Updated existing PR body for ${branchName}`);
1153
1337
  } catch (err) {
1154
- log.warn(TAG11, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1338
+ log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1155
1339
  }
1156
1340
  }
1157
- var cachedExecFileAsync, TAG11 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE;
1341
+ var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1158
1342
  var init_git_pr = __esm(() => {
1159
1343
  init_dist();
1160
1344
  init_log();
1161
1345
  VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
1162
1346
  PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
1163
1347
  REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
1348
+ CI_REVIEW_REQUESTED_SHA_RE = /^CI-Review-Requested-SHA:\s*([0-9a-f]{7,40})\s*$/im;
1164
1349
  });
1165
1350
 
1166
1351
  // src/artifact-judge.ts
@@ -1211,6 +1396,8 @@ var AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth to
1211
1396
  var OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
1212
1397
  var USAGE_LIMIT = /usage limit|daily limit|monthly limit|quota (?:exceeded|reached)|reached your .{0,20}limit|usage_limit_reached|limit will reset/i;
1213
1398
  var RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
1399
+ var SPEND_LIMIT = /spend(?:ing)? (?:limit|cap)|monthly spend/i;
1400
+ var SPEND_LIMIT_REMEDY = /admin-settings\/usage/i;
1214
1401
  function parseRetryAfterMs(message) {
1215
1402
  const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
1216
1403
  if (!match)
@@ -1226,12 +1413,16 @@ function classifyRunError(message) {
1226
1413
  const retryAfterMs = parseRetryAfterMs(message);
1227
1414
  if (AUTH.test(message))
1228
1415
  return { kind: "auth", retryAfterMs };
1416
+ if (SPEND_LIMIT.test(message))
1417
+ return { kind: "spend_limit", retryAfterMs };
1229
1418
  if (OUT_OF_CREDITS.test(message))
1230
1419
  return { kind: "out_of_credits", retryAfterMs };
1231
1420
  if (USAGE_LIMIT.test(message))
1232
1421
  return { kind: "usage_limit", retryAfterMs };
1233
1422
  if (RATE_LIMIT.test(message))
1234
1423
  return { kind: "rate_limit", retryAfterMs };
1424
+ if (SPEND_LIMIT_REMEDY.test(message))
1425
+ return { kind: "spend_limit", retryAfterMs };
1235
1426
  return { kind: null };
1236
1427
  }
1237
1428
  function describeApiError(kind) {
@@ -1244,6 +1435,8 @@ function describeApiError(kind) {
1244
1435
  return "Anthropic usage limit reached — retrying after reset";
1245
1436
  case "rate_limit":
1246
1437
  return "Anthropic rate limit hit — retrying shortly";
1438
+ case "spend_limit":
1439
+ return "Account spend limit reached — raise it at claude.ai/admin-settings/usage, then run `harmony-agent resume`";
1247
1440
  }
1248
1441
  }
1249
1442
  function cooldownMsFor(kind) {
@@ -1256,6 +1449,8 @@ function cooldownMsFor(kind) {
1256
1449
  return 30 * 60000;
1257
1450
  case "auth":
1258
1451
  return 30 * 60000;
1452
+ case "spend_limit":
1453
+ return 30 * 60000;
1259
1454
  }
1260
1455
  }
1261
1456
 
@@ -1609,7 +1804,7 @@ ${this.capturedStderr}`);
1609
1804
  kind: "error",
1610
1805
  source: "system",
1611
1806
  payload: {
1612
- message: `result ${r.subtype}: ${joined || "(no detail)"}`,
1807
+ message: resultErrorMessage(r.subtype, joined),
1613
1808
  errorKind: cls.kind,
1614
1809
  retryable: cls.kind !== "auth" && cls.kind !== null
1615
1810
  }
@@ -1620,6 +1815,13 @@ ${this.capturedStderr}`);
1620
1815
  }
1621
1816
  }
1622
1817
  }
1818
+ function resultErrorMessage(subtype, detail) {
1819
+ return `result ${subtype}: ${detail || "(no detail)"}`;
1820
+ }
1821
+ function resultErrorSubtype(message) {
1822
+ const match = message.match(/^result ([A-Za-z0-9_]+):/);
1823
+ return match ? match[1] : null;
1824
+ }
1623
1825
  function normalize(raw) {
1624
1826
  if (raw == null)
1625
1827
  return;
@@ -1839,6 +2041,168 @@ class ArtifactCollector {
1839
2041
  };
1840
2042
  }
1841
2043
  }
2044
+ // src/ci-failure.ts
2045
+ init_log();
2046
+ import { execFile, execFileSync } from "node:child_process";
2047
+ import { promisify } from "node:util";
2048
+ function createExecFileAsync() {
2049
+ return promisify(execFile);
2050
+ }
2051
+ var cachedExecFileAsync;
2052
+ function execFileAsync() {
2053
+ return cachedExecFileAsync ??= createExecFileAsync();
2054
+ }
2055
+ var TAG3 = "ci-failure";
2056
+ var LOG_TAIL_BYTES = 16000;
2057
+ var MAX_CHECK_NAME = 200;
2058
+ function sanitizeCiText(text, maxLength) {
2059
+ return text.replace(/`/g, "'").replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, " ").slice(0, maxLength);
2060
+ }
2061
+ function githubSlug(remoteOrPrUrl) {
2062
+ const m = remoteOrPrUrl.match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:\/|$)/i);
2063
+ return m ? `${m[1].toLowerCase()}/${m[2].toLowerCase()}` : null;
2064
+ }
2065
+ var STRICT_PR_URL_RE = /^https:\/\/github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/pull\/\d+$/;
2066
+ function isPrOnOrigin(prUrl, cwd) {
2067
+ if (!STRICT_PR_URL_RE.test(prUrl))
2068
+ return false;
2069
+ const prSlug = githubSlug(prUrl);
2070
+ if (!prSlug)
2071
+ return false;
2072
+ try {
2073
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
2074
+ cwd,
2075
+ encoding: "utf-8",
2076
+ stdio: ["ignore", "pipe", "pipe"]
2077
+ }).trim();
2078
+ return githubSlug(remote) === prSlug;
2079
+ } catch {
2080
+ return false;
2081
+ }
2082
+ }
2083
+ var RUN_ID_RE = /\/actions\/runs\/(\d+)/;
2084
+ function extractWorkflowRunId(detailsUrl) {
2085
+ if (!detailsUrl)
2086
+ return null;
2087
+ return detailsUrl.match(RUN_ID_RE)?.[1] ?? null;
2088
+ }
2089
+ function parseFailedChecks(rollup) {
2090
+ if (!Array.isArray(rollup))
2091
+ return [];
2092
+ const failed = [];
2093
+ for (const check of rollup) {
2094
+ if (typeof check !== "object" || check === null)
2095
+ continue;
2096
+ const c = check;
2097
+ const name = sanitizeCiText(typeof c.name === "string" && c.name || typeof c.context === "string" && c.context || "unnamed check", MAX_CHECK_NAME);
2098
+ const detailsUrl = typeof c.detailsUrl === "string" && c.detailsUrl || typeof c.targetUrl === "string" && c.targetUrl || null;
2099
+ if (typeof c.status === "string") {
2100
+ if (c.status.toUpperCase() !== "COMPLETED")
2101
+ continue;
2102
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
2103
+ if (["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion))
2104
+ continue;
2105
+ failed.push({
2106
+ name,
2107
+ conclusion: conclusion || "FAILURE",
2108
+ detailsUrl,
2109
+ workflowRunId: extractWorkflowRunId(detailsUrl)
2110
+ });
2111
+ continue;
2112
+ }
2113
+ if (typeof c.state === "string") {
2114
+ const state = c.state.toUpperCase();
2115
+ if (state === "SUCCESS" || state === "PENDING")
2116
+ continue;
2117
+ failed.push({
2118
+ name,
2119
+ conclusion: state,
2120
+ detailsUrl,
2121
+ workflowRunId: extractWorkflowRunId(detailsUrl)
2122
+ });
2123
+ }
2124
+ }
2125
+ return failed;
2126
+ }
2127
+ var INFRA_SIGNATURES = [
2128
+ { signal: "esm.sh 5xx", pattern: /esm\.sh[^\n]{0,200}?\b5\d\d\b/i },
2129
+ {
2130
+ signal: "setup-bun unavailable",
2131
+ pattern: /setup-bun[^\n]{0,200}?\b(?:503|502|504)\b/i
2132
+ },
2133
+ {
2134
+ signal: "runner setup failed",
2135
+ pattern: /(?:##\[error\])?Set up job[^\n]{0,120}?(?:fail|error)/i
2136
+ },
2137
+ {
2138
+ signal: "runner network",
2139
+ pattern: /(?:getaddrinfo\s+(?:EAI_AGAIN|ENOTFOUND)|connect\s+(?:ETIMEDOUT|ECONNREFUSED)\s+[\d.:]|(?:read|write)\s+ECONNRESET\b[^\n]{0,80}(?:https?:|\.com|\.org|\.io|\.sh|\.dev)|TLS handshake timeout)/i
2140
+ },
2141
+ {
2142
+ signal: "actions infrastructure",
2143
+ pattern: /(?:The (?:self-hosted )?runner[^\n]{0,80}lost communication|The operation was canceled[^\n]{0,80}runner|We are experiencing (?:a )?(?:degraded|high) )/i
2144
+ },
2145
+ {
2146
+ signal: "registry 5xx",
2147
+ pattern: /(?:npm|bun|yarn|pnpm)[^\n]{0,200}?\b(?:502 Bad Gateway|503 Service Unavailable|504 Gateway Time-?out)\b/i
2148
+ }
2149
+ ];
2150
+ function classifyCiFailure(input) {
2151
+ const haystack = `${input.checkName}
2152
+ ${input.log}`;
2153
+ for (const { signal, pattern } of INFRA_SIGNATURES) {
2154
+ if (pattern.test(haystack))
2155
+ return { kind: "infra", signal };
2156
+ }
2157
+ return { kind: "code", signal: null };
2158
+ }
2159
+ function describeCiFailure(failure) {
2160
+ return failure.kind === "infra" ? `infrastructure failure (${failure.signal}) — re-dispatching, no code change` : "a real build or test failure — repairing the branch";
2161
+ }
2162
+ function classifyCiFailures(failures) {
2163
+ if (failures.length === 0)
2164
+ return { kind: "code", signal: null };
2165
+ const infra = failures.filter((f) => f.kind === "infra");
2166
+ if (infra.length !== failures.length)
2167
+ return { kind: "code", signal: null };
2168
+ const signals = [...new Set(infra.map((f) => f.signal).filter(Boolean))];
2169
+ return { kind: "infra", signal: signals.join(", ") || null };
2170
+ }
2171
+ async function getPrFailedChecks(prUrl, cwd, provider) {
2172
+ if (provider !== "github")
2173
+ return [];
2174
+ try {
2175
+ const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup"], { cwd, encoding: "utf-8", timeout: 15000 });
2176
+ const parsed = JSON.parse(stdout.trim());
2177
+ return parseFailedChecks(parsed.statusCheckRollup);
2178
+ } catch (err) {
2179
+ log.warn(TAG3, `could not read failing checks for ${prUrl}: ${err instanceof Error ? err.message : err}`);
2180
+ return [];
2181
+ }
2182
+ }
2183
+ async function getFailedRunLog(runId, cwd) {
2184
+ try {
2185
+ const { stdout } = await execFileAsync()("gh", ["run", "view", runId, "--log-failed"], { cwd, encoding: "utf-8", timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
2186
+ return sanitizeCiText(stdout.slice(-LOG_TAIL_BYTES), LOG_TAIL_BYTES);
2187
+ } catch (err) {
2188
+ log.debug(TAG3, `could not read failed-step log for run ${runId}: ${err instanceof Error ? err.message : err}`);
2189
+ return "";
2190
+ }
2191
+ }
2192
+ async function rerunFailedJobs(runId, cwd) {
2193
+ try {
2194
+ await execFileAsync()("gh", ["run", "rerun", runId, "--failed"], {
2195
+ cwd,
2196
+ encoding: "utf-8",
2197
+ timeout: 30000
2198
+ });
2199
+ log.info(TAG3, `re-dispatched failed jobs of run ${runId}`);
2200
+ return true;
2201
+ } catch (err) {
2202
+ log.warn(TAG3, `could not re-dispatch run ${runId}: ${err instanceof Error ? err.message : err}`);
2203
+ return false;
2204
+ }
2205
+ }
1842
2206
  // src/command-metric.ts
1843
2207
  init_dist();
1844
2208
 
@@ -1850,7 +2214,7 @@ init_dist();
1850
2214
 
1851
2215
  // src/command-metric.ts
1852
2216
  init_log();
1853
- var TAG3 = "command-metric";
2217
+ var TAG4 = "command-metric";
1854
2218
  var MAX_RAW_CHARS = 2000;
1855
2219
  var MAX_OUTPUT_BUFFER = 10 * 1024 * 1024;
1856
2220
  var MAX_STDERR_CHARS = 64 * 1024;
@@ -2030,7 +2394,7 @@ class CommandMetricCollector {
2030
2394
  const requested = typeof def.timeoutMs === "number" && def.timeoutMs > 0 ? Math.floor(def.timeoutMs) : DEFAULT_METRIC_TIMEOUT_MS;
2031
2395
  const timeoutMs = Math.min(requested, MAX_METRIC_TIMEOUT_MS);
2032
2396
  if (timeoutMs < requested) {
2033
- log.warn(TAG3, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
2397
+ log.warn(TAG4, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
2034
2398
  }
2035
2399
  const run = this.deps.runCommand ?? runMetricCommand;
2036
2400
  let stdout;
@@ -2043,15 +2407,15 @@ class CommandMetricCollector {
2043
2407
  });
2044
2408
  } catch (err) {
2045
2409
  const reason = describeRunFailure(err, timeoutMs);
2046
- log.warn(TAG3, `Metric "${name}" did not produce a measurement: ${reason}`);
2410
+ log.warn(TAG4, `Metric "${name}" did not produce a measurement: ${reason}`);
2047
2411
  return blocked(name, reason);
2048
2412
  }
2049
2413
  const parsed = parseMetricValue(mode, stdout);
2050
2414
  if (!parsed.ok) {
2051
- log.warn(TAG3, `Metric "${name}" output unusable: ${parsed.reason}`);
2415
+ log.warn(TAG4, `Metric "${name}" output unusable: ${parsed.reason}`);
2052
2416
  return blocked(name, `Metric "${name}": ${parsed.reason}.`, stdout);
2053
2417
  }
2054
- log.info(TAG3, `Metric "${name}" measured: ${JSON.stringify(parsed.value)}`);
2418
+ log.info(TAG4, `Metric "${name}" measured: ${JSON.stringify(parsed.value)}`);
2055
2419
  return {
2056
2420
  result: "passed",
2057
2421
  structured: {
@@ -2099,147 +2463,587 @@ function truncate(value, max) {
2099
2463
  return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
2100
2464
  }
2101
2465
  // src/confine-to-repo.ts
2102
- import { isAbsolute, resolve, sep } from "node:path";
2103
- var PATH_ARG_BY_TOOL = {
2466
+ import { realpathSync } from "node:fs";
2467
+ import { dirname, isAbsolute, parse, resolve, sep } from "node:path";
2468
+ var READ_PATH_ARGS = {
2104
2469
  Read: ["file_path", "path", "notebook_path"],
2105
2470
  Grep: ["path"],
2106
2471
  Glob: ["path"]
2107
2472
  };
2473
+ var WRITE_PATH_ARGS = {
2474
+ Write: ["file_path"],
2475
+ Edit: ["file_path"],
2476
+ MultiEdit: ["file_path"],
2477
+ NotebookEdit: ["notebook_path"]
2478
+ };
2479
+ var CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
2480
+ var CONFINED_WRITE_TOOLS = Object.freeze([
2481
+ ...Object.keys(READ_PATH_ARGS),
2482
+ ...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
2483
+ ]);
2484
+ var PATTERN_ARG_BY_TOOL = {
2485
+ Glob: ["pattern"],
2486
+ Grep: ["glob"]
2487
+ };
2488
+ function isGitMetadata(repoRoot, candidate) {
2489
+ const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
2490
+ return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
2491
+ }
2492
+ function patternEscapes(pattern) {
2493
+ if (isAbsolute(pattern))
2494
+ return true;
2495
+ if (/[{}[\]~()!+@]/.test(pattern))
2496
+ return true;
2497
+ return pattern.split(/[\\/]/).some((segment) => segment === "..");
2498
+ }
2499
+ function pathArgsFor(mode) {
2500
+ return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
2501
+ }
2502
+ function realPathOrNearest(p) {
2503
+ const abs = isAbsolute(p) ? p : resolve(p);
2504
+ const { root } = parse(abs);
2505
+ let real = root;
2506
+ for (const part of abs.slice(root.length).split(sep)) {
2507
+ if (part === "" || part === ".")
2508
+ continue;
2509
+ if (part === "..") {
2510
+ real = dirname(real);
2511
+ continue;
2512
+ }
2513
+ const next = real.endsWith(sep) ? real + part : real + sep + part;
2514
+ try {
2515
+ real = realpathSync(next);
2516
+ } catch {
2517
+ real = next;
2518
+ }
2519
+ }
2520
+ return real;
2521
+ }
2108
2522
  function isInsideTree(root, target) {
2109
- const normalizedRoot = resolve(root);
2110
- const normalizedTarget = resolve(target);
2523
+ const normalizedRoot = realPathOrNearest(root);
2524
+ const normalizedTarget = realPathOrNearest(target);
2111
2525
  if (normalizedTarget === normalizedRoot)
2112
2526
  return true;
2113
2527
  return normalizedTarget.startsWith(normalizedRoot + sep);
2114
2528
  }
2115
- function decideConfinedTool(repoRoot, toolName, input) {
2116
- const pathArgs = PATH_ARG_BY_TOOL[toolName];
2529
+ function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
2530
+ const pathArgs = pathArgsFor(mode)[toolName];
2117
2531
  if (!pathArgs) {
2118
2532
  return {
2119
2533
  behavior: "deny",
2120
2534
  message: `${toolName} is not available to this run.`
2121
2535
  };
2122
2536
  }
2537
+ const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
2538
+ for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
2539
+ const value = input[key];
2540
+ if (typeof value !== "string" || value.length === 0)
2541
+ continue;
2542
+ if (patternEscapes(value)) {
2543
+ return {
2544
+ behavior: "deny",
2545
+ message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
2546
+ };
2547
+ }
2548
+ }
2123
2549
  for (const key of pathArgs) {
2124
2550
  const value = input[key];
2125
2551
  if (typeof value !== "string" || value.length === 0)
2126
2552
  continue;
2127
- const candidate = isAbsolute(value) ? value : resolve(repoRoot, value);
2553
+ const candidate = isAbsolute(value) ? value : `${repoRoot}${sep}${value}`;
2554
+ if (isGitMetadata(repoRoot, candidate)) {
2555
+ return {
2556
+ behavior: "deny",
2557
+ message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
2558
+ };
2559
+ }
2128
2560
  if (!isInsideTree(repoRoot, candidate)) {
2129
2561
  return {
2130
2562
  behavior: "deny",
2131
- message: `${toolName} may only read inside the repository. Refused: ${value}`
2563
+ message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
2132
2564
  };
2133
2565
  }
2134
2566
  }
2135
2567
  return { behavior: "allow" };
2136
2568
  }
2137
- function confineToRepo(repoRoot) {
2138
- return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input);
2569
+ function confineToRepo(repoRoot, mode = "read") {
2570
+ return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
2139
2571
  }
2140
2572
  // src/gate-collectors.ts
2141
2573
  init_dist();
2142
2574
  init_log();
2143
2575
 
2144
2576
  // src/oracle-collector.ts
2145
- init_log();
2146
2577
  import { createHash } from "node:crypto";
2147
- var TAG4 = "oracle-collector";
2578
+ init_log();
2148
2579
 
2149
- class OracleCollector {
2150
- deps;
2151
- kind = "oracle_passed";
2152
- constructor(deps) {
2153
- this.deps = deps;
2580
+ // src/oracle.ts
2581
+ import { lstatSync, readFileSync, statSync } from "node:fs";
2582
+ import {
2583
+ chmod,
2584
+ lstat,
2585
+ mkdir,
2586
+ mkdtemp,
2587
+ realpath,
2588
+ rm,
2589
+ writeFile
2590
+ } from "node:fs/promises";
2591
+ import { tmpdir } from "node:os";
2592
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve2, sep as sep2 } from "node:path";
2593
+ import { StringDecoder } from "node:string_decoder";
2594
+ init_log();
2595
+ var TAG5 = "oracle";
2596
+ async function resolveContained(repoPath, relativePath) {
2597
+ if (isAbsolute2(relativePath)) {
2598
+ throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
2154
2599
  }
2155
- async collect(context) {
2156
- const oracle = await this.deps.fetchOracle(context.cardId, context.stageId, this.deps.sessionId);
2157
- if (!oracle) {
2158
- log.info(TAG4, `No oracle held for stage ${context.stageId} — blocked`);
2159
- return {
2160
- result: "blocked",
2161
- structured: {
2162
- reason: `No oracle is held for stage ${context.stageId}.`
2163
- }
2164
- };
2165
- }
2166
- return await this.runHeld(oracle);
2600
+ if (relativePath === "" || relativePath === ".") {
2601
+ throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
2167
2602
  }
2168
- async runHeld(oracle) {
2169
- const identity = {
2170
- oracleId: oracle.id ?? null,
2171
- contentHash: createHash("sha256").update(oracle.content).digest("hex")
2172
- };
2173
- await this.deps.place(this.deps.repoPath, oracle);
2174
- try {
2175
- const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
2176
- const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
2177
- ${output}`;
2178
- if (exitCode === 0) {
2179
- log.info(TAG4, logLine);
2180
- } else {
2181
- log.warn(TAG4, logLine);
2182
- }
2183
- return {
2184
- result: exitCode === 0 ? "passed" : "failed",
2185
- structured: {
2186
- oracle: {
2187
- exitCode,
2188
- path: oracle.path,
2189
- ...identity,
2190
- output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
2191
- }
2192
- }
2193
- };
2194
- } catch (err) {
2195
- const message = errText(err);
2196
- log.warn(TAG4, `Oracle run threw: ${message} — blocked`);
2197
- return {
2198
- result: "blocked",
2199
- structured: {
2200
- oracle: { path: oracle.path, ...identity },
2201
- error: message
2202
- }
2203
- };
2204
- } finally {
2205
- await this.removeBestEffort(oracle);
2206
- }
2603
+ const root = await realpath(repoPath);
2604
+ const target = resolve2(root, relativePath);
2605
+ if (target !== root && !target.startsWith(root + sep2)) {
2606
+ throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
2207
2607
  }
2208
- async removeBestEffort(oracle) {
2209
- try {
2210
- await this.deps.remove(this.deps.repoPath, oracle);
2211
- return;
2212
- } catch (err) {
2213
- log.warn(TAG4, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
2214
- }
2215
- try {
2216
- await this.deps.remove(this.deps.repoPath, oracle);
2217
- log.info(TAG4, `Held test at ${oracle.path} removed on the second attempt`);
2218
- } catch (err) {
2219
- 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.");
2608
+ let cursor = root;
2609
+ for (const segment of relativePath.split("/")) {
2610
+ cursor = resolve2(cursor, segment);
2611
+ const stat = await lstat(cursor).catch(() => null);
2612
+ if (stat?.isSymbolicLink()) {
2613
+ throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
2220
2614
  }
2221
2615
  }
2616
+ return target;
2222
2617
  }
2223
- function errText(err) {
2224
- return err instanceof Error ? err.message : String(err);
2618
+ async function place(repoPath, oracle) {
2619
+ const target = await resolveContained(repoPath, oracle.path);
2620
+ await mkdir(dirname2(target), { recursive: true });
2621
+ await writeFile(target, oracle.content, "utf8");
2225
2622
  }
2226
-
2623
+ async function remove(repoPath, oracle) {
2624
+ const target = await resolveContained(repoPath, oracle.path);
2625
+ await rm(target, { force: true });
2626
+ }
2627
+ var ORACLE_RUNNERS = {
2628
+ vitest: {
2629
+ argv: (path) => ({
2630
+ command: "npx",
2631
+ args: ["--no-install", "vitest", "run", path]
2632
+ }),
2633
+ verdictStream: "stdout",
2634
+ report: {
2635
+ file: "report.json",
2636
+ flags: (reportPath) => [
2637
+ "--reporter=default",
2638
+ "--reporter=json",
2639
+ `--outputFile=${reportPath}`
2640
+ ],
2641
+ parse: (content) => {
2642
+ const parsed = JSON.parse(content);
2643
+ if (typeof parsed !== "object" || parsed === null)
2644
+ return null;
2645
+ const { numTotalTests, numFailedTests } = parsed;
2646
+ if (typeof numTotalTests !== "number" || typeof numFailedTests !== "number" || !Number.isFinite(numTotalTests) || !Number.isFinite(numFailedTests)) {
2647
+ return null;
2648
+ }
2649
+ return { total: numTotalTests, failed: numFailedTests };
2650
+ }
2651
+ }
2652
+ },
2653
+ bun: {
2654
+ argv: (path) => ({ command: "bun", args: ["test", path] }),
2655
+ verdictStream: "stderr",
2656
+ report: {
2657
+ file: "report.xml",
2658
+ flags: (reportPath) => [
2659
+ "--reporter=junit",
2660
+ `--reporter-outfile=${reportPath}`
2661
+ ],
2662
+ parse: (content) => {
2663
+ const root = /<testsuites\b[^>]*>/.exec(content);
2664
+ if (!root)
2665
+ return null;
2666
+ const total = /\btests="(\d+)"/.exec(root[0]);
2667
+ const failed = /\bfailures="(\d+)"/.exec(root[0]);
2668
+ if (!total || !failed)
2669
+ return null;
2670
+ return { total: Number(total[1]), failed: Number(failed[1]) };
2671
+ }
2672
+ }
2673
+ }
2674
+ };
2675
+ var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
2676
+ var ORACLE_OUTPUT_LIMIT = 64 * 1024;
2677
+ var ORACLE_SIGINT_GRACE_MS = 2000;
2678
+ var ORACLE_SIGTERM_GRACE_MS = 3000;
2679
+ var ORACLE_DRAIN_GRACE_MS = 500;
2680
+ function resolveOracleRunner(oracle) {
2681
+ return resolveOracleRunnerSpec(oracle).argv(argvPath(oracle.path));
2682
+ }
2683
+ function resolveOracleRunnerSpec(oracle) {
2684
+ const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
2685
+ const spec = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
2686
+ if (!spec) {
2687
+ 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(", ")})`);
2688
+ }
2689
+ return spec;
2690
+ }
2691
+ function summarizeOracleReport(oracle, content) {
2692
+ const spec = resolveOracleRunnerSpec(oracle);
2693
+ try {
2694
+ return spec.report.parse(content) ?? null;
2695
+ } catch {
2696
+ return null;
2697
+ }
2698
+ }
2699
+ function gradeOracleRed(summary, exitCode) {
2700
+ if (!summary) {
2701
+ return {
2702
+ outcome: "no_verdict",
2703
+ 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"
2704
+ };
2705
+ }
2706
+ if (summary.total === 0) {
2707
+ return {
2708
+ outcome: "no_verdict",
2709
+ reason: `the runner started but executed no tests (exit ${exitCode}), so the held test produced no verdict`
2710
+ };
2711
+ }
2712
+ if (summary.failed > 0) {
2713
+ return { outcome: "reproduced", summary };
2714
+ }
2715
+ if (exitCode === 0) {
2716
+ return { outcome: "not_reproduced", summary };
2717
+ }
2718
+ return {
2719
+ outcome: "no_verdict",
2720
+ reason: `all ${summary.total} test(s) passed but the runner exited ${exitCode}, so the failure is not the held test's`
2721
+ };
2722
+ }
2723
+ function argvPath(path) {
2724
+ return path.startsWith("./") ? path : `./${path}`;
2725
+ }
2726
+ async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
2727
+ const spec = resolveOracleRunnerSpec(oracle);
2728
+ const reportDir = await mkdtemp(join(tmpdir(), reportDirPrefix()));
2729
+ const reportPath = join(reportDir, spec.report.file);
2730
+ try {
2731
+ return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
2732
+ } finally {
2733
+ await removeReportDir(reportDir);
2734
+ }
2735
+ }
2736
+ function reportDirPrefix() {
2737
+ return "harmony-oracle-report-";
2738
+ }
2739
+ function assertUntampered(reportPath) {
2740
+ const dir = statSync(dirname2(reportPath));
2741
+ if ((dir.mode & 511) !== 448) {
2742
+ throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
2743
+ }
2744
+ const file = lstatSync(reportPath);
2745
+ if (!file.isFile()) {
2746
+ throw new Error("the oracle report is not a regular file — refusing it");
2747
+ }
2748
+ if ((file.mode & 128) === 0) {
2749
+ 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`);
2750
+ }
2751
+ }
2752
+ async function removeReportDir(reportDir) {
2753
+ try {
2754
+ await chmod(reportDir, 448).catch(() => {});
2755
+ await rm(reportDir, { recursive: true, force: true });
2756
+ } catch (err) {
2757
+ log.warn(TAG5, `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.`);
2758
+ }
2759
+ }
2760
+ async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
2761
+ const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
2762
+ const args = [...baseArgs, ...spec.report.flags(reportPath)];
2763
+ return await new Promise((settleOk, settleErr) => {
2764
+ let child;
2765
+ try {
2766
+ child = spawnInGroup(command, args, {
2767
+ cwd: repoPath,
2768
+ stdio: ["ignore", "pipe", "pipe"]
2769
+ });
2770
+ } catch (err) {
2771
+ settleErr(err);
2772
+ return;
2773
+ }
2774
+ const pgid = child.pid;
2775
+ let output = "";
2776
+ let truncated = false;
2777
+ const outDecoder = new StringDecoder("utf8");
2778
+ const errDecoder = new StringDecoder("utf8");
2779
+ let verdict = "";
2780
+ let flushed = false;
2781
+ const flush = () => {
2782
+ if (flushed)
2783
+ return;
2784
+ flushed = true;
2785
+ const outTail = outDecoder.end();
2786
+ const errTail = errDecoder.end();
2787
+ output += outTail + errTail;
2788
+ verdict += spec.verdictStream === "stdout" ? outTail : errTail;
2789
+ };
2790
+ const finalOutput = () => {
2791
+ flush();
2792
+ return truncated ? `… earlier output dropped; kept the last ${ORACLE_OUTPUT_LIMIT} characters
2793
+ ${output}` : output;
2794
+ };
2795
+ const finalVerdict = () => {
2796
+ flush();
2797
+ return verdict;
2798
+ };
2799
+ let report = null;
2800
+ const captureReport = () => {
2801
+ try {
2802
+ assertUntampered(reportPath);
2803
+ report = spec.report.parse(readFileSync(reportPath, "utf8"));
2804
+ } catch (err) {
2805
+ report = null;
2806
+ const message = err instanceof Error ? err.message : String(err);
2807
+ const absent = err instanceof Error && err.code === "ENOENT";
2808
+ if (absent) {
2809
+ log.info(TAG5, `No oracle report at ${reportPath} — no verdict.`);
2810
+ } else {
2811
+ log.warn(TAG5, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
2812
+ }
2813
+ }
2814
+ };
2815
+ let settled = false;
2816
+ let killing = false;
2817
+ let timer;
2818
+ let drainTimer;
2819
+ const settle = (failure, result) => {
2820
+ if (settled)
2821
+ return;
2822
+ settled = true;
2823
+ if (timer)
2824
+ clearTimeout(timer);
2825
+ if (drainTimer)
2826
+ clearTimeout(drainTimer);
2827
+ reapGroup(pgid);
2828
+ if (failure)
2829
+ settleErr(failure);
2830
+ else
2831
+ settleOk(result);
2832
+ };
2833
+ const append = (stream, chunk) => {
2834
+ const text = (stream === "stdout" ? outDecoder : errDecoder).write(chunk);
2835
+ output += text;
2836
+ if (output.length > ORACLE_OUTPUT_LIMIT) {
2837
+ output = output.slice(-ORACLE_OUTPUT_LIMIT);
2838
+ truncated = true;
2839
+ }
2840
+ if (stream === spec.verdictStream) {
2841
+ verdict += text;
2842
+ if (verdict.length > ORACLE_OUTPUT_LIMIT) {
2843
+ verdict = verdict.slice(-ORACLE_OUTPUT_LIMIT);
2844
+ }
2845
+ }
2846
+ };
2847
+ child.stdout?.on("data", (chunk) => append("stdout", chunk));
2848
+ child.stderr?.on("data", (chunk) => append("stderr", chunk));
2849
+ child.once("error", (err) => settle(err));
2850
+ const settleFromExit = (code, signal) => {
2851
+ if (drainTimer)
2852
+ clearTimeout(drainTimer);
2853
+ if (code === null) {
2854
+ settle(new Error(`the held test was terminated by signal ${signal}`));
2855
+ return;
2856
+ }
2857
+ settle(null, {
2858
+ exitCode: code,
2859
+ output: finalOutput(),
2860
+ verdict: finalVerdict(),
2861
+ report
2862
+ });
2863
+ };
2864
+ child.once("exit", (code, signal) => {
2865
+ if (killing)
2866
+ return;
2867
+ captureReport();
2868
+ if (timer)
2869
+ clearTimeout(timer);
2870
+ reapGroup(pgid);
2871
+ drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
2872
+ child.once("close", () => settleFromExit(code, signal));
2873
+ });
2874
+ timer = setTimeout(() => {
2875
+ if (settled)
2876
+ return;
2877
+ killing = true;
2878
+ terminateGroup(child, {
2879
+ sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
2880
+ sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
2881
+ }).catch(() => {}).then(() => {
2882
+ settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
2883
+ });
2884
+ }, timeoutMs);
2885
+ });
2886
+ }
2887
+
2888
+ // src/oracle-collector.ts
2889
+ var TAG6 = "oracle-collector";
2890
+
2891
+ class HeldOracleCollector {
2892
+ deps;
2893
+ constructor(deps) {
2894
+ this.deps = deps;
2895
+ }
2896
+ async collect(context) {
2897
+ const stageId = this.oracleStageId(context);
2898
+ if (!stageId) {
2899
+ 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.";
2900
+ log.warn(TAG6, `${reason} — blocked (config)`);
2901
+ return {
2902
+ result: "blocked",
2903
+ structured: { reason, ...GATE_CONFIG_ERROR_MARK }
2904
+ };
2905
+ }
2906
+ const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
2907
+ if (!oracle) {
2908
+ log.info(TAG6, `No oracle held for stage ${stageId} — blocked`);
2909
+ return {
2910
+ result: "blocked",
2911
+ structured: {
2912
+ reason: `No oracle is held for stage ${stageId}.`
2913
+ }
2914
+ };
2915
+ }
2916
+ return await this.runHeld(oracle);
2917
+ }
2918
+ async runHeld(oracle) {
2919
+ const identity = {
2920
+ oracleId: oracle.id ?? null,
2921
+ contentHash: createHash("sha256").update(oracle.content).digest("hex")
2922
+ };
2923
+ await this.deps.place(this.deps.repoPath, oracle);
2924
+ try {
2925
+ const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
2926
+ const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
2927
+ ${output}`;
2928
+ if (exitCode === 0) {
2929
+ log.info(TAG6, logLine);
2930
+ } else {
2931
+ log.warn(TAG6, logLine);
2932
+ }
2933
+ return this.verdict({
2934
+ oracle,
2935
+ identity,
2936
+ exitCode,
2937
+ output,
2938
+ report: report ?? null
2939
+ });
2940
+ } catch (err) {
2941
+ const message = errText(err);
2942
+ log.warn(TAG6, `Oracle run threw: ${message} — blocked`);
2943
+ return {
2944
+ result: "blocked",
2945
+ structured: {
2946
+ oracle: { path: oracle.path, ...identity },
2947
+ error: message
2948
+ }
2949
+ };
2950
+ } finally {
2951
+ await this.removeBestEffort(oracle);
2952
+ }
2953
+ }
2954
+ async removeBestEffort(oracle) {
2955
+ try {
2956
+ await this.deps.remove(this.deps.repoPath, oracle);
2957
+ return;
2958
+ } catch (err) {
2959
+ log.warn(TAG6, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
2960
+ }
2961
+ try {
2962
+ await this.deps.remove(this.deps.repoPath, oracle);
2963
+ log.info(TAG6, `Held test at ${oracle.path} removed on the second attempt`);
2964
+ } catch (err) {
2965
+ log.error(TAG6, `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.");
2966
+ }
2967
+ }
2968
+ }
2969
+
2970
+ class OracleCollector extends HeldOracleCollector {
2971
+ kind = "oracle_passed";
2972
+ oracleStageId(context) {
2973
+ return context.stageId;
2974
+ }
2975
+ verdict({
2976
+ oracle,
2977
+ identity,
2978
+ exitCode
2979
+ }) {
2980
+ return {
2981
+ result: exitCode === 0 ? "passed" : "failed",
2982
+ structured: {
2983
+ oracle: {
2984
+ exitCode,
2985
+ path: oracle.path,
2986
+ ...identity,
2987
+ output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
2988
+ }
2989
+ }
2990
+ };
2991
+ }
2992
+ }
2993
+
2994
+ class OracleRedCollector extends HeldOracleCollector {
2995
+ kind = "oracle_red";
2996
+ oracleStageId() {
2997
+ return this.deps.targetStageId ?? null;
2998
+ }
2999
+ verdict({
3000
+ oracle,
3001
+ identity,
3002
+ exitCode,
3003
+ report
3004
+ }) {
3005
+ const graded = gradeOracleRed(report, exitCode);
3006
+ const base = { exitCode, path: oracle.path, ...identity };
3007
+ if (graded.outcome === "no_verdict") {
3008
+ log.warn(TAG6, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
3009
+ return {
3010
+ result: "blocked",
3011
+ structured: { oracle: base, reason: graded.reason }
3012
+ };
3013
+ }
3014
+ return {
3015
+ result: graded.outcome === "reproduced" ? "passed" : "failed",
3016
+ structured: {
3017
+ oracle: {
3018
+ ...base,
3019
+ testsRun: graded.summary.total,
3020
+ testsFailed: graded.summary.failed,
3021
+ output: "withheld — oracle_red is a secrecy gate; see the motor's local log"
3022
+ }
3023
+ }
3024
+ };
3025
+ }
3026
+ }
3027
+ function errText(err) {
3028
+ return err instanceof Error ? err.message : String(err);
3029
+ }
3030
+
2227
3031
  // src/verification.ts
2228
3032
  init_log();
2229
- import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_process";
3033
+ import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
2230
3034
 
2231
3035
  // src/pm.ts
2232
3036
  init_log();
2233
- import { execFileSync } from "node:child_process";
3037
+ import { execFileSync as execFileSync2 } from "node:child_process";
2234
3038
  import { existsSync } from "node:fs";
2235
- var TAG5 = "pm";
3039
+ var TAG7 = "pm";
2236
3040
  var cached = null;
2237
3041
  function detectPackageManager() {
2238
3042
  if (cached)
2239
3043
  return cached;
2240
3044
  let repoRoot;
2241
3045
  try {
2242
- repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
3046
+ repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
2243
3047
  encoding: "utf-8"
2244
3048
  }).trim();
2245
3049
  } catch {
@@ -2254,20 +3058,21 @@ function detectPackageManager() {
2254
3058
  } else {
2255
3059
  cached = "npm";
2256
3060
  }
2257
- log.info(TAG5, `Detected package manager: ${cached}`);
3061
+ log.info(TAG7, `Detected package manager: ${cached}`);
2258
3062
  return cached;
2259
3063
  }
2260
- function installCommand() {
3064
+ function installCommand(ignoreScripts = false) {
2261
3065
  const pm = detectPackageManager();
3066
+ const skip = ignoreScripts ? " --ignore-scripts" : "";
2262
3067
  switch (pm) {
2263
3068
  case "bun":
2264
- return "bun install --frozen-lockfile";
3069
+ return `bun install --frozen-lockfile${skip}`;
2265
3070
  case "pnpm":
2266
- return "pnpm install --frozen-lockfile";
3071
+ return `pnpm install --frozen-lockfile${skip}`;
2267
3072
  case "yarn":
2268
- return "yarn install --frozen-lockfile";
3073
+ return `yarn install --frozen-lockfile${skip}`;
2269
3074
  case "npm":
2270
- return "npm ci";
3075
+ return `npm ci${skip}`;
2271
3076
  }
2272
3077
  }
2273
3078
  function spawnRunArgs(script, ...extra) {
@@ -2280,9 +3085,9 @@ function spawnRunArgs(script, ...extra) {
2280
3085
 
2281
3086
  // src/project-type.ts
2282
3087
  init_log();
2283
- import { execFileSync as execFileSync2 } from "node:child_process";
2284
- import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
2285
- var TAG6 = "project-type";
3088
+ import { execFileSync as execFileSync3 } from "node:child_process";
3089
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
3090
+ var TAG8 = "project-type";
2286
3091
  var _cache = new Map;
2287
3092
  function _resetCache() {
2288
3093
  _cache.clear();
@@ -2293,7 +3098,7 @@ function detect(dir) {
2293
3098
  return cached2;
2294
3099
  const result = detectUncached(dir);
2295
3100
  _cache.set(dir, result);
2296
- log.info(TAG6, `Detected project type in ${dir}: ${result.kind}`);
3101
+ log.info(TAG8, `Detected project type in ${dir}: ${result.kind}`);
2297
3102
  return result;
2298
3103
  }
2299
3104
  function detectUncached(dir) {
@@ -2384,16 +3189,16 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
2384
3189
  function hasNodeTestScript(dir) {
2385
3190
  let script;
2386
3191
  try {
2387
- const pkg = JSON.parse(readFileSync(`${dir}/package.json`, "utf-8"));
3192
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
2388
3193
  script = pkg.scripts?.test;
2389
3194
  } catch (err) {
2390
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
3195
+ log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
2391
3196
  return false;
2392
3197
  }
2393
3198
  if (typeof script !== "string" || script.trim().length === 0)
2394
3199
  return false;
2395
3200
  if (NPM_PLACEHOLDER_TEST.test(script)) {
2396
- log.info(TAG6, `package.json 'test' is the npm placeholder — skipping tests`);
3201
+ log.info(TAG8, `package.json 'test' is the npm placeholder — skipping tests`);
2397
3202
  return false;
2398
3203
  }
2399
3204
  return true;
@@ -2401,10 +3206,10 @@ function hasNodeTestScript(dir) {
2401
3206
  function firstNodeScript(dir, candidates) {
2402
3207
  let scripts;
2403
3208
  try {
2404
- const pkg = JSON.parse(readFileSync(`${dir}/package.json`, "utf-8"));
3209
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
2405
3210
  scripts = pkg.scripts ?? {};
2406
3211
  } catch (err) {
2407
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
3212
+ log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
2408
3213
  return null;
2409
3214
  }
2410
3215
  for (const name of candidates) {
@@ -2423,7 +3228,7 @@ function xcodeBuildCommand(pt) {
2423
3228
  return null;
2424
3229
  const scheme = resolveXcodeScheme(pt);
2425
3230
  if (!scheme) {
2426
- log.warn(TAG6, "Could not resolve an Xcode scheme — skipping build (best-effort)");
3231
+ log.warn(TAG8, "Could not resolve an Xcode scheme — skipping build (best-effort)");
2427
3232
  return null;
2428
3233
  }
2429
3234
  const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
@@ -2446,20 +3251,20 @@ function resolveXcodeScheme(pt) {
2446
3251
  return null;
2447
3252
  const flag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
2448
3253
  try {
2449
- const out = execFileSync2("xcodebuild", ["-list", "-json", flag, pt.xcodeContainer], { encoding: "utf-8", timeout: 30000, stdio: "pipe" });
3254
+ const out = execFileSync3("xcodebuild", ["-list", "-json", flag, pt.xcodeContainer], { encoding: "utf-8", timeout: 30000, stdio: "pipe" });
2450
3255
  const parsed = JSON.parse(out);
2451
3256
  const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
2452
3257
  return schemes[0] ?? null;
2453
3258
  } catch (err) {
2454
- log.warn(TAG6, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
3259
+ log.warn(TAG8, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
2455
3260
  return null;
2456
3261
  }
2457
3262
  }
2458
3263
 
2459
3264
  // src/revert-guard.ts
2460
3265
  init_log();
2461
- import { execFileSync as execFileSync3 } from "node:child_process";
2462
- var TAG7 = "revert-guard";
3266
+ import { execFileSync as execFileSync4 } from "node:child_process";
3267
+ var TAG9 = "revert-guard";
2463
3268
  var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
2464
3269
  function isTestFile(path) {
2465
3270
  return TEST_FILE.test(path);
@@ -2469,21 +3274,21 @@ function filterTestFiles(paths) {
2469
3274
  }
2470
3275
  function refetchBase(worktreePath, baseBranch) {
2471
3276
  try {
2472
- execFileSync3("git", ["fetch", "origin", baseBranch], {
3277
+ execFileSync4("git", ["fetch", "origin", baseBranch], {
2473
3278
  cwd: worktreePath,
2474
3279
  stdio: "pipe"
2475
3280
  });
2476
3281
  } catch {
2477
- log.warn(TAG7, "Failed to re-fetch base for revert guard — using last fetch");
3282
+ log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
2478
3283
  }
2479
3284
  }
2480
3285
  function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
2481
3286
  try {
2482
- const out = execFileSync3("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
3287
+ const out = execFileSync4("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
2483
3288
  return out.split(`
2484
3289
  `).map((l) => l.trim()).filter((l) => l.length > 0);
2485
3290
  } catch (err) {
2486
- log.warn(TAG7, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3291
+ log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
2487
3292
  return [];
2488
3293
  }
2489
3294
  }
@@ -2493,7 +3298,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
2493
3298
  }
2494
3299
 
2495
3300
  // src/verification.ts
2496
- var TAG8 = "verification";
3301
+ var TAG10 = "verification";
2497
3302
  var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
2498
3303
  async function runVerification(worktreePath, config, workerId) {
2499
3304
  const result = {
@@ -2505,52 +3310,52 @@ async function runVerification(worktreePath, config, workerId) {
2505
3310
  revertWarnings: []
2506
3311
  };
2507
3312
  if (config.verification.revertGuard) {
2508
- log.info(TAG8, `[worker:${workerId}] Checking for reverted merged work...`);
3313
+ log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
2509
3314
  const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
2510
3315
  if (deletedTests.length > 0) {
2511
3316
  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.");
2512
- log.warn(TAG8, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3317
+ log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
2513
3318
  result.passed = false;
2514
3319
  } else {
2515
- log.info(TAG8, `[worker:${workerId}] Revert guard passed`);
3320
+ log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
2516
3321
  }
2517
3322
  }
2518
3323
  if (config.verification.build) {
2519
- log.info(TAG8, `[worker:${workerId}] Running build...`);
3324
+ log.info(TAG10, `[worker:${workerId}] Running build...`);
2520
3325
  result.buildErrors = runBuild(worktreePath, config.verification.timeout);
2521
3326
  if (result.buildErrors.length > 0) {
2522
- log.warn(TAG8, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3327
+ log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2523
3328
  result.passed = false;
2524
3329
  } else {
2525
- log.info(TAG8, `[worker:${workerId}] Build passed`);
3330
+ log.info(TAG10, `[worker:${workerId}] Build passed`);
2526
3331
  }
2527
3332
  }
2528
3333
  if (config.verification.test && result.buildErrors.length === 0) {
2529
- log.info(TAG8, `[worker:${workerId}] Running tests...`);
3334
+ log.info(TAG10, `[worker:${workerId}] Running tests...`);
2530
3335
  result.testFailures = runTests(worktreePath, config.verification.testTimeout);
2531
3336
  if (result.testFailures.length > 0) {
2532
- log.warn(TAG8, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3337
+ log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
2533
3338
  result.passed = false;
2534
3339
  } else {
2535
- log.info(TAG8, `[worker:${workerId}] Tests passed`);
3340
+ log.info(TAG10, `[worker:${workerId}] Tests passed`);
2536
3341
  }
2537
3342
  }
2538
3343
  if (config.verification.lint) {
2539
- log.info(TAG8, `[worker:${workerId}] Running lint...`);
3344
+ log.info(TAG10, `[worker:${workerId}] Running lint...`);
2540
3345
  result.lintWarnings = runLint(worktreePath, config.verification.timeout);
2541
3346
  if (result.lintWarnings.length > 0) {
2542
- log.warn(TAG8, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3347
+ log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2543
3348
  } else {
2544
- log.info(TAG8, `[worker:${workerId}] Lint passed`);
3349
+ log.info(TAG10, `[worker:${workerId}] Lint passed`);
2545
3350
  }
2546
3351
  }
2547
3352
  if (config.verification.deepReview) {
2548
- log.info(TAG8, `[worker:${workerId}] Running deep review...`);
3353
+ log.info(TAG10, `[worker:${workerId}] Running deep review...`);
2549
3354
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
2550
3355
  if (result.reviewFindings.length > 0) {
2551
- log.warn(TAG8, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3356
+ log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2552
3357
  } else {
2553
- log.info(TAG8, `[worker:${workerId}] Deep review passed`);
3358
+ log.info(TAG10, `[worker:${workerId}] Deep review passed`);
2554
3359
  }
2555
3360
  }
2556
3361
  return result;
@@ -2558,11 +3363,11 @@ async function runVerification(worktreePath, config, workerId) {
2558
3363
  function runBuild(worktreePath, timeout) {
2559
3364
  const command = buildCommand(worktreePath);
2560
3365
  if (!command) {
2561
- log.warn(TAG8, `No known build toolchain for ${worktreePath} — skipping build`);
3366
+ log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
2562
3367
  return [];
2563
3368
  }
2564
3369
  try {
2565
- execFileSync4(command.cmd, command.args, {
3370
+ execFileSync5(command.cmd, command.args, {
2566
3371
  cwd: worktreePath,
2567
3372
  timeout,
2568
3373
  stdio: "pipe",
@@ -2576,11 +3381,11 @@ function runBuild(worktreePath, timeout) {
2576
3381
  function runTests(worktreePath, timeout) {
2577
3382
  const command = testCommand(worktreePath);
2578
3383
  if (!command) {
2579
- log.warn(TAG8, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3384
+ log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
2580
3385
  return [];
2581
3386
  }
2582
3387
  try {
2583
- execFileSync4(command.cmd, command.args, {
3388
+ execFileSync5(command.cmd, command.args, {
2584
3389
  cwd: worktreePath,
2585
3390
  timeout,
2586
3391
  stdio: "pipe",
@@ -2589,7 +3394,7 @@ function runTests(worktreePath, timeout) {
2589
3394
  return [];
2590
3395
  } catch (err) {
2591
3396
  const output = combineOutput(err);
2592
- log.warn(TAG8, `Test run failed:
3397
+ log.warn(TAG10, `Test run failed:
2593
3398
  ${output.slice(-4000) || "(no output captured)"}`);
2594
3399
  return parseTestFailures(err, timeout);
2595
3400
  }
@@ -2599,25 +3404,25 @@ function runFormatFix(worktreePath, timeout, workerId) {
2599
3404
  if (!command)
2600
3405
  return;
2601
3406
  try {
2602
- execFileSync4(command.cmd, command.args, {
3407
+ execFileSync5(command.cmd, command.args, {
2603
3408
  cwd: worktreePath,
2604
3409
  timeout,
2605
3410
  stdio: "pipe",
2606
3411
  maxBuffer: MAX_OUTPUT_BUFFER2
2607
3412
  });
2608
- log.info(TAG8, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3413
+ log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2609
3414
  } catch (err) {
2610
- log.warn(TAG8, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
3415
+ log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2611
3416
  }
2612
3417
  }
2613
3418
  function runLint(worktreePath, timeout) {
2614
3419
  const command = lintCommand(worktreePath);
2615
3420
  if (!command) {
2616
- log.info(TAG8, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3421
+ log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
2617
3422
  return [];
2618
3423
  }
2619
3424
  try {
2620
- execFileSync4(command.cmd, command.args, {
3425
+ execFileSync5(command.cmd, command.args, {
2621
3426
  cwd: worktreePath,
2622
3427
  timeout,
2623
3428
  stdio: "pipe",
@@ -2630,7 +3435,7 @@ function runLint(worktreePath, timeout) {
2630
3435
  }
2631
3436
  async function runDeepReview(worktreePath, config, workerId) {
2632
3437
  if (!supportsDevServer(worktreePath)) {
2633
- log.info(TAG8, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3438
+ log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
2634
3439
  return [];
2635
3440
  }
2636
3441
  const port = config.verification.devServerBasePort + workerId;
@@ -2645,12 +3450,12 @@ async function runDeepReview(worktreePath, config, workerId) {
2645
3450
  await waitForDevServer(devServer, 30000);
2646
3451
  await probeDevServer(port);
2647
3452
  } catch (err) {
2648
- log.error(TAG8, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
3453
+ log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
2649
3454
  return [];
2650
3455
  }
2651
3456
  let diff = "";
2652
3457
  try {
2653
- diff = execFileSync4("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
3458
+ diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
2654
3459
  cwd: worktreePath,
2655
3460
  encoding: "utf-8",
2656
3461
  timeout: 30000,
@@ -2672,7 +3477,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2672
3477
  ].join(`
2673
3478
  `);
2674
3479
  const leanSources = config.claude.leanSettingSources;
2675
- const output = execFileSync4("claude", [
3480
+ const output = execFileSync5("claude", [
2676
3481
  "--print",
2677
3482
  "--model",
2678
3483
  "sonnet",
@@ -2690,7 +3495,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2690
3495
  });
2691
3496
  return parseReviewFindings(output);
2692
3497
  } catch (err) {
2693
- log.error(TAG8, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3498
+ log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
2694
3499
  return [];
2695
3500
  } finally {
2696
3501
  if (devServer && !devServer.killed) {
@@ -2729,8 +3534,8 @@ function attemptAutoFix(worktreePath, config, errors) {
2729
3534
  "--",
2730
3535
  fixPrompt
2731
3536
  ];
2732
- log.info(TAG8, "Spawning Claude for auto-fix...");
2733
- execFileSync4("claude", args, {
3537
+ log.info(TAG10, "Spawning Claude for auto-fix...");
3538
+ execFileSync5("claude", args, {
2734
3539
  cwd: worktreePath,
2735
3540
  timeout: config.verification.timeout,
2736
3541
  stdio: "pipe",
@@ -2767,7 +3572,7 @@ async function reportFindings(client, cardId, result, recovery) {
2767
3572
  try {
2768
3573
  await client.createSubtask(cardId, title);
2769
3574
  } catch (err) {
2770
- log.error(TAG8, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3575
+ log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
2771
3576
  }
2772
3577
  }));
2773
3578
  if (overflow > 0) {
@@ -2775,7 +3580,7 @@ async function reportFindings(client, cardId, result, recovery) {
2775
3580
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
2776
3581
  } catch {}
2777
3582
  }
2778
- log.info(TAG8, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
3583
+ log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
2779
3584
  }
2780
3585
  function combineOutput(err) {
2781
3586
  const stderr = err?.stderr?.toString() ?? "";
@@ -2835,7 +3640,7 @@ class DevServerReadinessError extends Error {
2835
3640
  }
2836
3641
  }
2837
3642
  function waitForDevServer(proc, timeout) {
2838
- return new Promise((resolve2, reject) => {
3643
+ return new Promise((resolve3, reject) => {
2839
3644
  let settled = false;
2840
3645
  const cleanup = () => {
2841
3646
  proc.stdout?.off("data", onData);
@@ -2849,7 +3654,7 @@ function waitForDevServer(proc, timeout) {
2849
3654
  return;
2850
3655
  settled = true;
2851
3656
  cleanup();
2852
- resolve2();
3657
+ resolve3();
2853
3658
  };
2854
3659
  const settleReject = (err) => {
2855
3660
  if (settled)
@@ -2899,7 +3704,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
2899
3704
  }
2900
3705
 
2901
3706
  // src/gate-collectors.ts
2902
- var TAG9 = "gate-collectors";
3707
+ var TAG11 = "gate-collectors";
2903
3708
  async function resolveStageGate(client, card) {
2904
3709
  const currentStage = card.current_stage;
2905
3710
  const playbookId = card.playbook_id;
@@ -2917,7 +3722,7 @@ async function resolveStageGate(client, card) {
2917
3722
  return null;
2918
3723
  return { stage: resolution.stage, gate };
2919
3724
  } catch (err) {
2920
- log.warn(TAG9, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
3725
+ log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
2921
3726
  return null;
2922
3727
  }
2923
3728
  }
@@ -3042,13 +3847,14 @@ function buildGateCollectorRegistry(deps) {
3042
3847
  }
3043
3848
  if (deps.oracle) {
3044
3849
  registry.oracle_passed = new OracleCollector(deps.oracle);
3850
+ registry.oracle_red = new OracleRedCollector(deps.oracle);
3045
3851
  }
3046
3852
  return registry;
3047
3853
  }
3048
3854
  async function collectGateEvidence(registry, context) {
3049
3855
  const collector = registry[context.gate.kind];
3050
3856
  if (!collector) {
3051
- log.info(TAG9, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3857
+ log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3052
3858
  return {
3053
3859
  result: "blocked",
3054
3860
  structured: {
@@ -3060,14 +3866,14 @@ async function collectGateEvidence(registry, context) {
3060
3866
  return await collector.collect(context);
3061
3867
  } catch (err) {
3062
3868
  const msg = err instanceof Error ? err.message : String(err);
3063
- log.warn(TAG9, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3869
+ log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3064
3870
  return { result: "blocked", structured: { error: msg } };
3065
3871
  }
3066
3872
  }
3067
3873
  // src/git-diff-stat.ts
3068
3874
  init_log();
3069
- import { execFileSync as execFileSync5 } from "node:child_process";
3070
- var TAG10 = "git-diff-stat";
3875
+ import { execFileSync as execFileSync6 } from "node:child_process";
3876
+ var TAG12 = "git-diff-stat";
3071
3877
  var MAX_CHANGED_FILES = 30;
3072
3878
  function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
3073
3879
  const files = [];
@@ -3146,10 +3952,10 @@ function formatDiffSummary(diff, maxFiles = 100) {
3146
3952
  }
3147
3953
  function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
3148
3954
  try {
3149
- const raw = execFileSync5("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
3955
+ const raw = execFileSync6("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
3150
3956
  return parseNumstat(raw, maxFiles);
3151
3957
  } catch (err) {
3152
- log.warn(TAG10, "git diff --numstat failed", {
3958
+ log.warn(TAG12, "git diff --numstat failed", {
3153
3959
  event: "diff_stat_failed",
3154
3960
  error: err instanceof Error ? err.message : String(err)
3155
3961
  });
@@ -3162,7 +3968,7 @@ init_git_pr();
3162
3968
 
3163
3969
  // src/harmony-client.ts
3164
3970
  init_log();
3165
- var TAG12 = "harmony-client";
3971
+ var TAG14 = "harmony-client";
3166
3972
  function readClientConfig(env) {
3167
3973
  const apiUrl = env.HARMONY_API_URL?.trim();
3168
3974
  const apiKey = env.HARMONY_API_KEY?.trim();
@@ -3214,7 +4020,7 @@ class HarmonyClient {
3214
4020
  purpose: "gate_evaluation"
3215
4021
  });
3216
4022
  if (!response.ok) {
3217
- log.warn(TAG12, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4023
+ log.warn(TAG14, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
3218
4024
  return null;
3219
4025
  }
3220
4026
  const body = await response.json();
@@ -3294,138 +4100,101 @@ function relayAgentEvent(draft) {
3294
4100
  }
3295
4101
  return { type: "agent_event", event: draft };
3296
4102
  }
3297
- // src/oracle.ts
3298
- import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
3299
- import { dirname, isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
3300
- async function resolveContained(repoPath, relativePath) {
3301
- if (isAbsolute2(relativePath)) {
3302
- throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
3303
- }
3304
- if (relativePath === "" || relativePath === ".") {
3305
- throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
3306
- }
3307
- const root = await realpath(repoPath);
3308
- const target = resolve2(root, relativePath);
3309
- if (target !== root && !target.startsWith(root + sep2)) {
3310
- throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
3311
- }
3312
- let cursor = root;
3313
- for (const segment of relativePath.split("/")) {
3314
- cursor = resolve2(cursor, segment);
3315
- const stat = await lstat(cursor).catch(() => null);
3316
- if (stat?.isSymbolicLink()) {
3317
- throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
3318
- }
3319
- }
3320
- return target;
4103
+ // src/repair-sandbox.ts
4104
+ init_log();
4105
+ import { randomUUID } from "node:crypto";
4106
+ import { promisify as promisify3 } from "node:util";
4107
+ var TAG15 = "repair-sandbox";
4108
+ async function dockerExec(argv, opts) {
4109
+ const { execFile: execFile3 } = await import("node:child_process");
4110
+ return promisify3(execFile3)("docker", argv, {
4111
+ encoding: "utf-8",
4112
+ ...opts
4113
+ });
3321
4114
  }
3322
- async function place(repoPath, oracle) {
3323
- const target = await resolveContained(repoPath, oracle.path);
3324
- await mkdir(dirname(target), { recursive: true });
3325
- await writeFile(target, oracle.content, "utf8");
4115
+ var MAX_OUTPUT_BUFFER3 = 20971520;
4116
+ var PROBE_TIMEOUT_MS = 1e4;
4117
+ var SANDBOX_MOUNT = "/repo";
4118
+ var SANDBOX_MEMORY = "4g";
4119
+ var SANDBOX_PIDS = "512";
4120
+ var dockerProbe = null;
4121
+ async function sandboxAvailable() {
4122
+ if (dockerProbe === true)
4123
+ return true;
4124
+ try {
4125
+ await dockerExec(["version", "--format", "{{.Server.Version}}"], {
4126
+ timeout: PROBE_TIMEOUT_MS
4127
+ });
4128
+ dockerProbe = true;
4129
+ } catch {
4130
+ dockerProbe = false;
4131
+ }
4132
+ return dockerProbe;
3326
4133
  }
3327
- async function remove(repoPath, oracle) {
3328
- const target = await resolveContained(repoPath, oracle.path);
3329
- await rm(target, { force: true });
4134
+ function __resetSandboxProbe() {
4135
+ dockerProbe = null;
3330
4136
  }
3331
- var ORACLE_RUNNERS = {
3332
- vitest: (path) => ({
3333
- command: "npx",
3334
- args: ["--no-install", "vitest", "run", path]
3335
- }),
3336
- bun: (path) => ({ command: "bun", args: ["test", path] })
3337
- };
3338
- var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
3339
- var ORACLE_OUTPUT_LIMIT = 64 * 1024;
3340
- var ORACLE_SIGINT_GRACE_MS = 2000;
3341
- var ORACLE_SIGTERM_GRACE_MS = 3000;
3342
- var ORACLE_DRAIN_GRACE_MS = 500;
3343
- function resolveOracleRunner(oracle) {
3344
- const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
3345
- const build = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
3346
- if (!build) {
3347
- 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(", ")})`);
3348
- }
3349
- return build(argvPath(oracle.path));
4137
+ async function removeContainer(name) {
4138
+ try {
4139
+ await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
4140
+ log.warn(TAG15, `removed the timed-out sandbox container ${name}`);
4141
+ } catch {}
3350
4142
  }
3351
- function argvPath(path) {
3352
- return path.startsWith("./") ? path : `./${path}`;
4143
+ function sandboxRunArgs(image, worktree, command, name) {
4144
+ return [
4145
+ "run",
4146
+ "--rm",
4147
+ ...name ? ["--name", name] : [],
4148
+ "--network=none",
4149
+ "--cap-drop=ALL",
4150
+ "--security-opt=no-new-privileges",
4151
+ `--memory=${SANDBOX_MEMORY}`,
4152
+ `--pids-limit=${SANDBOX_PIDS}`,
4153
+ ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
4154
+ "--env",
4155
+ "HOME=/tmp",
4156
+ "--volume",
4157
+ `${worktree}:${SANDBOX_MOUNT}`,
4158
+ "--workdir",
4159
+ SANDBOX_MOUNT,
4160
+ "--entrypoint",
4161
+ command.cmd,
4162
+ image,
4163
+ ...command.args
4164
+ ];
3353
4165
  }
3354
- async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
3355
- const { command, args } = resolveOracleRunner(oracle);
3356
- return await new Promise((settleOk, settleErr) => {
3357
- let child;
3358
- try {
3359
- child = spawnInGroup(command, args, {
3360
- cwd: repoPath,
3361
- stdio: ["ignore", "pipe", "pipe"]
3362
- });
3363
- } catch (err) {
3364
- settleErr(err);
3365
- return;
3366
- }
3367
- const pgid = child.pid;
3368
- let output = "";
3369
- let settled = false;
3370
- let killing = false;
3371
- let timer;
3372
- let drainTimer;
3373
- const settle = (failure, result) => {
3374
- if (settled)
3375
- return;
3376
- settled = true;
3377
- if (timer)
3378
- clearTimeout(timer);
3379
- if (drainTimer)
3380
- clearTimeout(drainTimer);
3381
- reapGroup(pgid);
3382
- if (failure)
3383
- settleErr(failure);
3384
- else
3385
- settleOk(result);
3386
- };
3387
- const append = (chunk) => {
3388
- if (output.length >= ORACLE_OUTPUT_LIMIT)
3389
- return;
3390
- output += chunk.toString("utf8");
3391
- if (output.length > ORACLE_OUTPUT_LIMIT) {
3392
- output = `${output.slice(0, ORACLE_OUTPUT_LIMIT)}
3393
- … output truncated at ${ORACLE_OUTPUT_LIMIT} characters`;
3394
- }
3395
- };
3396
- child.stdout?.on("data", append);
3397
- child.stderr?.on("data", append);
3398
- child.once("error", (err) => settle(err));
3399
- const settleFromExit = (code, signal) => {
3400
- if (drainTimer)
3401
- clearTimeout(drainTimer);
3402
- if (code === null) {
3403
- settle(new Error(`the held test was terminated by signal ${signal}`));
3404
- return;
3405
- }
3406
- settle(null, { exitCode: code, output });
3407
- };
3408
- child.once("exit", (code, signal) => {
3409
- if (killing)
3410
- return;
3411
- if (timer)
3412
- clearTimeout(timer);
3413
- reapGroup(pgid);
3414
- drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
3415
- child.once("close", () => settleFromExit(code, signal));
4166
+ async function runInSandbox(args) {
4167
+ const name = `harmony-repair-${randomUUID()}`;
4168
+ const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
4169
+ log.info(TAG15, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
4170
+ try {
4171
+ const { stdout } = await dockerExec(argv, {
4172
+ timeout: args.timeoutMs,
4173
+ maxBuffer: MAX_OUTPUT_BUFFER3
3416
4174
  });
3417
- timer = setTimeout(() => {
3418
- if (settled)
3419
- return;
3420
- killing = true;
3421
- terminateGroup(child, {
3422
- sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
3423
- sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
3424
- }).catch(() => {}).then(() => {
3425
- settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
3426
- });
3427
- }, timeoutMs);
3428
- });
4175
+ return { passed: true, output: stdout ?? "" };
4176
+ } catch (err) {
4177
+ const e = err;
4178
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
4179
+ if (typeof e.code !== "number") {
4180
+ const timedOut = e.killed === true || e.signal != null;
4181
+ if (timedOut)
4182
+ await removeContainer(name);
4183
+ return {
4184
+ passed: false,
4185
+ output,
4186
+ sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
4187
+ };
4188
+ }
4189
+ if (e.code === 125) {
4190
+ return {
4191
+ passed: false,
4192
+ output,
4193
+ sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
4194
+ };
4195
+ }
4196
+ return { passed: false, output };
4197
+ }
3429
4198
  }
3430
4199
  // src/run-sizing.ts
3431
4200
  init_dist();
@@ -3525,14 +4294,37 @@ var defaultRunSize = async ({
3525
4294
  cwd,
3526
4295
  model
3527
4296
  };
4297
+ return collectSizingOutput(runner.start(input));
4298
+ };
4299
+ async function collectSizingOutput(events) {
3528
4300
  const parts = [];
3529
- for await (const ev of runner.start(input)) {
4301
+ let named;
4302
+ let sawError = false;
4303
+ for await (const ev of events) {
3530
4304
  if (ev.kind === "assistant_text")
3531
4305
  parts.push(ev.payload.text);
4306
+ if (ev.kind === "error") {
4307
+ sawError = true;
4308
+ named ??= sizingFailureFromError(ev.payload.message) ?? undefined;
4309
+ }
3532
4310
  }
3533
- return parts.join(`
3534
- `);
3535
- };
4311
+ const failure = named ?? (sawError ? "spawn" : undefined);
4312
+ return { text: parts.join(`
4313
+ `), ...failure ? { failure } : {} };
4314
+ }
4315
+ function sizingFailureFromError(message) {
4316
+ const subtype = resultErrorSubtype(message);
4317
+ if (!subtype)
4318
+ return null;
4319
+ switch (subtype) {
4320
+ case "error_max_turns":
4321
+ return "turns";
4322
+ case "error_max_budget_usd":
4323
+ return "budget";
4324
+ default:
4325
+ return "spawn";
4326
+ }
4327
+ }
3536
4328
  function buildSizingPrompt(title, description) {
3537
4329
  const card = JSON.stringify({
3538
4330
  title: title.slice(0, PROMPT_TITLE_MAX),
@@ -3583,7 +4375,7 @@ function sizingEventSource(source) {
3583
4375
  async function sizeRun(deps) {
3584
4376
  const requested = deps.model ?? SIZING_MODEL;
3585
4377
  if (!requested)
3586
- return null;
4378
+ return { status: "disabled" };
3587
4379
  const model = clampWithdrawn(requested);
3588
4380
  const timeoutMs = deps.timeoutMs ?? SIZING_TIMEOUT_MS;
3589
4381
  const run = deps.runSize ?? defaultRunSize;
@@ -3591,7 +4383,7 @@ async function sizeRun(deps) {
3591
4383
  let timer;
3592
4384
  let runner = null;
3593
4385
  try {
3594
- const text = await Promise.race([
4386
+ const result = await Promise.race([
3595
4387
  run({
3596
4388
  prompt,
3597
4389
  cwd: deps.cwd,
@@ -3610,11 +4402,16 @@ async function sizeRun(deps) {
3610
4402
  }, timeoutMs);
3611
4403
  })
3612
4404
  ]);
3613
- if (text === null)
3614
- return null;
3615
- return parseVerdict(text);
4405
+ if (result === null)
4406
+ return { status: "failed", reason: "timeout" };
4407
+ const sizing = parseVerdict(result.text);
4408
+ if (sizing)
4409
+ return { status: "sized", sizing };
4410
+ if (result.failure)
4411
+ return { status: "failed", reason: result.failure };
4412
+ return { status: "failed", reason: "malformed" };
3616
4413
  } catch {
3617
- return null;
4414
+ return { status: "failed", reason: "spawn" };
3618
4415
  } finally {
3619
4416
  if (timer)
3620
4417
  clearTimeout(timer);
@@ -3646,10 +4443,10 @@ async function runStage(request, deps) {
3646
4443
  }
3647
4444
  // src/worktree.ts
3648
4445
  init_log();
3649
- import { execFileSync as execFileSync7, execSync } from "node:child_process";
3650
- import { existsSync as existsSync3, rmSync } from "node:fs";
4446
+ import { execFileSync as execFileSync8, execSync } from "node:child_process";
4447
+ import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
3651
4448
  import { resolve as resolve3 } from "node:path";
3652
- var TAG13 = "worktree";
4449
+ var TAG16 = "worktree";
3653
4450
 
3654
4451
  class WorktreeBaseError extends Error {
3655
4452
  constructor(message) {
@@ -3657,7 +4454,7 @@ class WorktreeBaseError extends Error {
3657
4454
  this.name = "WorktreeBaseError";
3658
4455
  }
3659
4456
  }
3660
- function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync7("git", ["fetch", "origin", branch], {
4457
+ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", ["fetch", "origin", branch], {
3661
4458
  cwd: root,
3662
4459
  stdio: "pipe"
3663
4460
  })) {
@@ -3668,7 +4465,7 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
3668
4465
  return;
3669
4466
  } catch (err) {
3670
4467
  lastErr = err;
3671
- log.warn(TAG13, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
4468
+ log.warn(TAG16, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
3672
4469
  }
3673
4470
  }
3674
4471
  throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${gitErrorDetail(lastErr)}`);
@@ -3683,7 +4480,7 @@ function resolveWorktreeStartRef(baseBranch, branchName, continueExisting, branc
3683
4480
  }
3684
4481
  return `origin/${baseBranch}`;
3685
4482
  }
3686
- function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync7("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branch}`], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync7("git", [
4483
+ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl = (root, branch) => execFileSync8("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branch}`], { cwd: root, stdio: "pipe" }), fetchImpl = (root, branch) => execFileSync8("git", [
3687
4484
  "fetch",
3688
4485
  "origin",
3689
4486
  `+refs/heads/${branch}:refs/remotes/origin/${branch}`
@@ -3698,7 +4495,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
3698
4495
  if (err.status === 2)
3699
4496
  return false;
3700
4497
  lastErr = err;
3701
- log.warn(TAG13, `ls-remote ${branchName} failed (attempt ${attempt}/${attempts})`);
4498
+ log.warn(TAG16, `ls-remote ${branchName} failed (attempt ${attempt}/${attempts})`);
3702
4499
  }
3703
4500
  }
3704
4501
  if (!probed) {
@@ -3710,14 +4507,14 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
3710
4507
  return true;
3711
4508
  } catch (err) {
3712
4509
  lastErr = err;
3713
- log.warn(TAG13, `fetch ${branchName} failed (attempt ${attempt}/${attempts})`);
4510
+ log.warn(TAG16, `fetch ${branchName} failed (attempt ${attempt}/${attempts})`);
3714
4511
  }
3715
4512
  }
3716
4513
  throw new WorktreeBaseError(`${branchName} exists on origin but could not be fetched after ${attempts} attempts: ${gitErrorDetail(lastErr)}. Refusing to rebuild the branch.`);
3717
4514
  }
3718
4515
  function readWorktreeHead(worktreePath) {
3719
4516
  try {
3720
- return execFileSync7("git", ["rev-parse", "HEAD"], {
4517
+ return execFileSync8("git", ["rev-parse", "HEAD"], {
3721
4518
  cwd: worktreePath,
3722
4519
  encoding: "utf-8"
3723
4520
  }).trim();
@@ -3726,50 +4523,50 @@ function readWorktreeHead(worktreePath) {
3726
4523
  }
3727
4524
  }
3728
4525
  function createWorktree(basePath, baseBranch, branchName, opts = {}) {
3729
- const repoRoot = execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4526
+ const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3730
4527
  encoding: "utf-8"
3731
4528
  }).trim();
3732
4529
  const worktreeDir = resolve3(repoRoot, basePath, branchName);
3733
4530
  if (existsSync3(worktreeDir)) {
3734
- log.warn(TAG13, `Worktree already exists at ${worktreeDir}, cleaning up`);
4531
+ log.warn(TAG16, `Worktree already exists at ${worktreeDir}, cleaning up`);
3735
4532
  cleanupWorktree(worktreeDir, branchName);
3736
4533
  }
3737
4534
  try {
3738
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4535
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3739
4536
  cwd: repoRoot,
3740
4537
  stdio: "pipe"
3741
4538
  });
3742
4539
  } catch {}
3743
4540
  fetchBaseBranch(repoRoot, baseBranch);
3744
4541
  const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => fetchExistingBranch(repoRoot, branchName));
3745
- log.info(TAG13, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
4542
+ log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
3746
4543
  try {
3747
- execFileSync7("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
4544
+ execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
3748
4545
  } catch (err) {
3749
4546
  const msg = err instanceof Error ? err.message : String(err);
3750
- log.warn(TAG13, `worktree add failed, attempting forced recovery: ${msg}`);
4547
+ log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
3751
4548
  removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
3752
4549
  try {
3753
- execFileSync7("git", ["worktree", "remove", worktreeDir, "--force"], {
4550
+ execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
3754
4551
  cwd: repoRoot,
3755
4552
  stdio: "pipe"
3756
4553
  });
3757
4554
  } catch {}
3758
4555
  try {
3759
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4556
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3760
4557
  cwd: repoRoot,
3761
4558
  stdio: "pipe"
3762
4559
  });
3763
4560
  } catch {}
3764
4561
  try {
3765
- execFileSync7("git", ["branch", "-D", branchName], {
4562
+ execFileSync8("git", ["branch", "-D", branchName], {
3766
4563
  cwd: repoRoot,
3767
4564
  stdio: "pipe"
3768
4565
  });
3769
4566
  } catch {}
3770
- execFileSync7("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
4567
+ execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
3771
4568
  }
3772
- log.info(TAG13, "Installing dependencies in worktree...");
4569
+ log.info(TAG16, "Installing dependencies in worktree...");
3773
4570
  try {
3774
4571
  execSync(installCommand(), {
3775
4572
  cwd: worktreeDir,
@@ -3777,28 +4574,42 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
3777
4574
  timeout: 60000
3778
4575
  });
3779
4576
  } catch {
3780
- log.warn(TAG13, "Install failed (may be fine if deps are hoisted)");
4577
+ log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
3781
4578
  }
3782
4579
  return worktreeDir;
3783
4580
  }
4581
+ function containsForeignWorktrees(dir) {
4582
+ if (existsSync3(resolve3(dir, ".git")))
4583
+ return false;
4584
+ let children;
4585
+ try {
4586
+ children = readdirSync2(dir);
4587
+ } catch {
4588
+ return false;
4589
+ }
4590
+ return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
4591
+ }
3784
4592
  function cleanupWorktree(worktreePath, branchName) {
3785
- const repoRoot = execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4593
+ const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3786
4594
  encoding: "utf-8"
3787
4595
  }).trim();
3788
4596
  if (existsSync3(worktreePath)) {
4597
+ if (containsForeignWorktrees(worktreePath)) {
4598
+ throw new Error(`Refusing to remove ${worktreePath}: it is not a git worktree itself, ` + `but git worktrees live directly beneath it. Removing it would ` + `destroy them — pass the individual worktree paths instead (#928).`);
4599
+ }
3789
4600
  try {
3790
- execFileSync7("git", ["worktree", "remove", worktreePath, "--force"], {
4601
+ execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
3791
4602
  cwd: repoRoot,
3792
4603
  stdio: "pipe"
3793
4604
  });
3794
- log.info(TAG13, `Removed worktree: ${worktreePath}`);
4605
+ log.info(TAG16, `Removed worktree: ${worktreePath}`);
3795
4606
  } catch (err) {
3796
- log.warn(TAG13, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
4607
+ log.warn(TAG16, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
3797
4608
  if (existsSync3(worktreePath)) {
3798
4609
  rmSync(worktreePath, { recursive: true, force: true });
3799
4610
  }
3800
4611
  try {
3801
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4612
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3802
4613
  cwd: repoRoot,
3803
4614
  stdio: "pipe"
3804
4615
  });
@@ -3806,7 +4617,7 @@ function cleanupWorktree(worktreePath, branchName) {
3806
4617
  }
3807
4618
  } else {
3808
4619
  try {
3809
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4620
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3810
4621
  cwd: repoRoot,
3811
4622
  stdio: "pipe"
3812
4623
  });
@@ -3814,7 +4625,7 @@ function cleanupWorktree(worktreePath, branchName) {
3814
4625
  }
3815
4626
  if (branchName) {
3816
4627
  try {
3817
- execFileSync7("git", ["branch", "-D", branchName], {
4628
+ execFileSync8("git", ["branch", "-D", branchName], {
3818
4629
  cwd: repoRoot,
3819
4630
  stdio: "pipe"
3820
4631
  });
@@ -3824,7 +4635,7 @@ function cleanupWorktree(worktreePath, branchName) {
3824
4635
  function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3825
4636
  let listing;
3826
4637
  try {
3827
- listing = execFileSync7("git", ["worktree", "list", "--porcelain"], {
4638
+ listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
3828
4639
  cwd: repoRoot,
3829
4640
  encoding: "utf-8",
3830
4641
  stdio: ["ignore", "pipe", "pipe"]
@@ -3852,17 +4663,17 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3852
4663
  if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
3853
4664
  return null;
3854
4665
  try {
3855
- execFileSync7("git", ["worktree", "remove", holderPath, "--force"], {
4666
+ execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
3856
4667
  cwd: repoRoot,
3857
4668
  stdio: "pipe"
3858
4669
  });
3859
- log.warn(TAG13, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
4670
+ log.warn(TAG16, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
3860
4671
  } catch (err) {
3861
- log.warn(TAG13, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
4672
+ log.warn(TAG16, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
3862
4673
  return null;
3863
4674
  }
3864
4675
  try {
3865
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4676
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3866
4677
  cwd: repoRoot,
3867
4678
  stdio: "pipe"
3868
4679
  });
@@ -3870,13 +4681,13 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3870
4681
  return holderPath;
3871
4682
  }
3872
4683
  function resolveRepoRoot() {
3873
- return execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4684
+ return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3874
4685
  encoding: "utf-8"
3875
4686
  }).trim();
3876
4687
  }
3877
4688
  function localBranchExists(branchName, repoRoot) {
3878
4689
  try {
3879
- execFileSync7("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
4690
+ execFileSync8("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
3880
4691
  return true;
3881
4692
  } catch {
3882
4693
  return false;
@@ -3886,7 +4697,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
3886
4697
  if (!localBranchExists(branchName, repoRoot))
3887
4698
  return false;
3888
4699
  try {
3889
- const out = execFileSync7("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4700
+ const out = execFileSync8("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3890
4701
  return out.length > 0;
3891
4702
  } catch {
3892
4703
  return false;
@@ -3897,17 +4708,17 @@ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resol
3897
4708
  try {
3898
4709
  pushBranch2(branchName, repoRoot);
3899
4710
  } catch (err) {
3900
- log.error(TAG13, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
4711
+ log.error(TAG16, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
3901
4712
  return false;
3902
4713
  }
3903
- log.warn(TAG13, `push-rescued unpushed branch ${branchName} to origin before teardown`);
4714
+ log.warn(TAG16, `push-rescued unpushed branch ${branchName} to origin before teardown`);
3904
4715
  try {
3905
4716
  const url = getBranchWebUrl2(branchName, repoRoot);
3906
4717
  const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
3907
4718
  const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
3908
4719
  await client.addComment(cardId, body, { commentType: "message" });
3909
4720
  } catch (err) {
3910
- log.warn(TAG13, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
4721
+ log.warn(TAG16, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
3911
4722
  }
3912
4723
  return true;
3913
4724
  }
@@ -3925,7 +4736,7 @@ async function teardownWorktree(client, cardId, worktreePath, branchName) {
3925
4736
  const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
3926
4737
  if (!ok) {
3927
4738
  skipBranchDelete = true;
3928
- log.error(TAG13, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
4739
+ log.error(TAG16, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
3929
4740
  }
3930
4741
  }
3931
4742
  }
@@ -3942,32 +4753,43 @@ export {
3942
4753
  waitForDevServer,
3943
4754
  validateGitProviderCli,
3944
4755
  upsertReviewedSha,
4756
+ upsertCiReviewRequestedSha,
3945
4757
  updateExistingPr,
3946
4758
  testCommand,
3947
4759
  terminateGroup,
3948
4760
  teardownWorktree,
3949
4761
  supportsDevServer,
3950
4762
  summarizeUnifiedDiff,
4763
+ summarizeOracleReport,
3951
4764
  spawnRunArgs,
3952
4765
  spawnInGroup,
4766
+ sizingFailureFromError,
3953
4767
  sizingEventSource,
3954
4768
  sizeRun,
3955
4769
  signalGroup,
4770
+ sanitizeCiText,
4771
+ sandboxRunArgs,
4772
+ sandboxAvailable,
3956
4773
  runVerification,
3957
4774
  runTests,
3958
4775
  runStage,
3959
4776
  runMetricCommand,
3960
4777
  runLint,
4778
+ runInSandbox,
3961
4779
  runHeldOracle,
3962
4780
  runFormatFix,
3963
4781
  runDeepReview,
3964
4782
  runBuild,
4783
+ resultErrorSubtype,
4784
+ resultErrorMessage,
3965
4785
  resolveWorktreeStartRef,
3966
4786
  resolveStageGate,
3967
4787
  resolvePrUrl,
3968
4788
  resolvePrHeadBranch,
3969
4789
  resolveOracleRunner,
3970
4790
  rescueUnpushedBranch,
4791
+ rerunFailedJobs,
4792
+ requestIndependentReview,
3971
4793
  reportFindings,
3972
4794
  renameRemoteBranch,
3973
4795
  removeWorktreeHoldingBranch,
@@ -3976,6 +4798,7 @@ export {
3976
4798
  relayAgentEvent,
3977
4799
  reapGroup,
3978
4800
  readWorktreeHead,
4801
+ readPrChecks,
3979
4802
  readClientConfig,
3980
4803
  pushBranch,
3981
4804
  probeDevServer,
@@ -3984,6 +4807,7 @@ export {
3984
4807
  parseNumstat,
3985
4808
  parseMetricValue,
3986
4809
  parseJudgeVerdict,
4810
+ parseFailedChecks,
3987
4811
  normalizeGateSpec,
3988
4812
  mergePullRequest,
3989
4813
  mapSdkErrorKind,
@@ -3993,10 +4817,14 @@ export {
3993
4817
  lintCommand,
3994
4818
  isTestFile,
3995
4819
  isPretty,
4820
+ isPrOnOrigin,
3996
4821
  isInsideTree,
3997
4822
  installCommand,
4823
+ gradeOracleRed,
3998
4824
  getPrStatus,
4825
+ getPrFailedChecks,
3999
4826
  getHeadSha,
4827
+ getFailedRunLog,
4000
4828
  getBranchWebUrl,
4001
4829
  gateConfigErrorReason,
4002
4830
  formatFixCommand,
@@ -4006,14 +4834,19 @@ export {
4006
4834
  filterTestFiles,
4007
4835
  fetchExistingBranch,
4008
4836
  fetchBaseBranch,
4837
+ extractWorkflowRunId,
4009
4838
  extractReviewedSha,
4010
4839
  extractPrUrl,
4840
+ extractCiReviewRequestedSha,
4011
4841
  extractAzurePrId,
4012
4842
  envKeysDroppedByLaunch,
4013
4843
  detectPackageManager,
4014
4844
  detectGitProvider,
4015
4845
  detect,
4846
+ describeCiFailure,
4016
4847
  describeApiError,
4848
+ deriveReviewVerdictState,
4849
+ deriveIndependentReviewState,
4017
4850
  deriveCiStatus,
4018
4851
  decidePrBranch,
4019
4852
  decideConfinedTool,
@@ -4022,9 +4855,12 @@ export {
4022
4855
  createPullRequest,
4023
4856
  cooldownMsFor,
4024
4857
  confineToRepo,
4858
+ collectSizingOutput,
4025
4859
  collectGateEvidence,
4026
4860
  cleanupWorktree,
4027
4861
  classifyRunError,
4862
+ classifyCiFailures,
4863
+ classifyCiFailure,
4028
4864
  clampWithdrawn,
4029
4865
  chooseImplementModel,
4030
4866
  checkPrMergeStatus,
@@ -4037,11 +4873,14 @@ export {
4037
4873
  branchAheadOfItsRemote,
4038
4874
  attemptAutoFix,
4039
4875
  _resetCache,
4876
+ __resetSandboxProbe,
4040
4877
  WorktreeBaseError,
4041
4878
  SdkAgentRunner,
4042
4879
  SIZING_MODEL,
4043
4880
  SDK_ALLOWED_TOOLS,
4881
+ SANDBOX_MOUNT,
4044
4882
  ReviewPassedCollector,
4883
+ OracleRedCollector,
4045
4884
  OracleCollector,
4046
4885
  ORACLE_RUNNER_HINTS,
4047
4886
  MOTOR_TOOL_INPUT_VALUE_MAX,
@@ -4058,6 +4897,8 @@ export {
4058
4897
  DEFAULT_METRIC_TIMEOUT_MS,
4059
4898
  CommandMetricCollector,
4060
4899
  ChecklistDodCollector,
4900
+ CONFINED_WRITE_TOOLS,
4901
+ CONFINED_READ_TOOLS,
4061
4902
  BuildGreenCollector,
4062
4903
  ArtifactCollector
4063
4904
  };