@h-rig/runtime 0.0.6-alpha.2 → 0.0.6-alpha.20

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 (46) hide show
  1. package/dist/bin/rig-agent-dispatch.js +84 -313
  2. package/dist/bin/rig-agent.js +85 -27
  3. package/dist/src/control-plane/agent-wrapper.js +101 -27
  4. package/dist/src/control-plane/authority-files.js +12 -6
  5. package/dist/src/control-plane/harness-main.js +1357 -180
  6. package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
  7. package/dist/src/control-plane/hooks/inject-context.js +2 -2
  8. package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
  9. package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
  10. package/dist/src/control-plane/native/git-ops.js +134 -68
  11. package/dist/src/control-plane/native/harness-cli.js +1357 -180
  12. package/dist/src/control-plane/native/pr-automation.js +1532 -54
  13. package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
  14. package/dist/src/control-plane/native/run-ops.js +35 -12
  15. package/dist/src/control-plane/native/task-ops.js +1274 -155
  16. package/dist/src/control-plane/native/validator.js +2 -2
  17. package/dist/src/control-plane/native/verifier.js +1274 -154
  18. package/dist/src/control-plane/native/workspace-ops.js +12 -6
  19. package/dist/src/control-plane/runtime/index.js +38 -9
  20. package/dist/src/control-plane/runtime/isolation/home.js +31 -6
  21. package/dist/src/control-plane/runtime/isolation/index.js +38 -9
  22. package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
  23. package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
  24. package/dist/src/control-plane/runtime/isolation.js +38 -9
  25. package/dist/src/control-plane/runtime/queue.js +38 -9
  26. package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
  27. package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
  28. package/dist/src/index.js +27 -20
  29. package/dist/src/layout.js +12 -7
  30. package/dist/src/local-server.js +20 -14
  31. package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
  32. package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
  33. package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
  34. package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
  35. package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
  36. package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
  37. package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
  38. package/package.json +6 -6
  39. package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
  40. package/native/darwin-arm64/manifest.json +0 -1
  41. package/native/linux-x64/bin/rig-git +0 -0
  42. package/native/linux-x64/bin/rig-shell +0 -0
  43. package/native/linux-x64/bin/rig-tools +0 -0
  44. package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
  45. package/native/linux-x64/lib/runtime-native.so +0 -0
  46. package/native/linux-x64/manifest.json +0 -1
@@ -2484,8 +2484,8 @@ function githubStatusFor(issue) {
2484
2484
  return "open";
2485
2485
  }
2486
2486
  function selectedGitHubEnv() {
2487
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
2488
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
2487
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
2488
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
2489
2489
  }
2490
2490
  function ghSpawnOptions() {
2491
2491
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -1706,8 +1706,8 @@ function githubStatusFor(issue) {
1706
1706
  return "open";
1707
1707
  }
1708
1708
  function selectedGitHubEnv() {
1709
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
1710
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
1709
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1710
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1711
1711
  }
1712
1712
  function ghSpawnOptions() {
1713
1713
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -4509,6 +4509,10 @@ function runtimeGitEnv(projectRoot) {
4509
4509
  }
4510
4510
  env[key] = value;
4511
4511
  }
4512
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
4513
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
4514
+ env.GITHUB_TOKEN = rigGithubToken;
4515
+ }
4512
4516
  if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
4513
4517
  env.GITHUB_TOKEN = env.GH_TOKEN;
4514
4518
  }
@@ -4532,6 +4536,13 @@ function runtimeGitEnv(projectRoot) {
4532
4536
  if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
4533
4537
  env.GH_TOKEN = env.GITHUB_TOKEN;
4534
4538
  }
