@h-rig/runtime 0.0.6-alpha.11 → 0.0.6-alpha.12
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 +5 -313
- package/dist/bin/rig-agent.js +3 -2
- package/dist/src/control-plane/agent-wrapper.js +10 -15
- package/dist/src/control-plane/harness-main.js +923 -156
- package/dist/src/control-plane/hooks/completion-verification.js +1191 -284
- package/dist/src/control-plane/native/git-ops.js +31 -43
- package/dist/src/control-plane/native/harness-cli.js +923 -156
- package/dist/src/control-plane/native/pr-automation.js +1010 -38
- package/dist/src/control-plane/native/pr-review-gate.js +907 -0
- package/dist/src/control-plane/native/task-ops.js +918 -154
- package/dist/src/control-plane/native/verifier.js +920 -153
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +6 -6
|
@@ -1793,6 +1793,777 @@ function isGitOpenPrResult(value) {
|
|
|
1793
1793
|
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
1794
1794
|
}
|
|
1795
1795
|
|
|
1796
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
1797
|
+
function parseJsonObject(value) {
|
|
1798
|
+
if (!value?.trim())
|
|
1799
|
+
return { value: {}, error: "empty JSON output" };
|
|
1800
|
+
try {
|
|
1801
|
+
const parsed = JSON.parse(value);
|
|
1802
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function flattenPaginatedArray(value) {
|
|
1808
|
+
if (!Array.isArray(value))
|
|
1809
|
+
return null;
|
|
1810
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
1811
|
+
return value.flatMap((entry) => entry);
|
|
1812
|
+
}
|
|
1813
|
+
return value;
|
|
1814
|
+
}
|
|
1815
|
+
function parseJsonArray(value) {
|
|
1816
|
+
if (!value?.trim())
|
|
1817
|
+
return { value: [], error: "empty JSON output" };
|
|
1818
|
+
try {
|
|
1819
|
+
const parsed = JSON.parse(value);
|
|
1820
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
1821
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function parseGithubPrUrl(prUrl) {
|
|
1827
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
1828
|
+
if (!match)
|
|
1829
|
+
return null;
|
|
1830
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
1831
|
+
if (!Number.isFinite(prNumber))
|
|
1832
|
+
return null;
|
|
1833
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
1834
|
+
}
|
|
1835
|
+
function checkName(check) {
|
|
1836
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
1837
|
+
}
|
|
1838
|
+
function checkState(check) {
|
|
1839
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
1840
|
+
}
|
|
1841
|
+
function isGreptileLabel(value) {
|
|
1842
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
1843
|
+
}
|
|
1844
|
+
function isGreptileGithubLogin(value) {
|
|
1845
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
1846
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
1847
|
+
}
|
|
1848
|
+
function isPassingCheck(check) {
|
|
1849
|
+
const state = checkState(check);
|
|
1850
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
1851
|
+
}
|
|
1852
|
+
function isPendingCheck(check) {
|
|
1853
|
+
const state = checkState(check);
|
|
1854
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
1855
|
+
}
|
|
1856
|
+
function isFailingCheck(check) {
|
|
1857
|
+
const state = checkState(check);
|
|
1858
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
1859
|
+
}
|
|
1860
|
+
function wildcardToRegExp(pattern) {
|
|
1861
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1862
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
1863
|
+
}
|
|
1864
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
1865
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
1866
|
+
}
|
|
1867
|
+
function greptileScorePatterns() {
|
|
1868
|
+
return [
|
|
1869
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
1870
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
1871
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
1872
|
+
];
|
|
1873
|
+
}
|
|
1874
|
+
function parseGreptileScores(input) {
|
|
1875
|
+
const text = stripHtml(input);
|
|
1876
|
+
const seen = new Set;
|
|
1877
|
+
const scores = [];
|
|
1878
|
+
for (const pattern of greptileScorePatterns()) {
|
|
1879
|
+
for (const match of text.matchAll(pattern)) {
|
|
1880
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
1881
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
1882
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
1883
|
+
continue;
|
|
1884
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
1885
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
1886
|
+
if (seen.has(key))
|
|
1887
|
+
continue;
|
|
1888
|
+
seen.add(key);
|
|
1889
|
+
scores.push({ value, scale, raw });
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return scores;
|
|
1893
|
+
}
|
|
1894
|
+
function parseGreptileScore(input) {
|
|
1895
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
1896
|
+
}
|
|
1897
|
+
function stripHtml(input) {
|
|
1898
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
1899
|
+
|
|
1900
|
+
`).trim();
|
|
1901
|
+
}
|
|
1902
|
+
function containsBlockerText(input) {
|
|
1903
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
1904
|
+
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/i.test(text);
|
|
1905
|
+
}
|
|
1906
|
+
function containsGreptileNegativeVerdict(input) {
|
|
1907
|
+
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
1908
|
+
if (!text)
|
|
1909
|
+
return false;
|
|
1910
|
+
return /\b(?:status|verdict|review state|state|conclusion|result)\s*:?\s*(?:reject(?:ed|ion)?|skip(?:ped)?|fail(?:ed|ure)?|changes[_ ]requested|not approved)\b/i.test(text) || /\bgreptile\b.{0,160}\b(?:reject(?:ed|s|ion)?|skip(?:ped|s)?|fail(?:ed|s|ure)?|changes requested|did not approve|not approved)\b/i.test(text);
|
|
1911
|
+
}
|
|
1912
|
+
function isStrictFiveOfFive(score) {
|
|
1913
|
+
return score.value === 5 && score.scale === 5;
|
|
1914
|
+
}
|
|
1915
|
+
function containsConflictingScoreText(input) {
|
|
1916
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
1917
|
+
}
|
|
1918
|
+
function firstString(record, keys) {
|
|
1919
|
+
for (const key of keys) {
|
|
1920
|
+
const value = record[key];
|
|
1921
|
+
if (typeof value === "string")
|
|
1922
|
+
return value;
|
|
1923
|
+
}
|
|
1924
|
+
return "";
|
|
1925
|
+
}
|
|
1926
|
+
function arrayField(record, key) {
|
|
1927
|
+
const value = record[key];
|
|
1928
|
+
return Array.isArray(value) ? value : [];
|
|
1929
|
+
}
|
|
1930
|
+
async function runJsonArray(command, args, cwd) {
|
|
1931
|
+
const result = await command(args, { cwd });
|
|
1932
|
+
const label = `gh ${args.join(" ")}`;
|
|
1933
|
+
if (result.exitCode !== 0) {
|
|
1934
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
1935
|
+
}
|
|
1936
|
+
const parsed = parseJsonArray(result.stdout);
|
|
1937
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
1938
|
+
}
|
|
1939
|
+
async function runJsonObject(command, args, cwd) {
|
|
1940
|
+
const result = await command(args, { cwd });
|
|
1941
|
+
const label = `gh ${args.join(" ")}`;
|
|
1942
|
+
if (result.exitCode !== 0) {
|
|
1943
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
1944
|
+
}
|
|
1945
|
+
const parsed = parseJsonObject(result.stdout);
|
|
1946
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
1947
|
+
}
|
|
1948
|
+
function normalizeStatusCheck(entry) {
|
|
1949
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
1950
|
+
return null;
|
|
1951
|
+
const record = entry;
|
|
1952
|
+
const name = firstString(record, ["name", "context"]);
|
|
1953
|
+
if (!name.trim())
|
|
1954
|
+
return null;
|
|
1955
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
1956
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
1957
|
+
return {
|
|
1958
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
1959
|
+
name,
|
|
1960
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
1961
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
1962
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
1963
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
1964
|
+
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,
|
|
1965
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
1966
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
1967
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
1968
|
+
output: output ? {
|
|
1969
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
1970
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
1971
|
+
text: typeof output.text === "string" ? output.text : null
|
|
1972
|
+
} : null,
|
|
1973
|
+
app: app ? {
|
|
1974
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
1975
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
1976
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
1977
|
+
} : null
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function normalizeReview(entry) {
|
|
1981
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
1982
|
+
return null;
|
|
1983
|
+
const record = entry;
|
|
1984
|
+
return {
|
|
1985
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
1986
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
1987
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
1988
|
+
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,
|
|
1989
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
1990
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function normalizeReviewComment(entry) {
|
|
1994
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
1995
|
+
return null;
|
|
1996
|
+
const record = entry;
|
|
1997
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
1998
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
1999
|
+
if (!body && !path)
|
|
2000
|
+
return null;
|
|
2001
|
+
return {
|
|
2002
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
2003
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
2004
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
2005
|
+
body,
|
|
2006
|
+
path,
|
|
2007
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
2008
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
2009
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
2010
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function normalizeIssueComment(entry) {
|
|
2014
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2015
|
+
return null;
|
|
2016
|
+
const record = entry;
|
|
2017
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
2018
|
+
if (!body)
|
|
2019
|
+
return null;
|
|
2020
|
+
return {
|
|
2021
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
2022
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
2023
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
2024
|
+
body,
|
|
2025
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
2026
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
2027
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function normalizeReviewThread(entry) {
|
|
2031
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2032
|
+
return null;
|
|
2033
|
+
const record = entry;
|
|
2034
|
+
return {
|
|
2035
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
2036
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
2037
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
2038
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
function relevantIssueComment(comment) {
|
|
2042
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
2043
|
+
const body = comment.body ?? "";
|
|
2044
|
+
return isGreptileGithubLogin(login) || /greptile|blocker|unsafe|not safe|do not merge|must fix|please fix|changes requested|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
2045
|
+
}
|
|
2046
|
+
function latestThreadComment(thread) {
|
|
2047
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
2048
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
2049
|
+
}
|
|
2050
|
+
function unresolvedThreadSummaries(threads) {
|
|
2051
|
+
return threads.flatMap((thread) => {
|
|
2052
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
2053
|
+
return [];
|
|
2054
|
+
const latest = latestThreadComment(thread);
|
|
2055
|
+
if (!latest)
|
|
2056
|
+
return ["Unresolved review thread"];
|
|
2057
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
2058
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
function collectBodies(evidence) {
|
|
2062
|
+
return [
|
|
2063
|
+
evidence.title ?? "",
|
|
2064
|
+
evidence.body,
|
|
2065
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
2066
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
2067
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
2068
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
2069
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
2070
|
+
].filter((body) => body.trim().length > 0);
|
|
2071
|
+
}
|
|
2072
|
+
function bodyExcerpt(body) {
|
|
2073
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
2074
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
2075
|
+
}
|
|
2076
|
+
function makeGreptileSignal(input) {
|
|
2077
|
+
const scores = parseGreptileScores(input.body);
|
|
2078
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
2079
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
2080
|
+
const blocker = input.blocker ?? (containsBlockerText(input.body) || containsGreptileNegativeVerdict(input.body));
|
|
2081
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
2082
|
+
return {
|
|
2083
|
+
source: input.source,
|
|
2084
|
+
trusted: input.trusted,
|
|
2085
|
+
authorLogin: input.authorLogin ?? null,
|
|
2086
|
+
reviewedSha,
|
|
2087
|
+
current,
|
|
2088
|
+
stale: current === false,
|
|
2089
|
+
score: scores[0] ?? null,
|
|
2090
|
+
scores,
|
|
2091
|
+
explicitApproval,
|
|
2092
|
+
blocker,
|
|
2093
|
+
actionable: input.actionable ?? blocker,
|
|
2094
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
2095
|
+
body: input.body,
|
|
2096
|
+
allScores: scores
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
function reviewAuthorLogin(review) {
|
|
2100
|
+
return review.author?.login ?? null;
|
|
2101
|
+
}
|
|
2102
|
+
function commentAuthorLogin(comment) {
|
|
2103
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
2104
|
+
}
|
|
2105
|
+
function collectGreptileSignals(evidence) {
|
|
2106
|
+
const signals = [];
|
|
2107
|
+
const contextSources = [
|
|
2108
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
2109
|
+
{ source: "pr-body", body: evidence.body }
|
|
2110
|
+
];
|
|
2111
|
+
for (const context of contextSources) {
|
|
2112
|
+
if (!context.body.trim())
|
|
2113
|
+
continue;
|
|
2114
|
+
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
2115
|
+
continue;
|
|
2116
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
2117
|
+
signals.push(makeGreptileSignal({
|
|
2118
|
+
source: context.source,
|
|
2119
|
+
body: context.body,
|
|
2120
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2121
|
+
trusted: false,
|
|
2122
|
+
blocker: contextBlocker,
|
|
2123
|
+
actionable: contextBlocker
|
|
2124
|
+
}));
|
|
2125
|
+
}
|
|
2126
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
2127
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
2128
|
+
|
|
2129
|
+
`);
|
|
2130
|
+
if (!body.trim())
|
|
2131
|
+
continue;
|
|
2132
|
+
signals.push(makeGreptileSignal({
|
|
2133
|
+
source: "api",
|
|
2134
|
+
body,
|
|
2135
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2136
|
+
trusted: true,
|
|
2137
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
2138
|
+
explicitApproval: false
|
|
2139
|
+
}));
|
|
2140
|
+
}
|
|
2141
|
+
for (const review of evidence.reviews) {
|
|
2142
|
+
const login = reviewAuthorLogin(review);
|
|
2143
|
+
if (!isGreptileGithubLogin(login))
|
|
2144
|
+
continue;
|
|
2145
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
2146
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
2147
|
+
|
|
2148
|
+
`);
|
|
2149
|
+
if (!body.trim())
|
|
2150
|
+
continue;
|
|
2151
|
+
const dismissed = state === "DISMISSED";
|
|
2152
|
+
signals.push(makeGreptileSignal({
|
|
2153
|
+
source: "github-review",
|
|
2154
|
+
body,
|
|
2155
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2156
|
+
trusted: !dismissed,
|
|
2157
|
+
authorLogin: login,
|
|
2158
|
+
reviewedSha: review.commit_id ?? null,
|
|
2159
|
+
explicitApproval: undefined,
|
|
2160
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
2161
|
+
}));
|
|
2162
|
+
}
|
|
2163
|
+
for (const comment of evidence.changedFileReviewComments) {
|
|
2164
|
+
const login = commentAuthorLogin(comment);
|
|
2165
|
+
const body = comment.body ?? "";
|
|
2166
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2167
|
+
continue;
|
|
2168
|
+
signals.push(makeGreptileSignal({
|
|
2169
|
+
source: "changed-file-comment",
|
|
2170
|
+
body,
|
|
2171
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2172
|
+
trusted: true,
|
|
2173
|
+
authorLogin: login,
|
|
2174
|
+
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
2175
|
+
}));
|
|
2176
|
+
}
|
|
2177
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
2178
|
+
const login = commentAuthorLogin(comment);
|
|
2179
|
+
const body = comment.body ?? "";
|
|
2180
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2181
|
+
continue;
|
|
2182
|
+
signals.push(makeGreptileSignal({
|
|
2183
|
+
source: "issue-comment",
|
|
2184
|
+
body,
|
|
2185
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2186
|
+
trusted: true,
|
|
2187
|
+
authorLogin: login
|
|
2188
|
+
}));
|
|
2189
|
+
}
|
|
2190
|
+
for (const thread of evidence.reviewThreads) {
|
|
2191
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
2192
|
+
continue;
|
|
2193
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
2194
|
+
const login = comment.author?.login ?? null;
|
|
2195
|
+
const body = comment.body ?? "";
|
|
2196
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2197
|
+
continue;
|
|
2198
|
+
signals.push(makeGreptileSignal({
|
|
2199
|
+
source: "review-thread",
|
|
2200
|
+
body,
|
|
2201
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2202
|
+
trusted: true,
|
|
2203
|
+
authorLogin: login
|
|
2204
|
+
}));
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
for (const check of evidence.checks) {
|
|
2208
|
+
if (!isGreptileLabel(checkName(check)))
|
|
2209
|
+
continue;
|
|
2210
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
2211
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
2212
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
2213
|
+
|
|
2214
|
+
`);
|
|
2215
|
+
signals.push(makeGreptileSignal({
|
|
2216
|
+
source: "github-check",
|
|
2217
|
+
body,
|
|
2218
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2219
|
+
trusted: false,
|
|
2220
|
+
reviewedSha,
|
|
2221
|
+
explicitApproval: false,
|
|
2222
|
+
blocker: isFailingCheck(check),
|
|
2223
|
+
actionable: isFailingCheck(check)
|
|
2224
|
+
}));
|
|
2225
|
+
}
|
|
2226
|
+
return signals;
|
|
2227
|
+
}
|
|
2228
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
2229
|
+
return threads.flatMap((thread) => {
|
|
2230
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
2231
|
+
return [];
|
|
2232
|
+
const comments = thread.comments?.nodes ?? [];
|
|
2233
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
2234
|
+
return [];
|
|
2235
|
+
const latest = latestThreadComment(thread);
|
|
2236
|
+
if (!latest)
|
|
2237
|
+
return ["Unresolved Greptile review thread"];
|
|
2238
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
2239
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
function issueLevelBlockerSummaries(comments) {
|
|
2243
|
+
return comments.flatMap((comment) => {
|
|
2244
|
+
const body = comment.body?.trim() ?? "";
|
|
2245
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
2246
|
+
return [];
|
|
2247
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
2248
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
2249
|
+
return [`${author}: ${body}`];
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
2253
|
+
return reviews.flatMap((review) => {
|
|
2254
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
2255
|
+
if (isGreptileGithubLogin(login))
|
|
2256
|
+
return [];
|
|
2257
|
+
const body = review.body?.trim() ?? "";
|
|
2258
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
2259
|
+
return [];
|
|
2260
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
2261
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
function signalLabel(signal) {
|
|
2265
|
+
const source = signal.source.replace(/-/g, " ");
|
|
2266
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
2267
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
2268
|
+
return `${source}${author}${sha}`;
|
|
2269
|
+
}
|
|
2270
|
+
function deriveGreptileEvidence(input) {
|
|
2271
|
+
const rawBodies = collectBodies(input);
|
|
2272
|
+
const signals = collectGreptileSignals(input);
|
|
2273
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
2274
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
2275
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
2276
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
2277
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
2278
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
2279
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
2280
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
2281
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
|
|
2282
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
2283
|
+
const approvedByExplicitMapping = false;
|
|
2284
|
+
const approvingSignal = approvingScoreEntry?.signal ?? null;
|
|
2285
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
2286
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
2287
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
2288
|
+
const blockerSignals = signals.filter((signal) => signal.source !== "changed-file-comment" && (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
2289
|
+
const staleBlockingSignals = [];
|
|
2290
|
+
const blockers = [
|
|
2291
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
2292
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
2293
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
2294
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
2295
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
2296
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
2297
|
+
];
|
|
2298
|
+
const unresolvedComments = [
|
|
2299
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
2300
|
+
];
|
|
2301
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
2302
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
2303
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
2304
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
2305
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
2306
|
+
});
|
|
2307
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
2308
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
2309
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
2310
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
2311
|
+
});
|
|
2312
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
2313
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
2314
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
2315
|
+
const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
|
|
2316
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
2317
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
2318
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
2319
|
+
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";
|
|
2320
|
+
return {
|
|
2321
|
+
source,
|
|
2322
|
+
currentHeadSha: input.currentHeadSha,
|
|
2323
|
+
reviewedSha,
|
|
2324
|
+
fresh,
|
|
2325
|
+
completed,
|
|
2326
|
+
approved,
|
|
2327
|
+
score,
|
|
2328
|
+
explicitApproval: approvedByExplicitMapping,
|
|
2329
|
+
blockers,
|
|
2330
|
+
unresolvedComments,
|
|
2331
|
+
rawBodies,
|
|
2332
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
2333
|
+
mapping
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
function isGreptileCheckDetail(check) {
|
|
2337
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
2338
|
+
}
|
|
2339
|
+
async function collectGreptileCheckDetails(input) {
|
|
2340
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
2341
|
+
"api",
|
|
2342
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
2343
|
+
"--paginate",
|
|
2344
|
+
"--slurp",
|
|
2345
|
+
"--jq",
|
|
2346
|
+
"map(.check_runs // []) | add // []"
|
|
2347
|
+
], input.projectRoot);
|
|
2348
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
2349
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
2350
|
+
}
|
|
2351
|
+
async function collectReviewThreads(input) {
|
|
2352
|
+
const reviewThreads = [];
|
|
2353
|
+
let afterCursor = null;
|
|
2354
|
+
for (let page = 0;page < 100; page += 1) {
|
|
2355
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
2356
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
2357
|
+
"api",
|
|
2358
|
+
"graphql",
|
|
2359
|
+
"-F",
|
|
2360
|
+
`owner=${input.owner}`,
|
|
2361
|
+
"-F",
|
|
2362
|
+
`name=${input.name}`,
|
|
2363
|
+
"-F",
|
|
2364
|
+
`prNumber=${input.prNumber}`,
|
|
2365
|
+
"-f",
|
|
2366
|
+
`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 } } } } }`
|
|
2367
|
+
], input.projectRoot);
|
|
2368
|
+
if (threadsResponse.error) {
|
|
2369
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
2370
|
+
}
|
|
2371
|
+
const data = threadsResponse.value.data;
|
|
2372
|
+
const repository = data?.repository;
|
|
2373
|
+
const pullRequest = repository?.pullRequest;
|
|
2374
|
+
const threads = pullRequest?.reviewThreads;
|
|
2375
|
+
const nodes = threads?.nodes;
|
|
2376
|
+
if (!Array.isArray(nodes)) {
|
|
2377
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
2378
|
+
}
|
|
2379
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
2380
|
+
reviewThreads.push(...normalized);
|
|
2381
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
2382
|
+
if (truncatedCommentThread) {
|
|
2383
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
2384
|
+
}
|
|
2385
|
+
const pageInfo = threads?.pageInfo;
|
|
2386
|
+
if (!pageInfo) {
|
|
2387
|
+
if (nodes.length >= 100) {
|
|
2388
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
2389
|
+
}
|
|
2390
|
+
return { value: reviewThreads };
|
|
2391
|
+
}
|
|
2392
|
+
if (pageInfo.hasNextPage !== true) {
|
|
2393
|
+
return { value: reviewThreads };
|
|
2394
|
+
}
|
|
2395
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
2396
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
2397
|
+
}
|
|
2398
|
+
afterCursor = pageInfo.endCursor;
|
|
2399
|
+
}
|
|
2400
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
2401
|
+
}
|
|
2402
|
+
async function collectPrReviewEvidence(input) {
|
|
2403
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
2404
|
+
if (!parsed) {
|
|
2405
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
2406
|
+
}
|
|
2407
|
+
const readErrors = [];
|
|
2408
|
+
const viewRead = await runJsonObject(input.command, [
|
|
2409
|
+
"pr",
|
|
2410
|
+
"view",
|
|
2411
|
+
input.prUrl,
|
|
2412
|
+
"--json",
|
|
2413
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
2414
|
+
], input.projectRoot);
|
|
2415
|
+
if (viewRead.error)
|
|
2416
|
+
readErrors.push(viewRead.error);
|
|
2417
|
+
const view = viewRead.value;
|
|
2418
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
2419
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
2420
|
+
}
|
|
2421
|
+
if (!Array.isArray(view.reviews)) {
|
|
2422
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
2423
|
+
}
|
|
2424
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
2425
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
2426
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
2427
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
2428
|
+
if (reviewCommentsRead.error)
|
|
2429
|
+
readErrors.push(reviewCommentsRead.error);
|
|
2430
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
2431
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
2432
|
+
if (issueCommentsRead.error)
|
|
2433
|
+
readErrors.push(issueCommentsRead.error);
|
|
2434
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
2435
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
2436
|
+
command: input.command,
|
|
2437
|
+
projectRoot: input.projectRoot,
|
|
2438
|
+
owner: parsed.owner,
|
|
2439
|
+
name: parsed.repo,
|
|
2440
|
+
prNumber: parsed.prNumber
|
|
2441
|
+
});
|
|
2442
|
+
if (reviewThreadsRead.error)
|
|
2443
|
+
readErrors.push(reviewThreadsRead.error);
|
|
2444
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
2445
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
2446
|
+
let greptileCheckDetails = [];
|
|
2447
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
2448
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
2449
|
+
command: input.command,
|
|
2450
|
+
projectRoot: input.projectRoot,
|
|
2451
|
+
repoName: parsed.repoName,
|
|
2452
|
+
headSha
|
|
2453
|
+
});
|
|
2454
|
+
if (checkDetailsRead.error)
|
|
2455
|
+
readErrors.push(checkDetailsRead.error);
|
|
2456
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
2457
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
2458
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
2462
|
+
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})` : ""}`);
|
|
2463
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
2464
|
+
const evidenceBase = {
|
|
2465
|
+
title: firstString(view, ["title"]),
|
|
2466
|
+
body: firstString(view, ["body"]),
|
|
2467
|
+
reviews,
|
|
2468
|
+
changedFileReviewComments: reviewComments,
|
|
2469
|
+
relevantIssueComments: issueComments,
|
|
2470
|
+
reviewThreads,
|
|
2471
|
+
checks: checksWithGreptileDetails,
|
|
2472
|
+
currentHeadSha: headSha,
|
|
2473
|
+
apiSignals: input.apiSignals ?? []
|
|
2474
|
+
};
|
|
2475
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
2476
|
+
return {
|
|
2477
|
+
prUrl: input.prUrl,
|
|
2478
|
+
prNumber: parsed.prNumber,
|
|
2479
|
+
repoName: parsed.repoName,
|
|
2480
|
+
title: evidenceBase.title,
|
|
2481
|
+
body: evidenceBase.body,
|
|
2482
|
+
headSha,
|
|
2483
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
2484
|
+
baseRefName: firstString(view, ["baseRefName"]),
|
|
2485
|
+
state: firstString(view, ["state"]),
|
|
2486
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
2487
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
2488
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
2489
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
2490
|
+
reviews,
|
|
2491
|
+
reviewThreads,
|
|
2492
|
+
changedFileReviewComments: reviewComments,
|
|
2493
|
+
relevantIssueComments: issueComments,
|
|
2494
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
2495
|
+
checkFailures,
|
|
2496
|
+
pendingChecks,
|
|
2497
|
+
readErrors,
|
|
2498
|
+
greptile
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
function evaluateEvidence(evidence) {
|
|
2502
|
+
const reasons = [];
|
|
2503
|
+
const warnings = [];
|
|
2504
|
+
let pending = false;
|
|
2505
|
+
if (evidence.readErrors.length > 0) {
|
|
2506
|
+
reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
|
|
2507
|
+
}
|
|
2508
|
+
if (!evidence.headSha)
|
|
2509
|
+
reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
|
|
2510
|
+
if (evidence.checkFailures.length > 0)
|
|
2511
|
+
reasons.push(...evidence.checkFailures);
|
|
2512
|
+
if (evidence.pendingChecks.length > 0) {
|
|
2513
|
+
pending = true;
|
|
2514
|
+
reasons.push(...evidence.pendingChecks);
|
|
2515
|
+
}
|
|
2516
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
2517
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
2518
|
+
reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
|
|
2519
|
+
}
|
|
2520
|
+
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
2521
|
+
if (unresolvedThreads.length > 0)
|
|
2522
|
+
reasons.push(...unresolvedThreads);
|
|
2523
|
+
const greptile = evidence.greptile;
|
|
2524
|
+
if (greptile.mapping === "missing")
|
|
2525
|
+
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
2526
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
2527
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
2528
|
+
reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
|
|
2529
|
+
}
|
|
2530
|
+
if (!greptile.completed) {
|
|
2531
|
+
pending = true;
|
|
2532
|
+
reasons.push("Greptile check/review has not completed for the current PR head.");
|
|
2533
|
+
}
|
|
2534
|
+
if (!greptile.fresh)
|
|
2535
|
+
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
2536
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
2537
|
+
reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
|
|
2538
|
+
}
|
|
2539
|
+
if (!greptile.score && greptile.mapping !== "score-5-of-5") {
|
|
2540
|
+
reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
|
|
2541
|
+
}
|
|
2542
|
+
if (greptile.mapping === "unproven") {
|
|
2543
|
+
reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
|
|
2544
|
+
}
|
|
2545
|
+
if (greptile.blockers.length > 0) {
|
|
2546
|
+
reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
|
|
2547
|
+
}
|
|
2548
|
+
if (greptile.unresolvedComments.length > 0)
|
|
2549
|
+
reasons.push(...greptile.unresolvedComments);
|
|
2550
|
+
if (!greptile.approved)
|
|
2551
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
2552
|
+
return { reasons: Array.from(new Set(reasons)), warnings, pending };
|
|
2553
|
+
}
|
|
2554
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
2555
|
+
const evaluated = evaluateEvidence(evidence);
|
|
2556
|
+
const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
|
|
2557
|
+
return {
|
|
2558
|
+
approved,
|
|
2559
|
+
pending: evaluated.pending,
|
|
2560
|
+
reasons: evaluated.reasons,
|
|
2561
|
+
warnings: evaluated.warnings,
|
|
2562
|
+
actionableFeedback: evaluated.reasons,
|
|
2563
|
+
evidence
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
|
|
1796
2567
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
1797
2568
|
async function verifyTask(options) {
|
|
1798
2569
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -2589,7 +3360,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
2589
3360
|
}
|
|
2590
3361
|
};
|
|
2591
3362
|
}
|
|
2592
|
-
|
|
3363
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3364
|
+
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/i.test(blockerScanBody)) {
|
|
2593
3365
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
2594
3366
|
return {
|
|
2595
3367
|
verdict: "REJECT",
|
|
@@ -2605,44 +3377,78 @@ async function runGreptileReviewForPr(options) {
|
|
|
2605
3377
|
}
|
|
2606
3378
|
};
|
|
2607
3379
|
}
|
|
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
|
-
|
|
3380
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
3381
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
3382
|
+
return {
|
|
3383
|
+
verdict: "REJECT",
|
|
3384
|
+
feedback,
|
|
3385
|
+
reasons,
|
|
3386
|
+
warnings,
|
|
3387
|
+
rawPayload: {
|
|
3388
|
+
pr: options.prState,
|
|
3389
|
+
codeReviews: reviewsPayload,
|
|
3390
|
+
selectedReview,
|
|
3391
|
+
reviewDetails,
|
|
3392
|
+
comments: commentsPayload,
|
|
3393
|
+
score
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
3398
|
+
let strictGate = null;
|
|
3399
|
+
try {
|
|
3400
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
3401
|
+
projectRoot: options.projectRoot,
|
|
3402
|
+
taskId: options.taskId,
|
|
3403
|
+
prUrl,
|
|
3404
|
+
apiSignals: [{
|
|
3405
|
+
id: selectedReview.id,
|
|
3406
|
+
body: reviewBody,
|
|
3407
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
3408
|
+
status: selectedReview.status
|
|
3409
|
+
}]
|
|
3410
|
+
});
|
|
3411
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3414
|
+
return {
|
|
3415
|
+
verdict: "REJECT",
|
|
3416
|
+
feedback,
|
|
3417
|
+
reasons,
|
|
3418
|
+
warnings,
|
|
3419
|
+
rawPayload: {
|
|
3420
|
+
pr: options.prState,
|
|
3421
|
+
codeReviews: reviewsPayload,
|
|
3422
|
+
selectedReview,
|
|
3423
|
+
reviewDetails,
|
|
3424
|
+
comments: commentsPayload,
|
|
3425
|
+
score
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
if (!strictGate.approved) {
|
|
3430
|
+
return {
|
|
3431
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
3432
|
+
feedback,
|
|
3433
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
3434
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
3435
|
+
rawPayload: {
|
|
3436
|
+
pr: options.prState,
|
|
3437
|
+
codeReviews: reviewsPayload,
|
|
3438
|
+
selectedReview,
|
|
3439
|
+
reviewDetails,
|
|
3440
|
+
comments: commentsPayload,
|
|
3441
|
+
score,
|
|
3442
|
+
strictGate: {
|
|
3443
|
+
approved: strictGate.approved,
|
|
3444
|
+
pending: strictGate.pending,
|
|
3445
|
+
reasons: strictGate.reasons,
|
|
3446
|
+
warnings: strictGate.warnings,
|
|
3447
|
+
greptile: strictGate.evidence.greptile,
|
|
3448
|
+
readErrors: strictGate.evidence.readErrors
|
|
2640
3449
|
}
|
|
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
|
-
}
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
2646
3452
|
}
|
|
2647
3453
|
return {
|
|
2648
3454
|
verdict: "APPROVE",
|
|
@@ -2654,7 +3460,15 @@ async function runGreptileReviewForPr(options) {
|
|
|
2654
3460
|
codeReviews: reviewsPayload,
|
|
2655
3461
|
selectedReview,
|
|
2656
3462
|
reviewDetails,
|
|
2657
|
-
comments: commentsPayload
|
|
3463
|
+
comments: commentsPayload,
|
|
3464
|
+
strictGate: {
|
|
3465
|
+
approved: strictGate.approved,
|
|
3466
|
+
pending: strictGate.pending,
|
|
3467
|
+
reasons: strictGate.reasons,
|
|
3468
|
+
warnings: strictGate.warnings,
|
|
3469
|
+
greptile: strictGate.evidence.greptile,
|
|
3470
|
+
readErrors: strictGate.evidence.readErrors
|
|
3471
|
+
}
|
|
2658
3472
|
}
|
|
2659
3473
|
};
|
|
2660
3474
|
}
|
|
@@ -2678,7 +3492,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2678
3492
|
let threads = [];
|
|
2679
3493
|
let actionableThreads = [];
|
|
2680
3494
|
let checkRollup = [];
|
|
2681
|
-
let
|
|
3495
|
+
let checkState2 = { pending: false, completed: false };
|
|
2682
3496
|
for (let attempt = 0;; attempt += 1) {
|
|
2683
3497
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
2684
3498
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -2687,15 +3501,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2687
3501
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
2688
3502
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
2689
3503
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
2690
|
-
|
|
2691
|
-
const
|
|
3504
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
3505
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
2692
3506
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
2693
3507
|
attempt,
|
|
2694
3508
|
pollAttempts: options.pollAttempts,
|
|
2695
|
-
checkState,
|
|
3509
|
+
checkState: checkState2,
|
|
2696
3510
|
fallbackReview,
|
|
2697
3511
|
selectedReview,
|
|
2698
|
-
approvedViaReviewedAncestor
|
|
3512
|
+
approvedViaReviewedAncestor
|
|
2699
3513
|
})) {
|
|
2700
3514
|
break;
|
|
2701
3515
|
}
|
|
@@ -2723,7 +3537,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2723
3537
|
].filter(Boolean).join(`
|
|
2724
3538
|
`);
|
|
2725
3539
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
2726
|
-
if (
|
|
3540
|
+
if (checkState2.pending) {
|
|
2727
3541
|
return {
|
|
2728
3542
|
verdict: "SKIP",
|
|
2729
3543
|
feedback,
|
|
@@ -2734,34 +3548,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2734
3548
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2735
3549
|
};
|
|
2736
3550
|
}
|
|
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) {
|
|
3551
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
3552
|
+
let strictGate;
|
|
3553
|
+
try {
|
|
3554
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
3555
|
+
projectRoot: options.projectRoot,
|
|
3556
|
+
taskId: options.taskId,
|
|
3557
|
+
prUrl
|
|
3558
|
+
});
|
|
3559
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
3560
|
+
} catch (error) {
|
|
2761
3561
|
return {
|
|
2762
3562
|
verdict: "REJECT",
|
|
2763
3563
|
feedback,
|
|
2764
|
-
reasons:
|
|
3564
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
2765
3565
|
warnings,
|
|
2766
3566
|
rawPayload: {
|
|
2767
3567
|
pr: options.prState,
|
|
@@ -2774,44 +3574,30 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2774
3574
|
}
|
|
2775
3575
|
};
|
|
2776
3576
|
}
|
|
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
|
-
}
|
|
3577
|
+
if (!strictGate.approved) {
|
|
2795
3578
|
return {
|
|
2796
|
-
verdict: "SKIP",
|
|
3579
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
2797
3580
|
feedback,
|
|
2798
|
-
reasons: [
|
|
2799
|
-
|
|
2800
|
-
],
|
|
2801
|
-
warnings,
|
|
3581
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
3582
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
2802
3583
|
rawPayload: {
|
|
2803
3584
|
pr: options.prState,
|
|
2804
3585
|
selectedReview: fallbackReview,
|
|
2805
3586
|
reviews,
|
|
2806
3587
|
threads,
|
|
2807
3588
|
checkRollup,
|
|
3589
|
+
actionableThreads,
|
|
3590
|
+
strictGate: {
|
|
3591
|
+
approved: strictGate.approved,
|
|
3592
|
+
pending: strictGate.pending,
|
|
3593
|
+
reasons: strictGate.reasons,
|
|
3594
|
+
warnings: strictGate.warnings,
|
|
3595
|
+
greptile: strictGate.evidence.greptile
|
|
3596
|
+
},
|
|
2808
3597
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
2809
3598
|
}
|
|
2810
3599
|
};
|
|
2811
3600
|
}
|
|
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
3601
|
return {
|
|
2816
3602
|
verdict: "APPROVE",
|
|
2817
3603
|
feedback,
|
|
@@ -2823,6 +3609,13 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2823
3609
|
reviews,
|
|
2824
3610
|
threads,
|
|
2825
3611
|
checkRollup,
|
|
3612
|
+
strictGate: {
|
|
3613
|
+
approved: strictGate.approved,
|
|
3614
|
+
pending: strictGate.pending,
|
|
3615
|
+
reasons: strictGate.reasons,
|
|
3616
|
+
warnings: strictGate.warnings,
|
|
3617
|
+
greptile: strictGate.evidence.greptile
|
|
3618
|
+
},
|
|
2826
3619
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
2827
3620
|
}
|
|
2828
3621
|
};
|
|
@@ -2935,21 +3728,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
2935
3728
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
2936
3729
|
return true;
|
|
2937
3730
|
}
|
|
2938
|
-
return
|
|
3731
|
+
return false;
|
|
2939
3732
|
}
|
|
2940
3733
|
function shouldContinueGreptileMcpPolling(options) {
|
|
2941
3734
|
if (options.githubCheckState.completed) {
|
|
2942
3735
|
return false;
|
|
2943
3736
|
}
|
|
3737
|
+
const hasRemainingBudget = options.attempt + 1 < options.pollAttempts;
|
|
3738
|
+
if (!hasRemainingBudget) {
|
|
3739
|
+
return false;
|
|
3740
|
+
}
|
|
2944
3741
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
2945
3742
|
return true;
|
|
2946
3743
|
}
|
|
2947
|
-
return
|
|
3744
|
+
return true;
|
|
2948
3745
|
}
|
|
2949
3746
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
2950
3747
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
2951
3748
|
if (waitingForVisiblePendingReview) {
|
|
2952
|
-
return
|
|
3749
|
+
return options.attempt + 1 < options.pollAttempts;
|
|
2953
3750
|
}
|
|
2954
3751
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
2955
3752
|
if (reviewNotVisibleYet) {
|
|
@@ -3008,6 +3805,20 @@ function runGhJson(projectRoot, args) {
|
|
|
3008
3805
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
3009
3806
|
}
|
|
3010
3807
|
}
|
|
3808
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
3809
|
+
return collectPrReviewEvidence({
|
|
3810
|
+
projectRoot: input.projectRoot,
|
|
3811
|
+
prUrl: input.prUrl,
|
|
3812
|
+
taskId: input.taskId,
|
|
3813
|
+
runId: "verifier",
|
|
3814
|
+
cycle: 0,
|
|
3815
|
+
apiSignals: input.apiSignals ?? [],
|
|
3816
|
+
command: async (args, options) => {
|
|
3817
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
3818
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3011
3822
|
function deriveRepoName(projectRoot, prState) {
|
|
3012
3823
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
3013
3824
|
if (fromUrl?.[1]) {
|
|
@@ -3022,8 +3833,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
3022
3833
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3023
3834
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
3024
3835
|
}
|
|
3025
|
-
function
|
|
3026
|
-
|
|
3836
|
+
function isGreptileGithubLogin2(login) {
|
|
3837
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3838
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
3027
3839
|
}
|
|
3028
3840
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
3029
3841
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -3040,7 +3852,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
3040
3852
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
3041
3853
|
}
|
|
3042
3854
|
function sortGithubGreptileReviews(reviews) {
|
|
3043
|
-
return reviews.filter((review) =>
|
|
3855
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
3044
3856
|
}
|
|
3045
3857
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
3046
3858
|
const response = runGhJson(projectRoot, [
|
|
@@ -3113,31 +3925,8 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
3113
3925
|
}
|
|
3114
3926
|
return { pending: false, completed: false };
|
|
3115
3927
|
}
|
|
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;
|
|
3928
|
+
function isGithubGreptileCheckApproved(_checks) {
|
|
3929
|
+
return false;
|
|
3141
3930
|
}
|
|
3142
3931
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
3143
3932
|
const [owner, name] = repoName.split("/");
|
|
@@ -3164,7 +3953,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
3164
3953
|
return [];
|
|
3165
3954
|
}
|
|
3166
3955
|
const comments = thread.comments?.nodes || [];
|
|
3167
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
3956
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
3168
3957
|
if (!latestGreptileComment?.path?.trim()) {
|
|
3169
3958
|
return [];
|
|
3170
3959
|
}
|
|
@@ -3186,11 +3975,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
3186
3975
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3187
3976
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
3188
3977
|
}
|
|
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
3978
|
function summarizeComment(input) {
|
|
3195
3979
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3196
3980
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -3199,31 +3983,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
3199
3983
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
3200
3984
|
}
|
|
3201
3985
|
function isAiReviewApproved(input) {
|
|
3986
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
3987
|
+
return false;
|
|
3988
|
+
}
|
|
3202
3989
|
if (input.reviewMode !== "required") {
|
|
3203
3990
|
return true;
|
|
3204
3991
|
}
|
|
3205
3992
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
3206
3993
|
}
|
|
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
3994
|
var __testOnly = {
|
|
3228
3995
|
asGreptileInfrastructureWarning,
|
|
3229
3996
|
callGreptileMcpToolWithTimeout,
|