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