@h-rig/runtime 0.0.6-alpha.11 → 0.0.6-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@
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
8
  escapeRegExp as escapeRegExp2,
9
9
  resolveBunCli,
@@ -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) {
@@ -4239,18 +4239,913 @@ ${JSON.stringify(result, null, 2)}
4239
4239
  }
4240
4240
 
4241
4241
  // packages/runtime/src/control-plane/native/verifier.ts
4242
- import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
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";
4243
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/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 firstString(record, keys) {
4364
+ for (const key of keys) {
4365
+ const value = record[key];
4366
+ if (typeof value === "string")
4367
+ return value;
4368
+ }
4369
+ return "";
4370
+ }
4371
+ function arrayField(record, key) {
4372
+ const value = record[key];
4373
+ return Array.isArray(value) ? value : [];
4374
+ }
4375
+ async function runJsonArray(command, args, cwd) {
4376
+ const result = await command(args, { cwd });
4377
+ const label = `gh ${args.join(" ")}`;
4378
+ if (result.exitCode !== 0) {
4379
+ return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4380
+ }
4381
+ const parsed = parseJsonArray(result.stdout);
4382
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4383
+ }
4384
+ async function runJsonObject(command, args, cwd) {
4385
+ const result = await command(args, { cwd });
4386
+ const label = `gh ${args.join(" ")}`;
4387
+ if (result.exitCode !== 0) {
4388
+ return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
4389
+ }
4390
+ const parsed = parseJsonObject(result.stdout);
4391
+ return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
4392
+ }
4393
+ function normalizeStatusCheck(entry) {
4394
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4395
+ return null;
4396
+ const record = entry;
4397
+ const name = firstString(record, ["name", "context"]);
4398
+ if (!name.trim())
4399
+ return null;
4400
+ const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
4401
+ const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
4402
+ return {
4403
+ __typename: typeof record.__typename === "string" ? record.__typename : null,
4404
+ name,
4405
+ context: typeof record.context === "string" ? record.context : null,
4406
+ status: typeof record.status === "string" ? record.status : null,
4407
+ state: typeof record.state === "string" ? record.state : null,
4408
+ conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
4409
+ 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,
4410
+ link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
4411
+ headSha: typeof record.headSha === "string" ? record.headSha : null,
4412
+ head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
4413
+ output: output ? {
4414
+ title: typeof output.title === "string" ? output.title : null,
4415
+ summary: typeof output.summary === "string" ? output.summary : null,
4416
+ text: typeof output.text === "string" ? output.text : null
4417
+ } : null,
4418
+ app: app ? {
4419
+ slug: typeof app.slug === "string" ? app.slug : null,
4420
+ name: typeof app.name === "string" ? app.name : null,
4421
+ owner: app.owner && typeof app.owner === "object" ? app.owner : null
4422
+ } : null
4423
+ };
4424
+ }
4425
+ function normalizeReview(entry) {
4426
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4427
+ return null;
4428
+ const record = entry;
4429
+ return {
4430
+ id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
4431
+ state: typeof record.state === "string" ? record.state : null,
4432
+ body: typeof record.body === "string" ? record.body : null,
4433
+ 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,
4434
+ html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
4435
+ author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
4436
+ };
4437
+ }
4438
+ function normalizeReviewComment(entry) {
4439
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4440
+ return null;
4441
+ const record = entry;
4442
+ const body = typeof record.body === "string" ? record.body : null;
4443
+ const path = typeof record.path === "string" ? record.path : null;
4444
+ if (!body && !path)
4445
+ return null;
4446
+ return {
4447
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4448
+ user: record.user && typeof record.user === "object" ? record.user : null,
4449
+ author: record.author && typeof record.author === "object" ? record.author : null,
4450
+ body,
4451
+ path,
4452
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4453
+ url: typeof record.url === "string" ? record.url : null,
4454
+ commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
4455
+ original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
4456
+ };
4457
+ }
4458
+ function normalizeIssueComment(entry) {
4459
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4460
+ return null;
4461
+ const record = entry;
4462
+ const body = typeof record.body === "string" ? record.body : null;
4463
+ if (!body)
4464
+ return null;
4465
+ return {
4466
+ id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
4467
+ user: record.user && typeof record.user === "object" ? record.user : null,
4468
+ author: record.author && typeof record.author === "object" ? record.author : null,
4469
+ body,
4470
+ html_url: typeof record.html_url === "string" ? record.html_url : null,
4471
+ url: typeof record.url === "string" ? record.url : null,
4472
+ created_at: typeof record.created_at === "string" ? record.created_at : null
4473
+ };
4474
+ }
4475
+ function normalizeReviewThread(entry) {
4476
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4477
+ return null;
4478
+ const record = entry;
4479
+ return {
4480
+ id: typeof record.id === "string" ? record.id : null,
4481
+ isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
4482
+ isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
4483
+ comments: record.comments && typeof record.comments === "object" ? record.comments : null
4484
+ };
4485
+ }
4486
+ function relevantIssueComment(comment) {
4487
+ const login = comment.user?.login ?? comment.author?.login ?? "";
4488
+ const body = comment.body ?? "";
4489
+ return isGreptileGithubLogin(login) || /greptile|blocker|unsafe|not safe|do not merge|must fix|please fix|changes requested|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4490
+ }
4491
+ function latestThreadComment(thread) {
4492
+ const nodes = thread.comments?.nodes ?? [];
4493
+ return nodes.length > 0 ? nodes[nodes.length - 1] : null;
4494
+ }
4495
+ function unresolvedThreadSummaries(threads) {
4496
+ return threads.flatMap((thread) => {
4497
+ if (thread.isResolved === true || thread.isOutdated === true)
4498
+ return [];
4499
+ const latest = latestThreadComment(thread);
4500
+ if (!latest)
4501
+ return ["Unresolved review thread"];
4502
+ const path = latest.path ? ` on ${latest.path}` : "";
4503
+ return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4504
+ });
4505
+ }
4506
+ function collectBodies(evidence) {
4507
+ return [
4508
+ evidence.title ?? "",
4509
+ evidence.body,
4510
+ ...evidence.reviews.map((review) => review.body ?? ""),
4511
+ ...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
4512
+ ...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
4513
+ ...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
4514
+ ...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
4515
+ ].filter((body) => body.trim().length > 0);
4516
+ }
4517
+ function bodyExcerpt(body) {
4518
+ const text = stripHtml(body).replace(/\s+/g, " ").trim();
4519
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
4520
+ }
4521
+ function makeGreptileSignal(input) {
4522
+ const scores = parseGreptileScores(input.body);
4523
+ const reviewedSha = input.reviewedSha?.trim() || null;
4524
+ const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
4525
+ const blocker = input.blocker ?? containsBlockerText(input.body);
4526
+ const explicitApproval = input.explicitApproval ?? false;
4527
+ return {
4528
+ source: input.source,
4529
+ trusted: input.trusted,
4530
+ authorLogin: input.authorLogin ?? null,
4531
+ reviewedSha,
4532
+ current,
4533
+ stale: current === false,
4534
+ score: scores[0] ?? null,
4535
+ scores,
4536
+ explicitApproval,
4537
+ blocker,
4538
+ actionable: input.actionable ?? blocker,
4539
+ bodyExcerpt: bodyExcerpt(input.body),
4540
+ body: input.body,
4541
+ allScores: scores
4542
+ };
4543
+ }
4544
+ function reviewAuthorLogin(review) {
4545
+ return review.author?.login ?? null;
4546
+ }
4547
+ function commentAuthorLogin(comment) {
4548
+ return comment.user?.login ?? comment.author?.login ?? null;
4549
+ }
4550
+ function collectGreptileSignals(evidence) {
4551
+ const signals = [];
4552
+ const contextSources = [
4553
+ { source: "pr-title", body: evidence.title ?? "" },
4554
+ { source: "pr-body", body: evidence.body }
4555
+ ];
4556
+ for (const context of contextSources) {
4557
+ if (!context.body.trim())
4558
+ continue;
4559
+ if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
4560
+ continue;
4561
+ const contextBlocker = containsBlockerText(context.body);
4562
+ signals.push(makeGreptileSignal({
4563
+ source: context.source,
4564
+ body: context.body,
4565
+ currentHeadSha: evidence.currentHeadSha,
4566
+ trusted: false,
4567
+ blocker: contextBlocker,
4568
+ actionable: contextBlocker
4569
+ }));
4570
+ }
4571
+ for (const apiSignal of evidence.apiSignals ?? []) {
4572
+ const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
4573
+
4574
+ `);
4575
+ if (!body.trim())
4576
+ continue;
4577
+ signals.push(makeGreptileSignal({
4578
+ source: "api",
4579
+ body,
4580
+ currentHeadSha: evidence.currentHeadSha,
4581
+ trusted: true,
4582
+ reviewedSha: apiSignal.reviewedSha ?? null,
4583
+ explicitApproval: false
4584
+ }));
4585
+ }
4586
+ for (const review of evidence.reviews) {
4587
+ const login = reviewAuthorLogin(review);
4588
+ if (!isGreptileGithubLogin(login))
4589
+ continue;
4590
+ const state = String(review.state ?? "").toUpperCase();
4591
+ const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
4592
+
4593
+ `);
4594
+ if (!body.trim())
4595
+ continue;
4596
+ const dismissed = state === "DISMISSED";
4597
+ signals.push(makeGreptileSignal({
4598
+ source: "github-review",
4599
+ body,
4600
+ currentHeadSha: evidence.currentHeadSha,
4601
+ trusted: !dismissed,
4602
+ authorLogin: login,
4603
+ reviewedSha: review.commit_id ?? null,
4604
+ explicitApproval: undefined,
4605
+ blocker: state === "CHANGES_REQUESTED" || undefined
4606
+ }));
4607
+ }
4608
+ for (const comment of evidence.changedFileReviewComments) {
4609
+ const login = commentAuthorLogin(comment);
4610
+ const body = comment.body ?? "";
4611
+ if (!body.trim() || !isGreptileGithubLogin(login))
4612
+ continue;
4613
+ signals.push(makeGreptileSignal({
4614
+ source: "changed-file-comment",
4615
+ body,
4616
+ currentHeadSha: evidence.currentHeadSha,
4617
+ trusted: true,
4618
+ authorLogin: login,
4619
+ reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
4620
+ }));
4621
+ }
4622
+ for (const comment of evidence.relevantIssueComments) {
4623
+ const login = commentAuthorLogin(comment);
4624
+ const body = comment.body ?? "";
4625
+ if (!body.trim() || !isGreptileGithubLogin(login))
4626
+ continue;
4627
+ signals.push(makeGreptileSignal({
4628
+ source: "issue-comment",
4629
+ body,
4630
+ currentHeadSha: evidence.currentHeadSha,
4631
+ trusted: true,
4632
+ authorLogin: login
4633
+ }));
4634
+ }
4635
+ for (const thread of evidence.reviewThreads) {
4636
+ if (thread.isOutdated === true || thread.isResolved === true)
4637
+ continue;
4638
+ for (const comment of thread.comments?.nodes ?? []) {
4639
+ const login = comment.author?.login ?? null;
4640
+ const body = comment.body ?? "";
4641
+ if (!body.trim() || !isGreptileGithubLogin(login))
4642
+ continue;
4643
+ signals.push(makeGreptileSignal({
4644
+ source: "review-thread",
4645
+ body,
4646
+ currentHeadSha: evidence.currentHeadSha,
4647
+ trusted: true,
4648
+ authorLogin: login
4649
+ }));
4650
+ }
4651
+ }
4652
+ for (const check of evidence.checks) {
4653
+ if (!isGreptileLabel(checkName(check)))
4654
+ continue;
4655
+ const reviewedSha = check.headSha ?? check.head_sha ?? null;
4656
+ const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
4657
+ const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
4658
+
4659
+ `);
4660
+ signals.push(makeGreptileSignal({
4661
+ source: "github-check",
4662
+ body,
4663
+ currentHeadSha: evidence.currentHeadSha,
4664
+ trusted: false,
4665
+ reviewedSha,
4666
+ explicitApproval: false,
4667
+ blocker: isFailingCheck(check),
4668
+ actionable: isFailingCheck(check)
4669
+ }));
4670
+ }
4671
+ return signals;
4672
+ }
4673
+ function unresolvedGreptileThreadSummaries(threads) {
4674
+ return threads.flatMap((thread) => {
4675
+ if (thread.isResolved === true || thread.isOutdated === true)
4676
+ return [];
4677
+ const comments = thread.comments?.nodes ?? [];
4678
+ if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
4679
+ return [];
4680
+ const latest = latestThreadComment(thread);
4681
+ if (!latest)
4682
+ return ["Unresolved Greptile review thread"];
4683
+ const path = latest.path ? ` on ${latest.path}` : "";
4684
+ return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
4685
+ });
4686
+ }
4687
+ function actionableChangedFileCommentSummaries(_comments) {
4688
+ return [];
4689
+ }
4690
+ function issueLevelBlockerSummaries(comments) {
4691
+ return comments.flatMap((comment) => {
4692
+ const body = comment.body?.trim() ?? "";
4693
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4694
+ return [];
4695
+ const login = commentAuthorLogin(comment) ?? "unknown";
4696
+ const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
4697
+ return [`${author}: ${body}`];
4698
+ });
4699
+ }
4700
+ function reviewBodyBlockerSummaries(reviews) {
4701
+ return reviews.flatMap((review) => {
4702
+ const login = reviewAuthorLogin(review) ?? "unknown";
4703
+ if (isGreptileGithubLogin(login))
4704
+ return [];
4705
+ const body = review.body?.trim() ?? "";
4706
+ if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
4707
+ return [];
4708
+ const state = review.state ? ` (${review.state})` : "";
4709
+ return [`PR review summary by ${login}${state}: ${body}`];
4710
+ });
4711
+ }
4712
+ function signalLabel(signal) {
4713
+ const source = signal.source.replace(/-/g, " ");
4714
+ const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
4715
+ const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
4716
+ return `${source}${author}${sha}`;
4717
+ }
4718
+ function deriveGreptileEvidence(input) {
4719
+ const rawBodies = collectBodies(input);
4720
+ const signals = collectGreptileSignals(input);
4721
+ const trustedSignals = signals.filter((signal) => signal.trusted);
4722
+ const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4723
+ const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
4724
+ const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
4725
+ const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
4726
+ const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
4727
+ const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
4728
+ const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
4729
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
4730
+ const approvedByScore = !!approvingScoreEntry;
4731
+ const approvedByExplicitMapping = false;
4732
+ const approvingSignal = approvingScoreEntry?.signal ?? null;
4733
+ const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
4734
+ const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
4735
+ const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
4736
+ const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
4737
+ const staleBlockingSignals = [];
4738
+ const blockers = [
4739
+ ...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
4740
+ ...reviewBodyBlockerSummaries(input.reviews),
4741
+ ...issueLevelBlockerSummaries(input.relevantIssueComments),
4742
+ ...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
4743
+ ...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
4744
+ ...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
4745
+ ];
4746
+ const unresolvedComments = [
4747
+ ...unresolvedGreptileThreadSummaries(input.reviewThreads),
4748
+ ...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
4749
+ ];
4750
+ const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
4751
+ const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
4752
+ const completedGreptileCheck = greptileChecks.some((check) => {
4753
+ const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
4754
+ return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
4755
+ });
4756
+ const completedGreptileReview = greptileReviews.some((review) => {
4757
+ const state = String(review.state ?? "").toUpperCase();
4758
+ const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
4759
+ return completedState && review.commit_id === input.currentHeadSha;
4760
+ });
4761
+ const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
4762
+ const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
4763
+ const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
4764
+ const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
4765
+ const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4766
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
4767
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
4768
+ 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";
4769
+ return {
4770
+ source,
4771
+ currentHeadSha: input.currentHeadSha,
4772
+ reviewedSha,
4773
+ fresh,
4774
+ completed,
4775
+ approved,
4776
+ score,
4777
+ explicitApproval: approvedByExplicitMapping,
4778
+ blockers,
4779
+ unresolvedComments,
4780
+ rawBodies,
4781
+ signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
4782
+ mapping
4783
+ };
4784
+ }
4785
+ function isGreptileCheckDetail(check) {
4786
+ return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4787
+ }
4788
+ async function collectGreptileCheckDetails(input) {
4789
+ const checkRunsRead = await runJsonArray(input.command, [
4790
+ "api",
4791
+ `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4792
+ "--paginate",
4793
+ "--slurp",
4794
+ "--jq",
4795
+ "map(.check_runs // []) | add // []"
4796
+ ], input.projectRoot);
4797
+ const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4798
+ return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4799
+ }
4800
+ async function collectReviewThreads(input) {
4801
+ const reviewThreads = [];
4802
+ let afterCursor = null;
4803
+ for (let page = 0;page < 100; page += 1) {
4804
+ const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
4805
+ const threadsResponse = await runJsonObject(input.command, [
4806
+ "api",
4807
+ "graphql",
4808
+ "-F",
4809
+ `owner=${input.owner}`,
4810
+ "-F",
4811
+ `name=${input.name}`,
4812
+ "-F",
4813
+ `prNumber=${input.prNumber}`,
4814
+ "-f",
4815
+ `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 } } } } }`
4816
+ ], input.projectRoot);
4817
+ if (threadsResponse.error) {
4818
+ return { value: reviewThreads, error: threadsResponse.error };
4819
+ }
4820
+ const data = threadsResponse.value.data;
4821
+ const repository = data?.repository;
4822
+ const pullRequest = repository?.pullRequest;
4823
+ const threads = pullRequest?.reviewThreads;
4824
+ const nodes = threads?.nodes;
4825
+ if (!Array.isArray(nodes)) {
4826
+ return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
4827
+ }
4828
+ const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
4829
+ reviewThreads.push(...normalized);
4830
+ const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
4831
+ if (truncatedCommentThread) {
4832
+ return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
4833
+ }
4834
+ const pageInfo = threads?.pageInfo;
4835
+ if (!pageInfo) {
4836
+ if (nodes.length >= 100) {
4837
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
4838
+ }
4839
+ return { value: reviewThreads };
4840
+ }
4841
+ if (pageInfo.hasNextPage !== true) {
4842
+ return { value: reviewThreads };
4843
+ }
4844
+ if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
4845
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
4846
+ }
4847
+ afterCursor = pageInfo.endCursor;
4848
+ }
4849
+ return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
4850
+ }
4851
+ async function collectPrReviewEvidence(input) {
4852
+ const parsed = parseGithubPrUrl(input.prUrl);
4853
+ if (!parsed) {
4854
+ throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
4855
+ }
4856
+ const readErrors = [];
4857
+ const viewRead = await runJsonObject(input.command, [
4858
+ "pr",
4859
+ "view",
4860
+ input.prUrl,
4861
+ "--json",
4862
+ "title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
4863
+ ], input.projectRoot);
4864
+ if (viewRead.error)
4865
+ readErrors.push(viewRead.error);
4866
+ const view = viewRead.value;
4867
+ if (!Array.isArray(view.statusCheckRollup)) {
4868
+ readErrors.push("gh pr view did not return required statusCheckRollup array");
4869
+ }
4870
+ if (!Array.isArray(view.reviews)) {
4871
+ readErrors.push("gh pr view did not return required reviews array");
4872
+ }
4873
+ const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
4874
+ const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4875
+ const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4876
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4877
+ if (reviewCommentsRead.error)
4878
+ readErrors.push(reviewCommentsRead.error);
4879
+ const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4880
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4881
+ if (issueCommentsRead.error)
4882
+ readErrors.push(issueCommentsRead.error);
4883
+ const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
4884
+ const reviewThreadsRead = await collectReviewThreads({
4885
+ command: input.command,
4886
+ projectRoot: input.projectRoot,
4887
+ owner: parsed.owner,
4888
+ name: parsed.repo,
4889
+ prNumber: parsed.prNumber
4890
+ });
4891
+ if (reviewThreadsRead.error)
4892
+ readErrors.push(reviewThreadsRead.error);
4893
+ const reviewThreads = reviewThreadsRead.value;
4894
+ const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
4895
+ let greptileCheckDetails = [];
4896
+ if (headSha && greptileRollupChecks.length > 0) {
4897
+ const checkDetailsRead = await collectGreptileCheckDetails({
4898
+ command: input.command,
4899
+ projectRoot: input.projectRoot,
4900
+ repoName: parsed.repoName,
4901
+ headSha
4902
+ });
4903
+ if (checkDetailsRead.error)
4904
+ readErrors.push(checkDetailsRead.error);
4905
+ greptileCheckDetails = checkDetailsRead.value;
4906
+ if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4907
+ readErrors.push("Greptile check details could not be found for the current PR head");
4908
+ }
4909
+ }
4910
+ const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4911
+ 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})` : ""}`);
4912
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
4913
+ const evidenceBase = {
4914
+ title: firstString(view, ["title"]),
4915
+ body: firstString(view, ["body"]),
4916
+ reviews,
4917
+ changedFileReviewComments: reviewComments,
4918
+ relevantIssueComments: issueComments,
4919
+ reviewThreads,
4920
+ checks: checksWithGreptileDetails,
4921
+ currentHeadSha: headSha,
4922
+ apiSignals: input.apiSignals ?? []
4923
+ };
4924
+ const greptile = deriveGreptileEvidence(evidenceBase);
4925
+ return {
4926
+ prUrl: input.prUrl,
4927
+ prNumber: parsed.prNumber,
4928
+ repoName: parsed.repoName,
4929
+ title: evidenceBase.title,
4930
+ body: evidenceBase.body,
4931
+ headSha,
4932
+ headRefName: firstString(view, ["headRefName"]),
4933
+ baseRefName: firstString(view, ["baseRefName"]),
4934
+ state: firstString(view, ["state"]),
4935
+ isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
4936
+ mergeable: firstString(view, ["mergeable"]),
4937
+ mergeStateStatus: firstString(view, ["mergeStateStatus"]),
4938
+ reviewDecision: firstString(view, ["reviewDecision"]),
4939
+ reviews,
4940
+ reviewThreads,
4941
+ changedFileReviewComments: reviewComments,
4942
+ relevantIssueComments: issueComments,
4943
+ statusCheckRollup: checksWithGreptileDetails,
4944
+ checkFailures,
4945
+ pendingChecks,
4946
+ readErrors,
4947
+ greptile
4948
+ };
4949
+ }
4950
+ function evaluateEvidence(evidence) {
4951
+ const reasons = [];
4952
+ const warnings = [];
4953
+ let pending = false;
4954
+ if (evidence.readErrors.length > 0) {
4955
+ reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
4956
+ }
4957
+ if (!evidence.headSha)
4958
+ reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
4959
+ if (evidence.checkFailures.length > 0)
4960
+ reasons.push(...evidence.checkFailures);
4961
+ if (evidence.pendingChecks.length > 0) {
4962
+ pending = true;
4963
+ reasons.push(...evidence.pendingChecks);
4964
+ }
4965
+ const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
4966
+ if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
4967
+ reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
4968
+ }
4969
+ const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
4970
+ if (unresolvedThreads.length > 0)
4971
+ reasons.push(...unresolvedThreads);
4972
+ const greptile = evidence.greptile;
4973
+ if (greptile.mapping === "missing")
4974
+ reasons.push("Missing Greptile check/review evidence for this PR.");
4975
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
4976
+ if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
4977
+ reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
4978
+ }
4979
+ if (!greptile.completed) {
4980
+ pending = true;
4981
+ reasons.push("Greptile check/review has not completed for the current PR head.");
4982
+ }
4983
+ if (!greptile.fresh)
4984
+ reasons.push("Greptile approval is not tied to the current PR head SHA.");
4985
+ if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
4986
+ reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
4987
+ }
4988
+ if (!greptile.score && greptile.mapping !== "score-5-of-5") {
4989
+ reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
4990
+ }
4991
+ if (greptile.mapping === "unproven") {
4992
+ reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
4993
+ }
4994
+ if (greptile.blockers.length > 0) {
4995
+ reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
4996
+ }
4997
+ if (greptile.unresolvedComments.length > 0)
4998
+ reasons.push(...greptile.unresolvedComments);
4999
+ if (!greptile.approved)
5000
+ warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
5001
+ return { reasons: Array.from(new Set(reasons)), warnings, pending };
5002
+ }
5003
+ function evaluateStrictPrMergeGate(evidence) {
5004
+ const evaluated = evaluateEvidence(evidence);
5005
+ const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
5006
+ return {
5007
+ approved,
5008
+ pending: evaluated.pending,
5009
+ reasons: evaluated.reasons,
5010
+ warnings: evaluated.warnings,
5011
+ actionableFeedback: evaluated.reasons,
5012
+ evidence
5013
+ };
5014
+ }
5015
+ function promptExcerpt(value, maxChars = 4000) {
5016
+ return value.length > maxChars ? `${value.slice(0, maxChars)}
5017
+
5018
+ [truncated for prompt; see full evidence artifact]` : value;
5019
+ }
5020
+ function promptJsonExcerpt(value, maxChars = 6000) {
5021
+ return promptExcerpt(JSON.stringify(value, null, 2), maxChars);
5022
+ }
5023
+ function buildStrictPrGateSteeringPrompt(result) {
5024
+ const evidence = result.evidence;
5025
+ const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
5026
+ const lines = [
5027
+ `Strict PR merge gate blocked ${evidence.prUrl}.`,
5028
+ `PR title: ${evidence.title || "(empty)"}`,
5029
+ `Current PR head SHA: ${evidence.headSha || "unknown"}`,
5030
+ `Greptile mapping: ${evidence.greptile.mapping}`,
5031
+ evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
5032
+ "",
5033
+ "Gate reasons:",
5034
+ ...result.reasons.length ? result.reasons.map((reason) => `- ${reason}`) : ["- No reasons recorded"],
5035
+ "",
5036
+ "Required evidence read status:",
5037
+ evidence.readErrors.length ? JSON.stringify(evidence.readErrors, null, 2) : "All required PR evidence surfaces were read completely.",
5038
+ "",
5039
+ "Full PR title:",
5040
+ evidence.title || "(empty)",
5041
+ "",
5042
+ "PR body excerpt:",
5043
+ evidence.body ? promptExcerpt(evidence.body) : "(empty)",
5044
+ "",
5045
+ "All review comments on changed files:",
5046
+ evidence.changedFileReviewComments.length ? promptJsonExcerpt(evidence.changedFileReviewComments) : "[]",
5047
+ "",
5048
+ "Unresolved review threads:",
5049
+ unresolvedReviewThreads.length ? promptJsonExcerpt(unresolvedReviewThreads) : "[]",
5050
+ "",
5051
+ "Relevant issue-level PR comments:",
5052
+ evidence.relevantIssueComments.length ? promptJsonExcerpt(evidence.relevantIssueComments) : "[]",
5053
+ "",
5054
+ "CI/check failures and pending checks:",
5055
+ promptJsonExcerpt({ failures: evidence.checkFailures, pending: evidence.pendingChecks, rollup: evidence.statusCheckRollup }),
5056
+ "",
5057
+ "Greptile evidence:",
5058
+ promptJsonExcerpt(evidence.greptile)
5059
+ ];
5060
+ if (result.artifacts) {
5061
+ lines.push("", "Full evidence artifacts:", JSON.stringify(result.artifacts, null, 2));
5062
+ }
5063
+ return lines.join(`
5064
+ `);
5065
+ }
5066
+ function persistPrReviewCycleArtifacts(input) {
5067
+ const cycleName = input.final ? `${input.cycle}-final` : String(input.cycle);
5068
+ const taskArtifactRoot = input.artifactRoot?.trim() ? input.artifactRoot : resolve21(input.projectRoot, "artifacts", input.taskId);
5069
+ const root = resolve21(taskArtifactRoot, "pr-review-cycles", cycleName);
5070
+ mkdirSync8(root, { recursive: true });
5071
+ const finalMergeGateResultPath = input.final ? resolve21(taskArtifactRoot, "merge-gate-final.json") : undefined;
5072
+ const paths = {
5073
+ root,
5074
+ prTitlePath: resolve21(root, "pr-title.md"),
5075
+ prBodyPath: resolve21(root, "pr-body.md"),
5076
+ prCommentsPath: resolve21(root, "pr-comments.json"),
5077
+ reviewThreadsPath: resolve21(root, "review-threads.json"),
5078
+ reviewCommentsPath: resolve21(root, "review-comments.json"),
5079
+ checkRollupPath: resolve21(root, "check-rollup.json"),
5080
+ greptileEvidencePath: resolve21(root, "greptile-evidence.json"),
5081
+ mergeGateResultPath: resolve21(root, "merge-gate-result.json"),
5082
+ steeringPromptPath: resolve21(root, "agent-steering-prompt.md"),
5083
+ ...finalMergeGateResultPath ? { finalMergeGateResultPath } : {}
5084
+ };
5085
+ writeFileSync8(paths.prTitlePath, input.result.evidence.title || "", "utf8");
5086
+ writeFileSync8(paths.prBodyPath, input.result.evidence.body || "", "utf8");
5087
+ writeFileSync8(paths.prCommentsPath, `${JSON.stringify(input.result.evidence.relevantIssueComments, null, 2)}
5088
+ `, "utf8");
5089
+ writeFileSync8(paths.reviewThreadsPath, `${JSON.stringify(input.result.evidence.reviewThreads, null, 2)}
5090
+ `, "utf8");
5091
+ writeFileSync8(paths.reviewCommentsPath, `${JSON.stringify(input.result.evidence.changedFileReviewComments, null, 2)}
5092
+ `, "utf8");
5093
+ writeFileSync8(paths.checkRollupPath, `${JSON.stringify(input.result.evidence.statusCheckRollup, null, 2)}
5094
+ `, "utf8");
5095
+ writeFileSync8(paths.greptileEvidencePath, `${JSON.stringify(input.result.evidence.greptile, null, 2)}
5096
+ `, "utf8");
5097
+ const mergeGatePayload = {
5098
+ approved: input.result.approved,
5099
+ pending: input.result.pending,
5100
+ reasons: input.result.reasons,
5101
+ warnings: input.result.warnings,
5102
+ actionableFeedback: input.result.actionableFeedback,
5103
+ prUrl: input.result.evidence.prUrl,
5104
+ title: input.result.evidence.title,
5105
+ headSha: input.result.evidence.headSha,
5106
+ readErrors: input.result.evidence.readErrors,
5107
+ greptile: input.result.evidence.greptile,
5108
+ evidence: input.result.evidence,
5109
+ cycleArtifactRoot: root
5110
+ };
5111
+ writeFileSync8(paths.mergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
5112
+ `, "utf8");
5113
+ if (paths.finalMergeGateResultPath) {
5114
+ writeFileSync8(paths.finalMergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
5115
+ `, "utf8");
5116
+ }
5117
+ writeFileSync8(paths.steeringPromptPath, input.steeringPrompt, "utf8");
5118
+ return paths;
5119
+ }
5120
+ async function runStrictPrMergeGate(input) {
5121
+ const evidence = await collectPrReviewEvidence(input);
5122
+ const base = evaluateStrictPrMergeGate(evidence);
5123
+ const preliminaryPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts: undefined });
5124
+ const artifacts = persistPrReviewCycleArtifacts({
5125
+ projectRoot: input.projectRoot,
5126
+ taskId: input.taskId,
5127
+ cycle: input.cycle,
5128
+ artifactRoot: input.artifactRoot,
5129
+ result: base,
5130
+ steeringPrompt: preliminaryPrompt,
5131
+ final: input.final
5132
+ });
5133
+ const steeringPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts });
5134
+ writeFileSync8(artifacts.steeringPromptPath, steeringPrompt, "utf8");
5135
+ return { ...base, artifacts, steeringPrompt };
5136
+ }
5137
+
5138
+ // packages/runtime/src/control-plane/native/verifier.ts
4244
5139
  async function verifyTask(options) {
4245
5140
  const paths = resolveHarnessPaths(options.projectRoot);
4246
5141
  const taskId = options.taskId;
4247
5142
  const normalizedTaskId = lookupTask(options.projectRoot, taskId);
4248
5143
  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");
5144
+ mkdirSync9(artifactDir, { recursive: true });
5145
+ const validationSummaryPath = resolve22(artifactDir, "validation-summary.json");
5146
+ const reviewFeedbackPath = resolve22(artifactDir, "review-feedback.md");
5147
+ const reviewStatePath = resolve22(artifactDir, "review-state.json");
5148
+ const greptileRawPath = resolve22(artifactDir, "review-greptile-raw.json");
4254
5149
  const prStates = readPrMetadata(options.projectRoot, taskId);
4255
5150
  const prState = prStates[0] || null;
4256
5151
  const localReasons = [];
@@ -4271,12 +5166,12 @@ async function verifyTask(options) {
4271
5166
  }
4272
5167
  }
