@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
|
@@ -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,7 +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);
|
|
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);
|
|
113
158
|
}
|
|
114
159
|
function isStrictFiveOfFive(score) {
|
|
115
160
|
return score.value === 5 && score.scale === 5;
|
|
@@ -117,6 +162,189 @@ function isStrictFiveOfFive(score) {
|
|
|
117
162
|
function containsConflictingScoreText(input) {
|
|
118
163
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
119
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
|
+
}
|
|
120
348
|
function firstString(record, keys) {
|
|
121
349
|
for (const key of keys) {
|
|
122
350
|
const value = record[key];
|
|
@@ -243,7 +471,7 @@ function normalizeReviewThread(entry) {
|
|
|
243
471
|
function relevantIssueComment(comment) {
|
|
244
472
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
245
473
|
const body = comment.body ?? "";
|
|
246
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
474
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
247
475
|
}
|
|
248
476
|
function latestThreadComment(thread) {
|
|
249
477
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -279,7 +507,8 @@ function makeGreptileSignal(input) {
|
|
|
279
507
|
const scores = parseGreptileScores(input.body);
|
|
280
508
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
281
509
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
282
|
-
const
|
|
510
|
+
const verdict = input.verdict ?? null;
|
|
511
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
283
512
|
const explicitApproval = input.explicitApproval ?? false;
|
|
284
513
|
return {
|
|
285
514
|
source: input.source,
|
|
@@ -291,6 +520,7 @@ function makeGreptileSignal(input) {
|
|
|
291
520
|
score: scores[0] ?? null,
|
|
292
521
|
scores,
|
|
293
522
|
explicitApproval,
|
|
523
|
+
verdict,
|
|
294
524
|
blocker,
|
|
295
525
|
actionable: input.actionable ?? blocker,
|
|
296
526
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -313,9 +543,9 @@ function collectGreptileSignals(evidence) {
|
|
|
313
543
|
for (const context of contextSources) {
|
|
314
544
|
if (!context.body.trim())
|
|
315
545
|
continue;
|
|
316
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
317
|
-
continue;
|
|
318
546
|
const contextBlocker = containsBlockerText(context.body);
|
|
547
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
548
|
+
continue;
|
|
319
549
|
signals.push(makeGreptileSignal({
|
|
320
550
|
source: context.source,
|
|
321
551
|
body: context.body,
|
|
@@ -328,16 +558,16 @@ function collectGreptileSignals(evidence) {
|
|
|
328
558
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
329
559
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
330
560
|
|
|
331
|
-
`);
|
|
332
|
-
|
|
333
|
-
continue;
|
|
561
|
+
`) || "Status: UNKNOWN";
|
|
562
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
334
563
|
signals.push(makeGreptileSignal({
|
|
335
564
|
source: "api",
|
|
336
565
|
body,
|
|
337
566
|
currentHeadSha: evidence.currentHeadSha,
|
|
338
567
|
trusted: true,
|
|
339
568
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
340
|
-
explicitApproval:
|
|
569
|
+
explicitApproval: verdict === "approved",
|
|
570
|
+
verdict
|
|
341
571
|
}));
|
|
342
572
|
}
|
|
343
573
|
for (const review of evidence.reviews) {
|
|
@@ -362,20 +592,6 @@ function collectGreptileSignals(evidence) {
|
|
|
362
592
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
363
593
|
}));
|
|
364
594
|
}
|
|
365
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
366
|
-
const login = commentAuthorLogin(comment);
|
|
367
|
-
const body = comment.body ?? "";
|
|
368
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
369
|
-
continue;
|
|
370
|
-
signals.push(makeGreptileSignal({
|
|
371
|
-
source: "changed-file-comment",
|
|
372
|
-
body,
|
|
373
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
374
|
-
trusted: true,
|
|
375
|
-
authorLogin: login,
|
|
376
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
595
|
for (const comment of evidence.relevantIssueComments) {
|
|
380
596
|
const login = commentAuthorLogin(comment);
|
|
381
597
|
const body = comment.body ?? "";
|
|
@@ -483,10 +699,17 @@ function deriveGreptileEvidence(input) {
|
|
|
483
699
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
484
700
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
485
701
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
486
|
-
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;
|
|
487
710
|
const approvedByScore = !!approvingScoreEntry;
|
|
488
|
-
const approvedByExplicitMapping =
|
|
489
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
711
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
712
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
490
713
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
491
714
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
492
715
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
@@ -515,13 +738,14 @@ function deriveGreptileEvidence(input) {
|
|
|
515
738
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
516
739
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
517
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"));
|
|
518
742
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
519
743
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
520
744
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
521
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
745
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
522
746
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
523
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
524
|
-
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";
|
|
525
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";
|
|
526
750
|
return {
|
|
527
751
|
source,
|
|
@@ -628,6 +852,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
628
852
|
readErrors.push("gh pr view did not return required reviews array");
|
|
629
853
|
}
|
|
630
854
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
855
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
631
856
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
632
857
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
633
858
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -665,8 +890,19 @@ async function collectPrReviewEvidence(input) {
|
|
|
665
890
|
}
|
|
666
891
|
}
|
|
667
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];
|
|
668
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})` : ""}`);
|
|
669
|
-
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
905
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
670
906
|
const evidenceBase = {
|
|
671
907
|
title: firstString(view, ["title"]),
|
|
672
908
|
body: firstString(view, ["body"]),
|
|
@@ -676,7 +912,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
676
912
|
reviewThreads,
|
|
677
913
|
checks: checksWithGreptileDetails,
|
|
678
914
|
currentHeadSha: headSha,
|
|
679
|
-
apiSignals
|
|
915
|
+
apiSignals
|
|
680
916
|
};
|
|
681
917
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
682
918
|
return {
|
|
@@ -687,7 +923,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
687
923
|
body: evidenceBase.body,
|
|
688
924
|
headSha,
|
|
689
925
|
headRefName: firstString(view, ["headRefName"]),
|
|
690
|
-
baseRefName
|
|
926
|
+
baseRefName,
|
|
691
927
|
state: firstString(view, ["state"]),
|
|
692
928
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
693
929
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -704,71 +940,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
704
940
|
greptile
|
|
705
941
|
};
|
|
706
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
|
+
}
|
|
707
948
|
function evaluateEvidence(evidence) {
|
|
708
|
-
const
|
|
949
|
+
const reasonDetails = [];
|
|
709
950
|
const warnings = [];
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
});
|
|
721
1005
|
}
|
|
722
1006
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
723
1007
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
724
|
-
|
|
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
|
+
});
|
|
725
1036
|
}
|
|
726
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
727
|
-
if (unresolvedThreads.length > 0)
|
|
728
|
-
reasons.push(...unresolvedThreads);
|
|
729
|
-
const greptile = evidence.greptile;
|
|
730
|
-
if (greptile.mapping === "missing")
|
|
731
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
732
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
733
1037
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
734
|
-
|
|
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
|
+
});
|
|
735
1069
|
}
|
|
736
1070
|
if (!greptile.completed) {
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
});
|
|
739
1091
|
}
|
|
740
|
-
if (!greptile.fresh)
|
|
741
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
742
1092
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
743
|
-
|
|
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
|
+
});
|
|
744
1102
|
}
|
|
745
|
-
|
|
746
|
-
|
|
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
|
+
});
|
|
747
1114
|
}
|
|
748
1115
|
if (greptile.mapping === "unproven") {
|
|
749
|
-
|
|
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
|
+
});
|
|
750
1125
|
}
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
});
|
|
753
1147
|
}
|
|
754
|
-
if (greptile.unresolvedComments.length > 0)
|
|
755
|
-
reasons.push(...greptile.unresolvedComments);
|
|
756
1148
|
if (!greptile.approved)
|
|
757
1149
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
758
|
-
|
|
1150
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
1151
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
759
1152
|
}
|
|
760
1153
|
function evaluateStrictPrMergeGate(evidence) {
|
|
761
1154
|
const evaluated = evaluateEvidence(evidence);
|
|
762
|
-
const approved = evaluated.
|
|
1155
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
763
1156
|
return {
|
|
764
1157
|
approved,
|
|
765
1158
|
pending: evaluated.pending,
|
|
766
1159
|
reasons: evaluated.reasons,
|
|
1160
|
+
reasonDetails: evaluated.reasonDetails,
|
|
767
1161
|
warnings: evaluated.warnings,
|
|
768
1162
|
actionableFeedback: evaluated.reasons,
|
|
769
1163
|
evidence
|
|
770
1164
|
};
|
|
771
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
|
+
}
|
|
772
1188
|
function promptExcerpt(value, maxChars = 4000) {
|
|
773
1189
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
774
1190
|
|
|
@@ -780,6 +1196,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
780
1196
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
781
1197
|
const evidence = result.evidence;
|
|
782
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
|
+
}
|
|
783
1203
|
const lines = [
|
|
784
1204
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
785
1205
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -788,10 +1208,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
788
1208
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
789
1209
|
"",
|
|
790
1210
|
"Gate reasons:",
|
|
791
|
-
...
|
|
1211
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
1212
|
+
"",
|
|
1213
|
+
"Structured gate reason details:",
|
|
1214
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
792
1215
|
"",
|
|
793
1216
|
"Required evidence read status:",
|
|
794
|
-
evidence.readErrors.length ?
|
|
1217
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
795
1218
|
"",
|
|
796
1219
|
"Full PR title:",
|
|
797
1220
|
evidence.title || "(empty)",
|
|
@@ -855,6 +1278,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
855
1278
|
approved: input.result.approved,
|
|
856
1279
|
pending: input.result.pending,
|
|
857
1280
|
reasons: input.result.reasons,
|
|
1281
|
+
reasonDetails: input.result.reasonDetails,
|
|
858
1282
|
warnings: input.result.warnings,
|
|
859
1283
|
actionableFeedback: input.result.actionableFeedback,
|
|
860
1284
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -893,6 +1317,7 @@ async function runStrictPrMergeGate(input) {
|
|
|
893
1317
|
}
|
|
894
1318
|
export {
|
|
895
1319
|
stripHtml,
|
|
1320
|
+
strictMergeHeadShaFromGate,
|
|
896
1321
|
runStrictPrMergeGate,
|
|
897
1322
|
persistPrReviewCycleArtifacts,
|
|
898
1323
|
parseGreptileScore,
|