@h-rig/runtime 0.0.6-alpha.13 → 0.0.6-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig-agent.js +1 -1
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +430 -71
- package/dist/src/control-plane/hooks/completion-verification.js +469 -87
- package/dist/src/control-plane/native/git-ops.js +28 -7
- package/dist/src/control-plane/native/harness-cli.js +430 -71
- package/dist/src/control-plane/native/pr-automation.js +523 -86
- package/dist/src/control-plane/native/pr-review-gate.js +494 -69
- package/dist/src/control-plane/native/run-ops.js +12 -6
- package/dist/src/control-plane/native/task-ops.js +466 -105
- package/dist/src/control-plane/native/verifier.js +466 -107
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/package.json +6 -6
|
@@ -4352,7 +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);
|
|
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);
|
|
4356
4356
|
}
|
|
4357
4357
|
function isStrictFiveOfFive(score) {
|
|
4358
4358
|
return score.value === 5 && score.scale === 5;
|
|
@@ -4360,6 +4360,189 @@ function isStrictFiveOfFive(score) {
|
|
|
4360
4360
|
function containsConflictingScoreText(input) {
|
|
4361
4361
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
4362
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
|
+
}
|
|
4363
4546
|
function firstString(record, keys) {
|
|
4364
4547
|
for (const key of keys) {
|
|
4365
4548
|
const value = record[key];
|
|
@@ -4486,7 +4669,7 @@ function normalizeReviewThread(entry) {
|
|
|
4486
4669
|
function relevantIssueComment(comment) {
|
|
4487
4670
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4488
4671
|
const body = comment.body ?? "";
|
|
4489
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4672
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4490
4673
|
}
|
|
4491
4674
|
function latestThreadComment(thread) {
|
|
4492
4675
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -4522,7 +4705,8 @@ function makeGreptileSignal(input) {
|
|
|
4522
4705
|
const scores = parseGreptileScores(input.body);
|
|
4523
4706
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4524
4707
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4525
|
-
const
|
|
4708
|
+
const verdict = input.verdict ?? null;
|
|
4709
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4526
4710
|
const explicitApproval = input.explicitApproval ?? false;
|
|
4527
4711
|
return {
|
|
4528
4712
|
source: input.source,
|
|
@@ -4534,6 +4718,7 @@ function makeGreptileSignal(input) {
|
|
|
4534
4718
|
score: scores[0] ?? null,
|
|
4535
4719
|
scores,
|
|
4536
4720
|
explicitApproval,
|
|
4721
|
+
verdict,
|
|
4537
4722
|
blocker,
|
|
4538
4723
|
actionable: input.actionable ?? blocker,
|
|
4539
4724
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -4556,9 +4741,9 @@ function collectGreptileSignals(evidence) {
|
|
|
4556
4741
|
for (const context of contextSources) {
|
|
4557
4742
|
if (!context.body.trim())
|
|
4558
4743
|
continue;
|
|
4559
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
4560
|
-
continue;
|
|
4561
4744
|
const contextBlocker = containsBlockerText(context.body);
|
|
4745
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4746
|
+
continue;
|
|
4562
4747
|
signals.push(makeGreptileSignal({
|
|
4563
4748
|
source: context.source,
|
|
4564
4749
|
body: context.body,
|
|
@@ -4571,16 +4756,16 @@ function collectGreptileSignals(evidence) {
|
|
|
4571
4756
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4572
4757
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4573
4758
|
|
|
4574
|
-
`);
|
|
4575
|
-
|
|
4576
|
-
continue;
|
|
4759
|
+
`) || "Status: UNKNOWN";
|
|
4760
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4577
4761
|
signals.push(makeGreptileSignal({
|
|
4578
4762
|
source: "api",
|
|
4579
4763
|
body,
|
|
4580
4764
|
currentHeadSha: evidence.currentHeadSha,
|
|
4581
4765
|
trusted: true,
|
|
4582
4766
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4583
|
-
explicitApproval:
|
|
4767
|
+
explicitApproval: verdict === "approved",
|
|
4768
|
+
verdict
|
|
4584
4769
|
}));
|
|
4585
4770
|
}
|
|
4586
4771
|
for (const review of evidence.reviews) {
|
|
@@ -4605,20 +4790,6 @@ function collectGreptileSignals(evidence) {
|
|
|
4605
4790
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4606
4791
|
}));
|
|
4607
4792
|
}
|
|
4608
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
4609
|
-
const login = commentAuthorLogin(comment);
|
|
4610
|
-
const body = comment.body ?? "";
|
|
4611
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4612
|
-
continue;
|
|
4613
|
-
signals.push(makeGreptileSignal({
|
|
4614
|
-
source: "changed-file-comment",
|
|
4615
|
-
body,
|
|
4616
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
4617
|
-
trusted: true,
|
|
4618
|
-
authorLogin: login,
|
|
4619
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
4620
|
-
}));
|
|
4621
|
-
}
|
|
4622
4793
|
for (const comment of evidence.relevantIssueComments) {
|
|
4623
4794
|
const login = commentAuthorLogin(comment);
|
|
4624
4795
|
const body = comment.body ?? "";
|
|
@@ -4726,10 +4897,17 @@ function deriveGreptileEvidence(input) {
|
|
|
4726
4897
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4727
4898
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4728
4899
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4729
|
-
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;
|
|
4730
4908
|
const approvedByScore = !!approvingScoreEntry;
|
|
4731
|
-
const approvedByExplicitMapping =
|
|
4732
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4909
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4910
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4733
4911
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4734
4912
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4735
4913
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
@@ -4758,13 +4936,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4758
4936
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4759
4937
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4760
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"));
|
|
4761
4940
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4762
4941
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4763
4942
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4764
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4943
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4765
4944
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4766
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4767
|
-
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";
|
|
4768
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";
|
|
4769
4948
|
return {
|
|
4770
4949
|
source,
|
|
@@ -4871,6 +5050,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4871
5050
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4872
5051
|
}
|
|
4873
5052
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
5053
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4874
5054
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4875
5055
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4876
5056
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4908,8 +5088,19 @@ async function collectPrReviewEvidence(input) {
|
|
|
4908
5088
|
}
|
|
4909
5089
|
}
|
|
4910
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];
|
|
4911
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})` : ""}`);
|
|
4912
|
-
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
5103
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4913
5104
|
const evidenceBase = {
|
|
4914
5105
|
title: firstString(view, ["title"]),
|
|
4915
5106
|
body: firstString(view, ["body"]),
|
|
@@ -4919,7 +5110,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4919
5110
|
reviewThreads,
|
|
4920
5111
|
checks: checksWithGreptileDetails,
|
|
4921
5112
|
currentHeadSha: headSha,
|
|
4922
|
-
apiSignals
|
|
5113
|
+
apiSignals
|
|
4923
5114
|
};
|
|
4924
5115
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4925
5116
|
return {
|
|
@@ -4930,7 +5121,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4930
5121
|
body: evidenceBase.body,
|
|
4931
5122
|
headSha,
|
|
4932
5123
|
headRefName: firstString(view, ["headRefName"]),
|
|
4933
|
-
baseRefName
|
|
5124
|
+
baseRefName,
|
|
4934
5125
|
state: firstString(view, ["state"]),
|
|
4935
5126
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4936
5127
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4947,71 +5138,251 @@ async function collectPrReviewEvidence(input) {
|
|
|
4947
5138
|
greptile
|
|
4948
5139
|
};
|
|
4949
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
|
+
}
|
|
4950
5146
|
function evaluateEvidence(evidence) {
|
|
4951
|
-
const
|
|
5147
|
+
const reasonDetails = [];
|
|
4952
5148
|
const warnings = [];
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
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
|
+
});
|
|
4964
5203
|
}
|
|
4965
5204
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4966
5205
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4967
|
-
|
|
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
|
+
});
|
|
4968
5234
|
}
|
|
4969
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4970
|
-
if (unresolvedThreads.length > 0)
|
|
4971
|
-
reasons.push(...unresolvedThreads);
|
|
4972
|
-
const greptile = evidence.greptile;
|
|
4973
|
-
if (greptile.mapping === "missing")
|
|
4974
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4975
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4976
5235
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4977
|
-
|
|
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
|
+
});
|
|
4978
5267
|
}
|
|
4979
5268
|
if (!greptile.completed) {
|
|
4980
|
-
|
|
4981
|
-
|
|
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
|
+
});
|
|
4982
5289
|
}
|
|
4983
|
-
if (!greptile.fresh)
|
|
4984
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4985
5290
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4986
|
-
|
|
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
|
+
});
|
|
4987
5300
|
}
|
|
4988
|
-
|
|
4989
|
-
|
|
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
|
+
});
|
|
4990
5312
|
}
|
|
4991
5313
|
if (greptile.mapping === "unproven") {
|
|
4992
|
-
|
|
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
|
+
});
|
|
4993
5323
|
}
|
|
4994
|
-
|
|
4995
|
-
|
|
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
|
+
});
|
|
5334
|
+
}
|
|
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
|
+
});
|
|
4996
5345
|
}
|
|
4997
|
-
if (greptile.unresolvedComments.length > 0)
|
|
4998
|
-
reasons.push(...greptile.unresolvedComments);
|
|
4999
5346
|
if (!greptile.approved)
|
|
5000
5347
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
5001
|
-
|
|
5348
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
5349
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
5002
5350
|
}
|
|
5003
5351
|
function evaluateStrictPrMergeGate(evidence) {
|
|
5004
5352
|
const evaluated = evaluateEvidence(evidence);
|
|
5005
|
-
const approved = evaluated.
|
|
5353
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
5006
5354
|
return {
|
|
5007
5355
|
approved,
|
|
5008
5356
|
pending: evaluated.pending,
|
|
5009
5357
|
reasons: evaluated.reasons,
|
|
5358
|
+
reasonDetails: evaluated.reasonDetails,
|
|
5010
5359
|
warnings: evaluated.warnings,
|
|
5011
5360
|
actionableFeedback: evaluated.reasons,
|
|
5012
5361
|
evidence
|
|
5013
5362
|
};
|
|
5014
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
|
+
}
|
|
5015
5386
|
function promptExcerpt(value, maxChars = 4000) {
|
|
5016
5387
|
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
5017
5388
|
|
|
@@ -5023,6 +5394,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
|
|
|
5023
5394
|
function buildStrictPrGateSteeringPrompt(result) {
|
|
5024
5395
|
const evidence = result.evidence;
|
|
5025
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
|
+
}
|
|
5026
5401
|
const lines = [
|
|
5027
5402
|
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
5028
5403
|
`PR title: ${evidence.title || "(empty)"}`,
|
|
@@ -5031,10 +5406,13 @@ function buildStrictPrGateSteeringPrompt(result) {
|
|
|
5031
5406
|
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
5032
5407
|
"",
|
|
5033
5408
|
"Gate reasons:",
|
|
5034
|
-
...
|
|
5409
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
5410
|
+
"",
|
|
5411
|
+
"Structured gate reason details:",
|
|
5412
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
5035
5413
|
"",
|
|
5036
5414
|
"Required evidence read status:",
|
|
5037
|
-
evidence.readErrors.length ?
|
|
5415
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
5038
5416
|
"",
|
|
5039
5417
|
"Full PR title:",
|
|
5040
5418
|
evidence.title || "(empty)",
|
|
@@ -5098,6 +5476,7 @@ function persistPrReviewCycleArtifacts(input) {
|
|
|
5098
5476
|
approved: input.result.approved,
|
|
5099
5477
|
pending: input.result.pending,
|
|
5100
5478
|
reasons: input.result.reasons,
|
|
5479
|
+
reasonDetails: input.result.reasonDetails,
|
|
5101
5480
|
warnings: input.result.warnings,
|
|
5102
5481
|
actionableFeedback: input.result.actionableFeedback,
|
|
5103
5482
|
prUrl: input.result.evidence.prUrl,
|
|
@@ -5932,7 +6311,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5932
6311
|
};
|
|
5933
6312
|
}
|
|
5934
6313
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5935
|
-
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)) {
|
|
5936
6315
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5937
6316
|
return {
|
|
5938
6317
|
verdict: "REJECT",
|
|
@@ -6014,6 +6393,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
6014
6393
|
approved: strictGate.approved,
|
|
6015
6394
|
pending: strictGate.pending,
|
|
6016
6395
|
reasons: strictGate.reasons,
|
|
6396
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6017
6397
|
warnings: strictGate.warnings,
|
|
6018
6398
|
greptile: strictGate.evidence.greptile,
|
|
6019
6399
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -6036,6 +6416,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
6036
6416
|
approved: strictGate.approved,
|
|
6037
6417
|
pending: strictGate.pending,
|
|
6038
6418
|
reasons: strictGate.reasons,
|
|
6419
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6039
6420
|
warnings: strictGate.warnings,
|
|
6040
6421
|
greptile: strictGate.evidence.greptile,
|
|
6041
6422
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -6162,6 +6543,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
6162
6543
|
approved: strictGate.approved,
|
|
6163
6544
|
pending: strictGate.pending,
|
|
6164
6545
|
reasons: strictGate.reasons,
|
|
6546
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6165
6547
|
warnings: strictGate.warnings,
|
|
6166
6548
|
greptile: strictGate.evidence.greptile
|
|
6167
6549
|
},
|
|
@@ -6184,6 +6566,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
6184
6566
|
approved: strictGate.approved,
|
|
6185
6567
|
pending: strictGate.pending,
|
|
6186
6568
|
reasons: strictGate.reasons,
|
|
6569
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6187
6570
|
warnings: strictGate.warnings,
|
|
6188
6571
|
greptile: strictGate.evidence.greptile
|
|
6189
6572
|
},
|
|
@@ -6299,19 +6682,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
6299
6682
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
6300
6683
|
return true;
|
|
6301
6684
|
}
|
|
6302
|
-
return
|
|
6685
|
+
return false;
|
|
6303
6686
|
}
|
|
6304
6687
|
function shouldContinueGreptileMcpPolling(options) {
|
|
6305
6688
|
if (options.githubCheckState.completed) {
|
|
6306
6689
|
return false;
|
|
6307
6690
|
}
|
|
6691
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6692
|
+
return false;
|
|
6693
|
+
}
|
|
6308
6694
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
6309
6695
|
return true;
|
|
6310
6696
|
}
|
|
6311
|
-
return
|
|
6697
|
+
return true;
|
|
6312
6698
|
}
|
|
6313
6699
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
6314
6700
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6701
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6702
|
+
return false;
|
|
6703
|
+
}
|
|
6315
6704
|
if (waitingForVisiblePendingReview) {
|
|
6316
6705
|
return true;
|
|
6317
6706
|
}
|
|
@@ -7047,7 +7436,7 @@ function gitOpenPr(options) {
|
|
|
7047
7436
|
"",
|
|
7048
7437
|
"## Review",
|
|
7049
7438
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
7050
|
-
"- When repository policy allows it, Rig
|
|
7439
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
7051
7440
|
].join(`
|
|
7052
7441
|
`);
|
|
7053
7442
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -7243,15 +7632,12 @@ function gitMergePr(options) {
|
|
|
7243
7632
|
return { status: "already-merged", url: options.pr.url };
|
|
7244
7633
|
}
|
|
7245
7634
|
if (state !== "OPEN") {
|
|
7246
|
-
throw new Error(`Cannot
|
|
7635
|
+
throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
|
|
7247
7636
|
}
|
|
7248
7637
|
if (isDraft) {
|
|
7249
|
-
throw new Error(`Cannot
|
|
7250
|
-
}
|
|
7251
|
-
const strictGateHeadSha = options.strictGateHeadSha?.trim();
|
|
7252
|
-
if (!strictGateHeadSha) {
|
|
7253
|
-
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}.`);
|
|
7254
7639
|
}
|
|
7640
|
+
const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
|
|
7255
7641
|
const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
|
|
7256
7642
|
const method = options.method || "squash";
|
|
7257
7643
|
mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
|
|
@@ -8242,7 +8628,6 @@ async function main() {
|
|
|
8242
8628
|
if (prs.length === 0) {
|
|
8243
8629
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
8244
8630
|
} else {
|
|
8245
|
-
let mergePending = false;
|
|
8246
8631
|
let cycle = 0;
|
|
8247
8632
|
for (const pr of prs) {
|
|
8248
8633
|
cycle += 1;
|
|
@@ -8271,16 +8656,13 @@ async function main() {
|
|
|
8271
8656
|
pr,
|
|
8272
8657
|
method: "squash",
|
|
8273
8658
|
deleteBranch: true,
|
|
8274
|
-
|
|
8659
|
+
strictGate: gate
|
|
8275
8660
|
});
|
|
8276
|
-
if (mergeResult.status === "
|
|
8277
|
-
|
|
8278
|
-
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}`);
|
|
8279
8663
|
}
|
|
8280
8664
|
}
|
|
8281
|
-
if (
|
|
8282
|
-
failed = true;
|
|
8283
|
-
} else if (!failed) {
|
|
8665
|
+
if (!failed) {
|
|
8284
8666
|
sourceCloseoutAllowed = true;
|
|
8285
8667
|
console.log("OK: Auto-merge complete");
|
|
8286
8668
|
}
|