@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
@@ -1089,8 +1089,8 @@ function githubStatusFor(issue) {
1089
1089
  return "open";
1090
1090
  }
1091
1091
  function selectedGitHubEnv() {
1092
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
1093
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
1092
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1093
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1094
1094
  }
1095
1095
  function ghSpawnOptions() {
1096
1096
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -3739,6 +3739,1126 @@ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
3739
3739
  // packages/runtime/src/control-plane/native/git-ops.ts
3740
3740
  import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
3741
3741
  import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve21 } from "path";
3742
+
3743
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
3744
+ function parseJsonObject(value) {
3745
+ if (!value?.trim())
3746
+ return { value: {}, error: "empty JSON output" };
3747
+ try {
3748
+ const parsed = JSON.parse(value);
3749
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
3750
+ } catch (error) {
3751
+ return { value: {}, error: error instanceof Error ? error.message : String(error) };
3752
+ }
3753
+ }
3754
+ function flattenPaginatedArray(value) {
3755
+ if (!Array.isArray(value))
3756
+ return null;
3757
+ if (value.every((entry) => Array.isArray(entry))) {
3758
+ return value.flatMap((entry) => entry);
3759
+ }
3760
+ return value;
3761
+ }
3762
+ function parseJsonArray(value) {
3763
+ if (!value?.trim())
3764
+ return { value: [], error: "empty JSON output" };
3765
+ try {
3766
+ const parsed = JSON.parse(value);
3767
+ const flattened = flattenPaginatedArray(parsed);
3768
+ return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3769
+ } catch (error) {
3770
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
3771
+ }
3772
+ }
3773
+ function parseGithubPrUrl(prUrl) {
3774
+ const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
3775
+ if (!match)
3776
+ return null;
3777
+ const prNumber = Number.parseInt(match[3], 10);
3778
+ if (!Number.isFinite(prNumber))
3779
+ return null;
3780
+ return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
3781
+ }
3782
+ function checkName(check) {
3783
+ return String(check.name ?? check.context ?? "").trim();
3784
+ }
3785
+ function checkState(check) {
3786
+ return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
3787
+ }
3788
+ function isGreptileLabel(value) {
3789
+ return String(value ?? "").toLowerCase().includes("greptile");
3790
+ }
3791
+ function isGreptileGithubLogin(value) {
3792
+ const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
3793
+ return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
3794
+ }
3795
+ function isPassingCheck(check) {
3796
+ const state = checkState(check);
3797
+ return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
3798
+ }
3799
+ function isPendingCheck(check) {
3800
+ const state = checkState(check);
3801
+ return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
3802
+ }
3803
+ function isFailingCheck(check) {
3804
+ const state = checkState(check);
3805
+ return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
3806
+ }
3807
+ function wildcardToRegExp(pattern) {
3808
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3809
+ return new RegExp(`^${escaped}$`, "i");
3810
+ }
3811
+ function isAllowedFailure(name, allowedFailures) {
3812
+ return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
3813
+ }
3814
+ function greptileScorePatterns() {
3815
+ return [
3816
+ /\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
3817
+ /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
3818
+ /\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
3819
+ ];
3820
+ }
3821
+ function parseGreptileScores(input) {
3822
+ const text = stripHtml(input);
3823
+ const seen = new Set;
3824
+ const scores = [];
3825
+ for (const pattern of greptileScorePatterns()) {
3826
+ for (const match of text.matchAll(pattern)) {
3827
+ const value = Number.parseInt(match[1] || "", 10);
3828
+ const scale = Number.parseInt(match[2] || "", 10);
3829
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
3830
+ continue;
3831
+ const raw = match[0] || `${value}/${scale}`;
3832
+ const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
3833
+ if (seen.has(key))
3834
+ continue;
3835
+ seen.add(key);
3836
+ scores.push({ value, scale, raw });
3837
+ }
3838
+ }
3839
+ return scores;
3840
+ }
3841
+ function parseGreptileScore(input) {
3842
+ return parseGreptileScores(input)[0] ?? null;
3843
+ }
3844
+ function stripHtml(input) {
3845
+ return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
3846
+
3847
+ `).trim();
3848
+ }
3849
+ function containsBlockerText(input) {
3850
+ const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
3851
+ 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);
3852
+ }
3853
+ function isStrictFiveOfFive(score) {
3854
+ return score.value === 5 && score.scale === 5;
3855
+ }
3856
+ function containsConflictingScoreText(input) {
3857
+ return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
3858
+ }
3859
+ function greptileStatusVerdict(status) {
3860
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
3861
+ if (!normalized)
3862
+ return null;
3863
+ if (["APPROVE", "APPROVED"].includes(normalized))
3864
+ return "approved";
3865
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
3866
+ return "rejected";
3867
+ if (["SKIP", "SKIPPED"].includes(normalized))
3868
+ return "skipped";
3869
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
3870
+ return "failed";
3871
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
3872
+ return "pending";
3873
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
3874
+ return "completed";
3875
+ return null;
3876
+ }
3877
+ function isBlockingGreptileVerdict(verdict) {
3878
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
3879
+ }
3880
+ function greptileRequestTimeoutMs(env) {
3881
+ const fallback = 30000;
3882
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
3883
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
3884
+ }
3885
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
3886
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
3887
+ return null;
3888
+ const record = entry;
3889
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
3890
+ if (!id)
3891
+ return null;
3892
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
3893
+ return {
3894
+ id,
3895
+ status: typeof record.status === "string" ? record.status : null,
3896
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
3897
+ body: typeof record.body === "string" ? record.body : null,
3898
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
3899
+ };
3900
+ }
3901
+ function uniqueGreptileCodeReviews(reviews) {
3902
+ const seen = new Set;
3903
+ const unique2 = [];
3904
+ for (const review of reviews) {
3905
+ if (seen.has(review.id))
3906
+ continue;
3907
+ seen.add(review.id);
3908
+ unique2.push(review);
3909
+ }
3910
+ return unique2;
3911
+ }
3912
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
3913
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
3914
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
3915
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
3916
+ const latest = sorted.slice(0, 1);
3917
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
3918
+ }
3919
+ function greptileApiSignalFromCodeReview(review, details) {
3920
+ const selected = details ?? review;
3921
+ return {
3922
+ id: selected.id || review.id,
3923
+ body: selected.body ?? review.body ?? null,
3924
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
3925
+ status: selected.status ?? review.status ?? null
3926
+ };
3927
+ }
3928
+ async function callGreptileMcpToolForGate(input) {
3929
+ const controller = new AbortController;
3930
+ const timeoutId = setTimeout(() => {
3931
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
3932
+ }, input.timeoutMs);
3933
+ let response;
3934
+ try {
3935
+ response = await input.fetchFn(input.apiBase, {
3936
+ method: "POST",
3937
+ headers: {
3938
+ Authorization: `Bearer ${input.apiKey}`,
3939
+ "Content-Type": "application/json"
3940
+ },
3941
+ body: JSON.stringify({
3942
+ jsonrpc: "2.0",
3943
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
3944
+ method: "tools/call",
3945
+ params: { name: input.name, arguments: input.args }
3946
+ }),
3947
+ signal: controller.signal
3948
+ });
3949
+ } catch (error) {
3950
+ if (controller.signal.aborted) {
3951
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
3952
+ }
3953
+ throw error;
3954
+ } finally {
3955
+ clearTimeout(timeoutId);
3956
+ }
3957
+ const raw = await response.text();
3958
+ if (!response.ok) {
3959
+ throw new Error(`HTTP ${response.status}: ${raw}`);
3960
+ }
3961
+ let envelope;
3962
+ try {
3963
+ envelope = JSON.parse(raw);
3964
+ } catch {
3965
+ throw new Error(`Malformed MCP response: ${raw}`);
3966
+ }
3967
+ if (envelope.error?.message) {
3968
+ throw new Error(envelope.error.message);
3969
+ }
3970
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
3971
+ `).trim();
3972
+ if (!text) {
3973
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
3974
+ }
3975
+ return text;
3976
+ }
3977
+ async function callGreptileMcpToolJsonForGate(input) {
3978
+ const text = await callGreptileMcpToolForGate(input);
3979
+ try {
3980
+ return JSON.parse(text);
3981
+ } catch {
3982
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
3983
+ }
3984
+ }
3985
+ async function collectConfiguredGreptileApiSignals(input) {
3986
+ if (!input.enabled || input.options?.enabled === false) {
3987
+ return { signals: [], errors: [] };
3988
+ }
3989
+ const env = input.options?.env ?? process.env;
3990
+ const secrets = resolveRuntimeSecrets(env);
3991
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
3992
+ if (!apiKey) {
3993
+ return { signals: [], errors: [] };
3994
+ }
3995
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
3996
+ if (typeof fetchFn !== "function") {
3997
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
3998
+ }
3999
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
4000
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
4001
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
4002
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
4003
+ const timeoutMs = greptileRequestTimeoutMs(env);
4004
+ try {
4005
+ const listPayload = await callGreptileMcpToolJsonForGate({
4006
+ apiBase,
4007
+ apiKey,
4008
+ name: "list_code_reviews",
4009
+ args: {
4010
+ name: repository,
4011
+ remote,
4012
+ defaultBranch,
4013
+ prNumber: input.prNumber,
4014
+ limit: 20
4015
+ },
4016
+ timeoutMs,
4017
+ fetchFn
4018
+ });
4019
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
4020
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
4021
+ const signals = [];
4022
+ for (const review of selectedReviews) {
4023
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
4024
+ apiBase,
4025
+ apiKey,
4026
+ name: "get_code_review",
4027
+ args: { codeReviewId: review.id },
4028
+ timeoutMs,
4029
+ fetchFn
4030
+ });
4031
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
4032
+ signals.push(greptileApiSignalFromCodeReview(review, details));
4033
+ }
4034
+ return { signals, errors: [] };
4035
+ } catch (error) {
4036
+ return {
4037
+ signals: [],
4038
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
4039
+ };
4040
+ }
4041
+ }
4042
+ function firstString(record, keys) {
4043
+ for (const key of keys) {
4044
+ const value = record[key];
4045
+ if (typeof value === "string")
4046
+ return value;
4047
+ }
4048
+ return "";
4049
+ }
4050
+ function arrayField(record, key) {
4051
+ const value = record[key];
4052
+ return Array.isArray(value) ? value : [];
4053
+ }
4054
+ async function runJsonArray(command, args, cwd) {
4055
+ const result = await command(args, { cwd });
4056
+ const label = `gh ${args.join(" ")}`;
4057
+ if (result.exitCode !== 0) {
4058
+ return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4059
+ }
4060
+ const parsed = parseJsonArray(result.stdout);
4061
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4062
+ }
4063
+ async function runJsonObject(command, args, cwd) {
4064
+ const result = await command(args, { cwd });
4065
+ const label = `gh ${args.join(" ")}`;
4066
+ if (result.exitCode !== 0) {
4067
+ return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4068
+ }
4069
+ const parsed = parseJsonObject(result.stdout);
4070
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4071
+ }
4072
+ function normalizeStatusCheck(entry) {
4073
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4074
+ return null;
4075
+ const record = entry;
4076
+ const name = firstString(record, ["name", "context"]);
4077
+ if (!name.trim())
4078
+ return null;
4079
+ const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
4080
+ const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
4081
+ return {
4082
+ __typename: typeof record.__typename === "string" ? record.__typename : null,
4083
+ name,
4084
+ context: typeof record.context === "string" ? record.context : null,
4085
+ status: typeof record.status === "string" ? record.status : null,
4086
+ state: typeof record.state === "string" ? record.state : null,
4087
+ conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
4088
+ 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,
4089
+ link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
4090
+ headSha: typeof record.headSha === "string" ? record.headSha : null,
4091
+ head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
4092
+ output: output ? {
4093
+ title: typeof output.title === "string" ? output.title : null,
4094
+ summary: typeof output.summary === "string" ? output.summary : null,
4095
+ text: typeof output.text === "string" ? output.text : null
4096
+ } : null,
4097
+ app: app ? {
4098
+ slug: typeof app.slug === "string" ? app.slug : null,
4099
+ name: typeof app.name === "string" ? app.name : null,
4100
+ owner: app.owner && typeof app.owner === "object" ? app.owner : null
4101
+ } : null
4102
+ };
4103
+ }
4104
+ function normalizeReview(entry) {
4105
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4106
+ return null;
4107
+ const record = entry;
4108
+ return {
4109
+ id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
4110
+ state: typeof record.state === "string" ? record.state : null,
4111
+ body: typeof record.body === "string" ? record.body : null,
4112
+ 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,
4113
+ html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
4114
+ author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
4115
+ };
4116
+ }
4117
+ function normalizeReviewComment(entry) {
4118
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4119
+ return null;
4120
+ const record = entry;
4121
+ const body = typeof record.body === "string" ? record.body : null;
4122
+ const path = typeof record.path === "string" ? record.path : null;
4123
+ if (!body && !path)
4124
+ return null;
4125
+ return {
4126
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4127
+ user: record.user && typeof record.user === "object" ? record.user : null,
4128
+ author: record.author && typeof record.author === "object" ? record.author : null,
4129
+ body,
4130
+ path,
4131
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4132
+ url: typeof record.url === "string" ? record.url : null,
4133
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
4134
+ original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
4135
+ };
4136
+ }
4137
+ function normalizeIssueComment(entry) {
4138
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4139
+ return null;
4140
+ const record = entry;
4141
+ const body = typeof record.body === "string" ? record.body : null;
4142
+ if (!body)
4143
+ return null;
4144
+ return {
4145
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4146
+ user: record.user && typeof record.user === "object" ? record.user : null,
4147
+ author: record.author && typeof record.author === "object" ? record.author : null,
4148
+ body,
4149
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4150
+ url: typeof record.url === "string" ? record.url : null,
4151
+ created_at: typeof record.created_at === "string" ? record.created_at : null
4152
+ };
4153
+ }
4154
+ function normalizeReviewThread(entry) {
4155
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4156
+ return null;
4157
+ const record = entry;
4158
+ return {
4159
+ id: typeof record.id === "string" ? record.id : null,
4160
+ isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
4161
+ isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
4162
+ comments: record.comments && typeof record.comments === "object" ? record.comments : null
4163
+ };
4164
+ }
4165
+ function relevantIssueComment(comment) {
4166
+ const login = comment.user?.login ?? comment.author?.login ?? "";
4167
+ const body = comment.body ?? "";
4168
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4169
+ }
4170
+ function latestThreadComment(thread) {
4171
+ const nodes = thread.comments?.nodes ?? [];
4172
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
4173
+ }
4174
+ function unresolvedThreadSummaries(threads) {
4175
+ return threads.flatMap((thread) => {
4176
+ if (thread.isResolved === true || thread.isOutdated === true)
4177
+ return [];
4178
+ const latest = latestThreadComment(thread);
4179
+ if (!latest)
4180
+ return ["Unresolved review thread"];
4181
+ const path = latest.path ? ` on ${latest.path}` : "";
4182
+ return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4183
+ });
4184
+ }
4185
+ function collectBodies(evidence) {
4186
+ return [
4187
+ evidence.title ?? "",
4188
+ evidence.body,
4189
+ ...evidence.reviews.map((review) => review.body ?? ""),
4190
+ ...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
4191
+ ...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
4192
+ ...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
4193
+ ...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
4194
+ ].filter((body) => body.trim().length > 0);
4195
+ }
4196
+ function bodyExcerpt(body) {
4197
+ const text = stripHtml(body).replace(/\s+/g, " ").trim();
4198
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
4199
+ }
4200
+ function makeGreptileSignal(input) {
4201
+ const scores = parseGreptileScores(input.body);
4202
+ const reviewedSha = input.reviewedSha?.trim() || null;
4203
+ const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
4204
+ const verdict = input.verdict ?? null;
4205
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
4206
+ const explicitApproval = input.explicitApproval ?? false;
4207
+ return {
4208
+ source: input.source,
4209
+ trusted: input.trusted,
4210
+ authorLogin: input.authorLogin ?? null,
4211
+ reviewedSha,
4212
+ current,
4213
+ stale: current === false,
4214
+ score: scores[0] ?? null,
4215
+ scores,
4216
+ explicitApproval,
4217
+ verdict,
4218
+ blocker,
4219
+ actionable: input.actionable ?? blocker,
4220
+ bodyExcerpt: bodyExcerpt(input.body),
4221
+ body: input.body,
4222
+ allScores: scores
4223
+ };
4224
+ }
4225
+ function reviewAuthorLogin(review) {
4226
+ return review.author?.login ?? null;
4227
+ }
4228
+ function commentAuthorLogin(comment) {
4229
+ return comment.user?.login ?? comment.author?.login ?? null;
4230
+ }
4231
+ function collectGreptileSignals(evidence) {
4232
+ const signals = [];
4233
+ const contextSources = [
4234
+ { source: "pr-title", body: evidence.title ?? "" },
4235
+ { source: "pr-body", body: evidence.body }
4236
+ ];
4237
+ for (const context of contextSources) {
4238
+ if (!context.body.trim())
4239
+ continue;
4240
+ const contextBlocker = containsBlockerText(context.body);
4241
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
4242
+ continue;
4243
+ signals.push(makeGreptileSignal({
4244
+ source: context.source,
4245
+ body: context.body,
4246
+ currentHeadSha: evidence.currentHeadSha,
4247
+ trusted: false,
4248
+ blocker: contextBlocker,
4249
+ actionable: contextBlocker
4250
+ }));
4251
+ }
4252
+ for (const apiSignal of evidence.apiSignals ?? []) {
4253
+ const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
4254
+
4255
+ `) || "Status: UNKNOWN";
4256
+ const verdict = greptileStatusVerdict(apiSignal.status);
4257
+ signals.push(makeGreptileSignal({
4258
+ source: "api",
4259
+ body,
4260
+ currentHeadSha: evidence.currentHeadSha,
4261
+ trusted: true,
4262
+ reviewedSha: apiSignal.reviewedSha ?? null,
4263
+ explicitApproval: verdict === "approved",
4264
+ verdict
4265
+ }));
4266
+ }
4267
+ for (const review of evidence.reviews) {
4268
+ const login = reviewAuthorLogin(review);
4269
+ if (!isGreptileGithubLogin(login))
4270
+ continue;
4271
+ const state = String(review.state ?? "").toUpperCase();
4272
+ const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
4273
+
4274
+ `);
4275
+ if (!body.trim())
4276
+ continue;
4277
+ const dismissed = state === "DISMISSED";
4278
+ signals.push(makeGreptileSignal({
4279
+ source: "github-review",
4280
+ body,
4281
+ currentHeadSha: evidence.currentHeadSha,
4282
+ trusted: !dismissed,
4283
+ authorLogin: login,
4284
+ reviewedSha: review.commit_id ?? null,
4285
+ explicitApproval: undefined,
4286
+ blocker: state === "CHANGES_REQUESTED" || undefined
4287
+ }));
4288
+ }
4289
+ for (const comment of evidence.relevantIssueComments) {
4290
+ const login = commentAuthorLogin(comment);
4291
+ const body = comment.body ?? "";
4292
+ if (!body.trim() || !isGreptileGithubLogin(login))
4293
+ continue;
4294
+ signals.push(makeGreptileSignal({
4295
+ source: "issue-comment",
4296
+ body,
4297
+ currentHeadSha: evidence.currentHeadSha,
4298
+ trusted: true,
4299
+ authorLogin: login
4300
+ }));
4301
+ }
4302
+ for (const thread of evidence.reviewThreads) {
4303
+ if (thread.isOutdated === true || thread.isResolved === true)
4304
+ continue;
4305
+ for (const comment of thread.comments?.nodes ?? []) {
4306
+ const login = comment.author?.login ?? null;
4307
+ const body = comment.body ?? "";
4308
+ if (!body.trim() || !isGreptileGithubLogin(login))
4309
+ continue;
4310
+ signals.push(makeGreptileSignal({
4311
+ source: "review-thread",
4312
+ body,
4313
+ currentHeadSha: evidence.currentHeadSha,
4314
+ trusted: true,
4315
+ authorLogin: login
4316
+ }));
4317
+ }
4318
+ }
4319
+ for (const check of evidence.checks) {
4320
+ if (!isGreptileLabel(checkName(check)))
4321
+ continue;
4322
+ const reviewedSha = check.headSha ?? check.head_sha ?? null;
4323
+ const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
4324
+ const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
4325
+
4326
+ `);
4327
+ signals.push(makeGreptileSignal({
4328
+ source: "github-check",
4329
+ body,
4330
+ currentHeadSha: evidence.currentHeadSha,
4331
+ trusted: false,
4332
+ reviewedSha,
4333
+ explicitApproval: false,
4334
+ blocker: isFailingCheck(check),
4335
+ actionable: isFailingCheck(check)
4336
+ }));
4337
+ }
4338
+ return signals;
4339
+ }
4340
+ function unresolvedGreptileThreadSummaries(threads) {
4341
+ return threads.flatMap((thread) => {
4342
+ if (thread.isResolved === true || thread.isOutdated === true)
4343
+ return [];
4344
+ const comments = thread.comments?.nodes ?? [];
4345
+ if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
4346
+ return [];
4347
+ const latest = latestThreadComment(thread);
4348
+ if (!latest)
4349
+ return ["Unresolved Greptile review thread"];
4350
+ const path = latest.path ? ` on ${latest.path}` : "";
4351
+ return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4352
+ });
4353
+ }
4354
+ function actionableChangedFileCommentSummaries(_comments) {
4355
+ return [];
4356
+ }
4357
+ function issueLevelBlockerSummaries(comments) {
4358
+ return comments.flatMap((comment) => {
4359
+ const body = comment.body?.trim() ?? "";
4360
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4361
+ return [];
4362
+ const login = commentAuthorLogin(comment) ?? "unknown";
4363
+ const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
4364
+ return [`${author}: ${body}`];
4365
+ });
4366
+ }
4367
+ function reviewBodyBlockerSummaries(reviews) {
4368
+ return reviews.flatMap((review) => {
4369
+ const login = reviewAuthorLogin(review) ?? "unknown";
4370
+ if (isGreptileGithubLogin(login))
4371
+ return [];
4372
+ const body = review.body?.trim() ?? "";
4373
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4374
+ return [];
4375
+ const state = review.state ? ` (${review.state})` : "";
4376
+ return [`PR review summary by ${login}${state}: ${body}`];
4377
+ });
4378
+ }
4379
+ function signalLabel(signal) {
4380
+ const source = signal.source.replace(/-/g, " ");
4381
+ const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
4382
+ const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
4383
+ return `${source}${author}${sha}`;
4384
+ }
4385
+ function deriveGreptileEvidence(input) {
4386
+ const rawBodies = collectBodies(input);
4387
+ const signals = collectGreptileSignals(input);
4388
+ const trustedSignals = signals.filter((signal) => signal.trusted);
4389
+ const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4390
+ const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4391
+ const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
4392
+ const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
4393
+ const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
4394
+ const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
4395
+ const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
4396
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
4397
+ const signalCanApproveByScore = (signal) => {
4398
+ if (signal.source === "api")
4399
+ return signal.verdict === "approved" || signal.verdict === "completed";
4400
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
4401
+ };
4402
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
4403
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
4404
+ const approvedByScore = !!approvingScoreEntry;
4405
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
4406
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
4407
+ const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
4408
+ const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
4409
+ const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
4410
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
4411
+ const staleBlockingSignals = [];
4412
+ const blockers = [
4413
+ ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
4414
+ ...reviewBodyBlockerSummaries(input.reviews),
4415
+ ...issueLevelBlockerSummaries(input.relevantIssueComments),
4416
+ ...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
4417
+ ...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
4418
+ ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
4419
+ ];
4420
+ const unresolvedComments = [
4421
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
4422
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
4423
+ ];
4424
+ const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
4425
+ const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
4426
+ const completedGreptileCheck = greptileChecks.some((check) => {
4427
+ const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
4428
+ return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
4429
+ });
4430
+ const completedGreptileReview = greptileReviews.some((review) => {
4431
+ const state = String(review.state ?? "").toUpperCase();
4432
+ const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
4433
+ return completedState && review.commit_id === input.currentHeadSha;
4434
+ });
4435
+ 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"));
4436
+ const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
4437
+ const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
4438
+ const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
4439
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
4440
+ const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4441
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4442
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4443
+ 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";
4444
+ return {
4445
+ source,
4446
+ currentHeadSha: input.currentHeadSha,
4447
+ reviewedSha,
4448
+ fresh,
4449
+ completed,
4450
+ approved,
4451
+ score,
4452
+ explicitApproval: approvedByExplicitMapping,
4453
+ blockers,
4454
+ unresolvedComments,
4455
+ rawBodies,
4456
+ signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
4457
+ mapping
4458
+ };
4459
+ }
4460
+ function isGreptileCheckDetail(check) {
4461
+ return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4462
+ }
4463
+ async function collectGreptileCheckDetails(input) {
4464
+ const checkRunsRead = await runJsonArray(input.command, [
4465
+ "api",
4466
+ `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4467
+ "--paginate",
4468
+ "--slurp",
4469
+ "--jq",
4470
+ "map(.check_runs // []) | add // []"
4471
+ ], input.projectRoot);
4472
+ const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4473
+ return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4474
+ }
4475
+ async function collectReviewThreads(input) {
4476
+ const reviewThreads = [];
4477
+ let afterCursor = null;
4478
+ for (let page = 0;page < 100; page += 1) {
4479
+ const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
4480
+ const threadsResponse = await runJsonObject(input.command, [
4481
+ "api",
4482
+ "graphql",
4483
+ "-F",
4484
+ `owner=${input.owner}`,
4485
+ "-F",
4486
+ `name=${input.name}`,
4487
+ "-F",
4488
+ `prNumber=${input.prNumber}`,
4489
+ "-f",
4490
+ `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 } } } } }`
4491
+ ], input.projectRoot);
4492
+ if (threadsResponse.error) {
4493
+ return { value: reviewThreads, error: threadsResponse.error };
4494
+ }
4495
+ const data = threadsResponse.value.data;
4496
+ const repository = data?.repository;
4497
+ const pullRequest = repository?.pullRequest;
4498
+ const threads = pullRequest?.reviewThreads;
4499
+ const nodes = threads?.nodes;
4500
+ if (!Array.isArray(nodes)) {
4501
+ return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
4502
+ }
4503
+ const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
4504
+ reviewThreads.push(...normalized);
4505
+ const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
4506
+ if (truncatedCommentThread) {
4507
+ return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
4508
+ }
4509
+ const pageInfo = threads?.pageInfo;
4510
+ if (!pageInfo) {
4511
+ if (nodes.length >= 100) {
4512
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
4513
+ }
4514
+ return { value: reviewThreads };
4515
+ }
4516
+ if (pageInfo.hasNextPage !== true) {
4517
+ return { value: reviewThreads };
4518
+ }
4519
+ if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
4520
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
4521
+ }
4522
+ afterCursor = pageInfo.endCursor;
4523
+ }
4524
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
4525
+ }
4526
+ async function collectPrReviewEvidence(input) {
4527
+ const parsed = parseGithubPrUrl(input.prUrl);
4528
+ if (!parsed) {
4529
+ throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
4530
+ }
4531
+ const readErrors = [];
4532
+ const viewRead = await runJsonObject(input.command, [
4533
+ "pr",
4534
+ "view",
4535
+ input.prUrl,
4536
+ "--json",
4537
+ "title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
4538
+ ], input.projectRoot);
4539
+ if (viewRead.error)
4540
+ readErrors.push(viewRead.error);
4541
+ const view = viewRead.value;
4542
+ if (!Array.isArray(view.statusCheckRollup)) {
4543
+ readErrors.push("gh pr view did not return required statusCheckRollup array");
4544
+ }
4545
+ if (!Array.isArray(view.reviews)) {
4546
+ readErrors.push("gh pr view did not return required reviews array");
4547
+ }
4548
+ const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
4549
+ const baseRefName = firstString(view, ["baseRefName"]);
4550
+ const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4551
+ const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4552
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4553
+ if (reviewCommentsRead.error)
4554
+ readErrors.push(reviewCommentsRead.error);
4555
+ const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4556
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4557
+ if (issueCommentsRead.error)
4558
+ readErrors.push(issueCommentsRead.error);
4559
+ const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
4560
+ const reviewThreadsRead = await collectReviewThreads({
4561
+ command: input.command,
4562
+ projectRoot: input.projectRoot,
4563
+ owner: parsed.owner,
4564
+ name: parsed.repo,
4565
+ prNumber: parsed.prNumber
4566
+ });
4567
+ if (reviewThreadsRead.error)
4568
+ readErrors.push(reviewThreadsRead.error);
4569
+ const reviewThreads = reviewThreadsRead.value;
4570
+ const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
4571
+ let greptileCheckDetails = [];
4572
+ if (headSha && greptileRollupChecks.length > 0) {
4573
+ const checkDetailsRead = await collectGreptileCheckDetails({
4574
+ command: input.command,
4575
+ projectRoot: input.projectRoot,
4576
+ repoName: parsed.repoName,
4577
+ headSha
4578
+ });
4579
+ if (checkDetailsRead.error)
4580
+ readErrors.push(checkDetailsRead.error);
4581
+ greptileCheckDetails = checkDetailsRead.value;
4582
+ if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4583
+ readErrors.push("Greptile check details could not be found for the current PR head");
4584
+ }
4585
+ }
4586
+ const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4587
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
4588
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
4589
+ enabled: shouldCollectConfiguredGreptileApi,
4590
+ options: input.greptileApi,
4591
+ repoName: parsed.repoName,
4592
+ prNumber: parsed.prNumber,
4593
+ headSha,
4594
+ baseRefName
4595
+ });
4596
+ readErrors.push(...configuredGreptileApiRead.errors);
4597
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
4598
+ 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})` : ""}`);
4599
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
4600
+ const evidenceBase = {
4601
+ title: firstString(view, ["title"]),
4602
+ body: firstString(view, ["body"]),
4603
+ reviews,
4604
+ changedFileReviewComments: reviewComments,
4605
+ relevantIssueComments: issueComments,
4606
+ reviewThreads,
4607
+ checks: checksWithGreptileDetails,
4608
+ currentHeadSha: headSha,
4609
+ apiSignals
4610
+ };
4611
+ const greptile = deriveGreptileEvidence(evidenceBase);
4612
+ return {
4613
+ prUrl: input.prUrl,
4614
+ prNumber: parsed.prNumber,
4615
+ repoName: parsed.repoName,
4616
+ title: evidenceBase.title,
4617
+ body: evidenceBase.body,
4618
+ headSha,
4619
+ headRefName: firstString(view, ["headRefName"]),
4620
+ baseRefName,
4621
+ state: firstString(view, ["state"]),
4622
+ isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
4623
+ mergeable: firstString(view, ["mergeable"]),
4624
+ mergeStateStatus: firstString(view, ["mergeStateStatus"]),
4625
+ reviewDecision: firstString(view, ["reviewDecision"]),
4626
+ reviews,
4627
+ reviewThreads,
4628
+ changedFileReviewComments: reviewComments,
4629
+ relevantIssueComments: issueComments,
4630
+ statusCheckRollup: checksWithGreptileDetails,
4631
+ checkFailures,
4632
+ pendingChecks,
4633
+ readErrors,
4634
+ greptile
4635
+ };
4636
+ }
4637
+ function capGateMessage(value, maxChars = 1200) {
4638
+ const normalized = value.trim();
4639
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
4640
+ [truncated for gate summary; see full evidence artifact]` : normalized;
4641
+ }
4642
+ function evaluateEvidence(evidence) {
4643
+ const reasonDetails = [];
4644
+ const warnings = [];
4645
+ const seen = new Set;
4646
+ const addReason = (reason) => {
4647
+ const capped = { ...reason, message: capGateMessage(reason.message) };
4648
+ const key = `${capped.code}:${capped.message}`;
4649
+ if (seen.has(key))
4650
+ return;
4651
+ seen.add(key);
4652
+ reasonDetails.push(capped);
4653
+ };
4654
+ const greptile = evidence.greptile;
4655
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
4656
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
4657
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
4658
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
4659
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
4660
+ for (const error of evidence.readErrors) {
4661
+ addReason({
4662
+ code: "read_error",
4663
+ reasonClass: "reject",
4664
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
4665
+ suggestedAction: "needs_attention",
4666
+ message: `Required PR evidence surface could not be read completely: ${error}`,
4667
+ headSha: evidence.headSha || null
4668
+ });
4669
+ }
4670
+ if (!evidence.headSha) {
4671
+ addReason({
4672
+ code: "missing_head_sha",
4673
+ reasonClass: "reject",
4674
+ surface: "github",
4675
+ suggestedAction: "needs_attention",
4676
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
4677
+ headSha: null
4678
+ });
4679
+ }
4680
+ for (const failure of evidence.checkFailures) {
4681
+ addReason({
4682
+ code: "ci_failed",
4683
+ reasonClass: "reject",
4684
+ surface: "ci",
4685
+ suggestedAction: "fix",
4686
+ message: failure,
4687
+ headSha: evidence.headSha || null
4688
+ });
4689
+ }
4690
+ for (const pendingCheck of evidence.pendingChecks) {
4691
+ addReason({
4692
+ code: "check_pending",
4693
+ reasonClass: "pending",
4694
+ surface: "ci",
4695
+ suggestedAction: "wait",
4696
+ message: pendingCheck,
4697
+ headSha: evidence.headSha || null
4698
+ });
4699
+ }
4700
+ const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
4701
+ if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
4702
+ addReason({
4703
+ code: "review_decision_blocking",
4704
+ reasonClass: "reject",
4705
+ surface: "review",
4706
+ suggestedAction: "fix",
4707
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
4708
+ headSha: evidence.headSha || null
4709
+ });
4710
+ }
4711
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
4712
+ addReason({
4713
+ code: "review_thread_unresolved",
4714
+ reasonClass: "reject",
4715
+ surface: "review",
4716
+ suggestedAction: "fix",
4717
+ message: thread,
4718
+ headSha: evidence.headSha || null
4719
+ });
4720
+ }
4721
+ if (greptile.mapping === "missing") {
4722
+ addReason({
4723
+ code: "greptile_missing",
4724
+ reasonClass: "pending",
4725
+ surface: "greptile",
4726
+ suggestedAction: "wait",
4727
+ message: "Missing Greptile check/review evidence for this PR.",
4728
+ headSha: evidence.headSha || null
4729
+ });
4730
+ }
4731
+ if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
4732
+ addReason({
4733
+ code: "greptile_stale",
4734
+ reasonClass: "pending",
4735
+ surface: "greptile",
4736
+ suggestedAction: "wait",
4737
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
4738
+ headSha: evidence.headSha || null,
4739
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
4740
+ });
4741
+ }
4742
+ for (const signal of pendingGreptileApiSignals) {
4743
+ addReason({
4744
+ code: "greptile_pending",
4745
+ reasonClass: "pending",
4746
+ surface: "greptile",
4747
+ suggestedAction: "wait",
4748
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
4749
+ headSha: evidence.headSha || null,
4750
+ reviewedSha: signal.reviewedSha ?? null
4751
+ });
4752
+ }
4753
+ for (const signal of unknownGreptileApiSignals) {
4754
+ addReason({
4755
+ code: "greptile_api_status_unknown",
4756
+ reasonClass: "reject",
4757
+ surface: "greptile",
4758
+ suggestedAction: "needs_attention",
4759
+ 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}` : "."}`,
4760
+ headSha: evidence.headSha || null,
4761
+ reviewedSha: signal.reviewedSha ?? null
4762
+ });
4763
+ }
4764
+ if (!greptile.completed) {
4765
+ addReason({
4766
+ code: "greptile_pending",
4767
+ reasonClass: "pending",
4768
+ surface: "greptile",
4769
+ suggestedAction: "wait",
4770
+ message: "Greptile check/review has not completed for the current PR head.",
4771
+ headSha: evidence.headSha || null,
4772
+ reviewedSha: greptile.reviewedSha ?? null
4773
+ });
4774
+ }
4775
+ if (!greptile.fresh) {
4776
+ addReason({
4777
+ code: "greptile_not_current_head",
4778
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4779
+ surface: "greptile",
4780
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4781
+ message: "Greptile approval is not tied to the current PR head SHA.",
4782
+ headSha: evidence.headSha || null,
4783
+ reviewedSha: greptile.reviewedSha ?? null
4784
+ });
4785
+ }
4786
+ if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
4787
+ addReason({
4788
+ code: "greptile_score_not_5",
4789
+ reasonClass: "reject",
4790
+ surface: "greptile",
4791
+ suggestedAction: "fix",
4792
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
4793
+ headSha: evidence.headSha || null,
4794
+ reviewedSha: greptile.reviewedSha ?? null
4795
+ });
4796
+ }
4797
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
4798
+ if (!greptile.score && !hasApprovedMapping) {
4799
+ addReason({
4800
+ code: "greptile_score_missing",
4801
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4802
+ surface: "greptile",
4803
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4804
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
4805
+ headSha: evidence.headSha || null,
4806
+ reviewedSha: greptile.reviewedSha ?? null
4807
+ });
4808
+ }
4809
+ if (greptile.mapping === "unproven") {
4810
+ addReason({
4811
+ code: "greptile_mapping_unproven",
4812
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
4813
+ surface: "greptile",
4814
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
4815
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
4816
+ headSha: evidence.headSha || null,
4817
+ reviewedSha: greptile.reviewedSha ?? null
4818
+ });
4819
+ }
4820
+ for (const blocker of greptile.blockers) {
4821
+ addReason({
4822
+ code: "greptile_blocker_text",
4823
+ reasonClass: "reject",
4824
+ surface: "greptile",
4825
+ suggestedAction: "fix",
4826
+ message: `Greptile/blocker text: ${blocker}`,
4827
+ headSha: evidence.headSha || null,
4828
+ reviewedSha: greptile.reviewedSha ?? null
4829
+ });
4830
+ }
4831
+ for (const comment of greptile.unresolvedComments) {
4832
+ addReason({
4833
+ code: "greptile_unresolved_comment",
4834
+ reasonClass: "reject",
4835
+ surface: "greptile",
4836
+ suggestedAction: "fix",
4837
+ message: comment,
4838
+ headSha: evidence.headSha || null,
4839
+ reviewedSha: greptile.reviewedSha ?? null
4840
+ });
4841
+ }
4842
+ if (!greptile.approved)
4843
+ warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
4844
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
4845
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
4846
+ }
4847
+ function evaluateStrictPrMergeGate(evidence) {
4848
+ const evaluated = evaluateEvidence(evidence);
4849
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
4850
+ return {
4851
+ approved,
4852
+ pending: evaluated.pending,
4853
+ reasons: evaluated.reasons,
4854
+ reasonDetails: evaluated.reasonDetails,
4855
+ warnings: evaluated.warnings,
4856
+ actionableFeedback: evaluated.reasons,
4857
+ evidence
4858
+ };
4859
+ }
4860
+
4861
+ // packages/runtime/src/control-plane/native/git-ops.ts
3742
4862
  var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
3743
4863
  "changed-files.txt",
3744
4864
  "contract-changes.md",
@@ -4571,7 +5691,8 @@ async function runGreptileReviewForPr(options) {
4571
5691
  }
4572
5692
  };
4573
5693
  }
4574
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
5694
+ const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
5695
+ 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)) {
4575
5696
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
4576
5697
  return {
4577
5698
  verdict: "REJECT",
@@ -4587,44 +5708,79 @@ async function runGreptileReviewForPr(options) {
4587
5708
  }
4588
5709
  };
4589
5710
  }
4590
- if (score) {
4591
- if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
4592
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
4593
- return {
4594
- verdict: "REJECT",
4595
- feedback,
4596
- reasons,
4597
- warnings,
4598
- rawPayload: {
4599
- pr: options.prState,
4600
- codeReviews: reviewsPayload,
4601
- selectedReview,
4602
- reviewDetails,
4603
- comments: commentsPayload,
4604
- score
4605
- }
4606
- };
4607
- }
4608
- if (score.scale === 5 && score.value <= 2) {
4609
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
4610
- return {
4611
- verdict: "REJECT",
4612
- feedback,
4613
- reasons,
4614
- warnings,
4615
- rawPayload: {
4616
- pr: options.prState,
4617
- codeReviews: reviewsPayload,
4618
- selectedReview,
4619
- reviewDetails,
4620
- comments: commentsPayload,
4621
- score
5711
+ if (score?.scale === 5 && score.value < 5) {
5712
+ reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
5713
+ return {
5714
+ verdict: "REJECT",
5715
+ feedback,
5716
+ reasons,
5717
+ warnings,
5718
+ rawPayload: {
5719
+ pr: options.prState,
5720
+ codeReviews: reviewsPayload,
5721
+ selectedReview,
5722
+ reviewDetails,
5723
+ comments: commentsPayload,
5724
+ score
5725
+ }
5726
+ };
5727
+ }
5728
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
5729
+ let strictGate = null;
5730
+ try {
5731
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
5732
+ projectRoot: options.projectRoot,
5733
+ taskId: options.taskId,
5734
+ prUrl,
5735
+ apiSignals: [{
5736
+ id: selectedReview.id,
5737
+ body: reviewBody,
5738
+ reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
5739
+ status: selectedReview.status
5740
+ }]
5741
+ });
5742
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
5743
+ } catch (error) {
5744
+ reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
5745
+ return {
5746
+ verdict: "REJECT",
5747
+ feedback,
5748
+ reasons,
5749
+ warnings,
5750
+ rawPayload: {
5751
+ pr: options.prState,
5752
+ codeReviews: reviewsPayload,
5753
+ selectedReview,
5754
+ reviewDetails,
5755
+ comments: commentsPayload,
5756
+ score
5757
+ }
5758
+ };
5759
+ }
5760
+ if (!strictGate.approved) {
5761
+ return {
5762
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
5763
+ feedback,
5764
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
5765
+ warnings: [...warnings, ...strictGate.warnings],
5766
+ rawPayload: {
5767
+ pr: options.prState,
5768
+ codeReviews: reviewsPayload,
5769
+ selectedReview,
5770
+ reviewDetails,
5771
+ comments: commentsPayload,
5772
+ score,
5773
+ strictGate: {
5774
+ approved: strictGate.approved,
5775
+ pending: strictGate.pending,
5776
+ reasons: strictGate.reasons,
5777
+ reasonDetails: strictGate.reasonDetails,
5778
+ warnings: strictGate.warnings,
5779
+ greptile: strictGate.evidence.greptile,
5780
+ readErrors: strictGate.evidence.readErrors
4622
5781
  }
4623
- };
4624
- }
4625
- if (score.scale === 5 && score.value < 5) {
4626
- warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
4627
- }
5782
+ }
5783
+ };
4628
5784
  }
4629
5785
  return {
4630
5786
  verdict: "APPROVE",
@@ -4636,7 +5792,16 @@ async function runGreptileReviewForPr(options) {
4636
5792
  codeReviews: reviewsPayload,
4637
5793
  selectedReview,
4638
5794
  reviewDetails,
4639
- comments: commentsPayload
5795
+ comments: commentsPayload,
5796
+ strictGate: {
5797
+ approved: strictGate.approved,
5798
+ pending: strictGate.pending,
5799
+ reasons: strictGate.reasons,
5800
+ reasonDetails: strictGate.reasonDetails,
5801
+ warnings: strictGate.warnings,
5802
+ greptile: strictGate.evidence.greptile,
5803
+ readErrors: strictGate.evidence.readErrors
5804
+ }
4640
5805
  }
4641
5806
  };
4642
5807
  }
@@ -4660,7 +5825,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4660
5825
  let threads = [];
4661
5826
  let actionableThreads = [];
4662
5827
  let checkRollup = [];
4663
- let checkState = { pending: false, completed: false };
5828
+ let checkState2 = { pending: false, completed: false };
4664
5829
  for (let attempt = 0;; attempt += 1) {
4665
5830
  reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
4666
5831
  selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
@@ -4669,15 +5834,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4669
5834
  threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
4670
5835
  actionableThreads = filterActionableGithubGreptileThreads(threads);
4671
5836
  checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
4672
- checkState = classifyGithubGreptileCheckState(checkRollup);
4673
- const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
5837
+ checkState2 = classifyGithubGreptileCheckState(checkRollup);
5838
+ const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
4674
5839
  if (!shouldContinueGithubGreptileFallbackPolling({
4675
5840
  attempt,
4676
5841
  pollAttempts: options.pollAttempts,
4677
- checkState,
5842
+ checkState: checkState2,
4678
5843
  fallbackReview,
4679
5844
  selectedReview,
4680
- approvedViaReviewedAncestor: approvedViaReviewedAncestor2
5845
+ approvedViaReviewedAncestor
4681
5846
  })) {
4682
5847
  break;
4683
5848
  }
@@ -4705,7 +5870,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4705
5870
  ].filter(Boolean).join(`
