@h-rig/runtime 0.0.6-alpha.2 → 0.0.6-alpha.20
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 +84 -313
- package/dist/bin/rig-agent.js +85 -27
- package/dist/src/control-plane/agent-wrapper.js +101 -27
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +1357 -180
- package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
- package/dist/src/control-plane/hooks/inject-context.js +2 -2
- package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
- package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
- package/dist/src/control-plane/native/git-ops.js +134 -68
- package/dist/src/control-plane/native/harness-cli.js +1357 -180
- package/dist/src/control-plane/native/pr-automation.js +1532 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
- package/dist/src/control-plane/native/run-ops.js +35 -12
- package/dist/src/control-plane/native/task-ops.js +1274 -155
- package/dist/src/control-plane/native/validator.js +2 -2
- package/dist/src/control-plane/native/verifier.js +1274 -154
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/runtime/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/home.js +31 -6
- package/dist/src/control-plane/runtime/isolation/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +38 -9
- package/dist/src/control-plane/runtime/queue.js +38 -9
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
- package/dist/src/index.js +27 -20
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
- package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
- package/package.json +6 -6
- package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
- package/native/darwin-arm64/manifest.json +0 -1
- package/native/linux-x64/bin/rig-git +0 -0
- package/native/linux-x64/bin/rig-shell +0 -0
- package/native/linux-x64/bin/rig-tools +0 -0
- package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
- package/native/linux-x64/lib/runtime-native.so +0 -0
- package/native/linux-x64/manifest.json +0 -1
|
@@ -1089,8 +1089,8 @@ function githubStatusFor(issue) {
|
|
|
1089
1089
|
return "open";
|
|
1090
1090
|
}
|
|
1091
1091
|
function selectedGitHubEnv() {
|
|
1092
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
1093
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1092
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1093
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
1094
1094
|
}
|
|
1095
1095
|
function ghSpawnOptions() {
|
|
1096
1096
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -3739,6 +3739,1126 @@ 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
|
+
|
|
3743
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3744
|
+
function parseJsonObject(value) {
|
|
3745
|
+
if (!value?.trim())
|
|
3746
|
+
return { value: {}, error: "empty JSON output" };
|
|
3747
|
+
try {
|
|
3748
|
+
const parsed = JSON.parse(value);
|
|
3749
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3750
|
+
} catch (error) {
|
|
3751
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
function flattenPaginatedArray(value) {
|
|
3755
|
+
if (!Array.isArray(value))
|
|
3756
|
+
return null;
|
|
3757
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3758
|
+
return value.flatMap((entry) => entry);
|
|
3759
|
+
}
|
|
3760
|
+
return value;
|
|
3761
|
+
}
|
|
3762
|
+
function parseJsonArray(value) {
|
|
3763
|
+
if (!value?.trim())
|
|
3764
|
+
return { value: [], error: "empty JSON output" };
|
|
3765
|
+
try {
|
|
3766
|
+
const parsed = JSON.parse(value);
|
|
3767
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3768
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
function parseGithubPrUrl(prUrl) {
|
|
3774
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3775
|
+
if (!match)
|
|
3776
|
+
return null;
|
|
3777
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3778
|
+
if (!Number.isFinite(prNumber))
|
|
3779
|
+
return null;
|
|
3780
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3781
|
+
}
|
|
3782
|
+
function checkName(check) {
|
|
3783
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3784
|
+
}
|
|
3785
|
+
function checkState(check) {
|
|
3786
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3787
|
+
}
|
|
3788
|
+
function isGreptileLabel(value) {
|
|
3789
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3790
|
+
}
|
|
3791
|
+
function isGreptileGithubLogin(value) {
|
|
3792
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3793
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3794
|
+
}
|
|
3795
|
+
function isPassingCheck(check) {
|
|
3796
|
+
const state = checkState(check);
|
|
3797
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3798
|
+
}
|
|
3799
|
+
function isPendingCheck(check) {
|
|
3800
|
+
const state = checkState(check);
|
|
3801
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3802
|
+
}
|
|
3803
|
+
function isFailingCheck(check) {
|
|
3804
|
+
const state = checkState(check);
|
|
3805
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3806
|
+
}
|
|
3807
|
+
function wildcardToRegExp(pattern) {
|
|
3808
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3809
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3810
|
+
}
|
|
3811
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3812
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3813
|
+
}
|
|
3814
|
+
function greptileScorePatterns() {
|
|
3815
|
+
return [
|
|
3816
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3817
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3818
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3819
|
+
];
|
|
3820
|
+
}
|
|
3821
|
+
function parseGreptileScores(input) {
|
|
3822
|
+
const text = stripHtml(input);
|
|
3823
|
+
const seen = new Set;
|
|
3824
|
+
const scores = [];
|
|
3825
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3826
|
+
for (const match of text.matchAll(pattern)) {
|
|
3827
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3828
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3829
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3830
|
+
continue;
|
|
3831
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3832
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3833
|
+
if (seen.has(key))
|
|
3834
|
+
continue;
|
|
3835
|
+
seen.add(key);
|
|
3836
|
+
scores.push({ value, scale, raw });
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
return scores;
|
|
3840
|
+
}
|
|
3841
|
+
function parseGreptileScore(input) {
|
|
3842
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3843
|
+
}
|
|
3844
|
+
function stripHtml(input) {
|
|
3845
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3846
|
+
|
|
3847
|
+
`).trim();
|
|
3848
|
+
}
|
|
3849
|
+
function containsBlockerText(input) {
|
|
3850
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
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);
|
|
3852
|
+
}
|
|
3853
|
+
function isStrictFiveOfFive(score) {
|
|
3854
|
+
return score.value === 5 && score.scale === 5;
|
|
3855
|
+
}
|
|
3856
|
+
function containsConflictingScoreText(input) {
|
|
3857
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
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
|
+
}
|
|
4042
|
+
function firstString(record, keys) {
|
|
4043
|
+
for (const key of keys) {
|
|
4044
|
+
const value = record[key];
|
|
4045
|
+
if (typeof value === "string")
|
|
4046
|
+
return value;
|
|
4047
|
+
}
|
|
4048
|
+
return "";
|
|
4049
|
+
}
|
|
4050
|
+
function arrayField(record, key) {
|
|
4051
|
+
const value = record[key];
|
|
4052
|
+
return Array.isArray(value) ? value : [];
|
|
4053
|
+
}
|
|
4054
|
+
async function runJsonArray(command, args, cwd) {
|
|
4055
|
+
const result = await command(args, { cwd });
|
|
4056
|
+
const label = `gh ${args.join(" ")}`;
|
|
4057
|
+
if (result.exitCode !== 0) {
|
|
4058
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4059
|
+
}
|
|
4060
|
+
const parsed = parseJsonArray(result.stdout);
|
|
4061
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4062
|
+
}
|
|
4063
|
+
async function runJsonObject(command, args, cwd) {
|
|
4064
|
+
const result = await command(args, { cwd });
|
|
4065
|
+
const label = `gh ${args.join(" ")}`;
|
|
4066
|
+
if (result.exitCode !== 0) {
|
|
4067
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4068
|
+
}
|
|
4069
|
+
const parsed = parseJsonObject(result.stdout);
|
|
4070
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4071
|
+
}
|
|
4072
|
+
function normalizeStatusCheck(entry) {
|
|
4073
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4074
|
+
return null;
|
|
4075
|
+
const record = entry;
|
|
4076
|
+
const name = firstString(record, ["name", "context"]);
|
|
4077
|
+
if (!name.trim())
|
|
4078
|
+
return null;
|
|
4079
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
4080
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
4081
|
+
return {
|
|
4082
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
4083
|
+
name,
|
|
4084
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
4085
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4086
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4087
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
4088
|
+
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,
|
|
4089
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
4090
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
4091
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
4092
|
+
output: output ? {
|
|
4093
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
4094
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
4095
|
+
text: typeof output.text === "string" ? output.text : null
|
|
4096
|
+
} : null,
|
|
4097
|
+
app: app ? {
|
|
4098
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
4099
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
4100
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
4101
|
+
} : null
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
function normalizeReview(entry) {
|
|
4105
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4106
|
+
return null;
|
|
4107
|
+
const record = entry;
|
|
4108
|
+
return {
|
|
4109
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
4110
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4111
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4112
|
+
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,
|
|
4113
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
4114
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
function normalizeReviewComment(entry) {
|
|
4118
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4119
|
+
return null;
|
|
4120
|
+
const record = entry;
|
|
4121
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4122
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
4123
|
+
if (!body && !path)
|
|
4124
|
+
return null;
|
|
4125
|
+
return {
|
|
4126
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4127
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4128
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4129
|
+
body,
|
|
4130
|
+
path,
|
|
4131
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4132
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4133
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4134
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
function normalizeIssueComment(entry) {
|
|
4138
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4139
|
+
return null;
|
|
4140
|
+
const record = entry;
|
|
4141
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4142
|
+
if (!body)
|
|
4143
|
+
return null;
|
|
4144
|
+
return {
|
|
4145
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4146
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4147
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4148
|
+
body,
|
|
4149
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4150
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4151
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
function normalizeReviewThread(entry) {
|
|
4155
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4156
|
+
return null;
|
|
4157
|
+
const record = entry;
|
|
4158
|
+
return {
|
|
4159
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4160
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4161
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4162
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
function relevantIssueComment(comment) {
|
|
4166
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4167
|
+
const body = comment.body ?? "";
|
|
4168
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4169
|
+
}
|
|
4170
|
+
function latestThreadComment(thread) {
|
|
4171
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4172
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4173
|
+
}
|
|
4174
|
+
function unresolvedThreadSummaries(threads) {
|
|
4175
|
+
return threads.flatMap((thread) => {
|
|
4176
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4177
|
+
return [];
|
|
4178
|
+
const latest = latestThreadComment(thread);
|
|
4179
|
+
if (!latest)
|
|
4180
|
+
return ["Unresolved review thread"];
|
|
4181
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4182
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
function collectBodies(evidence) {
|
|
4186
|
+
return [
|
|
4187
|
+
evidence.title ?? "",
|
|
4188
|
+
evidence.body,
|
|
4189
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4190
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4191
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4192
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4193
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4194
|
+
].filter((body) => body.trim().length > 0);
|
|
4195
|
+
}
|
|
4196
|
+
function bodyExcerpt(body) {
|
|
4197
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4198
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4199
|
+
}
|
|
4200
|
+
function makeGreptileSignal(input) {
|
|
4201
|
+
const scores = parseGreptileScores(input.body);
|
|
4202
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4203
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4204
|
+
const verdict = input.verdict ?? null;
|
|
4205
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4206
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4207
|
+
return {
|
|
4208
|
+
source: input.source,
|
|
4209
|
+
trusted: input.trusted,
|
|
4210
|
+
authorLogin: input.authorLogin ?? null,
|
|
4211
|
+
reviewedSha,
|
|
4212
|
+
current,
|
|
4213
|
+
stale: current === false,
|
|
4214
|
+
score: scores[0] ?? null,
|
|
4215
|
+
scores,
|
|
4216
|
+
explicitApproval,
|
|
4217
|
+
verdict,
|
|
4218
|
+
blocker,
|
|
4219
|
+
actionable: input.actionable ?? blocker,
|
|
4220
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4221
|
+
body: input.body,
|
|
4222
|
+
allScores: scores
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
function reviewAuthorLogin(review) {
|
|
4226
|
+
return review.author?.login ?? null;
|
|
4227
|
+
}
|
|
4228
|
+
function commentAuthorLogin(comment) {
|
|
4229
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4230
|
+
}
|
|
4231
|
+
function collectGreptileSignals(evidence) {
|
|
4232
|
+
const signals = [];
|
|
4233
|
+
const contextSources = [
|
|
4234
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4235
|
+
{ source: "pr-body", body: evidence.body }
|
|
4236
|
+
];
|
|
4237
|
+
for (const context of contextSources) {
|
|
4238
|
+
if (!context.body.trim())
|
|
4239
|
+
continue;
|
|
4240
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4241
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4242
|
+
continue;
|
|
4243
|
+
signals.push(makeGreptileSignal({
|
|
4244
|
+
source: context.source,
|
|
4245
|
+
body: context.body,
|
|
4246
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4247
|
+
trusted: false,
|
|
4248
|
+
blocker: contextBlocker,
|
|
4249
|
+
actionable: contextBlocker
|
|
4250
|
+
}));
|
|
4251
|
+
}
|
|
4252
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4253
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4254
|
+
|
|
4255
|
+
`) || "Status: UNKNOWN";
|
|
4256
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4257
|
+
signals.push(makeGreptileSignal({
|
|
4258
|
+
source: "api",
|
|
4259
|
+
body,
|
|
4260
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4261
|
+
trusted: true,
|
|
4262
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4263
|
+
explicitApproval: verdict === "approved",
|
|
4264
|
+
verdict
|
|
4265
|
+
}));
|
|
4266
|
+
}
|
|
4267
|
+
for (const review of evidence.reviews) {
|
|
4268
|
+
const login = reviewAuthorLogin(review);
|
|
4269
|
+
if (!isGreptileGithubLogin(login))
|
|
4270
|
+
continue;
|
|
4271
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4272
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4273
|
+
|
|
4274
|
+
`);
|
|
4275
|
+
if (!body.trim())
|
|
4276
|
+
continue;
|
|
4277
|
+
const dismissed = state === "DISMISSED";
|
|
4278
|
+
signals.push(makeGreptileSignal({
|
|
4279
|
+
source: "github-review",
|
|
4280
|
+
body,
|
|
4281
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4282
|
+
trusted: !dismissed,
|
|
4283
|
+
authorLogin: login,
|
|
4284
|
+
reviewedSha: review.commit_id ?? null,
|
|
4285
|
+
explicitApproval: undefined,
|
|
4286
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4287
|
+
}));
|
|
4288
|
+
}
|
|
4289
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4290
|
+
const login = commentAuthorLogin(comment);
|
|
4291
|
+
const body = comment.body ?? "";
|
|
4292
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4293
|
+
continue;
|
|
4294
|
+
signals.push(makeGreptileSignal({
|
|
4295
|
+
source: "issue-comment",
|
|
4296
|
+
body,
|
|
4297
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4298
|
+
trusted: true,
|
|
4299
|
+
authorLogin: login
|
|
4300
|
+
}));
|
|
4301
|
+
}
|
|
4302
|
+
for (const thread of evidence.reviewThreads) {
|
|
4303
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4304
|
+
continue;
|
|
4305
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4306
|
+
const login = comment.author?.login ?? null;
|
|
4307
|
+
const body = comment.body ?? "";
|
|
4308
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4309
|
+
continue;
|
|
4310
|
+
signals.push(makeGreptileSignal({
|
|
4311
|
+
source: "review-thread",
|
|
4312
|
+
body,
|
|
4313
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4314
|
+
trusted: true,
|
|
4315
|
+
authorLogin: login
|
|
4316
|
+
}));
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
for (const check of evidence.checks) {
|
|
4320
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4321
|
+
continue;
|
|
4322
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4323
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4324
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4325
|
+
|
|
4326
|
+
`);
|
|
4327
|
+
signals.push(makeGreptileSignal({
|
|
4328
|
+
source: "github-check",
|
|
4329
|
+
body,
|
|
4330
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4331
|
+
trusted: false,
|
|
4332
|
+
reviewedSha,
|
|
4333
|
+
explicitApproval: false,
|
|
4334
|
+
blocker: isFailingCheck(check),
|
|
4335
|
+
actionable: isFailingCheck(check)
|
|
4336
|
+
}));
|
|
4337
|
+
}
|
|
4338
|
+
return signals;
|
|
4339
|
+
}
|
|
4340
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4341
|
+
return threads.flatMap((thread) => {
|
|
4342
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4343
|
+
return [];
|
|
4344
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4345
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4346
|
+
return [];
|
|
4347
|
+
const latest = latestThreadComment(thread);
|
|
4348
|
+
if (!latest)
|
|
4349
|
+
return ["Unresolved Greptile review thread"];
|
|
4350
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4351
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4355
|
+
return [];
|
|
4356
|
+
}
|
|
4357
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4358
|
+
return comments.flatMap((comment) => {
|
|
4359
|
+
const body = comment.body?.trim() ?? "";
|
|
4360
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4361
|
+
return [];
|
|
4362
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4363
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4364
|
+
return [`${author}: ${body}`];
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4368
|
+
return reviews.flatMap((review) => {
|
|
4369
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4370
|
+
if (isGreptileGithubLogin(login))
|
|
4371
|
+
return [];
|
|
4372
|
+
const body = review.body?.trim() ?? "";
|
|
4373
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4374
|
+
return [];
|
|
4375
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4376
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
function signalLabel(signal) {
|
|
4380
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4381
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4382
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4383
|
+
return `${source}${author}${sha}`;
|
|
4384
|
+
}
|
|
4385
|
+
function deriveGreptileEvidence(input) {
|
|
4386
|
+
const rawBodies = collectBodies(input);
|
|
4387
|
+
const signals = collectGreptileSignals(input);
|
|
4388
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4389
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4390
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4391
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4392
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4393
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4394
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4395
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
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;
|
|
4404
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4405
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4406
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4407
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4408
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4409
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4410
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4411
|
+
const staleBlockingSignals = [];
|
|
4412
|
+
const blockers = [
|
|
4413
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4414
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4415
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4416
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4417
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4418
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4419
|
+
];
|
|
4420
|
+
const unresolvedComments = [
|
|
4421
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4422
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4423
|
+
];
|
|
4424
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4425
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4426
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4427
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4428
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4429
|
+
});
|
|
4430
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4431
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4432
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4433
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
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"));
|
|
4436
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4437
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4438
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4439
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4440
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
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";
|
|
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";
|
|
4444
|
+
return {
|
|
4445
|
+
source,
|
|
4446
|
+
currentHeadSha: input.currentHeadSha,
|
|
4447
|
+
reviewedSha,
|
|
4448
|
+
fresh,
|
|
4449
|
+
completed,
|
|
4450
|
+
approved,
|
|
4451
|
+
score,
|
|
4452
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4453
|
+
blockers,
|
|
4454
|
+
unresolvedComments,
|
|
4455
|
+
rawBodies,
|
|
4456
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4457
|
+
mapping
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
function isGreptileCheckDetail(check) {
|
|
4461
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4462
|
+
}
|
|
4463
|
+
async function collectGreptileCheckDetails(input) {
|
|
4464
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4465
|
+
"api",
|
|
4466
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4467
|
+
"--paginate",
|
|
4468
|
+
"--slurp",
|
|
4469
|
+
"--jq",
|
|
4470
|
+
"map(.check_runs // []) | add // []"
|
|
4471
|
+
], input.projectRoot);
|
|
4472
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4473
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4474
|
+
}
|
|
4475
|
+
async function collectReviewThreads(input) {
|
|
4476
|
+
const reviewThreads = [];
|
|
4477
|
+
let afterCursor = null;
|
|
4478
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4479
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4480
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4481
|
+
"api",
|
|
4482
|
+
"graphql",
|
|
4483
|
+
"-F",
|
|
4484
|
+
`owner=${input.owner}`,
|
|
4485
|
+
"-F",
|
|
4486
|
+
`name=${input.name}`,
|
|
4487
|
+
"-F",
|
|
4488
|
+
`prNumber=${input.prNumber}`,
|
|
4489
|
+
"-f",
|
|
4490
|
+
`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 } } } } }`
|
|
4491
|
+
], input.projectRoot);
|
|
4492
|
+
if (threadsResponse.error) {
|
|
4493
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4494
|
+
}
|
|
4495
|
+
const data = threadsResponse.value.data;
|
|
4496
|
+
const repository = data?.repository;
|
|
4497
|
+
const pullRequest = repository?.pullRequest;
|
|
4498
|
+
const threads = pullRequest?.reviewThreads;
|
|
4499
|
+
const nodes = threads?.nodes;
|
|
4500
|
+
if (!Array.isArray(nodes)) {
|
|
4501
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4502
|
+
}
|
|
4503
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4504
|
+
reviewThreads.push(...normalized);
|
|
4505
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4506
|
+
if (truncatedCommentThread) {
|
|
4507
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4508
|
+
}
|
|
4509
|
+
const pageInfo = threads?.pageInfo;
|
|
4510
|
+
if (!pageInfo) {
|
|
4511
|
+
if (nodes.length >= 100) {
|
|
4512
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4513
|
+
}
|
|
4514
|
+
return { value: reviewThreads };
|
|
4515
|
+
}
|
|
4516
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4517
|
+
return { value: reviewThreads };
|
|
4518
|
+
}
|
|
4519
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4520
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4521
|
+
}
|
|
4522
|
+
afterCursor = pageInfo.endCursor;
|
|
4523
|
+
}
|
|
4524
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4525
|
+
}
|
|
4526
|
+
async function collectPrReviewEvidence(input) {
|
|
4527
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4528
|
+
if (!parsed) {
|
|
4529
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4530
|
+
}
|
|
4531
|
+
const readErrors = [];
|
|
4532
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4533
|
+
"pr",
|
|
4534
|
+
"view",
|
|
4535
|
+
input.prUrl,
|
|
4536
|
+
"--json",
|
|
4537
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4538
|
+
], input.projectRoot);
|
|
4539
|
+
if (viewRead.error)
|
|
4540
|
+
readErrors.push(viewRead.error);
|
|
4541
|
+
const view = viewRead.value;
|
|
4542
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4543
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4544
|
+
}
|
|
4545
|
+
if (!Array.isArray(view.reviews)) {
|
|
4546
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4547
|
+
}
|
|
4548
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4549
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4550
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4551
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4552
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4553
|
+
if (reviewCommentsRead.error)
|
|
4554
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4555
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4556
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4557
|
+
if (issueCommentsRead.error)
|
|
4558
|
+
readErrors.push(issueCommentsRead.error);
|
|
4559
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4560
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4561
|
+
command: input.command,
|
|
4562
|
+
projectRoot: input.projectRoot,
|
|
4563
|
+
owner: parsed.owner,
|
|
4564
|
+
name: parsed.repo,
|
|
4565
|
+
prNumber: parsed.prNumber
|
|
4566
|
+
});
|
|
4567
|
+
if (reviewThreadsRead.error)
|
|
4568
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4569
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4570
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4571
|
+
let greptileCheckDetails = [];
|
|
4572
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4573
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4574
|
+
command: input.command,
|
|
4575
|
+
projectRoot: input.projectRoot,
|
|
4576
|
+
repoName: parsed.repoName,
|
|
4577
|
+
headSha
|
|
4578
|
+
});
|
|
4579
|
+
if (checkDetailsRead.error)
|
|
4580
|
+
readErrors.push(checkDetailsRead.error);
|
|
4581
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4582
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4583
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
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];
|
|
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})` : ""}`);
|
|
4599
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4600
|
+
const evidenceBase = {
|
|
4601
|
+
title: firstString(view, ["title"]),
|
|
4602
|
+
body: firstString(view, ["body"]),
|
|
4603
|
+
reviews,
|
|
4604
|
+
changedFileReviewComments: reviewComments,
|
|
4605
|
+
relevantIssueComments: issueComments,
|
|
4606
|
+
reviewThreads,
|
|
4607
|
+
checks: checksWithGreptileDetails,
|
|
4608
|
+
currentHeadSha: headSha,
|
|
4609
|
+
apiSignals
|
|
4610
|
+
};
|
|
4611
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4612
|
+
return {
|
|
4613
|
+
prUrl: input.prUrl,
|
|
4614
|
+
prNumber: parsed.prNumber,
|
|
4615
|
+
repoName: parsed.repoName,
|
|
4616
|
+
title: evidenceBase.title,
|
|
4617
|
+
body: evidenceBase.body,
|
|
4618
|
+
headSha,
|
|
4619
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4620
|
+
baseRefName,
|
|
4621
|
+
state: firstString(view, ["state"]),
|
|
4622
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4623
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4624
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4625
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4626
|
+
reviews,
|
|
4627
|
+
reviewThreads,
|
|
4628
|
+
changedFileReviewComments: reviewComments,
|
|
4629
|
+
relevantIssueComments: issueComments,
|
|
4630
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4631
|
+
checkFailures,
|
|
4632
|
+
pendingChecks,
|
|
4633
|
+
readErrors,
|
|
4634
|
+
greptile
|
|
4635
|
+
};
|
|
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
|
+
}
|
|
4642
|
+
function evaluateEvidence(evidence) {
|
|
4643
|
+
const reasonDetails = [];
|
|
4644
|
+
const warnings = [];
|
|
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
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4701
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
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
|
+
});
|
|
4730
|
+
}
|
|
4731
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
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
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
if (!greptile.completed) {
|
|
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
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
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
|
+
});
|
|
4796
|
+
}
|
|
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
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
if (greptile.mapping === "unproven") {
|
|
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
|
+
});
|
|
4819
|
+
}
|
|
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
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
if (!greptile.approved)
|
|
4843
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4844
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4845
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4846
|
+
}
|
|
4847
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4848
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4849
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4850
|
+
return {
|
|
4851
|
+
approved,
|
|
4852
|
+
pending: evaluated.pending,
|
|
4853
|
+
reasons: evaluated.reasons,
|
|
4854
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4855
|
+
warnings: evaluated.warnings,
|
|
4856
|
+
actionableFeedback: evaluated.reasons,
|
|
4857
|
+
evidence
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
3742
4862
|
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
3743
4863
|
"changed-files.txt",
|
|
3744
4864
|
"contract-changes.md",
|
|
@@ -4571,7 +5691,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4571
5691
|
}
|
|
4572
5692
|
};
|
|
4573
5693
|
}
|
|
4574
|
-
|
|
5694
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
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)) {
|
|
4575
5696
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4576
5697
|
return {
|
|
4577
5698
|
verdict: "REJECT",
|
|
@@ -4587,44 +5708,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
4587
5708
|
}
|
|
4588
5709
|
};
|
|
4589
5710
|
}
|
|
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
|
-
|
|
5711
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5712
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5713
|
+
return {
|
|
5714
|
+
verdict: "REJECT",
|
|
5715
|
+
feedback,
|
|
5716
|
+
reasons,
|
|
5717
|
+
warnings,
|
|
5718
|
+
rawPayload: {
|
|
5719
|
+
pr: options.prState,
|
|
5720
|
+
codeReviews: reviewsPayload,
|
|
5721
|
+
selectedReview,
|
|
5722
|
+
reviewDetails,
|
|
5723
|
+
comments: commentsPayload,
|
|
5724
|
+
score
|
|
5725
|
+
}
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5729
|
+
let strictGate = null;
|
|
5730
|
+
try {
|
|
5731
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5732
|
+
projectRoot: options.projectRoot,
|
|
5733
|
+
taskId: options.taskId,
|
|
5734
|
+
prUrl,
|
|
5735
|
+
apiSignals: [{
|
|
5736
|
+
id: selectedReview.id,
|
|
5737
|
+
body: reviewBody,
|
|
5738
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5739
|
+
status: selectedReview.status
|
|
5740
|
+
}]
|
|
5741
|
+
});
|
|
5742
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5743
|
+
} catch (error) {
|
|
5744
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5745
|
+
return {
|
|
5746
|
+
verdict: "REJECT",
|
|
5747
|
+
feedback,
|
|
5748
|
+
reasons,
|
|
5749
|
+
warnings,
|
|
5750
|
+
rawPayload: {
|
|
5751
|
+
pr: options.prState,
|
|
5752
|
+
codeReviews: reviewsPayload,
|
|
5753
|
+
selectedReview,
|
|
5754
|
+
reviewDetails,
|
|
5755
|
+
comments: commentsPayload,
|
|
5756
|
+
score
|
|
5757
|
+
}
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
if (!strictGate.approved) {
|
|
5761
|
+
return {
|
|
5762
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5763
|
+
feedback,
|
|
5764
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5765
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5766
|
+
rawPayload: {
|
|
5767
|
+
pr: options.prState,
|
|
5768
|
+
codeReviews: reviewsPayload,
|
|
5769
|
+
selectedReview,
|
|
5770
|
+
reviewDetails,
|
|
5771
|
+
comments: commentsPayload,
|
|
5772
|
+
score,
|
|
5773
|
+
strictGate: {
|
|
5774
|
+
approved: strictGate.approved,
|
|
5775
|
+
pending: strictGate.pending,
|
|
5776
|
+
reasons: strictGate.reasons,
|
|
5777
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5778
|
+
warnings: strictGate.warnings,
|
|
5779
|
+
greptile: strictGate.evidence.greptile,
|
|
5780
|
+
readErrors: strictGate.evidence.readErrors
|
|
4622
5781
|
}
|
|
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
|
-
}
|
|
5782
|
+
}
|
|
5783
|
+
};
|
|
4628
5784
|
}
|
|
4629
5785
|
return {
|
|
4630
5786
|
verdict: "APPROVE",
|
|
@@ -4636,7 +5792,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
4636
5792
|
codeReviews: reviewsPayload,
|
|
4637
5793
|
selectedReview,
|
|
4638
5794
|
reviewDetails,
|
|
4639
|
-
comments: commentsPayload
|
|
5795
|
+
comments: commentsPayload,
|
|
5796
|
+
strictGate: {
|
|
5797
|
+
approved: strictGate.approved,
|
|
5798
|
+
pending: strictGate.pending,
|
|
5799
|
+
reasons: strictGate.reasons,
|
|
5800
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5801
|
+
warnings: strictGate.warnings,
|
|
5802
|
+
greptile: strictGate.evidence.greptile,
|
|
5803
|
+
readErrors: strictGate.evidence.readErrors
|
|
5804
|
+
}
|
|
4640
5805
|
}
|
|
4641
5806
|
};
|
|
4642
5807
|
}
|
|
@@ -4660,7 +5825,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4660
5825
|
let threads = [];
|
|
4661
5826
|
let actionableThreads = [];
|
|
4662
5827
|
let checkRollup = [];
|
|
4663
|
-
let
|
|
5828
|
+
let checkState2 = { pending: false, completed: false };
|
|
4664
5829
|
for (let attempt = 0;; attempt += 1) {
|
|
4665
5830
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4666
5831
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4669,15 +5834,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4669
5834
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4670
5835
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4671
5836
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4672
|
-
|
|
4673
|
-
const
|
|
5837
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5838
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4674
5839
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4675
5840
|
attempt,
|
|
4676
5841
|
pollAttempts: options.pollAttempts,
|
|
4677
|
-
checkState,
|
|
5842
|
+
checkState: checkState2,
|
|
4678
5843
|
fallbackReview,
|
|
4679
5844
|
selectedReview,
|
|
4680
|
-
approvedViaReviewedAncestor
|
|
5845
|
+
approvedViaReviewedAncestor
|
|
4681
5846
|
})) {
|
|
4682
5847
|
break;
|
|
4683
5848
|
}
|
|
@@ -4705,7 +5870,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4705
5870
|
].filter(Boolean).join(`
|
|
4706
5871
|
`);
|
|
4707
5872
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4708
|
-
if (
|
|
5873
|
+
if (checkState2.pending) {
|
|
4709
5874
|
return {
|
|
4710
5875
|
verdict: "SKIP",
|
|
4711
5876
|
feedback,
|
|
@@ -4716,34 +5881,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4716
5881
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4717
5882
|
};
|
|
4718
5883
|
}
|
|
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) {
|
|
5884
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5885
|
+
let strictGate;
|
|
5886
|
+
try {
|
|
5887
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5888
|
+
projectRoot: options.projectRoot,
|
|
5889
|
+
taskId: options.taskId,
|
|
5890
|
+
prUrl
|
|
5891
|
+
});
|
|
5892
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5893
|
+
} catch (error) {
|
|
4743
5894
|
return {
|
|
4744
5895
|
verdict: "REJECT",
|
|
4745
5896
|
feedback,
|
|
4746
|
-
reasons:
|
|
5897
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4747
5898
|
warnings,
|
|
4748
5899
|
rawPayload: {
|
|
4749
5900
|
pr: options.prState,
|
|
@@ -4756,44 +5907,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4756
5907
|
}
|
|
4757
5908
|
};
|
|
4758
5909
|
}
|
|
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
|
-
}
|
|
5910
|
+
if (!strictGate.approved) {
|
|
4777
5911
|
return {
|
|
4778
|
-
verdict: "SKIP",
|
|
5912
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4779
5913
|
feedback,
|
|
4780
|
-
reasons: [
|
|
4781
|
-
|
|
4782
|
-
],
|
|
4783
|
-
warnings,
|
|
5914
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5915
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4784
5916
|
rawPayload: {
|
|
4785
5917
|
pr: options.prState,
|
|
4786
5918
|
selectedReview: fallbackReview,
|
|
4787
5919
|
reviews,
|
|
4788
5920
|
threads,
|
|
4789
5921
|
checkRollup,
|
|
5922
|
+
actionableThreads,
|
|
5923
|
+
strictGate: {
|
|
5924
|
+
approved: strictGate.approved,
|
|
5925
|
+
pending: strictGate.pending,
|
|
5926
|
+
reasons: strictGate.reasons,
|
|
5927
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5928
|
+
warnings: strictGate.warnings,
|
|
5929
|
+
greptile: strictGate.evidence.greptile
|
|
5930
|
+
},
|
|
4790
5931
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4791
5932
|
}
|
|
4792
5933
|
};
|
|
4793
5934
|
}
|
|
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
5935
|
return {
|
|
4798
5936
|
verdict: "APPROVE",
|
|
4799
5937
|
feedback,
|
|
@@ -4805,6 +5943,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4805
5943
|
reviews,
|
|
4806
5944
|
threads,
|
|
4807
5945
|
checkRollup,
|
|
5946
|
+
strictGate: {
|
|
5947
|
+
approved: strictGate.approved,
|
|
5948
|
+
pending: strictGate.pending,
|
|
5949
|
+
reasons: strictGate.reasons,
|
|
5950
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5951
|
+
warnings: strictGate.warnings,
|
|
5952
|
+
greptile: strictGate.evidence.greptile
|
|
5953
|
+
},
|
|
4808
5954
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4809
5955
|
}
|
|
4810
5956
|
};
|
|
@@ -4917,19 +6063,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4917
6063
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4918
6064
|
return true;
|
|
4919
6065
|
}
|
|
4920
|
-
return
|
|
6066
|
+
return false;
|
|
4921
6067
|
}
|
|
4922
6068
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4923
6069
|
if (options.githubCheckState.completed) {
|
|
4924
6070
|
return false;
|
|
4925
6071
|
}
|
|
6072
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6073
|
+
return false;
|
|
6074
|
+
}
|
|
4926
6075
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4927
6076
|
return true;
|
|
4928
6077
|
}
|
|
4929
|
-
return
|
|
6078
|
+
return true;
|
|
4930
6079
|
}
|
|
4931
6080
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4932
6081
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6082
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
4933
6085
|
if (waitingForVisiblePendingReview) {
|
|
4934
6086
|
return true;
|
|
4935
6087
|
}
|
|
@@ -4990,6 +6142,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4990
6142
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4991
6143
|
}
|
|
4992
6144
|
}
|
|
6145
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
6146
|
+
return collectPrReviewEvidence({
|
|
6147
|
+
projectRoot: input.projectRoot,
|
|
6148
|
+
prUrl: input.prUrl,
|
|
6149
|
+
taskId: input.taskId,
|
|
6150
|
+
runId: "verifier",
|
|
6151
|
+
cycle: 0,
|
|
6152
|
+
apiSignals: input.apiSignals ?? [],
|
|
6153
|
+
command: async (args, options) => {
|
|
6154
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
6155
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
6156
|
+
}
|
|
6157
|
+
});
|
|
6158
|
+
}
|
|
4993
6159
|
function deriveRepoName(projectRoot, prState) {
|
|
4994
6160
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4995
6161
|
if (fromUrl?.[1]) {
|
|
@@ -5004,8 +6170,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
5004
6170
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5005
6171
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
5006
6172
|
}
|
|
5007
|
-
function
|
|
5008
|
-
|
|
6173
|
+
function isGreptileGithubLogin2(login) {
|
|
6174
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6175
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
5009
6176
|
}
|
|
5010
6177
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
5011
6178
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -5022,7 +6189,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
5022
6189
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
5023
6190
|
}
|
|
5024
6191
|
function sortGithubGreptileReviews(reviews) {
|
|
5025
|
-
return reviews.filter((review) =>
|
|
6192
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
5026
6193
|
}
|
|
5027
6194
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
5028
6195
|
const response = runGhJson(projectRoot, [
|
|
@@ -5095,32 +6262,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
5095
6262
|
}
|
|
5096
6263
|
return { pending: false, completed: false };
|
|
5097
6264
|
}
|
|
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
6265
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
5125
6266
|
const [owner, name] = repoName.split("/");
|
|
5126
6267
|
if (!owner || !name) {
|
|
@@ -5146,7 +6287,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
5146
6287
|
return [];
|
|
5147
6288
|
}
|
|
5148
6289
|
const comments = thread.comments?.nodes || [];
|
|
5149
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6290
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
5150
6291
|
if (!latestGreptileComment?.path?.trim()) {
|
|
5151
6292
|
return [];
|
|
5152
6293
|
}
|
|
@@ -5168,11 +6309,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5168
6309
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5169
6310
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5170
6311
|
}
|
|
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
6312
|
function summarizeComment(input) {
|
|
5177
6313
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5178
6314
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5181,31 +6317,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5181
6317
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5182
6318
|
}
|
|
5183
6319
|
function isAiReviewApproved(input) {
|
|
6320
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6321
|
+
return false;
|
|
6322
|
+
}
|
|
5184
6323
|
if (input.reviewMode !== "required") {
|
|
5185
6324
|
return true;
|
|
5186
6325
|
}
|
|
5187
6326
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5188
6327
|
}
|
|
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
6328
|
|
|
5210
6329
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5211
6330
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|