@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.
@@ -2,6 +2,51 @@
2
2
  // packages/runtime/src/control-plane/native/pr-review-gate.ts
3
3
  import { mkdirSync, writeFileSync } from "fs";
4
4
  import { resolve } from "path";
5
+
6
+ // packages/runtime/src/control-plane/runtime/baked-secrets.ts
7
+ var BAKED_RUNTIME_SECRETS = {
8
+ ANTHROPIC_API_KEY: typeof RIG_BAKED_ANTHROPIC_API_KEY !== "undefined" ? RIG_BAKED_ANTHROPIC_API_KEY : "",
9
+ OPENAI_API_KEY: typeof RIG_BAKED_OPENAI_API_KEY !== "undefined" ? RIG_BAKED_OPENAI_API_KEY : "",
10
+ OPENROUTER_API_KEY: typeof RIG_BAKED_OPENROUTER_API_KEY !== "undefined" ? RIG_BAKED_OPENROUTER_API_KEY : "",
11
+ AI_REVIEW_MODE: typeof RIG_BAKED_AI_REVIEW_MODE !== "undefined" ? RIG_BAKED_AI_REVIEW_MODE : "",
12
+ AI_REVIEW_PROVIDER: typeof RIG_BAKED_AI_REVIEW_PROVIDER !== "undefined" ? RIG_BAKED_AI_REVIEW_PROVIDER : "",
13
+ GREPTILE_API_BASE: typeof RIG_BAKED_GREPTILE_API_BASE !== "undefined" ? RIG_BAKED_GREPTILE_API_BASE : "",
14
+ GREPTILE_REMOTE: typeof RIG_BAKED_GREPTILE_REMOTE !== "undefined" ? RIG_BAKED_GREPTILE_REMOTE : "",
15
+ GREPTILE_REPOSITORY: typeof RIG_BAKED_GREPTILE_REPOSITORY !== "undefined" ? RIG_BAKED_GREPTILE_REPOSITORY : "",
16
+ GREPTILE_CONTEXT_BRANCH: typeof RIG_BAKED_GREPTILE_CONTEXT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_CONTEXT_BRANCH : "",
17
+ GREPTILE_DEFAULT_BRANCH: typeof RIG_BAKED_GREPTILE_DEFAULT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_DEFAULT_BRANCH : "",
18
+ GREPTILE_API_KEY: typeof RIG_BAKED_GREPTILE_API_KEY !== "undefined" ? RIG_BAKED_GREPTILE_API_KEY : "",
19
+ GREPTILE_GITHUB_TOKEN: typeof RIG_BAKED_GREPTILE_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GREPTILE_GITHUB_TOKEN : "",
20
+ GREPTILE_POLL_ATTEMPTS: typeof RIG_BAKED_GREPTILE_POLL_ATTEMPTS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_ATTEMPTS : "",
21
+ GREPTILE_POLL_INTERVAL_MS: typeof RIG_BAKED_GREPTILE_POLL_INTERVAL_MS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_INTERVAL_MS : "",
22
+ GH_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
23
+ GITHUB_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
24
+ GITHUB_SSH_KEY: typeof RIG_BAKED_GITHUB_SSH_KEY !== "undefined" ? RIG_BAKED_GITHUB_SSH_KEY : "",
25
+ AWS_ACCESS_KEY_ID: typeof RIG_BAKED_AWS_ACCESS_KEY_ID !== "undefined" ? RIG_BAKED_AWS_ACCESS_KEY_ID : "",
26
+ AWS_SECRET_ACCESS_KEY: typeof RIG_BAKED_AWS_SECRET_ACCESS_KEY !== "undefined" ? RIG_BAKED_AWS_SECRET_ACCESS_KEY : "",
27
+ AWS_REGION: typeof RIG_BAKED_AWS_REGION !== "undefined" ? RIG_BAKED_AWS_REGION : "",
28
+ LINEAR_API_KEY: typeof RIG_BAKED_LINEAR_API_KEY !== "undefined" ? RIG_BAKED_LINEAR_API_KEY : "",
29
+ LINEAR_WEBHOOK_SECRET: typeof RIG_BAKED_LINEAR_WEBHOOK_SECRET !== "undefined" ? RIG_BAKED_LINEAR_WEBHOOK_SECRET : ""
30
+ };
31
+ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
32
+ const resolved = {};
33
+ const keys = new Set([
34
+ ...Object.keys(BAKED_RUNTIME_SECRETS),
35
+ ...Object.keys(baked)
36
+ ]);
37
+ for (const key of keys) {
38
+ const envValue = env[key]?.trim();
39
+ const bakedValue = baked[key]?.trim();
40
+ if (envValue) {
41
+ resolved[key] = envValue;
42
+ } else if (bakedValue) {
43
+ resolved[key] = bakedValue;
44
+ }
45
+ }
46
+ return resolved;
47
+ }
48
+
49
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
5
50
  function parseJsonObject(value) {
6
51
  if (!value?.trim())
7
52
  return { value: {}, error: "empty JSON output" };
@@ -106,7 +151,7 @@ function stripHtml(input) {
106
151
  }
107
152
  function containsBlockerText(input) {
108
153
  const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
109
- 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);
154
+ 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);
110
155
  }