4706
5871
  `);
4707
5872
  const warnings = buildGithubGreptileFallbackWarnings(options);
4708
- if (checkState.pending) {
5873
+ if (checkState2.pending) {
4709
5874
  return {
4710
5875
  verdict: "SKIP",
4711
5876
  feedback,
@@ -4716,34 +5881,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4716
5881
  rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4717
5882
  };
4718
5883
  }
4719
- const approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
4720
- if (!fallbackReview) {
4721
- if (approvedViaCompletedCheck) {
4722
- 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.`);
4723
- return {
4724
- verdict: "APPROVE",
4725
- feedback,
4726
- reasons: [],
4727
- warnings,
4728
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4729
- };
4730
- }
4731
- return {
4732
- verdict: "SKIP",
4733
- feedback,
4734
- reasons: [
4735
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
4736
- ],
4737
- warnings,
4738
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
4739
- };
4740
- }
4741
- const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
4742
- if (actionableThreads.length > 0) {
5884
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
5885
+ let strictGate;
5886
+ try {
5887
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
5888
+ projectRoot: options.projectRoot,
5889
+ taskId: options.taskId,
5890
+ prUrl
5891
+ });
5892
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
5893
+ } catch (error) {
4743
5894
  return {
4744
5895
  verdict: "REJECT",
4745
5896
  feedback,
4746
- reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
5897
+ reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
4747
5898
  warnings,
4748
5899
  rawPayload: {
4749
5900
  pr: options.prState,
@@ -4756,44 +5907,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4756
5907
  }
4757
5908
  };
4758
5909
  }
