@h-rig/runtime 0.0.6-alpha.13 → 0.0.6-alpha.15
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 +430 -71
- package/dist/src/control-plane/hooks/completion-verification.js +469 -87
- package/dist/src/control-plane/native/git-ops.js +28 -7
- package/dist/src/control-plane/native/harness-cli.js +430 -71
- package/dist/src/control-plane/native/pr-automation.js +523 -86
- package/dist/src/control-plane/native/pr-review-gate.js +494 -69
- package/dist/src/control-plane/native/run-ops.js +12 -6
- package/dist/src/control-plane/native/task-ops.js +466 -105
- package/dist/src/control-plane/native/verifier.js +466 -107
- 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,7 +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);
|
|
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);
|
|
1905
1868
|
}
|
|
1906
1869
|
function isStrictFiveOfFive(score) {
|
|
1907
1870
|
return score.value === 5 && score.scale === 5;
|
|
@@ -1909,6 +1872,189 @@ function isStrictFiveOfFive(score) {
|
|
|
1909
1872
|
function containsConflictingScoreText(input) {
|
|
1910
1873
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
1911
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
|
+
}
|
|
1912
2058
|
function firstString(record, keys) {
|
|
1913
2059
|
for (const key of keys) {
|
|
1914
2060
|
const value = record[key];
|
|
@@ -2035,7 +2181,7 @@ function normalizeReviewThread(entry) {
|
|
|
2035
2181
|
function relevantIssueComment(comment) {
|
|
2036
2182
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
2037
2183
|
const body = comment.body ?? "";
|
|
2038
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
2184
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
2039
2185
|
}
|
|
2040
2186
|
function latestThreadComment(thread) {
|
|
2041
2187
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -2071,7 +2217,8 @@ function makeGreptileSignal(input) {
|
|
|
2071
2217
|
const scores = parseGreptileScores(input.body);
|
|
2072
2218
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
2073
2219
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
2074
|
-
const
|
|
2220
|
+
const verdict = input.verdict ?? null;
|
|
2221
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
2075
2222
|
const explicitApproval = input.explicitApproval ?? false;
|
|
2076
2223
|
return {
|
|
2077
2224
|
source: input.source,
|
|
@@ -2083,6 +2230,7 @@ function makeGreptileSignal(input) {
|
|
|
2083
2230
|
score: scores[0] ?? null,
|
|
2084
2231
|
scores,
|
|
2085
2232
|
explicitApproval,
|
|
2233
|
+
verdict,
|
|
2086
2234
|
blocker,
|
|
2087
2235
|
actionable: input.actionable ?? blocker,
|
|
2088
2236
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -2105,9 +2253,9 @@ function collectGreptileSignals(evidence) {
|
|
|
2105
2253
|
for (const context of contextSources) {
|
|
2106
2254
|
if (!context.body.trim())
|
|
2107
2255
|
continue;
|
|
2108
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
2109
|
-
continue;
|
|
2110
2256
|
const contextBlocker = containsBlockerText(context.body);
|
|
2257
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
2258
|
+
continue;
|
|
2111
2259
|
signals.push(makeGreptileSignal({
|
|
2112
2260
|
source: context.source,
|
|
2113
2261
|
body: context.body,
|
|
@@ -2120,16 +2268,16 @@ function collectGreptileSignals(evidence) {
|
|
|
2120
2268
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
2121
2269
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
2122
2270
|
|
|
2123
|
-
`);
|
|
2124
|
-
|
|
2125
|
-
continue;
|
|
2271
|
+
`) || "Status: UNKNOWN";
|
|
2272
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
2126
2273
|
signals.push(makeGreptileSignal({
|
|
2127
2274
|
source: "api",
|
|
2128
2275
|
body,
|
|
2129
2276
|
currentHeadSha: evidence.currentHeadSha,
|
|
2130
2277
|
trusted: true,
|
|
2131
2278
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
2132
|
-
explicitApproval:
|
|
2279
|
+
explicitApproval: verdict === "approved",
|
|
2280
|
+
verdict
|
|
2133
2281
|
}));
|
|
2134
2282
|
}
|
|
2135
2283
|
for (const review of evidence.reviews) {
|
|
@@ -2154,20 +2302,6 @@ function collectGreptileSignals(evidence) {
|
|
|
2154
2302
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
2155
2303
|
}));
|
|
2156
2304
|
}
|
|
2157
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
2158
|
-
const login = commentAuthorLogin(comment);
|
|
2159
|
-
const body = comment.body ?? "";
|
|
2160
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2161
|
-
continue;
|
|
2162
|
-
signals.push(makeGreptileSignal({
|
|
2163
|
-
source: "changed-file-comment",
|
|
2164
|
-
body,
|
|
2165
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
2166
|
-
trusted: true,
|
|
2167
|
-
authorLogin: login,
|
|
2168
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
2169
|
-
}));
|
|
2170
|
-
}
|
|
2171
2305
|
for (const comment of evidence.relevantIssueComments) {
|
|
2172
2306
|
const login = commentAuthorLogin(comment);
|
|
2173
2307
|
const body = comment.body ?? "";
|
|
@@ -2275,10 +2409,17 @@ function deriveGreptileEvidence(input) {
|
|
|
2275
2409
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
2276
2410
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
2277
2411
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
2278
|
-
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;
|
|
2279
2420
|
const approvedByScore = !!approvingScoreEntry;
|
|
2280
|
-
const approvedByExplicitMapping =
|
|
2281
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
2421
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
2422
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
2282
2423
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
2283
2424
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
2284
2425
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
@@ -2307,13 +2448,14 @@ function deriveGreptileEvidence(input) {
|
|
|
2307
2448
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
2308
2449
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
2309
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"));
|
|
2310
2452
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
2311
2453
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
2312
2454
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
2313
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
2455
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
2314
2456
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
2315
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
2316
|
-
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";
|
|
2317
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";
|
|
2318
2460
|
return {
|
|
2319
2461
|
source,
|
|
@@ -2420,6 +2562,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2420
2562
|
readErrors.push("gh pr view did not return required reviews array");
|
|
2421
2563
|
}
|
|
2422
2564
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
2565
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
2423
2566
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
2424
2567
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
2425
2568
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -2457,8 +2600,19 @@ async function collectPrReviewEvidence(input) {
|
|
|
2457
2600
|
}
|
|
2458
2601
|
}
|
|
2459
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];
|
|
2460
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})` : ""}`);
|
|
2461
|
-
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
2615
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
2462
2616
|
const evidenceBase = {
|
|
2463
2617
|
title: firstString(view, ["title"]),
|
|
2464
2618
|
body: firstString(view, ["body"]),
|
|
@@ -2468,7 +2622,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2468
2622
|
reviewThreads,
|
|
2469
2623
|
checks: checksWithGreptileDetails,
|
|
2470
2624
|
currentHeadSha: headSha,
|
|
2471
|
-
apiSignals
|
|
2625
|
+
apiSignals
|
|
2472
2626
|
};
|
|
2473
2627
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
2474
2628
|
return {
|
|
@@ -2479,7 +2633,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
2479
2633
|
body: evidenceBase.body,
|
|
2480
2634
|
headSha,
|
|
2481
2635
|
headRefName: firstString(view, ["headRefName"]),
|
|
2482
|
-
baseRefName
|
|
2636
|
+
baseRefName,
|
|
2483
2637
|
state: firstString(view, ["state"]),
|
|
2484
2638
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
2485
2639
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -2496,72 +2650,267 @@ async function collectPrReviewEvidence(input) {
|
|
|
2496
2650
|
greptile
|
|
2497
2651
|
};
|
|
2498
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
|
+
}
|
|
2499
2658
|
function evaluateEvidence(evidence) {
|
|
2500
|
-
const
|
|
2659
|
+
const reasonDetails = [];
|
|
2501
2660
|
const warnings = [];
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
+
});
|
|
2513
2715
|
}
|
|
2514
2716
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
2515
2717
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
2516
|
-
|
|
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
|
+
});
|
|
2517
2746
|
}
|
|
2518
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
2519
|
-
if (unresolvedThreads.length > 0)
|
|
2520
|
-
reasons.push(...unresolvedThreads);
|
|
2521
|
-
const greptile = evidence.greptile;
|
|
2522
|
-
if (greptile.mapping === "missing")
|
|
2523
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
2524
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
2525
2747
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
2526
|
-
|
|
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
|
+
});
|
|
2527
2779
|
}
|
|
2528
2780
|
if (!greptile.completed) {
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
+
});
|
|
2531
2801
|
}
|
|
2532
|
-
if (!greptile.fresh)
|
|
2533
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
2534
2802
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
2535
|
-
|
|
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
|
+
});
|
|
2536
2812
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
+
});
|
|
2539
2824
|
}
|
|
2540
2825
|
if (greptile.mapping === "unproven") {
|
|
2541
|
-
|
|
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
|
+
});
|
|
2542
2835
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
+
});
|
|
2545
2857
|
}
|
|
2546
|
-
if (greptile.unresolvedComments.length > 0)
|
|
2547
|
-
reasons.push(...greptile.unresolvedComments);
|
|
2548
2858
|
if (!greptile.approved)
|
|
2549
2859
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
2550
|
-
|
|
2860
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
2861
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
2551
2862
|
}
|
|
2552
2863
|
function evaluateStrictPrMergeGate(evidence) {
|
|
2553
2864
|
const evaluated = evaluateEvidence(evidence);
|
|
2554
|
-
const approved = evaluated.
|
|
2865
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
2555
2866
|
return {
|
|
2556
2867
|
approved,
|
|
2557
2868
|
pending: evaluated.pending,
|
|
2558
2869
|
reasons: evaluated.reasons,
|
|
2870
|
+
reasonDetails: evaluated.reasonDetails,
|
|
2559
2871
|
warnings: evaluated.warnings,
|
|
2560
2872
|
actionableFeedback: evaluated.reasons,
|
|
2561
2873
|
evidence
|
|
2562
2874
|
};
|
|
2563
2875
|
}
|
|
2564
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
|
+
|
|
2565
2914
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
2566
2915
|
async function verifyTask(options) {
|
|
2567
2916
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -3359,7 +3708,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3359
3708
|
};
|
|
3360
3709
|
}
|
|
3361
3710
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3362
|
-
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)) {
|
|
3363
3712
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
3364
3713
|
return {
|
|
3365
3714
|
verdict: "REJECT",
|
|
@@ -3441,6 +3790,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3441
3790
|
approved: strictGate.approved,
|
|
3442
3791
|
pending: strictGate.pending,
|
|
3443
3792
|
reasons: strictGate.reasons,
|
|
3793
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3444
3794
|
warnings: strictGate.warnings,
|
|
3445
3795
|
greptile: strictGate.evidence.greptile,
|
|
3446
3796
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -3463,6 +3813,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
3463
3813
|
approved: strictGate.approved,
|
|
3464
3814
|
pending: strictGate.pending,
|
|
3465
3815
|
reasons: strictGate.reasons,
|
|
3816
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3466
3817
|
warnings: strictGate.warnings,
|
|
3467
3818
|
greptile: strictGate.evidence.greptile,
|
|
3468
3819
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -3589,6 +3940,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
3589
3940
|
approved: strictGate.approved,
|
|
3590
3941
|
pending: strictGate.pending,
|
|
3591
3942
|
reasons: strictGate.reasons,
|
|
3943
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3592
3944
|
warnings: strictGate.warnings,
|
|
3593
3945
|
greptile: strictGate.evidence.greptile
|
|
3594
3946
|
},
|
|
@@ -3611,6 +3963,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
3611
3963
|
approved: strictGate.approved,
|
|
3612
3964
|
pending: strictGate.pending,
|
|
3613
3965
|
reasons: strictGate.reasons,
|
|
3966
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3614
3967
|
warnings: strictGate.warnings,
|
|
3615
3968
|
greptile: strictGate.evidence.greptile
|
|
3616
3969
|
},
|
|
@@ -3726,19 +4079,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
3726
4079
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
3727
4080
|
return true;
|
|
3728
4081
|
}
|
|
3729
|
-
return
|
|
4082
|
+
return false;
|
|
3730
4083
|
}
|
|
3731
4084
|
function shouldContinueGreptileMcpPolling(options) {
|
|
3732
4085
|
if (options.githubCheckState.completed) {
|
|
3733
4086
|
return false;
|
|
3734
4087
|
}
|
|
4088
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
4089
|
+
return false;
|
|
4090
|
+
}
|
|
3735
4091
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
3736
4092
|
return true;
|
|
3737
4093
|
}
|
|
3738
|
-
return
|
|
4094
|
+
return true;
|
|
3739
4095
|
}
|
|
3740
4096
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
3741
4097
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
4098
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
4099
|
+
return false;
|
|
4100
|
+
}
|
|
3742
4101
|
if (waitingForVisiblePendingReview) {
|
|
3743
4102
|
return true;
|
|
3744
4103
|
}
|