@gethmy/harness 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +497 -225
- package/dist/index.js +1259 -402
- package/package.json +2 -2
- package/src/ci-failure.ts +465 -0
- package/src/cli.ts +11 -1
- package/src/confine-to-repo.test.ts +324 -1
- package/src/confine-to-repo.ts +274 -22
- package/src/error-classifier.ts +52 -1
- package/src/gate-collectors.ts +11 -3
- package/src/git-pr.ts +461 -8
- package/src/index.ts +2 -0
- package/src/model-tier.test.ts +11 -6
- package/src/model-tier.ts +4 -4
- package/src/oracle-collector.ts +244 -23
- package/src/oracle.ts +856 -108
- package/src/pm.ts +15 -5
- package/src/repair-sandbox.test.ts +116 -0
- package/src/repair-sandbox.ts +303 -0
- package/src/run-sizing.test.ts +264 -66
- package/src/run-sizing.ts +146 -26
- package/src/sdk-agent-runner.ts +22 -1
- package/src/worktree.ts +114 -2
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
|
|
627
|
-
import { promisify } from "node:util";
|
|
628
|
-
function
|
|
629
|
-
return
|
|
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
|
|
632
|
-
return
|
|
650
|
+
function execFileAsync2() {
|
|
651
|
+
return cachedExecFileAsync2 ??= createExecFileAsync2();
|
|
633
652
|
}
|
|
634
653
|
function detectGitProvider(cwd) {
|
|
635
654
|
try {
|
|
636
|
-
const url =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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}${
|
|
740
|
+
return `${description}${sep3}${line}`;
|
|
707
741
|
}
|
|
708
|
-
function
|
|
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
|
-
|
|
740
|
-
if (
|
|
741
|
-
return
|
|
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
|
|
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 {
|
|
917
|
+
return {
|
|
918
|
+
...readPrChecks(parsed.statusCheckRollup, {
|
|
919
|
+
independentReviewCheck,
|
|
920
|
+
reviewVerdictCheck
|
|
921
|
+
}),
|
|
922
|
+
headSha
|
|
923
|
+
};
|
|
748
924
|
} catch {
|
|
749
|
-
return {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1138
|
+
log.info(TAG13, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
953
1139
|
let expectedSha = null;
|
|
954
1140
|
try {
|
|
955
|
-
|
|
1141
|
+
execFileSync7("git", ["fetch", "origin", branchName], {
|
|
956
1142
|
cwd,
|
|
957
1143
|
stdio: "pipe"
|
|
958
1144
|
});
|
|
959
|
-
expectedSha =
|
|
1145
|
+
expectedSha = execFileSync7("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
960
1146
|
} catch (err) {
|
|
961
|
-
log.warn(
|
|
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
|
-
|
|
1150
|
+
execFileSync7("git", ["push", lease, "-u", "origin", branchName], {
|
|
965
1151
|
cwd,
|
|
966
1152
|
stdio: "pipe"
|
|
967
1153
|
});
|
|
968
1154
|
} else {
|
|
969
|
-
|
|
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 =
|
|
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(
|
|
988
|
-
|
|
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
|
-
|
|
1176
|
+
execFileSync7("git", ["push", "origin", `:refs/heads/${oldRef}`], {
|
|
991
1177
|
cwd,
|
|
992
1178
|
stdio: "pipe"
|
|
993
1179
|
});
|
|
994
1180
|
} catch (err) {
|
|
995
|
-
log.warn(
|
|
1181
|
+
log.warn(TAG13, `renameRemoteBranch: could not delete old ref ${oldRef}: ${err instanceof Error ? err.message : err}`);
|
|
996
1182
|
}
|
|
997
1183
|
try {
|
|
998
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
1307
|
+
log.info(TAG13, `PR created: ${result}`);
|
|
1122
1308
|
return result;
|
|
1123
1309
|
} catch (err) {
|
|
1124
|
-
log.error(
|
|
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
|
|
1318
|
+
return execFileSync7("gh", ["pr", "view", branchName, "--json", "url", "--jq", ".url"], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
1133
1319
|
case "gitlab": {
|
|
1134
|
-
const json =
|
|
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
|
-
|
|
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
|
-
|
|
1341
|
+
execFileSync7("glab", ["mr", "update", branchName, "--description", body], { cwd: worktreePath, stdio: "pipe" });
|
|
1156
1342
|
break;
|
|
1157
1343
|
}
|
|
1158
|
-
log.info(
|
|
1344
|
+
log.info(TAG13, `Updated existing PR body for ${branchName}`);
|
|
1159
1345
|
} catch (err) {
|
|
1160
|
-
log.warn(
|
|
1346
|
+
log.warn(TAG13, `Failed to update PR body: ${err instanceof Error ? err.message : err}`);
|
|
1161
1347
|
}
|
|
1162
1348
|
}
|
|
1163
|
-
var
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
2109
|
-
|
|
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 =
|
|
2116
|
-
const normalizedTarget =
|
|
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 =
|
|
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 :
|
|
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
|
|
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
|
-
|
|
2586
|
+
init_log();
|
|
2154
2587
|
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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
|
-
|
|
2162
|
-
|
|
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
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
|
2230
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
3077
|
+
return `bun install --frozen-lockfile${skip}`;
|
|
2271
3078
|
case "pnpm":
|
|
2272
|
-
return
|
|
3079
|
+
return `pnpm install --frozen-lockfile${skip}`;
|
|
2273
3080
|
case "yarn":
|
|
2274
|
-
return
|
|
3081
|
+
return `yarn install --frozen-lockfile${skip}`;
|
|
2275
3082
|
case "npm":
|
|
2276
|
-
return
|
|
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
|
|
2290
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
2291
|
-
var
|
|
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(
|
|
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(
|
|
3200
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
2394
3201
|
script = pkg.scripts?.test;
|
|
2395
3202
|
} catch (err) {
|
|
2396
|
-
log.warn(
|
|
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(
|
|
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(
|
|
3217
|
+
const pkg = JSON.parse(readFileSync2(`${dir}/package.json`, "utf-8"));
|
|
2411
3218
|
scripts = pkg.scripts ?? {};
|
|
2412
3219
|
} catch (err) {
|
|
2413
|
-
log.warn(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
2468
|
-
var
|
|
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
|
-
|
|
3285
|
+
execFileSync4("git", ["fetch", "origin", baseBranch], {
|
|
2479
3286
|
cwd: worktreePath,
|
|
2480
3287
|
stdio: "pipe"
|
|
2481
3288
|
});
|
|
2482
3289
|
} catch {
|
|
2483
|
-
log.warn(
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3328
|
+
log.info(TAG10, `[worker:${workerId}] Revert guard passed`);
|
|
2522
3329
|
}
|
|
2523
3330
|
}
|
|
2524
3331
|
if (config.verification.build) {
|
|
2525
|
-
log.info(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3348
|
+
log.info(TAG10, `[worker:${workerId}] Tests passed`);
|
|
2542
3349
|
}
|
|
2543
3350
|
}
|
|
2544
3351
|
if (config.verification.lint) {
|
|
2545
|
-
log.info(
|
|
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(
|
|
3355
|
+
log.warn(TAG10, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
2549
3356
|
} else {
|
|
2550
|
-
log.info(
|
|
3357
|
+
log.info(TAG10, `[worker:${workerId}] Lint passed`);
|
|
2551
3358
|
}
|
|
2552
3359
|
}
|
|
2553
3360
|
if (config.verification.deepReview) {
|
|
2554
|
-
log.info(
|
|
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(
|
|
3364
|
+
log.warn(TAG10, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
2558
3365
|
} else {
|
|
2559
|
-
log.info(
|
|
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(
|
|
3374
|
+
log.warn(TAG10, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
2568
3375
|
return [];
|
|
2569
3376
|
}
|
|
2570
3377
|
try {
|
|
2571
|
-
|
|
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(
|
|
3392
|
+
log.warn(TAG10, `No test command for detected toolchain in ${worktreePath} — skipping tests`);
|
|
2586
3393
|
return [];
|
|
2587
3394
|
}
|
|
2588
3395
|
try {
|
|
2589
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
3421
|
+
log.info(TAG10, `[worker:${workerId}] Auto-formatted worktree before commit/push`);
|
|
2615
3422
|
} catch (err) {
|
|
2616
|
-
log.warn(
|
|
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(
|
|
3429
|
+
log.info(TAG10, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2623
3430
|
return [];
|
|
2624
3431
|
}
|
|
2625
3432
|
try {
|
|
2626
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
2739
|
-
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
3076
|
-
var
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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/
|
|
3304
|
-
|
|
3305
|
-
import {
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
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
|
|
4140
|
+
return dockerProbe;
|
|
3327
4141
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
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
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
}
|
|
3337
|
-
|
|
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
|
|
3358
|
-
return
|
|
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
|
|
3361
|
-
const
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
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
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
3620
|
-
return
|
|
3621
|
-
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
4566
|
+
log.warn(TAG16, `Worktree already exists at ${worktreeDir}, cleaning up`);
|
|
3741
4567
|
cleanupWorktree(worktreeDir, branchName);
|
|
3742
4568
|
}
|
|
3743
4569
|
try {
|
|
3744
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
4582
|
+
log.warn(TAG16, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
3757
4583
|
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
3758
4584
|
try {
|
|
3759
|
-
|
|
4585
|
+
execFileSync8("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
3760
4586
|
cwd: repoRoot,
|
|
3761
4587
|
stdio: "pipe"
|
|
3762
4588
|
});
|
|
3763
4589
|
} catch {}
|
|
3764
4590
|
try {
|
|
3765
|
-
|
|
4591
|
+
execFileSync8("git", ["worktree", "prune", "--expire=now"], {
|
|
3766
4592
|
cwd: repoRoot,
|
|
3767
4593
|
stdio: "pipe"
|
|
3768
4594
|
});
|
|
3769
4595
|
} catch {}
|
|
3770
4596
|
try {
|
|
3771
|
-
|
|
4597
|
+
execFileSync8("git", ["branch", "-D", branchName], {
|
|
3772
4598
|
cwd: repoRoot,
|
|
3773
4599
|
stdio: "pipe"
|
|
3774
4600
|
});
|
|
3775
4601
|
} catch {}
|
|
3776
|
-
|
|
4602
|
+
execFileSync8("git", ["worktree", "add", "-B", branchName, worktreeDir, startRef], { cwd: repoRoot, stdio: "pipe" });
|
|
3777
4603
|
}
|
|
3778
|
-
log.info(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
4636
|
+
execFileSync8("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
3811
4637
|
cwd: repoRoot,
|
|
3812
4638
|
stdio: "pipe"
|
|
3813
4639
|
});
|
|
3814
|
-
log.info(
|
|
4640
|
+
log.info(TAG16, `Removed worktree: ${worktreePath}`);
|
|
3815
4641
|
} catch (err) {
|
|
3816
|
-
log.warn(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4701
|
+
execFileSync8("git", ["worktree", "remove", holderPath, "--force"], {
|
|
3876
4702
|
cwd: repoRoot,
|
|
3877
4703
|
stdio: "pipe"
|
|
3878
4704
|
});
|
|
3879
|
-
log.warn(
|
|
4705
|
+
log.warn(TAG16, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
|
|
3880
4706
|
} catch (err) {
|
|
3881
|
-
log.warn(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
};
|