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