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

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
@@ -1198,8 +1198,8 @@ function githubStatusFor(issue) {
1198
1198
  return "open";
1199
1199
  }
1200
1200
  function selectedGitHubEnv() {
1201
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
1202
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
1201
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1202
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1203
1203
  }
1204
1204
  function ghSpawnOptions() {
1205
1205
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -3605,6 +3605,1126 @@ ${JSON.stringify(result, null, 2)}
3605
3605
  // packages/runtime/src/control-plane/native/verifier.ts
3606
3606
  import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
3607
3607
  import { resolve as resolve22 } from "path";
3608
+
3609
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
3610
+ function parseJsonObject(value) {
3611
+ if (!value?.trim())
3612
+ return { value: {}, error: "empty JSON output" };
3613
+ try {
3614
+ const parsed = JSON.parse(value);
3615
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
3616
+ } catch (error) {
3617
+ return { value: {}, error: error instanceof Error ? error.message : String(error) };
3618
+ }
3619
+ }
3620
+ function flattenPaginatedArray(value) {
3621
+ if (!Array.isArray(value))
3622
+ return null;
3623
+ if (value.every((entry) => Array.isArray(entry))) {
3624
+ return value.flatMap((entry) => entry);
3625
+ }
3626
+ return value;
3627
+ }
3628
+ function parseJsonArray(value) {
3629
+ if (!value?.trim())
3630
+ return { value: [], error: "empty JSON output" };
3631
+ try {
3632
+ const parsed = JSON.parse(value);
3633
+ const flattened = flattenPaginatedArray(parsed);
3634
+ return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3635
+ } catch (error) {
3636
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
3637
+ }
3638
+ }
3639
+ function parseGithubPrUrl(prUrl) {
3640
+ const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
3641
+ if (!match)
3642
+ return null;
3643
+ const prNumber = Number.parseInt(match[3], 10);
3644
+ if (!Number.isFinite(prNumber))
3645
+ return null;
3646
+ return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
3647
+ }
3648
+ function checkName(check) {
3649
+ return String(check.name ?? check.context ?? "").trim();
3650
+ }
3651
+ function checkState(check) {
3652
+ return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
3653
+ }
3654
+ function isGreptileLabel(value) {
3655
+ return String(value ?? "").toLowerCase().includes("greptile");
3656
+ }
3657
+ function isGreptileGithubLogin(value) {
3658
+ const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
3659
+ return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
3660
+ }
3661
+ function isPassingCheck(check) {
3662
+ const state = checkState(check);
3663
+ return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
3664
+ }
3665
+ function isPendingCheck(check) {
3666
+ const state = checkState(check);
3667
+ return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
3668
+ }
3669
+ function isFailingCheck(check) {
3670
+ const state = checkState(check);
3671
+ return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
3672
+ }
3673
+ function wildcardToRegExp(pattern) {
3674
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3675
+ return new RegExp(`^${escaped}$`, "i");
3676
+ }
3677
+ function isAllowedFailure(name, allowedFailures) {
3678
+ return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
3679
+ }
3680
+ function greptileScorePatterns() {
3681
+ return [
3682
+ /\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
3683
+ /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
3684
+ /\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
3685
+ ];
3686
+ }
3687
+ function parseGreptileScores(input) {
3688
+ const text = stripHtml(input);
3689
+ const seen = new Set;
3690
+ const scores = [];
3691
+ for (const pattern of greptileScorePatterns()) {
3692
+ for (const match of text.matchAll(pattern)) {
3693
+ const value = Number.parseInt(match[1] || "", 10);
3694
+ const scale = Number.parseInt(match[2] || "", 10);
3695
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
3696
+ continue;
3697
+ const raw = match[0] || `${value}/${scale}`;
3698
+ const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
3699
+ if (seen.has(key))
3700
+ continue;
3701
+ seen.add(key);
3702
+ scores.push({ value, scale, raw });
3703
+ }
3704
+ }
3705
+ return scores;
3706
+ }
3707
+ function parseGreptileScore(input) {
3708
+ return parseGreptileScores(input)[0] ?? null;
3709
+ }
3710
+ function stripHtml(input) {
3711
+ return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
3712
+
3713
+ `).trim();
3714
+ }
3715
+ function containsBlockerText(input) {
3716
+ const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
3717
+ return /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(text);
3718
+ }
3719
+ function isStrictFiveOfFive(score) {
3720
+ return score.value === 5 && score.scale === 5;
3721
+ }
3722
+ function containsConflictingScoreText(input) {
3723
+ return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
3724
+ }
3725
+ function greptileStatusVerdict(status) {
3726
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
3727
+ if (!normalized)
3728
+ return null;
3729
+ if (["APPROVE", "APPROVED"].includes(normalized))
3730
+ return "approved";
3731
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
3732
+ return "rejected";
3733
+ if (["SKIP", "SKIPPED"].includes(normalized))
3734
+ return "skipped";
3735
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
3736
+ return "failed";
3737
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
3738
+ return "pending";
3739
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
3740
+ return "completed";
3741
+ return null;
3742
+ }
3743
+ function isBlockingGreptileVerdict(verdict) {
3744
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
3745
+ }
3746
+ function greptileRequestTimeoutMs(env) {
3747
+ const fallback = 30000;
3748
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
3749
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
3750
+ }
3751
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
3752
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3753
+ return null;
3754
+ const record = entry;
3755
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
3756
+ if (!id)
3757
+ return null;
3758
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
3759
+ return {
3760
+ id,
3761
+ status: typeof record.status === "string" ? record.status : null,
3762
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
3763
+ body: typeof record.body === "string" ? record.body : null,
3764
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
3765
+ };
3766
+ }
3767
+ function uniqueGreptileCodeReviews(reviews) {
3768
+ const seen = new Set;
3769
+ const unique2 = [];
3770
+ for (const review of reviews) {
3771
+ if (seen.has(review.id))
3772
+ continue;
3773
+ seen.add(review.id);
3774
+ unique2.push(review);
3775
+ }
3776
+ return unique2;
3777
+ }
3778
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
3779
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
3780
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
3781
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
3782
+ const latest = sorted.slice(0, 1);
3783
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
3784
+ }
3785
+ function greptileApiSignalFromCodeReview(review, details) {
3786
+ const selected = details ?? review;
3787
+ return {
3788
+ id: selected.id || review.id,
3789
+ body: selected.body ?? review.body ?? null,
3790
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
3791
+ status: selected.status ?? review.status ?? null
3792
+ };
3793
+ }
3794
+ async function callGreptileMcpToolForGate(input) {
3795
+ const controller = new AbortController;
3796
+ const timeoutId = setTimeout(() => {
3797
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
3798
+ }, input.timeoutMs);
3799
+ let response;
3800
+ try {
3801
+ response = await input.fetchFn(input.apiBase, {
3802
+ method: "POST",
3803
+ headers: {
3804
+ Authorization: `Bearer ${input.apiKey}`,
3805
+ "Content-Type": "application/json"
3806
+ },
3807
+ body: JSON.stringify({
3808
+ jsonrpc: "2.0",
3809
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
3810
+ method: "tools/call",
3811
+ params: { name: input.name, arguments: input.args }
3812
+ }),
3813
+ signal: controller.signal
3814
+ });
3815
+ } catch (error) {
3816
+ if (controller.signal.aborted) {
3817
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
3818
+ }
3819
+ throw error;
3820
+ } finally {
3821
+ clearTimeout(timeoutId);
3822
+ }
3823
+ const raw = await response.text();
3824
+ if (!response.ok) {
3825
+ throw new Error(`HTTP ${response.status}: ${raw}`);
3826
+ }
3827
+ let envelope;
3828
+ try {
3829
+ envelope = JSON.parse(raw);
3830
+ } catch {
3831
+ throw new Error(`Malformed MCP response: ${raw}`);
3832
+ }
3833
+ if (envelope.error?.message) {
3834
+ throw new Error(envelope.error.message);
3835
+ }
3836
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
3837
+ `).trim();
3838
+ if (!text) {
3839
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
3840
+ }
3841
+ return text;
3842
+ }
3843
+ async function callGreptileMcpToolJsonForGate(input) {
3844
+ const text = await callGreptileMcpToolForGate(input);
3845
+ try {
3846
+ return JSON.parse(text);
3847
+ } catch {
3848
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
3849
+ }
3850
+ }
3851
+ async function collectConfiguredGreptileApiSignals(input) {
3852
+ if (!input.enabled || input.options?.enabled === false) {
3853
+ return { signals: [], errors: [] };
3854
+ }
3855
+ const env = input.options?.env ?? process.env;
3856
+ const secrets = resolveRuntimeSecrets(env);
3857
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
3858
+ if (!apiKey) {
3859
+ return { signals: [], errors: [] };
3860
+ }
3861
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
3862
+ if (typeof fetchFn !== "function") {
3863
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
3864
+ }
3865
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
3866
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
3867
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
3868
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
3869
+ const timeoutMs = greptileRequestTimeoutMs(env);
3870
+ try {
3871
+ const listPayload = await callGreptileMcpToolJsonForGate({
3872
+ apiBase,
3873
+ apiKey,
3874
+ name: "list_code_reviews",
3875
+ args: {
3876
+ name: repository,
3877
+ remote,
3878
+ defaultBranch,
3879
+ prNumber: input.prNumber,
3880
+ limit: 20
3881
+ },
3882
+ timeoutMs,
3883
+ fetchFn
3884
+ });
3885
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
3886
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
3887
+ const signals = [];
3888
+ for (const review of selectedReviews) {
3889
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
3890
+ apiBase,
3891
+ apiKey,
3892
+ name: "get_code_review",
3893
+ args: { codeReviewId: review.id },
3894
+ timeoutMs,
3895
+ fetchFn
3896
+ });
3897
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
3898
+ signals.push(greptileApiSignalFromCodeReview(review, details));
3899
+ }
3900
+ return { signals, errors: [] };
3901
+ } catch (error) {
3902
+ return {
3903
+ signals: [],
3904
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
3905
+ };
3906
+ }
3907
+ }
3908
+ function firstString(record, keys) {
3909
+ for (const key of keys) {
3910
+ const value = record[key];
3911
+ if (typeof value === "string")
3912
+ return value;
3913
+ }
3914
+ return "";
3915
+ }
3916
+ function arrayField(record, key) {
3917
+ const value = record[key];
3918
+ return Array.isArray(value) ? value : [];
3919
+ }
3920
+ async function runJsonArray(command, args, cwd) {
3921
+ const result = await command(args, { cwd });
3922
+ const label = `gh ${args.join(" ")}`;
3923
+ if (result.exitCode !== 0) {
3924
+ return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
3925
+ }
3926
+ const parsed = parseJsonArray(result.stdout);
3927
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
3928
+ }
3929
+ async function runJsonObject(command, args, cwd) {
3930
+ const result = await command(args, { cwd });
3931
+ const label = `gh ${args.join(" ")}`;
3932
+ if (result.exitCode !== 0) {
3933
+ return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
3934
+ }
3935
+ const parsed = parseJsonObject(result.stdout);
3936
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
3937
+ }
3938
+ function normalizeStatusCheck(entry) {
3939
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3940
+ return null;
3941
+ const record = entry;
3942
+ const name = firstString(record, ["name", "context"]);
3943
+ if (!name.trim())
3944
+ return null;
3945
+ const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
3946
+ const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
3947
+ return {
3948
+ __typename: typeof record.__typename === "string" ? record.__typename : null,
3949
+ name,
3950
+ context: typeof record.context === "string" ? record.context : null,
3951
+ status: typeof record.status === "string" ? record.status : null,
3952
+ state: typeof record.state === "string" ? record.state : null,
3953
+ conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
3954
+ detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.details_url === "string" ? record.details_url : typeof record.html_url === "string" ? record.html_url : typeof record.link === "string" ? record.link : null,
3955
+ link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
3956
+ headSha: typeof record.headSha === "string" ? record.headSha : null,
3957
+ head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
3958
+ output: output ? {
3959
+ title: typeof output.title === "string" ? output.title : null,
3960
+ summary: typeof output.summary === "string" ? output.summary : null,
3961
+ text: typeof output.text === "string" ? output.text : null
3962
+ } : null,
3963
+ app: app ? {
3964
+ slug: typeof app.slug === "string" ? app.slug : null,
3965
+ name: typeof app.name === "string" ? app.name : null,
3966
+ owner: app.owner && typeof app.owner === "object" ? app.owner : null
3967
+ } : null
3968
+ };
3969
+ }
3970
+ function normalizeReview(entry) {
3971
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3972
+ return null;
3973
+ const record = entry;
3974
+ return {
3975
+ id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
3976
+ state: typeof record.state === "string" ? record.state : null,
3977
+ body: typeof record.body === "string" ? record.body : null,
3978
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : typeof record.commitId === "string" ? record.commitId : record.commit && typeof record.commit === "object" && typeof record.commit.oid === "string" ? record.commit.oid : null,
3979
+ html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
3980
+ author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
3981
+ };
3982
+ }
3983
+ function normalizeReviewComment(entry) {
3984
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3985
+ return null;
3986
+ const record = entry;
3987
+ const body = typeof record.body === "string" ? record.body : null;
3988
+ const path = typeof record.path === "string" ? record.path : null;
3989
+ if (!body && !path)
3990
+ return null;
3991
+ return {
3992
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
3993
+ user: record.user && typeof record.user === "object" ? record.user : null,
3994
+ author: record.author && typeof record.author === "object" ? record.author : null,
3995
+ body,
3996
+ path,
3997
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
3998
+ url: typeof record.url === "string" ? record.url : null,
3999
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
4000
+ original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
4001
+ };
4002
+ }
4003
+ function normalizeIssueComment(entry) {
4004
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4005
+ return null;
4006
+ const record = entry;
4007
+ const body = typeof record.body === "string" ? record.body : null;
4008
+ if (!body)
4009
+ return null;
4010
+ return {
4011
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4012
+ user: record.user && typeof record.user === "object" ? record.user : null,
4013
+ author: record.author && typeof record.author === "object" ? record.author : null,
4014
+ body,
4015
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4016
+ url: typeof record.url === "string" ? record.url : null,
4017
+ created_at: typeof record.created_at === "string" ? record.created_at : null
4018
+ };
4019
+ }
4020
+ function normalizeReviewThread(entry) {
4021
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4022
+ return null;
4023
+ const record = entry;
4024
+ return {
4025
+ id: typeof record.id === "string" ? record.id : null,
4026
+ isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
4027
+ isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
4028
+ comments: record.comments && typeof record.comments === "object" ? record.comments : null
4029
+ };
4030
+ }
4031
+ function relevantIssueComment(comment) {
4032
+ const login = comment.user?.login ?? comment.author?.login ?? "";
4033
+ const body = comment.body ?? "";
4034
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4035
+ }
4036
+ function latestThreadComment(thread) {
4037
+ const nodes = thread.comments?.nodes ?? [];
4038
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
4039
+ }
4040
+ function unresolvedThreadSummaries(threads) {
4041
+ return threads.flatMap((thread) => {
4042
+ if (thread.isResolved === true || thread.isOutdated === true)
4043
+ return [];
4044
+ const latest = latestThreadComment(thread);
4045
+ if (!latest)
4046
+ return ["Unresolved review thread"];
4047
+ const path = latest.path ? ` on ${latest.path}` : "";
4048
+ return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4049
+ });
4050
+ }
4051
+ function collectBodies(evidence) {
4052
+ return [
4053
+ evidence.title ?? "",
4054
+ evidence.body,
4055
+ ...evidence.reviews.map((review) => review.body ?? ""),
4056
+ ...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
4057
+ ...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
4058
+ ...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
4059
+ ...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
4060
+ ].filter((body) => body.trim().length > 0);
4061
+ }
4062
+ function bodyExcerpt(body) {
4063
+ const text = stripHtml(body).replace(/\s+/g, " ").trim();
4064
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
4065
+ }
4066
+ function makeGreptileSignal(input) {
4067
+ const scores = parseGreptileScores(input.body);
4068
+ const reviewedSha = input.reviewedSha?.trim() || null;
4069
+ const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
4070
+ const verdict = input.verdict ?? null;
4071
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
4072
+ const explicitApproval = input.explicitApproval ?? false;
4073
+ return {
4074
+ source: input.source,
4075
+ trusted: input.trusted,
4076
+ authorLogin: input.authorLogin ?? null,
4077
+ reviewedSha,
4078
+ current,
4079
+ stale: current === false,
4080
+ score: scores[0] ?? null,
4081
+ scores,
4082
+ explicitApproval,
4083
+ verdict,
4084
+ blocker,
4085
+ actionable: input.actionable ?? blocker,
4086
+ bodyExcerpt: bodyExcerpt(input.body),
4087
+ body: input.body,
4088
+ allScores: scores
4089
+ };
4090
+ }
4091
+ function reviewAuthorLogin(review) {
4092
+ return review.author?.login ?? null;
4093
+ }
4094
+ function commentAuthorLogin(comment) {
4095
+ return comment.user?.login ?? comment.author?.login ?? null;
4096
+ }
4097
+ function collectGreptileSignals(evidence) {
4098
+ const signals = [];
4099
+ const contextSources = [
4100
+ { source: "pr-title", body: evidence.title ?? "" },
4101
+ { source: "pr-body", body: evidence.body }
4102
+ ];
4103
+ for (const context of contextSources) {
4104
+ if (!context.body.trim())
4105
+ continue;
4106
+ const contextBlocker = containsBlockerText(context.body);
4107
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
4108
+ continue;
4109
+ signals.push(makeGreptileSignal({
4110
+ source: context.source,
4111
+ body: context.body,
4112
+ currentHeadSha: evidence.currentHeadSha,
4113
+ trusted: false,
4114
+ blocker: contextBlocker,
4115
+ actionable: contextBlocker
4116
+ }));
4117
+ }
4118
+ for (const apiSignal of evidence.apiSignals ?? []) {
4119
+ const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
4120
+
4121
+ `) || "Status: UNKNOWN";
4122
+ const verdict = greptileStatusVerdict(apiSignal.status);
4123
+ signals.push(makeGreptileSignal({
4124
+ source: "api",
4125
+ body,
4126
+ currentHeadSha: evidence.currentHeadSha,
4127
+ trusted: true,
4128
+ reviewedSha: apiSignal.reviewedSha ?? null,
4129
+ explicitApproval: verdict === "approved",
4130
+ verdict
4131
+ }));
4132
+ }
4133
+ for (const review of evidence.reviews) {
4134
+ const login = reviewAuthorLogin(review);
4135
+ if (!isGreptileGithubLogin(login))
4136
+ continue;
4137
+ const state = String(review.state ?? "").toUpperCase();
4138
+ const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
4139
+
4140
+ `);
4141
+ if (!body.trim())
4142
+ continue;
4143
+ const dismissed = state === "DISMISSED";
4144
+ signals.push(makeGreptileSignal({
4145
+ source: "github-review",
4146
+ body,
4147
+ currentHeadSha: evidence.currentHeadSha,
4148
+ trusted: !dismissed,
4149
+ authorLogin: login,
4150
+ reviewedSha: review.commit_id ?? null,
4151
+ explicitApproval: undefined,
4152
+ blocker: state === "CHANGES_REQUESTED" || undefined
4153
+ }));
4154
+ }
4155
+ for (const comment of evidence.relevantIssueComments) {
4156
+ const login = commentAuthorLogin(comment);
4157
+ const body = comment.body ?? "";
4158
+ if (!body.trim() || !isGreptileGithubLogin(login))
4159
+ continue;
4160
+ signals.push(makeGreptileSignal({
4161
+ source: "issue-comment",
4162
+ body,
4163
+ currentHeadSha: evidence.currentHeadSha,
4164
+ trusted: true,
4165
+ authorLogin: login
4166
+ }));
4167
+ }
4168
+ for (const thread of evidence.reviewThreads) {
4169
+ if (thread.isOutdated === true || thread.isResolved === true)
4170
+ continue;
4171
+ for (const comment of thread.comments?.nodes ?? []) {
4172
+ const login = comment.author?.login ?? null;
4173
+ const body = comment.body ?? "";
4174
+ if (!body.trim() || !isGreptileGithubLogin(login))
4175
+ continue;
4176
+ signals.push(makeGreptileSignal({
4177
+ source: "review-thread",
4178
+ body,
4179
+ currentHeadSha: evidence.currentHeadSha,
4180
+ trusted: true,
4181
+ authorLogin: login
4182
+ }));
4183
+ }
4184
+ }
4185
+ for (const check of evidence.checks) {
4186
+ if (!isGreptileLabel(checkName(check)))
4187
+ continue;
4188
+ const reviewedSha = check.headSha ?? check.head_sha ?? null;
4189
+ const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
4190
+ const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
4191
+
4192
+ `);
4193
+ signals.push(makeGreptileSignal({
4194
+ source: "github-check",
4195
+ body,
4196
+ currentHeadSha: evidence.currentHeadSha,
4197
+ trusted: false,
4198
+ reviewedSha,
4199
+ explicitApproval: false,
4200
+ blocker: isFailingCheck(check),
4201
+ actionable: isFailingCheck(check)
4202
+ }));
4203
+ }
4204
+ return signals;
4205
+ }
4206
+ function unresolvedGreptileThreadSummaries(threads) {
4207
+ return threads.flatMap((thread) => {
4208
+ if (thread.isResolved === true || thread.isOutdated === true)
4209
+ return [];
4210
+ const comments = thread.comments?.nodes ?? [];
4211
+ if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
4212
+ return [];
4213
+ const latest = latestThreadComment(thread);
4214
+ if (!latest)
4215
+ return ["Unresolved Greptile review thread"];
4216
+ const path = latest.path ? ` on ${latest.path}` : "";
4217
+ return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4218
+ });
4219
+ }
4220
+ function actionableChangedFileCommentSummaries(_comments) {
4221
+ return [];
4222
+ }
4223
+ function issueLevelBlockerSummaries(comments) {
4224
+ return comments.flatMap((comment) => {
4225
+ const body = comment.body?.trim() ?? "";
4226
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4227
+ return [];
4228
+ const login = commentAuthorLogin(comment) ?? "unknown";
4229
+ const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
4230
+ return [`${author}: ${body}`];
4231
+ });
4232
+ }
4233
+ function reviewBodyBlockerSummaries(reviews) {
4234
+ return reviews.flatMap((review) => {
4235
+ const login = reviewAuthorLogin(review) ?? "unknown";
4236
+ if (isGreptileGithubLogin(login))
4237
+ return [];
4238
+ const body = review.body?.trim() ?? "";
4239
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4240
+ return [];
4241
+ const state = review.state ? ` (${review.state})` : "";
4242
+ return [`PR review summary by ${login}${state}: ${body}`];
4243
+ });
4244
+ }
4245
+ function signalLabel(signal) {
4246
+ const source = signal.source.replace(/-/g, " ");
4247
+ const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
4248
+ const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
4249
+ return `${source}${author}${sha}`;
4250
+ }
4251
+ function deriveGreptileEvidence(input) {
4252
+ const rawBodies = collectBodies(input);
4253
+ const signals = collectGreptileSignals(input);
4254
+ const trustedSignals = signals.filter((signal) => signal.trusted);
4255
+ const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4256
+ const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4257
+ const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
4258
+ const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
4259
+ const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
4260
+ const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
4261
+ const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
4262
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
4263
+ const signalCanApproveByScore = (signal) => {
4264
+ if (signal.source === "api")
4265
+ return signal.verdict === "approved" || signal.verdict === "completed";
4266
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
4267
+ };
4268
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
4269
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
4270
+ const approvedByScore = !!approvingScoreEntry;
4271
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
4272
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
4273
+ const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
4274
+ const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
4275
+ const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
4276
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
4277
+ const staleBlockingSignals = [];
4278
+ const blockers = [
4279
+ ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
4280
+ ...reviewBodyBlockerSummaries(input.reviews),
4281
+ ...issueLevelBlockerSummaries(input.relevantIssueComments),
4282
+ ...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
4283
+ ...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
4284
+ ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
4285
+ ];
4286
+ const unresolvedComments = [
4287
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
4288
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
4289
+ ];
4290
+ const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
4291
+ const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
4292
+ const completedGreptileCheck = greptileChecks.some((check) => {
4293
+ const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
4294
+ return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
4295
+ });
4296
+ const completedGreptileReview = greptileReviews.some((review) => {
4297
+ const state = String(review.state ?? "").toUpperCase();
4298
+ const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
4299
+ return completedState && review.commit_id === input.currentHeadSha;
4300
+ });
4301
+ const completedGreptileApi = trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && (signal.verdict === "approved" || signal.verdict === "rejected" || signal.verdict === "skipped" || signal.verdict === "failed" || signal.verdict === "completed"));
4302
+ const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
4303
+ const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
4304
+ const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
4305
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
4306
+ const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4307
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4308
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4309
+ const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : approvingSignal?.source === "changed-file-comment" || approvingSignal?.source === "issue-comment" || approvingSignal?.source === "review-thread" ? "github-comment" : greptileReviews.length > 0 && greptileChecks.length > 0 ? "combined" : greptileReviews.length > 0 ? "github-review" : greptileChecks.length > 0 ? "github-check" : signals.some((signal) => signal.source === "pr-body" || signal.source === "pr-title") ? "pr-body" : "missing";
4310
+ return {
4311
+ source,
4312
+ currentHeadSha: input.currentHeadSha,
4313
+ reviewedSha,
4314
+ fresh,
4315
+ completed,
4316
+ approved,
4317
+ score,
4318
+ explicitApproval: approvedByExplicitMapping,
4319
+ blockers,
4320
+ unresolvedComments,
4321
+ rawBodies,
4322
+ signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
4323
+ mapping
4324
+ };
4325
+ }
4326
+ function isGreptileCheckDetail(check) {
4327
+ return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4328
+ }
4329
+ async function collectGreptileCheckDetails(input) {
4330
+ const checkRunsRead = await runJsonArray(input.command, [
4331
+ "api",
4332
+ `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4333
+ "--paginate",
4334
+ "--slurp",
4335
+ "--jq",
4336
+ "map(.check_runs // []) | add // []"
4337
+ ], input.projectRoot);
4338
+ const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4339
+ return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4340
+ }
4341
+ async function collectReviewThreads(input) {
4342
+ const reviewThreads = [];
4343
+ let afterCursor = null;
4344
+ for (let page = 0;page < 100; page += 1) {
4345
+ const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
4346
+ const threadsResponse = await runJsonObject(input.command, [
4347
+ "api",
4348
+ "graphql",
4349
+ "-F",
4350
+ `owner=${input.owner}`,
4351
+ "-F",
4352
+ `name=${input.name}`,
4353
+ "-F",
4354
+ `prNumber=${input.prNumber}`,
4355
+ "-f",
4356
+ `query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { reviewThreads(first: 100, after: ${afterLiteral}) { nodes { id isResolved isOutdated comments(first: 100) { nodes { author { login } body path url createdAt } pageInfo { hasNextPage endCursor } } } pageInfo { hasNextPage endCursor } } } } }`
4357
+ ], input.projectRoot);
4358
+ if (threadsResponse.error) {
4359
+ return { value: reviewThreads, error: threadsResponse.error };
4360
+ }
4361
+ const data = threadsResponse.value.data;
4362
+ const repository = data?.repository;
4363
+ const pullRequest = repository?.pullRequest;
4364
+ const threads = pullRequest?.reviewThreads;
4365
+ const nodes = threads?.nodes;
4366
+ if (!Array.isArray(nodes)) {
4367
+ return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
4368
+ }
4369
+ const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
4370
+ reviewThreads.push(...normalized);
4371
+ const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
4372
+ if (truncatedCommentThread) {
4373
+ return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
4374
+ }
4375
+ const pageInfo = threads?.pageInfo;
4376
+ if (!pageInfo) {
4377
+ if (nodes.length >= 100) {
4378
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
4379
+ }
4380
+ return { value: reviewThreads };
4381
+ }
4382
+ if (pageInfo.hasNextPage !== true) {
4383
+ return { value: reviewThreads };
4384
+ }
4385
+ if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
4386
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
4387
+ }
4388
+ afterCursor = pageInfo.endCursor;
4389
+ }
4390
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
4391
+ }
4392
+ async function collectPrReviewEvidence(input) {
4393
+ const parsed = parseGithubPrUrl(input.prUrl);
4394
+ if (!parsed) {
4395
+ throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
4396
+ }
4397
+ const readErrors = [];
4398
+ const viewRead = await runJsonObject(input.command, [
4399
+ "pr",
4400
+ "view",
4401
+ input.prUrl,
4402
+ "--json",
4403
+ "title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
4404
+ ], input.projectRoot);
4405
+ if (viewRead.error)
4406
+ readErrors.push(viewRead.error);
4407
+ const view = viewRead.value;
4408
+ if (!Array.isArray(view.statusCheckRollup)) {
4409
+ readErrors.push("gh pr view did not return required statusCheckRollup array");
4410
+ }
4411
+ if (!Array.isArray(view.reviews)) {
4412
+ readErrors.push("gh pr view did not return required reviews array");
4413
+ }
4414
+ const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
4415
+ const baseRefName = firstString(view, ["baseRefName"]);
4416
+ const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4417
+ const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4418
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4419
+ if (reviewCommentsRead.error)
4420
+ readErrors.push(reviewCommentsRead.error);
4421
+ const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4422
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4423
+ if (issueCommentsRead.error)
4424
+ readErrors.push(issueCommentsRead.error);
4425
+ const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
4426
+ const reviewThreadsRead = await collectReviewThreads({
4427
+ command: input.command,
4428
+ projectRoot: input.projectRoot,
4429
+ owner: parsed.owner,
4430
+ name: parsed.repo,
4431
+ prNumber: parsed.prNumber
4432
+ });
4433
+ if (reviewThreadsRead.error)
4434
+ readErrors.push(reviewThreadsRead.error);
4435
+ const reviewThreads = reviewThreadsRead.value;
4436
+ const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
4437
+ let greptileCheckDetails = [];
4438
+ if (headSha && greptileRollupChecks.length > 0) {
4439
+ const checkDetailsRead = await collectGreptileCheckDetails({
4440
+ command: input.command,
4441
+ projectRoot: input.projectRoot,
4442
+ repoName: parsed.repoName,
4443
+ headSha
4444
+ });
4445
+ if (checkDetailsRead.error)
4446
+ readErrors.push(checkDetailsRead.error);
4447
+ greptileCheckDetails = checkDetailsRead.value;
4448
+ if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4449
+ readErrors.push("Greptile check details could not be found for the current PR head");
4450
+ }
4451
+ }
4452
+ const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4453
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
4454
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
4455
+ enabled: shouldCollectConfiguredGreptileApi,
4456
+ options: input.greptileApi,
4457
+ repoName: parsed.repoName,
4458
+ prNumber: parsed.prNumber,
4459
+ headSha,
4460
+ baseRefName
4461
+ });
4462
+ readErrors.push(...configuredGreptileApiRead.errors);
4463
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
4464
+ const checkFailures = statusCheckRollup.filter((check) => !isGreptileLabel(checkName(check)) && isFailingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check failed: ${checkName(check)}${check.detailsUrl || check.link ? ` (${check.detailsUrl ?? check.link})` : ""}`);
4465
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
4466
+ const evidenceBase = {
4467
+ title: firstString(view, ["title"]),
4468
+ body: firstString(view, ["body"]),
4469
+ reviews,
4470
+ changedFileReviewComments: reviewComments,
4471
+ relevantIssueComments: issueComments,
4472
+ reviewThreads,
4473
+ checks: checksWithGreptileDetails,
4474
+ currentHeadSha: headSha,
4475
+ apiSignals
4476
+ };
4477
+ const greptile = deriveGreptileEvidence(evidenceBase);
4478
+ return {
4479
+ prUrl: input.prUrl,
4480
+ prNumber: parsed.prNumber,
4481
+ repoName: parsed.repoName,
4482
+ title: evidenceBase.title,
4483
+ body: evidenceBase.body,
4484
+ headSha,
4485
+ headRefName: firstString(view, ["headRefName"]),
4486
+ baseRefName,
4487
+ state: firstString(view, ["state"]),
4488
+ isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
4489
+ mergeable: firstString(view, ["mergeable"]),
4490
+ mergeStateStatus: firstString(view, ["mergeStateStatus"]),
4491
+ reviewDecision: firstString(view, ["reviewDecision"]),
4492
+ reviews,
4493
+ reviewThreads,
4494
+ changedFileReviewComments: reviewComments,
4495
+ relevantIssueComments: issueComments,
4496
+ statusCheckRollup: checksWithGreptileDetails,
4497
+ checkFailures,
4498
+ pendingChecks,
4499
+ readErrors,
4500
+ greptile
4501
+ };
4502
+ }
4503
+ function capGateMessage(value, maxChars = 1200) {
4504
+ const normalized = value.trim();
4505
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
4506
+ [truncated for gate summary; see full evidence artifact]` : normalized;
4507
+ }
4508
+ function evaluateEvidence(evidence) {
4509
+ const reasonDetails = [];
4510
+ const warnings = [];
4511
+ const seen = new Set;
4512
+ const addReason = (reason) => {
4513
+ const capped = { ...reason, message: capGateMessage(reason.message) };
4514
+ const key = `${capped.code}:${capped.message}`;
4515
+ if (seen.has(key))
4516
+ return;
4517
+ seen.add(key);
4518
+ reasonDetails.push(capped);
4519
+ };
4520
+ const greptile = evidence.greptile;
4521
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
4522
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
4523
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
4524
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
4525
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
4526
+ for (const error of evidence.readErrors) {
4527
+ addReason({
4528
+ code: "read_error",
4529
+ reasonClass: "reject",
4530
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
4531
+ suggestedAction: "needs_attention",
4532
+ message: `Required PR evidence surface could not be read completely: ${error}`,
4533
+ headSha: evidence.headSha || null
4534
+ });
4535
+ }
4536
+ if (!evidence.headSha) {
4537
+ addReason({
4538
+ code: "missing_head_sha",
4539
+ reasonClass: "reject",
4540
+ surface: "github",
4541
+ suggestedAction: "needs_attention",
4542
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
4543
+ headSha: null
4544
+ });
4545
+ }
4546
+ for (const failure of evidence.checkFailures) {
4547
+ addReason({
4548
+ code: "ci_failed",
4549
+ reasonClass: "reject",
4550
+ surface: "ci",
4551
+ suggestedAction: "fix",
4552
+ message: failure,
4553
+ headSha: evidence.headSha || null
4554
+ });
4555
+ }
4556
+ for (const pendingCheck of evidence.pendingChecks) {
4557
+ addReason({
4558
+ code: "check_pending",
4559
+ reasonClass: "pending",
4560
+ surface: "ci",
4561
+ suggestedAction: "wait",
4562
+ message: pendingCheck,
4563
+ headSha: evidence.headSha || null
4564
+ });
4565
+ }
4566
+ const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
4567
+ if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
4568
+ addReason({
4569
+ code: "review_decision_blocking",
4570
+ reasonClass: "reject",
4571
+ surface: "review",
4572
+ suggestedAction: "fix",
4573
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
4574
+ headSha: evidence.headSha || null
4575
+ });
4576
+ }
4577
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
4578
+ addReason({
4579
+ code: "review_thread_unresolved",
4580
+ reasonClass: "reject",
4581
+ surface: "review",
4582
+ suggestedAction: "fix",
4583
+ message: thread,
4584
+ headSha: evidence.headSha || null
4585
+ });
4586
+ }
4587
+ if (greptile.mapping === "missing") {
4588
+ addReason({
4589
+ code: "greptile_missing",
4590
+ reasonClass: "pending",
4591
+ surface: "greptile",
4592
+ suggestedAction: "wait",
4593
+ message: "Missing Greptile check/review evidence for this PR.",
4594
+ headSha: evidence.headSha || null
4595
+ });
4596
+ }
4597
+ if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
4598
+ addReason({
4599
+ code: "greptile_stale",
4600
+ reasonClass: "pending",
4601
+ surface: "greptile",
4602
+ suggestedAction: "wait",
4603
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
4604
+ headSha: evidence.headSha || null,
4605
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
4606
+ });
4607
+ }
4608
+ for (const signal of pendingGreptileApiSignals) {
4609
+ addReason({
4610
+ code: "greptile_pending",
4611
+ reasonClass: "pending",
4612
+ surface: "greptile",
4613
+ suggestedAction: "wait",
4614
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
4615
+ headSha: evidence.headSha || null,
4616
+ reviewedSha: signal.reviewedSha ?? null
4617
+ });
4618
+ }
4619
+ for (const signal of unknownGreptileApiSignals) {
4620
+ addReason({
4621
+ code: "greptile_api_status_unknown",
4622
+ reasonClass: "reject",
4623
+ surface: "greptile",
4624
+ suggestedAction: "needs_attention",
4625
+ message: `Greptile API/MCP review status is unknown; merge requires a known terminal APPROVED/COMPLETED 5/5 result or a known conservative status${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
4626
+ headSha: evidence.headSha || null,
4627
+ reviewedSha: signal.reviewedSha ?? null
4628
+ });
4629
+ }
4630
+ if (!greptile.completed) {
4631
+ addReason({
4632
+ code: "greptile_pending",
4633
+ reasonClass: "pending",
4634
+ surface: "greptile",
4635
+ suggestedAction: "wait",
4636
+ message: "Greptile check/review has not completed for the current PR head.",
4637
+ headSha: evidence.headSha || null,
4638
+ reviewedSha: greptile.reviewedSha ?? null
4639
+ });
4640
+ }
4641
+ if (!greptile.fresh) {
4642
+ addReason({
4643
+ code: "greptile_not_current_head",
4644
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4645
+ surface: "greptile",
4646
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4647
+ message: "Greptile approval is not tied to the current PR head SHA.",
4648
+ headSha: evidence.headSha || null,
4649
+ reviewedSha: greptile.reviewedSha ?? null
4650
+ });
4651
+ }
4652
+ if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
4653
+ addReason({
4654
+ code: "greptile_score_not_5",
4655
+ reasonClass: "reject",
4656
+ surface: "greptile",
4657
+ suggestedAction: "fix",
4658
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
4659
+ headSha: evidence.headSha || null,
4660
+ reviewedSha: greptile.reviewedSha ?? null
4661
+ });
4662
+ }
4663
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
4664
+ if (!greptile.score && !hasApprovedMapping) {
4665
+ addReason({
4666
+ code: "greptile_score_missing",
4667
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4668
+ surface: "greptile",
4669
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4670
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
4671
+ headSha: evidence.headSha || null,
4672
+ reviewedSha: greptile.reviewedSha ?? null
4673
+ });
4674
+ }
4675
+ if (greptile.mapping === "unproven") {
4676
+ addReason({
4677
+ code: "greptile_mapping_unproven",
4678
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4679
+ surface: "greptile",
4680
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4681
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
4682
+ headSha: evidence.headSha || null,
4683
+ reviewedSha: greptile.reviewedSha ?? null
4684
+ });
4685
+ }
4686
+ for (const blocker of greptile.blockers) {
4687
+ addReason({
4688
+ code: "greptile_blocker_text",
4689
+ reasonClass: "reject",
4690
+ surface: "greptile",
4691
+ suggestedAction: "fix",
4692
+ message: `Greptile/blocker text: ${blocker}`,
4693
+ headSha: evidence.headSha || null,
4694
+ reviewedSha: greptile.reviewedSha ?? null
4695
+ });
4696
+ }
4697
+ for (const comment of greptile.unresolvedComments) {
4698
+ addReason({
4699
+ code: "greptile_unresolved_comment",
4700
+ reasonClass: "reject",
4701
+ surface: "greptile",
4702
+ suggestedAction: "fix",
4703
+ message: comment,
4704
+ headSha: evidence.headSha || null,
4705
+ reviewedSha: greptile.reviewedSha ?? null
4706
+ });
4707
+ }
4708
+ if (!greptile.approved)
4709
+ warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
4710
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
4711
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
4712
+ }
4713
+ function evaluateStrictPrMergeGate(evidence) {
4714
+ const evaluated = evaluateEvidence(evidence);
4715
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
4716
+ return {
4717
+ approved,
4718
+ pending: evaluated.pending,
4719
+ reasons: evaluated.reasons,
4720
+ reasonDetails: evaluated.reasonDetails,
4721
+ warnings: evaluated.warnings,
4722
+ actionableFeedback: evaluated.reasons,
4723
+ evidence
4724
+ };
4725
+ }
4726
+
4727
+ // packages/runtime/src/control-plane/native/verifier.ts
3608
4728
  async function verifyTask(options) {
3609
4729
  const paths = resolveHarnessPaths(options.projectRoot);
3610
4730
  const taskId = options.taskId;
@@ -4400,7 +5520,8 @@ async function runGreptileReviewForPr(options) {
4400
5520
  }
4401
5521
  };
4402
5522
  }
4403
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
5523
+ const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
5524
+ 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)) {
4404
5525
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
4405
5526
  return {
4406
5527
  verdict: "REJECT",
@@ -4416,44 +5537,79 @@ async function runGreptileReviewForPr(options) {
4416
5537
  }
4417
5538
  };
4418
5539
  }
4419
- if (score) {
4420
- if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
4421
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
4422
- return {
4423
- verdict: "REJECT",
4424
- feedback,
4425
- reasons,
4426
- warnings,
4427
- rawPayload: {
4428
- pr: options.prState,
4429
- codeReviews: reviewsPayload,
4430
- selectedReview,
4431
- reviewDetails,
4432
- comments: commentsPayload,
4433
- score
4434
- }
4435
- };
4436
- }
4437
- if (score.scale === 5 && score.value <= 2) {
4438
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
4439
- return {
4440
- verdict: "REJECT",
4441
- feedback,
4442
- reasons,
4443
- warnings,
4444
- rawPayload: {
4445
- pr: options.prState,
4446
- codeReviews: reviewsPayload,
4447
- selectedReview,
4448
- reviewDetails,
4449
- comments: commentsPayload,
4450
- score
5540
+ if (score?.scale === 5 && score.value < 5) {
5541
+ reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
5542
+ return {
5543
+ verdict: "REJECT",
5544
+ feedback,
5545
+ reasons,
5546
+ warnings,
5547
+ rawPayload: {
5548
+ pr: options.prState,
5549
+ codeReviews: reviewsPayload,
5550
+ selectedReview,
5551
+ reviewDetails,
5552
+ comments: commentsPayload,
5553
+ score
5554
+ }
5555
+ };
5556
+ }
5557
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
5558
+ let strictGate = null;
5559
+ try {
5560
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
5561
+ projectRoot: options.projectRoot,
5562
+ taskId: options.taskId,
5563
+ prUrl,
5564
+ apiSignals: [{
5565
+ id: selectedReview.id,
5566
+ body: reviewBody,
5567
+ reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
5568
+ status: selectedReview.status
5569
+ }]
5570
+ });
5571
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
5572
+ } catch (error) {
5573
+ reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
5574
+ return {
5575
+ verdict: "REJECT",
5576
+ feedback,
5577
+ reasons,
5578
+ warnings,
5579
+ rawPayload: {
5580
+ pr: options.prState,
5581
+ codeReviews: reviewsPayload,
5582
+ selectedReview,
5583
+ reviewDetails,
5584
+ comments: commentsPayload,
5585
+ score
5586
+ }
5587
+ };
5588
+ }
5589
+ if (!strictGate.approved) {
5590
+ return {
5591
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
5592
+ feedback,
5593
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
5594
+ warnings: [...warnings, ...strictGate.warnings],
5595
+ rawPayload: {
5596
+ pr: options.prState,
5597
+ codeReviews: reviewsPayload,
5598
+ selectedReview,
5599
+ reviewDetails,
5600
+ comments: commentsPayload,
5601
+ score,
5602
+ strictGate: {
5603
+ approved: strictGate.approved,
5604
+ pending: strictGate.pending,
5605
+ reasons: strictGate.reasons,
5606
+ reasonDetails: strictGate.reasonDetails,
5607
+ warnings: strictGate.warnings,
5608
+ greptile: strictGate.evidence.greptile,
5609
+ readErrors: strictGate.evidence.readErrors
4451
5610
  }
4452
- };
4453
- }
4454
- if (score.scale === 5 && score.value < 5) {
4455
- warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
4456
- }
5611
+ }
5612
+ };
4457
5613
  }
4458
5614
  return {
4459
5615
  verdict: "APPROVE",
@@ -4465,7 +5621,16 @@ async function runGreptileReviewForPr(options) {
4465
5621
  codeReviews: reviewsPayload,
4466
5622
  selectedReview,
4467
5623
  reviewDetails,
4468
- comments: commentsPayload
5624
+ comments: commentsPayload,
5625
+ strictGate: {
5626
+ approved: strictGate.approved,
5627
+ pending: strictGate.pending,
5628
+ reasons: strictGate.reasons,
5629
+ reasonDetails: strictGate.reasonDetails,
5630
+ warnings: strictGate.warnings,
5631
+ greptile: strictGate.evidence.greptile,
5632
+ readErrors: strictGate.evidence.readErrors
5633
+ }
4469
5634
  }
4470
5635
  };
4471
5636
  }
@@ -4489,7 +5654,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4489
5654
  let threads = [];
4490
5655
  let actionableThreads = [];
4491
5656
  let checkRollup = [];
4492
- let checkState = { pending: false, completed: false };
5657
+ let checkState2 = { pending: false, completed: false };
4493
5658
  for (let attempt = 0;; attempt += 1) {
4494
5659
  reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
4495
5660
  selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
@@ -4498,15 +5663,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4498
5663
  threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
4499
5664
  actionableThreads = filterActionableGithubGreptileThreads(threads);
4500
5665
  checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
4501
- checkState = classifyGithubGreptileCheckState(checkRollup);
4502
- const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
5666
+ checkState2 = classifyGithubGreptileCheckState(checkRollup);
5667
+ const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
4503
5668
  if (!shouldContinueGithubGreptileFallbackPolling({
4504
5669
  attempt,
4505
5670
  pollAttempts: options.pollAttempts,
4506
- checkState,
5671
+ checkState: checkState2,
4507
5672
  fallbackReview,
4508
5673
  selectedReview,
4509
- approvedViaReviewedAncestor: approvedViaReviewedAncestor2
5674
+ approvedViaReviewedAncestor
4510
5675
  })) {
4511
5676
  break;
4512
5677
  }
@@ -4534,7 +5699,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4534
5699
  ].filter(Boolean).join(`
4535
5700
  `);
4536
5701
  const warnings = buildGithubGreptileFallbackWarnings(options);
4537
- if (checkState.pending) {
5702
+ if (checkState2.pending) {
4538
5703
  return {
4539
5704
  verdict: "SKIP",
4540
5705
  feedback,
@@ -4545,34 +5710,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4545
5710
  rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4546
5711
  };
4547
5712
  }
4548
- const approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
4549
- if (!fallbackReview) {
4550
- if (approvedViaCompletedCheck) {
4551
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no GitHub Greptile review object, but the Greptile check completed successfully and no unresolved Greptile threads remain.`);
4552
- return {
4553
- verdict: "APPROVE",
4554
- feedback,
4555
- reasons: [],
4556
- warnings,
4557
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4558
- };
4559
- }
4560
- return {
4561
- verdict: "SKIP",
4562
- feedback,
4563
- reasons: [
4564
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
4565
- ],
4566
- warnings,
4567
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4568
- };
4569
- }
4570
- const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
4571
- if (actionableThreads.length > 0) {
5713
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
5714
+ let strictGate;
5715
+ try {
5716
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
5717
+ projectRoot: options.projectRoot,
5718
+ taskId: options.taskId,
5719
+ prUrl
5720
+ });
5721
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
5722
+ } catch (error) {
4572
5723
  return {
4573
5724
  verdict: "REJECT",
4574
5725
  feedback,
4575
- reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
5726
+ reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
4576
5727
  warnings,
4577
5728
  rawPayload: {
4578
5729
  pr: options.prState,
@@ -4585,44 +5736,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4585
5736
  }
4586
5737
  };
4587
5738
  }