4273
5168
  for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
4274
- const requiredPath = resolve21(artifactDir, file);
5169
+ const requiredPath = resolve22(artifactDir, file);
4275
5170
  if (!existsSync18(requiredPath)) {
4276
5171
  localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
4277
5172
  }
4278
5173
  }
4279
- const taskResultPath = resolve21(artifactDir, "task-result.json");
5174
+ const taskResultPath = resolve22(artifactDir, "task-result.json");
4280
5175
  if (existsSync18(taskResultPath)) {
4281
5176
  const taskResult = await readJsonFile2(taskResultPath);
4282
5177
  const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
@@ -4290,7 +5185,7 @@ async function verifyTask(options) {
4290
5185
  localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
4291
5186
  }
4292
5187
  }
4293
- const nextActionsPath = resolve21(artifactDir, "next-actions.md");
5188
+ const nextActionsPath = resolve22(artifactDir, "next-actions.md");
4294
5189
  if (existsSync18(nextActionsPath)) {
4295
5190
  const nextActionsContent = await Bun.file(nextActionsPath).text();
4296
5191
  if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
@@ -4328,7 +5223,7 @@ async function verifyTask(options) {
4328
5223
  aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
4329
5224
  }
4330
5225
  if (persistArtifacts && ai.rawResponse) {
4331
- writeFileSync8(greptileRawPath, `${ai.rawResponse}
5226
+ writeFileSync9(greptileRawPath, `${ai.rawResponse}
4332
5227
  `, "utf-8");
4333
5228
  }
4334
5229
  } else if (!options.skipAiReview && reviewMode === "off") {
@@ -4837,7 +5732,7 @@ function writeFeedbackFile(options) {
4837
5732
  if (options.aiRawFeedback) {
4838
5733
  lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
4839
5734
  }
4840
- writeFileSync8(options.output, `${lines.join(`
5735
+ writeFileSync9(options.output, `${lines.join(`
4841
5736
  `)}
4842
5737
  `, "utf-8");
4843
5738
  }
@@ -4854,7 +5749,7 @@ function writeReviewStateFile(options) {
4854
5749
  ai_warnings: options.aiWarnings,
4855
5750
  updated_at: nowIso()
4856
5751
  };
4857
- writeFileSync8(options.output, `${JSON.stringify(payload, null, 2)}
5752
+ writeFileSync9(options.output, `${JSON.stringify(payload, null, 2)}
4858
5753
  `, "utf-8");
4859
5754
  }
4860
5755
  async function runGreptileReviewForPr(options) {
@@ -5036,7 +5931,8 @@ async function runGreptileReviewForPr(options) {
5036
5931
  }
5037
5932
  };
5038
5933
  }
5039
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
5934
+ const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
5935
+ if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(blockerScanBody)) {
5040
5936
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
5041
5937
  return {
5042
5938
  verdict: "REJECT",
@@ -5052,44 +5948,78 @@ async function runGreptileReviewForPr(options) {
5052
5948
  }
5053
5949
  };
5054
5950
  }
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
5951
+ if (score?.scale === 5 && score.value < 5) {
5952
+ reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
5953
+ return {
5954
+ verdict: "REJECT",
5955
+ feedback,
5956
+ reasons,
5957
+ warnings,
5958
+ rawPayload: {
5959
+ pr: options.prState,
5960
+ codeReviews: reviewsPayload,
5961
+ selectedReview,
5962
+ reviewDetails,
5963
+ comments: commentsPayload,
5964
+ score
5965
+ }
5966
+ };
5967
+ }
5968
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
5969
+ let strictGate = null;
5970
+ try {
5971
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
5972
+ projectRoot: options.projectRoot,
5973
+ taskId: options.taskId,
5974
+ prUrl,
5975
+ apiSignals: [{
5976
+ id: selectedReview.id,
5977
+ body: reviewBody,
5978
+ reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
5979
+ status: selectedReview.status
5980
+ }]
5981
+ });
5982
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
5983
+ } catch (error) {
5984
+ reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
5985
+ return {
5986
+ verdict: "REJECT",
5987
+ feedback,
5988
+ reasons,
5989
+ warnings,
5990
+ rawPayload: {
5991
+ pr: options.prState,
5992
+ codeReviews: reviewsPayload,
5993
+ selectedReview,
5994
+ reviewDetails,
5995
+ comments: commentsPayload,
5996
+ score
5997
+ }
5998
+ };
5999
+ }
6000
+ if (!strictGate.approved) {
6001
+ return {
6002
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
6003
+ feedback,
6004
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
6005
+ warnings: [...warnings, ...strictGate.warnings],
6006
+ rawPayload: {
6007
+ pr: options.prState,
6008
+ codeReviews: reviewsPayload,
6009
+ selectedReview,
6010
+ reviewDetails,
6011
+ comments: commentsPayload,
6012
+ score,
6013
+ strictGate: {
6014
+ approved: strictGate.approved,
6015
+ pending: strictGate.pending,
6016
+ reasons: strictGate.reasons,
6017
+ warnings: strictGate.warnings,
6018
+ greptile: strictGate.evidence.greptile,
6019
+ readErrors: strictGate.evidence.readErrors
5087
6020
  }
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
- }
6021
+ }
6022
+ };
5093
6023
  }