111
156
  function isStrictFiveOfFive(score) {
112
157
  return score.value === 5 && score.scale === 5;
@@ -114,6 +159,189 @@ function isStrictFiveOfFive(score) {
114
159
  function containsConflictingScoreText(input) {
115
160
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
116
161
  }
162
+ function greptileStatusVerdict(status) {
163
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
164
+ if (!normalized)
165
+ return null;
166
+ if (["APPROVE", "APPROVED"].includes(normalized))
167
+ return "approved";
168
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
169
+ return "rejected";
170
+ if (["SKIP", "SKIPPED"].includes(normalized))
171
+ return "skipped";
172
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
173
+ return "failed";
174
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
175
+ return "pending";
176
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
177
+ return "completed";
178
+ return null;
179
+ }
180
+ function isBlockingGreptileVerdict(verdict) {
181
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
182
+ }
183
+ function greptileRequestTimeoutMs(env) {
184
+ const fallback = 30000;
185
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
186
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
187
+ }
188
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
189
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
190
+ return null;
191
+ const record = entry;
192
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
193
+ if (!id)
194
+ return null;
195
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
196
+ return {
197
+ id,
198
+ status: typeof record.status === "string" ? record.status : null,
199
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
200
+ body: typeof record.body === "string" ? record.body : null,
201
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
202
+ };
203
+ }
204
+ function uniqueGreptileCodeReviews(reviews) {
205
+ const seen = new Set;
206
+ const unique = [];
207
+ for (const review of reviews) {
208
+ if (seen.has(review.id))
209
+ continue;
210
+ seen.add(review.id);
211
+ unique.push(review);
212
+ }
213
+ return unique;
214
+ }
215
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
216
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
217
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
218
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
219
+ const latest = sorted.slice(0, 1);
220
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
221
+ }
222
+ function greptileApiSignalFromCodeReview(review, details) {
223
+ const selected = details ?? review;
224
+ return {
225
+ id: selected.id || review.id,
226
+ body: selected.body ?? review.body ?? null,
227
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
228
+ status: selected.status ?? review.status ?? null
229
+ };
230
+ }
231
+ async function callGreptileMcpToolForGate(input) {
232
+ const controller = new AbortController;
233
+ const timeoutId = setTimeout(() => {
234
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
235
+ }, input.timeoutMs);
236
+ let response;
237
+ try {
238
+ response = await input.fetchFn(input.apiBase, {
239
+ method: "POST",
240
+ headers: {
241
+ Authorization: `Bearer ${input.apiKey}`,
242
+ "Content-Type": "application/json"
243
+ },
244
+ body: JSON.stringify({
245
+ jsonrpc: "2.0",
246
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
247
+ method: "tools/call",
248
+ params: { name: input.name, arguments: input.args }
249
+ }),
250
+ signal: controller.signal
251
+ });
252
+ } catch (error) {
253
+ if (controller.signal.aborted) {
254
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
255
+ }
256
+ throw error;
257
+ } finally {
258
+ clearTimeout(timeoutId);
259
+ }
260
+ const raw = await response.text();
261
+ if (!response.ok) {
262
+ throw new Error(`HTTP ${response.status}: ${raw}`);
263
+ }
264
+ let envelope;
265
+ try {
266
+ envelope = JSON.parse(raw);
267
+ } catch {
268
+ throw new Error(`Malformed MCP response: ${raw}`);
269
+ }
270
+ if (envelope.error?.message) {
271
+ throw new Error(envelope.error.message);
272
+ }
273
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
274
+ `).trim();
275
+ if (!text) {
276
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
277
+ }
278
+ return text;
279
+ }
280
+ async function callGreptileMcpToolJsonForGate(input) {
281
+ const text = await callGreptileMcpToolForGate(input);
282
+ try {
283
+ return JSON.parse(text);
284
+ } catch {
285
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
286
+ }
287
+ }
288
+ async function collectConfiguredGreptileApiSignals(input) {
289
+ if (!input.enabled || input.options?.enabled === false) {
290
+ return { signals: [], errors: [] };
291
+ }
292
+ const env = input.options?.env ?? process.env;
293
+ const secrets = resolveRuntimeSecrets(env);
294
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
295
+ if (!apiKey) {
296
+ return { signals: [], errors: [] };
297
+ }
298
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
299
+ if (typeof fetchFn !== "function") {
300
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
301
+ }
302
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
303
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
304
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
305
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
306
+ const timeoutMs = greptileRequestTimeoutMs(env);
307
+ try {
308
+ const listPayload = await callGreptileMcpToolJsonForGate({
309
+ apiBase,
310
+ apiKey,
311
+ name: "list_code_reviews",
312
+ args: {
313
+ name: repository,
314
+ remote,
315
+ defaultBranch,
316
+ prNumber: input.prNumber,
317
+ limit: 20
318
+ },
319
+ timeoutMs,
320
+ fetchFn
321
+ });
322
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
323
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
324
+ const signals = [];
325
+ for (const review of selectedReviews) {
326
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
327
+ apiBase,
328
+ apiKey,
329
+ name: "get_code_review",
330
+ args: { codeReviewId: review.id },
331
+ timeoutMs,
332
+ fetchFn
333
+ });
334
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
335
+ signals.push(greptileApiSignalFromCodeReview(review, details));
336
+ }
337
+ return { signals, errors: [] };
338
+ } catch (error) {
339
+ return {
340
+ signals: [],
341
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
342
+ };
343
+ }
344
+ }
117
345
  function firstString(record, keys) {
118
346
  for (const key of keys) {
119
347
  const value = record[key];
@@ -240,7 +468,7 @@ function normalizeReviewThread(entry) {
240
468
  function relevantIssueComment(comment) {
241
469
  const login = comment.user?.login ?? comment.author?.login ?? "";
242
470
  const body = comment.body ?? "";
243
- 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);
471
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
244
472
  }
245
473
  function latestThreadComment(thread) {
246
474
  const nodes = thread.comments?.nodes ?? [];
@@ -276,7 +504,8 @@ function makeGreptileSignal(input) {
276
504
  const scores = parseGreptileScores(input.body);
277
505
  const reviewedSha = input.reviewedSha?.trim() || null;
278
506
  const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
279
- const blocker = input.blocker ?? containsBlockerText(input.body);
507
+ const verdict = input.verdict ?? null;
508
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
280
509
  const explicitApproval = input.explicitApproval ?? false;
281
510
  return {
282
511
  source: input.source,
@@ -288,6 +517,7 @@ function makeGreptileSignal(input) {
288
517
  score: scores[0] ?? null,
289
518
  scores,
290
519
  explicitApproval,
520
+ verdict,
291
521
  blocker,
292
522
  actionable: input.actionable ?? blocker,
293
523
  bodyExcerpt: bodyExcerpt(input.body),
@@ -310,9 +540,9 @@ function collectGreptileSignals(evidence) {
310
540
  for (const context of contextSources) {
311
541
  if (!context.body.trim())
312
542
  continue;
313
- if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
314
- continue;
315
543
  const contextBlocker = containsBlockerText(context.body);
544
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
545
+ continue;
316
546
  signals.push(makeGreptileSignal({
317
547
  source: context.source,
318
548
  body: context.body,
@@ -325,16 +555,16 @@ function collectGreptileSignals(evidence) {
325
555
  for (const apiSignal of evidence.apiSignals ?? []) {
326
556
  const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
327
557
 
328
- `);
329
- if (!body.trim())
330
- continue;
558
+ `) || "Status: UNKNOWN";
559
+ const verdict = greptileStatusVerdict(apiSignal.status);
331
560
  signals.push(makeGreptileSignal({
332
561
  source: "api",
333
562
  body,
334
563
  currentHeadSha: evidence.currentHeadSha,
335
564
  trusted: true,
336
565
  reviewedSha: apiSignal.reviewedSha ?? null,
337
- explicitApproval: false
566
+ explicitApproval: verdict === "approved",
567
+ verdict
338
568
  }));
339
569
  }
340
570
  for (const review of evidence.reviews) {
@@ -359,20 +589,6 @@ function collectGreptileSignals(evidence) {
359
589
  blocker: state === "CHANGES_REQUESTED" || undefined
360
590
  }));
361
591
  }
362
- for (const comment of evidence.changedFileReviewComments) {
363
- const login = commentAuthorLogin(comment);
364
- const body = comment.body ?? "";
365
- if (!body.trim() || !isGreptileGithubLogin(login))
366
- continue;
367
- signals.push(makeGreptileSignal({
368
- source: "changed-file-comment",
369
- body,
370
- currentHeadSha: evidence.currentHeadSha,
371
- trusted: true,
372
- authorLogin: login,
373
- reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
374
- }));
375
- }
376
592
  for (const comment of evidence.relevantIssueComments) {
377
593
  const login = commentAuthorLogin(comment);
378
594
  const body = comment.body ?? "";
@@ -480,10 +696,17 @@ function deriveGreptileEvidence(input) {
480
696
  const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
481
697
  const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
482
698
  const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
483
- const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
699
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
700
+ const signalCanApproveByScore = (signal) => {
701
+ if (signal.source === "api")
702
+ return signal.verdict === "approved" || signal.verdict === "completed";
703
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
704
+ };
705
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
706
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
484
707
  const approvedByScore = !!approvingScoreEntry;
485
- const approvedByExplicitMapping = false;
486
- const approvingSignal = approvingScoreEntry?.signal ?? null;
708
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
709
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
487
710
  const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
488
711
  const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
489
712
  const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
@@ -512,13 +735,14 @@ function deriveGreptileEvidence(input) {
512
735
  const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
513
736
  return completedState && review.commit_id === input.currentHeadSha;
514
737
  });
738
+ 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"));
515
739
  const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
516
740
  const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
517
741
  const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
518
- const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
742
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
519
743
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
520
- const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
521
- const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
744
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
745
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
522
746
  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";
523
747
  return {
524
748
  source,
@@ -625,6 +849,7 @@ async function collectPrReviewEvidence(input) {
625
849
  readErrors.push("gh pr view did not return required reviews array");
626
850
  }
627
851
  const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
852
+ const baseRefName = firstString(view, ["baseRefName"]);
628
853
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
629
854
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
630
855
  const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
@@ -662,8 +887,19 @@ async function collectPrReviewEvidence(input) {
662
887
  }
663
888
  }
664
889
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
890
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
891
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
892
+ enabled: shouldCollectConfiguredGreptileApi,
893
+ options: input.greptileApi,
894
+ repoName: parsed.repoName,
895
+ prNumber: parsed.prNumber,
896
+ headSha,
897
+ baseRefName
898
+ });
899
+ readErrors.push(...configuredGreptileApiRead.errors);
900
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
665
901
  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})` : ""}`);
666
- const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
902
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
667
903
  const evidenceBase = {
668
904
  title: firstString(view, ["title"]),
669
905
  body: firstString(view, ["body"]),
@@ -673,7 +909,7 @@ async function collectPrReviewEvidence(input) {
673
909
  reviewThreads,
674
910
  checks: checksWithGreptileDetails,
675
911
  currentHeadSha: headSha,
676
- apiSignals: input.apiSignals ?? []
912
+ apiSignals
677
913
  };
678
914
  const greptile = deriveGreptileEvidence(evidenceBase);
679
915
  return {
@@ -684,7 +920,7 @@ async function collectPrReviewEvidence(input) {
684
920
  body: evidenceBase.body,
685
921
  headSha,
686
922
  headRefName: firstString(view, ["headRefName"]),
687
- baseRefName: firstString(view, ["baseRefName"]),
923
+ baseRefName,
688
924
  state: firstString(view, ["state"]),
689
925
  isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
690
926
  mergeable: firstString(view, ["mergeable"]),
@@ -701,71 +937,251 @@ async function collectPrReviewEvidence(input) {
701
937
  greptile
702
938
  };
703
939
  }
940
+ function capGateMessage(value, maxChars = 1200) {
941
+ const normalized = value.trim();
942
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
943
+ [truncated for gate summary; see full evidence artifact]` : normalized;
944
+ }
704
945
  function evaluateEvidence(evidence) {
705
- const reasons = [];
946
+ const reasonDetails = [];
706
947
  const warnings = [];
707
- let pending = false;
708
- if (evidence.readErrors.length > 0) {
709
- reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
710
- }
711
- if (!evidence.headSha)
712
- reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
713
- if (evidence.checkFailures.length > 0)
714
- reasons.push(...evidence.checkFailures);
715
- if (evidence.pendingChecks.length > 0) {
716
- pending = true;
717
- reasons.push(...evidence.pendingChecks);
948
+ const seen = new Set;
949
+ const addReason = (reason) => {
950
+ const capped = { ...reason, message: capGateMessage(reason.message) };
951
+ const key = `${capped.code}:${capped.message}`;
952
+ if (seen.has(key))
953
+ return;
954
+ seen.add(key);
955
+ reasonDetails.push(capped);
956
+ };
957
+ const greptile = evidence.greptile;
958
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
959
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
960
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
961
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
962
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
963
+ for (const error of evidence.readErrors) {
964
+ addReason({
965
+ code: "read_error",
966
+ reasonClass: "reject",
967
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
968
+ suggestedAction: "needs_attention",
969
+ message: `Required PR evidence surface could not be read completely: ${error}`,
970
+ headSha: evidence.headSha || null
971
+ });
972
+ }
973
+ if (!evidence.headSha) {
974
+ addReason({
975
+ code: "missing_head_sha",
976
+ reasonClass: "reject",
977
+ surface: "github",
978
+ suggestedAction: "needs_attention",
979
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
980
+ headSha: null
981
+ });
982
+ }
983
+ for (const failure of evidence.checkFailures) {
984
+ addReason({
985
+ code: "ci_failed",
986
+ reasonClass: "reject",
987
+ surface: "ci",
988
+ suggestedAction: "fix",
989
+ message: failure,
990
+ headSha: evidence.headSha || null
991
+ });
992
+ }
993
+ for (const pendingCheck of evidence.pendingChecks) {
994
+ addReason({
995
+ code: "check_pending",
996
+ reasonClass: "pending",
997
+ surface: "ci",
998
+ suggestedAction: "wait",
999
+ message: pendingCheck,
1000
+ headSha: evidence.headSha || null
1001
+ });
718
1002
  }
719
1003
  const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
720
1004
  if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
721
- reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
1005
+ addReason({
1006
+ code: "review_decision_blocking",
1007
+ reasonClass: "reject",
1008
+ surface: "review",
1009
+ suggestedAction: "fix",
1010
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
1011
+ headSha: evidence.headSha || null
1012
+ });
1013
+ }
1014
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
1015
+ addReason({
1016
+ code: "review_thread_unresolved",
1017
+ reasonClass: "reject",
1018
+ surface: "review",
1019
+ suggestedAction: "fix",
1020
+ message: thread,
1021
+ headSha: evidence.headSha || null
1022
+ });
1023
+ }
1024
+ if (greptile.mapping === "missing") {
1025
+ addReason({
1026
+ code: "greptile_missing",
1027
+ reasonClass: "pending",
1028
+ surface: "greptile",
1029
+ suggestedAction: "wait",
1030
+ message: "Missing Greptile check/review evidence for this PR.",
1031
+ headSha: evidence.headSha || null
1032
+ });
722
1033
  }
