@h-rig/runtime 0.0.6-alpha.2 → 0.0.6-alpha.21

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.
Files changed (46) hide show
  1. package/dist/bin/rig-agent-dispatch.js +84 -313
  2. package/dist/bin/rig-agent.js +85 -27
  3. package/dist/src/control-plane/agent-wrapper.js +101 -27
  4. package/dist/src/control-plane/authority-files.js +12 -6
  5. package/dist/src/control-plane/harness-main.js +1357 -180
  6. package/dist/src/control-plane/hooks/completion-verification.js +1669 -329
  7. package/dist/src/control-plane/hooks/inject-context.js +2 -2
  8. package/dist/src/control-plane/hooks/submodule-branch.js +26 -3
  9. package/dist/src/control-plane/hooks/task-runtime-start.js +26 -3
  10. package/dist/src/control-plane/native/git-ops.js +134 -68
  11. package/dist/src/control-plane/native/harness-cli.js +1357 -180
  12. package/dist/src/control-plane/native/pr-automation.js +1532 -54
  13. package/dist/src/control-plane/native/pr-review-gate.js +1330 -0
  14. package/dist/src/control-plane/native/run-ops.js +35 -12
  15. package/dist/src/control-plane/native/task-ops.js +1274 -155
  16. package/dist/src/control-plane/native/validator.js +2 -2
  17. package/dist/src/control-plane/native/verifier.js +1274 -154
  18. package/dist/src/control-plane/native/workspace-ops.js +12 -6
  19. package/dist/src/control-plane/runtime/index.js +38 -9
  20. package/dist/src/control-plane/runtime/isolation/home.js +31 -6
  21. package/dist/src/control-plane/runtime/isolation/index.js +38 -9
  22. package/dist/src/control-plane/runtime/isolation/runner.js +31 -6
  23. package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
  24. package/dist/src/control-plane/runtime/isolation.js +38 -9
  25. package/dist/src/control-plane/runtime/queue.js +38 -9
  26. package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
  27. package/dist/src/control-plane/tasks/source-lifecycle.js +2 -2
  28. package/dist/src/index.js +27 -20
  29. package/dist/src/layout.js +12 -7
  30. package/dist/src/local-server.js +20 -14
  31. package/native/darwin-arm64/{bin/rig-git → rig-git} +0 -0
  32. package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
  33. package/native/darwin-arm64/{bin/rig-shell → rig-shell} +0 -0
  34. package/native/darwin-arm64/rig-shell.build-manifest.json +4 -0
  35. package/native/darwin-arm64/{bin/rig-tools → rig-tools} +0 -0
  36. package/native/darwin-arm64/rig-tools.build-manifest.json +4 -0
  37. package/native/darwin-arm64/{lib/runtime-native.dylib → runtime-native.dylib} +0 -0
  38. package/package.json +6 -6
  39. package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
  40. package/native/darwin-arm64/manifest.json +0 -1
  41. package/native/linux-x64/bin/rig-git +0 -0
  42. package/native/linux-x64/bin/rig-shell +0 -0
  43. package/native/linux-x64/bin/rig-tools +0 -0
  44. package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
  45. package/native/linux-x64/lib/runtime-native.so +0 -0
  46. package/native/linux-x64/manifest.json +0 -1
@@ -2,10 +2,10 @@
2
2
  // @bun
3
3
 
4
4
  // packages/runtime/src/control-plane/hooks/completion-verification.ts
5
- import { appendFileSync as appendFileSync2, existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
6
- import { resolve as resolve24 } from "path";
5
+ import { appendFileSync as appendFileSync2, existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync12 } from "fs";
6
+ import { resolve as resolve25 } from "path";
7
7
  import {
8
- escapeRegExp,
8
+ escapeRegExp as escapeRegExp2,
9
9
  resolveBunCli,
10
10
  resolveBunCliInvocation,
11
11
  resolveProjectRoot,
@@ -1641,8 +1641,8 @@ function isAgentRuntimeContextPath(path) {
1641
1641
  }
1642
1642
 
1643
1643
  // packages/runtime/src/control-plane/native/git-ops.ts
1644
- import { existsSync as existsSync20, lstatSync, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
1645
- import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve23 } from "path";
1644
+ import { existsSync as existsSync20, lstatSync, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
1645
+ import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve24 } from "path";
1646
1646
  import { fileURLToPath as fileURLToPath2 } from "url";
1647
1647
 
1648
1648
  // packages/runtime/src/control-plane/runtime/baked-secrets.ts
@@ -1731,8 +1731,8 @@ function expandShellValue(rawValue, env) {
1731
1731
  }
1732
1732
 
1733
1733
  // packages/runtime/src/control-plane/native/task-ops.ts
1734
- import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
1735
- import { resolve as resolve22 } from "path";
1734
+ import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
1735
+ import { resolve as resolve23 } from "path";
1736
1736
 
1737
1737
  // packages/runtime/src/build-time-config.ts
1738
1738
  function normalizeBuildConfig(value) {
@@ -2645,8 +2645,8 @@ function ensureStatusLabel(bin, repo, spawnFn, label) {
2645
2645
  }
2646
2646
  }
2647
2647
  function selectedGitHubEnv() {
2648
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
2649
- return { GH_TOKEN: token, GITHUB_TOKEN: token };
2648
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
2649
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
2650
2650
  }
2651
2651
  function ghSpawnOptions() {
2652
2652
  return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
@@ -4225,32 +4225,1306 @@ ${JSON.stringify(result, null, 2)}
4225
4225
  }
4226
4226
  }
4227
4227
  }
