@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
@@ -977,8 +977,8 @@ function githubStatusFor(issue) {
977
977
  return "open";
978
978
  }
979
979
  function selectedGitHubEnv() {
980
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
981
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
980
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
981
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
982
982
  }
983
983
  function ghSpawnOptions() {
984
984
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -1756,6 +1756,1124 @@ var GENERATED_TASK_ARTIFACT_FILES = new Set([
1756
1756
  "git-state.txt"
1757
1757
  ]);
1758
1758
 
1759
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
1760
+ function parseJsonObject(value) {
1761
+ if (!value?.trim())
1762
+ return { value: {}, error: "empty JSON output" };
1763
+ try {
1764
+ const parsed = JSON.parse(value);
1765
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
1766
+ } catch (error) {
1767
+ return { value: {}, error: error instanceof Error ? error.message : String(error) };
1768
+ }
1769
+ }
1770
+ function flattenPaginatedArray(value) {
1771
+ if (!Array.isArray(value))
1772
+ return null;
1773
+ if (value.every((entry) => Array.isArray(entry))) {
1774
+ return value.flatMap((entry) => entry);
1775
+ }
1776
+ return value;
1777
+ }
1778
+ function parseJsonArray(value) {
1779
+ if (!value?.trim())
1780
+ return { value: [], error: "empty JSON output" };
1781
+ try {
1782
+ const parsed = JSON.parse(value);
1783
+ const flattened = flattenPaginatedArray(parsed);
1784
+ return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
1785
+ } catch (error) {
1786
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
1787
+ }
1788
+ }
1789
+ function parseGithubPrUrl(prUrl) {
1790
+ const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
1791
+ if (!match)
1792
+ return null;
1793
+ const prNumber = Number.parseInt(match[3], 10);
1794
+ if (!Number.isFinite(prNumber))
1795
+ return null;
1796
+ return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
1797
+ }
1798
+ function checkName(check) {
1799
+ return String(check.name ?? check.context ?? "").trim();
1800
+ }
1801
+ function checkState(check) {
1802
+ return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
1803
+ }
1804
+ function isGreptileLabel(value) {
1805
+ return String(value ?? "").toLowerCase().includes("greptile");
1806
+ }
1807
+ function isGreptileGithubLogin(value) {
1808
+ const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
1809
+ return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
1810
+ }
1811
+ function isPassingCheck(check) {
1812
+ const state = checkState(check);
1813
+ return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
1814
+ }
1815
+ function isPendingCheck(check) {
1816
+ const state = checkState(check);
1817
+ return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
1818
+ }
1819
+ function isFailingCheck(check) {
1820
+ const state = checkState(check);
1821
+ return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
1822
+ }
1823
+ function wildcardToRegExp(pattern) {
1824
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1825
+ return new RegExp(`^${escaped}$`, "i");
1826
+ }
1827
+ function isAllowedFailure(name, allowedFailures) {
1828
+ return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
1829
+ }
1830
+ function greptileScorePatterns() {
1831
+ return [
1832
+ /\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
1833
+ /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
1834
+ /\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
1835
+ ];
1836
+ }
1837
+ function parseGreptileScores(input) {
1838
+ const text = stripHtml(input);
1839
+ const seen = new Set;
1840
+ const scores = [];
1841
+ for (const pattern of greptileScorePatterns()) {
1842
+ for (const match of text.matchAll(pattern)) {
1843
+ const value = Number.parseInt(match[1] || "", 10);
1844
+ const scale = Number.parseInt(match[2] || "", 10);
1845
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
1846
+ continue;
1847
+ const raw = match[0] || `${value}/${scale}`;
1848
+ const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
1849
+ if (seen.has(key))
1850
+ continue;
1851
+ seen.add(key);
1852
+ scores.push({ value, scale, raw });
1853
+ }
1854
+ }
1855
+ return scores;
1856
+ }
1857
+ function parseGreptileScore(input) {
1858
+ return parseGreptileScores(input)[0] ?? null;
1859
+ }
1860
+ function stripHtml(input) {
1861
+ return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
1862
+
1863
+ `).trim();
1864
+ }
1865
+ function containsBlockerText(input) {
1866
+ const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
1867
+ 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);
1868
+ }
1869
+ function isStrictFiveOfFive(score) {
1870
+ return score.value === 5 && score.scale === 5;
1871
+ }
1872
+ function containsConflictingScoreText(input) {
1873
+ return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
1874
+ }
1875
+ function greptileStatusVerdict(status) {
1876
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
1877
+ if (!normalized)
1878
+ return null;
1879
+ if (["APPROVE", "APPROVED"].includes(normalized))
1880
+ return "approved";
1881
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
1882
+ return "rejected";
1883
+ if (["SKIP", "SKIPPED"].includes(normalized))
1884
+ return "skipped";
1885
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
1886
+ return "failed";
1887
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
1888
+ return "pending";
1889
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
1890
+ return "completed";
1891
+ return null;
1892
+ }
1893
+ function isBlockingGreptileVerdict(verdict) {
1894
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
1895
+ }
1896
+ function greptileRequestTimeoutMs(env) {
1897
+ const fallback = 30000;
1898
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
1899
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
1900
+ }
1901
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
1902
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
1903
+ return null;
1904
+ const record = entry;
1905
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
1906
+ if (!id)
1907
+ return null;
1908
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
1909
+ return {
1910
+ id,
1911
+ status: typeof record.status === "string" ? record.status : null,
1912
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
1913
+ body: typeof record.body === "string" ? record.body : null,
1914
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
1915
+ };
1916
+ }
1917
+ function uniqueGreptileCodeReviews(reviews) {
1918
+ const seen = new Set;
1919
+ const unique2 = [];
1920
+ for (const review of reviews) {
1921
+ if (seen.has(review.id))
1922
+ continue;
1923
+ seen.add(review.id);
1924
+ unique2.push(review);
1925
+ }
1926
+ return unique2;
1927
+ }
1928
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
1929
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
1930
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
1931
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
1932
+ const latest = sorted.slice(0, 1);
1933
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
1934
+ }
1935
+ function greptileApiSignalFromCodeReview(review, details) {
1936
+ const selected = details ?? review;
1937
+ return {
1938
+ id: selected.id || review.id,
1939
+ body: selected.body ?? review.body ?? null,
1940
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
1941
+ status: selected.status ?? review.status ?? null
1942
+ };
1943
+ }
1944
+ async function callGreptileMcpToolForGate(input) {
1945
+ const controller = new AbortController;
1946
+ const timeoutId = setTimeout(() => {
1947
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
1948
+ }, input.timeoutMs);
1949
+ let response;
1950
+ try {
1951
+ response = await input.fetchFn(input.apiBase, {
1952
+ method: "POST",
1953
+ headers: {
1954
+ Authorization: `Bearer ${input.apiKey}`,
1955
+ "Content-Type": "application/json"
1956
+ },
1957
+ body: JSON.stringify({
1958
+ jsonrpc: "2.0",
1959
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
1960
+ method: "tools/call",
1961
+ params: { name: input.name, arguments: input.args }
1962
+ }),
1963
+ signal: controller.signal
1964
+ });
1965
+ } catch (error) {
1966
+ if (controller.signal.aborted) {
1967
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
1968
+ }
1969
+ throw error;
1970
+ } finally {
1971
+ clearTimeout(timeoutId);
1972
+ }
1973
+ const raw = await response.text();
1974
+ if (!response.ok) {
1975
+ throw new Error(`HTTP ${response.status}: ${raw}`);
1976
+ }
1977
+ let envelope;
1978
+ try {
1979
+ envelope = JSON.parse(raw);
1980
+ } catch {
1981
+ throw new Error(`Malformed MCP response: ${raw}`);
1982
+ }
1983
+ if (envelope.error?.message) {
1984
+ throw new Error(envelope.error.message);
1985
+ }
1986
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
1987
+ `).trim();
1988
+ if (!text) {
1989
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
1990
+ }
1991
+ return text;
1992
+ }
1993
+ async function callGreptileMcpToolJsonForGate(input) {
1994
+ const text = await callGreptileMcpToolForGate(input);
1995
+ try {
1996
+ return JSON.parse(text);
1997
+ } catch {
1998
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
1999
+ }
2000
+ }
2001
+ async function collectConfiguredGreptileApiSignals(input) {
2002
+ if (!input.enabled || input.options?.enabled === false) {
2003
+ return { signals: [], errors: [] };
2004
+ }
2005
+ const env = input.options?.env ?? process.env;
2006
+ const secrets = resolveRuntimeSecrets(env);
2007
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
2008
+ if (!apiKey) {
2009
+ return { signals: [], errors: [] };
2010
+ }
2011
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
2012
+ if (typeof fetchFn !== "function") {
2013
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
2014
+ }
2015
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
2016
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
2017
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
2018
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
2019
+ const timeoutMs = greptileRequestTimeoutMs(env);
2020
+ try {
2021
+ const listPayload = await callGreptileMcpToolJsonForGate({
2022
+ apiBase,
2023
+ apiKey,
2024
+ name: "list_code_reviews",
2025
+ args: {
2026
+ name: repository,
2027
+ remote,
2028
+ defaultBranch,
2029
+ prNumber: input.prNumber,
2030
+ limit: 20
2031
+ },
2032
+ timeoutMs,
2033
+ fetchFn
2034
+ });
2035
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
2036
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
2037
+ const signals = [];
2038
+ for (const review of selectedReviews) {
2039
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
2040
+ apiBase,
2041
+ apiKey,
2042
+ name: "get_code_review",
2043
+ args: { codeReviewId: review.id },
2044
+ timeoutMs,
2045
+ fetchFn
2046
+ });
2047
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
2048
+ signals.push(greptileApiSignalFromCodeReview(review, details));
2049
+ }
2050
+ return { signals, errors: [] };
2051
+ } catch (error) {
2052
+ return {
2053
+ signals: [],
2054
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
2055
+ };
2056
+ }
2057
+ }
2058
+ function firstString(record, keys) {
2059
+ for (const key of keys) {
2060
+ const value = record[key];
2061
+ if (typeof value === "string")
2062
+ return value;
2063
+ }
2064
+ return "";
2065
+ }
2066
+ function arrayField(record, key) {
2067
+ const value = record[key];
2068
+ return Array.isArray(value) ? value : [];
2069
+ }
2070
+ async function runJsonArray(command, args, cwd) {
2071
+ const result = await command(args, { cwd });
2072
+ const label = `gh ${args.join(" ")}`;
2073
+ if (result.exitCode !== 0) {
2074
+ return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
2075
+ }
2076
+ const parsed = parseJsonArray(result.stdout);
2077
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
2078
+ }
2079
+ async function runJsonObject(command, args, cwd) {
2080
+ const result = await command(args, { cwd });
2081
+ const label = `gh ${args.join(" ")}`;
2082
+ if (result.exitCode !== 0) {
2083
+ return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
2084
+ }
2085
+ const parsed = parseJsonObject(result.stdout);
2086
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
2087
+ }
2088
+ function normalizeStatusCheck(entry) {
2089
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2090
+ return null;
2091
+ const record = entry;
2092
+ const name = firstString(record, ["name", "context"]);
2093
+ if (!name.trim())
2094
+ return null;
2095
+ const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
2096
+ const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
2097
+ return {
2098
+ __typename: typeof record.__typename === "string" ? record.__typename : null,
2099
+ name,
2100
+ context: typeof record.context === "string" ? record.context : null,
2101
+ status: typeof record.status === "string" ? record.status : null,
2102
+ state: typeof record.state === "string" ? record.state : null,
2103
+ conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
2104
+ 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,
2105
+ link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
2106
+ headSha: typeof record.headSha === "string" ? record.headSha : null,
2107
+ head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
2108
+ output: output ? {
2109
+ title: typeof output.title === "string" ? output.title : null,
2110
+ summary: typeof output.summary === "string" ? output.summary : null,
2111
+ text: typeof output.text === "string" ? output.text : null
2112
+ } : null,
2113
+ app: app ? {
2114
+ slug: typeof app.slug === "string" ? app.slug : null,
2115
+ name: typeof app.name === "string" ? app.name : null,
2116
+ owner: app.owner && typeof app.owner === "object" ? app.owner : null
2117
+ } : null
2118
+ };
2119
+ }
2120
+ function normalizeReview(entry) {
2121
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2122
+ return null;
2123
+ const record = entry;
2124
+ return {
2125
+ id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
2126
+ state: typeof record.state === "string" ? record.state : null,
2127
+ body: typeof record.body === "string" ? record.body : null,
2128
+ 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,
2129
+ html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
2130
+ author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
2131
+ };
2132
+ }
2133
+ function normalizeReviewComment(entry) {
2134
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2135
+ return null;
2136
+ const record = entry;
2137
+ const body = typeof record.body === "string" ? record.body : null;
2138
+ const path = typeof record.path === "string" ? record.path : null;
2139
+ if (!body && !path)
2140
+ return null;
2141
+ return {
2142
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
2143
+ user: record.user && typeof record.user === "object" ? record.user : null,
2144
+ author: record.author && typeof record.author === "object" ? record.author : null,
2145
+ body,
2146
+ path,
2147
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
2148
+ url: typeof record.url === "string" ? record.url : null,
2149
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
2150
+ original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
2151
+ };
2152
+ }
2153
+ function normalizeIssueComment(entry) {
2154
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2155
+ return null;
2156
+ const record = entry;
2157
+ const body = typeof record.body === "string" ? record.body : null;
2158
+ if (!body)
2159
+ return null;
2160
+ return {
2161
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
2162
+ user: record.user && typeof record.user === "object" ? record.user : null,
2163
+ author: record.author && typeof record.author === "object" ? record.author : null,
2164
+ body,
2165
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
2166
+ url: typeof record.url === "string" ? record.url : null,
2167
+ created_at: typeof record.created_at === "string" ? record.created_at : null
2168
+ };
2169
+ }
2170
+ function normalizeReviewThread(entry) {
2171
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
2172
+ return null;
2173
+ const record = entry;
2174
+ return {
2175
+ id: typeof record.id === "string" ? record.id : null,
2176
+ isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
2177
+ isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
2178
+ comments: record.comments && typeof record.comments === "object" ? record.comments : null
2179
+ };
2180
+ }
2181
+ function relevantIssueComment(comment) {
2182
+ const login = comment.user?.login ?? comment.author?.login ?? "";
2183
+ const body = comment.body ?? "";
2184
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
2185
+ }
2186
+ function latestThreadComment(thread) {
2187
+ const nodes = thread.comments?.nodes ?? [];
2188
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
2189
+ }
2190
+ function unresolvedThreadSummaries(threads) {
2191
+ return threads.flatMap((thread) => {
2192
+ if (thread.isResolved === true || thread.isOutdated === true)
2193
+ return [];
2194
+ const latest = latestThreadComment(thread);
2195
+ if (!latest)
2196
+ return ["Unresolved review thread"];
2197
+ const path = latest.path ? ` on ${latest.path}` : "";
2198
+ return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
2199
+ });
2200
+ }
2201
+ function collectBodies(evidence) {
2202
+ return [
2203
+ evidence.title ?? "",
2204
+ evidence.body,
2205
+ ...evidence.reviews.map((review) => review.body ?? ""),
2206
+ ...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
2207
+ ...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
2208
+ ...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
2209
+ ...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
2210
+ ].filter((body) => body.trim().length > 0);
2211
+ }
2212
+ function bodyExcerpt(body) {
2213
+ const text = stripHtml(body).replace(/\s+/g, " ").trim();
2214
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
2215
+ }
2216
+ function makeGreptileSignal(input) {
2217
+ const scores = parseGreptileScores(input.body);
2218
+ const reviewedSha = input.reviewedSha?.trim() || null;
2219
+ const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
2220
+ const verdict = input.verdict ?? null;
2221
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
2222
+ const explicitApproval = input.explicitApproval ?? false;
2223
+ return {
2224
+ source: input.source,
2225
+ trusted: input.trusted,
2226
+ authorLogin: input.authorLogin ?? null,
2227
+ reviewedSha,
2228
+ current,
2229
+ stale: current === false,
2230
+ score: scores[0] ?? null,
2231
+ scores,
2232
+ explicitApproval,
2233
+ verdict,
2234
+ blocker,
2235
+ actionable: input.actionable ?? blocker,
2236
+ bodyExcerpt: bodyExcerpt(input.body),
2237
+ body: input.body,
2238
+ allScores: scores
2239
+ };
2240
+ }
2241
+ function reviewAuthorLogin(review) {
2242
+ return review.author?.login ?? null;
2243
+ }
2244
+ function commentAuthorLogin(comment) {
2245
+ return comment.user?.login ?? comment.author?.login ?? null;
2246
+ }
2247
+ function collectGreptileSignals(evidence) {
2248
+ const signals = [];
2249
+ const contextSources = [
2250
+ { source: "pr-title", body: evidence.title ?? "" },
2251
+ { source: "pr-body", body: evidence.body }
2252
+ ];
2253
+ for (const context of contextSources) {
2254
+ if (!context.body.trim())
2255
+ continue;
2256
+ const contextBlocker = containsBlockerText(context.body);
2257
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
2258
+ continue;
2259
+ signals.push(makeGreptileSignal({
2260
+ source: context.source,
2261
+ body: context.body,
2262
+ currentHeadSha: evidence.currentHeadSha,
2263
+ trusted: false,
2264
+ blocker: contextBlocker,
2265
+ actionable: contextBlocker
2266
+ }));
2267
+ }
2268
+ for (const apiSignal of evidence.apiSignals ?? []) {
2269
+ const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
2270
+
2271
+ `) || "Status: UNKNOWN";
2272
+ const verdict = greptileStatusVerdict(apiSignal.status);
2273
+ signals.push(makeGreptileSignal({
2274
+ source: "api",
2275
+ body,
2276
+ currentHeadSha: evidence.currentHeadSha,
2277
+ trusted: true,
2278
+ reviewedSha: apiSignal.reviewedSha ?? null,
2279
+ explicitApproval: verdict === "approved",
2280
+ verdict
2281
+ }));
2282
+ }
2283
+ for (const review of evidence.reviews) {
2284
+ const login = reviewAuthorLogin(review);
2285
+ if (!isGreptileGithubLogin(login))
2286
+ continue;
2287
+ const state = String(review.state ?? "").toUpperCase();
2288
+ const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
2289
+
2290
+ `);
2291
+ if (!body.trim())
2292
+ continue;
2293
+ const dismissed = state === "DISMISSED";
2294
+ signals.push(makeGreptileSignal({
2295
+ source: "github-review",
2296
+ body,
2297
+ currentHeadSha: evidence.currentHeadSha,
2298
+ trusted: !dismissed,
2299
+ authorLogin: login,
2300
+ reviewedSha: review.commit_id ?? null,
2301
+ explicitApproval: undefined,
2302
+ blocker: state === "CHANGES_REQUESTED" || undefined
2303
+ }));
2304
+ }
2305
+ for (const comment of evidence.relevantIssueComments) {
2306
+ const login = commentAuthorLogin(comment);
2307
+ const body = comment.body ?? "";
2308
+ if (!body.trim() || !isGreptileGithubLogin(login))
2309
+ continue;
2310
+ signals.push(makeGreptileSignal({
2311
+ source: "issue-comment",
2312
+ body,
2313
+ currentHeadSha: evidence.currentHeadSha,
2314
+ trusted: true,
2315
+ authorLogin: login
2316
+ }));
2317
+ }
2318
+ for (const thread of evidence.reviewThreads) {
2319
+ if (thread.isOutdated === true || thread.isResolved === true)
2320
+ continue;
2321
+ for (const comment of thread.comments?.nodes ?? []) {
2322
+ const login = comment.author?.login ?? null;
2323
+ const body = comment.body ?? "";
2324
+ if (!body.trim() || !isGreptileGithubLogin(login))
2325
+ continue;
2326
+ signals.push(makeGreptileSignal({
2327
+ source: "review-thread",
2328
+ body,
2329
+ currentHeadSha: evidence.currentHeadSha,
2330
+ trusted: true,
2331
+ authorLogin: login
2332
+ }));
2333
+ }
2334
+ }
2335
+ for (const check of evidence.checks) {
2336
+ if (!isGreptileLabel(checkName(check)))
2337
+ continue;
2338
+ const reviewedSha = check.headSha ?? check.head_sha ?? null;
2339
+ const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
2340
+ const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
2341
+
2342
+ `);
2343
+ signals.push(makeGreptileSignal({
2344
+ source: "github-check",
2345
+ body,
2346
+ currentHeadSha: evidence.currentHeadSha,
2347
+ trusted: false,
2348
+ reviewedSha,
2349
+ explicitApproval: false,
2350
+ blocker: isFailingCheck(check),
2351
+ actionable: isFailingCheck(check)
2352
+ }));
2353
+ }
2354
+ return signals;
2355
+ }
2356
+ function unresolvedGreptileThreadSummaries(threads) {
2357
+ return threads.flatMap((thread) => {
2358
+ if (thread.isResolved === true || thread.isOutdated === true)
2359
+ return [];
2360
+ const comments = thread.comments?.nodes ?? [];
2361
+ if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
2362
+ return [];
2363
+ const latest = latestThreadComment(thread);
2364
+ if (!latest)
2365
+ return ["Unresolved Greptile review thread"];
2366
+ const path = latest.path ? ` on ${latest.path}` : "";
2367
+ return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
2368
+ });
2369
+ }
2370
+ function actionableChangedFileCommentSummaries(_comments) {
2371
+ return [];
2372
+ }
2373
+ function issueLevelBlockerSummaries(comments) {
2374
+ return comments.flatMap((comment) => {
2375
+ const body = comment.body?.trim() ?? "";
2376
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
2377
+ return [];
2378
+ const login = commentAuthorLogin(comment) ?? "unknown";
2379
+ const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
2380
+ return [`${author}: ${body}`];
2381
+ });
2382
+ }
2383
+ function reviewBodyBlockerSummaries(reviews) {
2384
+ return reviews.flatMap((review) => {
2385
+ const login = reviewAuthorLogin(review) ?? "unknown";
2386
+ if (isGreptileGithubLogin(login))
2387
+ return [];
2388
+ const body = review.body?.trim() ?? "";
2389
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
2390
+ return [];
2391
+ const state = review.state ? ` (${review.state})` : "";
2392
+ return [`PR review summary by ${login}${state}: ${body}`];
2393
+ });
2394
+ }
2395
+ function signalLabel(signal) {
2396
+ const source = signal.source.replace(/-/g, " ");
2397
+ const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
2398
+ const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
2399
+ return `${source}${author}${sha}`;
2400
+ }
2401
+ function deriveGreptileEvidence(input) {
2402
+ const rawBodies = collectBodies(input);
2403
+ const signals = collectGreptileSignals(input);
2404
+ const trustedSignals = signals.filter((signal) => signal.trusted);
2405
+ const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
2406
+ const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
2407
+ const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
2408
+ const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
2409
+ const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
2410
+ const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
2411
+ const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
2412
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
2413
+ const signalCanApproveByScore = (signal) => {
2414
+ if (signal.source === "api")
2415
+ return signal.verdict === "approved" || signal.verdict === "completed";
2416
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
2417
+ };
2418
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
2419
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
2420
+ const approvedByScore = !!approvingScoreEntry;
2421
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
2422
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
2423
+ const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
2424
+ const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
2425
+ const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
2426
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
2427
+ const staleBlockingSignals = [];
2428
+ const blockers = [
2429
+ ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
2430
+ ...reviewBodyBlockerSummaries(input.reviews),
2431
+ ...issueLevelBlockerSummaries(input.relevantIssueComments),
2432
+ ...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
2433
+ ...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
2434
+ ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
2435
+ ];
2436
+ const unresolvedComments = [
2437
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
2438
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
2439
+ ];
2440
+ const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
2441
+ const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
2442
+ const completedGreptileCheck = greptileChecks.some((check) => {
2443
+ const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
2444
+ return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
2445
+ });
2446
+ const completedGreptileReview = greptileReviews.some((review) => {
2447
+ const state = String(review.state ?? "").toUpperCase();
2448
+ const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
2449
+ return completedState && review.commit_id === input.currentHeadSha;
2450
+ });
2451
+ 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"));
2452
+ const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
2453
+ const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
2454
+ const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
2455
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
2456
+ const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
2457
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
2458
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
2459
+ 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";
2460
+ return {
2461
+ source,
2462
+ currentHeadSha: input.currentHeadSha,
2463
+ reviewedSha,
2464
+ fresh,
2465
+ completed,
2466
+ approved,
2467
+ score,
2468
+ explicitApproval: approvedByExplicitMapping,
2469
+ blockers,
2470
+ unresolvedComments,
2471
+ rawBodies,
2472
+ signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
2473
+ mapping
2474
+ };
2475
+ }
2476
+ function isGreptileCheckDetail(check) {
2477
+ return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
2478
+ }
2479
+ async function collectGreptileCheckDetails(input) {
2480
+ const checkRunsRead = await runJsonArray(input.command, [
2481
+ "api",
2482
+ `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
2483
+ "--paginate",
2484
+ "--slurp",
2485
+ "--jq",
2486
+ "map(.check_runs // []) | add // []"
2487
+ ], input.projectRoot);
2488
+ const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
2489
+ return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
2490
+ }
2491
+ async function collectReviewThreads(input) {
2492
+ const reviewThreads = [];
2493
+ let afterCursor = null;
2494
+ for (let page = 0;page < 100; page += 1) {
2495
+ const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
2496
+ const threadsResponse = await runJsonObject(input.command, [
2497
+ "api",
2498
+ "graphql",
2499
+ "-F",
2500
+ `owner=${input.owner}`,
2501
+ "-F",
2502
+ `name=${input.name}`,
2503
+ "-F",
2504
+ `prNumber=${input.prNumber}`,
2505
+ "-f",
2506
+ `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 } } } } }`
2507
+ ], input.projectRoot);
2508
+ if (threadsResponse.error) {
2509
+ return { value: reviewThreads, error: threadsResponse.error };
2510
+ }
2511
+ const data = threadsResponse.value.data;
2512
+ const repository = data?.repository;
2513
+ const pullRequest = repository?.pullRequest;
2514
+ const threads = pullRequest?.reviewThreads;
2515
+ const nodes = threads?.nodes;
2516
+ if (!Array.isArray(nodes)) {
2517
+ return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
2518
+ }
2519
+ const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
2520
+ reviewThreads.push(...normalized);
2521
+ const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
2522
+ if (truncatedCommentThread) {
2523
+ return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
2524
+ }
2525
+ const pageInfo = threads?.pageInfo;
2526
+ if (!pageInfo) {
2527
+ if (nodes.length >= 100) {
2528
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
2529
+ }
2530
+ return { value: reviewThreads };
2531
+ }
2532
+ if (pageInfo.hasNextPage !== true) {
2533
+ return { value: reviewThreads };
2534
+ }
2535
+ if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
2536
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
2537
+ }
2538
+ afterCursor = pageInfo.endCursor;
2539
+ }
2540
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
2541
+ }
2542
+ async function collectPrReviewEvidence(input) {
2543
+ const parsed = parseGithubPrUrl(input.prUrl);
2544
+ if (!parsed) {
2545
+ throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
2546
+ }
2547
+ const readErrors = [];
2548
+ const viewRead = await runJsonObject(input.command, [
2549
+ "pr",
2550
+ "view",
2551
+ input.prUrl,
2552
+ "--json",
2553
+ "title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
2554
+ ], input.projectRoot);
2555
+ if (viewRead.error)
2556
+ readErrors.push(viewRead.error);
2557
+ const view = viewRead.value;
2558
+ if (!Array.isArray(view.statusCheckRollup)) {
2559
+ readErrors.push("gh pr view did not return required statusCheckRollup array");
2560
+ }
2561
+ if (!Array.isArray(view.reviews)) {
2562
+ readErrors.push("gh pr view did not return required reviews array");
2563
+ }
2564
+ const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
2565
+ const baseRefName = firstString(view, ["baseRefName"]);
2566
+ const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
2567
+ const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
2568
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
2569
+ if (reviewCommentsRead.error)
2570
+ readErrors.push(reviewCommentsRead.error);
2571
+ const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
2572
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
2573
+ if (issueCommentsRead.error)
2574
+ readErrors.push(issueCommentsRead.error);
2575
+ const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
2576
+ const reviewThreadsRead = await collectReviewThreads({
2577
+ command: input.command,
2578
+ projectRoot: input.projectRoot,
2579
+ owner: parsed.owner,
2580
+ name: parsed.repo,
2581
+ prNumber: parsed.prNumber
2582
+ });
2583
+ if (reviewThreadsRead.error)
2584
+ readErrors.push(reviewThreadsRead.error);
2585
+ const reviewThreads = reviewThreadsRead.value;
2586
+ const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
2587
+ let greptileCheckDetails = [];
2588
+ if (headSha && greptileRollupChecks.length > 0) {
2589
+ const checkDetailsRead = await collectGreptileCheckDetails({
2590
+ command: input.command,
2591
+ projectRoot: input.projectRoot,
2592
+ repoName: parsed.repoName,
2593
+ headSha
2594
+ });
2595
+ if (checkDetailsRead.error)
2596
+ readErrors.push(checkDetailsRead.error);
2597
+ greptileCheckDetails = checkDetailsRead.value;
2598
+ if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
2599
+ readErrors.push("Greptile check details could not be found for the current PR head");
2600
+ }
2601
+ }
2602
+ const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
2603
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
2604
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
2605
+ enabled: shouldCollectConfiguredGreptileApi,
2606
+ options: input.greptileApi,
2607
+ repoName: parsed.repoName,
2608
+ prNumber: parsed.prNumber,
2609
+ headSha,
2610
+ baseRefName
2611
+ });
2612
+ readErrors.push(...configuredGreptileApiRead.errors);
2613
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
2614
+ 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})` : ""}`);
2615
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
2616
+ const evidenceBase = {
2617
+ title: firstString(view, ["title"]),
2618
+ body: firstString(view, ["body"]),
2619
+ reviews,
2620
+ changedFileReviewComments: reviewComments,
2621
+ relevantIssueComments: issueComments,
2622
+ reviewThreads,
2623
+ checks: checksWithGreptileDetails,
2624
+ currentHeadSha: headSha,
2625
+ apiSignals
2626
+ };
2627
+ const greptile = deriveGreptileEvidence(evidenceBase);
2628
+ return {
2629
+ prUrl: input.prUrl,
2630
+ prNumber: parsed.prNumber,
2631
+ repoName: parsed.repoName,
2632
+ title: evidenceBase.title,
2633
+ body: evidenceBase.body,
2634
+ headSha,
2635
+ headRefName: firstString(view, ["headRefName"]),
2636
+ baseRefName,
2637
+ state: firstString(view, ["state"]),
2638
+ isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
2639
+ mergeable: firstString(view, ["mergeable"]),
2640
+ mergeStateStatus: firstString(view, ["mergeStateStatus"]),
2641
+ reviewDecision: firstString(view, ["reviewDecision"]),
2642
+ reviews,
2643
+ reviewThreads,
2644
+ changedFileReviewComments: reviewComments,
2645
+ relevantIssueComments: issueComments,
2646
+ statusCheckRollup: checksWithGreptileDetails,
2647
+ checkFailures,
2648
+ pendingChecks,
2649
+ readErrors,
2650
+ greptile
2651
+ };
2652
+ }
2653
+ function capGateMessage(value, maxChars = 1200) {
2654
+ const normalized = value.trim();
2655
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
2656
+ [truncated for gate summary; see full evidence artifact]` : normalized;
2657
+ }
2658
+ function evaluateEvidence(evidence) {
2659
+ const reasonDetails = [];
2660
+ const warnings = [];
2661
+ const seen = new Set;
2662
+ const addReason = (reason) => {
2663
+ const capped = { ...reason, message: capGateMessage(reason.message) };
2664
+ const key = `${capped.code}:${capped.message}`;
2665
+ if (seen.has(key))
2666
+ return;
2667
+ seen.add(key);
2668
+ reasonDetails.push(capped);
2669
+ };
2670
+ const greptile = evidence.greptile;
2671
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
2672
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
2673
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
2674
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
2675
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
2676
+ for (const error of evidence.readErrors) {
2677
+ addReason({
2678
+ code: "read_error",
2679
+ reasonClass: "reject",
2680
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
2681
+ suggestedAction: "needs_attention",
2682
+ message: `Required PR evidence surface could not be read completely: ${error}`,
2683
+ headSha: evidence.headSha || null
2684
+ });
2685
+ }
2686
+ if (!evidence.headSha) {
2687
+ addReason({
2688
+ code: "missing_head_sha",
2689
+ reasonClass: "reject",
2690
+ surface: "github",
2691
+ suggestedAction: "needs_attention",
2692
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
2693
+ headSha: null
2694
+ });
2695
+ }
2696
+ for (const failure of evidence.checkFailures) {
2697
+ addReason({
2698
+ code: "ci_failed",
2699
+ reasonClass: "reject",
2700
+ surface: "ci",
2701
+ suggestedAction: "fix",
2702
+ message: failure,
2703
+ headSha: evidence.headSha || null
2704
+ });
2705
+ }
2706
+ for (const pendingCheck of evidence.pendingChecks) {
2707
+ addReason({
2708
+ code: "check_pending",
2709
+ reasonClass: "pending",
2710
+ surface: "ci",
2711
+ suggestedAction: "wait",
2712
+ message: pendingCheck,
2713
+ headSha: evidence.headSha || null
2714
+ });
2715
+ }
2716
+ const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
2717
+ if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
2718
+ addReason({
2719
+ code: "review_decision_blocking",
2720
+ reasonClass: "reject",
2721
+ surface: "review",
2722
+ suggestedAction: "fix",
2723
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
2724
+ headSha: evidence.headSha || null
2725
+ });
2726
+ }
2727
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
2728
+ addReason({
2729
+ code: "review_thread_unresolved",
2730
+ reasonClass: "reject",
2731
+ surface: "review",
2732
+ suggestedAction: "fix",
2733
+ message: thread,
2734
+ headSha: evidence.headSha || null
2735
+ });
2736
+ }
2737
+ if (greptile.mapping === "missing") {
2738
+ addReason({
2739
+ code: "greptile_missing",
2740
+ reasonClass: "pending",
2741
+ surface: "greptile",
2742
+ suggestedAction: "wait",
2743
+ message: "Missing Greptile check/review evidence for this PR.",
2744
+ headSha: evidence.headSha || null
2745
+ });
2746
+ }
2747
+ if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
2748
+ addReason({
2749
+ code: "greptile_stale",
2750
+ reasonClass: "pending",
2751
+ surface: "greptile",
2752
+ suggestedAction: "wait",
2753
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
2754
+ headSha: evidence.headSha || null,
2755
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
2756
+ });
2757
+ }
2758
+ for (const signal of pendingGreptileApiSignals) {
2759
+ addReason({
2760
+ code: "greptile_pending",
2761
+ reasonClass: "pending",
2762
+ surface: "greptile",
2763
+ suggestedAction: "wait",
2764
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
2765
+ headSha: evidence.headSha || null,
2766
+ reviewedSha: signal.reviewedSha ?? null
2767
+ });
2768
+ }
2769
+ for (const signal of unknownGreptileApiSignals) {
2770
+ addReason({
2771
+ code: "greptile_api_status_unknown",
2772
+ reasonClass: "reject",
2773
+ surface: "greptile",
2774
+ suggestedAction: "needs_attention",
2775
+ 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}` : "."}`,
2776
+ headSha: evidence.headSha || null,
2777
+ reviewedSha: signal.reviewedSha ?? null
2778
+ });
2779
+ }
2780
+ if (!greptile.completed) {
2781
+ addReason({
2782
+ code: "greptile_pending",
2783
+ reasonClass: "pending",
2784
+ surface: "greptile",
2785
+ suggestedAction: "wait",
2786
+ message: "Greptile check/review has not completed for the current PR head.",
2787
+ headSha: evidence.headSha || null,
2788
+ reviewedSha: greptile.reviewedSha ?? null
2789
+ });
2790
+ }
2791
+ if (!greptile.fresh) {
2792
+ addReason({
2793
+ code: "greptile_not_current_head",
2794
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2795
+ surface: "greptile",
2796
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2797
+ message: "Greptile approval is not tied to the current PR head SHA.",
2798
+ headSha: evidence.headSha || null,
2799
+ reviewedSha: greptile.reviewedSha ?? null
2800
+ });
2801
+ }
2802
+ if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
2803
+ addReason({
2804
+ code: "greptile_score_not_5",
2805
+ reasonClass: "reject",
2806
+ surface: "greptile",
2807
+ suggestedAction: "fix",
2808
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
2809
+ headSha: evidence.headSha || null,
2810
+ reviewedSha: greptile.reviewedSha ?? null
2811
+ });
2812
+ }
2813
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
2814
+ if (!greptile.score && !hasApprovedMapping) {
2815
+ addReason({
2816
+ code: "greptile_score_missing",
2817
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2818
+ surface: "greptile",
2819
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2820
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
2821
+ headSha: evidence.headSha || null,
2822
+ reviewedSha: greptile.reviewedSha ?? null
2823
+ });
2824
+ }
2825
+ if (greptile.mapping === "unproven") {
2826
+ addReason({
2827
+ code: "greptile_mapping_unproven",
2828
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2829
+ surface: "greptile",
2830
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2831
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
2832
+ headSha: evidence.headSha || null,
2833
+ reviewedSha: greptile.reviewedSha ?? null
2834
+ });
2835
+ }
2836
+ for (const blocker of greptile.blockers) {
2837
+ addReason({
2838
+ code: "greptile_blocker_text",
2839
+ reasonClass: "reject",
2840
+ surface: "greptile",
2841
+ suggestedAction: "fix",
2842
+ message: `Greptile/blocker text: ${blocker}`,
2843
+ headSha: evidence.headSha || null,
2844
+ reviewedSha: greptile.reviewedSha ?? null
2845
+ });
2846
+ }
2847
+ for (const comment of greptile.unresolvedComments) {
2848
+ addReason({
2849
+ code: "greptile_unresolved_comment",
2850
+ reasonClass: "reject",
2851
+ surface: "greptile",
2852
+ suggestedAction: "fix",
2853
+ message: comment,
2854
+ headSha: evidence.headSha || null,
2855
+ reviewedSha: greptile.reviewedSha ?? null
2856
+ });
2857
+ }
2858
+ if (!greptile.approved)
2859
+ warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
2860
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
2861
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
2862
+ }
2863
+ function evaluateStrictPrMergeGate(evidence) {
2864
+ const evaluated = evaluateEvidence(evidence);
2865
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
2866
+ return {
2867
+ approved,
2868
+ pending: evaluated.pending,
2869
+ reasons: evaluated.reasons,
2870
+ reasonDetails: evaluated.reasonDetails,
2871
+ warnings: evaluated.warnings,
2872
+ actionableFeedback: evaluated.reasons,
2873
+ evidence
2874
+ };
2875
+ }
2876
+
1759
2877
  // packages/runtime/src/control-plane/native/git-ops.ts
1760
2878
  var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
1761
2879
  "changed-files.txt",
@@ -2589,7 +3707,8 @@ async function runGreptileReviewForPr(options) {
2589
3707
  }
2590
3708
  };
2591
3709
  }
2592
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
3710
+ const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
3711
+ 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)) {
2593
3712
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
2594
3713
  return {
2595
3714
  verdict: "REJECT",
@@ -2605,44 +3724,79 @@ async function runGreptileReviewForPr(options) {
2605
3724
  }
2606
3725
  };
2607
3726
  }
2608
- if (score) {
2609
- if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
2610
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
2611
- return {
2612
- verdict: "REJECT",
2613
- feedback,
2614
- reasons,
2615
- warnings,
2616
- rawPayload: {
2617
- pr: options.prState,
2618
- codeReviews: reviewsPayload,
2619
- selectedReview,
2620
- reviewDetails,
2621
- comments: commentsPayload,
2622
- score
2623
- }
2624
- };
2625
- }
2626
- if (score.scale === 5 && score.value <= 2) {
2627
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
2628
- return {
2629
- verdict: "REJECT",
2630
- feedback,
2631
- reasons,
2632
- warnings,
2633
- rawPayload: {
2634
- pr: options.prState,
2635
- codeReviews: reviewsPayload,
2636
- selectedReview,
2637
- reviewDetails,
2638
- comments: commentsPayload,
2639
- score
3727
+ if (score?.scale === 5 && score.value < 5) {
3728
+ reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
3729
+ return {
3730
+ verdict: "REJECT",
3731
+ feedback,
3732
+ reasons,
3733
+ warnings,
3734
+ rawPayload: {
3735
+ pr: options.prState,
3736
+ codeReviews: reviewsPayload,
3737
+ selectedReview,
3738
+ reviewDetails,
3739
+ comments: commentsPayload,
3740
+ score
3741
+ }
3742
+ };
3743
+ }
3744
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
3745
+ let strictGate = null;
3746
+ try {
3747
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
3748
+ projectRoot: options.projectRoot,
3749
+ taskId: options.taskId,
3750
+ prUrl,
3751
+ apiSignals: [{
3752
+ id: selectedReview.id,
3753
+ body: reviewBody,
3754
+ reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
3755
+ status: selectedReview.status
3756
+ }]
3757
+ });
3758
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
3759
+ } catch (error) {
3760
+ reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
3761
+ return {
3762
+ verdict: "REJECT",
3763
+ feedback,
3764
+ reasons,
3765
+ warnings,
3766
+ rawPayload: {
3767
+ pr: options.prState,
3768
+ codeReviews: reviewsPayload,
3769
+ selectedReview,
3770
+ reviewDetails,
3771
+ comments: commentsPayload,
3772
+ score
3773
+ }
3774
+ };
3775
+ }
3776
+ if (!strictGate.approved) {
3777
+ return {
3778
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
3779
+ feedback,
3780
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
3781
+ warnings: [...warnings, ...strictGate.warnings],
3782
+ rawPayload: {
3783
+ pr: options.prState,
3784
+ codeReviews: reviewsPayload,
3785
+ selectedReview,
3786
+ reviewDetails,
3787
+ comments: commentsPayload,
3788
+ score,
3789
+ strictGate: {
3790
+ approved: strictGate.approved,
3791
+ pending: strictGate.pending,
3792
+ reasons: strictGate.reasons,
3793
+ reasonDetails: strictGate.reasonDetails,
3794
+ warnings: strictGate.warnings,
3795
+ greptile: strictGate.evidence.greptile,
3796
+ readErrors: strictGate.evidence.readErrors
2640
3797
  }
2641
- };
2642
- }
2643
- if (score.scale === 5 && score.value < 5) {
2644
- warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
2645
- }
3798
+ }
3799
+ };
2646
3800
  }
2647
3801
  return {
2648
3802
  verdict: "APPROVE",
@@ -2654,7 +3808,16 @@ async function runGreptileReviewForPr(options) {
2654
3808
  codeReviews: reviewsPayload,
2655
3809
  selectedReview,
2656
3810
  reviewDetails,
2657
- comments: commentsPayload
3811
+ comments: commentsPayload,
3812
+ strictGate: {
3813
+ approved: strictGate.approved,
3814
+ pending: strictGate.pending,
3815
+ reasons: strictGate.reasons,
3816
+ reasonDetails: strictGate.reasonDetails,
3817
+ warnings: strictGate.warnings,
3818
+ greptile: strictGate.evidence.greptile,
3819
+ readErrors: strictGate.evidence.readErrors
3820
+ }
2658
3821
  }
2659
3822
  };
2660
3823
  }
@@ -2678,7 +3841,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2678
3841
  let threads = [];
2679
3842
  let actionableThreads = [];
2680
3843
  let checkRollup = [];
2681
- let checkState = { pending: false, completed: false };
3844
+ let checkState2 = { pending: false, completed: false };
2682
3845
  for (let attempt = 0;; attempt += 1) {
2683
3846
  reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
2684
3847
  selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
@@ -2687,15 +3850,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2687
3850
  threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
2688
3851
  actionableThreads = filterActionableGithubGreptileThreads(threads);
2689
3852
  checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
2690
- checkState = classifyGithubGreptileCheckState(checkRollup);
2691
- const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
3853
+ checkState2 = classifyGithubGreptileCheckState(checkRollup);
3854
+ const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
2692
3855
  if (!shouldContinueGithubGreptileFallbackPolling({
2693
3856
  attempt,
2694
3857
  pollAttempts: options.pollAttempts,
2695
- checkState,
3858
+ checkState: checkState2,
2696
3859
  fallbackReview,
2697
3860
  selectedReview,
2698
- approvedViaReviewedAncestor: approvedViaReviewedAncestor2
3861
+ approvedViaReviewedAncestor
2699
3862
  })) {
2700
3863
  break;
2701
3864
  }
@@ -2723,7 +3886,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2723
3886
  ].filter(Boolean).join(`
