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