4228
- const summary = {
4229
- status: failed === 0 ? "pass" : "fail",
4230
- total: commands.length,
4231
- passed,
4232
- failed,
4233
- categories
4228
+ const summary = {
4229
+ status: failed === 0 ? "pass" : "fail",
4230
+ total: commands.length,
4231
+ passed,
4232
+ failed,
4233
+ categories
4234
+ };
4235
+ mkdirSync7(artifactDir, { recursive: true });
4236
+ writeFileSync7(resolve20(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
4237
+ `, "utf-8");
4238
+ return summary;
4239
+ }
4240
+
4241
+ // packages/runtime/src/control-plane/native/verifier.ts
4242
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4243
+ import { resolve as resolve22 } from "path";
4244
+
4245
+ // packages/runtime/src/control-plane/native/pr-review-gate.ts
4246
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
4247
+ import { resolve as resolve21 } from "path";
4248
+ function parseJsonObject(value) {
4249
+ if (!value?.trim())
4250
+ return { value: {}, error: "empty JSON output" };
4251
+ try {
4252
+ const parsed = JSON.parse(value);
4253
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
4254
+ } catch (error) {
4255
+ return { value: {}, error: error instanceof Error ? error.message : String(error) };
4256
+ }
4257
+ }
4258
+ function flattenPaginatedArray(value) {
4259
+ if (!Array.isArray(value))
4260
+ return null;
4261
+ if (value.every((entry) => Array.isArray(entry))) {
4262
+ return value.flatMap((entry) => entry);
4263
+ }
4264
+ return value;
4265
+ }
4266
+ function parseJsonArray(value) {
4267
+ if (!value?.trim())
4268
+ return { value: [], error: "empty JSON output" };
4269
+ try {
4270
+ const parsed = JSON.parse(value);
4271
+ const flattened = flattenPaginatedArray(parsed);
4272
+ return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
4273
+ } catch (error) {
4274
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
4275
+ }
4276
+ }
4277
+ function parseGithubPrUrl(prUrl) {
4278
+ const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
4279
+ if (!match)
4280
+ return null;
4281
+ const prNumber = Number.parseInt(match[3], 10);
4282
+ if (!Number.isFinite(prNumber))
4283
+ return null;
4284
+ return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
4285
+ }
4286
+ function checkName(check) {
4287
+ return String(check.name ?? check.context ?? "").trim();
4288
+ }
4289
+ function checkState(check) {
4290
+ return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
4291
+ }
4292
+ function isGreptileLabel(value) {
4293
+ return String(value ?? "").toLowerCase().includes("greptile");
4294
+ }
4295
+ function isGreptileGithubLogin(value) {
4296
+ const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
4297
+ return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
4298
+ }
4299
+ function isPassingCheck(check) {
4300
+ const state = checkState(check);
4301
+ return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
4302
+ }
4303
+ function isPendingCheck(check) {
4304
+ const state = checkState(check);
4305
+ return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
4306
+ }
4307
+ function isFailingCheck(check) {
4308
+ const state = checkState(check);
4309
+ return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
4310
+ }
4311
+ function wildcardToRegExp(pattern) {
4312
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
4313
+ return new RegExp(`^${escaped}$`, "i");
4314
+ }
4315
+ function isAllowedFailure(name, allowedFailures) {
4316
+ return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
4317
+ }
4318
+ function greptileScorePatterns() {
4319
+ return [
4320
+ /\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
4321
+ /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
4322
+ /\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
4323
+ ];
4324
+ }
4325
+ function parseGreptileScores(input) {
4326
+ const text = stripHtml(input);
4327
+ const seen = new Set;
4328
+ const scores = [];
4329
+ for (const pattern of greptileScorePatterns()) {
4330
+ for (const match of text.matchAll(pattern)) {
4331
+ const value = Number.parseInt(match[1] || "", 10);
4332
+ const scale = Number.parseInt(match[2] || "", 10);
4333
+ if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
4334
+ continue;
4335
+ const raw = match[0] || `${value}/${scale}`;
4336
+ const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
4337
+ if (seen.has(key))
4338
+ continue;
4339
+ seen.add(key);
4340
+ scores.push({ value, scale, raw });
4341
+ }
4342
+ }
4343
+ return scores;
4344
+ }
4345
+ function parseGreptileScore(input) {
4346
+ return parseGreptileScores(input)[0] ?? null;
4347
+ }
4348
+ function stripHtml(input) {
4349
+ return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
4350
+
4351
+ `).trim();
4352
+ }
4353
+ function containsBlockerText(input) {
4354
+ const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
4355
+ 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);
4356
+ }
4357
+ function isStrictFiveOfFive(score) {
4358
+ return score.value === 5 && score.scale === 5;
4359
+ }
4360
+ function containsConflictingScoreText(input) {
4361
+ return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
4362
+ }
4363
+ function greptileStatusVerdict(status) {
4364
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
4365
+ if (!normalized)
4366
+ return null;
4367
+ if (["APPROVE", "APPROVED"].includes(normalized))
4368
+ return "approved";
4369
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
4370
+ return "rejected";
4371
+ if (["SKIP", "SKIPPED"].includes(normalized))
4372
+ return "skipped";
4373
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
4374
+ return "failed";
4375
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
4376
+ return "pending";
4377
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
4378
+ return "completed";
4379
+ return null;
4380
+ }
4381
+ function isBlockingGreptileVerdict(verdict) {
4382
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
4383
+ }
4384
+ function greptileRequestTimeoutMs(env) {
4385
+ const fallback = 30000;
4386
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
4387
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
4388
+ }
4389
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
4390
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4391
+ return null;
4392
+ const record = entry;
4393
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
4394
+ if (!id)
4395
+ return null;
4396
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
4397
+ return {
4398
+ id,
4399
+ status: typeof record.status === "string" ? record.status : null,
4400
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
4401
+ body: typeof record.body === "string" ? record.body : null,
4402
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
4403
+ };
4404
+ }
4405
+ function uniqueGreptileCodeReviews(reviews) {
4406
+ const seen = new Set;
4407
+ const unique2 = [];
4408
+ for (const review of reviews) {
4409
+ if (seen.has(review.id))
4410
+ continue;
4411
+ seen.add(review.id);
4412
+ unique2.push(review);
4413
+ }
4414
+ return unique2;
4415
+ }
4416
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
4417
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
4418
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
4419
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
4420
+ const latest = sorted.slice(0, 1);
4421
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
4422
+ }
4423
+ function greptileApiSignalFromCodeReview(review, details) {
4424
+ const selected = details ?? review;
4425
+ return {
4426
+ id: selected.id || review.id,
4427
+ body: selected.body ?? review.body ?? null,
4428
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
4429
+ status: selected.status ?? review.status ?? null
4430
+ };
4431
+ }
4432
+ async function callGreptileMcpToolForGate(input) {
4433
+ const controller = new AbortController;
4434
+ const timeoutId = setTimeout(() => {
4435
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
4436
+ }, input.timeoutMs);
4437
+ let response;
4438
+ try {
4439
+ response = await input.fetchFn(input.apiBase, {
4440
+ method: "POST",
4441
+ headers: {
4442
+ Authorization: `Bearer ${input.apiKey}`,
4443
+ "Content-Type": "application/json"
4444
+ },
4445
+ body: JSON.stringify({
4446
+ jsonrpc: "2.0",
4447
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
4448
+ method: "tools/call",
4449
+ params: { name: input.name, arguments: input.args }
4450
+ }),
4451
+ signal: controller.signal
4452
+ });
4453
+ } catch (error) {
4454
+ if (controller.signal.aborted) {
4455
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
4456
+ }
4457
+ throw error;
4458
+ } finally {
4459
+ clearTimeout(timeoutId);
4460
+ }
4461
+ const raw = await response.text();
4462
+ if (!response.ok) {
4463
+ throw new Error(`HTTP ${response.status}: ${raw}`);
4464
+ }
4465
+ let envelope;
4466
+ try {
4467
+ envelope = JSON.parse(raw);
4468
+ } catch {
4469
+ throw new Error(`Malformed MCP response: ${raw}`);
4470
+ }
4471
+ if (envelope.error?.message) {
4472
+ throw new Error(envelope.error.message);
4473
+ }
4474
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
4475
+ `).trim();
4476
+ if (!text) {
4477
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
4478
+ }
4479
+ return text;
4480
+ }
4481
+ async function callGreptileMcpToolJsonForGate(input) {
4482
+ const text = await callGreptileMcpToolForGate(input);
4483
+ try {
4484
+ return JSON.parse(text);
4485
+ } catch {
4486
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
4487
+ }
4488
+ }
4489
+ async function collectConfiguredGreptileApiSignals(input) {
4490
+ if (!input.enabled || input.options?.enabled === false) {
4491
+ return { signals: [], errors: [] };
4492
+ }
4493
+ const env = input.options?.env ?? process.env;
4494
+ const secrets = resolveRuntimeSecrets(env);
4495
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
4496
+ if (!apiKey) {
4497
+ return { signals: [], errors: [] };
4498
+ }
4499
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
4500
+ if (typeof fetchFn !== "function") {
4501
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
4502
+ }
4503
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
4504
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
4505
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
4506
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
4507
+ const timeoutMs = greptileRequestTimeoutMs(env);
4508
+ try {
4509
+ const listPayload = await callGreptileMcpToolJsonForGate({
4510
+ apiBase,
4511
+ apiKey,
4512
+ name: "list_code_reviews",
4513
+ args: {
4514
+ name: repository,
4515
+ remote,
4516
+ defaultBranch,
4517
+ prNumber: input.prNumber,
4518
+ limit: 20
4519
+ },
4520
+ timeoutMs,
4521
+ fetchFn
4522
+ });
4523
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
4524
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
4525
+ const signals = [];
4526
+ for (const review of selectedReviews) {
4527
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
4528
+ apiBase,
4529
+ apiKey,
4530
+ name: "get_code_review",
4531
+ args: { codeReviewId: review.id },
4532
+ timeoutMs,
4533
+ fetchFn
4534
+ });
4535
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
4536
+ signals.push(greptileApiSignalFromCodeReview(review, details));
4537
+ }
4538
+ return { signals, errors: [] };
4539
+ } catch (error) {
4540
+ return {
4541
+ signals: [],
4542
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
4543
+ };
4544
+ }
4545
+ }
4546
+ function firstString(record, keys) {
4547
+ for (const key of keys) {
4548
+ const value = record[key];
4549
+ if (typeof value === "string")
4550
+ return value;
4551
+ }
4552
+ return "";
4553
+ }
4554
+ function arrayField(record, key) {
4555
+ const value = record[key];
4556
+ return Array.isArray(value) ? value : [];
4557
+ }
4558
+ async function runJsonArray(command, args, cwd) {
4559
+ const result = await command(args, { cwd });
4560
+ const label = `gh ${args.join(" ")}`;
4561
+ if (result.exitCode !== 0) {
4562
+ return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4563
+ }
4564
+ const parsed = parseJsonArray(result.stdout);
4565
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4566
+ }
4567
+ async function runJsonObject(command, args, cwd) {
4568
+ const result = await command(args, { cwd });
4569
+ const label = `gh ${args.join(" ")}`;
4570
+ if (result.exitCode !== 0) {
4571
+ return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4572
+ }
4573
+ const parsed = parseJsonObject(result.stdout);
4574
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4575
+ }
4576
+ function normalizeStatusCheck(entry) {
4577
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4578
+ return null;
4579
+ const record = entry;
4580
+ const name = firstString(record, ["name", "context"]);
4581
+ if (!name.trim())
4582
+ return null;
4583
+ const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
4584
+ const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
4585
+ return {
4586
+ __typename: typeof record.__typename === "string" ? record.__typename : null,
4587
+ name,
4588
+ context: typeof record.context === "string" ? record.context : null,
4589
+ status: typeof record.status === "string" ? record.status : null,
4590
+ state: typeof record.state === "string" ? record.state : null,
4591
+ conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
4592
+ detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.details_url === "string" ? record.details_url : typeof record.html_url === "string" ? record.html_url : typeof record.link === "string" ? record.link : null,
4593
+ link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
4594
+ headSha: typeof record.headSha === "string" ? record.headSha : null,
4595
+ head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
4596
+ output: output ? {
4597
+ title: typeof output.title === "string" ? output.title : null,
4598
+ summary: typeof output.summary === "string" ? output.summary : null,
4599
+ text: typeof output.text === "string" ? output.text : null
4600
+ } : null,
4601
+ app: app ? {
4602
+ slug: typeof app.slug === "string" ? app.slug : null,
4603
+ name: typeof app.name === "string" ? app.name : null,
4604
+ owner: app.owner && typeof app.owner === "object" ? app.owner : null
4605
+ } : null
4606
+ };
4607
+ }
4608
+ function normalizeReview(entry) {
4609
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4610
+ return null;
4611
+ const record = entry;
4612
+ return {
4613
+ id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
4614
+ state: typeof record.state === "string" ? record.state : null,
4615
+ body: typeof record.body === "string" ? record.body : null,
4616
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : typeof record.commitId === "string" ? record.commitId : record.commit && typeof record.commit === "object" && typeof record.commit.oid === "string" ? record.commit.oid : null,
4617
+ html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
4618
+ author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
4619
+ };
4620
+ }
4621
+ function normalizeReviewComment(entry) {
4622
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4623
+ return null;
4624
+ const record = entry;
4625
+ const body = typeof record.body === "string" ? record.body : null;
4626
+ const path = typeof record.path === "string" ? record.path : null;
4627
+ if (!body && !path)
4628
+ return null;
4629
+ return {
4630
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4631
+ user: record.user && typeof record.user === "object" ? record.user : null,
4632
+ author: record.author && typeof record.author === "object" ? record.author : null,
4633
+ body,
4634
+ path,
4635
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4636
+ url: typeof record.url === "string" ? record.url : null,
4637
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
4638
+ original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
4639
+ };
4640
+ }
4641
+ function normalizeIssueComment(entry) {
4642
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4643
+ return null;
4644
+ const record = entry;
4645
+ const body = typeof record.body === "string" ? record.body : null;
4646
+ if (!body)
4647
+ return null;
4648
+ return {
4649
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4650
+ user: record.user && typeof record.user === "object" ? record.user : null,
4651
+ author: record.author && typeof record.author === "object" ? record.author : null,
4652
+ body,
4653
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4654
+ url: typeof record.url === "string" ? record.url : null,
4655
+ created_at: typeof record.created_at === "string" ? record.created_at : null
4656
+ };
4657
+ }
4658
+ function normalizeReviewThread(entry) {
4659
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4660
+ return null;
4661
+ const record = entry;
4662
+ return {
4663
+ id: typeof record.id === "string" ? record.id : null,
4664
+ isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
4665
+ isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
4666
+ comments: record.comments && typeof record.comments === "object" ? record.comments : null
4667
+ };
4668
+ }
4669
+ function relevantIssueComment(comment) {
4670
+ const login = comment.user?.login ?? comment.author?.login ?? "";
4671
+ const body = comment.body ?? "";
4672
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4673
+ }
4674
+ function latestThreadComment(thread) {
4675
+ const nodes = thread.comments?.nodes ?? [];
4676
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
4677
+ }
4678
+ function unresolvedThreadSummaries(threads) {
4679
+ return threads.flatMap((thread) => {
4680
+ if (thread.isResolved === true || thread.isOutdated === true)
4681
+ return [];
4682
+ const latest = latestThreadComment(thread);
4683
+ if (!latest)
4684
+ return ["Unresolved review thread"];
4685
+ const path = latest.path ? ` on ${latest.path}` : "";
4686
+ return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4687
+ });
4688
+ }
4689
+ function collectBodies(evidence) {
4690
+ return [
4691
+ evidence.title ?? "",
4692
+ evidence.body,
4693
+ ...evidence.reviews.map((review) => review.body ?? ""),
4694
+ ...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
4695
+ ...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
4696
+ ...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
4697
+ ...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
4698
+ ].filter((body) => body.trim().length > 0);
4699
+ }
4700
+ function bodyExcerpt(body) {
4701
+ const text = stripHtml(body).replace(/\s+/g, " ").trim();
4702
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
4703
+ }
4704
+ function makeGreptileSignal(input) {
4705
+ const scores = parseGreptileScores(input.body);
4706
+ const reviewedSha = input.reviewedSha?.trim() || null;
4707
+ const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
4708
+ const verdict = input.verdict ?? null;
4709
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
4710
+ const explicitApproval = input.explicitApproval ?? false;
4711
+ return {
4712
+ source: input.source,
4713
+ trusted: input.trusted,
4714
+ authorLogin: input.authorLogin ?? null,
4715
+ reviewedSha,
4716
+ current,
4717
+ stale: current === false,
4718
+ score: scores[0] ?? null,
4719
+ scores,
4720
+ explicitApproval,
4721
+ verdict,
4722
+ blocker,
4723
+ actionable: input.actionable ?? blocker,
4724
+ bodyExcerpt: bodyExcerpt(input.body),
4725
+ body: input.body,
4726
+ allScores: scores
4727
+ };
4728
+ }
4729
+ function reviewAuthorLogin(review) {
4730
+ return review.author?.login ?? null;
4731
+ }
4732
+ function commentAuthorLogin(comment) {
4733
+ return comment.user?.login ?? comment.author?.login ?? null;
4734
+ }
4735
+ function collectGreptileSignals(evidence) {
4736
+ const signals = [];
4737
+ const contextSources = [
4738
+ { source: "pr-title", body: evidence.title ?? "" },
4739
+ { source: "pr-body", body: evidence.body }
4740
+ ];
4741
+ for (const context of contextSources) {
4742
+ if (!context.body.trim())
4743
+ continue;
4744
+ const contextBlocker = containsBlockerText(context.body);
4745
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
4746
+ continue;
4747
+ signals.push(makeGreptileSignal({
4748
+ source: context.source,
4749
+ body: context.body,
4750
+ currentHeadSha: evidence.currentHeadSha,
4751
+ trusted: false,
4752
+ blocker: contextBlocker,
4753
+ actionable: contextBlocker
4754
+ }));
4755
+ }
4756
+ for (const apiSignal of evidence.apiSignals ?? []) {
4757
+ const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
4758
+
4759
+ `) || "Status: UNKNOWN";
4760
+ const verdict = greptileStatusVerdict(apiSignal.status);
4761
+ signals.push(makeGreptileSignal({
4762
+ source: "api",
4763
+ body,
4764
+ currentHeadSha: evidence.currentHeadSha,
4765
+ trusted: true,
4766
+ reviewedSha: apiSignal.reviewedSha ?? null,
4767
+ explicitApproval: verdict === "approved",
4768
+ verdict
4769
+ }));
4770
+ }
4771
+ for (const review of evidence.reviews) {
4772
+ const login = reviewAuthorLogin(review);
4773
+ if (!isGreptileGithubLogin(login))
4774
+ continue;
4775
+ const state = String(review.state ?? "").toUpperCase();
4776
+ const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
4777
+
4778
+ `);
4779
+ if (!body.trim())
4780
+ continue;
4781
+ const dismissed = state === "DISMISSED";
4782
+ signals.push(makeGreptileSignal({
4783
+ source: "github-review",
4784
+ body,
4785
+ currentHeadSha: evidence.currentHeadSha,
4786
+ trusted: !dismissed,
4787
+ authorLogin: login,
4788
+ reviewedSha: review.commit_id ?? null,
4789
+ explicitApproval: undefined,
4790
+ blocker: state === "CHANGES_REQUESTED" || undefined
4791
+ }));
4792
+ }
4793
+ for (const comment of evidence.relevantIssueComments) {
4794
+ const login = commentAuthorLogin(comment);
4795
+ const body = comment.body ?? "";
4796
+ if (!body.trim() || !isGreptileGithubLogin(login))
4797
+ continue;
4798
+ signals.push(makeGreptileSignal({
4799
+ source: "issue-comment",
4800
+ body,
4801
+ currentHeadSha: evidence.currentHeadSha,
4802
+ trusted: true,
4803
+ authorLogin: login
4804
+ }));
4805
+ }
4806
+ for (const thread of evidence.reviewThreads) {
4807
+ if (thread.isOutdated === true || thread.isResolved === true)
4808
+ continue;
4809
+ for (const comment of thread.comments?.nodes ?? []) {
4810
+ const login = comment.author?.login ?? null;
4811
+ const body = comment.body ?? "";
4812
+ if (!body.trim() || !isGreptileGithubLogin(login))
4813
+ continue;
4814
+ signals.push(makeGreptileSignal({
4815
+ source: "review-thread",
4816
+ body,
4817
+ currentHeadSha: evidence.currentHeadSha,
4818
+ trusted: true,
4819
+ authorLogin: login
4820
+ }));
4821
+ }
4822
+ }
4823
+ for (const check of evidence.checks) {
4824
+ if (!isGreptileLabel(checkName(check)))
4825
+ continue;
4826
+ const reviewedSha = check.headSha ?? check.head_sha ?? null;
4827
+ const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
4828
+ const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
4829
+
4830
+ `);
4831
+ signals.push(makeGreptileSignal({
4832
+ source: "github-check",
4833
+ body,
4834
+ currentHeadSha: evidence.currentHeadSha,
4835
+ trusted: false,
4836
+ reviewedSha,
4837
+ explicitApproval: false,
4838
+ blocker: isFailingCheck(check),
4839
+ actionable: isFailingCheck(check)
4840
+ }));
4841
+ }
4842
+ return signals;
4843
+ }
4844
+ function unresolvedGreptileThreadSummaries(threads) {
4845
+ return threads.flatMap((thread) => {
4846
+ if (thread.isResolved === true || thread.isOutdated === true)
4847
+ return [];
4848
+ const comments = thread.comments?.nodes ?? [];
4849
+ if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
4850
+ return [];
4851
+ const latest = latestThreadComment(thread);
4852
+ if (!latest)
4853
+ return ["Unresolved Greptile review thread"];
4854
+ const path = latest.path ? ` on ${latest.path}` : "";
4855
+ return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4856
+ });
4857
+ }
4858
+ function actionableChangedFileCommentSummaries(_comments) {
4859
+ return [];
4860
+ }
4861
+ function issueLevelBlockerSummaries(comments) {
4862
+ return comments.flatMap((comment) => {
4863
+ const body = comment.body?.trim() ?? "";
4864
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4865
+ return [];
4866
+ const login = commentAuthorLogin(comment) ?? "unknown";
4867
+ const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
4868
+ return [`${author}: ${body}`];
4869
+ });
4870
+ }
4871
+ function reviewBodyBlockerSummaries(reviews) {
4872
+ return reviews.flatMap((review) => {
4873
+ const login = reviewAuthorLogin(review) ?? "unknown";
4874
+ if (isGreptileGithubLogin(login))
4875
+ return [];
4876
+ const body = review.body?.trim() ?? "";
4877
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4878
+ return [];
4879
+ const state = review.state ? ` (${review.state})` : "";
4880
+ return [`PR review summary by ${login}${state}: ${body}`];
4881
+ });
4882
+ }
4883
+ function signalLabel(signal) {
4884
+ const source = signal.source.replace(/-/g, " ");
4885
+ const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
4886
+ const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
4887
+ return `${source}${author}${sha}`;
4888
+ }
4889
+ function deriveGreptileEvidence(input) {
4890
+ const rawBodies = collectBodies(input);
4891
+ const signals = collectGreptileSignals(input);
4892
+ const trustedSignals = signals.filter((signal) => signal.trusted);
4893
+ const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4894
+ const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4895
+ const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
4896
+ const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
4897
+ const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
4898
+ const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
4899
+ const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
4900
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
4901
+ const signalCanApproveByScore = (signal) => {
4902
+ if (signal.source === "api")
4903
+ return signal.verdict === "approved" || signal.verdict === "completed";
4904
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
4905
+ };
4906
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
4907
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
4908
+ const approvedByScore = !!approvingScoreEntry;
4909
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
4910
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
4911
+ const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
4912
+ const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
4913
+ const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
4914
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
4915
+ const staleBlockingSignals = [];
4916
+ const blockers = [
4917
+ ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
4918
+ ...reviewBodyBlockerSummaries(input.reviews),
4919
+ ...issueLevelBlockerSummaries(input.relevantIssueComments),
4920
+ ...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
4921
+ ...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
4922
+ ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
4923
+ ];
4924
+ const unresolvedComments = [
4925
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
4926
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
4927
+ ];
4928
+ const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
4929
+ const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
4930
+ const completedGreptileCheck = greptileChecks.some((check) => {
4931
+ const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
4932
+ return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
4933
+ });
4934
+ const completedGreptileReview = greptileReviews.some((review) => {
4935
+ const state = String(review.state ?? "").toUpperCase();
4936
+ const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
4937
+ return completedState && review.commit_id === input.currentHeadSha;
4938
+ });
4939
+ 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"));
4940
+ const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
4941
+ const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
4942
+ const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
4943
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
4944
+ const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4945
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4946
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4947
+ 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";
4948
+ return {
4949
+ source,
4950
+ currentHeadSha: input.currentHeadSha,
4951
+ reviewedSha,
4952
+ fresh,
4953
+ completed,
4954
+ approved,
4955
+ score,
4956
+ explicitApproval: approvedByExplicitMapping,
4957
+ blockers,
4958
+ unresolvedComments,
4959
+ rawBodies,
4960
+ signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
4961
+ mapping
4962
+ };
4963
+ }
4964
+ function isGreptileCheckDetail(check) {
4965
+ return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4966
+ }
4967
+ async function collectGreptileCheckDetails(input) {
4968
+ const checkRunsRead = await runJsonArray(input.command, [
4969
+ "api",
4970
+ `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4971
+ "--paginate",
4972
+ "--slurp",
4973
+ "--jq",
4974
+ "map(.check_runs // []) | add // []"
4975
+ ], input.projectRoot);
4976
+ const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4977
+ return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4978
+ }
4979
+ async function collectReviewThreads(input) {
4980
+ const reviewThreads = [];
4981
+ let afterCursor = null;
4982
+ for (let page = 0;page < 100; page += 1) {
4983
+ const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
4984
+ const threadsResponse = await runJsonObject(input.command, [
4985
+ "api",
4986
+ "graphql",
4987
+ "-F",
4988
+ `owner=${input.owner}`,
4989
+ "-F",
4990
+ `name=${input.name}`,
4991
+ "-F",
4992
+ `prNumber=${input.prNumber}`,
4993
+ "-f",
4994
+ `query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { reviewThreads(first: 100, after: ${afterLiteral}) { nodes { id isResolved isOutdated comments(first: 100) { nodes { author { login } body path url createdAt } pageInfo { hasNextPage endCursor } } } pageInfo { hasNextPage endCursor } } } } }`
4995
+ ], input.projectRoot);
4996
+ if (threadsResponse.error) {
4997
+ return { value: reviewThreads, error: threadsResponse.error };
4998
+ }
4999
+ const data = threadsResponse.value.data;
5000
+ const repository = data?.repository;
5001
+ const pullRequest = repository?.pullRequest;
5002
+ const threads = pullRequest?.reviewThreads;
5003
+ const nodes = threads?.nodes;
5004
+ if (!Array.isArray(nodes)) {
5005
+ return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
5006
+ }
5007
+ const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
5008
+ reviewThreads.push(...normalized);
5009
+ const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
5010
+ if (truncatedCommentThread) {
5011
+ return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
5012
+ }
5013
+ const pageInfo = threads?.pageInfo;
5014
+ if (!pageInfo) {
5015
+ if (nodes.length >= 100) {
5016
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
5017
+ }
5018
+ return { value: reviewThreads };
5019
+ }
5020
+ if (pageInfo.hasNextPage !== true) {
5021
+ return { value: reviewThreads };
5022
+ }
5023
+ if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
5024
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
5025
+ }
5026
+ afterCursor = pageInfo.endCursor;
5027
+ }
5028
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
5029
+ }
5030
+ async function collectPrReviewEvidence(input) {
5031
+ const parsed = parseGithubPrUrl(input.prUrl);
5032
+ if (!parsed) {
5033
+ throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
5034
+ }
5035
+ const readErrors = [];
5036
+ const viewRead = await runJsonObject(input.command, [
5037
+ "pr",
5038
+ "view",
5039
+ input.prUrl,
5040
+ "--json",
5041
+ "title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
5042
+ ], input.projectRoot);
5043
+ if (viewRead.error)
5044
+ readErrors.push(viewRead.error);
5045
+ const view = viewRead.value;
5046
+ if (!Array.isArray(view.statusCheckRollup)) {
5047
+ readErrors.push("gh pr view did not return required statusCheckRollup array");
5048
+ }
5049
+ if (!Array.isArray(view.reviews)) {
5050
+ readErrors.push("gh pr view did not return required reviews array");
5051
+ }
5052
+ const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
5053
+ const baseRefName = firstString(view, ["baseRefName"]);
5054
+ const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
5055
+ const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
5056
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
5057
+ if (reviewCommentsRead.error)
5058
+ readErrors.push(reviewCommentsRead.error);
5059
+ const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
5060
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
5061
+ if (issueCommentsRead.error)
5062
+ readErrors.push(issueCommentsRead.error);
5063
+ const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
5064
+ const reviewThreadsRead = await collectReviewThreads({
5065
+ command: input.command,
5066
+ projectRoot: input.projectRoot,
5067
+ owner: parsed.owner,
5068
+ name: parsed.repo,
5069
+ prNumber: parsed.prNumber
5070
+ });
5071
+ if (reviewThreadsRead.error)
5072
+ readErrors.push(reviewThreadsRead.error);
5073
+ const reviewThreads = reviewThreadsRead.value;
5074
+ const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
5075
+ let greptileCheckDetails = [];
5076
+ if (headSha && greptileRollupChecks.length > 0) {
5077
+ const checkDetailsRead = await collectGreptileCheckDetails({
5078
+ command: input.command,
5079
+ projectRoot: input.projectRoot,
5080
+ repoName: parsed.repoName,
5081
+ headSha
5082
+ });
5083
+ if (checkDetailsRead.error)
5084
+ readErrors.push(checkDetailsRead.error);
5085
+ greptileCheckDetails = checkDetailsRead.value;
5086
+ if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
5087
+ readErrors.push("Greptile check details could not be found for the current PR head");
5088
+ }
5089
+ }
5090
+ const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
5091
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
5092
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
5093
+ enabled: shouldCollectConfiguredGreptileApi,
5094
+ options: input.greptileApi,
5095
+ repoName: parsed.repoName,
5096
+ prNumber: parsed.prNumber,
5097
+ headSha,
5098
+ baseRefName
5099
+ });
5100
+ readErrors.push(...configuredGreptileApiRead.errors);
5101
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
5102
+ 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})` : ""}`);
5103
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
5104
+ const evidenceBase = {
5105
+ title: firstString(view, ["title"]),
5106
+ body: firstString(view, ["body"]),
5107
+ reviews,
5108
+ changedFileReviewComments: reviewComments,
5109
+ relevantIssueComments: issueComments,
5110
+ reviewThreads,
5111
+ checks: checksWithGreptileDetails,
5112
+ currentHeadSha: headSha,
5113
+ apiSignals
5114
+ };
5115
+ const greptile = deriveGreptileEvidence(evidenceBase);
5116
+ return {
5117
+ prUrl: input.prUrl,
5118
+ prNumber: parsed.prNumber,
5119
+ repoName: parsed.repoName,
5120
+ title: evidenceBase.title,
5121
+ body: evidenceBase.body,
5122
+ headSha,
5123
+ headRefName: firstString(view, ["headRefName"]),
5124
+ baseRefName,
5125
+ state: firstString(view, ["state"]),
5126
+ isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
5127
+ mergeable: firstString(view, ["mergeable"]),
5128
+ mergeStateStatus: firstString(view, ["mergeStateStatus"]),
5129
+ reviewDecision: firstString(view, ["reviewDecision"]),
5130
+ reviews,
5131
+ reviewThreads,
5132
+ changedFileReviewComments: reviewComments,
5133
+ relevantIssueComments: issueComments,
5134
+ statusCheckRollup: checksWithGreptileDetails,
5135
+ checkFailures,
5136
+ pendingChecks,
5137
+ readErrors,
5138
+ greptile
5139
+ };
5140
+ }
5141
+ function capGateMessage(value, maxChars = 1200) {
5142
+ const normalized = value.trim();
5143
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
5144
+ [truncated for gate summary; see full evidence artifact]` : normalized;
5145
+ }
5146
+ function evaluateEvidence(evidence) {
5147
+ const reasonDetails = [];
5148
+ const warnings = [];
5149
+ const seen = new Set;
5150
+ const addReason = (reason) => {
5151
+ const capped = { ...reason, message: capGateMessage(reason.message) };
5152
+ const key = `${capped.code}:${capped.message}`;
5153
+ if (seen.has(key))
5154
+ return;
5155
+ seen.add(key);
5156
+ reasonDetails.push(capped);
5157
+ };
5158
+ const greptile = evidence.greptile;
5159
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
5160
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
5161
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
5162
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
5163
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
5164
+ for (const error of evidence.readErrors) {
5165
+ addReason({
5166
+ code: "read_error",
5167
+ reasonClass: "reject",
5168
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
5169
+ suggestedAction: "needs_attention",
5170
+ message: `Required PR evidence surface could not be read completely: ${error}`,
5171
+ headSha: evidence.headSha || null
5172
+ });
5173
+ }
5174
+ if (!evidence.headSha) {
5175
+ addReason({
5176
+ code: "missing_head_sha",
5177
+ reasonClass: "reject",
5178
+ surface: "github",
5179
+ suggestedAction: "needs_attention",
5180
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
5181
+ headSha: null
5182
+ });
5183
+ }
5184
+ for (const failure of evidence.checkFailures) {
5185
+ addReason({
5186
+ code: "ci_failed",
5187
+ reasonClass: "reject",
5188
+ surface: "ci",
5189
+ suggestedAction: "fix",
5190
+ message: failure,
5191
+ headSha: evidence.headSha || null
5192
+ });
5193
+ }
5194
+ for (const pendingCheck of evidence.pendingChecks) {
5195
+ addReason({
5196
+ code: "check_pending",
5197
+ reasonClass: "pending",
5198
+ surface: "ci",
5199
+ suggestedAction: "wait",
5200
+ message: pendingCheck,
5201
+ headSha: evidence.headSha || null
5202
+ });
5203
+ }
5204
+ const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
5205
+ if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
5206
+ addReason({
5207
+ code: "review_decision_blocking",
5208
+ reasonClass: "reject",
5209
+ surface: "review",
5210
+ suggestedAction: "fix",
5211
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
5212
+ headSha: evidence.headSha || null
5213
+ });
5214
+ }
5215
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
5216
+ addReason({
5217
+ code: "review_thread_unresolved",
5218
+ reasonClass: "reject",
5219
+ surface: "review",
5220
+ suggestedAction: "fix",
5221
+ message: thread,
5222
+ headSha: evidence.headSha || null
5223
+ });
5224
+ }
5225
+ if (greptile.mapping === "missing") {
5226
+ addReason({
5227
+ code: "greptile_missing",
5228
+ reasonClass: "pending",
5229
+ surface: "greptile",
5230
+ suggestedAction: "wait",
5231
+ message: "Missing Greptile check/review evidence for this PR.",
5232
+ headSha: evidence.headSha || null
5233
+ });
5234
+ }
5235
+ if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
5236
+ addReason({
5237
+ code: "greptile_stale",
5238
+ reasonClass: "pending",
5239
+ surface: "greptile",
5240
+ suggestedAction: "wait",
5241
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
5242
+ headSha: evidence.headSha || null,
5243
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
5244
+ });
5245
+ }
5246
+ for (const signal of pendingGreptileApiSignals) {
5247
+ addReason({
5248
+ code: "greptile_pending",
5249
+ reasonClass: "pending",
5250
+ surface: "greptile",
5251
+ suggestedAction: "wait",
5252
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
5253
+ headSha: evidence.headSha || null,
5254
+ reviewedSha: signal.reviewedSha ?? null
5255
+ });
5256
+ }
5257
+ for (const signal of unknownGreptileApiSignals) {
5258
+ addReason({
5259
+ code: "greptile_api_status_unknown",
5260
+ reasonClass: "reject",
5261
+ surface: "greptile",
5262
+ suggestedAction: "needs_attention",
5263
+ 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}` : "."}`,
5264
+ headSha: evidence.headSha || null,
5265
+ reviewedSha: signal.reviewedSha ?? null
5266
+ });
5267
+ }
5268
+ if (!greptile.completed) {
5269
+ addReason({
5270
+ code: "greptile_pending",
5271
+ reasonClass: "pending",
5272
+ surface: "greptile",
5273
+ suggestedAction: "wait",
5274
+ message: "Greptile check/review has not completed for the current PR head.",
5275
+ headSha: evidence.headSha || null,
5276
+ reviewedSha: greptile.reviewedSha ?? null
5277
+ });
5278
+ }
5279
+ if (!greptile.fresh) {
5280
+ addReason({
5281
+ code: "greptile_not_current_head",
5282
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5283
+ surface: "greptile",
5284
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5285
+ message: "Greptile approval is not tied to the current PR head SHA.",
5286
+ headSha: evidence.headSha || null,
5287
+ reviewedSha: greptile.reviewedSha ?? null
5288
+ });
5289
+ }
5290
+ if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
5291
+ addReason({
5292
+ code: "greptile_score_not_5",
5293
+ reasonClass: "reject",
5294
+ surface: "greptile",
5295
+ suggestedAction: "fix",
5296
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
5297
+ headSha: evidence.headSha || null,
5298
+ reviewedSha: greptile.reviewedSha ?? null
5299
+ });
5300
+ }
5301
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
5302
+ if (!greptile.score && !hasApprovedMapping) {
5303
+ addReason({
5304
+ code: "greptile_score_missing",
5305
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5306
+ surface: "greptile",
5307
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5308
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
5309
+ headSha: evidence.headSha || null,
5310
+ reviewedSha: greptile.reviewedSha ?? null
5311
+ });
5312
+ }
5313
+ if (greptile.mapping === "unproven") {
5314
+ addReason({
5315
+ code: "greptile_mapping_unproven",
5316
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5317
+ surface: "greptile",
5318
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5319
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
5320
+ headSha: evidence.headSha || null,
5321
+ reviewedSha: greptile.reviewedSha ?? null
5322
+ });
5323
+ }
5324
+ for (const blocker of greptile.blockers) {
5325
+ addReason({
5326
+ code: "greptile_blocker_text",
5327
+ reasonClass: "reject",
5328
+ surface: "greptile",
5329
+ suggestedAction: "fix",
5330
+ message: `Greptile/blocker text: ${blocker}`,
5331
+ headSha: evidence.headSha || null,
5332
+ reviewedSha: greptile.reviewedSha ?? null
5333
+ });
5334
+ }
5335
+ for (const comment of greptile.unresolvedComments) {
5336
+ addReason({
5337
+ code: "greptile_unresolved_comment",
5338
+ reasonClass: "reject",
5339
+ surface: "greptile",
5340
+ suggestedAction: "fix",
5341
+ message: comment,
5342
+ headSha: evidence.headSha || null,
5343
+ reviewedSha: greptile.reviewedSha ?? null
5344
+ });
5345
+ }
5346
+ if (!greptile.approved)
5347
+ warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
5348
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
5349
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
5350
+ }
5351
+ function evaluateStrictPrMergeGate(evidence) {
5352
+ const evaluated = evaluateEvidence(evidence);
5353
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
5354
+ return {
5355
+ approved,
5356
+ pending: evaluated.pending,
5357
+ reasons: evaluated.reasons,
5358
+ reasonDetails: evaluated.reasonDetails,
5359
+ warnings: evaluated.warnings,
5360
+ actionableFeedback: evaluated.reasons,
5361
+ evidence
4234
5362
  };
4235
- mkdirSync7(artifactDir, { recursive: true });
4236
- writeFileSync7(resolve20(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
4237
- `, "utf-8");
4238
- return summary;
5363
+ }
5364
+ function strictMergeHeadShaFromGate(result, prUrl) {
5365
+ if (!result.approved) {
5366
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
5367
+ }
5368
+ if (result.evidence.prUrl !== prUrl) {
5369
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
5370
+ }
5371
+ const headSha = result.evidence.headSha?.trim();
5372
+ if (!headSha) {
5373
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
5374
+ }
5375
+ if (!/^[0-9a-f]{40}$/i.test(headSha)) {
5376
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
5377
+ }
5378
+ if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
5379
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
5380
+ }
5381
+ if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
5382
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
5383
+ }
5384
+ return headSha;
5385
+ }
5386
+ function promptExcerpt(value, maxChars = 4000) {
5387
+ return value.length > maxChars ? `${value.slice(0, maxChars)}
5388
+
5389
+ [truncated for prompt; see full evidence artifact]` : value;
5390
+ }
5391
+ function promptJsonExcerpt(value, maxChars = 6000) {
5392
+ return promptExcerpt(JSON.stringify(value, null, 2), maxChars);
5393
+ }
5394
+ function buildStrictPrGateSteeringPrompt(result) {
5395
+ const evidence = result.evidence;
5396
+ const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
5397
+ const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
5398
+ if (result.reasons.length > displayedReasons.length) {
5399
+ displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
5400
+ }
5401
+ const lines = [
5402
+ `Strict PR merge gate blocked ${evidence.prUrl}.`,
5403
+ `PR title: ${evidence.title || "(empty)"}`,
5404
+ `Current PR head SHA: ${evidence.headSha || "unknown"}`,
5405
+ `Greptile mapping: ${evidence.greptile.mapping}`,
5406
+ evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
5407
+ "",
5408
+ "Gate reasons:",
5409
+ ...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
5410
+ "",
5411
+ "Structured gate reason details:",
5412
+ result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
5413
+ "",
5414
+ "Required evidence read status:",
5415
+ evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
5416
+ "",
5417
+ "Full PR title:",
5418
+ evidence.title || "(empty)",
5419
+ "",
5420
+ "PR body excerpt:",
5421
+ evidence.body ? promptExcerpt(evidence.body) : "(empty)",
5422
+ "",
5423
+ "All review comments on changed files:",
5424
+ evidence.changedFileReviewComments.length ? promptJsonExcerpt(evidence.changedFileReviewComments) : "[]",
5425
+ "",
5426
+ "Unresolved review threads:",
5427
+ unresolvedReviewThreads.length ? promptJsonExcerpt(unresolvedReviewThreads) : "[]",
5428
+ "",
5429
+ "Relevant issue-level PR comments:",
5430
+ evidence.relevantIssueComments.length ? promptJsonExcerpt(evidence.relevantIssueComments) : "[]",
5431
+ "",
5432
+ "CI/check failures and pending checks:",
5433
+ promptJsonExcerpt({ failures: evidence.checkFailures, pending: evidence.pendingChecks, rollup: evidence.statusCheckRollup }),
5434
+ "",
5435
+ "Greptile evidence:",
5436
+ promptJsonExcerpt(evidence.greptile)
5437
+ ];
5438
+ if (result.artifacts) {
5439
+ lines.push("", "Full evidence artifacts:", JSON.stringify(result.artifacts, null, 2));
5440
+ }
5441
+ return lines.join(`
5442
+ `);
5443
+ }
5444
+ function persistPrReviewCycleArtifacts(input) {
5445
+ const cycleName = input.final ? `${input.cycle}-final` : String(input.cycle);
5446
+ const taskArtifactRoot = input.artifactRoot?.trim() ? input.artifactRoot : resolve21(input.projectRoot, "artifacts", input.taskId);
5447
+ const root = resolve21(taskArtifactRoot, "pr-review-cycles", cycleName);
5448
+ mkdirSync8(root, { recursive: true });
5449
+ const finalMergeGateResultPath = input.final ? resolve21(taskArtifactRoot, "merge-gate-final.json") : undefined;
5450
+ const paths = {
5451
+ root,
5452
+ prTitlePath: resolve21(root, "pr-title.md"),
5453
+ prBodyPath: resolve21(root, "pr-body.md"),
5454
+ prCommentsPath: resolve21(root, "pr-comments.json"),
5455
+ reviewThreadsPath: resolve21(root, "review-threads.json"),
5456
+ reviewCommentsPath: resolve21(root, "review-comments.json"),
5457
+ checkRollupPath: resolve21(root, "check-rollup.json"),
5458
+ greptileEvidencePath: resolve21(root, "greptile-evidence.json"),
5459
+ mergeGateResultPath: resolve21(root, "merge-gate-result.json"),
5460
+ steeringPromptPath: resolve21(root, "agent-steering-prompt.md"),
5461
+ ...finalMergeGateResultPath ? { finalMergeGateResultPath } : {}
5462
+ };
5463
+ writeFileSync8(paths.prTitlePath, input.result.evidence.title || "", "utf8");
5464
+ writeFileSync8(paths.prBodyPath, input.result.evidence.body || "", "utf8");
5465
+ writeFileSync8(paths.prCommentsPath, `${JSON.stringify(input.result.evidence.relevantIssueComments, null, 2)}
5466
+ `, "utf8");
5467
+ writeFileSync8(paths.reviewThreadsPath, `${JSON.stringify(input.result.evidence.reviewThreads, null, 2)}
5468
+ `, "utf8");
5469
+ writeFileSync8(paths.reviewCommentsPath, `${JSON.stringify(input.result.evidence.changedFileReviewComments, null, 2)}
5470
+ `, "utf8");
5471
+ writeFileSync8(paths.checkRollupPath, `${JSON.stringify(input.result.evidence.statusCheckRollup, null, 2)}
5472
+ `, "utf8");
5473
+ writeFileSync8(paths.greptileEvidencePath, `${JSON.stringify(input.result.evidence.greptile, null, 2)}
5474
+ `, "utf8");
5475
+ const mergeGatePayload = {
5476
+ approved: input.result.approved,
5477
+ pending: input.result.pending,
5478
+ reasons: input.result.reasons,
5479
+ reasonDetails: input.result.reasonDetails,
5480
+ warnings: input.result.warnings,
5481
+ actionableFeedback: input.result.actionableFeedback,
5482
+ prUrl: input.result.evidence.prUrl,
5483
+ title: input.result.evidence.title,
5484
+ headSha: input.result.evidence.headSha,
5485
+ readErrors: input.result.evidence.readErrors,
5486
+ greptile: input.result.evidence.greptile,
5487
+ evidence: input.result.evidence,
5488
+ cycleArtifactRoot: root
5489
+ };
5490
+ writeFileSync8(paths.mergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
5491
+ `, "utf8");
5492
+ if (paths.finalMergeGateResultPath) {
5493
+ writeFileSync8(paths.finalMergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
5494
+ `, "utf8");
5495
+ }
5496
+ writeFileSync8(paths.steeringPromptPath, input.steeringPrompt, "utf8");
5497
+ return paths;
5498
+ }
5499
+ async function runStrictPrMergeGate(input) {
5500
+ const evidence = await collectPrReviewEvidence(input);
5501
+ const base = evaluateStrictPrMergeGate(evidence);
5502
+ const preliminaryPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts: undefined });
5503
+ const artifacts = persistPrReviewCycleArtifacts({
5504
+ projectRoot: input.projectRoot,
5505
+ taskId: input.taskId,
5506
+ cycle: input.cycle,
5507
+ artifactRoot: input.artifactRoot,
5508
+ result: base,
5509
+ steeringPrompt: preliminaryPrompt,
5510
+ final: input.final
5511
+ });
5512
+ const steeringPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts });
5513
+ writeFileSync8(artifacts.steeringPromptPath, steeringPrompt, "utf8");
5514
+ return { ...base, artifacts, steeringPrompt };
4239
5515
  }
4240
5516
 
4241
5517
  // packages/runtime/src/control-plane/native/verifier.ts
4242
- import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
4243
- import { resolve as resolve21 } from "path";
4244
5518
  async function verifyTask(options) {
4245
5519
  const paths = resolveHarnessPaths(options.projectRoot);
4246
5520
  const taskId = options.taskId;
4247
5521
  const normalizedTaskId = lookupTask(options.projectRoot, taskId);
4248
5522
  const artifactDir = artifactDirForId(options.projectRoot, taskId);
4249
- mkdirSync8(artifactDir, { recursive: true });
4250
- const validationSummaryPath = resolve21(artifactDir, "validation-summary.json");
4251
- const reviewFeedbackPath = resolve21(artifactDir, "review-feedback.md");
4252
- const reviewStatePath = resolve21(artifactDir, "review-state.json");
4253
- const greptileRawPath = resolve21(artifactDir, "review-greptile-raw.json");
5523
+ mkdirSync9(artifactDir, { recursive: true });
5524
+ const validationSummaryPath = resolve22(artifactDir, "validation-summary.json");
5525
+ const reviewFeedbackPath = resolve22(artifactDir, "review-feedback.md");
5526
+ const reviewStatePath = resolve22(artifactDir, "review-state.json");
5527
+ const greptileRawPath = resolve22(artifactDir, "review-greptile-raw.json");
4254
5528
  const prStates = readPrMetadata(options.projectRoot, taskId);
4255
5529
  const prState = prStates[0] || null;
4256
5530
  const localReasons = [];
@@ -4271,12 +5545,12 @@ async function verifyTask(options) {
4271
5545
  }
4272
5546
  }
4273
5547
  for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
4274
- const requiredPath = resolve21(artifactDir, file);
5548
+ const requiredPath = resolve22(artifactDir, file);
4275
5549
  if (!existsSync18(requiredPath)) {
4276
5550
  localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
4277
5551
  }
4278
5552
  }
4279
- const taskResultPath = resolve21(artifactDir, "task-result.json");
5553
+ const taskResultPath = resolve22(artifactDir, "task-result.json");
4280
5554
  if (existsSync18(taskResultPath)) {
4281
5555
  const taskResult = await readJsonFile2(taskResultPath);
4282
5556
  const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
@@ -4290,7 +5564,7 @@ async function verifyTask(options) {
4290
5564
  localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
4291
5565
  }
4292
5566
  }
4293
- const nextActionsPath = resolve21(artifactDir, "next-actions.md");
5567
+ const nextActionsPath = resolve22(artifactDir, "next-actions.md");
4294
5568
  if (existsSync18(nextActionsPath)) {
4295
5569
  const nextActionsContent = await Bun.file(nextActionsPath).text();
4296
5570
  if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
@@ -4328,7 +5602,7 @@ async function verifyTask(options) {
4328
5602
  aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
4329
5603
  }
4330
5604
  if (persistArtifacts && ai.rawResponse) {
4331
- writeFileSync8(greptileRawPath, `${ai.rawResponse}
5605
+ writeFileSync9(greptileRawPath, `${ai.rawResponse}
4332
5606
  `, "utf-8");
4333
5607
  }
4334
5608
  } else if (!options.skipAiReview && reviewMode === "off") {
@@ -4837,7 +6111,7 @@ function writeFeedbackFile(options) {
4837
6111
  if (options.aiRawFeedback) {
4838
6112
  lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
4839
6113
  }
4840
- writeFileSync8(options.output, `${lines.join(`
6114
+ writeFileSync9(options.output, `${lines.join(`
4841
6115
  `)}
4842
6116
  `, "utf-8");
4843
6117
  }
@@ -4854,7 +6128,7 @@ function writeReviewStateFile(options) {
4854
6128
  ai_warnings: options.aiWarnings,
4855
6129
  updated_at: nowIso()
4856
6130
  };
4857
- writeFileSync8(options.output, `${JSON.stringify(payload, null, 2)}
6131
+ writeFileSync9(options.output, `${JSON.stringify(payload, null, 2)}
4858
6132
  `, "utf-8");
4859
6133
  }
4860
6134
  async function runGreptileReviewForPr(options) {
@@ -5036,7 +6310,8 @@ async function runGreptileReviewForPr(options) {
5036
6310
  }
5037
6311
  };
5038
6312
  }
5039
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
6313
+ const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
6314
+ 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)) {
5040
6315
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
5041
6316
  return {
5042
6317
  verdict: "REJECT",
@@ -5052,44 +6327,79 @@ async function runGreptileReviewForPr(options) {
5052
6327
  }
5053
6328
  };
5054
6329
  }
5055
- if (score) {
5056
- if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
5057
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
5058
- return {
5059
- verdict: "REJECT",
5060
- feedback,
5061
- reasons,
5062
- warnings,
5063
- rawPayload: {
5064
- pr: options.prState,
5065
- codeReviews: reviewsPayload,
5066
- selectedReview,
5067
- reviewDetails,
5068
- comments: commentsPayload,
5069
- score
5070
- }
5071
- };
5072
- }
5073
- if (score.scale === 5 && score.value <= 2) {
5074
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
5075
- return {
5076
- verdict: "REJECT",
5077
- feedback,
5078
- reasons,
5079
- warnings,
5080
- rawPayload: {
5081
- pr: options.prState,
5082
- codeReviews: reviewsPayload,
5083
- selectedReview,
5084
- reviewDetails,
5085
- comments: commentsPayload,
5086
- score
6330
+ if (score?.scale === 5 && score.value < 5) {
6331
+ reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
6332
+ return {
6333
+ verdict: "REJECT",
6334
+ feedback,
6335
+ reasons,
6336
+ warnings,
6337
+ rawPayload: {
6338
+ pr: options.prState,
6339
+ codeReviews: reviewsPayload,
6340
+ selectedReview,
6341
+ reviewDetails,
6342
+ comments: commentsPayload,
6343
+ score
6344
+ }
6345
+ };
6346
+ }
6347
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
6348
+ let strictGate = null;
6349
+ try {
6350
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
6351
+ projectRoot: options.projectRoot,
6352
+ taskId: options.taskId,
6353
+ prUrl,
6354
+ apiSignals: [{
6355
+ id: selectedReview.id,
6356
+ body: reviewBody,
6357
+ reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
6358
+ status: selectedReview.status
6359
+ }]
6360
+ });
6361
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
6362
+ } catch (error) {
6363
+ reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
6364
+ return {
6365
+ verdict: "REJECT",
6366
+ feedback,
6367
+ reasons,
6368
+ warnings,
6369
+ rawPayload: {
6370
+ pr: options.prState,
6371
+ codeReviews: reviewsPayload,
6372
+ selectedReview,
6373
+ reviewDetails,
6374
+ comments: commentsPayload,
6375
+ score
6376
+ }
6377
+ };
6378
+ }
6379
+ if (!strictGate.approved) {
6380
+ return {
6381
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
6382
+ feedback,
6383
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
6384
+ warnings: [...warnings, ...strictGate.warnings],
6385
+ rawPayload: {
6386
+ pr: options.prState,
6387
+ codeReviews: reviewsPayload,
6388
+ selectedReview,
6389
+ reviewDetails,
6390
+ comments: commentsPayload,
6391
+ score,
6392
+ strictGate: {
6393
+ approved: strictGate.approved,
6394
+ pending: strictGate.pending,
6395
+ reasons: strictGate.reasons,
6396
+ reasonDetails: strictGate.reasonDetails,
6397
+ warnings: strictGate.warnings,
6398
+ greptile: strictGate.evidence.greptile,
6399
+ readErrors: strictGate.evidence.readErrors
5087
6400
  }
5088
- };
5089
- }
5090
- if (score.scale === 5 && score.value < 5) {
5091
- warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
5092
- }
6401
+ }
6402
+ };
5093
6403
  }
5094
6404
  return {
5095
6405
  verdict: "APPROVE",
@@ -5101,7 +6411,16 @@ async function runGreptileReviewForPr(options) {
5101
6411
  codeReviews: reviewsPayload,
5102
6412
  selectedReview,
5103
6413
  reviewDetails,
5104
- comments: commentsPayload
6414
+ comments: commentsPayload,
6415
+ strictGate: {
6416
+ approved: strictGate.approved,
6417
+ pending: strictGate.pending,
6418
+ reasons: strictGate.reasons,
6419
+ reasonDetails: strictGate.reasonDetails,
6420
+ warnings: strictGate.warnings,
6421
+ greptile: strictGate.evidence.greptile,
6422
+ readErrors: strictGate.evidence.readErrors
6423
+ }
5105
6424
  }
5106
6425
  };
5107
6426
  }
@@ -5125,7 +6444,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5125
6444
  let threads = [];
5126
6445
  let actionableThreads = [];
5127
6446
  let checkRollup = [];
5128
- let checkState = { pending: false, completed: false };
6447
+ let checkState2 = { pending: false, completed: false };
5129
6448
  for (let attempt = 0;; attempt += 1) {
5130
6449
  reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
5131
6450
  selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
@@ -5134,15 +6453,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5134
6453
  threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
5135
6454
  actionableThreads = filterActionableGithubGreptileThreads(threads);
5136
6455
  checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
5137
- checkState = classifyGithubGreptileCheckState(checkRollup);
5138
- const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
6456
+ checkState2 = classifyGithubGreptileCheckState(checkRollup);
6457
+ const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
5139
6458
  if (!shouldContinueGithubGreptileFallbackPolling({
5140
6459
  attempt,
5141
6460
  pollAttempts: options.pollAttempts,
5142
- checkState,
6461
+ checkState: checkState2,
5143
6462
  fallbackReview,
5144
6463
  selectedReview,
5145
- approvedViaReviewedAncestor: approvedViaReviewedAncestor2
6464
+ approvedViaReviewedAncestor
5146
6465
  })) {
5147
6466
  break;
5148
6467
  }
@@ -5170,7 +6489,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5170
6489
  ].filter(Boolean).join(`
5171
6490
  `);
5172
6491
  const warnings = buildGithubGreptileFallbackWarnings(options);
5173
- if (checkState.pending) {
6492
+ if (checkState2.pending) {
5174
6493
  return {
5175
6494
  verdict: "SKIP",
5176
6495
  feedback,
@@ -5181,34 +6500,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5181
6500
  rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
5182
6501
  };
5183
6502
  }
5184
- const approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
5185
- if (!fallbackReview) {
5186
- if (approvedViaCompletedCheck) {
5187
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no GitHub Greptile review object, but the Greptile check completed successfully and no unresolved Greptile threads remain.`);
5188
- return {
5189
- verdict: "APPROVE",
5190
- feedback,
5191
- reasons: [],
5192
- warnings,
5193
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
5194
- };
5195
- }
5196
- return {
5197
- verdict: "SKIP",
5198
- feedback,
5199
- reasons: [
5200
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
5201
- ],
5202
- warnings,
5203
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
5204
- };
5205
- }
5206
- const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
5207
- if (actionableThreads.length > 0) {
6503
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
6504
+ let strictGate;
6505
+ try {
6506
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
6507
+ projectRoot: options.projectRoot,
6508
+ taskId: options.taskId,
6509
+ prUrl
6510
+ });
6511
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
6512
+ } catch (error) {
5208
6513
  return {
5209
6514
  verdict: "REJECT",
5210
6515
  feedback,
5211
- reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
6516
+ reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
5212
6517
  warnings,
5213
6518
  rawPayload: {
5214
6519
  pr: options.prState,
@@ -5221,44 +6526,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5221
6526
  }
5222
6527
  };
5223
6528
  }
5224
- if (!selectedReview && !approvedViaReviewedAncestor) {
5225
- if (approvedViaCompletedCheck) {
5226
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
5227
- return {
5228
- verdict: "APPROVE",
5229
- feedback,
5230
- reasons: [],
5231
- warnings,
5232
- rawPayload: {
5233
- pr: options.prState,
5234
- selectedReview: fallbackReview,
5235
- reviews,
5236
- threads,
5237
- checkRollup,
5238
- ...buildGithubGreptileFallbackRawPayload(options)
5239
- }
5240
- };
5241
- }
6529
+ if (!strictGate.approved) {
5242
6530
  return {
5243
- verdict: "SKIP",
6531
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
5244
6532
  feedback,
5245
- reasons: [
5246
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
5247
- ],
5248
- warnings,
6533
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
6534
+ warnings: [...warnings, ...strictGate.warnings],
5249
6535
  rawPayload: {
5250
6536
  pr: options.prState,
5251
6537
  selectedReview: fallbackReview,
5252
6538
  reviews,
5253
6539
  threads,
5254
6540
  checkRollup,
6541
+ actionableThreads,
6542
+ strictGate: {
6543
+ approved: strictGate.approved,
6544
+ pending: strictGate.pending,
6545
+ reasons: strictGate.reasons,
6546
+ reasonDetails: strictGate.reasonDetails,
6547
+ warnings: strictGate.warnings,
6548
+ greptile: strictGate.evidence.greptile
6549
+ },
5255
6550
  ...buildGithubGreptileFallbackRawPayload(options)
5256
6551
  }
5257
6552
  };
5258
6553
  }
5259
- if (approvedViaReviewedAncestor) {
5260
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
5261
- }
5262
6554
  return {
5263
6555
  verdict: "APPROVE",
5264
6556
  feedback,
@@ -5270,6 +6562,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5270
6562
  reviews,
5271
6563
  threads,
5272
6564
  checkRollup,
6565
+ strictGate: {
6566
+ approved: strictGate.approved,
6567
+ pending: strictGate.pending,
6568
+ reasons: strictGate.reasons,
6569
+ reasonDetails: strictGate.reasonDetails,
6570
+ warnings: strictGate.warnings,
6571
+ greptile: strictGate.evidence.greptile
6572
+ },
5273
6573
  ...buildGithubGreptileFallbackRawPayload(options)
5274
6574
  }
5275
6575
  };
@@ -5382,19 +6682,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
5382
6682
  if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
5383
6683
  return true;
5384
6684
  }
