@h-rig/runtime 0.0.6-alpha.2 → 0.0.6-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig-agent-dispatch.js +84 -313
- package/dist/bin/rig-agent.js +85 -27
- package/dist/src/control-plane/agent-wrapper.js +101 -27
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +1357 -180
- package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
- package/dist/src/control-plane/hooks/inject-context.js +2 -2
- package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
- package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
- package/dist/src/control-plane/native/git-ops.js +134 -68
- package/dist/src/control-plane/native/harness-cli.js +1357 -180
- package/dist/src/control-plane/native/pr-automation.js +1532 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
- package/dist/src/control-plane/native/run-ops.js +35 -12
- package/dist/src/control-plane/native/task-ops.js +1274 -155
- package/dist/src/control-plane/native/validator.js +2 -2
- package/dist/src/control-plane/native/verifier.js +1274 -154
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/runtime/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/home.js +31 -6
- package/dist/src/control-plane/runtime/isolation/index.js +38 -9
- package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +38 -9
- package/dist/src/control-plane/runtime/queue.js +38 -9
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
- package/dist/src/index.js +27 -20
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
- package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
- package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
- package/package.json +6 -6
- package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
- package/native/darwin-arm64/manifest.json +0 -1
- package/native/linux-x64/bin/rig-git +0 -0
- package/native/linux-x64/bin/rig-shell +0 -0
- package/native/linux-x64/bin/rig-tools +0 -0
- package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
- package/native/linux-x64/lib/runtime-native.so +0 -0
- package/native/linux-x64/manifest.json +0 -1
|
@@ -1203,8 +1203,8 @@ function githubStatusFor(issue) {
|
|
|
1203
1203
|
return "open";
|
|
1204
1204
|
}
|
|
1205
1205
|
function selectedGitHubEnv() {
|
|
1206
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
1207
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1206
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1207
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
1208
1208
|
}
|
|
1209
1209
|
function ghSpawnOptions() {
|
|
1210
1210
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -3611,6 +3611,1126 @@ ${JSON.stringify(result, null, 2)}
|
|
|
3611
3611
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3612
3612
|
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3613
3613
|
import { resolve as resolve22 } from "path";
|
|
3614
|
+
|
|
3615
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3616
|
+
function parseJsonObject(value) {
|
|
3617
|
+
if (!value?.trim())
|
|
3618
|
+
return { value: {}, error: "empty JSON output" };
|
|
3619
|
+
try {
|
|
3620
|
+
const parsed = JSON.parse(value);
|
|
3621
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3622
|
+
} catch (error) {
|
|
3623
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
function flattenPaginatedArray(value) {
|
|
3627
|
+
if (!Array.isArray(value))
|
|
3628
|
+
return null;
|
|
3629
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3630
|
+
return value.flatMap((entry) => entry);
|
|
3631
|
+
}
|
|
3632
|
+
return value;
|
|
3633
|
+
}
|
|
3634
|
+
function parseJsonArray(value) {
|
|
3635
|
+
if (!value?.trim())
|
|
3636
|
+
return { value: [], error: "empty JSON output" };
|
|
3637
|
+
try {
|
|
3638
|
+
const parsed = JSON.parse(value);
|
|
3639
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3640
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3641
|
+
} catch (error) {
|
|
3642
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
function parseGithubPrUrl(prUrl) {
|
|
3646
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3647
|
+
if (!match)
|
|
3648
|
+
return null;
|
|
3649
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3650
|
+
if (!Number.isFinite(prNumber))
|
|
3651
|
+
return null;
|
|
3652
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3653
|
+
}
|
|
3654
|
+
function checkName(check) {
|
|
3655
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3656
|
+
}
|
|
3657
|
+
function checkState(check) {
|
|
3658
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3659
|
+
}
|
|
3660
|
+
function isGreptileLabel(value) {
|
|
3661
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3662
|
+
}
|
|
3663
|
+
function isGreptileGithubLogin(value) {
|
|
3664
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3665
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3666
|
+
}
|
|
3667
|
+
function isPassingCheck(check) {
|
|
3668
|
+
const state = checkState(check);
|
|
3669
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3670
|
+
}
|
|
3671
|
+
function isPendingCheck(check) {
|
|
3672
|
+
const state = checkState(check);
|
|
3673
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3674
|
+
}
|
|
3675
|
+
function isFailingCheck(check) {
|
|
3676
|
+
const state = checkState(check);
|
|
3677
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3678
|
+
}
|
|
3679
|
+
function wildcardToRegExp(pattern) {
|
|
3680
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3681
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3682
|
+
}
|
|
3683
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3684
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3685
|
+
}
|
|
3686
|
+
function greptileScorePatterns() {
|
|
3687
|
+
return [
|
|
3688
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3689
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3690
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3691
|
+
];
|
|
3692
|
+
}
|
|
3693
|
+
function parseGreptileScores(input) {
|
|
3694
|
+
const text = stripHtml(input);
|
|
3695
|
+
const seen = new Set;
|
|
3696
|
+
const scores = [];
|
|
3697
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3698
|
+
for (const match of text.matchAll(pattern)) {
|
|
3699
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3700
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3701
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3702
|
+
continue;
|
|
3703
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3704
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3705
|
+
if (seen.has(key))
|
|
3706
|
+
continue;
|
|
3707
|
+
seen.add(key);
|
|
3708
|
+
scores.push({ value, scale, raw });
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return scores;
|
|
3712
|
+
}
|
|
3713
|
+
function parseGreptileScore(input) {
|
|
3714
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3715
|
+
}
|
|
3716
|
+
function stripHtml(input) {
|
|
3717
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3718
|
+
|
|
3719
|
+
`).trim();
|
|
3720
|
+
}
|
|
3721
|
+
function containsBlockerText(input) {
|
|
3722
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3723
|
+
return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(text);
|
|
3724
|
+
}
|
|
3725
|
+
function isStrictFiveOfFive(score) {
|
|
3726
|
+
return score.value === 5 && score.scale === 5;
|
|
3727
|
+
}
|
|
3728
|
+
function containsConflictingScoreText(input) {
|
|
3729
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3730
|
+
}
|
|
3731
|
+
function greptileStatusVerdict(status) {
|
|
3732
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3733
|
+
if (!normalized)
|
|
3734
|
+
return null;
|
|
3735
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3736
|
+
return "approved";
|
|
3737
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3738
|
+
return "rejected";
|
|
3739
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3740
|
+
return "skipped";
|
|
3741
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3742
|
+
return "failed";
|
|
3743
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3744
|
+
return "pending";
|
|
3745
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3746
|
+
return "completed";
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3750
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3751
|
+
}
|
|
3752
|
+
function greptileRequestTimeoutMs(env) {
|
|
3753
|
+
const fallback = 30000;
|
|
3754
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3755
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3756
|
+
}
|
|
3757
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3758
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3759
|
+
return null;
|
|
3760
|
+
const record = entry;
|
|
3761
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3762
|
+
if (!id)
|
|
3763
|
+
return null;
|
|
3764
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3765
|
+
return {
|
|
3766
|
+
id,
|
|
3767
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3768
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3769
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3770
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3774
|
+
const seen = new Set;
|
|
3775
|
+
const unique2 = [];
|
|
3776
|
+
for (const review of reviews) {
|
|
3777
|
+
if (seen.has(review.id))
|
|
3778
|
+
continue;
|
|
3779
|
+
seen.add(review.id);
|
|
3780
|
+
unique2.push(review);
|
|
3781
|
+
}
|
|
3782
|
+
return unique2;
|
|
3783
|
+
}
|
|
3784
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3785
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3786
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3787
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3788
|
+
const latest = sorted.slice(0, 1);
|
|
3789
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3790
|
+
}
|
|
3791
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3792
|
+
const selected = details ?? review;
|
|
3793
|
+
return {
|
|
3794
|
+
id: selected.id || review.id,
|
|
3795
|
+
body: selected.body ?? review.body ?? null,
|
|
3796
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3797
|
+
status: selected.status ?? review.status ?? null
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3801
|
+
const controller = new AbortController;
|
|
3802
|
+
const timeoutId = setTimeout(() => {
|
|
3803
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3804
|
+
}, input.timeoutMs);
|
|
3805
|
+
let response;
|
|
3806
|
+
try {
|
|
3807
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3808
|
+
method: "POST",
|
|
3809
|
+
headers: {
|
|
3810
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3811
|
+
"Content-Type": "application/json"
|
|
3812
|
+
},
|
|
3813
|
+
body: JSON.stringify({
|
|
3814
|
+
jsonrpc: "2.0",
|
|
3815
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3816
|
+
method: "tools/call",
|
|
3817
|
+
params: { name: input.name, arguments: input.args }
|
|
3818
|
+
}),
|
|
3819
|
+
signal: controller.signal
|
|
3820
|
+
});
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
if (controller.signal.aborted) {
|
|
3823
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3824
|
+
}
|
|
3825
|
+
throw error;
|
|
3826
|
+
} finally {
|
|
3827
|
+
clearTimeout(timeoutId);
|
|
3828
|
+
}
|
|
3829
|
+
const raw = await response.text();
|
|
3830
|
+
if (!response.ok) {
|
|
3831
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3832
|
+
}
|
|
3833
|
+
let envelope;
|
|
3834
|
+
try {
|
|
3835
|
+
envelope = JSON.parse(raw);
|
|
3836
|
+
} catch {
|
|
3837
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3838
|
+
}
|
|
3839
|
+
if (envelope.error?.message) {
|
|
3840
|
+
throw new Error(envelope.error.message);
|
|
3841
|
+
}
|
|
3842
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3843
|
+
`).trim();
|
|
3844
|
+
if (!text) {
|
|
3845
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3846
|
+
}
|
|
3847
|
+
return text;
|
|
3848
|
+
}
|
|
3849
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3850
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3851
|
+
try {
|
|
3852
|
+
return JSON.parse(text);
|
|
3853
|
+
} catch {
|
|
3854
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3858
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3859
|
+
return { signals: [], errors: [] };
|
|
3860
|
+
}
|
|
3861
|
+
const env = input.options?.env ?? process.env;
|
|
3862
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3863
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3864
|
+
if (!apiKey) {
|
|
3865
|
+
return { signals: [], errors: [] };
|
|
3866
|
+
}
|
|
3867
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3868
|
+
if (typeof fetchFn !== "function") {
|
|
3869
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3870
|
+
}
|
|
3871
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
3872
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
3873
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
3874
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
3875
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
3876
|
+
try {
|
|
3877
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
3878
|
+
apiBase,
|
|
3879
|
+
apiKey,
|
|
3880
|
+
name: "list_code_reviews",
|
|
3881
|
+
args: {
|
|
3882
|
+
name: repository,
|
|
3883
|
+
remote,
|
|
3884
|
+
defaultBranch,
|
|
3885
|
+
prNumber: input.prNumber,
|
|
3886
|
+
limit: 20
|
|
3887
|
+
},
|
|
3888
|
+
timeoutMs,
|
|
3889
|
+
fetchFn
|
|
3890
|
+
});
|
|
3891
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
3892
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
3893
|
+
const signals = [];
|
|
3894
|
+
for (const review of selectedReviews) {
|
|
3895
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
3896
|
+
apiBase,
|
|
3897
|
+
apiKey,
|
|
3898
|
+
name: "get_code_review",
|
|
3899
|
+
args: { codeReviewId: review.id },
|
|
3900
|
+
timeoutMs,
|
|
3901
|
+
fetchFn
|
|
3902
|
+
});
|
|
3903
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
3904
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
3905
|
+
}
|
|
3906
|
+
return { signals, errors: [] };
|
|
3907
|
+
} catch (error) {
|
|
3908
|
+
return {
|
|
3909
|
+
signals: [],
|
|
3910
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
function firstString(record, keys) {
|
|
3915
|
+
for (const key of keys) {
|
|
3916
|
+
const value = record[key];
|
|
3917
|
+
if (typeof value === "string")
|
|
3918
|
+
return value;
|
|
3919
|
+
}
|
|
3920
|
+
return "";
|
|
3921
|
+
}
|
|
3922
|
+
function arrayField(record, key) {
|
|
3923
|
+
const value = record[key];
|
|
3924
|
+
return Array.isArray(value) ? value : [];
|
|
3925
|
+
}
|
|
3926
|
+
async function runJsonArray(command, args, cwd) {
|
|
3927
|
+
const result = await command(args, { cwd });
|
|
3928
|
+
const label = `gh ${args.join(" ")}`;
|
|
3929
|
+
if (result.exitCode !== 0) {
|
|
3930
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3931
|
+
}
|
|
3932
|
+
const parsed = parseJsonArray(result.stdout);
|
|
3933
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3934
|
+
}
|
|
3935
|
+
async function runJsonObject(command, args, cwd) {
|
|
3936
|
+
const result = await command(args, { cwd });
|
|
3937
|
+
const label = `gh ${args.join(" ")}`;
|
|
3938
|
+
if (result.exitCode !== 0) {
|
|
3939
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
3940
|
+
}
|
|
3941
|
+
const parsed = parseJsonObject(result.stdout);
|
|
3942
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
3943
|
+
}
|
|
3944
|
+
function normalizeStatusCheck(entry) {
|
|
3945
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3946
|
+
return null;
|
|
3947
|
+
const record = entry;
|
|
3948
|
+
const name = firstString(record, ["name", "context"]);
|
|
3949
|
+
if (!name.trim())
|
|
3950
|
+
return null;
|
|
3951
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
3952
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
3953
|
+
return {
|
|
3954
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
3955
|
+
name,
|
|
3956
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
3957
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3958
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3959
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
3960
|
+
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,
|
|
3961
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
3962
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
3963
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
3964
|
+
output: output ? {
|
|
3965
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
3966
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
3967
|
+
text: typeof output.text === "string" ? output.text : null
|
|
3968
|
+
} : null,
|
|
3969
|
+
app: app ? {
|
|
3970
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
3971
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
3972
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
3973
|
+
} : null
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
function normalizeReview(entry) {
|
|
3977
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3978
|
+
return null;
|
|
3979
|
+
const record = entry;
|
|
3980
|
+
return {
|
|
3981
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
3982
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
3983
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3984
|
+
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,
|
|
3985
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
3986
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3989
|
+
function normalizeReviewComment(entry) {
|
|
3990
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3991
|
+
return null;
|
|
3992
|
+
const record = entry;
|
|
3993
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
3994
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
3995
|
+
if (!body && !path)
|
|
3996
|
+
return null;
|
|
3997
|
+
return {
|
|
3998
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
3999
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4000
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4001
|
+
body,
|
|
4002
|
+
path,
|
|
4003
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4004
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4005
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4006
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function normalizeIssueComment(entry) {
|
|
4010
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4011
|
+
return null;
|
|
4012
|
+
const record = entry;
|
|
4013
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4014
|
+
if (!body)
|
|
4015
|
+
return null;
|
|
4016
|
+
return {
|
|
4017
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4018
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4019
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4020
|
+
body,
|
|
4021
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4022
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4023
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
function normalizeReviewThread(entry) {
|
|
4027
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4028
|
+
return null;
|
|
4029
|
+
const record = entry;
|
|
4030
|
+
return {
|
|
4031
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4032
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4033
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4034
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
function relevantIssueComment(comment) {
|
|
4038
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4039
|
+
const body = comment.body ?? "";
|
|
4040
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4041
|
+
}
|
|
4042
|
+
function latestThreadComment(thread) {
|
|
4043
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4044
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4045
|
+
}
|
|
4046
|
+
function unresolvedThreadSummaries(threads) {
|
|
4047
|
+
return threads.flatMap((thread) => {
|
|
4048
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4049
|
+
return [];
|
|
4050
|
+
const latest = latestThreadComment(thread);
|
|
4051
|
+
if (!latest)
|
|
4052
|
+
return ["Unresolved review thread"];
|
|
4053
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4054
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
function collectBodies(evidence) {
|
|
4058
|
+
return [
|
|
4059
|
+
evidence.title ?? "",
|
|
4060
|
+
evidence.body,
|
|
4061
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4062
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4063
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4064
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4065
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4066
|
+
].filter((body) => body.trim().length > 0);
|
|
4067
|
+
}
|
|
4068
|
+
function bodyExcerpt(body) {
|
|
4069
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4070
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4071
|
+
}
|
|
4072
|
+
function makeGreptileSignal(input) {
|
|
4073
|
+
const scores = parseGreptileScores(input.body);
|
|
4074
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4075
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4076
|
+
const verdict = input.verdict ?? null;
|
|
4077
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4078
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4079
|
+
return {
|
|
4080
|
+
source: input.source,
|
|
4081
|
+
trusted: input.trusted,
|
|
4082
|
+
authorLogin: input.authorLogin ?? null,
|
|
4083
|
+
reviewedSha,
|
|
4084
|
+
current,
|
|
4085
|
+
stale: current === false,
|
|
4086
|
+
score: scores[0] ?? null,
|
|
4087
|
+
scores,
|
|
4088
|
+
explicitApproval,
|
|
4089
|
+
verdict,
|
|
4090
|
+
blocker,
|
|
4091
|
+
actionable: input.actionable ?? blocker,
|
|
4092
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4093
|
+
body: input.body,
|
|
4094
|
+
allScores: scores
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
function reviewAuthorLogin(review) {
|
|
4098
|
+
return review.author?.login ?? null;
|
|
4099
|
+
}
|
|
4100
|
+
function commentAuthorLogin(comment) {
|
|
4101
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4102
|
+
}
|
|
4103
|
+
function collectGreptileSignals(evidence) {
|
|
4104
|
+
const signals = [];
|
|
4105
|
+
const contextSources = [
|
|
4106
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4107
|
+
{ source: "pr-body", body: evidence.body }
|
|
4108
|
+
];
|
|
4109
|
+
for (const context of contextSources) {
|
|
4110
|
+
if (!context.body.trim())
|
|
4111
|
+
continue;
|
|
4112
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4113
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4114
|
+
continue;
|
|
4115
|
+
signals.push(makeGreptileSignal({
|
|
4116
|
+
source: context.source,
|
|
4117
|
+
body: context.body,
|
|
4118
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4119
|
+
trusted: false,
|
|
4120
|
+
blocker: contextBlocker,
|
|
4121
|
+
actionable: contextBlocker
|
|
4122
|
+
}));
|
|
4123
|
+
}
|
|
4124
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4125
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4126
|
+
|
|
4127
|
+
`) || "Status: UNKNOWN";
|
|
4128
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4129
|
+
signals.push(makeGreptileSignal({
|
|
4130
|
+
source: "api",
|
|
4131
|
+
body,
|
|
4132
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4133
|
+
trusted: true,
|
|
4134
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4135
|
+
explicitApproval: verdict === "approved",
|
|
4136
|
+
verdict
|
|
4137
|
+
}));
|
|
4138
|
+
}
|
|
4139
|
+
for (const review of evidence.reviews) {
|
|
4140
|
+
const login = reviewAuthorLogin(review);
|
|
4141
|
+
if (!isGreptileGithubLogin(login))
|
|
4142
|
+
continue;
|
|
4143
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4144
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4145
|
+
|
|
4146
|
+
`);
|
|
4147
|
+
if (!body.trim())
|
|
4148
|
+
continue;
|
|
4149
|
+
const dismissed = state === "DISMISSED";
|
|
4150
|
+
signals.push(makeGreptileSignal({
|
|
4151
|
+
source: "github-review",
|
|
4152
|
+
body,
|
|
4153
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4154
|
+
trusted: !dismissed,
|
|
4155
|
+
authorLogin: login,
|
|
4156
|
+
reviewedSha: review.commit_id ?? null,
|
|
4157
|
+
explicitApproval: undefined,
|
|
4158
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4159
|
+
}));
|
|
4160
|
+
}
|
|
4161
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4162
|
+
const login = commentAuthorLogin(comment);
|
|
4163
|
+
const body = comment.body ?? "";
|
|
4164
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4165
|
+
continue;
|
|
4166
|
+
signals.push(makeGreptileSignal({
|
|
4167
|
+
source: "issue-comment",
|
|
4168
|
+
body,
|
|
4169
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4170
|
+
trusted: true,
|
|
4171
|
+
authorLogin: login
|
|
4172
|
+
}));
|
|
4173
|
+
}
|
|
4174
|
+
for (const thread of evidence.reviewThreads) {
|
|
4175
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4176
|
+
continue;
|
|
4177
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4178
|
+
const login = comment.author?.login ?? null;
|
|
4179
|
+
const body = comment.body ?? "";
|
|
4180
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4181
|
+
continue;
|
|
4182
|
+
signals.push(makeGreptileSignal({
|
|
4183
|
+
source: "review-thread",
|
|
4184
|
+
body,
|
|
4185
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4186
|
+
trusted: true,
|
|
4187
|
+
authorLogin: login
|
|
4188
|
+
}));
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
for (const check of evidence.checks) {
|
|
4192
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4193
|
+
continue;
|
|
4194
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4195
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4196
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4197
|
+
|
|
4198
|
+
`);
|
|
4199
|
+
signals.push(makeGreptileSignal({
|
|
4200
|
+
source: "github-check",
|
|
4201
|
+
body,
|
|
4202
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4203
|
+
trusted: false,
|
|
4204
|
+
reviewedSha,
|
|
4205
|
+
explicitApproval: false,
|
|
4206
|
+
blocker: isFailingCheck(check),
|
|
4207
|
+
actionable: isFailingCheck(check)
|
|
4208
|
+
}));
|
|
4209
|
+
}
|
|
4210
|
+
return signals;
|
|
4211
|
+
}
|
|
4212
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4213
|
+
return threads.flatMap((thread) => {
|
|
4214
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4215
|
+
return [];
|
|
4216
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4217
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4218
|
+
return [];
|
|
4219
|
+
const latest = latestThreadComment(thread);
|
|
4220
|
+
if (!latest)
|
|
4221
|
+
return ["Unresolved Greptile review thread"];
|
|
4222
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4223
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4227
|
+
return [];
|
|
4228
|
+
}
|
|
4229
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4230
|
+
return comments.flatMap((comment) => {
|
|
4231
|
+
const body = comment.body?.trim() ?? "";
|
|
4232
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4233
|
+
return [];
|
|
4234
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4235
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4236
|
+
return [`${author}: ${body}`];
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4240
|
+
return reviews.flatMap((review) => {
|
|
4241
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4242
|
+
if (isGreptileGithubLogin(login))
|
|
4243
|
+
return [];
|
|
4244
|
+
const body = review.body?.trim() ?? "";
|
|
4245
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4246
|
+
return [];
|
|
4247
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4248
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
function signalLabel(signal) {
|
|
4252
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4253
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4254
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4255
|
+
return `${source}${author}${sha}`;
|
|
4256
|
+
}
|
|
4257
|
+
function deriveGreptileEvidence(input) {
|
|
4258
|
+
const rawBodies = collectBodies(input);
|
|
4259
|
+
const signals = collectGreptileSignals(input);
|
|
4260
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4261
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4262
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4263
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4264
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4265
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4266
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4267
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4268
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4269
|
+
const signalCanApproveByScore = (signal) => {
|
|
4270
|
+
if (signal.source === "api")
|
|
4271
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4272
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4273
|
+
};
|
|
4274
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4275
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4276
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4277
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4278
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4279
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4280
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4281
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4282
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4283
|
+
const staleBlockingSignals = [];
|
|
4284
|
+
const blockers = [
|
|
4285
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4286
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4287
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4288
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4289
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4290
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4291
|
+
];
|
|
4292
|
+
const unresolvedComments = [
|
|
4293
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4294
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4295
|
+
];
|
|
4296
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4297
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4298
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4299
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4300
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4301
|
+
});
|
|
4302
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4303
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4304
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4305
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4306
|
+
});
|
|
4307
|
+
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"));
|
|
4308
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4309
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4310
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4311
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4312
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4313
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4314
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4315
|
+
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";
|
|
4316
|
+
return {
|
|
4317
|
+
source,
|
|
4318
|
+
currentHeadSha: input.currentHeadSha,
|
|
4319
|
+
reviewedSha,
|
|
4320
|
+
fresh,
|
|
4321
|
+
completed,
|
|
4322
|
+
approved,
|
|
4323
|
+
score,
|
|
4324
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4325
|
+
blockers,
|
|
4326
|
+
unresolvedComments,
|
|
4327
|
+
rawBodies,
|
|
4328
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4329
|
+
mapping
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
function isGreptileCheckDetail(check) {
|
|
4333
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4334
|
+
}
|
|
4335
|
+
async function collectGreptileCheckDetails(input) {
|
|
4336
|
+
const checkRunsRead = await runJsonArray(input.command, [
|
|
4337
|
+
"api",
|
|
4338
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4339
|
+
"--paginate",
|
|
4340
|
+
"--slurp",
|
|
4341
|
+
"--jq",
|
|
4342
|
+
"map(.check_runs // []) | add // []"
|
|
4343
|
+
], input.projectRoot);
|
|
4344
|
+
const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4345
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4346
|
+
}
|
|
4347
|
+
async function collectReviewThreads(input) {
|
|
4348
|
+
const reviewThreads = [];
|
|
4349
|
+
let afterCursor = null;
|
|
4350
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4351
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4352
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4353
|
+
"api",
|
|
4354
|
+
"graphql",
|
|
4355
|
+
"-F",
|
|
4356
|
+
`owner=${input.owner}`,
|
|
4357
|
+
"-F",
|
|
4358
|
+
`name=${input.name}`,
|
|
4359
|
+
"-F",
|
|
4360
|
+
`prNumber=${input.prNumber}`,
|
|
4361
|
+
"-f",
|
|
4362
|
+
`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 } } } } }`
|
|
4363
|
+
], input.projectRoot);
|
|
4364
|
+
if (threadsResponse.error) {
|
|
4365
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4366
|
+
}
|
|
4367
|
+
const data = threadsResponse.value.data;
|
|
4368
|
+
const repository = data?.repository;
|
|
4369
|
+
const pullRequest = repository?.pullRequest;
|
|
4370
|
+
const threads = pullRequest?.reviewThreads;
|
|
4371
|
+
const nodes = threads?.nodes;
|
|
4372
|
+
if (!Array.isArray(nodes)) {
|
|
4373
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4374
|
+
}
|
|
4375
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4376
|
+
reviewThreads.push(...normalized);
|
|
4377
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4378
|
+
if (truncatedCommentThread) {
|
|
4379
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4380
|
+
}
|
|
4381
|
+
const pageInfo = threads?.pageInfo;
|
|
4382
|
+
if (!pageInfo) {
|
|
4383
|
+
if (nodes.length >= 100) {
|
|
4384
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4385
|
+
}
|
|
4386
|
+
return { value: reviewThreads };
|
|
4387
|
+
}
|
|
4388
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4389
|
+
return { value: reviewThreads };
|
|
4390
|
+
}
|
|
4391
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4392
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4393
|
+
}
|
|
4394
|
+
afterCursor = pageInfo.endCursor;
|
|
4395
|
+
}
|
|
4396
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4397
|
+
}
|
|
4398
|
+
async function collectPrReviewEvidence(input) {
|
|
4399
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4400
|
+
if (!parsed) {
|
|
4401
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4402
|
+
}
|
|
4403
|
+
const readErrors = [];
|
|
4404
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4405
|
+
"pr",
|
|
4406
|
+
"view",
|
|
4407
|
+
input.prUrl,
|
|
4408
|
+
"--json",
|
|
4409
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4410
|
+
], input.projectRoot);
|
|
4411
|
+
if (viewRead.error)
|
|
4412
|
+
readErrors.push(viewRead.error);
|
|
4413
|
+
const view = viewRead.value;
|
|
4414
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4415
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4416
|
+
}
|
|
4417
|
+
if (!Array.isArray(view.reviews)) {
|
|
4418
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4419
|
+
}
|
|
4420
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4421
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4422
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4423
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4424
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4425
|
+
if (reviewCommentsRead.error)
|
|
4426
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4427
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4428
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
4429
|
+
if (issueCommentsRead.error)
|
|
4430
|
+
readErrors.push(issueCommentsRead.error);
|
|
4431
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4432
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4433
|
+
command: input.command,
|
|
4434
|
+
projectRoot: input.projectRoot,
|
|
4435
|
+
owner: parsed.owner,
|
|
4436
|
+
name: parsed.repo,
|
|
4437
|
+
prNumber: parsed.prNumber
|
|
4438
|
+
});
|
|
4439
|
+
if (reviewThreadsRead.error)
|
|
4440
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4441
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4442
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4443
|
+
let greptileCheckDetails = [];
|
|
4444
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4445
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4446
|
+
command: input.command,
|
|
4447
|
+
projectRoot: input.projectRoot,
|
|
4448
|
+
repoName: parsed.repoName,
|
|
4449
|
+
headSha
|
|
4450
|
+
});
|
|
4451
|
+
if (checkDetailsRead.error)
|
|
4452
|
+
readErrors.push(checkDetailsRead.error);
|
|
4453
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4454
|
+
if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
|
|
4455
|
+
readErrors.push("Greptile check details could not be found for the current PR head");
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4459
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4460
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4461
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4462
|
+
options: input.greptileApi,
|
|
4463
|
+
repoName: parsed.repoName,
|
|
4464
|
+
prNumber: parsed.prNumber,
|
|
4465
|
+
headSha,
|
|
4466
|
+
baseRefName
|
|
4467
|
+
});
|
|
4468
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4469
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4470
|
+
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})` : ""}`);
|
|
4471
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4472
|
+
const evidenceBase = {
|
|
4473
|
+
title: firstString(view, ["title"]),
|
|
4474
|
+
body: firstString(view, ["body"]),
|
|
4475
|
+
reviews,
|
|
4476
|
+
changedFileReviewComments: reviewComments,
|
|
4477
|
+
relevantIssueComments: issueComments,
|
|
4478
|
+
reviewThreads,
|
|
4479
|
+
checks: checksWithGreptileDetails,
|
|
4480
|
+
currentHeadSha: headSha,
|
|
4481
|
+
apiSignals
|
|
4482
|
+
};
|
|
4483
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4484
|
+
return {
|
|
4485
|
+
prUrl: input.prUrl,
|
|
4486
|
+
prNumber: parsed.prNumber,
|
|
4487
|
+
repoName: parsed.repoName,
|
|
4488
|
+
title: evidenceBase.title,
|
|
4489
|
+
body: evidenceBase.body,
|
|
4490
|
+
headSha,
|
|
4491
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4492
|
+
baseRefName,
|
|
4493
|
+
state: firstString(view, ["state"]),
|
|
4494
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4495
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4496
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4497
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4498
|
+
reviews,
|
|
4499
|
+
reviewThreads,
|
|
4500
|
+
changedFileReviewComments: reviewComments,
|
|
4501
|
+
relevantIssueComments: issueComments,
|
|
4502
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4503
|
+
checkFailures,
|
|
4504
|
+
pendingChecks,
|
|
4505
|
+
readErrors,
|
|
4506
|
+
greptile
|
|
4507
|
+
};
|
|
4508
|
+
}
|
|
4509
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4510
|
+
const normalized = value.trim();
|
|
4511
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4512
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4513
|
+
}
|
|
4514
|
+
function evaluateEvidence(evidence) {
|
|
4515
|
+
const reasonDetails = [];
|
|
4516
|
+
const warnings = [];
|
|
4517
|
+
const seen = new Set;
|
|
4518
|
+
const addReason = (reason) => {
|
|
4519
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4520
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4521
|
+
if (seen.has(key))
|
|
4522
|
+
return;
|
|
4523
|
+
seen.add(key);
|
|
4524
|
+
reasonDetails.push(capped);
|
|
4525
|
+
};
|
|
4526
|
+
const greptile = evidence.greptile;
|
|
4527
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4528
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4529
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4530
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4531
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4532
|
+
for (const error of evidence.readErrors) {
|
|
4533
|
+
addReason({
|
|
4534
|
+
code: "read_error",
|
|
4535
|
+
reasonClass: "reject",
|
|
4536
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4537
|
+
suggestedAction: "needs_attention",
|
|
4538
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4539
|
+
headSha: evidence.headSha || null
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
if (!evidence.headSha) {
|
|
4543
|
+
addReason({
|
|
4544
|
+
code: "missing_head_sha",
|
|
4545
|
+
reasonClass: "reject",
|
|
4546
|
+
surface: "github",
|
|
4547
|
+
suggestedAction: "needs_attention",
|
|
4548
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4549
|
+
headSha: null
|
|
4550
|
+
});
|
|
4551
|
+
}
|
|
4552
|
+
for (const failure of evidence.checkFailures) {
|
|
4553
|
+
addReason({
|
|
4554
|
+
code: "ci_failed",
|
|
4555
|
+
reasonClass: "reject",
|
|
4556
|
+
surface: "ci",
|
|
4557
|
+
suggestedAction: "fix",
|
|
4558
|
+
message: failure,
|
|
4559
|
+
headSha: evidence.headSha || null
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4563
|
+
addReason({
|
|
4564
|
+
code: "check_pending",
|
|
4565
|
+
reasonClass: "pending",
|
|
4566
|
+
surface: "ci",
|
|
4567
|
+
suggestedAction: "wait",
|
|
4568
|
+
message: pendingCheck,
|
|
4569
|
+
headSha: evidence.headSha || null
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4573
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4574
|
+
addReason({
|
|
4575
|
+
code: "review_decision_blocking",
|
|
4576
|
+
reasonClass: "reject",
|
|
4577
|
+
surface: "review",
|
|
4578
|
+
suggestedAction: "fix",
|
|
4579
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4580
|
+
headSha: evidence.headSha || null
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4584
|
+
addReason({
|
|
4585
|
+
code: "review_thread_unresolved",
|
|
4586
|
+
reasonClass: "reject",
|
|
4587
|
+
surface: "review",
|
|
4588
|
+
suggestedAction: "fix",
|
|
4589
|
+
message: thread,
|
|
4590
|
+
headSha: evidence.headSha || null
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
if (greptile.mapping === "missing") {
|
|
4594
|
+
addReason({
|
|
4595
|
+
code: "greptile_missing",
|
|
4596
|
+
reasonClass: "pending",
|
|
4597
|
+
surface: "greptile",
|
|
4598
|
+
suggestedAction: "wait",
|
|
4599
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4600
|
+
headSha: evidence.headSha || null
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4604
|
+
addReason({
|
|
4605
|
+
code: "greptile_stale",
|
|
4606
|
+
reasonClass: "pending",
|
|
4607
|
+
surface: "greptile",
|
|
4608
|
+
suggestedAction: "wait",
|
|
4609
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4610
|
+
headSha: evidence.headSha || null,
|
|
4611
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4615
|
+
addReason({
|
|
4616
|
+
code: "greptile_pending",
|
|
4617
|
+
reasonClass: "pending",
|
|
4618
|
+
surface: "greptile",
|
|
4619
|
+
suggestedAction: "wait",
|
|
4620
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4621
|
+
headSha: evidence.headSha || null,
|
|
4622
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4626
|
+
addReason({
|
|
4627
|
+
code: "greptile_api_status_unknown",
|
|
4628
|
+
reasonClass: "reject",
|
|
4629
|
+
surface: "greptile",
|
|
4630
|
+
suggestedAction: "needs_attention",
|
|
4631
|
+
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}` : "."}`,
|
|
4632
|
+
headSha: evidence.headSha || null,
|
|
4633
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
if (!greptile.completed) {
|
|
4637
|
+
addReason({
|
|
4638
|
+
code: "greptile_pending",
|
|
4639
|
+
reasonClass: "pending",
|
|
4640
|
+
surface: "greptile",
|
|
4641
|
+
suggestedAction: "wait",
|
|
4642
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4643
|
+
headSha: evidence.headSha || null,
|
|
4644
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
if (!greptile.fresh) {
|
|
4648
|
+
addReason({
|
|
4649
|
+
code: "greptile_not_current_head",
|
|
4650
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4651
|
+
surface: "greptile",
|
|
4652
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4653
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4654
|
+
headSha: evidence.headSha || null,
|
|
4655
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4659
|
+
addReason({
|
|
4660
|
+
code: "greptile_score_not_5",
|
|
4661
|
+
reasonClass: "reject",
|
|
4662
|
+
surface: "greptile",
|
|
4663
|
+
suggestedAction: "fix",
|
|
4664
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4665
|
+
headSha: evidence.headSha || null,
|
|
4666
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4670
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4671
|
+
addReason({
|
|
4672
|
+
code: "greptile_score_missing",
|
|
4673
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4674
|
+
surface: "greptile",
|
|
4675
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4676
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4677
|
+
headSha: evidence.headSha || null,
|
|
4678
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
if (greptile.mapping === "unproven") {
|
|
4682
|
+
addReason({
|
|
4683
|
+
code: "greptile_mapping_unproven",
|
|
4684
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4685
|
+
surface: "greptile",
|
|
4686
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4687
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4688
|
+
headSha: evidence.headSha || null,
|
|
4689
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4692
|
+
for (const blocker of greptile.blockers) {
|
|
4693
|
+
addReason({
|
|
4694
|
+
code: "greptile_blocker_text",
|
|
4695
|
+
reasonClass: "reject",
|
|
4696
|
+
surface: "greptile",
|
|
4697
|
+
suggestedAction: "fix",
|
|
4698
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4699
|
+
headSha: evidence.headSha || null,
|
|
4700
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4704
|
+
addReason({
|
|
4705
|
+
code: "greptile_unresolved_comment",
|
|
4706
|
+
reasonClass: "reject",
|
|
4707
|
+
surface: "greptile",
|
|
4708
|
+
suggestedAction: "fix",
|
|
4709
|
+
message: comment,
|
|
4710
|
+
headSha: evidence.headSha || null,
|
|
4711
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
if (!greptile.approved)
|
|
4715
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4716
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4717
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4718
|
+
}
|
|
4719
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4720
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4721
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4722
|
+
return {
|
|
4723
|
+
approved,
|
|
4724
|
+
pending: evaluated.pending,
|
|
4725
|
+
reasons: evaluated.reasons,
|
|
4726
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4727
|
+
warnings: evaluated.warnings,
|
|
4728
|
+
actionableFeedback: evaluated.reasons,
|
|
4729
|
+
evidence
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3614
4734
|
async function verifyTask(options) {
|
|
3615
4735
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
3616
4736
|
const taskId = options.taskId;
|
|
@@ -4406,7 +5526,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4406
5526
|
}
|
|
4407
5527
|
};
|
|
4408
5528
|
}
|
|
4409
|
-
|
|
5529
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5530
|
+
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)) {
|
|
4410
5531
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4411
5532
|
return {
|
|
4412
5533
|
verdict: "REJECT",
|
|
@@ -4422,44 +5543,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
4422
5543
|
}
|
|
4423
5544
|
};
|
|
4424
5545
|
}
|
|
4425
|
-
if (score) {
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
5546
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5547
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5548
|
+
return {
|
|
5549
|
+
verdict: "REJECT",
|
|
5550
|
+
feedback,
|
|
5551
|
+
reasons,
|
|
5552
|
+
warnings,
|
|
5553
|
+
rawPayload: {
|
|
5554
|
+
pr: options.prState,
|
|
5555
|
+
codeReviews: reviewsPayload,
|
|
5556
|
+
selectedReview,
|
|
5557
|
+
reviewDetails,
|
|
5558
|
+
comments: commentsPayload,
|
|
5559
|
+
score
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5564
|
+
let strictGate = null;
|
|
5565
|
+
try {
|
|
5566
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5567
|
+
projectRoot: options.projectRoot,
|
|
5568
|
+
taskId: options.taskId,
|
|
5569
|
+
prUrl,
|
|
5570
|
+
apiSignals: [{
|
|
5571
|
+
id: selectedReview.id,
|
|
5572
|
+
body: reviewBody,
|
|
5573
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5574
|
+
status: selectedReview.status
|
|
5575
|
+
}]
|
|
5576
|
+
});
|
|
5577
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5578
|
+
} catch (error) {
|
|
5579
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5580
|
+
return {
|
|
5581
|
+
verdict: "REJECT",
|
|
5582
|
+
feedback,
|
|
5583
|
+
reasons,
|
|
5584
|
+
warnings,
|
|
5585
|
+
rawPayload: {
|
|
5586
|
+
pr: options.prState,
|
|
5587
|
+
codeReviews: reviewsPayload,
|
|
5588
|
+
selectedReview,
|
|
5589
|
+
reviewDetails,
|
|
5590
|
+
comments: commentsPayload,
|
|
5591
|
+
score
|
|
5592
|
+
}
|
|
5593
|
+
};
|
|
5594
|
+
}
|
|
5595
|
+
if (!strictGate.approved) {
|
|
5596
|
+
return {
|
|
5597
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5598
|
+
feedback,
|
|
5599
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5600
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5601
|
+
rawPayload: {
|
|
5602
|
+
pr: options.prState,
|
|
5603
|
+
codeReviews: reviewsPayload,
|
|
5604
|
+
selectedReview,
|
|
5605
|
+
reviewDetails,
|
|
5606
|
+
comments: commentsPayload,
|
|
5607
|
+
score,
|
|
5608
|
+
strictGate: {
|
|
5609
|
+
approved: strictGate.approved,
|
|
5610
|
+
pending: strictGate.pending,
|
|
5611
|
+
reasons: strictGate.reasons,
|
|
5612
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5613
|
+
warnings: strictGate.warnings,
|
|
5614
|
+
greptile: strictGate.evidence.greptile,
|
|
5615
|
+
readErrors: strictGate.evidence.readErrors
|
|
4457
5616
|
}
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4461
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4462
|
-
}
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
4463
5619
|
}
|
|
4464
5620
|
return {
|
|
4465
5621
|
verdict: "APPROVE",
|
|
@@ -4471,7 +5627,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
4471
5627
|
codeReviews: reviewsPayload,
|
|
4472
5628
|
selectedReview,
|
|
4473
5629
|
reviewDetails,
|
|
4474
|
-
comments: commentsPayload
|
|
5630
|
+
comments: commentsPayload,
|
|
5631
|
+
strictGate: {
|
|
5632
|
+
approved: strictGate.approved,
|
|
5633
|
+
pending: strictGate.pending,
|
|
5634
|
+
reasons: strictGate.reasons,
|
|
5635
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5636
|
+
warnings: strictGate.warnings,
|
|
5637
|
+
greptile: strictGate.evidence.greptile,
|
|
5638
|
+
readErrors: strictGate.evidence.readErrors
|
|
5639
|
+
}
|
|
4475
5640
|
}
|
|
4476
5641
|
};
|
|
4477
5642
|
}
|
|
@@ -4495,7 +5660,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4495
5660
|
let threads = [];
|
|
4496
5661
|
let actionableThreads = [];
|
|
4497
5662
|
let checkRollup = [];
|
|
4498
|
-
let
|
|
5663
|
+
let checkState2 = { pending: false, completed: false };
|
|
4499
5664
|
for (let attempt = 0;; attempt += 1) {
|
|
4500
5665
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4501
5666
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4504,15 +5669,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4504
5669
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4505
5670
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4506
5671
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4507
|
-
|
|
4508
|
-
const
|
|
5672
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5673
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4509
5674
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4510
5675
|
attempt,
|
|
4511
5676
|
pollAttempts: options.pollAttempts,
|
|
4512
|
-
checkState,
|
|
5677
|
+
checkState: checkState2,
|
|
4513
5678
|
fallbackReview,
|
|
4514
5679
|
selectedReview,
|
|
4515
|
-
approvedViaReviewedAncestor
|
|
5680
|
+
approvedViaReviewedAncestor
|
|
4516
5681
|
})) {
|
|
4517
5682
|
break;
|
|
4518
5683
|
}
|
|
@@ -4540,7 +5705,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4540
5705
|
].filter(Boolean).join(`
|
|
4541
5706
|
`);
|
|
4542
5707
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4543
|
-
if (
|
|
5708
|
+
if (checkState2.pending) {
|
|
4544
5709
|
return {
|
|
4545
5710
|
verdict: "SKIP",
|
|
4546
5711
|
feedback,
|
|
@@ -4551,34 +5716,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4551
5716
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4552
5717
|
};
|
|
4553
5718
|
}
|
|
4554
|
-
const
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
};
|
|
4565
|
-
}
|
|
4566
|
-
return {
|
|
4567
|
-
verdict: "SKIP",
|
|
4568
|
-
feedback,
|
|
4569
|
-
reasons: [
|
|
4570
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4571
|
-
],
|
|
4572
|
-
warnings,
|
|
4573
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4577
|
-
if (actionableThreads.length > 0) {
|
|
5719
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5720
|
+
let strictGate;
|
|
5721
|
+
try {
|
|
5722
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5723
|
+
projectRoot: options.projectRoot,
|
|
5724
|
+
taskId: options.taskId,
|
|
5725
|
+
prUrl
|
|
5726
|
+
});
|
|
5727
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5728
|
+
} catch (error) {
|
|
4578
5729
|
return {
|
|
4579
5730
|
verdict: "REJECT",
|
|
4580
5731
|
feedback,
|
|
4581
|
-
reasons:
|
|
5732
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4582
5733
|
warnings,
|
|
4583
5734
|
rawPayload: {
|
|
4584
5735
|
pr: options.prState,
|
|
@@ -4591,44 +5742,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4591
5742
|
}
|
|
4592
5743
|
};
|
|
4593
5744
|
}
|
|
4594
|
-
if (!
|
|
4595
|
-
if (approvedViaCompletedCheck) {
|
|
4596
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
|
|
4597
|
-
return {
|
|
4598
|
-
verdict: "APPROVE",
|
|
4599
|
-
feedback,
|
|
4600
|
-
reasons: [],
|
|
4601
|
-
warnings,
|
|
4602
|
-
rawPayload: {
|
|
4603
|
-
pr: options.prState,
|
|
4604
|
-
selectedReview: fallbackReview,
|
|
4605
|
-
reviews,
|
|
4606
|
-
threads,
|
|
4607
|
-
checkRollup,
|
|
4608
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4609
|
-
}
|
|
4610
|
-
};
|
|
4611
|
-
}
|
|
5745
|
+
if (!strictGate.approved) {
|
|
4612
5746
|
return {
|
|
4613
|
-
verdict: "SKIP",
|
|
5747
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4614
5748
|
feedback,
|
|
4615
|
-
reasons: [
|
|
4616
|
-
|
|
4617
|
-
],
|
|
4618
|
-
warnings,
|
|
5749
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5750
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4619
5751
|
rawPayload: {
|
|
4620
5752
|
pr: options.prState,
|
|
4621
5753
|
selectedReview: fallbackReview,
|
|
4622
5754
|
reviews,
|
|
4623
5755
|
threads,
|
|
4624
5756
|
checkRollup,
|
|
5757
|
+
actionableThreads,
|
|
5758
|
+
strictGate: {
|
|
5759
|
+
approved: strictGate.approved,
|
|
5760
|
+
pending: strictGate.pending,
|
|
5761
|
+
reasons: strictGate.reasons,
|
|
5762
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5763
|
+
warnings: strictGate.warnings,
|
|
5764
|
+
greptile: strictGate.evidence.greptile
|
|
5765
|
+
},
|
|
4625
5766
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4626
5767
|
}
|
|
4627
5768
|
};
|
|
4628
5769
|
}
|
|
4629
|
-
if (approvedViaReviewedAncestor) {
|
|
4630
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
|
|
4631
|
-
}
|
|
4632
5770
|
return {
|
|
4633
5771
|
verdict: "APPROVE",
|
|
4634
5772
|
feedback,
|
|
@@ -4640,6 +5778,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4640
5778
|
reviews,
|
|
4641
5779
|
threads,
|
|
4642
5780
|
checkRollup,
|
|
5781
|
+
strictGate: {
|
|
5782
|
+
approved: strictGate.approved,
|
|
5783
|
+
pending: strictGate.pending,
|
|
5784
|
+
reasons: strictGate.reasons,
|
|
5785
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5786
|
+
warnings: strictGate.warnings,
|
|
5787
|
+
greptile: strictGate.evidence.greptile
|
|
5788
|
+
},
|
|
4643
5789
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4644
5790
|
}
|
|
4645
5791
|
};
|
|
@@ -4752,19 +5898,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4752
5898
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4753
5899
|
return true;
|
|
4754
5900
|
}
|
|
4755
|
-
return
|
|
5901
|
+
return false;
|
|
4756
5902
|
}
|
|
4757
5903
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4758
5904
|
if (options.githubCheckState.completed) {
|
|
4759
5905
|
return false;
|
|
4760
5906
|
}
|
|
5907
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5908
|
+
return false;
|
|
5909
|
+
}
|
|
4761
5910
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4762
5911
|
return true;
|
|
4763
5912
|
}
|
|
4764
|
-
return
|
|
5913
|
+
return true;
|
|
4765
5914
|
}
|
|
4766
5915
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4767
5916
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
5917
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
5918
|
+
return false;
|
|
5919
|
+
}
|
|
4768
5920
|
if (waitingForVisiblePendingReview) {
|
|
4769
5921
|
return true;
|
|
4770
5922
|
}
|
|
@@ -4825,6 +5977,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4825
5977
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4826
5978
|
}
|
|
4827
5979
|
}
|
|
5980
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
5981
|
+
return collectPrReviewEvidence({
|
|
5982
|
+
projectRoot: input.projectRoot,
|
|
5983
|
+
prUrl: input.prUrl,
|
|
5984
|
+
taskId: input.taskId,
|
|
5985
|
+
runId: "verifier",
|
|
5986
|
+
cycle: 0,
|
|
5987
|
+
apiSignals: input.apiSignals ?? [],
|
|
5988
|
+
command: async (args, options) => {
|
|
5989
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
5990
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
5991
|
+
}
|
|
5992
|
+
});
|
|
5993
|
+
}
|
|
4828
5994
|
function deriveRepoName(projectRoot, prState) {
|
|
4829
5995
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4830
5996
|
if (fromUrl?.[1]) {
|
|
@@ -4839,8 +6005,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
4839
6005
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4840
6006
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
4841
6007
|
}
|
|
4842
|
-
function
|
|
4843
|
-
|
|
6008
|
+
function isGreptileGithubLogin2(login) {
|
|
6009
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6010
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
4844
6011
|
}
|
|
4845
6012
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
4846
6013
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -4857,7 +6024,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
4857
6024
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
4858
6025
|
}
|
|
4859
6026
|
function sortGithubGreptileReviews(reviews) {
|
|
4860
|
-
return reviews.filter((review) =>
|
|
6027
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
4861
6028
|
}
|
|
4862
6029
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
4863
6030
|
const response = runGhJson(projectRoot, [
|
|
@@ -4930,32 +6097,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
4930
6097
|
}
|
|
4931
6098
|
return { pending: false, completed: false };
|
|
4932
6099
|
}
|
|
4933
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
4934
|
-
const greptileChecks = checks.filter((check) => {
|
|
4935
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
4936
|
-
return label.includes("greptile");
|
|
4937
|
-
});
|
|
4938
|
-
if (greptileChecks.length === 0) {
|
|
4939
|
-
return false;
|
|
4940
|
-
}
|
|
4941
|
-
for (const check of greptileChecks) {
|
|
4942
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
4943
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
4944
|
-
return false;
|
|
4945
|
-
}
|
|
4946
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
4947
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
4948
|
-
return false;
|
|
4949
|
-
}
|
|
4950
|
-
continue;
|
|
4951
|
-
}
|
|
4952
|
-
const state = (check.state || "").toUpperCase();
|
|
4953
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
4954
|
-
return false;
|
|
4955
|
-
}
|
|
4956
|
-
}
|
|
4957
|
-
return true;
|
|
4958
|
-
}
|
|
4959
6100
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
4960
6101
|
const [owner, name] = repoName.split("/");
|
|
4961
6102
|
if (!owner || !name) {
|
|
@@ -4981,7 +6122,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4981
6122
|
return [];
|
|
4982
6123
|
}
|
|
4983
6124
|
const comments = thread.comments?.nodes || [];
|
|
4984
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6125
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
4985
6126
|
if (!latestGreptileComment?.path?.trim()) {
|
|
4986
6127
|
return [];
|
|
4987
6128
|
}
|
|
@@ -5003,11 +6144,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5003
6144
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5004
6145
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5005
6146
|
}
|
|
5006
|
-
function stripHtml(input) {
|
|
5007
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5008
|
-
|
|
5009
|
-
`).trim();
|
|
5010
|
-
}
|
|
5011
6147
|
function summarizeComment(input) {
|
|
5012
6148
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5013
6149
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5016,31 +6152,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5016
6152
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5017
6153
|
}
|
|
5018
6154
|
function isAiReviewApproved(input) {
|
|
6155
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6156
|
+
return false;
|
|
6157
|
+
}
|
|
5019
6158
|
if (input.reviewMode !== "required") {
|
|
5020
6159
|
return true;
|
|
5021
6160
|
}
|
|
5022
6161
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5023
6162
|
}
|
|
5024
|
-
function parseGreptileScore(input) {
|
|
5025
|
-
const text = stripHtml(input);
|
|
5026
|
-
const patterns = [
|
|
5027
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5028
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5029
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5030
|
-
];
|
|
5031
|
-
for (const pattern of patterns) {
|
|
5032
|
-
const match = pattern.exec(text);
|
|
5033
|
-
if (!match) {
|
|
5034
|
-
continue;
|
|
5035
|
-
}
|
|
5036
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5037
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5038
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5039
|
-
return { value, scale };
|
|
5040
|
-
}
|
|
5041
|
-
}
|
|
5042
|
-
return null;
|
|
5043
|
-
}
|
|
5044
6163
|
|
|
5045
6164
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5046
6165
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -5989,12 +7108,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
5989
7108
|
"task-result.json",
|
|
5990
7109
|
"validation-summary.json"
|
|
5991
7110
|
]);
|
|
5992
|
-
function resolveHostRigBinDir(root) {
|
|
5993
|
-
return resolve24(root, ".rig", "bin");
|
|
5994
|
-
}
|
|
5995
7111
|
function isRuntimeGatewayGitPath(candidate) {
|
|
5996
7112
|
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
5997
7113
|
}
|
|
7114
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
7115
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
7116
|
+
}
|
|
5998
7117
|
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
5999
7118
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
6000
7119
|
if (runtimeWorkspace && existsSync21(resolve24(runtimeWorkspace, ".git"))) {
|
|
@@ -6029,6 +7148,9 @@ function resolveGitBinary(projectRoot) {
|
|
|
6029
7148
|
}
|
|
6030
7149
|
return "git";
|
|
6031
7150
|
}
|
|
7151
|
+
function escapeRegExp2(value) {
|
|
7152
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7153
|
+
}
|
|
6032
7154
|
function safeCurrentTaskId(projectRoot) {
|
|
6033
7155
|
try {
|
|
6034
7156
|
const taskId = currentTaskId(projectRoot);
|
|
@@ -6187,17 +7309,15 @@ function gitOpenPr(options) {
|
|
|
6187
7309
|
const target = options.target || (taskId ? "monorepo" : "project");
|
|
6188
7310
|
let repoRoot = options.projectRoot;
|
|
6189
7311
|
let repoLabel = "project-rig";
|
|
6190
|
-
|
|
7312
|
+
const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
|
|
6191
7313
|
if (target === "monorepo") {
|
|
6192
7314
|
repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot2(options.projectRoot);
|
|
6193
7315
|
repoLabel = "monorepo";
|
|
6194
|
-
defaultBase = process.env.RIG_PR_BASE_MONOREPO || "main";
|
|
6195
7316
|
if (taskId) {
|
|
6196
7317
|
gitSyncBranch(options.projectRoot, taskId, "monorepo");
|
|
6197
7318
|
}
|
|
6198
7319
|
} else if (taskId) {
|
|
6199
7320
|
gitSyncBranch(options.projectRoot, taskId, "project");
|
|
6200
|
-
defaultBase = inferProjectBase(options.projectRoot, defaultBase);
|
|
6201
7321
|
}
|
|
6202
7322
|
if (!existsSync21(resolve24(repoRoot, ".git"))) {
|
|
6203
7323
|
throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
|
|
@@ -6206,9 +7326,9 @@ function gitOpenPr(options) {
|
|
|
6206
7326
|
if (!branch || branch === "HEAD") {
|
|
6207
7327
|
throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
|
|
6208
7328
|
}
|
|
6209
|
-
const base = options.base || defaultBase;
|
|
6210
7329
|
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
6211
7330
|
const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
|
|
7331
|
+
const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
|
|
6212
7332
|
refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
|
|
6213
7333
|
let reviewer = (options.reviewer || "").trim();
|
|
6214
7334
|
let reviewerSource = reviewer ? "flag" : undefined;
|
|
@@ -6244,10 +7364,11 @@ function gitOpenPr(options) {
|
|
|
6244
7364
|
"",
|
|
6245
7365
|
"## Task",
|
|
6246
7366
|
`- beads: ${taskId || "n/a"}`,
|
|
7367
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
6247
7368
|
"",
|
|
6248
7369
|
"## Review",
|
|
6249
7370
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
6250
|
-
"- When repository policy allows it, Rig
|
|
7371
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
6251
7372
|
].join(`
|
|
6252
7373
|
`);
|
|
6253
7374
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -6335,6 +7456,30 @@ function gitOpenPr(options) {
|
|
|
6335
7456
|
}
|
|
6336
7457
|
return result;
|
|
6337
7458
|
}
|
|
7459
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
7460
|
+
const lines = [];
|
|
7461
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
7462
|
+
if (runId) {
|
|
7463
|
+
lines.push(`- Run: ${runId}`);
|
|
7464
|
+
}
|
|
7465
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
7466
|
+
if (closeout) {
|
|
7467
|
+
lines.push(`- ${closeout}`);
|
|
7468
|
+
}
|
|
7469
|
+
return lines;
|
|
7470
|
+
}
|
|
7471
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
7472
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
7473
|
+
if (sourceIssueId) {
|
|
7474
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
7475
|
+
if (match?.[1] && match[2]) {
|
|
7476
|
+
const sourceRepo = match[1];
|
|
7477
|
+
const issueNumber = match[2];
|
|
7478
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
7482
|
+
}
|
|
6338
7483
|
function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
|
|
6339
7484
|
const view = runCapture2(withGhRepo([
|
|
6340
7485
|
gh,
|
|
@@ -6485,32 +7630,19 @@ function resolveGithubCliBinary(projectRoot) {
|
|
|
6485
7630
|
if (explicit) {
|
|
6486
7631
|
candidates.add(explicit);
|
|
6487
7632
|
}
|
|
7633
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
7634
|
+
candidates.add(candidate);
|
|
7635
|
+
}
|
|
6488
7636
|
const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
|
|
6489
7637
|
for (const entry of explicitPathEntries) {
|
|
6490
7638
|
candidates.add(resolve24(entry, "gh"));
|
|
6491
7639
|
}
|
|
6492
|
-
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
|
|
6493
|
-
if (hostProjectRoot) {
|
|
6494
|
-
candidates.add(resolve24(resolveHostRigBinDir(hostProjectRoot), "gh"));
|
|
6495
|
-
}
|
|
6496
|
-
candidates.add(resolve24(resolveHostRigBinDir(projectRoot), "gh"));
|
|
6497
|
-
const runtimeContext = loadRuntimeContextFromEnv();
|
|
6498
|
-
if (runtimeContext?.binDir) {
|
|
6499
|
-
candidates.add(resolve24(runtimeContext.binDir, "gh"));
|
|
6500
|
-
}
|
|
6501
|
-
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
6502
|
-
if (runtimeHome) {
|
|
6503
|
-
candidates.add(resolve24(runtimeHome, "bin", "gh"));
|
|
6504
|
-
}
|
|
6505
|
-
for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
|
|
6506
|
-
candidates.add(candidate);
|
|
6507
|
-
}
|
|
6508
7640
|
const bunResolved = Bun.which("gh");
|
|
6509
7641
|
if (bunResolved) {
|
|
6510
7642
|
candidates.add(bunResolved);
|
|
6511
7643
|
}
|
|
6512
7644
|
for (const candidate of candidates) {
|
|
6513
|
-
if (candidate && existsSync21(candidate)) {
|
|
7645
|
+
if (candidate && existsSync21(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
6514
7646
|
return candidate;
|
|
6515
7647
|
}
|
|
6516
7648
|
}
|
|
@@ -6659,6 +7791,32 @@ function withGhRepo(command, repoNameWithOwner) {
|
|
|
6659
7791
|
}
|
|
6660
7792
|
return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
|
|
6661
7793
|
}
|
|
7794
|
+
function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
|
|
7795
|
+
const remote = remoteName || "origin";
|
|
7796
|
+
const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
|
|
7797
|
+
if (symbolic.exitCode === 0) {
|
|
7798
|
+
const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp2(remote)}/`), "");
|
|
7799
|
+
if (ref && ref !== "HEAD") {
|
|
7800
|
+
return ref;
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
const lsRemote = runCapture2(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
|
|
7804
|
+
if (lsRemote.exitCode === 0) {
|
|
7805
|
+
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
|
|
7806
|
+
if (match?.[1]) {
|
|
7807
|
+
return match[1];
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
const gh = resolveGithubCliBinary(projectRoot);
|
|
7811
|
+
if (gh && repoNameWithOwner) {
|
|
7812
|
+
const api = runCapture2(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
|
|
7813
|
+
const branch = api.exitCode === 0 ? api.stdout.trim() : "";
|
|
7814
|
+
if (branch) {
|
|
7815
|
+
return branch;
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
return fallback;
|
|
7819
|
+
}
|
|
6662
7820
|
function inferProjectBase(projectRoot, fallback) {
|
|
6663
7821
|
const containing = runCapture2(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
|
|
6664
7822
|
if (containing.exitCode !== 0) {
|
|
@@ -7040,6 +8198,10 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7040
8198
|
}
|
|
7041
8199
|
env[key] = value;
|
|
7042
8200
|
}
|
|
8201
|
+
const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
8202
|
+
if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
8203
|
+
env.GITHUB_TOKEN = rigGithubToken;
|
|
8204
|
+
}
|
|
7043
8205
|
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
7044
8206
|
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
7045
8207
|
}
|
|
@@ -7063,6 +8225,13 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7063
8225
|
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
7064
8226
|
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
7065
8227
|
}
|
|
8228
|
+
const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
|
|
8229
|
+
if (gitHubToken) {
|
|
8230
|
+
env.RIG_GITHUB_TOKEN = gitHubToken;
|
|
8231
|
+
env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
|
|
8232
|
+
env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
|
|
8233
|
+
applyGitHubCredentialHelperEnv(env);
|
|
8234
|
+
}
|
|
7066
8235
|
if (runtimeKnownHosts && existsSync21(runtimeKnownHosts)) {
|
|
7067
8236
|
const sshParts = [
|
|
7068
8237
|
"ssh",
|
|
@@ -7079,6 +8248,14 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7079
8248
|
}
|
|
7080
8249
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
7081
8250
|
}
|
|
8251
|
+
function applyGitHubCredentialHelperEnv(env) {
|
|
8252
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
8253
|
+
env.GIT_CONFIG_COUNT = "2";
|
|
8254
|
+
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
8255
|
+
env.GIT_CONFIG_VALUE_0 = "";
|
|
8256
|
+
env.GIT_CONFIG_KEY_1 = "credential.helper";
|
|
8257
|
+
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';
|
|
8258
|
+
}
|
|
7082
8259
|
function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
7083
8260
|
if (!runtimeRoot) {
|
|
7084
8261
|
return {};
|