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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -2,46 +2,1521 @@
2
2
  // @bun
3
3
 
4
4
  // packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
5
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
6
- import { resolve as resolve2 } from "path";
7
- import { safePathSegment } from "@rig/shared/safe-identifiers";
5
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6
+ import { resolve as resolve5 } from "path";
7
+ import { safePathSegment as safePathSegment2 } from "@rig/core/safe-identifiers";
8
8
  import {
9
- escapeRegExp,
9
+ escapeRegExp as escapeRegExp2,
10
10
  resolveBunCli,
11
11
  resolveBunCliInvocation,
12
12
  resolveTaskScopes,
13
13
  resolvePolicyContent
14
14
  } from "@rig/hook-kit";
15
- import { loadPolicy, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
16
- import { gitCommit, gitMergePr, gitOpenPr, readPrMetadata as readPrMetadata2, resolveTaskBranchRef } from "@rig/runtime/control-plane/native/git-ops";
17
- import { runStrictPrMergeGate } from "@rig/pr-review-plugin";
18
- import { strictMergeHeadShaFromGate } from "@rig/contracts";
19
15
 
20
- // packages/bundle-default-lifecycle/src/control-plane/verifier.ts
21
- import { existsSync, mkdirSync, writeFileSync } from "fs";
16
+ // packages/bundle-default-lifecycle/src/control-plane/policy.ts
17
+ import { existsSync, readFileSync, statSync } from "fs";
22
18
  import { resolve } from "path";
23
- import { resolveRuntimeSecrets } from "@rig/runtime/control-plane/runtime/baked-secrets";
24
- import { readPrMetadata } from "@rig/runtime/control-plane/native/git-ops";
25
- import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
26
- import { readConfiguredTaskSourceTask } from "@rig/runtime/control-plane/tasks/source-lifecycle";
27
- import { artifactDirForId, lookupTask, readTaskConfig } from "@rig/runtime/control-plane/native/task-state";
28
- import { nowIso, resolveHarnessPaths, runCapture } from "@rig/runtime/control-plane/native/utils";
29
19
  import {
30
- collectPrReviewEvidence,
31
- evaluateStrictPrMergeGate,
32
- parseGreptileScore,
33
- stripHtml
34
- } from "@rig/pr-review-plugin";
20
+ POLICY_VERSION
21
+ } from "@rig/contracts";
22
+ var DEFAULT_SCOPE = {
23
+ fail_closed: true,
24
+ harness_paths_exempt: true,
25
+ runtime_paths_exempt: true
26
+ };
27
+ var DEFAULT_SANDBOX = {
28
+ mode: "enforce",
29
+ network: true,
30
+ read_deny: [],
31
+ write_allow_from_runtime: true
32
+ };
33
+ var DEFAULT_ISOLATION = {
34
+ default_mode: "worktree",
35
+ repo_symlink_fallback: false,
36
+ strict_provisioning: true,
37
+ fail_closed_on_provision_error: true
38
+ };
39
+ var DEFAULT_COMPLETION = {
40
+ derive_checks_from_scope: true,
41
+ checks: [],
42
+ typescript_config_probe: ["tsconfig.json"],
43
+ eslint_config_probe: [".eslintrc.js", ".eslintrc.json", "eslint.config.js"]
44
+ };
45
+ var DEFAULT_RUNTIME_IMAGE = {
46
+ deps: {
47
+ monorepo_install: false,
48
+ hp_next_install: false
49
+ },
50
+ plugins_require_binaries: true
51
+ };
52
+ var DEFAULT_RUNTIME_SNAPSHOT = {
53
+ enabled: true
54
+ };
55
+ var policyCache = null;
56
+ var policyCachePath = null;
57
+ var seededPolicyConfig = null;
58
+ function defaultPolicy() {
59
+ return {
60
+ version: POLICY_VERSION,
61
+ mode: "enforce",
62
+ scope: { ...DEFAULT_SCOPE },
63
+ rules: [],
64
+ sandbox: { ...DEFAULT_SANDBOX },
65
+ isolation: { ...DEFAULT_ISOLATION },
66
+ completion: { ...DEFAULT_COMPLETION },
67
+ runtime_image: {
68
+ deps: { ...DEFAULT_RUNTIME_IMAGE.deps },
69
+ plugins_require_binaries: DEFAULT_RUNTIME_IMAGE.plugins_require_binaries
70
+ },
71
+ runtime_snapshot: { ...DEFAULT_RUNTIME_SNAPSHOT }
72
+ };
73
+ }
74
+ function seedPolicyFromContent(rawJson) {
75
+ try {
76
+ seededPolicyConfig = mergeWithDefaults(JSON.parse(rawJson));
77
+ } catch {
78
+ seededPolicyConfig = null;
79
+ }
80
+ }
81
+ function loadPolicy(projectRoot) {
82
+ if (seededPolicyConfig) {
83
+ return seededPolicyConfig;
84
+ }
85
+ const configPath = resolve(projectRoot, "rig/policy/policy.json");
86
+ if (!existsSync(configPath)) {
87
+ return defaultPolicy();
88
+ }
89
+ let mtimeMs;
90
+ try {
91
+ mtimeMs = statSync(configPath).mtimeMs;
92
+ } catch {
93
+ return defaultPolicy();
94
+ }
95
+ if (policyCache && policyCachePath === configPath && policyCache.mtimeMs === mtimeMs) {
96
+ return policyCache.config;
97
+ }
98
+ try {
99
+ const config = mergeWithDefaults(JSON.parse(readFileSync(configPath, "utf-8")));
100
+ policyCache = { mtimeMs, config };
101
+ policyCachePath = configPath;
102
+ return config;
103
+ } catch {
104
+ return defaultPolicy();
105
+ }
106
+ }
107
+ function mergeWithDefaults(parsed) {
108
+ const base = defaultPolicy();
109
+ if (typeof parsed.mode === "string" && isValidMode(parsed.mode)) {
110
+ base.mode = parsed.mode;
111
+ }
112
+ if (parsed.scope && typeof parsed.scope === "object" && !Array.isArray(parsed.scope)) {
113
+ const scope = parsed.scope;
114
+ if (typeof scope.fail_closed === "boolean")
115
+ base.scope.fail_closed = scope.fail_closed;
116
+ if (typeof scope.harness_paths_exempt === "boolean")
117
+ base.scope.harness_paths_exempt = scope.harness_paths_exempt;
118
+ if (typeof scope.runtime_paths_exempt === "boolean")
119
+ base.scope.runtime_paths_exempt = scope.runtime_paths_exempt;
120
+ }
121
+ if (Array.isArray(parsed.rules)) {
122
+ base.rules = precompilePolicyRuleRegexes(parsed.rules.filter(isValidRule));
123
+ }
124
+ if (Array.isArray(parsed.deny) && base.rules.length === 0) {
125
+ base.rules = precompilePolicyRuleRegexes(migrateLegacyDeny(parsed.deny));
126
+ }
127
+ if (parsed.sandbox && typeof parsed.sandbox === "object" && !Array.isArray(parsed.sandbox)) {
128
+ const sandbox = parsed.sandbox;
129
+ if (typeof sandbox.mode === "string" && isValidMode(sandbox.mode))
130
+ base.sandbox.mode = sandbox.mode;
131
+ if (typeof sandbox.network === "boolean")
132
+ base.sandbox.network = sandbox.network;
133
+ if (Array.isArray(sandbox.read_deny))
134
+ base.sandbox.read_deny = sandbox.read_deny.filter((value) => typeof value === "string");
135
+ if (typeof sandbox.write_allow_from_runtime === "boolean")
136
+ base.sandbox.write_allow_from_runtime = sandbox.write_allow_from_runtime;
137
+ }
138
+ if (parsed.isolation && typeof parsed.isolation === "object" && !Array.isArray(parsed.isolation)) {
139
+ const isolation = parsed.isolation;
140
+ if (isolation.default_mode === "worktree")
141
+ base.isolation.default_mode = isolation.default_mode;
142
+ if (typeof isolation.repo_symlink_fallback === "boolean")
143
+ base.isolation.repo_symlink_fallback = isolation.repo_symlink_fallback;
144
+ if (typeof isolation.strict_provisioning === "boolean")
145
+ base.isolation.strict_provisioning = isolation.strict_provisioning;
146
+ if (typeof isolation.fail_closed_on_provision_error === "boolean")
147
+ base.isolation.fail_closed_on_provision_error = isolation.fail_closed_on_provision_error;
148
+ }
149
+ if (parsed.completion && typeof parsed.completion === "object" && !Array.isArray(parsed.completion)) {
150
+ const completion = parsed.completion;
151
+ if (typeof completion.derive_checks_from_scope === "boolean")
152
+ base.completion.derive_checks_from_scope = completion.derive_checks_from_scope;
153
+ if (Array.isArray(completion.checks))
154
+ base.completion.checks = completion.checks.filter((value) => typeof value === "string");
155
+ if (Array.isArray(completion.typescript_config_probe))
156
+ base.completion.typescript_config_probe = completion.typescript_config_probe.filter((value) => typeof value === "string");
157
+ if (Array.isArray(completion.eslint_config_probe))
158
+ base.completion.eslint_config_probe = completion.eslint_config_probe.filter((value) => typeof value === "string");
159
+ }
160
+ if (parsed.runtime_image && typeof parsed.runtime_image === "object" && !Array.isArray(parsed.runtime_image)) {
161
+ const runtimeImage = parsed.runtime_image;
162
+ if (runtimeImage.deps && typeof runtimeImage.deps === "object" && !Array.isArray(runtimeImage.deps)) {
163
+ const deps = runtimeImage.deps;
164
+ if (typeof deps.monorepo_install === "boolean")
165
+ base.runtime_image.deps.monorepo_install = deps.monorepo_install;
166
+ if (typeof deps.hp_next_install === "boolean")
167
+ base.runtime_image.deps.hp_next_install = deps.hp_next_install;
168
+ }
169
+ if (typeof runtimeImage.plugins_require_binaries === "boolean") {
170
+ base.runtime_image.plugins_require_binaries = runtimeImage.plugins_require_binaries;
171
+ }
172
+ }
173
+ if (parsed.runtime_snapshot && typeof parsed.runtime_snapshot === "object" && !Array.isArray(parsed.runtime_snapshot)) {
174
+ const runtimeSnapshot = parsed.runtime_snapshot;
175
+ if (typeof runtimeSnapshot.enabled === "boolean")
176
+ base.runtime_snapshot.enabled = runtimeSnapshot.enabled;
177
+ }
178
+ return base;
179
+ }
180
+ function isValidMode(value) {
181
+ return value === "off" || value === "observe" || value === "enforce";
182
+ }
183
+ function isValidRule(value) {
184
+ if (!value || typeof value !== "object" || Array.isArray(value))
185
+ return false;
186
+ const rule = value;
187
+ return typeof rule.id === "string" && typeof rule.category === "string" && !!rule.match && typeof rule.match === "object";
188
+ }
189
+ function migrateLegacyDeny(deny) {
190
+ const rules = [];
191
+ for (const entry of deny) {
192
+ if (typeof entry.id !== "string")
193
+ continue;
194
+ const match = {};
195
+ if (typeof entry.pattern === "string")
196
+ match.pattern = entry.pattern;
197
+ if (typeof entry.regex === "string")
198
+ match.regex = entry.regex;
199
+ if (!match.pattern && !match.regex)
200
+ continue;
201
+ const rule = {
202
+ id: entry.id,
203
+ category: "command",
204
+ match,
205
+ action: entry.action === "warn" ? "warn" : "block"
206
+ };
207
+ if (typeof entry.reason === "string") {
208
+ rule.description = entry.reason;
209
+ }
210
+ rules.push(rule);
211
+ }
212
+ return rules;
213
+ }
214
+ function precompilePolicyRuleRegexes(rules) {
215
+ return rules.map((rule) => {
216
+ const compiled = { ...rule };
217
+ const matchRegex = compileRegex(rule.match?.regex);
218
+ const unlessRegex = compileRegex(rule.unless?.regex);
219
+ if (matchRegex) {
220
+ compiled.compiledRegex = matchRegex;
221
+ }
222
+ if (unlessRegex) {
223
+ compiled.compiledUnlessRegex = unlessRegex;
224
+ }
225
+ return compiled;
226
+ });
227
+ }
228
+ function compileRegex(pattern) {
229
+ if (!pattern)
230
+ return;
231
+ try {
232
+ return new RegExp(pattern);
233
+ } catch {
234
+ return;
235
+ }
236
+ }
237
+
238
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
239
+ import { existsSync as existsSync4, lstatSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
240
+ import { tmpdir } from "os";
241
+ import { dirname, isAbsolute, resolve as resolve3 } from "path";
242
+ import { fileURLToPath } from "url";
243
+ import { loadDotEnvSecrets, resolveRuntimeSecrets } from "@rig/core/baked-secrets";
244
+ import { loadRuntimeContext, loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
245
+
246
+ // packages/bundle-default-lifecycle/src/control-plane/task-data.ts
247
+ import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
248
+ import { defineCapability } from "@rig/core/capability";
249
+ import { requireInstalledCapability } from "@rig/core/capability-loaders";
250
+ var TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
251
+ function taskData() {
252
+ return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running the lifecycle.");
253
+ }
254
+
255
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
256
+ import { nowIso, runCapture as baseRunCapture } from "@rig/core/exec";
257
+ import { resolveCheckoutRoot as resolveMonorepoRoot } from "@rig/core/checkout-root";
258
+ import { getScopeRules } from "@rig/core/scope-rules";
259
+
260
+ // packages/bundle-default-lifecycle/src/native/github-auth-env.ts
261
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
262
+ function cleanToken(value) {
263
+ const trimmed = value?.trim() ?? "";
264
+ return trimmed.length > 0 ? trimmed : null;
265
+ }
266
+ function authStateToken(env = process.env) {
267
+ const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
268
+ if (!file || !existsSync2(file))
269
+ return null;
270
+ try {
271
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
272
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
279
+ import { safePathSegment } from "@rig/core/safe-identifiers";
280
+
281
+ // packages/bundle-default-lifecycle/src/native/host-git.ts
282
+ import { existsSync as existsSync3 } from "fs";
283
+ import { resolve as resolve2 } from "path";
284
+ function isRuntimeGatewayGitPath(candidate) {
285
+ return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
286
+ }
287
+ function isRuntimeGatewayGhPath(candidate) {
288
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
289
+ }
290
+ function resolveHostGitBinary() {
291
+ const candidates = [
292
+ process.env.RIG_GIT_BIN?.trim() || "",
293
+ "/usr/bin/git",
294
+ "/opt/homebrew/bin/git",
295
+ "/usr/local/bin/git"
296
+ ];
297
+ const bunResolved = Bun.which("git");
298
+ if (bunResolved && !isRuntimeGatewayGitPath(bunResolved)) {
299
+ candidates.push(bunResolved);
300
+ }
301
+ for (const candidate of candidates) {
302
+ if (!candidate || isRuntimeGatewayGitPath(candidate)) {
303
+ continue;
304
+ }
305
+ if (existsSync3(candidate)) {
306
+ return candidate;
307
+ }
308
+ }
309
+ return "git";
310
+ }
311
+ function resolveGithubCliBinary(options = {}) {
312
+ const candidates = new Set;
313
+ const explicit = process.env.RIG_GH_BIN?.trim();
314
+ if (explicit) {
315
+ candidates.add(explicit);
316
+ }
317
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
318
+ candidates.add(candidate);
319
+ }
320
+ if (options.scanPath) {
321
+ for (const entry of (process.env.PATH || "").split(":").map((e) => e.trim()).filter(Boolean)) {
322
+ candidates.add(resolve2(entry, "gh"));
323
+ }
324
+ }
325
+ const bunResolved = Bun.which("gh");
326
+ if (bunResolved) {
327
+ candidates.add(bunResolved);
328
+ }
329
+ for (const candidate of candidates) {
330
+ if (candidate && existsSync3(candidate) && !isRuntimeGatewayGhPath(candidate)) {
331
+ return candidate;
332
+ }
333
+ }
334
+ return "";
335
+ }
336
+
337
+ // packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
338
+ var TASK_RUNTIME_STAGE_EXCLUDES = [
339
+ ".rig/bin/**",
340
+ ".rig/cache/**",
341
+ ".rig/home/**",
342
+ ".rig/logs/**",
343
+ ".rig/runtime/**",
344
+ ".rig/session/**",
345
+ ".rig/state/**",
346
+ ".rig/runtime-context.json"
347
+ ];
348
+ var GENERATED_STAGE_EXCLUDES = ["artifacts/*/runtime-snapshots/**"];
349
+ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
350
+ "changed-files.txt",
351
+ "contract-changes.md",
352
+ "decision-log.md",
353
+ "git-state.txt",
354
+ "next-actions.md",
355
+ "pr-state.json",
356
+ "task-result.json",
357
+ "validation-summary.json"
358
+ ]);
359
+ function resolveOptionalMonorepoRoot(projectRoot) {
360
+ const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
361
+ if (runtimeWorkspace && existsSync4(resolve3(runtimeWorkspace, ".git"))) {
362
+ return resolve3(runtimeWorkspace);
363
+ }
364
+ try {
365
+ return resolveMonorepoRoot(projectRoot);
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function escapeRegExp(value) {
371
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
372
+ }
373
+ function safeCurrentTaskId(projectRoot) {
374
+ try {
375
+ const taskId = taskData().currentTaskId(projectRoot);
376
+ return /^bd-[a-z0-9-]+$/.test(taskId) ? taskId : "";
377
+ } catch {
378
+ return "";
379
+ }
380
+ }
381
+ function gitCmd(projectRoot, repoRoot, ...args) {
382
+ return [resolveHostGitBinary(), "-C", repoRoot, ...args];
383
+ }
384
+ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
385
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
386
+ if (!resolvedTask) {
387
+ throw new Error("No task specified and no active task in session.");
388
+ }
389
+ const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
390
+ const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
391
+ if (!existsSync4(resolve3(repoRoot, ".git"))) {
392
+ throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
393
+ }
394
+ const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
395
+ const branchTarget = `rig/${branchId}`;
396
+ const current = branchName(projectRoot, repoRoot);
397
+ if (current === branchTarget) {
398
+ console.log(`${repoLabel} branch: already on ${branchTarget}`);
399
+ return;
400
+ }
401
+ const hasBranch = runCapture(gitCmd(projectRoot, repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchTarget}`), projectRoot).exitCode === 0;
402
+ const cmd = hasBranch && current === "HEAD" ? gitCmd(projectRoot, repoRoot, "checkout", "-B", branchTarget) : hasBranch ? gitCmd(projectRoot, repoRoot, "checkout", branchTarget) : gitCmd(projectRoot, repoRoot, "checkout", "-b", branchTarget);
403
+ const checkout = runCapture(cmd, projectRoot);
404
+ if (checkout.exitCode !== 0) {
405
+ throw new Error(`Failed to sync ${repoLabel.toLowerCase()} branch: ${checkout.stderr || checkout.stdout}`);
406
+ }
407
+ const action = hasBranch && current === "HEAD" ? "reset" : hasBranch ? "checked out" : "created";
408
+ console.log(`${repoLabel} branch: ${action} ${branchTarget}`);
409
+ }
410
+ function gitCommit(options) {
411
+ const { projectRoot } = options;
412
+ const resolvedTask = options.taskId || safeCurrentTaskId(projectRoot);
413
+ const baseMessage = options.message || (resolvedTask ? `rig: ${resolvedTask}` : "rig: harness update");
414
+ const changedFilesManifest = resolvedTask && options.scoped === true ? refreshChangedFilesManifest(projectRoot, resolvedTask) : "";
415
+ const changedFiles = resolvedTask && options.scoped === true ? readChangedFilesManifest(projectRoot, resolvedTask) : resolvedTask ? taskData().changedFilesForTask(projectRoot, resolvedTask, false) : [];
416
+ if (options.target === "project" || options.target === "both") {
417
+ if (resolvedTask) {
418
+ gitSyncBranch(projectRoot, resolvedTask, "project");
419
+ }
420
+ const projectFiles = resolveScopedStageFilesForRepo(projectRoot, projectRoot, resolvedTask, changedFiles);
421
+ commitRepo(projectRoot, projectRoot, "project-rig", options.target === "both" ? `${baseMessage} [harness]` : baseMessage, options.allowEmpty, options.scoped === true, projectFiles, changedFilesManifest);
422
+ }
423
+ if (options.target === "monorepo" || options.target === "both") {
424
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot);
425
+ if (resolvedTask) {
426
+ gitSyncBranch(projectRoot, resolvedTask, "monorepo");
427
+ }
428
+ const monorepoFiles = resolveScopedStageFilesForRepo(projectRoot, monorepoRoot, resolvedTask, changedFiles);
429
+ commitRepo(projectRoot, monorepoRoot, "monorepo", options.target === "both" ? `${baseMessage} [monorepo]` : baseMessage, options.allowEmpty, options.scoped === true, monorepoFiles, changedFilesManifest);
430
+ }
431
+ }
432
+ function gitOpenPr(options) {
433
+ const gh = resolveGithubCliBinary({ scanPath: true });
434
+ if (!gh) {
435
+ throw new Error("gh CLI is required for open-pr. Install and authenticate with: gh auth login");
436
+ }
437
+ const taskId = options.taskId || safeCurrentTaskId(options.projectRoot);
438
+ const target = options.target || (taskId ? "monorepo" : "project");
439
+ let repoRoot = options.projectRoot;
440
+ let repoLabel = "project-rig";
441
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
442
+ if (target === "monorepo") {
443
+ repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot(options.projectRoot);
444
+ repoLabel = "monorepo";
445
+ if (taskId) {
446
+ gitSyncBranch(options.projectRoot, taskId, "monorepo");
447
+ }
448
+ } else if (taskId) {
449
+ gitSyncBranch(options.projectRoot, taskId, "project");
450
+ }
451
+ if (!existsSync4(resolve3(repoRoot, ".git"))) {
452
+ throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
453
+ }
454
+ const branch = branchName(options.projectRoot, repoRoot);
455
+ if (!branch || branch === "HEAD") {
456
+ throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
457
+ }
458
+ const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
459
+ const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
460
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
461
+ refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
462
+ let reviewer = (options.reviewer || "").trim();
463
+ let reviewerSource = reviewer ? "flag" : undefined;
464
+ if (!reviewer && taskId) {
465
+ reviewer = defaultReviewerForTask(options.projectRoot, taskId);
466
+ if (reviewer) {
467
+ reviewerSource = "task-config";
468
+ }
469
+ }
470
+ if (!reviewer) {
471
+ reviewer = inferReviewerFromChangedFiles(options.projectRoot, repoRoot, base, branch);
472
+ if (reviewer) {
473
+ reviewerSource = "changed-files";
474
+ }
475
+ }
476
+ if (!reviewer) {
477
+ reviewer = (process.env.RIG_PR_REVIEWER || "").trim();
478
+ if (reviewer) {
479
+ reviewerSource = "env";
480
+ }
481
+ }
482
+ let title = options.title || "";
483
+ if (!title) {
484
+ if (taskId) {
485
+ title = `rig: ${taskId}`;
486
+ } else {
487
+ title = `rig: update ${branch}`;
488
+ }
489
+ }
490
+ const body = options.body || [
491
+ "## Summary",
492
+ "- Automated task output prepared in isolated runtime.",
493
+ "",
494
+ "## Task",
495
+ `- beads: ${taskId || "n/a"}`,
496
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
497
+ "",
498
+ "## Review",
499
+ "- Completion verification will run validation, verifier review, and PR policy checks.",
500
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
501
+ ].join(`
502
+ `);
503
+ const preCheck = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
504
+ const preCheckEntry = preCheck.exitCode === 0 ? preCheck.stdout.trim() : "";
505
+ if (preCheckEntry && preCheckEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
506
+ const mergedPr = JSON.parse(preCheckEntry);
507
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
508
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
509
+ if (taskId)
510
+ writePrMetadata(options.projectRoot, taskId, result2);
511
+ return result2;
512
+ }
513
+ const pushArgs = gitCmd(options.projectRoot, repoRoot, "push", "-u", networkRemote, branch);
514
+ const fetchResult = runCapture(gitCmd(options.projectRoot, repoRoot, "fetch", networkRemote, branch), repoRoot);
515
+ if (fetchResult.exitCode === 0) {
516
+ const remoteAhead = runCapture(gitCmd(options.projectRoot, repoRoot, "log", "--oneline", `HEAD..${networkRemote}/${branch}`), repoRoot);
517
+ if (remoteAhead.exitCode === 0 && remoteAhead.stdout.trim()) {
518
+ console.log(`Remote branch has diverged \u2014 force pushing task-owned branch ${branch} with --force-with-lease...`);
519
+ pushArgs.splice(4, 0, "--force-with-lease");
520
+ }
521
+ }
522
+ runOrThrow(options.projectRoot, pushArgs, `Failed to push branch ${branch} in ${repoLabel}`);
523
+ const existing = runCapture(withGhRepo([gh, "pr", "list", "--state", "open", "--head", branch, "--json", "url", "--jq", ".[0].url"], repoNameWithOwner), repoRoot);
524
+ const existingUrl = existing.exitCode === 0 ? existing.stdout.trim() : "";
525
+ if (!existingUrl || existingUrl === "null") {
526
+ const merged = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
527
+ const mergedEntry = merged.exitCode === 0 ? merged.stdout.trim() : "";
528
+ if (mergedEntry && mergedEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
529
+ const mergedPr = JSON.parse(mergedEntry);
530
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
531
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
532
+ if (taskId)
533
+ writePrMetadata(options.projectRoot, taskId, result2);
534
+ return result2;
535
+ }
536
+ }
537
+ let prUrl = "";
538
+ if (existingUrl && existingUrl !== "null") {
539
+ prUrl = existingUrl;
540
+ } else {
541
+ const createArgs = [
542
+ gh,
543
+ "pr",
544
+ "create",
545
+ ...ghRepoArgs(repoNameWithOwner),
546
+ "--base",
547
+ base,
548
+ "--head",
549
+ branch,
550
+ "--title",
551
+ title,
552
+ "--body",
553
+ body
554
+ ];
555
+ if (options.draft) {
556
+ createArgs.push("--draft");
557
+ }
558
+ const created = runCapture(createArgs, repoRoot);
559
+ if (created.exitCode !== 0) {
560
+ throw new Error(`Failed to create PR in ${repoLabel}: ${created.stderr || created.stdout}`);
561
+ }
562
+ prUrl = created.stdout.trim();
563
+ }
564
+ if (!prUrl) {
565
+ throw new Error(`Failed to resolve PR URL for branch ${branch}.`);
566
+ }
567
+ assertPrHasNoGitConflicts(readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl), repoLabel, base);
568
+ if (reviewer) {
569
+ const edit = runCapture(withGhRepo([gh, "pr", "edit", prUrl, "--add-reviewer", reviewer], repoNameWithOwner), repoRoot);
570
+ if (edit.exitCode !== 0) {
571
+ throw new Error(`Failed to assign reviewer '${reviewer}': ${edit.stderr || edit.stdout}`);
572
+ }
573
+ }
574
+ const result = {
575
+ url: prUrl,
576
+ ...reviewer ? { reviewer } : {},
577
+ ...reviewerSource ? { reviewerSource } : {},
578
+ target,
579
+ repoLabel,
580
+ branch,
581
+ base
582
+ };
583
+ if (taskId) {
584
+ writePrMetadata(options.projectRoot, taskId, result);
585
+ }
586
+ return result;
587
+ }
588
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
589
+ const lines = [];
590
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
591
+ if (runId) {
592
+ lines.push(`- Run: ${runId}`);
593
+ }
594
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
595
+ if (closeout) {
596
+ lines.push(`- ${closeout}`);
597
+ }
598
+ return lines;
599
+ }
600
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
601
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
602
+ if (sourceIssueId) {
603
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
604
+ if (match?.[1] && match[2]) {
605
+ const sourceRepo = match[1];
606
+ const issueNumber = match[2];
607
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
608
+ }
609
+ }
610
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
611
+ }
612
+ function resolveTaskBranchRef(projectRoot, taskId) {
613
+ return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
614
+ }
615
+ function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
616
+ const view = runCapture(withGhRepo([
617
+ gh,
618
+ "pr",
619
+ "view",
620
+ prUrl,
621
+ "--json",
622
+ "state,isDraft,url,mergedAt,autoMergeRequest,mergeable,mergeStateStatus,reviewDecision,headRefOid,statusCheckRollup",
623
+ "--jq",
624
+ "."
625
+ ], repoNameWithOwner), repoRoot);
626
+ if (view.exitCode !== 0) {
627
+ throw new Error(`Failed to inspect PR ${prUrl}: ${view.stderr || view.stdout}`);
628
+ }
629
+ try {
630
+ const parsed = JSON.parse(view.stdout);
631
+ return {
632
+ state: parsed.state || "OPEN",
633
+ isDraft: parsed.isDraft === true,
634
+ url: typeof parsed.url === "string" ? parsed.url : prUrl,
635
+ mergedAt: typeof parsed.mergedAt === "string" ? parsed.mergedAt : null,
636
+ autoMergeRequest: parsed.autoMergeRequest ?? null,
637
+ mergeable: typeof parsed.mergeable === "string" ? parsed.mergeable : "",
638
+ mergeStateStatus: typeof parsed.mergeStateStatus === "string" ? parsed.mergeStateStatus : "",
639
+ reviewDecision: typeof parsed.reviewDecision === "string" ? parsed.reviewDecision : "",
640
+ headRefOid: typeof parsed.headRefOid === "string" ? parsed.headRefOid : null,
641
+ statusCheckRollup: Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
642
+ };
643
+ } catch {
644
+ return {
645
+ state: "OPEN",
646
+ isDraft: false,
647
+ url: prUrl,
648
+ mergedAt: null,
649
+ autoMergeRequest: null,
650
+ mergeable: "",
651
+ mergeStateStatus: "",
652
+ reviewDecision: "",
653
+ headRefOid: null,
654
+ statusCheckRollup: []
655
+ };
656
+ }
657
+ }
658
+ function hasSatisfiedStatusChecks(prState) {
659
+ if (prState.statusCheckRollup.length === 0) {
660
+ return false;
661
+ }
662
+ return prState.statusCheckRollup.every((entry) => {
663
+ if (entry.__typename === "CheckRun") {
664
+ const status = entry.status?.toUpperCase() || "";
665
+ const conclusion = entry.conclusion?.toUpperCase() || "";
666
+ return status === "COMPLETED" && (conclusion === "SUCCESS" || conclusion === "SKIPPED" || conclusion === "NEUTRAL");
667
+ }
668
+ if (entry.__typename === "StatusContext") {
669
+ return (entry.state?.toUpperCase() || "") === "SUCCESS";
670
+ }
671
+ return false;
672
+ });
673
+ }
674
+ function canAdminMergeApprovedPr(prState) {
675
+ return prState.state === "OPEN" && prState.autoMergeRequest !== null && prState.mergeable.toUpperCase() === "MERGEABLE" && prState.reviewDecision.toUpperCase() === "APPROVED" && hasSatisfiedStatusChecks(prState);
676
+ }
677
+ function gitMergePr(options) {
678
+ const gh = resolveGithubCliBinary({ scanPath: true });
679
+ if (!gh) {
680
+ throw new Error("gh CLI is required for merge-pr. Install and authenticate with: gh auth login");
681
+ }
682
+ const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
683
+ const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
684
+ if (!existsSync4(resolve3(repoRoot, ".git"))) {
685
+ throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
686
+ }
687
+ const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
688
+ const state = prState.state;
689
+ const isDraft = prState.isDraft;
690
+ assertPrHasNoGitConflicts(prState, options.pr.repoLabel, options.pr.base);
691
+ if (state === "MERGED") {
692
+ console.log(`PR already merged (${options.pr.repoLabel}): ${options.pr.url}`);
693
+ return { status: "already-merged", url: options.pr.url };
694
+ }
695
+ if (state !== "OPEN") {
696
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
697
+ }
698
+ if (isDraft) {
699
+ throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
700
+ }
701
+ const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
702
+ const method = options.method || "squash";
703
+ mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
704
+ mergeArgs.push("--match-head-commit", options.matchHeadCommit);
705
+ if (options.deleteBranch !== false) {
706
+ mergeArgs.push("--delete-branch");
707
+ }
708
+ const directMerge = runCapture(mergeArgs, repoRoot);
709
+ if (directMerge.exitCode === 0) {
710
+ console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
711
+ return { status: "merged", url: options.pr.url };
712
+ }
713
+ const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
714
+ if (canAdminMergeApprovedPr(postDirectState)) {
715
+ const adminMergeArgs = [...mergeArgs, "--admin"];
716
+ const adminMerge = runCapture(adminMergeArgs, repoRoot);
717
+ if (adminMerge.exitCode === 0) {
718
+ const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
719
+ if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
720
+ console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
721
+ return { status: "merged", url: options.pr.url };
722
+ }
723
+ throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
724
+ }
725
+ const adminMergeMessage = `${adminMerge.stderr}
726
+ ${adminMerge.stdout}`.trim();
727
+ if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
728
+ throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
729
+ }
730
+ }
731
+ const directMergeMessage = `${directMerge.stderr}
732
+ ${directMerge.stdout}`.trim();
733
+ throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
734
+ }
735
+ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
736
+ const mergeable = prState.mergeable.toUpperCase();
737
+ const mergeStateStatus = prState.mergeStateStatus.toUpperCase();
738
+ if (mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY") {
739
+ 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.`);
740
+ }
741
+ }
742
+ function writePrMetadata(projectRoot, taskId, result) {
743
+ const dir = taskData().artifactDirForId(projectRoot, taskId);
744
+ mkdirSync(dir, { recursive: true });
745
+ const path = resolve3(dir, "pr-state.json");
746
+ let prs = {};
747
+ if (existsSync4(path)) {
748
+ try {
749
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
750
+ if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
751
+ prs = parsed.prs;
752
+ }
753
+ } catch {
754
+ prs = {};
755
+ }
756
+ }
757
+ prs[result.target] = result;
758
+ const primary = prs.monorepo || prs.project;
759
+ const artifact = {
760
+ task_id: taskId,
761
+ prs,
762
+ ...primary || {},
763
+ updated_at: nowIso()
764
+ };
765
+ writeFileSync(path, `${JSON.stringify(artifact, null, 2)}
766
+ `, "utf-8");
767
+ }
768
+ function readPrMetadata(projectRoot, taskId) {
769
+ const path = resolve3(taskData().artifactDirForId(projectRoot, taskId), "pr-state.json");
770
+ if (!existsSync4(path)) {
771
+ return [];
772
+ }
773
+ try {
774
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
775
+ if (!parsed || typeof parsed !== "object") {
776
+ return [];
777
+ }
778
+ if (parsed.prs && typeof parsed.prs === "object") {
779
+ return Object.values(parsed.prs).filter(isGitOpenPrResult);
780
+ }
781
+ return isGitOpenPrResult(parsed) ? [parsed] : [];
782
+ } catch {
783
+ return [];
784
+ }
785
+ }
786
+ function isGitOpenPrResult(value) {
787
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
788
+ return false;
789
+ }
790
+ const record = value;
791
+ return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
792
+ }
793
+ function resolveRepoRoot(projectRoot, target) {
794
+ return target === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
795
+ }
796
+ function ensureFullGitHistory(projectRoot, repoRoot, remoteName = "origin") {
797
+ const shallow = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--is-shallow-repository"), projectRoot);
798
+ if (shallow.exitCode !== 0 || shallow.stdout.trim() !== "true") {
799
+ return;
800
+ }
801
+ const unshallow = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--unshallow", "--tags", remoteName), projectRoot);
802
+ if (unshallow.exitCode === 0) {
803
+ return;
804
+ }
805
+ const output = `${unshallow.stderr}
806
+ ${unshallow.stdout}`.trim();
807
+ if (/--unshallow on a complete repository|does not make sense/i.test(output)) {
808
+ return;
809
+ }
810
+ throw new Error(`Failed to expand git history for ${repoRoot}: ${output}`);
811
+ }
812
+ function refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner = "") {
813
+ const remoteName = resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner || resolveRepoNameWithOwner(projectRoot, repoRoot));
814
+ const remoteUrl = runCapture(gitCmd(projectRoot, repoRoot, "remote", "get-url", remoteName), projectRoot);
815
+ if (remoteUrl.exitCode !== 0) {
816
+ return "";
817
+ }
818
+ ensureFullGitHistory(projectRoot, repoRoot, remoteName);
819
+ const fetch2 = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--prune", "--tags", remoteName, `+refs/heads/${baseRef}:refs/remotes/${remoteName}/${baseRef}`), projectRoot);
820
+ if (fetch2.exitCode !== 0) {
821
+ throw new Error(`Failed to refresh ${remoteName}/${baseRef} at ${repoRoot}: ${fetch2.stderr || fetch2.stdout}`);
822
+ }
823
+ return remoteName;
824
+ }
825
+ function currentHeadMatchesMergedBase(projectRoot, repoRoot, baseRef, remoteName = "origin") {
826
+ const activeRemote = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef) || remoteName;
827
+ const remoteBase = `${activeRemote}/${baseRef}`;
828
+ const hasRemoteBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", remoteBase), repoRoot).exitCode === 0;
829
+ const targetRef = hasRemoteBase ? remoteBase : baseRef;
830
+ if (runCapture(gitCmd(projectRoot, repoRoot, "merge-base", "--is-ancestor", "HEAD", targetRef), repoRoot).exitCode === 0) {
831
+ return true;
832
+ }
833
+ return runCapture(gitCmd(projectRoot, repoRoot, "diff", "--quiet", "HEAD", targetRef, "--"), repoRoot).exitCode === 0;
834
+ }
835
+ function defaultReviewerForTask(projectRoot, taskId) {
836
+ if (!taskId) {
837
+ return "";
838
+ }
839
+ const entry = taskData().readTaskConfig(projectRoot)[taskId];
840
+ const reviewer = entry?.reviewer;
841
+ if (typeof reviewer === "string" && reviewer.trim()) {
842
+ return reviewer.trim();
843
+ }
844
+ const responsibleReviewer = entry?.responsible_reviewer;
845
+ if (typeof responsibleReviewer === "string" && responsibleReviewer.trim()) {
846
+ return responsibleReviewer.trim();
847
+ }
848
+ return "";
849
+ }
850
+ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
851
+ const explicit = normalizeGithubRepoNameWithOwner(process.env.GH_REPO || "");
852
+ if (explicit) {
853
+ return explicit;
854
+ }
855
+ const visited = new Set;
856
+ return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
857
+ }
858
+ function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
859
+ const normalizedGitRoot = resolve3(gitRoot);
860
+ if (visited.has(normalizedGitRoot)) {
861
+ return "";
862
+ }
863
+ visited.add(normalizedGitRoot);
864
+ const remotes = listGitRemotes(projectRoot, gitRoot, cwd);
865
+ for (const remote of remotes) {
866
+ const urls = listGitRemoteUrls(projectRoot, gitRoot, cwd, remote);
867
+ for (const url of urls) {
868
+ const direct = normalizeGithubRepoNameWithOwner(url);
869
+ if (direct) {
870
+ return direct;
871
+ }
872
+ const localGitRoot = resolveLocalGitRemoteRoot(url, gitRoot);
873
+ if (!localGitRoot) {
874
+ continue;
875
+ }
876
+ const viaMirror = resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, localGitRoot, cwd, visited);
877
+ if (viaMirror) {
878
+ return viaMirror;
879
+ }
880
+ }
881
+ }
882
+ return "";
883
+ }
884
+ function listGitRemotes(projectRoot, gitRoot, cwd) {
885
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote");
886
+ if (result.exitCode !== 0) {
887
+ return [];
888
+ }
889
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
890
+ }
891
+ function listGitRemoteUrls(projectRoot, gitRoot, cwd, remote) {
892
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote", "get-url", "--all", remote);
893
+ if (result.exitCode !== 0) {
894
+ return [];
895
+ }
896
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
897
+ }
898
+ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
899
+ const remotes = listGitRemotes(projectRoot, repoRoot, repoRoot);
900
+ if (remotes.length === 0) {
901
+ return "origin";
902
+ }
903
+ if (!repoNameWithOwner) {
904
+ return remotes.includes("origin") ? "origin" : remotes[0];
905
+ }
906
+ const expectedRepo = normalizeGithubRepoNameWithOwner(repoNameWithOwner).toLowerCase();
907
+ let directMatch = "";
908
+ for (const remote of remotes) {
909
+ const urls = listGitRemoteUrls(projectRoot, repoRoot, repoRoot, remote);
910
+ for (const url of urls) {
911
+ const direct = normalizeGithubRepoNameWithOwner(url);
912
+ if (direct && direct.toLowerCase() === expectedRepo) {
913
+ if (remote === "github") {
914
+ return remote;
915
+ }
916
+ if (!directMatch) {
917
+ directMatch = remote;
918
+ }
919
+ }
920
+ }
921
+ }
922
+ if (remotes.includes("github")) {
923
+ return "github";
924
+ }
925
+ if (directMatch) {
926
+ return directMatch;
927
+ }
928
+ return remotes.includes("origin") ? "origin" : remotes[0];
929
+ }
930
+ function gitQuery(projectRoot, gitRoot, cwd, ...args) {
931
+ const gitArgs = existsSync4(resolve3(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveHostGitBinary(), "--git-dir", gitRoot, ...args];
932
+ return runCapture(gitArgs, cwd, projectRoot);
933
+ }
934
+ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
935
+ const normalized = remoteUrl.trim();
936
+ if (!normalized) {
937
+ return "";
938
+ }
939
+ let candidate = normalized;
940
+ if (normalized.startsWith("file://")) {
941
+ try {
942
+ candidate = fileURLToPath(normalized);
943
+ } catch {
944
+ return "";
945
+ }
946
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
947
+ return "";
948
+ } else if (!isAbsolute(normalized)) {
949
+ candidate = resolve3(gitRoot, normalized);
950
+ }
951
+ return existsSync4(candidate) ? candidate : "";
952
+ }
953
+ function normalizeGithubRepoNameWithOwner(value) {
954
+ const normalized = value.trim();
955
+ if (!normalized) {
956
+ return "";
957
+ }
958
+ const scpMatch = normalized.match(/^(?:ssh:\/\/)?git@github\.com[:/](.+?)(?:\.git)?$/i);
959
+ if (scpMatch?.[1]) {
960
+ return scpMatch[1].replace(/^\/+|\/+$/g, "");
961
+ }
962
+ const httpMatch = normalized.match(/^https?:\/\/github\.com\/(.+?)(?:\.git)?(?:\/)?$/i);
963
+ if (httpMatch?.[1]) {
964
+ return httpMatch[1].replace(/^\/+|\/+$/g, "");
965
+ }
966
+ const bareMatch = normalized.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/);
967
+ return bareMatch ? bareMatch[0] : "";
968
+ }
969
+ function ghRepoArgs(repoNameWithOwner) {
970
+ return repoNameWithOwner ? ["-R", repoNameWithOwner] : [];
971
+ }
972
+ function withGhRepo(command, repoNameWithOwner) {
973
+ if (!repoNameWithOwner || command.length < 3) {
974
+ return command;
975
+ }
976
+ return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
977
+ }
978
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
979
+ const remote = remoteName || "origin";
980
+ const symbolic = runCapture(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
981
+ if (symbolic.exitCode === 0) {
982
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
983
+ if (ref && ref !== "HEAD") {
984
+ return ref;
985
+ }
986
+ }
987
+ const lsRemote = runCapture(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
988
+ if (lsRemote.exitCode === 0) {
989
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
990
+ if (match?.[1]) {
991
+ return match[1];
992
+ }
993
+ }
994
+ const gh = resolveGithubCliBinary({ scanPath: true });
995
+ if (gh && repoNameWithOwner) {
996
+ const api = runCapture(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
997
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
998
+ if (branch) {
999
+ return branch;
1000
+ }
1001
+ }
1002
+ return fallback;
1003
+ }
1004
+ function inferProjectBase(projectRoot, fallback) {
1005
+ const containing = runCapture(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
1006
+ if (containing.exitCode !== 0) {
1007
+ return fallback;
1008
+ }
1009
+ 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/"));
1010
+ return candidates[0] || fallback;
1011
+ }
1012
+ function currentGithubLogin(repoRoot) {
1013
+ const gh = resolveGithubCliBinary({ scanPath: true });
1014
+ if (!gh) {
1015
+ return "";
1016
+ }
1017
+ const result = runCapture([gh, "api", "user", "--jq", ".login"], repoRoot);
1018
+ return result.exitCode === 0 ? result.stdout.trim() : "";
1019
+ }
1020
+ function collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
1021
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
1022
+ const remoteName = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner);
1023
+ const hasRemoteBase = remoteName ? runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", `${remoteName}/${baseRef}`), projectRoot).exitCode === 0 : false;
1024
+ const hasLocalBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", baseRef), projectRoot).exitCode === 0;
1025
+ let changed = "";
1026
+ if (hasRemoteBase) {
1027
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${remoteName}/${baseRef}...${branchRef}`), projectRoot).stdout;
1028
+ } else if (hasLocalBase) {
1029
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${baseRef}...${branchRef}`), projectRoot).stdout;
1030
+ } else {
1031
+ const fallback = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `HEAD~1..${branchRef}`), projectRoot);
1032
+ changed = fallback.exitCode === 0 ? fallback.stdout : runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only"), projectRoot).stdout;
1033
+ }
1034
+ return changed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 60);
1035
+ }
1036
+ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
1037
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
1038
+ if (!repoNameWithOwner) {
1039
+ return "";
1040
+ }
1041
+ const actorLogin = currentGithubLogin(repoRoot);
1042
+ const changedFiles = collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef);
1043
+ if (changedFiles.length === 0) {
1044
+ return "";
1045
+ }
1046
+ const counts = new Map;
1047
+ for (const path of changedFiles) {
1048
+ const result = runCapture([
1049
+ resolveGithubCliBinary({ scanPath: true }) || "gh",
1050
+ "api",
1051
+ `repos/${repoNameWithOwner}/commits`,
1052
+ "-f",
1053
+ `path=${path}`,
1054
+ "-f",
1055
+ `sha=${baseRef}`,
1056
+ "-f",
1057
+ "per_page=1",
1058
+ "--jq",
1059
+ ".[0].author.login // empty"
1060
+ ], repoRoot);
1061
+ const author = result.exitCode === 0 ? result.stdout.trim() : "";
1062
+ if (!author || author === actorLogin) {
1063
+ continue;
1064
+ }
1065
+ counts.set(author, (counts.get(author) || 0) + 1);
1066
+ }
1067
+ let best = "";
1068
+ let max = -1;
1069
+ for (const [author, count] of counts.entries()) {
1070
+ if (count > max || count === max && author < best) {
1071
+ best = author;
1072
+ max = count;
1073
+ }
1074
+ }
1075
+ return best;
1076
+ }
1077
+ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
1078
+ if (!existsSync4(resolve3(repo, ".git"))) {
1079
+ console.log(`Skipping ${label}: repo not available (${repo})`);
1080
+ return;
1081
+ }
1082
+ const scopedFiles = (files || []).filter(Boolean).filter((file) => !pathResolvesBeyondSymlink(repo, file));
1083
+ const repoChanges = changeCount(projectRoot, repo);
1084
+ if (scopedFiles.length === 0 && repoChanges === 0 && !allowEmpty) {
1085
+ console.log(`Skipping ${label}: no changes to commit.`);
1086
+ return;
1087
+ }
1088
+ if (scopedFiles.length > 0) {
1089
+ const indexFiles = new Set(runCapture(gitCmd(projectRoot, repo, "ls-files"), projectRoot).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
1090
+ const stageable = scopedFiles.filter((file) => indexFiles.has(file) || existsSync4(resolve3(repo, file)));
1091
+ if (stageable.length === 0) {
1092
+ console.log(`Skipping ${label}: collected change list matched no stageable paths in ${repo}.`);
1093
+ return;
1094
+ }
1095
+ const pathspecFile = resolve3(tmpdir(), `rig-stage-${process.pid}-${Date.now()}.txt`);
1096
+ writeFileSync(pathspecFile, `${stageable.join(`
1097
+ `)}
1098
+ `, "utf-8");
1099
+ try {
1100
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "add", `--pathspec-from-file=${pathspecFile}`), `Failed to stage changes for ${label}`);
1101
+ } finally {
1102
+ try {
1103
+ unlinkSync(pathspecFile);
1104
+ } catch {}
1105
+ }
1106
+ } else {
1107
+ const addArgs = buildStageAddArgs(repo, scopedFiles, scoped);
1108
+ if (addArgs) {
1109
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, ...addArgs), `Failed to stage changes for ${label}`);
1110
+ }
1111
+ }
1112
+ const stagedChanges = stagedChangeCount(projectRoot, repo);
1113
+ if (stagedChanges === 0) {
1114
+ if (allowEmpty) {
1115
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", "--allow-empty", "-m", message), `Failed to commit ${label}`);
1116
+ console.log(`Committed ${label}: ${message}`);
1117
+ return;
1118
+ }
1119
+ if (scoped && repoChanges > 0) {
1120
+ const manifestHint = changedFilesManifest ? ` Refresh ${changedFilesManifest} and retry, or use --all/--unscoped intentionally.` : "";
1121
+ throw new Error(`Scoped commit for ${label} resolved no stageable files.${manifestHint}`);
1122
+ }
1123
+ console.log(`Skipping ${label}: no stageable changes to commit.`);
1124
+ return;
1125
+ }
1126
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", ...allowEmpty ? ["--allow-empty"] : [], "-m", message), `Failed to commit ${label}`);
1127
+ console.log(`Committed ${label}: ${message}`);
1128
+ }
1129
+ function readChangedFilesManifest(projectRoot, taskId) {
1130
+ const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1131
+ if (!existsSync4(manifestPath)) {
1132
+ return [];
1133
+ }
1134
+ const files = readFileSync3(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath(line)).filter(Boolean);
1135
+ return [...new Set(files)];
1136
+ }
1137
+ function refreshChangedFilesManifest(projectRoot, taskId) {
1138
+ const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1139
+ mkdirSync(dirname(manifestPath), { recursive: true });
1140
+ const changedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
1141
+ writeFileSync(manifestPath, `${changedFiles.join(`
1142
+ `)}
1143
+ `, "utf-8");
1144
+ return manifestPath;
1145
+ }
1146
+ function normalizeChangedFilePath(file) {
1147
+ return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1148
+ }
1149
+ function buildStageAddArgs(repoRoot, files, scoped) {
1150
+ if (files.length > 0) {
1151
+ return ["add", "--", ...files];
1152
+ }
1153
+ if (scoped) {
1154
+ return null;
1155
+ }
1156
+ return ["add", "-A", "--", ".", ...stageExcludePathspecs(repoRoot)];
1157
+ }
1158
+ function resolveScopedStageFilesForRepo(projectRoot, repoRoot, taskId, files) {
1159
+ const resolvedManifestFiles = resolveScopedFilesForRepo(projectRoot, repoRoot, files);
1160
+ if (resolvedManifestFiles.length > 0 || !taskId) {
1161
+ return resolvedManifestFiles;
1162
+ }
1163
+ return resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId);
1164
+ }
1165
+ function resolveScopedFilesForRepo(projectRoot, repoRoot, files) {
1166
+ const resolvedFiles = [];
1167
+ const seen = new Set;
1168
+ for (const file of files) {
1169
+ const candidate = resolveScopedRepoPath(repoRoot, file);
1170
+ if (!candidate || seen.has(candidate) || pathResolvesBeyondSymlink(repoRoot, candidate)) {
1171
+ continue;
1172
+ }
1173
+ if (!repoHasPathChange(projectRoot, repoRoot, candidate)) {
1174
+ continue;
1175
+ }
1176
+ seen.add(candidate);
1177
+ resolvedFiles.push(candidate);
1178
+ }
1179
+ return resolvedFiles;
1180
+ }
1181
+ function resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId) {
1182
+ const safeTaskId = safePathSegment(taskId, { fallback: "task", maxLength: 96 });
1183
+ const artifactPrefix = `artifacts/${safeTaskId}/`;
1184
+ const resolvedFiles = [];
1185
+ const seen = new Set;
1186
+ for (const file of collectRepoPendingFiles(projectRoot, repoRoot)) {
1187
+ if (!file.startsWith(artifactPrefix)) {
1188
+ continue;
1189
+ }
1190
+ const artifactRelativePath = file.slice(artifactPrefix.length);
1191
+ if (!TASK_ARTIFACT_STAGE_FALLBACK.has(artifactRelativePath)) {
1192
+ continue;
1193
+ }
1194
+ if (seen.has(file) || pathResolvesBeyondSymlink(repoRoot, file)) {
1195
+ continue;
1196
+ }
1197
+ seen.add(file);
1198
+ resolvedFiles.push(file);
1199
+ }
1200
+ return resolvedFiles.sort();
1201
+ }
1202
+ function collectRepoPendingFiles(projectRoot, repoRoot) {
1203
+ const files = new Set;
1204
+ for (const args of [
1205
+ ["diff", "--name-only"],
1206
+ ["diff", "--cached", "--name-only"],
1207
+ ["ls-files", "--others", "--exclude-standard"]
1208
+ ]) {
1209
+ const result = runCapture(gitCmd(projectRoot, repoRoot, ...args), projectRoot);
1210
+ if (result.exitCode !== 0) {
1211
+ continue;
1212
+ }
1213
+ for (const line of result.stdout.split(/\r?\n/)) {
1214
+ const normalized = normalizeChangedFilePath(line);
1215
+ if (!normalized) {
1216
+ continue;
1217
+ }
1218
+ files.add(normalized);
1219
+ }
1220
+ }
1221
+ return [...files].sort();
1222
+ }
1223
+ function resolveScopedRepoPath(repoRoot, file) {
1224
+ const normalized = normalizeChangedFilePath(file);
1225
+ if (!normalized) {
1226
+ return "";
1227
+ }
1228
+ const rules = getScopeRules();
1229
+ if (rules?.stripPrefixes) {
1230
+ let result = normalized;
1231
+ for (const prefix of rules.stripPrefixes) {
1232
+ if (result.startsWith(prefix)) {
1233
+ result = result.slice(prefix.length);
1234
+ }
1235
+ }
1236
+ return result;
1237
+ }
1238
+ return normalized;
1239
+ }
1240
+ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
1241
+ const result = runCapture(gitCmd(projectRoot, repoRoot, "status", "--short", "--", relativePath), projectRoot);
1242
+ return result.exitCode === 0 && result.stdout.trim().length > 0;
1243
+ }
1244
+ function stageExcludePathspecs(repoRoot) {
1245
+ const patterns = existsSync4(resolve3(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
1246
+ return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
1247
+ }
1248
+ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
1249
+ const parts = relativePath.split("/").filter(Boolean);
1250
+ if (parts.length <= 1) {
1251
+ return false;
1252
+ }
1253
+ let current = repoRoot;
1254
+ for (let index = 0;index < parts.length - 1; index += 1) {
1255
+ current = resolve3(current, parts[index]);
1256
+ try {
1257
+ if (lstatSync(current).isSymbolicLink()) {
1258
+ return true;
1259
+ }
1260
+ } catch {
1261
+ return false;
1262
+ }
1263
+ }
1264
+ return false;
1265
+ }
1266
+ function resolveTaskBranchId(projectRoot, taskId) {
1267
+ if (/^bd-[a-z0-9-]+$/.test(taskId)) {
1268
+ return taskId;
1269
+ }
1270
+ const normalizedTaskId = taskData().lookupTask(projectRoot, taskId);
1271
+ if (normalizedTaskId) {
1272
+ return normalizedTaskId;
1273
+ }
1274
+ const currentTask = taskData().currentTaskId(projectRoot);
1275
+ if (currentTask && currentTask === taskId) {
1276
+ return currentTask;
1277
+ }
1278
+ const runtimeIdFromEnv = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
1279
+ if (runtimeIdFromEnv.startsWith("task-") && runtimeIdFromEnv.length > "task-".length) {
1280
+ return runtimeIdFromEnv.slice("task-".length);
1281
+ }
1282
+ try {
1283
+ const runtimeIdFromContext = loadRuntimeContextFromEnv()?.runtimeId || "";
1284
+ if (runtimeIdFromContext.startsWith("task-") && runtimeIdFromContext.length > "task-".length) {
1285
+ return runtimeIdFromContext.slice("task-".length);
1286
+ }
1287
+ } catch {}
1288
+ const artifactDir = taskData().artifactDirForId(projectRoot, taskId);
1289
+ if (existsSync4(artifactDir)) {
1290
+ return taskId;
1291
+ }
1292
+ throw new Error(`Unknown task id: ${taskId}`);
1293
+ }
1294
+ function branchName(projectRoot, repo) {
1295
+ return runCapture(gitCmd(projectRoot, repo, "rev-parse", "--abbrev-ref", "HEAD"), projectRoot).stdout.trim();
1296
+ }
1297
+ function changeCount(projectRoot, repo) {
1298
+ const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
1299
+ return status ? status.split(/\r?\n/).filter(Boolean).length : 0;
1300
+ }
1301
+ function stagedChangeCount(projectRoot, repo) {
1302
+ const staged = runCapture(gitCmd(projectRoot, repo, "diff", "--cached", "--name-only"), projectRoot).stdout.trim();
1303
+ return staged ? staged.split(/\r?\n/).filter(Boolean).length : 0;
1304
+ }
1305
+ function runOrThrow(projectRoot, command, errorPrefix) {
1306
+ const result = runCapture(command, projectRoot);
1307
+ if (result.exitCode !== 0) {
1308
+ throw new Error(`${errorPrefix}:
1309
+ ${result.stderr || result.stdout}`);
1310
+ }
1311
+ }
1312
+ function runCapture(command, cwd, projectRoot = cwd) {
1313
+ return baseRunCapture(command, cwd, runtimeGitEnv(projectRoot));
1314
+ }
1315
+ function runtimeGitEnv(projectRoot) {
1316
+ const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
1317
+ const runtimeHome = runtimeRoot ? resolve3(runtimeRoot, "home") : "";
1318
+ const runtimeTmp = runtimeRoot ? resolve3(runtimeRoot, "tmp") : "";
1319
+ const runtimeCache = runtimeRoot ? resolve3(runtimeRoot, "cache") : "";
1320
+ const runtimeKnownHosts = runtimeHome ? resolve3(runtimeHome, ".ssh", "known_hosts") : "";
1321
+ const runtimeKey = runtimeHome ? resolve3(runtimeHome, ".ssh", "rig-agent-key") : "";
1322
+ const env = {};
1323
+ if (ctx?.workspaceDir) {
1324
+ env.PROJECT_RIG_ROOT = projectRoot;
1325
+ env.RIG_TASK_WORKSPACE = ctx.workspaceDir;
1326
+ env.MONOREPO_ROOT = ctx.workspaceDir;
1327
+ env.MONOREPO_MAIN_ROOT = resolveMonorepoRoot(projectRoot);
1328
+ } else if (projectRoot) {
1329
+ env.PROJECT_RIG_ROOT = projectRoot;
1330
+ }
1331
+ if (runtimeRoot) {
1332
+ env.RIG_RUNTIME_HOME = runtimeRoot;
1333
+ }
1334
+ if (runtimeHome && existsSync4(runtimeHome)) {
1335
+ env.HOME = runtimeHome;
1336
+ env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
1337
+ }
1338
+ if (runtimeTmp && existsSync4(runtimeTmp)) {
1339
+ env.TMPDIR = runtimeTmp;
1340
+ }
1341
+ if (runtimeCache && existsSync4(runtimeCache)) {
1342
+ env.XDG_CACHE_HOME = runtimeCache;
1343
+ }
1344
+ const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
1345
+ for (const [key, value] of Object.entries(resolveRuntimeSecrets(process.env, workspaceSecrets))) {
1346
+ if (key === "GITHUB_SSH_KEY" || !value) {
1347
+ continue;
1348
+ }
1349
+ env[key] = value;
1350
+ }
1351
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
1352
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
1353
+ env.GITHUB_TOKEN = rigGithubToken;
1354
+ }
1355
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1356
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1357
+ }
1358
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1359
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1360
+ }
1361
+ if (!env.GREPTILE_GITHUB_TOKEN && env.GITHUB_TOKEN) {
1362
+ env.GREPTILE_GITHUB_TOKEN = env.GITHUB_TOKEN;
1363
+ }
1364
+ const persistedSecrets = loadPersistedRuntimeSecrets(runtimeRoot);
1365
+ for (const [key, value] of Object.entries(persistedSecrets)) {
1366
+ if (!value)
1367
+ continue;
1368
+ if (!env[key]) {
1369
+ env[key] = value;
1370
+ }
1371
+ }
1372
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1373
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1374
+ }
1375
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1376
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1377
+ }
1378
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
1379
+ if (gitHubToken) {
1380
+ env.RIG_GITHUB_TOKEN = gitHubToken;
1381
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
1382
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
1383
+ applyGitHubCredentialHelperEnv(env);
1384
+ }
1385
+ if (runtimeKnownHosts && existsSync4(runtimeKnownHosts)) {
1386
+ const sshParts = [
1387
+ "ssh",
1388
+ `-o UserKnownHostsFile="${runtimeKnownHosts}"`,
1389
+ "-o StrictHostKeyChecking=yes",
1390
+ "-F /dev/null"
1391
+ ];
1392
+ if (runtimeKey && existsSync4(runtimeKey)) {
1393
+ sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
1394
+ }
1395
+ env.GIT_SSH_COMMAND = sshParts.join(" ");
1396
+ } else if (process.env.GIT_SSH_COMMAND?.trim()) {
1397
+ env.GIT_SSH_COMMAND = process.env.GIT_SSH_COMMAND;
1398
+ }
1399
+ return Object.keys(env).length > 0 ? env : undefined;
1400
+ }
1401
+ function applyGitHubCredentialHelperEnv(env) {
1402
+ env.GIT_TERMINAL_PROMPT = "0";
1403
+ env.GIT_CONFIG_COUNT = "2";
1404
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
1405
+ env.GIT_CONFIG_VALUE_0 = "";
1406
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
1407
+ 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';
1408
+ }
1409
+ function loadPersistedRuntimeSecrets(runtimeRoot) {
1410
+ if (!runtimeRoot) {
1411
+ return {};
1412
+ }
1413
+ const path = resolve3(runtimeRoot, "runtime-secrets.json");
1414
+ if (!existsSync4(path)) {
1415
+ return {};
1416
+ }
1417
+ try {
1418
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
1419
+ const allowed = new Set(["GITHUB_TOKEN", "GH_TOKEN", "RIG_GITHUB_TOKEN"]);
1420
+ const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string" && allowed.has(entry[0]));
1421
+ return Object.fromEntries(entries);
1422
+ } catch {
1423
+ return {};
1424
+ }
1425
+ }
1426
+ function ensureRuntimeOpenSslConfig(runtimeHome) {
1427
+ const sslDir = resolve3(runtimeHome, ".ssl");
1428
+ const sslConfig = resolve3(sslDir, "openssl.cnf");
1429
+ if (!existsSync4(sslDir)) {
1430
+ mkdirSync(sslDir, { recursive: true });
1431
+ }
1432
+ if (!existsSync4(sslConfig)) {
1433
+ writeFileSync(sslConfig, `# Rig runtime OpenSSL config placeholder
1434
+ `);
1435
+ }
1436
+ return sslConfig;
1437
+ }
1438
+ function resolveRuntimeMetadata(projectRoot) {
1439
+ const contextFile = process.env.RIG_RUNTIME_CONTEXT_FILE?.trim();
1440
+ const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
1441
+ let ctx = loadRuntimeContextFromEnv();
1442
+ if (runtimeHome) {
1443
+ return {
1444
+ ctx,
1445
+ runtimeRoot: runtimeHome
1446
+ };
1447
+ }
1448
+ if (contextFile) {
1449
+ return {
1450
+ ctx,
1451
+ runtimeRoot: dirname(resolve3(contextFile))
1452
+ };
1453
+ }
1454
+ const inferredContextFile = findRuntimeContextFile(projectRoot);
1455
+ if (existsSync4(inferredContextFile)) {
1456
+ try {
1457
+ ctx = loadRuntimeContext(inferredContextFile);
1458
+ } catch {}
1459
+ return {
1460
+ ctx,
1461
+ runtimeRoot: dirname(inferredContextFile)
1462
+ };
1463
+ }
1464
+ return { ctx, runtimeRoot: "" };
1465
+ }
1466
+ function findRuntimeContextFile(startPath) {
1467
+ let current = resolve3(startPath);
1468
+ while (true) {
1469
+ const candidate = resolve3(current, "runtime-context.json");
1470
+ if (existsSync4(candidate)) {
1471
+ return candidate;
1472
+ }
1473
+ const parent = dirname(current);
1474
+ if (parent === current) {
1475
+ return "";
1476
+ }
1477
+ current = parent;
1478
+ }
1479
+ }
1480
+
1481
+ // packages/bundle-default-lifecycle/src/control-plane/pr-merge-gate-cap.ts
1482
+ import { PR_MERGE_GATE } from "@rig/contracts";
1483
+ import { defineCapability as defineCapability2 } from "@rig/core/capability";
1484
+ import { resolvePluginHost } from "@rig/core/project-plugins";
1485
+ var PrMergeGateCap = defineCapability2(PR_MERGE_GATE);
1486
+ async function resolvePrMergeGateService(projectRoot) {
1487
+ const { host } = await resolvePluginHost(projectRoot);
1488
+ return PrMergeGateCap.require(host);
1489
+ }
1490
+
1491
+ // packages/bundle-default-lifecycle/src/control-plane/verifier.ts
1492
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1493
+ import { resolve as resolve4 } from "path";
1494
+ import { resolveRuntimeSecrets as resolveRuntimeSecrets2 } from "@rig/core/baked-secrets";
1495
+ import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/core/runtime-context";
1496
+ import { nowIso as nowIso2, runCapture as runCapture2 } from "@rig/core/exec";
1497
+ import { resolveHarnessPaths } from "@rig/core/harness-paths";
1498
+ var mergeGateHolder = null;
1499
+ async function ensureMergeGate(projectRoot) {
1500
+ mergeGateHolder = await resolvePrMergeGateService(projectRoot);
1501
+ return mergeGateHolder;
1502
+ }
1503
+ function mg() {
1504
+ if (!mergeGateHolder) {
1505
+ throw new Error("PR merge-gate capability not resolved (verifyTask must run first).");
1506
+ }
1507
+ return mergeGateHolder;
1508
+ }
35
1509
  async function verifyTask(options) {
1510
+ await ensureMergeGate(options.projectRoot);
36
1511
  const paths = resolveHarnessPaths(options.projectRoot);
37
1512
  const taskId = options.taskId;
38
- const normalizedTaskId = lookupTask(options.projectRoot, taskId);
39
- const artifactDir = artifactDirForId(options.projectRoot, taskId);
40
- mkdirSync(artifactDir, { recursive: true });
41
- const validationSummaryPath = resolve(artifactDir, "validation-summary.json");
42
- const reviewFeedbackPath = resolve(artifactDir, "review-feedback.md");
43
- const reviewStatePath = resolve(artifactDir, "review-state.json");
44
- const greptileRawPath = resolve(artifactDir, "review-greptile-raw.json");
1513
+ const normalizedTaskId = taskData().lookupTask(options.projectRoot, taskId);
1514
+ const artifactDir = taskData().artifactDirForId(options.projectRoot, taskId);
1515
+ mkdirSync2(artifactDir, { recursive: true });
1516
+ const validationSummaryPath = resolve4(artifactDir, "validation-summary.json");
1517
+ const reviewFeedbackPath = resolve4(artifactDir, "review-feedback.md");
1518
+ const reviewStatePath = resolve4(artifactDir, "review-state.json");
1519
+ const greptileRawPath = resolve4(artifactDir, "review-greptile-raw.json");
45
1520
  const prStates = readPrMetadata(options.projectRoot, taskId);
46
1521
  const prState = prStates[0] || null;
47
1522
  const localReasons = [];
@@ -53,7 +1528,7 @@ async function verifyTask(options) {
53
1528
  if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
54
1529
  localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
55
1530
  }
56
- if (!existsSync(validationSummaryPath)) {
1531
+ if (!existsSync5(validationSummaryPath)) {
57
1532
  localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
58
1533
  } else {
59
1534
  const summary = await parseValidationSummary(validationSummaryPath);
@@ -62,13 +1537,13 @@ async function verifyTask(options) {
62
1537
  }
63
1538
  }
64
1539
  for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
65
- const requiredPath = resolve(artifactDir, file);
66
- if (!existsSync(requiredPath)) {
1540
+ const requiredPath = resolve4(artifactDir, file);
1541
+ if (!existsSync5(requiredPath)) {
67
1542
  localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
68
1543
  }
69
1544
  }
70
- const taskResultPath = resolve(artifactDir, "task-result.json");
71
- if (existsSync(taskResultPath)) {
1545
+ const taskResultPath = resolve4(artifactDir, "task-result.json");
1546
+ if (existsSync5(taskResultPath)) {
72
1547
  const taskResult = await readJsonFile(taskResultPath);
73
1548
  const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
74
1549
  if (artifactStatus === "partial") {
@@ -81,8 +1556,8 @@ async function verifyTask(options) {
81
1556
  localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
82
1557
  }
83
1558
  }
84
- const nextActionsPath = resolve(artifactDir, "next-actions.md");
85
- if (existsSync(nextActionsPath)) {
1559
+ const nextActionsPath = resolve4(artifactDir, "next-actions.md");
1560
+ if (existsSync5(nextActionsPath)) {
86
1561
  const nextActionsContent = await Bun.file(nextActionsPath).text();
87
1562
  if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
88
1563
  localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
@@ -113,7 +1588,7 @@ async function verifyTask(options) {
113
1588
  aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
114
1589
  }
115
1590
  if (persistArtifacts && ai.rawResponse) {
116
- writeFileSync(greptileRawPath, `${ai.rawResponse}
1591
+ writeFileSync2(greptileRawPath, `${ai.rawResponse}
117
1592
  `, "utf-8");
118
1593
  }
119
1594
  } else if (!options.skipAiReview && reviewMode === "off") {
@@ -235,15 +1710,15 @@ function nextActionsIndicateRemainingScope(content) {
235
1710
  return /^\s*- \[ \]/m.test(normalized) || /\b(remaining scope|still need|needs? to be implemented|not yet implemented|not implemented|follow[- ]?up required|blocked by|blocker:)\b/i.test(lower);
236
1711
  }
237
1712
  async function hasConfiguredSourceTask(projectRoot, taskId) {
238
- return readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
1713
+ return taskData().readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
239
1714
  }
240
1715
  function resolveGithubSourceIssueId(projectRoot, taskId) {
241
- const fromRuntime = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
1716
+ const fromRuntime = loadRuntimeContextFromEnv2()?.sourceTask?.sourceIssueId;
242
1717
  if (typeof fromRuntime === "string" && isGithubSourceIssueId(fromRuntime)) {
243
1718
  return fromRuntime;
244
1719
  }
245
1720
  try {
246
- const taskConfig = readTaskConfig(projectRoot);
1721
+ const taskConfig = taskData().readTaskConfig(projectRoot);
247
1722
  const entry = taskConfig[taskId];
248
1723
  const sourceIssueId = typeof entry?.sourceIssueId === "string" ? entry.sourceIssueId : typeof entry?.source_issue_id === "string" ? entry.source_issue_id : null;
249
1724
  if (sourceIssueId && isGithubSourceIssueId(sourceIssueId)) {
@@ -314,14 +1789,15 @@ function loadGithubPullRequestCloseoutSnapshot(projectRoot, prState) {
314
1789
  "--json",
315
1790
  "state,isDraft,mergeable,mergeStateStatus,reviewDecision,title,body,statusCheckRollup"
316
1791
  ]);
1792
+ const isDraft = booleanField(view, "isDraft");
317
1793
  return {
318
- state: stringField(view, "state"),
319
- isDraft: booleanField(view, "isDraft"),
320
- mergeable: stringField(view, "mergeable"),
321
- mergeStateStatus: stringField(view, "mergeStateStatus"),
322
- reviewDecision: stringField(view, "reviewDecision"),
323
- title: stringField(view, "title"),
324
- body: stringField(view, "body"),
1794
+ ...objectField("state", stringField(view, "state")),
1795
+ ...isDraft !== undefined ? { isDraft } : {},
1796
+ ...objectField("mergeable", stringField(view, "mergeable")),
1797
+ ...objectField("mergeStateStatus", stringField(view, "mergeStateStatus")),
1798
+ ...objectField("reviewDecision", stringField(view, "reviewDecision")),
1799
+ ...objectField("title", stringField(view, "title")),
1800
+ ...objectField("body", stringField(view, "body")),
325
1801
  statusCheckRollup: statusCheckRollupField(view, "statusCheckRollup"),
326
1802
  reviewThreads: loadGithubReviewThreads(projectRoot, repoName, prNumber)
327
1803
  };
@@ -396,6 +1872,9 @@ function stringField(record, key) {
396
1872
  const value = record[key];
397
1873
  return typeof value === "string" ? value : undefined;
398
1874
  }
1875
+ function objectField(key, value) {
1876
+ return value === undefined ? {} : { [key]: value };
1877
+ }
399
1878
  function booleanField(record, key) {
400
1879
  const value = record[key];
401
1880
  return typeof value === "boolean" ? value : undefined;
@@ -452,7 +1931,7 @@ function isAcceptedValidationSummary(summary) {
452
1931
  return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
453
1932
  }
454
1933
  async function loadReviewMode(reviewProfilePath, fallback) {
455
- const parsed = existsSync(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
1934
+ const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
456
1935
  const mode = parsed?.mode;
457
1936
  if (mode === "off" || mode === "advisory" || mode === "required") {
458
1937
  return mode;
@@ -463,7 +1942,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
463
1942
  return "advisory";
464
1943
  }
465
1944
  async function loadReviewProvider(reviewProfilePath, fallback) {
466
- const parsed = existsSync(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
1945
+ const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
467
1946
  const provider = parsed?.provider;
468
1947
  if (typeof provider === "string" && provider.trim().length > 0) {
469
1948
  return provider;
@@ -472,7 +1951,7 @@ async function loadReviewProvider(reviewProfilePath, fallback) {
472
1951
  }
473
1952
  function resolveRepoSlug(projectRoot) {
474
1953
  const paths = resolveHarnessPaths(projectRoot);
475
- const remote = runCapture(["git", "-C", paths.monorepoRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim() || runCapture(["git", "-C", projectRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim();
1954
+ const remote = runCapture2(["git", "-C", paths.monorepoRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim() || runCapture2(["git", "-C", projectRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim();
476
1955
  if (!remote) {
477
1956
  return "";
478
1957
  }
@@ -486,7 +1965,7 @@ function resolveRepoSlug(projectRoot) {
486
1965
  async function runGreptileReview(options) {
487
1966
  const reasons = [];
488
1967
  const warnings = [];
489
- const secrets = resolveRuntimeSecrets(process.env);
1968
+ const secrets = resolveRuntimeSecrets2(process.env);
490
1969
  const apiKey = secrets.GREPTILE_API_KEY || "";
491
1970
  const apiBase = secrets.GREPTILE_API_BASE || "https://api.greptile.com/mcp";
492
1971
  const remote = secrets.GREPTILE_REMOTE || "github";
@@ -622,7 +2101,7 @@ function writeFeedbackFile(options) {
622
2101
  if (options.aiRawFeedback) {
623
2102
  lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
624
2103
  }
625
- writeFileSync(options.output, `${lines.join(`
2104
+ writeFileSync2(options.output, `${lines.join(`
626
2105
  `)}
627
2106
  `, "utf-8");
628
2107
  }
@@ -637,9 +2116,9 @@ function writeReviewStateFile(options) {
637
2116
  local_reasons: options.localReasons,
638
2117
  ai_reasons: options.aiReasons,
639
2118
  ai_warnings: options.aiWarnings,
640
- updated_at: nowIso()
2119
+ updated_at: nowIso2()
641
2120
  };
642
- writeFileSync(options.output, `${JSON.stringify(payload, null, 2)}
2121
+ writeFileSync2(options.output, `${JSON.stringify(payload, null, 2)}
643
2122
  `, "utf-8");
644
2123
  }
645
2124
  async function runGreptileReviewForPr(options) {
@@ -664,7 +2143,6 @@ async function runGreptileReviewForPr(options) {
664
2143
  taskId: options.taskId,
665
2144
  prState: options.prState,
666
2145
  reviewMode: options.reviewMode,
667
- infrastructureError: undefined,
668
2146
  pollAttempts: options.pollAttempts,
669
2147
  pollIntervalMs: options.pollIntervalMs
670
2148
  });
@@ -792,7 +2270,7 @@ async function runGreptileReviewForPr(options) {
792
2270
  });
793
2271
  const actionableComments = filterActionableGreptileComments(commentsPayload.comments || []);
794
2272
  const reviewBody = reviewDetails.codeReview?.body || "";
795
- const score = parseGreptileScore(reviewBody);
2273
+ const score = mg().parseGreptileScore(reviewBody);
796
2274
  const feedback = [
797
2275
  `## ${options.prState.repoLabel || repoName} PR Review`,
798
2276
  "",
@@ -800,7 +2278,7 @@ async function runGreptileReviewForPr(options) {
800
2278
  `- Review ID: ${selectedReview.id}`,
801
2279
  `- Status: ${selectedReview.status}`,
802
2280
  "",
803
- reviewBody ? stripHtml(reviewBody).trim() : "Greptile completed without summary body."
2281
+ reviewBody ? mg().stripHtml(reviewBody).trim() : "Greptile completed without summary body."
804
2282
  ].filter(Boolean).join(`
805
2283
  `);
806
2284
  if (actionableComments.length > 0) {
@@ -821,7 +2299,7 @@ async function runGreptileReviewForPr(options) {
821
2299
  }
822
2300
  };
823
2301
  }
824
- const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
2302
+ const blockerScanBody = mg().stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
825
2303
  if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
826
2304
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
827
2305
  return {
@@ -869,7 +2347,7 @@ async function runGreptileReviewForPr(options) {
869
2347
  status: selectedReview.status
870
2348
  }]
871
2349
  });
872
- strictGate = evaluateStrictPrMergeGate(strictEvidence);
2350
+ strictGate = mg().evaluateGate(strictEvidence);
873
2351
  } catch (error) {
874
2352
  reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
875
2353
  return {
@@ -996,7 +2474,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
996
2474
  fallbackReview?.html_url ? `- Review: ${fallbackReview.html_url}` : "",
997
2475
  fallbackReview?.state ? `- Status: ${fallbackReview.state}` : "",
998
2476
  "",
999
- fallbackReview?.body?.trim() ? stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
2477
+ fallbackReview?.body?.trim() ? mg().stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
1000
2478
  ].filter(Boolean).join(`
1001
2479
  `);
1002
2480
  const warnings = buildGithubGreptileFallbackWarnings(options);
@@ -1019,7 +2497,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
1019
2497
  taskId: options.taskId,
1020
2498
  prUrl
1021
2499
  });
1022
- strictGate = evaluateStrictPrMergeGate(strictEvidence);
2500
+ strictGate = mg().evaluateGate(strictEvidence);
1023
2501
  } catch (error) {
1024
2502
  return {
1025
2503
  verdict: "REJECT",
@@ -1244,7 +2722,7 @@ function loadGithubPullRequestState(projectRoot, repoName, prNumber) {
1244
2722
  ]);
1245
2723
  return {
1246
2724
  state: response.state || "",
1247
- merged: response.merged,
2725
+ ...response.merged !== undefined ? { merged: response.merged } : {},
1248
2726
  merged_at: response.merged_at ?? null
1249
2727
  };
1250
2728
  }
@@ -1262,7 +2740,7 @@ function parsePullRequestNumber(url) {
1262
2740
  return match ? Number.parseInt(match[1] || "0", 10) : 0;
1263
2741
  }
1264
2742
  function runGhJson(projectRoot, args) {
1265
- const result = runCapture(["gh", ...args], projectRoot);
2743
+ const result = runCapture2(["gh", ...args], projectRoot);
1266
2744
  if (result.exitCode !== 0) {
1267
2745
  throw new Error(result.stderr || result.stdout || `gh ${args.join(" ")} failed`);
1268
2746
  }
@@ -1273,7 +2751,7 @@ function runGhJson(projectRoot, args) {
1273
2751
  }
1274
2752
  }
1275
2753
  async function collectStrictPrEvidenceForVerifier(input) {
1276
- return collectPrReviewEvidence({
2754
+ return mg().collectEvidence({
1277
2755
  projectRoot: input.projectRoot,
1278
2756
  prUrl: input.prUrl,
1279
2757
  taskId: input.taskId,
@@ -1281,7 +2759,7 @@ async function collectStrictPrEvidenceForVerifier(input) {
1281
2759
  cycle: 0,
1282
2760
  apiSignals: input.apiSignals ?? [],
1283
2761
  command: async (args, options) => {
1284
- const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
2762
+ const result = runCapture2(["gh", ...args], options?.cwd ?? input.projectRoot);
1285
2763
  return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
1286
2764
  }
1287
2765
  });
@@ -1294,11 +2772,11 @@ function deriveRepoName(projectRoot, prState) {
1294
2772
  if (prState.target === "monorepo") {
1295
2773
  return resolveRepoSlug(projectRoot);
1296
2774
  }
1297
- return runCapture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
2775
+ return runCapture2(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
1298
2776
  }
1299
2777
  function resolvePrHeadSha(projectRoot, prState) {
1300
2778
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
1301
- return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
2779
+ return runCapture2(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
1302
2780
  }
1303
2781
  function isGreptileGithubLogin(login) {
1304
2782
  const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
@@ -1438,7 +2916,7 @@ function filterActionableGithubGreptileThreads(threads) {
1438
2916
  }
1439
2917
  function resolvePrRepoRoot(projectRoot, prState) {
1440
2918
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
1441
- if (prState.target === "monorepo" && runtimeWorkspace && existsSync(resolve(runtimeWorkspace, ".git"))) {
2919
+ if (prState.target === "monorepo" && runtimeWorkspace && existsSync5(resolve4(runtimeWorkspace, ".git"))) {
1442
2920
  return runtimeWorkspace;
1443
2921
  }
1444
2922
  const paths = resolveHarnessPaths(projectRoot);
@@ -1449,10 +2927,10 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
1449
2927
  return false;
1450
2928
  }
1451
2929
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
1452
- return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
2930
+ return runCapture2(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
1453
2931
  }
1454
2932
  function summarizeComment(input) {
1455
- const text = stripHtml(input).replace(/\s+/g, " ").trim();
2933
+ const text = mg().stripHtml(input).replace(/\s+/g, " ").trim();
1456
2934
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
1457
2935
  }
1458
2936
  function asGreptileInfrastructureWarning(reason) {
@@ -1469,29 +2947,23 @@ function isAiReviewApproved(input) {
1469
2947
  }
1470
2948
 
1471
2949
  // packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
1472
- import { changedFilesForTask, pendingFilesForTask, taskArtifacts, taskValidate } from "@rig/runtime/control-plane/native/task-ops";
1473
- import { currentTaskId } from "@rig/runtime/control-plane/native/task-state";
1474
- import { resolveHarnessPaths as resolveHarnessPaths2, runCapture as runCapture2 } from "@rig/runtime/control-plane/native/utils";
1475
- import { readSourceAwareTaskStatus } from "@rig/runtime/control-plane/tasks/source-aware-task-config-source";
1476
- import {
1477
- buildTaskRunLifecycleComment,
1478
- updateConfiguredTaskSourceTask
1479
- } from "@rig/runtime/control-plane/tasks/source-lifecycle";
1480
- import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
2950
+ import { runCapture as runCapture3 } from "@rig/core/exec";
2951
+ import { resolveHarnessPaths as resolveHarnessPaths2 } from "@rig/core/harness-paths";
2952
+ import { buildPluginHostContext } from "@rig/core/plugin-host-context";
1481
2953
  async function closeCompletedTaskSource(projectRoot, taskId) {
1482
- const comment = buildTaskRunLifecycleComment({
2954
+ const comment = taskData().buildTaskRunLifecycleComment({
1483
2955
  runId: process.env.RIG_SERVER_RUN_ID || taskId,
1484
2956
  status: "closed",
1485
2957
  summary: "Rig completion verification approved and closed this task.",
1486
- runtimeWorkspace: process.env.RIG_TASK_WORKSPACE,
1487
- logsDir: process.env.RIG_LOGS_DIR,
1488
- sessionDir: process.env.RIG_SESSION_FILE
2958
+ ...process.env.RIG_TASK_WORKSPACE !== undefined ? { runtimeWorkspace: process.env.RIG_TASK_WORKSPACE } : {},
2959
+ ...process.env.RIG_LOGS_DIR !== undefined ? { logsDir: process.env.RIG_LOGS_DIR } : {},
2960
+ ...process.env.RIG_SESSION_FILE !== undefined ? { sessionDir: process.env.RIG_SESSION_FILE } : {}
1489
2961
  });
1490
- const result = await updateConfiguredTaskSourceTask(projectRoot, {
2962
+ const result = await taskData().updateConfiguredTaskSourceTask(projectRoot, {
1491
2963
  taskId,
1492
2964
  update: { status: "closed", comment }
1493
2965
  });
1494
- const status = result.status ?? await readSourceAwareTaskStatus(projectRoot, result.taskId);
2966
+ const status = result.status ?? await taskData().readSourceAwareTaskStatus(projectRoot, result.taskId);
1495
2967
  if (!result.updated && status == null) {
1496
2968
  return {
1497
2969
  ok: true,
@@ -1511,7 +2983,7 @@ function isClosedStatus(status) {
1511
2983
  }
1512
2984
  async function runCompletionVerificationGate(projectRoot) {
1513
2985
  seedPolicyFromContent(resolvePolicyContent(projectRoot));
1514
- const taskId = currentTaskId(projectRoot);
2986
+ const taskId = taskData().currentTaskId(projectRoot);
1515
2987
  if (!taskId) {
1516
2988
  return { ok: true };
1517
2989
  }
@@ -1520,7 +2992,7 @@ async function runCompletionVerificationGate(projectRoot) {
1520
2992
  let sourceCloseoutAllowed = false;
1521
2993
  console.log(`=== Completion Verification: ${taskId} ===`);
1522
2994
  const scopes = await resolveTaskScopes(projectRoot, taskId);
1523
- const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
2995
+ const taskChangedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
1524
2996
  const sourceInArtifacts = taskChangedFiles.filter((file) => /^artifacts\//.test(file) && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
1525
2997
  if (sourceInArtifacts.length > 0) {
1526
2998
  console.log(`
@@ -1539,7 +3011,7 @@ async function runCompletionVerificationGate(projectRoot) {
1539
3011
  });
1540
3012
  console.log(`
1541
3013
  [1/3] Task validation...`);
1542
- if (!await taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
3014
+ if (!await taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
1543
3015
  console.log(`FAIL: Validation failed for ${taskId}`);
1544
3016
  failed = true;
1545
3017
  } else {
@@ -1552,7 +3024,7 @@ async function runCompletionVerificationGate(projectRoot) {
1552
3024
  failed = true;
1553
3025
  }
1554
3026
  }
1555
- taskArtifacts(projectRoot, taskId);
3027
+ taskData().taskArtifacts(projectRoot, taskId);
1556
3028
  const policy = loadPolicy(projectRoot);
1557
3029
  const openPrEnabled = policy.completion.checks.includes("open-pr");
1558
3030
  const autoMergeEnabled = policy.completion.checks.includes("auto-merge");
@@ -1579,7 +3051,7 @@ async function runCompletionVerificationGate(projectRoot) {
1579
3051
  } else {
1580
3052
  console.log("Verifier preflight: skipped (earlier checks failed)");
1581
3053
  }
1582
- const pendingTaskChangedFiles = pendingFilesForTask(projectRoot, taskId, true);
3054
+ const pendingTaskChangedFiles = taskData().pendingFilesForTask(projectRoot, taskId, true);
1583
3055
  const hasLocalChanges = pendingTaskChangedFiles.length > 0;
1584
3056
  console.log(`
1585
3057
  [post] Auto-committing task changes...`);
@@ -1652,14 +3124,15 @@ async function runCompletionVerificationGate(projectRoot) {
1652
3124
  console.log(`
1653
3125
  [post] Auto-merge...`);
1654
3126
  try {
1655
- const prs = readPrMetadata2(projectRoot, taskId);
3127
+ const prs = readPrMetadata(projectRoot, taskId);
1656
3128
  if (prs.length === 0) {
1657
3129
  console.log("Auto-merge: skipped (no PR metadata found)");
1658
3130
  } else {
3131
+ const mergeGate = await resolvePrMergeGateService(projectRoot);
1659
3132
  let cycle = 0;
1660
3133
  for (const pr of prs) {
1661
3134
  cycle += 1;
1662
- const gate = await runStrictPrMergeGate({
3135
+ const gate = await mergeGate.runGate({
1663
3136
  projectRoot,
1664
3137
  prUrl: pr.url,
1665
3138
  taskId,
@@ -1667,7 +3140,7 @@ async function runCompletionVerificationGate(projectRoot) {
1667
3140
  cycle,
1668
3141
  final: true,
1669
3142
  command: async (args, options) => {
1670
- const result = runCapture2(["gh", ...args], options?.cwd ?? projectRoot);
3143
+ const result = runCapture3(["gh", ...args], options?.cwd ?? projectRoot);
1671
3144
  return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
1672
3145
  }
1673
3146
  });
@@ -1684,7 +3157,7 @@ async function runCompletionVerificationGate(projectRoot) {
1684
3157
  pr,
1685
3158
  method: "squash",
1686
3159
  deleteBranch: true,
1687
- matchHeadCommit: strictMergeHeadShaFromGate(gate, pr.url)
3160
+ matchHeadCommit: mergeGate.resolveHeadSha({ result: gate, prUrl: pr.url })
1688
3161
  });
1689
3162
  if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
1690
3163
  console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
@@ -1704,9 +3177,9 @@ async function runCompletionVerificationGate(projectRoot) {
1704
3177
  console.log(`
1705
3178
  [post] Auto-merge: skipped (not in policy completion.checks)`);
1706
3179
  }
1707
- const artifactDir = resolve2(paths.artifactsDir, safePathSegment(taskId, { fallback: "task", maxLength: 96 }));
1708
- mkdirSync2(artifactDir, { recursive: true });
1709
- writeFileSync2(resolve2(artifactDir, "review-status.txt"), failed ? `REJECTED
3180
+ const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
3181
+ mkdirSync3(artifactDir, { recursive: true });
3182
+ writeFileSync3(resolve5(artifactDir, "review-status.txt"), failed ? `REJECTED
1710
3183
  ` : `APPROVED
1711
3184
  `, "utf-8");
1712
3185
  if (!failed) {
@@ -1756,8 +3229,8 @@ async function runBunTool(args, cwd) {
1756
3229
  };
1757
3230
  }
1758
3231
  async function runProtoQualityGate(monorepoRoot) {
1759
- const protosDir = resolve2(monorepoRoot, "packages", "protos");
1760
- if (!existsSync2(protosDir)) {
3232
+ const protosDir = resolve5(monorepoRoot, "packages", "protos");
3233
+ if (!existsSync6(protosDir)) {
1761
3234
  console.log(`FAIL: Proto workspace not found at ${protosDir}`);
1762
3235
  return false;
1763
3236
  }
@@ -1784,7 +3257,7 @@ async function runProtoQualityGate(monorepoRoot) {
1784
3257
  console.log(generate.stderr || generate.stdout);
1785
3258
  ok = false;
1786
3259
  } else {
1787
- const drift = runCapture2(["git", "-C", protosDir, "status", "--porcelain", "--", "gen/ts"], monorepoRoot);
3260
+ const drift = runCapture3(["git", "-C", protosDir, "status", "--porcelain", "--", "gen/ts"], monorepoRoot);
1788
3261
  if (drift.exitCode !== 0) {
1789
3262
  console.log("FAIL: Could not inspect generated proto drift");
1790
3263
  console.log(drift.stderr || drift.stdout);
@@ -1805,12 +3278,12 @@ async function runProtoQualityGate(monorepoRoot) {
1805
3278
  } else {
1806
3279
  console.log("OK: Generated TypeScript compiles");
1807
3280
  }
1808
- const workflowPath = resolve2(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
1809
- if (!existsSync2(workflowPath)) {
3281
+ const workflowPath = resolve5(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
3282
+ if (!existsSync6(workflowPath)) {
1810
3283
  console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
1811
3284
  ok = false;
1812
3285
  } else {
1813
- const workflow = readFileSync(workflowPath, "utf-8");
3286
+ const workflow = readFileSync4(workflowPath, "utf-8");
1814
3287
  if (workflow.includes("if: false && needs.detect.outputs.protos_changed == 'true'")) {
1815
3288
  console.log("FAIL: Proto quality CI gate is disabled in pull-request-gate.yml");
1816
3289
  ok = false;
@@ -1821,13 +3294,13 @@ async function runProtoQualityGate(monorepoRoot) {
1821
3294
  return ok;
1822
3295
  }
1823
3296
  function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
1824
- const unpushed = runCapture2(["git", "-C", repoRoot, "log", "@{u}..HEAD", "--oneline"], projectRoot);
3297
+ const unpushed = runCapture3(["git", "-C", repoRoot, "log", "@{u}..HEAD", "--oneline"], projectRoot);
1825
3298
  if (unpushed.exitCode === 0 && unpushed.stdout.trim().length > 0)
1826
3299
  return true;
1827
3300
  if (unpushed.exitCode !== 0) {
1828
- const branch = runCapture2(["git", "-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
3301
+ const branch = runCapture3(["git", "-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
1829
3302
  if (branch.exitCode === 0 && branch.stdout.trim()) {
1830
- const remote = runCapture2(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branch.stdout.trim()}`], projectRoot);
3303
+ const remote = runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branch.stdout.trim()}`], projectRoot);
1831
3304
  if (remote.exitCode !== 0)
1832
3305
  return true;
1833
3306
  }
@@ -1836,10 +3309,10 @@ function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
1836
3309
  }
1837
3310
  function repoHasPublishedTaskBranch(projectRoot, repoRoot, taskId) {
1838
3311
  const branchRef = resolveTaskBranchRef(projectRoot, taskId);
1839
- return runCapture2(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branchRef}`], projectRoot).exitCode === 0;
3312
+ return runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branchRef}`], projectRoot).exitCode === 0;
1840
3313
  }
1841
3314
  async function readJsonFileIfPresent(path) {
1842
- if (!existsSync2(path)) {
3315
+ if (!existsSync6(path)) {
1843
3316
  return null;
1844
3317
  }
1845
3318
  try {
@@ -1850,9 +3323,9 @@ async function readJsonFileIfPresent(path) {
1850
3323
  }
1851
3324
  async function recordVerifierFailure(projectRoot, taskId, paths) {
1852
3325
  const failedApproachesPath = paths.failedApproachesPath;
1853
- const artifactDir = resolve2(paths.artifactsDir, safePathSegment(taskId, { fallback: "task", maxLength: 96 }));
1854
- const reviewStatePath = resolve2(artifactDir, "review-state.json");
1855
- const reviewFeedbackPath = resolve2(artifactDir, "review-feedback.md");
3326
+ const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
3327
+ const reviewStatePath = resolve5(artifactDir, "review-state.json");
3328
+ const reviewFeedbackPath = resolve5(artifactDir, "review-feedback.md");
1856
3329
  let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
1857
3330
  const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
1858
3331
  if (parsedReviewState) {
@@ -1862,12 +3335,12 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
1862
3335
  }
1863
3336
  }
1864
3337
  let attempts = 1;
1865
- if (existsSync2(failedApproachesPath)) {
1866
- const content = readFileSync(failedApproachesPath, "utf-8");
1867
- attempts = (content.match(new RegExp(`^## ${escapeRegExp(taskId)}\\b`, "gm")) || []).length + 1;
3338
+ if (existsSync6(failedApproachesPath)) {
3339
+ const content = readFileSync4(failedApproachesPath, "utf-8");
3340
+ attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
1868
3341
  } else {
1869
- mkdirSync2(resolve2(failedApproachesPath, ".."), { recursive: true });
1870
- writeFileSync2(failedApproachesPath, `# Failed Approaches
3342
+ mkdirSync3(resolve5(failedApproachesPath, ".."), { recursive: true });
3343
+ writeFileSync3(failedApproachesPath, `# Failed Approaches
1871
3344
 
1872
3345
  `, "utf-8");
1873
3346
  }
@@ -1891,7 +3364,7 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
1891
3364
  statePaths.add(resolveHarnessPaths2(hostProjectRoot).taskRepoCommitsPath);
1892
3365
  }
1893
3366
  const repos = {};
1894
- const monoHead = runCapture2(["git", "-C", paths.monorepoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
3367
+ const monoHead = runCapture3(["git", "-C", paths.monorepoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
1895
3368
  if (monoHead) {
1896
3369
  repos["monorepo"] = monoHead;
1897
3370
  }
@@ -1905,8 +3378,8 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
1905
3378
  recorded_at: new Date().toISOString(),
1906
3379
  repos
1907
3380
  };
1908
- mkdirSync2(resolve2(statePath, ".."), { recursive: true });
1909
- writeFileSync2(statePath, `${JSON.stringify(state, null, 2)}
3381
+ mkdirSync3(resolve5(statePath, ".."), { recursive: true });
3382
+ writeFileSync3(statePath, `${JSON.stringify(state, null, 2)}
1910
3383
  `, "utf-8");
1911
3384
  }
1912
3385
  }