@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
|
@@ -3611,6 +3611,779 @@ ${JSON.stringify(result, null, 2)}
|
|
|
3611
3611
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3612
3612
|
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3613
3613
|
import { resolve as resolve22 } from "path";
|
|
3614
|
+
|
|
3615
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3616
|
+
function parseJsonObject(value) {
|
|
3617
|
+
if (!value?.trim())
|
|
3618
|
+
return { value: {}, error: "empty JSON output" };
|
|
3619
|
+
try {
|
|
3620
|
+
const parsed = JSON.parse(value);
|
|
3621
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3622
|
+
} catch (error) {
|
|
3623
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
function flattenPaginatedArray(value) {
|
|
3627
|
+
if (!Array.isArray(value))
|
|
3628
|
+
return null;
|
|
3629
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3630
|
+
return value.flatMap((entry) => entry);
|
|
3631
|
+
}
|
|
3632
|
+
return value;
|
|
3633
|
+
}
|
|
3634
|
+
function parseJsonArray(value) {
|
|
3635
|
+
if (!value?.trim())
|
|
3636
|
+
return { value: [], error: "empty JSON output" };
|
|
3637
|
+
try {
|
|
3638
|
+
const parsed = JSON.parse(value);
|
|
3639
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3640
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3641
|
+
} catch (error) {
|
|
3642
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
function parseGithubPrUrl(prUrl) {
|
|
3646
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3647
|
+
if (!match)
|
|
3648
|
+
return null;
|
|
3649
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3650
|
+
if (!Number.isFinite(prNumber))
|
|
3651
|
+
return null;
|
|
3652
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3653
|
+
}
|
|
3654
|
+
function checkName(check) {
|
|
3655
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3656
|
+
}
|
|
3657
|
+
function checkState(check) {
|
|
3658
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3659
|
+
}
|
|
3660
|
+
function isGreptileLabel(value) {
|
|
3661
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3662
|
+
}
|
|
3663
|
+
function isGreptileGithubLogin(value) {
|
|
3664
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3665
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3666
|
+
}
|
|
3667
|
+
function isPassingCheck(check) {
|
|
3668
|
+
const state = checkState(check);
|
|
3669
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3670
|
+
}
|
|
3671
|
+
function isPendingCheck(check) {
|
|
3672
|
+
const state = checkState(check);
|
|
3673
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3674
|
+
}
|
|
3675
|
+
function isFailingCheck(check) {
|
|
3676
|
+
const state = checkState(check);
|
|
3677
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3678
|
+
}
|
|
3679
|
+
function wildcardToRegExp(pattern) {
|
|
3680
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3681
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3682
|
+
}
|
|
3683
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3684
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3685
|
+
}
|
|
3686
|
+
function greptileScorePatterns() {
|
|
3687
|
+
return [
|
|
3688
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3689
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3690
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3691
|
+
];
|
|
3692
|
+
}
|
|
3693
|
+
function parseGreptileScores(input) {
|
|
3694
|
+
const text = stripHtml(input);
|
|
3695
|
+
const seen = new Set;
|
|
3696
|
+
const scores = [];
|
|
3697
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3698
|
+
for (const match of text.matchAll(pattern)) {
|
|
3699
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3700
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3701
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3702
|
+
continue;
|
|
3703
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3704
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3705
|
+
if (seen.has(key))
|
|
3706
|
+
continue;
|
|
3707
|
+
seen.add(key);
|
|
3708
|
+
scores.push({ value, scale, raw });
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return scores;
|
|
3712
|
+
}
|
|
3713
|
+
function parseGreptileScore(input) {
|
|
3714
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3715
|
+
}
|
|
3716
|
+
function stripHtml(input) {
|
|
3717
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3718
|
+
|
|
3719
|
+
`).trim();
|
|
3720
|
+
}
|
|
3721
|
+
function containsBlockerText(input) {
|
|
3722
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3723
|
+
return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(text);
|
|
3724
|
+
}
|
|
3725
|
+
function containsGreptileNegativeVerdict(input) {
|
|
3726
|
+
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3727
|
+
if (!text)
|
|
3728
|
+
return false;
|
|
3729
|
+
return /\b(?:status|verdict|review state|state|conclusion|result)\s*:?\s*(?:reject(?:ed|ion)?|skip(?:ped)?|fail(?:ed|ure)?|changes[_ ]requested|not approved)\b/i.test(text) || /\bgreptile\b.{0,160}\b(?:reject(?:ed|s|ion)?|skip(?:ped|s)?|fail(?:ed|s|ure)?|changes requested|did not approve|not approved)\b/i.test(text);
|
|
3730
|
+
}
|
|
3731
|
+
function isStrictFiveOfFive(score) {
|
|
3732
|
+
return score.value === 5 && score.scale === 5;
|
|
3733
|
+
}
|
|
3734
|
+
function containsConflictingScoreText(input) {
|
|
3735
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3736
|
+
}
|
|
3737
|
+
function firstString(record, keys) {
|
|
3738
|
+
for (const key of keys) {
|
|
3739
|
+
const value = record[key];
|
|
3740
|
+
if (typeof value === "string")
|
|
3741
|
+
return value;
|
|
3742
|
+
}
|
|
3743
|
+
return "";
|
|
3744
|
+
}
|
|
3745
|
+
function arrayField(record, key) {
|
|
3746
|
+
const value = record[key];
|
|
3747
|
+
return Array.isArray(value) ? value : [];
|
|
3748
|
+
}
|
|
3749
|
+
async function runJsonArray(command, args, cwd) {
|
|
3750
|
+
const result = await command(args, { cwd });
|
|
3751
|
+
const label = `gh ${args.join(" ")}`;
|
|
3752
|
+
if (result.exitCode !== 0) {
|
|
3753
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3754
|
+
}
|
|
3755
|
+
const parsed = parseJsonArray(result.stdout);
|
|
3756
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3757
|
+
}
|
|
3758
|
+
async function runJsonObject(command, args, cwd) {
|
|
3759
|
+
const result = await command(args, { cwd });
|
|
3760
|
+
const label = `gh ${args.join(" ")}`;
|
|
3761
|
+
if (result.exitCode !== 0) {
|
|
3762
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3763
|
+
}
|
|
3764
|
+
const parsed = parseJsonObject(result.stdout);
|
|
3765
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3766
|
+
}
|
|
3767
|
+
function normalizeStatusCheck(entry) {
|
|
3768
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3769
|
+
return null;
|
|
3770
|
+
const record = entry;
|
|
3771
|
+
const name = firstString(record, ["name", "context"]);
|
|
3772
|
+
if (!name.trim())
|
|
3773
|
+
return null;
|
|
3774
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
3775
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
3776
|
+
return {
|
|
3777
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
3778
|
+
name,
|
|
3779
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
3780
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3781
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3782
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
3783
|
+
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,
|
|
3784
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
3785
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
3786
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
3787
|
+
output: output ? {
|
|
3788
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
3789
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
3790
|
+
text: typeof output.text === "string" ? output.text : null
|
|
3791
|
+
} : null,
|
|
3792
|
+
app: app ? {
|
|
3793
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
3794
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
3795
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
3796
|
+
} : null
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function normalizeReview(entry) {
|
|
3800
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3801
|
+
return null;
|
|
3802
|
+
const record = entry;
|
|
3803
|
+
return {
|
|
3804
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
3805
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3806
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3807
|
+
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,
|
|
3808
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
3809
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
function normalizeReviewComment(entry) {
|
|
3813
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3814
|
+
return null;
|
|
3815
|
+
const record = entry;
|
|
3816
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3817
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
3818
|
+
if (!body && !path)
|
|
3819
|
+
return null;
|
|
3820
|
+
return {
|
|
3821
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3822
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3823
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3824
|
+
body,
|
|
3825
|
+
path,
|
|
3826
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3827
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3828
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
3829
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
function normalizeIssueComment(entry) {
|
|
3833
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3834
|
+
return null;
|
|
3835
|
+
const record = entry;
|
|
3836
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3837
|
+
if (!body)
|
|
3838
|
+
return null;
|
|
3839
|
+
return {
|
|
3840
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3841
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3842
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3843
|
+
body,
|
|
3844
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3845
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3846
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
function normalizeReviewThread(entry) {
|
|
3850
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3851
|
+
return null;
|
|
3852
|
+
const record = entry;
|
|
3853
|
+
return {
|
|
3854
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
3855
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
3856
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
3857
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3860
|
+
function relevantIssueComment(comment) {
|
|
3861
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
3862
|
+
const body = comment.body ?? "";
|
|
3863
|
+
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);
|
|
3864
|
+
}
|
|
3865
|
+
function latestThreadComment(thread) {
|
|
3866
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
3867
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
3868
|
+
}
|
|
3869
|
+
function unresolvedThreadSummaries(threads) {
|
|
3870
|
+
return threads.flatMap((thread) => {
|
|
3871
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
3872
|
+
return [];
|
|
3873
|
+
const latest = latestThreadComment(thread);
|
|
3874
|
+
if (!latest)
|
|
3875
|
+
return ["Unresolved review thread"];
|
|
3876
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
3877
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
function collectBodies(evidence) {
|
|
3881
|
+
return [
|
|
3882
|
+
evidence.title ?? "",
|
|
3883
|
+
evidence.body,
|
|
3884
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
3885
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
3886
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
3887
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
3888
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
3889
|
+
].filter((body) => body.trim().length > 0);
|
|
3890
|
+
}
|
|
3891
|
+
function bodyExcerpt(body) {
|
|
3892
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
3893
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
3894
|
+
}
|
|
3895
|
+
function makeGreptileSignal(input) {
|
|
3896
|
+
const scores = parseGreptileScores(input.body);
|
|
3897
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
3898
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
3899
|
+
const blocker = input.blocker ?? (containsBlockerText(input.body) || containsGreptileNegativeVerdict(input.body));
|
|
3900
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
3901
|
+
return {
|
|
3902
|
+
source: input.source,
|
|
3903
|
+
trusted: input.trusted,
|
|
3904
|
+
authorLogin: input.authorLogin ?? null,
|
|
3905
|
+
reviewedSha,
|
|
3906
|
+
current,
|
|
3907
|
+
stale: current === false,
|
|
3908
|
+
score: scores[0] ?? null,
|
|
3909
|
+
scores,
|
|
3910
|
+
explicitApproval,
|
|
3911
|
+
blocker,
|
|
3912
|
+
actionable: input.actionable ?? blocker,
|
|
3913
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
3914
|
+
body: input.body,
|
|
3915
|
+
allScores: scores
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
function reviewAuthorLogin(review) {
|
|
3919
|
+
return review.author?.login ?? null;
|
|
3920
|
+
}
|
|
3921
|
+
function commentAuthorLogin(comment) {
|
|
3922
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
3923
|
+
}
|
|
3924
|
+
function collectGreptileSignals(evidence) {
|
|
3925
|
+
const signals = [];
|
|
3926
|
+
const contextSources = [
|
|
3927
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
3928
|
+
{ source: "pr-body", body: evidence.body }
|
|
3929
|
+
];
|
|
3930
|
+
for (const context of contextSources) {
|
|
3931
|
+
if (!context.body.trim())
|
|
3932
|
+
continue;
|
|
3933
|
+
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
3934
|
+
continue;
|
|
3935
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
3936
|
+
signals.push(makeGreptileSignal({
|
|
3937
|
+
source: context.source,
|
|
3938
|
+
body: context.body,
|
|
3939
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3940
|
+
trusted: false,
|
|
3941
|
+
blocker: contextBlocker,
|
|
3942
|
+
actionable: contextBlocker
|
|
3943
|
+
}));
|
|
3944
|
+
}
|
|
3945
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
3946
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
3947
|
+
|
|
3948
|
+
`);
|
|
3949
|
+
if (!body.trim())
|
|
3950
|
+
continue;
|
|
3951
|
+
signals.push(makeGreptileSignal({
|
|
3952
|
+
source: "api",
|
|
3953
|
+
body,
|
|
3954
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3955
|
+
trusted: true,
|
|
3956
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
3957
|
+
explicitApproval: false
|
|
3958
|
+
}));
|
|
3959
|
+
}
|
|
3960
|
+
for (const review of evidence.reviews) {
|
|
3961
|
+
const login = reviewAuthorLogin(review);
|
|
3962
|
+
if (!isGreptileGithubLogin(login))
|
|
3963
|
+
continue;
|
|
3964
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
3965
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
3966
|
+
|
|
3967
|
+
`);
|
|
3968
|
+
if (!body.trim())
|
|
3969
|
+
continue;
|
|
3970
|
+
const dismissed = state === "DISMISSED";
|
|
3971
|
+
signals.push(makeGreptileSignal({
|
|
3972
|
+
source: "github-review",
|
|
3973
|
+
body,
|
|
3974
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3975
|
+
trusted: !dismissed,
|
|
3976
|
+
authorLogin: login,
|
|
3977
|
+
reviewedSha: review.commit_id ?? null,
|
|
3978
|
+
explicitApproval: undefined,
|
|
3979
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
3980
|
+
}));
|
|
3981
|
+
}
|
|
3982
|
+
for (const comment of evidence.changedFileReviewComments) {
|
|
3983
|
+
const login = commentAuthorLogin(comment);
|
|
3984
|
+
const body = comment.body ?? "";
|
|
3985
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
3986
|
+
continue;
|
|
3987
|
+
signals.push(makeGreptileSignal({
|
|
3988
|
+
source: "changed-file-comment",
|
|
3989
|
+
body,
|
|
3990
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3991
|
+
trusted: true,
|
|
3992
|
+
authorLogin: login,
|
|
3993
|
+
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
3994
|
+
}));
|
|
3995
|
+
}
|
|
3996
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
3997
|
+
const login = commentAuthorLogin(comment);
|
|
3998
|
+
const body = comment.body ?? "";
|
|
3999
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4000
|
+
continue;
|
|
4001
|
+
signals.push(makeGreptileSignal({
|
|
4002
|
+
source: "issue-comment",
|
|
4003
|
+
body,
|
|
4004
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4005
|
+
trusted: true,
|
|
4006
|
+
authorLogin: login
|
|
4007
|
+
}));
|
|
4008
|
+
}
|
|
4009
|
+
for (const thread of evidence.reviewThreads) {
|
|
4010
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4011
|
+
continue;
|
|
4012
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4013
|
+
const login = comment.author?.login ?? null;
|
|
4014
|
+
const body = comment.body ?? "";
|
|
4015
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4016
|
+
continue;
|
|
4017
|
+
signals.push(makeGreptileSignal({
|
|
4018
|
+
source: "review-thread",
|
|
4019
|
+
body,
|
|
4020
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4021
|
+
trusted: true,
|
|
4022
|
+
authorLogin: login
|
|
4023
|
+
}));
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
for (const check of evidence.checks) {
|
|
4027
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4028
|
+
continue;
|
|
4029
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4030
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4031
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4032
|
+
|
|
4033
|
+
`);
|
|
4034
|
+
signals.push(makeGreptileSignal({
|
|
4035
|
+
source: "github-check",
|
|
4036
|
+
body,
|
|
4037
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4038
|
+
trusted: false,
|
|
4039
|
+
reviewedSha,
|
|
4040
|
+
explicitApproval: false,
|
|
4041
|
+
blocker: isFailingCheck(check),
|
|
4042
|
+
actionable: isFailingCheck(check)
|
|
4043
|
+
}));
|
|
4044
|
+
}
|
|
4045
|
+
return signals;
|
|
4046
|
+
}
|
|
4047
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4048
|
+
return threads.flatMap((thread) => {
|
|
4049
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4050
|
+
return [];
|
|
4051
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4052
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4053
|
+
return [];
|
|
4054
|
+
const latest = latestThreadComment(thread);
|
|
4055
|
+
if (!latest)
|
|
4056
|
+
return ["Unresolved Greptile review thread"];
|
|
4057
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4058
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4062
|
+
return comments.flatMap((comment) => {
|
|
4063
|
+
const body = comment.body?.trim() ?? "";
|
|
4064
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4065
|
+
return [];
|
|
4066
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4067
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4068
|
+
return [`${author}: ${body}`];
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4072
|
+
return reviews.flatMap((review) => {
|
|
4073
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4074
|
+
if (isGreptileGithubLogin(login))
|
|
4075
|
+
return [];
|
|
4076
|
+
const body = review.body?.trim() ?? "";
|
|
4077
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4078
|
+
return [];
|
|
4079
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4080
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
function signalLabel(signal) {
|
|
4084
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4085
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4086
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4087
|
+
return `${source}${author}${sha}`;
|
|
4088
|
+
}
|
|
4089
|
+
function deriveGreptileEvidence(input) {
|
|
4090
|
+
const rawBodies = collectBodies(input);
|
|
4091
|
+
const signals = collectGreptileSignals(input);
|
|
4092
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4093
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4094
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4095
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4096
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4097
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4098
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4099
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4100
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4101
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4102
|
+
const approvedByExplicitMapping = false;
|
|
4103
|
+
const approvingSignal = approvingScoreEntry?.signal ?? null;
|
|
4104
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4105
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4106
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4107
|
+
const blockerSignals = signals.filter((signal) => signal.source !== "changed-file-comment" && (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4108
|
+
const staleBlockingSignals = [];
|
|
4109
|
+
const blockers = [
|
|
4110
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4111
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4112
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4113
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4114
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4115
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4116
|
+
];
|
|
4117
|
+
const unresolvedComments = [
|
|
4118
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads)
|
|
4119
|
+
];
|
|
4120
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4121
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4122
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4123
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4124
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4125
|
+
});
|
|
4126
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4127
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4128
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4129
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4130
|
+
});
|
|
4131
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4132
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4133
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4134
|
+
const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
|
|
4135
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4136
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4137
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4138
|
+
const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : approvingSignal?.source === "changed-file-comment" || approvingSignal?.source === "issue-comment" || approvingSignal?.source === "review-thread" ? "github-comment" : greptileReviews.length > 0 && greptileChecks.length > 0 ? "combined" : greptileReviews.length > 0 ? "github-review" : greptileChecks.length > 0 ? "github-check" : signals.some((signal) => signal.source === "pr-body" || signal.source === "pr-title") ? "pr-body" : "missing";
|
|
4139
|
+
return {
|
|
4140
|
+
source,
|
|
4141
|
+
currentHeadSha: input.currentHeadSha,
|
|
4142
|
+
reviewedSha,
|
|
4143
|
+
fresh,
|
|
4144
|
+
completed,
|
|
4145
|
+
approved,
|
|
4146
|
+
score,
|
|
4147
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4148
|
+
blockers,
|
|
4149
|
+
unresolvedComments,
|
|
4150
|
+
rawBodies,
|
|
4151
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4152
|
+
mapping
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
function isGreptileCheckDetail(check) {
|
|
4156
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4157
|
+
}
|
|
4158
|
+
async function collectGreptileCheckDetails(input) {
|
|
4159
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4160
|
+
"api",
|
|
4161
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4162
|
+
"--paginate",
|
|
4163
|
+
"--slurp",
|
|
4164
|
+
"--jq",
|
|
4165
|
+
"map(.check_runs // []) | add // []"
|
|
4166
|
+
], input.projectRoot);
|
|
4167
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4168
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4169
|
+
}
|
|
4170
|
+
async function collectReviewThreads(input) {
|
|
4171
|
+
const reviewThreads = [];
|
|
4172
|
+
let afterCursor = null;
|
|
4173
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4174
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4175
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4176
|
+
"api",
|
|
4177
|
+
"graphql",
|
|
4178
|
+
"-F",
|
|
4179
|
+
`owner=${input.owner}`,
|
|
4180
|
+
"-F",
|
|
4181
|
+
`name=${input.name}`,
|
|
4182
|
+
"-F",
|
|
4183
|
+
`prNumber=${input.prNumber}`,
|
|
4184
|
+
"-f",
|
|
4185
|
+
`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 } } } } }`
|
|
4186
|
+
], input.projectRoot);
|
|
4187
|
+
if (threadsResponse.error) {
|
|
4188
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4189
|
+
}
|
|
4190
|
+
const data = threadsResponse.value.data;
|
|
4191
|
+
const repository = data?.repository;
|
|
4192
|
+
const pullRequest = repository?.pullRequest;
|
|
4193
|
+
const threads = pullRequest?.reviewThreads;
|
|
4194
|
+
const nodes = threads?.nodes;
|
|
4195
|
+
if (!Array.isArray(nodes)) {
|
|
4196
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4197
|
+
}
|
|
4198
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4199
|
+
reviewThreads.push(...normalized);
|
|
4200
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4201
|
+
if (truncatedCommentThread) {
|
|
4202
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4203
|
+
}
|
|
4204
|
+
const pageInfo = threads?.pageInfo;
|
|
4205
|
+
if (!pageInfo) {
|
|
4206
|
+
if (nodes.length >= 100) {
|
|
4207
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4208
|
+
}
|
|
4209
|
+
return { value: reviewThreads };
|
|
4210
|
+
}
|
|
4211
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4212
|
+
return { value: reviewThreads };
|
|
4213
|
+
}
|
|
4214
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4215
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4216
|
+
}
|
|
4217
|
+
afterCursor = pageInfo.endCursor;
|
|
4218
|
+
}
|
|
4219
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4220
|
+
}
|
|
4221
|
+
async function collectPrReviewEvidence(input) {
|
|
4222
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4223
|
+
if (!parsed) {
|
|
4224
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4225
|
+
}
|
|
4226
|
+
const readErrors = [];
|
|
4227
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4228
|
+
"pr",
|
|
4229
|
+
"view",
|
|
4230
|
+
input.prUrl,
|
|
4231
|
+
"--json",
|
|
4232
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4233
|
+
], input.projectRoot);
|
|
4234
|
+
if (viewRead.error)
|
|
4235
|
+
readErrors.push(viewRead.error);
|
|
4236
|
+
const view = viewRead.value;
|
|
4237
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4238
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4239
|
+
}
|
|
4240
|
+
if (!Array.isArray(view.reviews)) {
|
|
4241
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4242
|
+
}
|
|
4243
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4244
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4245
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4246
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4247
|
+
if (reviewCommentsRead.error)
|
|
4248
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4249
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4250
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4251
|
+
if (issueCommentsRead.error)
|
|
4252
|
+
readErrors.push(issueCommentsRead.error);
|
|
4253
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4254
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4255
|
+
command: input.command,
|
|
4256
|
+
projectRoot: input.projectRoot,
|
|
4257
|
+
owner: parsed.owner,
|
|
4258
|
+
name: parsed.repo,
|
|
4259
|
+
prNumber: parsed.prNumber
|
|
4260
|
+
});
|
|
4261
|
+
if (reviewThreadsRead.error)
|
|
4262
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4263
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4264
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4265
|
+
let greptileCheckDetails = [];
|
|
4266
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4267
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4268
|
+
command: input.command,
|
|
4269
|
+
projectRoot: input.projectRoot,
|
|
4270
|
+
repoName: parsed.repoName,
|
|
4271
|
+
headSha
|
|
4272
|
+
});
|
|
4273
|
+
if (checkDetailsRead.error)
|
|
4274
|
+
readErrors.push(checkDetailsRead.error);
|
|
4275
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4276
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4277
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4281
|
+
const checkFailures = statusCheckRollup.filter((check) => !isGreptileLabel(checkName(check)) && isFailingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check failed: ${checkName(check)}${check.detailsUrl || check.link ? ` (${check.detailsUrl ?? check.link})` : ""}`);
|
|
4282
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4283
|
+
const evidenceBase = {
|
|
4284
|
+
title: firstString(view, ["title"]),
|
|
4285
|
+
body: firstString(view, ["body"]),
|
|
4286
|
+
reviews,
|
|
4287
|
+
changedFileReviewComments: reviewComments,
|
|
4288
|
+
relevantIssueComments: issueComments,
|
|
4289
|
+
reviewThreads,
|
|
4290
|
+
checks: checksWithGreptileDetails,
|
|
4291
|
+
currentHeadSha: headSha,
|
|
4292
|
+
apiSignals: input.apiSignals ?? []
|
|
4293
|
+
};
|
|
4294
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4295
|
+
return {
|
|
4296
|
+
prUrl: input.prUrl,
|
|
4297
|
+
prNumber: parsed.prNumber,
|
|
4298
|
+
repoName: parsed.repoName,
|
|
4299
|
+
title: evidenceBase.title,
|
|
4300
|
+
body: evidenceBase.body,
|
|
4301
|
+
headSha,
|
|
4302
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4303
|
+
baseRefName: firstString(view, ["baseRefName"]),
|
|
4304
|
+
state: firstString(view, ["state"]),
|
|
4305
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4306
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4307
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4308
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4309
|
+
reviews,
|
|
4310
|
+
reviewThreads,
|
|
4311
|
+
changedFileReviewComments: reviewComments,
|
|
4312
|
+
relevantIssueComments: issueComments,
|
|
4313
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4314
|
+
checkFailures,
|
|
4315
|
+
pendingChecks,
|
|
4316
|
+
readErrors,
|
|
4317
|
+
greptile
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
function evaluateEvidence(evidence) {
|
|
4321
|
+
const reasons = [];
|
|
4322
|
+
const warnings = [];
|
|
4323
|
+
let pending = false;
|
|
4324
|
+
if (evidence.readErrors.length > 0) {
|
|
4325
|
+
reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
|
|
4326
|
+
}
|
|
4327
|
+
if (!evidence.headSha)
|
|
4328
|
+
reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
|
|
4329
|
+
if (evidence.checkFailures.length > 0)
|
|
4330
|
+
reasons.push(...evidence.checkFailures);
|
|
4331
|
+
if (evidence.pendingChecks.length > 0) {
|
|
4332
|
+
pending = true;
|
|
4333
|
+
reasons.push(...evidence.pendingChecks);
|
|
4334
|
+
}
|
|
4335
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4336
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4337
|
+
reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
|
|
4338
|
+
}
|
|
4339
|
+
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4340
|
+
if (unresolvedThreads.length > 0)
|
|
4341
|
+
reasons.push(...unresolvedThreads);
|
|
4342
|
+
const greptile = evidence.greptile;
|
|
4343
|
+
if (greptile.mapping === "missing")
|
|
4344
|
+
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4345
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4346
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4347
|
+
reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
|
|
4348
|
+
}
|
|
4349
|
+
if (!greptile.completed) {
|
|
4350
|
+
pending = true;
|
|
4351
|
+
reasons.push("Greptile check/review has not completed for the current PR head.");
|
|
4352
|
+
}
|
|
4353
|
+
if (!greptile.fresh)
|
|
4354
|
+
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4355
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4356
|
+
reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
|
|
4357
|
+
}
|
|
4358
|
+
if (!greptile.score && greptile.mapping !== "score-5-of-5") {
|
|
4359
|
+
reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
|
|
4360
|
+
}
|
|
4361
|
+
if (greptile.mapping === "unproven") {
|
|
4362
|
+
reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
|
|
4363
|
+
}
|
|
4364
|
+
if (greptile.blockers.length > 0) {
|
|
4365
|
+
reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
|
|
4366
|
+
}
|
|
4367
|
+
if (greptile.unresolvedComments.length > 0)
|
|
4368
|
+
reasons.push(...greptile.unresolvedComments);
|
|
4369
|
+
if (!greptile.approved)
|
|
4370
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4371
|
+
return { reasons: Array.from(new Set(reasons)), warnings, pending };
|
|
4372
|
+
}
|
|
4373
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4374
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4375
|
+
const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
|
|
4376
|
+
return {
|
|
4377
|
+
approved,
|
|
4378
|
+
pending: evaluated.pending,
|
|
4379
|
+
reasons: evaluated.reasons,
|
|
4380
|
+
warnings: evaluated.warnings,
|
|
4381
|
+
actionableFeedback: evaluated.reasons,
|
|
4382
|
+
evidence
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3614
4387
|
async function verifyTask(options) {
|
|
3615
4388
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
3616
4389
|
const taskId = options.taskId;
|
|
@@ -4406,7 +5179,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4406
5179
|
}
|
|
4407
5180
|
};
|
|
4408
5181
|
}
|
|
4409
|
-
|
|
5182
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5183
|
+
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(blockerScanBody)) {
|
|
4410
5184
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4411
5185
|
return {
|
|
4412
5186
|
verdict: "REJECT",
|
|
@@ -4422,44 +5196,78 @@ async function runGreptileReviewForPr(options) {
|
|
|
4422
5196
|
}
|
|
4423
5197
|
};
|
|
4424
5198
|
}
|
|
4425
|
-
if (score) {
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
5199
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5200
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5201
|
+
return {
|
|
5202
|
+
verdict: "REJECT",
|
|
5203
|
+
feedback,
|
|
5204
|
+
reasons,
|
|
5205
|
+
warnings,
|
|
5206
|
+
rawPayload: {
|
|
5207
|
+
pr: options.prState,
|
|
5208
|
+
codeReviews: reviewsPayload,
|
|
5209
|
+
selectedReview,
|
|
5210
|
+
reviewDetails,
|
|
5211
|
+
comments: commentsPayload,
|
|
5212
|
+
score
|
|
5213
|
+
}
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5217
|
+
let strictGate = null;
|
|
5218
|
+
try {
|
|
5219
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5220
|
+
projectRoot: options.projectRoot,
|
|
5221
|
+
taskId: options.taskId,
|
|
5222
|
+
prUrl,
|
|
5223
|
+
apiSignals: [{
|
|
5224
|
+
id: selectedReview.id,
|
|
5225
|
+
body: reviewBody,
|
|
5226
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5227
|
+
status: selectedReview.status
|
|
5228
|
+
}]
|
|
5229
|
+
});
|
|
5230
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5231
|
+
} catch (error) {
|
|
5232
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5233
|
+
return {
|
|
5234
|
+
verdict: "REJECT",
|
|
5235
|
+
feedback,
|
|
5236
|
+
reasons,
|
|
5237
|
+
warnings,
|
|
5238
|
+
rawPayload: {
|
|
5239
|
+
pr: options.prState,
|
|
5240
|
+
codeReviews: reviewsPayload,
|
|
5241
|
+
selectedReview,
|
|
5242
|
+
reviewDetails,
|
|
5243
|
+
comments: commentsPayload,
|
|
5244
|
+
score
|
|
5245
|
+
}
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
if (!strictGate.approved) {
|
|
5249
|
+
return {
|
|
5250
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5251
|
+
feedback,
|
|
5252
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5253
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5254
|
+
rawPayload: {
|
|
5255
|
+
pr: options.prState,
|
|
5256
|
+
codeReviews: reviewsPayload,
|
|
5257
|
+
selectedReview,
|
|
5258
|
+
reviewDetails,
|
|
5259
|
+
comments: commentsPayload,
|
|
5260
|
+
score,
|
|
5261
|
+
strictGate: {
|
|
5262
|
+
approved: strictGate.approved,
|
|
5263
|
+
pending: strictGate.pending,
|
|
5264
|
+
reasons: strictGate.reasons,
|
|
5265
|
+
warnings: strictGate.warnings,
|
|
5266
|
+
greptile: strictGate.evidence.greptile,
|
|
5267
|
+
readErrors: strictGate.evidence.readErrors
|
|
4457
5268
|
}
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4461
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4462
|
-
}
|
|
5269
|
+
}
|
|
5270
|
+
};
|
|
4463
5271
|
}
|
|
4464
5272
|
return {
|
|
4465
5273
|
verdict: "APPROVE",
|
|
@@ -4471,7 +5279,15 @@ async function runGreptileReviewForPr(options) {
|
|
|
4471
5279
|
codeReviews: reviewsPayload,
|
|
4472
5280
|
selectedReview,
|
|
4473
5281
|
reviewDetails,
|
|
4474
|
-
comments: commentsPayload
|
|
5282
|
+
comments: commentsPayload,
|
|
5283
|
+
strictGate: {
|
|
5284
|
+
approved: strictGate.approved,
|
|
5285
|
+
pending: strictGate.pending,
|
|
5286
|
+
reasons: strictGate.reasons,
|
|
5287
|
+
warnings: strictGate.warnings,
|
|
5288
|
+
greptile: strictGate.evidence.greptile,
|
|
5289
|
+
readErrors: strictGate.evidence.readErrors
|
|
5290
|
+
}
|
|
4475
5291
|
}
|
|
4476
5292
|
};
|
|
4477
5293
|
}
|
|
@@ -4495,7 +5311,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4495
5311
|
let threads = [];
|
|
4496
5312
|
let actionableThreads = [];
|
|
4497
5313
|
let checkRollup = [];
|
|
4498
|
-
let
|
|
5314
|
+
let checkState2 = { pending: false, completed: false };
|
|
4499
5315
|
for (let attempt = 0;; attempt += 1) {
|
|
4500
5316
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4501
5317
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4504,15 +5320,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4504
5320
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4505
5321
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4506
5322
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4507
|
-
|
|
4508
|
-
const
|
|
5323
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5324
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4509
5325
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4510
5326
|
attempt,
|
|
4511
5327
|
pollAttempts: options.pollAttempts,
|
|
4512
|
-
checkState,
|
|
5328
|
+
checkState: checkState2,
|
|
4513
5329
|
fallbackReview,
|
|
4514
5330
|
selectedReview,
|
|
4515
|
-
approvedViaReviewedAncestor
|
|
5331
|
+
approvedViaReviewedAncestor
|
|
4516
5332
|
})) {
|
|
4517
5333
|
break;
|
|
4518
5334
|
}
|
|
@@ -4540,7 +5356,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4540
5356
|
].filter(Boolean).join(`
|
|
4541
5357
|
`);
|
|
4542
5358
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4543
|
-
if (
|
|
5359
|
+
if (checkState2.pending) {
|
|
4544
5360
|
return {
|
|
4545
5361
|
verdict: "SKIP",
|
|
4546
5362
|
feedback,
|
|
@@ -4551,34 +5367,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4551
5367
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4552
5368
|
};
|
|
4553
5369
|
}
|
|
4554
|
-
const
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
};
|
|
4565
|
-
}
|
|
4566
|
-
return {
|
|
4567
|
-
verdict: "SKIP",
|
|
4568
|
-
feedback,
|
|
4569
|
-
reasons: [
|
|
4570
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4571
|
-
],
|
|
4572
|
-
warnings,
|
|
4573
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4577
|
-
if (actionableThreads.length > 0) {
|
|
5370
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5371
|
+
let strictGate;
|
|
5372
|
+
try {
|
|
5373
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5374
|
+
projectRoot: options.projectRoot,
|
|
5375
|
+
taskId: options.taskId,
|
|
5376
|
+
prUrl
|
|
5377
|
+
});
|
|
5378
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5379
|
+
} catch (error) {
|
|
4578
5380
|
return {
|
|
4579
5381
|
verdict: "REJECT",
|
|
4580
5382
|
feedback,
|
|
4581
|
-
reasons:
|
|
5383
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4582
5384
|
warnings,
|
|
4583
5385
|
rawPayload: {
|
|
4584
5386
|
pr: options.prState,
|
|
@@ -4591,44 +5393,30 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4591
5393
|
}
|
|
4592
5394
|
};
|
|
4593
5395
|
}
|
|
4594
|
-
if (!
|
|
4595
|
-
if (approvedViaCompletedCheck) {
|
|
4596
|
-
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.`);
|
|
4597
|
-
return {
|
|
4598
|
-
verdict: "APPROVE",
|
|
4599
|
-
feedback,
|
|
4600
|
-
reasons: [],
|
|
4601
|
-
warnings,
|
|
4602
|
-
rawPayload: {
|
|
4603
|
-
pr: options.prState,
|
|
4604
|
-
selectedReview: fallbackReview,
|
|
4605
|
-
reviews,
|
|
4606
|
-
threads,
|
|
4607
|
-
checkRollup,
|
|
4608
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4609
|
-
}
|
|
4610
|
-
};
|
|
4611
|
-
}
|
|
5396
|
+
if (!strictGate.approved) {
|
|
4612
5397
|
return {
|
|
4613
|
-
verdict: "SKIP",
|
|
5398
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4614
5399
|
feedback,
|
|
4615
|
-
reasons: [
|
|
4616
|
-
|
|
4617
|
-
],
|
|
4618
|
-
warnings,
|
|
5400
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5401
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4619
5402
|
rawPayload: {
|
|
4620
5403
|
pr: options.prState,
|
|
4621
5404
|
selectedReview: fallbackReview,
|
|
4622
5405
|
reviews,
|
|
4623
5406
|
threads,
|
|
4624
5407
|
checkRollup,
|
|
5408
|
+
actionableThreads,
|
|
5409
|
+
strictGate: {
|
|
5410
|
+
approved: strictGate.approved,
|
|
5411
|
+
pending: strictGate.pending,
|
|
5412
|
+
reasons: strictGate.reasons,
|
|
5413
|
+
warnings: strictGate.warnings,
|
|
5414
|
+
greptile: strictGate.evidence.greptile
|
|
5415
|
+
},
|
|
4625
5416
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4626
5417
|
}
|
|
4627
5418
|
};
|
|
4628
5419
|
}
|
|
4629
|
-
if (approvedViaReviewedAncestor) {
|
|
4630
|
-
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.`);
|
|
4631
|
-
}
|
|
4632
5420
|
return {
|
|
4633
5421
|
verdict: "APPROVE",
|
|
4634
5422
|
feedback,
|
|
@@ -4640,6 +5428,13 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4640
5428
|
reviews,
|
|
4641
5429
|
threads,
|
|
4642
5430
|
checkRollup,
|
|
5431
|
+
strictGate: {
|
|
5432
|
+
approved: strictGate.approved,
|
|
5433
|
+
pending: strictGate.pending,
|
|
5434
|
+
reasons: strictGate.reasons,
|
|
5435
|
+
warnings: strictGate.warnings,
|
|
5436
|
+
greptile: strictGate.evidence.greptile
|
|
5437
|
+
},
|
|
4643
5438
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4644
5439
|
}
|
|
4645
5440
|
};
|
|
@@ -4752,21 +5547,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4752
5547
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4753
5548
|
return true;
|
|
4754
5549
|
}
|
|
4755
|
-
return
|
|
5550
|
+
return false;
|
|
4756
5551
|
}
|
|
4757
5552
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4758
5553
|
if (options.githubCheckState.completed) {
|
|
4759
5554
|
return false;
|
|
4760
5555
|
}
|
|
5556
|
+
const hasRemainingBudget = options.attempt + 1 < options.pollAttempts;
|
|
5557
|
+
if (!hasRemainingBudget) {
|
|
5558
|
+
return false;
|
|
5559
|
+
}
|
|
4761
5560
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4762
5561
|
return true;
|
|
4763
5562
|
}
|
|
4764
|
-
return
|
|
5563
|
+
return true;
|
|
4765
5564
|
}
|
|
4766
5565
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4767
5566
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
4768
5567
|
if (waitingForVisiblePendingReview) {
|
|
4769
|
-
return
|
|
5568
|
+
return options.attempt + 1 < options.pollAttempts;
|
|
4770
5569
|
}
|
|
4771
5570
|
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
4772
5571
|
if (reviewNotVisibleYet) {
|
|
@@ -4825,6 +5624,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4825
5624
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4826
5625
|
}
|
|
4827
5626
|
}
|
|
5627
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
5628
|
+
return collectPrReviewEvidence({
|
|
5629
|
+
projectRoot: input.projectRoot,
|
|
5630
|
+
prUrl: input.prUrl,
|
|
5631
|
+
taskId: input.taskId,
|
|
5632
|
+
runId: "verifier",
|
|
5633
|
+
cycle: 0,
|
|
5634
|
+
apiSignals: input.apiSignals ?? [],
|
|
5635
|
+
command: async (args, options) => {
|
|
5636
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
5637
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5638
|
+
}
|
|
5639
|
+
});
|
|
5640
|
+
}
|
|
4828
5641
|
function deriveRepoName(projectRoot, prState) {
|
|
4829
5642
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4830
5643
|
if (fromUrl?.[1]) {
|
|
@@ -4839,8 +5652,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
4839
5652
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4840
5653
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
4841
5654
|
}
|
|
4842
|
-
function
|
|
4843
|
-
|
|
5655
|
+
function isGreptileGithubLogin2(login) {
|
|
5656
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
5657
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
4844
5658
|
}
|
|
4845
5659
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
4846
5660
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -4857,7 +5671,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
4857
5671
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
4858
5672
|
}
|
|
4859
5673
|
function sortGithubGreptileReviews(reviews) {
|
|
4860
|
-
return reviews.filter((review) =>
|
|
5674
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
4861
5675
|
}
|
|
4862
5676
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
4863
5677
|
const response = runGhJson(projectRoot, [
|
|
@@ -4930,32 +5744,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
4930
5744
|
}
|
|
4931
5745
|
return { pending: false, completed: false };
|
|
4932
5746
|
}
|
|
4933
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
4934
|
-
const greptileChecks = checks.filter((check) => {
|
|
4935
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
4936
|
-
return label.includes("greptile");
|
|
4937
|
-
});
|
|
4938
|
-
if (greptileChecks.length === 0) {
|
|
4939
|
-
return false;
|
|
4940
|
-
}
|
|
4941
|
-
for (const check of greptileChecks) {
|
|
4942
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
4943
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
4944
|
-
return false;
|
|
4945
|
-
}
|
|
4946
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
4947
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
4948
|
-
return false;
|
|
4949
|
-
}
|
|
4950
|
-
continue;
|
|
4951
|
-
}
|
|
4952
|
-
const state = (check.state || "").toUpperCase();
|
|
4953
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
4954
|
-
return false;
|
|
4955
|
-
}
|
|
4956
|
-
}
|
|
4957
|
-
return true;
|
|
4958
|
-
}
|
|
4959
5747
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
4960
5748
|
const [owner, name] = repoName.split("/");
|
|
4961
5749
|
if (!owner || !name) {
|
|
@@ -4981,7 +5769,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4981
5769
|
return [];
|
|
4982
5770
|
}
|
|
4983
5771
|
const comments = thread.comments?.nodes || [];
|
|
4984
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
5772
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
4985
5773
|
if (!latestGreptileComment?.path?.trim()) {
|
|
4986
5774
|
return [];
|
|
4987
5775
|
}
|
|
@@ -5003,11 +5791,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5003
5791
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5004
5792
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5005
5793
|
}
|
|
5006
|
-
function stripHtml(input) {
|
|
5007
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5008
|
-
|
|
5009
|
-
`).trim();
|
|
5010
|
-
}
|
|
5011
5794
|
function summarizeComment(input) {
|
|
5012
5795
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5013
5796
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5016,31 +5799,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5016
5799
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5017
5800
|
}
|
|
5018
5801
|
function isAiReviewApproved(input) {
|
|
5802
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
5803
|
+
return false;
|
|
5804
|
+
}
|
|
5019
5805
|
if (input.reviewMode !== "required") {
|
|
5020
5806
|
return true;
|
|
5021
5807
|
}
|
|
5022
5808
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5023
5809
|
}
|
|
5024
|
-
function parseGreptileScore(input) {
|
|
5025
|
-
const text = stripHtml(input);
|
|
5026
|
-
const patterns = [
|
|
5027
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5028
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5029
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5030
|
-
];
|
|
5031
|
-
for (const pattern of patterns) {
|
|
5032
|
-
const match = pattern.exec(text);
|
|
5033
|
-
if (!match) {
|
|
5034
|
-
continue;
|
|
5035
|
-
}
|
|
5036
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5037
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5038
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5039
|
-
return { value, scale };
|
|
5040
|
-
}
|
|
5041
|
-
}
|
|
5042
|
-
return null;
|
|
5043
|
-
}
|
|
5044
5810
|
|
|
5045
5811
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5046
5812
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -6353,8 +7119,9 @@ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
|
6353
7119
|
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
6354
7120
|
if (sourceIssueId) {
|
|
6355
7121
|
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
6356
|
-
if (match) {
|
|
6357
|
-
const
|
|
7122
|
+
if (match?.[1] && match[2]) {
|
|
7123
|
+
const sourceRepo = match[1];
|
|
7124
|
+
const issueNumber = match[2];
|
|
6358
7125
|
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
6359
7126
|
}
|
|
6360
7127
|
}
|