2724
3887
  `);
2725
3888
  const warnings = buildGithubGreptileFallbackWarnings(options);
2726
- if (checkState.pending) {
3889
+ if (checkState2.pending) {
2727
3890
  return {
2728
3891
  verdict: "SKIP",
2729
3892
  feedback,
@@ -2734,34 +3897,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2734
3897
  rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
2735
3898
  };
2736
3899
  }
2737
- const approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
2738
- if (!fallbackReview) {
2739
- if (approvedViaCompletedCheck) {
2740
- 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.`);
2741
- return {
2742
- verdict: "APPROVE",
2743
- feedback,
2744
- reasons: [],
2745
- warnings,
2746
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
2747
- };
2748
- }
2749
- return {
2750
- verdict: "SKIP",
2751
- feedback,
2752
- reasons: [
2753
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
2754
- ],
2755
- warnings,
2756
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
2757
- };
2758
- }
2759
- const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
2760
- if (actionableThreads.length > 0) {
3900
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
3901
+ let strictGate;
3902
+ try {
3903
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
3904
+ projectRoot: options.projectRoot,
3905
+ taskId: options.taskId,
3906
+ prUrl
3907
+ });
3908
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
3909
+ } catch (error) {
2761
3910
  return {
2762
3911
  verdict: "REJECT",
2763
3912
  feedback,
2764
- reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
3913
+ reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
2765
3914
  warnings,
2766
3915
  rawPayload: {
2767
3916
  pr: options.prState,
@@ -2774,44 +3923,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2774
3923
  }
2775
3924
  };
2776
3925
  }