723
- const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
724
- if (unresolvedThreads.length > 0)
725
- reasons.push(...unresolvedThreads);
726
- const greptile = evidence.greptile;
727
- if (greptile.mapping === "missing")
728
- reasons.push("Missing Greptile check/review evidence for this PR.");
729
- const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
730
1034
  if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
731
- reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
1035
+ addReason({
1036
+ code: "greptile_stale",
1037
+ reasonClass: "pending",
1038
+ surface: "greptile",
1039
+ suggestedAction: "wait",
1040
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
1041
+ headSha: evidence.headSha || null,
1042
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
1043
+ });
1044
+ }
1045
+ for (const signal of pendingGreptileApiSignals) {
1046
+ addReason({
1047
+ code: "greptile_pending",
1048
+ reasonClass: "pending",
1049
+ surface: "greptile",
1050
+ suggestedAction: "wait",
1051
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
1052
+ headSha: evidence.headSha || null,
1053
+ reviewedSha: signal.reviewedSha ?? null
1054
+ });
1055
+ }
1056
+ for (const signal of unknownGreptileApiSignals) {
1057
+ addReason({
1058
+ code: "greptile_api_status_unknown",
1059
+ reasonClass: "reject",
1060
+ surface: "greptile",
1061
+ suggestedAction: "needs_attention",
1062
+ 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}` : "."}`,
1063
+ headSha: evidence.headSha || null,
1064
+ reviewedSha: signal.reviewedSha ?? null
1065
+ });
732
1066
  }
