@h-rig/bundle-default-lifecycle 0.0.6-alpha.157 → 0.0.6-alpha.159

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.
Files changed (54) hide show
  1. package/dist/src/cli.d.ts +1 -7
  2. package/dist/src/cli.js +5 -2
  3. package/dist/src/control-plane/completion-verification.js +1591 -118
  4. package/dist/src/control-plane/hooks/inject-context.d.ts +2 -0
  5. package/dist/src/control-plane/hooks/inject-context.js +175 -0
  6. package/dist/src/control-plane/hooks/shared.d.ts +11 -0
  7. package/dist/src/control-plane/hooks/shared.js +44 -0
  8. package/dist/src/control-plane/hooks/submodule-branch.d.ts +2 -0
  9. package/dist/src/control-plane/hooks/submodule-branch.js +432 -0
  10. package/dist/src/control-plane/hooks/task-runtime-start.d.ts +2 -0
  11. package/dist/src/control-plane/hooks/task-runtime-start.js +429 -0
  12. package/dist/src/control-plane/materialize-task-config.d.ts +29 -0
  13. package/dist/src/control-plane/materialize-task-config.js +95 -0
  14. package/dist/src/control-plane/native/git-ops.d.ts +67 -0
  15. package/dist/src/control-plane/native/git-ops.js +1390 -0
  16. package/dist/src/control-plane/policy.d.ts +3 -0
  17. package/dist/src/control-plane/policy.js +226 -0
  18. package/dist/src/control-plane/pr-automation.d.ts +2 -0
  19. package/dist/src/control-plane/pr-automation.js +26 -16
  20. package/dist/src/control-plane/pr-merge-gate-cap.d.ts +10 -0
  21. package/dist/src/control-plane/pr-merge-gate-cap.js +13 -0
  22. package/dist/src/control-plane/task-data.d.ts +13 -0
  23. package/dist/src/control-plane/task-data.js +12 -0
  24. package/dist/src/control-plane/task-verify.js +131 -59
  25. package/dist/src/control-plane/verifier.d.ts +1 -3
  26. package/dist/src/control-plane/verifier.js +133 -57
  27. package/dist/src/defaultPipeline.d.ts +1 -1
  28. package/dist/src/defaultPipeline.js +5 -2
  29. package/dist/src/index.d.ts +0 -2
  30. package/dist/src/index.js +1908 -290
  31. package/dist/src/native/closeout-runners.js +22 -2
  32. package/dist/src/native/github-auth-env.d.ts +2 -0
  33. package/dist/src/native/github-auth-env.js +25 -0
  34. package/dist/src/native/host-git.d.ts +6 -0
  35. package/dist/src/native/host-git.js +62 -0
  36. package/dist/src/native/in-process-closeout.d.ts +1 -3
  37. package/dist/src/native/in-process-closeout.js +0 -794
  38. package/dist/src/pipelineCloseout.js +1905 -185
  39. package/dist/src/plugin.js +2843 -145
  40. package/dist/src/stages/auto-merge.js +28 -16
  41. package/dist/src/stages/commit.js +28 -16
  42. package/dist/src/stages/isolation.d.ts +1 -1
  43. package/dist/src/stages/isolation.js +5 -3
  44. package/dist/src/stages/merge-gate.js +35 -3
  45. package/dist/src/stages/open-pr.js +28 -16
  46. package/dist/src/stages/push.js +28 -16
  47. package/dist/src/stages/source-closeout.js +28 -16
  48. package/package.json +29 -16
  49. package/dist/src/branch-naming.d.ts +0 -15
  50. package/dist/src/branch-naming.js +0 -33
  51. package/dist/src/closeoutEquivalence.d.ts +0 -37
  52. package/dist/src/closeoutEquivalence.js +0 -78
  53. package/dist/src/closeoutShadowHarness.d.ts +0 -27
  54. package/dist/src/closeoutShadowHarness.js +0 -29
