@h-rig/runtime 0.0.6-alpha.12 → 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.
@@ -1756,43 +1756,6 @@ var GENERATED_TASK_ARTIFACT_FILES = new Set([
1756
1756
  "git-state.txt"
1757
1757
  ]);
1758
1758
 
1759
- // packages/runtime/src/control-plane/native/git-ops.ts
1760
- var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
1761
- "changed-files.txt",
1762
- "contract-changes.md",
1763
- "decision-log.md",
1764
- "git-state.txt",
1765
- "next-actions.md",
1766
- "pr-state.json",
1767
- "task-result.json",
1768
- "validation-summary.json"
1769
- ]);
1770
- function readPrMetadata(projectRoot, taskId) {
1771
- const path = resolve12(artifactDirForId(projectRoot, taskId), "pr-state.json");
1772
- if (!existsSync10(path)) {
1773
- return [];
1774
- }
1775
- try {
1776
- const parsed = JSON.parse(readFileSync7(path, "utf-8"));
1777
- if (!parsed || typeof parsed !== "object") {
1778
- return [];
1779
- }
1780
- if (parsed.prs && typeof parsed.prs === "object") {
1781
- return Object.values(parsed.prs).filter(isGitOpenPrResult);
1782
- }
1783
- return isGitOpenPrResult(parsed) ? [parsed] : [];
1784
- } catch {
1785
- return [];
1786
- }
1787
- }
1788
- function isGitOpenPrResult(value) {
1789
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1790
- return false;
1791
- }
1792
- const record = value;
1793
- return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
1794
- }
1795
-
1796
1759
  // packages/runtime/src/control-plane/native/pr-review-gate.ts
1797
1760
  function parseJsonObject(value) {
1798
1761
  if (!value?.trim())
@@ -1901,13 +1864,7 @@ function stripHtml(input) {
1901
1864
  }
1902
1865
  function containsBlockerText(input) {
1903
1866
  const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
1904
- 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);
1905
- }
1906
- function containsGreptileNegativeVerdict(input) {
1907
- const text = stripHtml(input).replace(/\s+/g, " ").trim();
1908
- if (!text)
1909
- return false;
1910
- return /\b(?:status|verdict|review state|state|conclusion|result)\s*:?\s*(?:reject(?:ed|ion)?|skip(?:ped)?|fail(?:ed|ure)?|changes[_ ]requested|not approved)\b/i.test(text) || /\bgreptile\b.{0,160}\b(?:reject(?:ed|s|ion)?|skip(?:ped|s)?|fail(?:ed|s|ure)?|changes requested|did not approve|not approved)\b/i.test(text);
1867
+ 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);
1911
1868
  }
