@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
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3
3
|
import { mkdirSync, writeFileSync } from "fs";
|
|
4
4
|
import { resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
7
|
+
var BAKED_RUNTIME_SECRETS = {
|
|
8
|
+
ANTHROPIC_API_KEY: typeof RIG_BAKED_ANTHROPIC_API_KEY !== "undefined" ? RIG_BAKED_ANTHROPIC_API_KEY : "",
|
|
9
|
+
OPENAI_API_KEY: typeof RIG_BAKED_OPENAI_API_KEY !== "undefined" ? RIG_BAKED_OPENAI_API_KEY : "",
|
|
10
|
+
OPENROUTER_API_KEY: typeof RIG_BAKED_OPENROUTER_API_KEY !== "undefined" ? RIG_BAKED_OPENROUTER_API_KEY : "",
|
|
11
|
+
AI_REVIEW_MODE: typeof RIG_BAKED_AI_REVIEW_MODE !== "undefined" ? RIG_BAKED_AI_REVIEW_MODE : "",
|
|
12
|
+
AI_REVIEW_PROVIDER: typeof RIG_BAKED_AI_REVIEW_PROVIDER !== "undefined" ? RIG_BAKED_AI_REVIEW_PROVIDER : "",
|
|
13
|
+
GREPTILE_API_BASE: typeof RIG_BAKED_GREPTILE_API_BASE !== "undefined" ? RIG_BAKED_GREPTILE_API_BASE : "",
|
|
14
|
+
GREPTILE_REMOTE: typeof RIG_BAKED_GREPTILE_REMOTE !== "undefined" ? RIG_BAKED_GREPTILE_REMOTE : "",
|
|
15
|
+
GREPTILE_REPOSITORY: typeof RIG_BAKED_GREPTILE_REPOSITORY !== "undefined" ? RIG_BAKED_GREPTILE_REPOSITORY : "",
|
|
16
|
+
GREPTILE_CONTEXT_BRANCH: typeof RIG_BAKED_GREPTILE_CONTEXT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_CONTEXT_BRANCH : "",
|
|
17
|
+
GREPTILE_DEFAULT_BRANCH: typeof RIG_BAKED_GREPTILE_DEFAULT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_DEFAULT_BRANCH : "",
|
|
18
|
+
GREPTILE_API_KEY: typeof RIG_BAKED_GREPTILE_API_KEY !== "undefined" ? RIG_BAKED_GREPTILE_API_KEY : "",
|
|
19
|
+
GREPTILE_GITHUB_TOKEN: typeof RIG_BAKED_GREPTILE_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GREPTILE_GITHUB_TOKEN : "",
|
|
20
|
+
GREPTILE_POLL_ATTEMPTS: typeof RIG_BAKED_GREPTILE_POLL_ATTEMPTS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_ATTEMPTS : "",
|
|
21
|
+
GREPTILE_POLL_INTERVAL_MS: typeof RIG_BAKED_GREPTILE_POLL_INTERVAL_MS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_INTERVAL_MS : "",
|
|
22
|
+
GH_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
23
|
+
GITHUB_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
24
|
+
GITHUB_SSH_KEY: typeof RIG_BAKED_GITHUB_SSH_KEY !== "undefined" ? RIG_BAKED_GITHUB_SSH_KEY : "",
|
|
25
|
+
AWS_ACCESS_KEY_ID: typeof RIG_BAKED_AWS_ACCESS_KEY_ID !== "undefined" ? RIG_BAKED_AWS_ACCESS_KEY_ID : "",
|
|
26
|
+
AWS_SECRET_ACCESS_KEY: typeof RIG_BAKED_AWS_SECRET_ACCESS_KEY !== "undefined" ? RIG_BAKED_AWS_SECRET_ACCESS_KEY : "",
|
|
27
|
+
AWS_REGION: typeof RIG_BAKED_AWS_REGION !== "undefined" ? RIG_BAKED_AWS_REGION : "",
|
|
28
|
+
LINEAR_API_KEY: typeof RIG_BAKED_LINEAR_API_KEY !== "undefined" ? RIG_BAKED_LINEAR_API_KEY : "",
|
|
29
|
+
LINEAR_WEBHOOK_SECRET: typeof RIG_BAKED_LINEAR_WEBHOOK_SECRET !== "undefined" ? RIG_BAKED_LINEAR_WEBHOOK_SECRET : ""
|
|
30
|
+
};
|
|
31
|
+
function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
32
|
+
const resolved = {};
|
|
33
|
+
const keys = new Set([
|
|
34
|
+
...Object.keys(BAKED_RUNTIME_SECRETS),
|
|
35
|
+
...Object.keys(baked)
|
|
36
|
+
]);
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const envValue = env[key]?.trim();
|
|
39
|
+
const bakedValue = baked[key]?.trim();
|
|
40
|
+
if (envValue) {
|
|
41
|
+
resolved[key] = envValue;
|
|
42
|
+
} else if (bakedValue) {
|
|
43
|
+
resolved[key] = bakedValue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
5
50
|
function parseJsonObject(value) {
|
|
6
51
|
if (!value?.trim())
|
|
7
52
|
return { value: {}, error: "empty JSON output" };
|
|
@@ -109,13 +154,7 @@ function stripHtml(input) {
|
|
|
109
154
|
}
|
|
110
155
|
function containsBlockerText(input) {
|
|
111
156
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
112
|
-
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);
|
|
113
|
-
}
|
|
114
|
-
function containsGreptileNegativeVerdict(input) {
|
|
115
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
116
|
-
if (!text)
|
|
117
|
-
return false;
|
|
118
|
-
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);
|
|
157
|
+
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);
|
|
119
158
|
}
|
|
120
159
|
function isStrictFiveOfFive(score) {
|
|
121
160
|
return score.value === 5 && score.scale === 5;
|
|
@@ -123,6 +162,189 @@ function isStrictFiveOfFive(score) {
|
|
|
123
162
|
function containsConflictingScoreText(input) {
|
|
124
163
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
125
164
|
}
|
|
165
|
+
function greptileStatusVerdict(status) {
|
|
166
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
167
|
+
if (!normalized)
|
|
168
|
+
return null;
|
|
169
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
170
|
+
return "approved";
|
|
171
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
172
|
+
return "rejected";
|
|
173
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
174
|
+
return "skipped";
|
|
175
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
176
|
+
return "failed";
|
|
177
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
178
|
+
return "pending";
|
|
179
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
180
|
+
return "completed";
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
184
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
185
|
+
}
|
|
186
|
+
function greptileRequestTimeoutMs(env) {
|
|
187
|
+
const fallback = 30000;
|
|
188
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
189
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
190
|
+
}
|
|
191
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
192
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
193
|
+
return null;
|
|
194
|
+
const record = entry;
|
|
195
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
196
|
+
if (!id)
|
|
197
|
+
return null;
|
|
198
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
202
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
203
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
204
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
208
|
+
const seen = new Set;
|
|
209
|
+
const unique = [];
|
|
210
|
+
for (const review of reviews) {
|
|
211
|
+
if (seen.has(review.id))
|
|
212
|
+
continue;
|
|
213
|
+
seen.add(review.id);
|
|
214
|
+
unique.push(review);
|
|
215
|
+
}
|
|
216
|
+
return unique;
|
|
217
|
+
}
|
|
218
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
219
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
220
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
221
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
222
|
+
const latest = sorted.slice(0, 1);
|
|
223
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
224
|
+
}
|
|
225
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
226
|
+
const selected = details ?? review;
|
|
227
|
+
return {
|
|
228
|
+
id: selected.id || review.id,
|
|
229
|
+
body: selected.body ?? review.body ?? null,
|
|
230
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
231
|
+
status: selected.status ?? review.status ?? null
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function callGreptileMcpToolForGate(input) {
|
|
235
|
+
const controller = new AbortController;
|
|
236
|
+
const timeoutId = setTimeout(() => {
|
|
237
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
238
|
+
}, input.timeoutMs);
|
|
239
|
+
let response;
|
|
240
|
+
try {
|
|
241
|
+
response = await input.fetchFn(input.apiBase, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: {
|
|
244
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
245
|
+
"Content-Type": "application/json"
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
jsonrpc: "2.0",
|
|
249
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
250
|
+
method: "tools/call",
|
|
251
|
+
params: { name: input.name, arguments: input.args }
|
|
252
|
+
}),
|
|
253
|
+
signal: controller.signal
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (controller.signal.aborted) {
|
|
257
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
} finally {
|
|
261
|
+
clearTimeout(timeoutId);
|
|
262
|
+
}
|
|
263
|
+
const raw = await response.text();
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
266
|
+
}
|
|
267
|
+
let envelope;
|
|
268
|
+
try {
|
|
269
|
+
envelope = JSON.parse(raw);
|
|
270
|
+
} catch {
|
|
271
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
272
|
+
}
|
|
273
|
+
if (envelope.error?.message) {
|
|
274
|
+
throw new Error(envelope.error.message);
|
|
275
|
+
}
|
|
276
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
277
|
+
`).trim();
|
|
278
|
+
if (!text) {
|
|
279
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
280
|
+
}
|
|
281
|
+
return text;
|
|
282
|
+
}
|
|
283
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
284
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(text);
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
292
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
293
|
+
return { signals: [], errors: [] };
|
|
294
|
+
}
|
|
295
|
+
const env = input.options?.env ?? process.env;
|
|
296
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
297
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
298
|
+
if (!apiKey) {
|
|
299
|
+
return { signals: [], errors: [] };
|
|
300
|
+
}
|
|
301
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
302
|
+
if (typeof fetchFn !== "function") {
|
|
303
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
304
|
+
}
|
|
305
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
306
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
307
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
308
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
309
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
310
|
+
try {
|
|
311
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
312
|
+
apiBase,
|
|
313
|
+
apiKey,
|
|
314
|
+
name: "list_code_reviews",
|
|
315
|
+
args: {
|
|
316
|
+
name: repository,
|
|
317
|
+
remote,
|
|
318
|
+
defaultBranch,
|
|
319
|
+
prNumber: input.prNumber,
|
|
320
|
+
limit: 20
|
|
321
|
+
},
|
|
322
|
+
timeoutMs,
|
|
323
|
+
fetchFn
|
|
324
|
+
});
|
|
325
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
326
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
327
|
+
const signals = [];
|
|
328
|
+
for (const review of selectedReviews) {
|
|
329
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
330
|
+
apiBase,
|
|
331
|
+
apiKey,
|
|
332
|
+
name: "get_code_review",
|
|
333
|
+
args: { codeReviewId: review.id },
|
|
334
|
+
timeoutMs,
|
|
335
|
+
fetchFn
|
|
336
|
+
});
|
|
337
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
338
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
339
|
+
}
|
|
340
|
+
return { signals, errors: [] };
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return {
|
|
343
|
+
signals: [],
|
|
344
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
126
348
|
function firstString(record, keys) {
|
|
127
349
|
for (const key of keys) {
|
|
128
350
|
const value = record[key];
|
|
@@ -249,7 +471,7 @@ function normalizeReviewThread(entry) {
|
|
|
249
471
|
function relevantIssueComment(comment) {
|
|
250
472
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
251
473
|
const body = comment.body ?? "";
|
|
252
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
474
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
253
475
|
}
|
|
254
476
|
function latestThreadComment(thread) {
|
|
255
477
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -285,7 +507,8 @@ function makeGreptileSignal(input) {
|
|
|
285
507
|
const scores = parseGreptileScores(input.body);
|
|
286
508
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
287
509
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
288
|
-
const
|
|
510
|
+
const verdict = input.verdict ?? null;
|
|
511
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
289
512
|
const explicitApproval = input.explicitApproval ?? false;
|
|
290
513
|
return {
|
|
291
514
|
source: input.source,
|
|
@@ -297,6 +520,7 @@ function makeGreptileSignal(input) {
|
|
|
297
520
|
score: scores[0] ?? null,
|
|
298
521
|
scores,
|
|
299
522
|
explicitApproval,
|
|
523
|
+
verdict,
|
|
300
524
|
blocker,
|
|
301
525
|
actionable: input.actionable ?? blocker,
|
|
302
526
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -319,9 +543,9 @@ function collectGreptileSignals(evidence) {
|
|
|
319
543
|
for (const context of contextSources) {
|
|
320
544
|
if (!context.body.trim())
|
|
321
545
|
continue;
|
|
322
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
323
|
-
continue;
|
|
324
546
|
const contextBlocker = containsBlockerText(context.body);
|
|
547
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
548
|
+
continue;
|
|
325
549
|
signals.push(makeGreptileSignal({
|
|
326
550
|
source: context.source,
|
|
327
551
|
body: context.body,
|
|
@@ -334,16 +558,16 @@ function collectGreptileSignals(evidence) {
|
|
|
334
558
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
335
559
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
336
560
|
|
|
337
|
-
`);
|
|
338
|
-
|
|
339
|
-
continue;
|
|
561
|
+
`) || "Status: UNKNOWN";
|
|
562
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
340
563
|
signals.push(makeGreptileSignal({
|
|
341
564
|
source: "api",
|
|
342
565
|
body,
|
|
343
566
|
currentHeadSha: evidence.currentHeadSha,
|
|
344
567
|
trusted: true,
|
|
345
568
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
346
|
-
explicitApproval:
|
|
569
|
+
explicitApproval: verdict === "approved",
|
|
570
|
+
verdict
|
|
347
571
|
}));
|
|
348
572
|
}
|
|
349
573
|
for (const review of evidence.reviews) {
|
|
@@ -368,20 +592,6 @@ function collectGreptileSignals(evidence) {
|
|
|
368
592
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
369
593
|
}));
|
|
370
594
|
}
|
|
371
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
372
|
-
const login = commentAuthorLogin(comment);
|
|
373
|
-
const body = comment.body ?? "";
|
|
374
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
375
|
-
continue;
|
|
376
|
-
signals.push(makeGreptileSignal({
|
|
377
|
-
source: "changed-file-comment",
|
|
378
|
-
body,
|
|
379
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
380
|
-
trusted: true,
|
|
381
|
-
authorLogin: login,
|
|
382
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
383
|
-
}));
|
|
384
|
-
}
|
|
385
595
|
for (const comment of evidence.relevantIssueComments) {
|
|
386
596
|
const login = commentAuthorLogin(comment);
|
|
387
597
|
const body = comment.body ?? "";
|
|
@@ -447,6 +657,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
447
657
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
448
658
|
});
|
|
449
659
|
}
|
|
660
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
450
663
|
function issueLevelBlockerSummaries(comments) {
|
|
451
664
|
return comments.flatMap((comment) => {
|
|
452
665
|
const body = comment.body?.trim() ?? "";
|
|
@@ -486,14 +699,21 @@ function deriveGreptileEvidence(input) {
|
|
|
486
699
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
487
700
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
488
701
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
489
|
-
const
|
|
702
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
703
|
+
const signalCanApproveByScore = (signal) => {
|
|
704
|
+
if (signal.source === "api")
|
|
705
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
706
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
707
|
+
};
|
|
708
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
709
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
490
710
|
const approvedByScore = !!approvingScoreEntry;
|
|
491
|
-
const approvedByExplicitMapping =
|
|
492
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
711
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
712
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
493
713
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
494
714
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
495
715
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
496
|
-
const blockerSignals = signals.filter((signal) =>
|
|
716
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
497
717
|
const staleBlockingSignals = [];
|
|
498
718
|
const blockers = [
|
|
499
719
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -504,7 +724,8 @@ function deriveGreptileEvidence(input) {
|
|
|
504
724
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
505
725
|
];
|
|
506
726
|
const unresolvedComments = [
|
|
507
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
727
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
728
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
508
729
|
];
|
|
509
730
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
510
731
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -517,13 +738,14 @@ function deriveGreptileEvidence(input) {
|
|
|
517
738
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
518
739
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
519
740
|
});
|
|
741
|
+
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"));
|
|
520
742
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
521
743
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
522
744
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
523
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
745
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
524
746
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
525
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
526
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
747
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
748
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
527
749
|
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";
|
|
528
750
|
return {
|
|
529
751
|
source,
|
|
@@ -630,6 +852,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
630
852
|
readErrors.push("gh pr view did not return required reviews array");
|
|
631
853
|
}
|
|
632
854
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
855
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
633
856
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
634
857
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
635
858
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -667,6 +890,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
667
890
|
}
|
|
668
891
|
}
|
|
669
892
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
893
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
894
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
895
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
896
|
+
options: input.greptileApi,
|
|
897
|
+
repoName: parsed.repoName,
|
|
898
|
+
prNumber: parsed.prNumber,
|
|
899
|
+
headSha,
|
|
900
|
+
baseRefName
|
|
901
|
+
});
|
|
902
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
903
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
670
904
|
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})` : ""}`);
|
|
671
905
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
672
906
|
const evidenceBase = {
|
|
@@ -678,7 +912,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
678
912
|
reviewThreads,
|
|
679
913
|
checks: checksWithGreptileDetails,
|
|
680
914
|
currentHeadSha: headSha,
|
|
681
|
-
apiSignals
|
|
915
|
+
apiSignals
|
|
682
916
|
};
|
|
683
917
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
684
918
|
return {
|
|
@@ -689,7 +923,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
689
923
|
body: evidenceBase.body,
|
|
690
924
|
headSha,
|
|
691
925
|
headRefName: firstString(view, ["headRefName"]),
|
|
692
|
-
baseRefName
|
|
926
|
+
baseRefName,
|
|
693
927
|
state: firstString(view, ["state"]),
|
|
694
928
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
695
929
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -706,71 +940,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
706
940
|
greptile
|
|
707
941
|
};
|
|
708
942
|
}
|
|
943
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
944
|
+
const normalized = value.trim();
|
|
945
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
946
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
947
|
+
}
|
|
709
948
|
function evaluateEvidence(evidence) {
|
|
710
|
-
const
|
|
949
|
+
const reasonDetails = [];
|
|
711
950
|
const warnings = [];
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
951
|
+
const seen = new Set;
|
|
952
|
+
const addReason = (reason) => {
|
|
953
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
954
|
+
const key = `${capped.code}:${capped.message}`;
|
|
955
|
+
if (seen.has(key))
|
|
956
|
+
return;
|
|
957
|
+
seen.add(key);
|
|
958
|
+
reasonDetails.push(capped);
|
|
959
|
+
};
|
|
960
|
+
const greptile = evidence.greptile;
|
|
961
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
962
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
963
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
964
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
965
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
966
|
+
for (const error of evidence.readErrors) {
|
|
967
|
+
addReason({
|
|
968
|
+
code: "read_error",
|
|
969
|
+
reasonClass: "reject",
|
|
970
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
971
|
+
suggestedAction: "needs_attention",
|
|
972
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
973
|
+
headSha: evidence.headSha || null
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (!evidence.headSha) {
|
|
977
|
+
addReason({
|
|
978
|
+
code: "missing_head_sha",
|
|
979
|
+
reasonClass: "reject",
|
|
980
|
+
surface: "github",
|
|
981
|
+
suggestedAction: "needs_attention",
|
|
982
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
983
|
+
headSha: null
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
for (const failure of evidence.checkFailures) {
|
|
987
|
+
addReason({
|
|
988
|
+
code: "ci_failed",
|
|
989
|
+
reasonClass: "reject",
|
|
990
|
+
surface: "ci",
|
|
991
|
+
suggestedAction: "fix",
|
|
992
|
+
message: failure,
|
|
993
|
+
headSha: evidence.headSha || null
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
997
|
+
addReason({
|
|
998
|
+
code: "check_pending",
|
|
999
|
+
reasonClass: "pending",
|
|
1000
|
+
surface: "ci",
|
|
1001
|
+
suggestedAction: "wait",
|
|
1002
|
+
message: pendingCheck,
|
|
1003
|
+
headSha: evidence.headSha || null
|
|
1004
|
+
});
|
|
723
1005
|
}
|
|
724
1006
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
725
1007
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
726
|
-
|
|
1008
|
+
addReason({
|
|
1009
|
+
code: "review_decision_blocking",
|
|
1010
|
+
reasonClass: "reject",
|
|
1011
|
+
surface: "review",
|
|
1012
|
+
suggestedAction: "fix",
|
|
1013
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
1014
|
+
headSha: evidence.headSha || null
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
1018
|
+
addReason({
|
|
1019
|
+
code: "review_thread_unresolved",
|
|
1020
|
+
reasonClass: "reject",
|
|
1021
|
+
surface: "review",
|
|
1022
|
+
suggestedAction: "fix",
|
|
1023
|
+
message: thread,
|
|
1024
|
+
headSha: evidence.headSha || null
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (greptile.mapping === "missing") {
|
|
1028
|
+
addReason({
|
|
1029
|
+
code: "greptile_missing",
|
|
1030
|
+
reasonClass: "pending",
|
|
1031
|
+
surface: "greptile",
|
|
1032
|
+
suggestedAction: "wait",
|
|
1033
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
1034
|
+
headSha: evidence.headSha || null
|
|
1035
|
+
});
|
|
727
1036
|
}
|
|
728
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
729
|
-
if (unresolvedThreads.length > 0)
|
|
730
|
-
reasons.push(...unresolvedThreads);
|
|
731
|
-
const greptile = evidence.greptile;
|
|
732
|
-
if (greptile.mapping === "missing")
|
|
733
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
734
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
735
1037
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
736
|
-
|
|
1038
|
+
addReason({
|
|
1039
|
+
code: "greptile_stale",
|
|
1040
|
+
reasonClass: "pending",
|
|
1041
|
+
surface: "greptile",
|
|
1042
|
+
suggestedAction: "wait",
|
|
1043
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
1044
|
+
headSha: evidence.headSha || null,
|
|
1045
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
1049
|
+
addReason({
|
|
1050
|
+
code: "greptile_pending",
|
|
1051
|
+
reasonClass: "pending",
|
|
1052
|
+
surface: "greptile",
|
|
1053
|
+
suggestedAction: "wait",
|
|
1054
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
1055
|
+
headSha: evidence.headSha || null,
|
|
1056
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
1060
|
+
addReason({
|
|
1061
|
+
code: "greptile_api_status_unknown",
|
|
1062
|
+
reasonClass: "reject",
|
|
1063
|
+
surface: "greptile",
|
|
1064
|
+
suggestedAction: "needs_attention",
|
|
1065
|
+
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}` : "."}`,
|
|
1066
|
+
headSha: evidence.headSha || null,
|
|
1067
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
1068
|
+
});
|
|
737
1069
|
}
|
|
738
1070
|
if (!greptile.completed) {
|
|
739
|
-
|
|
740
|
-
|
|
1071
|
+
addReason({
|
|
1072
|
+
code: "greptile_pending",
|
|
1073
|
+
reasonClass: "pending",
|
|
1074
|
+
surface: "greptile",
|
|
1075
|
+
suggestedAction: "wait",
|
|
1076
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
1077
|
+
headSha: evidence.headSha || null,
|
|
1078
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (!greptile.fresh) {
|
|
1082
|
+
addReason({
|
|
1083
|
+
code: "greptile_not_current_head",
|
|
1084
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1085
|
+
surface: "greptile",
|
|
1086
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1087
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
1088
|
+
headSha: evidence.headSha || null,
|
|
1089
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1090
|
+
});
|
|
741
1091
|
}
|
|
742
|
-
if (!greptile.fresh)
|
|
743
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
744
1092
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
745
|
-
|
|
1093
|
+
addReason({
|
|
1094
|
+
code: "greptile_score_not_5",
|
|
1095
|
+
reasonClass: "reject",
|
|
1096
|
+
surface: "greptile",
|
|
1097
|
+
suggestedAction: "fix",
|
|
1098
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
1099
|
+
headSha: evidence.headSha || null,
|
|
1100
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1101
|
+
});
|
|
746
1102
|
}
|
|
747
|
-
|
|
748
|
-
|
|
1103
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
1104
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
1105
|
+
addReason({
|
|
1106
|
+
code: "greptile_score_missing",
|
|
1107
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1108
|
+
surface: "greptile",
|
|
1109
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1110
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
1111
|
+
headSha: evidence.headSha || null,
|
|
1112
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1113
|
+
});
|
|
749
1114
|
}
|
|
750
1115
|
if (greptile.mapping === "unproven") {
|
|
751
|
-
|
|
1116
|
+
addReason({
|
|
1117
|
+
code: "greptile_mapping_unproven",
|
|
1118
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1119
|
+
surface: "greptile",
|
|
1120
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1121
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
1122
|
+
headSha: evidence.headSha || null,
|
|
1123
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1124
|
+
});
|
|
752
1125
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1126
|
+
for (const blocker of greptile.blockers) {
|
|
1127
|
+
addReason({
|
|
1128
|
+
code: "greptile_blocker_text",
|
|
1129
|
+
reasonClass: "reject",
|
|
1130
|
+
surface: "greptile",
|
|
1131
|
+
suggestedAction: "fix",
|
|
1132
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
1133
|
+
headSha: evidence.headSha || null,
|
|
1134
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
for (const comment of greptile.unresolvedComments) {
|
|
1138
|
+
addReason({
|
|
1139
|
+
code: "greptile_unresolved_comment",
|
|
1140
|
+
reasonClass: "reject",
|
|
1141
|
+
surface: "greptile",
|
|
1142
|
+
suggestedAction: "fix",
|
|
1143
|
+
message: comment,
|
|
1144
|
+
headSha: evidence.headSha || null,
|
|
1145
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1146
|
+
});
|
|
755
1147
|
}
|
|
756
|
-
if (greptile.unresolvedComments.length > 0)
|
|
757
|
-
reasons.push(...greptile.unresolvedComments);
|
|
758
1148
|
if (!greptile.approved)
|
|
759
1149
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
760
|
-
|
|
1150
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
1151
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
761
1152
|
}
|
|
762
1153
|
function evaluateStrictPrMergeGate(evidence) {
|
|
763
1154
|
const evaluated = evaluateEvidence(evidence);
|
|
764
|
-
const approved = evaluated.
|
|
1155
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
765
1156
|
return {
|
|
766
1157
|
approved,
|
|
767
1158
|
pending: evaluated.pending,
|
|
768
1159
|
reasons: evaluated.reasons,
|
|
1160
|
+
reasonDetails: evaluated.reasonDetails,
|
|
769
1161
|
warnings: evaluated.warnings,
|
|
770
1162
|
actionableFeedback: evaluated.reasons,
|
|
771
1163
|
evidence
|
|
772
1164
|
};
|
|
773
1165
|
}
|
|
1166
|
+
function strictMergeHeadShaFromGate(result, prUrl) {
|
|
1167
|
+
if (!result.approved) {
|
|
1168
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
|
|
1169
|
+
}
|
|
1170
|
+
if (result.evidence.prUrl !== prUrl) {
|
|
1171
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
|
|
1172
|
+
}
|
|
1173
|
+
const headSha = result.evidence.headSha?.trim();
|
|
1174
|
+
if (!headSha) {
|
|
1175
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
1176
|
+
}
|
|
1177
|
+
if (!/^[0-9a-f]{40}$/i.test(headSha)) {
|
|
1178
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
|
|
1179
|
+
}
|
|
1180
|
+
if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
|
|
1181
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
|
|
1182
|
+
}
|
|
1183
|
+
if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
|
|
1184
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
|
|
1185
|
+
}
|
|
1186
|
+
return headSha;
|
|
1187
|
+
}
|
|
774
1188
|
function promptExcerpt(value, maxChars = 4000) {
|
|
775
1189
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
776
1190
|
|
|
@@ -782,6 +1196,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
782
1196
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
783
1197
|
const evidence = result.evidence;
|
|
784
1198
|
const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
|
|
1199
|
+
const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
|
|
1200
|
+
if (result.reasons.length > displayedReasons.length) {
|
|
1201
|
+
displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
|
|
1202
|
+
}
|
|
785
1203
|
const lines = [
|
|
786
1204
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
787
1205
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -790,10 +1208,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
790
1208
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
791
1209
|
"",
|
|
792
1210
|
"Gate reasons:",
|
|
793
|
-
...
|
|
1211
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
1212
|
+
"",
|
|
1213
|
+
"Structured gate reason details:",
|
|
1214
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
794
1215
|
"",
|
|
795
1216
|
"Required evidence read status:",
|
|
796
|
-
evidence.readErrors.length ?
|
|
1217
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
797
1218
|
"",
|
|
798
1219
|
"Full PR title:",
|
|
799
1220
|
evidence.title || "(empty)",
|
|
@@ -857,6 +1278,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
857
1278
|
approved: input.result.approved,
|
|
858
1279
|
pending: input.result.pending,
|
|
859
1280
|
reasons: input.result.reasons,
|
|
1281
|
+
reasonDetails: input.result.reasonDetails,
|
|
860
1282
|
warnings: input.result.warnings,
|
|
861
1283
|
actionableFeedback: input.result.actionableFeedback,
|
|
862
1284
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -895,6 +1317,7 @@ async function runStrictPrMergeGate(input) {
|
|
|
895
1317
|
}
|
|
896
1318
|
export {
|
|
897
1319
|
stripHtml,
|
|
1320
|
+
strictMergeHeadShaFromGate,
|
|
898
1321
|
runStrictPrMergeGate,
|
|
899
1322
|
persistPrReviewCycleArtifacts,
|
|
900
1323
|
parseGreptileScore,
|