4759
- if (!selectedReview && !approvedViaReviewedAncestor) {
4760
- if (approvedViaCompletedCheck) {
4761
- 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.`);
4762
- return {
4763
- verdict: "APPROVE",
4764
- feedback,
4765
- reasons: [],
4766
- warnings,
4767
- rawPayload: {
4768
- pr: options.prState,
4769
- selectedReview: fallbackReview,
4770
- reviews,
4771
- threads,
4772
- checkRollup,
4773
- ...buildGithubGreptileFallbackRawPayload(options)
4774
- }
4775
- };
4776
- }
5910
+ if (!strictGate.approved) {
4777
5911
  return {
4778
- verdict: "SKIP",
5912
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
4779
5913
  feedback,
4780
- reasons: [
4781
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
4782
- ],
4783
- warnings,
5914
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
5915
+ warnings: [...warnings, ...strictGate.warnings],
4784
5916
  rawPayload: {
4785
5917
  pr: options.prState,
4786
5918
  selectedReview: fallbackReview,
4787
5919
  reviews,
4788
5920
  threads,
4789
5921
  checkRollup,
5922
+ actionableThreads,
5923
+ strictGate: {
5924
+ approved: strictGate.approved,
5925
+ pending: strictGate.pending,
5926
+ reasons: strictGate.reasons,
5927
+ reasonDetails: strictGate.reasonDetails,
5928
+ warnings: strictGate.warnings,
5929
+ greptile: strictGate.evidence.greptile
5930
+ },
4790
5931
  ...buildGithubGreptileFallbackRawPayload(options)
4791
5932
  }
4792
5933
  };
4793
5934
  }
4794
- if (approvedViaReviewedAncestor) {
4795
- 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.`);
4796
- }
4797
5935
  return {
4798
5936
  verdict: "APPROVE",
4799
5937
  feedback,
@@ -4805,6 +5943,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
4805
5943
  reviews,
4806
5944
  threads,
4807
5945
  checkRollup,
5946
+ strictGate: {
5947
+ approved: strictGate.approved,
5948
+ pending: strictGate.pending,
5949
+ reasons: strictGate.reasons,
5950
+ reasonDetails: strictGate.reasonDetails,
5951
+ warnings: strictGate.warnings,
5952
+ greptile: strictGate.evidence.greptile
5953
+ },
4808
5954
  ...buildGithubGreptileFallbackRawPayload(options)
4809
5955
  }
