@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
|
@@ -3714,13 +3714,7 @@ function stripHtml(input) {
|
|
|
3714
3714
|
}
|
|
3715
3715
|
function containsBlockerText(input) {
|
|
3716
3716
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3717
|
-
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);
|
|
3718
|
-
}
|
|
3719
|
-
function containsGreptileNegativeVerdict(input) {
|
|
3720
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3721
|
-
if (!text)
|
|
3722
|
-
return false;
|
|
3723
|
-
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);
|
|
3717
|
+
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);
|
|
3724
3718
|
}
|
|
3725
3719
|
function isStrictFiveOfFive(score) {
|
|
3726
3720
|
return score.value === 5 && score.scale === 5;
|
|
@@ -3728,6 +3722,189 @@ function isStrictFiveOfFive(score) {
|
|
|
3728
3722
|
function containsConflictingScoreText(input) {
|
|
3729
3723
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3730
3724
|
}
|
|
3725
|
+
function greptileStatusVerdict(status) {
|
|
3726
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3727
|
+
if (!normalized)
|
|
3728
|
+
return null;
|
|
3729
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3730
|
+
return "approved";
|
|
3731
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3732
|
+
return "rejected";
|
|
3733
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3734
|
+
return "skipped";
|
|
3735
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3736
|
+
return "failed";
|
|
3737
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3738
|
+
return "pending";
|
|
3739
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3740
|
+
return "completed";
|
|
3741
|
+
return null;
|
|
3742
|
+
}
|
|
3743
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3744
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3745
|
+
}
|
|
3746
|
+
function greptileRequestTimeoutMs(env) {
|
|
3747
|
+
const fallback = 30000;
|
|
3748
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3749
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3750
|
+
}
|
|
3751
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3752
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3753
|
+
return null;
|
|
3754
|
+
const record = entry;
|
|
3755
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3756
|
+
if (!id)
|
|
3757
|
+
return null;
|
|
3758
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3759
|
+
return {
|
|
3760
|
+
id,
|
|
3761
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3762
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3763
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3764
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3768
|
+
const seen = new Set;
|
|
3769
|
+
const unique2 = [];
|
|
3770
|
+
for (const review of reviews) {
|
|
3771
|
+
if (seen.has(review.id))
|
|
3772
|
+
continue;
|
|
3773
|
+
seen.add(review.id);
|
|
3774
|
+
unique2.push(review);
|
|
3775
|
+
}
|
|
3776
|
+
return unique2;
|
|
3777
|
+
}
|
|
3778
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3779
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3780
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3781
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3782
|
+
const latest = sorted.slice(0, 1);
|
|
3783
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3784
|
+
}
|
|
3785
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3786
|
+
const selected = details ?? review;
|
|
3787
|
+
return {
|
|
3788
|
+
id: selected.id || review.id,
|
|
3789
|
+
body: selected.body ?? review.body ?? null,
|
|
3790
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3791
|
+
status: selected.status ?? review.status ?? null
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3795
|
+
const controller = new AbortController;
|
|
3796
|
+
const timeoutId = setTimeout(() => {
|
|
3797
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3798
|
+
}, input.timeoutMs);
|
|
3799
|
+
let response;
|
|
3800
|
+
try {
|
|
3801
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3802
|
+
method: "POST",
|
|
3803
|
+
headers: {
|
|
3804
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3805
|
+
"Content-Type": "application/json"
|
|
3806
|
+
},
|
|
3807
|
+
body: JSON.stringify({
|
|
3808
|
+
jsonrpc: "2.0",
|
|
3809
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3810
|
+
method: "tools/call",
|
|
3811
|
+
params: { name: input.name, arguments: input.args }
|
|
3812
|
+
}),
|
|
3813
|
+
signal: controller.signal
|
|
3814
|
+
});
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
if (controller.signal.aborted) {
|
|
3817
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3818
|
+
}
|
|
3819
|
+
throw error;
|
|
3820
|
+
} finally {
|
|
3821
|
+
clearTimeout(timeoutId);
|
|
3822
|
+
}
|
|
3823
|
+
const raw = await response.text();
|
|
3824
|
+
if (!response.ok) {
|
|
3825
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3826
|
+
}
|
|
3827
|
+
let envelope;
|
|
3828
|
+
try {
|
|
3829
|
+
envelope = JSON.parse(raw);
|
|
3830
|
+
} catch {
|
|
3831
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3832
|
+
}
|
|
3833
|
+
if (envelope.error?.message) {
|
|
3834
|
+
throw new Error(envelope.error.message);
|
|
3835
|
+
}
|
|
3836
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3837
|
+
`).trim();
|
|
3838
|
+
if (!text) {
|
|
3839
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3840
|
+
}
|
|
3841
|
+
return text;
|
|
3842
|
+
}
|
|
3843
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3844
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3845
|
+
try {
|
|
3846
|
+
return JSON.parse(text);
|
|
3847
|
+
} catch {
|
|
3848
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3852
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3853
|
+
return { signals: [], errors: [] };
|
|
3854
|
+
}
|
|
3855
|
+
const env = input.options?.env ?? process.env;
|
|
3856
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3857
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3858
|
+
if (!apiKey) {
|
|
3859
|
+
return { signals: [], errors: [] };
|
|
3860
|
+
}
|
|
3861
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3862
|
+
if (typeof fetchFn !== "function") {
|
|
3863
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3864
|
+
}
|
|
3865
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
3866
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
3867
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
3868
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
3869
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
3870
|
+
try {
|
|
3871
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
3872
|
+
apiBase,
|
|
3873
|
+
apiKey,
|
|
3874
|
+
name: "list_code_reviews",
|
|
3875
|
+
args: {
|
|
3876
|
+
name: repository,
|
|
3877
|
+
remote,
|
|
3878
|
+
defaultBranch,
|
|
3879
|
+
prNumber: input.prNumber,
|
|
3880
|
+
limit: 20
|
|
3881
|
+
},
|
|
3882
|
+
timeoutMs,
|
|
3883
|
+
fetchFn
|
|
3884
|
+
});
|
|
3885
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
3886
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
3887
|
+
const signals = [];
|
|
3888
|
+
for (const review of selectedReviews) {
|
|
3889
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
3890
|
+
apiBase,
|
|
3891
|
+
apiKey,
|
|
3892
|
+
name: "get_code_review",
|
|
3893
|
+
args: { codeReviewId: review.id },
|
|
3894
|
+
timeoutMs,
|
|
3895
|
+
fetchFn
|
|
3896
|
+
});
|
|
3897
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
3898
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
3899
|
+
}
|
|
3900
|
+
return { signals, errors: [] };
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
return {
|
|
3903
|
+
signals: [],
|
|
3904
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3731
3908
|
function firstString(record, keys) {
|
|
3732
3909
|
for (const key of keys) {
|
|
3733
3910
|
const value = record[key];
|
|
@@ -3854,7 +4031,7 @@ function normalizeReviewThread(entry) {
|
|
|
3854
4031
|
function relevantIssueComment(comment) {
|
|
3855
4032
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
3856
4033
|
const body = comment.body ?? "";
|
|
3857
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4034
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
3858
4035
|
}
|
|
3859
4036
|
function latestThreadComment(thread) {
|
|
3860
4037
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -3890,7 +4067,8 @@ function makeGreptileSignal(input) {
|
|
|
3890
4067
|
const scores = parseGreptileScores(input.body);
|
|
3891
4068
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
3892
4069
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
3893
|
-
const
|
|
4070
|
+
const verdict = input.verdict ?? null;
|
|
4071
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
3894
4072
|
const explicitApproval = input.explicitApproval ?? false;
|
|
3895
4073
|
return {
|
|
3896
4074
|
source: input.source,
|
|
@@ -3902,6 +4080,7 @@ function makeGreptileSignal(input) {
|
|
|
3902
4080
|
score: scores[0] ?? null,
|
|
3903
4081
|
scores,
|
|
3904
4082
|
explicitApproval,
|
|
4083
|
+
verdict,
|
|
3905
4084
|
blocker,
|
|
3906
4085
|
actionable: input.actionable ?? blocker,
|
|
3907
4086
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -3924,9 +4103,9 @@ function collectGreptileSignals(evidence) {
|
|
|
3924
4103
|
for (const context of contextSources) {
|
|
3925
4104
|
if (!context.body.trim())
|
|
3926
4105
|
continue;
|
|
3927
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
3928
|
-
continue;
|
|
3929
4106
|
const contextBlocker = containsBlockerText(context.body);
|
|
4107
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4108
|
+
continue;
|
|
3930
4109
|
signals.push(makeGreptileSignal({
|
|
3931
4110
|
source: context.source,
|
|
3932
4111
|
body: context.body,
|
|
@@ -3939,16 +4118,16 @@ function collectGreptileSignals(evidence) {
|
|
|
3939
4118
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
3940
4119
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
3941
4120
|
|
|
3942
|
-
`);
|
|
3943
|
-
|
|
3944
|
-
continue;
|
|
4121
|
+
`) || "Status: UNKNOWN";
|
|
4122
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
3945
4123
|
signals.push(makeGreptileSignal({
|
|
3946
4124
|
source: "api",
|
|
3947
4125
|
body,
|
|
3948
4126
|
currentHeadSha: evidence.currentHeadSha,
|
|
3949
4127
|
trusted: true,
|
|
3950
4128
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
3951
|
-
explicitApproval:
|
|
4129
|
+
explicitApproval: verdict === "approved",
|
|
4130
|
+
verdict
|
|
3952
4131
|
}));
|
|
3953
4132
|
}
|
|
3954
4133
|
for (const review of evidence.reviews) {
|
|
@@ -3973,20 +4152,6 @@ function collectGreptileSignals(evidence) {
|
|
|
3973
4152
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
3974
4153
|
}));
|
|
3975
4154
|
}
|
|
3976
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
3977
|
-
const login = commentAuthorLogin(comment);
|
|
3978
|
-
const body = comment.body ?? "";
|
|
3979
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
3980
|
-
continue;
|
|
3981
|
-
signals.push(makeGreptileSignal({
|
|
3982
|
-
source: "changed-file-comment",
|
|
3983
|
-
body,
|
|
3984
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
3985
|
-
trusted: true,
|
|
3986
|
-
authorLogin: login,
|
|
3987
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
3988
|
-
}));
|
|
3989
|
-
}
|
|
3990
4155
|
for (const comment of evidence.relevantIssueComments) {
|
|
3991
4156
|
const login = commentAuthorLogin(comment);
|
|
3992
4157
|
const body = comment.body ?? "";
|
|
@@ -4052,6 +4217,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
4052
4217
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4053
4218
|
});
|
|
4054
4219
|
}
|
|
4220
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4221
|
+
return [];
|
|
4222
|
+
}
|
|
4055
4223
|
function issueLevelBlockerSummaries(comments) {
|
|
4056
4224
|
return comments.flatMap((comment) => {
|
|
4057
4225
|
const body = comment.body?.trim() ?? "";
|
|
@@ -4091,14 +4259,21 @@ function deriveGreptileEvidence(input) {
|
|
|
4091
4259
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4092
4260
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4093
4261
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4094
|
-
const
|
|
4262
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4263
|
+
const signalCanApproveByScore = (signal) => {
|
|
4264
|
+
if (signal.source === "api")
|
|
4265
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4266
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4267
|
+
};
|
|
4268
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4269
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4095
4270
|
const approvedByScore = !!approvingScoreEntry;
|
|
4096
|
-
const approvedByExplicitMapping =
|
|
4097
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4271
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4272
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4098
4273
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4099
4274
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4100
4275
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4101
|
-
const blockerSignals = signals.filter((signal) =>
|
|
4276
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4102
4277
|
const staleBlockingSignals = [];
|
|
4103
4278
|
const blockers = [
|
|
4104
4279
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -4109,7 +4284,8 @@ function deriveGreptileEvidence(input) {
|
|
|
4109
4284
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4110
4285
|
];
|
|
4111
4286
|
const unresolvedComments = [
|
|
4112
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4287
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4288
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4113
4289
|
];
|
|
4114
4290
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4115
4291
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -4122,13 +4298,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4122
4298
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4123
4299
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4124
4300
|
});
|
|
4301
|
+
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"));
|
|
4125
4302
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4126
4303
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4127
4304
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4128
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4305
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4129
4306
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4130
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4131
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4307
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4308
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4132
4309
|
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";
|
|
4133
4310
|
return {
|
|
4134
4311
|
source,
|
|
@@ -4235,6 +4412,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4235
4412
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4236
4413
|
}
|
|
4237
4414
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4415
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4238
4416
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4239
4417
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4240
4418
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4272,6 +4450,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
4272
4450
|
}
|
|
4273
4451
|
}
|
|
4274
4452
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4453
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4454
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4455
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4456
|
+
options: input.greptileApi,
|
|
4457
|
+
repoName: parsed.repoName,
|
|
4458
|
+
prNumber: parsed.prNumber,
|
|
4459
|
+
headSha,
|
|
4460
|
+
baseRefName
|
|
4461
|
+
});
|
|
4462
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4463
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4275
4464
|
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})` : ""}`);
|
|
4276
4465
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4277
4466
|
const evidenceBase = {
|
|
@@ -4283,7 +4472,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4283
4472
|
reviewThreads,
|
|
4284
4473
|
checks: checksWithGreptileDetails,
|
|
4285
4474
|
currentHeadSha: headSha,
|
|
4286
|
-
apiSignals
|
|
4475
|
+
apiSignals
|
|
4287
4476
|
};
|
|
4288
4477
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4289
4478
|
return {
|
|
@@ -4294,7 +4483,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4294
4483
|
body: evidenceBase.body,
|
|
4295
4484
|
headSha,
|
|
4296
4485
|
headRefName: firstString(view, ["headRefName"]),
|
|
4297
|
-
baseRefName
|
|
4486
|
+
baseRefName,
|
|
4298
4487
|
state: firstString(view, ["state"]),
|
|
4299
4488
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4300
4489
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4311,66 +4500,224 @@ async function collectPrReviewEvidence(input) {
|
|
|
4311
4500
|
greptile
|
|
4312
4501
|
};
|
|
4313
4502
|
}
|
|
4503
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4504
|
+
const normalized = value.trim();
|
|
4505
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4506
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4507
|
+
}
|
|
4314
4508
|
function evaluateEvidence(evidence) {
|
|
4315
|
-
const
|
|
4509
|
+
const reasonDetails = [];
|
|
4316
4510
|
const warnings = [];
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4511
|
+
const seen = new Set;
|
|
4512
|
+
const addReason = (reason) => {
|
|
4513
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4514
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4515
|
+
if (seen.has(key))
|
|
4516
|
+
return;
|
|
4517
|
+
seen.add(key);
|
|
4518
|
+
reasonDetails.push(capped);
|
|
4519
|
+
};
|
|
4520
|
+
const greptile = evidence.greptile;
|
|
4521
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4522
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4523
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4524
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4525
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4526
|
+
for (const error of evidence.readErrors) {
|
|
4527
|
+
addReason({
|
|
4528
|
+
code: "read_error",
|
|
4529
|
+
reasonClass: "reject",
|
|
4530
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4531
|
+
suggestedAction: "needs_attention",
|
|
4532
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4533
|
+
headSha: evidence.headSha || null
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
if (!evidence.headSha) {
|
|
4537
|
+
addReason({
|
|
4538
|
+
code: "missing_head_sha",
|
|
4539
|
+
reasonClass: "reject",
|
|
4540
|
+
surface: "github",
|
|
4541
|
+
suggestedAction: "needs_attention",
|
|
4542
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4543
|
+
headSha: null
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
for (const failure of evidence.checkFailures) {
|
|
4547
|
+
addReason({
|
|
4548
|
+
code: "ci_failed",
|
|
4549
|
+
reasonClass: "reject",
|
|
4550
|
+
surface: "ci",
|
|
4551
|
+
suggestedAction: "fix",
|
|
4552
|
+
message: failure,
|
|
4553
|
+
headSha: evidence.headSha || null
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4557
|
+
addReason({
|
|
4558
|
+
code: "check_pending",
|
|
4559
|
+
reasonClass: "pending",
|
|
4560
|
+
surface: "ci",
|
|
4561
|
+
suggestedAction: "wait",
|
|
4562
|
+
message: pendingCheck,
|
|
4563
|
+
headSha: evidence.headSha || null
|
|
4564
|
+
});
|
|
4328
4565
|
}
|
|
4329
4566
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4330
4567
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4331
|
-
|
|
4568
|
+
addReason({
|
|
4569
|
+
code: "review_decision_blocking",
|
|
4570
|
+
reasonClass: "reject",
|
|
4571
|
+
surface: "review",
|
|
4572
|
+
suggestedAction: "fix",
|
|
4573
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4574
|
+
headSha: evidence.headSha || null
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4578
|
+
addReason({
|
|
4579
|
+
code: "review_thread_unresolved",
|
|
4580
|
+
reasonClass: "reject",
|
|
4581
|
+
surface: "review",
|
|
4582
|
+
suggestedAction: "fix",
|
|
4583
|
+
message: thread,
|
|
4584
|
+
headSha: evidence.headSha || null
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
if (greptile.mapping === "missing") {
|
|
4588
|
+
addReason({
|
|
4589
|
+
code: "greptile_missing",
|
|
4590
|
+
reasonClass: "pending",
|
|
4591
|
+
surface: "greptile",
|
|
4592
|
+
suggestedAction: "wait",
|
|
4593
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4594
|
+
headSha: evidence.headSha || null
|
|
4595
|
+
});
|
|
4332
4596
|
}
|
|
4333
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4334
|
-
if (unresolvedThreads.length > 0)
|
|
4335
|
-
reasons.push(...unresolvedThreads);
|
|
4336
|
-
const greptile = evidence.greptile;
|
|
4337
|
-
if (greptile.mapping === "missing")
|
|
4338
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4339
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4340
4597
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4341
|
-
|
|
4598
|
+
addReason({
|
|
4599
|
+
code: "greptile_stale",
|
|
4600
|
+
reasonClass: "pending",
|
|
4601
|
+
surface: "greptile",
|
|
4602
|
+
suggestedAction: "wait",
|
|
4603
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4604
|
+
headSha: evidence.headSha || null,
|
|
4605
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4609
|
+
addReason({
|
|
4610
|
+
code: "greptile_pending",
|
|
4611
|
+
reasonClass: "pending",
|
|
4612
|
+
surface: "greptile",
|
|
4613
|
+
suggestedAction: "wait",
|
|
4614
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4615
|
+
headSha: evidence.headSha || null,
|
|
4616
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4620
|
+
addReason({
|
|
4621
|
+
code: "greptile_api_status_unknown",
|
|
4622
|
+
reasonClass: "reject",
|
|
4623
|
+
surface: "greptile",
|
|
4624
|
+
suggestedAction: "needs_attention",
|
|
4625
|
+
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}` : "."}`,
|
|
4626
|
+
headSha: evidence.headSha || null,
|
|
4627
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4628
|
+
});
|
|
4342
4629
|
}
|
|
4343
4630
|
if (!greptile.completed) {
|
|
4344
|
-
|
|
4345
|
-
|
|
4631
|
+
addReason({
|
|
4632
|
+
code: "greptile_pending",
|
|
4633
|
+
reasonClass: "pending",
|
|
4634
|
+
surface: "greptile",
|
|
4635
|
+
suggestedAction: "wait",
|
|
4636
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4637
|
+
headSha: evidence.headSha || null,
|
|
4638
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
if (!greptile.fresh) {
|
|
4642
|
+
addReason({
|
|
4643
|
+
code: "greptile_not_current_head",
|
|
4644
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4645
|
+
surface: "greptile",
|
|
4646
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4647
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4648
|
+
headSha: evidence.headSha || null,
|
|
4649
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4650
|
+
});
|
|
4346
4651
|
}
|
|
4347
|
-
if (!greptile.fresh)
|
|
4348
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4349
4652
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4350
|
-
|
|
4653
|
+
addReason({
|
|
4654
|
+
code: "greptile_score_not_5",
|
|
4655
|
+
reasonClass: "reject",
|
|
4656
|
+
surface: "greptile",
|
|
4657
|
+
suggestedAction: "fix",
|
|
4658
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4659
|
+
headSha: evidence.headSha || null,
|
|
4660
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4661
|
+
});
|
|
4351
4662
|
}
|
|
4352
|
-
|
|
4353
|
-
|
|
4663
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4664
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4665
|
+
addReason({
|
|
4666
|
+
code: "greptile_score_missing",
|
|
4667
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4668
|
+
surface: "greptile",
|
|
4669
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4670
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4671
|
+
headSha: evidence.headSha || null,
|
|
4672
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4673
|
+
});
|
|
4354
4674
|
}
|
|
4355
4675
|
if (greptile.mapping === "unproven") {
|
|
4356
|
-
|
|
4676
|
+
addReason({
|
|
4677
|
+
code: "greptile_mapping_unproven",
|
|
4678
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4679
|
+
surface: "greptile",
|
|
4680
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4681
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4682
|
+
headSha: evidence.headSha || null,
|
|
4683
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4684
|
+
});
|
|
4357
4685
|
}
|
|
4358
|
-
|
|
4359
|
-
|
|
4686
|
+
for (const blocker of greptile.blockers) {
|
|
4687
|
+
addReason({
|
|
4688
|
+
code: "greptile_blocker_text",
|
|
4689
|
+
reasonClass: "reject",
|
|
4690
|
+
surface: "greptile",
|
|
4691
|
+
suggestedAction: "fix",
|
|
4692
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4693
|
+
headSha: evidence.headSha || null,
|
|
4694
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4698
|
+
addReason({
|
|
4699
|
+
code: "greptile_unresolved_comment",
|
|
4700
|
+
reasonClass: "reject",
|
|
4701
|
+
surface: "greptile",
|
|
4702
|
+
suggestedAction: "fix",
|
|
4703
|
+
message: comment,
|
|
4704
|
+
headSha: evidence.headSha || null,
|
|
4705
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4706
|
+
});
|
|
4360
4707
|
}
|
|
4361
|
-
if (greptile.unresolvedComments.length > 0)
|
|
4362
|
-
reasons.push(...greptile.unresolvedComments);
|
|
4363
4708
|
if (!greptile.approved)
|
|
4364
4709
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4365
|
-
|
|
4710
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4711
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4366
4712
|
}
|
|
4367
4713
|
function evaluateStrictPrMergeGate(evidence) {
|
|
4368
4714
|
const evaluated = evaluateEvidence(evidence);
|
|
4369
|
-
const approved = evaluated.
|
|
4715
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4370
4716
|
return {
|
|
4371
4717
|
approved,
|
|
4372
4718
|
pending: evaluated.pending,
|
|
4373
4719
|
reasons: evaluated.reasons,
|
|
4720
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4374
4721
|
warnings: evaluated.warnings,
|
|
4375
4722
|
actionableFeedback: evaluated.reasons,
|
|
4376
4723
|
evidence
|
|
@@ -5174,7 +5521,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5174
5521
|
};
|
|
5175
5522
|
}
|
|
5176
5523
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5177
|
-
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)) {
|
|
5524
|
+
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)) {
|
|
5178
5525
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5179
5526
|
return {
|
|
5180
5527
|
verdict: "REJECT",
|
|
@@ -5256,6 +5603,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5256
5603
|
approved: strictGate.approved,
|
|
5257
5604
|
pending: strictGate.pending,
|
|
5258
5605
|
reasons: strictGate.reasons,
|
|
5606
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5259
5607
|
warnings: strictGate.warnings,
|
|
5260
5608
|
greptile: strictGate.evidence.greptile,
|
|
5261
5609
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5278,6 +5626,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5278
5626
|
approved: strictGate.approved,
|
|
5279
5627
|
pending: strictGate.pending,
|
|
5280
5628
|
reasons: strictGate.reasons,
|
|
5629
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5281
5630
|
warnings: strictGate.warnings,
|
|
5282
5631
|
greptile: strictGate.evidence.greptile,
|
|
5283
5632
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5404,6 +5753,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5404
5753
|
approved: strictGate.approved,
|
|
5405
5754
|
pending: strictGate.pending,
|
|
5406
5755
|
reasons: strictGate.reasons,
|
|
5756
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5407
5757
|
warnings: strictGate.warnings,
|
|
5408
5758
|
greptile: strictGate.evidence.greptile
|
|
5409
5759
|
},
|
|
@@ -5426,6 +5776,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5426
5776
|
approved: strictGate.approved,
|
|
5427
5777
|
pending: strictGate.pending,
|
|
5428
5778
|
reasons: strictGate.reasons,
|
|
5779
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5429
5780
|
warnings: strictGate.warnings,
|
|
5430
5781
|
greptile: strictGate.evidence.greptile
|
|
5431
5782
|
},
|
|
@@ -5547,8 +5898,7 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5547
5898
|
if (options.githubCheckState.completed) {
|
|
5548
5899
|
return false;
|
|
5549
5900
|
}
|
|
5550
|
-
|
|
5551
|
-
if (!hasRemainingBudget) {
|
|
5901
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5552
5902
|
return false;
|
|
5553
5903
|
}
|
|
5554
5904
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
@@ -5558,8 +5908,11 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5558
5908
|
}
|
|
5559
5909
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
5560
5910
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
5911
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5912
|
+
return false;
|
|
5913
|
+
}
|
|
5561
5914
|
if (waitingForVisiblePendingReview) {
|
|
5562
|
-
return
|
|
5915
|
+
return true;
|
|
5563
5916
|
}
|
|
5564
5917
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
5565
5918
|
if (reviewNotVisibleYet) {
|
|
@@ -7009,7 +7362,7 @@ function gitOpenPr(options) {
|
|
|
7009
7362
|
"",
|
|
7010
7363
|
"## Review",
|
|
7011
7364
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
7012
|
-
"- When repository policy allows it, Rig
|
|
7365
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
7013
7366
|
].join(`
|
|
7014
7367
|
`);
|
|
7015
7368
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|