@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.
- package/dist/bin/rig-agent-dispatch.js +84 -313
- package/dist/bin/rig-agent.js +85 -27
- package/dist/src/control-plane/agent-wrapper.js +101 -27
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +1357 -180
- package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
- package/dist/src/control-plane/hooks/inject-context.js +2 -2
- package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
- package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
- package/dist/src/control-plane/native/git-ops.js +134 -68
- package/dist/src/control-plane/native/harness-cli.js +1357 -180
- package/dist/src/control-plane/native/pr-automation.js +1532 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
- package/dist/src/control-plane/native/run-ops.js +35 -12
- package/dist/src/control-plane/native/task-ops.js +1274 -155
- package/dist/src/control-plane/native/validator.js +2 -2
- package/dist/src/control-plane/native/verifier.js +1274 -154
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/runtime/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/home.js +31 -6
- package/dist/src/control-plane/runtime/isolation/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +38 -9
- package/dist/src/control-plane/runtime/queue.js +38 -9
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
- package/dist/src/index.js +27 -20
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
- package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
- package/package.json +6 -6
- package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
- package/native/darwin-arm64/manifest.json +0 -1
- package/native/linux-x64/bin/rig-git +0 -0
- package/native/linux-x64/bin/rig-shell +0 -0
- package/native/linux-x64/bin/rig-tools +0 -0
- package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
- package/native/linux-x64/lib/runtime-native.so +0 -0
- 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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/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
|
-
|
|
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
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
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
|
|
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
|
-
|
|
2691
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3026
|
-
|
|
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) =>
|
|
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(
|
|
3117
|
-
|
|
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) =>
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/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,
|