@h-rig/runtime 0.0.6-alpha.2 → 0.0.6-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig-agent-dispatch.js +84 -313
- package/dist/bin/rig-agent.js +85 -27
- package/dist/src/control-plane/agent-wrapper.js +101 -27
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +1357 -180
- package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
- package/dist/src/control-plane/hooks/inject-context.js +2 -2
- package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
- package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
- package/dist/src/control-plane/native/git-ops.js +134 -68
- package/dist/src/control-plane/native/harness-cli.js +1357 -180
- package/dist/src/control-plane/native/pr-automation.js +1532 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
- package/dist/src/control-plane/native/run-ops.js +35 -12
- package/dist/src/control-plane/native/task-ops.js +1274 -155
- package/dist/src/control-plane/native/validator.js +2 -2
- package/dist/src/control-plane/native/verifier.js +1274 -154
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/runtime/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/home.js +31 -6
- package/dist/src/control-plane/runtime/isolation/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +38 -9
- package/dist/src/control-plane/runtime/queue.js +38 -9
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
- package/dist/src/index.js +27 -20
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
- package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
- package/package.json +6 -6
- package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
- package/native/darwin-arm64/manifest.json +0 -1
- package/native/linux-x64/bin/rig-git +0 -0
- package/native/linux-x64/bin/rig-shell +0 -0
- package/native/linux-x64/bin/rig-tools +0 -0
- package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
- package/native/linux-x64/lib/runtime-native.so +0 -0
- package/native/linux-x64/manifest.json +0 -1
|
@@ -1198,8 +1198,8 @@ function githubStatusFor(issue) {
|
|
|
1198
1198
|
return "open";
|
|
1199
1199
|
}
|
|
1200
1200
|
function selectedGitHubEnv() {
|
|
1201
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
1202
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1201
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1202
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
1203
1203
|
}
|
|
1204
1204
|
function ghSpawnOptions() {
|
|
1205
1205
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -3605,6 +3605,1126 @@ ${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|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/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 greptileStatusVerdict(status) {
|
|
3726
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3727
|
+
if (!normalized)
|
|
3728
|
+
return null;
|
|
3729
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3730
|
+
return "approved";
|
|
3731
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3732
|
+
return "rejected";
|
|
3733
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3734
|
+
return "skipped";
|
|
3735
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3736
|
+
return "failed";
|
|
3737
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3738
|
+
return "pending";
|
|
3739
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3740
|
+
return "completed";
|
|
3741
|
+
return null;
|
|
3742
|
+
}
|
|
3743
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3744
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3745
|
+
}
|
|
3746
|
+
function greptileRequestTimeoutMs(env) {
|
|
3747
|
+
const fallback = 30000;
|
|
3748
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3749
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3750
|
+
}
|
|
3751
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3752
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3753
|
+
return null;
|
|
3754
|
+
const record = entry;
|
|
3755
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3756
|
+
if (!id)
|
|
3757
|
+
return null;
|
|
3758
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3759
|
+
return {
|
|
3760
|
+
id,
|
|
3761
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3762
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3763
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3764
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3768
|
+
const seen = new Set;
|
|
3769
|
+
const unique2 = [];
|
|
3770
|
+
for (const review of reviews) {
|
|
3771
|
+
if (seen.has(review.id))
|
|
3772
|
+
continue;
|
|
3773
|
+
seen.add(review.id);
|
|
3774
|
+
unique2.push(review);
|
|
3775
|
+
}
|
|
3776
|
+
return unique2;
|
|
3777
|
+
}
|
|
3778
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3779
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3780
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3781
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3782
|
+
const latest = sorted.slice(0, 1);
|
|
3783
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3784
|
+
}
|
|
3785
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3786
|
+
const selected = details ?? review;
|
|
3787
|
+
return {
|
|
3788
|
+
id: selected.id || review.id,
|
|
3789
|
+
body: selected.body ?? review.body ?? null,
|
|
3790
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3791
|
+
status: selected.status ?? review.status ?? null
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3795
|
+
const controller = new AbortController;
|
|
3796
|
+
const timeoutId = setTimeout(() => {
|
|
3797
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3798
|
+
}, input.timeoutMs);
|
|
3799
|
+
let response;
|
|
3800
|
+
try {
|
|
3801
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3802
|
+
method: "POST",
|
|
3803
|
+
headers: {
|
|
3804
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3805
|
+
"Content-Type": "application/json"
|
|
3806
|
+
},
|
|
3807
|
+
body: JSON.stringify({
|
|
3808
|
+
jsonrpc: "2.0",
|
|
3809
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3810
|
+
method: "tools/call",
|
|
3811
|
+
params: { name: input.name, arguments: input.args }
|
|
3812
|
+
}),
|
|
3813
|
+
signal: controller.signal
|
|
3814
|
+
});
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
if (controller.signal.aborted) {
|
|
3817
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3818
|
+
}
|
|
3819
|
+
throw error;
|
|
3820
|
+
} finally {
|
|
3821
|
+
clearTimeout(timeoutId);
|
|
3822
|
+
}
|
|
3823
|
+
const raw = await response.text();
|
|
3824
|
+
if (!response.ok) {
|
|
3825
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3826
|
+
}
|
|
3827
|
+
let envelope;
|
|
3828
|
+
try {
|
|
3829
|
+
envelope = JSON.parse(raw);
|
|
3830
|
+
} catch {
|
|
3831
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3832
|
+
}
|
|
3833
|
+
if (envelope.error?.message) {
|
|
3834
|
+
throw new Error(envelope.error.message);
|
|
3835
|
+
}
|
|
3836
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3837
|
+
`).trim();
|
|
3838
|
+
if (!text) {
|
|
3839
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3840
|
+
}
|
|
3841
|
+
return text;
|
|
3842
|
+
}
|
|
3843
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3844
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3845
|
+
try {
|
|
3846
|
+
return JSON.parse(text);
|
|
3847
|
+
} catch {
|
|
3848
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3852
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3853
|
+
return { signals: [], errors: [] };
|
|
3854
|
+
}
|
|
3855
|
+
const env = input.options?.env ?? process.env;
|
|
3856
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3857
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3858
|
+
if (!apiKey) {
|
|
3859
|
+
return { signals: [], errors: [] };
|
|
3860
|
+
}
|
|
3861
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3862
|
+
if (typeof fetchFn !== "function") {
|
|
3863
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3864
|
+
}
|
|
3865
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
3866
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
3867
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
3868
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
3869
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
3870
|
+
try {
|
|
3871
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
3872
|
+
apiBase,
|
|
3873
|
+
apiKey,
|
|
3874
|
+
name: "list_code_reviews",
|
|
3875
|
+
args: {
|
|
3876
|
+
name: repository,
|
|
3877
|
+
remote,
|
|
3878
|
+
defaultBranch,
|
|
3879
|
+
prNumber: input.prNumber,
|
|
3880
|
+
limit: 20
|
|
3881
|
+
},
|
|
3882
|
+
timeoutMs,
|
|
3883
|
+
fetchFn
|
|
3884
|
+
});
|
|
3885
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
3886
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
3887
|
+
const signals = [];
|
|
3888
|
+
for (const review of selectedReviews) {
|
|
3889
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
3890
|
+
apiBase,
|
|
3891
|
+
apiKey,
|
|
3892
|
+
name: "get_code_review",
|
|
3893
|
+
args: { codeReviewId: review.id },
|
|
3894
|
+
timeoutMs,
|
|
3895
|
+
fetchFn
|
|
3896
|
+
});
|
|
3897
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
3898
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
3899
|
+
}
|
|
3900
|
+
return { signals, errors: [] };
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
return {
|
|
3903
|
+
signals: [],
|
|
3904
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
function firstString(record, keys) {
|
|
3909
|
+
for (const key of keys) {
|
|
3910
|
+
const value = record[key];
|
|
3911
|
+
if (typeof value === "string")
|
|
3912
|
+
return value;
|
|
3913
|
+
}
|
|
3914
|
+
return "";
|
|
3915
|
+
}
|
|
3916
|
+
function arrayField(record, key) {
|
|
3917
|
+
const value = record[key];
|
|
3918
|
+
return Array.isArray(value) ? value : [];
|
|
3919
|
+
}
|
|
3920
|
+
async function runJsonArray(command, args, cwd) {
|
|
3921
|
+
const result = await command(args, { cwd });
|
|
3922
|
+
const label = `gh ${args.join(" ")}`;
|
|
3923
|
+
if (result.exitCode !== 0) {
|
|
3924
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3925
|
+
}
|
|
3926
|
+
const parsed = parseJsonArray(result.stdout);
|
|
3927
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3928
|
+
}
|
|
3929
|
+
async function runJsonObject(command, args, cwd) {
|
|
3930
|
+
const result = await command(args, { cwd });
|
|
3931
|
+
const label = `gh ${args.join(" ")}`;
|
|
3932
|
+
if (result.exitCode !== 0) {
|
|
3933
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3934
|
+
}
|
|
3935
|
+
const parsed = parseJsonObject(result.stdout);
|
|
3936
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3937
|
+
}
|
|
3938
|
+
function normalizeStatusCheck(entry) {
|
|
3939
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3940
|
+
return null;
|
|
3941
|
+
const record = entry;
|
|
3942
|
+
const name = firstString(record, ["name", "context"]);
|
|
3943
|
+
if (!name.trim())
|
|
3944
|
+
return null;
|
|
3945
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
3946
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
3947
|
+
return {
|
|
3948
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
3949
|
+
name,
|
|
3950
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
3951
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3952
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3953
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
3954
|
+
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,
|
|
3955
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
3956
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
3957
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
3958
|
+
output: output ? {
|
|
3959
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
3960
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
3961
|
+
text: typeof output.text === "string" ? output.text : null
|
|
3962
|
+
} : null,
|
|
3963
|
+
app: app ? {
|
|
3964
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
3965
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
3966
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
3967
|
+
} : null
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
function normalizeReview(entry) {
|
|
3971
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3972
|
+
return null;
|
|
3973
|
+
const record = entry;
|
|
3974
|
+
return {
|
|
3975
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
3976
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3977
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3978
|
+
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,
|
|
3979
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
3980
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
function normalizeReviewComment(entry) {
|
|
3984
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3985
|
+
return null;
|
|
3986
|
+
const record = entry;
|
|
3987
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3988
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
3989
|
+
if (!body && !path)
|
|
3990
|
+
return null;
|
|
3991
|
+
return {
|
|
3992
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3993
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
3994
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
3995
|
+
body,
|
|
3996
|
+
path,
|
|
3997
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
3998
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
3999
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4000
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
function normalizeIssueComment(entry) {
|
|
4004
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4005
|
+
return null;
|
|
4006
|
+
const record = entry;
|
|
4007
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4008
|
+
if (!body)
|
|
4009
|
+
return null;
|
|
4010
|
+
return {
|
|
4011
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4012
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4013
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4014
|
+
body,
|
|
4015
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4016
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4017
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
function normalizeReviewThread(entry) {
|
|
4021
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4022
|
+
return null;
|
|
4023
|
+
const record = entry;
|
|
4024
|
+
return {
|
|
4025
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4026
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4027
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4028
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
function relevantIssueComment(comment) {
|
|
4032
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4033
|
+
const body = comment.body ?? "";
|
|
4034
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4035
|
+
}
|
|
4036
|
+
function latestThreadComment(thread) {
|
|
4037
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4038
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4039
|
+
}
|
|
4040
|
+
function unresolvedThreadSummaries(threads) {
|
|
4041
|
+
return threads.flatMap((thread) => {
|
|
4042
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4043
|
+
return [];
|
|
4044
|
+
const latest = latestThreadComment(thread);
|
|
4045
|
+
if (!latest)
|
|
4046
|
+
return ["Unresolved review thread"];
|
|
4047
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4048
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
function collectBodies(evidence) {
|
|
4052
|
+
return [
|
|
4053
|
+
evidence.title ?? "",
|
|
4054
|
+
evidence.body,
|
|
4055
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4056
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4057
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4058
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4059
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4060
|
+
].filter((body) => body.trim().length > 0);
|
|
4061
|
+
}
|
|
4062
|
+
function bodyExcerpt(body) {
|
|
4063
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4064
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4065
|
+
}
|
|
4066
|
+
function makeGreptileSignal(input) {
|
|
4067
|
+
const scores = parseGreptileScores(input.body);
|
|
4068
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4069
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4070
|
+
const verdict = input.verdict ?? null;
|
|
4071
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4072
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4073
|
+
return {
|
|
4074
|
+
source: input.source,
|
|
4075
|
+
trusted: input.trusted,
|
|
4076
|
+
authorLogin: input.authorLogin ?? null,
|
|
4077
|
+
reviewedSha,
|
|
4078
|
+
current,
|
|
4079
|
+
stale: current === false,
|
|
4080
|
+
score: scores[0] ?? null,
|
|
4081
|
+
scores,
|
|
4082
|
+
explicitApproval,
|
|
4083
|
+
verdict,
|
|
4084
|
+
blocker,
|
|
4085
|
+
actionable: input.actionable ?? blocker,
|
|
4086
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4087
|
+
body: input.body,
|
|
4088
|
+
allScores: scores
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
4091
|
+
function reviewAuthorLogin(review) {
|
|
4092
|
+
return review.author?.login ?? null;
|
|
4093
|
+
}
|
|
4094
|
+
function commentAuthorLogin(comment) {
|
|
4095
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4096
|
+
}
|
|
4097
|
+
function collectGreptileSignals(evidence) {
|
|
4098
|
+
const signals = [];
|
|
4099
|
+
const contextSources = [
|
|
4100
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4101
|
+
{ source: "pr-body", body: evidence.body }
|
|
4102
|
+
];
|
|
4103
|
+
for (const context of contextSources) {
|
|
4104
|
+
if (!context.body.trim())
|
|
4105
|
+
continue;
|
|
4106
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4107
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4108
|
+
continue;
|
|
4109
|
+
signals.push(makeGreptileSignal({
|
|
4110
|
+
source: context.source,
|
|
4111
|
+
body: context.body,
|
|
4112
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4113
|
+
trusted: false,
|
|
4114
|
+
blocker: contextBlocker,
|
|
4115
|
+
actionable: contextBlocker
|
|
4116
|
+
}));
|
|
4117
|
+
}
|
|
4118
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4119
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4120
|
+
|
|
4121
|
+
`) || "Status: UNKNOWN";
|
|
4122
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4123
|
+
signals.push(makeGreptileSignal({
|
|
4124
|
+
source: "api",
|
|
4125
|
+
body,
|
|
4126
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4127
|
+
trusted: true,
|
|
4128
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4129
|
+
explicitApproval: verdict === "approved",
|
|
4130
|
+
verdict
|
|
4131
|
+
}));
|
|
4132
|
+
}
|
|
4133
|
+
for (const review of evidence.reviews) {
|
|
4134
|
+
const login = reviewAuthorLogin(review);
|
|
4135
|
+
if (!isGreptileGithubLogin(login))
|
|
4136
|
+
continue;
|
|
4137
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4138
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4139
|
+
|
|
4140
|
+
`);
|
|
4141
|
+
if (!body.trim())
|
|
4142
|
+
continue;
|
|
4143
|
+
const dismissed = state === "DISMISSED";
|
|
4144
|
+
signals.push(makeGreptileSignal({
|
|
4145
|
+
source: "github-review",
|
|
4146
|
+
body,
|
|
4147
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4148
|
+
trusted: !dismissed,
|
|
4149
|
+
authorLogin: login,
|
|
4150
|
+
reviewedSha: review.commit_id ?? null,
|
|
4151
|
+
explicitApproval: undefined,
|
|
4152
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4153
|
+
}));
|
|
4154
|
+
}
|
|
4155
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4156
|
+
const login = commentAuthorLogin(comment);
|
|
4157
|
+
const body = comment.body ?? "";
|
|
4158
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4159
|
+
continue;
|
|
4160
|
+
signals.push(makeGreptileSignal({
|
|
4161
|
+
source: "issue-comment",
|
|
4162
|
+
body,
|
|
4163
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4164
|
+
trusted: true,
|
|
4165
|
+
authorLogin: login
|
|
4166
|
+
}));
|
|
4167
|
+
}
|
|
4168
|
+
for (const thread of evidence.reviewThreads) {
|
|
4169
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4170
|
+
continue;
|
|
4171
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4172
|
+
const login = comment.author?.login ?? null;
|
|
4173
|
+
const body = comment.body ?? "";
|
|
4174
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4175
|
+
continue;
|
|
4176
|
+
signals.push(makeGreptileSignal({
|
|
4177
|
+
source: "review-thread",
|
|
4178
|
+
body,
|
|
4179
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4180
|
+
trusted: true,
|
|
4181
|
+
authorLogin: login
|
|
4182
|
+
}));
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
for (const check of evidence.checks) {
|
|
4186
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4187
|
+
continue;
|
|
4188
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4189
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4190
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4191
|
+
|
|
4192
|
+
`);
|
|
4193
|
+
signals.push(makeGreptileSignal({
|
|
4194
|
+
source: "github-check",
|
|
4195
|
+
body,
|
|
4196
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4197
|
+
trusted: false,
|
|
4198
|
+
reviewedSha,
|
|
4199
|
+
explicitApproval: false,
|
|
4200
|
+
blocker: isFailingCheck(check),
|
|
4201
|
+
actionable: isFailingCheck(check)
|
|
4202
|
+
}));
|
|
4203
|
+
}
|
|
4204
|
+
return signals;
|
|
4205
|
+
}
|
|
4206
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4207
|
+
return threads.flatMap((thread) => {
|
|
4208
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4209
|
+
return [];
|
|
4210
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4211
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4212
|
+
return [];
|
|
4213
|
+
const latest = latestThreadComment(thread);
|
|
4214
|
+
if (!latest)
|
|
4215
|
+
return ["Unresolved Greptile review thread"];
|
|
4216
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4217
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4221
|
+
return [];
|
|
4222
|
+
}
|
|
4223
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4224
|
+
return comments.flatMap((comment) => {
|
|
4225
|
+
const body = comment.body?.trim() ?? "";
|
|
4226
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4227
|
+
return [];
|
|
4228
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4229
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4230
|
+
return [`${author}: ${body}`];
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4234
|
+
return reviews.flatMap((review) => {
|
|
4235
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4236
|
+
if (isGreptileGithubLogin(login))
|
|
4237
|
+
return [];
|
|
4238
|
+
const body = review.body?.trim() ?? "";
|
|
4239
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4240
|
+
return [];
|
|
4241
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4242
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
function signalLabel(signal) {
|
|
4246
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4247
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4248
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4249
|
+
return `${source}${author}${sha}`;
|
|
4250
|
+
}
|
|
4251
|
+
function deriveGreptileEvidence(input) {
|
|
4252
|
+
const rawBodies = collectBodies(input);
|
|
4253
|
+
const signals = collectGreptileSignals(input);
|
|
4254
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4255
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4256
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4257
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4258
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4259
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4260
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4261
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4262
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4263
|
+
const signalCanApproveByScore = (signal) => {
|
|
4264
|
+
if (signal.source === "api")
|
|
4265
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4266
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4267
|
+
};
|
|
4268
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4269
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4270
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4271
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4272
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4273
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4274
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4275
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4276
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4277
|
+
const staleBlockingSignals = [];
|
|
4278
|
+
const blockers = [
|
|
4279
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4280
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4281
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4282
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4283
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4284
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4285
|
+
];
|
|
4286
|
+
const unresolvedComments = [
|
|
4287
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4288
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4289
|
+
];
|
|
4290
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4291
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4292
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4293
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4294
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4295
|
+
});
|
|
4296
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4297
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4298
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4299
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4300
|
+
});
|
|
4301
|
+
const completedGreptileApi = trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && (signal.verdict === "approved" || signal.verdict === "rejected" || signal.verdict === "skipped" || signal.verdict === "failed" || signal.verdict === "completed"));
|
|
4302
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4303
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4304
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4305
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4306
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4307
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4308
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4309
|
+
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";
|
|
4310
|
+
return {
|
|
4311
|
+
source,
|
|
4312
|
+
currentHeadSha: input.currentHeadSha,
|
|
4313
|
+
reviewedSha,
|
|
4314
|
+
fresh,
|
|
4315
|
+
completed,
|
|
4316
|
+
approved,
|
|
4317
|
+
score,
|
|
4318
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4319
|
+
blockers,
|
|
4320
|
+
unresolvedComments,
|
|
4321
|
+
rawBodies,
|
|
4322
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4323
|
+
mapping
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
function isGreptileCheckDetail(check) {
|
|
4327
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4328
|
+
}
|
|
4329
|
+
async function collectGreptileCheckDetails(input) {
|
|
4330
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4331
|
+
"api",
|
|
4332
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4333
|
+
"--paginate",
|
|
4334
|
+
"--slurp",
|
|
4335
|
+
"--jq",
|
|
4336
|
+
"map(.check_runs // []) | add // []"
|
|
4337
|
+
], input.projectRoot);
|
|
4338
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4339
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4340
|
+
}
|
|
4341
|
+
async function collectReviewThreads(input) {
|
|
4342
|
+
const reviewThreads = [];
|
|
4343
|
+
let afterCursor = null;
|
|
4344
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4345
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4346
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4347
|
+
"api",
|
|
4348
|
+
"graphql",
|
|
4349
|
+
"-F",
|
|
4350
|
+
`owner=${input.owner}`,
|
|
4351
|
+
"-F",
|
|
4352
|
+
`name=${input.name}`,
|
|
4353
|
+
"-F",
|
|
4354
|
+
`prNumber=${input.prNumber}`,
|
|
4355
|
+
"-f",
|
|
4356
|
+
`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 } } } } }`
|
|
4357
|
+
], input.projectRoot);
|
|
4358
|
+
if (threadsResponse.error) {
|
|
4359
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4360
|
+
}
|
|
4361
|
+
const data = threadsResponse.value.data;
|
|
4362
|
+
const repository = data?.repository;
|
|
4363
|
+
const pullRequest = repository?.pullRequest;
|
|
4364
|
+
const threads = pullRequest?.reviewThreads;
|
|
4365
|
+
const nodes = threads?.nodes;
|
|
4366
|
+
if (!Array.isArray(nodes)) {
|
|
4367
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4368
|
+
}
|
|
4369
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4370
|
+
reviewThreads.push(...normalized);
|
|
4371
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4372
|
+
if (truncatedCommentThread) {
|
|
4373
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4374
|
+
}
|
|
4375
|
+
const pageInfo = threads?.pageInfo;
|
|
4376
|
+
if (!pageInfo) {
|
|
4377
|
+
if (nodes.length >= 100) {
|
|
4378
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4379
|
+
}
|
|
4380
|
+
return { value: reviewThreads };
|
|
4381
|
+
}
|
|
4382
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4383
|
+
return { value: reviewThreads };
|
|
4384
|
+
}
|
|
4385
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4386
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4387
|
+
}
|
|
4388
|
+
afterCursor = pageInfo.endCursor;
|
|
4389
|
+
}
|
|
4390
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4391
|
+
}
|
|
4392
|
+
async function collectPrReviewEvidence(input) {
|
|
4393
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4394
|
+
if (!parsed) {
|
|
4395
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4396
|
+
}
|
|
4397
|
+
const readErrors = [];
|
|
4398
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4399
|
+
"pr",
|
|
4400
|
+
"view",
|
|
4401
|
+
input.prUrl,
|
|
4402
|
+
"--json",
|
|
4403
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4404
|
+
], input.projectRoot);
|
|
4405
|
+
if (viewRead.error)
|
|
4406
|
+
readErrors.push(viewRead.error);
|
|
4407
|
+
const view = viewRead.value;
|
|
4408
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4409
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4410
|
+
}
|
|
4411
|
+
if (!Array.isArray(view.reviews)) {
|
|
4412
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4413
|
+
}
|
|
4414
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4415
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4416
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4417
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4418
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4419
|
+
if (reviewCommentsRead.error)
|
|
4420
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4421
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4422
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4423
|
+
if (issueCommentsRead.error)
|
|
4424
|
+
readErrors.push(issueCommentsRead.error);
|
|
4425
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4426
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4427
|
+
command: input.command,
|
|
4428
|
+
projectRoot: input.projectRoot,
|
|
4429
|
+
owner: parsed.owner,
|
|
4430
|
+
name: parsed.repo,
|
|
4431
|
+
prNumber: parsed.prNumber
|
|
4432
|
+
});
|
|
4433
|
+
if (reviewThreadsRead.error)
|
|
4434
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4435
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4436
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4437
|
+
let greptileCheckDetails = [];
|
|
4438
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4439
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4440
|
+
command: input.command,
|
|
4441
|
+
projectRoot: input.projectRoot,
|
|
4442
|
+
repoName: parsed.repoName,
|
|
4443
|
+
headSha
|
|
4444
|
+
});
|
|
4445
|
+
if (checkDetailsRead.error)
|
|
4446
|
+
readErrors.push(checkDetailsRead.error);
|
|
4447
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4448
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4449
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4453
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4454
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4455
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4456
|
+
options: input.greptileApi,
|
|
4457
|
+
repoName: parsed.repoName,
|
|
4458
|
+
prNumber: parsed.prNumber,
|
|
4459
|
+
headSha,
|
|
4460
|
+
baseRefName
|
|
4461
|
+
});
|
|
4462
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4463
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4464
|
+
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})` : ""}`);
|
|
4465
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4466
|
+
const evidenceBase = {
|
|
4467
|
+
title: firstString(view, ["title"]),
|
|
4468
|
+
body: firstString(view, ["body"]),
|
|
4469
|
+
reviews,
|
|
4470
|
+
changedFileReviewComments: reviewComments,
|
|
4471
|
+
relevantIssueComments: issueComments,
|
|
4472
|
+
reviewThreads,
|
|
4473
|
+
checks: checksWithGreptileDetails,
|
|
4474
|
+
currentHeadSha: headSha,
|
|
4475
|
+
apiSignals
|
|
4476
|
+
};
|
|
4477
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4478
|
+
return {
|
|
4479
|
+
prUrl: input.prUrl,
|
|
4480
|
+
prNumber: parsed.prNumber,
|
|
4481
|
+
repoName: parsed.repoName,
|
|
4482
|
+
title: evidenceBase.title,
|
|
4483
|
+
body: evidenceBase.body,
|
|
4484
|
+
headSha,
|
|
4485
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4486
|
+
baseRefName,
|
|
4487
|
+
state: firstString(view, ["state"]),
|
|
4488
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4489
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4490
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4491
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4492
|
+
reviews,
|
|
4493
|
+
reviewThreads,
|
|
4494
|
+
changedFileReviewComments: reviewComments,
|
|
4495
|
+
relevantIssueComments: issueComments,
|
|
4496
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4497
|
+
checkFailures,
|
|
4498
|
+
pendingChecks,
|
|
4499
|
+
readErrors,
|
|
4500
|
+
greptile
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4504
|
+
const normalized = value.trim();
|
|
4505
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4506
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4507
|
+
}
|
|
4508
|
+
function evaluateEvidence(evidence) {
|
|
4509
|
+
const reasonDetails = [];
|
|
4510
|
+
const warnings = [];
|
|
4511
|
+
const seen = new Set;
|
|
4512
|
+
const addReason = (reason) => {
|
|
4513
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4514
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4515
|
+
if (seen.has(key))
|
|
4516
|
+
return;
|
|
4517
|
+
seen.add(key);
|
|
4518
|
+
reasonDetails.push(capped);
|
|
4519
|
+
};
|
|
4520
|
+
const greptile = evidence.greptile;
|
|
4521
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4522
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4523
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4524
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4525
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4526
|
+
for (const error of evidence.readErrors) {
|
|
4527
|
+
addReason({
|
|
4528
|
+
code: "read_error",
|
|
4529
|
+
reasonClass: "reject",
|
|
4530
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4531
|
+
suggestedAction: "needs_attention",
|
|
4532
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4533
|
+
headSha: evidence.headSha || null
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
if (!evidence.headSha) {
|
|
4537
|
+
addReason({
|
|
4538
|
+
code: "missing_head_sha",
|
|
4539
|
+
reasonClass: "reject",
|
|
4540
|
+
surface: "github",
|
|
4541
|
+
suggestedAction: "needs_attention",
|
|
4542
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4543
|
+
headSha: null
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
for (const failure of evidence.checkFailures) {
|
|
4547
|
+
addReason({
|
|
4548
|
+
code: "ci_failed",
|
|
4549
|
+
reasonClass: "reject",
|
|
4550
|
+
surface: "ci",
|
|
4551
|
+
suggestedAction: "fix",
|
|
4552
|
+
message: failure,
|
|
4553
|
+
headSha: evidence.headSha || null
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4557
|
+
addReason({
|
|
4558
|
+
code: "check_pending",
|
|
4559
|
+
reasonClass: "pending",
|
|
4560
|
+
surface: "ci",
|
|
4561
|
+
suggestedAction: "wait",
|
|
4562
|
+
message: pendingCheck,
|
|
4563
|
+
headSha: evidence.headSha || null
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4567
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4568
|
+
addReason({
|
|
4569
|
+
code: "review_decision_blocking",
|
|
4570
|
+
reasonClass: "reject",
|
|
4571
|
+
surface: "review",
|
|
4572
|
+
suggestedAction: "fix",
|
|
4573
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4574
|
+
headSha: evidence.headSha || null
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4578
|
+
addReason({
|
|
4579
|
+
code: "review_thread_unresolved",
|
|
4580
|
+
reasonClass: "reject",
|
|
4581
|
+
surface: "review",
|
|
4582
|
+
suggestedAction: "fix",
|
|
4583
|
+
message: thread,
|
|
4584
|
+
headSha: evidence.headSha || null
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
if (greptile.mapping === "missing") {
|
|
4588
|
+
addReason({
|
|
4589
|
+
code: "greptile_missing",
|
|
4590
|
+
reasonClass: "pending",
|
|
4591
|
+
surface: "greptile",
|
|
4592
|
+
suggestedAction: "wait",
|
|
4593
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4594
|
+
headSha: evidence.headSha || null
|
|
4595
|
+
});
|
|
4596
|
+
}
|
|
4597
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4598
|
+
addReason({
|
|
4599
|
+
code: "greptile_stale",
|
|
4600
|
+
reasonClass: "pending",
|
|
4601
|
+
surface: "greptile",
|
|
4602
|
+
suggestedAction: "wait",
|
|
4603
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4604
|
+
headSha: evidence.headSha || null,
|
|
4605
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4609
|
+
addReason({
|
|
4610
|
+
code: "greptile_pending",
|
|
4611
|
+
reasonClass: "pending",
|
|
4612
|
+
surface: "greptile",
|
|
4613
|
+
suggestedAction: "wait",
|
|
4614
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4615
|
+
headSha: evidence.headSha || null,
|
|
4616
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4620
|
+
addReason({
|
|
4621
|
+
code: "greptile_api_status_unknown",
|
|
4622
|
+
reasonClass: "reject",
|
|
4623
|
+
surface: "greptile",
|
|
4624
|
+
suggestedAction: "needs_attention",
|
|
4625
|
+
message: `Greptile API/MCP review status is unknown; merge requires a known terminal APPROVED/COMPLETED 5/5 result or a known conservative status${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4626
|
+
headSha: evidence.headSha || null,
|
|
4627
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
if (!greptile.completed) {
|
|
4631
|
+
addReason({
|
|
4632
|
+
code: "greptile_pending",
|
|
4633
|
+
reasonClass: "pending",
|
|
4634
|
+
surface: "greptile",
|
|
4635
|
+
suggestedAction: "wait",
|
|
4636
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4637
|
+
headSha: evidence.headSha || null,
|
|
4638
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
if (!greptile.fresh) {
|
|
4642
|
+
addReason({
|
|
4643
|
+
code: "greptile_not_current_head",
|
|
4644
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4645
|
+
surface: "greptile",
|
|
4646
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4647
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4648
|
+
headSha: evidence.headSha || null,
|
|
4649
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4653
|
+
addReason({
|
|
4654
|
+
code: "greptile_score_not_5",
|
|
4655
|
+
reasonClass: "reject",
|
|
4656
|
+
surface: "greptile",
|
|
4657
|
+
suggestedAction: "fix",
|
|
4658
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4659
|
+
headSha: evidence.headSha || null,
|
|
4660
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4661
|
+
});
|
|
4662
|
+
}
|
|
4663
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4664
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4665
|
+
addReason({
|
|
4666
|
+
code: "greptile_score_missing",
|
|
4667
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4668
|
+
surface: "greptile",
|
|
4669
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4670
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4671
|
+
headSha: evidence.headSha || null,
|
|
4672
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
if (greptile.mapping === "unproven") {
|
|
4676
|
+
addReason({
|
|
4677
|
+
code: "greptile_mapping_unproven",
|
|
4678
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4679
|
+
surface: "greptile",
|
|
4680
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4681
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4682
|
+
headSha: evidence.headSha || null,
|
|
4683
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4684
|
+
});
|
|
4685
|
+
}
|
|
4686
|
+
for (const blocker of greptile.blockers) {
|
|
4687
|
+
addReason({
|
|
4688
|
+
code: "greptile_blocker_text",
|
|
4689
|
+
reasonClass: "reject",
|
|
4690
|
+
surface: "greptile",
|
|
4691
|
+
suggestedAction: "fix",
|
|
4692
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4693
|
+
headSha: evidence.headSha || null,
|
|
4694
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4698
|
+
addReason({
|
|
4699
|
+
code: "greptile_unresolved_comment",
|
|
4700
|
+
reasonClass: "reject",
|
|
4701
|
+
surface: "greptile",
|
|
4702
|
+
suggestedAction: "fix",
|
|
4703
|
+
message: comment,
|
|
4704
|
+
headSha: evidence.headSha || null,
|
|
4705
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4706
|
+
});
|
|
4707
|
+
}
|
|
4708
|
+
if (!greptile.approved)
|
|
4709
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4710
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4711
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4712
|
+
}
|
|
4713
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4714
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4715
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4716
|
+
return {
|
|
4717
|
+
approved,
|
|
4718
|
+
pending: evaluated.pending,
|
|
4719
|
+
reasons: evaluated.reasons,
|
|
4720
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4721
|
+
warnings: evaluated.warnings,
|
|
4722
|
+
actionableFeedback: evaluated.reasons,
|
|
4723
|
+
evidence
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3608
4728
|
async function verifyTask(options) {
|
|
3609
4729
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
3610
4730
|
const taskId = options.taskId;
|
|
@@ -4400,7 +5520,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4400
5520
|
}
|
|
4401
5521
|
};
|
|
4402
5522
|
}
|
|
4403
|
-
|
|
5523
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5524
|
+
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
4404
5525
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4405
5526
|
return {
|
|
4406
5527
|
verdict: "REJECT",
|
|
@@ -4416,44 +5537,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
4416
5537
|
}
|
|
4417
5538
|
};
|
|
4418
5539
|
}
|
|
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
|
-
|
|
5540
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5541
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5542
|
+
return {
|
|
5543
|
+
verdict: "REJECT",
|
|
5544
|
+
feedback,
|
|
5545
|
+
reasons,
|
|
5546
|
+
warnings,
|
|
5547
|
+
rawPayload: {
|
|
5548
|
+
pr: options.prState,
|
|
5549
|
+
codeReviews: reviewsPayload,
|
|
5550
|
+
selectedReview,
|
|
5551
|
+
reviewDetails,
|
|
5552
|
+
comments: commentsPayload,
|
|
5553
|
+
score
|
|
5554
|
+
}
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5558
|
+
let strictGate = null;
|
|
5559
|
+
try {
|
|
5560
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5561
|
+
projectRoot: options.projectRoot,
|
|
5562
|
+
taskId: options.taskId,
|
|
5563
|
+
prUrl,
|
|
5564
|
+
apiSignals: [{
|
|
5565
|
+
id: selectedReview.id,
|
|
5566
|
+
body: reviewBody,
|
|
5567
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5568
|
+
status: selectedReview.status
|
|
5569
|
+
}]
|
|
5570
|
+
});
|
|
5571
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5572
|
+
} catch (error) {
|
|
5573
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5574
|
+
return {
|
|
5575
|
+
verdict: "REJECT",
|
|
5576
|
+
feedback,
|
|
5577
|
+
reasons,
|
|
5578
|
+
warnings,
|
|
5579
|
+
rawPayload: {
|
|
5580
|
+
pr: options.prState,
|
|
5581
|
+
codeReviews: reviewsPayload,
|
|
5582
|
+
selectedReview,
|
|
5583
|
+
reviewDetails,
|
|
5584
|
+
comments: commentsPayload,
|
|
5585
|
+
score
|
|
5586
|
+
}
|
|
5587
|
+
};
|
|
5588
|
+
}
|
|
5589
|
+
if (!strictGate.approved) {
|
|
5590
|
+
return {
|
|
5591
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5592
|
+
feedback,
|
|
5593
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5594
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5595
|
+
rawPayload: {
|
|
5596
|
+
pr: options.prState,
|
|
5597
|
+
codeReviews: reviewsPayload,
|
|
5598
|
+
selectedReview,
|
|
5599
|
+
reviewDetails,
|
|
5600
|
+
comments: commentsPayload,
|
|
5601
|
+
score,
|
|
5602
|
+
strictGate: {
|
|
5603
|
+
approved: strictGate.approved,
|
|
5604
|
+
pending: strictGate.pending,
|
|
5605
|
+
reasons: strictGate.reasons,
|
|
5606
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5607
|
+
warnings: strictGate.warnings,
|
|
5608
|
+
greptile: strictGate.evidence.greptile,
|
|
5609
|
+
readErrors: strictGate.evidence.readErrors
|
|
4451
5610
|
}
|
|
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
|
-
}
|
|
5611
|
+
}
|
|
5612
|
+
};
|
|
4457
5613
|
}
|
|
4458
5614
|
return {
|
|
4459
5615
|
verdict: "APPROVE",
|
|
@@ -4465,7 +5621,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
4465
5621
|
codeReviews: reviewsPayload,
|
|
4466
5622
|
selectedReview,
|
|
4467
5623
|
reviewDetails,
|
|
4468
|
-
comments: commentsPayload
|
|
5624
|
+
comments: commentsPayload,
|
|
5625
|
+
strictGate: {
|
|
5626
|
+
approved: strictGate.approved,
|
|
5627
|
+
pending: strictGate.pending,
|
|
5628
|
+
reasons: strictGate.reasons,
|
|
5629
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5630
|
+
warnings: strictGate.warnings,
|
|
5631
|
+
greptile: strictGate.evidence.greptile,
|
|
5632
|
+
readErrors: strictGate.evidence.readErrors
|
|
5633
|
+
}
|
|
4469
5634
|
}
|
|
4470
5635
|
};
|
|
4471
5636
|
}
|
|
@@ -4489,7 +5654,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4489
5654
|
let threads = [];
|
|
4490
5655
|
let actionableThreads = [];
|
|
4491
5656
|
let checkRollup = [];
|
|
4492
|
-
let
|
|
5657
|
+
let checkState2 = { pending: false, completed: false };
|
|
4493
5658
|
for (let attempt = 0;; attempt += 1) {
|
|
4494
5659
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4495
5660
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4498,15 +5663,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4498
5663
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4499
5664
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4500
5665
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4501
|
-
|
|
4502
|
-
const
|
|
5666
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5667
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4503
5668
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4504
5669
|
attempt,
|
|
4505
5670
|
pollAttempts: options.pollAttempts,
|
|
4506
|
-
checkState,
|
|
5671
|
+
checkState: checkState2,
|
|
4507
5672
|
fallbackReview,
|
|
4508
5673
|
selectedReview,
|
|
4509
|
-
approvedViaReviewedAncestor
|
|
5674
|
+
approvedViaReviewedAncestor
|
|
4510
5675
|
})) {
|
|
4511
5676
|
break;
|
|
4512
5677
|
}
|
|
@@ -4534,7 +5699,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4534
5699
|
].filter(Boolean).join(`
|
|
4535
5700
|
`);
|
|
4536
5701
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4537
|
-
if (
|
|
5702
|
+
if (checkState2.pending) {
|
|
4538
5703
|
return {
|
|
4539
5704
|
verdict: "SKIP",
|
|
4540
5705
|
feedback,
|
|
@@ -4545,34 +5710,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4545
5710
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4546
5711
|
};
|
|
4547
5712
|
}
|
|
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) {
|
|
5713
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5714
|
+
let strictGate;
|
|
5715
|
+
try {
|
|
5716
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5717
|
+
projectRoot: options.projectRoot,
|
|
5718
|
+
taskId: options.taskId,
|
|
5719
|
+
prUrl
|
|
5720
|
+
});
|
|
5721
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5722
|
+
} catch (error) {
|
|
4572
5723
|
return {
|
|
4573
5724
|
verdict: "REJECT",
|
|
4574
5725
|
feedback,
|
|
4575
|
-
reasons:
|
|
5726
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4576
5727
|
warnings,
|
|
4577
5728
|
rawPayload: {
|
|
4578
5729
|
pr: options.prState,
|
|
@@ -4585,44 +5736,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4585
5736
|
}
|
|
4586
5737
|
};
|
|
4587
5738
|
}
|
|
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
|
-
}
|
|
5739
|
+
if (!strictGate.approved) {
|
|
4606
5740
|
return {
|
|
4607
|
-
verdict: "SKIP",
|
|
5741
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4608
5742
|
feedback,
|
|
4609
|
-
reasons: [
|
|
4610
|
-
|
|
4611
|
-
],
|
|
4612
|
-
warnings,
|
|
5743
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5744
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4613
5745
|
rawPayload: {
|
|
4614
5746
|
pr: options.prState,
|
|
4615
5747
|
selectedReview: fallbackReview,
|
|
4616
5748
|
reviews,
|
|
4617
5749
|
threads,
|
|
4618
5750
|
checkRollup,
|
|
5751
|
+
actionableThreads,
|
|
5752
|
+
strictGate: {
|
|
5753
|
+
approved: strictGate.approved,
|
|
5754
|
+
pending: strictGate.pending,
|
|
5755
|
+
reasons: strictGate.reasons,
|
|
5756
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5757
|
+
warnings: strictGate.warnings,
|
|
5758
|
+
greptile: strictGate.evidence.greptile
|
|
5759
|
+
},
|
|
4619
5760
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4620
5761
|
}
|
|
4621
5762
|
};
|
|
4622
5763
|
}
|
|
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
5764
|
return {
|
|
4627
5765
|
verdict: "APPROVE",
|
|
4628
5766
|
feedback,
|
|
@@ -4634,6 +5772,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4634
5772
|
reviews,
|
|
4635
5773
|
threads,
|
|
4636
5774
|
checkRollup,
|
|
5775
|
+
strictGate: {
|
|
5776
|
+
approved: strictGate.approved,
|
|
5777
|
+
pending: strictGate.pending,
|
|
5778
|
+
reasons: strictGate.reasons,
|
|
5779
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5780
|
+
warnings: strictGate.warnings,
|
|
5781
|
+
greptile: strictGate.evidence.greptile
|
|
5782
|
+
},
|
|
4637
5783
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4638
5784
|
}
|
|
4639
5785
|
};
|
|
@@ -4746,19 +5892,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4746
5892
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4747
5893
|
return true;
|
|
4748
5894
|
}
|
|
4749
|
-
return
|
|
5895
|
+
return false;
|
|
4750
5896
|
}
|
|
4751
5897
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4752
5898
|
if (options.githubCheckState.completed) {
|
|
4753
5899
|
return false;
|
|
4754
5900
|
}
|
|
5901
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5902
|
+
return false;
|
|
5903
|
+
}
|
|
4755
5904
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4756
5905
|
return true;
|
|
4757
5906
|
}
|
|
4758
|
-
return
|
|
5907
|
+
return true;
|
|
4759
5908
|
}
|
|
4760
5909
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4761
5910
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
5911
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5912
|
+
return false;
|
|
5913
|
+
}
|
|
4762
5914
|
if (waitingForVisiblePendingReview) {
|
|
4763
5915
|
return true;
|
|
4764
5916
|
}
|
|
@@ -4819,6 +5971,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4819
5971
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4820
5972
|
}
|
|
4821
5973
|
}
|
|
5974
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
5975
|
+
return collectPrReviewEvidence({
|
|
5976
|
+
projectRoot: input.projectRoot,
|
|
5977
|
+
prUrl: input.prUrl,
|
|
5978
|
+
taskId: input.taskId,
|
|
5979
|
+
runId: "verifier",
|
|
5980
|
+
cycle: 0,
|
|
5981
|
+
apiSignals: input.apiSignals ?? [],
|
|
5982
|
+
command: async (args, options) => {
|
|
5983
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
5984
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5985
|
+
}
|
|
5986
|
+
});
|
|
5987
|
+
}
|
|
4822
5988
|
function deriveRepoName(projectRoot, prState) {
|
|
4823
5989
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4824
5990
|
if (fromUrl?.[1]) {
|
|
@@ -4833,8 +5999,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
4833
5999
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4834
6000
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
4835
6001
|
}
|
|
4836
|
-
function
|
|
4837
|
-
|
|
6002
|
+
function isGreptileGithubLogin2(login) {
|
|
6003
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6004
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
4838
6005
|
}
|
|
4839
6006
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
4840
6007
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -4851,7 +6018,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
4851
6018
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
4852
6019
|
}
|
|
4853
6020
|
function sortGithubGreptileReviews(reviews) {
|
|
4854
|
-
return reviews.filter((review) =>
|
|
6021
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
4855
6022
|
}
|
|
4856
6023
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
4857
6024
|
const response = runGhJson(projectRoot, [
|
|
@@ -4924,32 +6091,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
4924
6091
|
}
|
|
4925
6092
|
return { pending: false, completed: false };
|
|
4926
6093
|
}
|
|
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
6094
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
4954
6095
|
const [owner, name] = repoName.split("/");
|
|
4955
6096
|
if (!owner || !name) {
|
|
@@ -4975,7 +6116,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4975
6116
|
return [];
|
|
4976
6117
|
}
|
|
4977
6118
|
const comments = thread.comments?.nodes || [];
|
|
4978
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6119
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
4979
6120
|
if (!latestGreptileComment?.path?.trim()) {
|
|
4980
6121
|
return [];
|
|
4981
6122
|
}
|
|
@@ -4997,11 +6138,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
4997
6138
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4998
6139
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
4999
6140
|
}
|
|
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
6141
|
function summarizeComment(input) {
|
|
5006
6142
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5007
6143
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5010,31 +6146,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5010
6146
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5011
6147
|
}
|
|
5012
6148
|
function isAiReviewApproved(input) {
|
|
6149
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6150
|
+
return false;
|
|
6151
|
+
}
|
|
5013
6152
|
if (input.reviewMode !== "required") {
|
|
5014
6153
|
return true;
|
|
5015
6154
|
}
|
|
5016
6155
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5017
6156
|
}
|
|
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
6157
|
|
|
5039
6158
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5040
6159
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -5983,12 +7102,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
5983
7102
|
"task-result.json",
|
|
5984
7103
|
"validation-summary.json"
|
|
5985
7104
|
]);
|
|
5986
|
-
function resolveHostRigBinDir(root) {
|
|
5987
|
-
return resolve24(root, ".rig", "bin");
|
|
5988
|
-
}
|
|
5989
7105
|
function isRuntimeGatewayGitPath(candidate) {
|
|
5990
7106
|
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
5991
7107
|
}
|
|
7108
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
7109
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
7110
|
+
}
|
|
5992
7111
|
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
5993
7112
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
5994
7113
|
if (runtimeWorkspace && existsSync21(resolve24(runtimeWorkspace, ".git"))) {
|
|
@@ -6023,6 +7142,9 @@ function resolveGitBinary(projectRoot) {
|
|
|
6023
7142
|
}
|
|
6024
7143
|
return "git";
|
|
6025
7144
|
}
|
|
7145
|
+
function escapeRegExp2(value) {
|
|
7146
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7147
|
+
}
|
|
6026
7148
|
function safeCurrentTaskId(projectRoot) {
|
|
6027
7149
|
try {
|
|
6028
7150
|
const taskId = currentTaskId(projectRoot);
|
|
@@ -6181,17 +7303,15 @@ function gitOpenPr(options) {
|
|
|
6181
7303
|
const target = options.target || (taskId ? "monorepo" : "project");
|
|
6182
7304
|
let repoRoot = options.projectRoot;
|
|
6183
7305
|
let repoLabel = "project-rig";
|
|
6184
|
-
|
|
7306
|
+
const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
|
|
6185
7307
|
if (target === "monorepo") {
|
|
6186
7308
|
repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot2(options.projectRoot);
|
|
6187
7309
|
repoLabel = "monorepo";
|
|
6188
|
-
defaultBase = process.env.RIG_PR_BASE_MONOREPO || "main";
|
|
6189
7310
|
if (taskId) {
|
|
6190
7311
|
gitSyncBranch(options.projectRoot, taskId, "monorepo");
|
|
6191
7312
|
}
|
|
6192
7313
|
} else if (taskId) {
|
|
6193
7314
|
gitSyncBranch(options.projectRoot, taskId, "project");
|
|
6194
|
-
defaultBase = inferProjectBase(options.projectRoot, defaultBase);
|
|
6195
7315
|
}
|
|
6196
7316
|
if (!existsSync21(resolve24(repoRoot, ".git"))) {
|
|
6197
7317
|
throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
|
|
@@ -6200,9 +7320,9 @@ function gitOpenPr(options) {
|
|
|
6200
7320
|
if (!branch || branch === "HEAD") {
|
|
6201
7321
|
throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
|
|
6202
7322
|
}
|
|
6203
|
-
const base = options.base || defaultBase;
|
|
6204
7323
|
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
6205
7324
|
const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
|
|
7325
|
+
const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
|
|
6206
7326
|
refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
|
|
6207
7327
|
let reviewer = (options.reviewer || "").trim();
|
|
6208
7328
|
let reviewerSource = reviewer ? "flag" : undefined;
|
|
@@ -6238,10 +7358,11 @@ function gitOpenPr(options) {
|
|
|
6238
7358
|
"",
|
|
6239
7359
|
"## Task",
|
|
6240
7360
|
`- beads: ${taskId || "n/a"}`,
|
|
7361
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
6241
7362
|
"",
|
|
6242
7363
|
"## Review",
|
|
6243
7364
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
6244
|
-
"- When repository policy allows it, Rig
|
|
7365
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
6245
7366
|
].join(`
|
|
6246
7367
|
`);
|
|
6247
7368
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -6329,6 +7450,30 @@ function gitOpenPr(options) {
|
|
|
6329
7450
|
}
|
|
6330
7451
|
return result;
|
|
6331
7452
|
}
|
|
7453
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
7454
|
+
const lines = [];
|
|
7455
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
7456
|
+
if (runId) {
|
|
7457
|
+
lines.push(`- Run: ${runId}`);
|
|
7458
|
+
}
|
|
7459
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
7460
|
+
if (closeout) {
|
|
7461
|
+
lines.push(`- ${closeout}`);
|
|
7462
|
+
}
|
|
7463
|
+
return lines;
|
|
7464
|
+
}
|
|
7465
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
7466
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
7467
|
+
if (sourceIssueId) {
|
|
7468
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
7469
|
+
if (match?.[1] && match[2]) {
|
|
7470
|
+
const sourceRepo = match[1];
|
|
7471
|
+
const issueNumber = match[2];
|
|
7472
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
7473
|
+
}
|
|
7474
|
+
}
|
|
7475
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
7476
|
+
}
|
|
6332
7477
|
function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
|
|
6333
7478
|
const view = runCapture2(withGhRepo([
|
|
6334
7479
|
gh,
|
|
@@ -6479,32 +7624,19 @@ function resolveGithubCliBinary(projectRoot) {
|
|
|
6479
7624
|
if (explicit) {
|
|
6480
7625
|
candidates.add(explicit);
|
|
6481
7626
|
}
|
|
7627
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
7628
|
+
candidates.add(candidate);
|
|
7629
|
+
}
|
|
6482
7630
|
const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
|
|
6483
7631
|
for (const entry of explicitPathEntries) {
|
|
6484
7632
|
candidates.add(resolve24(entry, "gh"));
|
|
6485
7633
|
}
|
|
6486
|
-
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
|
|
6487
|
-
if (hostProjectRoot) {
|
|
6488
|
-
candidates.add(resolve24(resolveHostRigBinDir(hostProjectRoot), "gh"));
|
|
6489
|
-
}
|
|
6490
|
-
candidates.add(resolve24(resolveHostRigBinDir(projectRoot), "gh"));
|
|
6491
|
-
const runtimeContext = loadRuntimeContextFromEnv();
|
|
6492
|
-
if (runtimeContext?.binDir) {
|
|
6493
|
-
candidates.add(resolve24(runtimeContext.binDir, "gh"));
|
|
6494
|
-
}
|
|
6495
|
-
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
6496
|
-
if (runtimeHome) {
|
|
6497
|
-
candidates.add(resolve24(runtimeHome, "bin", "gh"));
|
|
6498
|
-
}
|
|
6499
|
-
for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
|
|
6500
|
-
candidates.add(candidate);
|
|
6501
|
-
}
|
|
6502
7634
|
const bunResolved = Bun.which("gh");
|
|
6503
7635
|
if (bunResolved) {
|
|
6504
7636
|
candidates.add(bunResolved);
|
|
6505
7637
|
}
|
|
6506
7638
|
for (const candidate of candidates) {
|
|
6507
|
-
if (candidate && existsSync21(candidate)) {
|
|
7639
|
+
if (candidate && existsSync21(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
6508
7640
|
return candidate;
|
|
6509
7641
|
}
|
|
6510
7642
|
}
|
|
@@ -6653,6 +7785,32 @@ function withGhRepo(command, repoNameWithOwner) {
|
|
|
6653
7785
|
}
|
|
6654
7786
|
return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
|
|
6655
7787
|
}
|
|
7788
|
+
function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
|
|
7789
|
+
const remote = remoteName || "origin";
|
|
7790
|
+
const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
|
|
7791
|
+
if (symbolic.exitCode === 0) {
|
|
7792
|
+
const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp2(remote)}/`), "");
|
|
7793
|
+
if (ref && ref !== "HEAD") {
|
|
7794
|
+
return ref;
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
const lsRemote = runCapture2(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
|
|
7798
|
+
if (lsRemote.exitCode === 0) {
|
|
7799
|
+
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
|
|
7800
|
+
if (match?.[1]) {
|
|
7801
|
+
return match[1];
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
const gh = resolveGithubCliBinary(projectRoot);
|
|
7805
|
+
if (gh && repoNameWithOwner) {
|
|
7806
|
+
const api = runCapture2(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
|
|
7807
|
+
const branch = api.exitCode === 0 ? api.stdout.trim() : "";
|
|
7808
|
+
if (branch) {
|
|
7809
|
+
return branch;
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
return fallback;
|
|
7813
|
+
}
|
|
6656
7814
|
function inferProjectBase(projectRoot, fallback) {
|
|
6657
7815
|
const containing = runCapture2(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
|
|
6658
7816
|
if (containing.exitCode !== 0) {
|
|
@@ -7034,6 +8192,10 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7034
8192
|
}
|
|
7035
8193
|
env[key] = value;
|
|
7036
8194
|
}
|
|
8195
|
+
const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
8196
|
+
if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
8197
|
+
env.GITHUB_TOKEN = rigGithubToken;
|
|
8198
|
+
}
|
|
7037
8199
|
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
7038
8200
|
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
7039
8201
|
}
|
|
@@ -7057,6 +8219,13 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7057
8219
|
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
7058
8220
|
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
7059
8221
|
}
|
|
8222
|
+
const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
|
|
8223
|
+
if (gitHubToken) {
|
|
8224
|
+
env.RIG_GITHUB_TOKEN = gitHubToken;
|
|
8225
|
+
env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
|
|
8226
|
+
env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
|
|
8227
|
+
applyGitHubCredentialHelperEnv(env);
|
|
8228
|
+
}
|
|
7060
8229
|
if (runtimeKnownHosts && existsSync21(runtimeKnownHosts)) {
|
|
7061
8230
|
const sshParts = [
|
|
7062
8231
|
"ssh",
|
|
@@ -7073,6 +8242,14 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7073
8242
|
}
|
|
7074
8243
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
7075
8244
|
}
|
|
8245
|
+
function applyGitHubCredentialHelperEnv(env) {
|
|
8246
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
8247
|
+
env.GIT_CONFIG_COUNT = "2";
|
|
8248
|
+
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
8249
|
+
env.GIT_CONFIG_VALUE_0 = "";
|
|
8250
|
+
env.GIT_CONFIG_KEY_1 = "credential.helper";
|
|
8251
|
+
env.GIT_CONFIG_VALUE_1 = '!f() { test "$1" = get || exit 0; token="${GITHUB_TOKEN:-${GH_TOKEN:-${RIG_GITHUB_TOKEN:-}}}"; test -n "$token" || exit 0; echo username=x-access-token; echo password="$token"; }; f';
|
|
8252
|
+
}
|
|
7076
8253
|
function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
7077
8254
|
if (!runtimeRoot) {
|
|
7078
8255
|
return {};
|