5385
- return isGreptileReviewTerminal(existingReview.status);
6685
+ return false;
5386
6686
  }
5387
6687
  function shouldContinueGreptileMcpPolling(options) {
5388
6688
  if (options.githubCheckState.completed) {
5389
6689
  return false;
5390
6690
  }
6691
+ if (options.attempt + 1 >= options.pollAttempts) {
6692
+ return false;
6693
+ }
5391
6694
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
5392
6695
  return true;
5393
6696
  }
5394
- return options.attempt + 1 < options.pollAttempts;
6697
+ return true;
5395
6698
  }
5396
6699
  function shouldContinueGithubGreptileFallbackPolling(options) {
5397
6700
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
6701
+ if (options.attempt + 1 >= options.pollAttempts) {
6702
+ return false;
6703
+ }
5398
6704
  if (waitingForVisiblePendingReview) {
5399
6705
  return true;
5400
6706
  }
@@ -5455,6 +6761,20 @@ function runGhJson(projectRoot, args) {
5455
6761
  throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
5456
6762
  }
5457
6763
  }
6764
+ async function collectStrictPrEvidenceForVerifier(input) {
6765
+ return collectPrReviewEvidence({
6766
+ projectRoot: input.projectRoot,
6767
+ prUrl: input.prUrl,
6768
+ taskId: input.taskId,
6769
+ runId: "verifier",
6770
+ cycle: 0,
6771
+ apiSignals: input.apiSignals ?? [],
6772
+ command: async (args, options) => {
6773
+ const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
6774
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
6775
+ }
6776
+ });
6777
+ }
5458
6778
  function deriveRepoName(projectRoot, prState) {
5459
6779
  const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
5460
6780
  if (fromUrl?.[1]) {
@@ -5469,8 +6789,9 @@ function resolvePrHeadSha(projectRoot, prState) {
5469
6789
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5470
6790
  return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
5471
6791
  }
5472
- function isGreptileGithubLogin(login) {
5473
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
6792
+ function isGreptileGithubLogin2(login) {
6793
+ const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
6794
+ return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
5474
6795
  }
5475
6796
  function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
5476
6797
  const matching = sortGithubGreptileReviews(reviews);
@@ -5487,7 +6808,7 @@ function pickLatestGithubGreptileReview(reviews) {
5487
6808
  return sortGithubGreptileReviews(reviews)[0] || null;
5488
6809
  }
5489
6810
  function sortGithubGreptileReviews(reviews) {
5490
- return reviews.filter((review) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
6811
+ return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
5491
6812
  }
5492
6813
  function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
5493
6814
  const response = runGhJson(projectRoot, [
@@ -5560,32 +6881,6 @@ function classifyGithubGreptileCheckState(checks) {
5560
6881
  }
5561
6882
  return { pending: false, completed: false };
5562
6883
  }
5563
- function isGithubGreptileCheckApproved(checks) {
5564
- const greptileChecks = checks.filter((check) => {
5565
- const label = (check.name || check.context || "").toLowerCase();
5566
- return label.includes("greptile");
5567
- });
5568
- if (greptileChecks.length === 0) {
5569
- return false;
5570
- }
5571
- for (const check of greptileChecks) {
5572
- if ((check.__typename || "") === "CheckRun") {
5573
- if ((check.status || "").toUpperCase() !== "COMPLETED") {
5574
- return false;
5575
- }
5576
- const conclusion = (check.conclusion || "").toUpperCase();
5577
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
5578
- return false;
5579
- }
5580
- continue;
5581
- }
5582
- const state = (check.state || "").toUpperCase();
5583
- if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
5584
- return false;
5585
- }
5586
- }
5587
- return true;
5588
- }
5589
6884
  function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
5590
6885
  const [owner, name] = repoName.split("/");
5591
6886
  if (!owner || !name) {
@@ -5611,7 +6906,7 @@ function filterActionableGithubGreptileThreads(threads) {
5611
6906
  return [];
5612
6907
  }
5613
6908
  const comments = thread.comments?.nodes || [];
5614
- const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
6909
+ const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
5615
6910
  if (!latestGreptileComment?.path?.trim()) {
5616
6911
  return [];
5617
6912
  }
@@ -5620,7 +6915,7 @@ function filterActionableGithubGreptileThreads(threads) {
5620
6915
  }
5621
6916
  function resolvePrRepoRoot(projectRoot, prState) {
5622
6917
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
5623
- if (prState.target === "monorepo" && runtimeWorkspace && existsSync18(resolve21(runtimeWorkspace, ".git"))) {
6918
+ if (prState.target === "monorepo" && runtimeWorkspace && existsSync18(resolve22(runtimeWorkspace, ".git"))) {
5624
6919
  return runtimeWorkspace;
5625
6920
  }
5626
6921
  const paths = resolveHarnessPaths(projectRoot);
@@ -5633,11 +6928,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
5633
6928
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5634
6929
  return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
5635
6930
  }
5636
- function stripHtml(input) {
5637
- return input.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
5638
-
5639
- `).trim();
5640
- }
5641
6931
  function summarizeComment(input) {
5642
6932
  const text = stripHtml(input).replace(/\s+/g, " ").trim();
5643
6933
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
@@ -5646,31 +6936,14 @@ function asGreptileInfrastructureWarning(reason) {
5646
6936
  return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
5647
6937
  }
5648
6938
  function isAiReviewApproved(input) {
6939
+ if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
6940
+ return false;
6941
+ }
5649
6942
  if (input.reviewMode !== "required") {
5650
6943
  return true;
5651
6944
  }
5652
6945
  return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
5653
6946
  }
5654
- function parseGreptileScore(input) {
5655
- const text = stripHtml(input);
5656
- const patterns = [
5657
- /confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
5658
- /\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
5659
- /\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
5660
- ];
5661
- for (const pattern of patterns) {
5662
- const match = pattern.exec(text);
5663
- if (!match) {
5664
- continue;
5665
- }
5666
- const value = Number.parseInt(match[1] || "", 10);
5667
- const scale = Number.parseInt(match[2] || "", 10);
5668
- if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
5669
- return { value, scale };
5670
- }
5671
- }
5672
- return null;
5673
- }
5674
6947
 
5675
6948
  // packages/runtime/src/control-plane/provider/runtime-instructions.ts
5676
6949
  var CLAUDE_ROUTER_TOOL_NAMES = [
@@ -5710,14 +6983,14 @@ function taskArtifacts(projectRoot, taskId) {
5710
6983
  throw new Error("No active task.");
5711
6984
  }
5712
6985
  const paths = resolveHarnessPaths(projectRoot);
5713
- const artifactDir = resolve22(paths.artifactsDir, activeTask);
5714
- mkdirSync9(artifactDir, { recursive: true });
6986
+ const artifactDir = resolve23(paths.artifactsDir, activeTask);
6987
+ mkdirSync10(artifactDir, { recursive: true });
5715
6988
  const changed = changedFilesForTask(projectRoot, activeTask, true);
5716
- writeFileSync9(resolve22(artifactDir, "changed-files.txt"), `${changed.join(`
6989
+ writeFileSync10(resolve23(artifactDir, "changed-files.txt"), `${changed.join(`
5717
6990
  `)}
5718
6991
  `, "utf-8");
5719
6992
  console.log(`changed-files.txt: ${changed.length} files`);
5720
- const taskResultPath = resolve22(artifactDir, "task-result.json");
6993
+ const taskResultPath = resolve23(artifactDir, "task-result.json");
5721
6994
  if (!existsSync19(taskResultPath)) {
5722
6995
  const template = {
5723
6996
  task_id: activeTask,
@@ -5725,24 +6998,24 @@ function taskArtifacts(projectRoot, taskId) {
5725
6998
  summary: "TODO: Write a one-line summary of what you did",
5726
6999
  completed_at: nowIso()
5727
7000
  };
5728
- writeFileSync9(taskResultPath, `${JSON.stringify(template, null, 2)}
7001
+ writeFileSync10(taskResultPath, `${JSON.stringify(template, null, 2)}
5729
7002
  `, "utf-8");
5730
7003
  console.log("task-result.json: created (update the summary!)");
5731
7004
  } else {
5732
7005
  console.log("task-result.json: already exists");
5733
7006
  }
5734
- const decisionLogPath = resolve22(artifactDir, "decision-log.md");
7007
+ const decisionLogPath = resolve23(artifactDir, "decision-log.md");
5735
7008
  if (!existsSync19(decisionLogPath)) {
5736
7009
  const content = `# Decision Log: ${activeTask}
5737
7010
 
5738
7011
  Record key decisions here using: rig-agent record decision "..."
5739
7012
  `;
5740
- writeFileSync9(decisionLogPath, content, "utf-8");
7013
+ writeFileSync10(decisionLogPath, content, "utf-8");
5741
7014
  console.log("decision-log.md: created (record your decisions!)");
5742
7015
  } else {
5743
7016
  console.log("decision-log.md: already exists");
5744
7017
  }
5745
- const nextActionsPath = resolve22(artifactDir, "next-actions.md");
7018
+ const nextActionsPath = resolve23(artifactDir, "next-actions.md");
5746
7019
  if (!existsSync19(nextActionsPath)) {
5747
7020
  const content = [
5748
7021
  `# Next Actions: ${activeTask}`,
@@ -5760,12 +7033,12 @@ Record key decisions here using: rig-agent record decision "..."
5760
7033
  ""
5761
7034
  ].join(`
5762
7035
  `);
5763
- writeFileSync9(nextActionsPath, content, "utf-8");
7036
+ writeFileSync10(nextActionsPath, content, "utf-8");
5764
7037
  console.log("next-actions.md: created (add recommendations for downstream tasks!)");
5765
7038
  } else {
5766
7039
  console.log("next-actions.md: already exists");
5767
7040
  }
5768
- const validationSummaryPath = resolve22(artifactDir, "validation-summary.json");
7041
+ const validationSummaryPath = resolve23(artifactDir, "validation-summary.json");
5769
7042
  if (existsSync19(validationSummaryPath)) {
5770
7043
  console.log("validation-summary.json: already exists");
5771
7044
  } else {
@@ -5832,7 +7105,7 @@ function collectTaskChangedFiles(projectRoot, taskId, includeCommitted) {
5832
7105
  [projectRoot, ""],
5833
7106
  [monorepoRepoRoot, ""]
5834
7107
  ]) {
5835
- if (!existsSync19(resolve22(repo, ".git"))) {
7108
+ if (!existsSync19(resolve23(repo, ".git"))) {
5836
7109
  continue;
5837
7110
  }
5838
7111
  if (includeCommitted && repo === monorepoRepoRoot) {
@@ -5870,8 +7143,8 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
5870
7143
  }
5871
7144
  function resolveTaskMonorepoRoot(projectRoot) {
5872
7145
  const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
5873
- if (runtimeWorkspace && existsSync19(resolve22(runtimeWorkspace, ".git"))) {
5874
- return resolve22(runtimeWorkspace);
7146
+ if (runtimeWorkspace && existsSync19(resolve23(runtimeWorkspace, ".git"))) {
7147
+ return resolve23(runtimeWorkspace);
5875
7148
  }
5876
7149
  return resolveHarnessPaths(projectRoot).monorepoRoot;
5877
7150
  }
@@ -5899,7 +7172,7 @@ function resolveRuntimeInitialHeadCommit(projectRoot, repo) {
5899
7172
  const runtimeContext = loadRuntimeContextFromEnv();
5900
7173
  if (runtimeContext?.initialHeadCommits?.monorepo?.trim()) {
5901
7174
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5902
- if (resolve22(monorepoRoot) === resolve22(repo)) {
7175
+ if (resolve23(monorepoRoot) === resolve23(repo)) {
5903
7176
  return runtimeContext.initialHeadCommits.monorepo.trim();
5904
7177
  }
5905
7178
  }
@@ -5909,7 +7182,7 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
5909
7182
  const runtimeContext = loadRuntimeContextFromEnv();
5910
7183
  if (runtimeContext?.monorepoBaseCommit?.trim()) {
5911
7184
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5912
- if (resolve22(monorepoRoot) === resolve22(repo)) {
7185
+ if (resolve23(monorepoRoot) === resolve23(repo)) {
5913
7186
  return runtimeContext.monorepoBaseCommit.trim();
5914
7187
  }
5915
7188
  }
@@ -5943,7 +7216,7 @@ function resolveRuntimeDirtyBaseline(projectRoot, repo) {
5943
7216
  return new Set;
5944
7217
  }
5945
7218
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5946
- const selected = resolve22(repo) === resolve22(monorepoRoot) ? dirtyFiles.monorepo : resolve22(repo) === resolve22(projectRoot) ? dirtyFiles.project : undefined;
7219
+ const selected = resolve23(repo) === resolve23(monorepoRoot) ? dirtyFiles.monorepo : resolve23(repo) === resolve23(projectRoot) ? dirtyFiles.project : undefined;
5947
7220
  return new Set((selected || []).map((file) => normalizeChangedFilePath(file)).filter(Boolean));
5948
7221
  }
5949
7222
  function normalizeChangedFilePath(file) {
@@ -5993,16 +7266,16 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
5993
7266
  "task-result.json",
5994
7267
  "validation-summary.json"
5995
7268
  ]);
5996
- function resolveHostRigBinDir(root) {
5997
- return resolve23(root, ".rig", "bin");
5998
- }
5999
7269
  function isRuntimeGatewayGitPath(candidate) {
6000
7270
  return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
6001
7271
  }
7272
+ function isRuntimeGatewayGhPath(candidate) {
7273
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
7274
+ }
6002
7275
  function resolveOptionalMonorepoRoot(projectRoot) {
6003
7276
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
6004
- if (runtimeWorkspace && existsSync20(resolve23(runtimeWorkspace, ".git"))) {
6005
- return resolve23(runtimeWorkspace);
7277
+ if (runtimeWorkspace && existsSync20(resolve24(runtimeWorkspace, ".git"))) {
7278
+ return resolve24(runtimeWorkspace);
6006
7279
  }
6007
7280
  try {
6008
7281
  return resolveMonorepoRoot2(projectRoot);
@@ -6033,6 +7306,9 @@ function resolveGitBinary(projectRoot) {
6033
7306
  }
6034
7307
  return "git";
6035
7308
  }
7309
+ function escapeRegExp(value) {
7310
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7311
+ }
6036
7312
  function safeCurrentTaskId(projectRoot) {
6037
7313
  try {
6038
7314
  const taskId = currentTaskId(projectRoot);
@@ -6051,7 +7327,7 @@ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
6051
7327
  }
6052
7328
  const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot2(projectRoot) : projectRoot;
6053
7329
  const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
6054
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
7330
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6055
7331
  throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
6056
7332
  }
6057
7333
  const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
@@ -6101,28 +7377,26 @@ function gitOpenPr(options) {
6101
7377
  const target = options.target || (taskId ? "monorepo" : "project");
6102
7378
  let repoRoot = options.projectRoot;
6103
7379
  let repoLabel = "project-rig";
6104
- let defaultBase = process.env.RIG_PR_BASE_PROJECT || "main";
7380
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
6105
7381
  if (target === "monorepo") {
6106
7382
  repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot2(options.projectRoot);
6107
7383
  repoLabel = "monorepo";
6108
- defaultBase = process.env.RIG_PR_BASE_MONOREPO || "main";
6109
7384
  if (taskId) {
6110
7385
  gitSyncBranch(options.projectRoot, taskId, "monorepo");
6111
7386
  }
6112
7387
  } else if (taskId) {
6113
7388
  gitSyncBranch(options.projectRoot, taskId, "project");
6114
- defaultBase = inferProjectBase(options.projectRoot, defaultBase);
6115
7389
  }
6116
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
7390
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6117
7391
  throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
6118
7392
  }
6119
7393
  const branch = branchName(options.projectRoot, repoRoot);
6120
7394
  if (!branch || branch === "HEAD") {
6121
7395
  throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
6122
7396
  }
6123
- const base = options.base || defaultBase;
6124
7397
  const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
6125
7398
  const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
7399
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
6126
7400
  refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
6127
7401
  let reviewer = (options.reviewer || "").trim();
6128
7402
  let reviewerSource = reviewer ? "flag" : undefined;
@@ -6158,10 +7432,11 @@ function gitOpenPr(options) {
6158
7432
  "",
6159
7433
  "## Task",
6160
7434
  `- beads: ${taskId || "n/a"}`,
7435
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
6161
7436
  "",
6162
7437
  "## Review",
6163
7438
  "- Completion verification will run validation, verifier review, and PR policy checks.",
6164
- "- When repository policy allows it, Rig enables GitHub auto-merge after approval."
7439
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
6165
7440
  ].join(`
6166
7441
  `);
6167
7442
  const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
@@ -6249,6 +7524,30 @@ function gitOpenPr(options) {
6249
7524
  }
6250
7525
  return result;
6251
7526
  }
7527
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
7528
+ const lines = [];
7529
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
7530
+ if (runId) {
7531
+ lines.push(`- Run: ${runId}`);
7532
+ }
7533
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
7534
+ if (closeout) {
7535
+ lines.push(`- ${closeout}`);
7536
+ }
7537
+ return lines;
7538
+ }
7539
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
7540
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
7541
+ if (sourceIssueId) {
7542
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
7543
+ if (match?.[1] && match[2]) {
7544
+ const sourceRepo = match[1];
7545
+ const issueNumber = match[2];
7546
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
7547
+ }
7548
+ }
7549
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
7550
+ }
6252
7551
  function resolveTaskBranchRef(projectRoot, taskId) {
6253
7552
  return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
6254
7553
  }
@@ -6321,7 +7620,7 @@ function gitMergePr(options) {
6321
7620
  }
6322
7621
  const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
6323
7622
  const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
6324
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
7623
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6325
7624
  throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
6326
7625
  }
6327
7626
  const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
@@ -6333,61 +7632,45 @@ function gitMergePr(options) {
6333
7632
  return { status: "already-merged", url: options.pr.url };
6334
7633
  }
6335
7634
  if (state !== "OPEN") {
6336
- throw new Error(`Cannot auto-merge PR ${options.pr.url}: state is ${state}.`);
7635
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
6337
7636
  }
6338
7637
  if (isDraft) {
6339
- throw new Error(`Cannot auto-merge draft PR ${options.pr.url}.`);
7638
+ throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
6340
7639
  }
7640
+ const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
6341
7641
  const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
6342
7642
  const method = options.method || "squash";
6343
7643
  mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
7644
+ mergeArgs.push("--match-head-commit", strictGateHeadSha);
6344
7645
  if (options.deleteBranch !== false) {
6345
7646
  mergeArgs.push("--delete-branch");
6346
7647
  }
6347
- const autoMergeArgs = [...mergeArgs, "--auto"];
6348
- const autoMerge = runCapture2(autoMergeArgs, repoRoot);
6349
- if (autoMerge.exitCode === 0) {
6350
- const postAutoMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
6351
- if (postAutoMergeState.state === "MERGED" || postAutoMergeState.mergedAt) {
6352
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6353
- return { status: "merged", url: options.pr.url };
6354
- }
6355
- if (postAutoMergeState.state === "OPEN" && postAutoMergeState.autoMergeRequest) {
6356
- if (canAdminMergeApprovedPr(postAutoMergeState)) {
6357
- const adminMergeArgs = [...mergeArgs];
6358
- if (postAutoMergeState.headRefOid) {
6359
- adminMergeArgs.push("--match-head-commit", postAutoMergeState.headRefOid);
6360
- }
6361
- adminMergeArgs.push("--admin");
6362
- const adminMerge = runCapture2(adminMergeArgs, repoRoot);
6363
- if (adminMerge.exitCode === 0) {
6364
- const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
6365
- if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
6366
- console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
6367
- return { status: "merged", url: options.pr.url };
6368
- }
6369
- throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
6370
- }
6371
- const adminMergeMessage = `${adminMerge.stderr}
6372
- ${adminMerge.stdout}`.trim();
6373
- if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
6374
- throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
6375
- }
7648
+ const directMerge = runCapture2(mergeArgs, repoRoot);
7649
+ if (directMerge.exitCode === 0) {
7650
+ console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
7651
+ return { status: "merged", url: options.pr.url };
7652
+ }
7653
+ const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
7654
+ if (canAdminMergeApprovedPr(postDirectState)) {
7655
+ const adminMergeArgs = [...mergeArgs, "--admin"];
7656
+ const adminMerge = runCapture2(adminMergeArgs, repoRoot);
7657
+ if (adminMerge.exitCode === 0) {
7658
+ const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
7659
+ if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
7660
+ console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
7661
+ return { status: "merged", url: options.pr.url };
6376
7662
  }
6377
- console.log(`Auto-merge enabled (${options.pr.repoLabel}): ${options.pr.url}`);
6378
- return { status: "auto-merge-enabled", url: options.pr.url };
7663
+ throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
7664
+ }
7665
+ const adminMergeMessage = `${adminMerge.stderr}
7666
+ ${adminMerge.stdout}`.trim();
7667
+ if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
7668
+ throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
6379
7669
  }
6380
- throw new Error(`Auto-merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub did not report a merged or auto-merge-enabled state.`);
6381
- }
6382
- const autoMergeMessage = `${autoMerge.stderr}
6383
- ${autoMerge.stdout}`.trim();
6384
- const autoMergeUnsupported = /auto.?merge.*(not enabled|not allowed|disabled|unsupported)|enablePullRequestAutoMerge|Auto merge is not allowed/i.test(autoMergeMessage);
6385
- if (!autoMergeUnsupported) {
6386
- throw new Error(`Failed to auto-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${autoMergeMessage}`);
6387
7670
  }
6388
- runOrThrow(options.projectRoot, mergeArgs, `Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}`);
6389
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6390
- return { status: "merged", url: options.pr.url };
7671
+ const directMergeMessage = `${directMerge.stderr}
7672
+ ${directMerge.stdout}`.trim();
7673
+ throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
6391
7674
  }
6392
7675
  function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
6393
7676
  const mergeable = prState.mergeable.toUpperCase();
@@ -6398,8 +7681,8 @@ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
6398
7681
  }
6399
7682
  function writePrMetadata(projectRoot, taskId, result) {
6400
7683
  const dir = artifactDirForId(projectRoot, taskId);
6401
- mkdirSync10(dir, { recursive: true });
6402
- const path = resolve23(dir, "pr-state.json");
7684
+ mkdirSync11(dir, { recursive: true });
7685
+ const path = resolve24(dir, "pr-state.json");
6403
7686
  let prs = {};
6404
7687
  if (existsSync20(path)) {
6405
7688
  try {
@@ -6419,11 +7702,11 @@ function writePrMetadata(projectRoot, taskId, result) {
6419
7702
  ...primary || {},
6420
7703
  updated_at: nowIso()
6421
7704
  };
6422
- writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
7705
+ writeFileSync11(path, `${JSON.stringify(artifact, null, 2)}
6423
7706
  `, "utf-8");
6424
7707
  }
6425
7708
  function readPrMetadata(projectRoot, taskId) {
6426
- const path = resolve23(artifactDirForId(projectRoot, taskId), "pr-state.json");
7709
+ const path = resolve24(artifactDirForId(projectRoot, taskId), "pr-state.json");
6427
7710
  if (!existsSync20(path)) {
6428
7711
  return [];
6429
7712
  }
@@ -6495,32 +7778,19 @@ function resolveGithubCliBinary(projectRoot) {
6495
7778
  if (explicit) {
6496
7779
  candidates.add(explicit);
6497
7780
  }
7781
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
7782
+ candidates.add(candidate);
7783
+ }
6498
7784
  const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
6499
7785
  for (const entry of explicitPathEntries) {
6500
- candidates.add(resolve23(entry, "gh"));
6501
- }
6502
- const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
6503
- if (hostProjectRoot) {
6504
- candidates.add(resolve23(resolveHostRigBinDir(hostProjectRoot), "gh"));
6505
- }
6506
- candidates.add(resolve23(resolveHostRigBinDir(projectRoot), "gh"));
6507
- const runtimeContext = loadRuntimeContextFromEnv();
6508
- if (runtimeContext?.binDir) {
6509
- candidates.add(resolve23(runtimeContext.binDir, "gh"));
6510
- }
6511
- const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
6512
- if (runtimeHome) {
6513
- candidates.add(resolve23(runtimeHome, "bin", "gh"));
6514
- }
6515
- for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
6516
- candidates.add(candidate);
7786
+ candidates.add(resolve24(entry, "gh"));
6517
7787
  }
6518
7788
  const bunResolved = Bun.which("gh");
6519
7789
  if (bunResolved) {
6520
7790
  candidates.add(bunResolved);
6521
7791
  }
6522
7792
  for (const candidate of candidates) {
6523
- if (candidate && existsSync20(candidate)) {
7793
+ if (candidate && existsSync20(candidate) && !isRuntimeGatewayGhPath(candidate)) {
6524
7794
  return candidate;
6525
7795
  }
6526
7796
  }
@@ -6550,7 +7820,7 @@ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
6550
7820
  return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
6551
7821
  }
6552
7822
  function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
6553
- const normalizedGitRoot = resolve23(gitRoot);
7823
+ const normalizedGitRoot = resolve24(gitRoot);
6554
7824
  if (visited.has(normalizedGitRoot)) {
6555
7825
  return "";
6556
7826
  }
@@ -6622,7 +7892,7 @@ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
6622
7892
  return remotes.includes("origin") ? "origin" : remotes[0];
6623
7893
  }
6624
7894
  function gitQuery(projectRoot, gitRoot, cwd, ...args) {
6625
- const gitArgs = existsSync20(resolve23(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveGitBinary(projectRoot), "--git-dir", gitRoot, ...args];
7895
+ const gitArgs = existsSync20(resolve24(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveGitBinary(projectRoot), "--git-dir", gitRoot, ...args];
6626
7896
  return runCapture2(gitArgs, cwd, projectRoot);
6627
7897
  }
6628
7898
  function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
@@ -6640,7 +7910,7 @@ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
6640
7910
  } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
6641
7911
  return "";
6642
7912
  } else if (!isAbsolute2(normalized)) {
6643
- candidate = resolve23(gitRoot, normalized);
7913
+ candidate = resolve24(gitRoot, normalized);
6644
7914
  }
6645
7915
  return existsSync20(candidate) ? candidate : "";
6646
7916
  }
@@ -6669,6 +7939,32 @@ function withGhRepo(command, repoNameWithOwner) {
6669
7939
  }
6670
7940
  return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
6671
7941
  }
7942
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
7943
+ const remote = remoteName || "origin";
7944
+ const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
7945
+ if (symbolic.exitCode === 0) {
7946
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
7947
+ if (ref && ref !== "HEAD") {
7948
+ return ref;
7949
+ }
7950
+ }
7951
+ const lsRemote = runCapture2(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
7952
+ if (lsRemote.exitCode === 0) {
7953
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
7954
+ if (match?.[1]) {
7955
+ return match[1];
7956
+ }
7957
+ }
7958
+ const gh = resolveGithubCliBinary(projectRoot);
7959
+ if (gh && repoNameWithOwner) {
7960
+ const api = runCapture2(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
7961
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
7962
+ if (branch) {
7963
+ return branch;
7964
+ }
7965
+ }
7966
+ return fallback;
7967
+ }
6672
7968
  function inferProjectBase(projectRoot, fallback) {
6673
7969
  const containing = runCapture2(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
6674
7970
  if (containing.exitCode !== 0) {
@@ -6743,7 +8039,7 @@ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef
6743
8039
  return best;
6744
8040
  }
6745
8041
  function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
6746
- if (!existsSync20(resolve23(repo, ".git"))) {
8042
+ if (!existsSync20(resolve24(repo, ".git"))) {
6747
8043
  console.log(`Skipping ${label}: repo not available (${repo})`);
6748
8044
  return;
6749
8045
  }
@@ -6775,7 +8071,7 @@ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files
6775
8071
  console.log(`Committed ${label}: ${message}`);
6776
8072
  }
6777
8073
  function readChangedFilesManifest(projectRoot, taskId) {
6778
- const manifestPath = resolve23(artifactDirForId(projectRoot, taskId), "changed-files.txt");
8074
+ const manifestPath = resolve24(artifactDirForId(projectRoot, taskId), "changed-files.txt");
6779
8075
  if (!existsSync20(manifestPath)) {
6780
8076
  return [];
6781
8077
  }
@@ -6783,10 +8079,10 @@ function readChangedFilesManifest(projectRoot, taskId) {
6783
8079
  return [...new Set(files)];
6784
8080
  }
6785
8081
  function refreshChangedFilesManifest(projectRoot, taskId) {
6786
- const manifestPath = resolve23(artifactDirForId(projectRoot, taskId), "changed-files.txt");
6787
- mkdirSync10(dirname11(manifestPath), { recursive: true });
8082
+ const manifestPath = resolve24(artifactDirForId(projectRoot, taskId), "changed-files.txt");
8083
+ mkdirSync11(dirname11(manifestPath), { recursive: true });
6788
8084
  const changedFiles = changedFilesForTask(projectRoot, taskId, true);
6789
- writeFileSync10(manifestPath, `${changedFiles.join(`
8085
+ writeFileSync11(manifestPath, `${changedFiles.join(`
6790
8086
  `)}
6791
8087
  `, "utf-8");
6792
8088
  return manifestPath;
@@ -6899,7 +8195,7 @@ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
6899
8195
  return result.exitCode === 0 && result.stdout.trim().length > 0;
6900
8196
  }
6901
8197
  function stageExcludePathspecs(repoRoot) {
6902
- const patterns = existsSync20(resolve23(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
8198
+ const patterns = existsSync20(resolve24(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
6903
8199
  return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
6904
8200
  }
6905
8201
  function pathResolvesBeyondSymlink(repoRoot, relativePath) {
@@ -6909,7 +8205,7 @@ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
6909
8205
  }
6910
8206
  let current = repoRoot;
6911
8207
  for (let index = 0;index < parts.length - 1; index += 1) {
6912
- current = resolve23(current, parts[index]);
8208
+ current = resolve24(current, parts[index]);
6913
8209
  try {
6914
8210
  if (lstatSync(current).isSymbolicLink()) {
6915
8211
  return true;
@@ -6979,11 +8275,11 @@ function runCapture2(command, cwd, projectRoot = cwd) {
6979
8275
  }
6980
8276
  function runtimeGitEnv(projectRoot) {
6981
8277
  const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
6982
- const runtimeHome = runtimeRoot ? resolve23(runtimeRoot, "home") : "";
6983
- const runtimeTmp = runtimeRoot ? resolve23(runtimeRoot, "tmp") : "";
6984
- const runtimeCache = runtimeRoot ? resolve23(runtimeRoot, "cache") : "";
6985
- const runtimeKnownHosts = runtimeHome ? resolve23(runtimeHome, ".ssh", "known_hosts") : "";
6986
- const runtimeKey = runtimeHome ? resolve23(runtimeHome, ".ssh", "rig-agent-key") : "";
8278
+ const runtimeHome = runtimeRoot ? resolve24(runtimeRoot, "home") : "";
8279
+ const runtimeTmp = runtimeRoot ? resolve24(runtimeRoot, "tmp") : "";
8280
+ const runtimeCache = runtimeRoot ? resolve24(runtimeRoot, "cache") : "";
8281
+ const runtimeKnownHosts = runtimeHome ? resolve24(runtimeHome, ".ssh", "known_hosts") : "";
8282
+ const runtimeKey = runtimeHome ? resolve24(runtimeHome, ".ssh", "rig-agent-key") : "";
6987
8283
  const env = {};
6988
8284
  if (ctx?.workspaceDir) {
6989
8285
  env.PROJECT_RIG_ROOT = projectRoot;
@@ -7013,6 +8309,10 @@ function runtimeGitEnv(projectRoot) {
7013
8309
  }
7014
8310
  env[key] = value;
7015
8311
  }
8312
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || "";
8313
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
8314
+ env.GITHUB_TOKEN = rigGithubToken;
8315
+ }
7016
8316
  if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
7017
8317
  env.GITHUB_TOKEN = env.GH_TOKEN;
7018
8318
  }
@@ -7036,6 +8336,13 @@ function runtimeGitEnv(projectRoot) {
7036
8336
  if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
7037
8337
  env.GH_TOKEN = env.GITHUB_TOKEN;
7038
8338
  }
8339
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
8340
+ if (gitHubToken) {
8341
+ env.RIG_GITHUB_TOKEN = gitHubToken;
8342
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
8343
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
8344
+ applyGitHubCredentialHelperEnv(env);
8345
+ }
7039
8346
  if (runtimeKnownHosts && existsSync20(runtimeKnownHosts)) {
7040
8347
  const sshParts = [
7041
8348
  "ssh",
@@ -7052,11 +8359,19 @@ function runtimeGitEnv(projectRoot) {
7052
8359
  }
7053
8360
  return Object.keys(env).length > 0 ? env : undefined;
7054
8361
  }
8362
+ function applyGitHubCredentialHelperEnv(env) {
8363
+ env.GIT_TERMINAL_PROMPT = "0";
8364
+ env.GIT_CONFIG_COUNT = "2";
8365
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
8366
+ env.GIT_CONFIG_VALUE_0 = "";
8367
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
8368
+ env.GIT_CONFIG_VALUE_1 = '!f() { test "$1" = get || exit 0; token="${GITHUB_TOKEN:-${GH_TOKEN:-${RIG_GITHUB_TOKEN:-}}}"; test -n "$token" || exit 0; echo username=x-access-token; echo password="$token"; }; f';
8369
+ }
7055
8370
  function loadPersistedRuntimeSecrets(runtimeRoot) {
7056
8371
  if (!runtimeRoot) {
7057
8372
  return {};
7058
8373
  }
7059
- const path = resolve23(runtimeRoot, "runtime-secrets.json");
8374
+ const path = resolve24(runtimeRoot, "runtime-secrets.json");
7060
8375
  if (!existsSync20(path)) {
7061
8376
  return {};
7062
8377
  }
@@ -7069,13 +8384,13 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
7069
8384
  }
7070
8385
  }
7071
8386
  function ensureRuntimeOpenSslConfig(runtimeHome) {
7072
- const sslDir = resolve23(runtimeHome, ".ssl");
7073
- const sslConfig = resolve23(sslDir, "openssl.cnf");
8387
+ const sslDir = resolve24(runtimeHome, ".ssl");
8388
+ const sslConfig = resolve24(sslDir, "openssl.cnf");
7074
8389
  if (!existsSync20(sslDir)) {
7075
- mkdirSync10(sslDir, { recursive: true });
8390
+ mkdirSync11(sslDir, { recursive: true });
7076
8391
  }
7077
8392
  if (!existsSync20(sslConfig)) {
7078
- writeFileSync10(sslConfig, `# Rig runtime OpenSSL config placeholder
8393
+ writeFileSync11(sslConfig, `# Rig runtime OpenSSL config placeholder
7079
8394
  `);
7080
8395
  }
7081
8396
  return sslConfig;
@@ -7093,7 +8408,7 @@ function resolveRuntimeMetadata(projectRoot) {
7093
8408
  if (contextFile) {
7094
8409
  return {
7095
8410
  ctx,
7096
- runtimeRoot: dirname11(resolve23(contextFile))
8411
+ runtimeRoot: dirname11(resolve24(contextFile))
7097
8412
  };
7098
8413
  }
7099
8414
  const inferredContextFile = findRuntimeContextFile2(projectRoot);
@@ -7109,9 +8424,9 @@ function resolveRuntimeMetadata(projectRoot) {
7109
8424
  return { ctx, runtimeRoot: "" };
7110
8425
  }
7111
8426
  function findRuntimeContextFile2(startPath) {
7112
- let current = resolve23(startPath);
8427
+ let current = resolve24(startPath);
7113
8428
  while (true) {
7114
- const candidate = resolve23(current, "runtime-context.json");
8429
+ const candidate = resolve24(current, "runtime-context.json");
7115
8430
  if (existsSync20(candidate)) {
7116
8431
  return candidate;
7117
8432
  }
@@ -7164,6 +8479,7 @@ async function main() {
7164
8479
  }
7165
8480
  const paths = resolveHarnessPaths(projectRoot);
7166
8481
  let failed = false;
8482
+ let sourceCloseoutAllowed = false;
7167
8483
  console.log(`=== Completion Verification: ${taskId} ===`);
7168
8484
  const scopes = await resolveTaskScopes(projectRoot, taskId);
7169
8485
  const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
@@ -7312,22 +8628,42 @@ async function main() {
7312
8628
  if (prs.length === 0) {
7313
8629
  console.log("Auto-merge: skipped (no PR metadata found)");
7314
8630
  } else {
7315
- let mergePending = false;
8631
+ let cycle = 0;
7316
8632
  for (const pr of prs) {
8633
+ cycle += 1;
8634
+ const gate = await runStrictPrMergeGate({
8635
+ projectRoot,
8636
+ prUrl: pr.url,
8637
+ taskId,
8638
+ runId: "completion-verification",
8639
+ cycle,
8640
+ final: true,
8641
+ command: async (args, options) => {
8642
+ const result = runCapture(["gh", ...args], options?.cwd ?? projectRoot);
8643
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
8644
+ }
8645
+ });
8646
+ if (!gate.approved) {
8647
+ console.log(`FAIL: Strict merge gate blocked PR (${pr.repoLabel}): ${pr.url}`);
8648
+ for (const reason of gate.reasons) {
8649
+ console.log(`- ${reason}`);
8650
+ }
8651
+ failed = true;
8652
+ continue;
8653
+ }
7317
8654
  const mergeResult = gitMergePr({
7318
8655
  projectRoot,
7319
8656
  pr,
7320
8657
  method: "squash",
7321
- deleteBranch: true
8658
+ deleteBranch: true,
8659
+ strictGate: gate
7322
8660
  });
7323
- if (mergeResult.status === "auto-merge-enabled") {
7324
- mergePending = true;
7325
- console.log(`WAIT: Auto-merge enabled but PR is still open (${pr.repoLabel}): ${pr.url}`);
8661
+ if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
8662
+ console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
7326
8663
  }
7327
8664
  }
7328
- if (mergePending) {
7329
- failed = true;
7330
- } else {
8665
+ if (!failed) {
8666
+ sourceCloseoutAllowed = true;
7331
8667
  console.log("OK: Auto-merge complete");
7332
8668
  }
7333
8669
  }
@@ -7340,19 +8676,23 @@ async function main() {
7340
8676
  console.log(`
7341
8677
  [post] Auto-merge: skipped (not in policy completion.checks)`);
7342
8678
  }
7343
- const artifactDir = resolve24(paths.artifactsDir, taskId);
7344
- mkdirSync11(artifactDir, { recursive: true });
7345
- writeFileSync11(resolve24(artifactDir, "review-status.txt"), failed ? `REJECTED
8679
+ const artifactDir = resolve25(paths.artifactsDir, taskId);
8680
+ mkdirSync12(artifactDir, { recursive: true });
8681
+ writeFileSync12(resolve25(artifactDir, "review-status.txt"), failed ? `REJECTED
7346
8682
  ` : `APPROVED
7347
8683
  `, "utf-8");
7348
8684
  if (!failed) {
7349
8685
  await recordTaskRepoCommits(projectRoot, taskId, paths);
7350
- const closeout = await closeCompletedTaskSource(projectRoot, taskId);
7351
- if (!closeout.ok) {
7352
- console.log(`FAIL: ${closeout.message}`);
7353
- failed = true;
8686
+ if (sourceCloseoutAllowed) {
8687
+ const closeout = await closeCompletedTaskSource(projectRoot, taskId);
8688
+ if (!closeout.ok) {
8689
+ console.log(`FAIL: ${closeout.message}`);
8690
+ failed = true;
8691
+ } else {
8692
+ console.log(`OK: ${closeout.message}`);
8693
+ }
7354
8694
  } else {
7355
- console.log(`OK: ${closeout.message}`);
8695
+ console.log("Task source closeout skipped until an approved PR merge completes.");
7356
8696
  }
7357
8697
  }
7358
8698
  if (!failed) {
@@ -7385,7 +8725,7 @@ async function runBunTool(args, cwd) {
7385
8725
  };
7386
8726
  }
7387
8727
  async function runProtoQualityGate(monorepoRoot) {
7388
- const protosDir = resolve24(monorepoRoot, "packages", "protos");
8728
+ const protosDir = resolve25(monorepoRoot, "packages", "protos");
7389
8729
  if (!existsSync21(protosDir)) {
7390
8730
  console.log(`FAIL: Proto workspace not found at ${protosDir}`);
7391
8731
  return false;
@@ -7434,7 +8774,7 @@ async function runProtoQualityGate(monorepoRoot) {
7434
8774
  } else {
7435
8775
  console.log("OK: Generated TypeScript compiles");
7436
8776
  }
7437
- const workflowPath = resolve24(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
8777
+ const workflowPath = resolve25(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
7438
8778
  if (!existsSync21(workflowPath)) {
7439
8779
  console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
7440
8780
  ok = false;
@@ -7479,9 +8819,9 @@ async function readJsonFileIfPresent(path) {
7479
8819
  }
7480
8820
  async function recordVerifierFailure(projectRoot, taskId, paths) {
7481
8821
  const failedApproachesPath = paths.failedApproachesPath;
7482
- const artifactDir = resolve24(paths.artifactsDir, taskId);
7483
- const reviewStatePath = resolve24(artifactDir, "review-state.json");
7484
- const reviewFeedbackPath = resolve24(artifactDir, "review-feedback.md");
8822
+ const artifactDir = resolve25(paths.artifactsDir, taskId);
8823
+ const reviewStatePath = resolve25(artifactDir, "review-state.json");
8824
+ const reviewFeedbackPath = resolve25(artifactDir, "review-feedback.md");
7485
8825
  let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
7486
8826
  const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
7487
8827
  if (parsedReviewState) {
@@ -7493,10 +8833,10 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
7493
8833
  let attempts = 1;
7494
8834
  if (existsSync21(failedApproachesPath)) {
7495
8835
  const content = readFileSync12(failedApproachesPath, "utf-8");
7496
- attempts = (content.match(new RegExp(`^## ${escapeRegExp(taskId)}\\b`, "gm")) || []).length + 1;
8836
+ attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
7497
8837
  } else {
7498
- mkdirSync11(resolve24(failedApproachesPath, ".."), { recursive: true });
7499
- writeFileSync11(failedApproachesPath, `# Failed Approaches
8838
+ mkdirSync12(resolve25(failedApproachesPath, ".."), { recursive: true });
8839
+ writeFileSync12(failedApproachesPath, `# Failed Approaches
7500
8840
 
7501
8841
  `, "utf-8");
7502
8842
  }
@@ -7534,8 +8874,8 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
7534
8874
  recorded_at: new Date().toISOString(),
7535
8875
  repos
7536
8876
  };
7537
- mkdirSync11(resolve24(statePath, ".."), { recursive: true });
7538
- writeFileSync11(statePath, `${JSON.stringify(state, null, 2)}
8877
+ mkdirSync12(resolve25(statePath, ".."), { recursive: true });
8878
+ writeFileSync12(statePath, `${JSON.stringify(state, null, 2)}
7539
8879
  `, "utf-8");
7540
8880
  }
7541
8881
  }