1912
1869
  function isStrictFiveOfFive(score) {
1913
1870
  return score.value === 5 && score.scale === 5;
@@ -1915,6 +1872,189 @@ function isStrictFiveOfFive(score) {
1915
1872
  function containsConflictingScoreText(input) {
1916
1873
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
1917
1874
  }
1875
+ function greptileStatusVerdict(status) {
1876
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
1877
+ if (!normalized)
1878
+ return null;
1879
+ if (["APPROVE", "APPROVED"].includes(normalized))
1880
+ return "approved";
1881
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
1882
+ return "rejected";
1883
+ if (["SKIP", "SKIPPED"].includes(normalized))
1884
+ return "skipped";
1885
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
1886
+ return "failed";
1887
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
1888
+ return "pending";
1889
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
1890
+ return "completed";
1891
+ return null;
1892
+ }
1893
+ function isBlockingGreptileVerdict(verdict) {
1894
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
1895
+ }
1896
+ function greptileRequestTimeoutMs(env) {
1897
+ const fallback = 30000;
1898
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
1899
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
1900
+ }
1901
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
1902
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
1903
+ return null;
1904
+ const record = entry;
1905
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
1906
+ if (!id)
1907
+ return null;
1908
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
1909
+ return {
1910
+ id,
1911
+ status: typeof record.status === "string" ? record.status : null,
1912
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
1913
+ body: typeof record.body === "string" ? record.body : null,
1914
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
1915
+ };
1916
+ }
1917
+ function uniqueGreptileCodeReviews(reviews) {
1918
+ const seen = new Set;
1919
+ const unique2 = [];
1920
+ for (const review of reviews) {
1921
+ if (seen.has(review.id))
1922
+ continue;
1923
+ seen.add(review.id);
1924
+ unique2.push(review);
1925
+ }
1926
+ return unique2;
1927
+ }
1928
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
1929
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
1930
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
1931
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
1932
+ const latest = sorted.slice(0, 1);
1933
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
1934
+ }
1935
+ function greptileApiSignalFromCodeReview(review, details) {
1936
+ const selected = details ?? review;
1937
+ return {
1938
+ id: selected.id || review.id,
1939
+ body: selected.body ?? review.body ?? null,
1940
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
1941
+ status: selected.status ?? review.status ?? null
1942
+ };
1943
+ }
1944
+ async function callGreptileMcpToolForGate(input) {
1945
+ const controller = new AbortController;
1946
+ const timeoutId = setTimeout(() => {
1947
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
1948
+ }, input.timeoutMs);
1949
+ let response;
1950
+ try {
1951
+ response = await input.fetchFn(input.apiBase, {
1952
+ method: "POST",
1953
+ headers: {
1954
+ Authorization: `Bearer ${input.apiKey}`,
1955
+ "Content-Type": "application/json"
1956
+ },
1957
+ body: JSON.stringify({
1958
+ jsonrpc: "2.0",
1959
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
1960
+ method: "tools/call",
1961
+ params: { name: input.name, arguments: input.args }
1962
+ }),
1963
+ signal: controller.signal
1964
+ });
1965
+ } catch (error) {
1966
+ if (controller.signal.aborted) {
1967
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
1968
+ }
1969
+ throw error;
1970
+ } finally {
1971
+ clearTimeout(timeoutId);
1972
+ }
1973
+ const raw = await response.text();
1974
+ if (!response.ok) {
1975
+ throw new Error(`HTTP ${response.status}: ${raw}`);
1976
+ }
1977
+ let envelope;
1978
+ try {
1979
+ envelope = JSON.parse(raw);
1980
+ } catch {
1981
+ throw new Error(`Malformed MCP response: ${raw}`);
1982
+ }
1983
+ if (envelope.error?.message) {
1984
+ throw new Error(envelope.error.message);
1985
+ }
1986
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
1987
+ `).trim();
1988
+ if (!text) {
1989
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
1990
+ }
1991
+ return text;
1992
+ }
1993
+ async function callGreptileMcpToolJsonForGate(input) {
1994
+ const text = await callGreptileMcpToolForGate(input);
1995
+ try {
1996
+ return JSON.parse(text);
1997
+ } catch {
1998
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
1999
+ }
2000
+ }
2001
+ async function collectConfiguredGreptileApiSignals(input) {
2002
+ if (!input.enabled || input.options?.enabled === false) {
2003
+ return { signals: [], errors: [] };
2004
+ }
2005
+ const env = input.options?.env ?? process.env;
2006
+ const secrets = resolveRuntimeSecrets(env);
2007
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
2008
+ if (!apiKey) {
2009
+ return { signals: [], errors: [] };
2010
+ }
2011
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
2012
+ if (typeof fetchFn !== "function") {
2013
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
2014
+ }
2015
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
2016
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
2017
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
2018
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
2019
+ const timeoutMs = greptileRequestTimeoutMs(env);
2020
+ try {
2021
+ const listPayload = await callGreptileMcpToolJsonForGate({
2022
+ apiBase,
2023
+ apiKey,
2024
+ name: "list_code_reviews",
2025
+ args: {
2026
+ name: repository,
2027
+ remote,
2028
+ defaultBranch,
2029
+ prNumber: input.prNumber,
2030
+ limit: 20
2031
+ },
2032
+ timeoutMs,
2033
+ fetchFn
2034
+ });
2035
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
2036
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
2037
+ const signals = [];
2038
+ for (const review of selectedReviews) {
2039
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
2040
+ apiBase,
2041
+ apiKey,
2042
+ name: "get_code_review",
2043
+ args: { codeReviewId: review.id },
2044
+ timeoutMs,
2045
+ fetchFn
2046
+ });
2047
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
2048
+ signals.push(greptileApiSignalFromCodeReview(review, details));
2049
+ }
2050
+ return { signals, errors: [] };
2051
+ } catch (error) {
2052
+ return {
2053
+ signals: [],
2054
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
2055
+ };
2056
+ }
2057
+ }
1918
2058
  function firstString(record, keys) {
1919
2059
  for (const key of keys) {
1920
2060
  const value = record[key];
@@ -2041,7 +2181,7 @@ function normalizeReviewThread(entry) {
2041
2181
  function relevantIssueComment(comment) {
2042
2182
  const login = comment.user?.login ?? comment.author?.login ?? "";
2043
2183
  const body = comment.body ?? "";
2044
- 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);
2184
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
2045
2185
  }
2046
2186
  function latestThreadComment(thread) {
2047
2187
  const nodes = thread.comments?.nodes ?? [];
@@ -2077,7 +2217,8 @@ function makeGreptileSignal(input) {
2077
2217
  const scores = parseGreptileScores(input.body);
2078
2218
  const reviewedSha = input.reviewedSha?.trim() || null;
2079
2219
  const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
2080
- const blocker = input.blocker ?? (containsBlockerText(input.body) || containsGreptileNegativeVerdict(input.body));
2220
+ const verdict = input.verdict ?? null;
2221
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
2081
2222
  const explicitApproval = input.explicitApproval ?? false;
2082
2223
  return {
2083
2224
  source: input.source,
@@ -2089,6 +2230,7 @@ function makeGreptileSignal(input) {
2089
2230
  score: scores[0] ?? null,
2090
2231
  scores,
2091
2232
  explicitApproval,
2233
+ verdict,
2092
2234
  blocker,
2093
2235
  actionable: input.actionable ?? blocker,
2094
2236
  bodyExcerpt: bodyExcerpt(input.body),
@@ -2111,9 +2253,9 @@ function collectGreptileSignals(evidence) {
2111
2253
  for (const context of contextSources) {
2112
2254
  if (!context.body.trim())
2113
2255
  continue;
2114
- if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
2115
- continue;
2116
2256
  const contextBlocker = containsBlockerText(context.body);
2257
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
2258
+ continue;
2117
2259
  signals.push(makeGreptileSignal({
2118
2260
  source: context.source,
2119
2261
  body: context.body,
@@ -2126,16 +2268,16 @@ function collectGreptileSignals(evidence) {
2126
2268
  for (const apiSignal of evidence.apiSignals ?? []) {
2127
2269
  const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
2128
2270
 
2129
- `);
2130
- if (!body.trim())
2131
- continue;
2271
+ `) || "Status: UNKNOWN";
2272
+ const verdict = greptileStatusVerdict(apiSignal.status);
2132
2273
  signals.push(makeGreptileSignal({
2133
2274
  source: "api",
2134
2275
  body,
2135
2276
  currentHeadSha: evidence.currentHeadSha,
2136
2277
  trusted: true,
2137
2278
  reviewedSha: apiSignal.reviewedSha ?? null,
2138
- explicitApproval: false
2279
+ explicitApproval: verdict === "approved",
2280
+ verdict
2139
2281
  }));
2140
2282
  }
2141
2283
  for (const review of evidence.reviews) {
@@ -2160,20 +2302,6 @@ function collectGreptileSignals(evidence) {
2160
2302
  blocker: state === "CHANGES_REQUESTED" || undefined
2161
2303
  }));
2162
2304
  }
2163
- for (const comment of evidence.changedFileReviewComments) {
2164
- const login = commentAuthorLogin(comment);
2165
- const body = comment.body ?? "";
2166
- if (!body.trim() || !isGreptileGithubLogin(login))
2167
- continue;
2168
- signals.push(makeGreptileSignal({
2169
- source: "changed-file-comment",
2170
- body,
2171
- currentHeadSha: evidence.currentHeadSha,
2172
- trusted: true,
2173
- authorLogin: login,
2174
- reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
2175
- }));
2176
- }
2177
2305
  for (const comment of evidence.relevantIssueComments) {
2178
2306
  const login = commentAuthorLogin(comment);
2179
2307
  const body = comment.body ?? "";
@@ -2239,6 +2367,9 @@ function unresolvedGreptileThreadSummaries(threads) {
2239
2367
  return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
2240
2368
  });
2241
2369
  }
2370
+ function actionableChangedFileCommentSummaries(_comments) {
2371
+ return [];
2372
+ }
2242
2373
  function issueLevelBlockerSummaries(comments) {
2243
2374
  return comments.flatMap((comment) => {
2244
2375
  const body = comment.body?.trim() ?? "";
@@ -2278,14 +2409,21 @@ function deriveGreptileEvidence(input) {
2278
2409
  const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
2279
2410
  const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
2280
2411
  const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
2281
- const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
2412
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
2413
+ const signalCanApproveByScore = (signal) => {
2414
+ if (signal.source === "api")
2415
+ return signal.verdict === "approved" || signal.verdict === "completed";
2416
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
2417
+ };
2418
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
2419
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
2282
2420
  const approvedByScore = !!approvingScoreEntry;
2283
- const approvedByExplicitMapping = false;
2284
- const approvingSignal = approvingScoreEntry?.signal ?? null;
2421
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
2422
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
2285
2423
  const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
2286
2424
  const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
2287
2425
  const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
2288
- const blockerSignals = signals.filter((signal) => signal.source !== "changed-file-comment" && (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
2426
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
2289
2427
  const staleBlockingSignals = [];
2290
2428
  const blockers = [
2291
2429
  ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
@@ -2296,7 +2434,8 @@ function deriveGreptileEvidence(input) {
2296
2434
  ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
2297
2435
  ];
2298
2436
  const unresolvedComments = [
2299
- ...unresolvedGreptileThreadSummaries(input.reviewThreads)
2437
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
2438
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
2300
2439
  ];
2301
2440
  const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
2302
2441
  const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
@@ -2309,13 +2448,14 @@ function deriveGreptileEvidence(input) {
2309
2448
  const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
2310
2449
  return completedState && review.commit_id === input.currentHeadSha;
2311
2450
  });
2451
+ 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"));
2312
2452
  const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
2313
2453
  const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
2314
2454
  const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
2315
- const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
2455
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
2316
2456
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
2317
- const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
2318
- const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
2457
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
2458
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
2319
2459
  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";
2320
2460
  return {
2321
2461
  source,
@@ -2422,6 +2562,7 @@ async function collectPrReviewEvidence(input) {
2422
2562
  readErrors.push("gh pr view did not return required reviews array");
2423
2563
  }
2424
2564
  const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
2565
+ const baseRefName = firstString(view, ["baseRefName"]);
2425
2566
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
2426
2567
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
2427
2568
  const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
@@ -2459,6 +2600,17 @@ async function collectPrReviewEvidence(input) {
2459
2600
  }
2460
2601
  }
2461
2602
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
2603
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
2604
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
2605
+ enabled: shouldCollectConfiguredGreptileApi,
2606
+ options: input.greptileApi,
2607
+ repoName: parsed.repoName,
2608
+ prNumber: parsed.prNumber,
2609
+ headSha,
2610
+ baseRefName
2611
+ });
2612
+ readErrors.push(...configuredGreptileApiRead.errors);
2613
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
2462
2614
  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})` : ""}`);
2463
2615
  const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
2464
2616
  const evidenceBase = {
@@ -2470,7 +2622,7 @@ async function collectPrReviewEvidence(input) {
2470
2622
  reviewThreads,
2471
2623
  checks: checksWithGreptileDetails,
2472
2624
  currentHeadSha: headSha,
2473
- apiSignals: input.apiSignals ?? []
2625
+ apiSignals
2474
2626
  };
2475
2627
  const greptile = deriveGreptileEvidence(evidenceBase);
2476
2628
  return {
@@ -2481,7 +2633,7 @@ async function collectPrReviewEvidence(input) {
2481
2633
  body: evidenceBase.body,
2482
2634
  headSha,
2483
2635
  headRefName: firstString(view, ["headRefName"]),
2484
- baseRefName: firstString(view, ["baseRefName"]),
2636
+ baseRefName,
2485
2637
  state: firstString(view, ["state"]),
2486
2638
  isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
2487
2639
  mergeable: firstString(view, ["mergeable"]),
@@ -2498,72 +2650,267 @@ async function collectPrReviewEvidence(input) {
2498
2650
  greptile
2499
2651
  };
2500
2652
  }
2653
+ function capGateMessage(value, maxChars = 1200) {
2654
+ const normalized = value.trim();
2655
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
2656
+ [truncated for gate summary; see full evidence artifact]` : normalized;
2657
+ }
2501
2658
  function evaluateEvidence(evidence) {
2502
- const reasons = [];
2659
+ const reasonDetails = [];
2503
2660
  const warnings = [];
2504
- let pending = false;
2505
- if (evidence.readErrors.length > 0) {
2506
- reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
2507
- }
2508
- if (!evidence.headSha)
2509
- reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
2510
- if (evidence.checkFailures.length > 0)
2511
- reasons.push(...evidence.checkFailures);
2512
- if (evidence.pendingChecks.length > 0) {
2513
- pending = true;
2514
- reasons.push(...evidence.pendingChecks);
2661
+ const seen = new Set;
2662
+ const addReason = (reason) => {
2663
+ const capped = { ...reason, message: capGateMessage(reason.message) };
2664
+ const key = `${capped.code}:${capped.message}`;
2665
+ if (seen.has(key))
2666
+ return;
2667
+ seen.add(key);
2668
+ reasonDetails.push(capped);
2669
+ };
2670
+ const greptile = evidence.greptile;
2671
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
2672
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
2673
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
2674
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
2675
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
2676
+ for (const error of evidence.readErrors) {
2677
+ addReason({
2678
+ code: "read_error",
2679
+ reasonClass: "reject",
2680
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
2681
+ suggestedAction: "needs_attention",
2682
+ message: `Required PR evidence surface could not be read completely: ${error}`,
2683
+ headSha: evidence.headSha || null
2684
+ });
2685
+ }
2686
+ if (!evidence.headSha) {
2687
+ addReason({
2688
+ code: "missing_head_sha",
2689
+ reasonClass: "reject",
2690
+ surface: "github",
2691
+ suggestedAction: "needs_attention",
2692
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
2693
+ headSha: null
2694
+ });
2695
+ }
2696
+ for (const failure of evidence.checkFailures) {
2697
+ addReason({
2698
+ code: "ci_failed",
2699
+ reasonClass: "reject",
2700
+ surface: "ci",
2701
+ suggestedAction: "fix",
2702
+ message: failure,
2703
+ headSha: evidence.headSha || null
2704
+ });
2705
+ }
2706
+ for (const pendingCheck of evidence.pendingChecks) {
2707
+ addReason({
2708
+ code: "check_pending",
2709
+ reasonClass: "pending",
2710
+ surface: "ci",
2711
+ suggestedAction: "wait",
2712
+ message: pendingCheck,
2713
+ headSha: evidence.headSha || null
2714
+ });
2515
2715
  }
2516
2716
  const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
2517
2717
  if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
2518
- reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
2718
+ addReason({
2719
+ code: "review_decision_blocking",
2720
+ reasonClass: "reject",
2721
+ surface: "review",
2722
+ suggestedAction: "fix",
2723
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
2724
+ headSha: evidence.headSha || null
2725
+ });
2726
+ }
2727
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
2728
+ addReason({
2729
+ code: "review_thread_unresolved",
2730
+ reasonClass: "reject",
2731
+ surface: "review",
2732
+ suggestedAction: "fix",
2733
+ message: thread,
2734
+ headSha: evidence.headSha || null
2735
+ });
2736
+ }
2737
+ if (greptile.mapping === "missing") {
2738
+ addReason({
2739
+ code: "greptile_missing",
2740
+ reasonClass: "pending",
2741
+ surface: "greptile",
2742
+ suggestedAction: "wait",
2743
+ message: "Missing Greptile check/review evidence for this PR.",
2744
+ headSha: evidence.headSha || null
2745
+ });
2519
2746
  }
