@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
|
@@ -3720,13 +3720,7 @@ function stripHtml(input) {
|
|
|
3720
3720
|
}
|
|
3721
3721
|
function containsBlockerText(input) {
|
|
3722
3722
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3723
|
-
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);
|
|
3724
|
-
}
|
|
3725
|
-
function containsGreptileNegativeVerdict(input) {
|
|
3726
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3727
|
-
if (!text)
|
|
3728
|
-
return false;
|
|
3729
|
-
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);
|
|
3723
|
+
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);
|
|
3730
3724
|
}
|
|
3731
3725
|
function isStrictFiveOfFive(score) {
|
|
3732
3726
|
return score.value === 5 && score.scale === 5;
|
|
@@ -3734,6 +3728,189 @@ function isStrictFiveOfFive(score) {
|
|
|
3734
3728
|
function containsConflictingScoreText(input) {
|
|
3735
3729
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3736
3730
|
}
|
|
3731
|
+
function greptileStatusVerdict(status) {
|
|
3732
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3733
|
+
if (!normalized)
|
|
3734
|
+
return null;
|
|
3735
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3736
|
+
return "approved";
|
|
3737
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3738
|
+
return "rejected";
|
|
3739
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3740
|
+
return "skipped";
|
|
3741
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3742
|
+
return "failed";
|
|
3743
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3744
|
+
return "pending";
|
|
3745
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3746
|
+
return "completed";
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3750
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3751
|
+
}
|
|
3752
|
+
function greptileRequestTimeoutMs(env) {
|
|
3753
|
+
const fallback = 30000;
|
|
3754
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3755
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3756
|
+
}
|
|
3757
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3758
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3759
|
+
return null;
|
|
3760
|
+
const record = entry;
|
|
3761
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3762
|
+
if (!id)
|
|
3763
|
+
return null;
|
|
3764
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3765
|
+
return {
|
|
3766
|
+
id,
|
|
3767
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3768
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3769
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3770
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3774
|
+
const seen = new Set;
|
|
3775
|
+
const unique2 = [];
|
|
3776
|
+
for (const review of reviews) {
|
|
3777
|
+
if (seen.has(review.id))
|
|
3778
|
+
continue;
|
|
3779
|
+
seen.add(review.id);
|
|
3780
|
+
unique2.push(review);
|
|
3781
|
+
}
|
|
3782
|
+
return unique2;
|
|
3783
|
+
}
|
|
3784
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3785
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3786
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3787
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3788
|
+
const latest = sorted.slice(0, 1);
|
|
3789
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3790
|
+
}
|
|
3791
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3792
|
+
const selected = details ?? review;
|
|
3793
|
+
return {
|
|
3794
|
+
id: selected.id || review.id,
|
|
3795
|
+
body: selected.body ?? review.body ?? null,
|
|
3796
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3797
|
+
status: selected.status ?? review.status ?? null
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3801
|
+
const controller = new AbortController;
|
|
3802
|
+
const timeoutId = setTimeout(() => {
|
|
3803
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3804
|
+
}, input.timeoutMs);
|
|
3805
|
+
let response;
|
|
3806
|
+
try {
|
|
3807
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3808
|
+
method: "POST",
|
|
3809
|
+
headers: {
|
|
3810
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3811
|
+
"Content-Type": "application/json"
|
|
3812
|
+
},
|
|
3813
|
+
body: JSON.stringify({
|
|
3814
|
+
jsonrpc: "2.0",
|
|
3815
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3816
|
+
method: "tools/call",
|
|
3817
|
+
params: { name: input.name, arguments: input.args }
|
|
3818
|
+
}),
|
|
3819
|
+
signal: controller.signal
|
|
3820
|
+
});
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
if (controller.signal.aborted) {
|
|
3823
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3824
|
+
}
|
|
3825
|
+
throw error;
|
|
3826
|
+
} finally {
|
|
3827
|
+
clearTimeout(timeoutId);
|
|
3828
|
+
}
|
|
3829
|
+
const raw = await response.text();
|
|
3830
|
+
if (!response.ok) {
|
|
3831
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3832
|
+
}
|
|
3833
|
+
let envelope;
|
|
3834
|
+
try {
|
|
3835
|
+
envelope = JSON.parse(raw);
|
|
3836
|
+
} catch {
|
|
3837
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3838
|
+
}
|
|
3839
|
+
if (envelope.error?.message) {
|
|
3840
|
+
throw new Error(envelope.error.message);
|
|
3841
|
+
}
|
|
3842
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3843
|
+
`).trim();
|
|
3844
|
+
if (!text) {
|
|
3845
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3846
|
+
}
|
|
3847
|
+
return text;
|
|
3848
|
+
}
|
|
3849
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3850
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3851
|
+
try {
|
|
3852
|
+
return JSON.parse(text);
|
|
3853
|
+
} catch {
|
|
3854
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3858
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3859
|
+
return { signals: [], errors: [] };
|
|
3860
|
+
}
|
|
3861
|
+
const env = input.options?.env ?? process.env;
|
|
3862
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3863
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3864
|
+
if (!apiKey) {
|
|
3865
|
+
return { signals: [], errors: [] };
|
|
3866
|
+
}
|
|
3867
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3868
|
+
if (typeof fetchFn !== "function") {
|
|
3869
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3870
|
+
}
|
|
3871
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
3872
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
3873
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
3874
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
3875
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
3876
|
+
try {
|
|
3877
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
3878
|
+
apiBase,
|
|
3879
|
+
apiKey,
|
|
3880
|
+
name: "list_code_reviews",
|
|
3881
|
+
args: {
|
|
3882
|
+
name: repository,
|
|
3883
|
+
remote,
|
|
3884
|
+
defaultBranch,
|
|
3885
|
+
prNumber: input.prNumber,
|
|
3886
|
+
limit: 20
|
|
3887
|
+
},
|
|
3888
|
+
timeoutMs,
|
|
3889
|
+
fetchFn
|
|
3890
|
+
});
|
|
3891
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
3892
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
3893
|
+
const signals = [];
|
|
3894
|
+
for (const review of selectedReviews) {
|
|
3895
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
3896
|
+
apiBase,
|
|
3897
|
+
apiKey,
|
|
3898
|
+
name: "get_code_review",
|
|
3899
|
+
args: { codeReviewId: review.id },
|
|
3900
|
+
timeoutMs,
|
|
3901
|
+
fetchFn
|
|
3902
|
+
});
|
|
3903
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
3904
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
3905
|
+
}
|
|
3906
|
+
return { signals, errors: [] };
|
|
3907
|
+
} catch (error) {
|
|
3908
|
+
return {
|
|
3909
|
+
signals: [],
|
|
3910
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3737
3914
|
function firstString(record, keys) {
|
|
3738
3915
|
for (const key of keys) {
|
|
3739
3916
|
const value = record[key];
|
|
@@ -3860,7 +4037,7 @@ function normalizeReviewThread(entry) {
|
|
|
3860
4037
|
function relevantIssueComment(comment) {
|
|
3861
4038
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
3862
4039
|
const body = comment.body ?? "";
|
|
3863
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4040
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
3864
4041
|
}
|
|
3865
4042
|
function latestThreadComment(thread) {
|
|
3866
4043
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -3896,7 +4073,8 @@ function makeGreptileSignal(input) {
|
|
|
3896
4073
|
const scores = parseGreptileScores(input.body);
|
|
3897
4074
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
3898
4075
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
3899
|
-
const
|
|
4076
|
+
const verdict = input.verdict ?? null;
|
|
4077
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
3900
4078
|
const explicitApproval = input.explicitApproval ?? false;
|
|
3901
4079
|
return {
|
|
3902
4080
|
source: input.source,
|
|
@@ -3908,6 +4086,7 @@ function makeGreptileSignal(input) {
|
|
|
3908
4086
|
score: scores[0] ?? null,
|
|
3909
4087
|
scores,
|
|
3910
4088
|
explicitApproval,
|
|
4089
|
+
verdict,
|
|
3911
4090
|
blocker,
|
|
3912
4091
|
actionable: input.actionable ?? blocker,
|
|
3913
4092
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -3930,9 +4109,9 @@ function collectGreptileSignals(evidence) {
|
|
|
3930
4109
|
for (const context of contextSources) {
|
|
3931
4110
|
if (!context.body.trim())
|
|
3932
4111
|
continue;
|
|
3933
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
3934
|
-
continue;
|
|
3935
4112
|
const contextBlocker = containsBlockerText(context.body);
|
|
4113
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4114
|
+
continue;
|
|
3936
4115
|
signals.push(makeGreptileSignal({
|
|
3937
4116
|
source: context.source,
|
|
3938
4117
|
body: context.body,
|
|
@@ -3945,16 +4124,16 @@ function collectGreptileSignals(evidence) {
|
|
|
3945
4124
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
3946
4125
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
3947
4126
|
|
|
3948
|
-
`);
|
|
3949
|
-
|
|
3950
|
-
continue;
|
|
4127
|
+
`) || "Status: UNKNOWN";
|
|
4128
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
3951
4129
|
signals.push(makeGreptileSignal({
|
|
3952
4130
|
source: "api",
|
|
3953
4131
|
body,
|
|
3954
4132
|
currentHeadSha: evidence.currentHeadSha,
|
|
3955
4133
|
trusted: true,
|
|
3956
4134
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
3957
|
-
explicitApproval:
|
|
4135
|
+
explicitApproval: verdict === "approved",
|
|
4136
|
+
verdict
|
|
3958
4137
|
}));
|
|
3959
4138
|
}
|
|
3960
4139
|
for (const review of evidence.reviews) {
|
|
@@ -3979,20 +4158,6 @@ function collectGreptileSignals(evidence) {
|
|
|
3979
4158
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
3980
4159
|
}));
|
|
3981
4160
|
}
|
|
3982
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
3983
|
-
const login = commentAuthorLogin(comment);
|
|
3984
|
-
const body = comment.body ?? "";
|
|
3985
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
3986
|
-
continue;
|
|
3987
|
-
signals.push(makeGreptileSignal({
|
|
3988
|
-
source: "changed-file-comment",
|
|
3989
|
-
body,
|
|
3990
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
3991
|
-
trusted: true,
|
|
3992
|
-
authorLogin: login,
|
|
3993
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
3994
|
-
}));
|
|
3995
|
-
}
|
|
3996
4161
|
for (const comment of evidence.relevantIssueComments) {
|
|
3997
4162
|
const login = commentAuthorLogin(comment);
|
|
3998
4163
|
const body = comment.body ?? "";
|
|
@@ -4058,6 +4223,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
4058
4223
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4059
4224
|
});
|
|
4060
4225
|
}
|
|
4226
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4227
|
+
return [];
|
|
4228
|
+
}
|
|
4061
4229
|
function issueLevelBlockerSummaries(comments) {
|
|
4062
4230
|
return comments.flatMap((comment) => {
|
|
4063
4231
|
const body = comment.body?.trim() ?? "";
|
|
@@ -4097,14 +4265,21 @@ function deriveGreptileEvidence(input) {
|
|
|
4097
4265
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4098
4266
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4099
4267
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4100
|
-
const
|
|
4268
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4269
|
+
const signalCanApproveByScore = (signal) => {
|
|
4270
|
+
if (signal.source === "api")
|
|
4271
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4272
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4273
|
+
};
|
|
4274
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4275
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4101
4276
|
const approvedByScore = !!approvingScoreEntry;
|
|
4102
|
-
const approvedByExplicitMapping =
|
|
4103
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4277
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4278
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4104
4279
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4105
4280
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4106
4281
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4107
|
-
const blockerSignals = signals.filter((signal) =>
|
|
4282
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4108
4283
|
const staleBlockingSignals = [];
|
|
4109
4284
|
const blockers = [
|
|
4110
4285
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -4115,7 +4290,8 @@ function deriveGreptileEvidence(input) {
|
|
|
4115
4290
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4116
4291
|
];
|
|
4117
4292
|
const unresolvedComments = [
|
|
4118
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4293
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4294
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4119
4295
|
];
|
|
4120
4296
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4121
4297
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -4128,13 +4304,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4128
4304
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4129
4305
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4130
4306
|
});
|
|
4307
|
+
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"));
|
|
4131
4308
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4132
4309
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4133
4310
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4134
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4311
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4135
4312
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4136
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4137
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4313
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4314
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4138
4315
|
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";
|
|
4139
4316
|
return {
|
|
4140
4317
|
source,
|
|
@@ -4241,6 +4418,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4241
4418
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4242
4419
|
}
|
|
4243
4420
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4421
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4244
4422
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4245
4423
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4246
4424
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4278,6 +4456,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
4278
4456
|
}
|
|
4279
4457
|
}
|
|
4280
4458
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4459
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4460
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4461
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4462
|
+
options: input.greptileApi,
|
|
4463
|
+
repoName: parsed.repoName,
|
|
4464
|
+
prNumber: parsed.prNumber,
|
|
4465
|
+
headSha,
|
|
4466
|
+
baseRefName
|
|
4467
|
+
});
|
|
4468
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4469
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4281
4470
|
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})` : ""}`);
|
|
4282
4471
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4283
4472
|
const evidenceBase = {
|
|
@@ -4289,7 +4478,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4289
4478
|
reviewThreads,
|
|
4290
4479
|
checks: checksWithGreptileDetails,
|
|
4291
4480
|
currentHeadSha: headSha,
|
|
4292
|
-
apiSignals
|
|
4481
|
+
apiSignals
|
|
4293
4482
|
};
|
|
4294
4483
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4295
4484
|
return {
|
|
@@ -4300,7 +4489,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4300
4489
|
body: evidenceBase.body,
|
|
4301
4490
|
headSha,
|
|
4302
4491
|
headRefName: firstString(view, ["headRefName"]),
|
|
4303
|
-
baseRefName
|
|
4492
|
+
baseRefName,
|
|
4304
4493
|
state: firstString(view, ["state"]),
|
|
4305
4494
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4306
4495
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4317,66 +4506,224 @@ async function collectPrReviewEvidence(input) {
|
|
|
4317
4506
|
greptile
|
|
4318
4507
|
};
|
|
4319
4508
|
}
|
|
4509
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4510
|
+
const normalized = value.trim();
|
|
4511
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4512
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4513
|
+
}
|
|
4320
4514
|
function evaluateEvidence(evidence) {
|
|
4321
|
-
const
|
|
4515
|
+
const reasonDetails = [];
|
|
4322
4516
|
const warnings = [];
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4517
|
+
const seen = new Set;
|
|
4518
|
+
const addReason = (reason) => {
|
|
4519
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4520
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4521
|
+
if (seen.has(key))
|
|
4522
|
+
return;
|
|
4523
|
+
seen.add(key);
|
|
4524
|
+
reasonDetails.push(capped);
|
|
4525
|
+
};
|
|
4526
|
+
const greptile = evidence.greptile;
|
|
4527
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4528
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4529
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4530
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4531
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4532
|
+
for (const error of evidence.readErrors) {
|
|
4533
|
+
addReason({
|
|
4534
|
+
code: "read_error",
|
|
4535
|
+
reasonClass: "reject",
|
|
4536
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4537
|
+
suggestedAction: "needs_attention",
|
|
4538
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4539
|
+
headSha: evidence.headSha || null
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
if (!evidence.headSha) {
|
|
4543
|
+
addReason({
|
|
4544
|
+
code: "missing_head_sha",
|
|
4545
|
+
reasonClass: "reject",
|
|
4546
|
+
surface: "github",
|
|
4547
|
+
suggestedAction: "needs_attention",
|
|
4548
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4549
|
+
headSha: null
|
|
4550
|
+
});
|
|
4551
|
+
}
|
|
4552
|
+
for (const failure of evidence.checkFailures) {
|
|
4553
|
+
addReason({
|
|
4554
|
+
code: "ci_failed",
|
|
4555
|
+
reasonClass: "reject",
|
|
4556
|
+
surface: "ci",
|
|
4557
|
+
suggestedAction: "fix",
|
|
4558
|
+
message: failure,
|
|
4559
|
+
headSha: evidence.headSha || null
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4563
|
+
addReason({
|
|
4564
|
+
code: "check_pending",
|
|
4565
|
+
reasonClass: "pending",
|
|
4566
|
+
surface: "ci",
|
|
4567
|
+
suggestedAction: "wait",
|
|
4568
|
+
message: pendingCheck,
|
|
4569
|
+
headSha: evidence.headSha || null
|
|
4570
|
+
});
|
|
4334
4571
|
}
|
|
4335
4572
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4336
4573
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4337
|
-
|
|
4574
|
+
addReason({
|
|
4575
|
+
code: "review_decision_blocking",
|
|
4576
|
+
reasonClass: "reject",
|
|
4577
|
+
surface: "review",
|
|
4578
|
+
suggestedAction: "fix",
|
|
4579
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4580
|
+
headSha: evidence.headSha || null
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4584
|
+
addReason({
|
|
4585
|
+
code: "review_thread_unresolved",
|
|
4586
|
+
reasonClass: "reject",
|
|
4587
|
+
surface: "review",
|
|
4588
|
+
suggestedAction: "fix",
|
|
4589
|
+
message: thread,
|
|
4590
|
+
headSha: evidence.headSha || null
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
if (greptile.mapping === "missing") {
|
|
4594
|
+
addReason({
|
|
4595
|
+
code: "greptile_missing",
|
|
4596
|
+
reasonClass: "pending",
|
|
4597
|
+
surface: "greptile",
|
|
4598
|
+
suggestedAction: "wait",
|
|
4599
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4600
|
+
headSha: evidence.headSha || null
|
|
4601
|
+
});
|
|
4338
4602
|
}
|
|
4339
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4340
|
-
if (unresolvedThreads.length > 0)
|
|
4341
|
-
reasons.push(...unresolvedThreads);
|
|
4342
|
-
const greptile = evidence.greptile;
|
|
4343
|
-
if (greptile.mapping === "missing")
|
|
4344
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4345
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4346
4603
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4347
|
-
|
|
4604
|
+
addReason({
|
|
4605
|
+
code: "greptile_stale",
|
|
4606
|
+
reasonClass: "pending",
|
|
4607
|
+
surface: "greptile",
|
|
4608
|
+
suggestedAction: "wait",
|
|
4609
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4610
|
+
headSha: evidence.headSha || null,
|
|
4611
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4615
|
+
addReason({
|
|
4616
|
+
code: "greptile_pending",
|
|
4617
|
+
reasonClass: "pending",
|
|
4618
|
+
surface: "greptile",
|
|
4619
|
+
suggestedAction: "wait",
|
|
4620
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4621
|
+
headSha: evidence.headSha || null,
|
|
4622
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4626
|
+
addReason({
|
|
4627
|
+
code: "greptile_api_status_unknown",
|
|
4628
|
+
reasonClass: "reject",
|
|
4629
|
+
surface: "greptile",
|
|
4630
|
+
suggestedAction: "needs_attention",
|
|
4631
|
+
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}` : "."}`,
|
|
4632
|
+
headSha: evidence.headSha || null,
|
|
4633
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4634
|
+
});
|
|
4348
4635
|
}
|
|
4349
4636
|
if (!greptile.completed) {
|
|
4350
|
-
|
|
4351
|
-
|
|
4637
|
+
addReason({
|
|
4638
|
+
code: "greptile_pending",
|
|
4639
|
+
reasonClass: "pending",
|
|
4640
|
+
surface: "greptile",
|
|
4641
|
+
suggestedAction: "wait",
|
|
4642
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4643
|
+
headSha: evidence.headSha || null,
|
|
4644
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
if (!greptile.fresh) {
|
|
4648
|
+
addReason({
|
|
4649
|
+
code: "greptile_not_current_head",
|
|
4650
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4651
|
+
surface: "greptile",
|
|
4652
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4653
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4654
|
+
headSha: evidence.headSha || null,
|
|
4655
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4656
|
+
});
|
|
4352
4657
|
}
|
|
4353
|
-
if (!greptile.fresh)
|
|
4354
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4355
4658
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4356
|
-
|
|
4659
|
+
addReason({
|
|
4660
|
+
code: "greptile_score_not_5",
|
|
4661
|
+
reasonClass: "reject",
|
|
4662
|
+
surface: "greptile",
|
|
4663
|
+
suggestedAction: "fix",
|
|
4664
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4665
|
+
headSha: evidence.headSha || null,
|
|
4666
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4667
|
+
});
|
|
4357
4668
|
}
|
|
4358
|
-
|
|
4359
|
-
|
|
4669
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4670
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4671
|
+
addReason({
|
|
4672
|
+
code: "greptile_score_missing",
|
|
4673
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4674
|
+
surface: "greptile",
|
|
4675
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4676
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4677
|
+
headSha: evidence.headSha || null,
|
|
4678
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4679
|
+
});
|
|
4360
4680
|
}
|
|
4361
4681
|
if (greptile.mapping === "unproven") {
|
|
4362
|
-
|
|
4682
|
+
addReason({
|
|
4683
|
+
code: "greptile_mapping_unproven",
|
|
4684
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4685
|
+
surface: "greptile",
|
|
4686
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4687
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4688
|
+
headSha: evidence.headSha || null,
|
|
4689
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4690
|
+
});
|
|
4363
4691
|
}
|
|
4364
|
-
|
|
4365
|
-
|
|
4692
|
+
for (const blocker of greptile.blockers) {
|
|
4693
|
+
addReason({
|
|
4694
|
+
code: "greptile_blocker_text",
|
|
4695
|
+
reasonClass: "reject",
|
|
4696
|
+
surface: "greptile",
|
|
4697
|
+
suggestedAction: "fix",
|
|
4698
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4699
|
+
headSha: evidence.headSha || null,
|
|
4700
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4704
|
+
addReason({
|
|
4705
|
+
code: "greptile_unresolved_comment",
|
|
4706
|
+
reasonClass: "reject",
|
|
4707
|
+
surface: "greptile",
|
|
4708
|
+
suggestedAction: "fix",
|
|
4709
|
+
message: comment,
|
|
4710
|
+
headSha: evidence.headSha || null,
|
|
4711
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4712
|
+
});
|
|
4366
4713
|
}
|
|
4367
|
-
if (greptile.unresolvedComments.length > 0)
|
|
4368
|
-
reasons.push(...greptile.unresolvedComments);
|
|
4369
4714
|
if (!greptile.approved)
|
|
4370
4715
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4371
|
-
|
|
4716
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4717
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4372
4718
|
}
|
|
4373
4719
|
function evaluateStrictPrMergeGate(evidence) {
|
|
4374
4720
|
const evaluated = evaluateEvidence(evidence);
|
|
4375
|
-
const approved = evaluated.
|
|
4721
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4376
4722
|
return {
|
|
4377
4723
|
approved,
|
|
4378
4724
|
pending: evaluated.pending,
|
|
4379
4725
|
reasons: evaluated.reasons,
|
|
4726
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4380
4727
|
warnings: evaluated.warnings,
|
|
4381
4728
|
actionableFeedback: evaluated.reasons,
|
|
4382
4729
|
evidence
|
|
@@ -5180,7 +5527,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5180
5527
|
};
|
|
5181
5528
|
}
|
|
5182
5529
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5183
|
-
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)) {
|
|
5530
|
+
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)) {
|
|
5184
5531
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5185
5532
|
return {
|
|
5186
5533
|
verdict: "REJECT",
|
|
@@ -5262,6 +5609,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5262
5609
|
approved: strictGate.approved,
|
|
5263
5610
|
pending: strictGate.pending,
|
|
5264
5611
|
reasons: strictGate.reasons,
|
|
5612
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5265
5613
|
warnings: strictGate.warnings,
|
|
5266
5614
|
greptile: strictGate.evidence.greptile,
|
|
5267
5615
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5284,6 +5632,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5284
5632
|
approved: strictGate.approved,
|
|
5285
5633
|
pending: strictGate.pending,
|
|
5286
5634
|
reasons: strictGate.reasons,
|
|
5635
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5287
5636
|
warnings: strictGate.warnings,
|
|
5288
5637
|
greptile: strictGate.evidence.greptile,
|
|
5289
5638
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5410,6 +5759,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5410
5759
|
approved: strictGate.approved,
|
|
5411
5760
|
pending: strictGate.pending,
|
|
5412
5761
|
reasons: strictGate.reasons,
|
|
5762
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5413
5763
|
warnings: strictGate.warnings,
|
|
5414
5764
|
greptile: strictGate.evidence.greptile
|
|
5415
5765
|
},
|
|
@@ -5432,6 +5782,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5432
5782
|
approved: strictGate.approved,
|
|
5433
5783
|
pending: strictGate.pending,
|
|
5434
5784
|
reasons: strictGate.reasons,
|
|
5785
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5435
5786
|
warnings: strictGate.warnings,
|
|
5436
5787
|
greptile: strictGate.evidence.greptile
|
|
5437
5788
|
},
|
|
@@ -5553,8 +5904,7 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5553
5904
|
if (options.githubCheckState.completed) {
|
|
5554
5905
|
return false;
|
|
5555
5906
|
}
|
|
5556
|
-
|
|
5557
|
-
if (!hasRemainingBudget) {
|
|
5907
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5558
5908
|
return false;
|
|
5559
5909
|
}
|
|
5560
5910
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
@@ -5564,8 +5914,11 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5564
5914
|
}
|
|
5565
5915
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
5566
5916
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
5917
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5918
|
+
return false;
|
|
5919
|
+
}
|
|
5567
5920
|
if (waitingForVisiblePendingReview) {
|
|
5568
|
-
return
|
|
5921
|
+
return true;
|
|
5569
5922
|
}
|
|
5570
5923
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
5571
5924
|
if (reviewNotVisibleYet) {
|
|
@@ -7015,7 +7368,7 @@ function gitOpenPr(options) {
|
|
|
7015
7368
|
"",
|
|
7016
7369
|
"## Review",
|
|
7017
7370
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
7018
|
-
"- When repository policy allows it, Rig
|
|
7371
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
7019
7372
|
].join(`
|
|
7020
7373
|
`);
|
|
7021
7374
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|