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