733
1067
  if (!greptile.completed) {
734
- pending = true;
735
- reasons.push("Greptile check/review has not completed for the current PR head.");
1068
+ addReason({
1069
+ code: "greptile_pending",
1070
+ reasonClass: "pending",
1071
+ surface: "greptile",
1072
+ suggestedAction: "wait",
1073
+ message: "Greptile check/review has not completed for the current PR head.",
1074
+ headSha: evidence.headSha || null,
1075
+ reviewedSha: greptile.reviewedSha ?? null
1076
+ });
1077
+ }
1078
+ if (!greptile.fresh) {
1079
+ addReason({
1080
+ code: "greptile_not_current_head",
1081
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
1082
+ surface: "greptile",
1083
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
1084
+ message: "Greptile approval is not tied to the current PR head SHA.",
1085
+ headSha: evidence.headSha || null,
1086
+ reviewedSha: greptile.reviewedSha ?? null
1087
+ });
736
1088
  }
737
- if (!greptile.fresh)
738
- reasons.push("Greptile approval is not tied to the current PR head SHA.");
739
1089
  if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
740
- reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
1090
+ addReason({
1091
+ code: "greptile_score_not_5",
1092
+ reasonClass: "reject",
1093
+ surface: "greptile",
1094
+ suggestedAction: "fix",
1095
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
1096
+ headSha: evidence.headSha || null,
1097
+ reviewedSha: greptile.reviewedSha ?? null
1098
+ });
741
1099
  }