5094
6024
  return {
5095
6025
  verdict: "APPROVE",
@@ -5101,7 +6031,15 @@ async function runGreptileReviewForPr(options) {
5101
6031
  codeReviews: reviewsPayload,
5102
6032
  selectedReview,
5103
6033
  reviewDetails,
5104
- comments: commentsPayload
6034
+ comments: commentsPayload,
6035
+ strictGate: {
6036
+ approved: strictGate.approved,
6037
+ pending: strictGate.pending,
6038
+ reasons: strictGate.reasons,
6039
+ warnings: strictGate.warnings,
6040
+ greptile: strictGate.evidence.greptile,
6041
+ readErrors: strictGate.evidence.readErrors
6042
+ }
5105
6043
  }
5106
6044
  };
5107
6045
  }
@@ -5125,7 +6063,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5125
6063
  let threads = [];
5126
6064
  let actionableThreads = [];
5127
6065
  let checkRollup = [];
5128
- let checkState = { pending: false, completed: false };
6066
+ let checkState2 = { pending: false, completed: false };
5129
6067
  for (let attempt = 0;; attempt += 1) {
5130
6068
  reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
5131
6069
  selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
@@ -5134,15 +6072,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5134
6072
  threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
5135
6073
  actionableThreads = filterActionableGithubGreptileThreads(threads);
5136
6074
  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);
