@h-rig/runtime 0.0.6-alpha.11 → 0.0.6-alpha.13
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 +914 -153
- package/dist/src/control-plane/hooks/completion-verification.js +1182 -281
- package/dist/src/control-plane/native/git-ops.js +31 -43
- package/dist/src/control-plane/native/harness-cli.js +914 -153
- package/dist/src/control-plane/native/pr-automation.js +1008 -38
- package/dist/src/control-plane/native/pr-review-gate.js +905 -0
- package/dist/src/control-plane/native/task-ops.js +909 -151
- package/dist/src/control-plane/native/verifier.js +911 -150
- 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
|
@@ -3605,6 +3605,777 @@ ${JSON.stringify(result, null, 2)}
|
|
|
3605
3605
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3606
3606
|
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3607
3607
|
import { resolve as resolve22 } from "path";
|
|
3608
|
+
|
|
3609
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3610
|
+
function parseJsonObject(value) {
|
|
3611
|
+
if (!value?.trim())
|
|
3612
|
+
return { value: {}, error: "empty JSON output" };
|
|
3613
|
+
try {
|
|
3614
|
+
const parsed = JSON.parse(value);
|
|
3615
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
function flattenPaginatedArray(value) {
|
|
3621
|
+
if (!Array.isArray(value))
|
|
3622
|
+
return null;
|
|
3623
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3624
|
+
return value.flatMap((entry) => entry);
|
|
3625
|
+
}
|
|
3626
|
+
return value;
|
|
3627
|
+
}
|
|
3628
|
+
function parseJsonArray(value) {
|
|
3629
|
+
if (!value?.trim())
|
|
3630
|
+
return { value: [], error: "empty JSON output" };
|
|
3631
|
+
try {
|
|
3632
|
+
const parsed = JSON.parse(value);
|
|
3633
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3634
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3635
|
+
} catch (error) {
|
|
3636
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
function parseGithubPrUrl(prUrl) {
|
|
3640
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3641
|
+
if (!match)
|
|
3642
|
+
return null;
|
|
3643
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3644
|
+
if (!Number.isFinite(prNumber))
|
|
3645
|
+
return null;
|
|
3646
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3647
|
+
}
|
|
3648
|
+
function checkName(check) {
|
|
3649
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3650
|
+
}
|
|
3651
|
+
function checkState(check) {
|
|
3652
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3653
|
+
}
|
|
3654
|
+
function isGreptileLabel(value) {
|
|
3655
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3656
|
+
}
|
|
3657
|
+
function isGreptileGithubLogin(value) {
|
|
3658
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3659
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3660
|
+
}
|
|
3661
|
+
function isPassingCheck(check) {
|
|
3662
|
+
const state = checkState(check);
|
|
3663
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3664
|
+
}
|
|
3665
|
+
function isPendingCheck(check) {
|
|
3666
|
+
const state = checkState(check);
|
|
3667
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3668
|
+
}
|
|
3669
|
+
function isFailingCheck(check) {
|
|
3670
|
+
const state = checkState(check);
|
|
3671
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3672
|
+
}
|
|
3673
|
+
function wildcardToRegExp(pattern) {
|
|
3674
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3675
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3676
|
+
}
|
|
3677
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3678
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3679
|
+
}
|
|
3680
|
+
function greptileScorePatterns() {
|
|
3681
|
+
return [
|
|
3682
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3683
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3684
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3685
|
+
];
|
|
3686
|
+
}
|
|
3687
|
+
function parseGreptileScores(input) {
|
|
3688
|
+
const text = stripHtml(input);
|
|
3689
|
+
const seen = new Set;
|
|
3690
|
+
const scores = [];
|
|
3691
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3692
|
+
for (const match of text.matchAll(pattern)) {
|
|
3693
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3694
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3695
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3696
|
+
continue;
|
|
3697
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3698
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3699
|
+
if (seen.has(key))
|
|
3700
|
+
continue;
|
|
3701
|
+
seen.add(key);
|
|
3702
|
+
scores.push({ value, scale, raw });
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
return scores;
|
|
3706
|
+
}
|
|
3707
|
+
function parseGreptileScore(input) {
|
|
3708
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3709
|
+
}
|
|
3710
|
+
function stripHtml(input) {
|
|
3711
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3712
|
+
|
|
3713
|
+
`).trim();
|
|
3714
|
+
}
|
|
3715
|
+
function containsBlockerText(input) {
|
|
3716
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3717
|
+
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);
|
|
3718
|
+
}
|
|
3719
|
+
function isStrictFiveOfFive(score) {
|
|
3720
|
+
return score.value === 5 && score.scale === 5;
|
|
3721
|
+
}
|
|
3722
|
+
function containsConflictingScoreText(input) {
|
|
3723
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3724
|
+
}
|
|
3725
|
+
function firstString(record, keys) {
|
|
3726
|
+
for (const key of keys) {
|
|
3727
|
+
const value = record[key];
|
|
3728
|
+
if (typeof value === "string")
|
|
3729
|
+
return value;
|
|
3730
|
+
}
|
|
3731
|
+
return "";
|
|
3732
|
+
}
|
|
3733
|
+
function arrayField(record, key) {
|
|
3734
|
+
const value = record[key];
|
|
3735
|
+
return Array.isArray(value) ? value : [];
|
|
3736
|
+
}
|
|
3737
|
+
async function runJsonArray(command, args, cwd) {
|
|
3738
|
+
const result = await command(args, { cwd });
|
|
3739
|
+
const label = `gh ${args.join(" ")}`;
|
|
3740
|
+
if (result.exitCode !== 0) {
|
|
3741
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3742
|
+
}
|
|
3743
|
+
const parsed = parseJsonArray(result.stdout);
|
|
3744
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3745
|
+
}
|
|
3746
|
+
async function runJsonObject(command, args, cwd) {
|
|
3747
|
+
const result = await command(args, { cwd });
|
|
3748
|
+
const label = `gh ${args.join(" ")}`;
|
|
3749
|
+
if (result.exitCode !== 0) {
|
|
3750
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3751
|
+
}
|
|
3752
|
+
const parsed = parseJsonObject(result.stdout);
|
|
3753
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3754
|
+
}
|
|
3755
|
+
function normalizeStatusCheck(entry) {
|
|
3756
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3757
|
+
return null;
|
|
3758
|
+
const record = entry;
|
|
3759
|
+
const name = firstString(record, ["name", "context"]);
|
|
3760
|
+
if (!name.trim())
|
|
3761
|
+
return null;
|
|
3762
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
3763
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
3764
|
+
return {
|
|
3765
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
3766
|
+
name,
|
|
3767
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
3768
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3769
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3770
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
3771
|
+
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,
|
|
3772
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
3773
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
3774
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
3775
|
+
output: output ? {
|
|
3776
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
3777
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
3778
|
+
text: typeof output.text === "string" ? output.text : null
|
|
3779
|
+
} : null,
|
|
3780
|
+
app: app ? {
|
|
3781
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
3782
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
3783
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
3784
|
+
} : null
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
function normalizeReview(entry) {
|
|
3788
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3789
|
+
return null;
|
|
3790
|
+
const record = entry;
|
|
3791
|
+
return {
|
|
3792
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
3793
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3794
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3795
|
+
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,
|
|
3796
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
3797
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
function normalizeReviewComment(entry) {
|
|
3801
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3802
|
+
return null;
|
|
3803
|
+
const record = entry;
|
|
3804
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3805
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
3806
|
+
if (!body && !path)
|
|
3807
|
+
return null;
|
|
3808
|
+
return {
|
|
3809
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3810
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3811
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3812
|
+
body,
|
|
3813
|
+
path,
|
|
3814
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3815
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3816
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
3817
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
function normalizeIssueComment(entry) {
|
|
3821
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3822
|
+
return null;
|
|
3823
|
+
const record = entry;
|
|
3824
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3825
|
+
if (!body)
|
|
3826
|
+
return null;
|
|
3827
|
+
return {
|
|
3828
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3829
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3830
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3831
|
+
body,
|
|
3832
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3833
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3834
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
function normalizeReviewThread(entry) {
|
|
3838
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3839
|
+
return null;
|
|
3840
|
+
const record = entry;
|
|
3841
|
+
return {
|
|
3842
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
3843
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
3844
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
3845
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
function relevantIssueComment(comment) {
|
|
3849
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
3850
|
+
const body = comment.body ?? "";
|
|
3851
|
+
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);
|
|
3852
|
+
}
|
|
3853
|
+
function latestThreadComment(thread) {
|
|
3854
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
3855
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
3856
|
+
}
|
|
3857
|
+
function unresolvedThreadSummaries(threads) {
|
|
3858
|
+
return threads.flatMap((thread) => {
|
|
3859
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
3860
|
+
return [];
|
|
3861
|
+
const latest = latestThreadComment(thread);
|
|
3862
|
+
if (!latest)
|
|
3863
|
+
return ["Unresolved review thread"];
|
|
3864
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
3865
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
function collectBodies(evidence) {
|
|
3869
|
+
return [
|
|
3870
|
+
evidence.title ?? "",
|
|
3871
|
+
evidence.body,
|
|
3872
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
3873
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
3874
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
3875
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
3876
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
3877
|
+
].filter((body) => body.trim().length > 0);
|
|
3878
|
+
}
|
|
3879
|
+
function bodyExcerpt(body) {
|
|
3880
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
3881
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
3882
|
+
}
|
|
3883
|
+
function makeGreptileSignal(input) {
|
|
3884
|
+
const scores = parseGreptileScores(input.body);
|
|
3885
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
3886
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
3887
|
+
const blocker = input.blocker ?? containsBlockerText(input.body);
|
|
3888
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
3889
|
+
return {
|
|
3890
|
+
source: input.source,
|
|
3891
|
+
trusted: input.trusted,
|
|
3892
|
+
authorLogin: input.authorLogin ?? null,
|
|
3893
|
+
reviewedSha,
|
|
3894
|
+
current,
|
|
3895
|
+
stale: current === false,
|
|
3896
|
+
score: scores[0] ?? null,
|
|
3897
|
+
scores,
|
|
3898
|
+
explicitApproval,
|
|
3899
|
+
blocker,
|
|
3900
|
+
actionable: input.actionable ?? blocker,
|
|
3901
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
3902
|
+
body: input.body,
|
|
3903
|
+
allScores: scores
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
function reviewAuthorLogin(review) {
|
|
3907
|
+
return review.author?.login ?? null;
|
|
3908
|
+
}
|
|
3909
|
+
function commentAuthorLogin(comment) {
|
|
3910
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
3911
|
+
}
|
|
3912
|
+
function collectGreptileSignals(evidence) {
|
|
3913
|
+
const signals = [];
|
|
3914
|
+
const contextSources = [
|
|
3915
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
3916
|
+
{ source: "pr-body", body: evidence.body }
|
|
3917
|
+
];
|
|
3918
|
+
for (const context of contextSources) {
|
|
3919
|
+
if (!context.body.trim())
|
|
3920
|
+
continue;
|
|
3921
|
+
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
3922
|
+
continue;
|
|
3923
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
3924
|
+
signals.push(makeGreptileSignal({
|
|
3925
|
+
source: context.source,
|
|
3926
|
+
body: context.body,
|
|
3927
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3928
|
+
trusted: false,
|
|
3929
|
+
blocker: contextBlocker,
|
|
3930
|
+
actionable: contextBlocker
|
|
3931
|
+
}));
|
|
3932
|
+
}
|
|
3933
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
3934
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
3935
|
+
|
|
3936
|
+
`);
|
|
3937
|
+
if (!body.trim())
|
|
3938
|
+
continue;
|
|
3939
|
+
signals.push(makeGreptileSignal({
|
|
3940
|
+
source: "api",
|
|
3941
|
+
body,
|
|
3942
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3943
|
+
trusted: true,
|
|
3944
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
3945
|
+
explicitApproval: false
|
|
3946
|
+
}));
|
|
3947
|
+
}
|
|
3948
|
+
for (const review of evidence.reviews) {
|
|
3949
|
+
const login = reviewAuthorLogin(review);
|
|
3950
|
+
if (!isGreptileGithubLogin(login))
|
|
3951
|
+
continue;
|
|
3952
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
3953
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
3954
|
+
|
|
3955
|
+
`);
|
|
3956
|
+
if (!body.trim())
|
|
3957
|
+
continue;
|
|
3958
|
+
const dismissed = state === "DISMISSED";
|
|
3959
|
+
signals.push(makeGreptileSignal({
|
|
3960
|
+
source: "github-review",
|
|
3961
|
+
body,
|
|
3962
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3963
|
+
trusted: !dismissed,
|
|
3964
|
+
authorLogin: login,
|
|
3965
|
+
reviewedSha: review.commit_id ?? null,
|
|
3966
|
+
explicitApproval: undefined,
|
|
3967
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
3968
|
+
}));
|
|
3969
|
+
}
|
|
3970
|
+
for (const comment of evidence.changedFileReviewComments) {
|
|
3971
|
+
const login = commentAuthorLogin(comment);
|
|
3972
|
+
const body = comment.body ?? "";
|
|
3973
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
3974
|
+
continue;
|
|
3975
|
+
signals.push(makeGreptileSignal({
|
|
3976
|
+
source: "changed-file-comment",
|
|
3977
|
+
body,
|
|
3978
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3979
|
+
trusted: true,
|
|
3980
|
+
authorLogin: login,
|
|
3981
|
+
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
3982
|
+
}));
|
|
3983
|
+
}
|
|
3984
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
3985
|
+
const login = commentAuthorLogin(comment);
|
|
3986
|
+
const body = comment.body ?? "";
|
|
3987
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
3988
|
+
continue;
|
|
3989
|
+
signals.push(makeGreptileSignal({
|
|
3990
|
+
source: "issue-comment",
|
|
3991
|
+
body,
|
|
3992
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
3993
|
+
trusted: true,
|
|
3994
|
+
authorLogin: login
|
|
3995
|
+
}));
|
|
3996
|
+
}
|
|
3997
|
+
for (const thread of evidence.reviewThreads) {
|
|
3998
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
3999
|
+
continue;
|
|
4000
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4001
|
+
const login = comment.author?.login ?? null;
|
|
4002
|
+
const body = comment.body ?? "";
|
|
4003
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4004
|
+
continue;
|
|
4005
|
+
signals.push(makeGreptileSignal({
|
|
4006
|
+
source: "review-thread",
|
|
4007
|
+
body,
|
|
4008
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4009
|
+
trusted: true,
|
|
4010
|
+
authorLogin: login
|
|
4011
|
+
}));
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
for (const check of evidence.checks) {
|
|
4015
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4016
|
+
continue;
|
|
4017
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4018
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4019
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4020
|
+
|
|
4021
|
+
`);
|
|
4022
|
+
signals.push(makeGreptileSignal({
|
|
4023
|
+
source: "github-check",
|
|
4024
|
+
body,
|
|
4025
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4026
|
+
trusted: false,
|
|
4027
|
+
reviewedSha,
|
|
4028
|
+
explicitApproval: false,
|
|
4029
|
+
blocker: isFailingCheck(check),
|
|
4030
|
+
actionable: isFailingCheck(check)
|
|
4031
|
+
}));
|
|
4032
|
+
}
|
|
4033
|
+
return signals;
|
|
4034
|
+
}
|
|
4035
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4036
|
+
return threads.flatMap((thread) => {
|
|
4037
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4038
|
+
return [];
|
|
4039
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4040
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4041
|
+
return [];
|
|
4042
|
+
const latest = latestThreadComment(thread);
|
|
4043
|
+
if (!latest)
|
|
4044
|
+
return ["Unresolved Greptile review thread"];
|
|
4045
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4046
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4050
|
+
return [];
|
|
4051
|
+
}
|
|
4052
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4053
|
+
return comments.flatMap((comment) => {
|
|
4054
|
+
const body = comment.body?.trim() ?? "";
|
|
4055
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4056
|
+
return [];
|
|
4057
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4058
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4059
|
+
return [`${author}: ${body}`];
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4063
|
+
return reviews.flatMap((review) => {
|
|
4064
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4065
|
+
if (isGreptileGithubLogin(login))
|
|
4066
|
+
return [];
|
|
4067
|
+
const body = review.body?.trim() ?? "";
|
|
4068
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4069
|
+
return [];
|
|
4070
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4071
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
function signalLabel(signal) {
|
|
4075
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4076
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4077
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4078
|
+
return `${source}${author}${sha}`;
|
|
4079
|
+
}
|
|
4080
|
+
function deriveGreptileEvidence(input) {
|
|
4081
|
+
const rawBodies = collectBodies(input);
|
|
4082
|
+
const signals = collectGreptileSignals(input);
|
|
4083
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4084
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4085
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4086
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4087
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4088
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4089
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4090
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4091
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4092
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4093
|
+
const approvedByExplicitMapping = false;
|
|
4094
|
+
const approvingSignal = approvingScoreEntry?.signal ?? null;
|
|
4095
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4096
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4097
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4098
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4099
|
+
const staleBlockingSignals = [];
|
|
4100
|
+
const blockers = [
|
|
4101
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4102
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4103
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4104
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4105
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4106
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4107
|
+
];
|
|
4108
|
+
const unresolvedComments = [
|
|
4109
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4110
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4111
|
+
];
|
|
4112
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4113
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4114
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4115
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4116
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4117
|
+
});
|
|
4118
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4119
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4120
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4121
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4122
|
+
});
|
|
4123
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4124
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4125
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4126
|
+
const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
|
|
4127
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4128
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4129
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4130
|
+
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";
|
|
4131
|
+
return {
|
|
4132
|
+
source,
|
|
4133
|
+
currentHeadSha: input.currentHeadSha,
|
|
4134
|
+
reviewedSha,
|
|
4135
|
+
fresh,
|
|
4136
|
+
completed,
|
|
4137
|
+
approved,
|
|
4138
|
+
score,
|
|
4139
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4140
|
+
blockers,
|
|
4141
|
+
unresolvedComments,
|
|
4142
|
+
rawBodies,
|
|
4143
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4144
|
+
mapping
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
function isGreptileCheckDetail(check) {
|
|
4148
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4149
|
+
}
|
|
4150
|
+
async function collectGreptileCheckDetails(input) {
|
|
4151
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4152
|
+
"api",
|
|
4153
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4154
|
+
"--paginate",
|
|
4155
|
+
"--slurp",
|
|
4156
|
+
"--jq",
|
|
4157
|
+
"map(.check_runs // []) | add // []"
|
|
4158
|
+
], input.projectRoot);
|
|
4159
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4160
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4161
|
+
}
|
|
4162
|
+
async function collectReviewThreads(input) {
|
|
4163
|
+
const reviewThreads = [];
|
|
4164
|
+
let afterCursor = null;
|
|
4165
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4166
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4167
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4168
|
+
"api",
|
|
4169
|
+
"graphql",
|
|
4170
|
+
"-F",
|
|
4171
|
+
`owner=${input.owner}`,
|
|
4172
|
+
"-F",
|
|
4173
|
+
`name=${input.name}`,
|
|
4174
|
+
"-F",
|
|
4175
|
+
`prNumber=${input.prNumber}`,
|
|
4176
|
+
"-f",
|
|
4177
|
+
`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 } } } } }`
|
|
4178
|
+
], input.projectRoot);
|
|
4179
|
+
if (threadsResponse.error) {
|
|
4180
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4181
|
+
}
|
|
4182
|
+
const data = threadsResponse.value.data;
|
|
4183
|
+
const repository = data?.repository;
|
|
4184
|
+
const pullRequest = repository?.pullRequest;
|
|
4185
|
+
const threads = pullRequest?.reviewThreads;
|
|
4186
|
+
const nodes = threads?.nodes;
|
|
4187
|
+
if (!Array.isArray(nodes)) {
|
|
4188
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4189
|
+
}
|
|
4190
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4191
|
+
reviewThreads.push(...normalized);
|
|
4192
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4193
|
+
if (truncatedCommentThread) {
|
|
4194
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4195
|
+
}
|
|
4196
|
+
const pageInfo = threads?.pageInfo;
|
|
4197
|
+
if (!pageInfo) {
|
|
4198
|
+
if (nodes.length >= 100) {
|
|
4199
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4200
|
+
}
|
|
4201
|
+
return { value: reviewThreads };
|
|
4202
|
+
}
|
|
4203
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4204
|
+
return { value: reviewThreads };
|
|
4205
|
+
}
|
|
4206
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4207
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4208
|
+
}
|
|
4209
|
+
afterCursor = pageInfo.endCursor;
|
|
4210
|
+
}
|
|
4211
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4212
|
+
}
|
|
4213
|
+
async function collectPrReviewEvidence(input) {
|
|
4214
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4215
|
+
if (!parsed) {
|
|
4216
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4217
|
+
}
|
|
4218
|
+
const readErrors = [];
|
|
4219
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4220
|
+
"pr",
|
|
4221
|
+
"view",
|
|
4222
|
+
input.prUrl,
|
|
4223
|
+
"--json",
|
|
4224
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4225
|
+
], input.projectRoot);
|
|
4226
|
+
if (viewRead.error)
|
|
4227
|
+
readErrors.push(viewRead.error);
|
|
4228
|
+
const view = viewRead.value;
|
|
4229
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4230
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4231
|
+
}
|
|
4232
|
+
if (!Array.isArray(view.reviews)) {
|
|
4233
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4234
|
+
}
|
|
4235
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4236
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4237
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4238
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4239
|
+
if (reviewCommentsRead.error)
|
|
4240
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4241
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4242
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4243
|
+
if (issueCommentsRead.error)
|
|
4244
|
+
readErrors.push(issueCommentsRead.error);
|
|
4245
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4246
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4247
|
+
command: input.command,
|
|
4248
|
+
projectRoot: input.projectRoot,
|
|
4249
|
+
owner: parsed.owner,
|
|
4250
|
+
name: parsed.repo,
|
|
4251
|
+
prNumber: parsed.prNumber
|
|
4252
|
+
});
|
|
4253
|
+
if (reviewThreadsRead.error)
|
|
4254
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4255
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4256
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4257
|
+
let greptileCheckDetails = [];
|
|
4258
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4259
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4260
|
+
command: input.command,
|
|
4261
|
+
projectRoot: input.projectRoot,
|
|
4262
|
+
repoName: parsed.repoName,
|
|
4263
|
+
headSha
|
|
4264
|
+
});
|
|
4265
|
+
if (checkDetailsRead.error)
|
|
4266
|
+
readErrors.push(checkDetailsRead.error);
|
|
4267
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4268
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4269
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4273
|
+
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})` : ""}`);
|
|
4274
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
4275
|
+
const evidenceBase = {
|
|
4276
|
+
title: firstString(view, ["title"]),
|
|
4277
|
+
body: firstString(view, ["body"]),
|
|
4278
|
+
reviews,
|
|
4279
|
+
changedFileReviewComments: reviewComments,
|
|
4280
|
+
relevantIssueComments: issueComments,
|
|
4281
|
+
reviewThreads,
|
|
4282
|
+
checks: checksWithGreptileDetails,
|
|
4283
|
+
currentHeadSha: headSha,
|
|
4284
|
+
apiSignals: input.apiSignals ?? []
|
|
4285
|
+
};
|
|
4286
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4287
|
+
return {
|
|
4288
|
+
prUrl: input.prUrl,
|
|
4289
|
+
prNumber: parsed.prNumber,
|
|
4290
|
+
repoName: parsed.repoName,
|
|
4291
|
+
title: evidenceBase.title,
|
|
4292
|
+
body: evidenceBase.body,
|
|
4293
|
+
headSha,
|
|
4294
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4295
|
+
baseRefName: firstString(view, ["baseRefName"]),
|
|
4296
|
+
state: firstString(view, ["state"]),
|
|
4297
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4298
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4299
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4300
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4301
|
+
reviews,
|
|
4302
|
+
reviewThreads,
|
|
4303
|
+
changedFileReviewComments: reviewComments,
|
|
4304
|
+
relevantIssueComments: issueComments,
|
|
4305
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4306
|
+
checkFailures,
|
|
4307
|
+
pendingChecks,
|
|
4308
|
+
readErrors,
|
|
4309
|
+
greptile
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
function evaluateEvidence(evidence) {
|
|
4313
|
+
const reasons = [];
|
|
4314
|
+
const warnings = [];
|
|
4315
|
+
let pending = false;
|
|
4316
|
+
if (evidence.readErrors.length > 0) {
|
|
4317
|
+
reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
|
|
4318
|
+
}
|
|
4319
|
+
if (!evidence.headSha)
|
|
4320
|
+
reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
|
|
4321
|
+
if (evidence.checkFailures.length > 0)
|
|
4322
|
+
reasons.push(...evidence.checkFailures);
|
|
4323
|
+
if (evidence.pendingChecks.length > 0) {
|
|
4324
|
+
pending = true;
|
|
4325
|
+
reasons.push(...evidence.pendingChecks);
|
|
4326
|
+
}
|
|
4327
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4328
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4329
|
+
reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
|
|
4330
|
+
}
|
|
4331
|
+
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4332
|
+
if (unresolvedThreads.length > 0)
|
|
4333
|
+
reasons.push(...unresolvedThreads);
|
|
4334
|
+
const greptile = evidence.greptile;
|
|
4335
|
+
if (greptile.mapping === "missing")
|
|
4336
|
+
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4337
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4338
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4339
|
+
reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
|
|
4340
|
+
}
|
|
4341
|
+
if (!greptile.completed) {
|
|
4342
|
+
pending = true;
|
|
4343
|
+
reasons.push("Greptile check/review has not completed for the current PR head.");
|
|
4344
|
+
}
|
|
4345
|
+
if (!greptile.fresh)
|
|
4346
|
+
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4347
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4348
|
+
reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
|
|
4349
|
+
}
|
|
4350
|
+
if (!greptile.score && greptile.mapping !== "score-5-of-5") {
|
|
4351
|
+
reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
|
|
4352
|
+
}
|
|
4353
|
+
if (greptile.mapping === "unproven") {
|
|
4354
|
+
reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
|
|
4355
|
+
}
|
|
4356
|
+
if (greptile.blockers.length > 0) {
|
|
4357
|
+
reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
|
|
4358
|
+
}
|
|
4359
|
+
if (greptile.unresolvedComments.length > 0)
|
|
4360
|
+
reasons.push(...greptile.unresolvedComments);
|
|
4361
|
+
if (!greptile.approved)
|
|
4362
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4363
|
+
return { reasons: Array.from(new Set(reasons)), warnings, pending };
|
|
4364
|
+
}
|
|
4365
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4366
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4367
|
+
const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
|
|
4368
|
+
return {
|
|
4369
|
+
approved,
|
|
4370
|
+
pending: evaluated.pending,
|
|
4371
|
+
reasons: evaluated.reasons,
|
|
4372
|
+
warnings: evaluated.warnings,
|
|
4373
|
+
actionableFeedback: evaluated.reasons,
|
|
4374
|
+
evidence
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3608
4379
|
async function verifyTask(options) {
|
|
3609
4380
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
3610
4381
|
const taskId = options.taskId;
|
|
@@ -4400,7 +5171,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4400
5171
|
}
|
|
4401
5172
|
};
|
|
4402
5173
|
}
|
|
4403
|
-
|
|
5174
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5175
|
+
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)) {
|
|
4404
5176
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4405
5177
|
return {
|
|
4406
5178
|
verdict: "REJECT",
|
|
@@ -4416,44 +5188,78 @@ async function runGreptileReviewForPr(options) {
|
|
|
4416
5188
|
}
|
|
4417
5189
|
};
|
|
4418
5190
|
}
|
|
4419
|
-
if (score) {
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
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
|
-
|
|
5191
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5192
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5193
|
+
return {
|
|
5194
|
+
verdict: "REJECT",
|
|
5195
|
+
feedback,
|
|
5196
|
+
reasons,
|
|
5197
|
+
warnings,
|
|
5198
|
+
rawPayload: {
|
|
5199
|
+
pr: options.prState,
|
|
5200
|
+
codeReviews: reviewsPayload,
|
|
5201
|
+
selectedReview,
|
|
5202
|
+
reviewDetails,
|
|
5203
|
+
comments: commentsPayload,
|
|
5204
|
+
score
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
}
|
|
5208
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5209
|
+
let strictGate = null;
|
|
5210
|
+
try {
|
|
5211
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5212
|
+
projectRoot: options.projectRoot,
|
|
5213
|
+
taskId: options.taskId,
|
|
5214
|
+
prUrl,
|
|
5215
|
+
apiSignals: [{
|
|
5216
|
+
id: selectedReview.id,
|
|
5217
|
+
body: reviewBody,
|
|
5218
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5219
|
+
status: selectedReview.status
|
|
5220
|
+
}]
|
|
5221
|
+
});
|
|
5222
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5223
|
+
} catch (error) {
|
|
5224
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5225
|
+
return {
|
|
5226
|
+
verdict: "REJECT",
|
|
5227
|
+
feedback,
|
|
5228
|
+
reasons,
|
|
5229
|
+
warnings,
|
|
5230
|
+
rawPayload: {
|
|
5231
|
+
pr: options.prState,
|
|
5232
|
+
codeReviews: reviewsPayload,
|
|
5233
|
+
selectedReview,
|
|
5234
|
+
reviewDetails,
|
|
5235
|
+
comments: commentsPayload,
|
|
5236
|
+
score
|
|
5237
|
+
}
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
5240
|
+
if (!strictGate.approved) {
|
|
5241
|
+
return {
|
|
5242
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5243
|
+
feedback,
|
|
5244
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5245
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5246
|
+
rawPayload: {
|
|
5247
|
+
pr: options.prState,
|
|
5248
|
+
codeReviews: reviewsPayload,
|
|
5249
|
+
selectedReview,
|
|
5250
|
+
reviewDetails,
|
|
5251
|
+
comments: commentsPayload,
|
|
5252
|
+
score,
|
|
5253
|
+
strictGate: {
|
|
5254
|
+
approved: strictGate.approved,
|
|
5255
|
+
pending: strictGate.pending,
|
|
5256
|
+
reasons: strictGate.reasons,
|
|
5257
|
+
warnings: strictGate.warnings,
|
|
5258
|
+
greptile: strictGate.evidence.greptile,
|
|
5259
|
+
readErrors: strictGate.evidence.readErrors
|
|
4451
5260
|
}
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4455
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4456
|
-
}
|
|
5261
|
+
}
|
|
5262
|
+
};
|
|
4457
5263
|
}
|
|
4458
5264
|
return {
|
|
4459
5265
|
verdict: "APPROVE",
|
|
@@ -4465,7 +5271,15 @@ async function runGreptileReviewForPr(options) {
|
|
|
4465
5271
|
codeReviews: reviewsPayload,
|
|
4466
5272
|
selectedReview,
|
|
4467
5273
|
reviewDetails,
|
|
4468
|
-
comments: commentsPayload
|
|
5274
|
+
comments: commentsPayload,
|
|
5275
|
+
strictGate: {
|
|
5276
|
+
approved: strictGate.approved,
|
|
5277
|
+
pending: strictGate.pending,
|
|
5278
|
+
reasons: strictGate.reasons,
|
|
5279
|
+
warnings: strictGate.warnings,
|
|
5280
|
+
greptile: strictGate.evidence.greptile,
|
|
5281
|
+
readErrors: strictGate.evidence.readErrors
|
|
5282
|
+
}
|
|
4469
5283
|
}
|
|
4470
5284
|
};
|
|
4471
5285
|
}
|
|
@@ -4489,7 +5303,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4489
5303
|
let threads = [];
|
|
4490
5304
|
let actionableThreads = [];
|
|
4491
5305
|
let checkRollup = [];
|
|
4492
|
-
let
|
|
5306
|
+
let checkState2 = { pending: false, completed: false };
|
|
4493
5307
|
for (let attempt = 0;; attempt += 1) {
|
|
4494
5308
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4495
5309
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4498,15 +5312,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4498
5312
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4499
5313
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4500
5314
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4501
|
-
|
|
4502
|
-
const
|
|
5315
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5316
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4503
5317
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4504
5318
|
attempt,
|
|
4505
5319
|
pollAttempts: options.pollAttempts,
|
|
4506
|
-
checkState,
|
|
5320
|
+
checkState: checkState2,
|
|
4507
5321
|
fallbackReview,
|
|
4508
5322
|
selectedReview,
|
|
4509
|
-
approvedViaReviewedAncestor
|
|
5323
|
+
approvedViaReviewedAncestor
|
|
4510
5324
|
})) {
|
|
4511
5325
|
break;
|
|
4512
5326
|
}
|
|
@@ -4534,7 +5348,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4534
5348
|
].filter(Boolean).join(`
|
|
4535
5349
|
`);
|
|
4536
5350
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4537
|
-
if (
|
|
5351
|
+
if (checkState2.pending) {
|
|
4538
5352
|
return {
|
|
4539
5353
|
verdict: "SKIP",
|
|
4540
5354
|
feedback,
|
|
@@ -4545,34 +5359,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4545
5359
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4546
5360
|
};
|
|
4547
5361
|
}
|
|
4548
|
-
const
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
};
|
|
4559
|
-
}
|
|
4560
|
-
return {
|
|
4561
|
-
verdict: "SKIP",
|
|
4562
|
-
feedback,
|
|
4563
|
-
reasons: [
|
|
4564
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4565
|
-
],
|
|
4566
|
-
warnings,
|
|
4567
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4568
|
-
};
|
|
4569
|
-
}
|
|
4570
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4571
|
-
if (actionableThreads.length > 0) {
|
|
5362
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5363
|
+
let strictGate;
|
|
5364
|
+
try {
|
|
5365
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5366
|
+
projectRoot: options.projectRoot,
|
|
5367
|
+
taskId: options.taskId,
|
|
5368
|
+
prUrl
|
|
5369
|
+
});
|
|
5370
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5371
|
+
} catch (error) {
|
|
4572
5372
|
return {
|
|
4573
5373
|
verdict: "REJECT",
|
|
4574
5374
|
feedback,
|
|
4575
|
-
reasons:
|
|
5375
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4576
5376
|
warnings,
|
|
4577
5377
|
rawPayload: {
|
|
4578
5378
|
pr: options.prState,
|
|
@@ -4585,44 +5385,30 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4585
5385
|
}
|
|
4586
5386
|
};
|
|
4587
5387
|
}
|
|
4588
|
-
if (!
|
|
4589
|
-
if (approvedViaCompletedCheck) {
|
|
4590
|
-
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.`);
|
|
4591
|
-
return {
|
|
4592
|
-
verdict: "APPROVE",
|
|
4593
|
-
feedback,
|
|
4594
|
-
reasons: [],
|
|
4595
|
-
warnings,
|
|
4596
|
-
rawPayload: {
|
|
4597
|
-
pr: options.prState,
|
|
4598
|
-
selectedReview: fallbackReview,
|
|
4599
|
-
reviews,
|
|
4600
|
-
threads,
|
|
4601
|
-
checkRollup,
|
|
4602
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4603
|
-
}
|
|
4604
|
-
};
|
|
4605
|
-
}
|
|
5388
|
+
if (!strictGate.approved) {
|
|
4606
5389
|
return {
|
|
4607
|
-
verdict: "SKIP",
|
|
5390
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4608
5391
|
feedback,
|
|
4609
|
-
reasons: [
|
|
4610
|
-
|
|
4611
|
-
],
|
|
4612
|
-
warnings,
|
|
5392
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5393
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4613
5394
|
rawPayload: {
|
|
4614
5395
|
pr: options.prState,
|
|
4615
5396
|
selectedReview: fallbackReview,
|
|
4616
5397
|
reviews,
|
|
4617
5398
|
threads,
|
|
4618
5399
|
checkRollup,
|
|
5400
|
+
actionableThreads,
|
|
5401
|
+
strictGate: {
|
|
5402
|
+
approved: strictGate.approved,
|
|
5403
|
+
pending: strictGate.pending,
|
|
5404
|
+
reasons: strictGate.reasons,
|
|
5405
|
+
warnings: strictGate.warnings,
|
|
5406
|
+
greptile: strictGate.evidence.greptile
|
|
5407
|
+
},
|
|
4619
5408
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4620
5409
|
}
|
|
4621
5410
|
};
|
|
4622
5411
|
}
|
|
4623
|
-
if (approvedViaReviewedAncestor) {
|
|
4624
|
-
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.`);
|
|
4625
|
-
}
|
|
4626
5412
|
return {
|
|
4627
5413
|
verdict: "APPROVE",
|
|
4628
5414
|
feedback,
|
|
@@ -4634,6 +5420,13 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4634
5420
|
reviews,
|
|
4635
5421
|
threads,
|
|
4636
5422
|
checkRollup,
|
|
5423
|
+
strictGate: {
|
|
5424
|
+
approved: strictGate.approved,
|
|
5425
|
+
pending: strictGate.pending,
|
|
5426
|
+
reasons: strictGate.reasons,
|
|
5427
|
+
warnings: strictGate.warnings,
|
|
5428
|
+
greptile: strictGate.evidence.greptile
|
|
5429
|
+
},
|
|
4637
5430
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4638
5431
|
}
|
|
4639
5432
|
};
|
|
@@ -4819,6 +5612,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4819
5612
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4820
5613
|
}
|
|
4821
5614
|
}
|
|
5615
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
5616
|
+
return collectPrReviewEvidence({
|
|
5617
|
+
projectRoot: input.projectRoot,
|
|
5618
|
+
prUrl: input.prUrl,
|
|
5619
|
+
taskId: input.taskId,
|
|
5620
|
+
runId: "verifier",
|
|
5621
|
+
cycle: 0,
|
|
5622
|
+
apiSignals: input.apiSignals ?? [],
|
|
5623
|
+
command: async (args, options) => {
|
|
5624
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
5625
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5626
|
+
}
|
|
5627
|
+
});
|
|
5628
|
+
}
|
|
4822
5629
|
function deriveRepoName(projectRoot, prState) {
|
|
4823
5630
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4824
5631
|
if (fromUrl?.[1]) {
|
|
@@ -4833,8 +5640,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
4833
5640
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4834
5641
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
4835
5642
|
}
|
|
4836
|
-
function
|
|
4837
|
-
|
|
5643
|
+
function isGreptileGithubLogin2(login) {
|
|
5644
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
5645
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
4838
5646
|
}
|
|
4839
5647
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
4840
5648
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -4851,7 +5659,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
4851
5659
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
4852
5660
|
}
|
|
4853
5661
|
function sortGithubGreptileReviews(reviews) {
|
|
4854
|
-
return reviews.filter((review) =>
|
|
5662
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
4855
5663
|
}
|
|
4856
5664
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
4857
5665
|
const response = runGhJson(projectRoot, [
|
|
@@ -4924,32 +5732,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
4924
5732
|
}
|
|
4925
5733
|
return { pending: false, completed: false };
|
|
4926
5734
|
}
|
|
4927
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
4928
|
-
const greptileChecks = checks.filter((check) => {
|
|
4929
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
4930
|
-
return label.includes("greptile");
|
|
4931
|
-
});
|
|
4932
|
-
if (greptileChecks.length === 0) {
|
|
4933
|
-
return false;
|
|
4934
|
-
}
|
|
4935
|
-
for (const check of greptileChecks) {
|
|
4936
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
4937
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
4938
|
-
return false;
|
|
4939
|
-
}
|
|
4940
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
4941
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
4942
|
-
return false;
|
|
4943
|
-
}
|
|
4944
|
-
continue;
|
|
4945
|
-
}
|
|
4946
|
-
const state = (check.state || "").toUpperCase();
|
|
4947
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
4948
|
-
return false;
|
|
4949
|
-
}
|
|
4950
|
-
}
|
|
4951
|
-
return true;
|
|
4952
|
-
}
|
|
4953
5735
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
4954
5736
|
const [owner, name] = repoName.split("/");
|
|
4955
5737
|
if (!owner || !name) {
|
|
@@ -4975,7 +5757,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4975
5757
|
return [];
|
|
4976
5758
|
}
|
|
4977
5759
|
const comments = thread.comments?.nodes || [];
|
|
4978
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
5760
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
4979
5761
|
if (!latestGreptileComment?.path?.trim()) {
|
|
4980
5762
|
return [];
|
|
4981
5763
|
}
|
|
@@ -4997,11 +5779,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
4997
5779
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4998
5780
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
4999
5781
|
}
|
|
5000
|
-
function stripHtml(input) {
|
|
5001
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5002
|
-
|
|
5003
|
-
`).trim();
|
|
5004
|
-
}
|
|
5005
5782
|
function summarizeComment(input) {
|
|
5006
5783
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5007
5784
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5010,31 +5787,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5010
5787
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5011
5788
|
}
|
|
5012
5789
|
function isAiReviewApproved(input) {
|
|
5790
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
5791
|
+
return false;
|
|
5792
|
+
}
|
|
5013
5793
|
if (input.reviewMode !== "required") {
|
|
5014
5794
|
return true;
|
|
5015
5795
|
}
|
|
5016
5796
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5017
5797
|
}
|
|
5018
|
-
function parseGreptileScore(input) {
|
|
5019
|
-
const text = stripHtml(input);
|
|
5020
|
-
const patterns = [
|
|
5021
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5022
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5023
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5024
|
-
];
|
|
5025
|
-
for (const pattern of patterns) {
|
|
5026
|
-
const match = pattern.exec(text);
|
|
5027
|
-
if (!match) {
|
|
5028
|
-
continue;
|
|
5029
|
-
}
|
|
5030
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5031
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5032
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5033
|
-
return { value, scale };
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
return null;
|
|
5037
|
-
}
|
|
5038
5798
|
|
|
5039
5799
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5040
5800
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -6347,8 +7107,9 @@ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
|
6347
7107
|
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
6348
7108
|
if (sourceIssueId) {
|
|
6349
7109
|
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
6350
|
-
if (match) {
|
|
6351
|
-
const
|
|
7110
|
+
if (match?.[1] && match[2]) {
|
|
7111
|
+
const sourceRepo = match[1];
|
|
7112
|
+
const issueNumber = match[2];
|
|
6352
7113
|
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
6353
7114
|
}
|
|
6354
7115
|
}
|