742
- if (!greptile.score && greptile.mapping !== "score-5-of-5") {
743
- reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
1100
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
1101
+ if (!greptile.score && !hasApprovedMapping) {
1102
+ addReason({
1103
+ code: "greptile_score_missing",
1104
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
1105
+ surface: "greptile",
1106
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
1107
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
1108
+ headSha: evidence.headSha || null,
1109
+ reviewedSha: greptile.reviewedSha ?? null
1110
+ });
744
1111
  }
745
1112
  if (greptile.mapping === "unproven") {
746
- reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
1113
+ addReason({
1114
+ code: "greptile_mapping_unproven",
1115
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
1116
+ surface: "greptile",
1117
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
1118
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
1119
+ headSha: evidence.headSha || null,
1120
+ reviewedSha: greptile.reviewedSha ?? null
1121
+ });
1122
+ }
1123
+ for (const blocker of greptile.blockers) {
1124
+ addReason({
1125
+ code: "greptile_blocker_text",
1126
+ reasonClass: "reject",
1127
+ surface: "greptile",
1128
+ suggestedAction: "fix",
1129
+ message: `Greptile/blocker text: ${blocker}`,
1130
+ headSha: evidence.headSha || null,
1131
+ reviewedSha: greptile.reviewedSha ?? null
1132
+ });
747
1133
  }
748
- if (greptile.blockers.length > 0) {
749
- reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
1134
+ for (const comment of greptile.unresolvedComments) {
1135
+ addReason({
1136
+ code: "greptile_unresolved_comment",
1137
+ reasonClass: "reject",
1138
+ surface: "greptile",
1139
+ suggestedAction: "fix",
1140
+ message: comment,
1141
+ headSha: evidence.headSha || null,
1142
+ reviewedSha: greptile.reviewedSha ?? null
1143
+ });
750
1144
  }
751
- if (greptile.unresolvedComments.length > 0)
752
- reasons.push(...greptile.unresolvedComments);
753
1145
  if (!greptile.approved)
754
1146
  warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
755
- return { reasons: Array.from(new Set(reasons)), warnings, pending };
1147
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
1148
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
756
1149
  }
757
1150
  function evaluateStrictPrMergeGate(evidence) {
758
1151
  const evaluated = evaluateEvidence(evidence);
759
- const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
1152
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
760
1153
  return {
761
1154
  approved,
762
1155
  pending: evaluated.pending,
763
1156
  reasons: evaluated.reasons,
1157
+ reasonDetails: evaluated.reasonDetails,
764
1158
  warnings: evaluated.warnings,
765
1159
  actionableFeedback: evaluated.reasons,
766
1160
  evidence
767
1161
  };
768
1162
  }
1163
+ function strictMergeHeadShaFromGate(result, prUrl) {
1164
+ if (!result.approved) {
1165
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
1166
+ }
1167
+ if (result.evidence.prUrl !== prUrl) {
1168
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
1169
+ }
1170
+ const headSha = result.evidence.headSha?.trim();
1171
+ if (!headSha) {
1172
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
1173
+ }
1174
+ if (!/^[0-9a-f]{40}$/i.test(headSha)) {
1175
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
1176
+ }
1177
+ if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
1178
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
1179
+ }
1180
+ if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
1181
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
1182
+ }
1183
+ return headSha;
1184
+ }
769
1185
  function promptExcerpt(value, maxChars = 4000) {
770
1186
  return value.length > maxChars ? `${value.slice(0, maxChars)}
771
1187
 
@@ -777,6 +1193,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
777
1193
  function buildStrictPrGateSteeringPrompt(result) {
778
1194
  const evidence = result.evidence;
779
1195
  const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
1196
+ const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
1197
+ if (result.reasons.length > displayedReasons.length) {
1198
+ displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
1199
+ }
780
1200
  const lines = [
781
1201
  `Strict PR merge gate blocked ${evidence.prUrl}.`,
782
1202
  `PR title: ${evidence.title || "(empty)"}`,
@@ -785,10 +1205,13 @@ function buildStrictPrGateSteeringPrompt(result) {
785
1205
  evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
786
1206
  "",
787
1207
  "Gate reasons:",
788
- ...result.reasons.length ? result.reasons.map((reason) => `- ${reason}`) : ["- No reasons recorded"],
1208
+ ...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
1209
+ "",
1210
+ "Structured gate reason details:",
1211
+ result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
789
1212
  "",
790
1213
  "Required evidence read status:",
791
- evidence.readErrors.length ? JSON.stringify(evidence.readErrors, null, 2) : "All required PR evidence surfaces were read completely.",
1214
+ evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
792
1215
  "",
793
1216
  "Full PR title:",
794
1217
  evidence.title || "(empty)",
@@ -852,6 +1275,7 @@ function persistPrReviewCycleArtifacts(input) {
852
1275
  approved: input.result.approved,
853
1276
  pending: input.result.pending,
854
1277
  reasons: input.result.reasons,
1278
+ reasonDetails: input.result.reasonDetails,
855
1279
  warnings: input.result.warnings,
856
1280
  actionableFeedback: input.result.actionableFeedback,
857
1281
  prUrl: input.result.evidence.prUrl,
@@ -1225,10 +1649,7 @@ async function runRepoDefaultMerge(input) {
1225
1649
  const merge = input.config?.merge ?? {};
1226
1650
  if (merge.mode === "off")
1227
1651
  return;
1228
- const matchHeadSha = input.matchHeadSha?.trim();
1229
- if (!matchHeadSha) {
1230
- throw new Error(`Refusing to merge ${input.prUrl}: strict merge gate did not provide a current head SHA.`);
1231
- }
1652
+ const matchHeadSha = strictMergeHeadShaFromGate(input.strictGate, input.prUrl);
1232
1653
  const method = merge.method ?? "repo-default";
1233
1654
  const args = ["pr", "merge", input.prUrl];
1234
1655
  if (method === "repo-default") {
@@ -1250,17 +1671,17 @@ function shouldAttemptRigMerge(config) {
1250
1671
  return mode !== "off" && mode !== "pr-ready";
1251
1672
  }
1252
1673
  function isPendingOnlyGate(result) {
1253
- if (!result.pending)
1254
- return false;
1255
- const evidence = result.evidence;
1256
- if (evidence.readErrors.length > 0 || evidence.checkFailures.length > 0)
1257
- return false;
1258
- if (evidence.greptile.blockers.length > 0 || evidence.greptile.unresolvedComments.length > 0)
1259
- return false;
1260
- const conflictingScore = evidence.greptile.signals.some((signal) => (!signal.reviewedSha || signal.reviewedSha === evidence.headSha) && signal.score && !(signal.score.scale === 5 && signal.score.value === 5));
1261
- if (conflictingScore)
1262
- return false;
1263
- return evidence.pendingChecks.length > 0 || !evidence.greptile.completed || evidence.greptile.mapping === "missing" || evidence.greptile.mapping === "stale" || evidence.greptile.mapping === "unproven";
1674
+ return result.pending && result.reasonDetails.length > 0 && result.reasonDetails.every((reason) => reason.reasonClass === "pending" && reason.suggestedAction === "wait");
1675
+ }
1676
+ async function syncBranchAfterPrFeedback(input) {
1677
+ if (!input.gitCommand)
1678
+ return;
1679
+ await commitRunChanges({
1680
+ cwd: input.projectRoot,
1681
+ message: `rig: address PR feedback for task ${input.taskId}`,
1682
+ command: input.gitCommand
1683
+ });
1684
+ await runChecked(input.gitCommand, ["push", "--set-upstream", "origin", input.branch], input.projectRoot, "git");
1264
1685
  }
1265
1686
  async function runPrAutomation(input) {
1266
1687
  const prConfig = input.config?.pr ?? {};
@@ -1340,6 +1761,7 @@ ${createResult.stdout ?? ""}`) : null;
1340
1761
  ...latestFeedback.map((entry) => `- ${entry}`)
