@gethmy/harness 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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)));
@@ -476,7 +479,8 @@ var init_gateEvaluate = __esm(() => {
476
479
  "artifact",
477
480
  "label",
478
481
  "custom",
479
- "oracle_passed"
482
+ "oracle_passed",
483
+ "oracle_red"
480
484
  ];
481
485
  GATE_OPERATORS = [
482
486
  "eq",
@@ -552,6 +556,13 @@ var init_playbookStage = __esm(() => {
552
556
 
553
557
  // ../harmony-shared/dist/projectTemplates.js
554
558
  var init_projectTemplates = () => {};
559
+
560
+ // ../harmony-shared/dist/realtimeChannel.js
561
+ var inFlightDetach;
562
+ var init_realtimeChannel = __esm(() => {
563
+ init_logger();
564
+ inFlightDetach = new WeakMap;
565
+ });
555
566
  // ../harmony-shared/dist/reviewTools.js
556
567
  var REVIEW_DISALLOWED_TOOLS;
557
568
  var init_reviewTools = __esm(() => {
@@ -579,6 +590,7 @@ var init_dist = __esm(() => {
579
590
  init_agentStaleness();
580
591
  init_branchRef();
581
592
  init_cardLinks();
593
+ init_cardModelStat();
582
594
  init_classification();
583
595
  init_columnCardSplit();
584
596
  init_columnSort();
@@ -592,6 +604,7 @@ var init_dist = __esm(() => {
592
604
  init_playbookCatalog();
593
605
  init_playbookStage();
594
606
  init_projectTemplates();
607
+ init_realtimeChannel();
595
608
  init_reviewTools();
596
609
  init_stageHandoff();
597
610
  init_types();
@@ -602,11 +615,14 @@ var exports_git_pr = {};
602
615
  __export(exports_git_pr, {
603
616
  validateGitProviderCli: () => validateGitProviderCli,
604
617
  upsertReviewedSha: () => upsertReviewedSha,
618
+ upsertCiReviewRequestedSha: () => upsertCiReviewRequestedSha,
605
619
  updateExistingPr: () => updateExistingPr,
606
620
  resolvePrUrl: () => resolvePrUrl,
607
621
  resolvePrHeadBranch: () => resolvePrHeadBranch,
622
+ requestIndependentReview: () => requestIndependentReview,
608
623
  renameRemoteBranch: () => renameRemoteBranch,
609
624
  remoteBranchExists: () => remoteBranchExists,
625
+ readPrChecks: () => readPrChecks,
610
626
  pushBranch: () => pushBranch,
611
627
  mergePullRequest: () => mergePullRequest,
612
628
  getPrStatus: () => getPrStatus,
@@ -615,25 +631,28 @@ __export(exports_git_pr, {
615
631
  findExistingPr: () => findExistingPr,
616
632
  extractReviewedSha: () => extractReviewedSha,
617
633
  extractPrUrl: () => extractPrUrl,
634
+ extractCiReviewRequestedSha: () => extractCiReviewRequestedSha,
618
635
  extractAzurePrId: () => extractAzurePrId,
619
636
  detectGitProvider: () => detectGitProvider,
637
+ deriveReviewVerdictState: () => deriveReviewVerdictState,
638
+ deriveIndependentReviewState: () => deriveIndependentReviewState,
620
639
  deriveCiStatus: () => deriveCiStatus,
621
640
  decidePrBranch: () => decidePrBranch,
622
641
  createPullRequest: () => createPullRequest,
623
642
  checkPrMergeStatus: () => checkPrMergeStatus,
624
643
  buildPrBody: () => buildPrBody
625
644
  });
626
- import { execFile, execFileSync as execFileSync6 } from "node:child_process";
627
- import { promisify } from "node:util";
628
- function createExecFileAsync() {
629
- return promisify(execFile);
645
+ import { execFile as execFile2, execFileSync as execFileSync7 } from "node:child_process";
646
+ import { promisify as promisify2 } from "node:util";
647
+ function createExecFileAsync2() {
648
+ return promisify2(execFile2);
630
649
  }
631
- function execFileAsync() {
632
- return cachedExecFileAsync ??= createExecFileAsync();
650
+ function execFileAsync2() {
651
+ return cachedExecFileAsync2 ??= createExecFileAsync2();
633
652
  }
634
653
  function detectGitProvider(cwd) {
635
654
  try {
636
- const url = execFileSync6("git", ["remote", "get-url", "origin"], {
655
+ const url = execFileSync7("git", ["remote", "get-url", "origin"], {
637
656
  cwd,
638
657
  encoding: "utf-8"
639
658
  }).trim();
@@ -654,7 +673,7 @@ function validateGitProviderCli(provider, cwd) {
654
673
  switch (provider) {
655
674
  case "github": {
656
675
  try {
657
- execFileSync6("gh", ["auth", "status"], { cwd, stdio: "pipe" });
676
+ execFileSync7("gh", ["auth", "status"], { cwd, stdio: "pipe" });
658
677
  } catch {
659
678
  throw new Error("GitHub CLI (gh) is not authenticated. Run: gh auth login");
660
679
  }
@@ -662,12 +681,12 @@ function validateGitProviderCli(provider, cwd) {
662
681
  }
663
682
  case "azure": {
664
683
  try {
665
- execFileSync6("az", ["--version"], { cwd, stdio: "pipe" });
684
+ execFileSync7("az", ["--version"], { cwd, stdio: "pipe" });
666
685
  } catch {
667
686
  throw new Error("Azure CLI (az) not found. Install it: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli");
668
687
  }
669
688
  try {
670
- execFileSync6("az", ["account", "show"], { cwd, stdio: "pipe" });
689
+ execFileSync7("az", ["account", "show"], { cwd, stdio: "pipe" });
671
690
  } catch {
672
691
  throw new Error("Azure CLI is not authenticated. Run: az login");
673
692
  }
@@ -675,7 +694,7 @@ function validateGitProviderCli(provider, cwd) {
675
694
  }
676
695
  case "gitlab": {
677
696
  try {
678
- execFileSync6("glab", ["auth", "status"], { cwd, stdio: "pipe" });
697
+ execFileSync7("glab", ["auth", "status"], { cwd, stdio: "pipe" });
679
698
  } catch {
680
699
  throw new Error("GitLab CLI (glab) is not installed or not authenticated. Install: https://gitlab.com/gitlab-org/cli — then run: glab auth login");
681
700
  }
@@ -683,7 +702,7 @@ function validateGitProviderCli(provider, cwd) {
683
702
  }
684
703
  case "bitbucket":
685
704
  case "unknown":
686
- log.warn(TAG11, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
705
+ log.warn(TAG13, `Git provider "${provider}" — PR creation will be skipped (no CLI support)`);
687
706
  break;
688
707
  }
689
708
  }
@@ -701,18 +720,51 @@ function upsertReviewedSha(description, sha) {
701
720
  if (REVIEWED_SHA_RE.test(description)) {
702
721
  return description.replace(REVIEWED_SHA_RE, line);
703
722
  }
704
- const sep2 = description ? `
723
+ const sep3 = description ? `
724
+ ` : "";
725
+ return `${description}${sep3}${line}`;
726
+ }
727
+ function extractCiReviewRequestedSha(description) {
728
+ if (!description)
729
+ return null;
730
+ const m = description.match(CI_REVIEW_REQUESTED_SHA_RE);
731
+ return m ? m[1] : null;
732
+ }
733
+ function upsertCiReviewRequestedSha(description, sha) {
734
+ const line = `CI-Review-Requested-SHA: ${sha}`;
735
+ if (CI_REVIEW_REQUESTED_SHA_RE.test(description)) {
736
+ return description.replace(CI_REVIEW_REQUESTED_SHA_RE, line);
737
+ }
738
+ const sep3 = description ? `
705
739
  ` : "";
706
- return `${description}${sep2}${line}`;
740
+ return `${description}${sep3}${line}`;
707
741
  }
708
- function deriveCiStatus(rollup) {
742
+ function checkMatchesName(c, wanted) {
743
+ if (!wanted)
744
+ return false;
745
+ const name = typeof c.name === "string" ? c.name.toLowerCase() : "";
746
+ const workflow = typeof c.workflowName === "string" ? c.workflowName.toLowerCase() : "";
747
+ return name === wanted || workflow === wanted;
748
+ }
749
+ function isNamedCheck(c, skip) {
750
+ if (!skip)
751
+ return false;
752
+ const wanted = skip.trim().toLowerCase();
753
+ if (!wanted)
754
+ return false;
755
+ return typeof c.name === "string" && c.name.toLowerCase() === wanted;
756
+ }
757
+ function deriveCiStatus(rollup, excludeChecks = []) {
709
758
  if (!Array.isArray(rollup) || rollup.length === 0)
710
759
  return "unknown";
760
+ const excluded = excludeChecks.map((n) => n.trim().toLowerCase()).filter(Boolean);
711
761
  let anyPending = false;
712
762
  for (const check of rollup) {
713
763
  if (typeof check !== "object" || check === null)
714
764
  continue;
715
765
  const c = check;
766
+ if (excluded.some((name) => checkMatchesName(c, name)))
767
+ continue;
716
768
  if (typeof c.status === "string") {
717
769
  if (c.status.toUpperCase() !== "COMPLETED") {
718
770
  anyPending = true;
@@ -736,18 +788,152 @@ function deriveCiStatus(rollup) {
736
788
  }
737
789
  return anyPending ? "pending" : "success";
738
790
  }
739
- async function getPrStatus(prUrl, cwd, provider) {
740
- if (provider !== "github" || !isValidPrUrl(prUrl)) {
741
- return { ciStatus: "unknown", headSha: null };
791
+ function deriveIndependentReviewState(rollup, checkName, skipCheckName) {
792
+ if (!Array.isArray(rollup))
793
+ return "absent";
794
+ const wanted = checkName.trim().toLowerCase();
795
+ if (!wanted)
796
+ return "absent";
797
+ let sawPending = false;
798
+ let sawFailed = false;
799
+ let sawSkipped = false;
800
+ for (const check of rollup) {
801
+ if (typeof check !== "object" || check === null)
802
+ continue;
803
+ const c = check;
804
+ if (isNamedCheck(c, skipCheckName))
805
+ continue;
806
+ if (!checkMatchesName(c, wanted))
807
+ continue;
808
+ if (typeof c.status === "string") {
809
+ if (c.status.toUpperCase() !== "COMPLETED") {
810
+ sawPending = true;
811
+ continue;
812
+ }
813
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
814
+ if (conclusion === "SKIPPED") {
815
+ sawSkipped = true;
816
+ continue;
817
+ }
818
+ if (conclusion === "SUCCESS" || conclusion === "NEUTRAL")
819
+ return "passed";
820
+ sawFailed = true;
821
+ continue;
822
+ }
823
+ if (typeof c.state === "string") {
824
+ const state = c.state.toUpperCase();
825
+ if (state === "SUCCESS")
826
+ return "passed";
827
+ if (state === "PENDING") {
828
+ sawPending = true;
829
+ continue;
830
+ }
831
+ sawFailed = true;
832
+ }
833
+ }
834
+ if (sawPending)
835
+ return "pending";
836
+ if (sawFailed)
837
+ return "failed";
838
+ if (sawSkipped)
839
+ return "skipped";
840
+ return "absent";
841
+ }
842
+ function deriveReviewVerdictState(rollup, checkName, skipCheckName) {
843
+ if (!Array.isArray(rollup))
844
+ return "absent";
845
+ const wanted = checkName.trim().toLowerCase();
846
+ if (!wanted)
847
+ return "absent";
848
+ let sawPending = false;
849
+ let sawUnknown = false;
850
+ let sawClean = false;
851
+ for (const check of rollup) {
852
+ if (typeof check !== "object" || check === null)
853
+ continue;
854
+ const c = check;
855
+ if (isNamedCheck(c, skipCheckName))
856
+ continue;
857
+ if (!checkMatchesName(c, wanted))
858
+ continue;
859
+ if (typeof c.status === "string") {
860
+ if (c.status.toUpperCase() !== "COMPLETED") {
861
+ sawPending = true;
862
+ continue;
863
+ }
864
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
865
+ if (conclusion === "SUCCESS") {
866
+ sawClean = true;
867
+ continue;
868
+ }
869
+ if (conclusion === "NEUTRAL" || conclusion === "SKIPPED") {
870
+ sawUnknown = true;
871
+ continue;
872
+ }
873
+ return "blocking";
874
+ }
875
+ if (typeof c.state === "string") {
876
+ const state = c.state.toUpperCase();
877
+ if (state === "SUCCESS") {
878
+ sawClean = true;
879
+ continue;
880
+ }
881
+ if (state === "PENDING") {
882
+ sawPending = true;
883
+ continue;
884
+ }
885
+ return "blocking";
886
+ }
742
887
  }
888
+ if (sawPending)
889
+ return "pending";
890
+ if (sawUnknown)
891
+ return "unknown";
892
+ if (sawClean)
893
+ return "clean";
894
+ return "absent";
895
+ }
896
+ function readPrChecks(rollup, checks = {}) {
897
+ const { independentReviewCheck, reviewVerdictCheck } = checks;
898
+ return {
899
+ ciStatus: deriveCiStatus(rollup, [independentReviewCheck, reviewVerdictCheck].filter((n) => typeof n === "string")),
900
+ independentReview: independentReviewCheck ? deriveIndependentReviewState(rollup, independentReviewCheck, reviewVerdictCheck) : "absent",
901
+ reviewVerdict: reviewVerdictCheck ? deriveReviewVerdictState(rollup, reviewVerdictCheck, independentReviewCheck) : "absent"
902
+ };
903
+ }
904
+ async function getPrStatus(prUrl, cwd, provider, independentReviewCheck, reviewVerdictCheck) {
905
+ const unreadable = {
906
+ ciStatus: "unknown",
907
+ headSha: null,
908
+ independentReview: "absent",
909
+ reviewVerdict: "absent"
910
+ };
911
+ if (provider !== "github" || !isValidPrUrl(prUrl))
912
+ return { ...unreadable };
743
913
  try {
744
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup,headRefOid"], { cwd, encoding: "utf-8", timeout: 1e4 });
914
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup,headRefOid"], { cwd, encoding: "utf-8", timeout: 1e4 });
745
915
  const parsed = JSON.parse(stdout.trim());
746
916
  const headSha = typeof parsed.headRefOid === "string" ? parsed.headRefOid : null;
747
- return { ciStatus: deriveCiStatus(parsed.statusCheckRollup), headSha };
917
+ return {
918
+ ...readPrChecks(parsed.statusCheckRollup, {
919
+ independentReviewCheck,
920
+ reviewVerdictCheck
921
+ }),
922
+ headSha
923
+ };
748
924
  } catch {
749
- return { ciStatus: "unknown", headSha: null };
925
+ return { ...unreadable };
926
+ }
927
+ }
928
+ async function requestIndependentReview(prUrl, cwd, provider, label) {
929
+ if (provider !== "github") {
930
+ throw new Error(`independent review request unsupported for "${provider}"`);
750
931
  }
932
+ const opts = { cwd, encoding: "utf-8", timeout: 30000 };
933
+ try {
934
+ await execFileAsync2()("gh", ["pr", "edit", prUrl, "--remove-label", label], opts);
935
+ } catch {}
936
+ await execFileAsync2()("gh", ["pr", "edit", prUrl, "--add-label", label], opts);
751
937
  }
752
938
  async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
753
939
  if (provider !== "github") {
@@ -756,7 +942,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
756
942
  const args = ["pr", "merge", prUrl, `--${strategy}`];
757
943
  if (deleteBranch)
758
944
  args.push("--delete-branch");
759
- await execFileAsync()("gh", args, {
945
+ await execFileAsync2()("gh", args, {
760
946
  cwd,
761
947
  encoding: "utf-8",
762
948
  timeout: 30000
@@ -764,7 +950,7 @@ async function mergePullRequest(prUrl, cwd, provider, strategy, deleteBranch) {
764
950
  }
765
951
  function getHeadSha(cwd) {
766
952
  try {
767
- return execFileSync6("git", ["rev-parse", "HEAD"], {
953
+ return execFileSync7("git", ["rev-parse", "HEAD"], {
768
954
  cwd,
769
955
  encoding: "utf-8"
770
956
  }).trim();
@@ -778,7 +964,7 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
778
964
  try {
779
965
  switch (provider) {
780
966
  case "github": {
781
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 1e4 });
967
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "state", "--jq", ".state"], { cwd, encoding: "utf-8", timeout: 1e4 });
782
968
  switch (stdout.trim()) {
783
969
  case "MERGED":
784
970
  return "merged";
@@ -794,12 +980,12 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
794
980
  const mrMatch = prUrl.match(/merge_requests\/(\d+)/);
795
981
  if (!mrMatch)
796
982
  return "unknown";
797
- const { stdout } = await execFileAsync()("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
983
+ const { stdout } = await execFileAsync2()("glab", ["mr", "view", mrMatch[1], "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
798
984
  let parsed;
799
985
  try {
800
986
  parsed = JSON.parse(stdout.trim());
801
987
  } catch {
802
- log.warn(TAG11, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
988
+ log.warn(TAG13, `Failed to parse glab JSON output for MR ${mrMatch[1]}`);
803
989
  return "unknown";
804
990
  }
805
991
  if (typeof parsed !== "object" || parsed === null)
@@ -894,10 +1080,10 @@ function extractAzurePrId(prUrl) {
894
1080
  async function resolvePrHeadBranch(prUrl, cwd, provider) {
895
1081
  if (provider === "github") {
896
1082
  try {
897
- const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1083
+ const { stdout } = await execFileAsync2()("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
898
1084
  return decidePrBranch("github", stdout);
899
1085
  } catch (err) {
900
- log.warn(TAG11, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1086
+ log.warn(TAG13, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
901
1087
  return decidePrBranch("github", null);
902
1088
  }
903
1089
  }
@@ -910,10 +1096,10 @@ async function resolvePrHeadBranch(prUrl, cwd, provider) {
910
1096
  };
911
1097
  }
912
1098
  try {
913
- const { stdout } = await execFileAsync()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1099
+ const { stdout } = await execFileAsync2()("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
914
1100
  return decidePrBranch("azure", stdout);
915
1101
  } catch (err) {
916
- log.warn(TAG11, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1102
+ log.warn(TAG13, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
917
1103
  return decidePrBranch("azure", null);
918
1104
  }
919
1105
  }
@@ -941,7 +1127,7 @@ function resolvePrUrl(description, branchName, cwd, provider) {
941
1127
  }
942
1128
  function remoteBranchExists(branchName, cwd) {
943
1129
  try {
944
- execFileSync6("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
1130
+ execFileSync7("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
945
1131
  return true;
946
1132
  } catch {
947
1133
  return false;
@@ -949,24 +1135,24 @@ function remoteBranchExists(branchName, cwd) {
949
1135
  }
950
1136
  function pushBranch(branchName, cwd) {
951
1137
  if (remoteBranchExists(branchName, cwd)) {
952
- log.info(TAG11, `Remote branch ${branchName} exists (rework), force-pushing`);
1138
+ log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
953
1139
  let expectedSha = null;
954
1140
  try {
955
- execFileSync6("git", ["fetch", "origin", branchName], {
1141
+ execFileSync7("git", ["fetch", "origin", branchName], {
956
1142
  cwd,
957
1143
  stdio: "pipe"
958
1144
  });
959
- expectedSha = execFileSync6("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
1145
+ expectedSha = execFileSync7("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
960
1146
  } catch (err) {
961
- log.warn(TAG11, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
1147
+ log.warn(TAG13, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
962
1148
  }
963
1149
  const lease = expectedSha ? `--force-with-lease=refs/heads/${branchName}:${expectedSha}` : "--force-with-lease";
964
- execFileSync6("git", ["push", lease, "-u", "origin", branchName], {
1150
+ execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
965
1151
  cwd,
966
1152
  stdio: "pipe"
967
1153
  });
968
1154
  } else {
969
- execFileSync6("git", ["push", "-u", "origin", branchName], {
1155
+ execFileSync7("git", ["push", "-u", "origin", branchName], {
970
1156
  cwd,
971
1157
  stdio: "pipe"
972
1158
  });
@@ -977,25 +1163,25 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
977
1163
  return;
978
1164
  let sha;
979
1165
  try {
980
- sha = execFileSync6("git", ["rev-parse", "HEAD"], {
1166
+ sha = execFileSync7("git", ["rev-parse", "HEAD"], {
981
1167
  cwd,
982
1168
  encoding: "utf-8"
983
1169
  }).trim();
984
1170
  } catch (err) {
985
1171
  throw new Error(`renameRemoteBranch: could not resolve HEAD: ${err instanceof Error ? err.message : err}`);
986
1172
  }
987
- log.info(TAG11, `Renaming remote ${oldRef} → ${newRef}`);
988
- execFileSync6("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
1173
+ log.info(TAG13, `Renaming remote ${oldRef} → ${newRef}`);
1174
+ execFileSync7("git", ["push", "origin", `${sha}:refs/heads/${newRef}`, "--force-with-lease"], { cwd, stdio: "pipe" });
989
1175
  try {
990
- execFileSync6("git", ["push", "origin", `:refs/heads/${oldRef}`], {
1176
+ execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
991
1177
  cwd,
992
1178
  stdio: "pipe"
993
1179
  });
994
1180
  } catch (err) {
995
- log.warn(TAG11, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
1181
+ log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
996
1182
  }
997
1183
  try {
998
- execFileSync6("git", ["branch", "-m", oldRef, newRef], {
1184
+ execFileSync7("git", ["branch", "-m", oldRef, newRef], {
999
1185
  cwd,
1000
1186
  stdio: "pipe"
1001
1187
  });
@@ -1003,7 +1189,7 @@ function renameRemoteBranch(oldRef, newRef, cwd) {
1003
1189
  }
1004
1190
  function getBranchWebUrl(branchName, cwd) {
1005
1191
  try {
1006
- const remoteUrl = execFileSync6("git", ["remote", "get-url", "origin"], {
1192
+ const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
1007
1193
  cwd,
1008
1194
  encoding: "utf-8"
1009
1195
  }).trim();
@@ -1051,12 +1237,12 @@ function buildPrBody(card, commitLog) {
1051
1237
  }
1052
1238
  function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1053
1239
  if (existingPrUrl) {
1054
- log.info(TAG11, `Reusing existing PR from card description: ${existingPrUrl}`);
1240
+ log.info(TAG13, `Reusing existing PR from card description: ${existingPrUrl}`);
1055
1241
  return existingPrUrl;
1056
1242
  }
1057
1243
  let commitLog = "";
1058
1244
  try {
1059
- commitLog = execFileSync6("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
1245
+ commitLog = execFileSync7("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
1060
1246
  } catch {
1061
1247
  commitLog = "(unable to retrieve commit log)";
1062
1248
  }
@@ -1065,7 +1251,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1065
1251
  const base = config.worktree.baseBranch;
1066
1252
  const existingUrl = findExistingPr(branchName, worktreePath, provider);
1067
1253
  if (existingUrl) {
1068
- log.info(TAG11, `PR already exists for ${branchName}, updating body...`);
1254
+ log.info(TAG13, `PR already exists for ${branchName}, updating body...`);
1069
1255
  updateExistingPr(branchName, body, worktreePath, provider);
1070
1256
  return existingUrl;
1071
1257
  }
@@ -1073,10 +1259,10 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1073
1259
  let result;
1074
1260
  switch (provider) {
1075
1261
  case "github":
1076
- result = execFileSync6("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
1262
+ result = execFileSync7("gh", ["pr", "create", "--title", title, "--body", body, "--base", base], { cwd: worktreePath, encoding: "utf-8" }).trim();
1077
1263
  break;
1078
1264
  case "azure": {
1079
- const azOutput = execFileSync6("az", [
1265
+ const azOutput = execFileSync7("az", [
1080
1266
  "repos",
1081
1267
  "pr",
1082
1268
  "create",
@@ -1100,7 +1286,7 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1100
1286
  break;
1101
1287
  }
1102
1288
  case "gitlab":
1103
- result = execFileSync6("glab", [
1289
+ result = execFileSync7("glab", [
1104
1290
  "mr",
1105
1291
  "create",
1106
1292
  "--title",
@@ -1115,13 +1301,13 @@ function createPullRequest(card, branchName, worktreePath, config, provider, exi
1115
1301
  ], { cwd: worktreePath, encoding: "utf-8" }).trim();
1116
1302
  break;
1117
1303
  default:
1118
- log.warn(TAG11, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1304
+ log.warn(TAG13, `No PR CLI for provider "${provider}" — branch pushed but no PR created`);
1119
1305
  return null;
1120
1306
  }
1121
- log.info(TAG11, `PR created: ${result}`);
1307
+ log.info(TAG13, `PR created: ${result}`);
1122
1308
  return result;
1123
1309
  } catch (err) {
1124
- log.error(TAG11, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1310
+ log.error(TAG13, `Failed to create PR: ${err instanceof Error ? err.message : err}`);
1125
1311
  return null;
1126
1312
  }
1127
1313
  }
@@ -1129,9 +1315,9 @@ function findExistingPr(branchName, worktreePath, provider) {
1129
1315
  try {
1130
1316
  switch (provider) {
1131
1317
  case "github":
1132
- return execFileSync6("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1318
+ return execFileSync7("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1133
1319
  case "gitlab": {
1134
- const json = execFileSync6("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1320
+ const json = execFileSync7("glab", ["mr", "view", branchName, "--output", "json"], { cwd: worktreePath, encoding: "utf-8" }).trim();
1135
1321
  const parsed = JSON.parse(json);
1136
1322
  return parsed.web_url || null;
1137
1323
  }
@@ -1146,27 +1332,28 @@ function updateExistingPr(branchName, body, worktreePath, provider) {
1146
1332
  try {
1147
1333
  switch (provider) {
1148
1334
  case "github":
1149
- execFileSync6("gh", ["pr", "edit", branchName, "--body", body], {
1335
+ execFileSync7("gh", ["pr", "edit", branchName, "--body", body], {
1150
1336
  cwd: worktreePath,
1151
1337
  stdio: "pipe"
1152
1338
  });
1153
1339
  break;
1154
1340
  case "gitlab":
1155
- execFileSync6("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1341
+ execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
1156
1342
  break;
1157
1343
  }
1158
- log.info(TAG11, `Updated existing PR body for ${branchName}`);
1344
+ log.info(TAG13, `Updated existing PR body for ${branchName}`);
1159
1345
  } catch (err) {
1160
- log.warn(TAG11, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1346
+ log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
1161
1347
  }
1162
1348
  }
1163
- var cachedExecFileAsync, TAG11 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE;
1349
+ var cachedExecFileAsync2, TAG13 = "git-pr", VALID_PR_URL_RE, PR_URL_RE, REVIEWED_SHA_RE, CI_REVIEW_REQUESTED_SHA_RE;
1164
1350
  var init_git_pr = __esm(() => {
1165
1351
  init_dist();
1166
1352
  init_log();
1167
1353
  VALID_PR_URL_RE = /^https:\/\/(github\.com|gitlab\.com|dev\.azure\.com|bitbucket\.org)\//;
1168
1354
  PR_URL_RE = /PR:\s*(https?:\/\/[^\s)]+)/;
1169
1355
  REVIEWED_SHA_RE = /^Reviewed-SHA:\s*([0-9a-f]{7,40})\s*$/im;
1356
+ CI_REVIEW_REQUESTED_SHA_RE = /^CI-Review-Requested-SHA:\s*([0-9a-f]{7,40})\s*$/im;
1170
1357
  });
1171
1358
 
1172
1359
  // src/artifact-judge.ts
@@ -1217,6 +1404,8 @@ var AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth to
1217
1404
  var OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
1218
1405
  var USAGE_LIMIT = /usage limit|daily limit|monthly limit|quota (?:exceeded|reached)|reached your .{0,20}limit|usage_limit_reached|limit will reset/i;
1219
1406
  var RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
1407
+ var SPEND_LIMIT = /spend(?:ing)? (?:limit|cap)|monthly spend/i;
1408
+ var SPEND_LIMIT_REMEDY = /admin-settings\/usage/i;
1220
1409
  function parseRetryAfterMs(message) {
1221
1410
  const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
1222
1411
  if (!match)
@@ -1232,12 +1421,16 @@ function classifyRunError(message) {
1232
1421
  const retryAfterMs = parseRetryAfterMs(message);
1233
1422
  if (AUTH.test(message))
1234
1423
  return { kind: "auth", retryAfterMs };
1424
+ if (SPEND_LIMIT.test(message))
1425
+ return { kind: "spend_limit", retryAfterMs };
1235
1426
  if (OUT_OF_CREDITS.test(message))
1236
1427
  return { kind: "out_of_credits", retryAfterMs };
1237
1428
  if (USAGE_LIMIT.test(message))
1238
1429
  return { kind: "usage_limit", retryAfterMs };
1239
1430
  if (RATE_LIMIT.test(message))
1240
1431
  return { kind: "rate_limit", retryAfterMs };
1432
+ if (SPEND_LIMIT_REMEDY.test(message))
1433
+ return { kind: "spend_limit", retryAfterMs };
1241
1434
  return { kind: null };
1242
1435
  }
1243
1436
  function describeApiError(kind) {
@@ -1250,6 +1443,8 @@ function describeApiError(kind) {
1250
1443
  return "Anthropic usage limit reached — retrying after reset";
1251
1444
  case "rate_limit":
1252
1445
  return "Anthropic rate limit hit — retrying shortly";
1446
+ case "spend_limit":
1447
+ return "Account spend limit reached — raise it at claude.ai/admin-settings/usage, then run `harmony-agent resume`";
1253
1448
  }
1254
1449
  }
1255
1450
  function cooldownMsFor(kind) {
@@ -1262,6 +1457,8 @@ function cooldownMsFor(kind) {
1262
1457
  return 30 * 60000;
1263
1458
  case "auth":
1264
1459
  return 30 * 60000;
1460
+ case "spend_limit":
1461
+ return 30 * 60000;
1265
1462
  }
1266
1463
  }
1267
1464
 
@@ -1615,7 +1812,7 @@ ${this.capturedStderr}`);
1615
1812
  kind: "error",
1616
1813
  source: "system",
1617
1814
  payload: {
1618
- message: `result ${r.subtype}: ${joined || "(no detail)"}`,
1815
+ message: resultErrorMessage(r.subtype, joined),
1619
1816
  errorKind: cls.kind,
1620
1817
  retryable: cls.kind !== "auth" && cls.kind !== null
1621
1818
  }
@@ -1626,6 +1823,13 @@ ${this.capturedStderr}`);
1626
1823
  }
1627
1824
  }
1628
1825
  }
1826
+ function resultErrorMessage(subtype, detail) {
1827
+ return `result ${subtype}: ${detail || "(no detail)"}`;
1828
+ }
1829
+ function resultErrorSubtype(message) {
1830
+ const match = message.match(/^result ([A-Za-z0-9_]+):/);
1831
+ return match ? match[1] : null;
1832
+ }
1629
1833
  function normalize(raw) {
1630
1834
  if (raw == null)
1631
1835
  return;
@@ -1845,6 +2049,168 @@ class ArtifactCollector {
1845
2049
  };
1846
2050
  }
1847
2051
  }
2052
+ // src/ci-failure.ts
2053
+ init_log();
2054
+ import { execFile, execFileSync } from "node:child_process";
2055
+ import { promisify } from "node:util";
2056
+ function createExecFileAsync() {
2057
+ return promisify(execFile);
2058
+ }
2059
+ var cachedExecFileAsync;
2060
+ function execFileAsync() {
2061
+ return cachedExecFileAsync ??= createExecFileAsync();
2062
+ }
2063
+ var TAG3 = "ci-failure";
2064
+ var LOG_TAIL_BYTES = 16000;
2065
+ var MAX_CHECK_NAME = 200;
2066
+ function sanitizeCiText(text, maxLength) {
2067
+ return text.replace(/`/g, "'").replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, " ").slice(0, maxLength);
2068
+ }
2069
+ function githubSlug(remoteOrPrUrl) {
2070
+ const m = remoteOrPrUrl.match(/github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?(?:\/|$)/i);
2071
+ return m ? `${m[1].toLowerCase()}/${m[2].toLowerCase()}` : null;
2072
+ }
2073
+ var STRICT_PR_URL_RE = /^https:\/\/github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/pull\/\d+$/;
2074
+ function isPrOnOrigin(prUrl, cwd) {
2075
+ if (!STRICT_PR_URL_RE.test(prUrl))
2076
+ return false;
2077
+ const prSlug = githubSlug(prUrl);
2078
+ if (!prSlug)
2079
+ return false;
2080
+ try {
2081
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
2082
+ cwd,
2083
+ encoding: "utf-8",
2084
+ stdio: ["ignore", "pipe", "pipe"]
2085
+ }).trim();
2086
+ return githubSlug(remote) === prSlug;
2087
+ } catch {
2088
+ return false;
2089
+ }
2090
+ }
2091
+ var RUN_ID_RE = /\/actions\/runs\/(\d+)/;
2092
+ function extractWorkflowRunId(detailsUrl) {
2093
+ if (!detailsUrl)
2094
+ return null;
2095
+ return detailsUrl.match(RUN_ID_RE)?.[1] ?? null;
2096
+ }
2097
+ function parseFailedChecks(rollup) {
2098
+ if (!Array.isArray(rollup))
2099
+ return [];
2100
+ const failed = [];
2101
+ for (const check of rollup) {
2102
+ if (typeof check !== "object" || check === null)
2103
+ continue;
2104
+ const c = check;
2105
+ const name = sanitizeCiText(typeof c.name === "string" && c.name || typeof c.context === "string" && c.context || "unnamed check", MAX_CHECK_NAME);
2106
+ const detailsUrl = typeof c.detailsUrl === "string" && c.detailsUrl || typeof c.targetUrl === "string" && c.targetUrl || null;
2107
+ if (typeof c.status === "string") {
2108
+ if (c.status.toUpperCase() !== "COMPLETED")
2109
+ continue;
2110
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toUpperCase() : "";
2111
+ if (["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion))
2112
+ continue;
2113
+ failed.push({
2114
+ name,
2115
+ conclusion: conclusion || "FAILURE",
2116
+ detailsUrl,
2117
+ workflowRunId: extractWorkflowRunId(detailsUrl)
2118
+ });
2119
+ continue;
2120
+ }
2121
+ if (typeof c.state === "string") {
2122
+ const state = c.state.toUpperCase();
2123
+ if (state === "SUCCESS" || state === "PENDING")
2124
+ continue;
2125
+ failed.push({
2126
+ name,
2127
+ conclusion: state,
2128
+ detailsUrl,
2129
+ workflowRunId: extractWorkflowRunId(detailsUrl)
2130
+ });
2131
+ }
2132
+ }
2133
+ return failed;
2134
+ }
2135
+ var INFRA_SIGNATURES = [
2136
+ { signal: "esm.sh 5xx", pattern: /esm\.sh[^\n]{0,200}?\b5\d\d\b/i },
2137
+ {
2138
+ signal: "setup-bun unavailable",
2139
+ pattern: /setup-bun[^\n]{0,200}?\b(?:503|502|504)\b/i
2140
+ },
2141
+ {
2142
+ signal: "runner setup failed",
2143
+ pattern: /(?:##\[error\])?Set up job[^\n]{0,120}?(?:fail|error)/i
2144
+ },
2145
+ {
2146
+ signal: "runner network",
2147
+ 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
2148
+ },
2149
+ {
2150
+ signal: "actions infrastructure",
2151
+ 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
2152
+ },
2153
+ {
2154
+ signal: "registry 5xx",
2155
+ pattern: /(?:npm|bun|yarn|pnpm)[^\n]{0,200}?\b(?:502 Bad Gateway|503 Service Unavailable|504 Gateway Time-?out)\b/i
2156
+ }
2157
+ ];
2158
+ function classifyCiFailure(input) {
2159
+ const haystack = `${input.checkName}
2160
+ ${input.log}`;
2161
+ for (const { signal, pattern } of INFRA_SIGNATURES) {
2162
+ if (pattern.test(haystack))
2163
+ return { kind: "infra", signal };
2164
+ }
2165
+ return { kind: "code", signal: null };
2166
+ }
2167
+ function describeCiFailure(failure) {
2168
+ return failure.kind === "infra" ? `infrastructure failure (${failure.signal}) — re-dispatching, no code change` : "a real build or test failure — repairing the branch";
2169
+ }
2170
+ function classifyCiFailures(failures) {
2171
+ if (failures.length === 0)
2172
+ return { kind: "code", signal: null };
2173
+ const infra = failures.filter((f) => f.kind === "infra");
2174
+ if (infra.length !== failures.length)
2175
+ return { kind: "code", signal: null };
2176
+ const signals = [...new Set(infra.map((f) => f.signal).filter(Boolean))];
2177
+ return { kind: "infra", signal: signals.join(", ") || null };
2178
+ }
2179
+ async function getPrFailedChecks(prUrl, cwd, provider) {
2180
+ if (provider !== "github")
2181
+ return [];
2182
+ try {
2183
+ const { stdout } = await execFileAsync()("gh", ["pr", "view", prUrl, "--json", "statusCheckRollup"], { cwd, encoding: "utf-8", timeout: 15000 });
2184
+ const parsed = JSON.parse(stdout.trim());
2185
+ return parseFailedChecks(parsed.statusCheckRollup);
2186
+ } catch (err) {
2187
+ log.warn(TAG3, `could not read failing checks for ${prUrl}: ${err instanceof Error ? err.message : err}`);
2188
+ return [];
2189
+ }
2190
+ }
2191
+ async function getFailedRunLog(runId, cwd) {
2192
+ try {
2193
+ const { stdout } = await execFileAsync()("gh", ["run", "view", runId, "--log-failed"], { cwd, encoding: "utf-8", timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
2194
+ return sanitizeCiText(stdout.slice(-LOG_TAIL_BYTES), LOG_TAIL_BYTES);
2195
+ } catch (err) {
2196
+ log.debug(TAG3, `could not read failed-step log for run ${runId}: ${err instanceof Error ? err.message : err}`);
2197
+ return "";
2198
+ }
2199
+ }
2200
+ async function rerunFailedJobs(runId, cwd) {
2201
+ try {
2202
+ await execFileAsync()("gh", ["run", "rerun", runId, "--failed"], {
2203
+ cwd,
2204
+ encoding: "utf-8",
2205
+ timeout: 30000
2206
+ });
2207
+ log.info(TAG3, `re-dispatched failed jobs of run ${runId}`);
2208
+ return true;
2209
+ } catch (err) {
2210
+ log.warn(TAG3, `could not re-dispatch run ${runId}: ${err instanceof Error ? err.message : err}`);
2211
+ return false;
2212
+ }
2213
+ }
1848
2214
  // src/command-metric.ts
1849
2215
  init_dist();
1850
2216
 
@@ -1856,7 +2222,7 @@ init_dist();
1856
2222
 
1857
2223
  // src/command-metric.ts
1858
2224
  init_log();
1859
- var TAG3 = "command-metric";
2225
+ var TAG4 = "command-metric";
1860
2226
  var MAX_RAW_CHARS = 2000;
1861
2227
  var MAX_OUTPUT_BUFFER = 10 * 1024 * 1024;
1862
2228
  var MAX_STDERR_CHARS = 64 * 1024;
@@ -2036,7 +2402,7 @@ class CommandMetricCollector {
2036
2402
  const requested = typeof def.timeoutMs === "number" && def.timeoutMs > 0 ? Math.floor(def.timeoutMs) : DEFAULT_METRIC_TIMEOUT_MS;
2037
2403
  const timeoutMs = Math.min(requested, MAX_METRIC_TIMEOUT_MS);
2038
2404
  if (timeoutMs < requested) {
2039
- log.warn(TAG3, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
2405
+ log.warn(TAG4, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
2040
2406
  }
2041
2407
  const run = this.deps.runCommand ?? runMetricCommand;
2042
2408
  let stdout;
@@ -2049,15 +2415,15 @@ class CommandMetricCollector {
2049
2415
  });
2050
2416
  } catch (err) {
2051
2417
  const reason = describeRunFailure(err, timeoutMs);
2052
- log.warn(TAG3, `Metric "${name}" did not produce a measurement: ${reason}`);
2418
+ log.warn(TAG4, `Metric "${name}" did not produce a measurement: ${reason}`);
2053
2419
  return blocked(name, reason);
2054
2420
  }
2055
2421
  const parsed = parseMetricValue(mode, stdout);
2056
2422
  if (!parsed.ok) {
2057
- log.warn(TAG3, `Metric "${name}" output unusable: ${parsed.reason}`);
2423
+ log.warn(TAG4, `Metric "${name}" output unusable: ${parsed.reason}`);
2058
2424
  return blocked(name, `Metric "${name}": ${parsed.reason}.`, stdout);
2059
2425
  }
2060
- log.info(TAG3, `Metric "${name}" measured: ${JSON.stringify(parsed.value)}`);
2426
+ log.info(TAG4, `Metric "${name}" measured: ${JSON.stringify(parsed.value)}`);
2061
2427
  return {
2062
2428
  result: "passed",
2063
2429
  structured: {
@@ -2105,147 +2471,587 @@ function truncate(value, max) {
2105
2471
  return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
2106
2472
  }
2107
2473
  // src/confine-to-repo.ts
2108
- import { isAbsolute, resolve, sep } from "node:path";
2109
- var PATH_ARG_BY_TOOL = {
2474
+ import { realpathSync } from "node:fs";
2475
+ import { dirname, isAbsolute, parse, resolve, sep } from "node:path";
2476
+ var READ_PATH_ARGS = {
2110
2477
  Read: ["file_path", "path", "notebook_path"],
2111
2478
  Grep: ["path"],
2112
2479
  Glob: ["path"]
2113
2480
  };
2481
+ var WRITE_PATH_ARGS = {
2482
+ Write: ["file_path"],
2483
+ Edit: ["file_path"],
2484
+ MultiEdit: ["file_path"],
2485
+ NotebookEdit: ["notebook_path"]
2486
+ };
2487
+ var CONFINED_READ_TOOLS = Object.freeze(Object.keys(READ_PATH_ARGS));
2488
+ var CONFINED_WRITE_TOOLS = Object.freeze([
2489
+ ...Object.keys(READ_PATH_ARGS),
2490
+ ...Object.keys(WRITE_PATH_ARGS).filter((t) => t !== "MultiEdit")
2491
+ ]);
2492
+ var PATTERN_ARG_BY_TOOL = {
2493
+ Glob: ["pattern"],
2494
+ Grep: ["glob"]
2495
+ };
2496
+ function isGitMetadata(repoRoot, candidate) {
2497
+ const rel = candidate.startsWith(repoRoot) ? candidate.slice(repoRoot.length) : candidate;
2498
+ return rel.split(/[\\/]/).some((segment) => segment.toLowerCase() === ".git");
2499
+ }
2500
+ function patternEscapes(pattern) {
2501
+ if (isAbsolute(pattern))
2502
+ return true;
2503
+ if (/[{}[\]~()!+@]/.test(pattern))
2504
+ return true;
2505
+ return pattern.split(/[\\/]/).some((segment) => segment === "..");
2506
+ }
2507
+ function pathArgsFor(mode) {
2508
+ return mode === "write" ? { ...READ_PATH_ARGS, ...WRITE_PATH_ARGS } : READ_PATH_ARGS;
2509
+ }
2510
+ function realPathOrNearest(p) {
2511
+ const abs = isAbsolute(p) ? p : resolve(p);
2512
+ const { root } = parse(abs);
2513
+ let real = root;
2514
+ for (const part of abs.slice(root.length).split(sep)) {
2515
+ if (part === "" || part === ".")
2516
+ continue;
2517
+ if (part === "..") {
2518
+ real = dirname(real);
2519
+ continue;
2520
+ }
2521
+ const next = real.endsWith(sep) ? real + part : real + sep + part;
2522
+ try {
2523
+ real = realpathSync(next);
2524
+ } catch {
2525
+ real = next;
2526
+ }
2527
+ }
2528
+ return real;
2529
+ }
2114
2530
  function isInsideTree(root, target) {
2115
- const normalizedRoot = resolve(root);
2116
- const normalizedTarget = resolve(target);
2531
+ const normalizedRoot = realPathOrNearest(root);
2532
+ const normalizedTarget = realPathOrNearest(target);
2117
2533
  if (normalizedTarget === normalizedRoot)
2118
2534
  return true;
2119
2535
  return normalizedTarget.startsWith(normalizedRoot + sep);
2120
2536
  }
2121
- function decideConfinedTool(repoRoot, toolName, input) {
2122
- const pathArgs = PATH_ARG_BY_TOOL[toolName];
2537
+ function decideConfinedTool(repoRoot, toolName, input, mode = "read") {
2538
+ const pathArgs = pathArgsFor(mode)[toolName];
2123
2539
  if (!pathArgs) {
2124
2540
  return {
2125
2541
  behavior: "deny",
2126
2542
  message: `${toolName} is not available to this run.`
2127
2543
  };
2128
2544
  }
2545
+ const verb = toolName in WRITE_PATH_ARGS ? "write" : "read";
2546
+ for (const key of PATTERN_ARG_BY_TOOL[toolName] ?? []) {
2547
+ const value = input[key];
2548
+ if (typeof value !== "string" || value.length === 0)
2549
+ continue;
2550
+ if (patternEscapes(value)) {
2551
+ return {
2552
+ behavior: "deny",
2553
+ message: `${toolName} patterns must stay inside the repository — no absolute path and no "..". Refused: ${value}`
2554
+ };
2555
+ }
2556
+ }
2129
2557
  for (const key of pathArgs) {
2130
2558
  const value = input[key];
2131
2559
  if (typeof value !== "string" || value.length === 0)
2132
2560
  continue;
2133
- const candidate = isAbsolute(value) ? value : resolve(repoRoot, value);
2561
+ const candidate = isAbsolute(value) ? value : `${repoRoot}${sep}${value}`;
2562
+ if (isGitMetadata(repoRoot, candidate)) {
2563
+ return {
2564
+ behavior: "deny",
2565
+ message: `${toolName} may not touch the repository's git metadata. Refused: ${value}`
2566
+ };
2567
+ }
2134
2568
  if (!isInsideTree(repoRoot, candidate)) {
2135
2569
  return {
2136
2570
  behavior: "deny",
2137
- message: `${toolName} may only read inside the repository. Refused: ${value}`
2571
+ message: `${toolName} may only ${verb} inside the repository. Refused: ${value}`
2138
2572
  };
2139
2573
  }
2140
2574
  }
2141
2575
  return { behavior: "allow" };
2142
2576
  }
2143
- function confineToRepo(repoRoot) {
2144
- return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input);
2577
+ function confineToRepo(repoRoot, mode = "read") {
2578
+ return async (toolName, input) => decideConfinedTool(repoRoot, toolName, input, mode);
2145
2579
  }
2146
2580
  // src/gate-collectors.ts
2147
2581
  init_dist();
2148
2582
  init_log();
2149
2583
 
2150
2584
  // src/oracle-collector.ts
2151
- init_log();
2152
2585
  import { createHash } from "node:crypto";
2153
- var TAG4 = "oracle-collector";
2586
+ init_log();
2154
2587
 
2155
- class OracleCollector {
2156
- deps;
2157
- kind = "oracle_passed";
2158
- constructor(deps) {
2159
- this.deps = deps;
2588
+ // src/oracle.ts
2589
+ import { lstatSync, readFileSync, statSync } from "node:fs";
2590
+ import {
2591
+ chmod,
2592
+ lstat,
2593
+ mkdir,
2594
+ mkdtemp,
2595
+ realpath,
2596
+ rm,
2597
+ writeFile
2598
+ } from "node:fs/promises";
2599
+ import { tmpdir } from "node:os";
2600
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join, resolve as resolve2, sep as sep2 } from "node:path";
2601
+ import { StringDecoder } from "node:string_decoder";
2602
+ init_log();
2603
+ var TAG5 = "oracle";
2604
+ async function resolveContained(repoPath, relativePath) {
2605
+ if (isAbsolute2(relativePath)) {
2606
+ throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
2160
2607
  }
2161
- async collect(context) {
2162
- const oracle = await this.deps.fetchOracle(context.cardId, context.stageId, this.deps.sessionId);
2163
- if (!oracle) {
2164
- log.info(TAG4, `No oracle held for stage ${context.stageId} — blocked`);
2165
- return {
2166
- result: "blocked",
2167
- structured: {
2168
- reason: `No oracle is held for stage ${context.stageId}.`
2169
- }
2170
- };
2171
- }
2172
- return await this.runHeld(oracle);
2608
+ if (relativePath === "" || relativePath === ".") {
2609
+ throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
2173
2610
  }
2174
- async runHeld(oracle) {
2175
- const identity = {
2176
- oracleId: oracle.id ?? null,
2177
- contentHash: createHash("sha256").update(oracle.content).digest("hex")
2178
- };
2179
- await this.deps.place(this.deps.repoPath, oracle);
2180
- try {
2181
- const { exitCode, output } = await this.deps.run(this.deps.repoPath, oracle);
2182
- const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
2183
- ${output}`;
2184
- if (exitCode === 0) {
2185
- log.info(TAG4, logLine);
2186
- } else {
2187
- log.warn(TAG4, logLine);
2188
- }
2189
- return {
2190
- result: exitCode === 0 ? "passed" : "failed",
2191
- structured: {
2192
- oracle: {
2193
- exitCode,
2194
- path: oracle.path,
2195
- ...identity,
2196
- output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
2197
- }
2198
- }
2199
- };
2200
- } catch (err) {
2201
- const message = errText(err);
2202
- log.warn(TAG4, `Oracle run threw: ${message} — blocked`);
2203
- return {
2204
- result: "blocked",
2205
- structured: {
2206
- oracle: { path: oracle.path, ...identity },
2207
- error: message
2208
- }
2209
- };
2210
- } finally {
2211
- await this.removeBestEffort(oracle);
2212
- }
2611
+ const root = await realpath(repoPath);
2612
+ const target = resolve2(root, relativePath);
2613
+ if (target !== root && !target.startsWith(root + sep2)) {
2614
+ throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
2213
2615
  }
2214
- async removeBestEffort(oracle) {
2215
- try {
2216
- await this.deps.remove(this.deps.repoPath, oracle);
2217
- return;
2218
- } catch (err) {
2219
- log.warn(TAG4, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
2220
- }
2221
- try {
2222
- await this.deps.remove(this.deps.repoPath, oracle);
2223
- log.info(TAG4, `Held test at ${oracle.path} removed on the second attempt`);
2224
- } catch (err) {
2225
- 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.");
2616
+ let cursor = root;
2617
+ for (const segment of relativePath.split("/")) {
2618
+ cursor = resolve2(cursor, segment);
2619
+ const stat = await lstat(cursor).catch(() => null);
2620
+ if (stat?.isSymbolicLink()) {
2621
+ throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
2226
2622
  }
2227
2623
  }
2624
+ return target;
2228
2625
  }
2229
- function errText(err) {
2230
- return err instanceof Error ? err.message : String(err);
2231
- }
2232
-
2626
+ async function place(repoPath, oracle) {
2627
+ const target = await resolveContained(repoPath, oracle.path);
2628
+ await mkdir(dirname2(target), { recursive: true });
2629
+ await writeFile(target, oracle.content, "utf8");
2630
+ }
2631
+ async function remove(repoPath, oracle) {
2632
+ const target = await resolveContained(repoPath, oracle.path);
2633
+ await rm(target, { force: true });
2634
+ }
2635
+ var ORACLE_RUNNERS = {
2636
+ vitest: {
2637
+ argv: (path) => ({
2638
+ command: "npx",
2639
+ args: ["--no-install", "vitest", "run", path]
2640
+ }),
2641
+ verdictStream: "stdout",
2642
+ report: {
2643
+ file: "report.json",
2644
+ flags: (reportPath) => [
2645
+ "--reporter=default",
2646
+ "--reporter=json",
2647
+ `--outputFile=${reportPath}`
2648
+ ],
2649
+ parse: (content) => {
2650
+ const parsed = JSON.parse(content);
2651
+ if (typeof parsed !== "object" || parsed === null)
2652
+ return null;
2653
+ const { numTotalTests, numFailedTests } = parsed;
2654
+ if (typeof numTotalTests !== "number" || typeof numFailedTests !== "number" || !Number.isFinite(numTotalTests) || !Number.isFinite(numFailedTests)) {
2655
+ return null;
2656
+ }
2657
+ return { total: numTotalTests, failed: numFailedTests };
2658
+ }
2659
+ }
2660
+ },
2661
+ bun: {
2662
+ argv: (path) => ({ command: "bun", args: ["test", path] }),
2663
+ verdictStream: "stderr",
2664
+ report: {
2665
+ file: "report.xml",
2666
+ flags: (reportPath) => [
2667
+ "--reporter=junit",
2668
+ `--reporter-outfile=${reportPath}`
2669
+ ],
2670
+ parse: (content) => {
2671
+ const root = /<testsuites\b[^>]*>/.exec(content);
2672
+ if (!root)
2673
+ return null;
2674
+ const total = /\btests="(\d+)"/.exec(root[0]);
2675
+ const failed = /\bfailures="(\d+)"/.exec(root[0]);
2676
+ if (!total || !failed)
2677
+ return null;
2678
+ return { total: Number(total[1]), failed: Number(failed[1]) };
2679
+ }
2680
+ }
2681
+ }
2682
+ };
2683
+ var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
2684
+ var ORACLE_OUTPUT_LIMIT = 64 * 1024;
2685
+ var ORACLE_SIGINT_GRACE_MS = 2000;
2686
+ var ORACLE_SIGTERM_GRACE_MS = 3000;
2687
+ var ORACLE_DRAIN_GRACE_MS = 500;
2688
+ function resolveOracleRunner(oracle) {
2689
+ return resolveOracleRunnerSpec(oracle).argv(argvPath(oracle.path));
2690
+ }
2691
+ function resolveOracleRunnerSpec(oracle) {
2692
+ const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
2693
+ const spec = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
2694
+ if (!spec) {
2695
+ 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(", ")})`);
2696
+ }
2697
+ return spec;
2698
+ }
2699
+ function summarizeOracleReport(oracle, content) {
2700
+ const spec = resolveOracleRunnerSpec(oracle);
2701
+ try {
2702
+ return spec.report.parse(content) ?? null;
2703
+ } catch {
2704
+ return null;
2705
+ }
2706
+ }
2707
+ function gradeOracleRed(summary, exitCode) {
2708
+ if (!summary) {
2709
+ return {
2710
+ outcome: "no_verdict",
2711
+ 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"
2712
+ };
2713
+ }
2714
+ if (summary.total === 0) {
2715
+ return {
2716
+ outcome: "no_verdict",
2717
+ reason: `the runner started but executed no tests (exit ${exitCode}), so the held test produced no verdict`
2718
+ };
2719
+ }
2720
+ if (summary.failed > 0) {
2721
+ return { outcome: "reproduced", summary };
2722
+ }
2723
+ if (exitCode === 0) {
2724
+ return { outcome: "not_reproduced", summary };
2725
+ }
2726
+ return {
2727
+ outcome: "no_verdict",
2728
+ reason: `all ${summary.total} test(s) passed but the runner exited ${exitCode}, so the failure is not the held test's`
2729
+ };
2730
+ }
2731
+ function argvPath(path) {
2732
+ return path.startsWith("./") ? path : `./${path}`;
2733
+ }
2734
+ async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
2735
+ const spec = resolveOracleRunnerSpec(oracle);
2736
+ const reportDir = await mkdtemp(join(tmpdir(), reportDirPrefix()));
2737
+ const reportPath = join(reportDir, spec.report.file);
2738
+ try {
2739
+ return await spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs);
2740
+ } finally {
2741
+ await removeReportDir(reportDir);
2742
+ }
2743
+ }
2744
+ function reportDirPrefix() {
2745
+ return "harmony-oracle-report-";
2746
+ }
2747
+ function assertUntampered(reportPath) {
2748
+ const dir = statSync(dirname2(reportPath));
2749
+ if ((dir.mode & 511) !== 448) {
2750
+ throw new Error(`the oracle report directory's mode changed to ${(dir.mode & 511).toString(8)} — refusing the report`);
2751
+ }
2752
+ const file = lstatSync(reportPath);
2753
+ if (!file.isFile()) {
2754
+ throw new Error("the oracle report is not a regular file — refusing it");
2755
+ }
2756
+ if ((file.mode & 128) === 0) {
2757
+ 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`);
2758
+ }
2759
+ }
2760
+ async function removeReportDir(reportDir) {
2761
+ try {
2762
+ await chmod(reportDir, 448).catch(() => {});
2763
+ await rm(reportDir, { recursive: true, force: true });
2764
+ } catch (err) {
2765
+ 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.`);
2766
+ }
2767
+ }
2768
+ async function spawnHeldOracle(spec, repoPath, oracle, reportPath, timeoutMs) {
2769
+ const { command, args: baseArgs } = spec.argv(argvPath(oracle.path));
2770
+ const args = [...baseArgs, ...spec.report.flags(reportPath)];
2771
+ return await new Promise((settleOk, settleErr) => {
2772
+ let child;
2773
+ try {
2774
+ child = spawnInGroup(command, args, {
2775
+ cwd: repoPath,
2776
+ stdio: ["ignore", "pipe", "pipe"]
2777
+ });
2778
+ } catch (err) {
2779
+ settleErr(err);
2780
+ return;
2781
+ }
2782
+ const pgid = child.pid;
2783
+ let output = "";
2784
+ let truncated = false;
2785
+ const outDecoder = new StringDecoder("utf8");
2786
+ const errDecoder = new StringDecoder("utf8");
2787
+ let verdict = "";
2788
+ let flushed = false;
2789
+ const flush = () => {
2790
+ if (flushed)
2791
+ return;
2792
+ flushed = true;
2793
+ const outTail = outDecoder.end();
2794
+ const errTail = errDecoder.end();
2795
+ output += outTail + errTail;
2796
+ verdict += spec.verdictStream === "stdout" ? outTail : errTail;
2797
+ };
2798
+ const finalOutput = () => {
2799
+ flush();
2800
+ return truncated ? `… earlier output dropped; kept the last ${ORACLE_OUTPUT_LIMIT} characters
2801
+ ${output}` : output;
2802
+ };
2803
+ const finalVerdict = () => {
2804
+ flush();
2805
+ return verdict;
2806
+ };
2807
+ let report = null;
2808
+ const captureReport = () => {
2809
+ try {
2810
+ assertUntampered(reportPath);
2811
+ report = spec.report.parse(readFileSync(reportPath, "utf8"));
2812
+ } catch (err) {
2813
+ report = null;
2814
+ const message = err instanceof Error ? err.message : String(err);
2815
+ const absent = err instanceof Error && err.code === "ENOENT";
2816
+ if (absent) {
2817
+ log.info(TAG5, `No oracle report at ${reportPath} — no verdict.`);
2818
+ } else {
2819
+ log.warn(TAG5, `Refusing the oracle report at ${reportPath}: ${message} — the gate will report no verdict.`);
2820
+ }
2821
+ }
2822
+ };
2823
+ let settled = false;
2824
+ let killing = false;
2825
+ let timer;
2826
+ let drainTimer;
2827
+ const settle = (failure, result) => {
2828
+ if (settled)
2829
+ return;
2830
+ settled = true;
2831
+ if (timer)
2832
+ clearTimeout(timer);
2833
+ if (drainTimer)
2834
+ clearTimeout(drainTimer);
2835
+ reapGroup(pgid);
2836
+ if (failure)
2837
+ settleErr(failure);
2838
+ else
2839
+ settleOk(result);
2840
+ };
2841
+ const append = (stream, chunk) => {
2842
+ const text = (stream === "stdout" ? outDecoder : errDecoder).write(chunk);
2843
+ output += text;
2844
+ if (output.length > ORACLE_OUTPUT_LIMIT) {
2845
+ output = output.slice(-ORACLE_OUTPUT_LIMIT);
2846
+ truncated = true;
2847
+ }
2848
+ if (stream === spec.verdictStream) {
2849
+ verdict += text;
2850
+ if (verdict.length > ORACLE_OUTPUT_LIMIT) {
2851
+ verdict = verdict.slice(-ORACLE_OUTPUT_LIMIT);
2852
+ }
2853
+ }
2854
+ };
2855
+ child.stdout?.on("data", (chunk) => append("stdout", chunk));
2856
+ child.stderr?.on("data", (chunk) => append("stderr", chunk));
2857
+ child.once("error", (err) => settle(err));
2858
+ const settleFromExit = (code, signal) => {
2859
+ if (drainTimer)
2860
+ clearTimeout(drainTimer);
2861
+ if (code === null) {
2862
+ settle(new Error(`the held test was terminated by signal ${signal}`));
2863
+ return;
2864
+ }
2865
+ settle(null, {
2866
+ exitCode: code,
2867
+ output: finalOutput(),
2868
+ verdict: finalVerdict(),
2869
+ report
2870
+ });
2871
+ };
2872
+ child.once("exit", (code, signal) => {
2873
+ if (killing)
2874
+ return;
2875
+ captureReport();
2876
+ if (timer)
2877
+ clearTimeout(timer);
2878
+ reapGroup(pgid);
2879
+ drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
2880
+ child.once("close", () => settleFromExit(code, signal));
2881
+ });
2882
+ timer = setTimeout(() => {
2883
+ if (settled)
2884
+ return;
2885
+ killing = true;
2886
+ terminateGroup(child, {
2887
+ sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
2888
+ sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
2889
+ }).catch(() => {}).then(() => {
2890
+ settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
2891
+ });
2892
+ }, timeoutMs);
2893
+ });
2894
+ }
2895
+
2896
+ // src/oracle-collector.ts
2897
+ var TAG6 = "oracle-collector";
2898
+
2899
+ class HeldOracleCollector {
2900
+ deps;
2901
+ constructor(deps) {
2902
+ this.deps = deps;
2903
+ }
2904
+ async collect(context) {
2905
+ const stageId = this.oracleStageId(context);
2906
+ if (!stageId) {
2907
+ 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.";
2908
+ log.warn(TAG6, `${reason} — blocked (config)`);
2909
+ return {
2910
+ result: "blocked",
2911
+ structured: { reason, ...GATE_CONFIG_ERROR_MARK }
2912
+ };
2913
+ }
2914
+ const oracle = await this.deps.fetchOracle(context.cardId, stageId, this.deps.sessionId);
2915
+ if (!oracle) {
2916
+ log.info(TAG6, `No oracle held for stage ${stageId} — blocked`);
2917
+ return {
2918
+ result: "blocked",
2919
+ structured: {
2920
+ reason: `No oracle is held for stage ${stageId}.`
2921
+ }
2922
+ };
2923
+ }
2924
+ return await this.runHeld(oracle);
2925
+ }
2926
+ async runHeld(oracle) {
2927
+ const identity = {
2928
+ oracleId: oracle.id ?? null,
2929
+ contentHash: createHash("sha256").update(oracle.content).digest("hex")
2930
+ };
2931
+ await this.deps.place(this.deps.repoPath, oracle);
2932
+ try {
2933
+ const { exitCode, output, report } = await this.deps.run(this.deps.repoPath, oracle);
2934
+ const logLine = `Oracle run for ${oracle.path} exited ${exitCode}:
2935
+ ${output}`;
2936
+ if (exitCode === 0) {
2937
+ log.info(TAG6, logLine);
2938
+ } else {
2939
+ log.warn(TAG6, logLine);
2940
+ }
2941
+ return this.verdict({
2942
+ oracle,
2943
+ identity,
2944
+ exitCode,
2945
+ output,
2946
+ report: report ?? null
2947
+ });
2948
+ } catch (err) {
2949
+ const message = errText(err);
2950
+ log.warn(TAG6, `Oracle run threw: ${message} — blocked`);
2951
+ return {
2952
+ result: "blocked",
2953
+ structured: {
2954
+ oracle: { path: oracle.path, ...identity },
2955
+ error: message
2956
+ }
2957
+ };
2958
+ } finally {
2959
+ await this.removeBestEffort(oracle);
2960
+ }
2961
+ }
2962
+ async removeBestEffort(oracle) {
2963
+ try {
2964
+ await this.deps.remove(this.deps.repoPath, oracle);
2965
+ return;
2966
+ } catch (err) {
2967
+ log.warn(TAG6, `Removing the held test at ${oracle.path} failed (${errText(err)}) — retrying once`);
2968
+ }
2969
+ try {
2970
+ await this.deps.remove(this.deps.repoPath, oracle);
2971
+ log.info(TAG6, `Held test at ${oracle.path} removed on the second attempt`);
2972
+ } catch (err) {
2973
+ 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.");
2974
+ }
2975
+ }
2976
+ }
2977
+
2978
+ class OracleCollector extends HeldOracleCollector {
2979
+ kind = "oracle_passed";
2980
+ oracleStageId(context) {
2981
+ return context.stageId;
2982
+ }
2983
+ verdict({
2984
+ oracle,
2985
+ identity,
2986
+ exitCode
2987
+ }) {
2988
+ return {
2989
+ result: exitCode === 0 ? "passed" : "failed",
2990
+ structured: {
2991
+ oracle: {
2992
+ exitCode,
2993
+ path: oracle.path,
2994
+ ...identity,
2995
+ output: "withheld — oracle_passed is a secrecy gate; see the motor's local log"
2996
+ }
2997
+ }
2998
+ };
2999
+ }
3000
+ }
3001
+
3002
+ class OracleRedCollector extends HeldOracleCollector {
3003
+ kind = "oracle_red";
3004
+ oracleStageId() {
3005
+ return this.deps.targetStageId ?? null;
3006
+ }
3007
+ verdict({
3008
+ oracle,
3009
+ identity,
3010
+ exitCode,
3011
+ report
3012
+ }) {
3013
+ const graded = gradeOracleRed(report, exitCode);
3014
+ const base = { exitCode, path: oracle.path, ...identity };
3015
+ if (graded.outcome === "no_verdict") {
3016
+ log.warn(TAG6, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
3017
+ return {
3018
+ result: "blocked",
3019
+ structured: { oracle: base, reason: graded.reason }
3020
+ };
3021
+ }
3022
+ return {
3023
+ result: graded.outcome === "reproduced" ? "passed" : "failed",
3024
+ structured: {
3025
+ oracle: {
3026
+ ...base,
3027
+ testsRun: graded.summary.total,
3028
+ testsFailed: graded.summary.failed,
3029
+ output: "withheld — oracle_red is a secrecy gate; see the motor's local log"
3030
+ }
3031
+ }
3032
+ };
3033
+ }
3034
+ }
3035
+ function errText(err) {
3036
+ return err instanceof Error ? err.message : String(err);
3037
+ }
3038
+
2233
3039
  // src/verification.ts
2234
3040
  init_log();
2235
- import { execFileSync as execFileSync4, spawn as spawn2 } from "node:child_process";
3041
+ import { execFileSync as execFileSync5, spawn as spawn2 } from "node:child_process";
2236
3042
 
2237
3043
  // src/pm.ts
2238
3044
  init_log();
2239
- import { execFileSync } from "node:child_process";
3045
+ import { execFileSync as execFileSync2 } from "node:child_process";
2240
3046
  import { existsSync } from "node:fs";
2241
- var TAG5 = "pm";
3047
+ var TAG7 = "pm";
2242
3048
  var cached = null;
2243
3049
  function detectPackageManager() {
2244
3050
  if (cached)
2245
3051
  return cached;
2246
3052
  let repoRoot;
2247
3053
  try {
2248
- repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
3054
+ repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
2249
3055
  encoding: "utf-8"
2250
3056
  }).trim();
2251
3057
  } catch {
@@ -2260,20 +3066,21 @@ function detectPackageManager() {
2260
3066
  } else {
2261
3067
  cached = "npm";
2262
3068
  }
2263
- log.info(TAG5, `Detected package manager: ${cached}`);
3069
+ log.info(TAG7, `Detected package manager: ${cached}`);
2264
3070
  return cached;
2265
3071
  }
2266
- function installCommand() {
3072
+ function installCommand(ignoreScripts = false) {
2267
3073
  const pm = detectPackageManager();
3074
+ const skip = ignoreScripts ? " --ignore-scripts" : "";
2268
3075
  switch (pm) {
2269
3076
  case "bun":
2270
- return "bun install --frozen-lockfile";
3077
+ return `bun install --frozen-lockfile${skip}`;
2271
3078
  case "pnpm":
2272
- return "pnpm install --frozen-lockfile";
3079
+ return `pnpm install --frozen-lockfile${skip}`;
2273
3080
  case "yarn":
2274
- return "yarn install --frozen-lockfile";
3081
+ return `yarn install --frozen-lockfile${skip}`;
2275
3082
  case "npm":
2276
- return "npm ci";
3083
+ return `npm ci${skip}`;
2277
3084
  }
2278
3085
  }
2279
3086
  function spawnRunArgs(script, ...extra) {
@@ -2286,9 +3093,9 @@ function spawnRunArgs(script, ...extra) {
2286
3093
 
2287
3094
  // src/project-type.ts
2288
3095
  init_log();
2289
- import { execFileSync as execFileSync2 } from "node:child_process";
2290
- import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
2291
- var TAG6 = "project-type";
3096
+ import { execFileSync as execFileSync3 } from "node:child_process";
3097
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
3098
+ var TAG8 = "project-type";
2292
3099
  var _cache = new Map;
2293
3100
  function _resetCache() {
2294
3101
  _cache.clear();
@@ -2299,7 +3106,7 @@ function detect(dir) {
2299
3106
  return cached2;
2300
3107
  const result = detectUncached(dir);
2301
3108
  _cache.set(dir, result);
2302
- log.info(TAG6, `Detected project type in ${dir}: ${result.kind}`);
3109
+ log.info(TAG8, `Detected project type in ${dir}: ${result.kind}`);
2303
3110
  return result;
2304
3111
  }
2305
3112
  function detectUncached(dir) {
@@ -2390,16 +3197,16 @@ var NPM_PLACEHOLDER_TEST = /no test specified/i;
2390
3197
  function hasNodeTestScript(dir) {
2391
3198
  let script;
2392
3199
  try {
2393
- const pkg = JSON.parse(readFileSync(`${dir}/package.json`, "utf-8"));
3200
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
2394
3201
  script = pkg.scripts?.test;
2395
3202
  } catch (err) {
2396
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
3203
+ log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
2397
3204
  return false;
2398
3205
  }
2399
3206
  if (typeof script !== "string" || script.trim().length === 0)
2400
3207
  return false;
2401
3208
  if (NPM_PLACEHOLDER_TEST.test(script)) {
2402
- log.info(TAG6, `package.json 'test' is the npm placeholder — skipping tests`);
3209
+ log.info(TAG8, `package.json 'test' is the npm placeholder — skipping tests`);
2403
3210
  return false;
2404
3211
  }
2405
3212
  return true;
@@ -2407,10 +3214,10 @@ function hasNodeTestScript(dir) {
2407
3214
  function firstNodeScript(dir, candidates) {
2408
3215
  let scripts;
2409
3216
  try {
2410
- const pkg = JSON.parse(readFileSync(`${dir}/package.json`, "utf-8"));
3217
+ const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
2411
3218
  scripts = pkg.scripts ?? {};
2412
3219
  } catch (err) {
2413
- log.warn(TAG6, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
3220
+ log.warn(TAG8, `Could not read package.json in ${dir}: ${err instanceof Error ? err.message : err}`);
2414
3221
  return null;
2415
3222
  }
2416
3223
  for (const name of candidates) {
@@ -2429,7 +3236,7 @@ function xcodeBuildCommand(pt) {
2429
3236
  return null;
2430
3237
  const scheme = resolveXcodeScheme(pt);
2431
3238
  if (!scheme) {
2432
- log.warn(TAG6, "Could not resolve an Xcode scheme — skipping build (best-effort)");
3239
+ log.warn(TAG8, "Could not resolve an Xcode scheme — skipping build (best-effort)");
2433
3240
  return null;
2434
3241
  }
2435
3242
  const containerFlag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
@@ -2452,20 +3259,20 @@ function resolveXcodeScheme(pt) {
2452
3259
  return null;
2453
3260
  const flag = pt.xcodeIsWorkspace ? "-workspace" : "-project";
2454
3261
  try {
2455
- const out = execFileSync2("xcodebuild", ["-list", "-json", flag, pt.xcodeContainer], { encoding: "utf-8", timeout: 30000, stdio: "pipe" });
3262
+ const out = execFileSync3("xcodebuild", ["-list", "-json", flag, pt.xcodeContainer], { encoding: "utf-8", timeout: 30000, stdio: "pipe" });
2456
3263
  const parsed = JSON.parse(out);
2457
3264
  const schemes = pt.xcodeIsWorkspace ? parsed.workspace?.schemes ?? [] : parsed.project?.schemes ?? [];
2458
3265
  return schemes[0] ?? null;
2459
3266
  } catch (err) {
2460
- log.warn(TAG6, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
3267
+ log.warn(TAG8, `xcodebuild -list failed: ${err instanceof Error ? err.message : err}`);
2461
3268
  return null;
2462
3269
  }
2463
3270
  }
2464
3271
 
2465
3272
  // src/revert-guard.ts
2466
3273
  init_log();
2467
- import { execFileSync as execFileSync3 } from "node:child_process";
2468
- var TAG7 = "revert-guard";
3274
+ import { execFileSync as execFileSync4 } from "node:child_process";
3275
+ var TAG9 = "revert-guard";
2469
3276
  var TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
2470
3277
  function isTestFile(path) {
2471
3278
  return TEST_FILE.test(path);
@@ -2475,21 +3282,21 @@ function filterTestFiles(paths) {
2475
3282
  }
2476
3283
  function refetchBase(worktreePath, baseBranch) {
2477
3284
  try {
2478
- execFileSync3("git", ["fetch", "origin", baseBranch], {
3285
+ execFileSync4("git", ["fetch", "origin", baseBranch], {
2479
3286
  cwd: worktreePath,
2480
3287
  stdio: "pipe"
2481
3288
  });
2482
3289
  } catch {
2483
- log.warn(TAG7, "Failed to re-fetch base for revert guard — using last fetch");
3290
+ log.warn(TAG9, "Failed to re-fetch base for revert guard — using last fetch");
2484
3291
  }
2485
3292
  }
2486
3293
  function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
2487
3294
  try {
2488
- const out = execFileSync3("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
3295
+ const out = execFileSync4("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
2489
3296
  return out.split(`
2490
3297
  `).map((l) => l.trim()).filter((l) => l.length > 0);
2491
3298
  } catch (err) {
2492
- log.warn(TAG7, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
3299
+ log.warn(TAG9, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
2493
3300
  return [];
2494
3301
  }
2495
3302
  }
@@ -2499,7 +3306,7 @@ function findDeletedTestFiles(worktreePath, baseBranch) {
2499
3306
  }
2500
3307
 
2501
3308
  // src/verification.ts
2502
- var TAG8 = "verification";
3309
+ var TAG10 = "verification";
2503
3310
  var MAX_OUTPUT_BUFFER2 = 64 * 1024 * 1024;
2504
3311
  async function runVerification(worktreePath, config, workerId) {
2505
3312
  const result = {
@@ -2511,52 +3318,52 @@ async function runVerification(worktreePath, config, workerId) {
2511
3318
  revertWarnings: []
2512
3319
  };
2513
3320
  if (config.verification.revertGuard) {
2514
- log.info(TAG8, `[worker:${workerId}] Checking for reverted merged work...`);
3321
+ log.info(TAG10, `[worker:${workerId}] Checking for reverted merged work...`);
2515
3322
  const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
2516
3323
  if (deletedTests.length > 0) {
2517
3324
  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.");
2518
- log.warn(TAG8, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
3325
+ log.warn(TAG10, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
2519
3326
  result.passed = false;
2520
3327
  } else {
2521
- log.info(TAG8, `[worker:${workerId}] Revert guard passed`);
3328
+ log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
2522
3329
  }
2523
3330
  }
2524
3331
  if (config.verification.build) {
2525
- log.info(TAG8, `[worker:${workerId}] Running build...`);
3332
+ log.info(TAG10, `[worker:${workerId}] Running build...`);
2526
3333
  result.buildErrors = runBuild(worktreePath, config.verification.timeout);
2527
3334
  if (result.buildErrors.length > 0) {
2528
- log.warn(TAG8, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
3335
+ log.warn(TAG10, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2529
3336
  result.passed = false;
2530
3337
  } else {
2531
- log.info(TAG8, `[worker:${workerId}] Build passed`);
3338
+ log.info(TAG10, `[worker:${workerId}] Build passed`);
2532
3339
  }
2533
3340
  }
2534
3341
  if (config.verification.test && result.buildErrors.length === 0) {
2535
- log.info(TAG8, `[worker:${workerId}] Running tests...`);
3342
+ log.info(TAG10, `[worker:${workerId}] Running tests...`);
2536
3343
  result.testFailures = runTests(worktreePath, config.verification.testTimeout);
2537
3344
  if (result.testFailures.length > 0) {
2538
- log.warn(TAG8, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
3345
+ log.warn(TAG10, `[worker:${workerId}] Tests failed with ${result.testFailures.length} failure(s)`);
2539
3346
  result.passed = false;
2540
3347
  } else {
2541
- log.info(TAG8, `[worker:${workerId}] Tests passed`);
3348
+ log.info(TAG10, `[worker:${workerId}] Tests passed`);
2542
3349
  }
2543
3350
  }
2544
3351
  if (config.verification.lint) {
2545
- log.info(TAG8, `[worker:${workerId}] Running lint...`);
3352
+ log.info(TAG10, `[worker:${workerId}] Running lint...`);
2546
3353
  result.lintWarnings = runLint(worktreePath, config.verification.timeout);
2547
3354
  if (result.lintWarnings.length > 0) {
2548
- log.warn(TAG8, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
3355
+ log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2549
3356
  } else {
2550
- log.info(TAG8, `[worker:${workerId}] Lint passed`);
3357
+ log.info(TAG10, `[worker:${workerId}] Lint passed`);
2551
3358
  }
2552
3359
  }
2553
3360
  if (config.verification.deepReview) {
2554
- log.info(TAG8, `[worker:${workerId}] Running deep review...`);
3361
+ log.info(TAG10, `[worker:${workerId}] Running deep review...`);
2555
3362
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
2556
3363
  if (result.reviewFindings.length > 0) {
2557
- log.warn(TAG8, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
3364
+ log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2558
3365
  } else {
2559
- log.info(TAG8, `[worker:${workerId}] Deep review passed`);
3366
+ log.info(TAG10, `[worker:${workerId}] Deep review passed`);
2560
3367
  }
2561
3368
  }
2562
3369
  return result;
@@ -2564,11 +3371,11 @@ async function runVerification(worktreePath, config, workerId) {
2564
3371
  function runBuild(worktreePath, timeout) {
2565
3372
  const command = buildCommand(worktreePath);
2566
3373
  if (!command) {
2567
- log.warn(TAG8, `No known build toolchain for ${worktreePath} — skipping build`);
3374
+ log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
2568
3375
  return [];
2569
3376
  }
2570
3377
  try {
2571
- execFileSync4(command.cmd, command.args, {
3378
+ execFileSync5(command.cmd, command.args, {
2572
3379
  cwd: worktreePath,
2573
3380
  timeout,
2574
3381
  stdio: "pipe",
@@ -2582,11 +3389,11 @@ function runBuild(worktreePath, timeout) {
2582
3389
  function runTests(worktreePath, timeout) {
2583
3390
  const command = testCommand(worktreePath);
2584
3391
  if (!command) {
2585
- log.warn(TAG8, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
3392
+ log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
2586
3393
  return [];
2587
3394
  }
2588
3395
  try {
2589
- execFileSync4(command.cmd, command.args, {
3396
+ execFileSync5(command.cmd, command.args, {
2590
3397
  cwd: worktreePath,
2591
3398
  timeout,
2592
3399
  stdio: "pipe",
@@ -2595,7 +3402,7 @@ function runTests(worktreePath, timeout) {
2595
3402
  return [];
2596
3403
  } catch (err) {
2597
3404
  const output = combineOutput(err);
2598
- log.warn(TAG8, `Test run failed:
3405
+ log.warn(TAG10, `Test run failed:
2599
3406
  ${output.slice(-4000) || "(no output captured)"}`);
2600
3407
  return parseTestFailures(err, timeout);
2601
3408
  }
@@ -2605,25 +3412,25 @@ function runFormatFix(worktreePath, timeout, workerId) {
2605
3412
  if (!command)
2606
3413
  return;
2607
3414
  try {
2608
- execFileSync4(command.cmd, command.args, {
3415
+ execFileSync5(command.cmd, command.args, {
2609
3416
  cwd: worktreePath,
2610
3417
  timeout,
2611
3418
  stdio: "pipe",
2612
3419
  maxBuffer: MAX_OUTPUT_BUFFER2
2613
3420
  });
2614
- log.info(TAG8, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
3421
+ log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
2615
3422
  } catch (err) {
2616
- log.warn(TAG8, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
3423
+ log.warn(TAG10, `[worker:${workerId}] Auto-format step exited non-zero (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2617
3424
  }
2618
3425
  }
2619
3426
  function runLint(worktreePath, timeout) {
2620
3427
  const command = lintCommand(worktreePath);
2621
3428
  if (!command) {
2622
- log.info(TAG8, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
3429
+ log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
2623
3430
  return [];
2624
3431
  }
2625
3432
  try {
2626
- execFileSync4(command.cmd, command.args, {
3433
+ execFileSync5(command.cmd, command.args, {
2627
3434
  cwd: worktreePath,
2628
3435
  timeout,
2629
3436
  stdio: "pipe",
@@ -2636,7 +3443,7 @@ function runLint(worktreePath, timeout) {
2636
3443
  }
2637
3444
  async function runDeepReview(worktreePath, config, workerId) {
2638
3445
  if (!supportsDevServer(worktreePath)) {
2639
- log.info(TAG8, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
3446
+ log.info(TAG10, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
2640
3447
  return [];
2641
3448
  }
2642
3449
  const port = config.verification.devServerBasePort + workerId;
@@ -2651,12 +3458,12 @@ async function runDeepReview(worktreePath, config, workerId) {
2651
3458
  await waitForDevServer(devServer, 30000);
2652
3459
  await probeDevServer(port);
2653
3460
  } catch (err) {
2654
- log.error(TAG8, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
3461
+ log.error(TAG10, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
2655
3462
  return [];
2656
3463
  }
2657
3464
  let diff = "";
2658
3465
  try {
2659
- diff = execFileSync4("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
3466
+ diff = execFileSync5("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], {
2660
3467
  cwd: worktreePath,
2661
3468
  encoding: "utf-8",
2662
3469
  timeout: 30000,
@@ -2678,7 +3485,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2678
3485
  ].join(`
2679
3486
  `);
2680
3487
  const leanSources = config.claude.leanSettingSources;
2681
- const output = execFileSync4("claude", [
3488
+ const output = execFileSync5("claude", [
2682
3489
  "--print",
2683
3490
  "--model",
2684
3491
  "sonnet",
@@ -2696,7 +3503,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2696
3503
  });
2697
3504
  return parseReviewFindings(output);
2698
3505
  } catch (err) {
2699
- log.error(TAG8, `Deep review failed: ${err instanceof Error ? err.message : err}`);
3506
+ log.error(TAG10, `Deep review failed: ${err instanceof Error ? err.message : err}`);
2700
3507
  return [];
2701
3508
  } finally {
2702
3509
  if (devServer && !devServer.killed) {
@@ -2735,8 +3542,8 @@ function attemptAutoFix(worktreePath, config, errors) {
2735
3542
  "--",
2736
3543
  fixPrompt
2737
3544
  ];
2738
- log.info(TAG8, "Spawning Claude for auto-fix...");
2739
- execFileSync4("claude", args, {
3545
+ log.info(TAG10, "Spawning Claude for auto-fix...");
3546
+ execFileSync5("claude", args, {
2740
3547
  cwd: worktreePath,
2741
3548
  timeout: config.verification.timeout,
2742
3549
  stdio: "pipe",
@@ -2773,7 +3580,7 @@ async function reportFindings(client, cardId, result, recovery) {
2773
3580
  try {
2774
3581
  await client.createSubtask(cardId, title);
2775
3582
  } catch (err) {
2776
- log.error(TAG8, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3583
+ log.error(TAG10, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
2777
3584
  }
2778
3585
  }));
2779
3586
  if (overflow > 0) {
@@ -2781,7 +3588,7 @@ async function reportFindings(client, cardId, result, recovery) {
2781
3588
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
2782
3589
  } catch {}
2783
3590
  }
2784
- log.info(TAG8, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
3591
+ log.info(TAG10, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
2785
3592
  }
2786
3593
  function combineOutput(err) {
2787
3594
  const stderr = err?.stderr?.toString() ?? "";
@@ -2841,7 +3648,7 @@ class DevServerReadinessError extends Error {
2841
3648
  }
2842
3649
  }
2843
3650
  function waitForDevServer(proc, timeout) {
2844
- return new Promise((resolve2, reject) => {
3651
+ return new Promise((resolve3, reject) => {
2845
3652
  let settled = false;
2846
3653
  const cleanup = () => {
2847
3654
  proc.stdout?.off("data", onData);
@@ -2855,7 +3662,7 @@ function waitForDevServer(proc, timeout) {
2855
3662
  return;
2856
3663
  settled = true;
2857
3664
  cleanup();
2858
- resolve2();
3665
+ resolve3();
2859
3666
  };
2860
3667
  const settleReject = (err) => {
2861
3668
  if (settled)
@@ -2905,7 +3712,7 @@ async function probeDevServer(port, timeoutMs = 5000) {
2905
3712
  }
2906
3713
 
2907
3714
  // src/gate-collectors.ts
2908
- var TAG9 = "gate-collectors";
3715
+ var TAG11 = "gate-collectors";
2909
3716
  async function resolveStageGate(client, card) {
2910
3717
  const currentStage = card.current_stage;
2911
3718
  const playbookId = card.playbook_id;
@@ -2923,7 +3730,7 @@ async function resolveStageGate(client, card) {
2923
3730
  return null;
2924
3731
  return { stage: resolution.stage, gate };
2925
3732
  } catch (err) {
2926
- log.warn(TAG9, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
3733
+ log.warn(TAG11, `resolveStageGate failed for stage "${currentStage}": ${err instanceof Error ? err.message : err}`);
2927
3734
  return null;
2928
3735
  }
2929
3736
  }
@@ -3048,13 +3855,14 @@ function buildGateCollectorRegistry(deps) {
3048
3855
  }
3049
3856
  if (deps.oracle) {
3050
3857
  registry.oracle_passed = new OracleCollector(deps.oracle);
3858
+ registry.oracle_red = new OracleRedCollector(deps.oracle);
3051
3859
  }
3052
3860
  return registry;
3053
3861
  }
3054
3862
  async function collectGateEvidence(registry, context) {
3055
3863
  const collector = registry[context.gate.kind];
3056
3864
  if (!collector) {
3057
- log.info(TAG9, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3865
+ log.info(TAG11, `No collector for gate kind "${context.gate.kind}" — reporting blocked`);
3058
3866
  return {
3059
3867
  result: "blocked",
3060
3868
  structured: {
@@ -3066,14 +3874,14 @@ async function collectGateEvidence(registry, context) {
3066
3874
  return await collector.collect(context);
3067
3875
  } catch (err) {
3068
3876
  const msg = err instanceof Error ? err.message : String(err);
3069
- log.warn(TAG9, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3877
+ log.warn(TAG11, `Collector for "${context.gate.kind}" threw: ${msg} — reporting blocked`);
3070
3878
  return { result: "blocked", structured: { error: msg } };
3071
3879
  }
3072
3880
  }
3073
3881
  // src/git-diff-stat.ts
3074
3882
  init_log();
3075
- import { execFileSync as execFileSync5 } from "node:child_process";
3076
- var TAG10 = "git-diff-stat";
3883
+ import { execFileSync as execFileSync6 } from "node:child_process";
3884
+ var TAG12 = "git-diff-stat";
3077
3885
  var MAX_CHANGED_FILES = 30;
3078
3886
  function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES) {
3079
3887
  const files = [];
@@ -3152,10 +3960,10 @@ function formatDiffSummary(diff, maxFiles = 100) {
3152
3960
  }
3153
3961
  function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES) {
3154
3962
  try {
3155
- const raw = execFileSync5("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
3963
+ const raw = execFileSync6("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
3156
3964
  return parseNumstat(raw, maxFiles);
3157
3965
  } catch (err) {
3158
- log.warn(TAG10, "git diff --numstat failed", {
3966
+ log.warn(TAG12, "git diff --numstat failed", {
3159
3967
  event: "diff_stat_failed",
3160
3968
  error: err instanceof Error ? err.message : String(err)
3161
3969
  });
@@ -3168,7 +3976,7 @@ init_git_pr();
3168
3976
 
3169
3977
  // src/harmony-client.ts
3170
3978
  init_log();
3171
- var TAG12 = "harmony-client";
3979
+ var TAG14 = "harmony-client";
3172
3980
  function readClientConfig(env) {
3173
3981
  const apiUrl = env.HARMONY_API_URL?.trim();
3174
3982
  const apiKey = env.HARMONY_API_KEY?.trim();
@@ -3220,7 +4028,7 @@ class HarmonyClient {
3220
4028
  purpose: "gate_evaluation"
3221
4029
  });
3222
4030
  if (!response.ok) {
3223
- log.warn(TAG12, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
4031
+ log.warn(TAG14, `Oracle fetch for stage ${stageId} returned ${response.status} — no oracle read, the gate will report blocked`);
3224
4032
  return null;
3225
4033
  }
3226
4034
  const body = await response.json();
@@ -3300,138 +4108,101 @@ function relayAgentEvent(draft) {
3300
4108
  }
3301
4109
  return { type: "agent_event", event: draft };
3302
4110
  }
3303
- // src/oracle.ts
3304
- import { lstat, mkdir, realpath, rm, writeFile } from "node:fs/promises";
3305
- import { dirname, isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
3306
- async function resolveContained(repoPath, relativePath) {
3307
- if (isAbsolute2(relativePath)) {
3308
- throw new Error(`refusing to place an oracle at an absolute path: ${relativePath}`);
3309
- }
3310
- if (relativePath === "" || relativePath === ".") {
3311
- throw new Error(`refusing to place an oracle at the empty/self path: "${relativePath}"`);
3312
- }
3313
- const root = await realpath(repoPath);
3314
- const target = resolve2(root, relativePath);
3315
- if (target !== root && !target.startsWith(root + sep2)) {
3316
- throw new Error(`refusing to place an oracle outside the worktree: ${relativePath}`);
3317
- }
3318
- let cursor = root;
3319
- for (const segment of relativePath.split("/")) {
3320
- cursor = resolve2(cursor, segment);
3321
- const stat = await lstat(cursor).catch(() => null);
3322
- if (stat?.isSymbolicLink()) {
3323
- throw new Error(`refusing an oracle path through a symlink component: ${relativePath}`);
3324
- }
4111
+ // src/repair-sandbox.ts
4112
+ init_log();
4113
+ import { randomUUID } from "node:crypto";
4114
+ import { promisify as promisify3 } from "node:util";
4115
+ var TAG15 = "repair-sandbox";
4116
+ async function dockerExec(argv, opts) {
4117
+ const { execFile: execFile3 } = await import("node:child_process");
4118
+ return promisify3(execFile3)("docker", argv, {
4119
+ encoding: "utf-8",
4120
+ ...opts
4121
+ });
4122
+ }
4123
+ var MAX_OUTPUT_BUFFER3 = 20971520;
4124
+ var PROBE_TIMEOUT_MS = 1e4;
4125
+ var SANDBOX_MOUNT = "/repo";
4126
+ var SANDBOX_MEMORY = "4g";
4127
+ var SANDBOX_PIDS = "512";
4128
+ var dockerProbe = null;
4129
+ async function sandboxAvailable() {
4130
+ if (dockerProbe === true)
4131
+ return true;
4132
+ try {
4133
+ await dockerExec(["version", "--format", "{{.Server.Version}}"], {
4134
+ timeout: PROBE_TIMEOUT_MS
4135
+ });
4136
+ dockerProbe = true;
4137
+ } catch {
4138
+ dockerProbe = false;
3325
4139
  }
3326
- return target;
4140
+ return dockerProbe;
3327
4141
  }
3328
- async function place(repoPath, oracle) {
3329
- const target = await resolveContained(repoPath, oracle.path);
3330
- await mkdir(dirname(target), { recursive: true });
3331
- await writeFile(target, oracle.content, "utf8");
4142
+ function __resetSandboxProbe() {
4143
+ dockerProbe = null;
3332
4144
  }
3333
- async function remove(repoPath, oracle) {
3334
- const target = await resolveContained(repoPath, oracle.path);
3335
- await rm(target, { force: true });
3336
- }
3337
- var ORACLE_RUNNERS = {
3338
- vitest: (path) => ({
3339
- command: "npx",
3340
- args: ["--no-install", "vitest", "run", path]
3341
- }),
3342
- bun: (path) => ({ command: "bun", args: ["test", path] })
3343
- };
3344
- var ORACLE_RUNNER_HINTS = Object.keys(ORACLE_RUNNERS).sort();
3345
- var ORACLE_OUTPUT_LIMIT = 64 * 1024;
3346
- var ORACLE_SIGINT_GRACE_MS = 2000;
3347
- var ORACLE_SIGTERM_GRACE_MS = 3000;
3348
- var ORACLE_DRAIN_GRACE_MS = 500;
3349
- function resolveOracleRunner(oracle) {
3350
- const hint = oracle.runnerHint?.trim().toLowerCase() ?? "";
3351
- const build = Object.hasOwn(ORACLE_RUNNERS, hint) ? ORACLE_RUNNERS[hint] : undefined;
3352
- if (!build) {
3353
- 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(", ")})`);
3354
- }
3355
- return build(argvPath(oracle.path));
4145
+ async function removeContainer(name) {
4146
+ try {
4147
+ await dockerExec(["rm", "--force", name], { timeout: PROBE_TIMEOUT_MS });
4148
+ log.warn(TAG15, `removed the timed-out sandbox container ${name}`);
4149
+ } catch {}
3356
4150
  }
3357
- function argvPath(path) {
3358
- return path.startsWith("./") ? path : `./${path}`;
4151
+ function sandboxRunArgs(image, worktree, command, name) {
4152
+ return [
4153
+ "run",
4154
+ "--rm",
4155
+ ...name ? ["--name", name] : [],
4156
+ "--network=none",
4157
+ "--cap-drop=ALL",
4158
+ "--security-opt=no-new-privileges",
4159
+ `--memory=${SANDBOX_MEMORY}`,
4160
+ `--pids-limit=${SANDBOX_PIDS}`,
4161
+ ...typeof process.getuid === "function" && typeof process.getgid === "function" ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
4162
+ "--env",
4163
+ "HOME=/tmp",
4164
+ "--volume",
4165
+ `${worktree}:${SANDBOX_MOUNT}`,
4166
+ "--workdir",
4167
+ SANDBOX_MOUNT,
4168
+ "--entrypoint",
4169
+ command.cmd,
4170
+ image,
4171
+ ...command.args
4172
+ ];
3359
4173
  }
3360
- async function runHeldOracle(repoPath, oracle, timeoutMs = DEFAULT_METRIC_TIMEOUT_MS) {
3361
- const { command, args } = resolveOracleRunner(oracle);
3362
- return await new Promise((settleOk, settleErr) => {
3363
- let child;
3364
- try {
3365
- child = spawnInGroup(command, args, {
3366
- cwd: repoPath,
3367
- stdio: ["ignore", "pipe", "pipe"]
3368
- });
3369
- } catch (err) {
3370
- settleErr(err);
3371
- return;
3372
- }
3373
- const pgid = child.pid;
3374
- let output = "";
3375
- let settled = false;
3376
- let killing = false;
3377
- let timer;
3378
- let drainTimer;
3379
- const settle = (failure, result) => {
3380
- if (settled)
3381
- return;
3382
- settled = true;
3383
- if (timer)
3384
- clearTimeout(timer);
3385
- if (drainTimer)
3386
- clearTimeout(drainTimer);
3387
- reapGroup(pgid);
3388
- if (failure)
3389
- settleErr(failure);
3390
- else
3391
- settleOk(result);
3392
- };
3393
- const append = (chunk) => {
3394
- if (output.length >= ORACLE_OUTPUT_LIMIT)
3395
- return;
3396
- output += chunk.toString("utf8");
3397
- if (output.length > ORACLE_OUTPUT_LIMIT) {
3398
- output = `${output.slice(0, ORACLE_OUTPUT_LIMIT)}
3399
- … output truncated at ${ORACLE_OUTPUT_LIMIT} characters`;
3400
- }
3401
- };
3402
- child.stdout?.on("data", append);
3403
- child.stderr?.on("data", append);
3404
- child.once("error", (err) => settle(err));
3405
- const settleFromExit = (code, signal) => {
3406
- if (drainTimer)
3407
- clearTimeout(drainTimer);
3408
- if (code === null) {
3409
- settle(new Error(`the held test was terminated by signal ${signal}`));
3410
- return;
3411
- }
3412
- settle(null, { exitCode: code, output });
3413
- };
3414
- child.once("exit", (code, signal) => {
3415
- if (killing)
3416
- return;
3417
- if (timer)
3418
- clearTimeout(timer);
3419
- reapGroup(pgid);
3420
- drainTimer = setTimeout(() => settleFromExit(code, signal), ORACLE_DRAIN_GRACE_MS);
3421
- child.once("close", () => settleFromExit(code, signal));
4174
+ async function runInSandbox(args) {
4175
+ const name = `harmony-repair-${randomUUID()}`;
4176
+ const argv = sandboxRunArgs(args.image, args.worktree, args.command, name);
4177
+ log.info(TAG15, `sandbox: ${args.command.cmd} ${args.command.args.join(" ")} (image ${args.image})`);
4178
+ try {
4179
+ const { stdout } = await dockerExec(argv, {
4180
+ timeout: args.timeoutMs,
4181
+ maxBuffer: MAX_OUTPUT_BUFFER3
3422
4182
  });
3423
- timer = setTimeout(() => {
3424
- if (settled)
3425
- return;
3426
- killing = true;
3427
- terminateGroup(child, {
3428
- sigintTimeoutMs: ORACLE_SIGINT_GRACE_MS,
3429
- sigtermTimeoutMs: ORACLE_SIGTERM_GRACE_MS
3430
- }).catch(() => {}).then(() => {
3431
- settle(new Error(`the held test did not finish within ${timeoutMs}ms`));
3432
- });
3433
- }, timeoutMs);
3434
- });
4183
+ return { passed: true, output: stdout ?? "" };
4184
+ } catch (err) {
4185
+ const e = err;
4186
+ const output = `${e.stdout ?? ""}${e.stderr ?? ""}`;
4187
+ if (typeof e.code !== "number") {
4188
+ const timedOut = e.killed === true || e.signal != null;
4189
+ if (timedOut)
4190
+ await removeContainer(name);
4191
+ return {
4192
+ passed: false,
4193
+ output,
4194
+ sandboxError: timedOut ? `the sandbox timed out after ${args.timeoutMs}ms` : `the sandbox did not run: ${e.message ?? "unknown error"}`
4195
+ };
4196
+ }
4197
+ if (e.code === 125) {
4198
+ return {
4199
+ passed: false,
4200
+ output,
4201
+ sandboxError: `the sandbox could not start (image "${args.image}" missing or unusable)`
4202
+ };
4203
+ }
4204
+ return { passed: false, output };
4205
+ }
3435
4206
  }
3436
4207
  // src/run-sizing.ts
3437
4208
  init_dist();
@@ -3531,14 +4302,37 @@ var defaultRunSize = async ({
3531
4302
  cwd,
3532
4303
  model
3533
4304
  };
4305
+ return collectSizingOutput(runner.start(input));
4306
+ };
4307
+ async function collectSizingOutput(events) {
3534
4308
  const parts = [];
3535
- for await (const ev of runner.start(input)) {
4309
+ let named;
4310
+ let sawError = false;
4311
+ for await (const ev of events) {
3536
4312
  if (ev.kind === "assistant_text")
3537
4313
  parts.push(ev.payload.text);
4314
+ if (ev.kind === "error") {
4315
+ sawError = true;
4316
+ named ??= sizingFailureFromError(ev.payload.message) ?? undefined;
4317
+ }
3538
4318
  }
3539
- return parts.join(`
3540
- `);
3541
- };
4319
+ const failure = named ?? (sawError ? "spawn" : undefined);
4320
+ return { text: parts.join(`
4321
+ `), ...failure ? { failure } : {} };
4322
+ }
4323
+ function sizingFailureFromError(message) {
4324
+ const subtype = resultErrorSubtype(message);
4325
+ if (!subtype)
4326
+ return null;
4327
+ switch (subtype) {
4328
+ case "error_max_turns":
4329
+ return "turns";
4330
+ case "error_max_budget_usd":
4331
+ return "budget";
4332
+ default:
4333
+ return "spawn";
4334
+ }
4335
+ }
3542
4336
  function buildSizingPrompt(title, description) {
3543
4337
  const card = JSON.stringify({
3544
4338
  title: title.slice(0, PROMPT_TITLE_MAX),
@@ -3589,7 +4383,7 @@ function sizingEventSource(source) {
3589
4383
  async function sizeRun(deps) {
3590
4384
  const requested = deps.model ?? SIZING_MODEL;
3591
4385
  if (!requested)
3592
- return null;
4386
+ return { status: "disabled" };
3593
4387
  const model = clampWithdrawn(requested);
3594
4388
  const timeoutMs = deps.timeoutMs ?? SIZING_TIMEOUT_MS;
3595
4389
  const run = deps.runSize ?? defaultRunSize;
@@ -3597,7 +4391,7 @@ async function sizeRun(deps) {
3597
4391
  let timer;
3598
4392
  let runner = null;
3599
4393
  try {
3600
- const text = await Promise.race([
4394
+ const result = await Promise.race([
3601
4395
  run({
3602
4396
  prompt,
3603
4397
  cwd: deps.cwd,
@@ -3616,11 +4410,16 @@ async function sizeRun(deps) {
3616
4410
  }, timeoutMs);
3617
4411
  })
3618
4412
  ]);
3619
- if (text === null)
3620
- return null;
3621
- return parseVerdict(text);
4413
+ if (result === null)
4414
+ return { status: "failed", reason: "timeout" };
4415
+ const sizing = parseVerdict(result.text);
4416
+ if (sizing)
4417
+ return { status: "sized", sizing };
4418
+ if (result.failure)
4419
+ return { status: "failed", reason: result.failure };
4420
+ return { status: "failed", reason: "malformed" };
3622
4421
  } catch {
3623
- return null;
4422
+ return { status: "failed", reason: "spawn" };
3624
4423
  } finally {
3625
4424
  if (timer)
3626
4425
  clearTimeout(timer);
@@ -3652,10 +4451,10 @@ async function runStage(request, deps) {
3652
4451
  }
3653
4452
  // src/worktree.ts
3654
4453
  init_log();
3655
- import { execFileSync as execFileSync7, execSync } from "node:child_process";
4454
+ import { execFileSync as execFileSync8, execSync } from "node:child_process";
3656
4455
  import { existsSync as existsSync3, readdirSync as readdirSync2, rmSync } from "node:fs";
3657
4456
  import { resolve as resolve3 } from "node:path";
3658
- var TAG13 = "worktree";
4457
+ var TAG16 = "worktree";
3659
4458
 
3660
4459
  class WorktreeBaseError extends Error {
3661
4460
  constructor(message) {
@@ -3663,7 +4462,7 @@ class WorktreeBaseError extends Error {
3663
4462
  this.name = "WorktreeBaseError";
3664
4463
  }
3665
4464
  }
3666
- function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync7("git", ["fetch", "origin", branch], {
4465
+ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync8("git", ["fetch", "origin", branch], {
3667
4466
  cwd: root,
3668
4467
  stdio: "pipe"
3669
4468
  })) {
@@ -3674,7 +4473,7 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
3674
4473
  return;
3675
4474
  } catch (err) {
3676
4475
  lastErr = err;
3677
- log.warn(TAG13, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
4476
+ log.warn(TAG16, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
3678
4477
  }
3679
4478
  }
3680
4479
  throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${gitErrorDetail(lastErr)}`);
@@ -3689,7 +4488,34 @@ function resolveWorktreeStartRef(baseBranch, branchName, continueExisting, branc
3689
4488
  }
3690
4489
  return `origin/${baseBranch}`;
3691
4490
  }
3692
- 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", [
4491
+ function resolveContinuationTarget(branchName, continueRequested, failedBranchPrefix, approvedBranchPrefix, branchExistsOnOrigin) {
4492
+ if (branchExistsOnOrigin(branchName)) {
4493
+ return {
4494
+ branchName,
4495
+ continueExisting: true,
4496
+ existsOnOrigin: true,
4497
+ reason: continueRequested ? "requested" : "exists_on_origin"
4498
+ };
4499
+ }
4500
+ if (failedBranchPrefix && approvedBranchPrefix && branchName.startsWith(failedBranchPrefix)) {
4501
+ const sibling = approvedBranchPrefix + branchName.slice(failedBranchPrefix.length);
4502
+ if (branchExistsOnOrigin(sibling)) {
4503
+ return {
4504
+ branchName: sibling,
4505
+ continueExisting: true,
4506
+ existsOnOrigin: true,
4507
+ reason: "approved_rename"
4508
+ };
4509
+ }
4510
+ }
4511
+ return {
4512
+ branchName,
4513
+ continueExisting: continueRequested,
4514
+ existsOnOrigin: false,
4515
+ reason: "fresh"
4516
+ };
4517
+ }
4518
+ 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", [
3693
4519
  "fetch",
3694
4520
  "origin",
3695
4521
  `+refs/heads/${branch}:refs/remotes/origin/${branch}`
@@ -3704,7 +4530,7 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
3704
4530
  if (err.status === 2)
3705
4531
  return false;
3706
4532
  lastErr = err;
3707
- log.warn(TAG13, `ls-remote ${branchName} failed (attempt ${attempt}/${attempts})`);
4533
+ log.warn(TAG16, `ls-remote ${branchName} failed (attempt ${attempt}/${attempts})`);
3708
4534
  }
3709
4535
  }
3710
4536
  if (!probed) {
@@ -3716,14 +4542,14 @@ function fetchExistingBranch(repoRoot, branchName, attempts = 3, lsRemoteImpl =
3716
4542
  return true;
3717
4543
  } catch (err) {
3718
4544
  lastErr = err;
3719
- log.warn(TAG13, `fetch ${branchName} failed (attempt ${attempt}/${attempts})`);
4545
+ log.warn(TAG16, `fetch ${branchName} failed (attempt ${attempt}/${attempts})`);
3720
4546
  }
3721
4547
  }
3722
4548
  throw new WorktreeBaseError(`${branchName} exists on origin but could not be fetched after ${attempts} attempts: ${gitErrorDetail(lastErr)}. Refusing to rebuild the branch.`);
3723
4549
  }
3724
4550
  function readWorktreeHead(worktreePath) {
3725
4551
  try {
3726
- return execFileSync7("git", ["rev-parse", "HEAD"], {
4552
+ return execFileSync8("git", ["rev-parse", "HEAD"], {
3727
4553
  cwd: worktreePath,
3728
4554
  encoding: "utf-8"
3729
4555
  }).trim();
@@ -3732,50 +4558,50 @@ function readWorktreeHead(worktreePath) {
3732
4558
  }
3733
4559
  }
3734
4560
  function createWorktree(basePath, baseBranch, branchName, opts = {}) {
3735
- const repoRoot = execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4561
+ const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3736
4562
  encoding: "utf-8"
3737
4563
  }).trim();
3738
4564
  const worktreeDir = resolve3(repoRoot, basePath, branchName);
3739
4565
  if (existsSync3(worktreeDir)) {
3740
- log.warn(TAG13, `Worktree already exists at ${worktreeDir}, cleaning up`);
4566
+ log.warn(TAG16, `Worktree already exists at ${worktreeDir}, cleaning up`);
3741
4567
  cleanupWorktree(worktreeDir, branchName);
3742
4568
  }
3743
4569
  try {
3744
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4570
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3745
4571
  cwd: repoRoot,
3746
4572
  stdio: "pipe"
3747
4573
  });
3748
4574
  } catch {}
3749
4575
  fetchBaseBranch(repoRoot, baseBranch);
3750
- const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => fetchExistingBranch(repoRoot, branchName));
3751
- log.info(TAG13, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
4576
+ const startRef = resolveWorktreeStartRef(baseBranch, branchName, opts.continueExisting ?? false, () => opts.branchExistsOnOrigin ?? fetchExistingBranch(repoRoot, branchName));
4577
+ log.info(TAG16, `Creating worktree: ${worktreeDir} (branch: ${branchName}, base: ${startRef})`);
3752
4578
  try {
3753
- execFileSync7("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
4579
+ execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
3754
4580
  } catch (err) {
3755
4581
  const msg = err instanceof Error ? err.message : String(err);
3756
- log.warn(TAG13, `worktree add failed, attempting forced recovery: ${msg}`);
4582
+ log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
3757
4583
  removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
3758
4584
  try {
3759
- execFileSync7("git", ["worktree", "remove", worktreeDir, "--force"], {
4585
+ execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
3760
4586
  cwd: repoRoot,
3761
4587
  stdio: "pipe"
3762
4588
  });
3763
4589
  } catch {}
3764
4590
  try {
3765
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4591
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3766
4592
  cwd: repoRoot,
3767
4593
  stdio: "pipe"
3768
4594
  });
3769
4595
  } catch {}
3770
4596
  try {
3771
- execFileSync7("git", ["branch", "-D", branchName], {
4597
+ execFileSync8("git", ["branch", "-D", branchName], {
3772
4598
  cwd: repoRoot,
3773
4599
  stdio: "pipe"
3774
4600
  });
3775
4601
  } catch {}
3776
- execFileSync7("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
4602
+ execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
3777
4603
  }
3778
- log.info(TAG13, "Installing dependencies in worktree...");
4604
+ log.info(TAG16, "Installing dependencies in worktree...");
3779
4605
  try {
3780
4606
  execSync(installCommand(), {
3781
4607
  cwd: worktreeDir,
@@ -3783,7 +4609,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
3783
4609
  timeout: 60000
3784
4610
  });
3785
4611
  } catch {
3786
- log.warn(TAG13, "Install failed (may be fine if deps are hoisted)");
4612
+ log.warn(TAG16, "Install failed (may be fine if deps are hoisted)");
3787
4613
  }
3788
4614
  return worktreeDir;
3789
4615
  }
@@ -3799,7 +4625,7 @@ function containsForeignWorktrees(dir) {
3799
4625
  return children.some((child) => existsSync3(resolve3(dir, child, ".git")));
3800
4626
  }
3801
4627
  function cleanupWorktree(worktreePath, branchName) {
3802
- const repoRoot = execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4628
+ const repoRoot = execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3803
4629
  encoding: "utf-8"
3804
4630
  }).trim();
3805
4631
  if (existsSync3(worktreePath)) {
@@ -3807,18 +4633,18 @@ function cleanupWorktree(worktreePath, branchName) {
3807
4633
  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).`);
3808
4634
  }
3809
4635
  try {
3810
- execFileSync7("git", ["worktree", "remove", worktreePath, "--force"], {
4636
+ execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
3811
4637
  cwd: repoRoot,
3812
4638
  stdio: "pipe"
3813
4639
  });
3814
- log.info(TAG13, `Removed worktree: ${worktreePath}`);
4640
+ log.info(TAG16, `Removed worktree: ${worktreePath}`);
3815
4641
  } catch (err) {
3816
- log.warn(TAG13, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
4642
+ log.warn(TAG16, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
3817
4643
  if (existsSync3(worktreePath)) {
3818
4644
  rmSync(worktreePath, { recursive: true, force: true });
3819
4645
  }
3820
4646
  try {
3821
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4647
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3822
4648
  cwd: repoRoot,
3823
4649
  stdio: "pipe"
3824
4650
  });
@@ -3826,7 +4652,7 @@ function cleanupWorktree(worktreePath, branchName) {
3826
4652
  }
3827
4653
  } else {
3828
4654
  try {
3829
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4655
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3830
4656
  cwd: repoRoot,
3831
4657
  stdio: "pipe"
3832
4658
  });
@@ -3834,7 +4660,7 @@ function cleanupWorktree(worktreePath, branchName) {
3834
4660
  }
3835
4661
  if (branchName) {
3836
4662
  try {
3837
- execFileSync7("git", ["branch", "-D", branchName], {
4663
+ execFileSync8("git", ["branch", "-D", branchName], {
3838
4664
  cwd: repoRoot,
3839
4665
  stdio: "pipe"
3840
4666
  });
@@ -3844,7 +4670,7 @@ function cleanupWorktree(worktreePath, branchName) {
3844
4670
  function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3845
4671
  let listing;
3846
4672
  try {
3847
- listing = execFileSync7("git", ["worktree", "list", "--porcelain"], {
4673
+ listing = execFileSync8("git", ["worktree", "list", "--porcelain"], {
3848
4674
  cwd: repoRoot,
3849
4675
  encoding: "utf-8",
3850
4676
  stdio: ["ignore", "pipe", "pipe"]
@@ -3872,17 +4698,17 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3872
4698
  if (exceptDir && resolve3(holderPath) === resolve3(exceptDir))
3873
4699
  return null;
3874
4700
  try {
3875
- execFileSync7("git", ["worktree", "remove", holderPath, "--force"], {
4701
+ execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
3876
4702
  cwd: repoRoot,
3877
4703
  stdio: "pipe"
3878
4704
  });
3879
- log.warn(TAG13, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
4705
+ log.warn(TAG16, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
3880
4706
  } catch (err) {
3881
- log.warn(TAG13, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
4707
+ log.warn(TAG16, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
3882
4708
  return null;
3883
4709
  }
3884
4710
  try {
3885
- execFileSync7("git", ["worktree", "prune", "--expire=now"], {
4711
+ execFileSync8("git", ["worktree", "prune", "--expire=now"], {
3886
4712
  cwd: repoRoot,
3887
4713
  stdio: "pipe"
3888
4714
  });
@@ -3890,13 +4716,13 @@ function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
3890
4716
  return holderPath;
3891
4717
  }
3892
4718
  function resolveRepoRoot() {
3893
- return execFileSync7("git", ["rev-parse", "--show-toplevel"], {
4719
+ return execFileSync8("git", ["rev-parse", "--show-toplevel"], {
3894
4720
  encoding: "utf-8"
3895
4721
  }).trim();
3896
4722
  }
3897
4723
  function localBranchExists(branchName, repoRoot) {
3898
4724
  try {
3899
- execFileSync7("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
4725
+ execFileSync8("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
3900
4726
  return true;
3901
4727
  } catch {
3902
4728
  return false;
@@ -3906,7 +4732,7 @@ function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
3906
4732
  if (!localBranchExists(branchName, repoRoot))
3907
4733
  return false;
3908
4734
  try {
3909
- const out = execFileSync7("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4735
+ const out = execFileSync8("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
3910
4736
  return out.length > 0;
3911
4737
  } catch {
3912
4738
  return false;
@@ -3917,17 +4743,17 @@ async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resol
3917
4743
  try {
3918
4744
  pushBranch2(branchName, repoRoot);
3919
4745
  } catch (err) {
3920
- 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}`);
4746
+ 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}`);
3921
4747
  return false;
3922
4748
  }
3923
- log.warn(TAG13, `push-rescued unpushed branch ${branchName} to origin before teardown`);
4749
+ log.warn(TAG16, `push-rescued unpushed branch ${branchName} to origin before teardown`);
3924
4750
  try {
3925
4751
  const url = getBranchWebUrl2(branchName, repoRoot);
3926
4752
  const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
3927
4753
  const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
3928
4754
  await client.addComment(cardId, body, { commentType: "message" });
3929
4755
  } catch (err) {
3930
- log.warn(TAG13, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
4756
+ log.warn(TAG16, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
3931
4757
  }
3932
4758
  return true;
3933
4759
  }
@@ -3945,7 +4771,7 @@ async function teardownWorktree(client, cardId, worktreePath, branchName) {
3945
4771
  const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
3946
4772
  if (!ok) {
3947
4773
  skipBranchDelete = true;
3948
- log.error(TAG13, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
4774
+ log.error(TAG16, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
3949
4775
  }
3950
4776
  }
3951
4777
  }
@@ -3962,32 +4788,44 @@ export {
3962
4788
  waitForDevServer,
3963
4789
  validateGitProviderCli,
3964
4790
  upsertReviewedSha,
4791
+ upsertCiReviewRequestedSha,
3965
4792
  updateExistingPr,
3966
4793
  testCommand,
3967
4794
  terminateGroup,
3968
4795
  teardownWorktree,
3969
4796
  supportsDevServer,
3970
4797
  summarizeUnifiedDiff,
4798
+ summarizeOracleReport,
3971
4799
  spawnRunArgs,
3972
4800
  spawnInGroup,
4801
+ sizingFailureFromError,
3973
4802
  sizingEventSource,
3974
4803
  sizeRun,
3975
4804
  signalGroup,
4805
+ sanitizeCiText,
4806
+ sandboxRunArgs,
4807
+ sandboxAvailable,
3976
4808
  runVerification,
3977
4809
  runTests,
3978
4810
  runStage,
3979
4811
  runMetricCommand,
3980
4812
  runLint,
4813
+ runInSandbox,
3981
4814
  runHeldOracle,
3982
4815
  runFormatFix,
3983
4816
  runDeepReview,
3984
4817
  runBuild,
4818
+ resultErrorSubtype,
4819
+ resultErrorMessage,
3985
4820
  resolveWorktreeStartRef,
3986
4821
  resolveStageGate,
3987
4822
  resolvePrUrl,
3988
4823
  resolvePrHeadBranch,
3989
4824
  resolveOracleRunner,
4825
+ resolveContinuationTarget,
3990
4826
  rescueUnpushedBranch,
4827
+ rerunFailedJobs,
4828
+ requestIndependentReview,
3991
4829
  reportFindings,
3992
4830
  renameRemoteBranch,
3993
4831
  removeWorktreeHoldingBranch,
@@ -3996,6 +4834,7 @@ export {
3996
4834
  relayAgentEvent,
3997
4835
  reapGroup,
3998
4836
  readWorktreeHead,
4837
+ readPrChecks,
3999
4838
  readClientConfig,
4000
4839
  pushBranch,
4001
4840
  probeDevServer,
@@ -4004,6 +4843,7 @@ export {
4004
4843
  parseNumstat,
4005
4844
  parseMetricValue,
4006
4845
  parseJudgeVerdict,
4846
+ parseFailedChecks,
4007
4847
  normalizeGateSpec,
4008
4848
  mergePullRequest,
4009
4849
  mapSdkErrorKind,
@@ -4013,10 +4853,14 @@ export {
4013
4853
  lintCommand,
4014
4854
  isTestFile,
4015
4855
  isPretty,
4856
+ isPrOnOrigin,
4016
4857
  isInsideTree,
4017
4858
  installCommand,
4859
+ gradeOracleRed,
4018
4860
  getPrStatus,
4861
+ getPrFailedChecks,
4019
4862
  getHeadSha,
4863
+ getFailedRunLog,
4020
4864
  getBranchWebUrl,
4021
4865
  gateConfigErrorReason,
4022
4866
  formatFixCommand,
@@ -4026,14 +4870,19 @@ export {
4026
4870
  filterTestFiles,
4027
4871
  fetchExistingBranch,
4028
4872
  fetchBaseBranch,
4873
+ extractWorkflowRunId,
4029
4874
  extractReviewedSha,
4030
4875
  extractPrUrl,
4876
+ extractCiReviewRequestedSha,
4031
4877
  extractAzurePrId,
4032
4878
  envKeysDroppedByLaunch,
4033
4879
  detectPackageManager,
4034
4880
  detectGitProvider,
4035
4881
  detect,
4882
+ describeCiFailure,
4036
4883
  describeApiError,
4884
+ deriveReviewVerdictState,
4885
+ deriveIndependentReviewState,
4037
4886
  deriveCiStatus,
4038
4887
  decidePrBranch,
4039
4888
  decideConfinedTool,
@@ -4042,9 +4891,12 @@ export {
4042
4891
  createPullRequest,
4043
4892
  cooldownMsFor,
4044
4893
  confineToRepo,
4894
+ collectSizingOutput,
4045
4895
  collectGateEvidence,
4046
4896
  cleanupWorktree,
4047
4897
  classifyRunError,
4898
+ classifyCiFailures,
4899
+ classifyCiFailure,
4048
4900
  clampWithdrawn,
4049
4901
  chooseImplementModel,
4050
4902
  checkPrMergeStatus,
@@ -4057,11 +4909,14 @@ export {
4057
4909
  branchAheadOfItsRemote,
4058
4910
  attemptAutoFix,
4059
4911
  _resetCache,
4912
+ __resetSandboxProbe,
4060
4913
  WorktreeBaseError,
4061
4914
  SdkAgentRunner,
4062
4915
  SIZING_MODEL,
4063
4916
  SDK_ALLOWED_TOOLS,
4917
+ SANDBOX_MOUNT,
4064
4918
  ReviewPassedCollector,
4919
+ OracleRedCollector,
4065
4920
  OracleCollector,
4066
4921
  ORACLE_RUNNER_HINTS,
4067
4922
  MOTOR_TOOL_INPUT_VALUE_MAX,
@@ -4078,6 +4933,8 @@ export {
4078
4933
  DEFAULT_METRIC_TIMEOUT_MS,
4079
4934
  CommandMetricCollector,
4080
4935
  ChecklistDodCollector,
4936
+ CONFINED_WRITE_TOOLS,
4937
+ CONFINED_READ_TOOLS,
4081
4938
  BuildGreenCollector,
4082
4939
  ArtifactCollector
4083
4940
  };