@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
|
@@ -3739,41 +3739,6 @@ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
|
3739
3739
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
3740
3740
|
import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
3741
3741
|
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve21 } from "path";
|
|
3742
|
-
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
3743
|
-
"changed-files.txt",
|
|
3744
|
-
"contract-changes.md",
|
|
3745
|
-
"decision-log.md",
|
|
3746
|
-
"git-state.txt",
|
|
3747
|
-
"next-actions.md",
|
|
3748
|
-
"pr-state.json",
|
|
3749
|
-
"task-result.json",
|
|
3750
|
-
"validation-summary.json"
|
|
3751
|
-
]);
|
|
3752
|
-
function readPrMetadata(projectRoot, taskId) {
|
|
3753
|
-
const path = resolve21(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
3754
|
-
if (!existsSync18(path)) {
|
|
3755
|
-
return [];
|
|
3756
|
-
}
|
|
3757
|
-
try {
|
|
3758
|
-
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
3759
|
-
if (!parsed || typeof parsed !== "object") {
|
|
3760
|
-
return [];
|
|
3761
|
-
}
|
|
3762
|
-
if (parsed.prs && typeof parsed.prs === "object") {
|
|
3763
|
-
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
3764
|
-
}
|
|
3765
|
-
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
3766
|
-
} catch {
|
|
3767
|
-
return [];
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
function isGitOpenPrResult(value) {
|
|
3771
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3772
|
-
return false;
|
|
3773
|
-
}
|
|
3774
|
-
const record = value;
|
|
3775
|
-
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
3776
|
-
}
|
|
3777
3742
|
|
|
3778
3743
|
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3779
3744
|
function parseJsonObject(value) {
|
|
@@ -3883,13 +3848,7 @@ function stripHtml(input) {
|
|
|
3883
3848
|
}
|
|
3884
3849
|
function containsBlockerText(input) {
|
|
3885
3850
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3886
|
-
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);
|
|
3887
|
-
}
|
|
3888
|
-
function containsGreptileNegativeVerdict(input) {
|
|
3889
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3890
|
-
if (!text)
|
|
3891
|
-
return false;
|
|
3892
|
-
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);
|
|
3851
|
+
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);
|
|
3893
3852
|
}
|
|
3894
3853
|
function isStrictFiveOfFive(score) {
|
|
3895
3854
|
return score.value === 5 && score.scale === 5;
|
|
@@ -3897,6 +3856,189 @@ function isStrictFiveOfFive(score) {
|
|
|
3897
3856
|
function containsConflictingScoreText(input) {
|
|
3898
3857
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3899
3858
|
}
|
|
3859
|
+
function greptileStatusVerdict(status) {
|
|
3860
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3861
|
+
if (!normalized)
|
|
3862
|
+
return null;
|
|
3863
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3864
|
+
return "approved";
|
|
3865
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3866
|
+
return "rejected";
|
|
3867
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3868
|
+
return "skipped";
|
|
3869
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3870
|
+
return "failed";
|
|
3871
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3872
|
+
return "pending";
|
|
3873
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3874
|
+
return "completed";
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3878
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3879
|
+
}
|
|
3880
|
+
function greptileRequestTimeoutMs(env) {
|
|
3881
|
+
const fallback = 30000;
|
|
3882
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3883
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3884
|
+
}
|
|
3885
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3886
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3887
|
+
return null;
|
|
3888
|
+
const record = entry;
|
|
3889
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3890
|
+
if (!id)
|
|
3891
|
+
return null;
|
|
3892
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3893
|
+
return {
|
|
3894
|
+
id,
|
|
3895
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3896
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3897
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3898
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3902
|
+
const seen = new Set;
|
|
3903
|
+
const unique2 = [];
|
|
3904
|
+
for (const review of reviews) {
|
|
3905
|
+
if (seen.has(review.id))
|
|
3906
|
+
continue;
|
|
3907
|
+
seen.add(review.id);
|
|
3908
|
+
unique2.push(review);
|
|
3909
|
+
}
|
|
3910
|
+
return unique2;
|
|
3911
|
+
}
|
|
3912
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3913
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3914
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3915
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3916
|
+
const latest = sorted.slice(0, 1);
|
|
3917
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3918
|
+
}
|
|
3919
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3920
|
+
const selected = details ?? review;
|
|
3921
|
+
return {
|
|
3922
|
+
id: selected.id || review.id,
|
|
3923
|
+
body: selected.body ?? review.body ?? null,
|
|
3924
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3925
|
+
status: selected.status ?? review.status ?? null
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3929
|
+
const controller = new AbortController;
|
|
3930
|
+
const timeoutId = setTimeout(() => {
|
|
3931
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3932
|
+
}, input.timeoutMs);
|
|
3933
|
+
let response;
|
|
3934
|
+
try {
|
|
3935
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3936
|
+
method: "POST",
|
|
3937
|
+
headers: {
|
|
3938
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3939
|
+
"Content-Type": "application/json"
|
|
3940
|
+
},
|
|
3941
|
+
body: JSON.stringify({
|
|
3942
|
+
jsonrpc: "2.0",
|
|
3943
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3944
|
+
method: "tools/call",
|
|
3945
|
+
params: { name: input.name, arguments: input.args }
|
|
3946
|
+
}),
|
|
3947
|
+
signal: controller.signal
|
|
3948
|
+
});
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
if (controller.signal.aborted) {
|
|
3951
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3952
|
+
}
|
|
3953
|
+
throw error;
|
|
3954
|
+
} finally {
|
|
3955
|
+
clearTimeout(timeoutId);
|
|
3956
|
+
}
|
|
3957
|
+
const raw = await response.text();
|
|
3958
|
+
if (!response.ok) {
|
|
3959
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3960
|
+
}
|
|
3961
|
+
let envelope;
|
|
3962
|
+
try {
|
|
3963
|
+
envelope = JSON.parse(raw);
|
|
3964
|
+
} catch {
|
|
3965
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3966
|
+
}
|
|
3967
|
+
if (envelope.error?.message) {
|
|
3968
|
+
throw new Error(envelope.error.message);
|
|
3969
|
+
}
|
|
3970
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3971
|
+
`).trim();
|
|
3972
|
+
if (!text) {
|
|
3973
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3974
|
+
}
|
|
3975
|
+
return text;
|
|
3976
|
+
}
|
|
3977
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3978
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3979
|
+
try {
|
|
3980
|
+
return JSON.parse(text);
|
|
3981
|
+
} catch {
|
|
3982
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3986
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3987
|
+
return { signals: [], errors: [] };
|
|
3988
|
+
}
|
|
3989
|
+
const env = input.options?.env ?? process.env;
|
|
3990
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3991
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3992
|
+
if (!apiKey) {
|
|
3993
|
+
return { signals: [], errors: [] };
|
|
3994
|
+
}
|
|
3995
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3996
|
+
if (typeof fetchFn !== "function") {
|
|
3997
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3998
|
+
}
|
|
3999
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
4000
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
4001
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
4002
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
4003
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
4004
|
+
try {
|
|
4005
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
4006
|
+
apiBase,
|
|
4007
|
+
apiKey,
|
|
4008
|
+
name: "list_code_reviews",
|
|
4009
|
+
args: {
|
|
4010
|
+
name: repository,
|
|
4011
|
+
remote,
|
|
4012
|
+
defaultBranch,
|
|
4013
|
+
prNumber: input.prNumber,
|
|
4014
|
+
limit: 20
|
|
4015
|
+
},
|
|
4016
|
+
timeoutMs,
|
|
4017
|
+
fetchFn
|
|
4018
|
+
});
|
|
4019
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4020
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4021
|
+
const signals = [];
|
|
4022
|
+
for (const review of selectedReviews) {
|
|
4023
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4024
|
+
apiBase,
|
|
4025
|
+
apiKey,
|
|
4026
|
+
name: "get_code_review",
|
|
4027
|
+
args: { codeReviewId: review.id },
|
|
4028
|
+
timeoutMs,
|
|
4029
|
+
fetchFn
|
|
4030
|
+
});
|
|
4031
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4032
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4033
|
+
}
|
|
4034
|
+
return { signals, errors: [] };
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
return {
|
|
4037
|
+
signals: [],
|
|
4038
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3900
4042
|
function firstString(record, keys) {
|
|
3901
4043
|
for (const key of keys) {
|
|
3902
4044
|
const value = record[key];
|
|
@@ -4023,7 +4165,7 @@ function normalizeReviewThread(entry) {
|
|
|
4023
4165
|
function relevantIssueComment(comment) {
|
|
4024
4166
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4025
4167
|
const body = comment.body ?? "";
|
|
4026
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4168
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4027
4169
|
}
|
|
4028
4170
|
function latestThreadComment(thread) {
|
|
4029
4171
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -4059,7 +4201,8 @@ function makeGreptileSignal(input) {
|
|
|
4059
4201
|
const scores = parseGreptileScores(input.body);
|
|
4060
4202
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4061
4203
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4062
|
-
const
|
|
4204
|
+
const verdict = input.verdict ?? null;
|
|
4205
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4063
4206
|
const explicitApproval = input.explicitApproval ?? false;
|
|
4064
4207
|
return {
|
|
4065
4208
|
source: input.source,
|
|
@@ -4071,6 +4214,7 @@ function makeGreptileSignal(input) {
|
|
|
4071
4214
|
score: scores[0] ?? null,
|
|
4072
4215
|
scores,
|
|
4073
4216
|
explicitApproval,
|
|
4217
|
+
verdict,
|
|
4074
4218
|
blocker,
|
|
4075
4219
|
actionable: input.actionable ?? blocker,
|
|
4076
4220
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -4093,9 +4237,9 @@ function collectGreptileSignals(evidence) {
|
|
|
4093
4237
|
for (const context of contextSources) {
|
|
4094
4238
|
if (!context.body.trim())
|
|
4095
4239
|
continue;
|
|
4096
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
4097
|
-
continue;
|
|
4098
4240
|
const contextBlocker = containsBlockerText(context.body);
|
|
4241
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4242
|
+
continue;
|
|
4099
4243
|
signals.push(makeGreptileSignal({
|
|
4100
4244
|
source: context.source,
|
|
4101
4245
|
body: context.body,
|
|
@@ -4108,16 +4252,16 @@ function collectGreptileSignals(evidence) {
|
|
|
4108
4252
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4109
4253
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4110
4254
|
|
|
4111
|
-
`);
|
|
4112
|
-
|
|
4113
|
-
continue;
|
|
4255
|
+
`) || "Status: UNKNOWN";
|
|
4256
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4114
4257
|
signals.push(makeGreptileSignal({
|
|
4115
4258
|
source: "api",
|
|
4116
4259
|
body,
|
|
4117
4260
|
currentHeadSha: evidence.currentHeadSha,
|
|
4118
4261
|
trusted: true,
|
|
4119
4262
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4120
|
-
explicitApproval:
|
|
4263
|
+
explicitApproval: verdict === "approved",
|
|
4264
|
+
verdict
|
|
4121
4265
|
}));
|
|
4122
4266
|
}
|
|
4123
4267
|
for (const review of evidence.reviews) {
|
|
@@ -4142,20 +4286,6 @@ function collectGreptileSignals(evidence) {
|
|
|
4142
4286
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4143
4287
|
}));
|
|
4144
4288
|
}
|
|
4145
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
4146
|
-
const login = commentAuthorLogin(comment);
|
|
4147
|
-
const body = comment.body ?? "";
|
|
4148
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4149
|
-
continue;
|
|
4150
|
-
signals.push(makeGreptileSignal({
|
|
4151
|
-
source: "changed-file-comment",
|
|
4152
|
-
body,
|
|
4153
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
4154
|
-
trusted: true,
|
|
4155
|
-
authorLogin: login,
|
|
4156
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
4157
|
-
}));
|
|
4158
|
-
}
|
|
4159
4289
|
for (const comment of evidence.relevantIssueComments) {
|
|
4160
4290
|
const login = commentAuthorLogin(comment);
|
|
4161
4291
|
const body = comment.body ?? "";
|
|
@@ -4221,6 +4351,9 @@ function unresolvedGreptileThreadSummaries(threads) {
|
|
|
4221
4351
|
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4222
4352
|
});
|
|
4223
4353
|
}
|
|
4354
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4355
|
+
return [];
|
|
4356
|
+
}
|
|
4224
4357
|
function issueLevelBlockerSummaries(comments) {
|
|
4225
4358
|
return comments.flatMap((comment) => {
|
|
4226
4359
|
const body = comment.body?.trim() ?? "";
|
|
@@ -4260,14 +4393,21 @@ function deriveGreptileEvidence(input) {
|
|
|
4260
4393
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4261
4394
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4262
4395
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4263
|
-
const
|
|
4396
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4397
|
+
const signalCanApproveByScore = (signal) => {
|
|
4398
|
+
if (signal.source === "api")
|
|
4399
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4400
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4401
|
+
};
|
|
4402
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4403
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4264
4404
|
const approvedByScore = !!approvingScoreEntry;
|
|
4265
|
-
const approvedByExplicitMapping =
|
|
4266
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4405
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4406
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4267
4407
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4268
4408
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4269
4409
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4270
|
-
const blockerSignals = signals.filter((signal) =>
|
|
4410
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4271
4411
|
const staleBlockingSignals = [];
|
|
4272
4412
|
const blockers = [
|
|
4273
4413
|
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
@@ -4278,7 +4418,8 @@ function deriveGreptileEvidence(input) {
|
|
|
4278
4418
|
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4279
4419
|
];
|
|
4280
4420
|
const unresolvedComments = [
|
|
4281
|
-
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4421
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4422
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4282
4423
|
];
|
|
4283
4424
|
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4284
4425
|
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
@@ -4291,13 +4432,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4291
4432
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4292
4433
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4293
4434
|
});
|
|
4435
|
+
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"));
|
|
4294
4436
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4295
4437
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4296
4438
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4297
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4439
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4298
4440
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4299
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4300
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4441
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4442
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4301
4443
|
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";
|
|
4302
4444
|
return {
|
|
4303
4445
|
source,
|
|
@@ -4404,6 +4546,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4404
4546
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4405
4547
|
}
|
|
4406
4548
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4549
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4407
4550
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4408
4551
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4409
4552
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4441,6 +4584,17 @@ async function collectPrReviewEvidence(input) {
|
|
|
4441
4584
|
}
|
|
4442
4585
|
}
|
|
4443
4586
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4587
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4588
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4589
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4590
|
+
options: input.greptileApi,
|
|
4591
|
+
repoName: parsed.repoName,
|
|
4592
|
+
prNumber: parsed.prNumber,
|
|
4593
|
+
headSha,
|
|
4594
|
+
baseRefName
|
|
4595
|
+
});
|
|
4596
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4597
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4444
4598
|
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})` : ""}`);
|
|
4445
4599
|
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4446
4600
|
const evidenceBase = {
|
|
@@ -4452,7 +4606,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4452
4606
|
reviewThreads,
|
|
4453
4607
|
checks: checksWithGreptileDetails,
|
|
4454
4608
|
currentHeadSha: headSha,
|
|
4455
|
-
apiSignals
|
|
4609
|
+
apiSignals
|
|
4456
4610
|
};
|
|
4457
4611
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4458
4612
|
return {
|
|
@@ -4463,7 +4617,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4463
4617
|
body: evidenceBase.body,
|
|
4464
4618
|
headSha,
|
|
4465
4619
|
headRefName: firstString(view, ["headRefName"]),
|
|
4466
|
-
baseRefName
|
|
4620
|
+
baseRefName,
|
|
4467
4621
|
state: firstString(view, ["state"]),
|
|
4468
4622
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4469
4623
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4480,72 +4634,267 @@ async function collectPrReviewEvidence(input) {
|
|
|
4480
4634
|
greptile
|
|
4481
4635
|
};
|
|
4482
4636
|
}
|
|
4637
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4638
|
+
const normalized = value.trim();
|
|
4639
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4640
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4641
|
+
}
|
|
4483
4642
|
function evaluateEvidence(evidence) {
|
|
4484
|
-
const
|
|
4643
|
+
const reasonDetails = [];
|
|
4485
4644
|
const warnings = [];
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4645
|
+
const seen = new Set;
|
|
4646
|
+
const addReason = (reason) => {
|
|
4647
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4648
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4649
|
+
if (seen.has(key))
|
|
4650
|
+
return;
|
|
4651
|
+
seen.add(key);
|
|
4652
|
+
reasonDetails.push(capped);
|
|
4653
|
+
};
|
|
4654
|
+
const greptile = evidence.greptile;
|
|
4655
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4656
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4657
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4658
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4659
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4660
|
+
for (const error of evidence.readErrors) {
|
|
4661
|
+
addReason({
|
|
4662
|
+
code: "read_error",
|
|
4663
|
+
reasonClass: "reject",
|
|
4664
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4665
|
+
suggestedAction: "needs_attention",
|
|
4666
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4667
|
+
headSha: evidence.headSha || null
|
|
4668
|
+
});
|
|
4669
|
+
}
|
|
4670
|
+
if (!evidence.headSha) {
|
|
4671
|
+
addReason({
|
|
4672
|
+
code: "missing_head_sha",
|
|
4673
|
+
reasonClass: "reject",
|
|
4674
|
+
surface: "github",
|
|
4675
|
+
suggestedAction: "needs_attention",
|
|
4676
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4677
|
+
headSha: null
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
for (const failure of evidence.checkFailures) {
|
|
4681
|
+
addReason({
|
|
4682
|
+
code: "ci_failed",
|
|
4683
|
+
reasonClass: "reject",
|
|
4684
|
+
surface: "ci",
|
|
4685
|
+
suggestedAction: "fix",
|
|
4686
|
+
message: failure,
|
|
4687
|
+
headSha: evidence.headSha || null
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4691
|
+
addReason({
|
|
4692
|
+
code: "check_pending",
|
|
4693
|
+
reasonClass: "pending",
|
|
4694
|
+
surface: "ci",
|
|
4695
|
+
suggestedAction: "wait",
|
|
4696
|
+
message: pendingCheck,
|
|
4697
|
+
headSha: evidence.headSha || null
|
|
4698
|
+
});
|
|
4497
4699
|
}
|
|
4498
4700
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4499
4701
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4500
|
-
|
|
4702
|
+
addReason({
|
|
4703
|
+
code: "review_decision_blocking",
|
|
4704
|
+
reasonClass: "reject",
|
|
4705
|
+
surface: "review",
|
|
4706
|
+
suggestedAction: "fix",
|
|
4707
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4708
|
+
headSha: evidence.headSha || null
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4712
|
+
addReason({
|
|
4713
|
+
code: "review_thread_unresolved",
|
|
4714
|
+
reasonClass: "reject",
|
|
4715
|
+
surface: "review",
|
|
4716
|
+
suggestedAction: "fix",
|
|
4717
|
+
message: thread,
|
|
4718
|
+
headSha: evidence.headSha || null
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
if (greptile.mapping === "missing") {
|
|
4722
|
+
addReason({
|
|
4723
|
+
code: "greptile_missing",
|
|
4724
|
+
reasonClass: "pending",
|
|
4725
|
+
surface: "greptile",
|
|
4726
|
+
suggestedAction: "wait",
|
|
4727
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4728
|
+
headSha: evidence.headSha || null
|
|
4729
|
+
});
|
|
4501
4730
|
}
|
|
4502
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4503
|
-
if (unresolvedThreads.length > 0)
|
|
4504
|
-
reasons.push(...unresolvedThreads);
|
|
4505
|
-
const greptile = evidence.greptile;
|
|
4506
|
-
if (greptile.mapping === "missing")
|
|
4507
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4508
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4509
4731
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4510
|
-
|
|
4732
|
+
addReason({
|
|
4733
|
+
code: "greptile_stale",
|
|
4734
|
+
reasonClass: "pending",
|
|
4735
|
+
surface: "greptile",
|
|
4736
|
+
suggestedAction: "wait",
|
|
4737
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4738
|
+
headSha: evidence.headSha || null,
|
|
4739
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4743
|
+
addReason({
|
|
4744
|
+
code: "greptile_pending",
|
|
4745
|
+
reasonClass: "pending",
|
|
4746
|
+
surface: "greptile",
|
|
4747
|
+
suggestedAction: "wait",
|
|
4748
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4749
|
+
headSha: evidence.headSha || null,
|
|
4750
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4754
|
+
addReason({
|
|
4755
|
+
code: "greptile_api_status_unknown",
|
|
4756
|
+
reasonClass: "reject",
|
|
4757
|
+
surface: "greptile",
|
|
4758
|
+
suggestedAction: "needs_attention",
|
|
4759
|
+
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}` : "."}`,
|
|
4760
|
+
headSha: evidence.headSha || null,
|
|
4761
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4762
|
+
});
|
|
4511
4763
|
}
|
|
4512
4764
|
if (!greptile.completed) {
|
|
4513
|
-
|
|
4514
|
-
|
|
4765
|
+
addReason({
|
|
4766
|
+
code: "greptile_pending",
|
|
4767
|
+
reasonClass: "pending",
|
|
4768
|
+
surface: "greptile",
|
|
4769
|
+
suggestedAction: "wait",
|
|
4770
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4771
|
+
headSha: evidence.headSha || null,
|
|
4772
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
if (!greptile.fresh) {
|
|
4776
|
+
addReason({
|
|
4777
|
+
code: "greptile_not_current_head",
|
|
4778
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4779
|
+
surface: "greptile",
|
|
4780
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4781
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4782
|
+
headSha: evidence.headSha || null,
|
|
4783
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4784
|
+
});
|
|
4515
4785
|
}
|
|
4516
|
-
if (!greptile.fresh)
|
|
4517
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4518
4786
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4519
|
-
|
|
4787
|
+
addReason({
|
|
4788
|
+
code: "greptile_score_not_5",
|
|
4789
|
+
reasonClass: "reject",
|
|
4790
|
+
surface: "greptile",
|
|
4791
|
+
suggestedAction: "fix",
|
|
4792
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4793
|
+
headSha: evidence.headSha || null,
|
|
4794
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4795
|
+
});
|
|
4520
4796
|
}
|
|
4521
|
-
|
|
4522
|
-
|
|
4797
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4798
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4799
|
+
addReason({
|
|
4800
|
+
code: "greptile_score_missing",
|
|
4801
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4802
|
+
surface: "greptile",
|
|
4803
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4804
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4805
|
+
headSha: evidence.headSha || null,
|
|
4806
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4807
|
+
});
|
|
4523
4808
|
}
|
|
4524
4809
|
if (greptile.mapping === "unproven") {
|
|
4525
|
-
|
|
4810
|
+
addReason({
|
|
4811
|
+
code: "greptile_mapping_unproven",
|
|
4812
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4813
|
+
surface: "greptile",
|
|
4814
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4815
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4816
|
+
headSha: evidence.headSha || null,
|
|
4817
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4818
|
+
});
|
|
4526
4819
|
}
|
|
4527
|
-
|
|
4528
|
-
|
|
4820
|
+
for (const blocker of greptile.blockers) {
|
|
4821
|
+
addReason({
|
|
4822
|
+
code: "greptile_blocker_text",
|
|
4823
|
+
reasonClass: "reject",
|
|
4824
|
+
surface: "greptile",
|
|
4825
|
+
suggestedAction: "fix",
|
|
4826
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4827
|
+
headSha: evidence.headSha || null,
|
|
4828
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4832
|
+
addReason({
|
|
4833
|
+
code: "greptile_unresolved_comment",
|
|
4834
|
+
reasonClass: "reject",
|
|
4835
|
+
surface: "greptile",
|
|
4836
|
+
suggestedAction: "fix",
|
|
4837
|
+
message: comment,
|
|
4838
|
+
headSha: evidence.headSha || null,
|
|
4839
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4840
|
+
});
|
|
4529
4841
|
}
|
|
4530
|
-
if (greptile.unresolvedComments.length > 0)
|
|
4531
|
-
reasons.push(...greptile.unresolvedComments);
|
|
4532
4842
|
if (!greptile.approved)
|
|
4533
4843
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4534
|
-
|
|
4844
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4845
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4535
4846
|
}
|
|
4536
4847
|
function evaluateStrictPrMergeGate(evidence) {
|
|
4537
4848
|
const evaluated = evaluateEvidence(evidence);
|
|
4538
|
-
const approved = evaluated.
|
|
4849
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4539
4850
|
return {
|
|
4540
4851
|
approved,
|
|
4541
4852
|
pending: evaluated.pending,
|
|
4542
4853
|
reasons: evaluated.reasons,
|
|
4854
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4543
4855
|
warnings: evaluated.warnings,
|
|
4544
4856
|
actionableFeedback: evaluated.reasons,
|
|
4545
4857
|
evidence
|
|
4546
4858
|
};
|
|
4547
4859
|
}
|
|
4548
4860
|
|
|
4861
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
4862
|
+
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
4863
|
+
"changed-files.txt",
|
|
4864
|
+
"contract-changes.md",
|
|
4865
|
+
"decision-log.md",
|
|
4866
|
+
"git-state.txt",
|
|
4867
|
+
"next-actions.md",
|
|
4868
|
+
"pr-state.json",
|
|
4869
|
+
"task-result.json",
|
|
4870
|
+
"validation-summary.json"
|
|
4871
|
+
]);
|
|
4872
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
4873
|
+
const path = resolve21(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
4874
|
+
if (!existsSync18(path)) {
|
|
4875
|
+
return [];
|
|
4876
|
+
}
|
|
4877
|
+
try {
|
|
4878
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
4879
|
+
if (!parsed || typeof parsed !== "object") {
|
|
4880
|
+
return [];
|
|
4881
|
+
}
|
|
4882
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
4883
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
4884
|
+
}
|
|
4885
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
4886
|
+
} catch {
|
|
4887
|
+
return [];
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
function isGitOpenPrResult(value) {
|
|
4891
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4892
|
+
return false;
|
|
4893
|
+
}
|
|
4894
|
+
const record = value;
|
|
4895
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4549
4898
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
4550
4899
|
async function verifyTask(options) {
|
|
4551
4900
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -5343,7 +5692,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5343
5692
|
};
|
|
5344
5693
|
}
|
|
5345
5694
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5346
|
-
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)) {
|
|
5695
|
+
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)) {
|
|
5347
5696
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5348
5697
|
return {
|
|
5349
5698
|
verdict: "REJECT",
|
|
@@ -5425,6 +5774,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5425
5774
|
approved: strictGate.approved,
|
|
5426
5775
|
pending: strictGate.pending,
|
|
5427
5776
|
reasons: strictGate.reasons,
|
|
5777
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5428
5778
|
warnings: strictGate.warnings,
|
|
5429
5779
|
greptile: strictGate.evidence.greptile,
|
|
5430
5780
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5447,6 +5797,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5447
5797
|
approved: strictGate.approved,
|
|
5448
5798
|
pending: strictGate.pending,
|
|
5449
5799
|
reasons: strictGate.reasons,
|
|
5800
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5450
5801
|
warnings: strictGate.warnings,
|
|
5451
5802
|
greptile: strictGate.evidence.greptile,
|
|
5452
5803
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5573,6 +5924,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5573
5924
|
approved: strictGate.approved,
|
|
5574
5925
|
pending: strictGate.pending,
|
|
5575
5926
|
reasons: strictGate.reasons,
|
|
5927
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5576
5928
|
warnings: strictGate.warnings,
|
|
5577
5929
|
greptile: strictGate.evidence.greptile
|
|
5578
5930
|
},
|
|
@@ -5595,6 +5947,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5595
5947
|
approved: strictGate.approved,
|
|
5596
5948
|
pending: strictGate.pending,
|
|
5597
5949
|
reasons: strictGate.reasons,
|
|
5950
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5598
5951
|
warnings: strictGate.warnings,
|
|
5599
5952
|
greptile: strictGate.evidence.greptile
|
|
5600
5953
|
},
|
|
@@ -5716,8 +6069,7 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5716
6069
|
if (options.githubCheckState.completed) {
|
|
5717
6070
|
return false;
|
|
5718
6071
|
}
|
|
5719
|
-
|
|
5720
|
-
if (!hasRemainingBudget) {
|
|
6072
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5721
6073
|
return false;
|
|
5722
6074
|
}
|
|
5723
6075
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
@@ -5727,8 +6079,11 @@ function shouldContinueGreptileMcpPolling(options) {
|
|
|
5727
6079
|
}
|
|
5728
6080
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
5729
6081
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6082
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
5730
6085
|
if (waitingForVisiblePendingReview) {
|
|
5731
|
-
return
|
|
6086
|
+
return true;
|
|
5732
6087
|
}
|
|
5733
6088
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
5734
6089
|
if (reviewNotVisibleYet) {
|