2520
- const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
2521
- if (unresolvedThreads.length > 0)
2522
- reasons.push(...unresolvedThreads);
2523
- const greptile = evidence.greptile;
2524
- if (greptile.mapping === "missing")
2525
- reasons.push("Missing Greptile check/review evidence for this PR.");
2526
- const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
2527
2747
  if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
2528
- reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
2748
+ addReason({
2749
+ code: "greptile_stale",
2750
+ reasonClass: "pending",
2751
+ surface: "greptile",
2752
+ suggestedAction: "wait",
2753
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
2754
+ headSha: evidence.headSha || null,
2755
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
2756
+ });
2757
+ }
2758
+ for (const signal of pendingGreptileApiSignals) {
2759
+ addReason({
2760
+ code: "greptile_pending",
2761
+ reasonClass: "pending",
2762
+ surface: "greptile",
2763
+ suggestedAction: "wait",
2764
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
2765
+ headSha: evidence.headSha || null,
2766
+ reviewedSha: signal.reviewedSha ?? null
2767
+ });
2768
+ }
2769
+ for (const signal of unknownGreptileApiSignals) {
2770
+ addReason({
2771
+ code: "greptile_api_status_unknown",
2772
+ reasonClass: "reject",
2773
+ surface: "greptile",
2774
+ suggestedAction: "needs_attention",
2775
+ 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}` : "."}`,
2776
+ headSha: evidence.headSha || null,
2777
+ reviewedSha: signal.reviewedSha ?? null
2778
+ });
2529
2779
  }
