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