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