@h-rig/runtime 0.0.6-alpha.11 → 0.0.6-alpha.12
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-dispatch.js +5 -313
- package/dist/bin/rig-agent.js +3 -2
- package/dist/src/control-plane/agent-wrapper.js +10 -15
- package/dist/src/control-plane/harness-main.js +923 -156
- package/dist/src/control-plane/hooks/completion-verification.js +1191 -284
- package/dist/src/control-plane/native/git-ops.js +31 -43
- package/dist/src/control-plane/native/harness-cli.js +923 -156
- package/dist/src/control-plane/native/pr-automation.js +1010 -38
- package/dist/src/control-plane/native/pr-review-gate.js +907 -0
- package/dist/src/control-plane/native/task-ops.js +918 -154
- package/dist/src/control-plane/native/verifier.js +920 -153
- 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/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +6 -6
|
@@ -3775,6 +3775,777 @@ function isGitOpenPrResult(value) {
|
|
|
3775
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
3776
|
}
|
|
3777
3777
|
|
|
3778
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3779
|
+
function parseJsonObject(value) {
|
|
3780
|
+
if (!value?.trim())
|
|
3781
|
+
return { value: {}, error: "empty JSON output" };
|
|
3782
|
+
try {
|
|
3783
|
+
const parsed = JSON.parse(value);
|
|
3784
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3785
|
+
} catch (error) {
|
|
3786
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
function flattenPaginatedArray(value) {
|
|
3790
|
+
if (!Array.isArray(value))
|
|
3791
|
+
return null;
|
|
3792
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3793
|
+
return value.flatMap((entry) => entry);
|
|
3794
|
+
}
|
|
3795
|
+
return value;
|
|
3796
|
+
}
|
|
3797
|
+
function parseJsonArray(value) {
|
|
3798
|
+
if (!value?.trim())
|
|
3799
|
+
return { value: [], error: "empty JSON output" };
|
|
3800
|
+
try {
|
|
3801
|
+
const parsed = JSON.parse(value);
|
|
3802
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3803
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
function parseGithubPrUrl(prUrl) {
|
|
3809
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3810
|
+
if (!match)
|
|
3811
|
+
return null;
|
|
3812
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3813
|
+
if (!Number.isFinite(prNumber))
|
|
3814
|
+
return null;
|
|
3815
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3816
|
+
}
|
|
3817
|
+
function checkName(check) {
|
|
3818
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3819
|
+
}
|
|
3820
|
+
function checkState(check) {
|
|
3821
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3822
|
+
}
|
|
3823
|
+
function isGreptileLabel(value) {
|
|
3824
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3825
|
+
}
|
|
3826
|
+
function isGreptileGithubLogin(value) {
|
|
3827
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3828
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3829
|
+
}
|
|
3830
|
+
function isPassingCheck(check) {
|
|
3831
|
+
const state = checkState(check);
|
|
3832
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3833
|
+
}
|
|
3834
|
+
function isPendingCheck(check) {
|
|
3835
|
+
const state = checkState(check);
|
|
3836
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3837
|
+
}
|
|
3838
|
+
function isFailingCheck(check) {
|
|
3839
|
+
const state = checkState(check);
|
|
3840
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3841
|
+
}
|
|
3842
|
+
function wildcardToRegExp(pattern) {
|
|
3843
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3844
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3845
|
+
}
|
|
3846
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3847
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3848
|
+
}
|
|
3849
|
+
function greptileScorePatterns() {
|
|
3850
|
+
return [
|
|
3851
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3852
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3853
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3854
|
+
];
|
|
3855
|
+
}
|
|
3856
|
+
function parseGreptileScores(input) {
|
|
3857
|
+
const text = stripHtml(input);
|
|
3858
|
+
const seen = new Set;
|
|
3859
|
+
const scores = [];
|
|
3860
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3861
|
+
for (const match of text.matchAll(pattern)) {
|
|
3862
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3863
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3864
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3865
|
+
continue;
|
|
3866
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3867
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3868
|
+
if (seen.has(key))
|
|
3869
|
+
continue;
|
|
3870
|
+
seen.add(key);
|
|
3871
|
+
scores.push({ value, scale, raw });
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return scores;
|
|
3875
|
+
}
|
|
3876
|
+
function parseGreptileScore(input) {
|
|
3877
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3878
|
+
}
|
|
3879
|
+
function stripHtml(input) {
|
|
3880
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3881
|
+
|
|
3882
|
+
`).trim();
|
|
3883
|
+
}
|
|
3884
|
+
function containsBlockerText(input) {
|
|
3885
|
+
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);
|
|
3893
|
+
}
|
|
3894
|
+
function isStrictFiveOfFive(score) {
|
|
3895
|
+
return score.value === 5 && score.scale === 5;
|
|
3896
|
+
}
|
|
3897
|
+
function containsConflictingScoreText(input) {
|
|
3898
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3899
|
+
}
|
|
3900
|
+
function firstString(record, keys) {
|
|
3901
|
+
for (const key of keys) {
|
|
3902
|
+
const value = record[key];
|
|
3903
|
+
if (typeof value === "string")
|
|
3904
|
+
return value;
|
|
3905
|
+
}
|
|
3906
|
+
return "";
|
|
3907
|
+
}
|
|
3908
|
+
function arrayField(record, key) {
|
|
3909
|
+
const value = record[key];
|
|
3910
|
+
return Array.isArray(value) ? value : [];
|
|
3911
|
+
}
|
|
3912
|
+
async function runJsonArray(command, args, cwd) {
|
|
3913
|
+
const result = await command(args, { cwd });
|
|
3914
|
+
const label = `gh ${args.join(" ")}`;
|
|
3915
|
+
if (result.exitCode !== 0) {
|
|
3916
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3917
|
+
}
|
|
3918
|
+
const parsed = parseJsonArray(result.stdout);
|
|
3919
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3920
|
+
}
|
|
3921
|
+
async function runJsonObject(command, args, cwd) {
|
|
3922
|
+
const result = await command(args, { cwd });
|
|
3923
|
+
const label = `gh ${args.join(" ")}`;
|
|
3924
|
+
if (result.exitCode !== 0) {
|
|
3925
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3926
|
+
}
|
|
3927
|
+
const parsed = parseJsonObject(result.stdout);
|
|
3928
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3929
|
+
}
|
|
3930
|
+
function normalizeStatusCheck(entry) {
|
|
3931
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3932
|
+
return null;
|
|
3933
|
+
const record = entry;
|
|
3934
|
+
const name = firstString(record, ["name", "context"]);
|
|
3935
|
+
if (!name.trim())
|
|
3936
|
+
return null;
|
|
3937
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
3938
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
3939
|
+
return {
|
|
3940
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
3941
|
+
name,
|
|
3942
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
3943
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3944
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3945
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
3946
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.details_url === "string" ? record.details_url : typeof record.html_url === "string" ? record.html_url : typeof record.link === "string" ? record.link : null,
|
|
3947
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
3948
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
3949
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
3950
|
+
output: output ? {
|
|
3951
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
3952
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
3953
|
+
text: typeof output.text === "string" ? output.text : null
|
|
3954
|
+
} : null,
|
|
3955
|
+
app: app ? {
|
|
3956
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
3957
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
3958
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
3959
|
+
} : null
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
function normalizeReview(entry) {
|
|
3963
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3964
|
+
return null;
|
|
3965
|
+
const record = entry;
|
|
3966
|
+
return {
|
|
3967
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
3968
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3969
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3970
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : typeof record.commitId === "string" ? record.commitId : record.commit && typeof record.commit === "object" && typeof record.commit.oid === "string" ? record.commit.oid : null,
|
|
3971
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
3972
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
function normalizeReviewComment(entry) {
|
|
3976
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3977
|
+
return null;
|
|
3978
|
+
const record = entry;
|
|
3979
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3980
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
3981
|
+
if (!body && !path)
|
|
3982
|
+
return null;
|
|
3983
|
+
return {
|
|
3984
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3985
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3986
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3987
|
+
body,
|
|
3988
|
+
path,
|
|
3989
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3990
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3991
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
3992
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
function normalizeIssueComment(entry) {
|
|
3996
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3997
|
+
return null;
|
|
3998
|
+
const record = entry;
|
|
3999
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4000
|
+
if (!body)
|
|
4001
|
+
return null;
|
|
4002
|
+
return {
|
|
4003
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4004
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4005
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4006
|
+
body,
|
|
4007
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4008
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4009
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
function normalizeReviewThread(entry) {
|
|
4013
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4014
|
+
return null;
|
|
4015
|
+
const record = entry;
|
|
4016
|
+
return {
|
|
4017
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4018
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4019
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4020
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
function relevantIssueComment(comment) {
|
|
4024
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4025
|
+
const body = comment.body ?? "";
|
|
4026
|
+
return isGreptileGithubLogin(login) || /greptile|blocker|unsafe|not safe|do not merge|must fix|please fix|changes requested|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4027
|
+
}
|
|
4028
|
+
function latestThreadComment(thread) {
|
|
4029
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4030
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4031
|
+
}
|
|
4032
|
+
function unresolvedThreadSummaries(threads) {
|
|
4033
|
+
return threads.flatMap((thread) => {
|
|
4034
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4035
|
+
return [];
|
|
4036
|
+
const latest = latestThreadComment(thread);
|
|
4037
|
+
if (!latest)
|
|
4038
|
+
return ["Unresolved review thread"];
|
|
4039
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4040
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
function collectBodies(evidence) {
|
|
4044
|
+
return [
|
|
4045
|
+
evidence.title ?? "",
|
|
4046
|
+
evidence.body,
|
|
4047
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4048
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4049
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4050
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4051
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4052
|
+
].filter((body) => body.trim().length > 0);
|
|
4053
|
+
}
|
|
4054
|
+
function bodyExcerpt(body) {
|
|
4055
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4056
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4057
|
+
}
|
|
4058
|
+
function makeGreptileSignal(input) {
|
|
4059
|
+
const scores = parseGreptileScores(input.body);
|
|
4060
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4061
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4062
|
+
const blocker = input.blocker ?? (containsBlockerText(input.body) || containsGreptileNegativeVerdict(input.body));
|
|
4063
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4064
|
+
return {
|
|
4065
|
+
source: input.source,
|
|
4066
|
+
trusted: input.trusted,
|
|
4067
|
+
authorLogin: input.authorLogin ?? null,
|
|
4068
|
+
reviewedSha,
|
|
4069
|
+
current,
|
|
4070
|
+
stale: current === false,
|
|
4071
|
+
score: scores[0] ?? null,
|
|
4072
|
+
scores,
|
|
4073
|
+
explicitApproval,
|
|
4074
|
+
blocker,
|
|
4075
|
+
actionable: input.actionable ?? blocker,
|
|
4076
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4077
|
+
body: input.body,
|
|
4078
|
+
allScores: scores
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
function reviewAuthorLogin(review) {
|
|
4082
|
+
return review.author?.login ?? null;
|
|
4083
|
+
}
|
|
4084
|
+
function commentAuthorLogin(comment) {
|
|
4085
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4086
|
+
}
|
|
4087
|
+
function collectGreptileSignals(evidence) {
|
|
4088
|
+
const signals = [];
|
|
4089
|
+
const contextSources = [
|
|
4090
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4091
|
+
{ source: "pr-body", body: evidence.body }
|
|
4092
|
+
];
|
|
4093
|
+
for (const context of contextSources) {
|
|
4094
|
+
if (!context.body.trim())
|
|
4095
|
+
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
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4099
|
+
signals.push(makeGreptileSignal({
|
|
4100
|
+
source: context.source,
|
|
4101
|
+
body: context.body,
|
|
4102
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4103
|
+
trusted: false,
|
|
4104
|
+
blocker: contextBlocker,
|
|
4105
|
+
actionable: contextBlocker
|
|
4106
|
+
}));
|
|
4107
|
+
}
|
|
4108
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4109
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4110
|
+
|
|
4111
|
+
`);
|
|
4112
|
+
if (!body.trim())
|
|
4113
|
+
continue;
|
|
4114
|
+
signals.push(makeGreptileSignal({
|
|
4115
|
+
source: "api",
|
|
4116
|
+
body,
|
|
4117
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4118
|
+
trusted: true,
|
|
4119
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4120
|
+
explicitApproval: false
|
|
4121
|
+
}));
|
|
4122
|
+
}
|
|
4123
|
+
for (const review of evidence.reviews) {
|
|
4124
|
+
const login = reviewAuthorLogin(review);
|
|
4125
|
+
if (!isGreptileGithubLogin(login))
|
|
4126
|
+
continue;
|
|
4127
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4128
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4129
|
+
|
|
4130
|
+
`);
|
|
4131
|
+
if (!body.trim())
|
|
4132
|
+
continue;
|
|
4133
|
+
const dismissed = state === "DISMISSED";
|
|
4134
|
+
signals.push(makeGreptileSignal({
|
|
4135
|
+
source: "github-review",
|
|
4136
|
+
body,
|
|
4137
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4138
|
+
trusted: !dismissed,
|
|
4139
|
+
authorLogin: login,
|
|
4140
|
+
reviewedSha: review.commit_id ?? null,
|
|
4141
|
+
explicitApproval: undefined,
|
|
4142
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4143
|
+
}));
|
|
4144
|
+
}
|
|
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
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4160
|
+
const login = commentAuthorLogin(comment);
|
|
4161
|
+
const body = comment.body ?? "";
|
|
4162
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4163
|
+
continue;
|
|
4164
|
+
signals.push(makeGreptileSignal({
|
|
4165
|
+
source: "issue-comment",
|
|
4166
|
+
body,
|
|
4167
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4168
|
+
trusted: true,
|
|
4169
|
+
authorLogin: login
|
|
4170
|
+
}));
|
|
4171
|
+
}
|
|
4172
|
+
for (const thread of evidence.reviewThreads) {
|
|
4173
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4174
|
+
continue;
|
|
4175
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4176
|
+
const login = comment.author?.login ?? null;
|
|
4177
|
+
const body = comment.body ?? "";
|
|
4178
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4179
|
+
continue;
|
|
4180
|
+
signals.push(makeGreptileSignal({
|
|
4181
|
+
source: "review-thread",
|
|
4182
|
+
body,
|
|
4183
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4184
|
+
trusted: true,
|
|
4185
|
+
authorLogin: login
|
|
4186
|
+
}));
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
for (const check of evidence.checks) {
|
|
4190
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4191
|
+
continue;
|
|
4192
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4193
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4194
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4195
|
+
|
|
4196
|
+
`);
|
|
4197
|
+
signals.push(makeGreptileSignal({
|
|
4198
|
+
source: "github-check",
|
|
4199
|
+
body,
|
|
4200
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4201
|
+
trusted: false,
|
|
4202
|
+
reviewedSha,
|
|
4203
|
+
explicitApproval: false,
|
|
4204
|
+
blocker: isFailingCheck(check),
|
|
4205
|
+
actionable: isFailingCheck(check)
|
|
4206
|
+
}));
|
|
4207
|
+
}
|
|
4208
|
+
return signals;
|
|
4209
|
+
}
|
|
4210
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4211
|
+
return threads.flatMap((thread) => {
|
|
4212
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4213
|
+
return [];
|
|
4214
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4215
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4216
|
+
return [];
|
|
4217
|
+
const latest = latestThreadComment(thread);
|
|
4218
|
+
if (!latest)
|
|
4219
|
+
return ["Unresolved Greptile review thread"];
|
|
4220
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4221
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4222
|
+
});
|
|
4223
|
+
}
|
|
4224
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4225
|
+
return comments.flatMap((comment) => {
|
|
4226
|
+
const body = comment.body?.trim() ?? "";
|
|
4227
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4228
|
+
return [];
|
|
4229
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4230
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4231
|
+
return [`${author}: ${body}`];
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4235
|
+
return reviews.flatMap((review) => {
|
|
4236
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4237
|
+
if (isGreptileGithubLogin(login))
|
|
4238
|
+
return [];
|
|
4239
|
+
const body = review.body?.trim() ?? "";
|
|
4240
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4241
|
+
return [];
|
|
4242
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4243
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
function signalLabel(signal) {
|
|
4247
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4248
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4249
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4250
|
+
return `${source}${author}${sha}`;
|
|
4251
|
+
}
|
|
4252
|
+
function deriveGreptileEvidence(input) {
|
|
4253
|
+
const rawBodies = collectBodies(input);
|
|
4254
|
+
const signals = collectGreptileSignals(input);
|
|
4255
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4256
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4257
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4258
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4259
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4260
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4261
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4262
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4263
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4264
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4265
|
+
const approvedByExplicitMapping = false;
|
|
4266
|
+
const approvingSignal = approvingScoreEntry?.signal ?? null;
|
|
4267
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4268
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4269
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4270
|
+
const blockerSignals = signals.filter((signal) => signal.source !== "changed-file-comment" && (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4271
|
+
const staleBlockingSignals = [];
|
|
4272
|
+
const blockers = [
|
|
4273
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4274
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4275
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4276
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4277
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4278
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4279
|
+
];
|
|
4280
|
+
const unresolvedComments = [
|
|
4281
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4282
|
+
];
|
|
4283
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4284
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4285
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4286
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4287
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4288
|
+
});
|
|
4289
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4290
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4291
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4292
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4293
|
+
});
|
|
4294
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4295
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4296
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4297
|
+
const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
|
|
4298
|
+
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";
|
|
4301
|
+
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
|
+
return {
|
|
4303
|
+
source,
|
|
4304
|
+
currentHeadSha: input.currentHeadSha,
|
|
4305
|
+
reviewedSha,
|
|
4306
|
+
fresh,
|
|
4307
|
+
completed,
|
|
4308
|
+
approved,
|
|
4309
|
+
score,
|
|
4310
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4311
|
+
blockers,
|
|
4312
|
+
unresolvedComments,
|
|
4313
|
+
rawBodies,
|
|
4314
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4315
|
+
mapping
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
function isGreptileCheckDetail(check) {
|
|
4319
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4320
|
+
}
|
|
4321
|
+
async function collectGreptileCheckDetails(input) {
|
|
4322
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4323
|
+
"api",
|
|
4324
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4325
|
+
"--paginate",
|
|
4326
|
+
"--slurp",
|
|
4327
|
+
"--jq",
|
|
4328
|
+
"map(.check_runs // []) | add // []"
|
|
4329
|
+
], input.projectRoot);
|
|
4330
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4331
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4332
|
+
}
|
|
4333
|
+
async function collectReviewThreads(input) {
|
|
4334
|
+
const reviewThreads = [];
|
|
4335
|
+
let afterCursor = null;
|
|
4336
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4337
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4338
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4339
|
+
"api",
|
|
4340
|
+
"graphql",
|
|
4341
|
+
"-F",
|
|
4342
|
+
`owner=${input.owner}`,
|
|
4343
|
+
"-F",
|
|
4344
|
+
`name=${input.name}`,
|
|
4345
|
+
"-F",
|
|
4346
|
+
`prNumber=${input.prNumber}`,
|
|
4347
|
+
"-f",
|
|
4348
|
+
`query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { reviewThreads(first: 100, after: ${afterLiteral}) { nodes { id isResolved isOutdated comments(first: 100) { nodes { author { login } body path url createdAt } pageInfo { hasNextPage endCursor } } } pageInfo { hasNextPage endCursor } } } } }`
|
|
4349
|
+
], input.projectRoot);
|
|
4350
|
+
if (threadsResponse.error) {
|
|
4351
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4352
|
+
}
|
|
4353
|
+
const data = threadsResponse.value.data;
|
|
4354
|
+
const repository = data?.repository;
|
|
4355
|
+
const pullRequest = repository?.pullRequest;
|
|
4356
|
+
const threads = pullRequest?.reviewThreads;
|
|
4357
|
+
const nodes = threads?.nodes;
|
|
4358
|
+
if (!Array.isArray(nodes)) {
|
|
4359
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4360
|
+
}
|
|
4361
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4362
|
+
reviewThreads.push(...normalized);
|
|
4363
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4364
|
+
if (truncatedCommentThread) {
|
|
4365
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4366
|
+
}
|
|
4367
|
+
const pageInfo = threads?.pageInfo;
|
|
4368
|
+
if (!pageInfo) {
|
|
4369
|
+
if (nodes.length >= 100) {
|
|
4370
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4371
|
+
}
|
|
4372
|
+
return { value: reviewThreads };
|
|
4373
|
+
}
|
|
4374
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4375
|
+
return { value: reviewThreads };
|
|
4376
|
+
}
|
|
4377
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4378
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4379
|
+
}
|
|
4380
|
+
afterCursor = pageInfo.endCursor;
|
|
4381
|
+
}
|
|
4382
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4383
|
+
}
|
|
4384
|
+
async function collectPrReviewEvidence(input) {
|
|
4385
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4386
|
+
if (!parsed) {
|
|
4387
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4388
|
+
}
|
|
4389
|
+
const readErrors = [];
|
|
4390
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4391
|
+
"pr",
|
|
4392
|
+
"view",
|
|
4393
|
+
input.prUrl,
|
|
4394
|
+
"--json",
|
|
4395
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4396
|
+
], input.projectRoot);
|
|
4397
|
+
if (viewRead.error)
|
|
4398
|
+
readErrors.push(viewRead.error);
|
|
4399
|
+
const view = viewRead.value;
|
|
4400
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4401
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4402
|
+
}
|
|
4403
|
+
if (!Array.isArray(view.reviews)) {
|
|
4404
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4405
|
+
}
|
|
4406
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4407
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4408
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4409
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4410
|
+
if (reviewCommentsRead.error)
|
|
4411
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4412
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4413
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4414
|
+
if (issueCommentsRead.error)
|
|
4415
|
+
readErrors.push(issueCommentsRead.error);
|
|
4416
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4417
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4418
|
+
command: input.command,
|
|
4419
|
+
projectRoot: input.projectRoot,
|
|
4420
|
+
owner: parsed.owner,
|
|
4421
|
+
name: parsed.repo,
|
|
4422
|
+
prNumber: parsed.prNumber
|
|
4423
|
+
});
|
|
4424
|
+
if (reviewThreadsRead.error)
|
|
4425
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4426
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4427
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4428
|
+
let greptileCheckDetails = [];
|
|
4429
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4430
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4431
|
+
command: input.command,
|
|
4432
|
+
projectRoot: input.projectRoot,
|
|
4433
|
+
repoName: parsed.repoName,
|
|
4434
|
+
headSha
|
|
4435
|
+
});
|
|
4436
|
+
if (checkDetailsRead.error)
|
|
4437
|
+
readErrors.push(checkDetailsRead.error);
|
|
4438
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4439
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4440
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4444
|
+
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
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4446
|
+
const evidenceBase = {
|
|
4447
|
+
title: firstString(view, ["title"]),
|
|
4448
|
+
body: firstString(view, ["body"]),
|
|
4449
|
+
reviews,
|
|
4450
|
+
changedFileReviewComments: reviewComments,
|
|
4451
|
+
relevantIssueComments: issueComments,
|
|
4452
|
+
reviewThreads,
|
|
4453
|
+
checks: checksWithGreptileDetails,
|
|
4454
|
+
currentHeadSha: headSha,
|
|
4455
|
+
apiSignals: input.apiSignals ?? []
|
|
4456
|
+
};
|
|
4457
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4458
|
+
return {
|
|
4459
|
+
prUrl: input.prUrl,
|
|
4460
|
+
prNumber: parsed.prNumber,
|
|
4461
|
+
repoName: parsed.repoName,
|
|
4462
|
+
title: evidenceBase.title,
|
|
4463
|
+
body: evidenceBase.body,
|
|
4464
|
+
headSha,
|
|
4465
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4466
|
+
baseRefName: firstString(view, ["baseRefName"]),
|
|
4467
|
+
state: firstString(view, ["state"]),
|
|
4468
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4469
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4470
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4471
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4472
|
+
reviews,
|
|
4473
|
+
reviewThreads,
|
|
4474
|
+
changedFileReviewComments: reviewComments,
|
|
4475
|
+
relevantIssueComments: issueComments,
|
|
4476
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4477
|
+
checkFailures,
|
|
4478
|
+
pendingChecks,
|
|
4479
|
+
readErrors,
|
|
4480
|
+
greptile
|
|
4481
|
+
};
|
|
4482
|
+
}
|
|
4483
|
+
function evaluateEvidence(evidence) {
|
|
4484
|
+
const reasons = [];
|
|
4485
|
+
const warnings = [];
|
|
4486
|
+
let pending = false;
|
|
4487
|
+
if (evidence.readErrors.length > 0) {
|
|
4488
|
+
reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
|
|
4489
|
+
}
|
|
4490
|
+
if (!evidence.headSha)
|
|
4491
|
+
reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
|
|
4492
|
+
if (evidence.checkFailures.length > 0)
|
|
4493
|
+
reasons.push(...evidence.checkFailures);
|
|
4494
|
+
if (evidence.pendingChecks.length > 0) {
|
|
4495
|
+
pending = true;
|
|
4496
|
+
reasons.push(...evidence.pendingChecks);
|
|
4497
|
+
}
|
|
4498
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4499
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4500
|
+
reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
|
|
4501
|
+
}
|
|
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
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4510
|
+
reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
|
|
4511
|
+
}
|
|
4512
|
+
if (!greptile.completed) {
|
|
4513
|
+
pending = true;
|
|
4514
|
+
reasons.push("Greptile check/review has not completed for the current PR head.");
|
|
4515
|
+
}
|
|
4516
|
+
if (!greptile.fresh)
|
|
4517
|
+
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4518
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4519
|
+
reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
|
|
4520
|
+
}
|
|
4521
|
+
if (!greptile.score && greptile.mapping !== "score-5-of-5") {
|
|
4522
|
+
reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
|
|
4523
|
+
}
|
|
4524
|
+
if (greptile.mapping === "unproven") {
|
|
4525
|
+
reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
|
|
4526
|
+
}
|
|
4527
|
+
if (greptile.blockers.length > 0) {
|
|
4528
|
+
reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
|
|
4529
|
+
}
|
|
4530
|
+
if (greptile.unresolvedComments.length > 0)
|
|
4531
|
+
reasons.push(...greptile.unresolvedComments);
|
|
4532
|
+
if (!greptile.approved)
|
|
4533
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4534
|
+
return { reasons: Array.from(new Set(reasons)), warnings, pending };
|
|
4535
|
+
}
|
|
4536
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4537
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4538
|
+
const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
|
|
4539
|
+
return {
|
|
4540
|
+
approved,
|
|
4541
|
+
pending: evaluated.pending,
|
|
4542
|
+
reasons: evaluated.reasons,
|
|
4543
|
+
warnings: evaluated.warnings,
|
|
4544
|
+
actionableFeedback: evaluated.reasons,
|
|
4545
|
+
evidence
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
|
|
3778
4549
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3779
4550
|
async function verifyTask(options) {
|
|
3780
4551
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -4571,7 +5342,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4571
5342
|
}
|
|
4572
5343
|
};
|
|
4573
5344
|
}
|
|
4574
|
-
|
|
5345
|
+
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)) {
|
|
4575
5347
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4576
5348
|
return {
|
|
4577
5349
|
verdict: "REJECT",
|
|
@@ -4587,44 +5359,78 @@ async function runGreptileReviewForPr(options) {
|
|
|
4587
5359
|
}
|
|
4588
5360
|
};
|
|
4589
5361
|
}
|
|
4590
|
-
if (score) {
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
5362
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5363
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5364
|
+
return {
|
|
5365
|
+
verdict: "REJECT",
|
|
5366
|
+
feedback,
|
|
5367
|
+
reasons,
|
|
5368
|
+
warnings,
|
|
5369
|
+
rawPayload: {
|
|
5370
|
+
pr: options.prState,
|
|
5371
|
+
codeReviews: reviewsPayload,
|
|
5372
|
+
selectedReview,
|
|
5373
|
+
reviewDetails,
|
|
5374
|
+
comments: commentsPayload,
|
|
5375
|
+
score
|
|
5376
|
+
}
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5380
|
+
let strictGate = null;
|
|
5381
|
+
try {
|
|
5382
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5383
|
+
projectRoot: options.projectRoot,
|
|
5384
|
+
taskId: options.taskId,
|
|
5385
|
+
prUrl,
|
|
5386
|
+
apiSignals: [{
|
|
5387
|
+
id: selectedReview.id,
|
|
5388
|
+
body: reviewBody,
|
|
5389
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5390
|
+
status: selectedReview.status
|
|
5391
|
+
}]
|
|
5392
|
+
});
|
|
5393
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5394
|
+
} catch (error) {
|
|
5395
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5396
|
+
return {
|
|
5397
|
+
verdict: "REJECT",
|
|
5398
|
+
feedback,
|
|
5399
|
+
reasons,
|
|
5400
|
+
warnings,
|
|
5401
|
+
rawPayload: {
|
|
5402
|
+
pr: options.prState,
|
|
5403
|
+
codeReviews: reviewsPayload,
|
|
5404
|
+
selectedReview,
|
|
5405
|
+
reviewDetails,
|
|
5406
|
+
comments: commentsPayload,
|
|
5407
|
+
score
|
|
5408
|
+
}
|
|
5409
|
+
};
|
|
5410
|
+
}
|
|
5411
|
+
if (!strictGate.approved) {
|
|
5412
|
+
return {
|
|
5413
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5414
|
+
feedback,
|
|
5415
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5416
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5417
|
+
rawPayload: {
|
|
5418
|
+
pr: options.prState,
|
|
5419
|
+
codeReviews: reviewsPayload,
|
|
5420
|
+
selectedReview,
|
|
5421
|
+
reviewDetails,
|
|
5422
|
+
comments: commentsPayload,
|
|
5423
|
+
score,
|
|
5424
|
+
strictGate: {
|
|
5425
|
+
approved: strictGate.approved,
|
|
5426
|
+
pending: strictGate.pending,
|
|
5427
|
+
reasons: strictGate.reasons,
|
|
5428
|
+
warnings: strictGate.warnings,
|
|
5429
|
+
greptile: strictGate.evidence.greptile,
|
|
5430
|
+
readErrors: strictGate.evidence.readErrors
|
|
4622
5431
|
}
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4626
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4627
|
-
}
|
|
5432
|
+
}
|
|
5433
|
+
};
|
|
4628
5434
|
}
|
|
4629
5435
|
return {
|
|
4630
5436
|
verdict: "APPROVE",
|
|
@@ -4636,7 +5442,15 @@ async function runGreptileReviewForPr(options) {
|
|
|
4636
5442
|
codeReviews: reviewsPayload,
|
|
4637
5443
|
selectedReview,
|
|
4638
5444
|
reviewDetails,
|
|
4639
|
-
comments: commentsPayload
|
|
5445
|
+
comments: commentsPayload,
|
|
5446
|
+
strictGate: {
|
|
5447
|
+
approved: strictGate.approved,
|
|
5448
|
+
pending: strictGate.pending,
|
|
5449
|
+
reasons: strictGate.reasons,
|
|
5450
|
+
warnings: strictGate.warnings,
|
|
5451
|
+
greptile: strictGate.evidence.greptile,
|
|
5452
|
+
readErrors: strictGate.evidence.readErrors
|
|
5453
|
+
}
|
|
4640
5454
|
}
|
|
4641
5455
|
};
|
|
4642
5456
|
}
|
|
@@ -4660,7 +5474,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4660
5474
|
let threads = [];
|
|
4661
5475
|
let actionableThreads = [];
|
|
4662
5476
|
let checkRollup = [];
|
|
4663
|
-
let
|
|
5477
|
+
let checkState2 = { pending: false, completed: false };
|
|
4664
5478
|
for (let attempt = 0;; attempt += 1) {
|
|
4665
5479
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4666
5480
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4669,15 +5483,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4669
5483
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4670
5484
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4671
5485
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4672
|
-
|
|
4673
|
-
const
|
|
5486
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5487
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4674
5488
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4675
5489
|
attempt,
|
|
4676
5490
|
pollAttempts: options.pollAttempts,
|
|
4677
|
-
checkState,
|
|
5491
|
+
checkState: checkState2,
|
|
4678
5492
|
fallbackReview,
|
|
4679
5493
|
selectedReview,
|
|
4680
|
-
approvedViaReviewedAncestor
|
|
5494
|
+
approvedViaReviewedAncestor
|
|
4681
5495
|
})) {
|
|
4682
5496
|
break;
|
|
4683
5497
|
}
|
|
@@ -4705,7 +5519,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4705
5519
|
].filter(Boolean).join(`
|
|
4706
5520
|
`);
|
|
4707
5521
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4708
|
-
if (
|
|
5522
|
+
if (checkState2.pending) {
|
|
4709
5523
|
return {
|
|
4710
5524
|
verdict: "SKIP",
|
|
4711
5525
|
feedback,
|
|
@@ -4716,34 +5530,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4716
5530
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4717
5531
|
};
|
|
4718
5532
|
}
|
|
4719
|
-
const
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
};
|
|
4730
|
-
}
|
|
4731
|
-
return {
|
|
4732
|
-
verdict: "SKIP",
|
|
4733
|
-
feedback,
|
|
4734
|
-
reasons: [
|
|
4735
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4736
|
-
],
|
|
4737
|
-
warnings,
|
|
4738
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4739
|
-
};
|
|
4740
|
-
}
|
|
4741
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4742
|
-
if (actionableThreads.length > 0) {
|
|
5533
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5534
|
+
let strictGate;
|
|
5535
|
+
try {
|
|
5536
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5537
|
+
projectRoot: options.projectRoot,
|
|
5538
|
+
taskId: options.taskId,
|
|
5539
|
+
prUrl
|
|
5540
|
+
});
|
|
5541
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5542
|
+
} catch (error) {
|
|
4743
5543
|
return {
|
|
4744
5544
|
verdict: "REJECT",
|
|
4745
5545
|
feedback,
|
|
4746
|
-
reasons:
|
|
5546
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4747
5547
|
warnings,
|
|
4748
5548
|
rawPayload: {
|
|
4749
5549
|
pr: options.prState,
|
|
@@ -4756,44 +5556,30 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4756
5556
|
}
|
|
4757
5557
|
};
|
|
4758
5558
|
}
|
|
4759
|
-
if (!
|
|
4760
|
-
if (approvedViaCompletedCheck) {
|
|
4761
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
|
|
4762
|
-
return {
|
|
4763
|
-
verdict: "APPROVE",
|
|
4764
|
-
feedback,
|
|
4765
|
-
reasons: [],
|
|
4766
|
-
warnings,
|
|
4767
|
-
rawPayload: {
|
|
4768
|
-
pr: options.prState,
|
|
4769
|
-
selectedReview: fallbackReview,
|
|
4770
|
-
reviews,
|
|
4771
|
-
threads,
|
|
4772
|
-
checkRollup,
|
|
4773
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4774
|
-
}
|
|
4775
|
-
};
|
|
4776
|
-
}
|
|
5559
|
+
if (!strictGate.approved) {
|
|
4777
5560
|
return {
|
|
4778
|
-
verdict: "SKIP",
|
|
5561
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4779
5562
|
feedback,
|
|
4780
|
-
reasons: [
|
|
4781
|
-
|
|
4782
|
-
],
|
|
4783
|
-
warnings,
|
|
5563
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5564
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4784
5565
|
rawPayload: {
|
|
4785
5566
|
pr: options.prState,
|
|
4786
5567
|
selectedReview: fallbackReview,
|
|
4787
5568
|
reviews,
|
|
4788
5569
|
threads,
|
|
4789
5570
|
checkRollup,
|
|
5571
|
+
actionableThreads,
|
|
5572
|
+
strictGate: {
|
|
5573
|
+
approved: strictGate.approved,
|
|
5574
|
+
pending: strictGate.pending,
|
|
5575
|
+
reasons: strictGate.reasons,
|
|
5576
|
+
warnings: strictGate.warnings,
|
|
5577
|
+
greptile: strictGate.evidence.greptile
|
|
5578
|
+
},
|
|
4790
5579
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4791
5580
|
}
|
|
4792
5581
|
};
|
|
4793
5582
|
}
|
|
4794
|
-
if (approvedViaReviewedAncestor) {
|
|
4795
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
|
|
4796
|
-
}
|
|
4797
5583
|
return {
|
|
4798
5584
|
verdict: "APPROVE",
|
|
4799
5585
|
feedback,
|
|
@@ -4805,6 +5591,13 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4805
5591
|
reviews,
|
|
4806
5592
|
threads,
|
|
4807
5593
|
checkRollup,
|
|
5594
|
+
strictGate: {
|
|
5595
|
+
approved: strictGate.approved,
|
|
5596
|
+
pending: strictGate.pending,
|
|
5597
|
+
reasons: strictGate.reasons,
|
|
5598
|
+
warnings: strictGate.warnings,
|
|
5599
|
+
greptile: strictGate.evidence.greptile
|
|
5600
|
+
},
|
|
4808
5601
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4809
5602
|
}
|
|
4810
5603
|
};
|
|
@@ -4917,21 +5710,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4917
5710
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4918
5711
|
return true;
|
|
4919
5712
|
}
|
|
4920
|
-
return
|
|
5713
|
+
return false;
|
|
4921
5714
|
}
|
|
4922
5715
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4923
5716
|
if (options.githubCheckState.completed) {
|
|
4924
5717
|
return false;
|
|
4925
5718
|
}
|
|
5719
|
+
const hasRemainingBudget = options.attempt + 1 < options.pollAttempts;
|
|
5720
|
+
if (!hasRemainingBudget) {
|
|
5721
|
+
return false;
|
|
5722
|
+
}
|
|
4926
5723
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4927
5724
|
return true;
|
|
4928
5725
|
}
|
|
4929
|
-
return
|
|
5726
|
+
return true;
|
|
4930
5727
|
}
|
|
4931
5728
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4932
5729
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
4933
5730
|
if (waitingForVisiblePendingReview) {
|
|
4934
|
-
return
|
|
5731
|
+
return options.attempt + 1 < options.pollAttempts;
|
|
4935
5732
|
}
|
|
4936
5733
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
4937
5734
|
if (reviewNotVisibleYet) {
|
|
@@ -4990,6 +5787,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4990
5787
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4991
5788
|
}
|
|
4992
5789
|
}
|
|
5790
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
5791
|
+
return collectPrReviewEvidence({
|
|
5792
|
+
projectRoot: input.projectRoot,
|
|
5793
|
+
prUrl: input.prUrl,
|
|
5794
|
+
taskId: input.taskId,
|
|
5795
|
+
runId: "verifier",
|
|
5796
|
+
cycle: 0,
|
|
5797
|
+
apiSignals: input.apiSignals ?? [],
|
|
5798
|
+
command: async (args, options) => {
|
|
5799
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
5800
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5801
|
+
}
|
|
5802
|
+
});
|
|
5803
|
+
}
|
|
4993
5804
|
function deriveRepoName(projectRoot, prState) {
|
|
4994
5805
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4995
5806
|
if (fromUrl?.[1]) {
|
|
@@ -5004,8 +5815,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
5004
5815
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5005
5816
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
5006
5817
|
}
|
|
5007
|
-
function
|
|
5008
|
-
|
|
5818
|
+
function isGreptileGithubLogin2(login) {
|
|
5819
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
5820
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
5009
5821
|
}
|
|
5010
5822
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
5011
5823
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -5022,7 +5834,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
5022
5834
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
5023
5835
|
}
|
|
5024
5836
|
function sortGithubGreptileReviews(reviews) {
|
|
5025
|
-
return reviews.filter((review) =>
|
|
5837
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
5026
5838
|
}
|
|
5027
5839
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
5028
5840
|
const response = runGhJson(projectRoot, [
|
|
@@ -5095,32 +5907,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
5095
5907
|
}
|
|
5096
5908
|
return { pending: false, completed: false };
|
|
5097
5909
|
}
|
|
5098
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
5099
|
-
const greptileChecks = checks.filter((check) => {
|
|
5100
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
5101
|
-
return label.includes("greptile");
|
|
5102
|
-
});
|
|
5103
|
-
if (greptileChecks.length === 0) {
|
|
5104
|
-
return false;
|
|
5105
|
-
}
|
|
5106
|
-
for (const check of greptileChecks) {
|
|
5107
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
5108
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
5109
|
-
return false;
|
|
5110
|
-
}
|
|
5111
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
5112
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
5113
|
-
return false;
|
|
5114
|
-
}
|
|
5115
|
-
continue;
|
|
5116
|
-
}
|
|
5117
|
-
const state = (check.state || "").toUpperCase();
|
|
5118
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
5119
|
-
return false;
|
|
5120
|
-
}
|
|
5121
|
-
}
|
|
5122
|
-
return true;
|
|
5123
|
-
}
|
|
5124
5910
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
5125
5911
|
const [owner, name] = repoName.split("/");
|
|
5126
5912
|
if (!owner || !name) {
|
|
@@ -5146,7 +5932,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
5146
5932
|
return [];
|
|
5147
5933
|
}
|
|
5148
5934
|
const comments = thread.comments?.nodes || [];
|
|
5149
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
5935
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
5150
5936
|
if (!latestGreptileComment?.path?.trim()) {
|
|
5151
5937
|
return [];
|
|
5152
5938
|
}
|
|
@@ -5168,11 +5954,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5168
5954
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5169
5955
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5170
5956
|
}
|
|
5171
|
-
function stripHtml(input) {
|
|
5172
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5173
|
-
|
|
5174
|
-
`).trim();
|
|
5175
|
-
}
|
|
5176
5957
|
function summarizeComment(input) {
|
|
5177
5958
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5178
5959
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5181,31 +5962,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5181
5962
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5182
5963
|
}
|
|
5183
5964
|
function isAiReviewApproved(input) {
|
|
5965
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
5966
|
+
return false;
|
|
5967
|
+
}
|
|
5184
5968
|
if (input.reviewMode !== "required") {
|
|
5185
5969
|
return true;
|
|
5186
5970
|
}
|
|
5187
5971
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5188
5972
|
}
|
|
5189
|
-
function parseGreptileScore(input) {
|
|
5190
|
-
const text = stripHtml(input);
|
|
5191
|
-
const patterns = [
|
|
5192
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5193
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5194
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5195
|
-
];
|
|
5196
|
-
for (const pattern of patterns) {
|
|
5197
|
-
const match = pattern.exec(text);
|
|
5198
|
-
if (!match) {
|
|
5199
|
-
continue;
|
|
5200
|
-
}
|
|
5201
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5202
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5203
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5204
|
-
return { value, scale };
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
return null;
|
|
5208
|
-
}
|
|
5209
5973
|
|
|
5210
5974
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5211
5975
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|