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