2530
2780
  if (!greptile.completed) {
2531
- pending = true;
2532
- reasons.push("Greptile check/review has not completed for the current PR head.");
2781
+ addReason({
2782
+ code: "greptile_pending",
2783
+ reasonClass: "pending",
2784
+ surface: "greptile",
2785
+ suggestedAction: "wait",
2786
+ message: "Greptile check/review has not completed for the current PR head.",
2787
+ headSha: evidence.headSha || null,
2788
+ reviewedSha: greptile.reviewedSha ?? null
2789
+ });
2790
+ }
2791
+ if (!greptile.fresh) {
2792
+ addReason({
2793
+ code: "greptile_not_current_head",
2794
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2795
+ surface: "greptile",
2796
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2797
+ message: "Greptile approval is not tied to the current PR head SHA.",
2798
+ headSha: evidence.headSha || null,
2799
+ reviewedSha: greptile.reviewedSha ?? null
2800
+ });
2533
2801
  }
2534
- if (!greptile.fresh)
2535
- reasons.push("Greptile approval is not tied to the current PR head SHA.");
2536
2802
  if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
2537
- reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
2803
+ addReason({
2804
+ code: "greptile_score_not_5",
2805
+ reasonClass: "reject",
2806
+ surface: "greptile",
2807
+ suggestedAction: "fix",
2808
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
2809
+ headSha: evidence.headSha || null,
2810
+ reviewedSha: greptile.reviewedSha ?? null
2811
+ });
2538
2812
  }
