@h-rig/bundle-default-lifecycle 0.0.6-alpha.157 → 0.0.6-alpha.158
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/src/cli.d.ts +1 -7
- package/dist/src/cli.js +5 -2
- package/dist/src/control-plane/completion-verification.js +1591 -118
- package/dist/src/control-plane/hooks/inject-context.d.ts +2 -0
- package/dist/src/control-plane/hooks/inject-context.js +175 -0
- package/dist/src/control-plane/hooks/shared.d.ts +11 -0
- package/dist/src/control-plane/hooks/shared.js +44 -0
- package/dist/src/control-plane/hooks/submodule-branch.d.ts +2 -0
- package/dist/src/control-plane/hooks/submodule-branch.js +432 -0
- package/dist/src/control-plane/hooks/task-runtime-start.d.ts +2 -0
- package/dist/src/control-plane/hooks/task-runtime-start.js +429 -0
- package/dist/src/control-plane/materialize-task-config.d.ts +29 -0
- package/dist/src/control-plane/materialize-task-config.js +95 -0
- package/dist/src/control-plane/native/git-ops.d.ts +67 -0
- package/dist/src/control-plane/native/git-ops.js +1390 -0
- package/dist/src/control-plane/policy.d.ts +3 -0
- package/dist/src/control-plane/policy.js +226 -0
- package/dist/src/control-plane/pr-automation.d.ts +2 -0
- package/dist/src/control-plane/pr-automation.js +26 -16
- package/dist/src/control-plane/pr-merge-gate-cap.d.ts +10 -0
- package/dist/src/control-plane/pr-merge-gate-cap.js +13 -0
- package/dist/src/control-plane/task-data.d.ts +13 -0
- package/dist/src/control-plane/task-data.js +12 -0
- package/dist/src/control-plane/task-verify.js +131 -59
- package/dist/src/control-plane/verifier.d.ts +1 -3
- package/dist/src/control-plane/verifier.js +133 -57
- package/dist/src/defaultPipeline.d.ts +1 -1
- package/dist/src/defaultPipeline.js +5 -2
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.js +1908 -290
- package/dist/src/native/closeout-runners.js +22 -2
- package/dist/src/native/github-auth-env.d.ts +2 -0
- package/dist/src/native/github-auth-env.js +25 -0
- package/dist/src/native/host-git.d.ts +6 -0
- package/dist/src/native/host-git.js +62 -0
- package/dist/src/native/in-process-closeout.d.ts +1 -3
- package/dist/src/native/in-process-closeout.js +0 -794
- package/dist/src/pipelineCloseout.js +1905 -185
- package/dist/src/plugin.js +2843 -145
- package/dist/src/stages/auto-merge.js +28 -16
- package/dist/src/stages/commit.js +28 -16
- package/dist/src/stages/isolation.d.ts +1 -1
- package/dist/src/stages/isolation.js +5 -3
- package/dist/src/stages/merge-gate.js +35 -3
- package/dist/src/stages/open-pr.js +28 -16
- package/dist/src/stages/push.js +28 -16
- package/dist/src/stages/source-closeout.js +28 -16
- package/package.json +29 -16
- package/dist/src/branch-naming.d.ts +0 -15
- package/dist/src/branch-naming.js +0 -33
- package/dist/src/closeoutEquivalence.d.ts +0 -37
- package/dist/src/closeoutEquivalence.js +0 -78
- package/dist/src/closeoutShadowHarness.d.ts +0 -27
- package/dist/src/closeoutShadowHarness.js +0 -29
|
@@ -16,6 +16,35 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
var __require = import.meta.require;
|
|
18
18
|
|
|
19
|
+
// packages/bundle-default-lifecycle/src/control-plane/task-data.ts
|
|
20
|
+
import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
|
|
21
|
+
import { defineCapability } from "@rig/core/capability";
|
|
22
|
+
import { requireInstalledCapability } from "@rig/core/capability-loaders";
|
|
23
|
+
function taskData() {
|
|
24
|
+
return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running the lifecycle.");
|
|
25
|
+
}
|
|
26
|
+
var TaskDataCap;
|
|
27
|
+
var init_task_data = __esm(() => {
|
|
28
|
+
TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// packages/bundle-default-lifecycle/src/control-plane/pr-merge-gate-cap.ts
|
|
32
|
+
var exports_pr_merge_gate_cap = {};
|
|
33
|
+
__export(exports_pr_merge_gate_cap, {
|
|
34
|
+
resolvePrMergeGateService: () => resolvePrMergeGateService
|
|
35
|
+
});
|
|
36
|
+
import { PR_MERGE_GATE } from "@rig/contracts";
|
|
37
|
+
import { defineCapability as defineCapability2 } from "@rig/core/capability";
|
|
38
|
+
import { resolvePluginHost } from "@rig/core/project-plugins";
|
|
39
|
+
async function resolvePrMergeGateService(projectRoot) {
|
|
40
|
+
const { host } = await resolvePluginHost(projectRoot);
|
|
41
|
+
return PrMergeGateCap.require(host);
|
|
42
|
+
}
|
|
43
|
+
var PrMergeGateCap;
|
|
44
|
+
var init_pr_merge_gate_cap = __esm(() => {
|
|
45
|
+
PrMergeGateCap = defineCapability2(PR_MERGE_GATE);
|
|
46
|
+
});
|
|
47
|
+
|
|
19
48
|
// packages/bundle-default-lifecycle/src/control-plane/pr-automation.ts
|
|
20
49
|
var exports_pr_automation = {};
|
|
21
50
|
__export(exports_pr_automation, {
|
|
@@ -33,11 +62,7 @@ __export(exports_pr_automation, {
|
|
|
33
62
|
buildPrAutomationBody: () => buildPrAutomationBody,
|
|
34
63
|
UPLOADED_SNAPSHOT_PR_MARKER: () => UPLOADED_SNAPSHOT_PR_MARKER
|
|
35
64
|
});
|
|
36
|
-
import { assertSafeGitBranchName } from "@rig/
|
|
37
|
-
import { runStrictPrMergeGate } from "@rig/pr-review-plugin";
|
|
38
|
-
import {
|
|
39
|
-
strictMergeHeadShaFromGate
|
|
40
|
-
} from "@rig/contracts";
|
|
65
|
+
import { assertSafeGitBranchName } from "@rig/core/safe-identifiers";
|
|
41
66
|
function positiveInt(value, fallback) {
|
|
42
67
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
43
68
|
}
|
|
@@ -340,7 +365,7 @@ async function commitRunChanges(input) {
|
|
|
340
365
|
async function closeIssueAfterMergedPr(input) {
|
|
341
366
|
await input.updateTaskSource(input.projectRoot, {
|
|
342
367
|
taskId: input.taskId,
|
|
343
|
-
sourceTask: input.sourceTask,
|
|
368
|
+
...input.sourceTask !== undefined ? { sourceTask: input.sourceTask } : {},
|
|
344
369
|
update: {
|
|
345
370
|
status: "closed",
|
|
346
371
|
comment: [
|
|
@@ -386,11 +411,12 @@ async function runRepoDefaultMerge(input) {
|
|
|
386
411
|
if (merge.mode === "off")
|
|
387
412
|
return;
|
|
388
413
|
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
389
|
-
const
|
|
414
|
+
const mergeGate = await resolvePrMergeGateService(input.projectRoot ?? input.cwd ?? process.cwd());
|
|
415
|
+
const matchHeadSha = mergeGate.resolveHeadSha({ result: input.strictGate, prUrl: input.prUrl, requireGreptile });
|
|
390
416
|
const method = merge.method ?? "repo-default";
|
|
391
417
|
const args = ["pr", "merge", input.prUrl];
|
|
392
418
|
if (method === "repo-default") {
|
|
393
|
-
args.push(await resolveRepoDefaultMergeFlag({ prUrl: input.prUrl, command: input.command, cwd: input.cwd }));
|
|
419
|
+
args.push(await resolveRepoDefaultMergeFlag({ prUrl: input.prUrl, command: input.command, ...input.cwd !== undefined ? { cwd: input.cwd } : {} }));
|
|
394
420
|
} else {
|
|
395
421
|
args.push(`--${method}`);
|
|
396
422
|
}
|
|
@@ -467,6 +493,7 @@ async function syncBranchAfterPrFeedback(input) {
|
|
|
467
493
|
}
|
|
468
494
|
async function runPrAutomation(input) {
|
|
469
495
|
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
496
|
+
const mergeGate = await resolvePrMergeGateService(input.projectRoot);
|
|
470
497
|
const prConfig = input.config?.pr ?? {};
|
|
471
498
|
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
472
499
|
if (prConfig.mode === "off" || prConfig.mode === "ask") {
|
|
@@ -476,7 +503,7 @@ async function runPrAutomation(input) {
|
|
|
476
503
|
taskId: input.taskId,
|
|
477
504
|
runId: input.runId,
|
|
478
505
|
summary: input.sourceTask?.title ? `Rig completed: ${input.sourceTask.title}` : null,
|
|
479
|
-
uploadedSnapshot: input.uploadedSnapshot
|
|
506
|
+
...input.uploadedSnapshot !== undefined ? { uploadedSnapshot: input.uploadedSnapshot } : {}
|
|
480
507
|
});
|
|
481
508
|
if (input.gitCommand) {
|
|
482
509
|
await pushBranchSyncedWithOrigin({ projectRoot: input.projectRoot, branch, gitCommand: input.gitCommand });
|
|
@@ -548,16 +575,16 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
548
575
|
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
549
576
|
continue;
|
|
550
577
|
}
|
|
551
|
-
const gate = await
|
|
578
|
+
const gate = await mergeGate.runGate({
|
|
552
579
|
projectRoot: input.projectRoot,
|
|
553
580
|
prUrl,
|
|
554
581
|
taskId: input.taskId,
|
|
555
582
|
runId: input.runId,
|
|
556
583
|
cycle: iteration,
|
|
557
584
|
command: input.command,
|
|
558
|
-
artifactRoot: input.artifactRoot,
|
|
585
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
559
586
|
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
560
|
-
greptileApi
|
|
587
|
+
...requireGreptile && input.greptileApi ? { greptileApi: input.greptileApi } : {},
|
|
561
588
|
requireGreptile
|
|
562
589
|
});
|
|
563
590
|
latestFeedback = [...gate.actionableFeedback];
|
|
@@ -587,22 +614,22 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
587
614
|
}
|
|
588
615
|
if (gate.approved) {
|
|
589
616
|
pendingElapsedMs = 0;
|
|
590
|
-
const finalGate = await
|
|
617
|
+
const finalGate = await mergeGate.runGate({
|
|
591
618
|
projectRoot: input.projectRoot,
|
|
592
619
|
prUrl,
|
|
593
620
|
taskId: input.taskId,
|
|
594
621
|
runId: input.runId,
|
|
595
622
|
cycle: iteration,
|
|
596
623
|
command: input.command,
|
|
597
|
-
artifactRoot: input.artifactRoot,
|
|
624
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
598
625
|
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
599
|
-
greptileApi
|
|
626
|
+
...requireGreptile && input.greptileApi ? { greptileApi: input.greptileApi } : {},
|
|
600
627
|
requireGreptile,
|
|
601
628
|
final: true
|
|
602
629
|
});
|
|
603
630
|
if (finalGate.approved) {
|
|
604
631
|
await input.lifecycle?.onMergeStarted?.({ prUrl });
|
|
605
|
-
await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
|
|
632
|
+
await runRepoDefaultMerge({ prUrl, ...input.config !== undefined ? { config: input.config } : {}, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
|
|
606
633
|
await input.lifecycle?.onMerged?.({ prUrl });
|
|
607
634
|
return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
|
|
608
635
|
}
|
|
@@ -649,6 +676,7 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
649
676
|
}
|
|
650
677
|
var UPLOADED_SNAPSHOT_PR_MARKER = "<!-- rig:uploaded-snapshot -->", RIG_RUNTIME_COMMIT_EXCLUDES, GREPTILE_REREVIEW_MARKER_PREFIX = "rig:greptile-rereview";
|
|
651
678
|
var init_pr_automation = __esm(() => {
|
|
679
|
+
init_pr_merge_gate_cap();
|
|
652
680
|
RIG_RUNTIME_COMMIT_EXCLUDES = [
|
|
653
681
|
".rig",
|
|
654
682
|
"artifacts",
|
|
@@ -656,31 +684,1619 @@ var init_pr_automation = __esm(() => {
|
|
|
656
684
|
];
|
|
657
685
|
});
|
|
658
686
|
|
|
659
|
-
// packages/bundle-default-lifecycle/src/
|
|
660
|
-
import { existsSync,
|
|
687
|
+
// packages/bundle-default-lifecycle/src/native/github-auth-env.ts
|
|
688
|
+
import { existsSync, readFileSync } from "fs";
|
|
689
|
+
function cleanToken(value) {
|
|
690
|
+
const trimmed = value?.trim() ?? "";
|
|
691
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
692
|
+
}
|
|
693
|
+
function authStateToken(env = process.env) {
|
|
694
|
+
const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
|
|
695
|
+
if (!file || !existsSync(file))
|
|
696
|
+
return null;
|
|
697
|
+
try {
|
|
698
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
699
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
700
|
+
} catch {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function resolveGitHubAuthToken(env = process.env) {
|
|
705
|
+
return cleanToken(env.RIG_GITHUB_TOKEN) ?? cleanToken(env.GH_TOKEN) ?? cleanToken(env.GITHUB_TOKEN) ?? authStateToken(env);
|
|
706
|
+
}
|
|
707
|
+
var init_github_auth_env = () => {};
|
|
708
|
+
|
|
709
|
+
// packages/bundle-default-lifecycle/src/native/host-git.ts
|
|
710
|
+
import { existsSync as existsSync2 } from "fs";
|
|
661
711
|
import { resolve } from "path";
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
712
|
+
function isRuntimeGatewayGitPath(candidate) {
|
|
713
|
+
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
714
|
+
}
|
|
715
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
716
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
717
|
+
}
|
|
718
|
+
function resolveHostGitBinary() {
|
|
719
|
+
const candidates = [
|
|
720
|
+
process.env.RIG_GIT_BIN?.trim() || "",
|
|
721
|
+
"/usr/bin/git",
|
|
722
|
+
"/opt/homebrew/bin/git",
|
|
723
|
+
"/usr/local/bin/git"
|
|
724
|
+
];
|
|
725
|
+
const bunResolved = Bun.which("git");
|
|
726
|
+
if (bunResolved && !isRuntimeGatewayGitPath(bunResolved)) {
|
|
727
|
+
candidates.push(bunResolved);
|
|
728
|
+
}
|
|
729
|
+
for (const candidate of candidates) {
|
|
730
|
+
if (!candidate || isRuntimeGatewayGitPath(candidate)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (existsSync2(candidate)) {
|
|
734
|
+
return candidate;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return "git";
|
|
738
|
+
}
|
|
739
|
+
function resolveGithubCliBinary(options = {}) {
|
|
740
|
+
const candidates = new Set;
|
|
741
|
+
const explicit = process.env.RIG_GH_BIN?.trim();
|
|
742
|
+
if (explicit) {
|
|
743
|
+
candidates.add(explicit);
|
|
744
|
+
}
|
|
745
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
746
|
+
candidates.add(candidate);
|
|
747
|
+
}
|
|
748
|
+
if (options.scanPath) {
|
|
749
|
+
for (const entry of (process.env.PATH || "").split(":").map((e) => e.trim()).filter(Boolean)) {
|
|
750
|
+
candidates.add(resolve(entry, "gh"));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const bunResolved = Bun.which("gh");
|
|
754
|
+
if (bunResolved) {
|
|
755
|
+
candidates.add(bunResolved);
|
|
756
|
+
}
|
|
757
|
+
for (const candidate of candidates) {
|
|
758
|
+
if (candidate && existsSync2(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
759
|
+
return candidate;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return "";
|
|
763
|
+
}
|
|
764
|
+
var init_host_git = () => {};
|
|
765
|
+
|
|
766
|
+
// packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
|
|
767
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
768
|
+
import { tmpdir } from "os";
|
|
769
|
+
import { dirname, isAbsolute, resolve as resolve2 } from "path";
|
|
770
|
+
import { fileURLToPath } from "url";
|
|
771
|
+
import { loadDotEnvSecrets, resolveRuntimeSecrets } from "@rig/core/baked-secrets";
|
|
772
|
+
import { loadRuntimeContext, loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
|
|
773
|
+
import { nowIso, runCapture as baseRunCapture } from "@rig/core/exec";
|
|
774
|
+
import { resolveCheckoutRoot as resolveMonorepoRoot } from "@rig/core/checkout-root";
|
|
775
|
+
import { getScopeRules } from "@rig/core/scope-rules";
|
|
776
|
+
import { safePathSegment } from "@rig/core/safe-identifiers";
|
|
777
|
+
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
778
|
+
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
779
|
+
if (runtimeWorkspace && existsSync3(resolve2(runtimeWorkspace, ".git"))) {
|
|
780
|
+
return resolve2(runtimeWorkspace);
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
return resolveMonorepoRoot(projectRoot);
|
|
784
|
+
} catch {
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function escapeRegExp(value) {
|
|
789
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
790
|
+
}
|
|
791
|
+
function safeCurrentTaskId(projectRoot) {
|
|
792
|
+
try {
|
|
793
|
+
const taskId = taskData().currentTaskId(projectRoot);
|
|
794
|
+
return /^bd-[a-z0-9-]+$/.test(taskId) ? taskId : "";
|
|
795
|
+
} catch {
|
|
796
|
+
return "";
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function gitCmd(projectRoot, repoRoot, ...args) {
|
|
800
|
+
return [resolveHostGitBinary(), "-C", repoRoot, ...args];
|
|
801
|
+
}
|
|
802
|
+
function shouldScopeGitCommit(args, hasTaskContext) {
|
|
803
|
+
if (!hasTaskContext) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
return args.includes("--scoped");
|
|
807
|
+
}
|
|
808
|
+
function gitStatus(projectRoot, taskId) {
|
|
809
|
+
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
|
|
810
|
+
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
811
|
+
const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
|
|
812
|
+
console.log("=== Git Flow Status ===");
|
|
813
|
+
if (resolvedTask) {
|
|
814
|
+
console.log(`Task: ${resolvedTask}`);
|
|
815
|
+
console.log(`Expected monorepo branch: ${expected}`);
|
|
816
|
+
} else {
|
|
817
|
+
console.log("Task: (none active)");
|
|
818
|
+
}
|
|
819
|
+
console.log("");
|
|
820
|
+
printRepoStatus(projectRoot, "project-rig", projectRoot, "");
|
|
821
|
+
const monorepoPath = monorepoRoot || resolveMonorepoRoot(projectRoot);
|
|
822
|
+
if (monorepoPath !== projectRoot) {
|
|
823
|
+
printRepoStatus(projectRoot, "monorepo", monorepoPath, expected);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function gitChanged(projectRoot, taskId, scoped) {
|
|
827
|
+
if (scoped) {
|
|
828
|
+
const resolvedTask = taskId || taskData().currentTaskId(projectRoot);
|
|
829
|
+
if (!resolvedTask) {
|
|
830
|
+
throw new Error("No task specified and no active task in session. Use --task or omit --scoped.");
|
|
831
|
+
}
|
|
832
|
+
return taskData().changedFilesForTask(projectRoot, resolvedTask, true);
|
|
833
|
+
}
|
|
834
|
+
return taskData().changedFilesForTask(projectRoot, taskId || taskData().currentTaskId(projectRoot) || "", false);
|
|
835
|
+
}
|
|
836
|
+
function gitPreflight(projectRoot, taskId, strict) {
|
|
837
|
+
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
|
|
838
|
+
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
839
|
+
const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
|
|
840
|
+
console.log("=== Git Flow Preflight ===");
|
|
841
|
+
let issues = 0;
|
|
842
|
+
if (!existsSync3(resolve2(projectRoot, ".git"))) {
|
|
843
|
+
console.log(`ERROR: project root is not a git repo (${projectRoot})`);
|
|
844
|
+
issues += 1;
|
|
845
|
+
}
|
|
846
|
+
if (monorepoRoot && existsSync3(resolve2(monorepoRoot, ".git"))) {
|
|
847
|
+
const monoBranch = branchName(projectRoot, monorepoRoot);
|
|
848
|
+
if (expected && monoBranch !== expected) {
|
|
849
|
+
console.log(`WARN: monorepo branch is ${monoBranch}, expected ${expected} for task ${resolvedTask}`);
|
|
850
|
+
if (strict) {
|
|
851
|
+
issues += 1;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const monoChanges = changeCount(projectRoot, monorepoRoot);
|
|
855
|
+
if (monoChanges > 0 && !monoBranch.startsWith("rig/")) {
|
|
856
|
+
console.log(`WARN: monorepo has uncommitted changes on non-rig branch (${monoBranch})`);
|
|
857
|
+
issues += 1;
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
console.log(`WARN: monorepo repo unavailable.`);
|
|
861
|
+
}
|
|
862
|
+
const projectChanges = changeCount(projectRoot, projectRoot);
|
|
863
|
+
if (projectChanges > 0) {
|
|
864
|
+
console.log(`INFO: project-rig has ${projectChanges} changed file(s).`);
|
|
865
|
+
}
|
|
866
|
+
if (issues > 0) {
|
|
867
|
+
console.log(`Preflight: ${issues} issue(s) detected.`);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
console.log("Preflight: OK");
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
|
|
874
|
+
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
875
|
+
if (!resolvedTask) {
|
|
876
|
+
throw new Error("No task specified and no active task in session.");
|
|
877
|
+
}
|
|
878
|
+
const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
|
|
879
|
+
const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
|
|
880
|
+
if (!existsSync3(resolve2(repoRoot, ".git"))) {
|
|
881
|
+
throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
|
|
882
|
+
}
|
|
883
|
+
const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
|
|
884
|
+
const branchTarget = `rig/${branchId}`;
|
|
885
|
+
const current = branchName(projectRoot, repoRoot);
|
|
886
|
+
if (current === branchTarget) {
|
|
887
|
+
console.log(`${repoLabel} branch: already on ${branchTarget}`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const hasBranch = runCapture(gitCmd(projectRoot, repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchTarget}`), projectRoot).exitCode === 0;
|
|
891
|
+
const cmd = hasBranch && current === "HEAD" ? gitCmd(projectRoot, repoRoot, "checkout", "-B", branchTarget) : hasBranch ? gitCmd(projectRoot, repoRoot, "checkout", branchTarget) : gitCmd(projectRoot, repoRoot, "checkout", "-b", branchTarget);
|
|
892
|
+
const checkout = runCapture(cmd, projectRoot);
|
|
893
|
+
if (checkout.exitCode !== 0) {
|
|
894
|
+
throw new Error(`Failed to sync ${repoLabel.toLowerCase()} branch: ${checkout.stderr || checkout.stdout}`);
|
|
895
|
+
}
|
|
896
|
+
const action = hasBranch && current === "HEAD" ? "reset" : hasBranch ? "checked out" : "created";
|
|
897
|
+
console.log(`${repoLabel} branch: ${action} ${branchTarget}`);
|
|
898
|
+
}
|
|
899
|
+
function gitCommit(options) {
|
|
900
|
+
const { projectRoot } = options;
|
|
901
|
+
const resolvedTask = options.taskId || safeCurrentTaskId(projectRoot);
|
|
902
|
+
const baseMessage = options.message || (resolvedTask ? `rig: ${resolvedTask}` : "rig: harness update");
|
|
903
|
+
const changedFilesManifest = resolvedTask && options.scoped === true ? refreshChangedFilesManifest(projectRoot, resolvedTask) : "";
|
|
904
|
+
const changedFiles = resolvedTask && options.scoped === true ? readChangedFilesManifest(projectRoot, resolvedTask) : resolvedTask ? taskData().changedFilesForTask(projectRoot, resolvedTask, false) : [];
|
|
905
|
+
if (options.target === "project" || options.target === "both") {
|
|
906
|
+
if (resolvedTask) {
|
|
907
|
+
gitSyncBranch(projectRoot, resolvedTask, "project");
|
|
908
|
+
}
|
|
909
|
+
const projectFiles = resolveScopedStageFilesForRepo(projectRoot, projectRoot, resolvedTask, changedFiles);
|
|
910
|
+
commitRepo(projectRoot, projectRoot, "project-rig", options.target === "both" ? `${baseMessage} [harness]` : baseMessage, options.allowEmpty, options.scoped === true, projectFiles, changedFilesManifest);
|
|
911
|
+
}
|
|
912
|
+
if (options.target === "monorepo" || options.target === "both") {
|
|
913
|
+
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot);
|
|
914
|
+
if (resolvedTask) {
|
|
915
|
+
gitSyncBranch(projectRoot, resolvedTask, "monorepo");
|
|
916
|
+
}
|
|
917
|
+
const monorepoFiles = resolveScopedStageFilesForRepo(projectRoot, monorepoRoot, resolvedTask, changedFiles);
|
|
918
|
+
commitRepo(projectRoot, monorepoRoot, "monorepo", options.target === "both" ? `${baseMessage} [monorepo]` : baseMessage, options.allowEmpty, options.scoped === true, monorepoFiles, changedFilesManifest);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function gitSnapshot(projectRoot, taskId, outputPath) {
|
|
922
|
+
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
|
|
923
|
+
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
924
|
+
const output = outputPath || (resolvedTask ? resolveArtifactSnapshot(projectRoot, resolvedTask) : resolve2(resolve2(projectRoot, ".rig", "state"), "git-state.txt"));
|
|
925
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
926
|
+
const lines = ["# Git Snapshot", `timestamp: ${nowIso()}`];
|
|
927
|
+
if (resolvedTask) {
|
|
928
|
+
lines.push(`task: ${resolvedTask}`);
|
|
929
|
+
}
|
|
930
|
+
lines.push("");
|
|
931
|
+
lines.push(...snapshotRepo(projectRoot, "project-rig", projectRoot));
|
|
932
|
+
if (monorepoRoot && monorepoRoot !== projectRoot) {
|
|
933
|
+
lines.push(...snapshotRepo(projectRoot, "monorepo", monorepoRoot));
|
|
934
|
+
}
|
|
935
|
+
writeFileSync(output, `${lines.join(`
|
|
936
|
+
`)}
|
|
937
|
+
`, "utf-8");
|
|
938
|
+
return output;
|
|
939
|
+
}
|
|
940
|
+
function gitOpenPr(options) {
|
|
941
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
942
|
+
if (!gh) {
|
|
943
|
+
throw new Error("gh CLI is required for open-pr. Install and authenticate with: gh auth login");
|
|
944
|
+
}
|
|
945
|
+
const taskId = options.taskId || safeCurrentTaskId(options.projectRoot);
|
|
946
|
+
const target = options.target || (taskId ? "monorepo" : "project");
|
|
947
|
+
let repoRoot = options.projectRoot;
|
|
948
|
+
let repoLabel = "project-rig";
|
|
949
|
+
const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
|
|
950
|
+
if (target === "monorepo") {
|
|
951
|
+
repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot(options.projectRoot);
|
|
952
|
+
repoLabel = "monorepo";
|
|
953
|
+
if (taskId) {
|
|
954
|
+
gitSyncBranch(options.projectRoot, taskId, "monorepo");
|
|
955
|
+
}
|
|
956
|
+
} else if (taskId) {
|
|
957
|
+
gitSyncBranch(options.projectRoot, taskId, "project");
|
|
958
|
+
}
|
|
959
|
+
if (!existsSync3(resolve2(repoRoot, ".git"))) {
|
|
960
|
+
throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
|
|
961
|
+
}
|
|
962
|
+
const branch = branchName(options.projectRoot, repoRoot);
|
|
963
|
+
if (!branch || branch === "HEAD") {
|
|
964
|
+
throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
|
|
965
|
+
}
|
|
966
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
967
|
+
const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
|
|
968
|
+
const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
|
|
969
|
+
refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
|
|
970
|
+
let reviewer = (options.reviewer || "").trim();
|
|
971
|
+
let reviewerSource = reviewer ? "flag" : undefined;
|
|
972
|
+
if (!reviewer && taskId) {
|
|
973
|
+
reviewer = defaultReviewerForTask(options.projectRoot, taskId);
|
|
974
|
+
if (reviewer) {
|
|
975
|
+
reviewerSource = "task-config";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!reviewer) {
|
|
979
|
+
reviewer = inferReviewerFromChangedFiles(options.projectRoot, repoRoot, base, branch);
|
|
980
|
+
if (reviewer) {
|
|
981
|
+
reviewerSource = "changed-files";
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (!reviewer) {
|
|
985
|
+
reviewer = (process.env.RIG_PR_REVIEWER || "").trim();
|
|
986
|
+
if (reviewer) {
|
|
987
|
+
reviewerSource = "env";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
let title = options.title || "";
|
|
991
|
+
if (!title) {
|
|
992
|
+
if (taskId) {
|
|
993
|
+
title = `rig: ${taskId}`;
|
|
994
|
+
} else {
|
|
995
|
+
title = `rig: update ${branch}`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const body = options.body || [
|
|
999
|
+
"## Summary",
|
|
1000
|
+
"- Automated task output prepared in isolated runtime.",
|
|
1001
|
+
"",
|
|
1002
|
+
"## Task",
|
|
1003
|
+
`- beads: ${taskId || "n/a"}`,
|
|
1004
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
1005
|
+
"",
|
|
1006
|
+
"## Review",
|
|
1007
|
+
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
1008
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
1009
|
+
].join(`
|
|
1010
|
+
`);
|
|
1011
|
+
const preCheck = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
1012
|
+
const preCheckEntry = preCheck.exitCode === 0 ? preCheck.stdout.trim() : "";
|
|
1013
|
+
if (preCheckEntry && preCheckEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
|
|
1014
|
+
const mergedPr = JSON.parse(preCheckEntry);
|
|
1015
|
+
console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
|
|
1016
|
+
const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
|
|
1017
|
+
if (taskId)
|
|
1018
|
+
writePrMetadata(options.projectRoot, taskId, result2);
|
|
1019
|
+
return result2;
|
|
1020
|
+
}
|
|
1021
|
+
const pushArgs = gitCmd(options.projectRoot, repoRoot, "push", "-u", networkRemote, branch);
|
|
1022
|
+
const fetchResult = runCapture(gitCmd(options.projectRoot, repoRoot, "fetch", networkRemote, branch), repoRoot);
|
|
1023
|
+
if (fetchResult.exitCode === 0) {
|
|
1024
|
+
const remoteAhead = runCapture(gitCmd(options.projectRoot, repoRoot, "log", "--oneline", `HEAD..${networkRemote}/${branch}`), repoRoot);
|
|
1025
|
+
if (remoteAhead.exitCode === 0 && remoteAhead.stdout.trim()) {
|
|
1026
|
+
console.log(`Remote branch has diverged \u2014 force pushing task-owned branch ${branch} with --force-with-lease...`);
|
|
1027
|
+
pushArgs.splice(4, 0, "--force-with-lease");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
runOrThrow(options.projectRoot, pushArgs, `Failed to push branch ${branch} in ${repoLabel}`);
|
|
1031
|
+
const existing = runCapture(withGhRepo([gh, "pr", "list", "--state", "open", "--head", branch, "--json", "url", "--jq", ".[0].url"], repoNameWithOwner), repoRoot);
|
|
1032
|
+
const existingUrl = existing.exitCode === 0 ? existing.stdout.trim() : "";
|
|
1033
|
+
if (!existingUrl || existingUrl === "null") {
|
|
1034
|
+
const merged = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
1035
|
+
const mergedEntry = merged.exitCode === 0 ? merged.stdout.trim() : "";
|
|
1036
|
+
if (mergedEntry && mergedEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
|
|
1037
|
+
const mergedPr = JSON.parse(mergedEntry);
|
|
1038
|
+
console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
|
|
1039
|
+
const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
|
|
1040
|
+
if (taskId)
|
|
1041
|
+
writePrMetadata(options.projectRoot, taskId, result2);
|
|
1042
|
+
return result2;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
let prUrl = "";
|
|
1046
|
+
if (existingUrl && existingUrl !== "null") {
|
|
1047
|
+
prUrl = existingUrl;
|
|
1048
|
+
} else {
|
|
1049
|
+
const createArgs = [
|
|
1050
|
+
gh,
|
|
1051
|
+
"pr",
|
|
1052
|
+
"create",
|
|
1053
|
+
...ghRepoArgs(repoNameWithOwner),
|
|
1054
|
+
"--base",
|
|
1055
|
+
base,
|
|
1056
|
+
"--head",
|
|
1057
|
+
branch,
|
|
1058
|
+
"--title",
|
|
1059
|
+
title,
|
|
1060
|
+
"--body",
|
|
1061
|
+
body
|
|
1062
|
+
];
|
|
1063
|
+
if (options.draft) {
|
|
1064
|
+
createArgs.push("--draft");
|
|
1065
|
+
}
|
|
1066
|
+
const created = runCapture(createArgs, repoRoot);
|
|
1067
|
+
if (created.exitCode !== 0) {
|
|
1068
|
+
throw new Error(`Failed to create PR in ${repoLabel}: ${created.stderr || created.stdout}`);
|
|
1069
|
+
}
|
|
1070
|
+
prUrl = created.stdout.trim();
|
|
1071
|
+
}
|
|
1072
|
+
if (!prUrl) {
|
|
1073
|
+
throw new Error(`Failed to resolve PR URL for branch ${branch}.`);
|
|
1074
|
+
}
|
|
1075
|
+
assertPrHasNoGitConflicts(readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl), repoLabel, base);
|
|
1076
|
+
if (reviewer) {
|
|
1077
|
+
const edit = runCapture(withGhRepo([gh, "pr", "edit", prUrl, "--add-reviewer", reviewer], repoNameWithOwner), repoRoot);
|
|
1078
|
+
if (edit.exitCode !== 0) {
|
|
1079
|
+
throw new Error(`Failed to assign reviewer '${reviewer}': ${edit.stderr || edit.stdout}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const result = {
|
|
1083
|
+
url: prUrl,
|
|
1084
|
+
...reviewer ? { reviewer } : {},
|
|
1085
|
+
...reviewerSource ? { reviewerSource } : {},
|
|
1086
|
+
target,
|
|
1087
|
+
repoLabel,
|
|
1088
|
+
branch,
|
|
1089
|
+
base
|
|
1090
|
+
};
|
|
1091
|
+
if (taskId) {
|
|
1092
|
+
writePrMetadata(options.projectRoot, taskId, result);
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
1097
|
+
const lines = [];
|
|
1098
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
1099
|
+
if (runId) {
|
|
1100
|
+
lines.push(`- Run: ${runId}`);
|
|
1101
|
+
}
|
|
1102
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
1103
|
+
if (closeout) {
|
|
1104
|
+
lines.push(`- ${closeout}`);
|
|
1105
|
+
}
|
|
1106
|
+
return lines;
|
|
1107
|
+
}
|
|
1108
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
1109
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
1110
|
+
if (sourceIssueId) {
|
|
1111
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
1112
|
+
if (match?.[1] && match[2]) {
|
|
1113
|
+
const sourceRepo = match[1];
|
|
1114
|
+
const issueNumber = match[2];
|
|
1115
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
1119
|
+
}
|
|
1120
|
+
function resolveTaskBranchRef(projectRoot, taskId) {
|
|
1121
|
+
return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
|
|
1122
|
+
}
|
|
1123
|
+
function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
|
|
1124
|
+
const view = runCapture(withGhRepo([
|
|
1125
|
+
gh,
|
|
1126
|
+
"pr",
|
|
1127
|
+
"view",
|
|
1128
|
+
prUrl,
|
|
1129
|
+
"--json",
|
|
1130
|
+
"state,isDraft,url,mergedAt,autoMergeRequest,mergeable,mergeStateStatus,reviewDecision,headRefOid,statusCheckRollup",
|
|
1131
|
+
"--jq",
|
|
1132
|
+
"."
|
|
1133
|
+
], repoNameWithOwner), repoRoot);
|
|
1134
|
+
if (view.exitCode !== 0) {
|
|
1135
|
+
throw new Error(`Failed to inspect PR ${prUrl}: ${view.stderr || view.stdout}`);
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(view.stdout);
|
|
1139
|
+
return {
|
|
1140
|
+
state: parsed.state || "OPEN",
|
|
1141
|
+
isDraft: parsed.isDraft === true,
|
|
1142
|
+
url: typeof parsed.url === "string" ? parsed.url : prUrl,
|
|
1143
|
+
mergedAt: typeof parsed.mergedAt === "string" ? parsed.mergedAt : null,
|
|
1144
|
+
autoMergeRequest: parsed.autoMergeRequest ?? null,
|
|
1145
|
+
mergeable: typeof parsed.mergeable === "string" ? parsed.mergeable : "",
|
|
1146
|
+
mergeStateStatus: typeof parsed.mergeStateStatus === "string" ? parsed.mergeStateStatus : "",
|
|
1147
|
+
reviewDecision: typeof parsed.reviewDecision === "string" ? parsed.reviewDecision : "",
|
|
1148
|
+
headRefOid: typeof parsed.headRefOid === "string" ? parsed.headRefOid : null,
|
|
1149
|
+
statusCheckRollup: Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
|
|
1150
|
+
};
|
|
1151
|
+
} catch {
|
|
1152
|
+
return {
|
|
1153
|
+
state: "OPEN",
|
|
1154
|
+
isDraft: false,
|
|
1155
|
+
url: prUrl,
|
|
1156
|
+
mergedAt: null,
|
|
1157
|
+
autoMergeRequest: null,
|
|
1158
|
+
mergeable: "",
|
|
1159
|
+
mergeStateStatus: "",
|
|
1160
|
+
reviewDecision: "",
|
|
1161
|
+
headRefOid: null,
|
|
1162
|
+
statusCheckRollup: []
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function hasSatisfiedStatusChecks(prState) {
|
|
1167
|
+
if (prState.statusCheckRollup.length === 0) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
return prState.statusCheckRollup.every((entry) => {
|
|
1171
|
+
if (entry.__typename === "CheckRun") {
|
|
1172
|
+
const status = entry.status?.toUpperCase() || "";
|
|
1173
|
+
const conclusion = entry.conclusion?.toUpperCase() || "";
|
|
1174
|
+
return status === "COMPLETED" && (conclusion === "SUCCESS" || conclusion === "SKIPPED" || conclusion === "NEUTRAL");
|
|
1175
|
+
}
|
|
1176
|
+
if (entry.__typename === "StatusContext") {
|
|
1177
|
+
return (entry.state?.toUpperCase() || "") === "SUCCESS";
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
function canAdminMergeApprovedPr(prState) {
|
|
1183
|
+
return prState.state === "OPEN" && prState.autoMergeRequest !== null && prState.mergeable.toUpperCase() === "MERGEABLE" && prState.reviewDecision.toUpperCase() === "APPROVED" && hasSatisfiedStatusChecks(prState);
|
|
1184
|
+
}
|
|
1185
|
+
function gitMergePr(options) {
|
|
1186
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
1187
|
+
if (!gh) {
|
|
1188
|
+
throw new Error("gh CLI is required for merge-pr. Install and authenticate with: gh auth login");
|
|
1189
|
+
}
|
|
1190
|
+
const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
|
|
1191
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
1192
|
+
if (!existsSync3(resolve2(repoRoot, ".git"))) {
|
|
1193
|
+
throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
|
|
1194
|
+
}
|
|
1195
|
+
const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
1196
|
+
const state = prState.state;
|
|
1197
|
+
const isDraft = prState.isDraft;
|
|
1198
|
+
assertPrHasNoGitConflicts(prState, options.pr.repoLabel, options.pr.base);
|
|
1199
|
+
if (state === "MERGED") {
|
|
1200
|
+
console.log(`PR already merged (${options.pr.repoLabel}): ${options.pr.url}`);
|
|
1201
|
+
return { status: "already-merged", url: options.pr.url };
|
|
1202
|
+
}
|
|
1203
|
+
if (state !== "OPEN") {
|
|
1204
|
+
throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
|
|
1205
|
+
}
|
|
1206
|
+
if (isDraft) {
|
|
1207
|
+
throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
|
|
1208
|
+
}
|
|
1209
|
+
const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
|
|
1210
|
+
const method = options.method || "squash";
|
|
1211
|
+
mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
|
|
1212
|
+
mergeArgs.push("--match-head-commit", options.matchHeadCommit);
|
|
1213
|
+
if (options.deleteBranch !== false) {
|
|
1214
|
+
mergeArgs.push("--delete-branch");
|
|
1215
|
+
}
|
|
1216
|
+
const directMerge = runCapture(mergeArgs, repoRoot);
|
|
1217
|
+
if (directMerge.exitCode === 0) {
|
|
1218
|
+
console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
|
|
1219
|
+
return { status: "merged", url: options.pr.url };
|
|
1220
|
+
}
|
|
1221
|
+
const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
1222
|
+
if (canAdminMergeApprovedPr(postDirectState)) {
|
|
1223
|
+
const adminMergeArgs = [...mergeArgs, "--admin"];
|
|
1224
|
+
const adminMerge = runCapture(adminMergeArgs, repoRoot);
|
|
1225
|
+
if (adminMerge.exitCode === 0) {
|
|
1226
|
+
const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
1227
|
+
if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
|
|
1228
|
+
console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
|
|
1229
|
+
return { status: "merged", url: options.pr.url };
|
|
1230
|
+
}
|
|
1231
|
+
throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
|
|
1232
|
+
}
|
|
1233
|
+
const adminMergeMessage = `${adminMerge.stderr}
|
|
1234
|
+
${adminMerge.stdout}`.trim();
|
|
1235
|
+
if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
|
|
1236
|
+
throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const directMergeMessage = `${directMerge.stderr}
|
|
1240
|
+
${directMerge.stdout}`.trim();
|
|
1241
|
+
throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
|
|
1242
|
+
}
|
|
1243
|
+
function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
|
|
1244
|
+
const mergeable = prState.mergeable.toUpperCase();
|
|
1245
|
+
const mergeStateStatus = prState.mergeStateStatus.toUpperCase();
|
|
1246
|
+
if (mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY") {
|
|
1247
|
+
throw new Error(`PR ${prState.url || "unknown"} conflicts with ${baseRef} in ${repoLabel} (mergeable=${prState.mergeable || "unknown"}, mergeStateStatus=${prState.mergeStateStatus || "unknown"}). Rebase or merge ${baseRef} and resolve conflicts before completion-verification.`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function writePrMetadata(projectRoot, taskId, result) {
|
|
1251
|
+
const dir = taskData().artifactDirForId(projectRoot, taskId);
|
|
1252
|
+
mkdirSync(dir, { recursive: true });
|
|
1253
|
+
const path = resolve2(dir, "pr-state.json");
|
|
1254
|
+
let prs = {};
|
|
1255
|
+
if (existsSync3(path)) {
|
|
1256
|
+
try {
|
|
1257
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1258
|
+
if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
|
|
1259
|
+
prs = parsed.prs;
|
|
1260
|
+
}
|
|
1261
|
+
} catch {
|
|
1262
|
+
prs = {};
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
prs[result.target] = result;
|
|
1266
|
+
const primary = prs.monorepo || prs.project;
|
|
1267
|
+
const artifact = {
|
|
1268
|
+
task_id: taskId,
|
|
1269
|
+
prs,
|
|
1270
|
+
...primary || {},
|
|
1271
|
+
updated_at: nowIso()
|
|
1272
|
+
};
|
|
1273
|
+
writeFileSync(path, `${JSON.stringify(artifact, null, 2)}
|
|
1274
|
+
`, "utf-8");
|
|
1275
|
+
}
|
|
1276
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
1277
|
+
const path = resolve2(taskData().artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
1278
|
+
if (!existsSync3(path)) {
|
|
1279
|
+
return [];
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1283
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1284
|
+
return [];
|
|
1285
|
+
}
|
|
1286
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
1287
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
1288
|
+
}
|
|
1289
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
1290
|
+
} catch {
|
|
1291
|
+
return [];
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function resolveArtifactSnapshot(projectRoot, taskId) {
|
|
1295
|
+
return resolve2(taskData().artifactDirForId(projectRoot, taskId), "git-state.txt");
|
|
1296
|
+
}
|
|
1297
|
+
function isGitOpenPrResult(value) {
|
|
1298
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
const record = value;
|
|
1302
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
1303
|
+
}
|
|
1304
|
+
function resolveRepoRoot(projectRoot, target) {
|
|
1305
|
+
return target === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
|
|
1306
|
+
}
|
|
1307
|
+
function ensureFullGitHistory(projectRoot, repoRoot, remoteName = "origin") {
|
|
1308
|
+
const shallow = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--is-shallow-repository"), projectRoot);
|
|
1309
|
+
if (shallow.exitCode !== 0 || shallow.stdout.trim() !== "true") {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const unshallow = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--unshallow", "--tags", remoteName), projectRoot);
|
|
1313
|
+
if (unshallow.exitCode === 0) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const output = `${unshallow.stderr}
|
|
1317
|
+
${unshallow.stdout}`.trim();
|
|
1318
|
+
if (/--unshallow on a complete repository|does not make sense/i.test(output)) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
throw new Error(`Failed to expand git history for ${repoRoot}: ${output}`);
|
|
1322
|
+
}
|
|
1323
|
+
function refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner = "") {
|
|
1324
|
+
const remoteName = resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner || resolveRepoNameWithOwner(projectRoot, repoRoot));
|
|
1325
|
+
const remoteUrl = runCapture(gitCmd(projectRoot, repoRoot, "remote", "get-url", remoteName), projectRoot);
|
|
1326
|
+
if (remoteUrl.exitCode !== 0) {
|
|
1327
|
+
return "";
|
|
1328
|
+
}
|
|
1329
|
+
ensureFullGitHistory(projectRoot, repoRoot, remoteName);
|
|
1330
|
+
const fetch2 = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--prune", "--tags", remoteName, `+refs/heads/${baseRef}:refs/remotes/${remoteName}/${baseRef}`), projectRoot);
|
|
1331
|
+
if (fetch2.exitCode !== 0) {
|
|
1332
|
+
throw new Error(`Failed to refresh ${remoteName}/${baseRef} at ${repoRoot}: ${fetch2.stderr || fetch2.stdout}`);
|
|
1333
|
+
}
|
|
1334
|
+
return remoteName;
|
|
1335
|
+
}
|
|
1336
|
+
function currentHeadMatchesMergedBase(projectRoot, repoRoot, baseRef, remoteName = "origin") {
|
|
1337
|
+
const activeRemote = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef) || remoteName;
|
|
1338
|
+
const remoteBase = `${activeRemote}/${baseRef}`;
|
|
1339
|
+
const hasRemoteBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", remoteBase), repoRoot).exitCode === 0;
|
|
1340
|
+
const targetRef = hasRemoteBase ? remoteBase : baseRef;
|
|
1341
|
+
if (runCapture(gitCmd(projectRoot, repoRoot, "merge-base", "--is-ancestor", "HEAD", targetRef), repoRoot).exitCode === 0) {
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
return runCapture(gitCmd(projectRoot, repoRoot, "diff", "--quiet", "HEAD", targetRef, "--"), repoRoot).exitCode === 0;
|
|
1345
|
+
}
|
|
1346
|
+
function defaultReviewerForTask(projectRoot, taskId) {
|
|
1347
|
+
if (!taskId) {
|
|
1348
|
+
return "";
|
|
1349
|
+
}
|
|
1350
|
+
const entry = taskData().readTaskConfig(projectRoot)[taskId];
|
|
1351
|
+
const reviewer = entry?.reviewer;
|
|
1352
|
+
if (typeof reviewer === "string" && reviewer.trim()) {
|
|
1353
|
+
return reviewer.trim();
|
|
1354
|
+
}
|
|
1355
|
+
const responsibleReviewer = entry?.responsible_reviewer;
|
|
1356
|
+
if (typeof responsibleReviewer === "string" && responsibleReviewer.trim()) {
|
|
1357
|
+
return responsibleReviewer.trim();
|
|
1358
|
+
}
|
|
1359
|
+
return "";
|
|
1360
|
+
}
|
|
1361
|
+
function resolveRepoNameWithOwner(projectRoot, repoRoot) {
|
|
1362
|
+
const explicit = normalizeGithubRepoNameWithOwner(process.env.GH_REPO || "");
|
|
1363
|
+
if (explicit) {
|
|
1364
|
+
return explicit;
|
|
1365
|
+
}
|
|
1366
|
+
const visited = new Set;
|
|
1367
|
+
return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
|
|
1368
|
+
}
|
|
1369
|
+
function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
|
|
1370
|
+
const normalizedGitRoot = resolve2(gitRoot);
|
|
1371
|
+
if (visited.has(normalizedGitRoot)) {
|
|
1372
|
+
return "";
|
|
1373
|
+
}
|
|
1374
|
+
visited.add(normalizedGitRoot);
|
|
1375
|
+
const remotes = listGitRemotes(projectRoot, gitRoot, cwd);
|
|
1376
|
+
for (const remote of remotes) {
|
|
1377
|
+
const urls = listGitRemoteUrls(projectRoot, gitRoot, cwd, remote);
|
|
1378
|
+
for (const url of urls) {
|
|
1379
|
+
const direct = normalizeGithubRepoNameWithOwner(url);
|
|
1380
|
+
if (direct) {
|
|
1381
|
+
return direct;
|
|
1382
|
+
}
|
|
1383
|
+
const localGitRoot = resolveLocalGitRemoteRoot(url, gitRoot);
|
|
1384
|
+
if (!localGitRoot) {
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
const viaMirror = resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, localGitRoot, cwd, visited);
|
|
1388
|
+
if (viaMirror) {
|
|
1389
|
+
return viaMirror;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return "";
|
|
1394
|
+
}
|
|
1395
|
+
function listGitRemotes(projectRoot, gitRoot, cwd) {
|
|
1396
|
+
const result = gitQuery(projectRoot, gitRoot, cwd, "remote");
|
|
1397
|
+
if (result.exitCode !== 0) {
|
|
1398
|
+
return [];
|
|
1399
|
+
}
|
|
1400
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1401
|
+
}
|
|
1402
|
+
function listGitRemoteUrls(projectRoot, gitRoot, cwd, remote) {
|
|
1403
|
+
const result = gitQuery(projectRoot, gitRoot, cwd, "remote", "get-url", "--all", remote);
|
|
1404
|
+
if (result.exitCode !== 0) {
|
|
1405
|
+
return [];
|
|
1406
|
+
}
|
|
1407
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1408
|
+
}
|
|
1409
|
+
function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
|
|
1410
|
+
const remotes = listGitRemotes(projectRoot, repoRoot, repoRoot);
|
|
1411
|
+
if (remotes.length === 0) {
|
|
1412
|
+
return "origin";
|
|
1413
|
+
}
|
|
1414
|
+
if (!repoNameWithOwner) {
|
|
1415
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
1416
|
+
}
|
|
1417
|
+
const expectedRepo = normalizeGithubRepoNameWithOwner(repoNameWithOwner).toLowerCase();
|
|
1418
|
+
let directMatch = "";
|
|
1419
|
+
for (const remote of remotes) {
|
|
1420
|
+
const urls = listGitRemoteUrls(projectRoot, repoRoot, repoRoot, remote);
|
|
1421
|
+
for (const url of urls) {
|
|
1422
|
+
const direct = normalizeGithubRepoNameWithOwner(url);
|
|
1423
|
+
if (direct && direct.toLowerCase() === expectedRepo) {
|
|
1424
|
+
if (remote === "github") {
|
|
1425
|
+
return remote;
|
|
1426
|
+
}
|
|
1427
|
+
if (!directMatch) {
|
|
1428
|
+
directMatch = remote;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (remotes.includes("github")) {
|
|
1434
|
+
return "github";
|
|
1435
|
+
}
|
|
1436
|
+
if (directMatch) {
|
|
1437
|
+
return directMatch;
|
|
1438
|
+
}
|
|
1439
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
1440
|
+
}
|
|
1441
|
+
function gitQuery(projectRoot, gitRoot, cwd, ...args) {
|
|
1442
|
+
const gitArgs = existsSync3(resolve2(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveHostGitBinary(), "--git-dir", gitRoot, ...args];
|
|
1443
|
+
return runCapture(gitArgs, cwd, projectRoot);
|
|
1444
|
+
}
|
|
1445
|
+
function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
|
|
1446
|
+
const normalized = remoteUrl.trim();
|
|
1447
|
+
if (!normalized) {
|
|
1448
|
+
return "";
|
|
1449
|
+
}
|
|
1450
|
+
let candidate = normalized;
|
|
1451
|
+
if (normalized.startsWith("file://")) {
|
|
1452
|
+
try {
|
|
1453
|
+
candidate = fileURLToPath(normalized);
|
|
1454
|
+
} catch {
|
|
1455
|
+
return "";
|
|
1456
|
+
}
|
|
1457
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
|
|
1458
|
+
return "";
|
|
1459
|
+
} else if (!isAbsolute(normalized)) {
|
|
1460
|
+
candidate = resolve2(gitRoot, normalized);
|
|
1461
|
+
}
|
|
1462
|
+
return existsSync3(candidate) ? candidate : "";
|
|
1463
|
+
}
|
|
1464
|
+
function normalizeGithubRepoNameWithOwner(value) {
|
|
1465
|
+
const normalized = value.trim();
|
|
1466
|
+
if (!normalized) {
|
|
1467
|
+
return "";
|
|
1468
|
+
}
|
|
1469
|
+
const scpMatch = normalized.match(/^(?:ssh:\/\/)?git@github\.com[:/](.+?)(?:\.git)?$/i);
|
|
1470
|
+
if (scpMatch?.[1]) {
|
|
1471
|
+
return scpMatch[1].replace(/^\/+|\/+$/g, "");
|
|
1472
|
+
}
|
|
1473
|
+
const httpMatch = normalized.match(/^https?:\/\/github\.com\/(.+?)(?:\.git)?(?:\/)?$/i);
|
|
1474
|
+
if (httpMatch?.[1]) {
|
|
1475
|
+
return httpMatch[1].replace(/^\/+|\/+$/g, "");
|
|
1476
|
+
}
|
|
1477
|
+
const bareMatch = normalized.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/);
|
|
1478
|
+
return bareMatch ? bareMatch[0] : "";
|
|
1479
|
+
}
|
|
1480
|
+
function ghRepoArgs(repoNameWithOwner) {
|
|
1481
|
+
return repoNameWithOwner ? ["-R", repoNameWithOwner] : [];
|
|
1482
|
+
}
|
|
1483
|
+
function withGhRepo(command, repoNameWithOwner) {
|
|
1484
|
+
if (!repoNameWithOwner || command.length < 3) {
|
|
1485
|
+
return command;
|
|
1486
|
+
}
|
|
1487
|
+
return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
|
|
1488
|
+
}
|
|
1489
|
+
function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
|
|
1490
|
+
const remote = remoteName || "origin";
|
|
1491
|
+
const symbolic = runCapture(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
|
|
1492
|
+
if (symbolic.exitCode === 0) {
|
|
1493
|
+
const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
|
|
1494
|
+
if (ref && ref !== "HEAD") {
|
|
1495
|
+
return ref;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const lsRemote = runCapture(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
|
|
1499
|
+
if (lsRemote.exitCode === 0) {
|
|
1500
|
+
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
|
|
1501
|
+
if (match?.[1]) {
|
|
1502
|
+
return match[1];
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
1506
|
+
if (gh && repoNameWithOwner) {
|
|
1507
|
+
const api = runCapture(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
|
|
1508
|
+
const branch = api.exitCode === 0 ? api.stdout.trim() : "";
|
|
1509
|
+
if (branch) {
|
|
1510
|
+
return branch;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return fallback;
|
|
1514
|
+
}
|
|
1515
|
+
function inferProjectBase(projectRoot, fallback) {
|
|
1516
|
+
const containing = runCapture(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
|
|
1517
|
+
if (containing.exitCode !== 0) {
|
|
1518
|
+
return fallback;
|
|
1519
|
+
}
|
|
1520
|
+
const candidates = containing.stdout.split(/\r?\n/).map((line) => line.replace(/^\*/, "").trim()).filter(Boolean).map((line) => line.replace(/^origin\//, "")).filter((line) => line !== "HEAD" && !line.startsWith("rig/"));
|
|
1521
|
+
return candidates[0] || fallback;
|
|
1522
|
+
}
|
|
1523
|
+
function currentGithubLogin(repoRoot) {
|
|
1524
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
1525
|
+
if (!gh) {
|
|
1526
|
+
return "";
|
|
1527
|
+
}
|
|
1528
|
+
const result = runCapture([gh, "api", "user", "--jq", ".login"], repoRoot);
|
|
1529
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
1530
|
+
}
|
|
1531
|
+
function collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
|
|
1532
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
|
|
1533
|
+
const remoteName = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner);
|
|
1534
|
+
const hasRemoteBase = remoteName ? runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", `${remoteName}/${baseRef}`), projectRoot).exitCode === 0 : false;
|
|
1535
|
+
const hasLocalBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", baseRef), projectRoot).exitCode === 0;
|
|
1536
|
+
let changed = "";
|
|
1537
|
+
if (hasRemoteBase) {
|
|
1538
|
+
changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${remoteName}/${baseRef}...${branchRef}`), projectRoot).stdout;
|
|
1539
|
+
} else if (hasLocalBase) {
|
|
1540
|
+
changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${baseRef}...${branchRef}`), projectRoot).stdout;
|
|
1541
|
+
} else {
|
|
1542
|
+
const fallback = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `HEAD~1..${branchRef}`), projectRoot);
|
|
1543
|
+
changed = fallback.exitCode === 0 ? fallback.stdout : runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only"), projectRoot).stdout;
|
|
1544
|
+
}
|
|
1545
|
+
return changed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 60);
|
|
1546
|
+
}
|
|
1547
|
+
function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
|
|
1548
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
|
|
1549
|
+
if (!repoNameWithOwner) {
|
|
1550
|
+
return "";
|
|
1551
|
+
}
|
|
1552
|
+
const actorLogin = currentGithubLogin(repoRoot);
|
|
1553
|
+
const changedFiles = collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef);
|
|
1554
|
+
if (changedFiles.length === 0) {
|
|
1555
|
+
return "";
|
|
1556
|
+
}
|
|
1557
|
+
const counts = new Map;
|
|
1558
|
+
for (const path of changedFiles) {
|
|
1559
|
+
const result = runCapture([
|
|
1560
|
+
resolveGithubCliBinary({ scanPath: true }) || "gh",
|
|
1561
|
+
"api",
|
|
1562
|
+
`repos/${repoNameWithOwner}/commits`,
|
|
1563
|
+
"-f",
|
|
1564
|
+
`path=${path}`,
|
|
1565
|
+
"-f",
|
|
1566
|
+
`sha=${baseRef}`,
|
|
1567
|
+
"-f",
|
|
1568
|
+
"per_page=1",
|
|
1569
|
+
"--jq",
|
|
1570
|
+
".[0].author.login // empty"
|
|
1571
|
+
], repoRoot);
|
|
1572
|
+
const author = result.exitCode === 0 ? result.stdout.trim() : "";
|
|
1573
|
+
if (!author || author === actorLogin) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
counts.set(author, (counts.get(author) || 0) + 1);
|
|
1577
|
+
}
|
|
1578
|
+
let best = "";
|
|
1579
|
+
let max = -1;
|
|
1580
|
+
for (const [author, count] of counts.entries()) {
|
|
1581
|
+
if (count > max || count === max && author < best) {
|
|
1582
|
+
best = author;
|
|
1583
|
+
max = count;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return best;
|
|
1587
|
+
}
|
|
1588
|
+
function snapshotRepo(projectRoot, label, repo) {
|
|
1589
|
+
if (!existsSync3(resolve2(repo, ".git"))) {
|
|
1590
|
+
return [`## ${label}`, `repo: ${repo}`, "status: unavailable", ""];
|
|
1591
|
+
}
|
|
1592
|
+
const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
|
|
1593
|
+
const branch = branchName(projectRoot, repo);
|
|
1594
|
+
const head = runCapture(gitCmd(projectRoot, repo, "rev-parse", "HEAD"), projectRoot).stdout.trim();
|
|
1595
|
+
return [
|
|
1596
|
+
`## ${label}`,
|
|
1597
|
+
`repo: ${repo}`,
|
|
1598
|
+
`branch: ${branch}`,
|
|
1599
|
+
`head: ${head}`,
|
|
1600
|
+
"status:",
|
|
1601
|
+
status || "(clean)",
|
|
1602
|
+
""
|
|
1603
|
+
];
|
|
1604
|
+
}
|
|
1605
|
+
function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
|
|
1606
|
+
if (!existsSync3(resolve2(repo, ".git"))) {
|
|
1607
|
+
console.log(`Skipping ${label}: repo not available (${repo})`);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const scopedFiles = (files || []).filter(Boolean).filter((file) => !pathResolvesBeyondSymlink(repo, file));
|
|
1611
|
+
const repoChanges = changeCount(projectRoot, repo);
|
|
1612
|
+
if (scopedFiles.length === 0 && repoChanges === 0 && !allowEmpty) {
|
|
1613
|
+
console.log(`Skipping ${label}: no changes to commit.`);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (scopedFiles.length > 0) {
|
|
1617
|
+
const indexFiles = new Set(runCapture(gitCmd(projectRoot, repo, "ls-files"), projectRoot).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
1618
|
+
const stageable = scopedFiles.filter((file) => indexFiles.has(file) || existsSync3(resolve2(repo, file)));
|
|
1619
|
+
if (stageable.length === 0) {
|
|
1620
|
+
console.log(`Skipping ${label}: collected change list matched no stageable paths in ${repo}.`);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const pathspecFile = resolve2(tmpdir(), `rig-stage-${process.pid}-${Date.now()}.txt`);
|
|
1624
|
+
writeFileSync(pathspecFile, `${stageable.join(`
|
|
1625
|
+
`)}
|
|
1626
|
+
`, "utf-8");
|
|
1627
|
+
try {
|
|
1628
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "add", `--pathspec-from-file=${pathspecFile}`), `Failed to stage changes for ${label}`);
|
|
1629
|
+
} finally {
|
|
1630
|
+
try {
|
|
1631
|
+
unlinkSync(pathspecFile);
|
|
1632
|
+
} catch {}
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
const addArgs = buildStageAddArgs(repo, scopedFiles, scoped);
|
|
1636
|
+
if (addArgs) {
|
|
1637
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, ...addArgs), `Failed to stage changes for ${label}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const stagedChanges = stagedChangeCount(projectRoot, repo);
|
|
1641
|
+
if (stagedChanges === 0) {
|
|
1642
|
+
if (allowEmpty) {
|
|
1643
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", "--allow-empty", "-m", message), `Failed to commit ${label}`);
|
|
1644
|
+
console.log(`Committed ${label}: ${message}`);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (scoped && repoChanges > 0) {
|
|
1648
|
+
const manifestHint = changedFilesManifest ? ` Refresh ${changedFilesManifest} and retry, or use --all/--unscoped intentionally.` : "";
|
|
1649
|
+
throw new Error(`Scoped commit for ${label} resolved no stageable files.${manifestHint}`);
|
|
1650
|
+
}
|
|
1651
|
+
console.log(`Skipping ${label}: no stageable changes to commit.`);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", ...allowEmpty ? ["--allow-empty"] : [], "-m", message), `Failed to commit ${label}`);
|
|
1655
|
+
console.log(`Committed ${label}: ${message}`);
|
|
1656
|
+
}
|
|
1657
|
+
function readChangedFilesManifest(projectRoot, taskId) {
|
|
1658
|
+
const manifestPath = resolve2(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
1659
|
+
if (!existsSync3(manifestPath)) {
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
const files = readFileSync2(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath(line)).filter(Boolean);
|
|
1663
|
+
return [...new Set(files)];
|
|
1664
|
+
}
|
|
1665
|
+
function refreshChangedFilesManifest(projectRoot, taskId) {
|
|
1666
|
+
const manifestPath = resolve2(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
1667
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
1668
|
+
const changedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
|
|
1669
|
+
writeFileSync(manifestPath, `${changedFiles.join(`
|
|
1670
|
+
`)}
|
|
1671
|
+
`, "utf-8");
|
|
1672
|
+
return manifestPath;
|
|
1673
|
+
}
|
|
1674
|
+
function normalizeChangedFilePath(file) {
|
|
1675
|
+
return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1676
|
+
}
|
|
1677
|
+
function buildStageAddArgs(repoRoot, files, scoped) {
|
|
1678
|
+
if (files.length > 0) {
|
|
1679
|
+
return ["add", "--", ...files];
|
|
1680
|
+
}
|
|
1681
|
+
if (scoped) {
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
return ["add", "-A", "--", ".", ...stageExcludePathspecs(repoRoot)];
|
|
1685
|
+
}
|
|
1686
|
+
function resolveScopedStageFilesForRepo(projectRoot, repoRoot, taskId, files) {
|
|
1687
|
+
const resolvedManifestFiles = resolveScopedFilesForRepo(projectRoot, repoRoot, files);
|
|
1688
|
+
if (resolvedManifestFiles.length > 0 || !taskId) {
|
|
1689
|
+
return resolvedManifestFiles;
|
|
1690
|
+
}
|
|
1691
|
+
return resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId);
|
|
1692
|
+
}
|
|
1693
|
+
function resolveScopedFilesForRepo(projectRoot, repoRoot, files) {
|
|
1694
|
+
const resolvedFiles = [];
|
|
1695
|
+
const seen = new Set;
|
|
1696
|
+
for (const file of files) {
|
|
1697
|
+
const candidate = resolveScopedRepoPath(repoRoot, file);
|
|
1698
|
+
if (!candidate || seen.has(candidate) || pathResolvesBeyondSymlink(repoRoot, candidate)) {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (!repoHasPathChange(projectRoot, repoRoot, candidate)) {
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
seen.add(candidate);
|
|
1705
|
+
resolvedFiles.push(candidate);
|
|
1706
|
+
}
|
|
1707
|
+
return resolvedFiles;
|
|
1708
|
+
}
|
|
1709
|
+
function resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId) {
|
|
1710
|
+
const safeTaskId = safePathSegment(taskId, { fallback: "task", maxLength: 96 });
|
|
1711
|
+
const artifactPrefix = `artifacts/${safeTaskId}/`;
|
|
1712
|
+
const resolvedFiles = [];
|
|
1713
|
+
const seen = new Set;
|
|
1714
|
+
for (const file of collectRepoPendingFiles(projectRoot, repoRoot)) {
|
|
1715
|
+
if (!file.startsWith(artifactPrefix)) {
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
const artifactRelativePath = file.slice(artifactPrefix.length);
|
|
1719
|
+
if (!TASK_ARTIFACT_STAGE_FALLBACK.has(artifactRelativePath)) {
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
if (seen.has(file) || pathResolvesBeyondSymlink(repoRoot, file)) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
seen.add(file);
|
|
1726
|
+
resolvedFiles.push(file);
|
|
1727
|
+
}
|
|
1728
|
+
return resolvedFiles.sort();
|
|
1729
|
+
}
|
|
1730
|
+
function collectRepoPendingFiles(projectRoot, repoRoot) {
|
|
1731
|
+
const files = new Set;
|
|
1732
|
+
for (const args of [
|
|
1733
|
+
["diff", "--name-only"],
|
|
1734
|
+
["diff", "--cached", "--name-only"],
|
|
1735
|
+
["ls-files", "--others", "--exclude-standard"]
|
|
1736
|
+
]) {
|
|
1737
|
+
const result = runCapture(gitCmd(projectRoot, repoRoot, ...args), projectRoot);
|
|
1738
|
+
if (result.exitCode !== 0) {
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
1742
|
+
const normalized = normalizeChangedFilePath(line);
|
|
1743
|
+
if (!normalized) {
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
files.add(normalized);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return [...files].sort();
|
|
1750
|
+
}
|
|
1751
|
+
function resolveScopedRepoPath(repoRoot, file) {
|
|
1752
|
+
const normalized = normalizeChangedFilePath(file);
|
|
1753
|
+
if (!normalized) {
|
|
1754
|
+
return "";
|
|
1755
|
+
}
|
|
1756
|
+
const rules = getScopeRules();
|
|
1757
|
+
if (rules?.stripPrefixes) {
|
|
1758
|
+
let result = normalized;
|
|
1759
|
+
for (const prefix of rules.stripPrefixes) {
|
|
1760
|
+
if (result.startsWith(prefix)) {
|
|
1761
|
+
result = result.slice(prefix.length);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
return normalized;
|
|
1767
|
+
}
|
|
1768
|
+
function repoHasPathChange(projectRoot, repoRoot, relativePath) {
|
|
1769
|
+
const result = runCapture(gitCmd(projectRoot, repoRoot, "status", "--short", "--", relativePath), projectRoot);
|
|
1770
|
+
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
1771
|
+
}
|
|
1772
|
+
function stageExcludePathspecs(repoRoot) {
|
|
1773
|
+
const patterns = existsSync3(resolve2(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
|
|
1774
|
+
return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
|
|
1775
|
+
}
|
|
1776
|
+
function pathResolvesBeyondSymlink(repoRoot, relativePath) {
|
|
1777
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
1778
|
+
if (parts.length <= 1) {
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
let current = repoRoot;
|
|
1782
|
+
for (let index = 0;index < parts.length - 1; index += 1) {
|
|
1783
|
+
current = resolve2(current, parts[index]);
|
|
1784
|
+
try {
|
|
1785
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
} catch {
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
function printRepoStatus(projectRoot, label, repo, expectedBranch) {
|
|
1795
|
+
if (!existsSync3(resolve2(repo, ".git"))) {
|
|
1796
|
+
console.log(`${label}: unavailable (${repo})`);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const branch = branchName(projectRoot, repo);
|
|
1800
|
+
const changes = changeCount(projectRoot, repo);
|
|
1801
|
+
console.log(`${label}:`);
|
|
1802
|
+
console.log(` branch: ${branch || "unknown"}`);
|
|
1803
|
+
console.log(` changed files: ${changes}`);
|
|
1804
|
+
if (expectedBranch && label !== "project-rig" && branch !== expectedBranch) {
|
|
1805
|
+
console.log(` warning: branch mismatch (expected ${expectedBranch})`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
function resolveTaskBranchId(projectRoot, taskId) {
|
|
1809
|
+
if (/^bd-[a-z0-9-]+$/.test(taskId)) {
|
|
1810
|
+
return taskId;
|
|
1811
|
+
}
|
|
1812
|
+
const normalizedTaskId = taskData().lookupTask(projectRoot, taskId);
|
|
1813
|
+
if (normalizedTaskId) {
|
|
1814
|
+
return normalizedTaskId;
|
|
1815
|
+
}
|
|
1816
|
+
const currentTask = taskData().currentTaskId(projectRoot);
|
|
1817
|
+
if (currentTask && currentTask === taskId) {
|
|
1818
|
+
return currentTask;
|
|
1819
|
+
}
|
|
1820
|
+
const runtimeIdFromEnv = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
|
|
1821
|
+
if (runtimeIdFromEnv.startsWith("task-") && runtimeIdFromEnv.length > "task-".length) {
|
|
1822
|
+
return runtimeIdFromEnv.slice("task-".length);
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
const runtimeIdFromContext = loadRuntimeContextFromEnv()?.runtimeId || "";
|
|
1826
|
+
if (runtimeIdFromContext.startsWith("task-") && runtimeIdFromContext.length > "task-".length) {
|
|
1827
|
+
return runtimeIdFromContext.slice("task-".length);
|
|
1828
|
+
}
|
|
1829
|
+
} catch {}
|
|
1830
|
+
const artifactDir = taskData().artifactDirForId(projectRoot, taskId);
|
|
1831
|
+
if (existsSync3(artifactDir)) {
|
|
1832
|
+
return taskId;
|
|
1833
|
+
}
|
|
1834
|
+
throw new Error(`Unknown task id: ${taskId}`);
|
|
1835
|
+
}
|
|
1836
|
+
function branchName(projectRoot, repo) {
|
|
1837
|
+
return runCapture(gitCmd(projectRoot, repo, "rev-parse", "--abbrev-ref", "HEAD"), projectRoot).stdout.trim();
|
|
1838
|
+
}
|
|
1839
|
+
function changeCount(projectRoot, repo) {
|
|
1840
|
+
const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
|
|
1841
|
+
return status ? status.split(/\r?\n/).filter(Boolean).length : 0;
|
|
1842
|
+
}
|
|
1843
|
+
function stagedChangeCount(projectRoot, repo) {
|
|
1844
|
+
const staged = runCapture(gitCmd(projectRoot, repo, "diff", "--cached", "--name-only"), projectRoot).stdout.trim();
|
|
1845
|
+
return staged ? staged.split(/\r?\n/).filter(Boolean).length : 0;
|
|
1846
|
+
}
|
|
1847
|
+
function runOrThrow(projectRoot, command, errorPrefix) {
|
|
1848
|
+
const result = runCapture(command, projectRoot);
|
|
1849
|
+
if (result.exitCode !== 0) {
|
|
1850
|
+
throw new Error(`${errorPrefix}:
|
|
1851
|
+
${result.stderr || result.stdout}`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function runCapture(command, cwd, projectRoot = cwd) {
|
|
1855
|
+
return baseRunCapture(command, cwd, runtimeGitEnv(projectRoot));
|
|
1856
|
+
}
|
|
1857
|
+
function runtimeGitEnv(projectRoot) {
|
|
1858
|
+
const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
|
|
1859
|
+
const runtimeHome = runtimeRoot ? resolve2(runtimeRoot, "home") : "";
|
|
1860
|
+
const runtimeTmp = runtimeRoot ? resolve2(runtimeRoot, "tmp") : "";
|
|
1861
|
+
const runtimeCache = runtimeRoot ? resolve2(runtimeRoot, "cache") : "";
|
|
1862
|
+
const runtimeKnownHosts = runtimeHome ? resolve2(runtimeHome, ".ssh", "known_hosts") : "";
|
|
1863
|
+
const runtimeKey = runtimeHome ? resolve2(runtimeHome, ".ssh", "rig-agent-key") : "";
|
|
1864
|
+
const env = {};
|
|
1865
|
+
if (ctx?.workspaceDir) {
|
|
1866
|
+
env.PROJECT_RIG_ROOT = projectRoot;
|
|
1867
|
+
env.RIG_TASK_WORKSPACE = ctx.workspaceDir;
|
|
1868
|
+
env.MONOREPO_ROOT = ctx.workspaceDir;
|
|
1869
|
+
env.MONOREPO_MAIN_ROOT = resolveMonorepoRoot(projectRoot);
|
|
1870
|
+
} else if (projectRoot) {
|
|
1871
|
+
env.PROJECT_RIG_ROOT = projectRoot;
|
|
1872
|
+
}
|
|
1873
|
+
if (runtimeRoot) {
|
|
1874
|
+
env.RIG_RUNTIME_HOME = runtimeRoot;
|
|
1875
|
+
}
|
|
1876
|
+
if (runtimeHome && existsSync3(runtimeHome)) {
|
|
1877
|
+
env.HOME = runtimeHome;
|
|
1878
|
+
env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
|
|
1879
|
+
}
|
|
1880
|
+
if (runtimeTmp && existsSync3(runtimeTmp)) {
|
|
1881
|
+
env.TMPDIR = runtimeTmp;
|
|
1882
|
+
}
|
|
1883
|
+
if (runtimeCache && existsSync3(runtimeCache)) {
|
|
1884
|
+
env.XDG_CACHE_HOME = runtimeCache;
|
|
1885
|
+
}
|
|
1886
|
+
const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
|
|
1887
|
+
for (const [key, value] of Object.entries(resolveRuntimeSecrets(process.env, workspaceSecrets))) {
|
|
1888
|
+
if (key === "GITHUB_SSH_KEY" || !value) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
env[key] = value;
|
|
1892
|
+
}
|
|
1893
|
+
const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
|
|
1894
|
+
if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
1895
|
+
env.GITHUB_TOKEN = rigGithubToken;
|
|
1896
|
+
}
|
|
1897
|
+
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
1898
|
+
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
1899
|
+
}
|
|
1900
|
+
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
1901
|
+
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
1902
|
+
}
|
|
1903
|
+
if (!env.GREPTILE_GITHUB_TOKEN && env.GITHUB_TOKEN) {
|
|
1904
|
+
env.GREPTILE_GITHUB_TOKEN = env.GITHUB_TOKEN;
|
|
1905
|
+
}
|
|
1906
|
+
const persistedSecrets = loadPersistedRuntimeSecrets(runtimeRoot);
|
|
1907
|
+
for (const [key, value] of Object.entries(persistedSecrets)) {
|
|
1908
|
+
if (!value)
|
|
1909
|
+
continue;
|
|
1910
|
+
if (!env[key]) {
|
|
1911
|
+
env[key] = value;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
1915
|
+
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
1916
|
+
}
|
|
1917
|
+
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
1918
|
+
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
1919
|
+
}
|
|
1920
|
+
const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
|
|
1921
|
+
if (gitHubToken) {
|
|
1922
|
+
env.RIG_GITHUB_TOKEN = gitHubToken;
|
|
1923
|
+
env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
|
|
1924
|
+
env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
|
|
1925
|
+
applyGitHubCredentialHelperEnv(env);
|
|
1926
|
+
}
|
|
1927
|
+
if (runtimeKnownHosts && existsSync3(runtimeKnownHosts)) {
|
|
1928
|
+
const sshParts = [
|
|
1929
|
+
"ssh",
|
|
1930
|
+
`-o UserKnownHostsFile="${runtimeKnownHosts}"`,
|
|
1931
|
+
"-o StrictHostKeyChecking=yes",
|
|
1932
|
+
"-F /dev/null"
|
|
1933
|
+
];
|
|
1934
|
+
if (runtimeKey && existsSync3(runtimeKey)) {
|
|
1935
|
+
sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
|
|
1936
|
+
}
|
|
1937
|
+
env.GIT_SSH_COMMAND = sshParts.join(" ");
|
|
1938
|
+
} else if (process.env.GIT_SSH_COMMAND?.trim()) {
|
|
1939
|
+
env.GIT_SSH_COMMAND = process.env.GIT_SSH_COMMAND;
|
|
1940
|
+
}
|
|
1941
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
1942
|
+
}
|
|
1943
|
+
function applyGitHubCredentialHelperEnv(env) {
|
|
1944
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
1945
|
+
env.GIT_CONFIG_COUNT = "2";
|
|
1946
|
+
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
1947
|
+
env.GIT_CONFIG_VALUE_0 = "";
|
|
1948
|
+
env.GIT_CONFIG_KEY_1 = "credential.helper";
|
|
1949
|
+
env.GIT_CONFIG_VALUE_1 = '!f() { test "$1" = get || exit 0; token="${GITHUB_TOKEN:-${GH_TOKEN:-${RIG_GITHUB_TOKEN:-}}}"; test -n "$token" || exit 0; echo username=x-access-token; echo password="$token"; }; f';
|
|
1950
|
+
}
|
|
1951
|
+
function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
1952
|
+
if (!runtimeRoot) {
|
|
1953
|
+
return {};
|
|
1954
|
+
}
|
|
1955
|
+
const path = resolve2(runtimeRoot, "runtime-secrets.json");
|
|
1956
|
+
if (!existsSync3(path)) {
|
|
1957
|
+
return {};
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1961
|
+
const allowed = new Set(["GITHUB_TOKEN", "GH_TOKEN", "RIG_GITHUB_TOKEN"]);
|
|
1962
|
+
const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string" && allowed.has(entry[0]));
|
|
1963
|
+
return Object.fromEntries(entries);
|
|
1964
|
+
} catch {
|
|
1965
|
+
return {};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function ensureRuntimeOpenSslConfig(runtimeHome) {
|
|
1969
|
+
const sslDir = resolve2(runtimeHome, ".ssl");
|
|
1970
|
+
const sslConfig = resolve2(sslDir, "openssl.cnf");
|
|
1971
|
+
if (!existsSync3(sslDir)) {
|
|
1972
|
+
mkdirSync(sslDir, { recursive: true });
|
|
1973
|
+
}
|
|
1974
|
+
if (!existsSync3(sslConfig)) {
|
|
1975
|
+
writeFileSync(sslConfig, `# Rig runtime OpenSSL config placeholder
|
|
1976
|
+
`);
|
|
1977
|
+
}
|
|
1978
|
+
return sslConfig;
|
|
1979
|
+
}
|
|
1980
|
+
function resolveRuntimeMetadata(projectRoot) {
|
|
1981
|
+
const contextFile = process.env.RIG_RUNTIME_CONTEXT_FILE?.trim();
|
|
1982
|
+
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
1983
|
+
let ctx = loadRuntimeContextFromEnv();
|
|
1984
|
+
if (runtimeHome) {
|
|
1985
|
+
return {
|
|
1986
|
+
ctx,
|
|
1987
|
+
runtimeRoot: runtimeHome
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
if (contextFile) {
|
|
1991
|
+
return {
|
|
1992
|
+
ctx,
|
|
1993
|
+
runtimeRoot: dirname(resolve2(contextFile))
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const inferredContextFile = findRuntimeContextFile(projectRoot);
|
|
1997
|
+
if (existsSync3(inferredContextFile)) {
|
|
1998
|
+
try {
|
|
1999
|
+
ctx = loadRuntimeContext(inferredContextFile);
|
|
2000
|
+
} catch {}
|
|
2001
|
+
return {
|
|
2002
|
+
ctx,
|
|
2003
|
+
runtimeRoot: dirname(inferredContextFile)
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
return { ctx, runtimeRoot: "" };
|
|
2007
|
+
}
|
|
2008
|
+
function findRuntimeContextFile(startPath) {
|
|
2009
|
+
let current = resolve2(startPath);
|
|
2010
|
+
while (true) {
|
|
2011
|
+
const candidate = resolve2(current, "runtime-context.json");
|
|
2012
|
+
if (existsSync3(candidate)) {
|
|
2013
|
+
return candidate;
|
|
2014
|
+
}
|
|
2015
|
+
const parent = dirname(current);
|
|
2016
|
+
if (parent === current) {
|
|
2017
|
+
return "";
|
|
2018
|
+
}
|
|
2019
|
+
current = parent;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
var TASK_RUNTIME_STAGE_EXCLUDES, GENERATED_STAGE_EXCLUDES, TASK_ARTIFACT_STAGE_FALLBACK;
|
|
2023
|
+
var init_git_ops = __esm(() => {
|
|
2024
|
+
init_task_data();
|
|
2025
|
+
init_github_auth_env();
|
|
2026
|
+
init_host_git();
|
|
2027
|
+
TASK_RUNTIME_STAGE_EXCLUDES = [
|
|
2028
|
+
".rig/bin/**",
|
|
2029
|
+
".rig/cache/**",
|
|
2030
|
+
".rig/home/**",
|
|
2031
|
+
".rig/logs/**",
|
|
2032
|
+
".rig/runtime/**",
|
|
2033
|
+
".rig/session/**",
|
|
2034
|
+
".rig/state/**",
|
|
2035
|
+
".rig/runtime-context.json"
|
|
2036
|
+
];
|
|
2037
|
+
GENERATED_STAGE_EXCLUDES = ["artifacts/*/runtime-snapshots/**"];
|
|
2038
|
+
TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
2039
|
+
"changed-files.txt",
|
|
2040
|
+
"contract-changes.md",
|
|
2041
|
+
"decision-log.md",
|
|
2042
|
+
"git-state.txt",
|
|
2043
|
+
"next-actions.md",
|
|
2044
|
+
"pr-state.json",
|
|
2045
|
+
"task-result.json",
|
|
2046
|
+
"validation-summary.json"
|
|
2047
|
+
]);
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
// packages/bundle-default-lifecycle/src/control-plane/policy.ts
|
|
2051
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
2052
|
+
import { resolve as resolve3 } from "path";
|
|
668
2053
|
import {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
2054
|
+
POLICY_VERSION
|
|
2055
|
+
} from "@rig/contracts";
|
|
2056
|
+
function defaultPolicy() {
|
|
2057
|
+
return {
|
|
2058
|
+
version: POLICY_VERSION,
|
|
2059
|
+
mode: "enforce",
|
|
2060
|
+
scope: { ...DEFAULT_SCOPE },
|
|
2061
|
+
rules: [],
|
|
2062
|
+
sandbox: { ...DEFAULT_SANDBOX },
|
|
2063
|
+
isolation: { ...DEFAULT_ISOLATION },
|
|
2064
|
+
completion: { ...DEFAULT_COMPLETION },
|
|
2065
|
+
runtime_image: {
|
|
2066
|
+
deps: { ...DEFAULT_RUNTIME_IMAGE.deps },
|
|
2067
|
+
plugins_require_binaries: DEFAULT_RUNTIME_IMAGE.plugins_require_binaries
|
|
2068
|
+
},
|
|
2069
|
+
runtime_snapshot: { ...DEFAULT_RUNTIME_SNAPSHOT }
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
function seedPolicyFromContent(rawJson) {
|
|
2073
|
+
try {
|
|
2074
|
+
seededPolicyConfig = mergeWithDefaults(JSON.parse(rawJson));
|
|
2075
|
+
} catch {
|
|
2076
|
+
seededPolicyConfig = null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
function loadPolicy(projectRoot) {
|
|
2080
|
+
if (seededPolicyConfig) {
|
|
2081
|
+
return seededPolicyConfig;
|
|
2082
|
+
}
|
|
2083
|
+
const configPath = resolve3(projectRoot, "rig/policy/policy.json");
|
|
2084
|
+
if (!existsSync4(configPath)) {
|
|
2085
|
+
return defaultPolicy();
|
|
2086
|
+
}
|
|
2087
|
+
let mtimeMs;
|
|
2088
|
+
try {
|
|
2089
|
+
mtimeMs = statSync(configPath).mtimeMs;
|
|
2090
|
+
} catch {
|
|
2091
|
+
return defaultPolicy();
|
|
2092
|
+
}
|
|
2093
|
+
if (policyCache && policyCachePath === configPath && policyCache.mtimeMs === mtimeMs) {
|
|
2094
|
+
return policyCache.config;
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
const config = mergeWithDefaults(JSON.parse(readFileSync3(configPath, "utf-8")));
|
|
2098
|
+
policyCache = { mtimeMs, config };
|
|
2099
|
+
policyCachePath = configPath;
|
|
2100
|
+
return config;
|
|
2101
|
+
} catch {
|
|
2102
|
+
return defaultPolicy();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
function mergeWithDefaults(parsed) {
|
|
2106
|
+
const base = defaultPolicy();
|
|
2107
|
+
if (typeof parsed.mode === "string" && isValidMode(parsed.mode)) {
|
|
2108
|
+
base.mode = parsed.mode;
|
|
2109
|
+
}
|
|
2110
|
+
if (parsed.scope && typeof parsed.scope === "object" && !Array.isArray(parsed.scope)) {
|
|
2111
|
+
const scope = parsed.scope;
|
|
2112
|
+
if (typeof scope.fail_closed === "boolean")
|
|
2113
|
+
base.scope.fail_closed = scope.fail_closed;
|
|
2114
|
+
if (typeof scope.harness_paths_exempt === "boolean")
|
|
2115
|
+
base.scope.harness_paths_exempt = scope.harness_paths_exempt;
|
|
2116
|
+
if (typeof scope.runtime_paths_exempt === "boolean")
|
|
2117
|
+
base.scope.runtime_paths_exempt = scope.runtime_paths_exempt;
|
|
2118
|
+
}
|
|
2119
|
+
if (Array.isArray(parsed.rules)) {
|
|
2120
|
+
base.rules = precompilePolicyRuleRegexes(parsed.rules.filter(isValidRule));
|
|
2121
|
+
}
|
|
2122
|
+
if (Array.isArray(parsed.deny) && base.rules.length === 0) {
|
|
2123
|
+
base.rules = precompilePolicyRuleRegexes(migrateLegacyDeny(parsed.deny));
|
|
2124
|
+
}
|
|
2125
|
+
if (parsed.sandbox && typeof parsed.sandbox === "object" && !Array.isArray(parsed.sandbox)) {
|
|
2126
|
+
const sandbox = parsed.sandbox;
|
|
2127
|
+
if (typeof sandbox.mode === "string" && isValidMode(sandbox.mode))
|
|
2128
|
+
base.sandbox.mode = sandbox.mode;
|
|
2129
|
+
if (typeof sandbox.network === "boolean")
|
|
2130
|
+
base.sandbox.network = sandbox.network;
|
|
2131
|
+
if (Array.isArray(sandbox.read_deny))
|
|
2132
|
+
base.sandbox.read_deny = sandbox.read_deny.filter((value) => typeof value === "string");
|
|
2133
|
+
if (typeof sandbox.write_allow_from_runtime === "boolean")
|
|
2134
|
+
base.sandbox.write_allow_from_runtime = sandbox.write_allow_from_runtime;
|
|
2135
|
+
}
|
|
2136
|
+
if (parsed.isolation && typeof parsed.isolation === "object" && !Array.isArray(parsed.isolation)) {
|
|
2137
|
+
const isolation = parsed.isolation;
|
|
2138
|
+
if (isolation.default_mode === "worktree")
|
|
2139
|
+
base.isolation.default_mode = isolation.default_mode;
|
|
2140
|
+
if (typeof isolation.repo_symlink_fallback === "boolean")
|
|
2141
|
+
base.isolation.repo_symlink_fallback = isolation.repo_symlink_fallback;
|
|
2142
|
+
if (typeof isolation.strict_provisioning === "boolean")
|
|
2143
|
+
base.isolation.strict_provisioning = isolation.strict_provisioning;
|
|
2144
|
+
if (typeof isolation.fail_closed_on_provision_error === "boolean")
|
|
2145
|
+
base.isolation.fail_closed_on_provision_error = isolation.fail_closed_on_provision_error;
|
|
2146
|
+
}
|
|
2147
|
+
if (parsed.completion && typeof parsed.completion === "object" && !Array.isArray(parsed.completion)) {
|
|
2148
|
+
const completion = parsed.completion;
|
|
2149
|
+
if (typeof completion.derive_checks_from_scope === "boolean")
|
|
2150
|
+
base.completion.derive_checks_from_scope = completion.derive_checks_from_scope;
|
|
2151
|
+
if (Array.isArray(completion.checks))
|
|
2152
|
+
base.completion.checks = completion.checks.filter((value) => typeof value === "string");
|
|
2153
|
+
if (Array.isArray(completion.typescript_config_probe))
|
|
2154
|
+
base.completion.typescript_config_probe = completion.typescript_config_probe.filter((value) => typeof value === "string");
|
|
2155
|
+
if (Array.isArray(completion.eslint_config_probe))
|
|
2156
|
+
base.completion.eslint_config_probe = completion.eslint_config_probe.filter((value) => typeof value === "string");
|
|
2157
|
+
}
|
|
2158
|
+
if (parsed.runtime_image && typeof parsed.runtime_image === "object" && !Array.isArray(parsed.runtime_image)) {
|
|
2159
|
+
const runtimeImage = parsed.runtime_image;
|
|
2160
|
+
if (runtimeImage.deps && typeof runtimeImage.deps === "object" && !Array.isArray(runtimeImage.deps)) {
|
|
2161
|
+
const deps = runtimeImage.deps;
|
|
2162
|
+
if (typeof deps.monorepo_install === "boolean")
|
|
2163
|
+
base.runtime_image.deps.monorepo_install = deps.monorepo_install;
|
|
2164
|
+
if (typeof deps.hp_next_install === "boolean")
|
|
2165
|
+
base.runtime_image.deps.hp_next_install = deps.hp_next_install;
|
|
2166
|
+
}
|
|
2167
|
+
if (typeof runtimeImage.plugins_require_binaries === "boolean") {
|
|
2168
|
+
base.runtime_image.plugins_require_binaries = runtimeImage.plugins_require_binaries;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (parsed.runtime_snapshot && typeof parsed.runtime_snapshot === "object" && !Array.isArray(parsed.runtime_snapshot)) {
|
|
2172
|
+
const runtimeSnapshot = parsed.runtime_snapshot;
|
|
2173
|
+
if (typeof runtimeSnapshot.enabled === "boolean")
|
|
2174
|
+
base.runtime_snapshot.enabled = runtimeSnapshot.enabled;
|
|
2175
|
+
}
|
|
2176
|
+
return base;
|
|
2177
|
+
}
|
|
2178
|
+
function isValidMode(value) {
|
|
2179
|
+
return value === "off" || value === "observe" || value === "enforce";
|
|
2180
|
+
}
|
|
2181
|
+
function isValidRule(value) {
|
|
2182
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2183
|
+
return false;
|
|
2184
|
+
const rule = value;
|
|
2185
|
+
return typeof rule.id === "string" && typeof rule.category === "string" && !!rule.match && typeof rule.match === "object";
|
|
2186
|
+
}
|
|
2187
|
+
function migrateLegacyDeny(deny) {
|
|
2188
|
+
const rules = [];
|
|
2189
|
+
for (const entry of deny) {
|
|
2190
|
+
if (typeof entry.id !== "string")
|
|
2191
|
+
continue;
|
|
2192
|
+
const match = {};
|
|
2193
|
+
if (typeof entry.pattern === "string")
|
|
2194
|
+
match.pattern = entry.pattern;
|
|
2195
|
+
if (typeof entry.regex === "string")
|
|
2196
|
+
match.regex = entry.regex;
|
|
2197
|
+
if (!match.pattern && !match.regex)
|
|
2198
|
+
continue;
|
|
2199
|
+
const rule = {
|
|
2200
|
+
id: entry.id,
|
|
2201
|
+
category: "command",
|
|
2202
|
+
match,
|
|
2203
|
+
action: entry.action === "warn" ? "warn" : "block"
|
|
2204
|
+
};
|
|
2205
|
+
if (typeof entry.reason === "string") {
|
|
2206
|
+
rule.description = entry.reason;
|
|
2207
|
+
}
|
|
2208
|
+
rules.push(rule);
|
|
2209
|
+
}
|
|
2210
|
+
return rules;
|
|
2211
|
+
}
|
|
2212
|
+
function precompilePolicyRuleRegexes(rules) {
|
|
2213
|
+
return rules.map((rule) => {
|
|
2214
|
+
const compiled = { ...rule };
|
|
2215
|
+
const matchRegex = compileRegex(rule.match?.regex);
|
|
2216
|
+
const unlessRegex = compileRegex(rule.unless?.regex);
|
|
2217
|
+
if (matchRegex) {
|
|
2218
|
+
compiled.compiledRegex = matchRegex;
|
|
2219
|
+
}
|
|
2220
|
+
if (unlessRegex) {
|
|
2221
|
+
compiled.compiledUnlessRegex = unlessRegex;
|
|
2222
|
+
}
|
|
2223
|
+
return compiled;
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
function compileRegex(pattern) {
|
|
2227
|
+
if (!pattern)
|
|
2228
|
+
return;
|
|
2229
|
+
try {
|
|
2230
|
+
return new RegExp(pattern);
|
|
2231
|
+
} catch {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
var DEFAULT_SCOPE, DEFAULT_SANDBOX, DEFAULT_ISOLATION, DEFAULT_COMPLETION, DEFAULT_RUNTIME_IMAGE, DEFAULT_RUNTIME_SNAPSHOT, policyCache = null, policyCachePath = null, seededPolicyConfig = null;
|
|
2236
|
+
var init_policy = __esm(() => {
|
|
2237
|
+
DEFAULT_SCOPE = {
|
|
2238
|
+
fail_closed: true,
|
|
2239
|
+
harness_paths_exempt: true,
|
|
2240
|
+
runtime_paths_exempt: true
|
|
2241
|
+
};
|
|
2242
|
+
DEFAULT_SANDBOX = {
|
|
2243
|
+
mode: "enforce",
|
|
2244
|
+
network: true,
|
|
2245
|
+
read_deny: [],
|
|
2246
|
+
write_allow_from_runtime: true
|
|
2247
|
+
};
|
|
2248
|
+
DEFAULT_ISOLATION = {
|
|
2249
|
+
default_mode: "worktree",
|
|
2250
|
+
repo_symlink_fallback: false,
|
|
2251
|
+
strict_provisioning: true,
|
|
2252
|
+
fail_closed_on_provision_error: true
|
|
2253
|
+
};
|
|
2254
|
+
DEFAULT_COMPLETION = {
|
|
2255
|
+
derive_checks_from_scope: true,
|
|
2256
|
+
checks: [],
|
|
2257
|
+
typescript_config_probe: ["tsconfig.json"],
|
|
2258
|
+
eslint_config_probe: [".eslintrc.js", ".eslintrc.json", "eslint.config.js"]
|
|
2259
|
+
};
|
|
2260
|
+
DEFAULT_RUNTIME_IMAGE = {
|
|
2261
|
+
deps: {
|
|
2262
|
+
monorepo_install: false,
|
|
2263
|
+
hp_next_install: false
|
|
2264
|
+
},
|
|
2265
|
+
plugins_require_binaries: true
|
|
2266
|
+
};
|
|
2267
|
+
DEFAULT_RUNTIME_SNAPSHOT = {
|
|
2268
|
+
enabled: true
|
|
2269
|
+
};
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
// packages/bundle-default-lifecycle/src/control-plane/verifier.ts
|
|
2273
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2274
|
+
import { resolve as resolve4 } from "path";
|
|
2275
|
+
import { resolveRuntimeSecrets as resolveRuntimeSecrets2 } from "@rig/core/baked-secrets";
|
|
2276
|
+
import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/core/runtime-context";
|
|
2277
|
+
import { nowIso as nowIso2, runCapture as runCapture2 } from "@rig/core/exec";
|
|
2278
|
+
import { resolveHarnessPaths } from "@rig/core/harness-paths";
|
|
2279
|
+
async function ensureMergeGate(projectRoot) {
|
|
2280
|
+
mergeGateHolder = await resolvePrMergeGateService(projectRoot);
|
|
2281
|
+
return mergeGateHolder;
|
|
2282
|
+
}
|
|
2283
|
+
function mg() {
|
|
2284
|
+
if (!mergeGateHolder) {
|
|
2285
|
+
throw new Error("PR merge-gate capability not resolved (verifyTask must run first).");
|
|
2286
|
+
}
|
|
2287
|
+
return mergeGateHolder;
|
|
2288
|
+
}
|
|
674
2289
|
async function verifyTask(options) {
|
|
2290
|
+
await ensureMergeGate(options.projectRoot);
|
|
675
2291
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
676
2292
|
const taskId = options.taskId;
|
|
677
|
-
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
678
|
-
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
679
|
-
|
|
680
|
-
const validationSummaryPath =
|
|
681
|
-
const reviewFeedbackPath =
|
|
682
|
-
const reviewStatePath =
|
|
683
|
-
const greptileRawPath =
|
|
2293
|
+
const normalizedTaskId = taskData().lookupTask(options.projectRoot, taskId);
|
|
2294
|
+
const artifactDir = taskData().artifactDirForId(options.projectRoot, taskId);
|
|
2295
|
+
mkdirSync2(artifactDir, { recursive: true });
|
|
2296
|
+
const validationSummaryPath = resolve4(artifactDir, "validation-summary.json");
|
|
2297
|
+
const reviewFeedbackPath = resolve4(artifactDir, "review-feedback.md");
|
|
2298
|
+
const reviewStatePath = resolve4(artifactDir, "review-state.json");
|
|
2299
|
+
const greptileRawPath = resolve4(artifactDir, "review-greptile-raw.json");
|
|
684
2300
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
685
2301
|
const prState = prStates[0] || null;
|
|
686
2302
|
const localReasons = [];
|
|
@@ -692,7 +2308,7 @@ async function verifyTask(options) {
|
|
|
692
2308
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
693
2309
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
694
2310
|
}
|
|
695
|
-
if (!
|
|
2311
|
+
if (!existsSync5(validationSummaryPath)) {
|
|
696
2312
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
697
2313
|
} else {
|
|
698
2314
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -701,13 +2317,13 @@ async function verifyTask(options) {
|
|
|
701
2317
|
}
|
|
702
2318
|
}
|
|
703
2319
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
704
|
-
const requiredPath =
|
|
705
|
-
if (!
|
|
2320
|
+
const requiredPath = resolve4(artifactDir, file);
|
|
2321
|
+
if (!existsSync5(requiredPath)) {
|
|
706
2322
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
707
2323
|
}
|
|
708
2324
|
}
|
|
709
|
-
const taskResultPath =
|
|
710
|
-
if (
|
|
2325
|
+
const taskResultPath = resolve4(artifactDir, "task-result.json");
|
|
2326
|
+
if (existsSync5(taskResultPath)) {
|
|
711
2327
|
const taskResult = await readJsonFile(taskResultPath);
|
|
712
2328
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
713
2329
|
if (artifactStatus === "partial") {
|
|
@@ -720,8 +2336,8 @@ async function verifyTask(options) {
|
|
|
720
2336
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
721
2337
|
}
|
|
722
2338
|
}
|
|
723
|
-
const nextActionsPath =
|
|
724
|
-
if (
|
|
2339
|
+
const nextActionsPath = resolve4(artifactDir, "next-actions.md");
|
|
2340
|
+
if (existsSync5(nextActionsPath)) {
|
|
725
2341
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
726
2342
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
727
2343
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -752,7 +2368,7 @@ async function verifyTask(options) {
|
|
|
752
2368
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
753
2369
|
}
|
|
754
2370
|
if (persistArtifacts && ai.rawResponse) {
|
|
755
|
-
|
|
2371
|
+
writeFileSync2(greptileRawPath, `${ai.rawResponse}
|
|
756
2372
|
`, "utf-8");
|
|
757
2373
|
}
|
|
758
2374
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -874,15 +2490,15 @@ function nextActionsIndicateRemainingScope(content) {
|
|
|
874
2490
|
return /^\s*- \[ \]/m.test(normalized) || /\b(remaining scope|still need|needs? to be implemented|not yet implemented|not implemented|follow[- ]?up required|blocked by|blocker:)\b/i.test(lower);
|
|
875
2491
|
}
|
|
876
2492
|
async function hasConfiguredSourceTask(projectRoot, taskId) {
|
|
877
|
-
return readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
2493
|
+
return taskData().readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
878
2494
|
}
|
|
879
2495
|
function resolveGithubSourceIssueId(projectRoot, taskId) {
|
|
880
|
-
const fromRuntime =
|
|
2496
|
+
const fromRuntime = loadRuntimeContextFromEnv2()?.sourceTask?.sourceIssueId;
|
|
881
2497
|
if (typeof fromRuntime === "string" && isGithubSourceIssueId(fromRuntime)) {
|
|
882
2498
|
return fromRuntime;
|
|
883
2499
|
}
|
|
884
2500
|
try {
|
|
885
|
-
const taskConfig = readTaskConfig(projectRoot);
|
|
2501
|
+
const taskConfig = taskData().readTaskConfig(projectRoot);
|
|
886
2502
|
const entry = taskConfig[taskId];
|
|
887
2503
|
const sourceIssueId = typeof entry?.sourceIssueId === "string" ? entry.sourceIssueId : typeof entry?.source_issue_id === "string" ? entry.source_issue_id : null;
|
|
888
2504
|
if (sourceIssueId && isGithubSourceIssueId(sourceIssueId)) {
|
|
@@ -953,14 +2569,15 @@ function loadGithubPullRequestCloseoutSnapshot(projectRoot, prState) {
|
|
|
953
2569
|
"--json",
|
|
954
2570
|
"state,isDraft,mergeable,mergeStateStatus,reviewDecision,title,body,statusCheckRollup"
|
|
955
2571
|
]);
|
|
2572
|
+
const isDraft = booleanField(view, "isDraft");
|
|
956
2573
|
return {
|
|
957
|
-
state
|
|
958
|
-
isDraft
|
|
959
|
-
mergeable
|
|
960
|
-
mergeStateStatus
|
|
961
|
-
reviewDecision
|
|
962
|
-
title
|
|
963
|
-
body
|
|
2574
|
+
...objectField("state", stringField(view, "state")),
|
|
2575
|
+
...isDraft !== undefined ? { isDraft } : {},
|
|
2576
|
+
...objectField("mergeable", stringField(view, "mergeable")),
|
|
2577
|
+
...objectField("mergeStateStatus", stringField(view, "mergeStateStatus")),
|
|
2578
|
+
...objectField("reviewDecision", stringField(view, "reviewDecision")),
|
|
2579
|
+
...objectField("title", stringField(view, "title")),
|
|
2580
|
+
...objectField("body", stringField(view, "body")),
|
|
964
2581
|
statusCheckRollup: statusCheckRollupField(view, "statusCheckRollup"),
|
|
965
2582
|
reviewThreads: loadGithubReviewThreads(projectRoot, repoName, prNumber)
|
|
966
2583
|
};
|
|
@@ -1035,6 +2652,9 @@ function stringField(record, key) {
|
|
|
1035
2652
|
const value = record[key];
|
|
1036
2653
|
return typeof value === "string" ? value : undefined;
|
|
1037
2654
|
}
|
|
2655
|
+
function objectField(key, value) {
|
|
2656
|
+
return value === undefined ? {} : { [key]: value };
|
|
2657
|
+
}
|
|
1038
2658
|
function booleanField(record, key) {
|
|
1039
2659
|
const value = record[key];
|
|
1040
2660
|
return typeof value === "boolean" ? value : undefined;
|
|
@@ -1091,7 +2711,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
1091
2711
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
1092
2712
|
}
|
|
1093
2713
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
1094
|
-
const parsed =
|
|
2714
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
1095
2715
|
const mode = parsed?.mode;
|
|
1096
2716
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
1097
2717
|
return mode;
|
|
@@ -1102,7 +2722,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
1102
2722
|
return "advisory";
|
|
1103
2723
|
}
|
|
1104
2724
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
1105
|
-
const parsed =
|
|
2725
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
1106
2726
|
const provider = parsed?.provider;
|
|
1107
2727
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
1108
2728
|
return provider;
|
|
@@ -1111,7 +2731,7 @@ async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
|
1111
2731
|
}
|
|
1112
2732
|
function resolveRepoSlug(projectRoot) {
|
|
1113
2733
|
const paths = resolveHarnessPaths(projectRoot);
|
|
1114
|
-
const remote =
|
|
2734
|
+
const remote = runCapture2(["git", "-C", paths.monorepoRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim() || runCapture2(["git", "-C", projectRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim();
|
|
1115
2735
|
if (!remote) {
|
|
1116
2736
|
return "";
|
|
1117
2737
|
}
|
|
@@ -1125,7 +2745,7 @@ function resolveRepoSlug(projectRoot) {
|
|
|
1125
2745
|
async function runGreptileReview(options) {
|
|
1126
2746
|
const reasons = [];
|
|
1127
2747
|
const warnings = [];
|
|
1128
|
-
const secrets =
|
|
2748
|
+
const secrets = resolveRuntimeSecrets2(process.env);
|
|
1129
2749
|
const apiKey = secrets.GREPTILE_API_KEY || "";
|
|
1130
2750
|
const apiBase = secrets.GREPTILE_API_BASE || "https://api.greptile.com/mcp";
|
|
1131
2751
|
const remote = secrets.GREPTILE_REMOTE || "github";
|
|
@@ -1261,7 +2881,7 @@ function writeFeedbackFile(options) {
|
|
|
1261
2881
|
if (options.aiRawFeedback) {
|
|
1262
2882
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
1263
2883
|
}
|
|
1264
|
-
|
|
2884
|
+
writeFileSync2(options.output, `${lines.join(`
|
|
1265
2885
|
`)}
|
|
1266
2886
|
`, "utf-8");
|
|
1267
2887
|
}
|
|
@@ -1276,9 +2896,9 @@ function writeReviewStateFile(options) {
|
|
|
1276
2896
|
local_reasons: options.localReasons,
|
|
1277
2897
|
ai_reasons: options.aiReasons,
|
|
1278
2898
|
ai_warnings: options.aiWarnings,
|
|
1279
|
-
updated_at:
|
|
2899
|
+
updated_at: nowIso2()
|
|
1280
2900
|
};
|
|
1281
|
-
|
|
2901
|
+
writeFileSync2(options.output, `${JSON.stringify(payload, null, 2)}
|
|
1282
2902
|
`, "utf-8");
|
|
1283
2903
|
}
|
|
1284
2904
|
async function runGreptileReviewForPr(options) {
|
|
@@ -1303,7 +2923,6 @@ async function runGreptileReviewForPr(options) {
|
|
|
1303
2923
|
taskId: options.taskId,
|
|
1304
2924
|
prState: options.prState,
|
|
1305
2925
|
reviewMode: options.reviewMode,
|
|
1306
|
-
infrastructureError: undefined,
|
|
1307
2926
|
pollAttempts: options.pollAttempts,
|
|
1308
2927
|
pollIntervalMs: options.pollIntervalMs
|
|
1309
2928
|
});
|
|
@@ -1431,7 +3050,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
1431
3050
|
});
|
|
1432
3051
|
const actionableComments = filterActionableGreptileComments(commentsPayload.comments || []);
|
|
1433
3052
|
const reviewBody = reviewDetails.codeReview?.body || "";
|
|
1434
|
-
const score = parseGreptileScore(reviewBody);
|
|
3053
|
+
const score = mg().parseGreptileScore(reviewBody);
|
|
1435
3054
|
const feedback = [
|
|
1436
3055
|
`## ${options.prState.repoLabel || repoName} PR Review`,
|
|
1437
3056
|
"",
|
|
@@ -1439,7 +3058,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
1439
3058
|
`- Review ID: ${selectedReview.id}`,
|
|
1440
3059
|
`- Status: ${selectedReview.status}`,
|
|
1441
3060
|
"",
|
|
1442
|
-
reviewBody ? stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
3061
|
+
reviewBody ? mg().stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
1443
3062
|
].filter(Boolean).join(`
|
|
1444
3063
|
`);
|
|
1445
3064
|
if (actionableComments.length > 0) {
|
|
@@ -1460,7 +3079,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
1460
3079
|
}
|
|
1461
3080
|
};
|
|
1462
3081
|
}
|
|
1463
|
-
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3082
|
+
const blockerScanBody = mg().stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
1464
3083
|
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
1465
3084
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
1466
3085
|
return {
|
|
@@ -1508,7 +3127,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
1508
3127
|
status: selectedReview.status
|
|
1509
3128
|
}]
|
|
1510
3129
|
});
|
|
1511
|
-
strictGate =
|
|
3130
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
1512
3131
|
} catch (error) {
|
|
1513
3132
|
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1514
3133
|
return {
|
|
@@ -1635,7 +3254,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
1635
3254
|
fallbackReview?.html_url ? `- Review: ${fallbackReview.html_url}` : "",
|
|
1636
3255
|
fallbackReview?.state ? `- Status: ${fallbackReview.state}` : "",
|
|
1637
3256
|
"",
|
|
1638
|
-
fallbackReview?.body?.trim() ? stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
|
|
3257
|
+
fallbackReview?.body?.trim() ? mg().stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
|
|
1639
3258
|
].filter(Boolean).join(`
|
|
1640
3259
|
`);
|
|
1641
3260
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
@@ -1658,7 +3277,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
1658
3277
|
taskId: options.taskId,
|
|
1659
3278
|
prUrl
|
|
1660
3279
|
});
|
|
1661
|
-
strictGate =
|
|
3280
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
1662
3281
|
} catch (error) {
|
|
1663
3282
|
return {
|
|
1664
3283
|
verdict: "REJECT",
|
|
@@ -1883,7 +3502,7 @@ function loadGithubPullRequestState(projectRoot, repoName, prNumber) {
|
|
|
1883
3502
|
]);
|
|
1884
3503
|
return {
|
|
1885
3504
|
state: response.state || "",
|
|
1886
|
-
merged: response.merged,
|
|
3505
|
+
...response.merged !== undefined ? { merged: response.merged } : {},
|
|
1887
3506
|
merged_at: response.merged_at ?? null
|
|
1888
3507
|
};
|
|
1889
3508
|
}
|
|
@@ -1901,7 +3520,7 @@ function parsePullRequestNumber(url) {
|
|
|
1901
3520
|
return match ? Number.parseInt(match[1] || "0", 10) : 0;
|
|
1902
3521
|
}
|
|
1903
3522
|
function runGhJson(projectRoot, args) {
|
|
1904
|
-
const result =
|
|
3523
|
+
const result = runCapture2(["gh", ...args], projectRoot);
|
|
1905
3524
|
if (result.exitCode !== 0) {
|
|
1906
3525
|
throw new Error(result.stderr || result.stdout || `gh ${args.join(" ")} failed`);
|
|
1907
3526
|
}
|
|
@@ -1912,7 +3531,7 @@ function runGhJson(projectRoot, args) {
|
|
|
1912
3531
|
}
|
|
1913
3532
|
}
|
|
1914
3533
|
async function collectStrictPrEvidenceForVerifier(input) {
|
|
1915
|
-
return
|
|
3534
|
+
return mg().collectEvidence({
|
|
1916
3535
|
projectRoot: input.projectRoot,
|
|
1917
3536
|
prUrl: input.prUrl,
|
|
1918
3537
|
taskId: input.taskId,
|
|
@@ -1920,7 +3539,7 @@ async function collectStrictPrEvidenceForVerifier(input) {
|
|
|
1920
3539
|
cycle: 0,
|
|
1921
3540
|
apiSignals: input.apiSignals ?? [],
|
|
1922
3541
|
command: async (args, options) => {
|
|
1923
|
-
const result =
|
|
3542
|
+
const result = runCapture2(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
1924
3543
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
1925
3544
|
}
|
|
1926
3545
|
});
|
|
@@ -1933,11 +3552,11 @@ function deriveRepoName(projectRoot, prState) {
|
|
|
1933
3552
|
if (prState.target === "monorepo") {
|
|
1934
3553
|
return resolveRepoSlug(projectRoot);
|
|
1935
3554
|
}
|
|
1936
|
-
return
|
|
3555
|
+
return runCapture2(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
|
|
1937
3556
|
}
|
|
1938
3557
|
function resolvePrHeadSha(projectRoot, prState) {
|
|
1939
3558
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
1940
|
-
return
|
|
3559
|
+
return runCapture2(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
1941
3560
|
}
|
|
1942
3561
|
function isGreptileGithubLogin(login) {
|
|
1943
3562
|
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
@@ -2077,7 +3696,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
2077
3696
|
}
|
|
2078
3697
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
2079
3698
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2080
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
3699
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync5(resolve4(runtimeWorkspace, ".git"))) {
|
|
2081
3700
|
return runtimeWorkspace;
|
|
2082
3701
|
}
|
|
2083
3702
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -2088,10 +3707,10 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
2088
3707
|
return false;
|
|
2089
3708
|
}
|
|
2090
3709
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
2091
|
-
return
|
|
3710
|
+
return runCapture2(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
2092
3711
|
}
|
|
2093
3712
|
function summarizeComment(input) {
|
|
2094
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3713
|
+
const text = mg().stripHtml(input).replace(/\s+/g, " ").trim();
|
|
2095
3714
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
2096
3715
|
}
|
|
2097
3716
|
function asGreptileInfrastructureWarning(reason) {
|
|
@@ -2106,7 +3725,12 @@ function isAiReviewApproved(input) {
|
|
|
2106
3725
|
}
|
|
2107
3726
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
2108
3727
|
}
|
|
2109
|
-
var
|
|
3728
|
+
var mergeGateHolder = null;
|
|
3729
|
+
var init_verifier = __esm(() => {
|
|
3730
|
+
init_git_ops();
|
|
3731
|
+
init_task_data();
|
|
3732
|
+
init_pr_merge_gate_cap();
|
|
3733
|
+
});
|
|
2110
3734
|
|
|
2111
3735
|
// packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
|
|
2112
3736
|
var exports_completion_verification = {};
|
|
@@ -2115,43 +3739,33 @@ __export(exports_completion_verification, {
|
|
|
2115
3739
|
formatCompletionBlockedMessage: () => formatCompletionBlockedMessage,
|
|
2116
3740
|
closeCompletedTaskSource: () => closeCompletedTaskSource
|
|
2117
3741
|
});
|
|
2118
|
-
import { appendFileSync, existsSync as
|
|
2119
|
-
import { resolve as
|
|
2120
|
-
import { safePathSegment } from "@rig/
|
|
3742
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
3743
|
+
import { resolve as resolve5 } from "path";
|
|
3744
|
+
import { safePathSegment as safePathSegment2 } from "@rig/core/safe-identifiers";
|
|
2121
3745
|
import {
|
|
2122
|
-
escapeRegExp,
|
|
3746
|
+
escapeRegExp as escapeRegExp2,
|
|
2123
3747
|
resolveBunCli,
|
|
2124
3748
|
resolveBunCliInvocation,
|
|
2125
3749
|
resolveTaskScopes,
|
|
2126
3750
|
resolvePolicyContent
|
|
2127
3751
|
} from "@rig/hook-kit";
|
|
2128
|
-
import {
|
|
2129
|
-
import {
|
|
2130
|
-
import {
|
|
2131
|
-
import { strictMergeHeadShaFromGate as strictMergeHeadShaFromGate2 } from "@rig/contracts";
|
|
2132
|
-
import { changedFilesForTask, pendingFilesForTask, taskArtifacts, taskValidate as taskValidate2 } from "@rig/runtime/control-plane/native/task-ops";
|
|
2133
|
-
import { currentTaskId } from "@rig/runtime/control-plane/native/task-state";
|
|
2134
|
-
import { resolveHarnessPaths as resolveHarnessPaths2, runCapture as runCapture2 } from "@rig/runtime/control-plane/native/utils";
|
|
2135
|
-
import { readSourceAwareTaskStatus } from "@rig/runtime/control-plane/tasks/source-aware-task-config-source";
|
|
2136
|
-
import {
|
|
2137
|
-
buildTaskRunLifecycleComment,
|
|
2138
|
-
updateConfiguredTaskSourceTask
|
|
2139
|
-
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
2140
|
-
import { buildPluginHostContext as buildPluginHostContext2 } from "@rig/runtime/control-plane/plugin-host-context";
|
|
3752
|
+
import { runCapture as runCapture3 } from "@rig/core/exec";
|
|
3753
|
+
import { resolveHarnessPaths as resolveHarnessPaths2 } from "@rig/core/harness-paths";
|
|
3754
|
+
import { buildPluginHostContext } from "@rig/core/plugin-host-context";
|
|
2141
3755
|
async function closeCompletedTaskSource(projectRoot, taskId) {
|
|
2142
|
-
const comment = buildTaskRunLifecycleComment({
|
|
3756
|
+
const comment = taskData().buildTaskRunLifecycleComment({
|
|
2143
3757
|
runId: process.env.RIG_SERVER_RUN_ID || taskId,
|
|
2144
3758
|
status: "closed",
|
|
2145
3759
|
summary: "Rig completion verification approved and closed this task.",
|
|
2146
|
-
runtimeWorkspace: process.env.RIG_TASK_WORKSPACE,
|
|
2147
|
-
logsDir: process.env.RIG_LOGS_DIR,
|
|
2148
|
-
sessionDir: process.env.RIG_SESSION_FILE
|
|
3760
|
+
...process.env.RIG_TASK_WORKSPACE !== undefined ? { runtimeWorkspace: process.env.RIG_TASK_WORKSPACE } : {},
|
|
3761
|
+
...process.env.RIG_LOGS_DIR !== undefined ? { logsDir: process.env.RIG_LOGS_DIR } : {},
|
|
3762
|
+
...process.env.RIG_SESSION_FILE !== undefined ? { sessionDir: process.env.RIG_SESSION_FILE } : {}
|
|
2149
3763
|
});
|
|
2150
|
-
const result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
3764
|
+
const result = await taskData().updateConfiguredTaskSourceTask(projectRoot, {
|
|
2151
3765
|
taskId,
|
|
2152
3766
|
update: { status: "closed", comment }
|
|
2153
3767
|
});
|
|
2154
|
-
const status = result.status ?? await readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
3768
|
+
const status = result.status ?? await taskData().readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
2155
3769
|
if (!result.updated && status == null) {
|
|
2156
3770
|
return {
|
|
2157
3771
|
ok: true,
|
|
@@ -2171,7 +3785,7 @@ function isClosedStatus(status) {
|
|
|
2171
3785
|
}
|
|
2172
3786
|
async function runCompletionVerificationGate(projectRoot) {
|
|
2173
3787
|
seedPolicyFromContent(resolvePolicyContent(projectRoot));
|
|
2174
|
-
const taskId = currentTaskId(projectRoot);
|
|
3788
|
+
const taskId = taskData().currentTaskId(projectRoot);
|
|
2175
3789
|
if (!taskId) {
|
|
2176
3790
|
return { ok: true };
|
|
2177
3791
|
}
|
|
@@ -2180,7 +3794,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2180
3794
|
let sourceCloseoutAllowed = false;
|
|
2181
3795
|
console.log(`=== Completion Verification: ${taskId} ===`);
|
|
2182
3796
|
const scopes = await resolveTaskScopes(projectRoot, taskId);
|
|
2183
|
-
const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
3797
|
+
const taskChangedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
|
|
2184
3798
|
const sourceInArtifacts = taskChangedFiles.filter((file) => /^artifacts\//.test(file) && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
|
|
2185
3799
|
if (sourceInArtifacts.length > 0) {
|
|
2186
3800
|
console.log(`
|
|
@@ -2193,13 +3807,13 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2193
3807
|
}
|
|
2194
3808
|
failed = true;
|
|
2195
3809
|
}
|
|
2196
|
-
const pluginHostCtx = await
|
|
3810
|
+
const pluginHostCtx = await buildPluginHostContext(projectRoot).catch((error) => {
|
|
2197
3811
|
console.warn(`[completion-verification] plugin host unavailable for validators: ${error instanceof Error ? error.message : String(error)}`);
|
|
2198
3812
|
return null;
|
|
2199
3813
|
});
|
|
2200
3814
|
console.log(`
|
|
2201
3815
|
[1/3] Task validation...`);
|
|
2202
|
-
if (!await
|
|
3816
|
+
if (!await taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
|
|
2203
3817
|
console.log(`FAIL: Validation failed for ${taskId}`);
|
|
2204
3818
|
failed = true;
|
|
2205
3819
|
} else {
|
|
@@ -2212,7 +3826,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2212
3826
|
failed = true;
|
|
2213
3827
|
}
|
|
2214
3828
|
}
|
|
2215
|
-
taskArtifacts(projectRoot, taskId);
|
|
3829
|
+
taskData().taskArtifacts(projectRoot, taskId);
|
|
2216
3830
|
const policy = loadPolicy(projectRoot);
|
|
2217
3831
|
const openPrEnabled = policy.completion.checks.includes("open-pr");
|
|
2218
3832
|
const autoMergeEnabled = policy.completion.checks.includes("auto-merge");
|
|
@@ -2239,7 +3853,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2239
3853
|
} else {
|
|
2240
3854
|
console.log("Verifier preflight: skipped (earlier checks failed)");
|
|
2241
3855
|
}
|
|
2242
|
-
const pendingTaskChangedFiles = pendingFilesForTask(projectRoot, taskId, true);
|
|
3856
|
+
const pendingTaskChangedFiles = taskData().pendingFilesForTask(projectRoot, taskId, true);
|
|
2243
3857
|
const hasLocalChanges = pendingTaskChangedFiles.length > 0;
|
|
2244
3858
|
console.log(`
|
|
2245
3859
|
[post] Auto-committing task changes...`);
|
|
@@ -2312,14 +3926,15 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2312
3926
|
console.log(`
|
|
2313
3927
|
[post] Auto-merge...`);
|
|
2314
3928
|
try {
|
|
2315
|
-
const prs =
|
|
3929
|
+
const prs = readPrMetadata(projectRoot, taskId);
|
|
2316
3930
|
if (prs.length === 0) {
|
|
2317
3931
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
2318
3932
|
} else {
|
|
3933
|
+
const mergeGate = await resolvePrMergeGateService(projectRoot);
|
|
2319
3934
|
let cycle = 0;
|
|
2320
3935
|
for (const pr of prs) {
|
|
2321
3936
|
cycle += 1;
|
|
2322
|
-
const gate = await
|
|
3937
|
+
const gate = await mergeGate.runGate({
|
|
2323
3938
|
projectRoot,
|
|
2324
3939
|
prUrl: pr.url,
|
|
2325
3940
|
taskId,
|
|
@@ -2327,7 +3942,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2327
3942
|
cycle,
|
|
2328
3943
|
final: true,
|
|
2329
3944
|
command: async (args, options) => {
|
|
2330
|
-
const result =
|
|
3945
|
+
const result = runCapture3(["gh", ...args], options?.cwd ?? projectRoot);
|
|
2331
3946
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
2332
3947
|
}
|
|
2333
3948
|
});
|
|
@@ -2344,7 +3959,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2344
3959
|
pr,
|
|
2345
3960
|
method: "squash",
|
|
2346
3961
|
deleteBranch: true,
|
|
2347
|
-
matchHeadCommit:
|
|
3962
|
+
matchHeadCommit: mergeGate.resolveHeadSha({ result: gate, prUrl: pr.url })
|
|
2348
3963
|
});
|
|
2349
3964
|
if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
|
|
2350
3965
|
console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
|
|
@@ -2364,9 +3979,9 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
2364
3979
|
console.log(`
|
|
2365
3980
|
[post] Auto-merge: skipped (not in policy completion.checks)`);
|
|
2366
3981
|
}
|
|
2367
|
-
const artifactDir =
|
|
2368
|
-
|
|
2369
|
-
|
|
3982
|
+
const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
3983
|
+
mkdirSync3(artifactDir, { recursive: true });
|
|
3984
|
+
writeFileSync3(resolve5(artifactDir, "review-status.txt"), failed ? `REJECTED
|
|
2370
3985
|
` : `APPROVED
|
|
2371
3986
|
`, "utf-8");
|
|
2372
3987
|
if (!failed) {
|
|
@@ -2416,8 +4031,8 @@ async function runBunTool(args, cwd) {
|
|
|
2416
4031
|
};
|
|
2417
4032
|
}
|
|
2418
4033
|
async function runProtoQualityGate(monorepoRoot) {
|
|
2419
|
-
const protosDir =
|
|
2420
|
-
if (!
|
|
4034
|
+
const protosDir = resolve5(monorepoRoot, "packages", "protos");
|
|
4035
|
+
if (!existsSync6(protosDir)) {
|
|
2421
4036
|
console.log(`FAIL: Proto workspace not found at ${protosDir}`);
|
|
2422
4037
|
return false;
|
|
2423
4038
|
}
|
|
@@ -2444,7 +4059,7 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
2444
4059
|
console.log(generate.stderr || generate.stdout);
|
|
2445
4060
|
ok = false;
|
|
2446
4061
|
} else {
|
|
2447
|
-
const drift =
|
|
4062
|
+
const drift = runCapture3(["git", "-C", protosDir, "status", "--porcelain", "--", "gen/ts"], monorepoRoot);
|
|
2448
4063
|
if (drift.exitCode !== 0) {
|
|
2449
4064
|
console.log("FAIL: Could not inspect generated proto drift");
|
|
2450
4065
|
console.log(drift.stderr || drift.stdout);
|
|
@@ -2465,12 +4080,12 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
2465
4080
|
} else {
|
|
2466
4081
|
console.log("OK: Generated TypeScript compiles");
|
|
2467
4082
|
}
|
|
2468
|
-
const workflowPath =
|
|
2469
|
-
if (!
|
|
4083
|
+
const workflowPath = resolve5(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
|
|
4084
|
+
if (!existsSync6(workflowPath)) {
|
|
2470
4085
|
console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
|
|
2471
4086
|
ok = false;
|
|
2472
4087
|
} else {
|
|
2473
|
-
const workflow =
|
|
4088
|
+
const workflow = readFileSync4(workflowPath, "utf-8");
|
|
2474
4089
|
if (workflow.includes("if: false && needs.detect.outputs.protos_changed == 'true'")) {
|
|
2475
4090
|
console.log("FAIL: Proto quality CI gate is disabled in pull-request-gate.yml");
|
|
2476
4091
|
ok = false;
|
|
@@ -2481,13 +4096,13 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
2481
4096
|
return ok;
|
|
2482
4097
|
}
|
|
2483
4098
|
function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
2484
|
-
const unpushed =
|
|
4099
|
+
const unpushed = runCapture3(["git", "-C", repoRoot, "log", "@{u}..HEAD", "--oneline"], projectRoot);
|
|
2485
4100
|
if (unpushed.exitCode === 0 && unpushed.stdout.trim().length > 0)
|
|
2486
4101
|
return true;
|
|
2487
4102
|
if (unpushed.exitCode !== 0) {
|
|
2488
|
-
const branch =
|
|
4103
|
+
const branch = runCapture3(["git", "-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
|
|
2489
4104
|
if (branch.exitCode === 0 && branch.stdout.trim()) {
|
|
2490
|
-
const remote =
|
|
4105
|
+
const remote = runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branch.stdout.trim()}`], projectRoot);
|
|
2491
4106
|
if (remote.exitCode !== 0)
|
|
2492
4107
|
return true;
|
|
2493
4108
|
}
|
|
@@ -2496,10 +4111,10 @@ function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
|
2496
4111
|
}
|
|
2497
4112
|
function repoHasPublishedTaskBranch(projectRoot, repoRoot, taskId) {
|
|
2498
4113
|
const branchRef = resolveTaskBranchRef(projectRoot, taskId);
|
|
2499
|
-
return
|
|
4114
|
+
return runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branchRef}`], projectRoot).exitCode === 0;
|
|
2500
4115
|
}
|
|
2501
4116
|
async function readJsonFileIfPresent(path) {
|
|
2502
|
-
if (!
|
|
4117
|
+
if (!existsSync6(path)) {
|
|
2503
4118
|
return null;
|
|
2504
4119
|
}
|
|
2505
4120
|
try {
|
|
@@ -2510,9 +4125,9 @@ async function readJsonFileIfPresent(path) {
|
|
|
2510
4125
|
}
|
|
2511
4126
|
async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
2512
4127
|
const failedApproachesPath = paths.failedApproachesPath;
|
|
2513
|
-
const artifactDir =
|
|
2514
|
-
const reviewStatePath =
|
|
2515
|
-
const reviewFeedbackPath =
|
|
4128
|
+
const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
4129
|
+
const reviewStatePath = resolve5(artifactDir, "review-state.json");
|
|
4130
|
+
const reviewFeedbackPath = resolve5(artifactDir, "review-feedback.md");
|
|
2516
4131
|
let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
|
|
2517
4132
|
const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
|
|
2518
4133
|
if (parsedReviewState) {
|
|
@@ -2522,12 +4137,12 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
|
2522
4137
|
}
|
|
2523
4138
|
}
|
|
2524
4139
|
let attempts = 1;
|
|
2525
|
-
if (
|
|
2526
|
-
const content =
|
|
2527
|
-
attempts = (content.match(new RegExp(`^## ${
|
|
4140
|
+
if (existsSync6(failedApproachesPath)) {
|
|
4141
|
+
const content = readFileSync4(failedApproachesPath, "utf-8");
|
|
4142
|
+
attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
|
|
2528
4143
|
} else {
|
|
2529
|
-
|
|
2530
|
-
|
|
4144
|
+
mkdirSync3(resolve5(failedApproachesPath, ".."), { recursive: true });
|
|
4145
|
+
writeFileSync3(failedApproachesPath, `# Failed Approaches
|
|
2531
4146
|
|
|
2532
4147
|
`, "utf-8");
|
|
2533
4148
|
}
|
|
@@ -2551,7 +4166,7 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
2551
4166
|
statePaths.add(resolveHarnessPaths2(hostProjectRoot).taskRepoCommitsPath);
|
|
2552
4167
|
}
|
|
2553
4168
|
const repos = {};
|
|
2554
|
-
const monoHead =
|
|
4169
|
+
const monoHead = runCapture3(["git", "-C", paths.monorepoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
2555
4170
|
if (monoHead) {
|
|
2556
4171
|
repos["monorepo"] = monoHead;
|
|
2557
4172
|
}
|
|
@@ -2565,13 +4180,17 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
2565
4180
|
recorded_at: new Date().toISOString(),
|
|
2566
4181
|
repos
|
|
2567
4182
|
};
|
|
2568
|
-
|
|
2569
|
-
|
|
4183
|
+
mkdirSync3(resolve5(statePath, ".."), { recursive: true });
|
|
4184
|
+
writeFileSync3(statePath, `${JSON.stringify(state, null, 2)}
|
|
2570
4185
|
`, "utf-8");
|
|
2571
4186
|
}
|
|
2572
4187
|
}
|
|
2573
4188
|
var init_completion_verification = __esm(() => {
|
|
4189
|
+
init_policy();
|
|
4190
|
+
init_git_ops();
|
|
4191
|
+
init_pr_merge_gate_cap();
|
|
2574
4192
|
init_verifier();
|
|
4193
|
+
init_task_data();
|
|
2575
4194
|
});
|
|
2576
4195
|
|
|
2577
4196
|
// packages/bundle-default-lifecycle/src/control-plane/task-verify.ts
|
|
@@ -2579,9 +4198,8 @@ var exports_task_verify = {};
|
|
|
2579
4198
|
__export(exports_task_verify, {
|
|
2580
4199
|
taskVerify: () => taskVerify
|
|
2581
4200
|
});
|
|
2582
|
-
import { currentTaskId as currentTaskId2 } from "@rig/runtime/control-plane/native/task-state";
|
|
2583
4201
|
async function taskVerify(projectRoot, taskId) {
|
|
2584
|
-
const activeTask = taskId ||
|
|
4202
|
+
const activeTask = taskId || taskData().currentTaskId(projectRoot);
|
|
2585
4203
|
if (!activeTask) {
|
|
2586
4204
|
throw new Error("No active task.");
|
|
2587
4205
|
}
|
|
@@ -2606,42 +4224,28 @@ async function taskVerify(projectRoot, taskId) {
|
|
|
2606
4224
|
return true;
|
|
2607
4225
|
}
|
|
2608
4226
|
var init_task_verify = __esm(() => {
|
|
4227
|
+
init_task_data();
|
|
2609
4228
|
init_verifier();
|
|
2610
4229
|
});
|
|
2611
4230
|
|
|
2612
4231
|
// packages/bundle-default-lifecycle/src/pipelineCloseout.ts
|
|
2613
|
-
import { resolve as
|
|
2614
|
-
import { loadConfig as loadConfig2 } from "@rig/core/load-config";
|
|
2615
|
-
import { resolvePluginHost } from "@rig/core/project-plugins";
|
|
2616
|
-
import { createDefaultKernel } from "@rig/kernel/default-kernel";
|
|
2617
|
-
import { buildPluginHostContext as buildPluginHostContext3 } from "@rig/runtime/control-plane/plugin-host-context";
|
|
2618
|
-
|
|
2619
|
-
// packages/bundle-default-lifecycle/src/native/in-process-closeout.ts
|
|
2620
|
-
init_pr_automation();
|
|
4232
|
+
import { resolve as resolve6 } from "path";
|
|
2621
4233
|
import { loadConfig } from "@rig/core/load-config";
|
|
2622
|
-
import {
|
|
2623
|
-
import {
|
|
2624
|
-
|
|
2625
|
-
"queued",
|
|
2626
|
-
"commit",
|
|
2627
|
-
"push",
|
|
2628
|
-
"pr-review-merge",
|
|
2629
|
-
"pr-opened",
|
|
2630
|
-
"merge",
|
|
2631
|
-
"close-source",
|
|
2632
|
-
"completed"
|
|
2633
|
-
]);
|
|
4234
|
+
import { resolvePluginHost as resolvePluginHost2 } from "@rig/core/project-plugins";
|
|
4235
|
+
import { createDefaultKernel } from "@rig/kernel-seed/default-kernel";
|
|
4236
|
+
import { buildPluginHostContext as buildPluginHostContext2 } from "@rig/core/plugin-host-context";
|
|
2634
4237
|
|
|
4238
|
+
// packages/bundle-default-lifecycle/src/native/in-process-closeout.ts
|
|
2635
4239
|
class CloseoutValidationError extends Error {
|
|
2636
4240
|
name = "CloseoutValidationError";
|
|
2637
4241
|
}
|
|
2638
4242
|
|
|
2639
4243
|
// packages/bundle-default-lifecycle/src/pipelineCloseout.ts
|
|
2640
|
-
|
|
4244
|
+
init_task_data();
|
|
2641
4245
|
|
|
2642
4246
|
// packages/bundle-default-lifecycle/src/defaultPipeline.ts
|
|
2643
|
-
import {
|
|
2644
|
-
import {
|
|
4247
|
+
import { createDefaultKernelPlugin } from "@rig/kernel-seed/default-kernel";
|
|
4248
|
+
import { resolveKernelStages } from "@rig/kernel-seed/resolver";
|
|
2645
4249
|
|
|
2646
4250
|
// packages/bundle-default-lifecycle/src/stages/types.ts
|
|
2647
4251
|
function defineDefaultLifecycleStage(input) {
|
|
@@ -2677,6 +4281,9 @@ async function runCommitStage(input) {
|
|
|
2677
4281
|
}
|
|
2678
4282
|
|
|
2679
4283
|
// packages/bundle-default-lifecycle/src/stages/isolation.ts
|
|
4284
|
+
import { ISOLATION_BACKEND } from "@rig/contracts";
|
|
4285
|
+
import { defineCapability as defineCapability3 } from "@rig/core/capability";
|
|
4286
|
+
import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
2680
4287
|
var isolationStage = defineDefaultLifecycleStage({
|
|
2681
4288
|
id: "isolation",
|
|
2682
4289
|
kind: "transform",
|
|
@@ -2700,8 +4307,9 @@ var mergeGateStage = defineDefaultLifecycleStage({
|
|
|
2700
4307
|
calls: ["runStrictPrMergeGate"]
|
|
2701
4308
|
});
|
|
2702
4309
|
async function runMergeGateStage(input) {
|
|
2703
|
-
const {
|
|
2704
|
-
|
|
4310
|
+
const { resolvePrMergeGateService: resolvePrMergeGateService2 } = await Promise.resolve().then(() => (init_pr_merge_gate_cap(), exports_pr_merge_gate_cap));
|
|
4311
|
+
const mergeGate = await resolvePrMergeGateService2(input.projectRoot);
|
|
4312
|
+
return await mergeGate.runGate({
|
|
2705
4313
|
projectRoot: input.projectRoot,
|
|
2706
4314
|
prUrl: input.prUrl,
|
|
2707
4315
|
taskId: input.taskId,
|
|
@@ -2893,9 +4501,13 @@ function formatDefaultPipelineShow(input = {}) {
|
|
|
2893
4501
|
|
|
2894
4502
|
// packages/bundle-default-lifecycle/src/plugin.ts
|
|
2895
4503
|
import { definePlugin } from "@rig/core/config";
|
|
4504
|
+
import { defineCapability as defineCapability4 } from "@rig/core/capability";
|
|
2896
4505
|
import {
|
|
2897
|
-
|
|
2898
|
-
|
|
4506
|
+
COMPLETION_VERIFICATION_CAPABILITY,
|
|
4507
|
+
LIFECYCLE_GIT_AGENT,
|
|
4508
|
+
LIFECYCLE_TOOLCHAIN_SOURCES,
|
|
4509
|
+
RUN_CLOSEOUT_CAPABILITY,
|
|
4510
|
+
TASK_VERIFY_CAPABILITY
|
|
2899
4511
|
} from "@rig/contracts";
|
|
2900
4512
|
|
|
2901
4513
|
// packages/bundle-default-lifecycle/src/cli.ts
|
|
@@ -2970,7 +4582,46 @@ var defaultLifecycleCliCommands = [
|
|
|
2970
4582
|
}
|
|
2971
4583
|
];
|
|
2972
4584
|
|
|
4585
|
+
// packages/bundle-default-lifecycle/src/native/closeout-runners.ts
|
|
4586
|
+
init_github_auth_env();
|
|
4587
|
+
function commandEnv(env) {
|
|
4588
|
+
const token = resolveGitHubAuthToken(env);
|
|
4589
|
+
return token ? { ...env, RIG_GITHUB_TOKEN: token, GH_TOKEN: token, GITHUB_TOKEN: token } : { ...env };
|
|
4590
|
+
}
|
|
4591
|
+
function createBunCommandRunner(binary, env) {
|
|
4592
|
+
return async (args, options) => {
|
|
4593
|
+
try {
|
|
4594
|
+
const child = Bun.spawn([binary, ...args], {
|
|
4595
|
+
...options?.cwd !== undefined ? { cwd: options.cwd } : {},
|
|
4596
|
+
env: commandEnv(env),
|
|
4597
|
+
stdin: "ignore",
|
|
4598
|
+
stdout: "pipe",
|
|
4599
|
+
stderr: "pipe"
|
|
4600
|
+
});
|
|
4601
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
4602
|
+
child.exited,
|
|
4603
|
+
new Response(child.stdout).text(),
|
|
4604
|
+
new Response(child.stderr).text()
|
|
4605
|
+
]);
|
|
4606
|
+
return { exitCode, stdout, stderr };
|
|
4607
|
+
} catch (error) {
|
|
4608
|
+
return {
|
|
4609
|
+
exitCode: 1,
|
|
4610
|
+
stdout: "",
|
|
4611
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
}
|
|
4616
|
+
function createEnvCloseoutRunners(env = process.env) {
|
|
4617
|
+
return {
|
|
4618
|
+
command: createBunCommandRunner("gh", env),
|
|
4619
|
+
gitCommand: createBunCommandRunner("git", env)
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
|
|
2973
4623
|
// packages/bundle-default-lifecycle/src/plugin.ts
|
|
4624
|
+
init_git_ops();
|
|
2974
4625
|
var DEFAULT_LIFECYCLE_PLUGIN_ID = "@rig/bundle-default-lifecycle";
|
|
2975
4626
|
var COMPLETION_VERIFICATION_HOOK_ID = "@rig/bundle-default-lifecycle:completion-verification";
|
|
2976
4627
|
async function runCompletionGate(projectRoot) {
|
|
@@ -2994,26 +4645,85 @@ var LIFECYCLE_HOOKS = [
|
|
|
2994
4645
|
handler: completionVerificationStopHandler
|
|
2995
4646
|
}
|
|
2996
4647
|
];
|
|
4648
|
+
var TaskVerifyCap = defineCapability4(TASK_VERIFY_CAPABILITY);
|
|
4649
|
+
var CompletionVerificationCap = defineCapability4(COMPLETION_VERIFICATION_CAPABILITY);
|
|
4650
|
+
var RunCloseoutCap = defineCapability4(RUN_CLOSEOUT_CAPABILITY);
|
|
4651
|
+
var LifecycleGitAgentCap = defineCapability4(LIFECYCLE_GIT_AGENT);
|
|
4652
|
+
var ToolchainSourcesCap = defineCapability4(LIFECYCLE_TOOLCHAIN_SOURCES);
|
|
4653
|
+
var LIFECYCLE_TOOLCHAIN_SOURCE_CONTRIBUTION = {
|
|
4654
|
+
hookBinaries: [
|
|
4655
|
+
{
|
|
4656
|
+
name: "inject-context",
|
|
4657
|
+
source: "packages/bundle-default-lifecycle/src/control-plane/hooks/inject-context.ts"
|
|
4658
|
+
},
|
|
4659
|
+
{
|
|
4660
|
+
name: "task-runtime-start",
|
|
4661
|
+
source: "packages/bundle-default-lifecycle/src/control-plane/hooks/task-runtime-start.ts"
|
|
4662
|
+
}
|
|
4663
|
+
]
|
|
4664
|
+
};
|
|
2997
4665
|
var LIFECYCLE_CAPABILITIES = [
|
|
2998
4666
|
{ id: "default-lifecycle.pipeline", title: "Default lifecycle pipeline", commandId: DEFAULT_PIPELINE_CLI_ID },
|
|
2999
4667
|
{ id: "default-lifecycle.kernel-status", title: "Default kernel status", commandId: DEFAULT_KERNEL_CLI_ID },
|
|
3000
|
-
{
|
|
3001
|
-
|
|
4668
|
+
TaskVerifyCap.provide(() => async (input) => {
|
|
4669
|
+
const { taskVerify: taskVerify2 } = await Promise.resolve().then(() => (init_task_verify(), exports_task_verify));
|
|
4670
|
+
return taskVerify2(input.projectRoot, input.taskId);
|
|
4671
|
+
}, {
|
|
3002
4672
|
title: "Task verification",
|
|
3003
|
-
description: "Verify a task's changes (local checks + AI review) and report approval."
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
4673
|
+
description: "Verify a task's changes (local checks + AI review) and report approval."
|
|
4674
|
+
}),
|
|
4675
|
+
CompletionVerificationCap.provide(() => async (input) => runCompletionGate(input.projectRoot), {
|
|
4676
|
+
title: "Completion verification gate",
|
|
4677
|
+
description: "Run the full completion gate for the active task and report whether it passed."
|
|
4678
|
+
}),
|
|
4679
|
+
RunCloseoutCap.provide(() => async (input) => {
|
|
4680
|
+
const closeout = await runPipelineCloseout({ ...input, ...createEnvCloseoutRunners(process.env) });
|
|
4681
|
+
return closeout.result;
|
|
4682
|
+
}, {
|
|
4683
|
+
title: "Run closeout",
|
|
4684
|
+
description: "Run the default lifecycle closeout driver for a completed OMP run."
|
|
4685
|
+
}),
|
|
4686
|
+
LifecycleGitAgentCap.provide(() => ({
|
|
4687
|
+
shouldScopeGitCommit,
|
|
4688
|
+
gitStatus,
|
|
4689
|
+
gitChanged,
|
|
4690
|
+
gitPreflight,
|
|
4691
|
+
gitSyncBranch,
|
|
4692
|
+
gitCommit,
|
|
4693
|
+
gitSnapshot,
|
|
4694
|
+
gitOpenPr
|
|
4695
|
+
}), {
|
|
4696
|
+
title: "Lifecycle git agent commands",
|
|
4697
|
+
description: "Task-aware git helpers used by the provider-owned rig-agent command surface."
|
|
4698
|
+
}),
|
|
4699
|
+
ToolchainSourcesCap.provide(() => LIFECYCLE_TOOLCHAIN_SOURCE_CONTRIBUTION, {
|
|
4700
|
+
title: "Lifecycle toolchain sources",
|
|
4701
|
+
description: "Source paths for the lifecycle control-plane hook binaries (inject-context, task-runtime-start), compiled by the isolation runtime toolchain."
|
|
4702
|
+
})
|
|
4703
|
+
];
|
|
4704
|
+
async function runLifecycleHookRunner(hookId, role) {
|
|
4705
|
+
process.env.RIG_HOOK_ROLE = role;
|
|
4706
|
+
const { main } = await import("@rig/core/hook-runner");
|
|
4707
|
+
await main(["--plugin", DEFAULT_LIFECYCLE_PLUGIN_ID, "--hook", hookId]);
|
|
4708
|
+
}
|
|
4709
|
+
var LIFECYCLE_SEED_ENTRYPOINTS = [
|
|
4710
|
+
{
|
|
4711
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:completion-verification-entrypoint`,
|
|
4712
|
+
basename: "completion-verification",
|
|
4713
|
+
run: ({ basename }) => runLifecycleHookRunner(COMPLETION_VERIFICATION_HOOK_ID, basename ?? "completion-verification")
|
|
4714
|
+
},
|
|
4715
|
+
{
|
|
4716
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:inject-context-entrypoint`,
|
|
4717
|
+
basename: "inject-context",
|
|
4718
|
+
run: async () => {
|
|
4719
|
+
await import("@rig/bundle-default-lifecycle/control-plane/hooks/inject-context");
|
|
3008
4720
|
}
|
|
3009
4721
|
},
|
|
3010
4722
|
{
|
|
3011
|
-
id:
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
const { projectRoot } = input;
|
|
3016
|
-
return runCompletionGate(projectRoot);
|
|
4723
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:task-runtime-start-entrypoint`,
|
|
4724
|
+
basename: "task-runtime-start",
|
|
4725
|
+
run: async () => {
|
|
4726
|
+
await import("@rig/bundle-default-lifecycle/control-plane/hooks/task-runtime-start");
|
|
3017
4727
|
}
|
|
3018
4728
|
}
|
|
3019
4729
|
];
|
|
@@ -3023,10 +4733,20 @@ function createDefaultLifecyclePlugin(stages = {}) {
|
|
|
3023
4733
|
version: "0.0.0-alpha.1",
|
|
3024
4734
|
provides: [],
|
|
3025
4735
|
contributes: {
|
|
3026
|
-
stages: defaultLifecycleStages.map((stage) =>
|
|
4736
|
+
stages: defaultLifecycleStages.map((stage) => {
|
|
4737
|
+
const run = Object.prototype.hasOwnProperty.call(stages, stage.id) ? stages[stage.id] : undefined;
|
|
4738
|
+
return run ? { ...stage, run } : stage;
|
|
4739
|
+
}),
|
|
3027
4740
|
hooks: LIFECYCLE_HOOKS,
|
|
3028
4741
|
capabilities: LIFECYCLE_CAPABILITIES,
|
|
3029
|
-
cliCommands: defaultLifecycleCliCommands
|
|
4742
|
+
cliCommands: defaultLifecycleCliCommands,
|
|
4743
|
+
seedEntrypoints: LIFECYCLE_SEED_ENTRYPOINTS,
|
|
4744
|
+
config: {
|
|
4745
|
+
defaults: () => ({
|
|
4746
|
+
merge: { mode: "auto", method: "repo-default", deleteBranch: "repo-default", bypass: false },
|
|
4747
|
+
automation: { maxValidationAttempts: 30, maxPrFixIterations: 100500 }
|
|
4748
|
+
})
|
|
4749
|
+
}
|
|
3030
4750
|
}
|
|
3031
4751
|
});
|
|
3032
4752
|
}
|
|
@@ -3049,18 +4769,18 @@ function closeoutOutcome(status) {
|
|
|
3049
4769
|
}
|
|
3050
4770
|
}
|
|
3051
4771
|
async function loadRigAutomationConfig(projectRoot) {
|
|
3052
|
-
return await
|
|
4772
|
+
return await loadConfig(projectRoot);
|
|
3053
4773
|
}
|
|
3054
4774
|
async function runRigProjectValidation({ projectRoot, taskId }) {
|
|
3055
|
-
const pluginHostCtx = await
|
|
3056
|
-
return
|
|
4775
|
+
const pluginHostCtx = await buildPluginHostContext2(projectRoot);
|
|
4776
|
+
return taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined);
|
|
3057
4777
|
}
|
|
3058
4778
|
function shouldAttemptRigMerge2(config) {
|
|
3059
4779
|
const mode = config.merge?.mode;
|
|
3060
4780
|
return mode !== "off" && mode !== "pr-ready";
|
|
3061
4781
|
}
|
|
3062
4782
|
async function loadPluginStageContributions(projectRoot) {
|
|
3063
|
-
const { host } = await
|
|
4783
|
+
const { host } = await resolvePluginHost2(projectRoot);
|
|
3064
4784
|
return { executors: host.listStageExecutors(), mutations: host.listStageMutations() };
|
|
3065
4785
|
}
|
|
3066
4786
|
async function runPipelineCloseout(input) {
|
|
@@ -3082,7 +4802,7 @@ async function runPipelineCloseout(input) {
|
|
|
3082
4802
|
};
|
|
3083
4803
|
const shouldMerge = shouldAttemptRigMerge2(effectiveConfig);
|
|
3084
4804
|
const workspace = input.workspace;
|
|
3085
|
-
const artifactRoot = input.artifactRoot ??
|
|
4805
|
+
const artifactRoot = input.artifactRoot ?? resolve6(input.projectRoot, "artifacts", taskId);
|
|
3086
4806
|
const journal = async (phase, status, detail) => {
|
|
3087
4807
|
await input.journalPhase(phase, closeoutOutcome(status), detail ?? null);
|
|
3088
4808
|
};
|