@h-rig/runtime 0.0.6-alpha.13 → 0.0.6-alpha.14
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.js +1 -1
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +430 -71
- package/dist/src/control-plane/hooks/completion-verification.js +469 -87
- package/dist/src/control-plane/native/git-ops.js +28 -7
- package/dist/src/control-plane/native/harness-cli.js +430 -71
- package/dist/src/control-plane/native/pr-automation.js +523 -86
- package/dist/src/control-plane/native/pr-review-gate.js +494 -69
- package/dist/src/control-plane/native/run-ops.js +12 -6
- package/dist/src/control-plane/native/task-ops.js +466 -105
- package/dist/src/control-plane/native/verifier.js +466 -107
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/package.json +6 -6
|
@@ -3739,41 +3739,6 @@ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
|
3739
3739
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
3740
3740
|
import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
3741
3741
|
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve21 } from "path";
|
|
3742
|
-
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
3743
|
-
"changed-files.txt",
|
|
3744
|
-
"contract-changes.md",
|
|
3745
|
-
"decision-log.md",
|
|
3746
|
-
"git-state.txt",
|
|
3747
|
-
"next-actions.md",
|
|
3748
|
-
"pr-state.json",
|
|
3749
|
-
"task-result.json",
|
|
3750
|
-
"validation-summary.json"
|
|
3751
|
-
]);
|
|
3752
|
-
function readPrMetadata(projectRoot, taskId) {
|
|
3753
|
-
const path = resolve21(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
3754
|
-
if (!existsSync18(path)) {
|
|
3755
|
-
return [];
|
|
3756
|
-
}
|
|
3757
|
-
try {
|
|
3758
|
-
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
3759
|
-
if (!parsed || typeof parsed !== "object") {
|
|
3760
|
-
return [];
|
|
3761
|
-
}
|
|
3762
|
-
if (parsed.prs && typeof parsed.prs === "object") {
|
|
3763
|
-
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
3764
|
-
}
|
|
3765
|
-
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
3766
|
-
} catch {
|
|
3767
|
-
return [];
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
function isGitOpenPrResult(value) {
|
|
3771
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3772
|
-
return false;
|
|
3773
|
-
}
|
|
3774
|
-
const record = value;
|
|
3775
|
-
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
3776
|
-
}
|
|
3777
3742
|
|
|
3778
3743
|
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3779
3744
|
function parseJsonObject(value) {
|
|
@@ -3883,7 +3848,7 @@ function stripHtml(input) {
|
|
|
3883
3848
|
}
|
|
3884
3849
|
function containsBlockerText(input) {
|
|
3885
3850
|
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3886
|
-
return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(text);
|
|
3851
|
+
return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(text);
|
|
3887
3852
|
}
|
|
3888
3853
|
function isStrictFiveOfFive(score) {
|
|
3889
3854
|
return score.value === 5 && score.scale === 5;
|
|
@@ -3891,6 +3856,189 @@ function isStrictFiveOfFive(score) {
|
|
|
3891
3856
|
function containsConflictingScoreText(input) {
|
|
3892
3857
|
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3893
3858
|
}
|
|
3859
|
+
function greptileStatusVerdict(status) {
|
|
3860
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3861
|
+
if (!normalized)
|
|
3862
|
+
return null;
|
|
3863
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3864
|
+
return "approved";
|
|
3865
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3866
|
+
return "rejected";
|
|
3867
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3868
|
+
return "skipped";
|
|
3869
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3870
|
+
return "failed";
|
|
3871
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3872
|
+
return "pending";
|
|
3873
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3874
|
+
return "completed";
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3878
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3879
|
+
}
|
|
3880
|
+
function greptileRequestTimeoutMs(env) {
|
|
3881
|
+
const fallback = 30000;
|
|
3882
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3883
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3884
|
+
}
|
|
3885
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3886
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3887
|
+
return null;
|
|
3888
|
+
const record = entry;
|
|
3889
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3890
|
+
if (!id)
|
|
3891
|
+
return null;
|
|
3892
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3893
|
+
return {
|
|
3894
|
+
id,
|
|
3895
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3896
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3897
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3898
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3902
|
+
const seen = new Set;
|
|
3903
|
+
const unique2 = [];
|
|
3904
|
+
for (const review of reviews) {
|
|
3905
|
+
if (seen.has(review.id))
|
|
3906
|
+
continue;
|
|
3907
|
+
seen.add(review.id);
|
|
3908
|
+
unique2.push(review);
|
|
3909
|
+
}
|
|
3910
|
+
return unique2;
|
|
3911
|
+
}
|
|
3912
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3913
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3914
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3915
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3916
|
+
const latest = sorted.slice(0, 1);
|
|
3917
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3918
|
+
}
|
|
3919
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3920
|
+
const selected = details ?? review;
|
|
3921
|
+
return {
|
|
3922
|
+
id: selected.id || review.id,
|
|
3923
|
+
body: selected.body ?? review.body ?? null,
|
|
3924
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3925
|
+
status: selected.status ?? review.status ?? null
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3929
|
+
const controller = new AbortController;
|
|
3930
|
+
const timeoutId = setTimeout(() => {
|
|
3931
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3932
|
+
}, input.timeoutMs);
|
|
3933
|
+
let response;
|
|
3934
|
+
try {
|
|
3935
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3936
|
+
method: "POST",
|
|
3937
|
+
headers: {
|
|
3938
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3939
|
+
"Content-Type": "application/json"
|
|
3940
|
+
},
|
|
3941
|
+
body: JSON.stringify({
|
|
3942
|
+
jsonrpc: "2.0",
|
|
3943
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3944
|
+
method: "tools/call",
|
|
3945
|
+
params: { name: input.name, arguments: input.args }
|
|
3946
|
+
}),
|
|
3947
|
+
signal: controller.signal
|
|
3948
|
+
});
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
if (controller.signal.aborted) {
|
|
3951
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3952
|
+
}
|
|
3953
|
+
throw error;
|
|
3954
|
+
} finally {
|
|
3955
|
+
clearTimeout(timeoutId);
|
|
3956
|
+
}
|
|
3957
|
+
const raw = await response.text();
|
|
3958
|
+
if (!response.ok) {
|
|
3959
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3960
|
+
}
|
|
3961
|
+
let envelope;
|
|
3962
|
+
try {
|
|
3963
|
+
envelope = JSON.parse(raw);
|
|
3964
|
+
} catch {
|
|
3965
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3966
|
+
}
|
|
3967
|
+
if (envelope.error?.message) {
|
|
3968
|
+
throw new Error(envelope.error.message);
|
|
3969
|
+
}
|
|
3970
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3971
|
+
`).trim();
|
|
3972
|
+
if (!text) {
|
|
3973
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3974
|
+
}
|
|
3975
|
+
return text;
|
|
3976
|
+
}
|
|
3977
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3978
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3979
|
+
try {
|
|
3980
|
+
return JSON.parse(text);
|
|
3981
|
+
} catch {
|
|
3982
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3986
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3987
|
+
return { signals: [], errors: [] };
|
|
3988
|
+
}
|
|
3989
|
+
const env = input.options?.env ?? process.env;
|
|
3990
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3991
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3992
|
+
if (!apiKey) {
|
|
3993
|
+
return { signals: [], errors: [] };
|
|
3994
|
+
}
|
|
3995
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3996
|
+
if (typeof fetchFn !== "function") {
|
|
3997
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3998
|
+
}
|
|
3999
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
4000
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
4001
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
4002
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
4003
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
4004
|
+
try {
|
|
4005
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
4006
|
+
apiBase,
|
|
4007
|
+
apiKey,
|
|
4008
|
+
name: "list_code_reviews",
|
|
4009
|
+
args: {
|
|
4010
|
+
name: repository,
|
|
4011
|
+
remote,
|
|
4012
|
+
defaultBranch,
|
|
4013
|
+
prNumber: input.prNumber,
|
|
4014
|
+
limit: 20
|
|
4015
|
+
},
|
|
4016
|
+
timeoutMs,
|
|
4017
|
+
fetchFn
|
|
4018
|
+
});
|
|
4019
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4020
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4021
|
+
const signals = [];
|
|
4022
|
+
for (const review of selectedReviews) {
|
|
4023
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4024
|
+
apiBase,
|
|
4025
|
+
apiKey,
|
|
4026
|
+
name: "get_code_review",
|
|
4027
|
+
args: { codeReviewId: review.id },
|
|
4028
|
+
timeoutMs,
|
|
4029
|
+
fetchFn
|
|
4030
|
+
});
|
|
4031
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4032
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4033
|
+
}
|
|
4034
|
+
return { signals, errors: [] };
|
|
4035
|
+
} catch (error) {
|
|
4036
|
+
return {
|
|
4037
|
+
signals: [],
|
|
4038
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3894
4042
|
function firstString(record, keys) {
|
|
3895
4043
|
for (const key of keys) {
|
|
3896
4044
|
const value = record[key];
|
|
@@ -4017,7 +4165,7 @@ function normalizeReviewThread(entry) {
|
|
|
4017
4165
|
function relevantIssueComment(comment) {
|
|
4018
4166
|
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4019
4167
|
const body = comment.body ?? "";
|
|
4020
|
-
return isGreptileGithubLogin(login) || /greptile|
|
|
4168
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4021
4169
|
}
|
|
4022
4170
|
function latestThreadComment(thread) {
|
|
4023
4171
|
const nodes = thread.comments?.nodes ?? [];
|
|
@@ -4053,7 +4201,8 @@ function makeGreptileSignal(input) {
|
|
|
4053
4201
|
const scores = parseGreptileScores(input.body);
|
|
4054
4202
|
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4055
4203
|
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4056
|
-
const
|
|
4204
|
+
const verdict = input.verdict ?? null;
|
|
4205
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4057
4206
|
const explicitApproval = input.explicitApproval ?? false;
|
|
4058
4207
|
return {
|
|
4059
4208
|
source: input.source,
|
|
@@ -4065,6 +4214,7 @@ function makeGreptileSignal(input) {
|
|
|
4065
4214
|
score: scores[0] ?? null,
|
|
4066
4215
|
scores,
|
|
4067
4216
|
explicitApproval,
|
|
4217
|
+
verdict,
|
|
4068
4218
|
blocker,
|
|
4069
4219
|
actionable: input.actionable ?? blocker,
|
|
4070
4220
|
bodyExcerpt: bodyExcerpt(input.body),
|
|
@@ -4087,9 +4237,9 @@ function collectGreptileSignals(evidence) {
|
|
|
4087
4237
|
for (const context of contextSources) {
|
|
4088
4238
|
if (!context.body.trim())
|
|
4089
4239
|
continue;
|
|
4090
|
-
if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
|
|
4091
|
-
continue;
|
|
4092
4240
|
const contextBlocker = containsBlockerText(context.body);
|
|
4241
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4242
|
+
continue;
|
|
4093
4243
|
signals.push(makeGreptileSignal({
|
|
4094
4244
|
source: context.source,
|
|
4095
4245
|
body: context.body,
|
|
@@ -4102,16 +4252,16 @@ function collectGreptileSignals(evidence) {
|
|
|
4102
4252
|
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4103
4253
|
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4104
4254
|
|
|
4105
|
-
`);
|
|
4106
|
-
|
|
4107
|
-
continue;
|
|
4255
|
+
`) || "Status: UNKNOWN";
|
|
4256
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4108
4257
|
signals.push(makeGreptileSignal({
|
|
4109
4258
|
source: "api",
|
|
4110
4259
|
body,
|
|
4111
4260
|
currentHeadSha: evidence.currentHeadSha,
|
|
4112
4261
|
trusted: true,
|
|
4113
4262
|
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4114
|
-
explicitApproval:
|
|
4263
|
+
explicitApproval: verdict === "approved",
|
|
4264
|
+
verdict
|
|
4115
4265
|
}));
|
|
4116
4266
|
}
|
|
4117
4267
|
for (const review of evidence.reviews) {
|
|
@@ -4136,20 +4286,6 @@ function collectGreptileSignals(evidence) {
|
|
|
4136
4286
|
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4137
4287
|
}));
|
|
4138
4288
|
}
|
|
4139
|
-
for (const comment of evidence.changedFileReviewComments) {
|
|
4140
|
-
const login = commentAuthorLogin(comment);
|
|
4141
|
-
const body = comment.body ?? "";
|
|
4142
|
-
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4143
|
-
continue;
|
|
4144
|
-
signals.push(makeGreptileSignal({
|
|
4145
|
-
source: "changed-file-comment",
|
|
4146
|
-
body,
|
|
4147
|
-
currentHeadSha: evidence.currentHeadSha,
|
|
4148
|
-
trusted: true,
|
|
4149
|
-
authorLogin: login,
|
|
4150
|
-
reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
|
|
4151
|
-
}));
|
|
4152
|
-
}
|
|
4153
4289
|
for (const comment of evidence.relevantIssueComments) {
|
|
4154
4290
|
const login = commentAuthorLogin(comment);
|
|
4155
4291
|
const body = comment.body ?? "";
|
|
@@ -4257,10 +4393,17 @@ function deriveGreptileEvidence(input) {
|
|
|
4257
4393
|
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4258
4394
|
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4259
4395
|
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4260
|
-
const
|
|
4396
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4397
|
+
const signalCanApproveByScore = (signal) => {
|
|
4398
|
+
if (signal.source === "api")
|
|
4399
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4400
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4401
|
+
};
|
|
4402
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4403
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4261
4404
|
const approvedByScore = !!approvingScoreEntry;
|
|
4262
|
-
const approvedByExplicitMapping =
|
|
4263
|
-
const approvingSignal = approvingScoreEntry?.signal ??
|
|
4405
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4406
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4264
4407
|
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4265
4408
|
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4266
4409
|
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
@@ -4289,13 +4432,14 @@ function deriveGreptileEvidence(input) {
|
|
|
4289
4432
|
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4290
4433
|
return completedState && review.commit_id === input.currentHeadSha;
|
|
4291
4434
|
});
|
|
4435
|
+
const completedGreptileApi = trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && (signal.verdict === "approved" || signal.verdict === "rejected" || signal.verdict === "skipped" || signal.verdict === "failed" || signal.verdict === "completed"));
|
|
4292
4436
|
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4293
4437
|
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4294
4438
|
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4295
|
-
const completed = completedGreptileCheck || completedGreptileReview ||
|
|
4439
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4296
4440
|
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4297
|
-
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
|
|
4298
|
-
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
|
|
4441
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4442
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4299
4443
|
const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : approvingSignal?.source === "changed-file-comment" || approvingSignal?.source === "issue-comment" || approvingSignal?.source === "review-thread" ? "github-comment" : greptileReviews.length > 0 && greptileChecks.length > 0 ? "combined" : greptileReviews.length > 0 ? "github-review" : greptileChecks.length > 0 ? "github-check" : signals.some((signal) => signal.source === "pr-body" || signal.source === "pr-title") ? "pr-body" : "missing";
|
|
4300
4444
|
return {
|
|
4301
4445
|
source,
|
|
@@ -4402,6 +4546,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4402
4546
|
readErrors.push("gh pr view did not return required reviews array");
|
|
4403
4547
|
}
|
|
4404
4548
|
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4549
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4405
4550
|
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4406
4551
|
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4407
4552
|
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
|
|
@@ -4439,8 +4584,19 @@ async function collectPrReviewEvidence(input) {
|
|
|
4439
4584
|
}
|
|
4440
4585
|
}
|
|
4441
4586
|
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4587
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4588
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4589
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4590
|
+
options: input.greptileApi,
|
|
4591
|
+
repoName: parsed.repoName,
|
|
4592
|
+
prNumber: parsed.prNumber,
|
|
4593
|
+
headSha,
|
|
4594
|
+
baseRefName
|
|
4595
|
+
});
|
|
4596
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4597
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4442
4598
|
const checkFailures = statusCheckRollup.filter((check) => !isGreptileLabel(checkName(check)) && isFailingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check failed: ${checkName(check)}${check.detailsUrl || check.link ? ` (${check.detailsUrl ?? check.link})` : ""}`);
|
|
4443
|
-
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
|
|
4599
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4444
4600
|
const evidenceBase = {
|
|
4445
4601
|
title: firstString(view, ["title"]),
|
|
4446
4602
|
body: firstString(view, ["body"]),
|
|
@@ -4450,7 +4606,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4450
4606
|
reviewThreads,
|
|
4451
4607
|
checks: checksWithGreptileDetails,
|
|
4452
4608
|
currentHeadSha: headSha,
|
|
4453
|
-
apiSignals
|
|
4609
|
+
apiSignals
|
|
4454
4610
|
};
|
|
4455
4611
|
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4456
4612
|
return {
|
|
@@ -4461,7 +4617,7 @@ async function collectPrReviewEvidence(input) {
|
|
|
4461
4617
|
body: evidenceBase.body,
|
|
4462
4618
|
headSha,
|
|
4463
4619
|
headRefName: firstString(view, ["headRefName"]),
|
|
4464
|
-
baseRefName
|
|
4620
|
+
baseRefName,
|
|
4465
4621
|
state: firstString(view, ["state"]),
|
|
4466
4622
|
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4467
4623
|
mergeable: firstString(view, ["mergeable"]),
|
|
@@ -4478,72 +4634,267 @@ async function collectPrReviewEvidence(input) {
|
|
|
4478
4634
|
greptile
|
|
4479
4635
|
};
|
|
4480
4636
|
}
|
|
4637
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4638
|
+
const normalized = value.trim();
|
|
4639
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4640
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4641
|
+
}
|
|
4481
4642
|
function evaluateEvidence(evidence) {
|
|
4482
|
-
const
|
|
4643
|
+
const reasonDetails = [];
|
|
4483
4644
|
const warnings = [];
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4645
|
+
const seen = new Set;
|
|
4646
|
+
const addReason = (reason) => {
|
|
4647
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4648
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4649
|
+
if (seen.has(key))
|
|
4650
|
+
return;
|
|
4651
|
+
seen.add(key);
|
|
4652
|
+
reasonDetails.push(capped);
|
|
4653
|
+
};
|
|
4654
|
+
const greptile = evidence.greptile;
|
|
4655
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4656
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4657
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4658
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4659
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4660
|
+
for (const error of evidence.readErrors) {
|
|
4661
|
+
addReason({
|
|
4662
|
+
code: "read_error",
|
|
4663
|
+
reasonClass: "reject",
|
|
4664
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4665
|
+
suggestedAction: "needs_attention",
|
|
4666
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4667
|
+
headSha: evidence.headSha || null
|
|
4668
|
+
});
|
|
4669
|
+
}
|
|
4670
|
+
if (!evidence.headSha) {
|
|
4671
|
+
addReason({
|
|
4672
|
+
code: "missing_head_sha",
|
|
4673
|
+
reasonClass: "reject",
|
|
4674
|
+
surface: "github",
|
|
4675
|
+
suggestedAction: "needs_attention",
|
|
4676
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4677
|
+
headSha: null
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
for (const failure of evidence.checkFailures) {
|
|
4681
|
+
addReason({
|
|
4682
|
+
code: "ci_failed",
|
|
4683
|
+
reasonClass: "reject",
|
|
4684
|
+
surface: "ci",
|
|
4685
|
+
suggestedAction: "fix",
|
|
4686
|
+
message: failure,
|
|
4687
|
+
headSha: evidence.headSha || null
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4691
|
+
addReason({
|
|
4692
|
+
code: "check_pending",
|
|
4693
|
+
reasonClass: "pending",
|
|
4694
|
+
surface: "ci",
|
|
4695
|
+
suggestedAction: "wait",
|
|
4696
|
+
message: pendingCheck,
|
|
4697
|
+
headSha: evidence.headSha || null
|
|
4698
|
+
});
|
|
4495
4699
|
}
|
|
4496
4700
|
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4497
4701
|
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4498
|
-
|
|
4702
|
+
addReason({
|
|
4703
|
+
code: "review_decision_blocking",
|
|
4704
|
+
reasonClass: "reject",
|
|
4705
|
+
surface: "review",
|
|
4706
|
+
suggestedAction: "fix",
|
|
4707
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4708
|
+
headSha: evidence.headSha || null
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4712
|
+
addReason({
|
|
4713
|
+
code: "review_thread_unresolved",
|
|
4714
|
+
reasonClass: "reject",
|
|
4715
|
+
surface: "review",
|
|
4716
|
+
suggestedAction: "fix",
|
|
4717
|
+
message: thread,
|
|
4718
|
+
headSha: evidence.headSha || null
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
if (greptile.mapping === "missing") {
|
|
4722
|
+
addReason({
|
|
4723
|
+
code: "greptile_missing",
|
|
4724
|
+
reasonClass: "pending",
|
|
4725
|
+
surface: "greptile",
|
|
4726
|
+
suggestedAction: "wait",
|
|
4727
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4728
|
+
headSha: evidence.headSha || null
|
|
4729
|
+
});
|
|
4499
4730
|
}
|
|
4500
|
-
const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
|
|
4501
|
-
if (unresolvedThreads.length > 0)
|
|
4502
|
-
reasons.push(...unresolvedThreads);
|
|
4503
|
-
const greptile = evidence.greptile;
|
|
4504
|
-
if (greptile.mapping === "missing")
|
|
4505
|
-
reasons.push("Missing Greptile check/review evidence for this PR.");
|
|
4506
|
-
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4507
4731
|
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4508
|
-
|
|
4732
|
+
addReason({
|
|
4733
|
+
code: "greptile_stale",
|
|
4734
|
+
reasonClass: "pending",
|
|
4735
|
+
surface: "greptile",
|
|
4736
|
+
suggestedAction: "wait",
|
|
4737
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4738
|
+
headSha: evidence.headSha || null,
|
|
4739
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4743
|
+
addReason({
|
|
4744
|
+
code: "greptile_pending",
|
|
4745
|
+
reasonClass: "pending",
|
|
4746
|
+
surface: "greptile",
|
|
4747
|
+
suggestedAction: "wait",
|
|
4748
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4749
|
+
headSha: evidence.headSha || null,
|
|
4750
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4754
|
+
addReason({
|
|
4755
|
+
code: "greptile_api_status_unknown",
|
|
4756
|
+
reasonClass: "reject",
|
|
4757
|
+
surface: "greptile",
|
|
4758
|
+
suggestedAction: "needs_attention",
|
|
4759
|
+
message: `Greptile API/MCP review status is unknown; merge requires a known terminal APPROVED/COMPLETED 5/5 result or a known conservative status${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4760
|
+
headSha: evidence.headSha || null,
|
|
4761
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4762
|
+
});
|
|
4509
4763
|
}
|
|
4510
4764
|
if (!greptile.completed) {
|
|
4511
|
-
|
|
4512
|
-
|
|
4765
|
+
addReason({
|
|
4766
|
+
code: "greptile_pending",
|
|
4767
|
+
reasonClass: "pending",
|
|
4768
|
+
surface: "greptile",
|
|
4769
|
+
suggestedAction: "wait",
|
|
4770
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4771
|
+
headSha: evidence.headSha || null,
|
|
4772
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
if (!greptile.fresh) {
|
|
4776
|
+
addReason({
|
|
4777
|
+
code: "greptile_not_current_head",
|
|
4778
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4779
|
+
surface: "greptile",
|
|
4780
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4781
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4782
|
+
headSha: evidence.headSha || null,
|
|
4783
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4784
|
+
});
|
|
4513
4785
|
}
|
|
4514
|
-
if (!greptile.fresh)
|
|
4515
|
-
reasons.push("Greptile approval is not tied to the current PR head SHA.");
|
|
4516
4786
|
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4517
|
-
|
|
4787
|
+
addReason({
|
|
4788
|
+
code: "greptile_score_not_5",
|
|
4789
|
+
reasonClass: "reject",
|
|
4790
|
+
surface: "greptile",
|
|
4791
|
+
suggestedAction: "fix",
|
|
4792
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4793
|
+
headSha: evidence.headSha || null,
|
|
4794
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4795
|
+
});
|
|
4518
4796
|
}
|
|
4519
|
-
|
|
4520
|
-
|
|
4797
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4798
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4799
|
+
addReason({
|
|
4800
|
+
code: "greptile_score_missing",
|
|
4801
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4802
|
+
surface: "greptile",
|
|
4803
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4804
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4805
|
+
headSha: evidence.headSha || null,
|
|
4806
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4807
|
+
});
|
|
4521
4808
|
}
|
|
4522
4809
|
if (greptile.mapping === "unproven") {
|
|
4523
|
-
|
|
4810
|
+
addReason({
|
|
4811
|
+
code: "greptile_mapping_unproven",
|
|
4812
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4813
|
+
surface: "greptile",
|
|
4814
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4815
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4816
|
+
headSha: evidence.headSha || null,
|
|
4817
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
for (const blocker of greptile.blockers) {
|
|
4821
|
+
addReason({
|
|
4822
|
+
code: "greptile_blocker_text",
|
|
4823
|
+
reasonClass: "reject",
|
|
4824
|
+
surface: "greptile",
|
|
4825
|
+
suggestedAction: "fix",
|
|
4826
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4827
|
+
headSha: evidence.headSha || null,
|
|
4828
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4829
|
+
});
|
|
4524
4830
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4831
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4832
|
+
addReason({
|
|
4833
|
+
code: "greptile_unresolved_comment",
|
|
4834
|
+
reasonClass: "reject",
|
|
4835
|
+
surface: "greptile",
|
|
4836
|
+
suggestedAction: "fix",
|
|
4837
|
+
message: comment,
|
|
4838
|
+
headSha: evidence.headSha || null,
|
|
4839
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4840
|
+
});
|
|
4527
4841
|
}
|
|
4528
|
-
if (greptile.unresolvedComments.length > 0)
|
|
4529
|
-
reasons.push(...greptile.unresolvedComments);
|
|
4530
4842
|
if (!greptile.approved)
|
|
4531
4843
|
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4532
|
-
|
|
4844
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4845
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4533
4846
|
}
|
|
4534
4847
|
function evaluateStrictPrMergeGate(evidence) {
|
|
4535
4848
|
const evaluated = evaluateEvidence(evidence);
|
|
4536
|
-
const approved = evaluated.
|
|
4849
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4537
4850
|
return {
|
|
4538
4851
|
approved,
|
|
4539
4852
|
pending: evaluated.pending,
|
|
4540
4853
|
reasons: evaluated.reasons,
|
|
4854
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4541
4855
|
warnings: evaluated.warnings,
|
|
4542
4856
|
actionableFeedback: evaluated.reasons,
|
|
4543
4857
|
evidence
|
|
4544
4858
|
};
|
|
4545
4859
|
}
|
|
4546
4860
|
|
|
4861
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
4862
|
+
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
4863
|
+
"changed-files.txt",
|
|
4864
|
+
"contract-changes.md",
|
|
4865
|
+
"decision-log.md",
|
|
4866
|
+
"git-state.txt",
|
|
4867
|
+
"next-actions.md",
|
|
4868
|
+
"pr-state.json",
|
|
4869
|
+
"task-result.json",
|
|
4870
|
+
"validation-summary.json"
|
|
4871
|
+
]);
|
|
4872
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
4873
|
+
const path = resolve21(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
4874
|
+
if (!existsSync18(path)) {
|
|
4875
|
+
return [];
|
|
4876
|
+
}
|
|
4877
|
+
try {
|
|
4878
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
4879
|
+
if (!parsed || typeof parsed !== "object") {
|
|
4880
|
+
return [];
|
|
4881
|
+
}
|
|
4882
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
4883
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
4884
|
+
}
|
|
4885
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
4886
|
+
} catch {
|
|
4887
|
+
return [];
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
function isGitOpenPrResult(value) {
|
|
4891
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4892
|
+
return false;
|
|
4893
|
+
}
|
|
4894
|
+
const record = value;
|
|
4895
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4547
4898
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
4548
4899
|
async function verifyTask(options) {
|
|
4549
4900
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
@@ -5341,7 +5692,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5341
5692
|
};
|
|
5342
5693
|
}
|
|
5343
5694
|
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5344
|
-
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(blockerScanBody)) {
|
|
5695
|
+
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
5345
5696
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5346
5697
|
return {
|
|
5347
5698
|
verdict: "REJECT",
|
|
@@ -5423,6 +5774,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5423
5774
|
approved: strictGate.approved,
|
|
5424
5775
|
pending: strictGate.pending,
|
|
5425
5776
|
reasons: strictGate.reasons,
|
|
5777
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5426
5778
|
warnings: strictGate.warnings,
|
|
5427
5779
|
greptile: strictGate.evidence.greptile,
|
|
5428
5780
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5445,6 +5797,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
5445
5797
|
approved: strictGate.approved,
|
|
5446
5798
|
pending: strictGate.pending,
|
|
5447
5799
|
reasons: strictGate.reasons,
|
|
5800
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5448
5801
|
warnings: strictGate.warnings,
|
|
5449
5802
|
greptile: strictGate.evidence.greptile,
|
|
5450
5803
|
readErrors: strictGate.evidence.readErrors
|
|
@@ -5571,6 +5924,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5571
5924
|
approved: strictGate.approved,
|
|
5572
5925
|
pending: strictGate.pending,
|
|
5573
5926
|
reasons: strictGate.reasons,
|
|
5927
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5574
5928
|
warnings: strictGate.warnings,
|
|
5575
5929
|
greptile: strictGate.evidence.greptile
|
|
5576
5930
|
},
|
|
@@ -5593,6 +5947,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5593
5947
|
approved: strictGate.approved,
|
|
5594
5948
|
pending: strictGate.pending,
|
|
5595
5949
|
reasons: strictGate.reasons,
|
|
5950
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5596
5951
|
warnings: strictGate.warnings,
|
|
5597
5952
|
greptile: strictGate.evidence.greptile
|
|
5598
5953
|
},
|
|
@@ -5708,19 +6063,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
5708
6063
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
5709
6064
|
return true;
|
|
5710
6065
|
}
|
|
5711
|
-
return
|
|
6066
|
+
return false;
|
|
5712
6067
|
}
|
|
5713
6068
|
function shouldContinueGreptileMcpPolling(options) {
|
|
5714
6069
|
if (options.githubCheckState.completed) {
|
|
5715
6070
|
return false;
|
|
5716
6071
|
}
|
|
6072
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6073
|
+
return false;
|
|
6074
|
+
}
|
|
5717
6075
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
5718
6076
|
return true;
|
|
5719
6077
|
}
|
|
5720
|
-
return
|
|
6078
|
+
return true;
|
|
5721
6079
|
}
|
|
5722
6080
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
5723
6081
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6082
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
5724
6085
|
if (waitingForVisiblePendingReview) {
|
|
5725
6086
|
return true;
|
|
5726
6087
|
}
|