2539
- if (!greptile.score && greptile.mapping !== "score-5-of-5") {
2540
- reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
2813
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
2814
+ if (!greptile.score && !hasApprovedMapping) {
2815
+ addReason({
2816
+ code: "greptile_score_missing",
2817
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2818
+ surface: "greptile",
2819
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2820
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
2821
+ headSha: evidence.headSha || null,
2822
+ reviewedSha: greptile.reviewedSha ?? null
2823
+ });
2541
2824
  }
2542
2825
  if (greptile.mapping === "unproven") {
2543
- reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
2826
+ addReason({
2827
+ code: "greptile_mapping_unproven",
2828
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
2829
+ surface: "greptile",
2830
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
2831
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
2832
+ headSha: evidence.headSha || null,
2833
+ reviewedSha: greptile.reviewedSha ?? null
2834
+ });
2544
2835
  }
2545
- if (greptile.blockers.length > 0) {
2546
- reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
2836
+ for (const blocker of greptile.blockers) {
2837
+ addReason({
2838
+ code: "greptile_blocker_text",
2839
+ reasonClass: "reject",
2840
+ surface: "greptile",
2841
+ suggestedAction: "fix",
2842
+ message: `Greptile/blocker text: ${blocker}`,
2843
+ headSha: evidence.headSha || null,
2844
+ reviewedSha: greptile.reviewedSha ?? null
2845
+ });
2846
+ }
2847
+ for (const comment of greptile.unresolvedComments) {
2848
+ addReason({
2849
+ code: "greptile_unresolved_comment",
2850
+ reasonClass: "reject",
2851
+ surface: "greptile",
2852
+ suggestedAction: "fix",
2853
+ message: comment,
2854
+ headSha: evidence.headSha || null,
2855
+ reviewedSha: greptile.reviewedSha ?? null
2856
+ });
2547
2857
  }
2548
- if (greptile.unresolvedComments.length > 0)
2549
- reasons.push(...greptile.unresolvedComments);
2550
2858
  if (!greptile.approved)
2551
2859
  warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
2552
- return { reasons: Array.from(new Set(reasons)), warnings, pending };
2860
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
2861
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
2553
2862
  }
2554
2863
  function evaluateStrictPrMergeGate(evidence) {
2555
2864
  const evaluated = evaluateEvidence(evidence);
2556
- const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
2865
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
2557
2866
  return {
2558
2867
  approved,
2559
2868
  pending: evaluated.pending,
2560
2869
  reasons: evaluated.reasons,
2870
+ reasonDetails: evaluated.reasonDetails,
2561
2871
  warnings: evaluated.warnings,
2562
2872
  actionableFeedback: evaluated.reasons,
2563
2873
  evidence
2564
2874
  };
2565
2875
  }
2566
2876
 
2877
+ // packages/runtime/src/control-plane/native/git-ops.ts
2878
+ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
2879
+ "changed-files.txt",
2880
+ "contract-changes.md",
2881
+ "decision-log.md",
2882
+ "git-state.txt",
2883
+ "next-actions.md",
2884
+ "pr-state.json",
2885
+ "task-result.json",
2886
+ "validation-summary.json"
2887
+ ]);
2888
+ function readPrMetadata(projectRoot, taskId) {
2889
+ const path = resolve12(artifactDirForId(projectRoot, taskId), "pr-state.json");
2890
+ if (!existsSync10(path)) {
2891
+ return [];
2892
+ }
2893
+ try {
2894
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
2895
+ if (!parsed || typeof parsed !== "object") {
2896
+ return [];
2897
+ }
2898
+ if (parsed.prs && typeof parsed.prs === "object") {
2899
+ return Object.values(parsed.prs).filter(isGitOpenPrResult);
2900
+ }
2901
+ return isGitOpenPrResult(parsed) ? [parsed] : [];
2902
+ } catch {
2903
+ return [];
2904
+ }
2905
+ }
2906
+ function isGitOpenPrResult(value) {
2907
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2908
+ return false;
2909
+ }
2910
+ const record = value;
2911
+ return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
2912
+ }
2913
+
2567
2914
  // packages/runtime/src/control-plane/native/verifier.ts