2777
- if (!selectedReview && !approvedViaReviewedAncestor) {
2778
- if (approvedViaCompletedCheck) {
2779
- 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.`);
2780
- return {
2781
- verdict: "APPROVE",
2782
- feedback,
2783
- reasons: [],
2784
- warnings,
2785
- rawPayload: {
2786
- pr: options.prState,
2787
- selectedReview: fallbackReview,
2788
- reviews,
2789
- threads,
2790
- checkRollup,
2791
- ...buildGithubGreptileFallbackRawPayload(options)
2792
- }
2793
- };
2794
- }
3926
+ if (!strictGate.approved) {
2795
3927
  return {
2796
- verdict: "SKIP",
3928
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
2797
3929
  feedback,
2798
- reasons: [
2799
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
2800
- ],
2801
- warnings,
3930
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
3931
+ warnings: [...warnings, ...strictGate.warnings],
2802
3932
  rawPayload: {
2803
3933
  pr: options.prState,
2804
3934
  selectedReview: fallbackReview,
2805
3935
  reviews,
2806
3936
  threads,
2807
3937
  checkRollup,
3938
+ actionableThreads,
3939
+ strictGate: {
3940
+ approved: strictGate.approved,
3941
+ pending: strictGate.pending,
3942
+ reasons: strictGate.reasons,
3943
+ reasonDetails: strictGate.reasonDetails,
3944
+ warnings: strictGate.warnings,
3945
+ greptile: strictGate.evidence.greptile
3946
+ },
2808
3947
  ...buildGithubGreptileFallbackRawPayload(options)
2809
3948
  }
2810
3949
  };
2811
3950
  }
2812
- if (approvedViaReviewedAncestor) {
2813
- 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.`);
2814
- }
2815
3951
  return {
2816
3952
  verdict: "APPROVE",
2817
3953
  feedback,
@@ -2823,6 +3959,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
2823
3959
  reviews,
2824
3960
  threads,
2825
3961
  checkRollup,
3962
+ strictGate: {
3963
+ approved: strictGate.approved,
3964
+ pending: strictGate.pending,
3965
+ reasons: strictGate.reasons,
3966
+ reasonDetails: strictGate.reasonDetails,
3967
+ warnings: strictGate.warnings,
3968
+ greptile: strictGate.evidence.greptile
3969
+ },
2826
3970
  ...buildGithubGreptileFallbackRawPayload(options)
2827
3971
  }