1341
1762
  ].join(`
1342
1763
  `));
1764
+ await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
1343
1765
  continue;
1344
1766
  }
1345
1767
  const gate = await runStrictPrMergeGate({
@@ -1350,7 +1772,8 @@ ${createResult.stdout ?? ""}`) : null;
1350
1772
  cycle: iteration,
1351
1773
  command: input.command,
1352
1774
  artifactRoot: input.artifactRoot,
1353
- allowedFailures: input.config?.merge?.allowedFailures ?? []
1775
+ allowedFailures: input.config?.merge?.allowedFailures ?? [],
1776
+ greptileApi: input.greptileApi
1354
1777
  });
1355
1778
  latestFeedback = [...gate.actionableFeedback];
1356
1779
  if (gate.approved) {
@@ -1364,20 +1787,33 @@ ${createResult.stdout ?? ""}`) : null;
1364
1787
  command: input.command,
1365
1788
  artifactRoot: input.artifactRoot,
1366
1789
  allowedFailures: input.config?.merge?.allowedFailures ?? [],
1790
+ greptileApi: input.greptileApi,
1367
1791
  final: true
1368
1792
  });
1369
1793
  if (finalGate.approved) {
1370
1794
  await input.lifecycle?.onMergeStarted?.({ prUrl });
1371
- await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot, matchHeadSha: finalGate.evidence.headSha });
1795
+ await runRepoDefaultMerge({ prUrl, config: input.config, command: input.command, cwd: input.projectRoot, strictGate: finalGate });
1372
1796
  await input.lifecycle?.onMerged?.({ prUrl });
