@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
package/dist/src/plugin.js
CHANGED
|
@@ -14,32 +14,2289 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = import.meta.require;
|
|
17
18
|
|
|
18
|
-
// packages/bundle-default-lifecycle/src/control-plane/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
19
|
+
// packages/bundle-default-lifecycle/src/control-plane/pr-merge-gate-cap.ts
|
|
20
|
+
var exports_pr_merge_gate_cap = {};
|
|
21
|
+
__export(exports_pr_merge_gate_cap, {
|
|
22
|
+
resolvePrMergeGateService: () => resolvePrMergeGateService
|
|
23
|
+
});
|
|
24
|
+
import { PR_MERGE_GATE } from "@rig/contracts";
|
|
25
|
+
import { defineCapability } from "@rig/core/capability";
|
|
26
|
+
import { resolvePluginHost } from "@rig/core/project-plugins";
|
|
27
|
+
async function resolvePrMergeGateService(projectRoot) {
|
|
28
|
+
const { host } = await resolvePluginHost(projectRoot);
|
|
29
|
+
return PrMergeGateCap.require(host);
|
|
30
|
+
}
|
|
31
|
+
var PrMergeGateCap;
|
|
32
|
+
var init_pr_merge_gate_cap = __esm(() => {
|
|
33
|
+
PrMergeGateCap = defineCapability(PR_MERGE_GATE);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// packages/bundle-default-lifecycle/src/control-plane/pr-automation.ts
|
|
37
|
+
var exports_pr_automation = {};
|
|
38
|
+
__export(exports_pr_automation, {
|
|
39
|
+
runRepoDefaultMerge: () => runRepoDefaultMerge,
|
|
40
|
+
runPrAutomation: () => runPrAutomation,
|
|
41
|
+
resolvePrAutomationLimits: () => resolvePrAutomationLimits,
|
|
42
|
+
requestGreptileRereview: () => requestGreptileRereview,
|
|
43
|
+
pushBranchSyncedWithOrigin: () => pushBranchSyncedWithOrigin,
|
|
44
|
+
hasCommittableRunChanges: () => hasCommittableRunChanges,
|
|
45
|
+
gateNeedsGreptileRereview: () => gateNeedsGreptileRereview,
|
|
46
|
+
commitRunChanges: () => commitRunChanges,
|
|
47
|
+
collectPendingPrChecks: () => collectPendingPrChecks,
|
|
48
|
+
collectActionablePrFeedback: () => collectActionablePrFeedback,
|
|
49
|
+
closeIssueAfterMergedPr: () => closeIssueAfterMergedPr,
|
|
50
|
+
buildPrAutomationBody: () => buildPrAutomationBody,
|
|
51
|
+
UPLOADED_SNAPSHOT_PR_MARKER: () => UPLOADED_SNAPSHOT_PR_MARKER
|
|
52
|
+
});
|
|
53
|
+
import { assertSafeGitBranchName } from "@rig/core/safe-identifiers";
|
|
54
|
+
function positiveInt(value, fallback) {
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
56
|
+
}
|
|
57
|
+
function resolvePrAutomationLimits(config) {
|
|
58
|
+
return {
|
|
59
|
+
maxPrFixIterations: positiveInt(config?.automation?.maxPrFixIterations, 100500)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function buildPrAutomationBody(input) {
|
|
63
|
+
const lines = [
|
|
64
|
+
input.summary?.trim() || "Rig completed this task autonomously.",
|
|
65
|
+
"",
|
|
66
|
+
`Run: ${input.runId}`
|
|
67
|
+
];
|
|
68
|
+
if (/^\d+$/.test(input.taskId)) {
|
|
69
|
+
lines.push("", `Closes #${input.taskId}`);
|
|
70
|
+
}
|
|
71
|
+
if (input.uploadedSnapshot) {
|
|
72
|
+
lines.push("", UPLOADED_SNAPSHOT_PR_MARKER, "This PR was seeded from an uploaded local working-tree snapshot. Review local snapshot provenance before merging if repository policy requires it.");
|
|
73
|
+
}
|
|
74
|
+
return lines.join(`
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
function wildcardToRegExp(pattern) {
|
|
78
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
79
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
80
|
+
}
|
|
81
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
82
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
83
|
+
}
|
|
84
|
+
function isPendingCheck(check) {
|
|
85
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
86
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
87
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(conclusion) || ["pending", "queued", "in_progress", "waiting", "requested", "expected"].includes(state);
|
|
88
|
+
}
|
|
89
|
+
function isPassingCheck(check) {
|
|
90
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
91
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
92
|
+
return ["success", "successful", "passed", "neutral", "skipped"].includes(conclusion) || ["success", "successful", "passed", "completed"].includes(state);
|
|
93
|
+
}
|
|
94
|
+
function isFailingCheck(check) {
|
|
95
|
+
const conclusion = String(check.conclusion ?? "").toLowerCase();
|
|
96
|
+
const state = String(check.state ?? check.status ?? "").toLowerCase();
|
|
97
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "error"].includes(conclusion) || ["failure", "failed", "timed_out", "action_required", "cancelled", "error"].includes(state);
|
|
98
|
+
}
|
|
99
|
+
function collectPendingPrChecks(input) {
|
|
100
|
+
const allowedFailures = input.allowedFailures ?? [];
|
|
101
|
+
const pending = [];
|
|
102
|
+
for (const check of input.checks ?? []) {
|
|
103
|
+
const name = check.name.trim();
|
|
104
|
+
if (!name || isAllowedFailure(name, allowedFailures))
|
|
105
|
+
continue;
|
|
106
|
+
if (isPendingCheck(check) && !isPassingCheck(check))
|
|
107
|
+
pending.push(name);
|
|
108
|
+
}
|
|
109
|
+
return pending;
|
|
110
|
+
}
|
|
111
|
+
function collectActionablePrFeedback(input) {
|
|
112
|
+
const allowedFailures = input.allowedFailures ?? [];
|
|
113
|
+
const feedback = [];
|
|
114
|
+
for (const check of input.checks ?? []) {
|
|
115
|
+
const name = check.name.trim();
|
|
116
|
+
if (!name || !isFailingCheck(check) || isAllowedFailure(name, allowedFailures))
|
|
117
|
+
continue;
|
|
118
|
+
feedback.push(`Check failed: ${name}${check.detailsUrl ? ` (${check.detailsUrl})` : ""}`);
|
|
119
|
+
}
|
|
120
|
+
for (const thread of input.reviewThreads ?? []) {
|
|
121
|
+
if (thread.resolved === true)
|
|
122
|
+
continue;
|
|
123
|
+
const body = thread.body.trim();
|
|
124
|
+
if (!body)
|
|
125
|
+
continue;
|
|
126
|
+
feedback.push(`Review feedback from ${thread.author?.trim() || "reviewer"}: ${body}`);
|
|
127
|
+
}
|
|
128
|
+
return feedback;
|
|
129
|
+
}
|
|
130
|
+
function parseJsonArray(value) {
|
|
131
|
+
if (!value?.trim())
|
|
132
|
+
return [];
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(value);
|
|
135
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function parsePrChecks(value) {
|
|
141
|
+
return parseJsonArray(value).flatMap((entry) => {
|
|
142
|
+
if (!entry || typeof entry !== "object")
|
|
143
|
+
return [];
|
|
144
|
+
const record = entry;
|
|
145
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
146
|
+
if (!name.trim())
|
|
147
|
+
return [];
|
|
148
|
+
return [{
|
|
149
|
+
name,
|
|
150
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
151
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
152
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
153
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.link === "string" ? record.link : null
|
|
154
|
+
}];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function parsePrViewStatusCheckRollup(value) {
|
|
158
|
+
if (!value?.trim())
|
|
159
|
+
return [];
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(value);
|
|
162
|
+
const rollup = Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : [];
|
|
163
|
+
return rollup.flatMap((entry) => {
|
|
164
|
+
if (!entry || typeof entry !== "object")
|
|
165
|
+
return [];
|
|
166
|
+
const record = entry;
|
|
167
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
168
|
+
if (!name.trim())
|
|
169
|
+
return [];
|
|
170
|
+
return [{
|
|
171
|
+
name,
|
|
172
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
173
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
174
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
175
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.link === "string" ? record.link : null
|
|
176
|
+
}];
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function readPrChecks(input) {
|
|
183
|
+
const checks = await input.command(["pr", "checks", input.prUrl, "--json", "name,state,link"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
184
|
+
if (checks.exitCode === 0) {
|
|
185
|
+
return parsePrChecks(checks.stdout);
|
|
186
|
+
}
|
|
187
|
+
const combined = `${checks.stderr ?? ""}
|
|
188
|
+
${checks.stdout ?? ""}`;
|
|
189
|
+
if (!/unknown flag.*--json|unknown flag: --json|unknown shorthand flag/i.test(combined)) {
|
|
190
|
+
throw new Error(`gh pr checks ${input.prUrl} --json name,state,link failed (${checks.exitCode}): ${checks.stderr ?? checks.stdout ?? ""}`.trim());
|
|
191
|
+
}
|
|
192
|
+
const view = await runChecked(input.command, ["pr", "view", input.prUrl, "--json", "statusCheckRollup"], input.cwd, "gh");
|
|
193
|
+
return parsePrViewStatusCheckRollup(view.stdout);
|
|
194
|
+
}
|
|
195
|
+
function parsePrViewReviewThreads(value) {
|
|
196
|
+
if (!value?.trim())
|
|
197
|
+
return [];
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(value);
|
|
200
|
+
const feedback = [];
|
|
201
|
+
const reviewThreads = parsed.reviewThreads;
|
|
202
|
+
if (Array.isArray(reviewThreads)) {
|
|
203
|
+
feedback.push(...reviewThreads.flatMap((entry) => {
|
|
204
|
+
if (!entry || typeof entry !== "object")
|
|
205
|
+
return [];
|
|
206
|
+
const record = entry;
|
|
207
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
208
|
+
if (!body)
|
|
209
|
+
return [];
|
|
210
|
+
return [{
|
|
211
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
212
|
+
body,
|
|
213
|
+
resolved: typeof record.resolved === "boolean" ? record.resolved : false,
|
|
214
|
+
author: typeof record.author === "string" ? record.author : null
|
|
215
|
+
}];
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
const reviews = parsed.reviews;
|
|
219
|
+
if (Array.isArray(reviews)) {
|
|
220
|
+
feedback.push(...reviews.flatMap((entry) => {
|
|
221
|
+
if (!entry || typeof entry !== "object")
|
|
222
|
+
return [];
|
|
223
|
+
const record = entry;
|
|
224
|
+
const state = typeof record.state === "string" ? record.state.toUpperCase() : "";
|
|
225
|
+
if (state !== "CHANGES_REQUESTED")
|
|
226
|
+
return [];
|
|
227
|
+
const body = typeof record.body === "string" && record.body.trim() ? record.body : "Changes requested by reviewer.";
|
|
228
|
+
const author = record.author && typeof record.author === "object" ? record.author.login : null;
|
|
229
|
+
return [{
|
|
230
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
231
|
+
body,
|
|
232
|
+
resolved: false,
|
|
233
|
+
author: typeof author === "string" ? author : null
|
|
234
|
+
}];
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
const reviewDecision = typeof parsed.reviewDecision === "string" ? parsed.reviewDecision.toUpperCase() : "";
|
|
238
|
+
if (reviewDecision === "CHANGES_REQUESTED" && feedback.length === 0) {
|
|
239
|
+
feedback.push({ body: "Changes requested by reviewer.", resolved: false });
|
|
240
|
+
}
|
|
241
|
+
return feedback;
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function findPrUrl(output) {
|
|
247
|
+
return output?.split(/\s+/).map((part) => part.trim()).find((part) => /^https?:\/\/\S+\/pull\/\d+$/i.test(part)) ?? null;
|
|
248
|
+
}
|
|
249
|
+
function normalizePrUrl(stdout) {
|
|
250
|
+
const url = findPrUrl(stdout);
|
|
251
|
+
if (!url)
|
|
252
|
+
throw new Error("gh pr create did not return a PR URL");
|
|
253
|
+
return url;
|
|
254
|
+
}
|
|
255
|
+
function parseGitHubPullRequestUrl(prUrl) {
|
|
256
|
+
try {
|
|
257
|
+
const parsed = new URL(prUrl);
|
|
258
|
+
const [owner, repo, kind, number] = parsed.pathname.split("/").filter(Boolean);
|
|
259
|
+
if (!owner || !repo || kind !== "pull" || !number || !/^\d+$/.test(number))
|
|
260
|
+
return null;
|
|
261
|
+
return { owner, repo, number };
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function ensureExistingPrBodyHasRigMarkers(input) {
|
|
267
|
+
const view = await input.command(["pr", "view", input.prUrl, "--json", "body"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
268
|
+
if (view.exitCode !== 0) {
|
|
269
|
+
throw new Error(`gh pr view ${input.prUrl} --json body failed (${view.exitCode}): ${view.stderr ?? view.stdout ?? ""}`.trim());
|
|
270
|
+
}
|
|
271
|
+
let currentBody = "";
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(view.stdout ?? "{}");
|
|
274
|
+
currentBody = typeof parsed.body === "string" ? parsed.body : "";
|
|
275
|
+
} catch {
|
|
276
|
+
currentBody = "";
|
|
277
|
+
}
|
|
278
|
+
const requiredBlocks = input.body.split(/\n{2,}/).map((block) => block.trim()).filter((block) => /^Run: /i.test(block) || /^Closes #\d+/i.test(block));
|
|
279
|
+
const missing = requiredBlocks.filter((block) => {
|
|
280
|
+
if (/^Run: /i.test(block))
|
|
281
|
+
return !/^Run: \S+/im.test(currentBody);
|
|
282
|
+
return !currentBody.includes(block);
|
|
283
|
+
});
|
|
284
|
+
if (missing.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
const nextBody = [currentBody.trim(), ...missing].filter(Boolean).join(`
|
|
287
|
+
|
|
288
|
+
`);
|
|
289
|
+
const pr = parseGitHubPullRequestUrl(input.prUrl);
|
|
290
|
+
if (!pr)
|
|
291
|
+
throw new Error(`Cannot update existing PR body for unrecognized PR URL: ${input.prUrl}`);
|
|
292
|
+
const edit = await input.command(["api", `repos/${pr.owner}/${pr.repo}/issues/${pr.number}`, "-X", "PATCH", "-f", `body=${nextBody}`], input.cwd ? { cwd: input.cwd } : undefined);
|
|
293
|
+
if (edit.exitCode !== 0) {
|
|
294
|
+
throw new Error(`gh api repos/${pr.owner}/${pr.repo}/issues/${pr.number} -X PATCH -f body=<redacted> failed (${edit.exitCode}): ${edit.stderr ?? edit.stdout ?? ""}`.trim());
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function runChecked(command, args, cwd, label = "gh") {
|
|
298
|
+
const result = await command(args, cwd ? { cwd } : undefined);
|
|
299
|
+
if (result.exitCode !== 0) {
|
|
300
|
+
throw new Error(`${label} ${args.join(" ")} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim());
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
function statusPathFromShortLine(line) {
|
|
305
|
+
const rawPath = line.length > 3 ? line.slice(3).trim() : "";
|
|
306
|
+
const renamedPath = rawPath.includes(" -> ") ? rawPath.split(" -> ").at(-1).trim() : rawPath;
|
|
307
|
+
if (renamedPath.startsWith('"') && renamedPath.endsWith('"')) {
|
|
308
|
+
try {
|
|
309
|
+
return JSON.parse(renamedPath);
|
|
310
|
+
} catch {
|
|
311
|
+
return renamedPath.slice(1, -1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return renamedPath;
|
|
315
|
+
}
|
|
316
|
+
function isRuntimeCommitExcludedPath(path) {
|
|
317
|
+
const normalized = path.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
318
|
+
return RIG_RUNTIME_COMMIT_EXCLUDES.some((excluded) => normalized === excluded || normalized.startsWith(`${excluded}/`));
|
|
319
|
+
}
|
|
320
|
+
function committableRunChangePaths(statusText) {
|
|
321
|
+
const seen = new Set;
|
|
322
|
+
const paths = [];
|
|
323
|
+
for (const line of statusText.split(/\r?\n/)) {
|
|
324
|
+
const path = statusPathFromShortLine(line);
|
|
325
|
+
if (!path || isRuntimeCommitExcludedPath(path) || seen.has(path))
|
|
326
|
+
continue;
|
|
327
|
+
seen.add(path);
|
|
328
|
+
paths.push(path);
|
|
329
|
+
}
|
|
330
|
+
return paths;
|
|
331
|
+
}
|
|
332
|
+
function hasCommittableRunChanges(statusText) {
|
|
333
|
+
return committableRunChangePaths(statusText).length > 0;
|
|
334
|
+
}
|
|
335
|
+
async function commitRunChanges(input) {
|
|
336
|
+
const status = await runChecked(input.command, ["status", "--short", "--untracked-files=all"], input.cwd, "git");
|
|
337
|
+
const statusText = status.stdout ?? "";
|
|
338
|
+
const committablePaths = committableRunChangePaths(statusText);
|
|
339
|
+
if (!statusText.trim() || committablePaths.length === 0) {
|
|
340
|
+
return { committed: false, status: statusText };
|
|
341
|
+
}
|
|
342
|
+
await runChecked(input.command, ["add", "-A", "--", ...committablePaths], input.cwd, "git");
|
|
343
|
+
const staged = await input.command(["diff", "--cached", "--quiet"], { cwd: input.cwd });
|
|
344
|
+
if (staged.exitCode === 0) {
|
|
345
|
+
return { committed: false, status: statusText };
|
|
346
|
+
}
|
|
347
|
+
if (staged.exitCode !== 1) {
|
|
348
|
+
throw new Error(`git diff --cached --quiet failed (${staged.exitCode}): ${staged.stderr ?? staged.stdout ?? ""}`.trim());
|
|
349
|
+
}
|
|
350
|
+
await runChecked(input.command, ["commit", "-m", input.message], input.cwd, "git");
|
|
351
|
+
return { committed: true, status: statusText };
|
|
352
|
+
}
|
|
353
|
+
async function closeIssueAfterMergedPr(input) {
|
|
354
|
+
await input.updateTaskSource(input.projectRoot, {
|
|
355
|
+
taskId: input.taskId,
|
|
356
|
+
...input.sourceTask !== undefined ? { sourceTask: input.sourceTask } : {},
|
|
357
|
+
update: {
|
|
358
|
+
status: "closed",
|
|
359
|
+
comment: [
|
|
360
|
+
"<!-- rig:status-comment -->",
|
|
361
|
+
"### Rig status: closed",
|
|
362
|
+
"",
|
|
363
|
+
"Rig PR merged and closed this task.",
|
|
364
|
+
"",
|
|
365
|
+
`- Run: ${input.runId}`,
|
|
366
|
+
`- PR merged: ${input.prUrl}`
|
|
367
|
+
].join(`
|
|
368
|
+
`)
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function parseGitHubRepoFromPrUrl(prUrl) {
|
|
373
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/\d+\/?$/i.exec(prUrl.trim());
|
|
374
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
375
|
+
}
|
|
376
|
+
async function resolveRepoDefaultMergeFlag(input) {
|
|
377
|
+
const repo = parseGitHubRepoFromPrUrl(input.prUrl);
|
|
378
|
+
if (!repo)
|
|
379
|
+
throw new Error(`Cannot resolve GitHub repository from PR URL: ${input.prUrl}`);
|
|
380
|
+
const result = await input.command(["api", `repos/${repo.owner}/${repo.repo}`], input.cwd ? { cwd: input.cwd } : undefined);
|
|
381
|
+
if (result.exitCode !== 0) {
|
|
382
|
+
throw new Error(`Could not read repository merge policy for ${repo.owner}/${repo.repo}: ${result.stderr ?? result.stdout ?? "gh api failed"}`.trim());
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(result.stdout ?? "{}");
|
|
386
|
+
if (parsed.allow_merge_commit !== false)
|
|
387
|
+
return "--merge";
|
|
388
|
+
if (parsed.allow_squash_merge !== false)
|
|
389
|
+
return "--squash";
|
|
390
|
+
if (parsed.allow_rebase_merge !== false)
|
|
391
|
+
return "--rebase";
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw new Error(`Could not parse repository merge policy for ${repo.owner}/${repo.repo}: ${error instanceof Error ? error.message : String(error)}`);
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`Repository ${repo.owner}/${repo.repo} has no enabled merge method for repo-default merge.`);
|
|
396
|
+
}
|
|
397
|
+
async function runRepoDefaultMerge(input) {
|
|
398
|
+
const merge = input.config?.merge ?? {};
|
|
399
|
+
if (merge.mode === "off")
|
|
400
|
+
return;
|
|
401
|
+
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
402
|
+
const mergeGate = await resolvePrMergeGateService(input.projectRoot ?? input.cwd ?? process.cwd());
|
|
403
|
+
const matchHeadSha = mergeGate.resolveHeadSha({ result: input.strictGate, prUrl: input.prUrl, requireGreptile });
|
|
404
|
+
const method = merge.method ?? "repo-default";
|
|
405
|
+
const args = ["pr", "merge", input.prUrl];
|
|
406
|
+
if (method === "repo-default") {
|
|
407
|
+
args.push(await resolveRepoDefaultMergeFlag({ prUrl: input.prUrl, command: input.command, ...input.cwd !== undefined ? { cwd: input.cwd } : {} }));
|
|
408
|
+
} else {
|
|
409
|
+
args.push(`--${method}`);
|
|
410
|
+
}
|
|
411
|
+
args.push("--match-head-commit", matchHeadSha);
|
|
412
|
+
if (merge.deleteBranch === true) {
|
|
413
|
+
args.push("--delete-branch");
|
|
414
|
+
}
|
|
415
|
+
if (merge.bypass === true) {
|
|
416
|
+
args.push("--admin");
|
|
417
|
+
}
|
|
418
|
+
await runChecked(input.command, args, input.cwd);
|
|
419
|
+
}
|
|
420
|
+
function shouldAttemptRigMerge(config) {
|
|
421
|
+
const mode = config?.merge?.mode;
|
|
422
|
+
return mode !== "off" && mode !== "pr-ready";
|
|
423
|
+
}
|
|
424
|
+
function isPendingOnlyGate(result) {
|
|
425
|
+
return result.pending && result.reasonDetails.length > 0 && result.reasonDetails.every((reason) => reason.reasonClass === "pending" && reason.suggestedAction === "wait");
|
|
426
|
+
}
|
|
427
|
+
function gateNeedsGreptileRereview(result) {
|
|
428
|
+
if (result.approved)
|
|
429
|
+
return false;
|
|
430
|
+
const staleShaEvidence = result.reasonDetails.some((reason) => reason.surface === "greptile" && (reason.code === "greptile_stale" || reason.code === "greptile_not_current_head" && !!reason.reviewedSha && reason.reviewedSha !== reason.headSha));
|
|
431
|
+
const hasParseableGreptileScore = !!result.evidence.greptile.score;
|
|
432
|
+
const greptileNeedsPrompt = !hasParseableGreptileScore && result.reasonDetails.some((reason) => reason.surface === "greptile" && reason.suggestedAction === "ask_greptile" && (reason.code === "greptile_not_current_head" || reason.code === "greptile_score_missing" || reason.code === "greptile_mapping_unproven"));
|
|
433
|
+
const greptileStillRunning = result.reasonDetails.some((reason) => reason.code === "greptile_pending" || reason.code === "greptile_missing");
|
|
434
|
+
return (staleShaEvidence || greptileNeedsPrompt) && !greptileStillRunning;
|
|
435
|
+
}
|
|
436
|
+
async function requestGreptileRereview(input) {
|
|
437
|
+
const sha = input.headSha?.trim() || "unknown";
|
|
438
|
+
const marker = `<!-- ${GREPTILE_REREVIEW_MARKER_PREFIX}:${sha} -->`;
|
|
439
|
+
const existing = await input.command(["pr", "view", input.prUrl, "--json", "comments"], input.cwd ? { cwd: input.cwd } : undefined);
|
|
440
|
+
if (existing.exitCode === 0 && (existing.stdout ?? "").includes(marker)) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
await runChecked(input.command, [
|
|
444
|
+
"pr",
|
|
445
|
+
"comment",
|
|
446
|
+
input.prUrl,
|
|
447
|
+
"--body",
|
|
448
|
+
`${marker}
|
|
449
|
+
@greptileai review
|
|
450
|
+
|
|
451
|
+
Rig strict merge gate: Greptile evidence is bound to an older commit. Requesting a fresh review of the current head (${sha}). Merge waits until Greptile re-reviews this exact commit.`
|
|
452
|
+
], input.cwd);
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
async function pushBranchSyncedWithOrigin(input) {
|
|
456
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
457
|
+
const fetched = await input.gitCommand(["fetch", "origin", branch], { cwd: input.projectRoot });
|
|
458
|
+
if (fetched.exitCode === 0) {
|
|
459
|
+
const originRef = `origin/${branch}`;
|
|
460
|
+
const behind = await input.gitCommand(["rev-list", "--count", `HEAD..${originRef}`], { cwd: input.projectRoot });
|
|
461
|
+
const behindCount = Number.parseInt((behind.stdout ?? "").trim(), 10);
|
|
462
|
+
if (behind.exitCode === 0 && Number.isFinite(behindCount) && behindCount > 0) {
|
|
463
|
+
await runChecked(input.gitCommand, ["rebase", "--autostash", originRef], input.projectRoot, "git");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const pushed = await input.gitCommand(["push", "--set-upstream", "origin", branch], { cwd: input.projectRoot });
|
|
467
|
+
if (pushed.exitCode !== 0) {
|
|
468
|
+
await runChecked(input.gitCommand, ["push", "--set-upstream", "--force-with-lease", "origin", branch], input.projectRoot, "git");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function syncBranchAfterPrFeedback(input) {
|
|
472
|
+
if (!input.gitCommand)
|
|
473
|
+
return;
|
|
474
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
475
|
+
await commitRunChanges({
|
|
476
|
+
cwd: input.projectRoot,
|
|
477
|
+
message: `rig: address PR feedback for task ${input.taskId}`,
|
|
478
|
+
command: input.gitCommand
|
|
479
|
+
});
|
|
480
|
+
await pushBranchSyncedWithOrigin({ projectRoot: input.projectRoot, branch, gitCommand: input.gitCommand });
|
|
481
|
+
}
|
|
482
|
+
async function runPrAutomation(input) {
|
|
483
|
+
const branch = assertSafeGitBranchName(input.branch, "PR branch");
|
|
484
|
+
const mergeGate = await resolvePrMergeGateService(input.projectRoot);
|
|
485
|
+
const prConfig = input.config?.pr ?? {};
|
|
486
|
+
const requireGreptile = (input.config?.review?.provider ?? "greptile") === "greptile";
|
|
487
|
+
if (prConfig.mode === "off" || prConfig.mode === "ask") {
|
|
488
|
+
return { status: "skipped", iterations: 0, actionableFeedback: [] };
|
|
489
|
+
}
|
|
490
|
+
const body = buildPrAutomationBody({
|
|
491
|
+
taskId: input.taskId,
|
|
492
|
+
runId: input.runId,
|
|
493
|
+
summary: input.sourceTask?.title ? `Rig completed: ${input.sourceTask.title}` : null,
|
|
494
|
+
...input.uploadedSnapshot !== undefined ? { uploadedSnapshot: input.uploadedSnapshot } : {}
|
|
495
|
+
});
|
|
496
|
+
if (input.gitCommand) {
|
|
497
|
+
await pushBranchSyncedWithOrigin({ projectRoot: input.projectRoot, branch, gitCommand: input.gitCommand });
|
|
498
|
+
}
|
|
499
|
+
const createArgs = [
|
|
500
|
+
"pr",
|
|
501
|
+
"create",
|
|
502
|
+
"--head",
|
|
503
|
+
branch,
|
|
504
|
+
"--title",
|
|
505
|
+
input.sourceTask?.title?.trim() || `Rig task ${input.taskId}`,
|
|
506
|
+
"--body",
|
|
507
|
+
body
|
|
508
|
+
];
|
|
509
|
+
const createResult = await input.command(createArgs, { cwd: input.projectRoot });
|
|
510
|
+
const existingPrUrl = createResult.exitCode === 0 ? null : /pull request .*already exists/i.test(`${createResult.stderr ?? ""}
|
|
511
|
+
${createResult.stdout ?? ""}`) ? findPrUrl(`${createResult.stderr ?? ""}
|
|
512
|
+
${createResult.stdout ?? ""}`) : null;
|
|
513
|
+
if (createResult.exitCode !== 0 && !existingPrUrl) {
|
|
514
|
+
throw new Error(`gh ${createArgs.join(" ")} failed (${createResult.exitCode}): ${createResult.stderr ?? createResult.stdout ?? ""}`.trim());
|
|
515
|
+
}
|
|
516
|
+
const prUrl = existingPrUrl ?? normalizePrUrl(createResult.stdout);
|
|
517
|
+
if (existingPrUrl) {
|
|
518
|
+
await ensureExistingPrBodyHasRigMarkers({ prUrl, body, command: input.command, cwd: input.projectRoot });
|
|
519
|
+
}
|
|
520
|
+
await input.lifecycle?.onPrOpened?.({ prUrl });
|
|
521
|
+
const { maxPrFixIterations } = resolvePrAutomationLimits(input.config);
|
|
522
|
+
let latestFeedback = [];
|
|
523
|
+
let pendingElapsedMs = 0;
|
|
524
|
+
const shouldMerge = shouldAttemptRigMerge(input.config);
|
|
525
|
+
for (let iteration = 1;iteration <= maxPrFixIterations; iteration += 1) {
|
|
526
|
+
await input.lifecycle?.onReviewCiStarted?.({ prUrl, iteration });
|
|
527
|
+
if (!shouldMerge) {
|
|
528
|
+
const checks = prConfig.watchChecks === false ? [] : await readPrChecks({ prUrl, command: input.command, cwd: input.projectRoot });
|
|
529
|
+
const reviewThreads = prConfig.autoFixReview === false ? [] : parsePrViewReviewThreads((await runChecked(input.command, ["pr", "view", prUrl, "--json", "reviewDecision,reviews"], input.projectRoot)).stdout);
|
|
530
|
+
latestFeedback = collectActionablePrFeedback({
|
|
531
|
+
checks,
|
|
532
|
+
reviewThreads,
|
|
533
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? []
|
|
534
|
+
});
|
|
535
|
+
const pendingChecks = collectPendingPrChecks({ checks, allowedFailures: input.config?.merge?.allowedFailures ?? [] });
|
|
536
|
+
if (latestFeedback.length === 0 && pendingChecks.length > 0) {
|
|
537
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
538
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
539
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
540
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: pendingChecks.map((name) => `Check still pending: ${name}`), merged: false };
|
|
541
|
+
}
|
|
542
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
543
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
544
|
+
pendingElapsedMs += sleepMs;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (latestFeedback.length === 0) {
|
|
548
|
+
pendingElapsedMs = 0;
|
|
549
|
+
return { status: "opened", prUrl, iterations: iteration, actionableFeedback: [], merged: false };
|
|
550
|
+
}
|
|
551
|
+
pendingElapsedMs = 0;
|
|
552
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
553
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
554
|
+
}
|
|
555
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
556
|
+
await input.steerPi([
|
|
557
|
+
`PR automation found actionable feedback on ${prUrl}.`,
|
|
558
|
+
`Fix iteration ${iteration + 1}/${maxPrFixIterations}.`,
|
|
559
|
+
"",
|
|
560
|
+
...latestFeedback.map((entry) => `- ${entry}`)
|
|
561
|
+
].join(`
|
|
562
|
+
`));
|
|
563
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const gate = await mergeGate.runGate({
|
|
567
|
+
projectRoot: input.projectRoot,
|
|
568
|
+
prUrl,
|
|
569
|
+
taskId: input.taskId,
|
|
570
|
+
runId: input.runId,
|
|
571
|
+
cycle: iteration,
|
|
572
|
+
command: input.command,
|
|
573
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
574
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
575
|
+
...requireGreptile && input.greptileApi ? { greptileApi: input.greptileApi } : {},
|
|
576
|
+
requireGreptile
|
|
577
|
+
});
|
|
578
|
+
latestFeedback = [...gate.actionableFeedback];
|
|
579
|
+
if (requireGreptile && gateNeedsGreptileRereview(gate)) {
|
|
580
|
+
const requested = await requestGreptileRereview({
|
|
581
|
+
prUrl,
|
|
582
|
+
headSha: gate.evidence.headSha ?? null,
|
|
583
|
+
command: input.command,
|
|
584
|
+
cwd: input.projectRoot
|
|
585
|
+
});
|
|
586
|
+
if (requested) {
|
|
587
|
+
await input.lifecycle?.onFeedback?.({
|
|
588
|
+
prUrl,
|
|
589
|
+
iteration,
|
|
590
|
+
feedback: [`Requested a fresh Greptile review for current head ${gate.evidence.headSha ?? "unknown"}; merge stays blocked until Greptile re-reviews it.`]
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
594
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
595
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
596
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
597
|
+
}
|
|
598
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
599
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
600
|
+
pendingElapsedMs += sleepMs;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (gate.approved) {
|
|
604
|
+
pendingElapsedMs = 0;
|
|
605
|
+
const finalGate = await mergeGate.runGate({
|
|
606
|
+
projectRoot: input.projectRoot,
|
|
607
|
+
prUrl,
|
|
608
|
+
taskId: input.taskId,
|
|
609
|
+
runId: input.runId,
|
|
610
|
+
cycle: iteration,
|
|
611
|
+
command: input.command,
|
|
612
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
613
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
614
|
+
...requireGreptile && input.greptileApi ? { greptileApi: input.greptileApi } : {},
|
|
615
|
+
requireGreptile,
|
|
616
|
+
final: true
|
|
617
|
+
});
|
|
618
|
+
if (finalGate.approved) {
|
|
619
|
+
await input.lifecycle?.onMergeStarted?.({ prUrl });
|
|
620
|
+
await runRepoDefaultMerge({ prUrl, ...input.config !== undefined ? { config: input.config } : {}, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
|
|
621
|
+
await input.lifecycle?.onMerged?.({ prUrl });
|
|
622
|
+
return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
|
|
623
|
+
}
|
|
624
|
+
latestFeedback = [...finalGate.actionableFeedback];
|
|
625
|
+
if (isPendingOnlyGate(finalGate)) {
|
|
626
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
627
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
628
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
629
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
630
|
+
}
|
|
631
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
632
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
633
|
+
pendingElapsedMs += sleepMs;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
637
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
638
|
+
}
|
|
639
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
640
|
+
await input.steerPi(finalGate.steeringPrompt);
|
|
641
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (isPendingOnlyGate(gate)) {
|
|
645
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
646
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
647
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
648
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
649
|
+
}
|
|
650
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
651
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
652
|
+
pendingElapsedMs += sleepMs;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
pendingElapsedMs = 0;
|
|
656
|
+
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
657
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
658
|
+
}
|
|
659
|
+
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
660
|
+
await input.steerPi(gate.steeringPrompt);
|
|
661
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch, gitCommand: input.gitCommand });
|
|
662
|
+
}
|
|
663
|
+
return { status: "needs_attention", prUrl, iterations: maxPrFixIterations, actionableFeedback: latestFeedback, merged: false };
|
|
664
|
+
}
|
|
665
|
+
var UPLOADED_SNAPSHOT_PR_MARKER = "<!-- rig:uploaded-snapshot -->", RIG_RUNTIME_COMMIT_EXCLUDES, GREPTILE_REREVIEW_MARKER_PREFIX = "rig:greptile-rereview";
|
|
666
|
+
var init_pr_automation = __esm(() => {
|
|
667
|
+
init_pr_merge_gate_cap();
|
|
668
|
+
RIG_RUNTIME_COMMIT_EXCLUDES = [
|
|
669
|
+
".rig",
|
|
670
|
+
"artifacts",
|
|
671
|
+
"node_modules"
|
|
672
|
+
];
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// packages/bundle-default-lifecycle/src/control-plane/task-data.ts
|
|
676
|
+
import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
|
|
677
|
+
import { defineCapability as defineCapability3 } from "@rig/core/capability";
|
|
678
|
+
import { requireInstalledCapability } from "@rig/core/capability-loaders";
|
|
679
|
+
function taskData() {
|
|
680
|
+
return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running the lifecycle.");
|
|
681
|
+
}
|
|
682
|
+
var TaskDataCap;
|
|
683
|
+
var init_task_data = __esm(() => {
|
|
684
|
+
TaskDataCap = defineCapability3(TASK_DATA_SERVICE_CAPABILITY);
|
|
685
|
+
});
|
|
686
|
+
|
|
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";
|
|
711
|
+
import { resolve as resolve2 } from "path";
|
|
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(resolve2(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 resolve3 } 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(resolve3(runtimeWorkspace, ".git"))) {
|
|
780
|
+
return resolve3(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(resolve3(projectRoot, ".git"))) {
|
|
843
|
+
console.log(`ERROR: project root is not a git repo (${projectRoot})`);
|
|
844
|
+
issues += 1;
|
|
845
|
+
}
|
|
846
|
+
if (monorepoRoot && existsSync3(resolve3(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(resolve3(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) : resolve3(resolve3(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(resolve3(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(resolve3(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 = resolve3(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 = resolve3(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 resolve3(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 = resolve3(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(resolve3(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 = resolve3(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(resolve3(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(resolve3(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(resolve3(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 = resolve3(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 = resolve3(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 = resolve3(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(resolve3(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 = resolve3(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(resolve3(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 ? resolve3(runtimeRoot, "home") : "";
|
|
1860
|
+
const runtimeTmp = runtimeRoot ? resolve3(runtimeRoot, "tmp") : "";
|
|
1861
|
+
const runtimeCache = runtimeRoot ? resolve3(runtimeRoot, "cache") : "";
|
|
1862
|
+
const runtimeKnownHosts = runtimeHome ? resolve3(runtimeHome, ".ssh", "known_hosts") : "";
|
|
1863
|
+
const runtimeKey = runtimeHome ? resolve3(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 = resolve3(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 = resolve3(runtimeHome, ".ssl");
|
|
1970
|
+
const sslConfig = resolve3(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(resolve3(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 = resolve3(startPath);
|
|
2010
|
+
while (true) {
|
|
2011
|
+
const candidate = resolve3(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 resolve4 } from "path";
|
|
27
2053
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 = resolve4(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 resolve5 } 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
|
+
}
|
|
33
2289
|
async function verifyTask(options) {
|
|
2290
|
+
await ensureMergeGate(options.projectRoot);
|
|
34
2291
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
35
2292
|
const taskId = options.taskId;
|
|
36
|
-
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
37
|
-
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
38
|
-
|
|
39
|
-
const validationSummaryPath =
|
|
40
|
-
const reviewFeedbackPath =
|
|
41
|
-
const reviewStatePath =
|
|
42
|
-
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 = resolve5(artifactDir, "validation-summary.json");
|
|
2297
|
+
const reviewFeedbackPath = resolve5(artifactDir, "review-feedback.md");
|
|
2298
|
+
const reviewStatePath = resolve5(artifactDir, "review-state.json");
|
|
2299
|
+
const greptileRawPath = resolve5(artifactDir, "review-greptile-raw.json");
|
|
43
2300
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
44
2301
|
const prState = prStates[0] || null;
|
|
45
2302
|
const localReasons = [];
|
|
@@ -51,7 +2308,7 @@ async function verifyTask(options) {
|
|
|
51
2308
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
52
2309
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
53
2310
|
}
|
|
54
|
-
if (!
|
|
2311
|
+
if (!existsSync5(validationSummaryPath)) {
|
|
55
2312
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
56
2313
|
} else {
|
|
57
2314
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -60,13 +2317,13 @@ async function verifyTask(options) {
|
|
|
60
2317
|
}
|
|
61
2318
|
}
|
|
62
2319
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
63
|
-
const requiredPath =
|
|
64
|
-
if (!
|
|
2320
|
+
const requiredPath = resolve5(artifactDir, file);
|
|
2321
|
+
if (!existsSync5(requiredPath)) {
|
|
65
2322
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
66
2323
|
}
|
|
67
2324
|
}
|
|
68
|
-
const taskResultPath =
|
|
69
|
-
if (
|
|
2325
|
+
const taskResultPath = resolve5(artifactDir, "task-result.json");
|
|
2326
|
+
if (existsSync5(taskResultPath)) {
|
|
70
2327
|
const taskResult = await readJsonFile(taskResultPath);
|
|
71
2328
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
72
2329
|
if (artifactStatus === "partial") {
|
|
@@ -79,8 +2336,8 @@ async function verifyTask(options) {
|
|
|
79
2336
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
80
2337
|
}
|
|
81
2338
|
}
|
|
82
|
-
const nextActionsPath =
|
|
83
|
-
if (
|
|
2339
|
+
const nextActionsPath = resolve5(artifactDir, "next-actions.md");
|
|
2340
|
+
if (existsSync5(nextActionsPath)) {
|
|
84
2341
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
85
2342
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
86
2343
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -111,7 +2368,7 @@ async function verifyTask(options) {
|
|
|
111
2368
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
112
2369
|
}
|
|
113
2370
|
if (persistArtifacts && ai.rawResponse) {
|
|
114
|
-
|
|
2371
|
+
writeFileSync2(greptileRawPath, `${ai.rawResponse}
|
|
115
2372
|
`, "utf-8");
|
|
116
2373
|
}
|
|
117
2374
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -233,15 +2490,15 @@ function nextActionsIndicateRemainingScope(content) {
|
|
|
233
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);
|
|
234
2491
|
}
|
|
235
2492
|
async function hasConfiguredSourceTask(projectRoot, taskId) {
|
|
236
|
-
return readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
2493
|
+
return taskData().readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
237
2494
|
}
|
|
238
2495
|
function resolveGithubSourceIssueId(projectRoot, taskId) {
|
|
239
|
-
const fromRuntime =
|
|
2496
|
+
const fromRuntime = loadRuntimeContextFromEnv2()?.sourceTask?.sourceIssueId;
|
|
240
2497
|
if (typeof fromRuntime === "string" && isGithubSourceIssueId(fromRuntime)) {
|
|
241
2498
|
return fromRuntime;
|
|
242
2499
|
}
|
|
243
2500
|
try {
|
|
244
|
-
const taskConfig = readTaskConfig(projectRoot);
|
|
2501
|
+
const taskConfig = taskData().readTaskConfig(projectRoot);
|
|
245
2502
|
const entry = taskConfig[taskId];
|
|
246
2503
|
const sourceIssueId = typeof entry?.sourceIssueId === "string" ? entry.sourceIssueId : typeof entry?.source_issue_id === "string" ? entry.source_issue_id : null;
|
|
247
2504
|
if (sourceIssueId && isGithubSourceIssueId(sourceIssueId)) {
|
|
@@ -312,14 +2569,15 @@ function loadGithubPullRequestCloseoutSnapshot(projectRoot, prState) {
|
|
|
312
2569
|
"--json",
|
|
313
2570
|
"state,isDraft,mergeable,mergeStateStatus,reviewDecision,title,body,statusCheckRollup"
|
|
314
2571
|
]);
|
|
2572
|
+
const isDraft = booleanField(view, "isDraft");
|
|
315
2573
|
return {
|
|
316
|
-
state
|
|
317
|
-
isDraft
|
|
318
|
-
mergeable
|
|
319
|
-
mergeStateStatus
|
|
320
|
-
reviewDecision
|
|
321
|
-
title
|
|
322
|
-
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")),
|
|
323
2581
|
statusCheckRollup: statusCheckRollupField(view, "statusCheckRollup"),
|
|
324
2582
|
reviewThreads: loadGithubReviewThreads(projectRoot, repoName, prNumber)
|
|
325
2583
|
};
|
|
@@ -394,6 +2652,9 @@ function stringField(record, key) {
|
|
|
394
2652
|
const value = record[key];
|
|
395
2653
|
return typeof value === "string" ? value : undefined;
|
|
396
2654
|
}
|
|
2655
|
+
function objectField(key, value) {
|
|
2656
|
+
return value === undefined ? {} : { [key]: value };
|
|
2657
|
+
}
|
|
397
2658
|
function booleanField(record, key) {
|
|
398
2659
|
const value = record[key];
|
|
399
2660
|
return typeof value === "boolean" ? value : undefined;
|
|
@@ -450,7 +2711,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
450
2711
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
451
2712
|
}
|
|
452
2713
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
453
|
-
const parsed =
|
|
2714
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
454
2715
|
const mode = parsed?.mode;
|
|
455
2716
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
456
2717
|
return mode;
|
|
@@ -461,7 +2722,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
461
2722
|
return "advisory";
|
|
462
2723
|
}
|
|
463
2724
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
464
|
-
const parsed =
|
|
2725
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
465
2726
|
const provider = parsed?.provider;
|
|
466
2727
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
467
2728
|
return provider;
|
|
@@ -470,7 +2731,7 @@ async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
|
470
2731
|
}
|
|
471
2732
|
function resolveRepoSlug(projectRoot) {
|
|
472
2733
|
const paths = resolveHarnessPaths(projectRoot);
|
|
473
|
-
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();
|
|
474
2735
|
if (!remote) {
|
|
475
2736
|
return "";
|
|
476
2737
|
}
|
|
@@ -484,7 +2745,7 @@ function resolveRepoSlug(projectRoot) {
|
|
|
484
2745
|
async function runGreptileReview(options) {
|
|
485
2746
|
const reasons = [];
|
|
486
2747
|
const warnings = [];
|
|
487
|
-
const secrets =
|
|
2748
|
+
const secrets = resolveRuntimeSecrets2(process.env);
|
|
488
2749
|
const apiKey = secrets.GREPTILE_API_KEY || "";
|
|
489
2750
|
const apiBase = secrets.GREPTILE_API_BASE || "https://api.greptile.com/mcp";
|
|
490
2751
|
const remote = secrets.GREPTILE_REMOTE || "github";
|
|
@@ -620,7 +2881,7 @@ function writeFeedbackFile(options) {
|
|
|
620
2881
|
if (options.aiRawFeedback) {
|
|
621
2882
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
622
2883
|
}
|
|
623
|
-
|
|
2884
|
+
writeFileSync2(options.output, `${lines.join(`
|
|
624
2885
|
`)}
|
|
625
2886
|
`, "utf-8");
|
|
626
2887
|
}
|
|
@@ -635,9 +2896,9 @@ function writeReviewStateFile(options) {
|
|
|
635
2896
|
local_reasons: options.localReasons,
|
|
636
2897
|
ai_reasons: options.aiReasons,
|
|
637
2898
|
ai_warnings: options.aiWarnings,
|
|
638
|
-
updated_at:
|
|
2899
|
+
updated_at: nowIso2()
|
|
639
2900
|
};
|
|
640
|
-
|
|
2901
|
+
writeFileSync2(options.output, `${JSON.stringify(payload, null, 2)}
|
|
641
2902
|
`, "utf-8");
|
|
642
2903
|
}
|
|
643
2904
|
async function runGreptileReviewForPr(options) {
|
|
@@ -662,7 +2923,6 @@ async function runGreptileReviewForPr(options) {
|
|
|
662
2923
|
taskId: options.taskId,
|
|
663
2924
|
prState: options.prState,
|
|
664
2925
|
reviewMode: options.reviewMode,
|
|
665
|
-
infrastructureError: undefined,
|
|
666
2926
|
pollAttempts: options.pollAttempts,
|
|
667
2927
|
pollIntervalMs: options.pollIntervalMs
|
|
668
2928
|
});
|
|
@@ -790,7 +3050,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
790
3050
|
});
|
|
791
3051
|
const actionableComments = filterActionableGreptileComments(commentsPayload.comments || []);
|
|
792
3052
|
const reviewBody = reviewDetails.codeReview?.body || "";
|
|
793
|
-
const score = parseGreptileScore(reviewBody);
|
|
3053
|
+
const score = mg().parseGreptileScore(reviewBody);
|
|
794
3054
|
const feedback = [
|
|
795
3055
|
`## ${options.prState.repoLabel || repoName} PR Review`,
|
|
796
3056
|
"",
|
|
@@ -798,7 +3058,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
798
3058
|
`- Review ID: ${selectedReview.id}`,
|
|
799
3059
|
`- Status: ${selectedReview.status}`,
|
|
800
3060
|
"",
|
|
801
|
-
reviewBody ? stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
3061
|
+
reviewBody ? mg().stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
802
3062
|
].filter(Boolean).join(`
|
|
803
3063
|
`);
|
|
804
3064
|
if (actionableComments.length > 0) {
|
|
@@ -819,7 +3079,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
819
3079
|
}
|
|
820
3080
|
};
|
|
821
3081
|
}
|
|
822
|
-
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, " ");
|
|
823
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)) {
|
|
824
3084
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
825
3085
|
return {
|
|
@@ -867,7 +3127,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
867
3127
|
status: selectedReview.status
|
|
868
3128
|
}]
|
|
869
3129
|
});
|
|
870
|
-
strictGate =
|
|
3130
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
871
3131
|
} catch (error) {
|
|
872
3132
|
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
873
3133
|
return {
|
|
@@ -994,7 +3254,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
994
3254
|
fallbackReview?.html_url ? `- Review: ${fallbackReview.html_url}` : "",
|
|
995
3255
|
fallbackReview?.state ? `- Status: ${fallbackReview.state}` : "",
|
|
996
3256
|
"",
|
|
997
|
-
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."
|
|
998
3258
|
].filter(Boolean).join(`
|
|
999
3259
|
`);
|
|
1000
3260
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
@@ -1017,7 +3277,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
1017
3277
|
taskId: options.taskId,
|
|
1018
3278
|
prUrl
|
|
1019
3279
|
});
|
|
1020
|
-
strictGate =
|
|
3280
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
1021
3281
|
} catch (error) {
|
|
1022
3282
|
return {
|
|
1023
3283
|
verdict: "REJECT",
|
|
@@ -1242,7 +3502,7 @@ function loadGithubPullRequestState(projectRoot, repoName, prNumber) {
|
|
|
1242
3502
|
]);
|
|
1243
3503
|
return {
|
|
1244
3504
|
state: response.state || "",
|
|
1245
|
-
merged: response.merged,
|
|
3505
|
+
...response.merged !== undefined ? { merged: response.merged } : {},
|
|
1246
3506
|
merged_at: response.merged_at ?? null
|
|
1247
3507
|
};
|
|
1248
3508
|
}
|
|
@@ -1260,7 +3520,7 @@ function parsePullRequestNumber(url) {
|
|
|
1260
3520
|
return match ? Number.parseInt(match[1] || "0", 10) : 0;
|
|
1261
3521
|
}
|
|
1262
3522
|
function runGhJson(projectRoot, args) {
|
|
1263
|
-
const result =
|
|
3523
|
+
const result = runCapture2(["gh", ...args], projectRoot);
|
|
1264
3524
|
if (result.exitCode !== 0) {
|
|
1265
3525
|
throw new Error(result.stderr || result.stdout || `gh ${args.join(" ")} failed`);
|
|
1266
3526
|
}
|
|
@@ -1271,7 +3531,7 @@ function runGhJson(projectRoot, args) {
|
|
|
1271
3531
|
}
|
|
1272
3532
|
}
|
|
1273
3533
|
async function collectStrictPrEvidenceForVerifier(input) {
|
|
1274
|
-
return
|
|
3534
|
+
return mg().collectEvidence({
|
|
1275
3535
|
projectRoot: input.projectRoot,
|
|
1276
3536
|
prUrl: input.prUrl,
|
|
1277
3537
|
taskId: input.taskId,
|
|
@@ -1279,7 +3539,7 @@ async function collectStrictPrEvidenceForVerifier(input) {
|
|
|
1279
3539
|
cycle: 0,
|
|
1280
3540
|
apiSignals: input.apiSignals ?? [],
|
|
1281
3541
|
command: async (args, options) => {
|
|
1282
|
-
const result =
|
|
3542
|
+
const result = runCapture2(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
1283
3543
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
1284
3544
|
}
|
|
1285
3545
|
});
|
|
@@ -1292,11 +3552,11 @@ function deriveRepoName(projectRoot, prState) {
|
|
|
1292
3552
|
if (prState.target === "monorepo") {
|
|
1293
3553
|
return resolveRepoSlug(projectRoot);
|
|
1294
3554
|
}
|
|
1295
|
-
return
|
|
3555
|
+
return runCapture2(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
|
|
1296
3556
|
}
|
|
1297
3557
|
function resolvePrHeadSha(projectRoot, prState) {
|
|
1298
3558
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
1299
|
-
return
|
|
3559
|
+
return runCapture2(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
1300
3560
|
}
|
|
1301
3561
|
function isGreptileGithubLogin(login) {
|
|
1302
3562
|
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
@@ -1332,7 +3592,7 @@ function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
|
1332
3592
|
return response.statusCheckRollup || [];
|
|
1333
3593
|
}
|
|
1334
3594
|
function evaluatePullRequestCiChecks(checks, repoName, prNumber, options = {}) {
|
|
1335
|
-
const
|
|
3595
|
+
const isPendingCheck2 = (check) => {
|
|
1336
3596
|
if ((check.__typename || "") === "CheckRun") {
|
|
1337
3597
|
return (check.status || "").toUpperCase() !== "COMPLETED";
|
|
1338
3598
|
}
|
|
@@ -1341,7 +3601,7 @@ function evaluatePullRequestCiChecks(checks, repoName, prNumber, options = {}) {
|
|
|
1341
3601
|
};
|
|
1342
3602
|
const pendingGreptile = checks.filter((check) => {
|
|
1343
3603
|
const label = (check.name || check.context || "").toLowerCase();
|
|
1344
|
-
return label.includes("greptile") &&
|
|
3604
|
+
return label.includes("greptile") && isPendingCheck2(check);
|
|
1345
3605
|
});
|
|
1346
3606
|
if (pendingGreptile.length > 0) {
|
|
1347
3607
|
return {
|
|
@@ -1353,7 +3613,7 @@ function evaluatePullRequestCiChecks(checks, repoName, prNumber, options = {}) {
|
|
|
1353
3613
|
const label = (check.name || check.context || "").toLowerCase();
|
|
1354
3614
|
return label.length > 0 && !label.includes("greptile");
|
|
1355
3615
|
});
|
|
1356
|
-
const pending = nonGreptileChecks.filter(
|
|
3616
|
+
const pending = nonGreptileChecks.filter(isPendingCheck2);
|
|
1357
3617
|
const mergeClean = (options.mergeStateStatus || "").toUpperCase() === "CLEAN";
|
|
1358
3618
|
if (pending.length > 0 && !mergeClean) {
|
|
1359
3619
|
return {
|
|
@@ -1436,7 +3696,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
1436
3696
|
}
|
|
1437
3697
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
1438
3698
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1439
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
3699
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync5(resolve5(runtimeWorkspace, ".git"))) {
|
|
1440
3700
|
return runtimeWorkspace;
|
|
1441
3701
|
}
|
|
1442
3702
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -1447,10 +3707,10 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
1447
3707
|
return false;
|
|
1448
3708
|
}
|
|
1449
3709
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
1450
|
-
return
|
|
3710
|
+
return runCapture2(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
1451
3711
|
}
|
|
1452
3712
|
function summarizeComment(input) {
|
|
1453
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3713
|
+
const text = mg().stripHtml(input).replace(/\s+/g, " ").trim();
|
|
1454
3714
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
1455
3715
|
}
|
|
1456
3716
|
function asGreptileInfrastructureWarning(reason) {
|
|
@@ -1465,7 +3725,12 @@ function isAiReviewApproved(input) {
|
|
|
1465
3725
|
}
|
|
1466
3726
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
1467
3727
|
}
|
|
1468
|
-
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
|
+
});
|
|
1469
3734
|
|
|
1470
3735
|
// packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
|
|
1471
3736
|
var exports_completion_verification = {};
|
|
@@ -1474,43 +3739,33 @@ __export(exports_completion_verification, {
|
|
|
1474
3739
|
formatCompletionBlockedMessage: () => formatCompletionBlockedMessage,
|
|
1475
3740
|
closeCompletedTaskSource: () => closeCompletedTaskSource
|
|
1476
3741
|
});
|
|
1477
|
-
import { appendFileSync, existsSync as
|
|
1478
|
-
import { resolve as
|
|
1479
|
-
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 resolve6 } from "path";
|
|
3744
|
+
import { safePathSegment as safePathSegment2 } from "@rig/core/safe-identifiers";
|
|
1480
3745
|
import {
|
|
1481
|
-
escapeRegExp,
|
|
3746
|
+
escapeRegExp as escapeRegExp2,
|
|
1482
3747
|
resolveBunCli,
|
|
1483
3748
|
resolveBunCliInvocation,
|
|
1484
3749
|
resolveTaskScopes,
|
|
1485
3750
|
resolvePolicyContent
|
|
1486
3751
|
} from "@rig/hook-kit";
|
|
1487
|
-
import {
|
|
1488
|
-
import {
|
|
1489
|
-
import {
|
|
1490
|
-
import { strictMergeHeadShaFromGate } from "@rig/contracts";
|
|
1491
|
-
import { changedFilesForTask, pendingFilesForTask, taskArtifacts, taskValidate } from "@rig/runtime/control-plane/native/task-ops";
|
|
1492
|
-
import { currentTaskId } from "@rig/runtime/control-plane/native/task-state";
|
|
1493
|
-
import { resolveHarnessPaths as resolveHarnessPaths2, runCapture as runCapture2 } from "@rig/runtime/control-plane/native/utils";
|
|
1494
|
-
import { readSourceAwareTaskStatus } from "@rig/runtime/control-plane/tasks/source-aware-task-config-source";
|
|
1495
|
-
import {
|
|
1496
|
-
buildTaskRunLifecycleComment,
|
|
1497
|
-
updateConfiguredTaskSourceTask
|
|
1498
|
-
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1499
|
-
import { buildPluginHostContext } 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 as buildPluginHostContext2 } from "@rig/core/plugin-host-context";
|
|
1500
3755
|
async function closeCompletedTaskSource(projectRoot, taskId) {
|
|
1501
|
-
const comment = buildTaskRunLifecycleComment({
|
|
3756
|
+
const comment = taskData().buildTaskRunLifecycleComment({
|
|
1502
3757
|
runId: process.env.RIG_SERVER_RUN_ID || taskId,
|
|
1503
3758
|
status: "closed",
|
|
1504
3759
|
summary: "Rig completion verification approved and closed this task.",
|
|
1505
|
-
runtimeWorkspace: process.env.RIG_TASK_WORKSPACE,
|
|
1506
|
-
logsDir: process.env.RIG_LOGS_DIR,
|
|
1507
|
-
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 } : {}
|
|
1508
3763
|
});
|
|
1509
|
-
const result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
3764
|
+
const result = await taskData().updateConfiguredTaskSourceTask(projectRoot, {
|
|
1510
3765
|
taskId,
|
|
1511
3766
|
update: { status: "closed", comment }
|
|
1512
3767
|
});
|
|
1513
|
-
const status = result.status ?? await readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
3768
|
+
const status = result.status ?? await taskData().readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
1514
3769
|
if (!result.updated && status == null) {
|
|
1515
3770
|
return {
|
|
1516
3771
|
ok: true,
|
|
@@ -1530,7 +3785,7 @@ function isClosedStatus(status) {
|
|
|
1530
3785
|
}
|
|
1531
3786
|
async function runCompletionVerificationGate(projectRoot) {
|
|
1532
3787
|
seedPolicyFromContent(resolvePolicyContent(projectRoot));
|
|
1533
|
-
const taskId = currentTaskId(projectRoot);
|
|
3788
|
+
const taskId = taskData().currentTaskId(projectRoot);
|
|
1534
3789
|
if (!taskId) {
|
|
1535
3790
|
return { ok: true };
|
|
1536
3791
|
}
|
|
@@ -1539,7 +3794,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1539
3794
|
let sourceCloseoutAllowed = false;
|
|
1540
3795
|
console.log(`=== Completion Verification: ${taskId} ===`);
|
|
1541
3796
|
const scopes = await resolveTaskScopes(projectRoot, taskId);
|
|
1542
|
-
const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
3797
|
+
const taskChangedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
|
|
1543
3798
|
const sourceInArtifacts = taskChangedFiles.filter((file) => /^artifacts\//.test(file) && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
|
|
1544
3799
|
if (sourceInArtifacts.length > 0) {
|
|
1545
3800
|
console.log(`
|
|
@@ -1552,13 +3807,13 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1552
3807
|
}
|
|
1553
3808
|
failed = true;
|
|
1554
3809
|
}
|
|
1555
|
-
const pluginHostCtx = await
|
|
3810
|
+
const pluginHostCtx = await buildPluginHostContext2(projectRoot).catch((error) => {
|
|
1556
3811
|
console.warn(`[completion-verification] plugin host unavailable for validators: ${error instanceof Error ? error.message : String(error)}`);
|
|
1557
3812
|
return null;
|
|
1558
3813
|
});
|
|
1559
3814
|
console.log(`
|
|
1560
3815
|
[1/3] Task validation...`);
|
|
1561
|
-
if (!await taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
|
|
3816
|
+
if (!await taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
|
|
1562
3817
|
console.log(`FAIL: Validation failed for ${taskId}`);
|
|
1563
3818
|
failed = true;
|
|
1564
3819
|
} else {
|
|
@@ -1571,7 +3826,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1571
3826
|
failed = true;
|
|
1572
3827
|
}
|
|
1573
3828
|
}
|
|
1574
|
-
taskArtifacts(projectRoot, taskId);
|
|
3829
|
+
taskData().taskArtifacts(projectRoot, taskId);
|
|
1575
3830
|
const policy = loadPolicy(projectRoot);
|
|
1576
3831
|
const openPrEnabled = policy.completion.checks.includes("open-pr");
|
|
1577
3832
|
const autoMergeEnabled = policy.completion.checks.includes("auto-merge");
|
|
@@ -1598,7 +3853,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1598
3853
|
} else {
|
|
1599
3854
|
console.log("Verifier preflight: skipped (earlier checks failed)");
|
|
1600
3855
|
}
|
|
1601
|
-
const pendingTaskChangedFiles = pendingFilesForTask(projectRoot, taskId, true);
|
|
3856
|
+
const pendingTaskChangedFiles = taskData().pendingFilesForTask(projectRoot, taskId, true);
|
|
1602
3857
|
const hasLocalChanges = pendingTaskChangedFiles.length > 0;
|
|
1603
3858
|
console.log(`
|
|
1604
3859
|
[post] Auto-committing task changes...`);
|
|
@@ -1671,14 +3926,15 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1671
3926
|
console.log(`
|
|
1672
3927
|
[post] Auto-merge...`);
|
|
1673
3928
|
try {
|
|
1674
|
-
const prs =
|
|
3929
|
+
const prs = readPrMetadata(projectRoot, taskId);
|
|
1675
3930
|
if (prs.length === 0) {
|
|
1676
3931
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
1677
3932
|
} else {
|
|
3933
|
+
const mergeGate = await resolvePrMergeGateService(projectRoot);
|
|
1678
3934
|
let cycle = 0;
|
|
1679
3935
|
for (const pr of prs) {
|
|
1680
3936
|
cycle += 1;
|
|
1681
|
-
const gate = await
|
|
3937
|
+
const gate = await mergeGate.runGate({
|
|
1682
3938
|
projectRoot,
|
|
1683
3939
|
prUrl: pr.url,
|
|
1684
3940
|
taskId,
|
|
@@ -1686,7 +3942,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1686
3942
|
cycle,
|
|
1687
3943
|
final: true,
|
|
1688
3944
|
command: async (args, options) => {
|
|
1689
|
-
const result =
|
|
3945
|
+
const result = runCapture3(["gh", ...args], options?.cwd ?? projectRoot);
|
|
1690
3946
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
1691
3947
|
}
|
|
1692
3948
|
});
|
|
@@ -1703,7 +3959,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1703
3959
|
pr,
|
|
1704
3960
|
method: "squash",
|
|
1705
3961
|
deleteBranch: true,
|
|
1706
|
-
matchHeadCommit:
|
|
3962
|
+
matchHeadCommit: mergeGate.resolveHeadSha({ result: gate, prUrl: pr.url })
|
|
1707
3963
|
});
|
|
1708
3964
|
if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
|
|
1709
3965
|
console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
|
|
@@ -1723,9 +3979,9 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1723
3979
|
console.log(`
|
|
1724
3980
|
[post] Auto-merge: skipped (not in policy completion.checks)`);
|
|
1725
3981
|
}
|
|
1726
|
-
const artifactDir =
|
|
1727
|
-
|
|
1728
|
-
|
|
3982
|
+
const artifactDir = resolve6(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
3983
|
+
mkdirSync3(artifactDir, { recursive: true });
|
|
3984
|
+
writeFileSync3(resolve6(artifactDir, "review-status.txt"), failed ? `REJECTED
|
|
1729
3985
|
` : `APPROVED
|
|
1730
3986
|
`, "utf-8");
|
|
1731
3987
|
if (!failed) {
|
|
@@ -1775,8 +4031,8 @@ async function runBunTool(args, cwd) {
|
|
|
1775
4031
|
};
|
|
1776
4032
|
}
|
|
1777
4033
|
async function runProtoQualityGate(monorepoRoot) {
|
|
1778
|
-
const protosDir =
|
|
1779
|
-
if (!
|
|
4034
|
+
const protosDir = resolve6(monorepoRoot, "packages", "protos");
|
|
4035
|
+
if (!existsSync6(protosDir)) {
|
|
1780
4036
|
console.log(`FAIL: Proto workspace not found at ${protosDir}`);
|
|
1781
4037
|
return false;
|
|
1782
4038
|
}
|
|
@@ -1803,7 +4059,7 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1803
4059
|
console.log(generate.stderr || generate.stdout);
|
|
1804
4060
|
ok = false;
|
|
1805
4061
|
} else {
|
|
1806
|
-
const drift =
|
|
4062
|
+
const drift = runCapture3(["git", "-C", protosDir, "status", "--porcelain", "--", "gen/ts"], monorepoRoot);
|
|
1807
4063
|
if (drift.exitCode !== 0) {
|
|
1808
4064
|
console.log("FAIL: Could not inspect generated proto drift");
|
|
1809
4065
|
console.log(drift.stderr || drift.stdout);
|
|
@@ -1824,12 +4080,12 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1824
4080
|
} else {
|
|
1825
4081
|
console.log("OK: Generated TypeScript compiles");
|
|
1826
4082
|
}
|
|
1827
|
-
const workflowPath =
|
|
1828
|
-
if (!
|
|
4083
|
+
const workflowPath = resolve6(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
|
|
4084
|
+
if (!existsSync6(workflowPath)) {
|
|
1829
4085
|
console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
|
|
1830
4086
|
ok = false;
|
|
1831
4087
|
} else {
|
|
1832
|
-
const workflow =
|
|
4088
|
+
const workflow = readFileSync4(workflowPath, "utf-8");
|
|
1833
4089
|
if (workflow.includes("if: false && needs.detect.outputs.protos_changed == 'true'")) {
|
|
1834
4090
|
console.log("FAIL: Proto quality CI gate is disabled in pull-request-gate.yml");
|
|
1835
4091
|
ok = false;
|
|
@@ -1840,13 +4096,13 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1840
4096
|
return ok;
|
|
1841
4097
|
}
|
|
1842
4098
|
function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
1843
|
-
const unpushed =
|
|
4099
|
+
const unpushed = runCapture3(["git", "-C", repoRoot, "log", "@{u}..HEAD", "--oneline"], projectRoot);
|
|
1844
4100
|
if (unpushed.exitCode === 0 && unpushed.stdout.trim().length > 0)
|
|
1845
4101
|
return true;
|
|
1846
4102
|
if (unpushed.exitCode !== 0) {
|
|
1847
|
-
const branch =
|
|
4103
|
+
const branch = runCapture3(["git", "-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
|
|
1848
4104
|
if (branch.exitCode === 0 && branch.stdout.trim()) {
|
|
1849
|
-
const remote =
|
|
4105
|
+
const remote = runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branch.stdout.trim()}`], projectRoot);
|
|
1850
4106
|
if (remote.exitCode !== 0)
|
|
1851
4107
|
return true;
|
|
1852
4108
|
}
|
|
@@ -1855,10 +4111,10 @@ function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
|
1855
4111
|
}
|
|
1856
4112
|
function repoHasPublishedTaskBranch(projectRoot, repoRoot, taskId) {
|
|
1857
4113
|
const branchRef = resolveTaskBranchRef(projectRoot, taskId);
|
|
1858
|
-
return
|
|
4114
|
+
return runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branchRef}`], projectRoot).exitCode === 0;
|
|
1859
4115
|
}
|
|
1860
4116
|
async function readJsonFileIfPresent(path) {
|
|
1861
|
-
if (!
|
|
4117
|
+
if (!existsSync6(path)) {
|
|
1862
4118
|
return null;
|
|
1863
4119
|
}
|
|
1864
4120
|
try {
|
|
@@ -1869,9 +4125,9 @@ async function readJsonFileIfPresent(path) {
|
|
|
1869
4125
|
}
|
|
1870
4126
|
async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
1871
4127
|
const failedApproachesPath = paths.failedApproachesPath;
|
|
1872
|
-
const artifactDir =
|
|
1873
|
-
const reviewStatePath =
|
|
1874
|
-
const reviewFeedbackPath =
|
|
4128
|
+
const artifactDir = resolve6(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
4129
|
+
const reviewStatePath = resolve6(artifactDir, "review-state.json");
|
|
4130
|
+
const reviewFeedbackPath = resolve6(artifactDir, "review-feedback.md");
|
|
1875
4131
|
let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
|
|
1876
4132
|
const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
|
|
1877
4133
|
if (parsedReviewState) {
|
|
@@ -1881,12 +4137,12 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
|
1881
4137
|
}
|
|
1882
4138
|
}
|
|
1883
4139
|
let attempts = 1;
|
|
1884
|
-
if (
|
|
1885
|
-
const content =
|
|
1886
|
-
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;
|
|
1887
4143
|
} else {
|
|
1888
|
-
|
|
1889
|
-
|
|
4144
|
+
mkdirSync3(resolve6(failedApproachesPath, ".."), { recursive: true });
|
|
4145
|
+
writeFileSync3(failedApproachesPath, `# Failed Approaches
|
|
1890
4146
|
|
|
1891
4147
|
`, "utf-8");
|
|
1892
4148
|
}
|
|
@@ -1910,7 +4166,7 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
1910
4166
|
statePaths.add(resolveHarnessPaths2(hostProjectRoot).taskRepoCommitsPath);
|
|
1911
4167
|
}
|
|
1912
4168
|
const repos = {};
|
|
1913
|
-
const monoHead =
|
|
4169
|
+
const monoHead = runCapture3(["git", "-C", paths.monorepoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
1914
4170
|
if (monoHead) {
|
|
1915
4171
|
repos["monorepo"] = monoHead;
|
|
1916
4172
|
}
|
|
@@ -1924,13 +4180,17 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
1924
4180
|
recorded_at: new Date().toISOString(),
|
|
1925
4181
|
repos
|
|
1926
4182
|
};
|
|
1927
|
-
|
|
1928
|
-
|
|
4183
|
+
mkdirSync3(resolve6(statePath, ".."), { recursive: true });
|
|
4184
|
+
writeFileSync3(statePath, `${JSON.stringify(state, null, 2)}
|
|
1929
4185
|
`, "utf-8");
|
|
1930
4186
|
}
|
|
1931
4187
|
}
|
|
1932
4188
|
var init_completion_verification = __esm(() => {
|
|
4189
|
+
init_policy();
|
|
4190
|
+
init_git_ops();
|
|
4191
|
+
init_pr_merge_gate_cap();
|
|
1933
4192
|
init_verifier();
|
|
4193
|
+
init_task_data();
|
|
1934
4194
|
});
|
|
1935
4195
|
|
|
1936
4196
|
// packages/bundle-default-lifecycle/src/control-plane/task-verify.ts
|
|
@@ -1938,9 +4198,8 @@ var exports_task_verify = {};
|
|
|
1938
4198
|
__export(exports_task_verify, {
|
|
1939
4199
|
taskVerify: () => taskVerify
|
|
1940
4200
|
});
|
|
1941
|
-
import { currentTaskId as currentTaskId2 } from "@rig/runtime/control-plane/native/task-state";
|
|
1942
4201
|
async function taskVerify(projectRoot, taskId) {
|
|
1943
|
-
const activeTask = taskId ||
|
|
4202
|
+
const activeTask = taskId || taskData().currentTaskId(projectRoot);
|
|
1944
4203
|
if (!activeTask) {
|
|
1945
4204
|
throw new Error("No active task.");
|
|
1946
4205
|
}
|
|
@@ -1965,19 +4224,24 @@ async function taskVerify(projectRoot, taskId) {
|
|
|
1965
4224
|
return true;
|
|
1966
4225
|
}
|
|
1967
4226
|
var init_task_verify = __esm(() => {
|
|
4227
|
+
init_task_data();
|
|
1968
4228
|
init_verifier();
|
|
1969
4229
|
});
|
|
1970
4230
|
|
|
1971
4231
|
// packages/bundle-default-lifecycle/src/plugin.ts
|
|
1972
4232
|
import { definePlugin } from "@rig/core/config";
|
|
4233
|
+
import { defineCapability as defineCapability4 } from "@rig/core/capability";
|
|
1973
4234
|
import {
|
|
1974
|
-
|
|
1975
|
-
|
|
4235
|
+
COMPLETION_VERIFICATION_CAPABILITY,
|
|
4236
|
+
LIFECYCLE_GIT_AGENT,
|
|
4237
|
+
LIFECYCLE_TOOLCHAIN_SOURCES,
|
|
4238
|
+
RUN_CLOSEOUT_CAPABILITY,
|
|
4239
|
+
TASK_VERIFY_CAPABILITY
|
|
1976
4240
|
} from "@rig/contracts";
|
|
1977
4241
|
|
|
1978
4242
|
// packages/bundle-default-lifecycle/src/defaultPipeline.ts
|
|
1979
|
-
import {
|
|
1980
|
-
import {
|
|
4243
|
+
import { createDefaultKernelPlugin } from "@rig/kernel-seed/default-kernel";
|
|
4244
|
+
import { resolveKernelStages } from "@rig/kernel-seed/resolver";
|
|
1981
4245
|
|
|
1982
4246
|
// packages/bundle-default-lifecycle/src/stages/types.ts
|
|
1983
4247
|
function defineDefaultLifecycleStage(input) {
|
|
@@ -1995,6 +4259,10 @@ var autoMergeStage = defineDefaultLifecycleStage({
|
|
|
1995
4259
|
description: "Merge an approved PR using the repository default merge method through the runtime helper.",
|
|
1996
4260
|
calls: ["runRepoDefaultMerge"]
|
|
1997
4261
|
});
|
|
4262
|
+
async function runAutoMergeStage(input) {
|
|
4263
|
+
const { runRepoDefaultMerge: runRepoDefaultMerge2 } = await Promise.resolve().then(() => (init_pr_automation(), exports_pr_automation));
|
|
4264
|
+
await runRepoDefaultMerge2(input);
|
|
4265
|
+
}
|
|
1998
4266
|
|
|
1999
4267
|
// packages/bundle-default-lifecycle/src/stages/commit.ts
|
|
2000
4268
|
var commitStage = defineDefaultLifecycleStage({
|
|
@@ -2003,8 +4271,15 @@ var commitStage = defineDefaultLifecycleStage({
|
|
|
2003
4271
|
description: "Commit the agent worktree changes using the runtime git closeout helper.",
|
|
2004
4272
|
calls: ["commitRunChanges"]
|
|
2005
4273
|
});
|
|
4274
|
+
async function runCommitStage(input) {
|
|
4275
|
+
const { commitRunChanges: commitRunChanges2 } = await Promise.resolve().then(() => (init_pr_automation(), exports_pr_automation));
|
|
4276
|
+
return await commitRunChanges2(input);
|
|
4277
|
+
}
|
|
2006
4278
|
|
|
2007
4279
|
// packages/bundle-default-lifecycle/src/stages/isolation.ts
|
|
4280
|
+
import { ISOLATION_BACKEND } from "@rig/contracts";
|
|
4281
|
+
import { defineCapability as defineCapability2 } from "@rig/core/capability";
|
|
4282
|
+
import { requireCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
2008
4283
|
var isolationStage = defineDefaultLifecycleStage({
|
|
2009
4284
|
id: "isolation",
|
|
2010
4285
|
kind: "transform",
|
|
@@ -2027,6 +4302,23 @@ var mergeGateStage = defineDefaultLifecycleStage({
|
|
|
2027
4302
|
description: "Enforce GitHub review state, required checks, and configured review gates through runtime PR automation.",
|
|
2028
4303
|
calls: ["runStrictPrMergeGate"]
|
|
2029
4304
|
});
|
|
4305
|
+
async function runMergeGateStage(input) {
|
|
4306
|
+
const { resolvePrMergeGateService: resolvePrMergeGateService2 } = await Promise.resolve().then(() => (init_pr_merge_gate_cap(), exports_pr_merge_gate_cap));
|
|
4307
|
+
const mergeGate = await resolvePrMergeGateService2(input.projectRoot);
|
|
4308
|
+
return await mergeGate.runGate({
|
|
4309
|
+
projectRoot: input.projectRoot,
|
|
4310
|
+
prUrl: input.prUrl,
|
|
4311
|
+
taskId: input.taskId,
|
|
4312
|
+
runId: input.runId,
|
|
4313
|
+
cycle: input.cycle,
|
|
4314
|
+
command: input.command,
|
|
4315
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
4316
|
+
...input.allowedFailures !== undefined ? { allowedFailures: input.allowedFailures } : {},
|
|
4317
|
+
...input.greptileApi !== undefined ? { greptileApi: input.greptileApi } : {},
|
|
4318
|
+
...input.final !== undefined ? { final: input.final } : {},
|
|
4319
|
+
...input.requireGreptile !== undefined ? { requireGreptile: input.requireGreptile } : {}
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
2030
4322
|
|
|
2031
4323
|
// packages/bundle-default-lifecycle/src/stages/open-pr.ts
|
|
2032
4324
|
var openPrStage = defineDefaultLifecycleStage({
|
|
@@ -2035,6 +4327,24 @@ var openPrStage = defineDefaultLifecycleStage({
|
|
|
2035
4327
|
description: "Open or reuse the closeout PR through the existing runtime PR automation seam.",
|
|
2036
4328
|
calls: ["runPrAutomation"]
|
|
2037
4329
|
});
|
|
4330
|
+
async function runOpenPrStage(input) {
|
|
4331
|
+
const { runPrAutomation: runPrAutomation2 } = await Promise.resolve().then(() => (init_pr_automation(), exports_pr_automation));
|
|
4332
|
+
return await runPrAutomation2({
|
|
4333
|
+
projectRoot: input.workspace,
|
|
4334
|
+
taskId: input.taskId,
|
|
4335
|
+
runId: input.runId,
|
|
4336
|
+
branch: input.branch,
|
|
4337
|
+
sourceTask: input.sourceTask ? { title: typeof input.sourceTask.title === "string" ? input.sourceTask.title : null } : null,
|
|
4338
|
+
command: input.command,
|
|
4339
|
+
gitCommand: input.gitCommand,
|
|
4340
|
+
steerPi: input.steerPi,
|
|
4341
|
+
...input.config ? { config: input.config } : {},
|
|
4342
|
+
...input.artifactRoot !== undefined ? { artifactRoot: input.artifactRoot } : {},
|
|
4343
|
+
...input.greptileApi !== undefined ? { greptileApi: input.greptileApi } : {},
|
|
4344
|
+
...input.lifecycle !== undefined ? { lifecycle: input.lifecycle } : {},
|
|
4345
|
+
...input.uploadedSnapshot !== undefined ? { uploadedSnapshot: input.uploadedSnapshot } : {}
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
2038
4348
|
|
|
2039
4349
|
// packages/bundle-default-lifecycle/src/stages/push.ts
|
|
2040
4350
|
var pushStage = defineDefaultLifecycleStage({
|
|
@@ -2043,6 +4353,10 @@ var pushStage = defineDefaultLifecycleStage({
|
|
|
2043
4353
|
description: "Synchronize and push the task branch using the runtime closeout git helper.",
|
|
2044
4354
|
calls: ["pushBranchSyncedWithOrigin"]
|
|
2045
4355
|
});
|
|
4356
|
+
async function runPushStage(input) {
|
|
4357
|
+
const { pushBranchSyncedWithOrigin: pushBranchSyncedWithOrigin2 } = await Promise.resolve().then(() => (init_pr_automation(), exports_pr_automation));
|
|
4358
|
+
await pushBranchSyncedWithOrigin2(input);
|
|
4359
|
+
}
|
|
2046
4360
|
|
|
2047
4361
|
// packages/bundle-default-lifecycle/src/stages/source-closeout.ts
|
|
2048
4362
|
var sourceCloseoutStage = defineDefaultLifecycleStage({
|
|
@@ -2051,6 +4365,19 @@ var sourceCloseoutStage = defineDefaultLifecycleStage({
|
|
|
2051
4365
|
description: "Reflect the merged PR into the task source using the existing runtime closeout helper.",
|
|
2052
4366
|
calls: ["closeIssueAfterMergedPr"]
|
|
2053
4367
|
});
|
|
4368
|
+
async function runSourceCloseoutStage(input) {
|
|
4369
|
+
if (input.pr.status !== "merged" || !input.pr.prUrl)
|
|
4370
|
+
return;
|
|
4371
|
+
const { closeIssueAfterMergedPr: closeIssueAfterMergedPr2 } = await Promise.resolve().then(() => (init_pr_automation(), exports_pr_automation));
|
|
4372
|
+
await closeIssueAfterMergedPr2({
|
|
4373
|
+
projectRoot: input.projectRoot,
|
|
4374
|
+
taskId: input.taskId,
|
|
4375
|
+
runId: input.runId,
|
|
4376
|
+
prUrl: input.pr.prUrl,
|
|
4377
|
+
updateTaskSource: input.updateTaskSource,
|
|
4378
|
+
...input.sourceTask !== undefined ? { sourceTask: input.sourceTask } : {}
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
2054
4381
|
|
|
2055
4382
|
// packages/bundle-default-lifecycle/src/stages/validate.ts
|
|
2056
4383
|
var validateStage = defineDefaultLifecycleStage({
|
|
@@ -2059,6 +4386,9 @@ var validateStage = defineDefaultLifecycleStage({
|
|
|
2059
4386
|
description: "Run plugin-host validators against the isolated worktree before closeout side effects.",
|
|
2060
4387
|
calls: ["taskValidate"]
|
|
2061
4388
|
});
|
|
4389
|
+
async function runValidateStage(input, runner) {
|
|
4390
|
+
return await runner(input);
|
|
4391
|
+
}
|
|
2062
4392
|
|
|
2063
4393
|
// packages/bundle-default-lifecycle/src/stages/verify.ts
|
|
2064
4394
|
var verifyStage = defineDefaultLifecycleStage({
|
|
@@ -2237,7 +4567,306 @@ var defaultLifecycleCliCommands = [
|
|
|
2237
4567
|
}
|
|
2238
4568
|
];
|
|
2239
4569
|
|
|
4570
|
+
// packages/bundle-default-lifecycle/src/pipelineCloseout.ts
|
|
4571
|
+
import { resolve } from "path";
|
|
4572
|
+
import { loadConfig } from "@rig/core/load-config";
|
|
4573
|
+
import { resolvePluginHost as resolvePluginHost2 } from "@rig/core/project-plugins";
|
|
4574
|
+
import { createDefaultKernel } from "@rig/kernel-seed/default-kernel";
|
|
4575
|
+
import { buildPluginHostContext } from "@rig/core/plugin-host-context";
|
|
4576
|
+
|
|
4577
|
+
// packages/bundle-default-lifecycle/src/native/in-process-closeout.ts
|
|
4578
|
+
class CloseoutValidationError extends Error {
|
|
4579
|
+
name = "CloseoutValidationError";
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
// packages/bundle-default-lifecycle/src/pipelineCloseout.ts
|
|
4583
|
+
init_task_data();
|
|
4584
|
+
function cleanString(value) {
|
|
4585
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
4586
|
+
}
|
|
4587
|
+
function closeoutOutcome(status) {
|
|
4588
|
+
switch (status) {
|
|
4589
|
+
case "completed":
|
|
4590
|
+
return "completed";
|
|
4591
|
+
case "failed":
|
|
4592
|
+
case "needs-attention":
|
|
4593
|
+
return "failed";
|
|
4594
|
+
case "pending":
|
|
4595
|
+
case "running":
|
|
4596
|
+
return "started";
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
async function loadRigAutomationConfig(projectRoot) {
|
|
4600
|
+
return await loadConfig(projectRoot);
|
|
4601
|
+
}
|
|
4602
|
+
async function runRigProjectValidation({ projectRoot, taskId }) {
|
|
4603
|
+
const pluginHostCtx = await buildPluginHostContext(projectRoot);
|
|
4604
|
+
return taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined);
|
|
4605
|
+
}
|
|
4606
|
+
function shouldAttemptRigMerge2(config) {
|
|
4607
|
+
const mode = config.merge?.mode;
|
|
4608
|
+
return mode !== "off" && mode !== "pr-ready";
|
|
4609
|
+
}
|
|
4610
|
+
async function loadPluginStageContributions(projectRoot) {
|
|
4611
|
+
const { host } = await resolvePluginHost2(projectRoot);
|
|
4612
|
+
return { executors: host.listStageExecutors(), mutations: host.listStageMutations() };
|
|
4613
|
+
}
|
|
4614
|
+
async function runPipelineCloseout(input) {
|
|
4615
|
+
const taskId = cleanString(input.taskId);
|
|
4616
|
+
if (!taskId) {
|
|
4617
|
+
throw new Error("Pipeline closeout requires a task id.");
|
|
4618
|
+
}
|
|
4619
|
+
const loadedConfig = input.config ?? await loadRigAutomationConfig(input.projectRoot);
|
|
4620
|
+
const prMode = loadedConfig?.pr?.mode ?? "off";
|
|
4621
|
+
const reviewProvider = loadedConfig?.review?.provider ?? "github";
|
|
4622
|
+
const effectiveConfig = {
|
|
4623
|
+
...loadedConfig ?? {},
|
|
4624
|
+
pr: { ...loadedConfig?.pr ?? {}, mode: prMode },
|
|
4625
|
+
review: { ...loadedConfig?.review ?? {}, provider: reviewProvider }
|
|
4626
|
+
};
|
|
4627
|
+
const openOnlyConfig = {
|
|
4628
|
+
...effectiveConfig,
|
|
4629
|
+
merge: { ...effectiveConfig.merge ?? {}, mode: "pr-ready" }
|
|
4630
|
+
};
|
|
4631
|
+
const shouldMerge = shouldAttemptRigMerge2(effectiveConfig);
|
|
4632
|
+
const workspace = input.workspace;
|
|
4633
|
+
const artifactRoot = input.artifactRoot ?? resolve(input.projectRoot, "artifacts", taskId);
|
|
4634
|
+
const journal = async (phase, status, detail) => {
|
|
4635
|
+
await input.journalPhase(phase, closeoutOutcome(status), detail ?? null);
|
|
4636
|
+
};
|
|
4637
|
+
if (prMode === "off" || prMode === "ask") {
|
|
4638
|
+
const reason = prMode === "ask" ? "PR creation awaits operator approval." : "PR automation disabled.";
|
|
4639
|
+
await input.reflect("under_review", reason);
|
|
4640
|
+
await journal("completed", "completed", reason);
|
|
4641
|
+
return {
|
|
4642
|
+
mode: "pipeline",
|
|
4643
|
+
pipelineStageIds: [...resolveDefaultLifecycle().order],
|
|
4644
|
+
result: { status: "skipped", iterations: 0, feedback: [] }
|
|
4645
|
+
};
|
|
4646
|
+
}
|
|
4647
|
+
const state = {
|
|
4648
|
+
branch: input.branch,
|
|
4649
|
+
validationPassed: false,
|
|
4650
|
+
committed: false,
|
|
4651
|
+
pushed: false,
|
|
4652
|
+
prUrl: null,
|
|
4653
|
+
prReady: false,
|
|
4654
|
+
pr: null,
|
|
4655
|
+
gate: null,
|
|
4656
|
+
mergeGate: null,
|
|
4657
|
+
merged: false,
|
|
4658
|
+
iterations: 0,
|
|
4659
|
+
feedback: [],
|
|
4660
|
+
blockedDetail: null
|
|
4661
|
+
};
|
|
4662
|
+
const cont = (ctx2) => ({ kind: "continue", ctx: ctx2 });
|
|
4663
|
+
const executors = {
|
|
4664
|
+
isolation: (ctx2) => cont(ctx2),
|
|
4665
|
+
validate: async (ctx2) => {
|
|
4666
|
+
await input.onValidationStart?.();
|
|
4667
|
+
await journal("queued", "running", `Validating task ${taskId} before closeout.`);
|
|
4668
|
+
let passed = false;
|
|
4669
|
+
try {
|
|
4670
|
+
passed = await runValidateStage({ projectRoot: input.projectRoot, taskId }, input.runValidation ?? runRigProjectValidation);
|
|
4671
|
+
} catch (error) {
|
|
4672
|
+
const detail = `Rig validation failed before closeout: ${error instanceof Error ? error.message : String(error)}`;
|
|
4673
|
+
await input.reflect("needs_attention", "Rig validation failed before closeout; commit/push/PR automation is blocked.", { errorText: detail });
|
|
4674
|
+
throw new CloseoutValidationError(detail);
|
|
4675
|
+
}
|
|
4676
|
+
if (!passed) {
|
|
4677
|
+
const detail = `Rig validation failed for task ${taskId}; closeout blocked before commit.`;
|
|
4678
|
+
await input.reflect("needs_attention", "Rig validation failed before closeout; commit/push/PR automation is blocked.", { errorText: detail });
|
|
4679
|
+
throw new CloseoutValidationError(detail);
|
|
4680
|
+
}
|
|
4681
|
+
state.validationPassed = true;
|
|
4682
|
+
await journal("queued", "completed", `Validation passed for task ${taskId}.`);
|
|
4683
|
+
const workspaceBranch = await input.gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
|
|
4684
|
+
const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? cleanString(workspaceBranch.stdout) : null;
|
|
4685
|
+
if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== state.branch) {
|
|
4686
|
+
state.branch = currentWorkspaceBranch;
|
|
4687
|
+
}
|
|
4688
|
+
return cont(ctx2);
|
|
4689
|
+
},
|
|
4690
|
+
verify: () => state.validationPassed ? { kind: "allow" } : { kind: "block", reason: "validation did not pass" },
|
|
4691
|
+
commit: async (ctx2) => {
|
|
4692
|
+
await journal("commit", "running", `Committing changes in ${workspace}.`);
|
|
4693
|
+
const committed = await runCommitStage({ cwd: workspace, message: `rig: complete task ${taskId}`, command: input.gitCommand });
|
|
4694
|
+
state.committed = committed.committed;
|
|
4695
|
+
return cont(ctx2);
|
|
4696
|
+
},
|
|
4697
|
+
push: async (ctx2) => {
|
|
4698
|
+
await journal("push", "running", `Pushing branch ${state.branch}.`);
|
|
4699
|
+
await runPushStage({ projectRoot: workspace, branch: state.branch, gitCommand: input.gitCommand });
|
|
4700
|
+
state.pushed = true;
|
|
4701
|
+
return cont(ctx2);
|
|
4702
|
+
},
|
|
4703
|
+
"open-pr": async (ctx2) => {
|
|
4704
|
+
await journal("pr-review-merge", "running", `Opening a pull request for ${state.branch}.`);
|
|
4705
|
+
const pr = await runOpenPrStage({
|
|
4706
|
+
...input,
|
|
4707
|
+
taskId,
|
|
4708
|
+
branch: state.branch,
|
|
4709
|
+
artifactRoot,
|
|
4710
|
+
config: openOnlyConfig,
|
|
4711
|
+
sourceTask: { title: cleanString(input.sourceTask?.title) },
|
|
4712
|
+
lifecycle: {
|
|
4713
|
+
onPrOpened: async ({ prUrl }) => {
|
|
4714
|
+
await journal("pr-opened", "running", prUrl);
|
|
4715
|
+
await input.reflect("under_review", "Rig opened a pull request for this task.");
|
|
4716
|
+
},
|
|
4717
|
+
onFeedback: async ({ feedback }) => {
|
|
4718
|
+
await input.reflect("ci_fixing", "Rig is fixing CI/review feedback for this task.", { errorText: feedback.join(`
|
|
4719
|
+
`) || null });
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
});
|
|
4723
|
+
state.pr = pr;
|
|
4724
|
+
state.prUrl = pr.prUrl ?? null;
|
|
4725
|
+
state.prReady = pr.status === "opened" || pr.status === "merged";
|
|
4726
|
+
state.iterations = pr.iterations;
|
|
4727
|
+
state.feedback = [...pr.actionableFeedback];
|
|
4728
|
+
if (pr.status === "needs_attention") {
|
|
4729
|
+
state.blockedDetail = pr.actionableFeedback.join(`
|
|
4730
|
+
`) || "PR automation did not produce a mergeable PR.";
|
|
4731
|
+
}
|
|
4732
|
+
return cont(ctx2);
|
|
4733
|
+
},
|
|
4734
|
+
"merge-gate": async (ctx2) => {
|
|
4735
|
+
if (!shouldMerge || !state.prReady || !state.prUrl) {
|
|
4736
|
+
state.mergeGate = state.prReady ? "skipped" : null;
|
|
4737
|
+
return state.prReady ? cont(ctx2) : { kind: "block", reason: state.blockedDetail ?? "no mergeable PR to gate" };
|
|
4738
|
+
}
|
|
4739
|
+
const gate = await runMergeGateStage({
|
|
4740
|
+
projectRoot: workspace,
|
|
4741
|
+
prUrl: state.prUrl,
|
|
4742
|
+
taskId,
|
|
4743
|
+
runId: input.runId,
|
|
4744
|
+
cycle: 1,
|
|
4745
|
+
command: input.command,
|
|
4746
|
+
artifactRoot,
|
|
4747
|
+
final: true,
|
|
4748
|
+
...effectiveConfig.merge?.allowedFailures ? { allowedFailures: effectiveConfig.merge.allowedFailures } : {},
|
|
4749
|
+
...input.greptileApi !== undefined ? { greptileApi: input.greptileApi } : {},
|
|
4750
|
+
requireGreptile: reviewProvider === "greptile"
|
|
4751
|
+
});
|
|
4752
|
+
state.gate = gate;
|
|
4753
|
+
if (gate.approved) {
|
|
4754
|
+
state.mergeGate = "passed";
|
|
4755
|
+
return cont(ctx2);
|
|
4756
|
+
}
|
|
4757
|
+
state.mergeGate = "blocked";
|
|
4758
|
+
const detail = gate.actionableFeedback.join(`
|
|
4759
|
+
`) || gate.reasons.join("; ") || "merge gate blocked the PR.";
|
|
4760
|
+
state.blockedDetail = detail;
|
|
4761
|
+
await input.reflect("needs_attention", "Rig needs operator attention before this task can merge.", { errorText: detail });
|
|
4762
|
+
await journal("pr-review-merge", "needs-attention", detail);
|
|
4763
|
+
return { kind: "block", reason: detail };
|
|
4764
|
+
},
|
|
4765
|
+
"auto-merge": async (ctx2) => {
|
|
4766
|
+
if (!shouldMerge || state.mergeGate !== "passed" || !state.prUrl || !state.gate) {
|
|
4767
|
+
return cont(ctx2);
|
|
4768
|
+
}
|
|
4769
|
+
await journal("merge", "running", state.prUrl);
|
|
4770
|
+
await input.reflect("merging", "Rig is merging the pull request for this task.");
|
|
4771
|
+
await runAutoMergeStage({ prUrl: state.prUrl, config: effectiveConfig, command: input.command, cwd: workspace, strictGate: state.gate });
|
|
4772
|
+
state.merged = true;
|
|
4773
|
+
return cont(ctx2);
|
|
4774
|
+
},
|
|
4775
|
+
"source-closeout": async (ctx2) => {
|
|
4776
|
+
if (!state.merged || !state.prUrl)
|
|
4777
|
+
return cont(ctx2);
|
|
4778
|
+
await journal("close-source", "running", state.prUrl);
|
|
4779
|
+
const mergedPr = { ...state.pr ?? { iterations: state.iterations, actionableFeedback: state.feedback }, status: "merged", prUrl: state.prUrl, merged: true };
|
|
4780
|
+
await runSourceCloseoutStage({
|
|
4781
|
+
projectRoot: input.projectRoot,
|
|
4782
|
+
taskId,
|
|
4783
|
+
runId: input.runId,
|
|
4784
|
+
pr: mergedPr,
|
|
4785
|
+
...input.sourceTask !== undefined ? { sourceTask: input.sourceTask } : {},
|
|
4786
|
+
updateTaskSource: async () => {
|
|
4787
|
+
await input.reflect("closed", "Rig merged the pull request and closed this task source.");
|
|
4788
|
+
return { updated: true, taskId, status: "closed", source: "runtime", sourceKind: "runtime" };
|
|
4789
|
+
}
|
|
4790
|
+
});
|
|
4791
|
+
return cont(ctx2);
|
|
4792
|
+
},
|
|
4793
|
+
"journal-append": (ctx2) => cont(ctx2)
|
|
4794
|
+
};
|
|
4795
|
+
const defaultLifecyclePlugin = createDefaultLifecyclePlugin(executors);
|
|
4796
|
+
const pluginStages = await loadPluginStageContributions(input.projectRoot);
|
|
4797
|
+
const kernel = createDefaultKernel({ ...input.kernelJournal ? { journal: input.kernelJournal } : {}, stageExecutors: { ...executors, ...pluginStages.executors } });
|
|
4798
|
+
const resolved = kernel.stageRunner.resolve(defaultLifecyclePlugin.contributes?.stages ?? [], pluginStages.mutations);
|
|
4799
|
+
const ctx = {
|
|
4800
|
+
runId: input.runId,
|
|
4801
|
+
taskId,
|
|
4802
|
+
state,
|
|
4803
|
+
metadata: { projectRoot: input.projectRoot, workspace, closeoutState: state }
|
|
4804
|
+
};
|
|
4805
|
+
await kernel.stageRunner.runPipeline(input.runId, resolved, ctx);
|
|
4806
|
+
const result = mapStateToResult(state);
|
|
4807
|
+
if (result.status === "merged" || result.status === "opened") {
|
|
4808
|
+
await journal("completed", "completed", result.prUrl ? `${result.status === "merged" ? "PR merged and issue closed" : "PR ready without merge"}: ${result.prUrl}` : result.status);
|
|
4809
|
+
}
|
|
4810
|
+
return { mode: "pipeline", pipelineStageIds: [...resolved.order], result };
|
|
4811
|
+
}
|
|
4812
|
+
function mapStateToResult(state) {
|
|
4813
|
+
if (state.merged && state.prUrl) {
|
|
4814
|
+
return { status: "merged", prUrl: state.prUrl, iterations: state.iterations, feedback: state.feedback };
|
|
4815
|
+
}
|
|
4816
|
+
if (state.mergeGate === "blocked" || state.blockedDetail) {
|
|
4817
|
+
return {
|
|
4818
|
+
status: "needs-attention",
|
|
4819
|
+
...state.prUrl ? { prUrl: state.prUrl } : {},
|
|
4820
|
+
iterations: state.iterations,
|
|
4821
|
+
feedback: state.feedback
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
if (state.prReady && state.prUrl) {
|
|
4825
|
+
return { status: "opened", prUrl: state.prUrl, iterations: state.iterations, feedback: state.feedback };
|
|
4826
|
+
}
|
|
4827
|
+
return { status: "needs-attention", ...state.prUrl ? { prUrl: state.prUrl } : {}, iterations: state.iterations, feedback: state.feedback };
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// packages/bundle-default-lifecycle/src/native/closeout-runners.ts
|
|
4831
|
+
init_github_auth_env();
|
|
4832
|
+
function commandEnv(env) {
|
|
4833
|
+
const token = resolveGitHubAuthToken(env);
|
|
4834
|
+
return token ? { ...env, RIG_GITHUB_TOKEN: token, GH_TOKEN: token, GITHUB_TOKEN: token } : { ...env };
|
|
4835
|
+
}
|
|
4836
|
+
function createBunCommandRunner(binary, env) {
|
|
4837
|
+
return async (args, options) => {
|
|
4838
|
+
try {
|
|
4839
|
+
const child = Bun.spawn([binary, ...args], {
|
|
4840
|
+
...options?.cwd !== undefined ? { cwd: options.cwd } : {},
|
|
4841
|
+
env: commandEnv(env),
|
|
4842
|
+
stdin: "ignore",
|
|
4843
|
+
stdout: "pipe",
|
|
4844
|
+
stderr: "pipe"
|
|
4845
|
+
});
|
|
4846
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
4847
|
+
child.exited,
|
|
4848
|
+
new Response(child.stdout).text(),
|
|
4849
|
+
new Response(child.stderr).text()
|
|
4850
|
+
]);
|
|
4851
|
+
return { exitCode, stdout, stderr };
|
|
4852
|
+
} catch (error) {
|
|
4853
|
+
return {
|
|
4854
|
+
exitCode: 1,
|
|
4855
|
+
stdout: "",
|
|
4856
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
4857
|
+
};
|
|
4858
|
+
}
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
function createEnvCloseoutRunners(env = process.env) {
|
|
4862
|
+
return {
|
|
4863
|
+
command: createBunCommandRunner("gh", env),
|
|
4864
|
+
gitCommand: createBunCommandRunner("git", env)
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
|
|
2240
4868
|
// packages/bundle-default-lifecycle/src/plugin.ts
|
|
4869
|
+
init_git_ops();
|
|
2241
4870
|
var DEFAULT_LIFECYCLE_PLUGIN_ID = "@rig/bundle-default-lifecycle";
|
|
2242
4871
|
var COMPLETION_VERIFICATION_HOOK_ID = "@rig/bundle-default-lifecycle:completion-verification";
|
|
2243
4872
|
async function runCompletionGate(projectRoot) {
|
|
@@ -2261,26 +4890,85 @@ var LIFECYCLE_HOOKS = [
|
|
|
2261
4890
|
handler: completionVerificationStopHandler
|
|
2262
4891
|
}
|
|
2263
4892
|
];
|
|
4893
|
+
var TaskVerifyCap = defineCapability4(TASK_VERIFY_CAPABILITY);
|
|
4894
|
+
var CompletionVerificationCap = defineCapability4(COMPLETION_VERIFICATION_CAPABILITY);
|
|
4895
|
+
var RunCloseoutCap = defineCapability4(RUN_CLOSEOUT_CAPABILITY);
|
|
4896
|
+
var LifecycleGitAgentCap = defineCapability4(LIFECYCLE_GIT_AGENT);
|
|
4897
|
+
var ToolchainSourcesCap = defineCapability4(LIFECYCLE_TOOLCHAIN_SOURCES);
|
|
4898
|
+
var LIFECYCLE_TOOLCHAIN_SOURCE_CONTRIBUTION = {
|
|
4899
|
+
hookBinaries: [
|
|
4900
|
+
{
|
|
4901
|
+
name: "inject-context",
|
|
4902
|
+
source: "packages/bundle-default-lifecycle/src/control-plane/hooks/inject-context.ts"
|
|
4903
|
+
},
|
|
4904
|
+
{
|
|
4905
|
+
name: "task-runtime-start",
|
|
4906
|
+
source: "packages/bundle-default-lifecycle/src/control-plane/hooks/task-runtime-start.ts"
|
|
4907
|
+
}
|
|
4908
|
+
]
|
|
4909
|
+
};
|
|
2264
4910
|
var LIFECYCLE_CAPABILITIES = [
|
|
2265
4911
|
{ id: "default-lifecycle.pipeline", title: "Default lifecycle pipeline", commandId: DEFAULT_PIPELINE_CLI_ID },
|
|
2266
4912
|
{ id: "default-lifecycle.kernel-status", title: "Default kernel status", commandId: DEFAULT_KERNEL_CLI_ID },
|
|
2267
|
-
{
|
|
2268
|
-
|
|
4913
|
+
TaskVerifyCap.provide(() => async (input) => {
|
|
4914
|
+
const { taskVerify: taskVerify2 } = await Promise.resolve().then(() => (init_task_verify(), exports_task_verify));
|
|
4915
|
+
return taskVerify2(input.projectRoot, input.taskId);
|
|
4916
|
+
}, {
|
|
2269
4917
|
title: "Task verification",
|
|
2270
|
-
description: "Verify a task's changes (local checks + AI review) and report approval."
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
4918
|
+
description: "Verify a task's changes (local checks + AI review) and report approval."
|
|
4919
|
+
}),
|
|
4920
|
+
CompletionVerificationCap.provide(() => async (input) => runCompletionGate(input.projectRoot), {
|
|
4921
|
+
title: "Completion verification gate",
|
|
4922
|
+
description: "Run the full completion gate for the active task and report whether it passed."
|
|
4923
|
+
}),
|
|
4924
|
+
RunCloseoutCap.provide(() => async (input) => {
|
|
4925
|
+
const closeout = await runPipelineCloseout({ ...input, ...createEnvCloseoutRunners(process.env) });
|
|
4926
|
+
return closeout.result;
|
|
4927
|
+
}, {
|
|
4928
|
+
title: "Run closeout",
|
|
4929
|
+
description: "Run the default lifecycle closeout driver for a completed OMP run."
|
|
4930
|
+
}),
|
|
4931
|
+
LifecycleGitAgentCap.provide(() => ({
|
|
4932
|
+
shouldScopeGitCommit,
|
|
4933
|
+
gitStatus,
|
|
4934
|
+
gitChanged,
|
|
4935
|
+
gitPreflight,
|
|
4936
|
+
gitSyncBranch,
|
|
4937
|
+
gitCommit,
|
|
4938
|
+
gitSnapshot,
|
|
4939
|
+
gitOpenPr
|
|
4940
|
+
}), {
|
|
4941
|
+
title: "Lifecycle git agent commands",
|
|
4942
|
+
description: "Task-aware git helpers used by the provider-owned rig-agent command surface."
|
|
4943
|
+
}),
|
|
4944
|
+
ToolchainSourcesCap.provide(() => LIFECYCLE_TOOLCHAIN_SOURCE_CONTRIBUTION, {
|
|
4945
|
+
title: "Lifecycle toolchain sources",
|
|
4946
|
+
description: "Source paths for the lifecycle control-plane hook binaries (inject-context, task-runtime-start), compiled by the isolation runtime toolchain."
|
|
4947
|
+
})
|
|
4948
|
+
];
|
|
4949
|
+
async function runLifecycleHookRunner(hookId, role) {
|
|
4950
|
+
process.env.RIG_HOOK_ROLE = role;
|
|
4951
|
+
const { main } = await import("@rig/core/hook-runner");
|
|
4952
|
+
await main(["--plugin", DEFAULT_LIFECYCLE_PLUGIN_ID, "--hook", hookId]);
|
|
4953
|
+
}
|
|
4954
|
+
var LIFECYCLE_SEED_ENTRYPOINTS = [
|
|
4955
|
+
{
|
|
4956
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:completion-verification-entrypoint`,
|
|
4957
|
+
basename: "completion-verification",
|
|
4958
|
+
run: ({ basename }) => runLifecycleHookRunner(COMPLETION_VERIFICATION_HOOK_ID, basename ?? "completion-verification")
|
|
4959
|
+
},
|
|
4960
|
+
{
|
|
4961
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:inject-context-entrypoint`,
|
|
4962
|
+
basename: "inject-context",
|
|
4963
|
+
run: async () => {
|
|
4964
|
+
await import("@rig/bundle-default-lifecycle/control-plane/hooks/inject-context");
|
|
2275
4965
|
}
|
|
2276
4966
|
},
|
|
2277
4967
|
{
|
|
2278
|
-
id:
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
const { projectRoot } = input;
|
|
2283
|
-
return runCompletionGate(projectRoot);
|
|
4968
|
+
id: `${DEFAULT_LIFECYCLE_PLUGIN_ID}:task-runtime-start-entrypoint`,
|
|
4969
|
+
basename: "task-runtime-start",
|
|
4970
|
+
run: async () => {
|
|
4971
|
+
await import("@rig/bundle-default-lifecycle/control-plane/hooks/task-runtime-start");
|
|
2284
4972
|
}
|
|
2285
4973
|
}
|
|
2286
4974
|
];
|
|
@@ -2290,10 +4978,20 @@ function createDefaultLifecyclePlugin(stages = {}) {
|
|
|
2290
4978
|
version: "0.0.0-alpha.1",
|
|
2291
4979
|
provides: [],
|
|
2292
4980
|
contributes: {
|
|
2293
|
-
stages: defaultLifecycleStages.map((stage) =>
|
|
4981
|
+
stages: defaultLifecycleStages.map((stage) => {
|
|
4982
|
+
const run = Object.prototype.hasOwnProperty.call(stages, stage.id) ? stages[stage.id] : undefined;
|
|
4983
|
+
return run ? { ...stage, run } : stage;
|
|
4984
|
+
}),
|
|
2294
4985
|
hooks: LIFECYCLE_HOOKS,
|
|
2295
4986
|
capabilities: LIFECYCLE_CAPABILITIES,
|
|
2296
|
-
cliCommands: defaultLifecycleCliCommands
|
|
4987
|
+
cliCommands: defaultLifecycleCliCommands,
|
|
4988
|
+
seedEntrypoints: LIFECYCLE_SEED_ENTRYPOINTS,
|
|
4989
|
+
config: {
|
|
4990
|
+
defaults: () => ({
|
|
4991
|
+
merge: { mode: "auto", method: "repo-default", deleteBranch: "repo-default", bypass: false },
|
|
4992
|
+
automation: { maxValidationAttempts: 30, maxPrFixIterations: 100500 }
|
|
4993
|
+
})
|
|
4994
|
+
}
|
|
2297
4995
|
}
|
|
2298
4996
|
});
|
|
2299
4997
|
}
|