6075
+ checkState2 = classifyGithubGreptileCheckState(checkRollup);
6076
+ const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
5139
6077
  if (!shouldContinueGithubGreptileFallbackPolling({
5140
6078
  attempt,
5141
6079
  pollAttempts: options.pollAttempts,
5142
- checkState,
6080
+ checkState: checkState2,
5143
6081
  fallbackReview,
5144
6082
  selectedReview,
5145
- approvedViaReviewedAncestor: approvedViaReviewedAncestor2
6083
+ approvedViaReviewedAncestor
5146
6084
  })) {
5147
6085
  break;
5148
6086
  }
@@ -5170,7 +6108,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5170
6108
  ].filter(Boolean).join(`
5171
6109
  `);
5172
6110
  const warnings = buildGithubGreptileFallbackWarnings(options);
5173
- if (checkState.pending) {
6111
+ if (checkState2.pending) {
5174
6112
  return {
5175
6113
  verdict: "SKIP",
5176
6114
  feedback,
@@ -5181,34 +6119,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5181
6119
  rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
5182
6120
  };
5183
6121
  }
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) {
6122
+ const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
6123
+ let strictGate;
6124
+ try {
6125
+ const strictEvidence = await collectStrictPrEvidenceForVerifier({
6126
+ projectRoot: options.projectRoot,
6127
+ taskId: options.taskId,
6128
+ prUrl
6129
+ });
6130
+ strictGate = evaluateStrictPrMergeGate(strictEvidence);
6131
+ } catch (error) {
5208
6132
  return {
5209
6133
  verdict: "REJECT",
5210
6134
  feedback,
5211
- reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
6135
+ reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
5212
6136
  warnings,
5213
6137
  rawPayload: {
5214
6138
  pr: options.prState,
@@ -5221,44 +6145,30 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5221
6145
  }
5222
6146
  };
5223
6147
  }
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
- }
6148
+ if (!strictGate.approved) {
5242
6149
  return {
5243
- verdict: "SKIP",
6150
+ verdict: strictGate.pending ? "SKIP" : "REJECT",
5244
6151
  feedback,
5245
- reasons: [
5246
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
5247
- ],
5248
- warnings,
6152
+ reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
6153
+ warnings: [...warnings, ...strictGate.warnings],
5249
6154
  rawPayload: {
5250
6155
  pr: options.prState,
5251
6156
  selectedReview: fallbackReview,
5252
6157
  reviews,
5253
6158
  threads,
5254
6159
  checkRollup,
6160
+ actionableThreads,
6161
+ strictGate: {
6162
+ approved: strictGate.approved,
6163
+ pending: strictGate.pending,
6164
+ reasons: strictGate.reasons,
6165
+ warnings: strictGate.warnings,
6166
+ greptile: strictGate.evidence.greptile
6167
+ },
5255
6168
  ...buildGithubGreptileFallbackRawPayload(options)
5256
6169
  }
5257
6170
  };
5258
6171
  }
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
6172
  return {
5263
6173
  verdict: "APPROVE",
5264
6174
  feedback,
@@ -5270,6 +6180,13 @@ async function runGithubGreptileFallbackReviewForPr(options) {
5270
6180
  reviews,
5271
6181
  threads,
5272
6182
  checkRollup,
6183
+ strictGate: {
6184
+ approved: strictGate.approved,
6185
+ pending: strictGate.pending,
6186
+ reasons: strictGate.reasons,
6187
+ warnings: strictGate.warnings,
6188
+ greptile: strictGate.evidence.greptile
6189
+ },
5273
6190
  ...buildGithubGreptileFallbackRawPayload(options)
5274
6191
  }
5275
6192
  };
