@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.
@@ -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|blocker|unsafe|not safe|do not merge|must fix|please fix|changes requested|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
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 blocker = input.blocker ?? containsBlockerText(input.body);
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
- if (!body.trim())
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: false
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 approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
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 = false;
4263
- const approvingSignal = approvingScoreEntry?.signal ?? null;
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 || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
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: input.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: firstString(view, ["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 reasons = [];
4643
+ const reasonDetails = [];
4483
4644
  const warnings = [];
4484
- let pending = false;
4485
- if (evidence.readErrors.length > 0) {
4486
- reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
4487
- }
4488
- if (!evidence.headSha)
4489
- reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
4490
- if (evidence.checkFailures.length > 0)
4491
- reasons.push(...evidence.checkFailures);
4492
- if (evidence.pendingChecks.length > 0) {
4493
- pending = true;
4494
- reasons.push(...evidence.pendingChecks);
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
- reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
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
- reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
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
- pending = true;
4512
- reasons.push("Greptile check/review has not completed for the current PR head.");
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
- reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
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
- if (!greptile.score && greptile.mapping !== "score-5-of-5") {
4520
- reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
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
- reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
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
- if (greptile.blockers.length > 0) {
4526
- reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
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
- return { reasons: Array.from(new Set(reasons)), warnings, pending };
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.reasons.length === 0 && evidence.greptile.approved;
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 isGreptileReviewTerminal(existingReview.status);
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 options.attempt + 1 < options.pollAttempts;
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
  }