@@ -0,0 +1,1390 @@
1
+ // @bun
2
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
3
+ import { existsSync as existsSync3, lstatSync, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { dirname, isAbsolute, resolve as resolve2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { loadDotEnvSecrets, resolveRuntimeSecrets } from "@rig/core/baked-secrets";
8
+ import { loadRuntimeContext, loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
9
+
10
+ // packages/bundle-default-lifecycle/src/control-plane/task-data.ts
11
+ import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
12
+ import { defineCapability } from "@rig/core/capability";
13
+ import { requireInstalledCapability } from "@rig/core/capability-loaders";
14
+ var TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
15
+ function taskData() {
16
+ return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running the lifecycle.");
17
+ }
18
+
19
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
20
+ import { nowIso, runCapture as baseRunCapture } from "@rig/core/exec";
21
+ import { resolveCheckoutRoot as resolveMonorepoRoot } from "@rig/core/checkout-root";
22
+ import { getScopeRules } from "@rig/core/scope-rules";
23
+
24
+ // packages/bundle-default-lifecycle/src/native/github-auth-env.ts
25
+ import { existsSync, readFileSync } from "fs";
26
+ function cleanToken(value) {
27
+ const trimmed = value?.trim() ?? "";
28
+ return trimmed.length > 0 ? trimmed : null;
29
+ }
30
+ function authStateToken(env = process.env) {
31
+ const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
32
+ if (!file || !existsSync(file))
33
+ return null;
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
36
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
43
+ import { safePathSegment } from "@rig/core/safe-identifiers";
44
+
45
+ // packages/bundle-default-lifecycle/src/native/host-git.ts
46
+ import { existsSync as existsSync2 } from "fs";
47
+ import { resolve } from "path";
48
+ function isRuntimeGatewayGitPath(candidate) {
49
+ return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
50
+ }
51
+ function isRuntimeGatewayGhPath(candidate) {
52
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
53
+ }
54
+ function resolveHostGitBinary() {
55
+ const candidates = [
56
+ process.env.RIG_GIT_BIN?.trim() || "",
57
+ "/usr/bin/git",
58
+ "/opt/homebrew/bin/git",
59
+ "/usr/local/bin/git"
60
+ ];
61
+ const bunResolved = Bun.which("git");
62
+ if (bunResolved && !isRuntimeGatewayGitPath(bunResolved)) {
63
+ candidates.push(bunResolved);
64
+ }
65
+ for (const candidate of candidates) {
66
+ if (!candidate || isRuntimeGatewayGitPath(candidate)) {
67
+ continue;
68
+ }
69
+ if (existsSync2(candidate)) {
70
+ return candidate;
71
+ }
72
+ }
73
+ return "git";
74
+ }
75
+ function resolveGithubCliBinary(options = {}) {
76
+ const candidates = new Set;
77
+ const explicit = process.env.RIG_GH_BIN?.trim();
78
+ if (explicit) {
79
+ candidates.add(explicit);
80
+ }
81
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
82
+ candidates.add(candidate);
83
+ }
84
+ if (options.scanPath) {
85
+ for (const entry of (process.env.PATH || "").split(":").map((e) => e.trim()).filter(Boolean)) {
86
+ candidates.add(resolve(entry, "gh"));
87
+ }
88
+ }
89
+ const bunResolved = Bun.which("gh");
90
+ if (bunResolved) {
91
+ candidates.add(bunResolved);
92
+ }
93
+ for (const candidate of candidates) {
94
+ if (candidate && existsSync2(candidate) && !isRuntimeGatewayGhPath(candidate)) {
95
+ return candidate;
96
+ }
97
+ }
98
+ return "";
99
+ }
100
+
101
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
102
+ var TASK_RUNTIME_STAGE_EXCLUDES = [
103
+ ".rig/bin/**",
104
+ ".rig/cache/**",
105
+ ".rig/home/**",
106
+ ".rig/logs/**",
107
+ ".rig/runtime/**",
108
+ ".rig/session/**",
109
+ ".rig/state/**",
110
+ ".rig/runtime-context.json"
111
+ ];
112
+ var GENERATED_STAGE_EXCLUDES = ["artifacts/*/runtime-snapshots/**"];
113
+ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
114
+ "changed-files.txt",
115
+ "contract-changes.md",
116
+ "decision-log.md",
117
+ "git-state.txt",
118
+ "next-actions.md",
119
+ "pr-state.json",
120
+ "task-result.json",
121
+ "validation-summary.json"
122
+ ]);
123
+ function resolveOptionalMonorepoRoot(projectRoot) {
124
+ const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
125
+ if (runtimeWorkspace && existsSync3(resolve2(runtimeWorkspace, ".git"))) {
126
+ return resolve2(runtimeWorkspace);
127
+ }
128
+ try {
129
+ return resolveMonorepoRoot(projectRoot);
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ function escapeRegExp(value) {
135
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
136
+ }
137
+ function safeCurrentTaskId(projectRoot) {
138
+ try {
139
+ const taskId = taskData().currentTaskId(projectRoot);
140
+ return /^bd-[a-z0-9-]+$/.test(taskId) ? taskId : "";
141
+ } catch {
142
+ return "";
143
+ }
144
+ }
145
+ function gitCmd(projectRoot, repoRoot, ...args) {
146
+ return [resolveHostGitBinary(), "-C", repoRoot, ...args];
147
+ }
148
+ function shouldScopeGitCommit(args, hasTaskContext) {
149
+ if (!hasTaskContext) {
150
+ return false;
151
+ }
152
+ return args.includes("--scoped");
153
+ }
154
+ function gitStatus(projectRoot, taskId) {
155
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
156
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
157
+ const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
158
+ console.log("=== Git Flow Status ===");
159
+ if (resolvedTask) {
160
+ console.log(`Task: ${resolvedTask}`);
161
+ console.log(`Expected monorepo branch: ${expected}`);
162
+ } else {
163
+ console.log("Task: (none active)");
164
+ }
165
+ console.log("");
166
+ printRepoStatus(projectRoot, "project-rig", projectRoot, "");
167
+ const monorepoPath = monorepoRoot || resolveMonorepoRoot(projectRoot);
168
+ if (monorepoPath !== projectRoot) {
169
+ printRepoStatus(projectRoot, "monorepo", monorepoPath, expected);
170
+ }
171
+ }
172
+ function gitChanged(projectRoot, taskId, scoped) {
173
+ if (scoped) {
174
+ const resolvedTask = taskId || taskData().currentTaskId(projectRoot);
175
+ if (!resolvedTask) {
176
+ throw new Error("No task specified and no active task in session. Use --task or omit --scoped.");
177
+ }
178
+ return taskData().changedFilesForTask(projectRoot, resolvedTask, true);
179
+ }
180
+ return taskData().changedFilesForTask(projectRoot, taskId || taskData().currentTaskId(projectRoot) || "", false);
181
+ }
182
+ function gitPreflight(projectRoot, taskId, strict) {
183
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
184
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
185
+ const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
186
+ console.log("=== Git Flow Preflight ===");
187
+ let issues = 0;
188
+ if (!existsSync3(resolve2(projectRoot, ".git"))) {
189
+ console.log(`ERROR: project root is not a git repo (${projectRoot})`);
190
+ issues += 1;
191
+ }
192
+ if (monorepoRoot && existsSync3(resolve2(monorepoRoot, ".git"))) {
193
+ const monoBranch = branchName(projectRoot, monorepoRoot);
194
+ if (expected && monoBranch !== expected) {
195
+ console.log(`WARN: monorepo branch is ${monoBranch}, expected ${expected} for task ${resolvedTask}`);
196
+ if (strict) {
197
+ issues += 1;
198
+ }
199
+ }
200
+ const monoChanges = changeCount(projectRoot, monorepoRoot);
201
+ if (monoChanges > 0 && !monoBranch.startsWith("rig/")) {
202
+ console.log(`WARN: monorepo has uncommitted changes on non-rig branch (${monoBranch})`);
203
+ issues += 1;
204
+ }
205
+ } else {
206
+ console.log(`WARN: monorepo repo unavailable.`);
207
+ }
208
+ const projectChanges = changeCount(projectRoot, projectRoot);
209
+ if (projectChanges > 0) {
210
+ console.log(`INFO: project-rig has ${projectChanges} changed file(s).`);
211
+ }
212
+ if (issues > 0) {
213
+ console.log(`Preflight: ${issues} issue(s) detected.`);
214
+ return false;
215
+ }
216
+ console.log("Preflight: OK");
217
+ return true;
218
+ }
219
+ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
220
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
221
+ if (!resolvedTask) {
222
+ throw new Error("No task specified and no active task in session.");
223
+ }
224
+ const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
225
+ const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
226
+ if (!existsSync3(resolve2(repoRoot, ".git"))) {
227
+ throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
228
+ }
229
+ const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
230
+ const branchTarget = `rig/${branchId}`;
231
+ const current = branchName(projectRoot, repoRoot);
232
+ if (current === branchTarget) {
233
+ console.log(`${repoLabel} branch: already on ${branchTarget}`);
234
+ return;
235
+ }
236
+ const hasBranch = runCapture(gitCmd(projectRoot, repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchTarget}`), projectRoot).exitCode === 0;
237
+ const cmd = hasBranch && current === "HEAD" ? gitCmd(projectRoot, repoRoot, "checkout", "-B", branchTarget) : hasBranch ? gitCmd(projectRoot, repoRoot, "checkout", branchTarget) : gitCmd(projectRoot, repoRoot, "checkout", "-b", branchTarget);
238
+ const checkout = runCapture(cmd, projectRoot);
239
+ if (checkout.exitCode !== 0) {
240
+ throw new Error(`Failed to sync ${repoLabel.toLowerCase()} branch: ${checkout.stderr || checkout.stdout}`);
241
+ }
242
+ const action = hasBranch && current === "HEAD" ? "reset" : hasBranch ? "checked out" : "created";
243
+ console.log(`${repoLabel} branch: ${action} ${branchTarget}`);
244
+ }
245
+ function gitCommit(options) {
246
+ const { projectRoot } = options;
247
+ const resolvedTask = options.taskId || safeCurrentTaskId(projectRoot);
248
+ const baseMessage = options.message || (resolvedTask ? `rig: ${resolvedTask}` : "rig: harness update");
249
+ const changedFilesManifest = resolvedTask && options.scoped === true ? refreshChangedFilesManifest(projectRoot, resolvedTask) : "";
250
+ const changedFiles = resolvedTask && options.scoped === true ? readChangedFilesManifest(projectRoot, resolvedTask) : resolvedTask ? taskData().changedFilesForTask(projectRoot, resolvedTask, false) : [];
251
+ if (options.target === "project" || options.target === "both") {
252
+ if (resolvedTask) {
253
+ gitSyncBranch(projectRoot, resolvedTask, "project");
254
+ }
255
+ const projectFiles = resolveScopedStageFilesForRepo(projectRoot, projectRoot, resolvedTask, changedFiles);
256
+ commitRepo(projectRoot, projectRoot, "project-rig", options.target === "both" ? `${baseMessage} [harness]` : baseMessage, options.allowEmpty, options.scoped === true, projectFiles, changedFilesManifest);
257
+ }
258
+ if (options.target === "monorepo" || options.target === "both") {
259
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot);
260
+ if (resolvedTask) {
261
+ gitSyncBranch(projectRoot, resolvedTask, "monorepo");
262
+ }
263
+ const monorepoFiles = resolveScopedStageFilesForRepo(projectRoot, monorepoRoot, resolvedTask, changedFiles);
264
+ commitRepo(projectRoot, monorepoRoot, "monorepo", options.target === "both" ? `${baseMessage} [monorepo]` : baseMessage, options.allowEmpty, options.scoped === true, monorepoFiles, changedFilesManifest);
265
+ }
266
+ }
267
+ function gitSnapshot(projectRoot, taskId, outputPath) {
268
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
269
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
270
+ const output = outputPath || (resolvedTask ? resolveArtifactSnapshot(projectRoot, resolvedTask) : resolve2(resolve2(projectRoot, ".rig", "state"), "git-state.txt"));
271
+ mkdirSync(dirname(output), { recursive: true });
272
+ const lines = ["# Git Snapshot", `timestamp: ${nowIso()}`];
273
+ if (resolvedTask) {
274
+ lines.push(`task: ${resolvedTask}`);
275
+ }
276
+ lines.push("");
277
+ lines.push(...snapshotRepo(projectRoot, "project-rig", projectRoot));
278
+ if (monorepoRoot && monorepoRoot !== projectRoot) {
279
+ lines.push(...snapshotRepo(projectRoot, "monorepo", monorepoRoot));
280
+ }
281
+ writeFileSync(output, `${lines.join(`
282
+ `)}
283
+ `, "utf-8");
284
+ return output;
285
+ }
286
+ function gitOpenPr(options) {
287
+ const gh = resolveGithubCliBinary({ scanPath: true });
288
+ if (!gh) {
289
+ throw new Error("gh CLI is required for open-pr. Install and authenticate with: gh auth login");
290
+ }
291
+ const taskId = options.taskId || safeCurrentTaskId(options.projectRoot);
292
+ const target = options.target || (taskId ? "monorepo" : "project");
293
+ let repoRoot = options.projectRoot;
294
+ let repoLabel = "project-rig";
295
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
296
+ if (target === "monorepo") {
297
+ repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot(options.projectRoot);
298
+ repoLabel = "monorepo";
299
+ if (taskId) {
300
+ gitSyncBranch(options.projectRoot, taskId, "monorepo");
301
+ }
302
+ } else if (taskId) {
303
+ gitSyncBranch(options.projectRoot, taskId, "project");
304
+ }
305
+ if (!existsSync3(resolve2(repoRoot, ".git"))) {
306
+ throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
307
+ }
308
+ const branch = branchName(options.projectRoot, repoRoot);
309
+ if (!branch || branch === "HEAD") {
310
+ throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
311
+ }
312
+ const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
313
+ const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
314
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
315
+ refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
316
+ let reviewer = (options.reviewer || "").trim();
317
+ let reviewerSource = reviewer ? "flag" : undefined;
318
+ if (!reviewer && taskId) {
319
+ reviewer = defaultReviewerForTask(options.projectRoot, taskId);
320
+ if (reviewer) {
321
+ reviewerSource = "task-config";
322
+ }
323
+ }
324
+ if (!reviewer) {
325
+ reviewer = inferReviewerFromChangedFiles(options.projectRoot, repoRoot, base, branch);
326
+ if (reviewer) {
327
+ reviewerSource = "changed-files";
328
+ }
329
+ }
330
+ if (!reviewer) {
331
+ reviewer = (process.env.RIG_PR_REVIEWER || "").trim();
332
+ if (reviewer) {
333
+ reviewerSource = "env";
334
+ }
335
+ }
336
+ let title = options.title || "";
337
+ if (!title) {
338
+ if (taskId) {
339
+ title = `rig: ${taskId}`;
340
+ } else {
341
+ title = `rig: update ${branch}`;
342
+ }
343
+ }
344
+ const body = options.body || [
345
+ "## Summary",
346
+ "- Automated task output prepared in isolated runtime.",
347
+ "",
348
+ "## Task",
349
+ `- beads: ${taskId || "n/a"}`,
350
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
351
+ "",
352
+ "## Review",
353
+ "- Completion verification will run validation, verifier review, and PR policy checks.",
354
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
355
+ ].join(`
356
+ `);
357
+ const preCheck = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
358
+ const preCheckEntry = preCheck.exitCode === 0 ? preCheck.stdout.trim() : "";
359
+ if (preCheckEntry && preCheckEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
360
+ const mergedPr = JSON.parse(preCheckEntry);
361
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
362
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
363
+ if (taskId)
364
+ writePrMetadata(options.projectRoot, taskId, result2);
365
+ return result2;
366
+ }
367
+ const pushArgs = gitCmd(options.projectRoot, repoRoot, "push", "-u", networkRemote, branch);
368
+ const fetchResult = runCapture(gitCmd(options.projectRoot, repoRoot, "fetch", networkRemote, branch), repoRoot);
369
+ if (fetchResult.exitCode === 0) {
370
+ const remoteAhead = runCapture(gitCmd(options.projectRoot, repoRoot, "log", "--oneline", `HEAD..${networkRemote}/${branch}`), repoRoot);
371
+ if (remoteAhead.exitCode === 0 && remoteAhead.stdout.trim()) {
372
+ console.log(`Remote branch has diverged \u2014 force pushing task-owned branch ${branch} with --force-with-lease...`);
373
+ pushArgs.splice(4, 0, "--force-with-lease");
374
+ }
375
+ }
376
+ runOrThrow(options.projectRoot, pushArgs, `Failed to push branch ${branch} in ${repoLabel}`);
377
+ const existing = runCapture(withGhRepo([gh, "pr", "list", "--state", "open", "--head", branch, "--json", "url", "--jq", ".[0].url"], repoNameWithOwner), repoRoot);
378
+ const existingUrl = existing.exitCode === 0 ? existing.stdout.trim() : "";
379
+ if (!existingUrl || existingUrl === "null") {
380
+ const merged = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
381
+ const mergedEntry = merged.exitCode === 0 ? merged.stdout.trim() : "";
382
+ if (mergedEntry && mergedEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
383
+ const mergedPr = JSON.parse(mergedEntry);
384
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
385
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
386
+ if (taskId)
387
+ writePrMetadata(options.projectRoot, taskId, result2);
388
+ return result2;
389
+ }
390
+ }
391
+ let prUrl = "";
392
+ if (existingUrl && existingUrl !== "null") {
393
+ prUrl = existingUrl;
394
+ } else {
395
+ const createArgs = [
396
+ gh,
397
+ "pr",
398
+ "create",
399
+ ...ghRepoArgs(repoNameWithOwner),
400
+ "--base",
401
+ base,
402
+ "--head",
403
+ branch,
404
+ "--title",
405
+ title,
406
+ "--body",
407
+ body
408
+ ];
409
+ if (options.draft) {
410
+ createArgs.push("--draft");
411
+ }
412
+ const created = runCapture(createArgs, repoRoot);
413
+ if (created.exitCode !== 0) {
414
+ throw new Error(`Failed to create PR in ${repoLabel}: ${created.stderr || created.stdout}`);
415
+ }
416
+ prUrl = created.stdout.trim();
417
+ }
418
+ if (!prUrl) {
419
+ throw new Error(`Failed to resolve PR URL for branch ${branch}.`);
420
+ }
421
+ assertPrHasNoGitConflicts(readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl), repoLabel, base);
422
+ if (reviewer) {
423
+ const edit = runCapture(withGhRepo([gh, "pr", "edit", prUrl, "--add-reviewer", reviewer], repoNameWithOwner), repoRoot);
424
+ if (edit.exitCode !== 0) {
425
+ throw new Error(`Failed to assign reviewer '${reviewer}': ${edit.stderr || edit.stdout}`);
426
+ }
427
+ }
428
+ const result = {
429
+ url: prUrl,
430
+ ...reviewer ? { reviewer } : {},
431
+ ...reviewerSource ? { reviewerSource } : {},
432
+ target,
433
+ repoLabel,
434
+ branch,
435
+ base
436
+ };
437
+ if (taskId) {
438
+ writePrMetadata(options.projectRoot, taskId, result);
439
+ }
440
+ return result;
441
+ }
442
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
443
+ const lines = [];
444
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
445
+ if (runId) {
446
+ lines.push(`- Run: ${runId}`);
447
+ }
448
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
449
+ if (closeout) {
450
+ lines.push(`- ${closeout}`);
451
+ }
452
+ return lines;
453
+ }
454
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
455
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
456
+ if (sourceIssueId) {
457
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
458
+ if (match?.[1] && match[2]) {
459
+ const sourceRepo = match[1];
460
+ const issueNumber = match[2];
461
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
462
+ }
463
+ }
464
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
465
+ }
466
+ function resolveTaskBranchRef(projectRoot, taskId) {
467
+ return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
468
+ }
469
+ function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
470
+ const view = runCapture(withGhRepo([
471
+ gh,
472
+ "pr",
473
+ "view",
474
+ prUrl,
475
+ "--json",
476
+ "state,isDraft,url,mergedAt,autoMergeRequest,mergeable,mergeStateStatus,reviewDecision,headRefOid,statusCheckRollup",
477
+ "--jq",
478
+ "."
479
+ ], repoNameWithOwner), repoRoot);
480
+ if (view.exitCode !== 0) {
481
+ throw new Error(`Failed to inspect PR ${prUrl}: ${view.stderr || view.stdout}`);
482
+ }
483
+ try {
484
+ const parsed = JSON.parse(view.stdout);
485
+ return {
486
+ state: parsed.state || "OPEN",
487
+ isDraft: parsed.isDraft === true,
488
+ url: typeof parsed.url === "string" ? parsed.url : prUrl,
489
+ mergedAt: typeof parsed.mergedAt === "string" ? parsed.mergedAt : null,
490
+ autoMergeRequest: parsed.autoMergeRequest ?? null,
491
+ mergeable: typeof parsed.mergeable === "string" ? parsed.mergeable : "",
492
+ mergeStateStatus: typeof parsed.mergeStateStatus === "string" ? parsed.mergeStateStatus : "",
493
+ reviewDecision: typeof parsed.reviewDecision === "string" ? parsed.reviewDecision : "",
494
+ headRefOid: typeof parsed.headRefOid === "string" ? parsed.headRefOid : null,
495
+ statusCheckRollup: Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
496
+ };
497
+ } catch {
498
+ return {
499
+ state: "OPEN",
500
+ isDraft: false,
501
+ url: prUrl,
502
+ mergedAt: null,
503
+ autoMergeRequest: null,
504
+ mergeable: "",
505
+ mergeStateStatus: "",
506
+ reviewDecision: "",
507
+ headRefOid: null,
508
+ statusCheckRollup: []
509
+ };
510
+ }
511
+ }
512
+ function hasSatisfiedStatusChecks(prState) {
513
+ if (prState.statusCheckRollup.length === 0) {
514
+ return false;
515
+ }
516
+ return prState.statusCheckRollup.every((entry) => {
517
+ if (entry.__typename === "CheckRun") {
518
+ const status = entry.status?.toUpperCase() || "";
519
+ const conclusion = entry.conclusion?.toUpperCase() || "";
520
+ return status === "COMPLETED" && (conclusion === "SUCCESS" || conclusion === "SKIPPED" || conclusion === "NEUTRAL");
521
+ }
522
+ if (entry.__typename === "StatusContext") {
523
+ return (entry.state?.toUpperCase() || "") === "SUCCESS";
524
+ }
525
+ return false;
526
+ });
527
+ }
528
+ function canAdminMergeApprovedPr(prState) {
529
+ return prState.state === "OPEN" && prState.autoMergeRequest !== null && prState.mergeable.toUpperCase() === "MERGEABLE" && prState.reviewDecision.toUpperCase() === "APPROVED" && hasSatisfiedStatusChecks(prState);
530
+ }
531
+ function gitMergePr(options) {
532
+ const gh = resolveGithubCliBinary({ scanPath: true });
533
+ if (!gh) {
534
+ throw new Error("gh CLI is required for merge-pr. Install and authenticate with: gh auth login");
535
+ }
536
+ const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
537
+ const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
538
+ if (!existsSync3(resolve2(repoRoot, ".git"))) {
539
+ throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
540
+ }
541
+ const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
542
+ const state = prState.state;
543
+ const isDraft = prState.isDraft;
544
+ assertPrHasNoGitConflicts(prState, options.pr.repoLabel, options.pr.base);
545
+ if (state === "MERGED") {
546
+ console.log(`PR already merged (${options.pr.repoLabel}): ${options.pr.url}`);
547
+ return { status: "already-merged", url: options.pr.url };
548
+ }
549
+ if (state !== "OPEN") {
550
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
551
+ }
552
+ if (isDraft) {
553
+ throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
554
+ }
555
+ const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
556
+ const method = options.method || "squash";
557
+ mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
558
+ mergeArgs.push("--match-head-commit", options.matchHeadCommit);
559
+ if (options.deleteBranch !== false) {
560
+ mergeArgs.push("--delete-branch");
561
+ }
562
+ const directMerge = runCapture(mergeArgs, repoRoot);
563
+ if (directMerge.exitCode === 0) {
564
+ console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
565
+ return { status: "merged", url: options.pr.url };
566
+ }
567
+ const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
568
+ if (canAdminMergeApprovedPr(postDirectState)) {
569
+ const adminMergeArgs = [...mergeArgs, "--admin"];
570
+ const adminMerge = runCapture(adminMergeArgs, repoRoot);
571
+ if (adminMerge.exitCode === 0) {
572
+ const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
573
+ if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
574
+ console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
575
+ return { status: "merged", url: options.pr.url };
576
+ }
577
+ throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
578
+ }
579
+ const adminMergeMessage = `${adminMerge.stderr}
580
+ ${adminMerge.stdout}`.trim();
581
+ if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
582
+ throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
583
+ }
584
+ }
585
+ const directMergeMessage = `${directMerge.stderr}
586
+ ${directMerge.stdout}`.trim();
587
+ throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
588
+ }
589
+ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
590
+ const mergeable = prState.mergeable.toUpperCase();
591
+ const mergeStateStatus = prState.mergeStateStatus.toUpperCase();
592
+ if (mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY") {
593
+ 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.`);
594
+ }
595
+ }
596
+ function writePrMetadata(projectRoot, taskId, result) {
597
+ const dir = taskData().artifactDirForId(projectRoot, taskId);
598
+ mkdirSync(dir, { recursive: true });
599
+ const path = resolve2(dir, "pr-state.json");
600
+ let prs = {};
601
+ if (existsSync3(path)) {
602
+ try {
603
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
604
+ if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
605
+ prs = parsed.prs;
606
+ }
607
+ } catch {
608
+ prs = {};
609
+ }
610
+ }
611
+ prs[result.target] = result;
612
+ const primary = prs.monorepo || prs.project;
613
+ const artifact = {
614
+ task_id: taskId,
615
+ prs,
616
+ ...primary || {},
617
+ updated_at: nowIso()
618
+ };
619
+ writeFileSync(path, `${JSON.stringify(artifact, null, 2)}
620
+ `, "utf-8");
621
+ }
622
+ function readPrMetadata(projectRoot, taskId) {
623
+ const path = resolve2(taskData().artifactDirForId(projectRoot, taskId), "pr-state.json");
624
+ if (!existsSync3(path)) {
625
+ return [];
626
+ }
627
+ try {
628
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
629
+ if (!parsed || typeof parsed !== "object") {
630
+ return [];
631
+ }
632
+ if (parsed.prs && typeof parsed.prs === "object") {
633
+ return Object.values(parsed.prs).filter(isGitOpenPrResult);
634
+ }
635
+ return isGitOpenPrResult(parsed) ? [parsed] : [];
636
+ } catch {
637
+ return [];
638
+ }
639
+ }
640
+ function resolveArtifactSnapshot(projectRoot, taskId) {
641
+ return resolve2(taskData().artifactDirForId(projectRoot, taskId), "git-state.txt");
642
+ }
643
+ function isGitOpenPrResult(value) {
644
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
645
+ return false;
646
+ }
647
+ const record = value;
648
+ return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
649
+ }
650
+ function resolveRepoRoot(projectRoot, target) {
651
+ return target === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
652
+ }
653
+ function ensureFullGitHistory(projectRoot, repoRoot, remoteName = "origin") {
654
+ const shallow = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--is-shallow-repository"), projectRoot);
655
+ if (shallow.exitCode !== 0 || shallow.stdout.trim() !== "true") {
656
+ return;
657
+ }
658
+ const unshallow = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--unshallow", "--tags", remoteName), projectRoot);
659
+ if (unshallow.exitCode === 0) {
660
+ return;
661
+ }
662
+ const output = `${unshallow.stderr}
663
+ ${unshallow.stdout}`.trim();
664
+ if (/--unshallow on a complete repository|does not make sense/i.test(output)) {
665
+ return;
666
+ }
667
+ throw new Error(`Failed to expand git history for ${repoRoot}: ${output}`);
668
+ }
669
+ function refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner = "") {
670
+ const remoteName = resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner || resolveRepoNameWithOwner(projectRoot, repoRoot));
671
+ const remoteUrl = runCapture(gitCmd(projectRoot, repoRoot, "remote", "get-url", remoteName), projectRoot);
672
+ if (remoteUrl.exitCode !== 0) {
673
+ return "";
674
+ }
675
+ ensureFullGitHistory(projectRoot, repoRoot, remoteName);
676
+ const fetch = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--prune", "--tags", remoteName, `+refs/heads/${baseRef}:refs/remotes/${remoteName}/${baseRef}`), projectRoot);
677
+ if (fetch.exitCode !== 0) {
678
+ throw new Error(`Failed to refresh ${remoteName}/${baseRef} at ${repoRoot}: ${fetch.stderr || fetch.stdout}`);
679
+ }
680
+ return remoteName;
681
+ }
682
+ function currentHeadMatchesMergedBase(projectRoot, repoRoot, baseRef, remoteName = "origin") {
683
+ const activeRemote = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef) || remoteName;
684
+ const remoteBase = `${activeRemote}/${baseRef}`;
685
+ const hasRemoteBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", remoteBase), repoRoot).exitCode === 0;
686
+ const targetRef = hasRemoteBase ? remoteBase : baseRef;
687
+ if (runCapture(gitCmd(projectRoot, repoRoot, "merge-base", "--is-ancestor", "HEAD", targetRef), repoRoot).exitCode === 0) {
688
+ return true;
689
+ }
690
+ return runCapture(gitCmd(projectRoot, repoRoot, "diff", "--quiet", "HEAD", targetRef, "--"), repoRoot).exitCode === 0;
691
+ }
692
+ function defaultReviewerForTask(projectRoot, taskId) {
693
+ if (!taskId) {
694
+ return "";
695
+ }
696
+ const entry = taskData().readTaskConfig(projectRoot)[taskId];
697
+ const reviewer = entry?.reviewer;
698
+ if (typeof reviewer === "string" && reviewer.trim()) {
699
+ return reviewer.trim();
700
+ }
701
+ const responsibleReviewer = entry?.responsible_reviewer;
702
+ if (typeof responsibleReviewer === "string" && responsibleReviewer.trim()) {
703
+ return responsibleReviewer.trim();
704
+ }
705
+ return "";
706
+ }
707
+ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
708
+ const explicit = normalizeGithubRepoNameWithOwner(process.env.GH_REPO || "");
709
+ if (explicit) {
710
+ return explicit;
711
+ }
712
+ const visited = new Set;
713
+ return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
714
+ }
715
+ function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
716
+ const normalizedGitRoot = resolve2(gitRoot);
717
+ if (visited.has(normalizedGitRoot)) {
718
+ return "";
719
+ }
720
+ visited.add(normalizedGitRoot);
721
+ const remotes = listGitRemotes(projectRoot, gitRoot, cwd);
722
+ for (const remote of remotes) {
723
+ const urls = listGitRemoteUrls(projectRoot, gitRoot, cwd, remote);
724
+ for (const url of urls) {
725
+ const direct = normalizeGithubRepoNameWithOwner(url);
726
+ if (direct) {
727
+ return direct;
728
+ }
729
+ const localGitRoot = resolveLocalGitRemoteRoot(url, gitRoot);
730
+ if (!localGitRoot) {
731
+ continue;
732
+ }
733
+ const viaMirror = resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, localGitRoot, cwd, visited);
734
+ if (viaMirror) {
735
+ return viaMirror;
736
+ }
737
+ }
738
+ }
739
+ return "";
740
+ }
741
+ function listGitRemotes(projectRoot, gitRoot, cwd) {
742
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote");
743
+ if (result.exitCode !== 0) {
744
+ return [];
745
+ }
746
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
747
+ }
748
+ function listGitRemoteUrls(projectRoot, gitRoot, cwd, remote) {
749
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote", "get-url", "--all", remote);
750
+ if (result.exitCode !== 0) {
751
+ return [];
752
+ }
753
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
754
+ }
755
+ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
756
+ const remotes = listGitRemotes(projectRoot, repoRoot, repoRoot);
757
+ if (remotes.length === 0) {
758
+ return "origin";
759
+ }
760
+ if (!repoNameWithOwner) {
761
+ return remotes.includes("origin") ? "origin" : remotes[0];
762
+ }
763
+ const expectedRepo = normalizeGithubRepoNameWithOwner(repoNameWithOwner).toLowerCase();
764
+ let directMatch = "";
765
+ for (const remote of remotes) {
766
+ const urls = listGitRemoteUrls(projectRoot, repoRoot, repoRoot, remote);
767
+ for (const url of urls) {
768
+ const direct = normalizeGithubRepoNameWithOwner(url);
769
+ if (direct && direct.toLowerCase() === expectedRepo) {
770
+ if (remote === "github") {
771
+ return remote;
772
+ }
773
+ if (!directMatch) {
774
+ directMatch = remote;
775
+ }
776
+ }
777
+ }
778
+ }
779
+ if (remotes.includes("github")) {
780
+ return "github";
781
+ }
782
+ if (directMatch) {
783
+ return directMatch;
784
+ }
785
+ return remotes.includes("origin") ? "origin" : remotes[0];
786
+ }
787
+ function gitQuery(projectRoot, gitRoot, cwd, ...args) {
788
+ const gitArgs = existsSync3(resolve2(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveHostGitBinary(), "--git-dir", gitRoot, ...args];
789
+ return runCapture(gitArgs, cwd, projectRoot);
790
+ }
791
+ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
792
+ const normalized = remoteUrl.trim();
793
+ if (!normalized) {
794
+ return "";
795
+ }
796
+ let candidate = normalized;
797
+ if (normalized.startsWith("file://")) {
798
+ try {
799
+ candidate = fileURLToPath(normalized);
800
+ } catch {
801
+ return "";
802
+ }
803
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
804
+ return "";
805
+ } else if (!isAbsolute(normalized)) {
806
+ candidate = resolve2(gitRoot, normalized);
807
+ }
808
+ return existsSync3(candidate) ? candidate : "";
809
+ }
810
+ function normalizeGithubRepoNameWithOwner(value) {
811
+ const normalized = value.trim();
812
+ if (!normalized) {
813
+ return "";
814
+ }
815
+ const scpMatch = normalized.match(/^(?:ssh:\/\/)?git@github\.com[:/](.+?)(?:\.git)?$/i);
816
+ if (scpMatch?.[1]) {
817
+ return scpMatch[1].replace(/^\/+|\/+$/g, "");
818
+ }
819
+ const httpMatch = normalized.match(/^https?:\/\/github\.com\/(.+?)(?:\.git)?(?:\/)?$/i);
820
+ if (httpMatch?.[1]) {
821
+ return httpMatch[1].replace(/^\/+|\/+$/g, "");
822
+ }
823
+ const bareMatch = normalized.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/);
824
+ return bareMatch ? bareMatch[0] : "";
825
+ }
826
+ function ghRepoArgs(repoNameWithOwner) {
827
+ return repoNameWithOwner ? ["-R", repoNameWithOwner] : [];
828
+ }
829
+ function withGhRepo(command, repoNameWithOwner) {
830
+ if (!repoNameWithOwner || command.length < 3) {
831
+ return command;
832
+ }
833
+ return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
834
+ }
835
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
836
+ const remote = remoteName || "origin";
837
+ const symbolic = runCapture(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
838
+ if (symbolic.exitCode === 0) {
839
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
840
+ if (ref && ref !== "HEAD") {
841
+ return ref;
842
+ }
843
+ }
844
+ const lsRemote = runCapture(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
845
+ if (lsRemote.exitCode === 0) {
846
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
847
+ if (match?.[1]) {
848
+ return match[1];
849
+ }
850
+ }
851
+ const gh = resolveGithubCliBinary({ scanPath: true });
852
+ if (gh && repoNameWithOwner) {
853
+ const api = runCapture(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
854
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
855
+ if (branch) {
856
+ return branch;
857
+ }
858
+ }
859
+ return fallback;
860
+ }
861
+ function inferProjectBase(projectRoot, fallback) {
862
+ const containing = runCapture(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
863
+ if (containing.exitCode !== 0) {
864
+ return fallback;
865
+ }
866
+ 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/"));
867
+ return candidates[0] || fallback;
868
+ }
869
+ function currentGithubLogin(repoRoot) {
870
+ const gh = resolveGithubCliBinary({ scanPath: true });
871
+ if (!gh) {
872
+ return "";
873
+ }
874
+ const result = runCapture([gh, "api", "user", "--jq", ".login"], repoRoot);
875
+ return result.exitCode === 0 ? result.stdout.trim() : "";
876
+ }
877
+ function collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
878
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
879
+ const remoteName = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner);
880
+ const hasRemoteBase = remoteName ? runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", `${remoteName}/${baseRef}`), projectRoot).exitCode === 0 : false;
881
+ const hasLocalBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", baseRef), projectRoot).exitCode === 0;
882
+ let changed = "";
883
+ if (hasRemoteBase) {
884
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${remoteName}/${baseRef}...${branchRef}`), projectRoot).stdout;
885
+ } else if (hasLocalBase) {
886
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${baseRef}...${branchRef}`), projectRoot).stdout;
887
+ } else {
888
+ const fallback = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `HEAD~1..${branchRef}`), projectRoot);
889
+ changed = fallback.exitCode === 0 ? fallback.stdout : runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only"), projectRoot).stdout;
890
+ }
891
+ return changed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 60);
892
+ }
893
+ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
894
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
895
+ if (!repoNameWithOwner) {
896
+ return "";
897
+ }
898
+ const actorLogin = currentGithubLogin(repoRoot);
899
+ const changedFiles = collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef);
900
+ if (changedFiles.length === 0) {
901
+ return "";
902
+ }
903
+ const counts = new Map;
904
+ for (const path of changedFiles) {
905
+ const result = runCapture([
906
+ resolveGithubCliBinary({ scanPath: true }) || "gh",
907
+ "api",
908
+ `repos/${repoNameWithOwner}/commits`,
909
+ "-f",
910
+ `path=${path}`,
911
+ "-f",
912
+ `sha=${baseRef}`,
913
+ "-f",
914
+ "per_page=1",
915
+ "--jq",
916
+ ".[0].author.login // empty"
917
+ ], repoRoot);
918
+ const author = result.exitCode === 0 ? result.stdout.trim() : "";
919
+ if (!author || author === actorLogin) {
920
+ continue;
921
+ }
922
+ counts.set(author, (counts.get(author) || 0) + 1);
923
+ }
924
+ let best = "";
925
+ let max = -1;
926
+ for (const [author, count] of counts.entries()) {
927
+ if (count > max || count === max && author < best) {
928
+ best = author;
929
+ max = count;
930
+ }
931
+ }
932
+ return best;
933
+ }
934
+ function snapshotRepo(projectRoot, label, repo) {
935
+ if (!existsSync3(resolve2(repo, ".git"))) {
936
+ return [`## ${label}`, `repo: ${repo}`, "status: unavailable", ""];
937
+ }
938
+ const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
939
+ const branch = branchName(projectRoot, repo);
940
+ const head = runCapture(gitCmd(projectRoot, repo, "rev-parse", "HEAD"), projectRoot).stdout.trim();
941
+ return [
942
+ `## ${label}`,
943
+ `repo: ${repo}`,
944
+ `branch: ${branch}`,
945
+ `head: ${head}`,
946
+ "status:",
947
+ status || "(clean)",
948
+ ""
949
+ ];
950
+ }
951
+ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
952
+ if (!existsSync3(resolve2(repo, ".git"))) {
953
+ console.log(`Skipping ${label}: repo not available (${repo})`);
954
+ return;
955
+ }
956
+ const scopedFiles = (files || []).filter(Boolean).filter((file) => !pathResolvesBeyondSymlink(repo, file));
957
+ const repoChanges = changeCount(projectRoot, repo);
958
+ if (scopedFiles.length === 0 && repoChanges === 0 && !allowEmpty) {
959
+ console.log(`Skipping ${label}: no changes to commit.`);
960
+ return;
961
+ }
962
+ if (scopedFiles.length > 0) {
963
+ const indexFiles = new Set(runCapture(gitCmd(projectRoot, repo, "ls-files"), projectRoot).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
964
+ const stageable = scopedFiles.filter((file) => indexFiles.has(file) || existsSync3(resolve2(repo, file)));
965
+ if (stageable.length === 0) {
966
+ console.log(`Skipping ${label}: collected change list matched no stageable paths in ${repo}.`);
967
+ return;
968
+ }
969
+ const pathspecFile = resolve2(tmpdir(), `rig-stage-${process.pid}-${Date.now()}.txt`);
970
+ writeFileSync(pathspecFile, `${stageable.join(`
971
+ `)}
972
+ `, "utf-8");
973
+ try {
974
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "add", `--pathspec-from-file=${pathspecFile}`), `Failed to stage changes for ${label}`);
975
+ } finally {
976
+ try {
977
+ unlinkSync(pathspecFile);
978
+ } catch {}
979
+ }
980
+ } else {
981
+ const addArgs = buildStageAddArgs(repo, scopedFiles, scoped);
982
+ if (addArgs) {
983
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, ...addArgs), `Failed to stage changes for ${label}`);
984
+ }
985
+ }
986
+ const stagedChanges = stagedChangeCount(projectRoot, repo);
987
+ if (stagedChanges === 0) {
988
+ if (allowEmpty) {
989
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", "--allow-empty", "-m", message), `Failed to commit ${label}`);
990
+ console.log(`Committed ${label}: ${message}`);
991
+ return;
992
+ }
993
+ if (scoped && repoChanges > 0) {
994
+ const manifestHint = changedFilesManifest ? ` Refresh ${changedFilesManifest} and retry, or use --all/--unscoped intentionally.` : "";
995
+ throw new Error(`Scoped commit for ${label} resolved no stageable files.${manifestHint}`);
996
+ }
997
+ console.log(`Skipping ${label}: no stageable changes to commit.`);
998
+ return;
999
+ }
1000
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", ...allowEmpty ? ["--allow-empty"] : [], "-m", message), `Failed to commit ${label}`);
1001
+ console.log(`Committed ${label}: ${message}`);
1002
+ }
1003
+ function readChangedFilesManifest(projectRoot, taskId) {
1004
+ const manifestPath = resolve2(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1005
+ if (!existsSync3(manifestPath)) {
1006
+ return [];
1007
+ }
1008
+ const files = readFileSync2(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath(line)).filter(Boolean);
1009
+ return [...new Set(files)];
1010
+ }
1011
+ function refreshChangedFilesManifest(projectRoot, taskId) {
1012
+ const manifestPath = resolve2(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1013
+ mkdirSync(dirname(manifestPath), { recursive: true });
1014
+ const changedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
1015
+ writeFileSync(manifestPath, `${changedFiles.join(`
1016
+ `)}
1017
+ `, "utf-8");
1018
+ return manifestPath;
1019
+ }
1020
+ function normalizeChangedFilePath(file) {
1021
+ return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1022
+ }
1023
+ function buildStageAddArgs(repoRoot, files, scoped) {
1024
+ if (files.length > 0) {
1025
+ return ["add", "--", ...files];
1026
+ }
1027
+ if (scoped) {
1028
+ return null;
1029
+ }
1030
+ return ["add", "-A", "--", ".", ...stageExcludePathspecs(repoRoot)];
1031
+ }
1032
+ function resolveScopedStageFilesForRepo(projectRoot, repoRoot, taskId, files) {
1033
+ const resolvedManifestFiles = resolveScopedFilesForRepo(projectRoot, repoRoot, files);
1034
+ if (resolvedManifestFiles.length > 0 || !taskId) {
1035
+ return resolvedManifestFiles;
1036
+ }
1037
+ return resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId);
1038
+ }
1039
+ function resolveScopedFilesForRepo(projectRoot, repoRoot, files) {
1040
+ const resolvedFiles = [];
1041
+ const seen = new Set;
1042
+ for (const file of files) {
1043
+ const candidate = resolveScopedRepoPath(repoRoot, file);
1044
+ if (!candidate || seen.has(candidate) || pathResolvesBeyondSymlink(repoRoot, candidate)) {
1045
+ continue;
1046
+ }
1047
+ if (!repoHasPathChange(projectRoot, repoRoot, candidate)) {
1048
+ continue;
1049
+ }
1050
+ seen.add(candidate);
1051
+ resolvedFiles.push(candidate);
1052
+ }
1053
+ return resolvedFiles;
1054
+ }
1055
+ function resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId) {
1056
+ const safeTaskId = safePathSegment(taskId, { fallback: "task", maxLength: 96 });
1057
+ const artifactPrefix = `artifacts/${safeTaskId}/`;
1058
+ const resolvedFiles = [];
1059
+ const seen = new Set;
1060
+ for (const file of collectRepoPendingFiles(projectRoot, repoRoot)) {
1061
+ if (!file.startsWith(artifactPrefix)) {
1062
+ continue;
1063
+ }
1064
+ const artifactRelativePath = file.slice(artifactPrefix.length);
1065
+ if (!TASK_ARTIFACT_STAGE_FALLBACK.has(artifactRelativePath)) {
1066
+ continue;
1067
+ }
1068
+ if (seen.has(file) || pathResolvesBeyondSymlink(repoRoot, file)) {
1069
+ continue;
1070
+ }
1071
+ seen.add(file);
1072
+ resolvedFiles.push(file);
1073
+ }
1074
+ return resolvedFiles.sort();
1075
+ }
1076
+ function collectRepoPendingFiles(projectRoot, repoRoot) {
1077
+ const files = new Set;
1078
+ for (const args of [
1079
+ ["diff", "--name-only"],
1080
+ ["diff", "--cached", "--name-only"],
1081
+ ["ls-files", "--others", "--exclude-standard"]
1082
+ ]) {
1083
+ const result = runCapture(gitCmd(projectRoot, repoRoot, ...args), projectRoot);
1084
+ if (result.exitCode !== 0) {
1085
+ continue;
1086
+ }
1087
+ for (const line of result.stdout.split(/\r?\n/)) {
1088
+ const normalized = normalizeChangedFilePath(line);
1089
+ if (!normalized) {
1090
+ continue;
1091
+ }
1092
+ files.add(normalized);
1093
+ }
1094
+ }
1095
+ return [...files].sort();
1096
+ }
1097
+ function resolveScopedRepoPath(repoRoot, file) {
1098
+ const normalized = normalizeChangedFilePath(file);
1099
+ if (!normalized) {
1100
+ return "";
1101
+ }
1102
+ const rules = getScopeRules();
1103
+ if (rules?.stripPrefixes) {
1104
+ let result = normalized;
1105
+ for (const prefix of rules.stripPrefixes) {
1106
+ if (result.startsWith(prefix)) {
1107
+ result = result.slice(prefix.length);
1108
+ }
1109
+ }
1110
+ return result;
1111
+ }
1112
+ return normalized;
1113
+ }
1114
+ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
1115
+ const result = runCapture(gitCmd(projectRoot, repoRoot, "status", "--short", "--", relativePath), projectRoot);
1116
+ return result.exitCode === 0 && result.stdout.trim().length > 0;
1117
+ }
1118
+ function stageExcludePathspecs(repoRoot) {
1119
+ const patterns = existsSync3(resolve2(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
1120
+ return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
1121
+ }
1122
+ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
1123
+ const parts = relativePath.split("/").filter(Boolean);
1124
+ if (parts.length <= 1) {
1125
+ return false;
1126
+ }
1127
+ let current = repoRoot;
1128
+ for (let index = 0;index < parts.length - 1; index += 1) {
1129
+ current = resolve2(current, parts[index]);
1130
+ try {
1131
+ if (lstatSync(current).isSymbolicLink()) {
1132
+ return true;
1133
+ }
1134
+ } catch {
1135
+ return false;
1136
+ }
1137
+ }
1138
+ return false;
1139
+ }
1140
+ function printRepoStatus(projectRoot, label, repo, expectedBranch) {
1141
+ if (!existsSync3(resolve2(repo, ".git"))) {
1142
+ console.log(`${label}: unavailable (${repo})`);
1143
+ return;
1144
+ }
1145
+ const branch = branchName(projectRoot, repo);
1146
+ const changes = changeCount(projectRoot, repo);
1147
+ console.log(`${label}:`);
1148
+ console.log(` branch: ${branch || "unknown"}`);
1149
+ console.log(` changed files: ${changes}`);
1150
+ if (expectedBranch && label !== "project-rig" && branch !== expectedBranch) {
1151
+ console.log(` warning: branch mismatch (expected ${expectedBranch})`);
1152
+ }
1153
+ }
1154
+ function resolveTaskBranchId(projectRoot, taskId) {
1155
+ if (/^bd-[a-z0-9-]+$/.test(taskId)) {
1156
+ return taskId;
1157
+ }
1158
+ const normalizedTaskId = taskData().lookupTask(projectRoot, taskId);
1159
+ if (normalizedTaskId) {
1160
+ return normalizedTaskId;
1161
+ }
1162
+ const currentTask = taskData().currentTaskId(projectRoot);
1163
+ if (currentTask && currentTask === taskId) {
1164
+ return currentTask;
1165
+ }
1166
+ const runtimeIdFromEnv = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
1167
+ if (runtimeIdFromEnv.startsWith("task-") && runtimeIdFromEnv.length > "task-".length) {
1168
+ return runtimeIdFromEnv.slice("task-".length);
1169
+ }
1170
+ try {
1171
+ const runtimeIdFromContext = loadRuntimeContextFromEnv()?.runtimeId || "";
1172
+ if (runtimeIdFromContext.startsWith("task-") && runtimeIdFromContext.length > "task-".length) {
1173
+ return runtimeIdFromContext.slice("task-".length);
1174
+ }
1175
+ } catch {}
1176
+ const artifactDir = taskData().artifactDirForId(projectRoot, taskId);
1177
+ if (existsSync3(artifactDir)) {
1178
+ return taskId;
1179
+ }
1180
+ throw new Error(`Unknown task id: ${taskId}`);
1181
+ }
1182
+ function branchName(projectRoot, repo) {
1183
+ return runCapture(gitCmd(projectRoot, repo, "rev-parse", "--abbrev-ref", "HEAD"), projectRoot).stdout.trim();
1184
+ }
1185
+ function changeCount(projectRoot, repo) {
1186
+ const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
1187
+ return status ? status.split(/\r?\n/).filter(Boolean).length : 0;
1188
+ }
1189
+ function stagedChangeCount(projectRoot, repo) {
1190
+ const staged = runCapture(gitCmd(projectRoot, repo, "diff", "--cached", "--name-only"), projectRoot).stdout.trim();
1191
+ return staged ? staged.split(/\r?\n/).filter(Boolean).length : 0;
1192
+ }
1193
+ function runOrThrow(projectRoot, command, errorPrefix) {
1194
+ const result = runCapture(command, projectRoot);
1195
+ if (result.exitCode !== 0) {
1196
+ throw new Error(`${errorPrefix}:
1197
+ ${result.stderr || result.stdout}`);
1198
+ }
1199
+ }
1200
+ function runCapture(command, cwd, projectRoot = cwd) {
1201
+ return baseRunCapture(command, cwd, runtimeGitEnv(projectRoot));
1202
+ }
1203
+ function runtimeGitEnv(projectRoot) {
1204
+ const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
1205
+ const runtimeHome = runtimeRoot ? resolve2(runtimeRoot, "home") : "";
1206
+ const runtimeTmp = runtimeRoot ? resolve2(runtimeRoot, "tmp") : "";
1207
+ const runtimeCache = runtimeRoot ? resolve2(runtimeRoot, "cache") : "";
1208
+ const runtimeKnownHosts = runtimeHome ? resolve2(runtimeHome, ".ssh", "known_hosts") : "";
1209
+ const runtimeKey = runtimeHome ? resolve2(runtimeHome, ".ssh", "rig-agent-key") : "";
1210
+ const env = {};
1211
+ if (ctx?.workspaceDir) {
1212
+ env.PROJECT_RIG_ROOT = projectRoot;
1213
+ env.RIG_TASK_WORKSPACE = ctx.workspaceDir;
1214
+ env.MONOREPO_ROOT = ctx.workspaceDir;
1215
+ env.MONOREPO_MAIN_ROOT = resolveMonorepoRoot(projectRoot);
1216
+ } else if (projectRoot) {
1217
+ env.PROJECT_RIG_ROOT = projectRoot;
1218
+ }
1219
+ if (runtimeRoot) {
1220
+ env.RIG_RUNTIME_HOME = runtimeRoot;
1221
+ }
1222
+ if (runtimeHome && existsSync3(runtimeHome)) {
1223
+ env.HOME = runtimeHome;
1224
+ env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
1225
+ }
1226
+ if (runtimeTmp && existsSync3(runtimeTmp)) {
1227
+ env.TMPDIR = runtimeTmp;
1228
+ }
1229
+ if (runtimeCache && existsSync3(runtimeCache)) {
1230
+ env.XDG_CACHE_HOME = runtimeCache;
1231
+ }
1232
+ const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
1233
+ for (const [key, value] of Object.entries(resolveRuntimeSecrets(process.env, workspaceSecrets))) {
1234
+ if (key === "GITHUB_SSH_KEY" || !value) {
1235
+ continue;
1236
+ }
1237
+ env[key] = value;
1238
+ }
1239
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
1240
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
1241
+ env.GITHUB_TOKEN = rigGithubToken;
1242
+ }
1243
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1244
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1245
+ }
1246
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1247
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1248
+ }
1249
+ if (!env.GREPTILE_GITHUB_TOKEN && env.GITHUB_TOKEN) {
1250
+ env.GREPTILE_GITHUB_TOKEN = env.GITHUB_TOKEN;
1251
+ }
1252
+ const persistedSecrets = loadPersistedRuntimeSecrets(runtimeRoot);
1253
+ for (const [key, value] of Object.entries(persistedSecrets)) {
1254
+ if (!value)
1255
+ continue;
1256
+ if (!env[key]) {
1257
+ env[key] = value;
1258
+ }
1259
+ }
1260
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1261
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1262
+ }
1263
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1264
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1265
+ }
1266
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
1267
+ if (gitHubToken) {
1268
+ env.RIG_GITHUB_TOKEN = gitHubToken;
1269
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
1270
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
1271
+ applyGitHubCredentialHelperEnv(env);
1272
+ }
1273
+ if (runtimeKnownHosts && existsSync3(runtimeKnownHosts)) {
1274
+ const sshParts = [
1275
+ "ssh",
1276
+ `-o UserKnownHostsFile="${runtimeKnownHosts}"`,
1277
+ "-o StrictHostKeyChecking=yes",
1278
+ "-F /dev/null"
1279
+ ];
1280
+ if (runtimeKey && existsSync3(runtimeKey)) {
1281
+ sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
1282
+ }
1283
+ env.GIT_SSH_COMMAND = sshParts.join(" ");
1284
+ } else if (process.env.GIT_SSH_COMMAND?.trim()) {
1285
+ env.GIT_SSH_COMMAND = process.env.GIT_SSH_COMMAND;
1286
+ }
1287
+ return Object.keys(env).length > 0 ? env : undefined;
1288
+ }
1289
+ function applyGitHubCredentialHelperEnv(env) {
1290
+ env.GIT_TERMINAL_PROMPT = "0";
1291
+ env.GIT_CONFIG_COUNT = "2";
1292
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
1293
+ env.GIT_CONFIG_VALUE_0 = "";
1294
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
1295
+ 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';
1296
+ }
1297
+ function loadPersistedRuntimeSecrets(runtimeRoot) {
1298
+ if (!runtimeRoot) {
1299
+ return {};
1300
+ }
1301
+ const path = resolve2(runtimeRoot, "runtime-secrets.json");
1302
+ if (!existsSync3(path)) {
1303
+ return {};
1304
+ }
1305
+ try {
1306
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
1307
+ const allowed = new Set(["GITHUB_TOKEN", "GH_TOKEN", "RIG_GITHUB_TOKEN"]);
1308
+ const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string" && allowed.has(entry[0]));
1309
+ return Object.fromEntries(entries);
1310
+ } catch {
1311
+ return {};
1312
+ }
1313
+ }
1314
+ function ensureRuntimeOpenSslConfig(runtimeHome) {
1315
+ const sslDir = resolve2(runtimeHome, ".ssl");
1316
+ const sslConfig = resolve2(sslDir, "openssl.cnf");
1317
+ if (!existsSync3(sslDir)) {
1318
+ mkdirSync(sslDir, { recursive: true });
1319
+ }
1320
+ if (!existsSync3(sslConfig)) {
1321
+ writeFileSync(sslConfig, `# Rig runtime OpenSSL config placeholder
1322
+ `);
1323
+ }
1324
+ return sslConfig;
1325
+ }
1326
+ function resolveRuntimeMetadata(projectRoot) {
1327
+ const contextFile = process.env.RIG_RUNTIME_CONTEXT_FILE?.trim();
1328
+ const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
1329
+ let ctx = loadRuntimeContextFromEnv();
1330
+ if (runtimeHome) {
1331
+ return {
1332
+ ctx,
1333
+ runtimeRoot: runtimeHome
1334
+ };
1335
+ }
1336
+ if (contextFile) {
1337
+ return {
1338
+ ctx,
1339
+ runtimeRoot: dirname(resolve2(contextFile))
1340
+ };
1341
+ }
1342
+ const inferredContextFile = findRuntimeContextFile(projectRoot);
1343
+ if (existsSync3(inferredContextFile)) {
1344
+ try {
1345
+ ctx = loadRuntimeContext(inferredContextFile);
1346
+ } catch {}
1347
+ return {
1348
+ ctx,
1349
+ runtimeRoot: dirname(inferredContextFile)
1350
+ };
1351
+ }
1352
+ return { ctx, runtimeRoot: "" };
1353
+ }
1354
+ function findRuntimeContextFile(startPath) {
1355
+ let current = resolve2(startPath);
1356
+ while (true) {
1357
+ const candidate = resolve2(current, "runtime-context.json");
1358
+ if (existsSync3(candidate)) {
1359
+ return candidate;
1360
+ }
1361
+ const parent = dirname(current);
1362
+ if (parent === current) {
1363
+ return "";
1364
+ }
1365
+ current = parent;
1366
+ }
1367
+ }
1368
+ var __testOnly = {
1369
+ buildStageAddArgs,
1370
+ refreshChangedFilesManifest,
1371
+ readChangedFilesManifest,
1372
+ resolveChangedTaskArtifactFiles,
1373
+ resolveScopedStageFilesForRepo,
1374
+ resolveScopedFilesForRepo,
1375
+ stageExcludePathspecs
1376
+ };
1377
+ export {
1378
+ shouldScopeGitCommit,
1379
+ resolveTaskBranchRef,
1380
+ readPrMetadata,
1381
+ gitSyncBranch,
1382
+ gitStatus,
1383
+ gitSnapshot,
1384
+ gitPreflight,
1385
+ gitOpenPr,
1386
+ gitMergePr,
1387
+ gitCommit,
1388
+ gitChanged,
1389
+ __testOnly
1390
+ };