4588
- if (!selectedReview && !approvedViaReviewedAncestor) {
4589
- if (approvedViaCompletedCheck) {
4590
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
4591
- return {
4592
- verdict: "APPROVE",
4593
- feedback,
4594
- reasons: [],
4595
- warnings,
4596
- rawPayload: {
4597
- pr: options.prState,
4598
- selectedReview: fallbackReview,
4599
- reviews,
4600
- threads,
4601
- checkRollup,
4602
- ...buildGithubGreptileFallbackRawPayload(options)
4603
- }
4604
- };
4605
- }
5739
+ if (!strictGate.approved) {
4606
5740
  return {
4607
- verdict: "SKIP",
5741
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
4608
5742
  feedback,
4609
- reasons: [
4610
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
4611
- ],
4612
- warnings,
5743
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
5744
+ warnings: [...warnings, ...strictGate.warnings],
4613
5745
  rawPayload: {
4614
5746
  pr: options.prState,
4615
5747
  selectedReview: fallbackReview,
4616
5748
  reviews,
4617
5749
  threads,
4618
5750
  checkRollup,
5751
+ actionableThreads,
5752
+ strictGate: {
5753
+ approved: strictGate.approved,
5754
+ pending: strictGate.pending,
5755
+ reasons: strictGate.reasons,
5756
+ reasonDetails: strictGate.reasonDetails,
5757
+ warnings: strictGate.warnings,
5758
+ greptile: strictGate.evidence.greptile
5759
+ },
4619
5760
  ...buildGithubGreptileFallbackRawPayload(options)
4620
5761
  }
4621
5762
  };
4622
5763
  }
4623
- if (approvedViaReviewedAncestor) {
4624
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
4625
- }
4626
5764
  return {
4627
5765
  verdict: "APPROVE",
4628
5766
  feedback,
@@ -4634,6 +5772,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4634
5772
  reviews,
4635
5773
  threads,
4636
5774
  checkRollup,
5775
+ strictGate: {
5776
+ approved: strictGate.approved,
5777
+ pending: strictGate.pending,
5778
+ reasons: strictGate.reasons,
5779
+ reasonDetails: strictGate.reasonDetails,
5780
+ warnings: strictGate.warnings,
5781
+ greptile: strictGate.evidence.greptile
5782
+ },
4637
5783
  ...buildGithubGreptileFallbackRawPayload(options)
4638
5784
  }
4639
5785
  };