4810
5956
  };
@@ -4917,19 +6063,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
4917
6063
  if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
4918
6064
  return true;
4919
6065
  }
4920
- return isGreptileReviewTerminal(existingReview.status);
6066
+ return false;
4921
6067
  }
4922
6068
  function shouldContinueGreptileMcpPolling(options) {
4923
6069
  if (options.githubCheckState.completed) {
4924
6070
  return false;
4925
6071
  }
6072
+ if (options.attempt + 1 >= options.pollAttempts) {
6073
+ return false;
6074
+ }
4926
6075
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
4927
6076
  return true;
4928
6077
  }
4929
- return options.attempt + 1 < options.pollAttempts;
6078
+ return true;
4930
6079
  }
4931
6080
  function shouldContinueGithubGreptileFallbackPolling(options) {
4932
6081
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
6082
+ if (options.attempt + 1 >= options.pollAttempts) {
6083
+ return false;
6084
+ }
4933
6085
  if (waitingForVisiblePendingReview) {
4934
6086
  return true;
4935
6087
  }
@@ -4990,6 +6142,20 @@ function runGhJson(projectRoot, args) {
4990
6142
  throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
4991
6143
  }
4992
6144
  }
6145
+ async function collectStrictPrEvidenceForVerifier(input) {
6146
+ return collectPrReviewEvidence({
6147
+ projectRoot: input.projectRoot,
6148
+ prUrl: input.prUrl,
6149
+ taskId: input.taskId,
6150
+ runId: "verifier",
6151
+ cycle: 0,
6152
+ apiSignals: input.apiSignals ?? [],
6153
+ command: async (args, options) => {
6154
+ const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
6155
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
6156
+ }
6157
+ });
6158
+ }
4993
6159
  function deriveRepoName(projectRoot, prState) {
4994
6160
  const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
4995
6161
  if (fromUrl?.[1]) {
@@ -5004,8 +6170,9 @@ function resolvePrHeadSha(projectRoot, prState) {
5004
6170
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5005
6171
  return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
5006
6172
  }
5007
- function isGreptileGithubLogin(login) {
5008
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
6173
+ function isGreptileGithubLogin2(login) {
6174
+ const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
6175
+ return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
5009
6176
  }
5010
6177
  function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
5011
6178
  const matching = sortGithubGreptileReviews(reviews);
@@ -5022,7 +6189,7 @@ function pickLatestGithubGreptileReview(reviews) {
5022
6189
  return sortGithubGreptileReviews(reviews)[0] || null;
5023
6190
  }
5024
6191
  function sortGithubGreptileReviews(reviews) {
5025
- return reviews.filter((review) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
6192
+ return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
5026
6193
  }
5027
6194
  function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
5028
6195
  const response = runGhJson(projectRoot, [
@@ -5095,32 +6262,6 @@ function classifyGithubGreptileCheckState(checks) {
5095
6262
  }
5096
6263
  return { pending: false, completed: false };
5097
6264
  }
5098
- function isGithubGreptileCheckApproved(checks) {
5099
- const greptileChecks = checks.filter((check) => {
5100
- const label = (check.name || check.context || "").toLowerCase();
5101
- return label.includes("greptile");
5102
- });
5103
- if (greptileChecks.length === 0) {
5104
- return false;
5105
- }
5106
- for (const check of greptileChecks) {
5107
- if ((check.__typename || "") === "CheckRun") {
5108
- if ((check.status || "").toUpperCase() !== "COMPLETED") {
5109
- return false;
5110
- }
5111
- const conclusion = (check.conclusion || "").toUpperCase();
5112
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
5113
- return false;
5114
- }
5115
- continue;
5116
- }
5117
- const state = (check.state || "").toUpperCase();
5118
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
5119
- return false;
5120
- }
5121
- }
5122
- return true;
5123
- }
5124
6265
  function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
5125
6266
  const [owner, name] = repoName.split("/");
5126
6267
  if (!owner || !name) {
@@ -5146,7 +6287,7 @@ function filterActionableGithubGreptileThreads(threads) {
5146
6287
  return [];
5147
6288
  }
5148
6289
  const comments = thread.comments?.nodes || [];
5149
- const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
6290
+ const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
5150
6291
  if (!latestGreptileComment?.path?.trim()) {
5151
6292
  return [];
5152
6293
  }
@@ -5168,11 +6309,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
5168
6309
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5169
6310
  return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
5170
6311
  }
5171
- function stripHtml(input) {
5172
- return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
5173
-
5174
- `).trim();
5175
- }
5176
6312
  function summarizeComment(input) {
5177
6313
  const text = stripHtml(input).replace(/\s+/g, " ").trim();
5178
6314
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
@@ -5181,31 +6317,14 @@ function asGreptileInfrastructureWarning(reason) {
5181
6317
  return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
5182
6318
  }
5183
6319
  function isAiReviewApproved(input) {
6320
+ if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
6321
+ return false;
6322
+ }
5184
6323
  if (input.reviewMode !== "required") {
5185
6324
  return true;
5186
6325
  }
5187
6326
  return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
5188
6327
  }
5189
- function parseGreptileScore(input) {
5190
- const text = stripHtml(input);
5191
- const patterns = [
5192
- /confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
5193
- /\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
5194
- /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
5195
- ];
5196
- for (const pattern of patterns) {
5197
- const match = pattern.exec(text);
5198
- if (!match) {
5199
- continue;
5200
- }
5201
- const value = Number.parseInt(match[1] || "", 10);
5202
- const scale = Number.parseInt(match[2] || "", 10);
5203
- if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
5204
- return { value, scale };
5205
- }
5206
- }
5207
- return null;
5208
- }
5209
6328
 
5210
6329
  // packages/runtime/src/control-plane/provider/runtime-instructions.ts
5211
6330
  var CLAUDE_ROUTER_TOOL_NAMES = [