@@ -5455,6 +6372,20 @@ function runGhJson(projectRoot, args) {
5455
6372
  throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
5456
6373
  }
5457
6374
  }
6375
+ async function collectStrictPrEvidenceForVerifier(input) {
6376
+ return collectPrReviewEvidence({
6377
+ projectRoot: input.projectRoot,
6378
+ prUrl: input.prUrl,
6379
+ taskId: input.taskId,
6380
+ runId: "verifier",
6381
+ cycle: 0,
6382
+ apiSignals: input.apiSignals ?? [],
6383
+ command: async (args, options) => {
6384
+ const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
6385
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
6386
+ }
6387
+ });
6388
+ }
5458
6389
  function deriveRepoName(projectRoot, prState) {
5459
6390
  const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
5460
6391
  if (fromUrl?.[1]) {
@@ -5469,8 +6400,9 @@ function resolvePrHeadSha(projectRoot, prState) {
5469
6400
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5470
6401
  return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
5471
6402
  }
5472
- function isGreptileGithubLogin(login) {
5473
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
6403
+ function isGreptileGithubLogin2(login) {
6404
+ const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
6405
+ return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
5474
6406
  }
5475
6407
  function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
5476
6408
  const matching = sortGithubGreptileReviews(reviews);
@@ -5487,7 +6419,7 @@ function pickLatestGithubGreptileReview(reviews) {
5487
6419
  return sortGithubGreptileReviews(reviews)[0] || null;
5488
6420
  }
5489
6421
  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 || ""));
6422
+ return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
5491
6423
  }
5492
6424
  function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
