@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" };
|
|
@@ -106,7 +151,7 @@ function stripHtml(input) {
|
|
|
106
151
|
}
|
|
107
152
|
function containsBlockerText(input) {
|
|
108
153
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
109
|
-
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);
|
|
154
|
+
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);
|
|
110
155
|
}
|
|
111
156
|
function isStrictFiveOfFive(score) {
|
|
112
157
|
return score.value === 5 && score.scale === 5;
|
|
@@ -114,6 +159,189 @@ function isStrictFiveOfFive(score) {
|
|
|
114
159
|
function containsConflictingScoreText(input) {
|
|
115
160
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
116
161
|
}
|
|
162
|
+
function greptileStatusVerdict(status) {
|
|
163
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
164
|
+
if (!normalized)
|
|
165
|
+
return null;
|
|
166
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
167
|
+
return "approved";
|
|
168
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
169
|
+
return "rejected";
|
|
170
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
171
|
+
return "skipped";
|
|
172
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
173
|
+
return "failed";
|
|
174
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
175
|
+
return "pending";
|
|
176
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
177
|
+
return "completed";
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
181
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
182
|
+
}
|
|
183
|
+
function greptileRequestTimeoutMs(env) {
|
|
184
|
+
const fallback = 30000;
|
|
185
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
186
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
187
|
+
}
|
|
188
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
189
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
190
|
+
return null;
|
|
191
|
+
const record = entry;
|
|
192
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
193
|
+
if (!id)
|
|
194
|
+
return null;
|
|
195
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
199
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
200
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
201
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
205
|
+
const seen = new Set;
|
|
206
|
+
const unique = [];
|
|
207
|
+
for (const review of reviews) {
|
|
208
|
+
if (seen.has(review.id))
|
|
209
|
+
continue;
|
|
210
|
+
seen.add(review.id);
|
|
211
|
+
unique.push(review);
|
|
212
|
+
}
|
|
213
|
+
return unique;
|
|
214
|
+
}
|
|
215
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
216
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
217
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
218
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
219
|
+
const latest = sorted.slice(0, 1);
|
|
220
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
221
|
+
}
|
|
222
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
223
|
+
const selected = details ?? review;
|
|
224
|
+
return {
|
|
225
|
+
id: selected.id || review.id,
|
|
226
|
+
body: selected.body ?? review.body ?? null,
|
|
227
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
228
|
+
status: selected.status ?? review.status ?? null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function callGreptileMcpToolForGate(input) {
|
|
232
|
+
const controller = new AbortController;
|
|
233
|
+
const timeoutId = setTimeout(() => {
|
|
234
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
235
|
+
}, input.timeoutMs);
|
|
236
|
+
let response;
|
|
237
|
+
try {
|
|
238
|
+
response = await input.fetchFn(input.apiBase, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
242
|
+
"Content-Type": "application/json"
|
|
243
|
+
},
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
247
|
+
method: "tools/call",
|
|
248
|
+
params: { name: input.name, arguments: input.args }
|
|
249
|
+
}),
|
|
250
|
+
signal: controller.signal
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (controller.signal.aborted) {
|
|
254
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
} finally {
|
|
258
|
+
clearTimeout(timeoutId);
|
|
259
|
+
}
|
|
260
|
+
const raw = await response.text();
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
263
|
+
}
|
|
264
|
+
let envelope;
|
|
265
|
+
try {
|
|
266
|
+
envelope = JSON.parse(raw);
|
|
267
|
+
} catch {
|
|
268
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
269
|
+
}
|
|
270
|
+
if (envelope.error?.message) {
|
|
271
|
+
throw new Error(envelope.error.message);
|
|
272
|
+
}
|
|
273
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
274
|
+
`).trim();
|
|
275
|
+
if (!text) {
|
|
276
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
277
|
+
}
|
|
278
|
+
return text;
|
|
279
|
+
}
|
|
280
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
281
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(text);
|
|
284
|
+
} catch {
|
|
285
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
289
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
290
|
+
return { signals: [], errors: [] };
|
|
291
|
+
}
|
|
292
|
+
const env = input.options?.env ?? process.env;
|
|
293
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
294
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
295
|
+
if (!apiKey) {
|
|
296
|
+
return { signals: [], errors: [] };
|
|
297
|
+
}
|
|
298
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
299
|
+
if (typeof fetchFn !== "function") {
|
|
300
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
301
|
+
}
|
|
302
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
303
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
304
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
305
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
306
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
307
|
+
try {
|
|
308
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
309
|
+
apiBase,
|
|
310
|
+
apiKey,
|
|
311
|
+
name: "list_code_reviews",
|
|
312
|
+
args: {
|
|
313
|
+
name: repository,
|
|
314
|
+
remote,
|
|
315
|
+
defaultBranch,
|
|
316
|
+
prNumber: input.prNumber,
|
|
317
|
+
limit: 20
|
|
318
|
+
},
|
|
319
|
+
timeoutMs,
|
|
320
|
+
fetchFn
|
|
321
|
+
});
|
|
322
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
323
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
324
|
+
const signals = [];
|
|
325
|
+
for (const review of selectedReviews) {
|
|
326
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
327
|
+
apiBase,
|
|
328
|
+
apiKey,
|
|
329
|
+
name: "get_code_review",
|
|
330
|
+
args: { codeReviewId: review.id },
|
|
331
|
+
timeoutMs,
|
|
332
|
+
fetchFn
|
|
333
|
+
});
|
|
334
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
335
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
336
|
+
}
|
|
337
|
+
return { signals, errors: [] };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
signals: [],
|
|
341
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
117
345
|
function firstString(record, keys) {
|
|
118
346
|
for (const key of keys) {
|
|
119
347
|
const value = record[key];
|
|
@@ -240,7 +468,7 @@ function normalizeReviewThread(entry) {
|
|
|
240
468
|
function relevantIssueComment(comment) {
|
|
241
469
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
242
470
|
const body = comment.body ?? "";
|
|
243
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
471
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
244
472
|
}
|
|
245
473
|
function latestThreadComment(thread) {
|
|
246
474
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -276,7 +504,8 @@ function makeGreptileSignal(input) {
|
|
|
276
504
|
const scores = parseGreptileScores(input.body);
|
|
277
505
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
278
506
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
279
|
-
const
|
|
507
|
+
const verdict = input.verdict ?? null;
|
|
508
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
280
509
|
const explicitApproval = input.explicitApproval ?? false;
|
|
281
510
|
return {
|
|
282
511
|
source: input.source,
|
|
@@ -288,6 +517,7 @@ function makeGreptileSignal(input) {
|
|
|
288
517
|
score: scores[0] ?? null,
|
|
289
518
|
scores,
|
|
290
519
|
explicitApproval,
|
|
520
|
+
verdict,
|
|
291
521
|
blocker,
|
|
292
522
|
actionable: input.actionable ?? blocker,
|
|
293
523
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -310,9 +540,9 @@ function collectGreptileSignals(evidence) {
|
|
|
310
540
|
for (const context of contextSources) {
|
|
311
541
|
if (!context.body.trim())
|
|
312
542
|
continue;
|
|
313
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
314
|
-
continue;
|
|
315
543
|
const contextBlocker = containsBlockerText(context.body);
|
|
544
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
545
|
+
continue;
|
|
316
546
|
signals.push(makeGreptileSignal({
|
|
317
547
|
source: context.source,
|
|
318
548
|
body: context.body,
|
|
@@ -325,16 +555,16 @@ function collectGreptileSignals(evidence) {
|
|
|
325
555
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
326
556
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
327
557
|
|
|
328
|
-
`);
|
|
329
|
-
|
|
330
|
-
continue;
|
|
558
|
+
`) || "Status: UNKNOWN";
|
|
559
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
331
560
|
signals.push(makeGreptileSignal({
|
|
332
561
|
source: "api",
|
|
333
562
|
body,
|
|
334
563
|
currentHeadSha: evidence.currentHeadSha,
|
|
335
564
|
trusted: true,
|
|
336
565
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
337
|
-
explicitApproval:
|
|
566
|
+
explicitApproval: verdict === "approved",
|
|
567
|
+
verdict
|
|
338
568
|
}));
|
|
339
569
|
}
|
|
340
570
|
for (const review of evidence.reviews) {
|
|
@@ -359,20 +589,6 @@ function collectGreptileSignals(evidence) {
|
|
|
359
589
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
360
590
|
}));
|
|
361
591
|
}
|
|
362
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
363
|
-
const login = commentAuthorLogin(comment);
|
|
364
|
-
const body = comment.body ?? "";
|
|
365
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
366
|
-
continue;
|
|
367
|
-
signals.push(makeGreptileSignal({
|
|
368
|
-
source: "changed-file-comment",
|
|
369
|
-
body,
|
|
370
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
371
|
-
trusted: true,
|
|
372
|
-
authorLogin: login,
|
|
373
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
374
|
-
}));
|
|
375
|
-
}
|
|
376
592
|
for (const comment of evidence.relevantIssueComments) {
|
|
377
593
|
const login = commentAuthorLogin(comment);
|
|
378
594
|
const body = comment.body ?? "";
|
|
@@ -480,10 +696,17 @@ function deriveGreptileEvidence(input) {
|
|
|
480
696
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
481
697
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
482
698
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
483
|
-
const
|
|
699
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
700
|
+
const signalCanApproveByScore = (signal) => {
|
|
701
|
+
if (signal.source === "api")
|
|
702
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
703
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
704
|
+
};
|
|
705
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
706
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
484
707
|
const approvedByScore = !!approvingScoreEntry;
|
|
485
|
-
const approvedByExplicitMapping =
|
|
486
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
708
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
709
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
487
710
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
488
711
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
489
712
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
@@ -512,13 +735,14 @@ function deriveGreptileEvidence(input) {
|
|
|
512
735
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
513
736
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
514
737
|
});
|
|
738
|
+
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"));
|
|
515
739
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
516
740
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
517
741
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
518
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
742
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
519
743
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
520
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
521
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
744
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
745
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
522
746
|
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";
|
|
523
747
|
return {
|
|
524
748
|
source,
|
|
@@ -625,6 +849,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
625
849
|
readErrors.push("gh pr view did not return required reviews array");
|
|
626
850
|
}
|
|
627
851
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
852
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
628
853
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
629
854
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
630
855
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -662,8 +887,19 @@ async function collectPrReviewEvidence(input) {
|
|
|
662
887
|
}
|
|
663
888
|
}
|
|
664
889
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
890
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
891
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
892
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
893
|
+
options: input.greptileApi,
|
|
894
|
+
repoName: parsed.repoName,
|
|
895
|
+
prNumber: parsed.prNumber,
|
|
896
|
+
headSha,
|
|
897
|
+
baseRefName
|
|
898
|
+
});
|
|
899
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
900
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
665
901
|
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})` : ""}`);
|
|
666
|
-
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
902
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
667
903
|
const evidenceBase = {
|
|
668
904
|
title: firstString(view, ["title"]),
|
|
669
905
|
body: firstString(view, ["body"]),
|
|
@@ -673,7 +909,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
673
909
|
reviewThreads,
|
|
674
910
|
checks: checksWithGreptileDetails,
|
|
675
911
|
currentHeadSha: headSha,
|
|
676
|
-
apiSignals
|
|
912
|
+
apiSignals
|
|
677
913
|
};
|
|
678
914
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
679
915
|
return {
|
|
@@ -684,7 +920,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
684
920
|
body: evidenceBase.body,
|
|
685
921
|
headSha,
|
|
686
922
|
headRefName: firstString(view, ["headRefName"]),
|
|
687
|
-
baseRefName
|
|
923
|
+
baseRefName,
|
|
688
924
|
state: firstString(view, ["state"]),
|
|
689
925
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
690
926
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -701,71 +937,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
701
937
|
greptile
|
|
702
938
|
};
|
|
703
939
|
}
|
|
940
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
941
|
+
const normalized = value.trim();
|
|
942
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
943
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
944
|
+
}
|
|
704
945
|
function evaluateEvidence(evidence) {
|
|
705
|
-
const
|
|
946
|
+
const reasonDetails = [];
|
|
706
947
|
const warnings = [];
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
948
|
+
const seen = new Set;
|
|
949
|
+
const addReason = (reason) => {
|
|
950
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
951
|
+
const key = `${capped.code}:${capped.message}`;
|
|
952
|
+
if (seen.has(key))
|
|
953
|
+
return;
|
|
954
|
+
seen.add(key);
|
|
955
|
+
reasonDetails.push(capped);
|
|
956
|
+
};
|
|
957
|
+
const greptile = evidence.greptile;
|
|
958
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
959
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
960
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
961
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
962
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
963
|
+
for (const error of evidence.readErrors) {
|
|
964
|
+
addReason({
|
|
965
|
+
code: "read_error",
|
|
966
|
+
reasonClass: "reject",
|
|
967
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
968
|
+
suggestedAction: "needs_attention",
|
|
969
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
970
|
+
headSha: evidence.headSha || null
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (!evidence.headSha) {
|
|
974
|
+
addReason({
|
|
975
|
+
code: "missing_head_sha",
|
|
976
|
+
reasonClass: "reject",
|
|
977
|
+
surface: "github",
|
|
978
|
+
suggestedAction: "needs_attention",
|
|
979
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
980
|
+
headSha: null
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
for (const failure of evidence.checkFailures) {
|
|
984
|
+
addReason({
|
|
985
|
+
code: "ci_failed",
|
|
986
|
+
reasonClass: "reject",
|
|
987
|
+
surface: "ci",
|
|
988
|
+
suggestedAction: "fix",
|
|
989
|
+
message: failure,
|
|
990
|
+
headSha: evidence.headSha || null
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
994
|
+
addReason({
|
|
995
|
+
code: "check_pending",
|
|
996
|
+
reasonClass: "pending",
|
|
997
|
+
surface: "ci",
|
|
998
|
+
suggestedAction: "wait",
|
|
999
|
+
message: pendingCheck,
|
|
1000
|
+
headSha: evidence.headSha || null
|
|
1001
|
+
});
|
|
718
1002
|
}
|
|
719
1003
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
720
1004
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
721
|
-
|
|
1005
|
+
addReason({
|
|
1006
|
+
code: "review_decision_blocking",
|
|
1007
|
+
reasonClass: "reject",
|
|
1008
|
+
surface: "review",
|
|
1009
|
+
suggestedAction: "fix",
|
|
1010
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
1011
|
+
headSha: evidence.headSha || null
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
1015
|
+
addReason({
|
|
1016
|
+
code: "review_thread_unresolved",
|
|
1017
|
+
reasonClass: "reject",
|
|
1018
|
+
surface: "review",
|
|
1019
|
+
suggestedAction: "fix",
|
|
1020
|
+
message: thread,
|
|
1021
|
+
headSha: evidence.headSha || null
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (greptile.mapping === "missing") {
|
|
1025
|
+
addReason({
|
|
1026
|
+
code: "greptile_missing",
|
|
1027
|
+
reasonClass: "pending",
|
|
1028
|
+
surface: "greptile",
|
|
1029
|
+
suggestedAction: "wait",
|
|
1030
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
1031
|
+
headSha: evidence.headSha || null
|
|
1032
|
+
});
|
|
722
1033
|
}
|
|
723
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
724
|
-
if (unresolvedThreads.length > 0)
|
|
725
|
-
reasons.push(...unresolvedThreads);
|
|
726
|
-
const greptile = evidence.greptile;
|
|
727
|
-
if (greptile.mapping === "missing")
|
|
728
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
729
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
730
1034
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
731
|
-
|
|
1035
|
+
addReason({
|
|
1036
|
+
code: "greptile_stale",
|
|
1037
|
+
reasonClass: "pending",
|
|
1038
|
+
surface: "greptile",
|
|
1039
|
+
suggestedAction: "wait",
|
|
1040
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
1041
|
+
headSha: evidence.headSha || null,
|
|
1042
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
1046
|
+
addReason({
|
|
1047
|
+
code: "greptile_pending",
|
|
1048
|
+
reasonClass: "pending",
|
|
1049
|
+
surface: "greptile",
|
|
1050
|
+
suggestedAction: "wait",
|
|
1051
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
1052
|
+
headSha: evidence.headSha || null,
|
|
1053
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
1057
|
+
addReason({
|
|
1058
|
+
code: "greptile_api_status_unknown",
|
|
1059
|
+
reasonClass: "reject",
|
|
1060
|
+
surface: "greptile",
|
|
1061
|
+
suggestedAction: "needs_attention",
|
|
1062
|
+
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}` : "."}`,
|
|
1063
|
+
headSha: evidence.headSha || null,
|
|
1064
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
1065
|
+
});
|
|
732
1066
|
}
|
|
733
1067
|
if (!greptile.completed) {
|
|
734
|
-
|
|
735
|
-
|
|
1068
|
+
addReason({
|
|
1069
|
+
code: "greptile_pending",
|
|
1070
|
+
reasonClass: "pending",
|
|
1071
|
+
surface: "greptile",
|
|
1072
|
+
suggestedAction: "wait",
|
|
1073
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
1074
|
+
headSha: evidence.headSha || null,
|
|
1075
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
if (!greptile.fresh) {
|
|
1079
|
+
addReason({
|
|
1080
|
+
code: "greptile_not_current_head",
|
|
1081
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1082
|
+
surface: "greptile",
|
|
1083
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1084
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
1085
|
+
headSha: evidence.headSha || null,
|
|
1086
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1087
|
+
});
|
|
736
1088
|
}
|
|
737
|
-
if (!greptile.fresh)
|
|
738
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
739
1089
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
740
|
-
|
|
1090
|
+
addReason({
|
|
1091
|
+
code: "greptile_score_not_5",
|
|
1092
|
+
reasonClass: "reject",
|
|
1093
|
+
surface: "greptile",
|
|
1094
|
+
suggestedAction: "fix",
|
|
1095
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
1096
|
+
headSha: evidence.headSha || null,
|
|
1097
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1098
|
+
});
|
|
741
1099
|
}
|
|
742
|
-
|
|
743
|
-
|
|
1100
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
1101
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
1102
|
+
addReason({
|
|
1103
|
+
code: "greptile_score_missing",
|
|
1104
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1105
|
+
surface: "greptile",
|
|
1106
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1107
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
1108
|
+
headSha: evidence.headSha || null,
|
|
1109
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1110
|
+
});
|
|
744
1111
|
}
|
|
745
1112
|
if (greptile.mapping === "unproven") {
|
|
746
|
-
|
|
1113
|
+
addReason({
|
|
1114
|
+
code: "greptile_mapping_unproven",
|
|
1115
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
1116
|
+
surface: "greptile",
|
|
1117
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
1118
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
1119
|
+
headSha: evidence.headSha || null,
|
|
1120
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
for (const blocker of greptile.blockers) {
|
|
1124
|
+
addReason({
|
|
1125
|
+
code: "greptile_blocker_text",
|
|
1126
|
+
reasonClass: "reject",
|
|
1127
|
+
surface: "greptile",
|
|
1128
|
+
suggestedAction: "fix",
|
|
1129
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
1130
|
+
headSha: evidence.headSha || null,
|
|
1131
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1132
|
+
});
|
|
747
1133
|
}
|
|
748
|
-
|
|
749
|
-
|
|
1134
|
+
for (const comment of greptile.unresolvedComments) {
|
|
1135
|
+
addReason({
|
|
1136
|
+
code: "greptile_unresolved_comment",
|
|
1137
|
+
reasonClass: "reject",
|
|
1138
|
+
surface: "greptile",
|
|
1139
|
+
suggestedAction: "fix",
|
|
1140
|
+
message: comment,
|
|
1141
|
+
headSha: evidence.headSha || null,
|
|
1142
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
1143
|
+
});
|
|
750
1144
|
}
|
|
751
|
-
if (greptile.unresolvedComments.length > 0)
|
|
752
|
-
reasons.push(...greptile.unresolvedComments);
|
|
753
1145
|
if (!greptile.approved)
|
|
754
1146
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
755
|
-
|
|
1147
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
1148
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
756
1149
|
}
|
|
757
1150
|
function evaluateStrictPrMergeGate(evidence) {
|
|
758
1151
|
const evaluated = evaluateEvidence(evidence);
|
|
759
|
-
const approved = evaluated.
|
|
1152
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
760
1153
|
return {
|
|
761
1154
|
approved,
|
|
762
1155
|
pending: evaluated.pending,
|
|
763
1156
|
reasons: evaluated.reasons,
|
|
1157
|
+
reasonDetails: evaluated.reasonDetails,
|
|
764
1158
|
warnings: evaluated.warnings,
|
|
765
1159
|
actionableFeedback: evaluated.reasons,
|
|
766
1160
|
evidence
|
|
767
1161
|
};
|
|
768
1162
|
}
|
|
1163
|
+
function strictMergeHeadShaFromGate(result, prUrl) {
|
|
1164
|
+
if (!result.approved) {
|
|
1165
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
|
|
1166
|
+
}
|
|
1167
|
+
if (result.evidence.prUrl !== prUrl) {
|
|
1168
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
|
|
1169
|
+
}
|
|
1170
|
+
const headSha = result.evidence.headSha?.trim();
|
|
1171
|
+
if (!headSha) {
|
|
1172
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!/^[0-9a-f]{40}$/i.test(headSha)) {
|
|
1175
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
|
|
1176
|
+
}
|
|
1177
|
+
if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
|
|
1178
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
|
|
1179
|
+
}
|
|
1180
|
+
if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
|
|
1181
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
|
|
1182
|
+
}
|
|
1183
|
+
return headSha;
|
|
1184
|
+
}
|
|
769
1185
|
function promptExcerpt(value, maxChars = 4000) {
|
|
770
1186
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
771
1187
|
|
|
@@ -777,6 +1193,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
777
1193
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
778
1194
|
const evidence = result.evidence;
|
|
779
1195
|
const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
|
|
1196
|
+
const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
|
|
1197
|
+
if (result.reasons.length > displayedReasons.length) {
|
|
1198
|
+
displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
|
|
1199
|
+
}
|
|
780
1200
|
const lines = [
|
|
781
1201
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
782
1202
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -785,10 +1205,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
785
1205
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
786
1206
|
"",
|
|
787
1207
|
"Gate reasons:",
|
|
788
|
-
...
|
|
1208
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
1209
|
+
"",
|
|
1210
|
+
"Structured gate reason details:",
|
|
1211
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
789
1212
|
"",
|
|
790
1213
|
"Required evidence read status:",
|
|
791
|
-
evidence.readErrors.length ?
|
|
1214
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
792
1215
|
"",
|
|
793
1216
|
"Full PR title:",
|
|
794
1217
|
evidence.title || "(empty)",
|
|
@@ -852,6 +1275,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
852
1275
|
approved: input.result.approved,
|
|
853
1276
|
pending: input.result.pending,
|
|
854
1277
|
reasons: input.result.reasons,
|
|
1278
|
+
reasonDetails: input.result.reasonDetails,
|
|
855
1279
|
warnings: input.result.warnings,
|
|
856
1280
|
actionableFeedback: input.result.actionableFeedback,
|
|
857
1281
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -1225,10 +1649,7 @@ async function runRepoDefaultMerge(input) {
|
|
|
1225
1649
|
const merge = input.config?.merge ?? {};
|
|
1226
1650
|
if (merge.mode === "off")
|
|
1227
1651
|
return;
|
|
1228
|
-
const matchHeadSha = input.
|
|
1229
|
-
if (!matchHeadSha) {
|
|
1230
|
-
throw new Error(`Refusing to merge ${input.prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
1231
|
-
}
|
|
1652
|
+
const matchHeadSha = strictMergeHeadShaFromGate(input.strictGate, input.prUrl);
|
|
1232
1653
|
const method = merge.method ?? "repo-default";
|
|
1233
1654
|
const args = ["pr", "merge", input.prUrl];
|
|
1234
1655
|
if (method === "repo-default") {
|
|
@@ -1250,17 +1671,17 @@ function shouldAttemptRigMerge(config) {
|
|
|
1250
1671
|
return mode !== "off" && mode !== "pr-ready";
|
|
1251
1672
|
}
|
|
1252
1673
|
function isPendingOnlyGate(result) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
if (
|
|
1257
|
-
return
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1674
|
+
return result.pending && result.reasonDetails.length > 0 && result.reasonDetails.every((reason) => reason.reasonClass === "pending" && reason.suggestedAction === "wait");
|
|
1675
|
+
}
|
|
1676
|
+
async function syncBranchAfterPrFeedback(input) {
|
|
1677
|
+
if (!input.gitCommand)
|
|
1678
|
+
return;
|
|
1679
|
+
await commitRunChanges({
|
|
1680
|
+
cwd: input.projectRoot,
|
|
1681
|
+
message: `rig: address PR feedback for task ${input.taskId}`,
|
|
1682
|
+
command: input.gitCommand
|
|
1683
|
+
});
|
|
1684
|
+
await runChecked(input.gitCommand, ["push", "--set-upstream", "origin", input.branch], input.projectRoot, "git");
|
|
1264
1685
|
}
|
|
1265
1686
|
async function runPrAutomation(input) {
|
|
1266
1687
|
const prConfig = input.config?.pr ?? {};
|
|
@@ -1340,6 +1761,7 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1340
1761
|
...latestFeedback.map((entry) => `- ${entry}`)
|
|
1341
1762
|
].join(`
|
|
1342
1763
|
`));
|
|
1764
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1343
1765
|
continue;
|
|
1344
1766
|
}
|
|
1345
1767
|
const gate = await runStrictPrMergeGate({
|
|
@@ -1350,7 +1772,8 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1350
1772
|
cycle: iteration,
|
|
1351
1773
|
command: input.command,
|
|
1352
1774
|
artifactRoot: input.artifactRoot,
|
|
1353
|
-
allowedFailures: input.config?.merge?.allowedFailures ?? []
|
|
1775
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
1776
|
+
greptileApi: input.greptileApi
|
|
1354
1777
|
});
|
|
1355
1778
|
latestFeedback = [...gate.actionableFeedback];
|
|
1356
1779
|
if (gate.approved) {
|
|
@@ -1364,20 +1787,33 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1364
1787
|
command: input.command,
|
|
1365
1788
|
artifactRoot: input.artifactRoot,
|
|
1366
1789
|
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
1790
|
+
greptileApi: input.greptileApi,
|
|
1367
1791
|
final: true
|
|
1368
1792
|
});
|
|
1369
1793
|
if (finalGate.approved) {
|
|
1370
1794
|
await input.lifecycle?.onMergeStarted?.({ prUrl });
|
|
1371
|
-
await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot,
|
|
1795
|
+
await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
|
|
1372
1796
|
await input.lifecycle?.onMerged?.({ prUrl });
|
|
1373
1797
|
return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
|
|
1374
1798
|
}
|
|
1375
1799
|
latestFeedback = [...finalGate.actionableFeedback];
|
|
1800
|
+
if (isPendingOnlyGate(finalGate)) {
|
|
1801
|
+
const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
|
|
1802
|
+
const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
|
|
1803
|
+
if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
|
|
1804
|
+
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
1805
|
+
}
|
|
1806
|
+
const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
|
|
1807
|
+
await (input.sleep ?? Bun.sleep)(sleepMs);
|
|
1808
|
+
pendingElapsedMs += sleepMs;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1376
1811
|
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
1377
1812
|
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
1378
1813
|
}
|
|
1379
1814
|
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
1380
1815
|
await input.steerPi(finalGate.steeringPrompt);
|
|
1816
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1381
1817
|
continue;
|
|
1382
1818
|
}
|
|
1383
1819
|
if (isPendingOnlyGate(gate)) {
|
|
@@ -1397,6 +1833,7 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1397
1833
|
}
|
|
1398
1834
|
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
1399
1835
|
await input.steerPi(gate.steeringPrompt);
|
|
1836
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1400
1837
|
}
|
|
1401
1838
|
return { status: "needs_attention", prUrl, iterations: maxPrFixIterations, actionableFeedback: latestFeedback, merged: false };
|
|
1402
1839
|
}
|