4539
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
4540
+ if (gitHubToken) {
4541
+ env.RIG_GITHUB_TOKEN = gitHubToken;
4542
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
4543
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
4544
+ applyGitHubCredentialHelperEnv(env);
4545
+ }
4535
4546
  if (runtimeKnownHosts && existsSync22(runtimeKnownHosts)) {
4536
4547
  const sshParts = [
4537
4548
  "ssh",
@@ -4548,6 +4559,14 @@ function runtimeGitEnv(projectRoot) {
4548
4559
  }
4549
4560
  return Object.keys(env).length > 0 ? env : undefined;
4550
4561
  }
4562
+ function applyGitHubCredentialHelperEnv(env) {
4563
+ env.GIT_TERMINAL_PROMPT = "0";
4564
+ env.GIT_CONFIG_COUNT = "2";
4565
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
4566
+ env.GIT_CONFIG_VALUE_0 = "";
4567
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
4568
+ 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';
4569
+ }
4551
4570
  function loadPersistedRuntimeSecrets(runtimeRoot) {
4552
4571
  if (!runtimeRoot) {
4553
4572
  return {};
@@ -7603,7 +7622,11 @@ async function ensureAgentRuntime(options) {
7603
7622
  mkdirSync21(runtime.binDir, { recursive: true });
7604
7623
  mkdirSync21(workspaceLayout.distDir, { recursive: true });
7605
7624
  prepareRuntimeWorkspace(options.projectRoot, workspaceDir);
7606
- await resetEphemeralTaskArtifacts(workspaceDir, options.taskId);
7625
+ if (options.preserveTaskArtifacts) {
7626
+ console.log(`[rig-agent] Preserving runtime task artifacts for resume of ${options.taskId}.`);
7627
+ } else {
7628
+ await resetEphemeralTaskArtifacts(workspaceDir, options.taskId);
7629
+ }
7607
7630
  const ctx = {
7608
7631
  runtimeId: options.id,
7609
7632
  taskId: options.taskId,
@@ -1706,8 +1706,8 @@ function githubStatusFor(issue) {
1706
1706
  return "open";
1707
1707
  }
1708
1708
  function selectedGitHubEnv() {
1709
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
1710
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
1709
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1710
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1711
1711
  }
1712
1712
  function ghSpawnOptions() {
1713
1713
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -4509,6 +4509,10 @@ function runtimeGitEnv(projectRoot) {
4509
4509
  }
4510
4510
  env[key] = value;
4511
4511
  }
4512
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
4513
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
4514
+ env.GITHUB_TOKEN = rigGithubToken;
4515
+ }
4512
4516
  if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
4513
4517
  env.GITHUB_TOKEN = env.GH_TOKEN;
4514
4518
  }
@@ -4532,6 +4536,13 @@ function runtimeGitEnv(projectRoot) {
4532
4536
  if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
4533
4537
  env.GH_TOKEN = env.GITHUB_TOKEN;
4534
4538
  }
4539
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
4540
+ if (gitHubToken) {
4541
+ env.RIG_GITHUB_TOKEN = gitHubToken;
4542
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
4543
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
4544
+ applyGitHubCredentialHelperEnv(env);
4545
+ }
4535
4546
  if (runtimeKnownHosts && existsSync22(runtimeKnownHosts)) {
4536
4547
  const sshParts = [
4537
4548
  "ssh",
@@ -4548,6 +4559,14 @@ function runtimeGitEnv(projectRoot) {
4548
4559
  }
4549
4560
  return Object.keys(env).length > 0 ? env : undefined;
4550
4561
  }
4562
+ function applyGitHubCredentialHelperEnv(env) {
4563
+ env.GIT_TERMINAL_PROMPT = "0";
4564
+ env.GIT_CONFIG_COUNT = "2";
4565
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
4566
+ env.GIT_CONFIG_VALUE_0 = "";
4567
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
4568
+ 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';
4569
+ }
4551
4570
  function loadPersistedRuntimeSecrets(runtimeRoot) {
4552
4571
  if (!runtimeRoot) {
4553
4572
  return {};
@@ -7603,7 +7622,11 @@ async function ensureAgentRuntime(options) {
7603
7622
  mkdirSync21(runtime.binDir, { recursive: true });
7604
7623
  mkdirSync21(workspaceLayout.distDir, { recursive: true });
7605
7624
  prepareRuntimeWorkspace(options.projectRoot, workspaceDir);
7606
- await resetEphemeralTaskArtifacts(workspaceDir, options.taskId);
7625
+ if (options.preserveTaskArtifacts) {
7626
+ console.log(`[rig-agent] Preserving runtime task artifacts for resume of ${options.taskId}.`);
7627
+ } else {
7628
+ await resetEphemeralTaskArtifacts(workspaceDir, options.taskId);
7629
+ }
7607
7630
  const ctx = {
7608
7631
  runtimeId: options.id,
7609
7632
  taskId: options.taskId,
@@ -1365,6 +1365,30 @@ function sourceTaskConfigCandidates(projectRoot) {
1365
1365
 
1366
1366
  // packages/runtime/src/binary-run.ts
1367
1367
  var runtimeBinaryBuildQueue = Promise.resolve();
1368
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
1369
+ function strictMergeHeadShaFromGate(result, prUrl) {
1370
+ if (!result.approved) {
1371
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
1372
+ }
1373
+ if (result.evidence.prUrl !== prUrl) {
1374
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
1375
+ }
1376
+ const headSha = result.evidence.headSha?.trim();
1377
+ if (!headSha) {
1378
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
1379
+ }
1380
+ if (!/^[0-9a-f]{40}$/i.test(headSha)) {
1381
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
1382
+ }
1383
+ if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
1384
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
1385
+ }
1386
+ if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
1387
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
1388
+ }
1389
+ return headSha;
1390
+ }
1391
+
1368
1392
  // packages/runtime/src/control-plane/provider/runtime-instructions.ts
1369
1393
  var CLAUDE_ROUTER_TOOL_NAMES = [
1370
1394
  "`mcp__rig_runtime_tools__read`",
@@ -1575,12 +1599,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
1575
1599
  "task-result.json",
1576
1600
  "validation-summary.json"
1577
1601
  ]);
1578
- function resolveHostRigBinDir(root) {
1579
- return resolve11(root, ".rig", "bin");
1580
- }
1581
1602
  function isRuntimeGatewayGitPath(candidate) {
1582
1603
  return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
1583
1604
  }
1605
+ function isRuntimeGatewayGhPath(candidate) {
1606
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
1607
+ }
1584
1608
  function resolveOptionalMonorepoRoot(projectRoot) {
1585
1609
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
1586
1610
  if (runtimeWorkspace && existsSync9(resolve11(runtimeWorkspace, ".git"))) {
@@ -1615,6 +1639,9 @@ function resolveGitBinary(projectRoot) {
1615
1639
  }
1616
1640
  return "git";
1617
1641
  }
1642
+ function escapeRegExp(value) {
1643
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1644
+ }
1618
1645
  function safeCurrentTaskId(projectRoot) {
1619
1646
  try {
1620
1647
  const taskId = currentTaskId(projectRoot);
@@ -1773,17 +1800,15 @@ function gitOpenPr(options) {
1773
1800
  const target = options.target || (taskId ? "monorepo" : "project");
1774
1801
  let repoRoot = options.projectRoot;
1775
1802
  let repoLabel = "project-rig";
1776
- let defaultBase = process.env.RIG_PR_BASE_PROJECT || "main";
1803
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
1777
1804
  if (target === "monorepo") {
1778
1805
  repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot2(options.projectRoot);
1779
1806
  repoLabel = "monorepo";
1780
- defaultBase = process.env.RIG_PR_BASE_MONOREPO || "main";
1781
1807
  if (taskId) {
1782
1808
  gitSyncBranch(options.projectRoot, taskId, "monorepo");
1783
1809
  }
1784
1810
  } else if (taskId) {
1785
1811
  gitSyncBranch(options.projectRoot, taskId, "project");
1786
- defaultBase = inferProjectBase(options.projectRoot, defaultBase);
1787
1812
  }
1788
1813
  if (!existsSync9(resolve11(repoRoot, ".git"))) {
1789
1814
  throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
@@ -1792,9 +1817,9 @@ function gitOpenPr(options) {
1792
1817
  if (!branch || branch === "HEAD") {
1793
1818
  throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
1794
1819
  }
1795
- const base = options.base || defaultBase;
1796
1820
  const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
1797
1821
  const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
1822
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
1798
1823
  refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
1799
1824
  let reviewer = (options.reviewer || "").trim();
1800
1825
  let reviewerSource = reviewer ? "flag" : undefined;
@@ -1830,10 +1855,11 @@ function gitOpenPr(options) {
1830
1855
  "",
1831
1856
  "## Task",
1832
1857
  `- beads: ${taskId || "n/a"}`,
1858
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
1833
1859
  "",
1834
1860
  "## Review",
1835
1861
  "- Completion verification will run validation, verifier review, and PR policy checks.",
1836
- "- When repository policy allows it, Rig enables GitHub auto-merge after approval."
1862
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
1837
1863
  ].join(`
1838
1864
  `);
1839
1865
  const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
@@ -1921,6 +1947,30 @@ function gitOpenPr(options) {
1921
1947
  }
1922
1948
  return result;
1923
1949
  }
1950
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
1951
+ const lines = [];
1952
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
1953
+ if (runId) {
1954
+ lines.push(`- Run: ${runId}`);
1955
+ }
1956
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
1957
+ if (closeout) {
1958
+ lines.push(`- ${closeout}`);
1959
+ }
1960
+ return lines;
1961
+ }
1962
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
1963
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
1964
+ if (sourceIssueId) {
1965
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
1966
+ if (match?.[1] && match[2]) {
1967
+ const sourceRepo = match[1];
1968
+ const issueNumber = match[2];
1969
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
1970
+ }
1971
+ }
1972
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
1973
+ }
1924
1974
  function resolveTaskBranchRef(projectRoot, taskId) {
1925
1975
  return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
1926
1976
  }
@@ -2005,61 +2055,45 @@ function gitMergePr(options) {
2005
2055
  return { status: "already-merged", url: options.pr.url };
2006
2056
  }
2007
2057
  if (state !== "OPEN") {
2008
- throw new Error(`Cannot auto-merge PR ${options.pr.url}: state is ${state}.`);
2058
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
2009
2059
  }
2010
2060
  if (isDraft) {
2011
- throw new Error(`Cannot auto-merge draft PR ${options.pr.url}.`);
2061
+ throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
2012
2062
  }
2063
+ const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
2013
2064
  const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
2014
2065
  const method = options.method || "squash";
2015
2066
  mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
2067
+ mergeArgs.push("--match-head-commit", strictGateHeadSha);
2016
2068
  if (options.deleteBranch !== false) {
2017
2069
  mergeArgs.push("--delete-branch");
2018
2070
  }
2019
- const autoMergeArgs = [...mergeArgs, "--auto"];
2020
- const autoMerge = runCapture2(autoMergeArgs, repoRoot);
2021
- if (autoMerge.exitCode === 0) {
2022
- const postAutoMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
2023
- if (postAutoMergeState.state === "MERGED" || postAutoMergeState.mergedAt) {
2024
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
2025
- return { status: "merged", url: options.pr.url };
2026
- }
2027
- if (postAutoMergeState.state === "OPEN" && postAutoMergeState.autoMergeRequest) {
2028
- if (canAdminMergeApprovedPr(postAutoMergeState)) {
2029
- const adminMergeArgs = [...mergeArgs];
2030
- if (postAutoMergeState.headRefOid) {
2031
- adminMergeArgs.push("--match-head-commit", postAutoMergeState.headRefOid);
2032
- }
2033
- adminMergeArgs.push("--admin");
2034
- const adminMerge = runCapture2(adminMergeArgs, repoRoot);
2035
- if (adminMerge.exitCode === 0) {
2036
- const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
2037
- if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
2038
- console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
2039
- return { status: "merged", url: options.pr.url };
2040
- }
2041
- throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
2042
- }
2043
- const adminMergeMessage = `${adminMerge.stderr}
2044
- ${adminMerge.stdout}`.trim();
2045
- if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
2046
- throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
2047
- }
2071
+ const directMerge = runCapture2(mergeArgs, repoRoot);
2072
+ if (directMerge.exitCode === 0) {
2073
+ console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
2074
+ return { status: "merged", url: options.pr.url };
2075
+ }
2076
+ const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
2077
+ if (canAdminMergeApprovedPr(postDirectState)) {
2078
+ const adminMergeArgs = [...mergeArgs, "--admin"];
2079
+ const adminMerge = runCapture2(adminMergeArgs, repoRoot);
2080
+ if (adminMerge.exitCode === 0) {
2081
+ const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
2082
+ if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
2083
+ console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
2084
+ return { status: "merged", url: options.pr.url };
2048
2085
  }
2049
- console.log(`Auto-merge enabled (${options.pr.repoLabel}): ${options.pr.url}`);
2050
- return { status: "auto-merge-enabled", url: options.pr.url };
2086
+ throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
2087
+ }
2088
+ const adminMergeMessage = `${adminMerge.stderr}
2089
+ ${adminMerge.stdout}`.trim();
2090
+ if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
2091
+ throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
2051
2092
  }
2052
- throw new Error(`Auto-merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub did not report a merged or auto-merge-enabled state.`);
2053
- }
2054
- const autoMergeMessage = `${autoMerge.stderr}
2055
- ${autoMerge.stdout}`.trim();
2056
- const autoMergeUnsupported = /auto.?merge.*(not enabled|not allowed|disabled|unsupported)|enablePullRequestAutoMerge|Auto merge is not allowed/i.test(autoMergeMessage);
2057
- if (!autoMergeUnsupported) {
2058
- throw new Error(`Failed to auto-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${autoMergeMessage}`);
2059
2093
  }
2060
- runOrThrow(options.projectRoot, mergeArgs, `Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}`);
2061
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
2062
- return { status: "merged", url: options.pr.url };
2094
+ const directMergeMessage = `${directMerge.stderr}
2095
+ ${directMerge.stdout}`.trim();
2096
+ throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
2063
2097
  }
2064
2098
  function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
2065
2099
  const mergeable = prState.mergeable.toUpperCase();
@@ -2171,32 +2205,19 @@ function resolveGithubCliBinary(projectRoot) {
2171
2205
  if (explicit) {
2172
2206
  candidates.add(explicit);
2173
2207
  }
2208
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
2209
+ candidates.add(candidate);
2210
+ }
2174
2211
  const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
2175
2212
  for (const entry of explicitPathEntries) {
2176
2213
  candidates.add(resolve11(entry, "gh"));
2177
2214
  }
2178
- const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
2179
- if (hostProjectRoot) {
2180
- candidates.add(resolve11(resolveHostRigBinDir(hostProjectRoot), "gh"));
2181
- }
2182
- candidates.add(resolve11(resolveHostRigBinDir(projectRoot), "gh"));
2183
- const runtimeContext = loadRuntimeContextFromEnv();
2184
- if (runtimeContext?.binDir) {
2185
- candidates.add(resolve11(runtimeContext.binDir, "gh"));
2186
- }
2187
- const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
2188
- if (runtimeHome) {
2189
- candidates.add(resolve11(runtimeHome, "bin", "gh"));
2190
- }
2191
- for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
2192
- candidates.add(candidate);
2193
- }
2194
2215
  const bunResolved = Bun.which("gh");
2195
2216
  if (bunResolved) {
2196
2217
  candidates.add(bunResolved);
2197
2218
  }
2198
2219
  for (const candidate of candidates) {
2199
- if (candidate && existsSync9(candidate)) {
2220
+ if (candidate && existsSync9(candidate) && !isRuntimeGatewayGhPath(candidate)) {
2200
2221
  return candidate;
2201
2222
  }
2202
2223
  }
@@ -2345,6 +2366,32 @@ function withGhRepo(command, repoNameWithOwner) {
2345
2366
  }
2346
2367
  return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
2347
2368
  }
2369
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
2370
+ const remote = remoteName || "origin";
2371
+ const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
2372
+ if (symbolic.exitCode === 0) {
2373
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
2374
+ if (ref && ref !== "HEAD") {
2375
+ return ref;
2376
+ }
2377
+ }
2378
+ const lsRemote = runCapture2(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
2379
+ if (lsRemote.exitCode === 0) {
2380
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
2381
+ if (match?.[1]) {
2382
+ return match[1];
2383
+ }
2384
+ }
2385
+ const gh = resolveGithubCliBinary(projectRoot);
2386
+ if (gh && repoNameWithOwner) {
2387
+ const api = runCapture2(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
2388
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
2389
+ if (branch) {
2390
+ return branch;
2391
+ }
2392
+ }
2393
+ return fallback;
2394
+ }
2348
2395
  function inferProjectBase(projectRoot, fallback) {
2349
2396
  const containing = runCapture2(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
2350
2397
  if (containing.exitCode !== 0) {
@@ -2726,6 +2773,10 @@ function runtimeGitEnv(projectRoot) {
2726
2773
  }
2727
2774
  env[key] = value;
2728
2775
  }
2776
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
2777
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
2778
+ env.GITHUB_TOKEN = rigGithubToken;
2779
+ }
2729
2780
  if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
2730
2781
  env.GITHUB_TOKEN = env.GH_TOKEN;
2731
2782
  }
@@ -2749,6 +2800,13 @@ function runtimeGitEnv(projectRoot) {
2749
2800
  if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
2750
2801
  env.GH_TOKEN = env.GITHUB_TOKEN;
2751
2802
  }
2803
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
2804
+ if (gitHubToken) {
2805
+ env.RIG_GITHUB_TOKEN = gitHubToken;
2806
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
2807
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
2808
+ applyGitHubCredentialHelperEnv(env);
2809
+ }
2752
2810
  if (runtimeKnownHosts && existsSync9(runtimeKnownHosts)) {
2753
2811
  const sshParts = [
2754
2812
  "ssh",
@@ -2765,6 +2823,14 @@ function runtimeGitEnv(projectRoot) {
2765
2823
  }
2766
2824
  return Object.keys(env).length > 0 ? env : undefined;
2767
2825
  }
2826
+ function applyGitHubCredentialHelperEnv(env) {
2827
+ env.GIT_TERMINAL_PROMPT = "0";
2828
+ env.GIT_CONFIG_COUNT = "2";
2829
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
2830
+ env.GIT_CONFIG_VALUE_0 = "";
2831
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
2832
+ 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';
2833
+ }
2768
2834
  function loadPersistedRuntimeSecrets(runtimeRoot) {
2769
2835
  if (!runtimeRoot) {
2770
2836
  return {};