2828
3972
  };
@@ -2935,19 +4079,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
2935
4079
  if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
2936
4080
  return true;
2937
4081
  }
2938
- return isGreptileReviewTerminal(existingReview.status);
4082
+ return false;
2939
4083
  }
2940
4084
  function shouldContinueGreptileMcpPolling(options) {
2941
4085
  if (options.githubCheckState.completed) {
2942
4086
  return false;
2943
4087
  }
4088
+ if (options.attempt + 1 >= options.pollAttempts) {
4089
+ return false;
4090
+ }
2944
4091
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
2945
4092
  return true;
2946
4093
  }
2947
- return options.attempt + 1 < options.pollAttempts;
4094
+ return true;
2948
4095
  }
2949
4096
  function shouldContinueGithubGreptileFallbackPolling(options) {
2950
4097
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
4098
+ if (options.attempt + 1 >= options.pollAttempts) {
4099
+ return false;
4100
+ }
2951
4101
  if (waitingForVisiblePendingReview) {
2952
4102
  return true;
2953
4103
  }
@@ -3008,6 +4158,20 @@ function runGhJson(projectRoot, args) {
3008
4158
  throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
3009
4159
  }
3010
4160
  }
4161
+ async function collectStrictPrEvidenceForVerifier(input) {
4162
+ return collectPrReviewEvidence({
4163
+ projectRoot: input.projectRoot,
4164
+ prUrl: input.prUrl,
4165
+ taskId: input.taskId,
4166
+ runId: "verifier",
4167
+ cycle: 0,
4168
+ apiSignals: input.apiSignals ?? [],
4169
+ command: async (args, options) => {
4170
+ const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
4171
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
4172
+ }
4173
+ });
4174
+ }
3011
4175
  function deriveRepoName(projectRoot, prState) {
3012
4176
  const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
3013
4177
  if (fromUrl?.[1]) {
@@ -3022,8 +4186,9 @@ function resolvePrHeadSha(projectRoot, prState) {
3022
4186
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
3023
4187
  return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
3024
4188
  }
3025
- function isGreptileGithubLogin(login) {
3026
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
4189
+ function isGreptileGithubLogin2(login) {
4190
+ const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
4191
+ return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
3027
4192
  }
3028
4193
  function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
3029
4194
  const matching = sortGithubGreptileReviews(reviews);
@@ -3040,7 +4205,7 @@ function pickLatestGithubGreptileReview(reviews) {
3040
4205
  return sortGithubGreptileReviews(reviews)[0] || null;
3041
4206
  }
3042
4207
  function sortGithubGreptileReviews(reviews) {
3043
- return reviews.filter((review) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
4208
+ return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
3044
4209
  }
3045
4210
  function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
3046
4211
  const response = runGhJson(projectRoot, [
@@ -3113,31 +4278,8 @@ function classifyGithubGreptileCheckState(checks) {
3113
4278
  }
3114
4279
  return { pending: false, completed: false };
3115
4280
  }
3116
- function isGithubGreptileCheckApproved(checks) {
3117
- const greptileChecks = checks.filter((check) => {
3118
- const label = (check.name || check.context || "").toLowerCase();
3119
- return label.includes("greptile");
3120
- });
3121
- if (greptileChecks.length === 0) {
3122
- return false;
3123
- }
3124
- for (const check of greptileChecks) {
3125
- if ((check.__typename || "") === "CheckRun") {
3126
- if ((check.status || "").toUpperCase() !== "COMPLETED") {
3127
- return false;
3128
- }
3129
- const conclusion = (check.conclusion || "").toUpperCase();
3130
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
3131
- return false;
3132
- }
3133
- continue;
3134
- }
3135
- const state = (check.state || "").toUpperCase();
3136
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
3137
- return false;
3138
- }
3139
- }
3140
- return true;
4281
+ function isGithubGreptileCheckApproved(_checks) {
4282
+ return false;
3141
4283
  }
3142
4284
  function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
3143
4285
  const [owner, name] = repoName.split("/");
@@ -3164,7 +4306,7 @@ function filterActionableGithubGreptileThreads(threads) {
3164
4306
  return [];
3165
4307
  }
3166
4308
  const comments = thread.comments?.nodes || [];
3167
- const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
4309
+ const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
3168
4310
  if (!latestGreptileComment?.path?.trim()) {
3169
4311
  return [];
3170
4312
  }
@@ -3186,11 +4328,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
3186
4328
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
3187
4329
  return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
3188
4330
  }
3189
- function stripHtml(input) {
3190
- return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
3191
-
3192
- `).trim();
3193
- }
3194
4331
  function summarizeComment(input) {
3195
4332
  const text = stripHtml(input).replace(/\s+/g, " ").trim();
3196
4333
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
@@ -3199,31 +4336,14 @@ function asGreptileInfrastructureWarning(reason) {
3199
4336
  return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
3200
4337
  }
3201
4338
  function isAiReviewApproved(input) {
4339
+ if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
4340
+ return false;
4341
+ }
3202
4342
  if (input.reviewMode !== "required") {
3203
4343
  return true;
3204
4344
  }
3205
4345
  return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
3206
4346
  }
3207
- function parseGreptileScore(input) {
3208
- const text = stripHtml(input);
3209
- const patterns = [
3210
- /confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
3211
- /\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
3212
- /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
3213
- ];
3214
- for (const pattern of patterns) {
3215
- const match = pattern.exec(text);
3216
- if (!match) {
3217
- continue;
3218
- }
3219
- const value = Number.parseInt(match[1] || "", 10);
3220
- const scale = Number.parseInt(match[2] || "", 10);
3221
- if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
3222
- return { value, scale };
3223
- }
3224
- }
3225
- return null;
3226
- }
3227
4347
  var __testOnly = {
3228
4348
  asGreptileInfrastructureWarning,
3229
4349
  callGreptileMcpToolWithTimeout,