@@ -4746,19 +5892,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
4746
5892
  if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
4747
5893
  return true;
4748
5894
  }
4749
- return isGreptileReviewTerminal(existingReview.status);
5895
+ return false;
4750
5896
  }
4751
5897
  function shouldContinueGreptileMcpPolling(options) {
4752
5898
  if (options.githubCheckState.completed) {
4753
5899
  return false;
4754
5900
  }
5901
+ if (options.attempt + 1 >= options.pollAttempts) {
5902
+ return false;
5903
+ }
4755
5904
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
4756
5905
  return true;
4757
5906
  }
4758
- return options.attempt + 1 < options.pollAttempts;
5907
+ return true;
4759
5908
  }
4760
5909
  function shouldContinueGithubGreptileFallbackPolling(options) {
4761
5910
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
5911
+ if (options.attempt + 1 >= options.pollAttempts) {
5912
+ return false;
5913
+ }
4762
5914
  if (waitingForVisiblePendingReview) {
4763
5915
  return true;
4764
5916
  }
@@ -4819,6 +5971,20 @@ function runGhJson(projectRoot, args) {
4819
5971
  throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
4820
5972
  }
4821
5973
  }
5974
+ async function collectStrictPrEvidenceForVerifier(input) {
5975
+ return collectPrReviewEvidence({
5976
+ projectRoot: input.projectRoot,
5977
+ prUrl: input.prUrl,
5978
+ taskId: input.taskId,
5979
+ runId: "verifier",
5980
+ cycle: 0,
5981
+ apiSignals: input.apiSignals ?? [],
5982
+ command: async (args, options) => {
5983
+ const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
5984
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
5985
+ }
5986
+ });
5987
+ }
4822
5988
  function deriveRepoName(projectRoot, prState) {
4823
5989
  const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
4824
5990
  if (fromUrl?.[1]) {
@@ -4833,8 +5999,9 @@ function resolvePrHeadSha(projectRoot, prState) {
4833
5999
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
4834
6000
  return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
4835
6001
  }
4836
- function isGreptileGithubLogin(login) {
4837
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
6002
+ function isGreptileGithubLogin2(login) {
6003
+ const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
6004
+ return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
4838
6005
  }
4839
6006
  function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
4840
6007
  const matching = sortGithubGreptileReviews(reviews);
@@ -4851,7 +6018,7 @@ function pickLatestGithubGreptileReview(reviews) {
4851
6018
  return sortGithubGreptileReviews(reviews)[0] || null;
4852
6019
  }
4853
6020
  function sortGithubGreptileReviews(reviews) {
4854
- return reviews.filter((review) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
6021
+ return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
4855
6022
  }
4856
6023
  function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
4857
6024
  const response = runGhJson(projectRoot, [
@@ -4924,32 +6091,6 @@ function classifyGithubGreptileCheckState(checks) {
4924
6091
  }
4925
6092
  return { pending: false, completed: false };
4926
6093
  }
4927
- function isGithubGreptileCheckApproved(checks) {
4928
- const greptileChecks = checks.filter((check) => {
4929
- const label = (check.name || check.context || "").toLowerCase();
4930
- return label.includes("greptile");
4931
- });
4932
- if (greptileChecks.length === 0) {
4933
- return false;
4934
- }
4935
- for (const check of greptileChecks) {
4936
- if ((check.__typename || "") === "CheckRun") {
4937
- if ((check.status || "").toUpperCase() !== "COMPLETED") {
4938
- return false;
4939
- }
4940
- const conclusion = (check.conclusion || "").toUpperCase();
4941
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
4942
- return false;
4943
- }
4944
- continue;
4945
- }
4946
- const state = (check.state || "").toUpperCase();
4947
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
4948
- return false;
4949
- }
4950
- }
4951
- return true;
4952
- }
4953
6094
  function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
4954
6095
  const [owner, name] = repoName.split("/");
4955
6096
  if (!owner || !name) {
@@ -4975,7 +6116,7 @@ function filterActionableGithubGreptileThreads(threads) {
4975
6116
  return [];
4976
6117
  }
4977
6118
  const comments = thread.comments?.nodes || [];
4978
- const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
6119
+ const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
4979
6120
  if (!latestGreptileComment?.path?.trim()) {
4980
6121
  return [];
4981
6122
  }
@@ -4997,11 +6138,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
4997
6138
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
4998
6139
  return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
4999
6140
  }
5000
- function stripHtml(input) {
5001
- return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
5002
-
5003
- `).trim();
5004
- }
5005
6141
  function summarizeComment(input) {
5006
6142
  const text = stripHtml(input).replace(/\s+/g, " ").trim();
5007
6143
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
@@ -5010,31 +6146,14 @@ function asGreptileInfrastructureWarning(reason) {
5010
6146
  return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
5011
6147
  }
5012
6148
  function isAiReviewApproved(input) {
6149
+ if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
6150
+ return false;
6151
+ }
5013
6152
  if (input.reviewMode !== "required") {
5014
6153
  return true;
5015
6154
  }
5016
6155
  return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
5017
6156
  }
5018
- function parseGreptileScore(input) {
5019
- const text = stripHtml(input);
5020
- const patterns = [
5021
- /confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
5022
- /\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
5023
- /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
5024
- ];
5025
- for (const pattern of patterns) {
5026
- const match = pattern.exec(text);
5027
- if (!match) {
5028
- continue;
5029
- }
5030
- const value = Number.parseInt(match[1] || "", 10);
5031
- const scale = Number.parseInt(match[2] || "", 10);
5032
- if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
5033
- return { value, scale };
5034
- }
5035
- }
5036
- return null;
5037
- }
5038
6157
 
5039
6158
  // packages/runtime/src/control-plane/provider/runtime-instructions.ts
5040
6159
  var CLAUDE_ROUTER_TOOL_NAMES = [
@@ -5983,12 +7102,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
5983
7102
  "task-result.json",
5984
7103
  "validation-summary.json"
5985
7104
  ]);
5986
- function resolveHostRigBinDir(root) {
5987
- return resolve24(root, ".rig", "bin");
5988
- }
5989
7105
  function isRuntimeGatewayGitPath(candidate) {
5990
7106
  return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
5991
7107
  }
7108
+ function isRuntimeGatewayGhPath(candidate) {
7109
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
7110
+ }
5992
7111
  function resolveOptionalMonorepoRoot(projectRoot) {
5993
7112
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
5994
7113
  if (runtimeWorkspace && existsSync21(resolve24(runtimeWorkspace, ".git"))) {
@@ -6023,6 +7142,9 @@ function resolveGitBinary(projectRoot) {
6023
7142
  }
6024
7143
  return "git";
6025
7144
  }
7145
+ function escapeRegExp2(value) {
7146
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7147
+ }
6026
7148
  function safeCurrentTaskId(projectRoot) {
6027
7149
  try {
6028
7150
  const taskId = currentTaskId(projectRoot);
@@ -6181,17 +7303,15 @@ function gitOpenPr(options) {
6181
7303
  const target = options.target || (taskId ? "monorepo" : "project");
6182
7304
  let repoRoot = options.projectRoot;
6183
7305
  let repoLabel = "project-rig";
6184
- let defaultBase = process.env.RIG_PR_BASE_PROJECT || "main";
7306
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
6185
7307
  if (target === "monorepo") {
6186
7308
  repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot2(options.projectRoot);
6187
7309
  repoLabel = "monorepo";
6188
- defaultBase = process.env.RIG_PR_BASE_MONOREPO || "main";
6189
7310
  if (taskId) {
6190
7311
  gitSyncBranch(options.projectRoot, taskId, "monorepo");
6191
7312
  }
6192
7313
  } else if (taskId) {
6193
7314
  gitSyncBranch(options.projectRoot, taskId, "project");
6194
- defaultBase = inferProjectBase(options.projectRoot, defaultBase);
6195
7315
  }
6196
7316
  if (!existsSync21(resolve24(repoRoot, ".git"))) {
6197
7317
  throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
@@ -6200,9 +7320,9 @@ function gitOpenPr(options) {
6200
7320
  if (!branch || branch === "HEAD") {
6201
7321
  throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
6202
7322
  }
6203
- const base = options.base || defaultBase;
6204
7323
  const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
6205
7324
  const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
7325
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
6206
7326
  refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
6207
7327
  let reviewer = (options.reviewer || "").trim();
6208
7328
  let reviewerSource = reviewer ? "flag" : undefined;
@@ -6238,10 +7358,11 @@ function gitOpenPr(options) {
6238
7358
  "",
6239
7359
  "## Task",
6240
7360
  `- beads: ${taskId || "n/a"}`,
7361
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
6241
7362
  "",
6242
7363
  "## Review",
6243
7364
  "- Completion verification will run validation, verifier review, and PR policy checks.",
6244
- "- When repository policy allows it, Rig enables GitHub auto-merge after approval."
7365
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
6245
7366
  ].join(`
6246
7367
  `);
6247
7368
  const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
@@ -6329,6 +7450,30 @@ function gitOpenPr(options) {
6329
7450
  }
6330
7451
  return result;
6331
7452
  }
7453
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
7454
+ const lines = [];
7455
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
7456
+ if (runId) {
7457
+ lines.push(`- Run: ${runId}`);
7458
+ }
7459
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
7460
+ if (closeout) {
7461
+ lines.push(`- ${closeout}`);
7462
+ }
7463
+ return lines;
7464
+ }
7465
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
7466
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
7467
+ if (sourceIssueId) {
7468
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
7469
+ if (match?.[1] && match[2]) {
7470
+ const sourceRepo = match[1];
7471
+ const issueNumber = match[2];
7472
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
7473
+ }
7474
+ }
7475
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
7476
+ }
6332
7477
  function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
6333
7478
  const view = runCapture2(withGhRepo([
6334
7479
  gh,
@@ -6479,32 +7624,19 @@ function resolveGithubCliBinary(projectRoot) {
6479
7624
  if (explicit) {
6480
7625
  candidates.add(explicit);
6481
7626
  }
7627
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
7628
+ candidates.add(candidate);
7629
+ }
6482
7630
  const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
6483
7631
  for (const entry of explicitPathEntries) {
6484
7632
  candidates.add(resolve24(entry, "gh"));
6485
7633
  }
6486
- const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
6487
- if (hostProjectRoot) {
6488
- candidates.add(resolve24(resolveHostRigBinDir(hostProjectRoot), "gh"));
6489
- }
6490
- candidates.add(resolve24(resolveHostRigBinDir(projectRoot), "gh"));
6491
- const runtimeContext = loadRuntimeContextFromEnv();
6492
- if (runtimeContext?.binDir) {
6493
- candidates.add(resolve24(runtimeContext.binDir, "gh"));
6494
- }
6495
- const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
6496
- if (runtimeHome) {
6497
- candidates.add(resolve24(runtimeHome, "bin", "gh"));
6498
- }
6499
- for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
6500
- candidates.add(candidate);
6501
- }
6502
7634
  const bunResolved = Bun.which("gh");
6503
7635
  if (bunResolved) {
6504
7636
  candidates.add(bunResolved);
6505
7637
  }
6506
7638
  for (const candidate of candidates) {
6507
- if (candidate && existsSync21(candidate)) {
7639
+ if (candidate && existsSync21(candidate) && !isRuntimeGatewayGhPath(candidate)) {
6508
7640
  return candidate;
6509
7641
  }
6510
7642
  }
@@ -6653,6 +7785,32 @@ function withGhRepo(command, repoNameWithOwner) {
6653
7785
  }
6654
7786
  return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
6655
7787
  }
7788
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
7789
+ const remote = remoteName || "origin";
7790
+ const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
7791
+ if (symbolic.exitCode === 0) {
7792
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp2(remote)}/`), "");
7793
+ if (ref && ref !== "HEAD") {
7794
+ return ref;
7795
+ }
7796
+ }
7797
+ const lsRemote = runCapture2(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
7798
+ if (lsRemote.exitCode === 0) {
7799
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
7800
+ if (match?.[1]) {
7801
+ return match[1];
7802
+ }
7803
+ }
7804
+ const gh = resolveGithubCliBinary(projectRoot);
7805
+ if (gh && repoNameWithOwner) {
7806
+ const api = runCapture2(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
7807
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
7808
+ if (branch) {
7809
+ return branch;
7810
+ }
7811
+ }
7812
+ return fallback;
7813
+ }
6656
7814
  function inferProjectBase(projectRoot, fallback) {
6657
7815
  const containing = runCapture2(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
6658
7816
  if (containing.exitCode !== 0) {
@@ -7034,6 +8192,10 @@ function runtimeGitEnv(projectRoot) {
7034
8192
  }
7035
8193
  env[key] = value;
7036
8194
  }
8195
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
8196
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
8197
+ env.GITHUB_TOKEN = rigGithubToken;
8198
+ }
7037
8199
  if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
7038
8200
  env.GITHUB_TOKEN = env.GH_TOKEN;
7039
8201
  }
@@ -7057,6 +8219,13 @@ function runtimeGitEnv(projectRoot) {
7057
8219
  if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
7058
8220
  env.GH_TOKEN = env.GITHUB_TOKEN;
7059
8221
  }
8222
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
8223
+ if (gitHubToken) {
8224
+ env.RIG_GITHUB_TOKEN = gitHubToken;
8225
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
8226
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
8227
+ applyGitHubCredentialHelperEnv(env);
8228
+ }
7060
8229
  if (runtimeKnownHosts && existsSync21(runtimeKnownHosts)) {
7061
8230
  const sshParts = [
7062
8231
  "ssh",
@@ -7073,6 +8242,14 @@ function runtimeGitEnv(projectRoot) {
7073
8242
  }
7074
8243
  return Object.keys(env).length > 0 ? env : undefined;
7075
8244
  }
8245
+ function applyGitHubCredentialHelperEnv(env) {
8246
+ env.GIT_TERMINAL_PROMPT = "0";
8247
+ env.GIT_CONFIG_COUNT = "2";
8248
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
8249
+ env.GIT_CONFIG_VALUE_0 = "";
8250
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
8251
+ 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';
8252
+ }
7076
8253
  function loadPersistedRuntimeSecrets(runtimeRoot) {
7077
8254
  if (!runtimeRoot) {
7078
8255
  return {};