2568
2915
  async function verifyTask(options) {
2569
2916
  const paths = resolveHarnessPaths(options.projectRoot);
@@ -3361,7 +3708,7 @@ async function runGreptileReviewForPr(options) {
3361
3708
  };
3362
3709
  }
3363
3710
  const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
3364
- 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)) {
3711
+ 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)) {
3365
3712
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
3366
3713
  return {
3367
3714
  verdict: "REJECT",
@@ -3443,6 +3790,7 @@ async function runGreptileReviewForPr(options) {
3443
3790
  approved: strictGate.approved,
3444
3791
  pending: strictGate.pending,
3445
3792
  reasons: strictGate.reasons,
3793
+ reasonDetails: strictGate.reasonDetails,
3446
3794
  warnings: strictGate.warnings,
3447
3795
  greptile: strictGate.evidence.greptile,
3448
3796
  readErrors: strictGate.evidence.readErrors
@@ -3465,6 +3813,7 @@ async function runGreptileReviewForPr(options) {
3465
3813
  approved: strictGate.approved,
3466
3814
  pending: strictGate.pending,
3467
3815
  reasons: strictGate.reasons,
3816
+ reasonDetails: strictGate.reasonDetails,
3468
3817
  warnings: strictGate.warnings,
3469
3818
  greptile: strictGate.evidence.greptile,
3470
3819
  readErrors: strictGate.evidence.readErrors
@@ -3591,6 +3940,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
3591
3940
  approved: strictGate.approved,
3592
3941
  pending: strictGate.pending,
3593
3942
  reasons: strictGate.reasons,
3943
+ reasonDetails: strictGate.reasonDetails,
3594
3944
  warnings: strictGate.warnings,
3595
3945
  greptile: strictGate.evidence.greptile
3596
3946
  },
@@ -3613,6 +3963,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
3613
3963
  approved: strictGate.approved,
3614
3964
  pending: strictGate.pending,
3615
3965
  reasons: strictGate.reasons,
3966
+ reasonDetails: strictGate.reasonDetails,
3616
3967
  warnings: strictGate.warnings,
3617
3968
  greptile: strictGate.evidence.greptile
3618
3969
  },
@@ -3734,8 +4085,7 @@ function shouldContinueGreptileMcpPolling(options) {
3734
4085
  if (options.githubCheckState.completed) {
3735
4086
  return false;
3736
4087
  }
3737
- const hasRemainingBudget = options.attempt + 1 < options.pollAttempts;
3738
- if (!hasRemainingBudget) {
4088
+ if (options.attempt + 1 >= options.pollAttempts) {
3739
4089
  return false;
3740
4090
  }
3741
4091
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
@@ -3745,8 +4095,11 @@ function shouldContinueGreptileMcpPolling(options) {
3745
4095
  }
3746
4096
  function shouldContinueGithubGreptileFallbackPolling(options) {
3747
4097
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
4098
+ if (options.attempt + 1 >= options.pollAttempts) {
4099
+ return false;
4100
+ }
3748
4101
  if (waitingForVisiblePendingReview) {
3749
- return options.attempt + 1 < options.pollAttempts;
4102
+ return true;
3750
4103
  }
3751
4104
  const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
3752
4105
  if (reviewNotVisibleYet) {