@h-rig/runtime 0.0.6-alpha.12 → 0.0.6-alpha.14
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.js +1 -1
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +432 -79
- package/dist/src/control-plane/hooks/completion-verification.js +471 -95
- package/dist/src/control-plane/native/git-ops.js +28 -7
- package/dist/src/control-plane/native/harness-cli.js +432 -79
- package/dist/src/control-plane/native/pr-automation.js +528 -93
- package/dist/src/control-plane/native/pr-review-gate.js +499 -76
- package/dist/src/control-plane/native/run-ops.js +12 -6
- package/dist/src/control-plane/native/task-ops.js +468 -113
- package/dist/src/control-plane/native/verifier.js +468 -115
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- 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/package.json +6 -6
|
@@ -1756,43 +1756,6 @@ var GENERATED_TASK_ARTIFACT_FILES = new Set([
|
|
|
1756
1756
|
"git-state.txt"
|
|
1757
1757
|
]);
|
|
1758
1758
|
|
|
1759
|
-
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
1760
|
-
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
1761
|
-
"changed-files.txt",
|
|
1762
|
-
"contract-changes.md",
|
|
1763
|
-
"decision-log.md",
|
|
1764
|
-
"git-state.txt",
|
|
1765
|
-
"next-actions.md",
|
|
1766
|
-
"pr-state.json",
|
|
1767
|
-
"task-result.json",
|
|
1768
|
-
"validation-summary.json"
|
|
1769
|
-
]);
|
|
1770
|
-
function readPrMetadata(projectRoot, taskId) {
|
|
1771
|
-
const path = resolve12(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
1772
|
-
if (!existsSync10(path)) {
|
|
1773
|
-
return [];
|
|
1774
|
-
}
|
|
1775
|
-
try {
|
|
1776
|
-
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1777
|
-
if (!parsed || typeof parsed !== "object") {
|
|
1778
|
-
return [];
|
|
1779
|
-
}
|
|
1780
|
-
if (parsed.prs && typeof parsed.prs === "object") {
|
|
1781
|
-
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
1782
|
-
}
|
|
1783
|
-
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
1784
|
-
} catch {
|
|
1785
|
-
return [];
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
function isGitOpenPrResult(value) {
|
|
1789
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1790
|
-
return false;
|
|
1791
|
-
}
|
|
1792
|
-
const record = value;
|
|
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
|
-
}
|
|
1795
|
-
|
|
1796
1759
|
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
1797
1760
|
function parseJsonObject(value) {
|
|
1798
1761
|
if (!value?.trim())
|
|
@@ -1901,13 +1864,7 @@ function stripHtml(input) {
|
|
|
1901
1864
|
}
|
|
1902
1865
|
function containsBlockerText(input) {
|
|
1903
1866
|
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);
|
|
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);
|
|
1911
1868
|
}
|
|
1912
1869
|
function isStrictFiveOfFive(score) {
|
|
1913
1870
|
return score.value === 5 && score.scale === 5;
|
|
@@ -1915,6 +1872,189 @@ function isStrictFiveOfFive(score) {
|
|
|
1915
1872
|
function containsConflictingScoreText(input) {
|
|
1916
1873
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
1917
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
|
+
}
|
|
1918
2058
|
function firstString(record, keys) {
|
|
1919
2059
|
for (const key of keys) {
|
|
1920
2060
|
const value = record[key];
|
|
@@ -2041,7 +2181,7 @@ function normalizeReviewThread(entry) {
|
|
|
2041
2181
|
function relevantIssueComment(comment) {
|
|
2042
2182
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
2043
2183
|
const body = comment.body ?? "";
|
|
2044
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
2184
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
2045
2185
|
}
|
|
2046
2186
|
function latestThreadComment(thread) {
|
|
2047
2187
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -2077,7 +2217,8 @@ function makeGreptileSignal(input) {
|
|
|
2077
2217
|
const scores = parseGreptileScores(input.body);
|
|
2078
2218
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
2079
2219
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
2080
|
-
const
|
|
2220
|
+
const verdict = input.verdict ?? null;
|
|
2221
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
2081
2222
|
const explicitApproval = input.explicitApproval ?? false;
|
|
2082
2223
|
return {
|
|
2083
2224
|
source: input.source,
|
|
@@ -2089,6 +2230,7 @@ function makeGreptileSignal(input) {
|
|
|
2089
2230
|
score: scores[0] ?? null,
|
|
2090
2231
|
scores,
|
|
2091
2232
|
explicitApproval,
|
|
2233
|
+
verdict,
|
|
2092
2234
|
blocker,
|
|
2093
2235
|
actionable: input.actionable ?? blocker,
|
|
2094
2236
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -2111,9 +2253,9 @@ function collectGreptileSignals(evidence) {
|
|
|
2111
2253
|
for (const context of contextSources) {
|
|
2112
2254
|
if (!context.body.trim())
|
|
2113
2255
|
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
2256
|
const contextBlocker = containsBlockerText(context.body);
|
|
2257
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
2258
|
+
continue;
|
|
2117
2259
|
signals.push(makeGreptileSignal({
|
|
2118
2260
|
source: context.source,
|
|
2119
2261
|
body: context.body,
|
|
@@ -2126,16 +2268,16 @@ function collectGreptileSignals(evidence) {
|
|
|
2126
2268
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
2127
2269
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
2128
2270
|
|
|
2129
|
-
`);
|
|
2130
|
-
|
|
2131
|
-
continue;
|
|
2271
|
+
`) || "Status: UNKNOWN";
|
|
2272
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
2132
2273
|
signals.push(makeGreptileSignal({
|
|
2133
2274
|
source: "api",
|
|
2134
2275
|
body,
|
|
2135
2276
|
currentHeadSha: evidence.currentHeadSha,
|
|
2136
2277
|
trusted: true,
|
|
2137
2278
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
2138
|
-
explicitApproval:
|
|
2279
|
+
explicitApproval: verdict === "approved",
|
|
2280
|
+
verdict
|
|
2139
2281
|
}));
|
|
2140
2282
|
}
|
|
2141
2283
|
for (const review of evidence.reviews) {
|
|
@@ -2160,20 +2302,6 @@ function collectGreptileSignals(evidence) {
|
|
|
2160
2302
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
2161
2303
|
}));
|
|
2162
2304
|
}
|
|
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
2305
|
for (const comment of evidence.relevantIssueComments) {
|
|
2178
2306
|
const login = commentAuthorLogin(comment);
|
|
2179
2307
|
const body = comment.body ?? "";
|
|
@@ -2239,6 +2367,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
2239
2367
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
2240
2368
|
});
|
|
2241
2369
|
}
|
|
2370
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
2371
|
+
return [];
|
|
2372
|
+
}
|
|
2242
2373
|
function issueLevelBlockerSummaries(comments) {
|
|
2243
2374
|
return comments.flatMap((comment) => {
|
|
2244
2375
|
const body = comment.body?.trim() ?? "";
|
|
@@ -2278,14 +2409,21 @@ function deriveGreptileEvidence(input) {
|
|
|
2278
2409
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
2279
2410
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
2280
2411
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
2281
|
-
const
|
|
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;
|
|
2282
2420
|
const approvedByScore = !!approvingScoreEntry;
|
|
2283
|
-
const approvedByExplicitMapping =
|
|
2284
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
2421
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
2422
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
2285
2423
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
2286
2424
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
2287
2425
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
2288
|
-
const blockerSignals = signals.filter((signal) =>
|
|
2426
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
2289
2427
|
const staleBlockingSignals = [];
|
|
2290
2428
|
const blockers = [
|
|
2291
2429
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -2296,7 +2434,8 @@ function deriveGreptileEvidence(input) {
|
|
|
2296
2434
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
2297
2435
|
];
|
|
2298
2436
|
const unresolvedComments = [
|
|
2299
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
2437
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
2438
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
2300
2439
|
];
|
|
2301
2440
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
2302
2441
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -2309,13 +2448,14 @@ function deriveGreptileEvidence(input) {
|
|
|
2309
2448
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
2310
2449
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
2311
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"));
|
|
2312
2452
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
2313
2453
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
2314
2454
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
2315
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
2455
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
2316
2456
|
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";
|
|
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";
|
|
2319
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";
|
|
2320
2460
|
return {
|
|
2321
2461
|
source,
|
|
@@ -2422,6 +2562,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2422
2562
|
readErrors.push("gh pr view did not return required reviews array");
|
|
2423
2563
|
}
|
|
2424
2564
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
2565
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
2425
2566
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
2426
2567
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
2427
2568
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -2459,6 +2600,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
2459
2600
|
}
|
|
2460
2601
|
}
|
|
2461
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];
|
|
2462
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})` : ""}`);
|
|
2463
2615
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
2464
2616
|
const evidenceBase = {
|
|
@@ -2470,7 +2622,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2470
2622
|
reviewThreads,
|
|
2471
2623
|
checks: checksWithGreptileDetails,
|
|
2472
2624
|
currentHeadSha: headSha,
|
|
2473
|
-
apiSignals
|
|
2625
|
+
apiSignals
|
|
2474
2626
|
};
|
|
2475
2627
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
2476
2628
|
return {
|
|
@@ -2481,7 +2633,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2481
2633
|
body: evidenceBase.body,
|
|
2482
2634
|
headSha,
|
|
2483
2635
|
headRefName: firstString(view, ["headRefName"]),
|
|
2484
|
-
baseRefName
|
|
2636
|
+
baseRefName,
|
|
2485
2637
|
state: firstString(view, ["state"]),
|
|
2486
2638
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
2487
2639
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -2498,72 +2650,267 @@ async function collectPrReviewEvidence(input) {
|
|
|
2498
2650
|
greptile
|
|
2499
2651
|
};
|
|
2500
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
|
+
}
|
|
2501
2658
|
function evaluateEvidence(evidence) {
|
|
2502
|
-
const
|
|
2659
|
+
const reasonDetails = [];
|
|
2503
2660
|
const warnings = [];
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
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
|
+
});
|
|
2515
2715
|
}
|
|
2516
2716
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
2517
2717
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
2518
|
-
|
|
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
|
+
});
|
|
2519
2746
|
}
|
|
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
2747
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
2528
|
-
|
|
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
|
+
});
|
|
2529
2779
|
}
|
|
2530
2780
|
if (!greptile.completed) {
|
|
2531
|
-
|
|
2532
|
-
|
|
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
|
+
});
|
|
2533
2801
|
}
|
|
2534
|
-
if (!greptile.fresh)
|
|
2535
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
2536
2802
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
2537
|
-
|
|
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
|
+
});
|
|
2538
2812
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
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
|
+
});
|
|
2541
2824
|
}
|
|
2542
2825
|
if (greptile.mapping === "unproven") {
|
|
2543
|
-
|
|
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
|
+
});
|
|
2544
2835
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
+
});
|
|
2547
2857
|
}
|
|
2548
|
-
if (greptile.unresolvedComments.length > 0)
|
|
2549
|
-
reasons.push(...greptile.unresolvedComments);
|
|
2550
2858
|
if (!greptile.approved)
|
|
2551
2859
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
2552
|
-
|
|
2860
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
2861
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
2553
2862
|
}
|
|
2554
2863
|
function evaluateStrictPrMergeGate(evidence) {
|
|
2555
2864
|
const evaluated = evaluateEvidence(evidence);
|
|
2556
|
-
const approved = evaluated.
|
|
2865
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
2557
2866
|
return {
|
|
2558
2867
|
approved,
|
|
2559
2868
|
pending: evaluated.pending,
|
|
2560
2869
|
reasons: evaluated.reasons,
|
|
2870
|
+
reasonDetails: evaluated.reasonDetails,
|
|
2561
2871
|
warnings: evaluated.warnings,
|
|
2562
2872
|
actionableFeedback: evaluated.reasons,
|
|
2563
2873
|
evidence
|
|
2564
2874
|
};
|
|
2565
2875
|
}
|
|
2566
2876
|
|
|
2877
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
2878
|
+
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
2879
|
+
"changed-files.txt",
|
|
2880
|
+
"contract-changes.md",
|
|
2881
|
+
"decision-log.md",
|
|
2882
|
+
"git-state.txt",
|
|
2883
|
+
"next-actions.md",
|
|
2884
|
+
"pr-state.json",
|
|
2885
|
+
"task-result.json",
|
|
2886
|
+
"validation-summary.json"
|
|
2887
|
+
]);
|
|
2888
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
2889
|
+
const path = resolve12(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
2890
|
+
if (!existsSync10(path)) {
|
|
2891
|
+
return [];
|
|
2892
|
+
}
|
|
2893
|
+
try {
|
|
2894
|
+
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
2895
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2896
|
+
return [];
|
|
2897
|
+
}
|
|
2898
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
2899
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
2900
|
+
}
|
|
2901
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
2902
|
+
} catch {
|
|
2903
|
+
return [];
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function isGitOpenPrResult(value) {
|
|
2907
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2908
|
+
return false;
|
|
2909
|
+
}
|
|
2910
|
+
const record = value;
|
|
2911
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2567
2914
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
2568
2915
|
async function verifyTask(options) {
|
|
2569
2916
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -3361,7 +3708,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3361
3708
|
};
|
|
3362
3709
|
}
|
|
3363
3710
|
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)) {
|
|
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)) {
|
|
3365
3712
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
3366
3713
|
return {
|
|
3367
3714
|
verdict: "REJECT",
|
|
@@ -3443,6 +3790,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3443
3790
|
approved: strictGate.approved,
|
|
3444
3791
|
pending: strictGate.pending,
|
|
3445
3792
|
reasons: strictGate.reasons,
|
|
3793
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3446
3794
|
warnings: strictGate.warnings,
|
|
3447
3795
|
greptile: strictGate.evidence.greptile,
|
|
3448
3796
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -3465,6 +3813,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3465
3813
|
approved: strictGate.approved,
|
|
3466
3814
|
pending: strictGate.pending,
|
|
3467
3815
|
reasons: strictGate.reasons,
|
|
3816
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3468
3817
|
warnings: strictGate.warnings,
|
|
3469
3818
|
greptile: strictGate.evidence.greptile,
|
|
3470
3819
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -3591,6 +3940,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
3591
3940
|
approved: strictGate.approved,
|
|
3592
3941
|
pending: strictGate.pending,
|
|
3593
3942
|
reasons: strictGate.reasons,
|
|
3943
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3594
3944
|
warnings: strictGate.warnings,
|
|
3595
3945
|
greptile: strictGate.evidence.greptile
|
|
3596
3946
|
},
|
|
@@ -3613,6 +3963,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
3613
3963
|
approved: strictGate.approved,
|
|
3614
3964
|
pending: strictGate.pending,
|
|
3615
3965
|
reasons: strictGate.reasons,
|
|
3966
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3616
3967
|
warnings: strictGate.warnings,
|
|
3617
3968
|
greptile: strictGate.evidence.greptile
|
|
3618
3969
|
},
|
|
@@ -3734,8 +4085,7 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
3734
4085
|
if (options.githubCheckState.completed) {
|
|
3735
4086
|
return false;
|
|
3736
4087
|
}
|
|
3737
|
-
|
|
3738
|
-
if (!hasRemainingBudget) {
|
|
4088
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
3739
4089
|
return false;
|
|
3740
4090
|
}
|
|
3741
4091
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
@@ -3745,8 +4095,11 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
3745
4095
|
}
|
|
3746
4096
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
3747
4097
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
4098
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
4099
|
+
return false;
|
|
4100
|
+
}
|
|
3748
4101
|
if (waitingForVisiblePendingReview) {
|
|
3749
|
-
return
|
|
4102
|
+
return true;
|
|
3750
4103
|
}
|
|
3751
4104
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
3752
4105
|
if (reviewNotVisibleYet) {
|