1373
1797
  return { status: "merged", prUrl, iterations: iteration, actionableFeedback: [], merged: true };
1374
1798
  }
1375
1799
  latestFeedback = [...finalGate.actionableFeedback];
1800
+ if (isPendingOnlyGate(finalGate)) {
1801
+ const timeoutMs = positiveInt(prConfig.pendingTimeoutMs, 600000);
1802
+ const pollMs = positiveInt(prConfig.pendingPollMs, 15000);
1803
+ if (iteration >= maxPrFixIterations || timeoutMs <= 0 || pendingElapsedMs >= timeoutMs) {
1804
+ return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
1805
+ }
1806
+ const sleepMs = Math.min(pollMs, timeoutMs - pendingElapsedMs);
1807
+ await (input.sleep ?? Bun.sleep)(sleepMs);
1808
+ pendingElapsedMs += sleepMs;
1809
+ continue;
1810
+ }
1376
1811
  if (iteration >= maxPrFixIterations || prConfig.autoFixChecks === false && prConfig.autoFixReview === false) {
1377
1812
  return { status: "needs_attention", prUrl, iterations: iteration, actionableFeedback: latestFeedback, merged: false };
1378
1813
  }
1379
1814
  await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
1380
1815
  await input.steerPi(finalGate.steeringPrompt);
1816
+ await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
1381
1817
  continue;
1382
1818
  }
1383
1819
  if (isPendingOnlyGate(gate)) {
@@ -1397,6 +1833,7 @@ ${createResult.stdout ?? ""}`) : null;
1397
1833
  }
1398
1834
  await input.lifecycle?.onFeedback?.({ prUrl, iteration, feedback: latestFeedback });
1399
1835
  await input.steerPi(gate.steeringPrompt);
1836
+ await syncBranchAfterPrFeedback({ projectRoot: input.projectRoot, taskId: input.taskId, branch: input.branch, gitCommand: input.gitCommand });
1400
1837
  }
1401
1838
  return { status: "needs_attention", prUrl, iterations: maxPrFixIterations, actionableFeedback: latestFeedback, merged: false };
1402
1839
  }