5493
6425
  const response = runGhJson(projectRoot, [
@@ -5560,32 +6492,6 @@ function classifyGithubGreptileCheckState(checks) {
5560
6492
  }
5561
6493
  return { pending: false, completed: false };
5562
6494
  }
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
6495
  function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
5590
6496
  const [owner, name] = repoName.split("/");
5591
6497
  if (!owner || !name) {
@@ -5611,7 +6517,7 @@ function filterActionableGithubGreptileThreads(threads) {
5611
6517
  return [];
5612
6518
  }
5613
6519
  const comments = thread.comments?.nodes || [];
5614
- const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
6520
+ const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
5615
6521
  if (!latestGreptileComment?.path?.trim()) {
5616
6522
  return [];
5617
6523
  }
@@ -5620,7 +6526,7 @@ function filterActionableGithubGreptileThreads(threads) {
5620
6526
  }
5621
6527
  function resolvePrRepoRoot(projectRoot, prState) {
5622
6528
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
5623
- if (prState.target === "monorepo" && runtimeWorkspace && existsSync18(resolve21(runtimeWorkspace, ".git"))) {
6529
+ if (prState.target === "monorepo" && runtimeWorkspace && existsSync18(resolve22(runtimeWorkspace, ".git"))) {
5624
6530
  return runtimeWorkspace;
5625
6531
  }
5626
6532
  const paths = resolveHarnessPaths(projectRoot);
@@ -5633,11 +6539,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
5633
6539
  const repoRoot = resolvePrRepoRoot(projectRoot, prState);
5634
6540
  return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
5635
6541
  }
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
6542
  function summarizeComment(input) {
5642
6543
  const text = stripHtml(input).replace(/\s+/g, " ").trim();
5643
6544
  return text.length > 160 ? `${text.slice(0, 157)}...` : text;
@@ -5646,31 +6547,14 @@ function asGreptileInfrastructureWarning(reason) {
5646
6547
  return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
5647
6548
  }
5648
6549
  function isAiReviewApproved(input) {
6550
+ if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
6551
+ return false;
6552
+ }
5649
6553
  if (input.reviewMode !== "required") {
5650
6554
  return true;
5651
6555
  }
5652
6556
  return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
5653
6557
  }
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
6558
 
5675
6559
  // packages/runtime/src/control-plane/provider/runtime-instructions.ts
5676
6560
  var CLAUDE_ROUTER_TOOL_NAMES = [
@@ -5710,14 +6594,14 @@ function taskArtifacts(projectRoot, taskId) {
5710
6594
  throw new Error("No active task.");
5711
6595
  }
5712
6596
  const paths = resolveHarnessPaths(projectRoot);
5713
- const artifactDir = resolve22(paths.artifactsDir, activeTask);
5714
- mkdirSync9(artifactDir, { recursive: true });
6597
+ const artifactDir = resolve23(paths.artifactsDir, activeTask);
6598
+ mkdirSync10(artifactDir, { recursive: true });
5715
6599
  const changed = changedFilesForTask(projectRoot, activeTask, true);
5716
- writeFileSync9(resolve22(artifactDir, "changed-files.txt"), `${changed.join(`
6600
+ writeFileSync10(resolve23(artifactDir, "changed-files.txt"), `${changed.join(`
5717
6601
  `)}
5718
6602
  `, "utf-8");
5719
6603
  console.log(`changed-files.txt: ${changed.length} files`);
5720
- const taskResultPath = resolve22(artifactDir, "task-result.json");
6604
+ const taskResultPath = resolve23(artifactDir, "task-result.json");
5721
6605
  if (!existsSync19(taskResultPath)) {
5722
6606
  const template = {
5723
6607
  task_id: activeTask,
@@ -5725,24 +6609,24 @@ function taskArtifacts(projectRoot, taskId) {
5725
6609
  summary: "TODO: Write a one-line summary of what you did",
5726
6610
  completed_at: nowIso()
5727
6611
  };
5728
- writeFileSync9(taskResultPath, `${JSON.stringify(template, null, 2)}
6612
+ writeFileSync10(taskResultPath, `${JSON.stringify(template, null, 2)}
5729
6613
  `, "utf-8");
5730
6614
  console.log("task-result.json: created (update the summary!)");
5731
6615
  } else {
5732
6616
  console.log("task-result.json: already exists");
5733
6617
  }
5734
- const decisionLogPath = resolve22(artifactDir, "decision-log.md");
6618
+ const decisionLogPath = resolve23(artifactDir, "decision-log.md");
5735
6619
  if (!existsSync19(decisionLogPath)) {
5736
6620
  const content = `# Decision Log: ${activeTask}
5737
6621
 
5738
6622
  Record key decisions here using: rig-agent record decision "..."
5739
6623
  `;
5740
- writeFileSync9(decisionLogPath, content, "utf-8");
6624
+ writeFileSync10(decisionLogPath, content, "utf-8");
5741
6625
  console.log("decision-log.md: created (record your decisions!)");
5742
6626
  } else {
5743
6627
  console.log("decision-log.md: already exists");
5744
6628
  }
5745
- const nextActionsPath = resolve22(artifactDir, "next-actions.md");
6629
+ const nextActionsPath = resolve23(artifactDir, "next-actions.md");
5746
6630
  if (!existsSync19(nextActionsPath)) {
5747
6631
  const content = [
5748
6632
  `# Next Actions: ${activeTask}`,
@@ -5760,12 +6644,12 @@ Record key decisions here using: rig-agent record decision "..."
5760
6644
  ""
5761
6645
  ].join(`
5762
6646
  `);
5763
- writeFileSync9(nextActionsPath, content, "utf-8");
6647
+ writeFileSync10(nextActionsPath, content, "utf-8");
5764
6648
  console.log("next-actions.md: created (add recommendations for downstream tasks!)");
5765
6649
  } else {
5766
6650
  console.log("next-actions.md: already exists");
5767
6651
  }
5768
- const validationSummaryPath = resolve22(artifactDir, "validation-summary.json");
6652
+ const validationSummaryPath = resolve23(artifactDir, "validation-summary.json");
5769
6653
  if (existsSync19(validationSummaryPath)) {
5770
6654
  console.log("validation-summary.json: already exists");
5771
6655
  } else {
@@ -5832,7 +6716,7 @@ function collectTaskChangedFiles(projectRoot, taskId, includeCommitted) {
5832
6716
  [projectRoot, ""],
5833
6717
  [monorepoRepoRoot, ""]
5834
6718
  ]) {
5835
- if (!existsSync19(resolve22(repo, ".git"))) {
6719
+ if (!existsSync19(resolve23(repo, ".git"))) {
5836
6720
  continue;
5837
6721
  }
5838
6722
  if (includeCommitted && repo === monorepoRepoRoot) {
@@ -5870,8 +6754,8 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
5870
6754
  }
5871
6755
  function resolveTaskMonorepoRoot(projectRoot) {
5872
6756
  const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
5873
- if (runtimeWorkspace && existsSync19(resolve22(runtimeWorkspace, ".git"))) {
5874
- return resolve22(runtimeWorkspace);
6757
+ if (runtimeWorkspace && existsSync19(resolve23(runtimeWorkspace, ".git"))) {
6758
+ return resolve23(runtimeWorkspace);
5875
6759
  }
5876
6760
  return resolveHarnessPaths(projectRoot).monorepoRoot;
5877
6761
  }
@@ -5899,7 +6783,7 @@ function resolveRuntimeInitialHeadCommit(projectRoot, repo) {
5899
6783
  const runtimeContext = loadRuntimeContextFromEnv();
5900
6784
  if (runtimeContext?.initialHeadCommits?.monorepo?.trim()) {
5901
6785
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5902
- if (resolve22(monorepoRoot) === resolve22(repo)) {
6786
+ if (resolve23(monorepoRoot) === resolve23(repo)) {
5903
6787
  return runtimeContext.initialHeadCommits.monorepo.trim();
5904
6788
  }
5905
6789
  }
@@ -5909,7 +6793,7 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
5909
6793
  const runtimeContext = loadRuntimeContextFromEnv();
5910
6794
  if (runtimeContext?.monorepoBaseCommit?.trim()) {
5911
6795
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5912
- if (resolve22(monorepoRoot) === resolve22(repo)) {
6796
+ if (resolve23(monorepoRoot) === resolve23(repo)) {
5913
6797
  return runtimeContext.monorepoBaseCommit.trim();
5914
6798
  }
5915
6799
  }
@@ -5943,7 +6827,7 @@ function resolveRuntimeDirtyBaseline(projectRoot, repo) {
5943
6827
  return new Set;
5944
6828
  }
5945
6829
  const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
5946
- const selected = resolve22(repo) === resolve22(monorepoRoot) ? dirtyFiles.monorepo : resolve22(repo) === resolve22(projectRoot) ? dirtyFiles.project : undefined;
6830
+ const selected = resolve23(repo) === resolve23(monorepoRoot) ? dirtyFiles.monorepo : resolve23(repo) === resolve23(projectRoot) ? dirtyFiles.project : undefined;
5947
6831
  return new Set((selected || []).map((file) => normalizeChangedFilePath(file)).filter(Boolean));
5948
6832
  }
5949
6833
  function normalizeChangedFilePath(file) {
@@ -6001,8 +6885,8 @@ function isRuntimeGatewayGhPath(candidate) {
6001
6885
  }
6002
6886
  function resolveOptionalMonorepoRoot(projectRoot) {
6003
6887
  const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
6004
- if (runtimeWorkspace && existsSync20(resolve23(runtimeWorkspace, ".git"))) {
6005
- return resolve23(runtimeWorkspace);
6888
+ if (runtimeWorkspace && existsSync20(resolve24(runtimeWorkspace, ".git"))) {
6889
+ return resolve24(runtimeWorkspace);
6006
6890
  }
6007
6891
  try {
6008
6892
  return resolveMonorepoRoot2(projectRoot);
@@ -6054,7 +6938,7 @@ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
6054
6938
  }
6055
6939
  const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot2(projectRoot) : projectRoot;
6056
6940
  const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
6057
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
6941
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6058
6942
  throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
6059
6943
  }
6060
6944
  const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
@@ -6114,7 +6998,7 @@ function gitOpenPr(options) {
6114
6998
  } else if (taskId) {
6115
6999
  gitSyncBranch(options.projectRoot, taskId, "project");
6116
7000
  }
6117
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
7001
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6118
7002
  throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
6119
7003
  }
6120
7004
  const branch = branchName(options.projectRoot, repoRoot);
@@ -6267,8 +7151,9 @@ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
6267
7151
  const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
6268
7152
  if (sourceIssueId) {
6269
7153
  const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
6270
- if (match) {
6271
- const [, sourceRepo, issueNumber] = match;
7154
+ if (match?.[1] && match[2]) {
7155
+ const sourceRepo = match[1];
7156
+ const issueNumber = match[2];
6272
7157
  return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
6273
7158
  }
6274
7159
  }
@@ -6346,7 +7231,7 @@ function gitMergePr(options) {
6346
7231
  }
6347
7232
  const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
6348
7233
  const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
6349
- if (!existsSync20(resolve23(repoRoot, ".git"))) {
7234
+ if (!existsSync20(resolve24(repoRoot, ".git"))) {
6350
7235
  throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
6351
7236
  }
6352
7237
  const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
@@ -6363,56 +7248,43 @@ function gitMergePr(options) {
6363
7248
  if (isDraft) {
6364
7249
  throw new Error(`Cannot auto-merge draft PR ${options.pr.url}.`);
6365
7250
  }
7251
+ const strictGateHeadSha = options.strictGateHeadSha?.trim();
7252
+ if (!strictGateHeadSha) {
7253
+ throw new Error(`Refusing to merge PR ${options.pr.url}: strict merge gate did not provide a current head SHA.`);
7254
+ }
6366
7255
  const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
6367
7256
  const method = options.method || "squash";
6368
7257
  mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
7258
+ mergeArgs.push("--match-head-commit", strictGateHeadSha);
6369
7259
  if (options.deleteBranch !== false) {
6370
7260
  mergeArgs.push("--delete-branch");
6371
7261
  }
6372
- const autoMergeArgs = [...mergeArgs, "--auto"];
6373
- const autoMerge = runCapture2(autoMergeArgs, repoRoot);
6374
- if (autoMerge.exitCode === 0) {
6375
- const postAutoMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
6376
- if (postAutoMergeState.state === "MERGED" || postAutoMergeState.mergedAt) {
6377
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6378
- return { status: "merged", url: options.pr.url };
6379
- }
6380
- if (postAutoMergeState.state === "OPEN" && postAutoMergeState.autoMergeRequest) {
6381
- if (canAdminMergeApprovedPr(postAutoMergeState)) {
6382
- const adminMergeArgs = [...mergeArgs];
6383
- if (postAutoMergeState.headRefOid) {
6384
- adminMergeArgs.push("--match-head-commit", postAutoMergeState.headRefOid);
6385
- }
6386
- adminMergeArgs.push("--admin");
6387
- const adminMerge = runCapture2(adminMergeArgs, repoRoot);
6388
- if (adminMerge.exitCode === 0) {
6389
- const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
6390
- if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
6391
- console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
6392
- return { status: "merged", url: options.pr.url };
6393
- }
6394
- throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
6395
- }
6396
- const adminMergeMessage = `${adminMerge.stderr}
6397
- ${adminMerge.stdout}`.trim();
6398
- if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
6399
- throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
6400
- }
7262
+ const directMerge = runCapture2(mergeArgs, repoRoot);
7263
+ if (directMerge.exitCode === 0) {
7264
+ console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
7265
+ return { status: "merged", url: options.pr.url };
7266
+ }
7267
+ const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
7268
+ if (canAdminMergeApprovedPr(postDirectState)) {
7269
+ const adminMergeArgs = [...mergeArgs, "--admin"];
7270
+ const adminMerge = runCapture2(adminMergeArgs, repoRoot);
7271
+ if (adminMerge.exitCode === 0) {
7272
+ const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
7273
+ if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
7274
+ console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
7275
+ return { status: "merged", url: options.pr.url };
6401
7276
  }
6402
- console.log(`Auto-merge enabled (${options.pr.repoLabel}): ${options.pr.url}`);
6403
- return { status: "auto-merge-enabled", url: options.pr.url };
7277
+ throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
7278
+ }
7279
+ const adminMergeMessage = `${adminMerge.stderr}
7280
+ ${adminMerge.stdout}`.trim();
7281
+ if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
7282
+ throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
6404
7283
  }
6405
- 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.`);
6406
- }
6407
- const autoMergeMessage = `${autoMerge.stderr}
6408
- ${autoMerge.stdout}`.trim();
6409
- const autoMergeUnsupported = /auto.?merge.*(not enabled|not allowed|disabled|unsupported)|enablePullRequestAutoMerge|Auto merge is not allowed/i.test(autoMergeMessage);
6410
- if (!autoMergeUnsupported) {
6411
- throw new Error(`Failed to auto-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${autoMergeMessage}`);
6412
7284
  }
6413
- runOrThrow(options.projectRoot, mergeArgs, `Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}`);
6414
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6415
- return { status: "merged", url: options.pr.url };
7285
+ const directMergeMessage = `${directMerge.stderr}
7286
+ ${directMerge.stdout}`.trim();
7287
+ throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
6416
7288
  }
6417
7289
  function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
6418
7290
  const mergeable = prState.mergeable.toUpperCase();
@@ -6423,8 +7295,8 @@ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
6423
7295
  }
6424
7296
  function writePrMetadata(projectRoot, taskId, result) {
6425
7297
  const dir = artifactDirForId(projectRoot, taskId);
6426
- mkdirSync10(dir, { recursive: true });
6427
- const path = resolve23(dir, "pr-state.json");
7298
+ mkdirSync11(dir, { recursive: true });
7299
+ const path = resolve24(dir, "pr-state.json");
6428
7300
  let prs = {};
6429
7301
  if (existsSync20(path)) {
6430
7302
  try {
@@ -6444,11 +7316,11 @@ function writePrMetadata(projectRoot, taskId, result) {
6444
7316
  ...primary || {},
6445
7317
  updated_at: nowIso()
6446
7318
  };
6447
- writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
7319
+ writeFileSync11(path, `${JSON.stringify(artifact, null, 2)}
6448
7320
  `, "utf-8");
6449
7321
  }
6450
7322
  function readPrMetadata(projectRoot, taskId) {
6451
- const path = resolve23(artifactDirForId(projectRoot, taskId), "pr-state.json");
7323
+ const path = resolve24(artifactDirForId(projectRoot, taskId), "pr-state.json");
6452
7324
  if (!existsSync20(path)) {
6453
7325
  return [];
6454
7326
  }
@@ -6525,7 +7397,7 @@ function resolveGithubCliBinary(projectRoot) {
6525
7397
  }
6526
7398
  const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
6527
7399
  for (const entry of explicitPathEntries) {
6528
- candidates.add(resolve23(entry, "gh"));
7400
+ candidates.add(resolve24(entry, "gh"));
6529
7401
  }
6530
7402
  const bunResolved = Bun.which("gh");
6531
7403
  if (bunResolved) {
@@ -6562,7 +7434,7 @@ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
6562
7434
  return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
6563
7435
  }
6564
7436
  function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
6565
- const normalizedGitRoot = resolve23(gitRoot);
7437
+ const normalizedGitRoot = resolve24(gitRoot);
6566
7438
  if (visited.has(normalizedGitRoot)) {
6567
7439
  return "";
6568
7440
  }
@@ -6634,7 +7506,7 @@ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
6634
7506
  return remotes.includes("origin") ? "origin" : remotes[0];
6635
7507
  }
6636
7508
  function gitQuery(projectRoot, gitRoot, cwd, ...args) {
6637
- const gitArgs = existsSync20(resolve23(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveGitBinary(projectRoot), "--git-dir", gitRoot, ...args];
7509
+ const gitArgs = existsSync20(resolve24(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveGitBinary(projectRoot), "--git-dir", gitRoot, ...args];
6638
7510
  return runCapture2(gitArgs, cwd, projectRoot);
6639
7511
  }
6640
7512
  function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
@@ -6652,7 +7524,7 @@ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
6652
7524
  } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
6653
7525
  return "";
6654
7526
  } else if (!isAbsolute2(normalized)) {
6655
- candidate = resolve23(gitRoot, normalized);
7527
+ candidate = resolve24(gitRoot, normalized);
6656
7528
  }
6657
7529
  return existsSync20(candidate) ? candidate : "";
6658
7530
  }
@@ -6781,7 +7653,7 @@ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef
6781
7653
  return best;
6782
7654
  }
6783
7655
  function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
6784
- if (!existsSync20(resolve23(repo, ".git"))) {
7656
+ if (!existsSync20(resolve24(repo, ".git"))) {
6785
7657
  console.log(`Skipping ${label}: repo not available (${repo})`);
6786
7658
  return;
6787
7659
  }
@@ -6813,7 +7685,7 @@ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files
6813
7685
  console.log(`Committed ${label}: ${message}`);
6814
7686
  }
6815
7687
  function readChangedFilesManifest(projectRoot, taskId) {
6816
- const manifestPath = resolve23(artifactDirForId(projectRoot, taskId), "changed-files.txt");
7688
+ const manifestPath = resolve24(artifactDirForId(projectRoot, taskId), "changed-files.txt");
6817
7689
  if (!existsSync20(manifestPath)) {
6818
7690
  return [];
6819
7691
  }
@@ -6821,10 +7693,10 @@ function readChangedFilesManifest(projectRoot, taskId) {
6821
7693
  return [...new Set(files)];
6822
7694
  }
6823
7695
  function refreshChangedFilesManifest(projectRoot, taskId) {
6824
- const manifestPath = resolve23(artifactDirForId(projectRoot, taskId), "changed-files.txt");
6825
- mkdirSync10(dirname11(manifestPath), { recursive: true });
7696
+ const manifestPath = resolve24(artifactDirForId(projectRoot, taskId), "changed-files.txt");
7697
+ mkdirSync11(dirname11(manifestPath), { recursive: true });
6826
7698
  const changedFiles = changedFilesForTask(projectRoot, taskId, true);
6827
- writeFileSync10(manifestPath, `${changedFiles.join(`
7699
+ writeFileSync11(manifestPath, `${changedFiles.join(`
6828
7700
  `)}
6829
7701
  `, "utf-8");
6830
7702
  return manifestPath;
@@ -6937,7 +7809,7 @@ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
6937
7809
  return result.exitCode === 0 && result.stdout.trim().length > 0;
6938
7810
  }
6939
7811
  function stageExcludePathspecs(repoRoot) {
6940
- const patterns = existsSync20(resolve23(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
7812
+ const patterns = existsSync20(resolve24(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
6941
7813
  return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
6942
7814
  }
6943
7815
  function pathResolvesBeyondSymlink(repoRoot, relativePath) {
@@ -6947,7 +7819,7 @@ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
6947
7819
  }
6948
7820
  let current = repoRoot;
6949
7821
  for (let index = 0;index < parts.length - 1; index += 1) {
6950
- current = resolve23(current, parts[index]);
7822
+ current = resolve24(current, parts[index]);
6951
7823
  try {
6952
7824
  if (lstatSync(current).isSymbolicLink()) {
6953
7825
  return true;
@@ -7017,11 +7889,11 @@ function runCapture2(command, cwd, projectRoot = cwd) {
7017
7889
  }
7018
7890
  function runtimeGitEnv(projectRoot) {
7019
7891
  const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
7020
- const runtimeHome = runtimeRoot ? resolve23(runtimeRoot, "home") : "";
7021
- const runtimeTmp = runtimeRoot ? resolve23(runtimeRoot, "tmp") : "";
7022
- const runtimeCache = runtimeRoot ? resolve23(runtimeRoot, "cache") : "";
7023
- const runtimeKnownHosts = runtimeHome ? resolve23(runtimeHome, ".ssh", "known_hosts") : "";
7024
- const runtimeKey = runtimeHome ? resolve23(runtimeHome, ".ssh", "rig-agent-key") : "";
7892
+ const runtimeHome = runtimeRoot ? resolve24(runtimeRoot, "home") : "";
7893
+ const runtimeTmp = runtimeRoot ? resolve24(runtimeRoot, "tmp") : "";
7894
+ const runtimeCache = runtimeRoot ? resolve24(runtimeRoot, "cache") : "";
7895
+ const runtimeKnownHosts = runtimeHome ? resolve24(runtimeHome, ".ssh", "known_hosts") : "";
7896
+ const runtimeKey = runtimeHome ? resolve24(runtimeHome, ".ssh", "rig-agent-key") : "";
7025
7897
  const env = {};
7026
7898
  if (ctx?.workspaceDir) {
7027
7899
  env.PROJECT_RIG_ROOT = projectRoot;
@@ -7113,7 +7985,7 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
7113
7985
  if (!runtimeRoot) {
7114
7986
  return {};
7115
7987
  }
7116
- const path = resolve23(runtimeRoot, "runtime-secrets.json");
7988
+ const path = resolve24(runtimeRoot, "runtime-secrets.json");
7117
7989
  if (!existsSync20(path)) {
7118
7990
  return {};
7119
7991
  }
@@ -7126,13 +7998,13 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
7126
7998
  }
7127
7999
  }
7128
8000
  function ensureRuntimeOpenSslConfig(runtimeHome) {
7129
- const sslDir = resolve23(runtimeHome, ".ssl");
7130
- const sslConfig = resolve23(sslDir, "openssl.cnf");
8001
+ const sslDir = resolve24(runtimeHome, ".ssl");
8002
+ const sslConfig = resolve24(sslDir, "openssl.cnf");
7131
8003
  if (!existsSync20(sslDir)) {
7132
- mkdirSync10(sslDir, { recursive: true });
8004
+ mkdirSync11(sslDir, { recursive: true });
7133
8005
  }
7134
8006
  if (!existsSync20(sslConfig)) {
7135
- writeFileSync10(sslConfig, `# Rig runtime OpenSSL config placeholder
8007
+ writeFileSync11(sslConfig, `# Rig runtime OpenSSL config placeholder
7136
8008
  `);
7137
8009
  }
7138
8010
  return sslConfig;
@@ -7150,7 +8022,7 @@ function resolveRuntimeMetadata(projectRoot) {
7150
8022
  if (contextFile) {
7151
8023
  return {
7152
8024
  ctx,
7153
- runtimeRoot: dirname11(resolve23(contextFile))
8025
+ runtimeRoot: dirname11(resolve24(contextFile))
7154
8026
  };
7155
8027
  }
7156
8028
  const inferredContextFile = findRuntimeContextFile2(projectRoot);
@@ -7166,9 +8038,9 @@ function resolveRuntimeMetadata(projectRoot) {
7166
8038
  return { ctx, runtimeRoot: "" };
7167
8039
  }
7168
8040
  function findRuntimeContextFile2(startPath) {
7169
- let current = resolve23(startPath);
8041
+ let current = resolve24(startPath);
7170
8042
  while (true) {
7171
- const candidate = resolve23(current, "runtime-context.json");
8043
+ const candidate = resolve24(current, "runtime-context.json");
7172
8044
  if (existsSync20(candidate)) {
7173
8045
  return candidate;
7174
8046
  }
@@ -7221,6 +8093,7 @@ async function main() {
7221
8093
  }
7222
8094
  const paths = resolveHarnessPaths(projectRoot);
7223
8095
  let failed = false;
8096
+ let sourceCloseoutAllowed = false;
7224
8097
  console.log(`=== Completion Verification: ${taskId} ===`);
7225
8098
  const scopes = await resolveTaskScopes(projectRoot, taskId);
7226
8099
  const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
@@ -7370,12 +8243,35 @@ async function main() {
7370
8243
  console.log("Auto-merge: skipped (no PR metadata found)");
7371
8244
  } else {
7372
8245
  let mergePending = false;
8246
+ let cycle = 0;
7373
8247
  for (const pr of prs) {
8248
+ cycle += 1;
8249
+ const gate = await runStrictPrMergeGate({
8250
+ projectRoot,
8251
+ prUrl: pr.url,
8252
+ taskId,
8253
+ runId: "completion-verification",
8254
+ cycle,
8255
+ final: true,
8256
+ command: async (args, options) => {
8257
+ const result = runCapture(["gh", ...args], options?.cwd ?? projectRoot);
8258
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
8259
+ }
8260
+ });
8261
+ if (!gate.approved) {
8262
+ console.log(`FAIL: Strict merge gate blocked PR (${pr.repoLabel}): ${pr.url}`);
8263
+ for (const reason of gate.reasons) {
8264
+ console.log(`- ${reason}`);
8265
+ }
8266
+ failed = true;
8267
+ continue;
8268
+ }
7374
8269
  const mergeResult = gitMergePr({
7375
8270
  projectRoot,
7376
8271
  pr,
7377
8272
  method: "squash",
7378
- deleteBranch: true
8273
+ deleteBranch: true,
8274
+ strictGateHeadSha: gate.evidence.headSha
7379
8275
  });
7380
8276
  if (mergeResult.status === "auto-merge-enabled") {
7381
8277
  mergePending = true;
@@ -7384,7 +8280,8 @@ async function main() {
7384
8280
  }
7385
8281
  if (mergePending) {
7386
8282
  failed = true;
7387
- } else {
8283
+ } else if (!failed) {
8284
+ sourceCloseoutAllowed = true;
7388
8285
  console.log("OK: Auto-merge complete");
7389
8286
  }
7390
8287
  }
@@ -7397,19 +8294,23 @@ async function main() {
7397
8294
  console.log(`
7398
8295
  [post] Auto-merge: skipped (not in policy completion.checks)`);
7399
8296
  }
7400
- const artifactDir = resolve24(paths.artifactsDir, taskId);
7401
- mkdirSync11(artifactDir, { recursive: true });
7402
- writeFileSync11(resolve24(artifactDir, "review-status.txt"), failed ? `REJECTED
8297
+ const artifactDir = resolve25(paths.artifactsDir, taskId);
8298
+ mkdirSync12(artifactDir, { recursive: true });
8299
+ writeFileSync12(resolve25(artifactDir, "review-status.txt"), failed ? `REJECTED
7403
8300
  ` : `APPROVED
7404
8301
  `, "utf-8");
7405
8302
  if (!failed) {
7406
8303
  await recordTaskRepoCommits(projectRoot, taskId, paths);
7407
- const closeout = await closeCompletedTaskSource(projectRoot, taskId);
7408
- if (!closeout.ok) {
7409
- console.log(`FAIL: ${closeout.message}`);
7410
- failed = true;
8304
+ if (sourceCloseoutAllowed) {
8305
+ const closeout = await closeCompletedTaskSource(projectRoot, taskId);
8306
+ if (!closeout.ok) {
8307
+ console.log(`FAIL: ${closeout.message}`);
8308
+ failed = true;
8309
+ } else {
8310
+ console.log(`OK: ${closeout.message}`);
8311
+ }
7411
8312
  } else {
7412
- console.log(`OK: ${closeout.message}`);
8313
+ console.log("Task source closeout skipped until an approved PR merge completes.");
7413
8314
  }
7414
8315
  }
7415
8316
  if (!failed) {
@@ -7442,7 +8343,7 @@ async function runBunTool(args, cwd) {
7442
8343
  };
7443
8344
  }
7444
8345
  async function runProtoQualityGate(monorepoRoot) {
7445
- const protosDir = resolve24(monorepoRoot, "packages", "protos");
8346
+ const protosDir = resolve25(monorepoRoot, "packages", "protos");
7446
8347
  if (!existsSync21(protosDir)) {
7447
8348
  console.log(`FAIL: Proto workspace not found at ${protosDir}`);
7448
8349
  return false;
@@ -7491,7 +8392,7 @@ async function runProtoQualityGate(monorepoRoot) {
7491
8392
  } else {
7492
8393
  console.log("OK: Generated TypeScript compiles");
7493
8394
  }
7494
- const workflowPath = resolve24(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
8395
+ const workflowPath = resolve25(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
7495
8396
  if (!existsSync21(workflowPath)) {
7496
8397
  console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
7497
8398
  ok = false;
@@ -7536,9 +8437,9 @@ async function readJsonFileIfPresent(path) {
7536
8437
  }
7537
8438
  async function recordVerifierFailure(projectRoot, taskId, paths) {
7538
8439
  const failedApproachesPath = paths.failedApproachesPath;
7539
- const artifactDir = resolve24(paths.artifactsDir, taskId);
7540
- const reviewStatePath = resolve24(artifactDir, "review-state.json");
7541
- const reviewFeedbackPath = resolve24(artifactDir, "review-feedback.md");
8440
+ const artifactDir = resolve25(paths.artifactsDir, taskId);
8441
+ const reviewStatePath = resolve25(artifactDir, "review-state.json");
8442
+ const reviewFeedbackPath = resolve25(artifactDir, "review-feedback.md");
7542
8443
  let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
7543
8444
  const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
7544
8445
  if (parsedReviewState) {
@@ -7552,8 +8453,8 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
7552
8453
  const content = readFileSync12(failedApproachesPath, "utf-8");
7553
8454
  attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
7554
8455
  } else {
7555
- mkdirSync11(resolve24(failedApproachesPath, ".."), { recursive: true });
7556
- writeFileSync11(failedApproachesPath, `# Failed Approaches
8456
+ mkdirSync12(resolve25(failedApproachesPath, ".."), { recursive: true });
8457
+ writeFileSync12(failedApproachesPath, `# Failed Approaches
7557
8458
 
7558
8459
  `, "utf-8");
7559
8460
  }
@@ -7591,8 +8492,8 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
7591
8492
  recorded_at: new Date().toISOString(),
7592
8493
  repos
7593
8494
  };
7594
- mkdirSync11(resolve24(statePath, ".."), { recursive: true });
7595
- writeFileSync11(statePath, `${JSON.stringify(state, null, 2)}
8495
+ mkdirSync12(resolve25(statePath, ".."), { recursive: true });
8496
+ writeFileSync12(statePath, `${JSON.stringify(state, null, 2)}
7596
8497
  `, "utf-8");
7597
8498
  }
7598
8499
  }