@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
|
@@ -4352,13 +4352,7 @@ function stripHtml(input) {
|
|
|
4352
4352
|
}
|
|
4353
4353
|
function containsBlockerText(input) {
|
|
4354
4354
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
4355
|
-
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);
|
|
4356
|
-
}
|
|
4357
|
-
function containsGreptileNegativeVerdict(input) {
|
|
4358
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
4359
|
-
if (!text)
|
|
4360
|
-
return false;
|
|
4361
|
-
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);
|
|
4355
|
+
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);
|
|
4362
4356
|
}
|
|
4363
4357
|
function isStrictFiveOfFive(score) {
|
|
4364
4358
|
return score.value === 5 && score.scale === 5;
|
|
@@ -4366,6 +4360,189 @@ function isStrictFiveOfFive(score) {
|
|
|
4366
4360
|
function containsConflictingScoreText(input) {
|
|
4367
4361
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
4368
4362
|
}
|
|
4363
|
+
function greptileStatusVerdict(status) {
|
|
4364
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
4365
|
+
if (!normalized)
|
|
4366
|
+
return null;
|
|
4367
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
4368
|
+
return "approved";
|
|
4369
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
4370
|
+
return "rejected";
|
|
4371
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
4372
|
+
return "skipped";
|
|
4373
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
4374
|
+
return "failed";
|
|
4375
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
4376
|
+
return "pending";
|
|
4377
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
4378
|
+
return "completed";
|
|
4379
|
+
return null;
|
|
4380
|
+
}
|
|
4381
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
4382
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
4383
|
+
}
|
|
4384
|
+
function greptileRequestTimeoutMs(env) {
|
|
4385
|
+
const fallback = 30000;
|
|
4386
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
4387
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
4388
|
+
}
|
|
4389
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
4390
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4391
|
+
return null;
|
|
4392
|
+
const record = entry;
|
|
4393
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
4394
|
+
if (!id)
|
|
4395
|
+
return null;
|
|
4396
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
4397
|
+
return {
|
|
4398
|
+
id,
|
|
4399
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4400
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
4401
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4402
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
4406
|
+
const seen = new Set;
|
|
4407
|
+
const unique2 = [];
|
|
4408
|
+
for (const review of reviews) {
|
|
4409
|
+
if (seen.has(review.id))
|
|
4410
|
+
continue;
|
|
4411
|
+
seen.add(review.id);
|
|
4412
|
+
unique2.push(review);
|
|
4413
|
+
}
|
|
4414
|
+
return unique2;
|
|
4415
|
+
}
|
|
4416
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
4417
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
4418
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
4419
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
4420
|
+
const latest = sorted.slice(0, 1);
|
|
4421
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
4422
|
+
}
|
|
4423
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
4424
|
+
const selected = details ?? review;
|
|
4425
|
+
return {
|
|
4426
|
+
id: selected.id || review.id,
|
|
4427
|
+
body: selected.body ?? review.body ?? null,
|
|
4428
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
4429
|
+
status: selected.status ?? review.status ?? null
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
async function callGreptileMcpToolForGate(input) {
|
|
4433
|
+
const controller = new AbortController;
|
|
4434
|
+
const timeoutId = setTimeout(() => {
|
|
4435
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
4436
|
+
}, input.timeoutMs);
|
|
4437
|
+
let response;
|
|
4438
|
+
try {
|
|
4439
|
+
response = await input.fetchFn(input.apiBase, {
|
|
4440
|
+
method: "POST",
|
|
4441
|
+
headers: {
|
|
4442
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
4443
|
+
"Content-Type": "application/json"
|
|
4444
|
+
},
|
|
4445
|
+
body: JSON.stringify({
|
|
4446
|
+
jsonrpc: "2.0",
|
|
4447
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
4448
|
+
method: "tools/call",
|
|
4449
|
+
params: { name: input.name, arguments: input.args }
|
|
4450
|
+
}),
|
|
4451
|
+
signal: controller.signal
|
|
4452
|
+
});
|
|
4453
|
+
} catch (error) {
|
|
4454
|
+
if (controller.signal.aborted) {
|
|
4455
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
4456
|
+
}
|
|
4457
|
+
throw error;
|
|
4458
|
+
} finally {
|
|
4459
|
+
clearTimeout(timeoutId);
|
|
4460
|
+
}
|
|
4461
|
+
const raw = await response.text();
|
|
4462
|
+
if (!response.ok) {
|
|
4463
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
4464
|
+
}
|
|
4465
|
+
let envelope;
|
|
4466
|
+
try {
|
|
4467
|
+
envelope = JSON.parse(raw);
|
|
4468
|
+
} catch {
|
|
4469
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
4470
|
+
}
|
|
4471
|
+
if (envelope.error?.message) {
|
|
4472
|
+
throw new Error(envelope.error.message);
|
|
4473
|
+
}
|
|
4474
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
4475
|
+
`).trim();
|
|
4476
|
+
if (!text) {
|
|
4477
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
4478
|
+
}
|
|
4479
|
+
return text;
|
|
4480
|
+
}
|
|
4481
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
4482
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
4483
|
+
try {
|
|
4484
|
+
return JSON.parse(text);
|
|
4485
|
+
} catch {
|
|
4486
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
4490
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
4491
|
+
return { signals: [], errors: [] };
|
|
4492
|
+
}
|
|
4493
|
+
const env = input.options?.env ?? process.env;
|
|
4494
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
4495
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
4496
|
+
if (!apiKey) {
|
|
4497
|
+
return { signals: [], errors: [] };
|
|
4498
|
+
}
|
|
4499
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
4500
|
+
if (typeof fetchFn !== "function") {
|
|
4501
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
4502
|
+
}
|
|
4503
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
4504
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
4505
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
4506
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
4507
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
4508
|
+
try {
|
|
4509
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
4510
|
+
apiBase,
|
|
4511
|
+
apiKey,
|
|
4512
|
+
name: "list_code_reviews",
|
|
4513
|
+
args: {
|
|
4514
|
+
name: repository,
|
|
4515
|
+
remote,
|
|
4516
|
+
defaultBranch,
|
|
4517
|
+
prNumber: input.prNumber,
|
|
4518
|
+
limit: 20
|
|
4519
|
+
},
|
|
4520
|
+
timeoutMs,
|
|
4521
|
+
fetchFn
|
|
4522
|
+
});
|
|
4523
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4524
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4525
|
+
const signals = [];
|
|
4526
|
+
for (const review of selectedReviews) {
|
|
4527
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4528
|
+
apiBase,
|
|
4529
|
+
apiKey,
|
|
4530
|
+
name: "get_code_review",
|
|
4531
|
+
args: { codeReviewId: review.id },
|
|
4532
|
+
timeoutMs,
|
|
4533
|
+
fetchFn
|
|
4534
|
+
});
|
|
4535
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4536
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4537
|
+
}
|
|
4538
|
+
return { signals, errors: [] };
|
|
4539
|
+
} catch (error) {
|
|
4540
|
+
return {
|
|
4541
|
+
signals: [],
|
|
4542
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4369
4546
|
function firstString(record, keys) {
|
|
4370
4547
|
for (const key of keys) {
|
|
4371
4548
|
const value = record[key];
|
|
@@ -4492,7 +4669,7 @@ function normalizeReviewThread(entry) {
|
|
|
4492
4669
|
function relevantIssueComment(comment) {
|
|
4493
4670
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4494
4671
|
const body = comment.body ?? "";
|
|
4495
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4672
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4496
4673
|
}
|
|
4497
4674
|
function latestThreadComment(thread) {
|
|
4498
4675
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -4528,7 +4705,8 @@ function makeGreptileSignal(input) {
|
|
|
4528
4705
|
const scores = parseGreptileScores(input.body);
|
|
4529
4706
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4530
4707
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4531
|
-
const
|
|
4708
|
+
const verdict = input.verdict ?? null;
|
|
4709
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4532
4710
|
const explicitApproval = input.explicitApproval ?? false;
|
|
4533
4711
|
return {
|
|
4534
4712
|
source: input.source,
|
|
@@ -4540,6 +4718,7 @@ function makeGreptileSignal(input) {
|
|
|
4540
4718
|
score: scores[0] ?? null,
|
|
4541
4719
|
scores,
|
|
4542
4720
|
explicitApproval,
|
|
4721
|
+
verdict,
|
|
4543
4722
|
blocker,
|
|
4544
4723
|
actionable: input.actionable ?? blocker,
|
|
4545
4724
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -4562,9 +4741,9 @@ function collectGreptileSignals(evidence) {
|
|
|
4562
4741
|
for (const context of contextSources) {
|
|
4563
4742
|
if (!context.body.trim())
|
|
4564
4743
|
continue;
|
|
4565
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
4566
|
-
continue;
|
|
4567
4744
|
const contextBlocker = containsBlockerText(context.body);
|
|
4745
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4746
|
+
continue;
|
|
4568
4747
|
signals.push(makeGreptileSignal({
|
|
4569
4748
|
source: context.source,
|
|
4570
4749
|
body: context.body,
|
|
@@ -4577,16 +4756,16 @@ function collectGreptileSignals(evidence) {
|
|
|
4577
4756
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4578
4757
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4579
4758
|
|
|
4580
|
-
`);
|
|
4581
|
-
|
|
4582
|
-
continue;
|
|
4759
|
+
`) || "Status: UNKNOWN";
|
|
4760
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4583
4761
|
signals.push(makeGreptileSignal({
|
|
4584
4762
|
source: "api",
|
|
4585
4763
|
body,
|
|
4586
4764
|
currentHeadSha: evidence.currentHeadSha,
|
|
4587
4765
|
trusted: true,
|
|
4588
4766
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4589
|
-
explicitApproval:
|
|
4767
|
+
explicitApproval: verdict === "approved",
|
|
4768
|
+
verdict
|
|
4590
4769
|
}));
|
|
4591
4770
|
}
|
|
4592
4771
|
for (const review of evidence.reviews) {
|
|
@@ -4611,20 +4790,6 @@ function collectGreptileSignals(evidence) {
|
|
|
4611
4790
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4612
4791
|
}));
|
|
4613
4792
|
}
|
|
4614
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
4615
|
-
const login = commentAuthorLogin(comment);
|
|
4616
|
-
const body = comment.body ?? "";
|
|
4617
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4618
|
-
continue;
|
|
4619
|
-
signals.push(makeGreptileSignal({
|
|
4620
|
-
source: "changed-file-comment",
|
|
4621
|
-
body,
|
|
4622
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
4623
|
-
trusted: true,
|
|
4624
|
-
authorLogin: login,
|
|
4625
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
4626
|
-
}));
|
|
4627
|
-
}
|
|
4628
4793
|
for (const comment of evidence.relevantIssueComments) {
|
|
4629
4794
|
const login = commentAuthorLogin(comment);
|
|
4630
4795
|
const body = comment.body ?? "";
|
|
@@ -4690,6 +4855,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
4690
4855
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4691
4856
|
});
|
|
4692
4857
|
}
|
|
4858
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4859
|
+
return [];
|
|
4860
|
+
}
|
|
4693
4861
|
function issueLevelBlockerSummaries(comments) {
|
|
4694
4862
|
return comments.flatMap((comment) => {
|
|
4695
4863
|
const body = comment.body?.trim() ?? "";
|
|
@@ -4729,14 +4897,21 @@ function deriveGreptileEvidence(input) {
|
|
|
4729
4897
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4730
4898
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4731
4899
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4732
|
-
const
|
|
4900
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4901
|
+
const signalCanApproveByScore = (signal) => {
|
|
4902
|
+
if (signal.source === "api")
|
|
4903
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4904
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4905
|
+
};
|
|
4906
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4907
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4733
4908
|
const approvedByScore = !!approvingScoreEntry;
|
|
4734
|
-
const approvedByExplicitMapping =
|
|
4735
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4909
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4910
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4736
4911
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4737
4912
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4738
4913
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4739
|
-
const blockerSignals = signals.filter((signal) =>
|
|
4914
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4740
4915
|
const staleBlockingSignals = [];
|
|
4741
4916
|
const blockers = [
|
|
4742
4917
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -4747,7 +4922,8 @@ function deriveGreptileEvidence(input) {
|
|
|
4747
4922
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4748
4923
|
];
|
|
4749
4924
|
const unresolvedComments = [
|
|
4750
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4925
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4926
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4751
4927
|
];
|
|
4752
4928
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4753
4929
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -4760,13 +4936,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4760
4936
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4761
4937
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4762
4938
|
});
|
|
4939
|
+
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"));
|
|
4763
4940
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4764
4941
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4765
4942
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4766
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4943
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4767
4944
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4768
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4769
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4945
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4946
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4770
4947
|
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";
|
|
4771
4948
|
return {
|
|
4772
4949
|
source,
|
|
@@ -4873,6 +5050,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4873
5050
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4874
5051
|
}
|
|
4875
5052
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
5053
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4876
5054
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4877
5055
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4878
5056
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4910,6 +5088,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
4910
5088
|
}
|
|
4911
5089
|
}
|
|
4912
5090
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
5091
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
5092
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
5093
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
5094
|
+
options: input.greptileApi,
|
|
5095
|
+
repoName: parsed.repoName,
|
|
5096
|
+
prNumber: parsed.prNumber,
|
|
5097
|
+
headSha,
|
|
5098
|
+
baseRefName
|
|
5099
|
+
});
|
|
5100
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
5101
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4913
5102
|
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})` : ""}`);
|
|
4914
5103
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4915
5104
|
const evidenceBase = {
|
|
@@ -4921,7 +5110,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4921
5110
|
reviewThreads,
|
|
4922
5111
|
checks: checksWithGreptileDetails,
|
|
4923
5112
|
currentHeadSha: headSha,
|
|
4924
|
-
apiSignals
|
|
5113
|
+
apiSignals
|
|
4925
5114
|
};
|
|
4926
5115
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4927
5116
|
return {
|
|
@@ -4932,7 +5121,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4932
5121
|
body: evidenceBase.body,
|
|
4933
5122
|
headSha,
|
|
4934
5123
|
headRefName: firstString(view, ["headRefName"]),
|
|
4935
|
-
baseRefName
|
|
5124
|
+
baseRefName,
|
|
4936
5125
|
state: firstString(view, ["state"]),
|
|
4937
5126
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4938
5127
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4949,71 +5138,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
4949
5138
|
greptile
|
|
4950
5139
|
};
|
|
4951
5140
|
}
|
|
5141
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
5142
|
+
const normalized = value.trim();
|
|
5143
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
5144
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
5145
|
+
}
|
|
4952
5146
|
function evaluateEvidence(evidence) {
|
|
4953
|
-
const
|
|
5147
|
+
const reasonDetails = [];
|
|
4954
5148
|
const warnings = [];
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5149
|
+
const seen = new Set;
|
|
5150
|
+
const addReason = (reason) => {
|
|
5151
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
5152
|
+
const key = `${capped.code}:${capped.message}`;
|
|
5153
|
+
if (seen.has(key))
|
|
5154
|
+
return;
|
|
5155
|
+
seen.add(key);
|
|
5156
|
+
reasonDetails.push(capped);
|
|
5157
|
+
};
|
|
5158
|
+
const greptile = evidence.greptile;
|
|
5159
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
5160
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
5161
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
5162
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
5163
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
5164
|
+
for (const error of evidence.readErrors) {
|
|
5165
|
+
addReason({
|
|
5166
|
+
code: "read_error",
|
|
5167
|
+
reasonClass: "reject",
|
|
5168
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
5169
|
+
suggestedAction: "needs_attention",
|
|
5170
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
5171
|
+
headSha: evidence.headSha || null
|
|
5172
|
+
});
|
|
5173
|
+
}
|
|
5174
|
+
if (!evidence.headSha) {
|
|
5175
|
+
addReason({
|
|
5176
|
+
code: "missing_head_sha",
|
|
5177
|
+
reasonClass: "reject",
|
|
5178
|
+
surface: "github",
|
|
5179
|
+
suggestedAction: "needs_attention",
|
|
5180
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
5181
|
+
headSha: null
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
for (const failure of evidence.checkFailures) {
|
|
5185
|
+
addReason({
|
|
5186
|
+
code: "ci_failed",
|
|
5187
|
+
reasonClass: "reject",
|
|
5188
|
+
surface: "ci",
|
|
5189
|
+
suggestedAction: "fix",
|
|
5190
|
+
message: failure,
|
|
5191
|
+
headSha: evidence.headSha || null
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
5195
|
+
addReason({
|
|
5196
|
+
code: "check_pending",
|
|
5197
|
+
reasonClass: "pending",
|
|
5198
|
+
surface: "ci",
|
|
5199
|
+
suggestedAction: "wait",
|
|
5200
|
+
message: pendingCheck,
|
|
5201
|
+
headSha: evidence.headSha || null
|
|
5202
|
+
});
|
|
4966
5203
|
}
|
|
4967
5204
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4968
5205
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4969
|
-
|
|
5206
|
+
addReason({
|
|
5207
|
+
code: "review_decision_blocking",
|
|
5208
|
+
reasonClass: "reject",
|
|
5209
|
+
surface: "review",
|
|
5210
|
+
suggestedAction: "fix",
|
|
5211
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
5212
|
+
headSha: evidence.headSha || null
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5215
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
5216
|
+
addReason({
|
|
5217
|
+
code: "review_thread_unresolved",
|
|
5218
|
+
reasonClass: "reject",
|
|
5219
|
+
surface: "review",
|
|
5220
|
+
suggestedAction: "fix",
|
|
5221
|
+
message: thread,
|
|
5222
|
+
headSha: evidence.headSha || null
|
|
5223
|
+
});
|
|
5224
|
+
}
|
|
5225
|
+
if (greptile.mapping === "missing") {
|
|
5226
|
+
addReason({
|
|
5227
|
+
code: "greptile_missing",
|
|
5228
|
+
reasonClass: "pending",
|
|
5229
|
+
surface: "greptile",
|
|
5230
|
+
suggestedAction: "wait",
|
|
5231
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
5232
|
+
headSha: evidence.headSha || null
|
|
5233
|
+
});
|
|
4970
5234
|
}
|
|
4971
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4972
|
-
if (unresolvedThreads.length > 0)
|
|
4973
|
-
reasons.push(...unresolvedThreads);
|
|
4974
|
-
const greptile = evidence.greptile;
|
|
4975
|
-
if (greptile.mapping === "missing")
|
|
4976
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4977
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4978
5235
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4979
|
-
|
|
5236
|
+
addReason({
|
|
5237
|
+
code: "greptile_stale",
|
|
5238
|
+
reasonClass: "pending",
|
|
5239
|
+
surface: "greptile",
|
|
5240
|
+
suggestedAction: "wait",
|
|
5241
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
5242
|
+
headSha: evidence.headSha || null,
|
|
5243
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5246
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
5247
|
+
addReason({
|
|
5248
|
+
code: "greptile_pending",
|
|
5249
|
+
reasonClass: "pending",
|
|
5250
|
+
surface: "greptile",
|
|
5251
|
+
suggestedAction: "wait",
|
|
5252
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
5253
|
+
headSha: evidence.headSha || null,
|
|
5254
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
5258
|
+
addReason({
|
|
5259
|
+
code: "greptile_api_status_unknown",
|
|
5260
|
+
reasonClass: "reject",
|
|
5261
|
+
surface: "greptile",
|
|
5262
|
+
suggestedAction: "needs_attention",
|
|
5263
|
+
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}` : "."}`,
|
|
5264
|
+
headSha: evidence.headSha || null,
|
|
5265
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
5266
|
+
});
|
|
4980
5267
|
}
|
|
4981
5268
|
if (!greptile.completed) {
|
|
4982
|
-
|
|
4983
|
-
|
|
5269
|
+
addReason({
|
|
5270
|
+
code: "greptile_pending",
|
|
5271
|
+
reasonClass: "pending",
|
|
5272
|
+
surface: "greptile",
|
|
5273
|
+
suggestedAction: "wait",
|
|
5274
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
5275
|
+
headSha: evidence.headSha || null,
|
|
5276
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5277
|
+
});
|
|
5278
|
+
}
|
|
5279
|
+
if (!greptile.fresh) {
|
|
5280
|
+
addReason({
|
|
5281
|
+
code: "greptile_not_current_head",
|
|
5282
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5283
|
+
surface: "greptile",
|
|
5284
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5285
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
5286
|
+
headSha: evidence.headSha || null,
|
|
5287
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5288
|
+
});
|
|
4984
5289
|
}
|
|
4985
|
-
if (!greptile.fresh)
|
|
4986
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4987
5290
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4988
|
-
|
|
5291
|
+
addReason({
|
|
5292
|
+
code: "greptile_score_not_5",
|
|
5293
|
+
reasonClass: "reject",
|
|
5294
|
+
surface: "greptile",
|
|
5295
|
+
suggestedAction: "fix",
|
|
5296
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
5297
|
+
headSha: evidence.headSha || null,
|
|
5298
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5299
|
+
});
|
|
4989
5300
|
}
|
|
4990
|
-
|
|
4991
|
-
|
|
5301
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
5302
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
5303
|
+
addReason({
|
|
5304
|
+
code: "greptile_score_missing",
|
|
5305
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5306
|
+
surface: "greptile",
|
|
5307
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5308
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
5309
|
+
headSha: evidence.headSha || null,
|
|
5310
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5311
|
+
});
|
|
4992
5312
|
}
|
|
4993
5313
|
if (greptile.mapping === "unproven") {
|
|
4994
|
-
|
|
5314
|
+
addReason({
|
|
5315
|
+
code: "greptile_mapping_unproven",
|
|
5316
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5317
|
+
surface: "greptile",
|
|
5318
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5319
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
5320
|
+
headSha: evidence.headSha || null,
|
|
5321
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
for (const blocker of greptile.blockers) {
|
|
5325
|
+
addReason({
|
|
5326
|
+
code: "greptile_blocker_text",
|
|
5327
|
+
reasonClass: "reject",
|
|
5328
|
+
surface: "greptile",
|
|
5329
|
+
suggestedAction: "fix",
|
|
5330
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
5331
|
+
headSha: evidence.headSha || null,
|
|
5332
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5333
|
+
});
|
|
4995
5334
|
}
|
|
4996
|
-
|
|
4997
|
-
|
|
5335
|
+
for (const comment of greptile.unresolvedComments) {
|
|
5336
|
+
addReason({
|
|
5337
|
+
code: "greptile_unresolved_comment",
|
|
5338
|
+
reasonClass: "reject",
|
|
5339
|
+
surface: "greptile",
|
|
5340
|
+
suggestedAction: "fix",
|
|
5341
|
+
message: comment,
|
|
5342
|
+
headSha: evidence.headSha || null,
|
|
5343
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5344
|
+
});
|
|
4998
5345
|
}
|
|
4999
|
-
if (greptile.unresolvedComments.length > 0)
|
|
5000
|
-
reasons.push(...greptile.unresolvedComments);
|
|
5001
5346
|
if (!greptile.approved)
|
|
5002
5347
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
5003
|
-
|
|
5348
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
5349
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
5004
5350
|
}
|
|
5005
5351
|
function evaluateStrictPrMergeGate(evidence) {
|
|
5006
5352
|
const evaluated = evaluateEvidence(evidence);
|
|
5007
|
-
const approved = evaluated.
|
|
5353
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
5008
5354
|
return {
|
|
5009
5355
|
approved,
|
|
5010
5356
|
pending: evaluated.pending,
|
|
5011
5357
|
reasons: evaluated.reasons,
|
|
5358
|
+
reasonDetails: evaluated.reasonDetails,
|
|
5012
5359
|
warnings: evaluated.warnings,
|
|
5013
5360
|
actionableFeedback: evaluated.reasons,
|
|
5014
5361
|
evidence
|
|
5015
5362
|
};
|
|
5016
5363
|
}
|
|
5364
|
+
function strictMergeHeadShaFromGate(result, prUrl) {
|
|
5365
|
+
if (!result.approved) {
|
|
5366
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
|
|
5367
|
+
}
|
|
5368
|
+
if (result.evidence.prUrl !== prUrl) {
|
|
5369
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
|
|
5370
|
+
}
|
|
5371
|
+
const headSha = result.evidence.headSha?.trim();
|
|
5372
|
+
if (!headSha) {
|
|
5373
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
5374
|
+
}
|
|
5375
|
+
if (!/^[0-9a-f]{40}$/i.test(headSha)) {
|
|
5376
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
|
|
5377
|
+
}
|
|
5378
|
+
if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
|
|
5379
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
|
|
5380
|
+
}
|
|
5381
|
+
if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
|
|
5382
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
|
|
5383
|
+
}
|
|
5384
|
+
return headSha;
|
|
5385
|
+
}
|
|
5017
5386
|
function promptExcerpt(value, maxChars = 4000) {
|
|
5018
5387
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
5019
5388
|
|
|
@@ -5025,6 +5394,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
5025
5394
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
5026
5395
|
const evidence = result.evidence;
|
|
5027
5396
|
const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
|
|
5397
|
+
const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
|
|
5398
|
+
if (result.reasons.length > displayedReasons.length) {
|
|
5399
|
+
displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
|
|
5400
|
+
}
|
|
5028
5401
|
const lines = [
|
|
5029
5402
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
5030
5403
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -5033,10 +5406,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
5033
5406
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
5034
5407
|
"",
|
|
5035
5408
|
"Gate reasons:",
|
|
5036
|
-
...
|
|
5409
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
5410
|
+
"",
|
|
5411
|
+
"Structured gate reason details:",
|
|
5412
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
5037
5413
|
"",
|
|
5038
5414
|
"Required evidence read status:",
|
|
5039
|
-
evidence.readErrors.length ?
|
|
5415
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
5040
5416
|
"",
|
|
5041
5417
|
"Full PR title:",
|
|
5042
5418
|
evidence.title || "(empty)",
|
|
@@ -5100,6 +5476,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
5100
5476
|
approved: input.result.approved,
|
|
5101
5477
|
pending: input.result.pending,
|
|
5102
5478
|
reasons: input.result.reasons,
|
|
5479
|
+
reasonDetails: input.result.reasonDetails,
|
|
5103
5480
|
warnings: input.result.warnings,
|
|
5104
5481
|
actionableFeedback: input.result.actionableFeedback,
|
|
5105
5482
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -5934,7 +6311,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5934
6311
|
};
|
|
5935
6312
|
}
|
|
5936
6313
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5937
|
-
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(blockerScanBody)) {
|
|
6314
|
+
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
5938
6315
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5939
6316
|
return {
|
|
5940
6317
|
verdict: "REJECT",
|
|
@@ -6016,6 +6393,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
6016
6393
|
approved: strictGate.approved,
|
|
6017
6394
|
pending: strictGate.pending,
|
|
6018
6395
|
reasons: strictGate.reasons,
|
|
6396
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6019
6397
|
warnings: strictGate.warnings,
|
|
6020
6398
|
greptile: strictGate.evidence.greptile,
|
|
6021
6399
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -6038,6 +6416,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
6038
6416
|
approved: strictGate.approved,
|
|
6039
6417
|
pending: strictGate.pending,
|
|
6040
6418
|
reasons: strictGate.reasons,
|
|
6419
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6041
6420
|
warnings: strictGate.warnings,
|
|
6042
6421
|
greptile: strictGate.evidence.greptile,
|
|
6043
6422
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -6164,6 +6543,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
6164
6543
|
approved: strictGate.approved,
|
|
6165
6544
|
pending: strictGate.pending,
|
|
6166
6545
|
reasons: strictGate.reasons,
|
|
6546
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6167
6547
|
warnings: strictGate.warnings,
|
|
6168
6548
|
greptile: strictGate.evidence.greptile
|
|
6169
6549
|
},
|
|
@@ -6186,6 +6566,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
6186
6566
|
approved: strictGate.approved,
|
|
6187
6567
|
pending: strictGate.pending,
|
|
6188
6568
|
reasons: strictGate.reasons,
|
|
6569
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6189
6570
|
warnings: strictGate.warnings,
|
|
6190
6571
|
greptile: strictGate.evidence.greptile
|
|
6191
6572
|
},
|
|
@@ -6307,8 +6688,7 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
6307
6688
|
if (options.githubCheckState.completed) {
|
|
6308
6689
|
return false;
|
|
6309
6690
|
}
|
|
6310
|
-
|
|
6311
|
-
if (!hasRemainingBudget) {
|
|
6691
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6312
6692
|
return false;
|
|
6313
6693
|
}
|
|
6314
6694
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
@@ -6318,8 +6698,11 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
6318
6698
|
}
|
|
6319
6699
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
6320
6700
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6701
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6702
|
+
return false;
|
|
6703
|
+
}
|
|
6321
6704
|
if (waitingForVisiblePendingReview) {
|
|
6322
|
-
return
|
|
6705
|
+
return true;
|
|
6323
6706
|
}
|
|
6324
6707
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
6325
6708
|
if (reviewNotVisibleYet) {
|
|
@@ -7053,7 +7436,7 @@ function gitOpenPr(options) {
|
|
|
7053
7436
|
"",
|
|
7054
7437
|
"## Review",
|
|
7055
7438
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
7056
|
-
"- When repository policy allows it, Rig
|
|
7439
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
7057
7440
|
].join(`
|
|
7058
7441
|
`);
|
|
7059
7442
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -7249,15 +7632,12 @@ function gitMergePr(options) {
|
|
|
7249
7632
|
return { status: "already-merged", url: options.pr.url };
|
|
7250
7633
|
}
|
|
7251
7634
|
if (state !== "OPEN") {
|
|
7252
|
-
throw new Error(`Cannot
|
|
7635
|
+
throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
|
|
7253
7636
|
}
|
|
7254
7637
|
if (isDraft) {
|
|
7255
|
-
throw new Error(`Cannot
|
|
7256
|
-
}
|
|
7257
|
-
const strictGateHeadSha = options.strictGateHeadSha?.trim();
|
|
7258
|
-
if (!strictGateHeadSha) {
|
|
7259
|
-
throw new Error(`Refusing to merge PR ${options.pr.url}: strict merge gate did not provide a current head SHA.`);
|
|
7638
|
+
throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
|
|
7260
7639
|
}
|
|
7640
|
+
const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
|
|
7261
7641
|
const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
|
|
7262
7642
|
const method = options.method || "squash";
|
|
7263
7643
|
mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
|
|
@@ -8248,7 +8628,6 @@ async function main() {
|
|
|
8248
8628
|
if (prs.length === 0) {
|
|
8249
8629
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
8250
8630
|
} else {
|
|
8251
|
-
let mergePending = false;
|
|
8252
8631
|
let cycle = 0;
|
|
8253
8632
|
for (const pr of prs) {
|
|
8254
8633
|
cycle += 1;
|
|
@@ -8277,16 +8656,13 @@ async function main() {
|
|
|
8277
8656
|
pr,
|
|
8278
8657
|
method: "squash",
|
|
8279
8658
|
deleteBranch: true,
|
|
8280
|
-
|
|
8659
|
+
strictGate: gate
|
|
8281
8660
|
});
|
|
8282
|
-
if (mergeResult.status === "
|
|
8283
|
-
|
|
8284
|
-
console.log(`WAIT: Auto-merge enabled but PR is still open (${pr.repoLabel}): ${pr.url}`);
|
|
8661
|
+
if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
|
|
8662
|
+
console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
|
|
8285
8663
|
}
|
|
8286
8664
|
}
|
|
8287
|
-
if (
|
|
8288
|
-
failed = true;
|
|
8289
|
-
} else if (!failed) {
|
|
8665
|
+
if (!failed) {
|
|
8290
8666
|
sourceCloseoutAllowed = true;
|
|
8291
8667
|
console.log("OK: Auto-merge complete");
|
|
8292
8668
|
}
|