@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" };
|
|
@@ -106,13 +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);
|
|
110
|
-
}
|
|
111
|
-
function containsGreptileNegativeVerdict(input) {
|
|
112
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
113
|
-
if (!text)
|
|
114
|
-
return false;
|
|
115
|
-
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);
|
|
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);
|
|
116
155
|
}
|
|
117
156
|
function isStrictFiveOfFive(score) {
|
|
118
157
|
return score.value === 5 && score.scale === 5;
|
|
@@ -120,6 +159,189 @@ function isStrictFiveOfFive(score) {
|
|
|
120
159
|
function containsConflictingScoreText(input) {
|
|
121
160
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
122
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
|
+
}
|
|
123
345
|
function firstString(record, keys) {
|
|
124
346
|
for (const key of keys) {
|
|
125
347
|
const value = record[key];
|
|
@@ -246,7 +468,7 @@ function normalizeReviewThread(entry) {
|
|
|
246
468
|
function relevantIssueComment(comment) {
|
|
247
469
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
248
470
|
const body = comment.body ?? "";
|
|
249
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
471
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
250
472
|
}
|
|
251
473
|
function latestThreadComment(thread) {
|
|
252
474
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -282,7 +504,8 @@ function makeGreptileSignal(input) {
|
|
|
282
504
|
const scores = parseGreptileScores(input.body);
|
|
283
505
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
284
506
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
285
|
-
const
|
|
507
|
+
const verdict = input.verdict ?? null;
|
|
508
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
286
509
|
const explicitApproval = input.explicitApproval ?? false;
|
|
287
510
|
return {
|
|
288
511
|
source: input.source,
|
|
@@ -294,6 +517,7 @@ function makeGreptileSignal(input) {
|
|
|
294
517
|
score: scores[0] ?? null,
|
|
295
518
|
scores,
|
|
296
519
|
explicitApproval,
|
|
520
|
+
verdict,
|
|
297
521
|
blocker,
|
|
298
522
|
actionable: input.actionable ?? blocker,
|
|
299
523
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -316,9 +540,9 @@ function collectGreptileSignals(evidence) {
|
|
|
316
540
|
for (const context of contextSources) {
|
|
317
541
|
if (!context.body.trim())
|
|
318
542
|
continue;
|
|
319
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
320
|
-
continue;
|
|
321
543
|
const contextBlocker = containsBlockerText(context.body);
|
|
544
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
545
|
+
continue;
|
|
322
546
|
signals.push(makeGreptileSignal({
|
|
323
547
|
source: context.source,
|
|
324
548
|
body: context.body,
|
|
@@ -331,16 +555,16 @@ function collectGreptileSignals(evidence) {
|
|
|
331
555
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
332
556
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
333
557
|
|
|
334
|
-
`);
|
|
335
|
-
|
|
336
|
-
continue;
|
|
558
|
+
`) || "Status: UNKNOWN";
|
|
559
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
337
560
|
signals.push(makeGreptileSignal({
|
|
338
561
|
source: "api",
|
|
339
562
|
body,
|
|
340
563
|
currentHeadSha: evidence.currentHeadSha,
|
|
341
564
|
trusted: true,
|
|
342
565
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
343
|
-
explicitApproval:
|
|
566
|
+
explicitApproval: verdict === "approved",
|
|
567
|
+
verdict
|
|
344
568
|
}));
|
|
345
569
|
}
|
|
346
570
|
for (const review of evidence.reviews) {
|
|
@@ -365,20 +589,6 @@ function collectGreptileSignals(evidence) {
|
|
|
365
589
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
366
590
|
}));
|
|
367
591
|
}
|
|
368
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
369
|
-
const login = commentAuthorLogin(comment);
|
|
370
|
-
const body = comment.body ?? "";
|
|
371
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
372
|
-
continue;
|
|
373
|
-
signals.push(makeGreptileSignal({
|
|
374
|
-
source: "changed-file-comment",
|
|
375
|
-
body,
|
|
376
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
377
|
-
trusted: true,
|
|
378
|
-
authorLogin: login,
|
|
379
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
380
|
-
}));
|
|
381
|
-
}
|
|
382
592
|
for (const comment of evidence.relevantIssueComments) {
|
|
383
593
|
const login = commentAuthorLogin(comment);
|
|
384
594
|
const body = comment.body ?? "";
|
|
@@ -444,6 +654,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
444
654
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
445
655
|
});
|
|
446
656
|
}
|
|
657
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
447
660
|
function issueLevelBlockerSummaries(comments) {
|
|
448
661
|
return comments.flatMap((comment) => {
|
|
449
662
|
const body = comment.body?.trim() ?? "";
|
|
@@ -483,14 +696,21 @@ function deriveGreptileEvidence(input) {
|
|
|
483
696
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
484
697
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
485
698
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
486
|
-
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;
|
|
487
707
|
const approvedByScore = !!approvingScoreEntry;
|
|
488
|
-
const approvedByExplicitMapping =
|
|
489
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
708
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
709
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
490
710
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
491
711
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
492
712
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
493
|
-
const blockerSignals = signals.filter((signal) =>
|
|
713
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
494
714
|
const staleBlockingSignals = [];
|
|
495
715
|
const blockers = [
|
|
496
716
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -501,7 +721,8 @@ function deriveGreptileEvidence(input) {
|
|
|
501
721
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
502
722
|
];
|
|
503
723
|
const unresolvedComments = [
|
|
504
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
724
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
725
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
505
726
|
];
|
|
506
727
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
507
728
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -514,13 +735,14 @@ function deriveGreptileEvidence(input) {
|
|
|
514
735
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
515
736
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
516
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"));
|
|
517
739
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
518
740
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
519
741
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
520
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
742
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
521
743
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
522
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
523
|
-
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";
|
|
524
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";
|
|
525
747
|
return {
|
|
526
748
|
source,
|
|
@@ -627,6 +849,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
627
849
|
readErrors.push("gh pr view did not return required reviews array");
|
|
628
850
|
}
|
|
629
851
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
852
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
630
853
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
631
854
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
632
855
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -664,6 +887,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
664
887
|
}
|
|
665
888
|
}
|
|
666
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];
|
|
667
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})` : ""}`);
|
|
668
902
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
669
903
|
const evidenceBase = {
|
|
@@ -675,7 +909,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
675
909
|
reviewThreads,
|
|
676
910
|
checks: checksWithGreptileDetails,
|
|
677
911
|
currentHeadSha: headSha,
|
|
678
|
-
apiSignals
|
|
912
|
+
apiSignals
|
|
679
913
|
};
|
|
680
914
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
681
915
|
return {
|
|
@@ -686,7 +920,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
686
920
|
body: evidenceBase.body,
|
|
687
921
|
headSha,
|
|
688
922
|
headRefName: firstString(view, ["headRefName"]),
|
|
689
|
-
baseRefName
|
|
923
|
+
baseRefName,
|
|
690
924
|
state: firstString(view, ["state"]),
|
|
691
925
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
692
926
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -703,71 +937,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
703
937
|
greptile
|
|
704
938
|
};
|
|
705
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
|
+
}
|
|
706
945
|
function evaluateEvidence(evidence) {
|
|
707
|
-
const
|
|
946
|
+
const reasonDetails = [];
|
|
708
947
|
const warnings = [];
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
+
});
|
|
720
1002
|
}
|
|
721
1003
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
722
1004
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
723
|
-
|
|
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
|
+
});
|
|
724
1033
|
}
|
|
725
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
726
|
-
if (unresolvedThreads.length > 0)
|
|
727
|
-
reasons.push(...unresolvedThreads);
|
|
728
|
-
const greptile = evidence.greptile;
|
|
729
|
-
if (greptile.mapping === "missing")
|
|
730
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
731
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
732
1034
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
733
|
-
|
|
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
|
+
});
|
|
734
1066
|
}
|
|
735
1067
|
if (!greptile.completed) {
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
});
|
|
738
1088
|
}
|
|
739
|
-
if (!greptile.fresh)
|
|
740
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
741
1089
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
742
|
-
|
|
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
|
+
});
|
|
743
1099
|
}
|
|
744
|
-
|
|
745
|
-
|
|
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
|
+
});
|
|
746
1111
|
}
|
|
747
1112
|
if (greptile.mapping === "unproven") {
|
|
748
|
-
|
|
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
|
+
});
|
|
749
1122
|
}
|
|
750
|
-
|
|
751
|
-
|
|
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
|
+
});
|
|
1133
|
+
}
|
|
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
|
+
});
|
|
752
1144
|
}
|
|
753
|
-
if (greptile.unresolvedComments.length > 0)
|
|
754
|
-
reasons.push(...greptile.unresolvedComments);
|
|
755
1145
|
if (!greptile.approved)
|
|
756
1146
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
757
|
-
|
|
1147
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
1148
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
758
1149
|
}
|
|
759
1150
|
function evaluateStrictPrMergeGate(evidence) {
|
|
760
1151
|
const evaluated = evaluateEvidence(evidence);
|
|
761
|
-
const approved = evaluated.
|
|
1152
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
762
1153
|
return {
|
|
763
1154
|
approved,
|
|
764
1155
|
pending: evaluated.pending,
|
|
765
1156
|
reasons: evaluated.reasons,
|
|
1157
|
+
reasonDetails: evaluated.reasonDetails,
|
|
766
1158
|
warnings: evaluated.warnings,
|
|
767
1159
|
actionableFeedback: evaluated.reasons,
|
|
768
1160
|
evidence
|
|
769
1161
|
};
|
|
770
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
|
+
}
|
|
771
1185
|
function promptExcerpt(value, maxChars = 4000) {
|
|
772
1186
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
773
1187
|
|
|
@@ -779,6 +1193,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
779
1193
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
780
1194
|
const evidence = result.evidence;
|
|
781
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
|
+
}
|
|
782
1200
|
const lines = [
|
|
783
1201
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
784
1202
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -787,10 +1205,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
787
1205
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
788
1206
|
"",
|
|
789
1207
|
"Gate reasons:",
|
|
790
|
-
...
|
|
1208
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
1209
|
+
"",
|
|
1210
|
+
"Structured gate reason details:",
|
|
1211
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
791
1212
|
"",
|
|
792
1213
|
"Required evidence read status:",
|
|
793
|
-
evidence.readErrors.length ?
|
|
1214
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
794
1215
|
"",
|
|
795
1216
|
"Full PR title:",
|
|
796
1217
|
evidence.title || "(empty)",
|
|
@@ -854,6 +1275,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
854
1275
|
approved: input.result.approved,
|
|
855
1276
|
pending: input.result.pending,
|
|
856
1277
|
reasons: input.result.reasons,
|
|
1278
|
+
reasonDetails: input.result.reasonDetails,
|
|
857
1279
|
warnings: input.result.warnings,
|
|
858
1280
|
actionableFeedback: input.result.actionableFeedback,
|
|
859
1281
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -1227,10 +1649,7 @@ async function runRepoDefaultMerge(input) {
|
|
|
1227
1649
|
const merge = input.config?.merge ?? {};
|
|
1228
1650
|
if (merge.mode === "off")
|
|
1229
1651
|
return;
|
|
1230
|
-
const matchHeadSha = input.
|
|
1231
|
-
if (!matchHeadSha) {
|
|
1232
|
-
throw new Error(`Refusing to merge ${input.prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
1233
|
-
}
|
|
1652
|
+
const matchHeadSha = strictMergeHeadShaFromGate(input.strictGate, input.prUrl);
|
|
1234
1653
|
const method = merge.method ?? "repo-default";
|
|
1235
1654
|
const args = ["pr", "merge", input.prUrl];
|
|
1236
1655
|
if (method === "repo-default") {
|
|
@@ -1252,17 +1671,17 @@ function shouldAttemptRigMerge(config) {
|
|
|
1252
1671
|
return mode !== "off" && mode !== "pr-ready";
|
|
1253
1672
|
}
|
|
1254
1673
|
function isPendingOnlyGate(result) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1259
|
-
return
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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");
|
|
1266
1685
|
}
|
|
1267
1686
|
async function runPrAutomation(input) {
|
|
1268
1687
|
const prConfig = input.config?.pr ?? {};
|
|
@@ -1342,6 +1761,7 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1342
1761
|
...latestFeedback.map((entry) => `- ${entry}`)
|
|
1343
1762
|
].join(`
|
|
1344
1763
|
`));
|
|
1764
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1345
1765
|
continue;
|
|
1346
1766
|
}
|
|
1347
1767
|
const gate = await runStrictPrMergeGate({
|
|
@@ -1352,7 +1772,8 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1352
1772
|
cycle: iteration,
|
|
1353
1773
|
command: input.command,
|
|
1354
1774
|
artifactRoot: input.artifactRoot,
|
|
1355
|
-
allowedFailures: input.config?.merge?.allowedFailures ?? []
|
|
1775
|
+
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
1776
|
+
greptileApi: input.greptileApi
|
|
1356
1777
|
});
|
|
1357
1778
|
latestFeedback = [...gate.actionableFeedback];
|
|
1358
1779
|
if (gate.approved) {
|
|
@@ -1366,20 +1787,33 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1366
1787
|
command: input.command,
|
|
1367
1788
|
artifactRoot: input.artifactRoot,
|
|
1368
1789
|
allowedFailures: input.config?.merge?.allowedFailures ?? [],
|
|
1790
|
+
greptileApi: input.greptileApi,
|
|
1369
1791
|
final: true
|
|
1370
1792
|
});
|
|
1371
1793
|
if (finalGate.approved) {
|
|
1372
1794
|
await input.lifecycle?.onMergeStarted?.({ prUrl });
|
|
1373
|
-
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 });
|
|
1374
1796
|
await input.lifecycle?.onMerged?.({ prUrl });
|
|
1375
1797
|
return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
|
|
1376
1798
|
}
|
|
1377
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
|
+
}
|
|
1378
1811
|
if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
|
|
1379
1812
|
return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
|
|
1380
1813
|
}
|
|
1381
1814
|
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
1382
1815
|
await input.steerPi(finalGate.steeringPrompt);
|
|
1816
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1383
1817
|
continue;
|
|
1384
1818
|
}
|
|
1385
1819
|
if (isPendingOnlyGate(gate)) {
|
|
@@ -1399,6 +1833,7 @@ ${createResult.stdout ?? ""}`) : null;
|
|
|
1399
1833
|
}
|
|
1400
1834
|
await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
|
|
1401
1835
|
await input.steerPi(gate.steeringPrompt);
|
|
1836
|
+
await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
|
|
1402
1837
|
}
|
|
1403
1838
|
return { status: "needs_attention", prUrl, iterations: maxPrFixIterations, actionableFeedback: latestFeedback, merged: false };
|
|
1404
1839
|
}
|