@pourkit/cli 0.0.0-next-20260613012953 → 0.0.0-next-20260613201753
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/cli.js +1062 -2467
- package/dist/cli.js.map +1 -1
- package/dist/e2e/run-live-e2e.js +691 -113
- package/dist/e2e/run-live-e2e.js.map +1 -1
- package/dist/issues/close-issues-on-merge.js +41 -12
- package/dist/issues/close-issues-on-merge.js.map +1 -1
- package/dist/issues/unblock.js +37 -14
- package/dist/issues/unblock.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -58,20 +58,20 @@ function createLogger(name, filePath) {
|
|
|
58
58
|
);
|
|
59
59
|
},
|
|
60
60
|
async close() {
|
|
61
|
-
await new Promise((
|
|
61
|
+
await new Promise((resolve3) => {
|
|
62
62
|
if (!fileStream) {
|
|
63
|
-
|
|
63
|
+
resolve3();
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
const timer = setTimeout(() => {
|
|
67
67
|
if (!fileStream.destroyed) {
|
|
68
68
|
fileStream.destroy();
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
resolve3();
|
|
71
71
|
}, 2e3);
|
|
72
72
|
fileStream.end(() => {
|
|
73
73
|
clearTimeout(timer);
|
|
74
|
-
|
|
74
|
+
resolve3();
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
}
|
|
@@ -265,7 +265,7 @@ async function execJson(command, args, options = {}) {
|
|
|
265
265
|
return JSON.parse(result.stdout);
|
|
266
266
|
}
|
|
267
267
|
function sleep(ms) {
|
|
268
|
-
return new Promise((
|
|
268
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
269
269
|
}
|
|
270
270
|
async function execCaptureWithRetry(command, args, options = {}) {
|
|
271
271
|
const retries = options.retries ?? 3;
|
|
@@ -415,6 +415,9 @@ var ReviewRefactorLoopStrategySchema = z.object({
|
|
|
415
415
|
})
|
|
416
416
|
)
|
|
417
417
|
}).strict().optional(),
|
|
418
|
+
issueFinalReview: StageAgentConfigSchema.extend({
|
|
419
|
+
maxAttempts: z.number().int().positive()
|
|
420
|
+
}),
|
|
418
421
|
finalize: z.object({
|
|
419
422
|
prDescriptionAgent: StageAgentConfigSchema,
|
|
420
423
|
maxAttempts: z.number().int().positive()
|
|
@@ -708,6 +711,7 @@ function parseConfig(raw) {
|
|
|
708
711
|
passWithNotesRefactorAttempts: t.strategy.review.passWithNotesRefactorAttempts
|
|
709
712
|
},
|
|
710
713
|
...t.strategy.verify ? { verify: { commands: verifyCommands } } : {},
|
|
714
|
+
issueFinalReview: t.strategy.issueFinalReview,
|
|
711
715
|
finalize: {
|
|
712
716
|
prDescriptionAgent: t.strategy.finalize.prDescriptionAgent,
|
|
713
717
|
maxAttempts: t.strategy.finalize.maxAttempts
|
|
@@ -786,13 +790,13 @@ function resolvePrdRunMode(target, opts) {
|
|
|
786
790
|
return { mode: "github", source: "default", targetName: target.name };
|
|
787
791
|
}
|
|
788
792
|
async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
|
|
789
|
-
const { existsSync:
|
|
793
|
+
const { existsSync: existsSync19 } = await import("fs");
|
|
790
794
|
const { mkdir: mkdir6, writeFile: writeFile4, rm } = await import("fs/promises");
|
|
791
795
|
const { join: pjoin, basename } = await import("path");
|
|
792
796
|
const { pathToFileURL: pathToFileURL2 } = await import("url");
|
|
793
797
|
const { build } = await import("esbuild");
|
|
794
798
|
const configPath = pjoin(repoRoot2, configFileName);
|
|
795
|
-
if (!
|
|
799
|
+
if (!existsSync19(configPath)) {
|
|
796
800
|
throw new Error(
|
|
797
801
|
`No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
|
|
798
802
|
);
|
|
@@ -912,6 +916,7 @@ function updateWorktreeRunState(worktreePath, update) {
|
|
|
912
916
|
...existing.review,
|
|
913
917
|
...update.review ?? {}
|
|
914
918
|
},
|
|
919
|
+
issueFinalReview: update.issueFinalReview !== void 0 ? { ...existing.issueFinalReview, ...update.issueFinalReview } : existing.issueFinalReview,
|
|
915
920
|
finalizer: update.finalizer !== void 0 ? { ...existing.finalizer, ...update.finalizer } : existing.finalizer,
|
|
916
921
|
finalCommit: update.finalCommit !== void 0 ? { ...existing.finalCommit, ...update.finalCommit } : existing.finalCommit,
|
|
917
922
|
pr: update.pr !== void 0 ? { ...existing.pr, ...update.pr } : existing.pr
|
|
@@ -1041,8 +1046,8 @@ async function cleanupRepository(options) {
|
|
|
1041
1046
|
// commands/artifact-validation.ts
|
|
1042
1047
|
import { createHash } from "crypto";
|
|
1043
1048
|
import { execSync } from "child_process";
|
|
1044
|
-
import { existsSync as
|
|
1045
|
-
import { isAbsolute, join as
|
|
1049
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
1050
|
+
import { isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
|
|
1046
1051
|
|
|
1047
1052
|
// pr/review-verdict.ts
|
|
1048
1053
|
var ReviewVerdictProtocolError = class extends Error {
|
|
@@ -1075,13 +1080,13 @@ function parseReviewVerdict(output) {
|
|
|
1075
1080
|
|
|
1076
1081
|
// commands/review.ts
|
|
1077
1082
|
import {
|
|
1078
|
-
existsSync as
|
|
1083
|
+
existsSync as existsSync5,
|
|
1079
1084
|
mkdirSync as mkdirSync5,
|
|
1080
|
-
readFileSync as
|
|
1081
|
-
readdirSync,
|
|
1085
|
+
readFileSync as readFileSync5,
|
|
1086
|
+
readdirSync as readdirSync2,
|
|
1082
1087
|
writeFileSync as writeFileSync3
|
|
1083
1088
|
} from "fs";
|
|
1084
|
-
import { join as
|
|
1089
|
+
import { join as join6 } from "path";
|
|
1085
1090
|
|
|
1086
1091
|
// execution/agent-output-retry.ts
|
|
1087
1092
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync } from "fs";
|
|
@@ -1156,6 +1161,10 @@ function prepareArtifactPath(artifactPath) {
|
|
|
1156
1161
|
rmSync(artifactPath, { recursive: true, force: true });
|
|
1157
1162
|
}
|
|
1158
1163
|
|
|
1164
|
+
// shared/run-context.ts
|
|
1165
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
1166
|
+
import { isAbsolute, join as join5, relative, resolve } from "path";
|
|
1167
|
+
|
|
1159
1168
|
// commands/run-verification.ts
|
|
1160
1169
|
init_common();
|
|
1161
1170
|
function buildRunVerificationCommand(target) {
|
|
@@ -1231,6 +1240,7 @@ async function runVerificationCommands(options) {
|
|
|
1231
1240
|
var RUN_CONTEXT_PATH_IN_WORKTREE = ".pourkit/.tmp/run-context.md";
|
|
1232
1241
|
var ALL_RUN_CONTEXT_SECTIONS = [
|
|
1233
1242
|
"issue",
|
|
1243
|
+
"prd",
|
|
1234
1244
|
"comments",
|
|
1235
1245
|
"branch",
|
|
1236
1246
|
"verification-commands",
|
|
@@ -1240,6 +1250,7 @@ var ALL_RUN_CONTEXT_SECTIONS = [
|
|
|
1240
1250
|
var STAGE_SECTIONS = {
|
|
1241
1251
|
builder: [
|
|
1242
1252
|
"issue",
|
|
1253
|
+
"prd",
|
|
1243
1254
|
"comments",
|
|
1244
1255
|
"branch",
|
|
1245
1256
|
"verification-commands",
|
|
@@ -1247,6 +1258,7 @@ var STAGE_SECTIONS = {
|
|
|
1247
1258
|
],
|
|
1248
1259
|
reviewer: [
|
|
1249
1260
|
"issue",
|
|
1261
|
+
"prd",
|
|
1250
1262
|
"comments",
|
|
1251
1263
|
"branch",
|
|
1252
1264
|
"verification-commands",
|
|
@@ -1255,6 +1267,7 @@ var STAGE_SECTIONS = {
|
|
|
1255
1267
|
],
|
|
1256
1268
|
refactor: [
|
|
1257
1269
|
"issue",
|
|
1270
|
+
"prd",
|
|
1258
1271
|
"comments",
|
|
1259
1272
|
"branch",
|
|
1260
1273
|
"verification-commands",
|
|
@@ -1283,6 +1296,15 @@ var STAGE_SECTIONS = {
|
|
|
1283
1296
|
"verification-commands",
|
|
1284
1297
|
"artifacts"
|
|
1285
1298
|
],
|
|
1299
|
+
issueFinalReview: [
|
|
1300
|
+
"issue",
|
|
1301
|
+
"prd",
|
|
1302
|
+
"comments",
|
|
1303
|
+
"branch",
|
|
1304
|
+
"verification-commands",
|
|
1305
|
+
"review-criteria",
|
|
1306
|
+
"artifacts"
|
|
1307
|
+
],
|
|
1286
1308
|
prdFinalReview: [
|
|
1287
1309
|
"issue",
|
|
1288
1310
|
"comments",
|
|
@@ -1307,8 +1329,10 @@ function buildRunContextArtifact(options) {
|
|
|
1307
1329
|
function buildRunContextMarkdown(options) {
|
|
1308
1330
|
const {
|
|
1309
1331
|
issue,
|
|
1332
|
+
parentPrdIssue,
|
|
1310
1333
|
target,
|
|
1311
1334
|
branchName,
|
|
1335
|
+
repoRoot: repoRoot2,
|
|
1312
1336
|
reviewerCriteria = [],
|
|
1313
1337
|
sections = ALL_RUN_CONTEXT_SECTIONS
|
|
1314
1338
|
} = options;
|
|
@@ -1326,6 +1350,9 @@ function buildRunContextMarkdown(options) {
|
|
|
1326
1350
|
""
|
|
1327
1351
|
);
|
|
1328
1352
|
}
|
|
1353
|
+
if (sections.includes("prd")) {
|
|
1354
|
+
parts.push(...renderPrdContext(issue, parentPrdIssue, repoRoot2));
|
|
1355
|
+
}
|
|
1329
1356
|
if (sections.includes("comments")) {
|
|
1330
1357
|
parts.push("## Comments", "");
|
|
1331
1358
|
if (issue.comments.length === 0) {
|
|
@@ -1370,12 +1397,129 @@ function buildRunContextMarkdown(options) {
|
|
|
1370
1397
|
return parts.join("\n");
|
|
1371
1398
|
}
|
|
1372
1399
|
function renderCommandList(target, heading) {
|
|
1373
|
-
|
|
1400
|
+
const commands = getVerificationCommands(target);
|
|
1401
|
+
const parts = [
|
|
1374
1402
|
`## ${heading}`,
|
|
1375
1403
|
"",
|
|
1376
1404
|
`Run this command from the repository root: \`${buildRunVerificationCommand(target)}\``,
|
|
1377
1405
|
""
|
|
1378
1406
|
];
|
|
1407
|
+
if (commands.length > 0) {
|
|
1408
|
+
parts.push(
|
|
1409
|
+
"Configured commands executed by the wrapper:",
|
|
1410
|
+
"",
|
|
1411
|
+
...commands.map(
|
|
1412
|
+
(command) => `- ${command.label}: \`${command.command}\``
|
|
1413
|
+
),
|
|
1414
|
+
""
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
return parts;
|
|
1418
|
+
}
|
|
1419
|
+
function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
|
|
1420
|
+
const parent = extractIssueSection(issue.body, "Parent");
|
|
1421
|
+
const documents = extractIssueSection(issue.body, "Plan Documents");
|
|
1422
|
+
const parentRef = parent?.match(/\bPRD-\d+\b/i)?.[0]?.toUpperCase();
|
|
1423
|
+
const parentPrdPath = parentRef && repoRoot2 ? findParentPrdPath(repoRoot2, parentRef) : null;
|
|
1424
|
+
const documentPaths = repoRoot2 ? extractRepoPaths(documents) : [];
|
|
1425
|
+
if (!parent && !documents) {
|
|
1426
|
+
return [];
|
|
1427
|
+
}
|
|
1428
|
+
const parts = ["## PRD", ""];
|
|
1429
|
+
if (parent) {
|
|
1430
|
+
parts.push(parent, "");
|
|
1431
|
+
} else {
|
|
1432
|
+
parts.push("(not declared in issue body)", "");
|
|
1433
|
+
}
|
|
1434
|
+
if (parentPrdPath && repoRoot2) {
|
|
1435
|
+
parts.push(
|
|
1436
|
+
`### Parent PRD Content: \`${relative(repoRoot2, parentPrdPath)}\``,
|
|
1437
|
+
"",
|
|
1438
|
+
"```markdown",
|
|
1439
|
+
readFileSync3(parentPrdPath, "utf-8").trimEnd(),
|
|
1440
|
+
"```",
|
|
1441
|
+
""
|
|
1442
|
+
);
|
|
1443
|
+
} else if (parentPrdIssue) {
|
|
1444
|
+
parts.push(
|
|
1445
|
+
`### Parent PRD Content: #${parentPrdIssue.number} ${parentPrdIssue.title}`,
|
|
1446
|
+
"",
|
|
1447
|
+
"```markdown",
|
|
1448
|
+
parentPrdIssue.body.trimEnd() || "(empty PRD body)",
|
|
1449
|
+
"```",
|
|
1450
|
+
""
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
parts.push("## PRD Documents", "");
|
|
1454
|
+
if (documents) {
|
|
1455
|
+
parts.push(documents, "");
|
|
1456
|
+
} else {
|
|
1457
|
+
parts.push("(not declared in issue body)", "");
|
|
1458
|
+
}
|
|
1459
|
+
for (const documentPath of documentPaths) {
|
|
1460
|
+
const absolutePath = resolveRepoPath(repoRoot2, documentPath);
|
|
1461
|
+
if (!absolutePath || !existsSync3(absolutePath)) continue;
|
|
1462
|
+
parts.push(
|
|
1463
|
+
`### Document Content: \`${documentPath}\``,
|
|
1464
|
+
"",
|
|
1465
|
+
"```markdown",
|
|
1466
|
+
readFileSync3(absolutePath, "utf-8").trimEnd(),
|
|
1467
|
+
"```",
|
|
1468
|
+
""
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return parts;
|
|
1472
|
+
}
|
|
1473
|
+
function extractIssueSection(body, heading) {
|
|
1474
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1475
|
+
const section = body.match(
|
|
1476
|
+
new RegExp(`^## ${escapedHeading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, "im")
|
|
1477
|
+
)?.[1];
|
|
1478
|
+
const trimmed = section?.trim();
|
|
1479
|
+
return trimmed ? trimmed : null;
|
|
1480
|
+
}
|
|
1481
|
+
function extractRepoPaths(section) {
|
|
1482
|
+
if (!section) return [];
|
|
1483
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1484
|
+
for (const match of section.matchAll(/`([^`]+)`/g)) {
|
|
1485
|
+
const value = match[1]?.trim();
|
|
1486
|
+
if (value) paths.add(value);
|
|
1487
|
+
}
|
|
1488
|
+
return Array.from(paths);
|
|
1489
|
+
}
|
|
1490
|
+
function resolveRepoPath(repoRoot2, path9) {
|
|
1491
|
+
if (isAbsolute(path9) || path9.includes("\0")) return null;
|
|
1492
|
+
const resolved = resolve(repoRoot2, path9);
|
|
1493
|
+
const repoRelative2 = relative(repoRoot2, resolved);
|
|
1494
|
+
if (repoRelative2.startsWith("..") || isAbsolute(repoRelative2)) return null;
|
|
1495
|
+
return resolved;
|
|
1496
|
+
}
|
|
1497
|
+
function findParentPrdPath(repoRoot2, parentRef) {
|
|
1498
|
+
const directPath = join5(
|
|
1499
|
+
repoRoot2,
|
|
1500
|
+
".pourkit",
|
|
1501
|
+
"architecture",
|
|
1502
|
+
parentRef,
|
|
1503
|
+
"PRD.md"
|
|
1504
|
+
);
|
|
1505
|
+
if (existsSync3(directPath)) return directPath;
|
|
1506
|
+
const architectureRoot = join5(repoRoot2, ".pourkit", "architecture");
|
|
1507
|
+
if (!existsSync3(architectureRoot)) return null;
|
|
1508
|
+
return findPrdMirror(architectureRoot, parentRef);
|
|
1509
|
+
}
|
|
1510
|
+
function findPrdMirror(directory, parentRef) {
|
|
1511
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1512
|
+
const entryPath = join5(directory, entry.name);
|
|
1513
|
+
if (entry.isDirectory()) {
|
|
1514
|
+
if (entry.name.startsWith(parentRef)) {
|
|
1515
|
+
const prdPath = join5(entryPath, "PRD.md");
|
|
1516
|
+
if (existsSync3(prdPath)) return prdPath;
|
|
1517
|
+
}
|
|
1518
|
+
const nested = findPrdMirror(entryPath, parentRef);
|
|
1519
|
+
if (nested) return nested;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1379
1523
|
}
|
|
1380
1524
|
function renderCriteria(criteria) {
|
|
1381
1525
|
if (criteria.length === 0) {
|
|
@@ -1401,7 +1545,7 @@ function appendProtectedWorkGuidance(promptBody) {
|
|
|
1401
1545
|
|
|
1402
1546
|
// shared/effect-services.ts
|
|
1403
1547
|
import { Context, Effect, Layer } from "effect";
|
|
1404
|
-
import { existsSync as
|
|
1548
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
1405
1549
|
var GitExecutionError = class extends Error {
|
|
1406
1550
|
_tag = "GitExecutionError";
|
|
1407
1551
|
message;
|
|
@@ -1417,7 +1561,7 @@ var FileSystemDefault = Layer.succeed(
|
|
|
1417
1561
|
FileSystem,
|
|
1418
1562
|
FileSystem.of({
|
|
1419
1563
|
readFile: (path9) => Effect.try({
|
|
1420
|
-
try: () =>
|
|
1564
|
+
try: () => readFileSync4(path9, "utf-8"),
|
|
1421
1565
|
catch: (error) => new Error(
|
|
1422
1566
|
`Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
|
|
1423
1567
|
)
|
|
@@ -1429,7 +1573,7 @@ var FileSystemDefault = Layer.succeed(
|
|
|
1429
1573
|
)
|
|
1430
1574
|
}),
|
|
1431
1575
|
exists: (path9) => Effect.try({
|
|
1432
|
-
try: () =>
|
|
1576
|
+
try: () => existsSync4(path9),
|
|
1433
1577
|
catch: (error) => new Error(
|
|
1434
1578
|
`Failed to check existence of ${path9}: ${error instanceof Error ? error.message : String(error)}`
|
|
1435
1579
|
)
|
|
@@ -1813,12 +1957,12 @@ function extractLatestFindingIds(reviewOutput, iteration) {
|
|
|
1813
1957
|
return ids;
|
|
1814
1958
|
}
|
|
1815
1959
|
function validateRefactorArtifact(artifactPath, findingIds) {
|
|
1816
|
-
if (!
|
|
1960
|
+
if (!existsSync5(artifactPath)) {
|
|
1817
1961
|
throw new RefactorArtifactValidationError(
|
|
1818
1962
|
`Refactor artifact missing at ${artifactPath}`
|
|
1819
1963
|
);
|
|
1820
1964
|
}
|
|
1821
|
-
const content =
|
|
1965
|
+
const content = readFileSync5(artifactPath, "utf-8");
|
|
1822
1966
|
if (!content.trim()) {
|
|
1823
1967
|
throw new RefactorArtifactValidationError("Refactor artifact is empty");
|
|
1824
1968
|
}
|
|
@@ -1958,6 +2102,7 @@ function runReviewCommandEffect(options) {
|
|
|
1958
2102
|
config,
|
|
1959
2103
|
target,
|
|
1960
2104
|
issue,
|
|
2105
|
+
parentPrdIssue,
|
|
1961
2106
|
builderBranch,
|
|
1962
2107
|
worktreePath,
|
|
1963
2108
|
repoRoot: repoRoot2,
|
|
@@ -1974,13 +2119,13 @@ function runReviewCommandEffect(options) {
|
|
|
1974
2119
|
new ReviewerFailure({ message: "No reviewer config found" })
|
|
1975
2120
|
);
|
|
1976
2121
|
}
|
|
1977
|
-
const artifactPathInWorktree =
|
|
2122
|
+
const artifactPathInWorktree = join6(
|
|
1978
2123
|
".pourkit",
|
|
1979
2124
|
".tmp",
|
|
1980
2125
|
"reviewers",
|
|
1981
2126
|
`iteration-${iteration ?? 1}.md`
|
|
1982
2127
|
);
|
|
1983
|
-
const artifactPath =
|
|
2128
|
+
const artifactPath = join6(worktreePath, artifactPathInWorktree);
|
|
1984
2129
|
return Effect2.gen(function* () {
|
|
1985
2130
|
const exec = yield* ExecutionProvider;
|
|
1986
2131
|
const fs = yield* FileSystem;
|
|
@@ -2041,8 +2186,10 @@ function runReviewCommandEffect(options) {
|
|
|
2041
2186
|
artifacts: [
|
|
2042
2187
|
buildRunContextArtifact({
|
|
2043
2188
|
issue,
|
|
2189
|
+
parentPrdIssue,
|
|
2044
2190
|
target,
|
|
2045
2191
|
branchName: builderBranch,
|
|
2192
|
+
repoRoot: repoRoot2,
|
|
2046
2193
|
reviewerCriteria: reviewer.criteria,
|
|
2047
2194
|
sections: STAGE_SECTIONS.reviewer
|
|
2048
2195
|
})
|
|
@@ -2144,7 +2291,7 @@ Before carrying forward old blockers, inspect newer issue comments and the curre
|
|
|
2144
2291
|
|
|
2145
2292
|
## Shared Run Context
|
|
2146
2293
|
|
|
2147
|
-
Read the selected issue requirements, branch context,
|
|
2294
|
+
Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
|
|
2148
2295
|
|
|
2149
2296
|
${hasCriteriaPlaceholder ? "" : `## Review Criteria
|
|
2150
2297
|
|
|
@@ -2180,12 +2327,12 @@ ${entry.trimEnd()}`).join("\n\n")}
|
|
|
2180
2327
|
function renderPriorRefactorArtifactsEffect(worktreePath, currentIteration) {
|
|
2181
2328
|
return Effect2.gen(function* () {
|
|
2182
2329
|
const fb = yield* FileSystem;
|
|
2183
|
-
const refactorsDir =
|
|
2330
|
+
const refactorsDir = join6(worktreePath, ".pourkit", ".tmp", "refactors");
|
|
2184
2331
|
const dirExists = yield* fb.exists(refactorsDir).pipe(Effect2.catchAll(() => Effect2.succeed(false)));
|
|
2185
2332
|
if (!dirExists) return "";
|
|
2186
2333
|
const iterationFiles = [];
|
|
2187
2334
|
for (let i = 0; i < currentIteration; i++) {
|
|
2188
|
-
const filePath =
|
|
2335
|
+
const filePath = join6(refactorsDir, `iteration-${i}.md`);
|
|
2189
2336
|
const fileExists = yield* fb.exists(filePath).pipe(Effect2.catchAll(() => Effect2.succeed(false)));
|
|
2190
2337
|
if (!fileExists) continue;
|
|
2191
2338
|
const content = yield* fb.readFile(filePath).pipe(Effect2.catchAll(() => Effect2.succeed("")));
|
|
@@ -2210,12 +2357,12 @@ ${iterationsBlocks}
|
|
|
2210
2357
|
function renderPriorReviewerArtifactsEffect(worktreePath, currentIteration) {
|
|
2211
2358
|
return Effect2.gen(function* () {
|
|
2212
2359
|
const fb = yield* FileSystem;
|
|
2213
|
-
const reviewersDir =
|
|
2360
|
+
const reviewersDir = join6(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
2214
2361
|
const dirExists = yield* fb.exists(reviewersDir).pipe(Effect2.catchAll(() => Effect2.succeed(false)));
|
|
2215
2362
|
if (!dirExists) return "";
|
|
2216
2363
|
const iterationFiles = [];
|
|
2217
2364
|
for (let i = 0; i < currentIteration; i++) {
|
|
2218
|
-
const filePath =
|
|
2365
|
+
const filePath = join6(reviewersDir, `iteration-${i}.md`);
|
|
2219
2366
|
const fileExists = yield* fb.exists(filePath).pipe(Effect2.catchAll(() => Effect2.succeed(false)));
|
|
2220
2367
|
if (!fileExists) continue;
|
|
2221
2368
|
const content = yield* fb.readFile(filePath).pipe(Effect2.catchAll(() => Effect2.succeed("")));
|
|
@@ -2256,7 +2403,7 @@ function renderReviewCriteriaEffect(repoRoot2, criteria, fs) {
|
|
|
2256
2403
|
return Effect2.gen(function* () {
|
|
2257
2404
|
const parts = [];
|
|
2258
2405
|
for (const criterion of criteria) {
|
|
2259
|
-
const snippetPath =
|
|
2406
|
+
const snippetPath = join6(
|
|
2260
2407
|
repoRoot2,
|
|
2261
2408
|
".pourkit",
|
|
2262
2409
|
"prompts",
|
|
@@ -2284,15 +2431,15 @@ function recoverReviewOutputFromString(logContent) {
|
|
|
2284
2431
|
return recoveredOutput.length > 0 ? recoveredOutput : null;
|
|
2285
2432
|
}
|
|
2286
2433
|
function recoverReviewOutputFromLog(logPath) {
|
|
2287
|
-
if (!
|
|
2434
|
+
if (!existsSync5(logPath)) {
|
|
2288
2435
|
return null;
|
|
2289
2436
|
}
|
|
2290
|
-
const logContent =
|
|
2437
|
+
const logContent = readFileSync5(logPath, "utf-8");
|
|
2291
2438
|
return recoverReviewOutputFromString(logContent);
|
|
2292
2439
|
}
|
|
2293
2440
|
function readReviewArtifact(artifactPath, logPath) {
|
|
2294
|
-
if (
|
|
2295
|
-
const output =
|
|
2441
|
+
if (existsSync5(artifactPath)) {
|
|
2442
|
+
const output = readFileSync5(artifactPath, "utf-8");
|
|
2296
2443
|
if (output.trim()) {
|
|
2297
2444
|
return output;
|
|
2298
2445
|
}
|
|
@@ -2302,7 +2449,7 @@ function readReviewArtifact(artifactPath, logPath) {
|
|
|
2302
2449
|
writeFileSync3(artifactPath, recoveredOutput, "utf-8");
|
|
2303
2450
|
return recoveredOutput;
|
|
2304
2451
|
}
|
|
2305
|
-
if (!
|
|
2452
|
+
if (!existsSync5(artifactPath)) {
|
|
2306
2453
|
throw new Error(`Reviewer did not produce output at ${artifactPath}`);
|
|
2307
2454
|
}
|
|
2308
2455
|
throw new Error(`Reviewer produced empty output at ${artifactPath}`);
|
|
@@ -2313,6 +2460,7 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2313
2460
|
config,
|
|
2314
2461
|
target,
|
|
2315
2462
|
issue,
|
|
2463
|
+
parentPrdIssue,
|
|
2316
2464
|
builderBranch,
|
|
2317
2465
|
worktreePath,
|
|
2318
2466
|
repoRoot: repoRoot2,
|
|
@@ -2334,9 +2482,9 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2334
2482
|
const passWithNotesRefactorAttempts = strategy.review.passWithNotesRefactorAttempts;
|
|
2335
2483
|
let resolvedStartingIteration = startingLifetimeIteration;
|
|
2336
2484
|
{
|
|
2337
|
-
const reviewersDir =
|
|
2485
|
+
const reviewersDir = join6(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
2338
2486
|
try {
|
|
2339
|
-
const files =
|
|
2487
|
+
const files = readdirSync2(reviewersDir);
|
|
2340
2488
|
let maxExistingIteration = 0;
|
|
2341
2489
|
for (const file of files) {
|
|
2342
2490
|
const match = file.match(/^iteration-(\d+)\.md$/);
|
|
@@ -2472,7 +2620,7 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2472
2620
|
}
|
|
2473
2621
|
if (reviewResult.verdict === "NEEDS_REFACTOR" || reviewResult.verdict === "PASS_WITH_NOTES" || reviewResult.verdict === "FAIL") {
|
|
2474
2622
|
logger.step("info", "Running refactor agent");
|
|
2475
|
-
const refactorArtifactPathInWorktree =
|
|
2623
|
+
const refactorArtifactPathInWorktree = join6(
|
|
2476
2624
|
".pourkit",
|
|
2477
2625
|
".tmp",
|
|
2478
2626
|
"refactors",
|
|
@@ -2508,8 +2656,10 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2508
2656
|
artifacts: [
|
|
2509
2657
|
buildRunContextArtifact({
|
|
2510
2658
|
issue,
|
|
2659
|
+
parentPrdIssue,
|
|
2511
2660
|
target,
|
|
2512
2661
|
branchName: builderBranch,
|
|
2662
|
+
repoRoot: repoRoot2,
|
|
2513
2663
|
reviewerCriteria: reviewer.criteria,
|
|
2514
2664
|
sections: STAGE_SECTIONS.refactor
|
|
2515
2665
|
})
|
|
@@ -2570,7 +2720,7 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2570
2720
|
reviewResult.output,
|
|
2571
2721
|
lifetimeIteration
|
|
2572
2722
|
);
|
|
2573
|
-
const refactorArtifactPath =
|
|
2723
|
+
const refactorArtifactPath = join6(
|
|
2574
2724
|
worktreePath,
|
|
2575
2725
|
refactorArtifactPathInWorktree
|
|
2576
2726
|
);
|
|
@@ -2661,9 +2811,9 @@ function runReviewWithRefactorLoop(options) {
|
|
|
2661
2811
|
}
|
|
2662
2812
|
function persistIterationArtifactEffect(worktreePath, output, iteration, fs) {
|
|
2663
2813
|
return Effect2.gen(function* () {
|
|
2664
|
-
const dir =
|
|
2814
|
+
const dir = join6(worktreePath, ".pourkit", ".tmp", "reviewers");
|
|
2665
2815
|
yield* fs.mkdir(dir).pipe(Effect2.catchAll(() => Effect2.void));
|
|
2666
|
-
yield* fs.writeFile(
|
|
2816
|
+
yield* fs.writeFile(join6(dir, `iteration-${iteration}.md`), output).pipe(Effect2.catchAll(() => Effect2.void));
|
|
2667
2817
|
});
|
|
2668
2818
|
}
|
|
2669
2819
|
function buildRefactorPromptEffect(repoRoot2, promptTemplate, latestReview, artifactPathInWorktree, iteration, fs) {
|
|
@@ -2680,7 +2830,7 @@ function buildRefactorPromptEffect(repoRoot2, promptTemplate, latestReview, arti
|
|
|
2680
2830
|
|
|
2681
2831
|
## Shared Run Context
|
|
2682
2832
|
|
|
2683
|
-
Read the selected issue requirements, branch context,
|
|
2833
|
+
Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
|
|
2684
2834
|
|
|
2685
2835
|
## Latest Review
|
|
2686
2836
|
|
|
@@ -2939,9 +3089,9 @@ function parseConflictResolutionArtifact(output) {
|
|
|
2939
3089
|
}
|
|
2940
3090
|
|
|
2941
3091
|
// prd-run/final-review-validation.ts
|
|
2942
|
-
import { existsSync as
|
|
3092
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
2943
3093
|
function parseFinalReviewArtifact(artifactPath) {
|
|
2944
|
-
if (!
|
|
3094
|
+
if (!existsSync6(artifactPath)) {
|
|
2945
3095
|
return {
|
|
2946
3096
|
ok: false,
|
|
2947
3097
|
reason: "Final Review artifact not found.",
|
|
@@ -2950,7 +3100,7 @@ function parseFinalReviewArtifact(artifactPath) {
|
|
|
2950
3100
|
}
|
|
2951
3101
|
let content;
|
|
2952
3102
|
try {
|
|
2953
|
-
content =
|
|
3103
|
+
content = readFileSync6(artifactPath, "utf-8");
|
|
2954
3104
|
} catch (error) {
|
|
2955
3105
|
return {
|
|
2956
3106
|
ok: false,
|
|
@@ -3146,14 +3296,14 @@ function valid(options, diagnostics = []) {
|
|
|
3146
3296
|
};
|
|
3147
3297
|
}
|
|
3148
3298
|
function readArtifact(options) {
|
|
3149
|
-
if (!
|
|
3299
|
+
if (!existsSync7(options.artifactPath)) {
|
|
3150
3300
|
return {
|
|
3151
3301
|
ok: false,
|
|
3152
3302
|
result: invalid(options, `Artifact missing at ${options.artifactPath}`)
|
|
3153
3303
|
};
|
|
3154
3304
|
}
|
|
3155
3305
|
try {
|
|
3156
|
-
const content =
|
|
3306
|
+
const content = readFileSync7(options.artifactPath, "utf-8");
|
|
3157
3307
|
if (!content.trim()) {
|
|
3158
3308
|
return { ok: false, result: invalid(options, "Artifact is empty") };
|
|
3159
3309
|
}
|
|
@@ -3167,6 +3317,144 @@ function readArtifact(options) {
|
|
|
3167
3317
|
};
|
|
3168
3318
|
}
|
|
3169
3319
|
}
|
|
3320
|
+
function validateIssueFinalReviewArtifact(parsed, options) {
|
|
3321
|
+
const diagnostics = [];
|
|
3322
|
+
if (parsed.kind !== "issue-final-review") {
|
|
3323
|
+
return {
|
|
3324
|
+
ok: false,
|
|
3325
|
+
reason: `Artifact kind must be "issue-final-review", got ${JSON.stringify(parsed.kind)}`,
|
|
3326
|
+
diagnostics
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
diagnostics.push("kind: issue-final-review");
|
|
3330
|
+
if (parsed.issueNumber !== options.issueNumber) {
|
|
3331
|
+
return {
|
|
3332
|
+
ok: false,
|
|
3333
|
+
reason: `Artifact issueNumber ${JSON.stringify(parsed.issueNumber)} does not match expected ${options.issueNumber}`,
|
|
3334
|
+
diagnostics
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
diagnostics.push(`issueNumber: ${options.issueNumber}`);
|
|
3338
|
+
if (parsed.branchName !== options.branchName) {
|
|
3339
|
+
return {
|
|
3340
|
+
ok: false,
|
|
3341
|
+
reason: `Artifact branchName ${JSON.stringify(parsed.branchName)} does not match expected ${JSON.stringify(options.branchName)}`,
|
|
3342
|
+
diagnostics
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
diagnostics.push(`branchName: ${options.branchName}`);
|
|
3346
|
+
const allowedVerdicts = ["pass", "needs_human_review"];
|
|
3347
|
+
if (!allowedVerdicts.includes(parsed.verdict)) {
|
|
3348
|
+
return {
|
|
3349
|
+
ok: false,
|
|
3350
|
+
reason: `Verdict must be "pass" or "needs_human_review", got ${JSON.stringify(parsed.verdict)}`,
|
|
3351
|
+
diagnostics: [
|
|
3352
|
+
...diagnostics,
|
|
3353
|
+
`verdict: ${JSON.stringify(parsed.verdict)}`
|
|
3354
|
+
]
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
diagnostics.push(`verdict: ${parsed.verdict}`);
|
|
3358
|
+
if (typeof parsed.summary !== "string" || parsed.summary.trim() === "") {
|
|
3359
|
+
return {
|
|
3360
|
+
ok: false,
|
|
3361
|
+
reason: "Summary must be a non-empty string",
|
|
3362
|
+
diagnostics
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
diagnostics.push("summary: present");
|
|
3366
|
+
if (!Array.isArray(parsed.changedPaths) || !parsed.changedPaths.every((p) => typeof p === "string")) {
|
|
3367
|
+
return {
|
|
3368
|
+
ok: false,
|
|
3369
|
+
reason: "changedPaths must be an array of strings",
|
|
3370
|
+
diagnostics
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
for (const p of parsed.changedPaths) {
|
|
3374
|
+
const normalized = p.replace(/\\/g, "/");
|
|
3375
|
+
const segments = normalized.split("/");
|
|
3376
|
+
if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute2(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
|
|
3377
|
+
return {
|
|
3378
|
+
ok: false,
|
|
3379
|
+
reason: `changedPaths must not contain absolute paths or path traversal: ${p}`,
|
|
3380
|
+
diagnostics
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
diagnostics.push(
|
|
3385
|
+
`changedPaths: ${parsed.changedPaths.length} paths`
|
|
3386
|
+
);
|
|
3387
|
+
if (typeof parsed.selfRetouched !== "boolean") {
|
|
3388
|
+
return {
|
|
3389
|
+
ok: false,
|
|
3390
|
+
reason: "selfRetouched must be a boolean",
|
|
3391
|
+
diagnostics
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
diagnostics.push(`selfRetouched: ${parsed.selfRetouched}`);
|
|
3395
|
+
const verification = parsed.verification;
|
|
3396
|
+
if (!verification || typeof verification !== "object") {
|
|
3397
|
+
return {
|
|
3398
|
+
ok: false,
|
|
3399
|
+
reason: "verification must be an object",
|
|
3400
|
+
diagnostics
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
if (typeof verification.required !== "boolean") {
|
|
3404
|
+
return {
|
|
3405
|
+
ok: false,
|
|
3406
|
+
reason: "verification.required must be a boolean",
|
|
3407
|
+
diagnostics
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
if (typeof verification.passed !== "boolean") {
|
|
3411
|
+
return {
|
|
3412
|
+
ok: false,
|
|
3413
|
+
reason: "verification.passed must be a boolean",
|
|
3414
|
+
diagnostics
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
if (!Array.isArray(verification.commands) || !verification.commands.every((c) => typeof c === "string")) {
|
|
3418
|
+
return {
|
|
3419
|
+
ok: false,
|
|
3420
|
+
reason: "verification.commands must be an array of strings",
|
|
3421
|
+
diagnostics
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
if (typeof verification.summary !== "string") {
|
|
3425
|
+
return {
|
|
3426
|
+
ok: false,
|
|
3427
|
+
reason: "verification.summary must be a string",
|
|
3428
|
+
diagnostics
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
diagnostics.push("verification: present");
|
|
3432
|
+
if (parsed.selfRetouched === true && parsed.verdict === "pass") {
|
|
3433
|
+
if (verification.required !== true || verification.passed !== true) {
|
|
3434
|
+
return {
|
|
3435
|
+
ok: false,
|
|
3436
|
+
reason: "Self-retouched pass requires verification.required === true and verification.passed === true",
|
|
3437
|
+
diagnostics: [
|
|
3438
|
+
...diagnostics,
|
|
3439
|
+
`verification.required: ${verification.required}`,
|
|
3440
|
+
`verification.passed: ${verification.passed}`
|
|
3441
|
+
]
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
diagnostics.push("self-retouched verification: valid");
|
|
3445
|
+
}
|
|
3446
|
+
if (parsed.verdict === "needs_human_review") {
|
|
3447
|
+
if (typeof parsed.needsHumanReason !== "string" || parsed.needsHumanReason.trim() === "") {
|
|
3448
|
+
return {
|
|
3449
|
+
ok: false,
|
|
3450
|
+
reason: "needs_human_review verdict requires non-empty needsHumanReason",
|
|
3451
|
+
diagnostics
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
diagnostics.push("needsHumanReason: present");
|
|
3455
|
+
}
|
|
3456
|
+
return { ok: true, verdict: parsed.verdict };
|
|
3457
|
+
}
|
|
3170
3458
|
function validateAgentArtifact(options) {
|
|
3171
3459
|
const artifact = readArtifact(options);
|
|
3172
3460
|
if (!artifact.ok) return artifact.result;
|
|
@@ -3186,7 +3474,7 @@ function validateAgentArtifact(options) {
|
|
|
3186
3474
|
case "refactor": {
|
|
3187
3475
|
let findingIds = options.findingIds ?? [];
|
|
3188
3476
|
if (findingIds.length === 0 && options.latestReviewArtifactPath) {
|
|
3189
|
-
const latestReview =
|
|
3477
|
+
const latestReview = readFileSync7(
|
|
3190
3478
|
options.latestReviewArtifactPath,
|
|
3191
3479
|
"utf-8"
|
|
3192
3480
|
);
|
|
@@ -3209,10 +3497,10 @@ function validateAgentArtifact(options) {
|
|
|
3209
3497
|
if (options.checkConflictMarkers !== false && parsed.status === "resolved") {
|
|
3210
3498
|
const base = options.worktreePath ?? process.cwd();
|
|
3211
3499
|
const filesWithMarkers = parsed.files.filter((file) => {
|
|
3212
|
-
const filePath =
|
|
3500
|
+
const filePath = resolve2(base, file);
|
|
3213
3501
|
try {
|
|
3214
3502
|
return CONFLICT_MARKER_PATTERN.test(
|
|
3215
|
-
|
|
3503
|
+
readFileSync7(filePath, "utf-8")
|
|
3216
3504
|
);
|
|
3217
3505
|
} catch {
|
|
3218
3506
|
return false;
|
|
@@ -3228,6 +3516,9 @@ function validateAgentArtifact(options) {
|
|
|
3228
3516
|
}
|
|
3229
3517
|
return valid(options, [`status: ${parsed.status}`]);
|
|
3230
3518
|
}
|
|
3519
|
+
// QUARANTINED: Retained for Issue Final Review artifact validation via
|
|
3520
|
+
// `runPrdRunValidateFinalReviewCommand`. Remove when Issue Final Review
|
|
3521
|
+
// is migrated to its own dedicated validator kind.
|
|
3231
3522
|
case "final-review": {
|
|
3232
3523
|
if (!options.prdRef || !options.checkoutBase || !options.reviewBase) {
|
|
3233
3524
|
return invalid(
|
|
@@ -3248,6 +3539,26 @@ function validateAgentArtifact(options) {
|
|
|
3248
3539
|
}
|
|
3249
3540
|
return valid(options, [`verdict: ${result.artifact.verdict}`]);
|
|
3250
3541
|
}
|
|
3542
|
+
case "issue-final-review": {
|
|
3543
|
+
if (options.issueNumber === void 0 || !options.branchName) {
|
|
3544
|
+
return invalid(
|
|
3545
|
+
options,
|
|
3546
|
+
"Issue Final Review artifact validation requires --issue-number and --branch-name."
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3549
|
+
const parsed = JSON.parse(artifact.content);
|
|
3550
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3551
|
+
return invalid(
|
|
3552
|
+
options,
|
|
3553
|
+
`Artifact must be a JSON object, got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}`
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
const result = validateIssueFinalReviewArtifact(parsed, options);
|
|
3557
|
+
if (!result.ok) {
|
|
3558
|
+
return invalid(options, result.reason, result.diagnostics);
|
|
3559
|
+
}
|
|
3560
|
+
return valid(options, [`verdict: ${result.verdict}`]);
|
|
3561
|
+
}
|
|
3251
3562
|
case "failure-resolution": {
|
|
3252
3563
|
const parsed = parseRecoveryArtifact(
|
|
3253
3564
|
artifact.content,
|
|
@@ -3294,8 +3605,8 @@ function validateAgentArtifact(options) {
|
|
|
3294
3605
|
}
|
|
3295
3606
|
}
|
|
3296
3607
|
function runValidateArtifactCommand(options) {
|
|
3297
|
-
const artifactPath =
|
|
3298
|
-
const latestReviewArtifactPath = options.latestReviewArtifactPath ?
|
|
3608
|
+
const artifactPath = resolve2(options.repoRoot, options.artifactPath);
|
|
3609
|
+
const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve2(options.repoRoot, options.latestReviewArtifactPath) : void 0;
|
|
3299
3610
|
return validateAgentArtifact({
|
|
3300
3611
|
...options,
|
|
3301
3612
|
artifactPath,
|
|
@@ -3304,9 +3615,9 @@ function runValidateArtifactCommand(options) {
|
|
|
3304
3615
|
});
|
|
3305
3616
|
}
|
|
3306
3617
|
function runLocalValidateArtifactCommand(options) {
|
|
3307
|
-
const resolvedPath =
|
|
3618
|
+
const resolvedPath = resolve2(options.repoRoot, options.artifactPath);
|
|
3308
3619
|
const resolvedExtra = (options.extraArgs ?? []).map(
|
|
3309
|
-
(p) =>
|
|
3620
|
+
(p) => isAbsolute2(p) ? p : resolve2(options.repoRoot, p)
|
|
3310
3621
|
);
|
|
3311
3622
|
let localResult;
|
|
3312
3623
|
switch (options.kind) {
|
|
@@ -3353,7 +3664,7 @@ function runLocalValidateArtifactCommand(options) {
|
|
|
3353
3664
|
}
|
|
3354
3665
|
function readLocalArtifact(path9, failureCode) {
|
|
3355
3666
|
try {
|
|
3356
|
-
const raw =
|
|
3667
|
+
const raw = readFileSync7(path9, "utf-8");
|
|
3357
3668
|
const parsed = JSON.parse(raw);
|
|
3358
3669
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3359
3670
|
return {
|
|
@@ -4052,8 +4363,8 @@ function validateLocalTriage(prdPath, issuePaths) {
|
|
|
4052
4363
|
}
|
|
4053
4364
|
|
|
4054
4365
|
// commands/issue-run.ts
|
|
4055
|
-
import { existsSync as
|
|
4056
|
-
import { join as
|
|
4366
|
+
import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
|
|
4367
|
+
import { isAbsolute as isAbsolute3, join as join15 } from "path";
|
|
4057
4368
|
|
|
4058
4369
|
// pr/templates.ts
|
|
4059
4370
|
init_common();
|
|
@@ -4188,20 +4499,20 @@ function runEffectAndMapExit(program) {
|
|
|
4188
4499
|
}
|
|
4189
4500
|
|
|
4190
4501
|
// shared/attempt-log.ts
|
|
4191
|
-
import { appendFileSync, existsSync as
|
|
4192
|
-
import { dirname as dirname3, join as
|
|
4502
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
4503
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
4193
4504
|
var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
|
|
4194
4505
|
function writeAttemptLog(worktreePath, entry) {
|
|
4195
|
-
const logPath =
|
|
4506
|
+
const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
|
|
4196
4507
|
mkdirSync6(dirname3(logPath), { recursive: true });
|
|
4197
4508
|
appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
4198
4509
|
}
|
|
4199
4510
|
function readAttemptLog(worktreePath) {
|
|
4200
|
-
const logPath =
|
|
4201
|
-
if (!
|
|
4511
|
+
const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
|
|
4512
|
+
if (!existsSync8(logPath)) {
|
|
4202
4513
|
return [];
|
|
4203
4514
|
}
|
|
4204
|
-
const raw =
|
|
4515
|
+
const raw = readFileSync8(logPath, "utf-8");
|
|
4205
4516
|
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
4206
4517
|
const entries = [];
|
|
4207
4518
|
for (const line of lines) {
|
|
@@ -4320,15 +4631,15 @@ async function runBaseRefreshAttempt(options) {
|
|
|
4320
4631
|
}
|
|
4321
4632
|
|
|
4322
4633
|
// commands/conflict-resolution.ts
|
|
4323
|
-
import { existsSync as
|
|
4324
|
-
import { join as
|
|
4634
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
4635
|
+
import { join as join9 } from "path";
|
|
4325
4636
|
init_common();
|
|
4326
4637
|
var CONFLICT_MARKER_PATTERN2 = /<<<<<<<|=======|>>>>>>>/m;
|
|
4327
4638
|
async function hasUnresolvedConflictMarkers(worktreePath, files) {
|
|
4328
4639
|
for (const file of files) {
|
|
4329
|
-
const filePath =
|
|
4640
|
+
const filePath = join9(worktreePath, file);
|
|
4330
4641
|
try {
|
|
4331
|
-
const content =
|
|
4642
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4332
4643
|
if (CONFLICT_MARKER_PATTERN2.test(content)) {
|
|
4333
4644
|
return true;
|
|
4334
4645
|
}
|
|
@@ -4608,7 +4919,7 @@ async function canReachMcp(url) {
|
|
|
4608
4919
|
return true;
|
|
4609
4920
|
} catch {
|
|
4610
4921
|
if (attempt < 9) {
|
|
4611
|
-
await new Promise((
|
|
4922
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4612
4923
|
}
|
|
4613
4924
|
}
|
|
4614
4925
|
}
|
|
@@ -4659,8 +4970,8 @@ async function prepareSerenaForTarget(options) {
|
|
|
4659
4970
|
}
|
|
4660
4971
|
|
|
4661
4972
|
// failure-resolution/failure-resolution-agent.ts
|
|
4662
|
-
import { readFileSync as
|
|
4663
|
-
import { join as
|
|
4973
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
4974
|
+
import { join as join10 } from "path";
|
|
4664
4975
|
|
|
4665
4976
|
// failure-resolution/recovery-policy.ts
|
|
4666
4977
|
function isSecuritySensitiveFailure(failure) {
|
|
@@ -4736,7 +5047,7 @@ async function runFailureResolutionAgent(options) {
|
|
|
4736
5047
|
} = options;
|
|
4737
5048
|
const frConfig = target.strategy.failureResolution;
|
|
4738
5049
|
const artifactPath = packet.artifactTarget;
|
|
4739
|
-
const fullArtifactPath =
|
|
5050
|
+
const fullArtifactPath = join10(worktreePath, artifactPath);
|
|
4740
5051
|
const fingerprint = computeFailureFingerprint(packet.stageName, failure._tag);
|
|
4741
5052
|
const prompt = [
|
|
4742
5053
|
`# Failure Resolution: ${packet.failureType}`,
|
|
@@ -4813,7 +5124,7 @@ async function runFailureResolutionAgent(options) {
|
|
|
4813
5124
|
}
|
|
4814
5125
|
let artifact;
|
|
4815
5126
|
try {
|
|
4816
|
-
const md =
|
|
5127
|
+
const md = readFileSync10(fullArtifactPath, "utf-8");
|
|
4817
5128
|
const validation2 = validateAgentArtifact({
|
|
4818
5129
|
kind: "failure-resolution",
|
|
4819
5130
|
artifactPath: fullArtifactPath,
|
|
@@ -4906,12 +5217,12 @@ async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary,
|
|
|
4906
5217
|
}
|
|
4907
5218
|
|
|
4908
5219
|
// commands/pr-description-agent.ts
|
|
4909
|
-
import { join as
|
|
4910
|
-
import { readFileSync as
|
|
5220
|
+
import { join as join12 } from "path";
|
|
5221
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
4911
5222
|
|
|
4912
5223
|
// pr/pr-description-context.ts
|
|
4913
5224
|
init_common();
|
|
4914
|
-
import { join as
|
|
5225
|
+
import { join as join11 } from "path";
|
|
4915
5226
|
import { readFile } from "fs/promises";
|
|
4916
5227
|
import { Effect as Effect5 } from "effect";
|
|
4917
5228
|
function collectFinalizerContextEffect(options) {
|
|
@@ -4976,7 +5287,7 @@ function remoteTargetBase(targetBase) {
|
|
|
4976
5287
|
return targetBase.includes("/") ? targetBase : `origin/${targetBase}`;
|
|
4977
5288
|
}
|
|
4978
5289
|
function buildFinalizerPrompt(context, promptTemplate) {
|
|
4979
|
-
const artifactPathInWorktree =
|
|
5290
|
+
const artifactPathInWorktree = join11(
|
|
4980
5291
|
".pourkit",
|
|
4981
5292
|
".tmp",
|
|
4982
5293
|
"finalizer",
|
|
@@ -5046,13 +5357,13 @@ function bridgeExecutionProvider2(ep) {
|
|
|
5046
5357
|
}
|
|
5047
5358
|
function runFinalizerAgent(options) {
|
|
5048
5359
|
const { executionProvider } = options;
|
|
5049
|
-
const artifactPathInWorktree =
|
|
5360
|
+
const artifactPathInWorktree = join12(
|
|
5050
5361
|
".pourkit",
|
|
5051
5362
|
".tmp",
|
|
5052
5363
|
"finalizer",
|
|
5053
5364
|
"agent-output.md"
|
|
5054
5365
|
);
|
|
5055
|
-
const artifactPath =
|
|
5366
|
+
const artifactPath = join12(options.worktreePath, artifactPathInWorktree);
|
|
5056
5367
|
const program = Effect6.gen(function* () {
|
|
5057
5368
|
const fs = yield* FileSystem;
|
|
5058
5369
|
const context = yield* collectFinalizerContextEffect({
|
|
@@ -5101,7 +5412,7 @@ function runFinalizerAgent(options) {
|
|
|
5101
5412
|
],
|
|
5102
5413
|
logger: options.logger
|
|
5103
5414
|
});
|
|
5104
|
-
const output =
|
|
5415
|
+
const output = readFileSync11(artifactPath, "utf-8");
|
|
5105
5416
|
yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
|
|
5106
5417
|
return result;
|
|
5107
5418
|
});
|
|
@@ -5113,7 +5424,7 @@ function runFinalizerAgent(options) {
|
|
|
5113
5424
|
);
|
|
5114
5425
|
}
|
|
5115
5426
|
function runPrDescriptionFinalizerCore(options) {
|
|
5116
|
-
const artifactPath =
|
|
5427
|
+
const artifactPath = join12(
|
|
5117
5428
|
options.worktreePath,
|
|
5118
5429
|
options.artifactPathInWorktree
|
|
5119
5430
|
);
|
|
@@ -5221,16 +5532,16 @@ function loadFinalizerPromptEffect(repoRoot2, promptTemplate, fs) {
|
|
|
5221
5532
|
}
|
|
5222
5533
|
function persistGeneratedArtifactEffect(worktreePath, output, fs) {
|
|
5223
5534
|
return Effect6.gen(function* () {
|
|
5224
|
-
const dir =
|
|
5535
|
+
const dir = join12(worktreePath, ".pourkit", ".tmp", "finalizer");
|
|
5225
5536
|
yield* fs.mkdir(dir).pipe(Effect6.catchAll(() => Effect6.void));
|
|
5226
|
-
yield* fs.writeFile(
|
|
5537
|
+
yield* fs.writeFile(join12(dir, "generated.md"), output).pipe(Effect6.catchAll(() => Effect6.void));
|
|
5227
5538
|
});
|
|
5228
5539
|
}
|
|
5229
5540
|
|
|
5230
5541
|
// prd-run/local-merge-coordinator.ts
|
|
5231
5542
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5232
|
-
import { existsSync as
|
|
5233
|
-
import { join as
|
|
5543
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
5544
|
+
import { join as join13 } from "path";
|
|
5234
5545
|
|
|
5235
5546
|
// prd-run/local-branches.ts
|
|
5236
5547
|
import { execFileSync, spawnSync as spawnSync2 } from "child_process";
|
|
@@ -5316,23 +5627,23 @@ function isProtectedBranch(name) {
|
|
|
5316
5627
|
|
|
5317
5628
|
// prd-run/local-merge-coordinator.ts
|
|
5318
5629
|
function getLocalStorePath(repoRoot2, prdId) {
|
|
5319
|
-
return
|
|
5630
|
+
return join13(repoRoot2, ".pourkit", "local-prd-runs", prdId);
|
|
5320
5631
|
}
|
|
5321
5632
|
function getMergeReceiptPath(repoRoot2, prdId, issueId) {
|
|
5322
|
-
return
|
|
5633
|
+
return join13(
|
|
5323
5634
|
getLocalStorePath(repoRoot2, prdId),
|
|
5324
5635
|
"merge-receipts",
|
|
5325
5636
|
`${issueId}.json`
|
|
5326
5637
|
);
|
|
5327
5638
|
}
|
|
5328
5639
|
function getIssueArtifactPath(repoRoot2, prdId, issueId) {
|
|
5329
|
-
return
|
|
5640
|
+
return join13(getLocalStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
|
|
5330
5641
|
}
|
|
5331
5642
|
function readIssueBranchName(repoRoot2, prdId, issueId) {
|
|
5332
5643
|
const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
|
|
5333
|
-
if (!
|
|
5644
|
+
if (!existsSync10(issuePath)) return null;
|
|
5334
5645
|
try {
|
|
5335
|
-
const content =
|
|
5646
|
+
const content = readFileSync12(issuePath, "utf-8");
|
|
5336
5647
|
const parsed = JSON.parse(content);
|
|
5337
5648
|
return typeof parsed.branchName === "string" && parsed.branchName ? parsed.branchName : null;
|
|
5338
5649
|
} catch {
|
|
@@ -5342,9 +5653,9 @@ function readIssueBranchName(repoRoot2, prdId, issueId) {
|
|
|
5342
5653
|
async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
|
|
5343
5654
|
const root = repoRoot2 ?? process.cwd();
|
|
5344
5655
|
const receiptPath = getMergeReceiptPath(root, prdId, issueId);
|
|
5345
|
-
if (!
|
|
5656
|
+
if (!existsSync10(receiptPath)) return null;
|
|
5346
5657
|
try {
|
|
5347
|
-
const content =
|
|
5658
|
+
const content = readFileSync12(receiptPath, "utf-8");
|
|
5348
5659
|
const parsed = JSON.parse(content);
|
|
5349
5660
|
if (typeof parsed.prdId === "string" && typeof parsed.issueId === "string" && typeof parsed.stage === "string" && typeof parsed.sourceBranch === "string" && typeof parsed.localPrdBranch === "string" && typeof parsed.mergeCommit === "string" && typeof parsed.completedAt === "string") {
|
|
5350
5661
|
return parsed;
|
|
@@ -5357,10 +5668,10 @@ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
|
|
|
5357
5668
|
async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
|
|
5358
5669
|
const root = repoRoot2 ?? process.cwd();
|
|
5359
5670
|
const receiptPath = getMergeReceiptPath(root, prdId, issueId);
|
|
5360
|
-
if (
|
|
5671
|
+
if (existsSync10(receiptPath)) {
|
|
5361
5672
|
try {
|
|
5362
5673
|
const existing = JSON.parse(
|
|
5363
|
-
|
|
5674
|
+
readFileSync12(receiptPath, "utf-8")
|
|
5364
5675
|
);
|
|
5365
5676
|
if (existing.prdId === prdId && existing.issueId === issueId && existing.mergeCommit) {
|
|
5366
5677
|
return {
|
|
@@ -5472,7 +5783,7 @@ async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
|
|
|
5472
5783
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5473
5784
|
};
|
|
5474
5785
|
try {
|
|
5475
|
-
const receiptsDir =
|
|
5786
|
+
const receiptsDir = join13(
|
|
5476
5787
|
root,
|
|
5477
5788
|
".pourkit",
|
|
5478
5789
|
"local-prd-runs",
|
|
@@ -5768,6 +6079,129 @@ function runMergeCoordinator(options) {
|
|
|
5768
6079
|
});
|
|
5769
6080
|
}
|
|
5770
6081
|
|
|
6082
|
+
// commands/issue-final-review-agent.ts
|
|
6083
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
6084
|
+
import { join as join14 } from "path";
|
|
6085
|
+
var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join14(
|
|
6086
|
+
".pourkit",
|
|
6087
|
+
".tmp",
|
|
6088
|
+
"issue-final-review",
|
|
6089
|
+
"agent-output.json"
|
|
6090
|
+
);
|
|
6091
|
+
async function runIssueFinalReviewAgent(options) {
|
|
6092
|
+
const {
|
|
6093
|
+
executionProvider,
|
|
6094
|
+
target,
|
|
6095
|
+
issue,
|
|
6096
|
+
parentPrdIssue,
|
|
6097
|
+
builderBranch,
|
|
6098
|
+
worktreePath,
|
|
6099
|
+
repoRoot: repoRoot2,
|
|
6100
|
+
logger
|
|
6101
|
+
} = options;
|
|
6102
|
+
const artifactPathInWorktree = ISSUE_FINAL_REVIEW_ARTIFACT_PATH;
|
|
6103
|
+
const artifactPath = join14(worktreePath, artifactPathInWorktree);
|
|
6104
|
+
const strategy = target.strategy;
|
|
6105
|
+
const issueFinalReview = strategy.issueFinalReview;
|
|
6106
|
+
const agent = issueFinalReview;
|
|
6107
|
+
const prompt = loadIssueFinalReviewPrompt(repoRoot2, agent.promptTemplate);
|
|
6108
|
+
const entry = await executeWithMissingOrEmptyArtifactRetry({
|
|
6109
|
+
executionProvider,
|
|
6110
|
+
missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(agent),
|
|
6111
|
+
logger,
|
|
6112
|
+
runningMessage: () => "Running Issue Final Review agent",
|
|
6113
|
+
retryMessage: (attempt, total) => `Retrying Issue Final Review after empty output (${attempt}/${total})`,
|
|
6114
|
+
executionOptions: {
|
|
6115
|
+
stage: "issueFinalReview",
|
|
6116
|
+
agent: agent.agent,
|
|
6117
|
+
model: agent.model,
|
|
6118
|
+
variant: agent.variant,
|
|
6119
|
+
env: agent.env,
|
|
6120
|
+
prompt,
|
|
6121
|
+
target,
|
|
6122
|
+
repoRoot: repoRoot2,
|
|
6123
|
+
branchName: builderBranch,
|
|
6124
|
+
sandbox: options.config.sandbox,
|
|
6125
|
+
autoApprove: true,
|
|
6126
|
+
artifactPath: artifactPathInWorktree,
|
|
6127
|
+
worktreePath,
|
|
6128
|
+
artifacts: [
|
|
6129
|
+
buildRunContextArtifact({
|
|
6130
|
+
issue,
|
|
6131
|
+
parentPrdIssue,
|
|
6132
|
+
target,
|
|
6133
|
+
branchName: builderBranch,
|
|
6134
|
+
repoRoot: repoRoot2,
|
|
6135
|
+
reviewerCriteria: strategy.review.reviewer.criteria,
|
|
6136
|
+
sections: STAGE_SECTIONS.issueFinalReview
|
|
6137
|
+
})
|
|
6138
|
+
],
|
|
6139
|
+
logger
|
|
6140
|
+
}
|
|
6141
|
+
});
|
|
6142
|
+
const executionResult = entry.executionResult;
|
|
6143
|
+
if (!executionResult.success) {
|
|
6144
|
+
throw new Error(
|
|
6145
|
+
`Issue Final Review agent execution failed: ${executionResult.error}`
|
|
6146
|
+
);
|
|
6147
|
+
}
|
|
6148
|
+
let content;
|
|
6149
|
+
if (entry.artifact._tag === "content") {
|
|
6150
|
+
content = entry.artifact.value;
|
|
6151
|
+
} else if (entry.artifact._tag === "empty") {
|
|
6152
|
+
throw new Error(
|
|
6153
|
+
`Issue Final Review agent produced empty output at ${artifactPath}`
|
|
6154
|
+
);
|
|
6155
|
+
} else {
|
|
6156
|
+
throw new Error(
|
|
6157
|
+
`Issue Final Review agent did not produce output at ${artifactPath}`
|
|
6158
|
+
);
|
|
6159
|
+
}
|
|
6160
|
+
const validation = validateAgentArtifact({
|
|
6161
|
+
kind: "issue-final-review",
|
|
6162
|
+
artifactPath,
|
|
6163
|
+
issueNumber: issue.number,
|
|
6164
|
+
branchName: builderBranch
|
|
6165
|
+
});
|
|
6166
|
+
if (!validation.ok) {
|
|
6167
|
+
throw new Error(
|
|
6168
|
+
`Issue Final Review artifact validation failed: ${validation.reason}`
|
|
6169
|
+
);
|
|
6170
|
+
}
|
|
6171
|
+
const parsed = JSON.parse(content);
|
|
6172
|
+
const verdict = parsed.verdict;
|
|
6173
|
+
if (verdict === "pass") {
|
|
6174
|
+
return {
|
|
6175
|
+
verdict: "pass",
|
|
6176
|
+
artifactPath,
|
|
6177
|
+
selfRetouched: parsed.selfRetouched === true,
|
|
6178
|
+
changedPaths: parsed.changedPaths,
|
|
6179
|
+
verificationPassed: parsed.verification?.passed === true
|
|
6180
|
+
};
|
|
6181
|
+
}
|
|
6182
|
+
if (verdict === "needs_human_review") {
|
|
6183
|
+
return {
|
|
6184
|
+
verdict: "needs_human_review",
|
|
6185
|
+
artifactPath,
|
|
6186
|
+
needsHumanReason: parsed.needsHumanReason,
|
|
6187
|
+
selfRetouched: parsed.selfRetouched === true,
|
|
6188
|
+
changedPaths: parsed.changedPaths
|
|
6189
|
+
};
|
|
6190
|
+
}
|
|
6191
|
+
throw new Error(
|
|
6192
|
+
`Unknown Issue Final Review verdict: ${JSON.stringify(verdict)}`
|
|
6193
|
+
);
|
|
6194
|
+
}
|
|
6195
|
+
function loadIssueFinalReviewPrompt(repoRoot2, promptTemplate) {
|
|
6196
|
+
const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
|
|
6197
|
+
const promptBody = existsSync11(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
|
|
6198
|
+
return appendProtectedWorkGuidance(`${promptBody}
|
|
6199
|
+
|
|
6200
|
+
## Shared Run Context
|
|
6201
|
+
|
|
6202
|
+
Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
|
|
6203
|
+
}
|
|
6204
|
+
|
|
5771
6205
|
// issues/issue-transitions.ts
|
|
5772
6206
|
function createIssueTransitions(deps, labels) {
|
|
5773
6207
|
return {
|
|
@@ -5939,19 +6373,95 @@ function resolveSerenaRuntimeConfig(config, target) {
|
|
|
5939
6373
|
sandboxMcpUrl: config.serena.sandboxMcpUrl
|
|
5940
6374
|
};
|
|
5941
6375
|
}
|
|
5942
|
-
|
|
6376
|
+
function assertIssueFinalReviewPassed(worktreeState) {
|
|
6377
|
+
const ifr = worktreeState?.issueFinalReview;
|
|
6378
|
+
if (!ifr || !ifr.completed || ifr.verdict !== "pass") {
|
|
6379
|
+
throw new Error(
|
|
6380
|
+
"Issue Final Review has not passed. Cannot proceed to finalizer, commit, or PR creation."
|
|
6381
|
+
);
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
async function advanceIssueFinalReview(options) {
|
|
5943
6385
|
const {
|
|
5944
|
-
issueNumber,
|
|
5945
|
-
targetName,
|
|
5946
|
-
config,
|
|
5947
|
-
issueProvider,
|
|
5948
|
-
prProvider,
|
|
5949
6386
|
executionProvider,
|
|
5950
|
-
|
|
5951
|
-
|
|
6387
|
+
config,
|
|
6388
|
+
target,
|
|
6389
|
+
issue,
|
|
6390
|
+
parentPrdIssue,
|
|
6391
|
+
builderBranch,
|
|
6392
|
+
worktreePath,
|
|
6393
|
+
repoRoot: repoRoot2,
|
|
6394
|
+
logger,
|
|
6395
|
+
reviewArtifactPath,
|
|
6396
|
+
worktreeState
|
|
6397
|
+
} = options;
|
|
6398
|
+
const ifrFromState = worktreeState?.issueFinalReview;
|
|
6399
|
+
if (ifrFromState?.completed && ifrFromState.verdict === "pass") {
|
|
6400
|
+
if (!ifrFromState.artifactPath) {
|
|
6401
|
+
throw new Error(
|
|
6402
|
+
"Issue Final Review state is incomplete: missing artifactPath"
|
|
6403
|
+
);
|
|
6404
|
+
}
|
|
6405
|
+
const artifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join15(worktreePath, ifrFromState.artifactPath);
|
|
6406
|
+
const validation = validateAgentArtifact({
|
|
6407
|
+
kind: "issue-final-review",
|
|
6408
|
+
artifactPath,
|
|
6409
|
+
issueNumber: issue.number,
|
|
6410
|
+
branchName: builderBranch
|
|
6411
|
+
});
|
|
6412
|
+
if (!validation.ok) {
|
|
6413
|
+
throw new Error(
|
|
6414
|
+
`Issue Final Review state artifact is invalid: ${validation.reason}`
|
|
6415
|
+
);
|
|
6416
|
+
}
|
|
6417
|
+
return {
|
|
6418
|
+
verdict: "pass",
|
|
6419
|
+
artifactPath,
|
|
6420
|
+
selfRetouched: ifrFromState.selfRetouched ?? false,
|
|
6421
|
+
changedPaths: ifrFromState.changedPaths ?? [],
|
|
6422
|
+
verificationPassed: ifrFromState.verificationPassed ?? false
|
|
6423
|
+
};
|
|
6424
|
+
}
|
|
6425
|
+
const result = await runIssueFinalReviewAgent({
|
|
6426
|
+
executionProvider,
|
|
6427
|
+
config,
|
|
6428
|
+
target,
|
|
6429
|
+
issue,
|
|
6430
|
+
parentPrdIssue,
|
|
6431
|
+
builderBranch,
|
|
6432
|
+
worktreePath,
|
|
6433
|
+
repoRoot: repoRoot2,
|
|
6434
|
+
logger,
|
|
6435
|
+
reviewArtifactPath
|
|
6436
|
+
});
|
|
6437
|
+
if (result.verdict === "pass") {
|
|
6438
|
+
updateWorktreeRunState(worktreePath, {
|
|
6439
|
+
issueFinalReview: {
|
|
6440
|
+
completed: true,
|
|
6441
|
+
verdict: "pass",
|
|
6442
|
+
artifactPath: result.artifactPath,
|
|
6443
|
+
selfRetouched: result.selfRetouched,
|
|
6444
|
+
changedPaths: result.changedPaths,
|
|
6445
|
+
verificationPassed: result.verificationPassed
|
|
6446
|
+
}
|
|
6447
|
+
});
|
|
6448
|
+
}
|
|
6449
|
+
return result;
|
|
6450
|
+
}
|
|
6451
|
+
async function startIssueRun(options) {
|
|
6452
|
+
const {
|
|
6453
|
+
issueNumber,
|
|
6454
|
+
targetName,
|
|
6455
|
+
config,
|
|
6456
|
+
issueProvider,
|
|
6457
|
+
prProvider,
|
|
6458
|
+
executionProvider,
|
|
6459
|
+
force,
|
|
6460
|
+
logger
|
|
5952
6461
|
} = options;
|
|
5953
6462
|
const ROOT = options.repoRoot;
|
|
5954
6463
|
const issue = await issueProvider.fetchIssue(issueNumber);
|
|
6464
|
+
const parentPrdIssue = await fetchParentPrdIssue(issue, issueProvider);
|
|
5955
6465
|
const gateResult = checkIssueGates(issue, config, force);
|
|
5956
6466
|
if (!gateResult.allowed) {
|
|
5957
6467
|
throw new Error(`Issue gates failed: ${gateResult.reason}`);
|
|
@@ -6104,8 +6614,10 @@ async function startIssueRun(options) {
|
|
|
6104
6614
|
}
|
|
6105
6615
|
const runContextArtifact = buildRunContextArtifact({
|
|
6106
6616
|
issue,
|
|
6617
|
+
parentPrdIssue,
|
|
6107
6618
|
target: effectiveTarget,
|
|
6108
6619
|
branchName,
|
|
6620
|
+
repoRoot: ROOT,
|
|
6109
6621
|
reviewerCriteria: strategy.review.reviewer.criteria,
|
|
6110
6622
|
sections: STAGE_SECTIONS.builder
|
|
6111
6623
|
});
|
|
@@ -6166,6 +6678,7 @@ async function startIssueRun(options) {
|
|
|
6166
6678
|
const finalWorktreeState = executionResult.worktreePath ? readWorktreeRunState(executionResult.worktreePath) : worktreeState;
|
|
6167
6679
|
return {
|
|
6168
6680
|
issue,
|
|
6681
|
+
parentPrdIssue,
|
|
6169
6682
|
target,
|
|
6170
6683
|
effectiveTarget,
|
|
6171
6684
|
branchName,
|
|
@@ -6174,6 +6687,18 @@ async function startIssueRun(options) {
|
|
|
6174
6687
|
...serenaExecutionContext ? { serena: serenaExecutionContext } : {}
|
|
6175
6688
|
};
|
|
6176
6689
|
}
|
|
6690
|
+
async function fetchParentPrdIssue(issue, issueProvider) {
|
|
6691
|
+
const parentSection = issue.body.match(
|
|
6692
|
+
/^## Parent\s*\n([\s\S]*?)(?=\n## |$)/im
|
|
6693
|
+
)?.[1];
|
|
6694
|
+
const parentNumber = parentSection?.match(/#(\d+)\b/)?.[1];
|
|
6695
|
+
if (!parentNumber) return void 0;
|
|
6696
|
+
try {
|
|
6697
|
+
return await issueProvider.fetchIssue(Number(parentNumber));
|
|
6698
|
+
} catch {
|
|
6699
|
+
return void 0;
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6177
6702
|
async function advanceIssueRunReview(options) {
|
|
6178
6703
|
const accumulatedRefactorPaths = [];
|
|
6179
6704
|
const reviewResult = await runEffectAndMapExit(
|
|
@@ -6225,6 +6750,8 @@ async function completeIssueRun(options) {
|
|
|
6225
6750
|
checksCompletionTimeoutMs: config.checks.checksCompletionTimeoutSeconds * 1e3,
|
|
6226
6751
|
pollIntervalMs: config.checks.pollIntervalSeconds * 1e3
|
|
6227
6752
|
};
|
|
6753
|
+
const ifrState = executionResult.worktreePath ? readWorktreeRunState(executionResult.worktreePath) ?? worktreeState : worktreeState;
|
|
6754
|
+
assertIssueFinalReviewPassed(ifrState);
|
|
6228
6755
|
let mergeCompleted = false;
|
|
6229
6756
|
try {
|
|
6230
6757
|
if (executionResult.worktreePath && !worktreeState?.finalCommit?.completed && !worktreeState?.pr?.created && !await hasWorktreeChanges(
|
|
@@ -6255,12 +6782,12 @@ async function completeIssueRun(options) {
|
|
|
6255
6782
|
prTitle = finalizerFromState.title;
|
|
6256
6783
|
prBody = finalizerFromState.body;
|
|
6257
6784
|
} else if (finalizerFromState.artifactPath) {
|
|
6258
|
-
if (!
|
|
6785
|
+
if (!existsSync12(finalizerFromState.artifactPath)) {
|
|
6259
6786
|
throw new FinalizerFailure({
|
|
6260
6787
|
message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
|
|
6261
6788
|
});
|
|
6262
6789
|
}
|
|
6263
|
-
const artifactContent =
|
|
6790
|
+
const artifactContent = readFileSync14(
|
|
6264
6791
|
finalizerFromState.artifactPath,
|
|
6265
6792
|
"utf-8"
|
|
6266
6793
|
);
|
|
@@ -6413,7 +6940,7 @@ async function completeIssueRun(options) {
|
|
|
6413
6940
|
failureCode: "already_merged",
|
|
6414
6941
|
localPrdBranch: existingReceipt.localPrdBranch,
|
|
6415
6942
|
mergeCommit: existingReceipt.mergeCommit,
|
|
6416
|
-
receiptPath:
|
|
6943
|
+
receiptPath: join15(
|
|
6417
6944
|
ROOT,
|
|
6418
6945
|
".pourkit",
|
|
6419
6946
|
"local-prd-runs",
|
|
@@ -6478,7 +7005,7 @@ async function completeIssueRun(options) {
|
|
|
6478
7005
|
mode: "local",
|
|
6479
7006
|
localPrdBranch: getLocalPrdBranchName(prdId),
|
|
6480
7007
|
mergeCommit: mergeResult.receipt.mergeCommit,
|
|
6481
|
-
receiptPath:
|
|
7008
|
+
receiptPath: join15(
|
|
6482
7009
|
ROOT,
|
|
6483
7010
|
".pourkit",
|
|
6484
7011
|
"local-prd-runs",
|
|
@@ -6929,7 +7456,7 @@ async function resolveIssueWorktree(root, branchName, baseBranch, logger) {
|
|
|
6929
7456
|
return { mode: "new", branchName, baseRef };
|
|
6930
7457
|
}
|
|
6931
7458
|
function issueWorktreePath(root, branchName) {
|
|
6932
|
-
return
|
|
7459
|
+
return join15(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
|
|
6933
7460
|
}
|
|
6934
7461
|
function resolveRegisteredIssueWorktreePath(worktreeListPorcelain, root, branchName) {
|
|
6935
7462
|
const branchWorktreePath = parseWorktreeListPorcelain(
|
|
@@ -6957,12 +7484,12 @@ async function syncTargetBranch(root, baseBranch, logger) {
|
|
|
6957
7484
|
}
|
|
6958
7485
|
function loadBuilderPrompt(repoRoot2, promptTemplate) {
|
|
6959
7486
|
const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
|
|
6960
|
-
const promptBody =
|
|
7487
|
+
const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
|
|
6961
7488
|
return appendProtectedWorkGuidance(`${promptBody}
|
|
6962
7489
|
|
|
6963
7490
|
## Shared Run Context
|
|
6964
7491
|
|
|
6965
|
-
Read the selected issue requirements, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
|
|
7492
|
+
Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
|
|
6966
7493
|
}
|
|
6967
7494
|
async function handleRebaseConflict(failure, context) {
|
|
6968
7495
|
const frConfig = context.target.strategy.failureResolution;
|
|
@@ -7228,6 +7755,7 @@ async function runIssueCommand(options) {
|
|
|
7228
7755
|
const startResult = await startIssueRun(runOptions);
|
|
7229
7756
|
const {
|
|
7230
7757
|
issue,
|
|
7758
|
+
parentPrdIssue,
|
|
7231
7759
|
target,
|
|
7232
7760
|
effectiveTarget,
|
|
7233
7761
|
branchName,
|
|
@@ -7246,6 +7774,7 @@ async function runIssueCommand(options) {
|
|
|
7246
7774
|
config,
|
|
7247
7775
|
target: effectiveTarget,
|
|
7248
7776
|
issue,
|
|
7777
|
+
parentPrdIssue,
|
|
7249
7778
|
builderBranch: branchName,
|
|
7250
7779
|
worktreePath: executionResult.worktreePath,
|
|
7251
7780
|
repoRoot: ROOT,
|
|
@@ -7276,6 +7805,54 @@ async function runIssueCommand(options) {
|
|
|
7276
7805
|
}
|
|
7277
7806
|
reviewArtifactPath = reviewResult.artifactPath;
|
|
7278
7807
|
}
|
|
7808
|
+
const finalReviewResult = await advanceIssueFinalReview({
|
|
7809
|
+
executionProvider: runOptions.executionProvider,
|
|
7810
|
+
config,
|
|
7811
|
+
target: effectiveTarget,
|
|
7812
|
+
issue,
|
|
7813
|
+
parentPrdIssue,
|
|
7814
|
+
builderBranch: branchName,
|
|
7815
|
+
worktreePath: executionResult.worktreePath,
|
|
7816
|
+
repoRoot: ROOT,
|
|
7817
|
+
logger,
|
|
7818
|
+
reviewArtifactPath,
|
|
7819
|
+
worktreeState
|
|
7820
|
+
});
|
|
7821
|
+
if (finalReviewResult.verdict === "needs_human_review") {
|
|
7822
|
+
const transitions = createIssueTransitions(
|
|
7823
|
+
{
|
|
7824
|
+
fetchIssue: issueProvider.fetchIssue.bind(issueProvider),
|
|
7825
|
+
addLabels: issueProvider.addLabels.bind(issueProvider),
|
|
7826
|
+
removeLabel: issueProvider.removeLabel.bind(issueProvider),
|
|
7827
|
+
closeIssue: issueProvider.closeIssue.bind(issueProvider)
|
|
7828
|
+
},
|
|
7829
|
+
{
|
|
7830
|
+
blocked: config.labels.blocked,
|
|
7831
|
+
readyForAgent: config.labels.readyForAgent,
|
|
7832
|
+
needsTriage: config.labels.needsTriage,
|
|
7833
|
+
agentInProgress: config.labels.agentInProgress,
|
|
7834
|
+
readyForHuman: config.labels.readyForHuman,
|
|
7835
|
+
prOpenAwaitingMerge: config.labels.prOpenAwaitingMerge
|
|
7836
|
+
}
|
|
7837
|
+
);
|
|
7838
|
+
await transitions.moveToReadyForHuman(issueNumber);
|
|
7839
|
+
const comment = [
|
|
7840
|
+
"Pourkit stopped the Issue Final Review because human review is needed.",
|
|
7841
|
+
"",
|
|
7842
|
+
finalReviewResult.needsHumanReason,
|
|
7843
|
+
"",
|
|
7844
|
+
"Artifacts:",
|
|
7845
|
+
`- Issue Final Review: ${finalReviewResult.artifactPath}`
|
|
7846
|
+
].join("\n");
|
|
7847
|
+
await issueProvider.commentIssue(issueNumber, comment);
|
|
7848
|
+
logger.step(
|
|
7849
|
+
"info",
|
|
7850
|
+
`Issue Final Review requires human handoff for issue ${issueNumber}`
|
|
7851
|
+
);
|
|
7852
|
+
throw new HumanHandoffStop(
|
|
7853
|
+
`Issue Final Review requires human handoff: ${finalReviewResult.needsHumanReason}`
|
|
7854
|
+
);
|
|
7855
|
+
}
|
|
7279
7856
|
return await completeIssueRun({
|
|
7280
7857
|
...runOptions,
|
|
7281
7858
|
startResult,
|
|
@@ -7400,29 +7977,29 @@ async function runIssueCreateCommand(args, issueProvider, logger) {
|
|
|
7400
7977
|
// commands/prd-run.ts
|
|
7401
7978
|
import {
|
|
7402
7979
|
cpSync,
|
|
7403
|
-
existsSync as
|
|
7980
|
+
existsSync as existsSync16,
|
|
7404
7981
|
lstatSync,
|
|
7405
7982
|
mkdirSync as mkdirSync10,
|
|
7406
7983
|
mkdtempSync,
|
|
7407
|
-
readFileSync as
|
|
7984
|
+
readFileSync as readFileSync17,
|
|
7408
7985
|
realpathSync,
|
|
7409
7986
|
rmSync as rmSync3
|
|
7410
7987
|
} from "fs";
|
|
7411
7988
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7412
|
-
import { dirname as dirname4, join as
|
|
7989
|
+
import { dirname as dirname4, join as join20, relative as relative2 } from "path";
|
|
7413
7990
|
import { tmpdir } from "os";
|
|
7414
7991
|
import { Match, pipe } from "effect";
|
|
7415
7992
|
|
|
7416
7993
|
// prd-run/state.ts
|
|
7417
7994
|
import {
|
|
7418
|
-
existsSync as
|
|
7995
|
+
existsSync as existsSync13,
|
|
7419
7996
|
mkdirSync as mkdirSync8,
|
|
7420
|
-
readFileSync as
|
|
7421
|
-
readdirSync as
|
|
7997
|
+
readFileSync as readFileSync15,
|
|
7998
|
+
readdirSync as readdirSync4,
|
|
7422
7999
|
writeFileSync as writeFileSync5
|
|
7423
8000
|
} from "fs";
|
|
7424
8001
|
import { mkdir as mkdir4, readFile as readFile4, writeFile } from "fs/promises";
|
|
7425
|
-
import { join as
|
|
8002
|
+
import { join as join16 } from "path";
|
|
7426
8003
|
import { z as z2 } from "zod";
|
|
7427
8004
|
var PRD_RUN_STATE_DIR = ".pourkit/prd-runs";
|
|
7428
8005
|
var PrdRunRecordSchema = z2.object({
|
|
@@ -7512,16 +8089,16 @@ function normalizePrdRunRef(ref) {
|
|
|
7512
8089
|
function readPrdRun(repoRoot2, prdRef) {
|
|
7513
8090
|
const normalized = normalizePrdRunRef(prdRef);
|
|
7514
8091
|
const recordPath = getRecordPath(repoRoot2, normalized);
|
|
7515
|
-
if (!
|
|
8092
|
+
if (!existsSync13(recordPath)) {
|
|
7516
8093
|
return { record: null, diagnostics: [] };
|
|
7517
8094
|
}
|
|
7518
8095
|
try {
|
|
7519
|
-
const raw = JSON.parse(
|
|
8096
|
+
const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
|
|
7520
8097
|
const parsed = PrdRunRecordSchema.parse(raw);
|
|
7521
8098
|
return { record: parsed, diagnostics: [] };
|
|
7522
8099
|
} catch (error) {
|
|
7523
8100
|
try {
|
|
7524
|
-
const raw = JSON.parse(
|
|
8101
|
+
const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
|
|
7525
8102
|
if (raw && typeof raw === "object" && raw.start && typeof raw.start === "object" && raw.start.startBaseBranch === void 0) {
|
|
7526
8103
|
return {
|
|
7527
8104
|
record: raw,
|
|
@@ -7542,20 +8119,20 @@ function readPrdRun(repoRoot2, prdRef) {
|
|
|
7542
8119
|
}
|
|
7543
8120
|
}
|
|
7544
8121
|
function listPrdRuns(repoRoot2) {
|
|
7545
|
-
const stateDir =
|
|
7546
|
-
if (!
|
|
8122
|
+
const stateDir = join16(repoRoot2, PRD_RUN_STATE_DIR);
|
|
8123
|
+
if (!existsSync13(stateDir)) {
|
|
7547
8124
|
return { records: [], diagnostics: [] };
|
|
7548
8125
|
}
|
|
7549
8126
|
const records = [];
|
|
7550
8127
|
const diagnostics = [];
|
|
7551
|
-
for (const entry of
|
|
8128
|
+
for (const entry of readdirSync4(stateDir, { withFileTypes: true }).sort(
|
|
7552
8129
|
(left, right) => left.name.localeCompare(right.name)
|
|
7553
8130
|
)) {
|
|
7554
8131
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
7555
|
-
const recordPath =
|
|
8132
|
+
const recordPath = join16(stateDir, entry.name);
|
|
7556
8133
|
try {
|
|
7557
8134
|
const record = PrdRunRecordSchema.parse(
|
|
7558
|
-
JSON.parse(
|
|
8135
|
+
JSON.parse(readFileSync15(recordPath, "utf-8"))
|
|
7559
8136
|
);
|
|
7560
8137
|
records.push(record);
|
|
7561
8138
|
} catch (error) {
|
|
@@ -7568,7 +8145,7 @@ function listPrdRuns(repoRoot2) {
|
|
|
7568
8145
|
}
|
|
7569
8146
|
function writePrdRunRecord(repoRoot2, record) {
|
|
7570
8147
|
const normalized = normalizePrdRunRef(record.prdRef);
|
|
7571
|
-
const stateDir =
|
|
8148
|
+
const stateDir = join16(repoRoot2, PRD_RUN_STATE_DIR);
|
|
7572
8149
|
const recordPath = getRecordPath(repoRoot2, normalized);
|
|
7573
8150
|
mkdirSync8(stateDir, { recursive: true });
|
|
7574
8151
|
writeFileSync5(
|
|
@@ -7577,7 +8154,6 @@ function writePrdRunRecord(repoRoot2, record) {
|
|
|
7577
8154
|
"utf-8"
|
|
7578
8155
|
);
|
|
7579
8156
|
}
|
|
7580
|
-
var LOCAL_PRD_RUN_STATE_DIR = ".pourkit/local-prd-runs";
|
|
7581
8157
|
var LocalPrdRunRecordSchema = z2.object({
|
|
7582
8158
|
prdId: z2.string().regex(
|
|
7583
8159
|
/^PRD-\d{4}$/,
|
|
@@ -7613,38 +8189,8 @@ var LocalPrdRunRecordSchema = z2.object({
|
|
|
7613
8189
|
}).strict(),
|
|
7614
8190
|
metadata: z2.record(z2.unknown())
|
|
7615
8191
|
}).strict();
|
|
7616
|
-
function getLocalStorePath2(repoRoot2, prdId) {
|
|
7617
|
-
return join14(
|
|
7618
|
-
repoRoot2,
|
|
7619
|
-
LOCAL_PRD_RUN_STATE_DIR,
|
|
7620
|
-
`${normalizePrdRunRef(prdId)}.json`
|
|
7621
|
-
);
|
|
7622
|
-
}
|
|
7623
|
-
async function readLocalPrdRun(repoRoot2, prdId) {
|
|
7624
|
-
const normalized = normalizePrdRunRef(prdId);
|
|
7625
|
-
const recordPath = getLocalStorePath2(repoRoot2, normalized);
|
|
7626
|
-
if (!existsSync11(recordPath)) {
|
|
7627
|
-
return null;
|
|
7628
|
-
}
|
|
7629
|
-
try {
|
|
7630
|
-
const content = await readFile4(recordPath, "utf-8");
|
|
7631
|
-
return LocalPrdRunRecordSchema.parse(JSON.parse(content));
|
|
7632
|
-
} catch {
|
|
7633
|
-
return null;
|
|
7634
|
-
}
|
|
7635
|
-
}
|
|
7636
|
-
async function writeLocalPrdRunRecord(repoRoot2, prdId, record) {
|
|
7637
|
-
const normalized = normalizePrdRunRef(prdId);
|
|
7638
|
-
const storeDir = join14(repoRoot2, LOCAL_PRD_RUN_STATE_DIR);
|
|
7639
|
-
await mkdir4(storeDir, { recursive: true });
|
|
7640
|
-
await writeFile(
|
|
7641
|
-
getLocalStorePath2(repoRoot2, normalized),
|
|
7642
|
-
JSON.stringify({ ...record, prdId: normalized }, null, 2),
|
|
7643
|
-
"utf-8"
|
|
7644
|
-
);
|
|
7645
|
-
}
|
|
7646
8192
|
function getRecordPath(repoRoot2, prdRef) {
|
|
7647
|
-
return
|
|
8193
|
+
return join16(
|
|
7648
8194
|
repoRoot2,
|
|
7649
8195
|
PRD_RUN_STATE_DIR,
|
|
7650
8196
|
`${normalizePrdRunRef(prdRef)}.json`
|
|
@@ -7668,102 +8214,13 @@ var EvidencePacketSchema = z3.object({
|
|
|
7668
8214
|
stage: StageSchema,
|
|
7669
8215
|
stageReceipts: z3.record(z3.string(), z3.unknown())
|
|
7670
8216
|
}).strict();
|
|
7671
|
-
function buildEvidencePacket(input) {
|
|
7672
|
-
const requiredFields = [
|
|
7673
|
-
"prdRef",
|
|
7674
|
-
"prdBranch",
|
|
7675
|
-
"mergeBase",
|
|
7676
|
-
"planningManifestPath",
|
|
7677
|
-
"stage"
|
|
7678
|
-
];
|
|
7679
|
-
for (const field of requiredFields) {
|
|
7680
|
-
const value = input[field];
|
|
7681
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
7682
|
-
throw new Error(
|
|
7683
|
-
`Evidence Packet construction failed: "${field}" is required and must be a non-empty string.`
|
|
7684
|
-
);
|
|
7685
|
-
}
|
|
7686
|
-
}
|
|
7687
|
-
if (!input.planningManifestFacts || typeof input.planningManifestFacts.parentPrdIssueUrl !== "string" || input.planningManifestFacts.parentPrdIssueUrl.trim().length === 0) {
|
|
7688
|
-
throw new Error(
|
|
7689
|
-
'Evidence Packet construction failed: "planningManifestFacts.parentPrdIssueUrl" is required and must be a non-empty string.'
|
|
7690
|
-
);
|
|
7691
|
-
}
|
|
7692
|
-
if (typeof input.planningManifestFacts.childIssueCount !== "number" || !Number.isInteger(input.planningManifestFacts.childIssueCount) || input.planningManifestFacts.childIssueCount < 0) {
|
|
7693
|
-
throw new Error(
|
|
7694
|
-
'Evidence Packet construction failed: "planningManifestFacts.childIssueCount" is required and must be a non-negative integer.'
|
|
7695
|
-
);
|
|
7696
|
-
}
|
|
7697
|
-
if (!PRD_REF_REGEX2.test(input.prdRef.trim())) {
|
|
7698
|
-
throw new Error(
|
|
7699
|
-
`Evidence Packet construction failed: "prdRef" must match PRD-\\d{3,4} format, got "${input.prdRef}".`
|
|
7700
|
-
);
|
|
7701
|
-
}
|
|
7702
|
-
const stageResult = StageSchema.safeParse(input.stage);
|
|
7703
|
-
if (!stageResult.success) {
|
|
7704
|
-
throw new Error(
|
|
7705
|
-
`Evidence Packet construction failed: "stage" must be one of "prdFinalReview" or "prdReconciliation", got "${input.stage}".`
|
|
7706
|
-
);
|
|
7707
|
-
}
|
|
7708
|
-
if (stageResult.data === "prdFinalReview") {
|
|
7709
|
-
const hasMergeBaseReceipt = "mergeBase" in input.stageReceipts;
|
|
7710
|
-
const hasFinalReviewRef = "finalReviewRef" in input.stageReceipts;
|
|
7711
|
-
if (!hasMergeBaseReceipt && !hasFinalReviewRef) {
|
|
7712
|
-
throw new Error(
|
|
7713
|
-
"Evidence Packet construction failed: Final Review stage receipts must include at minimum a mergeBase or finalReviewRef receipt."
|
|
7714
|
-
);
|
|
7715
|
-
}
|
|
7716
|
-
}
|
|
7717
|
-
if (stageResult.data === "prdReconciliation") {
|
|
7718
|
-
if (!("finalReviewReceipt" in input.stageReceipts)) {
|
|
7719
|
-
throw new Error(
|
|
7720
|
-
"Evidence Packet construction failed: Reconciliation stage receipts must include at minimum a finalReviewReceipt."
|
|
7721
|
-
);
|
|
7722
|
-
}
|
|
7723
|
-
}
|
|
7724
|
-
const mergeBase = input.mergeBase.trim();
|
|
7725
|
-
if (mergeBase.length < 6) {
|
|
7726
|
-
throw new Error(
|
|
7727
|
-
`Evidence Packet construction failed: "mergeBase" must be a non-empty commit SHA with at least 6 characters, got "${mergeBase}".`
|
|
7728
|
-
);
|
|
7729
|
-
}
|
|
7730
|
-
const packet = {
|
|
7731
|
-
prdRef: input.prdRef.trim(),
|
|
7732
|
-
prdBranch: input.prdBranch.trim(),
|
|
7733
|
-
mergeBase,
|
|
7734
|
-
planningManifestPath: input.planningManifestPath.trim(),
|
|
7735
|
-
planningManifestFacts: {
|
|
7736
|
-
parentPrdIssueUrl: input.planningManifestFacts.parentPrdIssueUrl.trim(),
|
|
7737
|
-
childIssueCount: input.planningManifestFacts.childIssueCount
|
|
7738
|
-
},
|
|
7739
|
-
stage: stageResult.data,
|
|
7740
|
-
stageReceipts: input.stageReceipts
|
|
7741
|
-
};
|
|
7742
|
-
return packet;
|
|
7743
|
-
}
|
|
7744
|
-
var TOKEN_LIKE_PATTERNS = [
|
|
7745
|
-
/gh[ps]_[A-Za-z0-9]{20,}/g,
|
|
7746
|
-
/token[=:_\s][A-Za-z0-9_-]{20,}/gi,
|
|
7747
|
-
/secret[=:_\s][A-Za-z0-9_-]{20,}/gi,
|
|
7748
|
-
/credential[=:_\s][A-Za-z0-9_-]{20,}/gi,
|
|
7749
|
-
/key[=:_\s][A-Za-z0-9_-]{20,}/gi,
|
|
7750
|
-
/-----BEGIN (RSA |EC )?PRIVATE KEY-----/g,
|
|
7751
|
-
/xox[baprs]-[A-Za-z0-9_-]{10,}/g
|
|
7752
|
-
];
|
|
7753
|
-
function redactSensitiveValues(input) {
|
|
7754
|
-
let redacted = input;
|
|
7755
|
-
for (const pattern of TOKEN_LIKE_PATTERNS) {
|
|
7756
|
-
redacted = redacted.replace(pattern, "[REDACTED]");
|
|
7757
|
-
}
|
|
7758
|
-
return redacted;
|
|
7759
|
-
}
|
|
7760
8217
|
|
|
7761
8218
|
// commands/prd-run.ts
|
|
7762
8219
|
init_common();
|
|
7763
8220
|
|
|
7764
8221
|
// prd-run/local-artifacts.ts
|
|
7765
|
-
import { existsSync as
|
|
7766
|
-
import { join as
|
|
8222
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8223
|
+
import { join as join17 } from "path";
|
|
7767
8224
|
var REQUIRED_PRD_FIELDS = [
|
|
7768
8225
|
"schemaVersion",
|
|
7769
8226
|
"kind",
|
|
@@ -7796,13 +8253,13 @@ var REQUIRED_ISSUE_FIELDS = [
|
|
|
7796
8253
|
"githubProjection"
|
|
7797
8254
|
];
|
|
7798
8255
|
function prdStorePath(repoRoot2, prdId) {
|
|
7799
|
-
return
|
|
8256
|
+
return join17(repoRoot2, ".pourkit", "local-prd-runs", prdId);
|
|
7800
8257
|
}
|
|
7801
8258
|
function prdArtifactPath(repoRoot2, prdId) {
|
|
7802
|
-
return
|
|
8259
|
+
return join17(prdStorePath(repoRoot2, prdId), "prd.json");
|
|
7803
8260
|
}
|
|
7804
8261
|
function issueArtifactPath(repoRoot2, prdId, issueId) {
|
|
7805
|
-
return
|
|
8262
|
+
return join17(prdStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
|
|
7806
8263
|
}
|
|
7807
8264
|
function hasRequiredFields(data, requiredFields) {
|
|
7808
8265
|
for (const field of requiredFields) {
|
|
@@ -7815,7 +8272,7 @@ function hasRequiredFields(data, requiredFields) {
|
|
|
7815
8272
|
async function resolveLocalPrdArtifact(prdId, repoRoot2) {
|
|
7816
8273
|
const root = repoRoot2 ?? process.cwd();
|
|
7817
8274
|
const prdPath = prdArtifactPath(root, prdId);
|
|
7818
|
-
if (!
|
|
8275
|
+
if (!existsSync14(prdPath)) {
|
|
7819
8276
|
return {
|
|
7820
8277
|
ok: false,
|
|
7821
8278
|
failureCode: "missing_prd_artifact",
|
|
@@ -7860,7 +8317,7 @@ async function resolveLocalIssueArtifacts(prdId, repoRoot2) {
|
|
|
7860
8317
|
const issues = [];
|
|
7861
8318
|
for (const childId of childIssueIds) {
|
|
7862
8319
|
const issuePath = issueArtifactPath(root, prdId, childId);
|
|
7863
|
-
if (!
|
|
8320
|
+
if (!existsSync14(issuePath)) {
|
|
7864
8321
|
return {
|
|
7865
8322
|
ok: false,
|
|
7866
8323
|
failureCode: "missing_child_issue",
|
|
@@ -7963,109 +8420,11 @@ async function getRunnableLocalIssues(prdId, repoRoot2) {
|
|
|
7963
8420
|
// prd-run/local-final-review.ts
|
|
7964
8421
|
import { execFileSync as execFileSync3, execSync as execSync2 } from "child_process";
|
|
7965
8422
|
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
7966
|
-
import { join as
|
|
7967
|
-
async function squashFinalReviewRetouch(prdId, repoRoot2, title, body) {
|
|
7968
|
-
const root = repoRoot2 ?? process.cwd();
|
|
7969
|
-
const targetBranch = `local/${prdId}`;
|
|
7970
|
-
const retouchBranch = `local-${prdId}-final-review-retouch`;
|
|
7971
|
-
try {
|
|
7972
|
-
execFileSync3(
|
|
7973
|
-
"git",
|
|
7974
|
-
["show-ref", "--verify", "--quiet", `refs/heads/${retouchBranch}`],
|
|
7975
|
-
{
|
|
7976
|
-
cwd: root,
|
|
7977
|
-
encoding: "utf8",
|
|
7978
|
-
stdio: "pipe"
|
|
7979
|
-
}
|
|
7980
|
-
);
|
|
7981
|
-
} catch {
|
|
7982
|
-
return;
|
|
7983
|
-
}
|
|
7984
|
-
try {
|
|
7985
|
-
execFileSync3(
|
|
7986
|
-
"git",
|
|
7987
|
-
["show-ref", "--verify", "--quiet", `refs/heads/${targetBranch}`],
|
|
7988
|
-
{
|
|
7989
|
-
cwd: root,
|
|
7990
|
-
encoding: "utf8",
|
|
7991
|
-
stdio: "pipe"
|
|
7992
|
-
}
|
|
7993
|
-
);
|
|
7994
|
-
} catch {
|
|
7995
|
-
return;
|
|
7996
|
-
}
|
|
7997
|
-
try {
|
|
7998
|
-
execFileSync3("git", ["checkout", targetBranch], {
|
|
7999
|
-
cwd: root,
|
|
8000
|
-
encoding: "utf8",
|
|
8001
|
-
stdio: "pipe"
|
|
8002
|
-
});
|
|
8003
|
-
execFileSync3("git", ["merge", "--squash", retouchBranch], {
|
|
8004
|
-
cwd: root,
|
|
8005
|
-
encoding: "utf8",
|
|
8006
|
-
stdio: "pipe"
|
|
8007
|
-
});
|
|
8008
|
-
try {
|
|
8009
|
-
execFileSync3("git", ["diff", "--cached", "--quiet"], {
|
|
8010
|
-
cwd: root,
|
|
8011
|
-
encoding: "utf8",
|
|
8012
|
-
stdio: "pipe"
|
|
8013
|
-
});
|
|
8014
|
-
return;
|
|
8015
|
-
} catch {
|
|
8016
|
-
}
|
|
8017
|
-
if (title) {
|
|
8018
|
-
const commitBody = body ? `${title}
|
|
8019
|
-
|
|
8020
|
-
${body}` : title;
|
|
8021
|
-
execFileSync3("git", ["commit", "-m", commitBody], {
|
|
8022
|
-
cwd: root,
|
|
8023
|
-
encoding: "utf8",
|
|
8024
|
-
stdio: "pipe"
|
|
8025
|
-
});
|
|
8026
|
-
} else {
|
|
8027
|
-
execFileSync3(
|
|
8028
|
-
"git",
|
|
8029
|
-
["commit", "-m", `Squash merge ${retouchBranch} into ${targetBranch}`],
|
|
8030
|
-
{
|
|
8031
|
-
cwd: root,
|
|
8032
|
-
encoding: "utf8",
|
|
8033
|
-
stdio: "pipe"
|
|
8034
|
-
}
|
|
8035
|
-
);
|
|
8036
|
-
}
|
|
8037
|
-
const mergeCommit = execFileSync3("git", ["rev-parse", "HEAD"], {
|
|
8038
|
-
cwd: root,
|
|
8039
|
-
encoding: "utf8",
|
|
8040
|
-
stdio: "pipe"
|
|
8041
|
-
}).trim();
|
|
8042
|
-
const changedPathsResult = execFileSync3(
|
|
8043
|
-
"git",
|
|
8044
|
-
["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"],
|
|
8045
|
-
{
|
|
8046
|
-
cwd: root,
|
|
8047
|
-
encoding: "utf8",
|
|
8048
|
-
stdio: "pipe"
|
|
8049
|
-
}
|
|
8050
|
-
);
|
|
8051
|
-
const changedPaths = changedPathsResult.split(/\r?\n/).filter(Boolean);
|
|
8052
|
-
return {
|
|
8053
|
-
mergeCommit,
|
|
8054
|
-
changedPaths,
|
|
8055
|
-
reviewedTimestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8056
|
-
};
|
|
8057
|
-
} catch (error) {
|
|
8058
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
8059
|
-
if (message.toLowerCase().includes("conflict")) {
|
|
8060
|
-
throw new Error(`Retouch squash merge conflict in ${targetBranch}`);
|
|
8061
|
-
}
|
|
8062
|
-
throw error;
|
|
8063
|
-
}
|
|
8064
|
-
}
|
|
8423
|
+
import { join as join18 } from "path";
|
|
8065
8424
|
|
|
8066
8425
|
// prd-run/local-queue-loop.ts
|
|
8067
|
-
import { readFileSync as
|
|
8068
|
-
import { join as
|
|
8426
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
|
|
8427
|
+
import { join as join19 } from "path";
|
|
8069
8428
|
|
|
8070
8429
|
// prd-run/local-issue-run.ts
|
|
8071
8430
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
@@ -8152,8 +8511,30 @@ async function runLocalIssue(prdId, issueId, builder, repoRoot2) {
|
|
|
8152
8511
|
failureCode: result.error ?? "Builder execution failed"
|
|
8153
8512
|
};
|
|
8154
8513
|
}
|
|
8514
|
+
if (!result.issueFinalReviewPassedAt || !result.finalizerTitle || !result.finalizerBody) {
|
|
8515
|
+
return {
|
|
8516
|
+
ok: false,
|
|
8517
|
+
issueId,
|
|
8518
|
+
branch,
|
|
8519
|
+
failureCode: "Local issue run did not produce Issue Final Review and finalizer receipts"
|
|
8520
|
+
};
|
|
8521
|
+
}
|
|
8522
|
+
return {
|
|
8523
|
+
ok: true,
|
|
8524
|
+
issueId,
|
|
8525
|
+
branch,
|
|
8526
|
+
issueFinalReviewPassedAt: result.issueFinalReviewPassedAt,
|
|
8527
|
+
finalizerTitle: result.finalizerTitle,
|
|
8528
|
+
finalizerBody: result.finalizerBody,
|
|
8529
|
+
finalizerArtifactPath: result.finalizerArtifactPath
|
|
8530
|
+
};
|
|
8155
8531
|
}
|
|
8156
|
-
return {
|
|
8532
|
+
return {
|
|
8533
|
+
ok: false,
|
|
8534
|
+
issueId,
|
|
8535
|
+
branch,
|
|
8536
|
+
failureCode: "Local issue runner requires Issue Final Review and finalizer lifecycle execution before merge"
|
|
8537
|
+
};
|
|
8157
8538
|
} catch (error) {
|
|
8158
8539
|
return {
|
|
8159
8540
|
ok: false,
|
|
@@ -8167,7 +8548,7 @@ async function runLocalIssue(prdId, issueId, builder, repoRoot2) {
|
|
|
8167
8548
|
// prd-run/local-queue-loop.ts
|
|
8168
8549
|
var CHILD_CLEANUP_LABELS = ["agent-in-progress", "pr-open-awaiting-merge"];
|
|
8169
8550
|
function getIssueArtifactPath2(repoRoot2, prdId, issueId) {
|
|
8170
|
-
return
|
|
8551
|
+
return join19(
|
|
8171
8552
|
repoRoot2,
|
|
8172
8553
|
".pourkit",
|
|
8173
8554
|
"local-prd-runs",
|
|
@@ -8178,7 +8559,7 @@ function getIssueArtifactPath2(repoRoot2, prdId, issueId) {
|
|
|
8178
8559
|
}
|
|
8179
8560
|
function readIssueArtifact(repoRoot2, prdId, issueId) {
|
|
8180
8561
|
try {
|
|
8181
|
-
const content =
|
|
8562
|
+
const content = readFileSync16(
|
|
8182
8563
|
getIssueArtifactPath2(repoRoot2, prdId, issueId),
|
|
8183
8564
|
"utf-8"
|
|
8184
8565
|
);
|
|
@@ -8281,10 +8662,40 @@ async function runLocalQueueLoop(prdId, repoRoot2, issueProvider) {
|
|
|
8281
8662
|
blockedGate: "queue"
|
|
8282
8663
|
};
|
|
8283
8664
|
}
|
|
8665
|
+
if (!runResult.issueFinalReviewPassedAt || !runResult.finalizerTitle || !runResult.finalizerBody) {
|
|
8666
|
+
return {
|
|
8667
|
+
ok: false,
|
|
8668
|
+
completedIssues,
|
|
8669
|
+
blockedIssues,
|
|
8670
|
+
failureCode: runResult.failureCode ?? "issue_final_review_not_passed",
|
|
8671
|
+
repairGuidance: "Local issue run must complete Issue Final Review and finalizer before local squash merge.",
|
|
8672
|
+
blockedGate: "queue"
|
|
8673
|
+
};
|
|
8674
|
+
}
|
|
8675
|
+
const runArtifact = readIssueArtifact(root, prdId, issue.id);
|
|
8676
|
+
if (!runArtifact) {
|
|
8677
|
+
return {
|
|
8678
|
+
ok: false,
|
|
8679
|
+
completedIssues,
|
|
8680
|
+
blockedIssues,
|
|
8681
|
+
failureCode: "missing_issue_artifact",
|
|
8682
|
+
repairGuidance: `Issue artifact not found for ${issue.id} under PRD ${prdId}. Ensure the issue exists before running the queue loop.`,
|
|
8683
|
+
blockedGate: "queue"
|
|
8684
|
+
};
|
|
8685
|
+
}
|
|
8686
|
+
runArtifact.issueFinalReviewPassedAt = runResult.issueFinalReviewPassedAt;
|
|
8687
|
+
runArtifact.receipts.reviewedAt = runResult.issueFinalReviewPassedAt;
|
|
8688
|
+
runArtifact.receipts.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8689
|
+
writeIssueArtifact(root, prdId, runArtifact);
|
|
8284
8690
|
const mergeResult = await squashMergeLocalIssue(
|
|
8285
8691
|
prdId,
|
|
8286
8692
|
issue.id,
|
|
8287
|
-
|
|
8693
|
+
{
|
|
8694
|
+
finalizerTitle: runResult.finalizerTitle,
|
|
8695
|
+
finalizerBody: runResult.finalizerBody,
|
|
8696
|
+
finalizerArtifactPath: runResult.finalizerArtifactPath ?? "",
|
|
8697
|
+
sourceBranch: runResult.branch
|
|
8698
|
+
},
|
|
8288
8699
|
root
|
|
8289
8700
|
);
|
|
8290
8701
|
if (!mergeResult.ok) {
|
|
@@ -8415,24 +8826,16 @@ function selectIssue(candidates, options = {}) {
|
|
|
8415
8826
|
}
|
|
8416
8827
|
|
|
8417
8828
|
// issues/blocked-issue.ts
|
|
8418
|
-
function parseBlockedBy(body) {
|
|
8419
|
-
if (!body) return [];
|
|
8420
|
-
const bm = body.match(/## Blocked by\s*\n([\s\S]*?)(?=\n## |$)/i);
|
|
8421
|
-
if (!bm) return [];
|
|
8422
|
-
const refs = [];
|
|
8423
|
-
const re = /#(\d+)/g;
|
|
8424
|
-
let m;
|
|
8425
|
-
while ((m = re.exec(bm[1])) !== null) {
|
|
8426
|
-
refs.push(Number(m[1]));
|
|
8427
|
-
}
|
|
8428
|
-
return refs;
|
|
8429
|
-
}
|
|
8430
8829
|
async function reconcileBlockedIssue(issue, deps) {
|
|
8431
|
-
const
|
|
8432
|
-
if (
|
|
8830
|
+
const parsedBlockers = parseBlockedByRefs(issue.body);
|
|
8831
|
+
if (!parsedBlockers.hasSection) {
|
|
8433
8832
|
await deps.transitions.moveToNeedsTriage(issue.number);
|
|
8434
8833
|
return "needs-triage";
|
|
8435
8834
|
}
|
|
8835
|
+
const blockers = await resolveBlockedRefs(parsedBlockers.refs, issue, deps);
|
|
8836
|
+
if (blockers.length === 0) {
|
|
8837
|
+
return "still-blocked";
|
|
8838
|
+
}
|
|
8436
8839
|
const stillBlocked = await anyBlockerStillOpen(blockers, deps.getIssueState);
|
|
8437
8840
|
if (stillBlocked) {
|
|
8438
8841
|
return "still-blocked";
|
|
@@ -8450,6 +8853,37 @@ async function reconcileBlockedIssue(issue, deps) {
|
|
|
8450
8853
|
await deps.transitions.moveToNeedsTriage(issue.number);
|
|
8451
8854
|
return "needs-triage";
|
|
8452
8855
|
}
|
|
8856
|
+
function parseBlockedByRefs(body) {
|
|
8857
|
+
if (!body) return { hasSection: false, refs: [] };
|
|
8858
|
+
const bm = body.match(/## Blocked by\s*\n([\s\S]*?)(?=\n## |$)/i);
|
|
8859
|
+
if (!bm) return { hasSection: false, refs: [] };
|
|
8860
|
+
const refs = [];
|
|
8861
|
+
const re = /#(\d+)|\bI-\d+\b/gi;
|
|
8862
|
+
let m;
|
|
8863
|
+
while ((m = re.exec(bm[1])) !== null) {
|
|
8864
|
+
refs.push((m[1] ? `#${m[1]}` : m[0]).toUpperCase());
|
|
8865
|
+
}
|
|
8866
|
+
return { hasSection: true, refs: Array.from(new Set(refs)) };
|
|
8867
|
+
}
|
|
8868
|
+
async function resolveBlockedRefs(refs, issue, deps) {
|
|
8869
|
+
const resolved = [];
|
|
8870
|
+
for (const ref of refs) {
|
|
8871
|
+
const issueNumber = parseGitHubIssueRef(ref);
|
|
8872
|
+
if (issueNumber !== null) {
|
|
8873
|
+
resolved.push(issueNumber);
|
|
8874
|
+
continue;
|
|
8875
|
+
}
|
|
8876
|
+
const siblingNumber = deps.resolveIssueRef ? await deps.resolveIssueRef(ref, issue) : null;
|
|
8877
|
+
if (siblingNumber !== null) {
|
|
8878
|
+
resolved.push(siblingNumber);
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
return Array.from(new Set(resolved));
|
|
8882
|
+
}
|
|
8883
|
+
function parseGitHubIssueRef(ref) {
|
|
8884
|
+
const match = ref.match(/^#?(\d+)$/);
|
|
8885
|
+
return match ? Number(match[1]) : null;
|
|
8886
|
+
}
|
|
8453
8887
|
async function reconcileBlockedIssues(issues, deps) {
|
|
8454
8888
|
const results = [];
|
|
8455
8889
|
for (const issue of issues) {
|
|
@@ -8598,6 +9032,16 @@ function makeReconcileDeps(options) {
|
|
|
8598
9032
|
const issue = await options.issueProvider.fetchIssue(issueNumber);
|
|
8599
9033
|
return issue.state === "closed" ? "CLOSED" : "OPEN";
|
|
8600
9034
|
},
|
|
9035
|
+
resolveIssueRef: async (ref, issue) => {
|
|
9036
|
+
const parentRef = options.prdRef ?? parseStackedIssue(issue.title ?? "", issue.body).parentRef;
|
|
9037
|
+
if (!parentRef) return null;
|
|
9038
|
+
const related = await options.issueProvider.listRelatedIssues(parentRef);
|
|
9039
|
+
const expectedPrefix = `${parentRef} / ${ref.toUpperCase()}:`;
|
|
9040
|
+
const match = related.find(
|
|
9041
|
+
(candidate) => candidate.title.toUpperCase().startsWith(expectedPrefix)
|
|
9042
|
+
);
|
|
9043
|
+
return match?.number ?? null;
|
|
9044
|
+
},
|
|
8601
9045
|
transitions,
|
|
8602
9046
|
typeLabels: TYPE_LABELS,
|
|
8603
9047
|
readyLabel: options.config.labels.readyForAgent
|
|
@@ -8675,6 +9119,11 @@ function runOneQueueIssueEffect(options) {
|
|
|
8675
9119
|
...prdRunMode ? { prdRunMode } : {}
|
|
8676
9120
|
}),
|
|
8677
9121
|
catch: (e) => {
|
|
9122
|
+
if (e instanceof Error && e.name === "HumanHandoffStop") {
|
|
9123
|
+
return new QueueProviderError(
|
|
9124
|
+
`Issue Final Review requires human handoff: ${e.message}`
|
|
9125
|
+
);
|
|
9126
|
+
}
|
|
8678
9127
|
if (e instanceof Error) return e;
|
|
8679
9128
|
throw e;
|
|
8680
9129
|
}
|
|
@@ -8691,1964 +9140,178 @@ function runOneQueueIssueEffect(options) {
|
|
|
8691
9140
|
logger.raw(` PR Title: ${runResult.prTitle}`);
|
|
8692
9141
|
logger.raw(` PR Number: ${runResult.prNumber}`);
|
|
8693
9142
|
logger.raw(` PR URL: ${runResult.prUrl}`);
|
|
8694
|
-
}
|
|
8695
|
-
logger.raw(` Target: ${runResult.target.name}`);
|
|
8696
|
-
return { selected, runResult };
|
|
8697
|
-
});
|
|
8698
|
-
}
|
|
8699
|
-
function runQueue(options) {
|
|
8700
|
-
return runOneQueueIssueEffect(options);
|
|
8701
|
-
}
|
|
8702
|
-
function runQueueLoopEffect(options, results) {
|
|
8703
|
-
return Effect8.gen(function* () {
|
|
8704
|
-
yield* reconcileBlockedEffect(options);
|
|
8705
|
-
const outcome = yield* runOneQueueIssueEffect(options);
|
|
8706
|
-
if (outcome.selected === null) {
|
|
8707
|
-
return {
|
|
8708
|
-
drained: true,
|
|
8709
|
-
processedCount: results.length,
|
|
8710
|
-
results,
|
|
8711
|
-
selected: null,
|
|
8712
|
-
reason: "Queue drained.",
|
|
8713
|
-
code: "drained"
|
|
8714
|
-
};
|
|
8715
|
-
}
|
|
8716
|
-
const newResults = [...results, outcome];
|
|
8717
|
-
const processedIssue = yield* Effect8.tryPromise({
|
|
8718
|
-
try: () => options.issueProvider.fetchIssue(outcome.selected.number),
|
|
8719
|
-
catch: (e) => {
|
|
8720
|
-
if (e instanceof Error) {
|
|
8721
|
-
return new QueueProviderError(e.message);
|
|
8722
|
-
}
|
|
8723
|
-
throw e;
|
|
8724
|
-
}
|
|
8725
|
-
});
|
|
8726
|
-
if (processedIssue.state === "closed") {
|
|
8727
|
-
yield* reconcileBlockedEffect(options);
|
|
8728
|
-
}
|
|
8729
|
-
return yield* runQueueLoopEffect(options, newResults);
|
|
8730
|
-
});
|
|
8731
|
-
}
|
|
8732
|
-
function runQueueLoop(options) {
|
|
8733
|
-
return runQueueLoopEffect(options, []);
|
|
8734
|
-
}
|
|
8735
|
-
|
|
8736
|
-
// commands/queue-run.ts
|
|
8737
|
-
async function runQueueCommand(options) {
|
|
8738
|
-
initializeEffectRuntime();
|
|
8739
|
-
const queueOptions = {
|
|
8740
|
-
targetName: options.targetName,
|
|
8741
|
-
config: options.config,
|
|
8742
|
-
issueProvider: options.issueProvider,
|
|
8743
|
-
prProvider: options.prProvider,
|
|
8744
|
-
executionProvider: options.executionProvider,
|
|
8745
|
-
force: options.force,
|
|
8746
|
-
logger: options.logger,
|
|
8747
|
-
repoRoot: options.repoRoot,
|
|
8748
|
-
prdRef: options.prdRef,
|
|
8749
|
-
queueRunContext: options.queueRunContext,
|
|
8750
|
-
prdRunMode: options.queueRunContext?.prdRunMode
|
|
8751
|
-
};
|
|
8752
|
-
if (!options.loop) {
|
|
8753
|
-
return runEffectAndMapExit(runQueue(queueOptions));
|
|
8754
|
-
}
|
|
8755
|
-
return runEffectAndMapExit(runQueueLoop(queueOptions));
|
|
8756
|
-
}
|
|
8757
|
-
|
|
8758
|
-
// commands/prd-run.ts
|
|
8759
|
-
function planLaunchResume(record) {
|
|
8760
|
-
if (!record) return { attempted: [], skipped: [], resumed: [] };
|
|
8761
|
-
return pipe(
|
|
8762
|
-
record,
|
|
8763
|
-
Match.value,
|
|
8764
|
-
Match.when({ status: "waiting_for_integration" }, () => ({
|
|
8765
|
-
attempted: [],
|
|
8766
|
-
skipped: ["start", "queue", "final-review"],
|
|
8767
|
-
resumed: []
|
|
8768
|
-
})),
|
|
8769
|
-
Match.when({ status: "completed_local_branch" }, () => ({
|
|
8770
|
-
attempted: [],
|
|
8771
|
-
skipped: ["start", "queue", "final-review"],
|
|
8772
|
-
resumed: []
|
|
8773
|
-
})),
|
|
8774
|
-
Match.when({ status: "complete" }, () => ({
|
|
8775
|
-
attempted: [],
|
|
8776
|
-
skipped: ["start", "queue", "final-review"],
|
|
8777
|
-
resumed: []
|
|
8778
|
-
})),
|
|
8779
|
-
Match.when({ status: "drained" }, () => ({
|
|
8780
|
-
attempted: [],
|
|
8781
|
-
skipped: ["start", "queue"],
|
|
8782
|
-
resumed: ["final-review"]
|
|
8783
|
-
})),
|
|
8784
|
-
Match.when({ status: "final_reviewed" }, () => ({
|
|
8785
|
-
attempted: [],
|
|
8786
|
-
skipped: ["start", "queue", "final-review"],
|
|
8787
|
-
resumed: []
|
|
8788
|
-
})),
|
|
8789
|
-
Match.when(
|
|
8790
|
-
(value) => value.status === "blocked" && value.blockedGate === "final-review" && canRetryFinalReviewBlock(value),
|
|
8791
|
-
() => ({
|
|
8792
|
-
attempted: [],
|
|
8793
|
-
skipped: [],
|
|
8794
|
-
resumed: ["start"]
|
|
8795
|
-
})
|
|
8796
|
-
),
|
|
8797
|
-
Match.when(
|
|
8798
|
-
(value) => value.status === "starting" || value.status === "running" || value.status === "blocked" && (value.blockedGate === "queue" || value.blockedGate === "branch-state"),
|
|
8799
|
-
(value) => value.start ? {
|
|
8800
|
-
attempted: [],
|
|
8801
|
-
skipped: [],
|
|
8802
|
-
resumed: ["start"]
|
|
8803
|
-
} : {
|
|
8804
|
-
attempted: [],
|
|
8805
|
-
skipped: ["start", "queue", "final-review"],
|
|
8806
|
-
resumed: [],
|
|
8807
|
-
blocked: "missing-start-receipt"
|
|
8808
|
-
}
|
|
8809
|
-
),
|
|
8810
|
-
Match.orElse(() => ({
|
|
8811
|
-
attempted: [],
|
|
8812
|
-
skipped: [],
|
|
8813
|
-
resumed: []
|
|
8814
|
-
}))
|
|
8815
|
-
);
|
|
8816
|
-
}
|
|
8817
|
-
async function validateFinalReviewChildCompleteness(prdRef, record, issueProvider) {
|
|
8818
|
-
const scopeChangeMap = /* @__PURE__ */ new Map();
|
|
8819
|
-
for (const sc of record.scopeChanges ?? []) {
|
|
8820
|
-
scopeChangeMap.set(sc.issueNumber, sc);
|
|
8821
|
-
}
|
|
8822
|
-
let childIssues;
|
|
8823
|
-
try {
|
|
8824
|
-
const listed = await issueProvider.listRelatedIssues(prdRef);
|
|
8825
|
-
if (!Array.isArray(listed)) {
|
|
8826
|
-
return {
|
|
8827
|
-
ok: true,
|
|
8828
|
-
diagnostics: [
|
|
8829
|
-
`IssueProvider.listRelatedIssues("${prdRef}") did not return child issue data; skipping child completeness validation.`
|
|
8830
|
-
]
|
|
8831
|
-
};
|
|
8832
|
-
}
|
|
8833
|
-
childIssues = listed;
|
|
8834
|
-
} catch (error) {
|
|
8835
|
-
return {
|
|
8836
|
-
ok: false,
|
|
8837
|
-
gate: "final-review",
|
|
8838
|
-
reason: `Final Review blocked: failed to list child issues for ${prdRef}.`,
|
|
8839
|
-
diagnostics: [error instanceof Error ? error.message : String(error)],
|
|
8840
|
-
offendingPaths: []
|
|
8841
|
-
};
|
|
8842
|
-
}
|
|
8843
|
-
if (childIssues.length === 0) {
|
|
8844
|
-
return {
|
|
8845
|
-
ok: false,
|
|
8846
|
-
gate: "final-review",
|
|
8847
|
-
reason: `Final Review blocked: no child issues found for ${prdRef}.`,
|
|
8848
|
-
diagnostics: [
|
|
8849
|
-
`IssueProvider.listRelatedIssues("${prdRef}") returned no child issues.`
|
|
8850
|
-
],
|
|
8851
|
-
offendingPaths: []
|
|
8852
|
-
};
|
|
8853
|
-
}
|
|
8854
|
-
const incompleteChildren = [];
|
|
8855
|
-
const scopeChangeDiagnostics = [];
|
|
8856
|
-
for (const child of childIssues) {
|
|
8857
|
-
if (!child.number || child.number <= 0) {
|
|
8858
|
-
incompleteChildren.push(`Issue has invalid number ${child.number}`);
|
|
8859
|
-
continue;
|
|
8860
|
-
}
|
|
8861
|
-
let issueData = null;
|
|
8862
|
-
try {
|
|
8863
|
-
issueData = await issueProvider.fetchIssue(child.number);
|
|
8864
|
-
} catch {
|
|
8865
|
-
incompleteChildren.push(`#${child.number}: failed to fetch issue state`);
|
|
8866
|
-
continue;
|
|
8867
|
-
}
|
|
8868
|
-
if (!issueData || typeof issueData.state !== "string") {
|
|
8869
|
-
incompleteChildren.push(`#${child.number}: failed to fetch issue state`);
|
|
8870
|
-
continue;
|
|
8871
|
-
}
|
|
8872
|
-
if (issueData.state === "closed") {
|
|
8873
|
-
continue;
|
|
8874
|
-
}
|
|
8875
|
-
const scopeChange = scopeChangeMap.get(child.number);
|
|
8876
|
-
const skipLabels = ["wontfix", "skipped"];
|
|
8877
|
-
const hasSkipLabel = child.labels.some(
|
|
8878
|
-
(label) => skipLabels.includes(label)
|
|
8879
|
-
);
|
|
8880
|
-
if (hasSkipLabel && scopeChange && scopeChange.decision === "accepted_scope_change" && scopeChange.reason && scopeChange.acceptedBy && scopeChange.acceptedAt) {
|
|
8881
|
-
scopeChangeDiagnostics.push(
|
|
8882
|
-
`#${child.number}: skipped/wontfix with accepted scope-change evidence`
|
|
8883
|
-
);
|
|
8884
|
-
continue;
|
|
8885
|
-
}
|
|
8886
|
-
if (scopeChange && scopeChange.decision === "accepted_scope_change" && scopeChange.reason && scopeChange.acceptedBy && scopeChange.acceptedAt) {
|
|
8887
|
-
scopeChangeDiagnostics.push(
|
|
8888
|
-
`#${child.number}: open but accepted scope-change evidence`
|
|
8889
|
-
);
|
|
8890
|
-
continue;
|
|
8891
|
-
}
|
|
8892
|
-
incompleteChildren.push(
|
|
8893
|
-
`#${child.number}: ${issueData.state} without accepted scope-change evidence`
|
|
8894
|
-
);
|
|
8895
|
-
}
|
|
8896
|
-
if (incompleteChildren.length > 0) {
|
|
8897
|
-
return {
|
|
8898
|
-
ok: false,
|
|
8899
|
-
gate: "final-review",
|
|
8900
|
-
reason: "Final Review blocked: one or more child issues are incomplete without accepted scope-change evidence.",
|
|
8901
|
-
diagnostics: [
|
|
8902
|
-
"Incomplete child issues:",
|
|
8903
|
-
...incompleteChildren.map((ic) => ` - ${ic}`)
|
|
8904
|
-
],
|
|
8905
|
-
offendingPaths: []
|
|
8906
|
-
};
|
|
8907
|
-
}
|
|
8908
|
-
const diagnostics = [];
|
|
8909
|
-
if (scopeChangeDiagnostics.length > 0) {
|
|
8910
|
-
diagnostics.push(
|
|
8911
|
-
"Scope-change evidence accepted for:",
|
|
8912
|
-
...scopeChangeDiagnostics.map((d) => ` - ${d}`)
|
|
8913
|
-
);
|
|
8914
|
-
}
|
|
8915
|
-
return { ok: true, diagnostics };
|
|
8916
|
-
}
|
|
8917
|
-
function validateLocalStartStore(repoRoot2, prdRef) {
|
|
8918
|
-
const localStoreDir = join18(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
|
|
8919
|
-
let localStoreReady = false;
|
|
8920
|
-
if (existsSync14(localStoreDir)) {
|
|
8921
|
-
const localStorePath = join18(localStoreDir, "prd.json");
|
|
8922
|
-
try {
|
|
8923
|
-
const content = JSON.parse(readFileSync15(localStorePath, "utf8"));
|
|
8924
|
-
localStoreReady = content?.id === prdRef && content?.kind === "prd";
|
|
8925
|
-
} catch {
|
|
8926
|
-
localStoreReady = false;
|
|
8927
|
-
}
|
|
8928
|
-
}
|
|
8929
|
-
if (existsSync14(localStoreDir) && !localStoreReady) {
|
|
8930
|
-
return {
|
|
8931
|
-
ok: false,
|
|
8932
|
-
gate: "branch-state",
|
|
8933
|
-
reason: `Local PRD Run Store not ready for ${prdRef}. Expected valid prd.json at .pourkit/local-prd-runs/${prdRef}/prd.json with matching PRD ID. Ensure Local PRD Run Store is initialized before starting.`,
|
|
8934
|
-
diagnostics: [
|
|
8935
|
-
`Expected store path: .pourkit/local-prd-runs/${prdRef}/prd.json`,
|
|
8936
|
-
`Expected PRD ID: ${prdRef}`
|
|
8937
|
-
],
|
|
8938
|
-
offendingPaths: []
|
|
8939
|
-
};
|
|
8940
|
-
}
|
|
8941
|
-
return { ok: true };
|
|
8942
|
-
}
|
|
8943
|
-
function buildStartReceipt(options) {
|
|
8944
|
-
return {
|
|
8945
|
-
status: options.status,
|
|
8946
|
-
targetName: options.targetName,
|
|
8947
|
-
prdBranch: options.prdBranch,
|
|
8948
|
-
startBaseBranch: options.startBaseBranch,
|
|
8949
|
-
startBaseCommit: options.startBaseCommit,
|
|
8950
|
-
branchAction: options.branchAction,
|
|
8951
|
-
startedAt: options.startedAt,
|
|
8952
|
-
refreshReceipts: []
|
|
8953
|
-
};
|
|
8954
|
-
}
|
|
8955
|
-
function canRetryFinalReviewBlock(record) {
|
|
8956
|
-
if (record.status !== "blocked" || record.blockedGate !== "final-review") {
|
|
8957
|
-
return false;
|
|
8958
|
-
}
|
|
8959
|
-
if (record.start?.queueDrainedAt || record.finalReview) {
|
|
8960
|
-
return true;
|
|
8961
|
-
}
|
|
8962
|
-
return Boolean(
|
|
8963
|
-
record.start && !record.blockedReason?.includes("not drained")
|
|
8964
|
-
);
|
|
8965
|
-
}
|
|
8966
|
-
function loadFinalReviewPrompt(repoRoot2, promptTemplate) {
|
|
8967
|
-
const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
|
|
8968
|
-
return existsSync14(promptPath) ? readFileSync15(promptPath, "utf-8") : promptTemplate;
|
|
8969
|
-
}
|
|
8970
|
-
function buildFinalReviewPrompt(options) {
|
|
8971
|
-
return [
|
|
8972
|
-
"# Active PRD Final Review Context",
|
|
8973
|
-
"",
|
|
8974
|
-
`Worktree checkout base: ${options.evidencePacket.prdBranch}`,
|
|
8975
|
-
`Review only this range: ${options.evidencePacket.mergeBase}..HEAD`,
|
|
8976
|
-
"Do not compare against current target branch HEAD; the merge base is the review baseline.",
|
|
8977
|
-
`Before handoff, run: pourkit validate-artifact final-review .pourkit/final-review-artifact.json --prd-ref ${options.evidencePacket.prdRef} --checkout-base ${options.evidencePacket.prdBranch} --review-base ${options.evidencePacket.mergeBase}`,
|
|
8978
|
-
"Fix any validation failures before handing off.",
|
|
8979
|
-
"",
|
|
8980
|
-
"## Verification",
|
|
8981
|
-
"",
|
|
8982
|
-
`Run this command before handoff when retouch changes are made: pourkit run-verification --target ${options.targetName}`,
|
|
8983
|
-
"",
|
|
8984
|
-
"Evidence Packet (do not infer PRD context from local state files):",
|
|
8985
|
-
JSON.stringify(options.evidencePacket, null, 2),
|
|
8986
|
-
"",
|
|
8987
|
-
loadFinalReviewPrompt(options.repoRoot, options.promptTemplate)
|
|
8988
|
-
].join("\n");
|
|
8989
|
-
}
|
|
8990
|
-
function buildRetouchBranchName(prdRef) {
|
|
8991
|
-
return `prd-run/${prdRef}-final-review-retouch`;
|
|
8992
|
-
}
|
|
8993
|
-
function buildFinalReviewBranchName(prdRef) {
|
|
8994
|
-
return `pourkit/${normalizePrdRunRef(prdRef).toLowerCase()}-final-review`;
|
|
8995
|
-
}
|
|
8996
|
-
function finalReviewWorktreePath(repoRoot2, branchName) {
|
|
8997
|
-
return join18(
|
|
8998
|
-
repoRoot2,
|
|
8999
|
-
".sandcastle",
|
|
9000
|
-
"worktrees",
|
|
9001
|
-
branchName.replace(/\//g, "-")
|
|
9002
|
-
);
|
|
9003
|
-
}
|
|
9004
|
-
function collectSpawnDiagnostics(result, fallback) {
|
|
9005
|
-
const diagnostics = [];
|
|
9006
|
-
const stderr = result.stderr?.toString?.().trim();
|
|
9007
|
-
const stdout = result.stdout?.toString?.().trim();
|
|
9008
|
-
if (stderr) diagnostics.push(stderr);
|
|
9009
|
-
if (stdout) diagnostics.push(stdout);
|
|
9010
|
-
return diagnostics.length > 0 ? diagnostics : [fallback];
|
|
9011
|
-
}
|
|
9012
|
-
function listFinalReviewChangedPaths(worktreePath, mergeBase) {
|
|
9013
|
-
const paths = /* @__PURE__ */ new Set();
|
|
9014
|
-
const addPath = (path9) => {
|
|
9015
|
-
if (path9 === ".pourkit/final-review-artifact.json" || path9 === ".pourkit/final-review-pr-artifact.json" || path9 === ".pourkit/.tmp/finalizer/agent-output.md") {
|
|
9016
|
-
return;
|
|
9017
|
-
}
|
|
9018
|
-
paths.add(path9);
|
|
9019
|
-
};
|
|
9020
|
-
const diffResult = spawnSync3(
|
|
9021
|
-
"git",
|
|
9022
|
-
["diff", "--name-only", mergeBase, "--", "."],
|
|
9023
|
-
{
|
|
9024
|
-
cwd: worktreePath,
|
|
9025
|
-
encoding: "utf8"
|
|
9026
|
-
}
|
|
9027
|
-
);
|
|
9028
|
-
for (const path9 of diffResult.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
9029
|
-
addPath(path9);
|
|
9030
|
-
}
|
|
9031
|
-
const statusResult = spawnSync3(
|
|
9032
|
-
"git",
|
|
9033
|
-
["status", "--porcelain", "--untracked-files=all"],
|
|
9034
|
-
{
|
|
9035
|
-
cwd: worktreePath,
|
|
9036
|
-
encoding: "utf8"
|
|
9037
|
-
}
|
|
9038
|
-
);
|
|
9039
|
-
if (statusResult.status === 0) {
|
|
9040
|
-
for (const line of statusResult.stdout.split(/\r?\n/)) {
|
|
9041
|
-
const parsed = parseGitStatusLine(line);
|
|
9042
|
-
if (parsed) addPath(parsed.path);
|
|
9043
|
-
}
|
|
9044
|
-
}
|
|
9045
|
-
return [...paths];
|
|
9046
|
-
}
|
|
9047
|
-
function buildFinalReviewFinalizerPrompt(options) {
|
|
9048
|
-
const promptPath = resolvePromptTemplatePath(
|
|
9049
|
-
options.repoRoot,
|
|
9050
|
-
options.promptTemplate
|
|
9051
|
-
);
|
|
9052
|
-
const promptBody = existsSync14(promptPath) ? readFileSync15(promptPath, "utf-8") : options.promptTemplate;
|
|
9053
|
-
return [
|
|
9054
|
-
"# Final Review Retouch PR Finalizer",
|
|
9055
|
-
"",
|
|
9056
|
-
"Write a human-facing retouch PR title and body for this Final Review result.",
|
|
9057
|
-
"Write the artifact using the standard finalizer protocol:",
|
|
9058
|
-
"",
|
|
9059
|
-
"## PR Title",
|
|
9060
|
-
"",
|
|
9061
|
-
"<one conventional PR title>",
|
|
9062
|
-
"",
|
|
9063
|
-
"## PR Body",
|
|
9064
|
-
"",
|
|
9065
|
-
"<PR body>",
|
|
9066
|
-
"Do not edit implementation files.",
|
|
9067
|
-
"",
|
|
9068
|
-
`PRD ref: ${options.prdRef}`,
|
|
9069
|
-
`PRD branch: ${options.prdBranch}`,
|
|
9070
|
-
`Review base: ${options.mergeBase}`,
|
|
9071
|
-
"",
|
|
9072
|
-
"Final Review summary:",
|
|
9073
|
-
options.summary,
|
|
9074
|
-
"",
|
|
9075
|
-
"Changed paths:",
|
|
9076
|
-
...options.changedPaths.map((p) => `- ${p}`),
|
|
9077
|
-
"",
|
|
9078
|
-
promptBody
|
|
9079
|
-
].join("\n");
|
|
9080
|
-
}
|
|
9081
|
-
async function runFinalReviewPrFinalizer(options) {
|
|
9082
|
-
const finalizer = options.target.strategy.finalize.prDescriptionAgent;
|
|
9083
|
-
const artifactPathInWorktree = join18(
|
|
9084
|
-
".pourkit",
|
|
9085
|
-
".tmp",
|
|
9086
|
-
"finalizer",
|
|
9087
|
-
"agent-output.md"
|
|
9088
|
-
);
|
|
9089
|
-
try {
|
|
9090
|
-
const result = await runEffectAndMapExit(
|
|
9091
|
-
runPrDescriptionFinalizerCore({
|
|
9092
|
-
executionProvider: options.executionProvider,
|
|
9093
|
-
config: options.config,
|
|
9094
|
-
target: options.target,
|
|
9095
|
-
finalizer,
|
|
9096
|
-
maxAttempts: options.target.strategy.finalize.maxAttempts,
|
|
9097
|
-
prompt: buildFinalReviewFinalizerPrompt({
|
|
9098
|
-
repoRoot: options.repoRoot,
|
|
9099
|
-
promptTemplate: finalizer.promptTemplate,
|
|
9100
|
-
prdRef: options.prdRef,
|
|
9101
|
-
prdBranch: options.prdBranch,
|
|
9102
|
-
mergeBase: options.mergeBase,
|
|
9103
|
-
summary: options.summary,
|
|
9104
|
-
changedPaths: options.changedPaths
|
|
9105
|
-
}),
|
|
9106
|
-
branchName: options.branchName,
|
|
9107
|
-
repoRoot: options.repoRoot,
|
|
9108
|
-
worktreePath: options.worktreePath,
|
|
9109
|
-
artifactPathInWorktree,
|
|
9110
|
-
commitSummaries: "",
|
|
9111
|
-
baseRef: options.prdBranch,
|
|
9112
|
-
checkoutBase: options.prdBranch,
|
|
9113
|
-
reviewBase: options.mergeBase,
|
|
9114
|
-
logger: options.logger
|
|
9115
|
-
})
|
|
9116
|
-
);
|
|
9117
|
-
return { ok: true, ...result };
|
|
9118
|
-
} catch (error) {
|
|
9119
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
9120
|
-
return { ok: false, reason, diagnostics: [reason] };
|
|
9121
|
-
}
|
|
9122
|
-
}
|
|
9123
|
-
async function lookupMergedPr(prProvider, pr) {
|
|
9124
|
-
const byBranch = await prProvider.getPr(pr.headRefName);
|
|
9125
|
-
if (byBranch?.state === "MERGED" && byBranch.mergeCommitSha) {
|
|
9126
|
-
return byBranch;
|
|
9127
|
-
}
|
|
9128
|
-
const byNumber = await getPrByNumberIfAvailable(prProvider, pr.number);
|
|
9129
|
-
if (byNumber?.state === "MERGED" && byNumber.mergeCommitSha) {
|
|
9130
|
-
return byNumber;
|
|
9131
|
-
}
|
|
9132
|
-
return byBranch ?? byNumber;
|
|
9133
|
-
}
|
|
9134
|
-
async function getPrByNumberIfAvailable(prProvider, prNumber) {
|
|
9135
|
-
if (!prProvider.getPrByNumber) return null;
|
|
9136
|
-
return await prProvider.getPrByNumber(prNumber);
|
|
9137
|
-
}
|
|
9138
|
-
function ensureFinalReviewWorktree(options) {
|
|
9139
|
-
const worktreePath = finalReviewWorktreePath(
|
|
9140
|
-
options.repoRoot,
|
|
9141
|
-
options.branchName
|
|
9142
|
-
);
|
|
9143
|
-
const listResult = spawnSync3("git", ["worktree", "list", "--porcelain"], {
|
|
9144
|
-
cwd: options.repoRoot,
|
|
9145
|
-
encoding: "utf8"
|
|
9146
|
-
});
|
|
9147
|
-
if (listResult.status !== 0) {
|
|
9148
|
-
return {
|
|
9149
|
-
ok: false,
|
|
9150
|
-
reason: "Final Review failed to list git worktrees.",
|
|
9151
|
-
diagnostics: collectSpawnDiagnostics(
|
|
9152
|
-
listResult,
|
|
9153
|
-
"git worktree list failed"
|
|
9154
|
-
)
|
|
9155
|
-
};
|
|
9156
|
-
}
|
|
9157
|
-
const registeredPath = parseWorktreeListPorcelain(
|
|
9158
|
-
listResult.stdout?.toString?.() ?? "",
|
|
9159
|
-
options.branchName
|
|
9160
|
-
);
|
|
9161
|
-
if (registeredPath) {
|
|
9162
|
-
if (registeredPath !== worktreePath) {
|
|
9163
|
-
return {
|
|
9164
|
-
ok: false,
|
|
9165
|
-
reason: `Final Review branch ${options.branchName} already has a registered worktree outside .sandcastle/worktrees.`,
|
|
9166
|
-
diagnostics: [`registered worktree: ${registeredPath}`]
|
|
9167
|
-
};
|
|
9168
|
-
}
|
|
9169
|
-
return { ok: true, worktreePath: registeredPath };
|
|
9170
|
-
}
|
|
9171
|
-
if (existsSync14(worktreePath)) {
|
|
9172
|
-
return {
|
|
9173
|
-
ok: false,
|
|
9174
|
-
reason: "Final Review worktree path exists but is not registered with git.",
|
|
9175
|
-
diagnostics: [`stale worktree path: ${worktreePath}`]
|
|
9176
|
-
};
|
|
9177
|
-
}
|
|
9178
|
-
mkdirSync10(dirname4(worktreePath), { recursive: true });
|
|
9179
|
-
if (options.isLocal) {
|
|
9180
|
-
const branchResult = spawnSync3(
|
|
9181
|
-
"git",
|
|
9182
|
-
["branch", "-f", options.branchName, options.checkoutBase],
|
|
9183
|
-
{ cwd: options.repoRoot, encoding: "utf8" }
|
|
9184
|
-
);
|
|
9185
|
-
if (branchResult.status !== 0) {
|
|
9186
|
-
return {
|
|
9187
|
-
ok: false,
|
|
9188
|
-
reason: `Final Review failed to prepare branch ${options.branchName}.`,
|
|
9189
|
-
diagnostics: collectSpawnDiagnostics(branchResult, "git branch failed")
|
|
9190
|
-
};
|
|
9191
|
-
}
|
|
9192
|
-
} else {
|
|
9193
|
-
const branchResult = spawnSync3(
|
|
9194
|
-
"git",
|
|
9195
|
-
["branch", "-f", options.branchName, `origin/${options.checkoutBase}`],
|
|
9196
|
-
{ cwd: options.repoRoot, encoding: "utf8" }
|
|
9197
|
-
);
|
|
9198
|
-
if (branchResult.status !== 0) {
|
|
9199
|
-
return {
|
|
9200
|
-
ok: false,
|
|
9201
|
-
reason: `Final Review failed to prepare branch ${options.branchName}.`,
|
|
9202
|
-
diagnostics: collectSpawnDiagnostics(branchResult, "git branch failed")
|
|
9203
|
-
};
|
|
9204
|
-
}
|
|
9205
|
-
}
|
|
9206
|
-
const addResult = spawnSync3(
|
|
9207
|
-
"git",
|
|
9208
|
-
["worktree", "add", worktreePath, options.branchName],
|
|
9209
|
-
{ cwd: options.repoRoot, encoding: "utf8" }
|
|
9210
|
-
);
|
|
9211
|
-
if (addResult.status !== 0) {
|
|
9212
|
-
return {
|
|
9213
|
-
ok: false,
|
|
9214
|
-
reason: `Final Review failed to create worktree for ${options.branchName}.`,
|
|
9215
|
-
diagnostics: collectSpawnDiagnostics(
|
|
9216
|
-
addResult,
|
|
9217
|
-
"git worktree add failed"
|
|
9218
|
-
)
|
|
9219
|
-
};
|
|
9220
|
-
}
|
|
9221
|
-
return { ok: true, worktreePath };
|
|
9222
|
-
}
|
|
9223
|
-
function buildRetouchPrTitle(prdRef) {
|
|
9224
|
-
return `chore: ${prdRef} Final Review retouch`;
|
|
9225
|
-
}
|
|
9226
|
-
function buildRetouchPrBody(options) {
|
|
9227
|
-
return [
|
|
9228
|
-
`# ${options.prdRef}: Final Review Retouch`,
|
|
9229
|
-
"",
|
|
9230
|
-
`Retouch PR for ${options.prdRef} Final Review.`,
|
|
9231
|
-
"",
|
|
9232
|
-
"## Summary",
|
|
9233
|
-
"",
|
|
9234
|
-
options.summary,
|
|
9235
|
-
"",
|
|
9236
|
-
"## Changed paths",
|
|
9237
|
-
"",
|
|
9238
|
-
...options.changedPaths.map((p) => `- ${p}`)
|
|
9239
|
-
].join("\n");
|
|
9240
|
-
}
|
|
9241
|
-
async function createOrReuseFinalReviewRetouchPr(options) {
|
|
9242
|
-
const prdRef = normalizePrdRunRef(options.prdRef);
|
|
9243
|
-
const branchName = buildRetouchBranchName(options.prdRef);
|
|
9244
|
-
const existingPr = await options.prProvider.getPr(branchName);
|
|
9245
|
-
if (existingPr && existingPr.state === "OPEN") {
|
|
9246
|
-
return existingPr;
|
|
9247
|
-
}
|
|
9248
|
-
const worktreePath = mkdtempSync(join18(tmpdir(), "pourkit-retouch-"));
|
|
9249
|
-
try {
|
|
9250
|
-
runGitOrThrow(
|
|
9251
|
-
options.repoRoot,
|
|
9252
|
-
["fetch", "origin", prdRef],
|
|
9253
|
-
`fetch origin/${prdRef}`
|
|
9254
|
-
);
|
|
9255
|
-
runGitOrThrow(
|
|
9256
|
-
options.repoRoot,
|
|
9257
|
-
["worktree", "add", "--detach", worktreePath, `origin/${prdRef}`],
|
|
9258
|
-
"create retouch worktree from PRD branch"
|
|
9259
|
-
);
|
|
9260
|
-
runGitOrThrow(
|
|
9261
|
-
worktreePath,
|
|
9262
|
-
["checkout", "-B", branchName],
|
|
9263
|
-
"create retouch branch"
|
|
9264
|
-
);
|
|
9265
|
-
for (const changedFile of options.changedPaths) {
|
|
9266
|
-
const sourcePath = join18(options.sourceWorktreePath, changedFile);
|
|
9267
|
-
const targetPath = join18(worktreePath, changedFile);
|
|
9268
|
-
mkdirSync10(dirname4(targetPath), { recursive: true });
|
|
9269
|
-
cpSync(sourcePath, targetPath, { recursive: true });
|
|
9270
|
-
}
|
|
9271
|
-
runGitOrThrow(
|
|
9272
|
-
worktreePath,
|
|
9273
|
-
["add", "--", ...options.changedPaths],
|
|
9274
|
-
"stage retouch diff"
|
|
9275
|
-
);
|
|
9276
|
-
runGitOrThrow(
|
|
9277
|
-
worktreePath,
|
|
9278
|
-
["commit", "-m", `${branchName}: final review retouch`],
|
|
9279
|
-
"commit retouch diff"
|
|
9280
|
-
);
|
|
9281
|
-
} finally {
|
|
9282
|
-
spawnSync3("git", ["worktree", "remove", "--force", worktreePath], {
|
|
9283
|
-
cwd: options.repoRoot,
|
|
9284
|
-
encoding: "utf8"
|
|
9285
|
-
});
|
|
9286
|
-
rmSync3(worktreePath, { recursive: true, force: true });
|
|
9287
|
-
}
|
|
9288
|
-
const pushResult = spawnSync3(
|
|
9289
|
-
"git",
|
|
9290
|
-
["push", "--force", "origin", `${branchName}:refs/heads/${branchName}`],
|
|
9291
|
-
{
|
|
9292
|
-
cwd: options.repoRoot,
|
|
9293
|
-
encoding: "utf8"
|
|
9294
|
-
}
|
|
9295
|
-
);
|
|
9296
|
-
if (pushResult.status !== 0) {
|
|
9297
|
-
throw new Error(
|
|
9298
|
-
`Failed to publish retouch branch ${branchName}: ${[
|
|
9299
|
-
pushResult.error instanceof Error ? pushResult.error.message : void 0,
|
|
9300
|
-
pushResult.stderr?.toString?.() ?? String(pushResult.stderr ?? ""),
|
|
9301
|
-
pushResult.stdout?.toString?.() ?? String(pushResult.stdout ?? "")
|
|
9302
|
-
].filter((value) => Boolean(value && value.trim())).join(" ")}`
|
|
9303
|
-
);
|
|
9304
|
-
}
|
|
9305
|
-
return options.prProvider.createPr({
|
|
9306
|
-
base: prdRef,
|
|
9307
|
-
head: branchName,
|
|
9308
|
-
title: options.title ?? buildRetouchPrTitle(prdRef),
|
|
9309
|
-
body: options.body ?? buildRetouchPrBody({
|
|
9310
|
-
prdRef,
|
|
9311
|
-
summary: options.summary,
|
|
9312
|
-
changedPaths: options.changedPaths
|
|
9313
|
-
})
|
|
9314
|
-
});
|
|
9315
|
-
}
|
|
9316
|
-
async function writeAndVerifyLocalDualReceipt(repoRoot2, prdRef, record, targetName, prdBranch, mergeBase, verdict, reviewedAt, isLocalMode) {
|
|
9317
|
-
if (!isLocalMode) return null;
|
|
9318
|
-
let currentRecord = await readLocalPrdRun(repoRoot2, prdRef);
|
|
9319
|
-
if (!currentRecord) {
|
|
9320
|
-
currentRecord = {
|
|
9321
|
-
prdId: normalizePrdRunRef(prdRef),
|
|
9322
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9323
|
-
receipts: {},
|
|
9324
|
-
metadata: {}
|
|
9325
|
-
};
|
|
9326
|
-
}
|
|
9327
|
-
const localStoreReceipt = {
|
|
9328
|
-
completedAt: reviewedAt,
|
|
9329
|
-
targetName,
|
|
9330
|
-
prdBranch,
|
|
9331
|
-
mergeBase,
|
|
9332
|
-
verdict,
|
|
9333
|
-
diagnostics: [],
|
|
9334
|
-
artifactPath: ".pourkit/final-review-artifact.json"
|
|
9335
|
-
};
|
|
9336
|
-
await writeLocalPrdRunRecord(repoRoot2, prdRef, {
|
|
9337
|
-
...currentRecord,
|
|
9338
|
-
receipts: {
|
|
9339
|
-
...currentRecord.receipts,
|
|
9340
|
-
finalReview: localStoreReceipt
|
|
9341
|
-
}
|
|
9342
|
-
});
|
|
9343
|
-
const prdRunStateResult = readPrdRun(repoRoot2, prdRef);
|
|
9344
|
-
const localStoreResult = await readLocalPrdRun(repoRoot2, prdRef);
|
|
9345
|
-
if (!prdRunStateResult.record?.finalReview) {
|
|
9346
|
-
const reason = `PRD Run State missing finalReview receipt after write for ${prdRef}.`;
|
|
9347
|
-
writePrdRunRecord(repoRoot2, {
|
|
9348
|
-
...record,
|
|
9349
|
-
prdRef,
|
|
9350
|
-
status: "blocked",
|
|
9351
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9352
|
-
blockedGate: "final-review",
|
|
9353
|
-
blockedReason: reason,
|
|
9354
|
-
diagnostics: [reason],
|
|
9355
|
-
targetName,
|
|
9356
|
-
offendingPaths: []
|
|
9357
|
-
});
|
|
9358
|
-
return {
|
|
9359
|
-
prdRef,
|
|
9360
|
-
status: "blocked",
|
|
9361
|
-
blockedGate: "final-review",
|
|
9362
|
-
blockedReason: reason,
|
|
9363
|
-
diagnostics: [reason],
|
|
9364
|
-
offendingPaths: []
|
|
9365
|
-
};
|
|
9366
|
-
}
|
|
9367
|
-
if (!localStoreResult?.receipts.finalReview) {
|
|
9368
|
-
const reason = `Local PRD Run store missing finalReview receipt after write for ${prdRef}.`;
|
|
9369
|
-
writePrdRunRecord(repoRoot2, {
|
|
9370
|
-
...record,
|
|
9371
|
-
prdRef,
|
|
9372
|
-
status: "blocked",
|
|
9373
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9374
|
-
blockedGate: "final-review",
|
|
9375
|
-
blockedReason: reason,
|
|
9376
|
-
diagnostics: [reason],
|
|
9377
|
-
targetName,
|
|
9378
|
-
offendingPaths: []
|
|
9379
|
-
});
|
|
9380
|
-
return {
|
|
9381
|
-
prdRef,
|
|
9382
|
-
status: "blocked",
|
|
9383
|
-
blockedGate: "final-review",
|
|
9384
|
-
blockedReason: reason,
|
|
9385
|
-
diagnostics: [reason],
|
|
9386
|
-
offendingPaths: []
|
|
9387
|
-
};
|
|
9388
|
-
}
|
|
9389
|
-
const stateFr = prdRunStateResult.record.finalReview;
|
|
9390
|
-
const localFr = localStoreResult.receipts.finalReview;
|
|
9391
|
-
const mismatchFields = [];
|
|
9392
|
-
if (stateFr.targetName !== localFr.targetName)
|
|
9393
|
-
mismatchFields.push("targetName");
|
|
9394
|
-
if (stateFr.prdBranch !== localFr.prdBranch) mismatchFields.push("prdBranch");
|
|
9395
|
-
if (stateFr.mergeBase !== localFr.mergeBase) mismatchFields.push("mergeBase");
|
|
9396
|
-
if (stateFr.verdict !== localFr.verdict) mismatchFields.push("verdict");
|
|
9397
|
-
if (!stateFr.artifactPath !== !localFr.artifactPath)
|
|
9398
|
-
mismatchFields.push("artifactPath");
|
|
9399
|
-
if (!stateFr.diagnostics !== !localFr.diagnostics)
|
|
9400
|
-
mismatchFields.push("diagnostics");
|
|
9401
|
-
if (!stateFr.reviewedAt !== !localFr.completedAt)
|
|
9402
|
-
mismatchFields.push("reviewedTimestamp");
|
|
9403
|
-
if (mismatchFields.length > 0) {
|
|
9404
|
-
const reason = `Dual receipt mismatch on fields: ${mismatchFields.join(", ")}`;
|
|
9405
|
-
writePrdRunRecord(repoRoot2, {
|
|
9406
|
-
...record,
|
|
9407
|
-
prdRef,
|
|
9408
|
-
status: "blocked",
|
|
9409
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9410
|
-
blockedGate: "final-review",
|
|
9411
|
-
blockedReason: reason,
|
|
9412
|
-
diagnostics: [reason],
|
|
9413
|
-
targetName,
|
|
9414
|
-
finalReview: stateFr,
|
|
9415
|
-
offendingPaths: []
|
|
9416
|
-
});
|
|
9417
|
-
return {
|
|
9418
|
-
prdRef,
|
|
9419
|
-
status: "blocked",
|
|
9420
|
-
blockedGate: "final-review",
|
|
9421
|
-
blockedReason: reason,
|
|
9422
|
-
diagnostics: [reason],
|
|
9423
|
-
offendingPaths: []
|
|
9424
|
-
};
|
|
9425
|
-
}
|
|
9426
|
-
return null;
|
|
9427
|
-
}
|
|
9428
|
-
async function runPrdRunFinalReviewCommand(options) {
|
|
9429
|
-
const prdRef = normalizePrdRunRef(options.prdRef);
|
|
9430
|
-
const targetName = options.targetName.trim();
|
|
9431
|
-
if (!targetName) {
|
|
9432
|
-
throw new Error(
|
|
9433
|
-
`Invalid target name "${options.targetName}". Expected a non-empty target name.`
|
|
9434
|
-
);
|
|
9435
|
-
}
|
|
9436
|
-
const { record, diagnostics: readDiagnostics } = readPrdRun(
|
|
9437
|
-
options.repoRoot,
|
|
9438
|
-
prdRef
|
|
9439
|
-
);
|
|
9440
|
-
if (readDiagnostics.length > 0 && !record) {
|
|
9441
|
-
const reason = `PRD Run ${prdRef} has a malformed record and cannot proceed to Final Review.`;
|
|
9442
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9443
|
-
prdRef,
|
|
9444
|
-
status: "blocked",
|
|
9445
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9446
|
-
blockedGate: "final-review",
|
|
9447
|
-
blockedReason: reason,
|
|
9448
|
-
diagnostics: readDiagnostics,
|
|
9449
|
-
targetName,
|
|
9450
|
-
offendingPaths: []
|
|
9451
|
-
});
|
|
9452
|
-
return {
|
|
9453
|
-
prdRef,
|
|
9454
|
-
status: "blocked",
|
|
9455
|
-
blockedGate: "final-review",
|
|
9456
|
-
blockedReason: reason,
|
|
9457
|
-
diagnostics: readDiagnostics,
|
|
9458
|
-
offendingPaths: []
|
|
9459
|
-
};
|
|
9460
|
-
}
|
|
9461
|
-
if (!record) {
|
|
9462
|
-
return {
|
|
9463
|
-
prdRef,
|
|
9464
|
-
status: "blocked",
|
|
9465
|
-
blockedGate: "final-review",
|
|
9466
|
-
blockedReason: `PRD Run ${prdRef} does not exist. Run prd-run prepare/start/launch first.`,
|
|
9467
|
-
diagnostics: [`No PRD Run record found for ${prdRef}`],
|
|
9468
|
-
offendingPaths: []
|
|
9469
|
-
};
|
|
9470
|
-
}
|
|
9471
|
-
if (record.status !== "drained" && !canRetryFinalReviewBlock(record)) {
|
|
9472
|
-
const reason = `PRD Run ${prdRef} is not drained. Final Review requires status "drained", got "${record.status}".`;
|
|
9473
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9474
|
-
...record,
|
|
9475
|
-
status: "blocked",
|
|
9476
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9477
|
-
blockedGate: "final-review",
|
|
9478
|
-
blockedReason: reason,
|
|
9479
|
-
diagnostics: [`Current status: ${record.status}`],
|
|
9480
|
-
targetName,
|
|
9481
|
-
offendingPaths: []
|
|
9482
|
-
});
|
|
9483
|
-
return {
|
|
9484
|
-
prdRef,
|
|
9485
|
-
status: "blocked",
|
|
9486
|
-
blockedGate: "final-review",
|
|
9487
|
-
blockedReason: reason,
|
|
9488
|
-
diagnostics: [`Current status: ${record.status}`],
|
|
9489
|
-
offendingPaths: []
|
|
9490
|
-
};
|
|
9491
|
-
}
|
|
9492
|
-
if (record.mode === "local") {
|
|
9493
|
-
const runnableIssues = await getRunnableLocalIssues(
|
|
9494
|
-
prdRef,
|
|
9495
|
-
options.repoRoot
|
|
9496
|
-
);
|
|
9497
|
-
if (runnableIssues.length > 0) {
|
|
9498
|
-
const reason = `PRD Run ${prdRef} is marked "drained" but ${runnableIssues.length} runnable local Issues remain. Queue drain receipt may be premature.`;
|
|
9499
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9500
|
-
...record,
|
|
9501
|
-
status: "blocked",
|
|
9502
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9503
|
-
blockedGate: "final-review",
|
|
9504
|
-
blockedReason: reason,
|
|
9505
|
-
diagnostics: [
|
|
9506
|
-
`Current status: ${record.status}`,
|
|
9507
|
-
`Runnable issues remaining: ${runnableIssues.length}`
|
|
9508
|
-
],
|
|
9509
|
-
targetName,
|
|
9510
|
-
offendingPaths: []
|
|
9511
|
-
});
|
|
9512
|
-
return {
|
|
9513
|
-
prdRef,
|
|
9514
|
-
status: "blocked",
|
|
9515
|
-
blockedGate: "final-review",
|
|
9516
|
-
blockedReason: reason,
|
|
9517
|
-
diagnostics: [
|
|
9518
|
-
`Current status: ${record.status}`,
|
|
9519
|
-
`Runnable issues remaining: ${runnableIssues.length}`
|
|
9520
|
-
],
|
|
9521
|
-
offendingPaths: []
|
|
9522
|
-
};
|
|
9523
|
-
}
|
|
9524
|
-
}
|
|
9525
|
-
if (record.mode !== "local" && !options.issueProvider) {
|
|
9526
|
-
const reason = "Missing IssueProvider. Cannot validate child completeness.";
|
|
9527
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9528
|
-
...record,
|
|
9529
|
-
status: "blocked",
|
|
9530
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9531
|
-
blockedGate: "final-review",
|
|
9532
|
-
blockedReason: reason,
|
|
9533
|
-
diagnostics: ["IssueProvider not provided."],
|
|
9534
|
-
targetName,
|
|
9535
|
-
offendingPaths: []
|
|
9536
|
-
});
|
|
9537
|
-
return {
|
|
9538
|
-
prdRef,
|
|
9539
|
-
status: "blocked",
|
|
9540
|
-
blockedGate: "final-review",
|
|
9541
|
-
blockedReason: reason,
|
|
9542
|
-
diagnostics: ["IssueProvider not provided."],
|
|
9543
|
-
offendingPaths: []
|
|
9544
|
-
};
|
|
9545
|
-
}
|
|
9546
|
-
const completenessResult = record.mode === "local" ? {
|
|
9547
|
-
ok: true,
|
|
9548
|
-
diagnostics: []
|
|
9549
|
-
} : await validateFinalReviewChildCompleteness(
|
|
9550
|
-
prdRef,
|
|
9551
|
-
record,
|
|
9552
|
-
options.issueProvider
|
|
9553
|
-
);
|
|
9554
|
-
if (!completenessResult.ok) {
|
|
9555
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9556
|
-
...record,
|
|
9557
|
-
status: "blocked",
|
|
9558
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9559
|
-
blockedGate: "final-review",
|
|
9560
|
-
blockedReason: completenessResult.reason,
|
|
9561
|
-
diagnostics: completenessResult.diagnostics,
|
|
9562
|
-
targetName,
|
|
9563
|
-
offendingPaths: completenessResult.offendingPaths
|
|
9564
|
-
});
|
|
9565
|
-
return {
|
|
9566
|
-
prdRef,
|
|
9567
|
-
status: "blocked",
|
|
9568
|
-
blockedGate: "final-review",
|
|
9569
|
-
blockedReason: completenessResult.reason,
|
|
9570
|
-
diagnostics: completenessResult.diagnostics,
|
|
9571
|
-
offendingPaths: completenessResult.offendingPaths
|
|
9572
|
-
};
|
|
9573
|
-
}
|
|
9574
|
-
const targetConfig = options.config ? resolveTarget(options.config, targetName) : null;
|
|
9575
|
-
if (!options.executionProvider || !options.config || !options.logger) {
|
|
9576
|
-
const reason = targetConfig?.strategy.prdRun?.finalReview ? `Missing ExecutionProvider, config, or logger for Final Review.` : "Final Review requires config with prdRun.finalReview, ExecutionProvider, and logger.";
|
|
9577
|
-
const diagnostics = targetConfig?.strategy.prdRun?.finalReview ? ["ExecutionProvider, config, or logger not provided."] : [
|
|
9578
|
-
"No prdRun.finalReview configured in target strategy or missing execution provider."
|
|
9579
|
-
];
|
|
9580
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9581
|
-
...record,
|
|
9582
|
-
status: "blocked",
|
|
9583
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9584
|
-
blockedGate: "final-review",
|
|
9585
|
-
blockedReason: reason,
|
|
9586
|
-
diagnostics,
|
|
9587
|
-
targetName,
|
|
9588
|
-
finalReview: {
|
|
9589
|
-
status: "blocked",
|
|
9590
|
-
targetName,
|
|
9591
|
-
prdBranch: record.prdBranch ?? prdRef,
|
|
9592
|
-
diagnostics,
|
|
9593
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9594
|
-
},
|
|
9595
|
-
offendingPaths: []
|
|
9596
|
-
});
|
|
9597
|
-
return {
|
|
9598
|
-
prdRef,
|
|
9599
|
-
status: "blocked",
|
|
9600
|
-
blockedGate: "final-review",
|
|
9601
|
-
blockedReason: reason,
|
|
9602
|
-
diagnostics,
|
|
9603
|
-
offendingPaths: []
|
|
9604
|
-
};
|
|
9605
|
-
}
|
|
9606
|
-
const finalReviewConfig = targetConfig.strategy.prdRun?.finalReview;
|
|
9607
|
-
if (!finalReviewConfig) {
|
|
9608
|
-
const reason = `Target "${targetName}" does not have prdRun.finalReview configured.`;
|
|
9609
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9610
|
-
...record,
|
|
9611
|
-
status: "blocked",
|
|
9612
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9613
|
-
blockedGate: "final-review",
|
|
9614
|
-
blockedReason: reason,
|
|
9615
|
-
diagnostics: [reason],
|
|
9616
|
-
targetName,
|
|
9617
|
-
finalReview: {
|
|
9618
|
-
status: "blocked",
|
|
9619
|
-
targetName,
|
|
9620
|
-
prdBranch: record.prdBranch ?? prdRef,
|
|
9621
|
-
diagnostics: [reason],
|
|
9622
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9623
|
-
},
|
|
9624
|
-
offendingPaths: []
|
|
9625
|
-
});
|
|
9626
|
-
return {
|
|
9627
|
-
prdRef,
|
|
9628
|
-
status: "blocked",
|
|
9629
|
-
blockedGate: "final-review",
|
|
9630
|
-
blockedReason: reason,
|
|
9631
|
-
diagnostics: [reason],
|
|
9632
|
-
offendingPaths: []
|
|
9633
|
-
};
|
|
9634
|
-
}
|
|
9635
|
-
const isLocalMode = record.mode === "local";
|
|
9636
|
-
let mergeBaseResult;
|
|
9637
|
-
let prdBranch;
|
|
9638
|
-
if (isLocalMode) {
|
|
9639
|
-
prdBranch = getLocalPrdBranchName(prdRef);
|
|
9640
|
-
mergeBaseResult = computeLocalFinalReviewMergeBase(
|
|
9641
|
-
options.repoRoot,
|
|
9642
|
-
prdBranch
|
|
9643
|
-
);
|
|
9644
|
-
} else {
|
|
9645
|
-
prdBranch = record.prdBranch ?? prdRef;
|
|
9646
|
-
const startBaseBranch = record.start?.startBaseBranch;
|
|
9647
|
-
if (!startBaseBranch) {
|
|
9648
|
-
const reason = `PRD Run ${prdRef} has a start receipt without startBaseBranch. This record predates target-owned PRD Run base branches and cannot proceed to Final Review.`;
|
|
9649
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9650
|
-
...record,
|
|
9651
|
-
status: "blocked",
|
|
9652
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9653
|
-
blockedGate: "final-review",
|
|
9654
|
-
blockedReason: reason,
|
|
9655
|
-
diagnostics: [reason],
|
|
9656
|
-
targetName,
|
|
9657
|
-
offendingPaths: []
|
|
9658
|
-
});
|
|
9659
|
-
return {
|
|
9660
|
-
prdRef,
|
|
9661
|
-
status: "blocked",
|
|
9662
|
-
blockedGate: "final-review",
|
|
9663
|
-
blockedReason: reason,
|
|
9664
|
-
diagnostics: [reason],
|
|
9665
|
-
offendingPaths: []
|
|
9666
|
-
};
|
|
9667
|
-
}
|
|
9668
|
-
mergeBaseResult = computeFinalReviewMergeBase(
|
|
9669
|
-
options.repoRoot,
|
|
9670
|
-
prdRef,
|
|
9671
|
-
startBaseBranch
|
|
9672
|
-
);
|
|
9673
|
-
}
|
|
9674
|
-
if (!mergeBaseResult.ok) {
|
|
9675
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9676
|
-
...record,
|
|
9677
|
-
status: "blocked",
|
|
9678
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9679
|
-
blockedGate: "final-review",
|
|
9680
|
-
blockedReason: mergeBaseResult.reason,
|
|
9681
|
-
diagnostics: mergeBaseResult.diagnostics,
|
|
9682
|
-
targetName,
|
|
9683
|
-
finalReview: {
|
|
9684
|
-
status: "blocked",
|
|
9685
|
-
targetName,
|
|
9686
|
-
prdBranch,
|
|
9687
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9688
|
-
diagnostics: mergeBaseResult.diagnostics,
|
|
9689
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9690
|
-
},
|
|
9691
|
-
offendingPaths: []
|
|
9692
|
-
});
|
|
9693
|
-
return {
|
|
9694
|
-
prdRef,
|
|
9695
|
-
status: "blocked",
|
|
9696
|
-
blockedGate: "final-review",
|
|
9697
|
-
blockedReason: mergeBaseResult.reason,
|
|
9698
|
-
diagnostics: mergeBaseResult.diagnostics,
|
|
9699
|
-
offendingPaths: []
|
|
9700
|
-
};
|
|
9701
|
-
}
|
|
9702
|
-
let relatedIssues = [];
|
|
9703
|
-
if (!isLocalMode && options.issueProvider) {
|
|
9704
|
-
try {
|
|
9705
|
-
const listed = await options.issueProvider.listRelatedIssues(prdRef);
|
|
9706
|
-
relatedIssues = Array.isArray(listed) ? listed : [];
|
|
9707
|
-
} catch {
|
|
9708
|
-
relatedIssues = [];
|
|
9709
|
-
}
|
|
9710
|
-
}
|
|
9711
|
-
const evidencePacket = buildEvidencePacket({
|
|
9712
|
-
prdRef,
|
|
9713
|
-
prdBranch,
|
|
9714
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9715
|
-
planningManifestPath: `runtime:${prdRef}`,
|
|
9716
|
-
planningManifestFacts: {
|
|
9717
|
-
parentPrdIssueUrl: prdRef,
|
|
9718
|
-
childIssueCount: relatedIssues.length
|
|
9719
|
-
},
|
|
9720
|
-
stage: "prdFinalReview",
|
|
9721
|
-
stageReceipts: {
|
|
9722
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9723
|
-
startBaseBranch: record.start?.startBaseBranch
|
|
9724
|
-
}
|
|
9725
|
-
});
|
|
9726
|
-
const startReceipt = {
|
|
9727
|
-
status: "started",
|
|
9728
|
-
targetName,
|
|
9729
|
-
prdBranch,
|
|
9730
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9731
|
-
diagnostics: [],
|
|
9732
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9733
|
-
};
|
|
9734
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9735
|
-
...record,
|
|
9736
|
-
status: "running",
|
|
9737
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9738
|
-
targetName,
|
|
9739
|
-
start: record.start,
|
|
9740
|
-
finalReview: startReceipt
|
|
9741
|
-
});
|
|
9742
|
-
const finalReviewBranchName = isLocalMode ? `${prdBranch.replace(/\//g, "-")}-final-review-retouch` : buildFinalReviewBranchName(prdBranch);
|
|
9743
|
-
const worktreeResult = ensureFinalReviewWorktree({
|
|
9744
|
-
repoRoot: options.repoRoot,
|
|
9745
|
-
branchName: finalReviewBranchName,
|
|
9746
|
-
checkoutBase: prdBranch,
|
|
9747
|
-
isLocal: isLocalMode
|
|
9748
|
-
});
|
|
9749
|
-
if (!worktreeResult.ok) {
|
|
9750
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9751
|
-
...record,
|
|
9752
|
-
status: "blocked",
|
|
9753
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9754
|
-
blockedGate: "final-review",
|
|
9755
|
-
blockedReason: worktreeResult.reason,
|
|
9756
|
-
diagnostics: worktreeResult.diagnostics,
|
|
9757
|
-
targetName,
|
|
9758
|
-
start: record.start,
|
|
9759
|
-
finalReview: {
|
|
9760
|
-
...startReceipt,
|
|
9761
|
-
status: "blocked",
|
|
9762
|
-
diagnostics: worktreeResult.diagnostics
|
|
9763
|
-
}
|
|
9764
|
-
});
|
|
9765
|
-
return {
|
|
9766
|
-
prdRef,
|
|
9767
|
-
status: "blocked",
|
|
9768
|
-
blockedGate: "final-review",
|
|
9769
|
-
blockedReason: worktreeResult.reason,
|
|
9770
|
-
diagnostics: worktreeResult.diagnostics,
|
|
9771
|
-
offendingPaths: []
|
|
9772
|
-
};
|
|
9773
|
-
}
|
|
9774
|
-
const retryResult = await executeWithMissingOrEmptyArtifactRetry({
|
|
9775
|
-
executionProvider: options.executionProvider,
|
|
9776
|
-
missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(finalReviewConfig),
|
|
9777
|
-
executionOptions: {
|
|
9778
|
-
stage: "prdFinalReview",
|
|
9779
|
-
agent: finalReviewConfig.agent,
|
|
9780
|
-
model: finalReviewConfig.model,
|
|
9781
|
-
variant: finalReviewConfig.variant,
|
|
9782
|
-
env: finalReviewConfig.env,
|
|
9783
|
-
prompt: buildFinalReviewPrompt({
|
|
9784
|
-
repoRoot: options.repoRoot,
|
|
9785
|
-
promptTemplate: finalReviewConfig.promptTemplate,
|
|
9786
|
-
targetName,
|
|
9787
|
-
evidencePacket
|
|
9788
|
-
}),
|
|
9789
|
-
target: targetConfig,
|
|
9790
|
-
repoRoot: options.repoRoot,
|
|
9791
|
-
branchName: finalReviewBranchName,
|
|
9792
|
-
baseRef: prdBranch,
|
|
9793
|
-
checkoutBase: prdBranch,
|
|
9794
|
-
reviewBase: mergeBaseResult.mergeBase,
|
|
9795
|
-
worktreePath: worktreeResult.worktreePath,
|
|
9796
|
-
sandbox: options.config.sandbox,
|
|
9797
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
9798
|
-
logger: options.logger
|
|
9799
|
-
}
|
|
9800
|
-
});
|
|
9801
|
-
const executionResult = retryResult.executionResult;
|
|
9802
|
-
if (!executionResult.success) {
|
|
9803
|
-
const reason = executionResult.error ?? "Final Review execution failed.";
|
|
9804
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9805
|
-
...record,
|
|
9806
|
-
status: "blocked",
|
|
9807
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9808
|
-
blockedGate: "final-review",
|
|
9809
|
-
blockedReason: reason,
|
|
9810
|
-
diagnostics: [reason],
|
|
9811
|
-
targetName,
|
|
9812
|
-
finalReview: {
|
|
9813
|
-
status: "blocked",
|
|
9814
|
-
targetName,
|
|
9815
|
-
prdBranch,
|
|
9816
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9817
|
-
diagnostics: [reason],
|
|
9818
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9819
|
-
},
|
|
9820
|
-
offendingPaths: []
|
|
9821
|
-
});
|
|
9822
|
-
return {
|
|
9823
|
-
prdRef,
|
|
9824
|
-
status: "blocked",
|
|
9825
|
-
blockedGate: "final-review",
|
|
9826
|
-
blockedReason: reason,
|
|
9827
|
-
diagnostics: [reason],
|
|
9828
|
-
offendingPaths: []
|
|
9829
|
-
};
|
|
9830
|
-
}
|
|
9831
|
-
if (retryResult.artifact._tag !== "content") {
|
|
9832
|
-
const reason = retryResult.artifact._tag === "empty" ? "Final Review artifact is empty." : "Final Review artifact was not produced.";
|
|
9833
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9834
|
-
...record,
|
|
9835
|
-
status: "blocked",
|
|
9836
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9837
|
-
blockedGate: "final-review",
|
|
9838
|
-
blockedReason: reason,
|
|
9839
|
-
diagnostics: [reason],
|
|
9840
|
-
targetName,
|
|
9841
|
-
finalReview: {
|
|
9842
|
-
status: "blocked",
|
|
9843
|
-
targetName,
|
|
9844
|
-
prdBranch,
|
|
9845
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9846
|
-
diagnostics: [reason],
|
|
9847
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9848
|
-
},
|
|
9849
|
-
offendingPaths: []
|
|
9850
|
-
});
|
|
9851
|
-
return {
|
|
9852
|
-
prdRef,
|
|
9853
|
-
status: "blocked",
|
|
9854
|
-
blockedGate: "final-review",
|
|
9855
|
-
blockedReason: reason,
|
|
9856
|
-
diagnostics: [reason],
|
|
9857
|
-
offendingPaths: []
|
|
9858
|
-
};
|
|
9859
|
-
}
|
|
9860
|
-
const resolvedWorktreePath = executionResult.worktreePath;
|
|
9861
|
-
const artifactPath = join18(
|
|
9862
|
-
resolvedWorktreePath,
|
|
9863
|
-
".pourkit/final-review-artifact.json"
|
|
9864
|
-
);
|
|
9865
|
-
const artifactResult = parseFinalReviewArtifact(artifactPath);
|
|
9866
|
-
if (!artifactResult.ok) {
|
|
9867
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9868
|
-
...record,
|
|
9869
|
-
status: "blocked",
|
|
9870
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9871
|
-
blockedGate: "final-review",
|
|
9872
|
-
blockedReason: artifactResult.reason,
|
|
9873
|
-
diagnostics: artifactResult.diagnostics,
|
|
9874
|
-
targetName,
|
|
9875
|
-
finalReview: {
|
|
9876
|
-
status: "blocked",
|
|
9877
|
-
targetName,
|
|
9878
|
-
prdBranch,
|
|
9879
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9880
|
-
diagnostics: artifactResult.diagnostics,
|
|
9881
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9882
|
-
},
|
|
9883
|
-
offendingPaths: []
|
|
9884
|
-
});
|
|
9885
|
-
return {
|
|
9886
|
-
prdRef,
|
|
9887
|
-
status: "blocked",
|
|
9888
|
-
blockedGate: "final-review",
|
|
9889
|
-
blockedReason: artifactResult.reason,
|
|
9890
|
-
diagnostics: artifactResult.diagnostics,
|
|
9891
|
-
offendingPaths: []
|
|
9892
|
-
};
|
|
9893
|
-
}
|
|
9894
|
-
const semanticResult = validateFinalReviewArtifactSemanticIds(
|
|
9895
|
-
artifactResult,
|
|
9896
|
-
{ prdRef, prdBranch, mergeBase: mergeBaseResult.mergeBase }
|
|
9897
|
-
);
|
|
9898
|
-
if (!semanticResult.ok) {
|
|
9899
|
-
const diagnostics = semanticResult.errors.map(redactSensitiveValues);
|
|
9900
|
-
const reason = "Final Review artifact has mismatched semantic IDs.";
|
|
9901
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9902
|
-
...record,
|
|
9903
|
-
status: "blocked",
|
|
9904
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9905
|
-
blockedGate: "final-review",
|
|
9906
|
-
blockedReason: reason,
|
|
9907
|
-
diagnostics,
|
|
9908
|
-
targetName,
|
|
9909
|
-
finalReview: {
|
|
9910
|
-
status: "blocked",
|
|
9911
|
-
targetName,
|
|
9912
|
-
prdBranch,
|
|
9913
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9914
|
-
diagnostics,
|
|
9915
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9916
|
-
},
|
|
9917
|
-
offendingPaths: []
|
|
9918
|
-
});
|
|
9919
|
-
return {
|
|
9920
|
-
prdRef,
|
|
9921
|
-
status: "blocked",
|
|
9922
|
-
blockedGate: "final-review",
|
|
9923
|
-
blockedReason: reason,
|
|
9924
|
-
diagnostics,
|
|
9925
|
-
offendingPaths: []
|
|
9926
|
-
};
|
|
9927
|
-
}
|
|
9928
|
-
const { verdict, summary, diagnostics: artifactDiagnostics } = artifactResult;
|
|
9929
|
-
if (verdict === "pass_no_changes") {
|
|
9930
|
-
const reviewedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9931
|
-
const receipt = {
|
|
9932
|
-
status: "succeeded",
|
|
9933
|
-
targetName,
|
|
9934
|
-
prdBranch,
|
|
9935
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9936
|
-
verdict: "pass_no_changes",
|
|
9937
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
9938
|
-
diagnostics: [],
|
|
9939
|
-
reviewedAt
|
|
9940
|
-
};
|
|
9941
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9942
|
-
...record,
|
|
9943
|
-
status: "final_reviewed",
|
|
9944
|
-
updatedAt: reviewedAt,
|
|
9945
|
-
targetName,
|
|
9946
|
-
start: record.start,
|
|
9947
|
-
finalReview: receipt
|
|
9948
|
-
});
|
|
9949
|
-
const blocked = await writeAndVerifyLocalDualReceipt(
|
|
9950
|
-
options.repoRoot,
|
|
9951
|
-
prdRef,
|
|
9952
|
-
record,
|
|
9953
|
-
targetName,
|
|
9954
|
-
prdBranch,
|
|
9955
|
-
mergeBaseResult.mergeBase,
|
|
9956
|
-
"pass_no_changes",
|
|
9957
|
-
reviewedAt,
|
|
9958
|
-
isLocalMode
|
|
9959
|
-
);
|
|
9960
|
-
if (blocked) return blocked;
|
|
9961
|
-
return {
|
|
9962
|
-
prdRef,
|
|
9963
|
-
status: "final_reviewed",
|
|
9964
|
-
finalReview: receipt,
|
|
9965
|
-
diagnostics: []
|
|
9966
|
-
};
|
|
9967
|
-
}
|
|
9968
|
-
if (verdict === "pass_with_retouch") {
|
|
9969
|
-
if (!options.prProvider && !isLocalMode) {
|
|
9970
|
-
const reason = "Missing PRProvider. Cannot create retouch PR without a PR provider.";
|
|
9971
|
-
writePrdRunRecord(options.repoRoot, {
|
|
9972
|
-
...record,
|
|
9973
|
-
status: "blocked",
|
|
9974
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9975
|
-
blockedGate: "final-review",
|
|
9976
|
-
blockedReason: reason,
|
|
9977
|
-
diagnostics: [reason],
|
|
9978
|
-
targetName,
|
|
9979
|
-
finalReview: {
|
|
9980
|
-
status: "blocked",
|
|
9981
|
-
targetName,
|
|
9982
|
-
prdBranch,
|
|
9983
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
9984
|
-
verdict: "pass_with_retouch",
|
|
9985
|
-
diagnostics: [reason],
|
|
9986
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9987
|
-
},
|
|
9988
|
-
offendingPaths: []
|
|
9989
|
-
});
|
|
9990
|
-
return {
|
|
9991
|
-
prdRef,
|
|
9992
|
-
status: "blocked",
|
|
9993
|
-
blockedGate: "final-review",
|
|
9994
|
-
blockedReason: reason,
|
|
9995
|
-
diagnostics: [reason],
|
|
9996
|
-
offendingPaths: []
|
|
9997
|
-
};
|
|
9998
|
-
}
|
|
9999
|
-
const autoMerge = options.autoMerge ?? true;
|
|
10000
|
-
const existingRetouchPrNumber = !isLocalMode ? record.finalReview?.retouchPrNumber : void 0;
|
|
10001
|
-
if (existingRetouchPrNumber && autoMerge) {
|
|
10002
|
-
const existingPr = await getPrByNumberIfAvailable(
|
|
10003
|
-
options.prProvider,
|
|
10004
|
-
existingRetouchPrNumber
|
|
10005
|
-
);
|
|
10006
|
-
if (existingPr?.state === "MERGED" && existingPr.mergeCommitSha) {
|
|
10007
|
-
const receipt2 = {
|
|
10008
|
-
status: "final_reviewed",
|
|
10009
|
-
targetName,
|
|
10010
|
-
prdBranch,
|
|
10011
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10012
|
-
verdict: "pass_with_retouch",
|
|
10013
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10014
|
-
diagnostics: [],
|
|
10015
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10016
|
-
retouchPrNumber: existingRetouchPrNumber,
|
|
10017
|
-
retouchPrUrl: existingPr.url,
|
|
10018
|
-
retouchMergeCommit: existingPr.mergeCommitSha,
|
|
10019
|
-
autoMerge,
|
|
10020
|
-
changedPaths: record.finalReview?.changedPaths ?? []
|
|
10021
|
-
};
|
|
10022
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10023
|
-
...record,
|
|
10024
|
-
status: "final_reviewed",
|
|
10025
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10026
|
-
targetName,
|
|
10027
|
-
start: record.start,
|
|
10028
|
-
finalReview: receipt2
|
|
10029
|
-
});
|
|
10030
|
-
return {
|
|
10031
|
-
prdRef,
|
|
10032
|
-
status: "final_reviewed",
|
|
10033
|
-
finalReview: receipt2,
|
|
10034
|
-
diagnostics: []
|
|
10035
|
-
};
|
|
10036
|
-
}
|
|
10037
|
-
}
|
|
10038
|
-
const changedPathsFromArtifact = artifactResult.changedPaths;
|
|
10039
|
-
let resolvedChangedPaths = changedPathsFromArtifact && changedPathsFromArtifact.length > 0 ? changedPathsFromArtifact : listFinalReviewChangedPaths(
|
|
10040
|
-
resolvedWorktreePath,
|
|
10041
|
-
mergeBaseResult.mergeBase
|
|
10042
|
-
);
|
|
10043
|
-
const scopeResult = validateFinalReviewRetouchScope(resolvedChangedPaths);
|
|
10044
|
-
if (!scopeResult.ok) {
|
|
10045
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10046
|
-
...record,
|
|
10047
|
-
status: "blocked",
|
|
10048
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10049
|
-
blockedGate: "final-review",
|
|
10050
|
-
blockedReason: scopeResult.reason,
|
|
10051
|
-
diagnostics: scopeResult.diagnostics,
|
|
10052
|
-
targetName,
|
|
10053
|
-
finalReview: {
|
|
10054
|
-
status: "blocked",
|
|
10055
|
-
targetName,
|
|
10056
|
-
prdBranch,
|
|
10057
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10058
|
-
verdict: "pass_with_retouch",
|
|
10059
|
-
diagnostics: scopeResult.diagnostics,
|
|
10060
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10061
|
-
},
|
|
10062
|
-
offendingPaths: scopeResult.offendingPaths
|
|
10063
|
-
});
|
|
10064
|
-
return {
|
|
10065
|
-
prdRef,
|
|
10066
|
-
status: "blocked",
|
|
10067
|
-
blockedGate: "final-review",
|
|
10068
|
-
blockedReason: scopeResult.reason,
|
|
10069
|
-
diagnostics: scopeResult.diagnostics,
|
|
10070
|
-
offendingPaths: scopeResult.offendingPaths
|
|
10071
|
-
};
|
|
10072
|
-
}
|
|
10073
|
-
const finalizerResult = await runFinalReviewPrFinalizer({
|
|
10074
|
-
executionProvider: options.executionProvider,
|
|
10075
|
-
config: options.config,
|
|
10076
|
-
target: targetConfig,
|
|
10077
|
-
repoRoot: options.repoRoot,
|
|
10078
|
-
worktreePath: resolvedWorktreePath,
|
|
10079
|
-
branchName: finalReviewBranchName,
|
|
10080
|
-
prdRef,
|
|
10081
|
-
prdBranch,
|
|
10082
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10083
|
-
summary,
|
|
10084
|
-
changedPaths: scopeResult.changedPaths,
|
|
10085
|
-
logger: options.logger
|
|
10086
|
-
});
|
|
10087
|
-
if (!finalizerResult.ok) {
|
|
10088
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10089
|
-
...record,
|
|
10090
|
-
status: "blocked",
|
|
10091
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10092
|
-
blockedGate: "final-review",
|
|
10093
|
-
blockedReason: finalizerResult.reason,
|
|
10094
|
-
diagnostics: finalizerResult.diagnostics,
|
|
10095
|
-
targetName,
|
|
10096
|
-
finalReview: {
|
|
10097
|
-
status: "blocked",
|
|
10098
|
-
targetName,
|
|
10099
|
-
prdBranch,
|
|
10100
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10101
|
-
verdict: "pass_with_retouch",
|
|
10102
|
-
diagnostics: finalizerResult.diagnostics,
|
|
10103
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10104
|
-
},
|
|
10105
|
-
offendingPaths: []
|
|
10106
|
-
});
|
|
10107
|
-
return {
|
|
10108
|
-
prdRef,
|
|
10109
|
-
status: "blocked",
|
|
10110
|
-
blockedGate: "final-review",
|
|
10111
|
-
blockedReason: finalizerResult.reason,
|
|
10112
|
-
diagnostics: finalizerResult.diagnostics,
|
|
10113
|
-
offendingPaths: []
|
|
10114
|
-
};
|
|
10115
|
-
}
|
|
10116
|
-
if (isLocalMode) {
|
|
10117
|
-
let localResult;
|
|
10118
|
-
try {
|
|
10119
|
-
const result = await squashFinalReviewRetouch(
|
|
10120
|
-
prdRef,
|
|
10121
|
-
options.repoRoot,
|
|
10122
|
-
finalizerResult.title,
|
|
10123
|
-
finalizerResult.body
|
|
10124
|
-
);
|
|
10125
|
-
if (!result) {
|
|
10126
|
-
const reason = "Retouch branch missing or empty. Cannot squash-merge.";
|
|
10127
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10128
|
-
...record,
|
|
10129
|
-
status: "blocked",
|
|
10130
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10131
|
-
blockedGate: "final-review",
|
|
10132
|
-
blockedReason: reason,
|
|
10133
|
-
diagnostics: [reason],
|
|
10134
|
-
targetName,
|
|
10135
|
-
finalReview: {
|
|
10136
|
-
status: "blocked",
|
|
10137
|
-
targetName,
|
|
10138
|
-
prdBranch,
|
|
10139
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10140
|
-
verdict: "pass_with_retouch",
|
|
10141
|
-
diagnostics: [reason],
|
|
10142
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10143
|
-
},
|
|
10144
|
-
offendingPaths: []
|
|
10145
|
-
});
|
|
10146
|
-
return {
|
|
10147
|
-
prdRef,
|
|
10148
|
-
status: "blocked",
|
|
10149
|
-
blockedGate: "final-review",
|
|
10150
|
-
blockedReason: reason,
|
|
10151
|
-
diagnostics: [reason],
|
|
10152
|
-
offendingPaths: []
|
|
10153
|
-
};
|
|
10154
|
-
}
|
|
10155
|
-
localResult = result;
|
|
10156
|
-
} catch (error) {
|
|
10157
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
10158
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10159
|
-
...record,
|
|
10160
|
-
status: "blocked",
|
|
10161
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10162
|
-
blockedGate: "final-review",
|
|
10163
|
-
blockedReason: `Local retouch squash-merge failed: ${msg}`,
|
|
10164
|
-
diagnostics: [msg],
|
|
10165
|
-
targetName,
|
|
10166
|
-
finalReview: {
|
|
10167
|
-
status: "blocked",
|
|
10168
|
-
targetName,
|
|
10169
|
-
prdBranch,
|
|
10170
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10171
|
-
verdict: "pass_with_retouch",
|
|
10172
|
-
diagnostics: [msg],
|
|
10173
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10174
|
-
},
|
|
10175
|
-
offendingPaths: []
|
|
10176
|
-
});
|
|
10177
|
-
return {
|
|
10178
|
-
prdRef,
|
|
10179
|
-
status: "blocked",
|
|
10180
|
-
blockedGate: "final-review",
|
|
10181
|
-
blockedReason: `Local retouch squash-merge failed: ${msg}`,
|
|
10182
|
-
diagnostics: [msg],
|
|
10183
|
-
offendingPaths: []
|
|
10184
|
-
};
|
|
10185
|
-
}
|
|
10186
|
-
const receipt2 = {
|
|
10187
|
-
status: "final_reviewed",
|
|
10188
|
-
targetName,
|
|
10189
|
-
prdBranch,
|
|
10190
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10191
|
-
verdict: "pass_with_retouch",
|
|
10192
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10193
|
-
diagnostics: [],
|
|
10194
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10195
|
-
retouchMergeCommit: localResult.mergeCommit,
|
|
10196
|
-
changedPaths: localResult.changedPaths
|
|
10197
|
-
};
|
|
10198
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10199
|
-
...record,
|
|
10200
|
-
status: "final_reviewed",
|
|
10201
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10202
|
-
targetName,
|
|
10203
|
-
start: record.start,
|
|
10204
|
-
finalReview: receipt2
|
|
10205
|
-
});
|
|
10206
|
-
const blocked = await writeAndVerifyLocalDualReceipt(
|
|
10207
|
-
options.repoRoot,
|
|
10208
|
-
prdRef,
|
|
10209
|
-
record,
|
|
10210
|
-
targetName,
|
|
10211
|
-
prdBranch,
|
|
10212
|
-
mergeBaseResult.mergeBase,
|
|
10213
|
-
"pass_with_retouch",
|
|
10214
|
-
(/* @__PURE__ */ new Date()).toISOString(),
|
|
10215
|
-
isLocalMode
|
|
10216
|
-
);
|
|
10217
|
-
if (blocked) return blocked;
|
|
10218
|
-
return {
|
|
10219
|
-
prdRef,
|
|
10220
|
-
status: "final_reviewed",
|
|
10221
|
-
finalReview: receipt2,
|
|
10222
|
-
diagnostics: []
|
|
10223
|
-
};
|
|
10224
|
-
}
|
|
10225
|
-
let retouchPr;
|
|
10226
|
-
try {
|
|
10227
|
-
retouchPr = await createOrReuseFinalReviewRetouchPr({
|
|
10228
|
-
repoRoot: options.repoRoot,
|
|
10229
|
-
sourceWorktreePath: resolvedWorktreePath,
|
|
10230
|
-
prdRef,
|
|
10231
|
-
changedPaths: scopeResult.changedPaths,
|
|
10232
|
-
summary,
|
|
10233
|
-
title: finalizerResult.title,
|
|
10234
|
-
body: finalizerResult.body,
|
|
10235
|
-
prProvider: options.prProvider
|
|
10236
|
-
});
|
|
10237
|
-
} catch (error) {
|
|
10238
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
10239
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10240
|
-
...record,
|
|
10241
|
-
status: "blocked",
|
|
10242
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10243
|
-
blockedGate: "final-review",
|
|
10244
|
-
blockedReason: `Failed to create retouch PR: ${msg}`,
|
|
10245
|
-
diagnostics: [msg],
|
|
10246
|
-
targetName,
|
|
10247
|
-
finalReview: {
|
|
10248
|
-
status: "blocked",
|
|
10249
|
-
targetName,
|
|
10250
|
-
prdBranch,
|
|
10251
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10252
|
-
verdict: "pass_with_retouch",
|
|
10253
|
-
diagnostics: [msg],
|
|
10254
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10255
|
-
},
|
|
10256
|
-
offendingPaths: []
|
|
10257
|
-
});
|
|
10258
|
-
return {
|
|
10259
|
-
prdRef,
|
|
10260
|
-
status: "blocked",
|
|
10261
|
-
blockedGate: "final-review",
|
|
10262
|
-
blockedReason: `Failed to create retouch PR: ${msg}`,
|
|
10263
|
-
diagnostics: [msg],
|
|
10264
|
-
offendingPaths: []
|
|
10265
|
-
};
|
|
10266
|
-
}
|
|
10267
|
-
if (!autoMerge) {
|
|
10268
|
-
const receipt2 = {
|
|
10269
|
-
status: "needs_human_review",
|
|
10270
|
-
targetName,
|
|
10271
|
-
prdBranch,
|
|
10272
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10273
|
-
verdict: "pass_with_retouch",
|
|
10274
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10275
|
-
diagnostics: [],
|
|
10276
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10277
|
-
retouchPrNumber: retouchPr.number,
|
|
10278
|
-
retouchPrUrl: retouchPr.url,
|
|
10279
|
-
autoMerge: false,
|
|
10280
|
-
changedPaths: scopeResult.changedPaths
|
|
10281
|
-
};
|
|
10282
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10283
|
-
...record,
|
|
10284
|
-
status: "blocked",
|
|
10285
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10286
|
-
blockedGate: "final-review",
|
|
10287
|
-
blockedReason: `Retouch PR #${retouchPr.number} created but auto-merge disabled. PRD Final Review will not advance until the PR is merged.`,
|
|
10288
|
-
diagnostics: [
|
|
10289
|
-
`Retouch PR #${retouchPr.number}: ${retouchPr.url}`,
|
|
10290
|
-
"Auto-merge disabled. Merge the PR manually or rerun with auto-merge enabled."
|
|
10291
|
-
],
|
|
10292
|
-
targetName,
|
|
10293
|
-
finalReview: receipt2,
|
|
10294
|
-
offendingPaths: []
|
|
10295
|
-
});
|
|
10296
|
-
return {
|
|
10297
|
-
prdRef,
|
|
10298
|
-
status: "needs_human_review",
|
|
10299
|
-
finalReview: receipt2,
|
|
10300
|
-
diagnostics: [
|
|
10301
|
-
`Retouch PR #${retouchPr.number}: ${retouchPr.url}`,
|
|
10302
|
-
"Auto-merge disabled. Merge the PR manually or rerun with auto-merge enabled."
|
|
10303
|
-
]
|
|
10304
|
-
};
|
|
10305
|
-
}
|
|
10306
|
-
try {
|
|
10307
|
-
await options.prProvider.waitForPrChecks(retouchPr.number);
|
|
10308
|
-
} catch (error) {
|
|
10309
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
10310
|
-
const receipt2 = {
|
|
10311
|
-
status: "started",
|
|
10312
|
-
targetName,
|
|
10313
|
-
prdBranch,
|
|
10314
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10315
|
-
verdict: "pass_with_retouch",
|
|
10316
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10317
|
-
diagnostics: [msg],
|
|
10318
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10319
|
-
retouchPrNumber: retouchPr.number,
|
|
10320
|
-
retouchPrUrl: retouchPr.url,
|
|
10321
|
-
autoMerge: true,
|
|
10322
|
-
changedPaths: scopeResult.changedPaths
|
|
10323
|
-
};
|
|
10324
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10325
|
-
...record,
|
|
10326
|
-
status: "blocked",
|
|
10327
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10328
|
-
blockedGate: "final-review",
|
|
10329
|
-
blockedReason: `Retouch PR #${retouchPr.number} checks failed: ${msg}`,
|
|
10330
|
-
diagnostics: [msg],
|
|
10331
|
-
targetName,
|
|
10332
|
-
finalReview: receipt2,
|
|
10333
|
-
offendingPaths: []
|
|
10334
|
-
});
|
|
10335
|
-
return {
|
|
10336
|
-
prdRef,
|
|
10337
|
-
status: "blocked",
|
|
10338
|
-
blockedGate: "final-review",
|
|
10339
|
-
blockedReason: `Retouch PR #${retouchPr.number} checks failed: ${msg}`,
|
|
10340
|
-
diagnostics: [msg],
|
|
10341
|
-
offendingPaths: []
|
|
10342
|
-
};
|
|
10343
|
-
}
|
|
10344
|
-
try {
|
|
10345
|
-
await options.prProvider.mergePr(retouchPr.number, {
|
|
10346
|
-
method: "squash"
|
|
10347
|
-
});
|
|
10348
|
-
} catch (error) {
|
|
10349
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
10350
|
-
const receipt2 = {
|
|
10351
|
-
status: "started",
|
|
10352
|
-
targetName,
|
|
10353
|
-
prdBranch,
|
|
10354
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10355
|
-
verdict: "pass_with_retouch",
|
|
10356
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10357
|
-
diagnostics: [msg],
|
|
10358
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10359
|
-
retouchPrNumber: retouchPr.number,
|
|
10360
|
-
retouchPrUrl: retouchPr.url,
|
|
10361
|
-
autoMerge: true,
|
|
10362
|
-
changedPaths: scopeResult.changedPaths
|
|
10363
|
-
};
|
|
10364
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10365
|
-
...record,
|
|
10366
|
-
status: "blocked",
|
|
10367
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10368
|
-
blockedGate: "final-review",
|
|
10369
|
-
blockedReason: `Retouch PR #${retouchPr.number} merge failed: ${msg}`,
|
|
10370
|
-
diagnostics: [msg],
|
|
10371
|
-
targetName,
|
|
10372
|
-
finalReview: receipt2,
|
|
10373
|
-
offendingPaths: []
|
|
10374
|
-
});
|
|
10375
|
-
return {
|
|
10376
|
-
prdRef,
|
|
10377
|
-
status: "blocked",
|
|
10378
|
-
blockedGate: "final-review",
|
|
10379
|
-
blockedReason: `Retouch PR #${retouchPr.number} merge failed: ${msg}`,
|
|
10380
|
-
diagnostics: [msg],
|
|
10381
|
-
offendingPaths: []
|
|
10382
|
-
};
|
|
10383
|
-
}
|
|
10384
|
-
let mergedPr = null;
|
|
10385
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
10386
|
-
try {
|
|
10387
|
-
const candidate = await lookupMergedPr(options.prProvider, retouchPr);
|
|
10388
|
-
if (candidate?.state === "MERGED" && candidate.mergeCommitSha) {
|
|
10389
|
-
mergedPr = candidate;
|
|
10390
|
-
break;
|
|
10391
|
-
}
|
|
10392
|
-
} catch {
|
|
10393
|
-
}
|
|
10394
|
-
if (attempt < 2) {
|
|
10395
|
-
await sleep(1e3);
|
|
10396
|
-
}
|
|
10397
|
-
}
|
|
10398
|
-
const mergeCommitSha = mergedPr?.mergeCommitSha;
|
|
10399
|
-
if (!mergeCommitSha) {
|
|
10400
|
-
const receipt2 = {
|
|
10401
|
-
status: "started",
|
|
10402
|
-
targetName,
|
|
10403
|
-
prdBranch,
|
|
10404
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10405
|
-
verdict: "pass_with_retouch",
|
|
10406
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10407
|
-
diagnostics: [
|
|
10408
|
-
`Retouch PR #${retouchPr.number} merged but merge commit SHA not available.`
|
|
10409
|
-
],
|
|
10410
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10411
|
-
retouchPrNumber: retouchPr.number,
|
|
10412
|
-
retouchPrUrl: retouchPr.url,
|
|
10413
|
-
autoMerge: true,
|
|
10414
|
-
changedPaths: scopeResult.changedPaths
|
|
10415
|
-
};
|
|
10416
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10417
|
-
...record,
|
|
10418
|
-
status: "blocked",
|
|
10419
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10420
|
-
blockedGate: "final-review",
|
|
10421
|
-
blockedReason: `Retouch PR #${retouchPr.number} merged without exposing a merge commit SHA.`,
|
|
10422
|
-
diagnostics: [
|
|
10423
|
-
`Retouch PR #${retouchPr.number} merged but merge commit SHA not available.`
|
|
10424
|
-
],
|
|
10425
|
-
targetName,
|
|
10426
|
-
finalReview: receipt2,
|
|
10427
|
-
offendingPaths: []
|
|
10428
|
-
});
|
|
10429
|
-
return {
|
|
10430
|
-
prdRef,
|
|
10431
|
-
status: "blocked",
|
|
10432
|
-
blockedGate: "final-review",
|
|
10433
|
-
blockedReason: `Retouch PR #${retouchPr.number} merged without exposing a merge commit SHA.`,
|
|
10434
|
-
diagnostics: [
|
|
10435
|
-
`Retouch PR #${retouchPr.number} merged but merge commit SHA not available.`
|
|
10436
|
-
],
|
|
10437
|
-
offendingPaths: []
|
|
10438
|
-
};
|
|
10439
|
-
}
|
|
10440
|
-
const receipt = {
|
|
10441
|
-
status: "final_reviewed",
|
|
10442
|
-
targetName,
|
|
10443
|
-
prdBranch,
|
|
10444
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10445
|
-
verdict: "pass_with_retouch",
|
|
10446
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10447
|
-
diagnostics: [],
|
|
10448
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10449
|
-
retouchPrNumber: retouchPr.number,
|
|
10450
|
-
retouchPrUrl: retouchPr.url,
|
|
10451
|
-
retouchMergeCommit: mergeCommitSha,
|
|
10452
|
-
autoMerge: true,
|
|
10453
|
-
changedPaths: scopeResult.changedPaths
|
|
10454
|
-
};
|
|
10455
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10456
|
-
...record,
|
|
10457
|
-
status: "final_reviewed",
|
|
10458
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10459
|
-
targetName,
|
|
10460
|
-
start: record.start,
|
|
10461
|
-
finalReview: receipt
|
|
10462
|
-
});
|
|
10463
|
-
return {
|
|
10464
|
-
prdRef,
|
|
10465
|
-
status: "final_reviewed",
|
|
10466
|
-
finalReview: receipt,
|
|
10467
|
-
diagnostics: []
|
|
10468
|
-
};
|
|
10469
|
-
}
|
|
10470
|
-
if (verdict === "needs_human_review") {
|
|
10471
|
-
const reason = summary || "Final Review requires human review.";
|
|
10472
|
-
const receipt = {
|
|
10473
|
-
status: "needs_human_review",
|
|
10474
|
-
targetName,
|
|
10475
|
-
prdBranch,
|
|
10476
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10477
|
-
verdict: "needs_human_review",
|
|
10478
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10479
|
-
diagnostics: artifactDiagnostics.length > 0 ? artifactDiagnostics : [reason],
|
|
10480
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10481
|
-
};
|
|
10482
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10483
|
-
...record,
|
|
10484
|
-
status: "blocked",
|
|
10485
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10486
|
-
blockedGate: "final-review",
|
|
10487
|
-
blockedReason: reason,
|
|
10488
|
-
diagnostics: [reason],
|
|
10489
|
-
targetName,
|
|
10490
|
-
finalReview: receipt,
|
|
10491
|
-
offendingPaths: []
|
|
10492
|
-
});
|
|
10493
|
-
return {
|
|
10494
|
-
prdRef,
|
|
10495
|
-
status: "needs_human_review",
|
|
10496
|
-
finalReview: receipt,
|
|
10497
|
-
diagnostics: [reason]
|
|
10498
|
-
};
|
|
10499
|
-
}
|
|
10500
|
-
if (verdict === "blocked") {
|
|
10501
|
-
const reason = summary || "Final Review blocked.";
|
|
10502
|
-
const receipt = {
|
|
10503
|
-
status: "blocked",
|
|
10504
|
-
targetName,
|
|
10505
|
-
prdBranch,
|
|
10506
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10507
|
-
verdict: "blocked",
|
|
10508
|
-
artifactPath: ".pourkit/final-review-artifact.json",
|
|
10509
|
-
diagnostics: artifactDiagnostics.length > 0 ? artifactDiagnostics : [reason],
|
|
10510
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10511
|
-
};
|
|
10512
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10513
|
-
...record,
|
|
10514
|
-
status: "blocked",
|
|
10515
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10516
|
-
blockedGate: "final-review",
|
|
10517
|
-
blockedReason: reason,
|
|
10518
|
-
diagnostics: [reason],
|
|
10519
|
-
targetName,
|
|
10520
|
-
finalReview: receipt,
|
|
10521
|
-
offendingPaths: []
|
|
10522
|
-
});
|
|
10523
|
-
return {
|
|
10524
|
-
prdRef,
|
|
10525
|
-
status: "blocked",
|
|
10526
|
-
blockedGate: "final-review",
|
|
10527
|
-
blockedReason: reason,
|
|
10528
|
-
diagnostics: [reason],
|
|
10529
|
-
offendingPaths: []
|
|
10530
|
-
};
|
|
10531
|
-
}
|
|
10532
|
-
const unsupportedReason = `Final Review returned unsupported verdict "${verdict}".`;
|
|
10533
|
-
writePrdRunRecord(options.repoRoot, {
|
|
10534
|
-
...record,
|
|
10535
|
-
status: "blocked",
|
|
10536
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10537
|
-
blockedGate: "final-review",
|
|
10538
|
-
blockedReason: unsupportedReason,
|
|
10539
|
-
diagnostics: [unsupportedReason],
|
|
10540
|
-
targetName,
|
|
10541
|
-
finalReview: {
|
|
10542
|
-
status: "blocked",
|
|
10543
|
-
targetName,
|
|
10544
|
-
prdBranch,
|
|
10545
|
-
mergeBase: mergeBaseResult.mergeBase,
|
|
10546
|
-
diagnostics: [unsupportedReason],
|
|
10547
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10548
|
-
},
|
|
10549
|
-
offendingPaths: []
|
|
9143
|
+
}
|
|
9144
|
+
logger.raw(` Target: ${runResult.target.name}`);
|
|
9145
|
+
return { selected, runResult };
|
|
10550
9146
|
});
|
|
10551
|
-
return {
|
|
10552
|
-
prdRef,
|
|
10553
|
-
status: "blocked",
|
|
10554
|
-
blockedGate: "final-review",
|
|
10555
|
-
blockedReason: unsupportedReason,
|
|
10556
|
-
diagnostics: [unsupportedReason],
|
|
10557
|
-
offendingPaths: []
|
|
10558
|
-
};
|
|
10559
9147
|
}
|
|
10560
|
-
function
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
9148
|
+
function runQueue(options) {
|
|
9149
|
+
return runOneQueueIssueEffect(options);
|
|
9150
|
+
}
|
|
9151
|
+
function runQueueLoopEffect(options, results) {
|
|
9152
|
+
return Effect8.gen(function* () {
|
|
9153
|
+
yield* reconcileBlockedEffect(options);
|
|
9154
|
+
const outcome = yield* runOneQueueIssueEffect(options);
|
|
9155
|
+
if (outcome.selected === null) {
|
|
9156
|
+
return {
|
|
9157
|
+
drained: true,
|
|
9158
|
+
processedCount: results.length,
|
|
9159
|
+
results,
|
|
9160
|
+
selected: null,
|
|
9161
|
+
reason: "Queue drained.",
|
|
9162
|
+
code: "drained"
|
|
9163
|
+
};
|
|
10568
9164
|
}
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
};
|
|
10582
|
-
}
|
|
10583
|
-
if (fetchResult.stderr?.toString?.()?.trim()) {
|
|
10584
|
-
fetchDiagnostics.push(fetchResult.stderr.toString().trim());
|
|
10585
|
-
}
|
|
10586
|
-
const mergeBaseResult = spawnSync3(
|
|
10587
|
-
"git",
|
|
10588
|
-
["merge-base", `origin/${baseBranch}`, `origin/${normalized}`],
|
|
10589
|
-
{
|
|
10590
|
-
cwd: repoRoot2,
|
|
10591
|
-
encoding: "utf8"
|
|
9165
|
+
const newResults = [...results, outcome];
|
|
9166
|
+
const processedIssue = yield* Effect8.tryPromise({
|
|
9167
|
+
try: () => options.issueProvider.fetchIssue(outcome.selected.number),
|
|
9168
|
+
catch: (e) => {
|
|
9169
|
+
if (e instanceof Error) {
|
|
9170
|
+
return new QueueProviderError(e.message);
|
|
9171
|
+
}
|
|
9172
|
+
throw e;
|
|
9173
|
+
}
|
|
9174
|
+
});
|
|
9175
|
+
if (processedIssue.state === "closed") {
|
|
9176
|
+
yield* reconcileBlockedEffect(options);
|
|
10592
9177
|
}
|
|
9178
|
+
return yield* runQueueLoopEffect(options, newResults);
|
|
9179
|
+
});
|
|
9180
|
+
}
|
|
9181
|
+
function runQueueLoop(options) {
|
|
9182
|
+
return runQueueLoopEffect(options, []);
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9185
|
+
// commands/queue-run.ts
|
|
9186
|
+
async function runQueueCommand(options) {
|
|
9187
|
+
initializeEffectRuntime();
|
|
9188
|
+
const queueOptions = {
|
|
9189
|
+
targetName: options.targetName,
|
|
9190
|
+
config: options.config,
|
|
9191
|
+
issueProvider: options.issueProvider,
|
|
9192
|
+
prProvider: options.prProvider,
|
|
9193
|
+
executionProvider: options.executionProvider,
|
|
9194
|
+
force: options.force,
|
|
9195
|
+
logger: options.logger,
|
|
9196
|
+
repoRoot: options.repoRoot,
|
|
9197
|
+
prdRef: options.prdRef,
|
|
9198
|
+
queueRunContext: options.queueRunContext,
|
|
9199
|
+
prdRunMode: options.queueRunContext?.prdRunMode
|
|
9200
|
+
};
|
|
9201
|
+
if (!options.loop) {
|
|
9202
|
+
return runEffectAndMapExit(runQueue(queueOptions));
|
|
9203
|
+
}
|
|
9204
|
+
return runEffectAndMapExit(runQueueLoop(queueOptions));
|
|
9205
|
+
}
|
|
9206
|
+
|
|
9207
|
+
// commands/prd-run.ts
|
|
9208
|
+
function planLaunchResume(record) {
|
|
9209
|
+
if (!record) return { attempted: [], skipped: [], resumed: [] };
|
|
9210
|
+
return pipe(
|
|
9211
|
+
record,
|
|
9212
|
+
Match.value,
|
|
9213
|
+
Match.when({ status: "waiting_for_integration" }, () => ({
|
|
9214
|
+
attempted: [],
|
|
9215
|
+
skipped: ["start", "queue"],
|
|
9216
|
+
resumed: []
|
|
9217
|
+
})),
|
|
9218
|
+
Match.when({ status: "completed_local_branch" }, () => ({
|
|
9219
|
+
attempted: [],
|
|
9220
|
+
skipped: ["start", "queue"],
|
|
9221
|
+
resumed: []
|
|
9222
|
+
})),
|
|
9223
|
+
Match.when({ status: "complete" }, () => ({
|
|
9224
|
+
attempted: [],
|
|
9225
|
+
skipped: ["start", "queue"],
|
|
9226
|
+
resumed: []
|
|
9227
|
+
})),
|
|
9228
|
+
Match.when({ status: "drained" }, () => ({
|
|
9229
|
+
attempted: [],
|
|
9230
|
+
skipped: ["start", "queue"],
|
|
9231
|
+
resumed: []
|
|
9232
|
+
})),
|
|
9233
|
+
Match.when({ status: "final_reviewed" }, () => ({
|
|
9234
|
+
attempted: [],
|
|
9235
|
+
skipped: ["start", "queue"],
|
|
9236
|
+
resumed: [],
|
|
9237
|
+
blocked: "final_reviewed-incompatible"
|
|
9238
|
+
})),
|
|
9239
|
+
Match.when(
|
|
9240
|
+
(value) => value.status === "blocked" && value.blockedGate === "final-review" && canRetryFinalReviewBlock(value),
|
|
9241
|
+
() => ({
|
|
9242
|
+
attempted: [],
|
|
9243
|
+
skipped: ["start", "queue"],
|
|
9244
|
+
resumed: []
|
|
9245
|
+
})
|
|
9246
|
+
),
|
|
9247
|
+
Match.when(
|
|
9248
|
+
(value) => value.status === "starting" || value.status === "running" || value.status === "blocked" && (value.blockedGate === "queue" || value.blockedGate === "branch-state"),
|
|
9249
|
+
(value) => value.start ? {
|
|
9250
|
+
attempted: [],
|
|
9251
|
+
skipped: [],
|
|
9252
|
+
resumed: ["start"]
|
|
9253
|
+
} : {
|
|
9254
|
+
attempted: [],
|
|
9255
|
+
skipped: ["start", "queue"],
|
|
9256
|
+
resumed: [],
|
|
9257
|
+
blocked: "missing-start-receipt"
|
|
9258
|
+
}
|
|
9259
|
+
),
|
|
9260
|
+
Match.orElse(() => ({
|
|
9261
|
+
attempted: [],
|
|
9262
|
+
skipped: [],
|
|
9263
|
+
resumed: []
|
|
9264
|
+
}))
|
|
10593
9265
|
);
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
9266
|
+
}
|
|
9267
|
+
function validateLocalStartStore(repoRoot2, prdRef) {
|
|
9268
|
+
const localStoreDir = join20(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
|
|
9269
|
+
let localStoreReady = false;
|
|
9270
|
+
if (existsSync16(localStoreDir)) {
|
|
9271
|
+
const localStorePath = join20(localStoreDir, "prd.json");
|
|
9272
|
+
try {
|
|
9273
|
+
const content = JSON.parse(readFileSync17(localStorePath, "utf8"));
|
|
9274
|
+
localStoreReady = content?.id === prdRef && content?.kind === "prd";
|
|
9275
|
+
} catch {
|
|
9276
|
+
localStoreReady = false;
|
|
10601
9277
|
}
|
|
10602
|
-
return {
|
|
10603
|
-
ok: false,
|
|
10604
|
-
gate: "final-review",
|
|
10605
|
-
reason: `Final Review merge-base computation failed for origin/${baseBranch} and origin/${normalized}.`,
|
|
10606
|
-
diagnostics: mergeBaseDiagnostics.length > 0 ? mergeBaseDiagnostics : ["git merge-base returned non-zero exit status"]
|
|
10607
|
-
};
|
|
10608
9278
|
}
|
|
10609
|
-
|
|
10610
|
-
if (!mergeBase) {
|
|
9279
|
+
if (existsSync16(localStoreDir) && !localStoreReady) {
|
|
10611
9280
|
return {
|
|
10612
9281
|
ok: false,
|
|
10613
|
-
gate: "
|
|
10614
|
-
reason: `
|
|
9282
|
+
gate: "branch-state",
|
|
9283
|
+
reason: `Local PRD Run Store not ready for ${prdRef}. Expected valid prd.json at .pourkit/local-prd-runs/${prdRef}/prd.json with matching PRD ID. Ensure Local PRD Run Store is initialized before starting.`,
|
|
10615
9284
|
diagnostics: [
|
|
10616
|
-
`
|
|
10617
|
-
|
|
9285
|
+
`Expected store path: .pourkit/local-prd-runs/${prdRef}/prd.json`,
|
|
9286
|
+
`Expected PRD ID: ${prdRef}`
|
|
9287
|
+
],
|
|
9288
|
+
offendingPaths: []
|
|
10618
9289
|
};
|
|
10619
9290
|
}
|
|
10620
|
-
return { ok: true
|
|
9291
|
+
return { ok: true };
|
|
10621
9292
|
}
|
|
10622
|
-
function
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
gate: "final-review",
|
|
10638
|
-
reason: `Final Review merge-base computation failed for dev and ${prdBranch}.`,
|
|
10639
|
-
diagnostics: diagnostics.length > 0 ? diagnostics : ["git merge-base returned non-zero exit status"]
|
|
10640
|
-
};
|
|
9293
|
+
function buildStartReceipt(options) {
|
|
9294
|
+
return {
|
|
9295
|
+
status: options.status,
|
|
9296
|
+
targetName: options.targetName,
|
|
9297
|
+
prdBranch: options.prdBranch,
|
|
9298
|
+
startBaseBranch: options.startBaseBranch,
|
|
9299
|
+
startBaseCommit: options.startBaseCommit,
|
|
9300
|
+
branchAction: options.branchAction,
|
|
9301
|
+
startedAt: options.startedAt,
|
|
9302
|
+
refreshReceipts: []
|
|
9303
|
+
};
|
|
9304
|
+
}
|
|
9305
|
+
function canRetryFinalReviewBlock(record) {
|
|
9306
|
+
if (record.status !== "blocked" || record.blockedGate !== "final-review") {
|
|
9307
|
+
return false;
|
|
10641
9308
|
}
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
return {
|
|
10645
|
-
ok: false,
|
|
10646
|
-
gate: "final-review",
|
|
10647
|
-
reason: `Final Review merge-base returned empty result for dev and ${prdBranch}.`,
|
|
10648
|
-
diagnostics: [`merge-base stdout was empty for dev..${prdBranch}`]
|
|
10649
|
-
};
|
|
9309
|
+
if (record.start?.queueDrainedAt || record.finalReview) {
|
|
9310
|
+
return true;
|
|
10650
9311
|
}
|
|
10651
|
-
return
|
|
9312
|
+
return Boolean(
|
|
9313
|
+
record.start && !record.blockedReason?.includes("not drained")
|
|
9314
|
+
);
|
|
10652
9315
|
}
|
|
10653
9316
|
async function runPrdRunLaunchCommand(options) {
|
|
10654
9317
|
const prdRef = normalizePrdRunRef(options.prdRef);
|
|
@@ -10670,7 +9333,7 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10670
9333
|
prdRef,
|
|
10671
9334
|
status: "blocked",
|
|
10672
9335
|
attempted: [],
|
|
10673
|
-
skipped: ["start", "queue"
|
|
9336
|
+
skipped: ["start", "queue"],
|
|
10674
9337
|
resumed: [],
|
|
10675
9338
|
diagnostics: [
|
|
10676
9339
|
`Recorded mode: ${existingRecord.record.mode}`,
|
|
@@ -10699,6 +9362,21 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10699
9362
|
offendingPaths: []
|
|
10700
9363
|
};
|
|
10701
9364
|
}
|
|
9365
|
+
if (plan.blocked === "final_reviewed-incompatible") {
|
|
9366
|
+
return {
|
|
9367
|
+
prdRef,
|
|
9368
|
+
status: "blocked",
|
|
9369
|
+
attempted: [],
|
|
9370
|
+
skipped: [],
|
|
9371
|
+
resumed: [],
|
|
9372
|
+
diagnostics: [
|
|
9373
|
+
`PRD Run ${prdRef} has obsolete status "final_reviewed". PRD-wide Final Review has been removed from the launch lifecycle.`
|
|
9374
|
+
],
|
|
9375
|
+
blockedGate: "final-review",
|
|
9376
|
+
blockedReason: `PRD Run ${prdRef} has status "final_reviewed", which is no longer a valid launch status. PRD-wide Final Review has been removed from the launch lifecycle. Update the PRD Run state to "drained" and rerun launch, or start a new PRD Run.`,
|
|
9377
|
+
offendingPaths: []
|
|
9378
|
+
};
|
|
9379
|
+
}
|
|
10702
9380
|
if (existingRecord.record?.status === "waiting_for_integration" || existingRecord.record?.status === "complete" || existingRecord.record?.status === "completed_local_branch") {
|
|
10703
9381
|
return {
|
|
10704
9382
|
prdRef,
|
|
@@ -10727,10 +9405,7 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10727
9405
|
adoptExistingBranch: options.adoptExistingBranch,
|
|
10728
9406
|
baseBranchOverride: options.baseBranchOverride
|
|
10729
9407
|
});
|
|
10730
|
-
const skippedAfterStart = [
|
|
10731
|
-
"queue",
|
|
10732
|
-
"final-review"
|
|
10733
|
-
];
|
|
9408
|
+
const skippedAfterStart = ["queue"];
|
|
10734
9409
|
if (startResult.status === "blocked") {
|
|
10735
9410
|
return {
|
|
10736
9411
|
prdRef,
|
|
@@ -10759,92 +9434,14 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10759
9434
|
diagnostics.push(...startResult.diagnostics);
|
|
10760
9435
|
attempted.push("queue");
|
|
10761
9436
|
}
|
|
10762
|
-
let finalReviewResult;
|
|
10763
|
-
if (!skipped.includes("final-review")) {
|
|
10764
|
-
attempted.push("final-review");
|
|
10765
|
-
if (existingRecord.record?.mode === "local") {
|
|
10766
|
-
const runnableIssues = await getRunnableLocalIssues(
|
|
10767
|
-
prdRef,
|
|
10768
|
-
options.repoRoot
|
|
10769
|
-
);
|
|
10770
|
-
if (runnableIssues.length > 0) {
|
|
10771
|
-
const reason = `Config-local Final Review blocked: Queue not drained (${runnableIssues.length} runnable Issues remain).`;
|
|
10772
|
-
return {
|
|
10773
|
-
prdRef,
|
|
10774
|
-
status: "blocked",
|
|
10775
|
-
attempted,
|
|
10776
|
-
skipped: ["start", "queue"],
|
|
10777
|
-
resumed,
|
|
10778
|
-
diagnostics: [reason],
|
|
10779
|
-
blockedGate: "final-review",
|
|
10780
|
-
blockedReason: reason,
|
|
10781
|
-
offendingPaths: [],
|
|
10782
|
-
start: startResult,
|
|
10783
|
-
finalReview: {
|
|
10784
|
-
prdRef,
|
|
10785
|
-
status: "blocked",
|
|
10786
|
-
blockedGate: "final-review",
|
|
10787
|
-
blockedReason: reason,
|
|
10788
|
-
diagnostics: [reason],
|
|
10789
|
-
offendingPaths: []
|
|
10790
|
-
}
|
|
10791
|
-
};
|
|
10792
|
-
}
|
|
10793
|
-
}
|
|
10794
|
-
finalReviewResult = await runPrdRunFinalReviewCommand({
|
|
10795
|
-
repoRoot: options.repoRoot,
|
|
10796
|
-
prdRef,
|
|
10797
|
-
targetName: options.targetName,
|
|
10798
|
-
autoMerge: options.autoMerge,
|
|
10799
|
-
issueProvider: options.issueProvider,
|
|
10800
|
-
prProvider: options.prProvider,
|
|
10801
|
-
executionProvider: options.executionProvider,
|
|
10802
|
-
config: options.config,
|
|
10803
|
-
logger: options.logger
|
|
10804
|
-
});
|
|
10805
|
-
const skippedAfterFr = [
|
|
10806
|
-
...skipped.includes("start") ? ["queue"] : []
|
|
10807
|
-
];
|
|
10808
|
-
if (finalReviewResult.status === "blocked") {
|
|
10809
|
-
return {
|
|
10810
|
-
prdRef,
|
|
10811
|
-
status: "blocked",
|
|
10812
|
-
attempted,
|
|
10813
|
-
skipped: skippedAfterFr,
|
|
10814
|
-
resumed,
|
|
10815
|
-
diagnostics: finalReviewResult.diagnostics,
|
|
10816
|
-
blockedGate: "final-review",
|
|
10817
|
-
blockedReason: finalReviewResult.blockedReason,
|
|
10818
|
-
offendingPaths: finalReviewResult.offendingPaths,
|
|
10819
|
-
start: startResult,
|
|
10820
|
-
finalReview: finalReviewResult
|
|
10821
|
-
};
|
|
10822
|
-
}
|
|
10823
|
-
if (finalReviewResult.status === "needs_human_review") {
|
|
10824
|
-
return {
|
|
10825
|
-
prdRef,
|
|
10826
|
-
status: "blocked",
|
|
10827
|
-
attempted,
|
|
10828
|
-
skipped: skippedAfterFr,
|
|
10829
|
-
resumed,
|
|
10830
|
-
diagnostics: finalReviewResult.diagnostics,
|
|
10831
|
-
blockedGate: "final-review",
|
|
10832
|
-
blockedReason: finalReviewResult.finalReview.verdict ?? "needs_human_review",
|
|
10833
|
-
offendingPaths: [],
|
|
10834
|
-
start: startResult,
|
|
10835
|
-
finalReview: finalReviewResult
|
|
10836
|
-
};
|
|
10837
|
-
}
|
|
10838
|
-
diagnostics.push(...finalReviewResult.diagnostics);
|
|
10839
|
-
}
|
|
10840
9437
|
const currentRecord = readPrdRun(options.repoRoot, prdRef).record;
|
|
10841
|
-
const localStorePath =
|
|
9438
|
+
const localStorePath = join20(
|
|
10842
9439
|
options.repoRoot,
|
|
10843
9440
|
".pourkit",
|
|
10844
9441
|
"local-prd-runs",
|
|
10845
9442
|
prdRef
|
|
10846
9443
|
);
|
|
10847
|
-
if (
|
|
9444
|
+
if (existsSync16(localStorePath)) {
|
|
10848
9445
|
writePrdRunRecord(options.repoRoot, {
|
|
10849
9446
|
prdRef,
|
|
10850
9447
|
status: "completed_local_branch",
|
|
@@ -10853,7 +9450,6 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10853
9450
|
targetName: currentRecord?.targetName ?? options.targetName,
|
|
10854
9451
|
prdBranch: currentRecord?.prdBranch ?? `local/${prdRef}`,
|
|
10855
9452
|
start: currentRecord?.start,
|
|
10856
|
-
finalReview: currentRecord?.finalReview,
|
|
10857
9453
|
scopeChanges: currentRecord?.scopeChanges
|
|
10858
9454
|
});
|
|
10859
9455
|
return {
|
|
@@ -10866,8 +9462,7 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10866
9462
|
diagnostics: [
|
|
10867
9463
|
`Local PRD Run ${prdRef} completed. Branch: local/${prdRef}.`
|
|
10868
9464
|
],
|
|
10869
|
-
start: startResult
|
|
10870
|
-
finalReview: finalReviewResult
|
|
9465
|
+
start: startResult
|
|
10871
9466
|
};
|
|
10872
9467
|
}
|
|
10873
9468
|
writePrdRunRecord(options.repoRoot, {
|
|
@@ -10877,7 +9472,6 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10877
9472
|
targetName: currentRecord?.targetName ?? options.targetName,
|
|
10878
9473
|
prdBranch: currentRecord?.prdBranch,
|
|
10879
9474
|
start: currentRecord?.start,
|
|
10880
|
-
finalReview: currentRecord?.finalReview,
|
|
10881
9475
|
scopeChanges: currentRecord?.scopeChanges
|
|
10882
9476
|
});
|
|
10883
9477
|
return {
|
|
@@ -10889,8 +9483,7 @@ async function runPrdRunLaunchCommand(options) {
|
|
|
10889
9483
|
diagnostics: [
|
|
10890
9484
|
`Integration Gate is not implemented. PRD Run is waiting for manual integration back to ${currentRecord?.start?.startBaseBranch ?? "(missing startBaseBranch)"}.`
|
|
10891
9485
|
],
|
|
10892
|
-
start: startResult
|
|
10893
|
-
finalReview: finalReviewResult
|
|
9486
|
+
start: startResult
|
|
10894
9487
|
};
|
|
10895
9488
|
}
|
|
10896
9489
|
async function runPrdRunStartCommand(options) {
|
|
@@ -11358,8 +9951,8 @@ async function processStartResult(startResult, options) {
|
|
|
11358
9951
|
start,
|
|
11359
9952
|
mode: modeForRecord
|
|
11360
9953
|
});
|
|
11361
|
-
const localStorePath =
|
|
11362
|
-
if (
|
|
9954
|
+
const localStorePath = join20(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
|
|
9955
|
+
if (existsSync16(localStorePath)) {
|
|
11363
9956
|
const queueResult = await runLocalQueueLoop(
|
|
11364
9957
|
prdRef,
|
|
11365
9958
|
repoRoot2,
|
|
@@ -11787,18 +10380,6 @@ async function ensurePrdBranchPublished(repoRoot2, prdRef, startBaseCommit) {
|
|
|
11787
10380
|
);
|
|
11788
10381
|
}
|
|
11789
10382
|
}
|
|
11790
|
-
function runGitOrThrow(cwd, args, label) {
|
|
11791
|
-
const result = spawnSync3("git", args, { cwd, encoding: "utf8" });
|
|
11792
|
-
if (result.status !== 0) {
|
|
11793
|
-
throw new Error(
|
|
11794
|
-
`Failed to ${label}: ${[
|
|
11795
|
-
result.error instanceof Error ? result.error.message : void 0,
|
|
11796
|
-
result.stderr?.toString?.() ?? String(result.stderr ?? ""),
|
|
11797
|
-
result.stdout?.toString?.() ?? String(result.stdout ?? "")
|
|
11798
|
-
].filter((value) => Boolean(value && value.trim())).join(" ")}`
|
|
11799
|
-
);
|
|
11800
|
-
}
|
|
11801
|
-
}
|
|
11802
10383
|
function runPrdRunStatusCommand(options) {
|
|
11803
10384
|
const prdRef = normalizePrdRunRef(options.prdRef);
|
|
11804
10385
|
const { record, diagnostics } = readPrdRun(options.repoRoot, prdRef);
|
|
@@ -11847,13 +10428,6 @@ function buildBlockedStartResult(prdRef, failure, manifestPath) {
|
|
|
11847
10428
|
offendingPaths: failure.offendingPaths.length > 0 ? failure.offendingPaths : manifestPath ? [manifestPath] : []
|
|
11848
10429
|
};
|
|
11849
10430
|
}
|
|
11850
|
-
function parseGitStatusLine(line) {
|
|
11851
|
-
if (!line.trim()) return null;
|
|
11852
|
-
const status = line.slice(0, 2);
|
|
11853
|
-
const rawPath = line.slice(3).trim();
|
|
11854
|
-
const path9 = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() : rawPath;
|
|
11855
|
-
return { status, path: path9.replace(/^"|"$/g, "") };
|
|
11856
|
-
}
|
|
11857
10431
|
|
|
11858
10432
|
// commands/pr-create.ts
|
|
11859
10433
|
init_common();
|
|
@@ -12256,7 +10830,7 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
|
|
|
12256
10830
|
}
|
|
12257
10831
|
|
|
12258
10832
|
// commands/init.ts
|
|
12259
|
-
import { existsSync as
|
|
10833
|
+
import { existsSync as existsSync17, statSync } from "fs";
|
|
12260
10834
|
import {
|
|
12261
10835
|
copyFile,
|
|
12262
10836
|
mkdir as mkdir5,
|
|
@@ -12759,7 +11333,7 @@ async function computeFileChecksum(filePath) {
|
|
|
12759
11333
|
return createHash3("sha256").update(content).digest("hex");
|
|
12760
11334
|
}
|
|
12761
11335
|
function lockfileExists(root, name) {
|
|
12762
|
-
return
|
|
11336
|
+
return existsSync17(path5.join(root, name));
|
|
12763
11337
|
}
|
|
12764
11338
|
function detectPackageManager(root) {
|
|
12765
11339
|
if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
|
|
@@ -12803,7 +11377,7 @@ async function discoverLocalSource(sourcePath) {
|
|
|
12803
11377
|
async function discoverReadme(root) {
|
|
12804
11378
|
for (const name of ["README.md", "readme.md"]) {
|
|
12805
11379
|
const p = path5.join(root, name);
|
|
12806
|
-
if (
|
|
11380
|
+
if (existsSync17(p)) {
|
|
12807
11381
|
return p;
|
|
12808
11382
|
}
|
|
12809
11383
|
}
|
|
@@ -12813,7 +11387,7 @@ async function discoverAgentFiles(root) {
|
|
|
12813
11387
|
const files = [];
|
|
12814
11388
|
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
12815
11389
|
const p = path5.join(root, name);
|
|
12816
|
-
if (
|
|
11390
|
+
if (existsSync17(p)) {
|
|
12817
11391
|
files.push(p);
|
|
12818
11392
|
}
|
|
12819
11393
|
}
|
|
@@ -12821,7 +11395,7 @@ async function discoverAgentFiles(root) {
|
|
|
12821
11395
|
}
|
|
12822
11396
|
async function discoverMerlleState(root) {
|
|
12823
11397
|
const p = path5.join(root, ".pourkit", "state.json");
|
|
12824
|
-
return
|
|
11398
|
+
return existsSync17(p) ? p : null;
|
|
12825
11399
|
}
|
|
12826
11400
|
async function discoverAgentSkills(root) {
|
|
12827
11401
|
const dirs = [
|
|
@@ -12830,7 +11404,7 @@ async function discoverAgentSkills(root) {
|
|
|
12830
11404
|
];
|
|
12831
11405
|
const found = [];
|
|
12832
11406
|
for (const d of dirs) {
|
|
12833
|
-
if (
|
|
11407
|
+
if (existsSync17(d)) {
|
|
12834
11408
|
found.push(d);
|
|
12835
11409
|
}
|
|
12836
11410
|
}
|
|
@@ -12840,12 +11414,12 @@ async function discoverRootDomainDocs(root) {
|
|
|
12840
11414
|
const docs = [];
|
|
12841
11415
|
for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
|
|
12842
11416
|
const p = path5.join(root, name);
|
|
12843
|
-
if (
|
|
11417
|
+
if (existsSync17(p)) {
|
|
12844
11418
|
docs.push(p);
|
|
12845
11419
|
}
|
|
12846
11420
|
}
|
|
12847
11421
|
const adrDir = path5.join(root, "docs", "adr");
|
|
12848
|
-
if (
|
|
11422
|
+
if (existsSync17(adrDir)) {
|
|
12849
11423
|
const entries = await readdir(adrDir, { withFileTypes: true });
|
|
12850
11424
|
for (const entry of entries) {
|
|
12851
11425
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -12988,7 +11562,7 @@ async function planInit(options) {
|
|
|
12988
11562
|
for (const file of skillFiles) {
|
|
12989
11563
|
const relPath = path5.relative(s, file);
|
|
12990
11564
|
const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
|
|
12991
|
-
if (!
|
|
11565
|
+
if (!existsSync17(destPath)) {
|
|
12992
11566
|
operations.push({
|
|
12993
11567
|
kind: "copy",
|
|
12994
11568
|
sourcePath: file,
|
|
@@ -13051,7 +11625,7 @@ async function planInit(options) {
|
|
|
13051
11625
|
});
|
|
13052
11626
|
}
|
|
13053
11627
|
if (sourceRoot) {
|
|
13054
|
-
if (!
|
|
11628
|
+
if (!existsSync17(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
|
|
13055
11629
|
warnings.push(
|
|
13056
11630
|
`--from-local path does not exist or is not a directory: ${sourceRoot}`
|
|
13057
11631
|
);
|
|
@@ -13170,7 +11744,7 @@ async function planInit(options) {
|
|
|
13170
11744
|
requiresConfirmation: false,
|
|
13171
11745
|
destructive: false
|
|
13172
11746
|
});
|
|
13173
|
-
} else if (
|
|
11747
|
+
} else if (existsSync17(destPath)) {
|
|
13174
11748
|
operations.push({
|
|
13175
11749
|
kind: "skip",
|
|
13176
11750
|
path: destPath,
|
|
@@ -13228,7 +11802,7 @@ async function planInit(options) {
|
|
|
13228
11802
|
}
|
|
13229
11803
|
}
|
|
13230
11804
|
const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
|
|
13231
|
-
if (!
|
|
11805
|
+
if (!existsSync17(contextPath) && !merleDestPaths.has(contextPath)) {
|
|
13232
11806
|
operations.push({
|
|
13233
11807
|
kind: "create",
|
|
13234
11808
|
path: contextPath,
|
|
@@ -13246,7 +11820,7 @@ async function planInit(options) {
|
|
|
13246
11820
|
"adr",
|
|
13247
11821
|
".gitkeep"
|
|
13248
11822
|
);
|
|
13249
|
-
if (!
|
|
11823
|
+
if (!existsSync17(adrGitkeep)) {
|
|
13250
11824
|
operations.push({
|
|
13251
11825
|
kind: "create",
|
|
13252
11826
|
path: adrGitkeep,
|
|
@@ -13258,7 +11832,7 @@ async function planInit(options) {
|
|
|
13258
11832
|
}
|
|
13259
11833
|
const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
|
|
13260
11834
|
const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
|
|
13261
|
-
if (
|
|
11835
|
+
if (existsSync17(srcDocAgents) && !existsSync17(tgtDocAgents)) {
|
|
13262
11836
|
const docFiles = await walkDir(srcDocAgents);
|
|
13263
11837
|
for (const file of docFiles) {
|
|
13264
11838
|
const relPath = path5.relative(srcDocAgents, file);
|
|
@@ -13291,7 +11865,7 @@ async function planInit(options) {
|
|
|
13291
11865
|
}
|
|
13292
11866
|
const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
|
|
13293
11867
|
const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
|
|
13294
|
-
if (
|
|
11868
|
+
if (existsSync17(srcPrompts) && !existsSync17(tgtPrompts)) {
|
|
13295
11869
|
const promptFiles = await walkDir(srcPrompts);
|
|
13296
11870
|
for (const file of promptFiles) {
|
|
13297
11871
|
const relPath = path5.relative(srcPrompts, file);
|
|
@@ -13318,7 +11892,7 @@ async function planInit(options) {
|
|
|
13318
11892
|
".sandcastle",
|
|
13319
11893
|
"Dockerfile"
|
|
13320
11894
|
);
|
|
13321
|
-
if (
|
|
11895
|
+
if (existsSync17(tgtSandboxDockerfile)) {
|
|
13322
11896
|
operations.push({
|
|
13323
11897
|
kind: "skip",
|
|
13324
11898
|
path: tgtSandboxDockerfile,
|
|
@@ -13327,7 +11901,7 @@ async function planInit(options) {
|
|
|
13327
11901
|
requiresConfirmation: false,
|
|
13328
11902
|
destructive: false
|
|
13329
11903
|
});
|
|
13330
|
-
} else if (
|
|
11904
|
+
} else if (existsSync17(srcSandboxDockerfile)) {
|
|
13331
11905
|
const checksum = await computeFileChecksum(srcSandboxDockerfile);
|
|
13332
11906
|
operations.push({
|
|
13333
11907
|
kind: "copy",
|
|
@@ -13341,7 +11915,7 @@ async function planInit(options) {
|
|
|
13341
11915
|
});
|
|
13342
11916
|
}
|
|
13343
11917
|
const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
|
|
13344
|
-
if (!
|
|
11918
|
+
if (!existsSync17(configTsPath)) {
|
|
13345
11919
|
const verifyCommands = inferVerificationCommands(
|
|
13346
11920
|
packageScripts,
|
|
13347
11921
|
pm || "npm"
|
|
@@ -13378,7 +11952,7 @@ async function planInit(options) {
|
|
|
13378
11952
|
const hasExistingAgents = operations.some(
|
|
13379
11953
|
(op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
|
|
13380
11954
|
);
|
|
13381
|
-
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !
|
|
11955
|
+
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync17(path5.join(targetRoot, "AGENTS.md"))) {
|
|
13382
11956
|
operations.push({
|
|
13383
11957
|
kind: "create",
|
|
13384
11958
|
path: path5.join(targetRoot, "AGENTS.md"),
|
|
@@ -13394,7 +11968,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
|
|
|
13394
11968
|
const hasExistingClaude = operations.some(
|
|
13395
11969
|
(op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
|
|
13396
11970
|
);
|
|
13397
|
-
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !
|
|
11971
|
+
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync17(path5.join(targetRoot, "CLAUDE.md"))) {
|
|
13398
11972
|
operations.push({
|
|
13399
11973
|
kind: "create",
|
|
13400
11974
|
path: path5.join(targetRoot, "CLAUDE.md"),
|
|
@@ -13409,7 +11983,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
|
|
|
13409
11983
|
}
|
|
13410
11984
|
const gitignoreTarget = path5.join(targetRoot, ".gitignore");
|
|
13411
11985
|
const gitignoreContent = generateGitignoreBlock();
|
|
13412
|
-
if (!
|
|
11986
|
+
if (!existsSync17(gitignoreTarget)) {
|
|
13413
11987
|
operations.push({
|
|
13414
11988
|
kind: "create",
|
|
13415
11989
|
path: gitignoreTarget,
|
|
@@ -13433,7 +12007,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
|
|
|
13433
12007
|
});
|
|
13434
12008
|
}
|
|
13435
12009
|
const openCodePath = path5.join(targetRoot, "opencode.json");
|
|
13436
|
-
if (!
|
|
12010
|
+
if (!existsSync17(openCodePath)) {
|
|
13437
12011
|
operations.push({
|
|
13438
12012
|
kind: "create",
|
|
13439
12013
|
path: openCodePath,
|
|
@@ -13490,7 +12064,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
|
|
|
13490
12064
|
}
|
|
13491
12065
|
}
|
|
13492
12066
|
const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
|
|
13493
|
-
if (
|
|
12067
|
+
if (existsSync17(manifestPath)) {
|
|
13494
12068
|
operations.push({
|
|
13495
12069
|
kind: "skip",
|
|
13496
12070
|
path: manifestPath,
|
|
@@ -13809,7 +12383,7 @@ async function updateManagedBlock(filePath, content) {
|
|
|
13809
12383
|
const blockContent = `${MANAGED_BLOCK_BEGIN}
|
|
13810
12384
|
${content}${MANAGED_BLOCK_END}
|
|
13811
12385
|
`;
|
|
13812
|
-
if (!
|
|
12386
|
+
if (!existsSync17(filePath)) {
|
|
13813
12387
|
const dir = path5.dirname(filePath);
|
|
13814
12388
|
await mkdir5(dir, { recursive: true });
|
|
13815
12389
|
await writeFileAtomic(filePath, blockContent);
|
|
@@ -13838,7 +12412,7 @@ async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
|
|
|
13838
12412
|
if (op.requiresConfirmation) continue;
|
|
13839
12413
|
const relPath = path5.relative(plan.targetRoot, op.path);
|
|
13840
12414
|
if (relPath === ".pourkit/manifest.json") continue;
|
|
13841
|
-
if (
|
|
12415
|
+
if (existsSync17(op.path)) {
|
|
13842
12416
|
const sha256 = await computeFileChecksum(op.path);
|
|
13843
12417
|
assets[relPath] = {
|
|
13844
12418
|
ownership: op.ownership || "managed",
|
|
@@ -13883,7 +12457,7 @@ async function applyInitPlan(plan, options) {
|
|
|
13883
12457
|
skipped++;
|
|
13884
12458
|
continue;
|
|
13885
12459
|
}
|
|
13886
|
-
if (
|
|
12460
|
+
if (existsSync17(op.path) && !op.destructive) {
|
|
13887
12461
|
skipped++;
|
|
13888
12462
|
continue;
|
|
13889
12463
|
}
|
|
@@ -13898,7 +12472,7 @@ async function applyInitPlan(plan, options) {
|
|
|
13898
12472
|
skipped++;
|
|
13899
12473
|
continue;
|
|
13900
12474
|
}
|
|
13901
|
-
if (
|
|
12475
|
+
if (existsSync17(op.path)) {
|
|
13902
12476
|
skipped++;
|
|
13903
12477
|
continue;
|
|
13904
12478
|
}
|
|
@@ -13927,7 +12501,7 @@ async function applyInitPlan(plan, options) {
|
|
|
13927
12501
|
skipped++;
|
|
13928
12502
|
continue;
|
|
13929
12503
|
}
|
|
13930
|
-
if (
|
|
12504
|
+
if (existsSync17(op.path)) {
|
|
13931
12505
|
skipped++;
|
|
13932
12506
|
continue;
|
|
13933
12507
|
}
|
|
@@ -14067,7 +12641,7 @@ async function applyInitFromSource(options) {
|
|
|
14067
12641
|
if (!manifestSkipped) {
|
|
14068
12642
|
const agentFiles = [];
|
|
14069
12643
|
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
14070
|
-
if (
|
|
12644
|
+
if (existsSync17(path5.join(targetRoot, name))) {
|
|
14071
12645
|
agentFiles.push(path5.join(targetRoot, name));
|
|
14072
12646
|
}
|
|
14073
12647
|
}
|
|
@@ -14499,6 +13073,7 @@ var GitHubIssueProvider = class {
|
|
|
14499
13073
|
);
|
|
14500
13074
|
return data.filter((issue) => !issue.pull_request).map((issue) => ({
|
|
14501
13075
|
number: issue.number,
|
|
13076
|
+
title: issue.title,
|
|
14502
13077
|
body: issue.body ?? null,
|
|
14503
13078
|
labels: issue.labels.map((l) => ({
|
|
14504
13079
|
name: typeof l === "string" ? l : l.name ?? ""
|
|
@@ -14887,7 +13462,7 @@ init_common();
|
|
|
14887
13462
|
|
|
14888
13463
|
// execution/sandcastle-execution.ts
|
|
14889
13464
|
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync8 } from "fs";
|
|
14890
|
-
import { join as
|
|
13465
|
+
import { join as join22 } from "path";
|
|
14891
13466
|
import { createWorktree, opencode } from "@ai-hero/sandcastle";
|
|
14892
13467
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
14893
13468
|
|
|
@@ -14896,10 +13471,10 @@ init_common();
|
|
|
14896
13471
|
import { mkdtempSync as mkdtempSync2 } from "fs";
|
|
14897
13472
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
14898
13473
|
import { tmpdir as tmpdir2 } from "os";
|
|
14899
|
-
import { dirname as dirname5, join as
|
|
13474
|
+
import { dirname as dirname5, join as join21 } from "path";
|
|
14900
13475
|
async function writeExecutionArtifacts(worktreePath, artifacts) {
|
|
14901
13476
|
for (const artifact of artifacts) {
|
|
14902
|
-
const filePath =
|
|
13477
|
+
const filePath = join21(worktreePath, artifact.path);
|
|
14903
13478
|
await ensureDir(dirname5(filePath));
|
|
14904
13479
|
await writeFile3(filePath, artifact.content, "utf-8");
|
|
14905
13480
|
}
|
|
@@ -14911,17 +13486,17 @@ import path7 from "path";
|
|
|
14911
13486
|
|
|
14912
13487
|
// execution/sandbox-image.ts
|
|
14913
13488
|
import { createHash as createHash4 } from "crypto";
|
|
14914
|
-
import { existsSync as
|
|
13489
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
|
|
14915
13490
|
import path6 from "path";
|
|
14916
13491
|
function sandboxImageName(repoRoot2) {
|
|
14917
13492
|
const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
|
|
14918
13493
|
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
14919
13494
|
const baseName = sanitized || "local";
|
|
14920
13495
|
const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
|
|
14921
|
-
if (!
|
|
13496
|
+
if (!existsSync18(dockerfilePath)) {
|
|
14922
13497
|
return `sandcastle:${baseName}`;
|
|
14923
13498
|
}
|
|
14924
|
-
const fingerprint = createHash4("sha256").update(
|
|
13499
|
+
const fingerprint = createHash4("sha256").update(readFileSync18(dockerfilePath)).digest("hex").slice(0, 8);
|
|
14925
13500
|
return `sandcastle:${baseName}-${fingerprint}`;
|
|
14926
13501
|
}
|
|
14927
13502
|
|
|
@@ -15217,12 +13792,12 @@ function isPlainObject(value) {
|
|
|
15217
13792
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15218
13793
|
}
|
|
15219
13794
|
function savePromptToFile(repoRoot2, stage, iteration, prompt) {
|
|
15220
|
-
const promptsDir =
|
|
13795
|
+
const promptsDir = join22(repoRoot2, ".pourkit", ".tmp", "prompts");
|
|
15221
13796
|
mkdirSync11(promptsDir, { recursive: true });
|
|
15222
13797
|
const timestamp2 = Date.now();
|
|
15223
13798
|
const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
|
|
15224
13799
|
const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
|
|
15225
|
-
const filePath =
|
|
13800
|
+
const filePath = join22(promptsDir, filename);
|
|
15226
13801
|
writeFileSync8(filePath, prompt, "utf-8");
|
|
15227
13802
|
}
|
|
15228
13803
|
|
|
@@ -15323,7 +13898,7 @@ function createCliProgram(version) {
|
|
|
15323
13898
|
});
|
|
15324
13899
|
program.command("validate-artifact").description("Validate an agent handoff artifact").argument(
|
|
15325
13900
|
"<kind>",
|
|
15326
|
-
"artifact kind: reviewer, refactor, finalizer, conflict-resolution, final-review, failure-resolution, local-prd, local-issue, or
|
|
13901
|
+
"artifact kind: reviewer, refactor, finalizer, conflict-resolution, final-review (retained for Issue Final Review validation), failure-resolution, local-prd, local-issue, local-triage, or issue-final-review"
|
|
15327
13902
|
).argument("<artifactPath>", "artifact path to validate").argument("[extraPaths...]", "additional artifact paths (for local-triage)").option("--iteration <number>", "review/refactor iteration", (value) => {
|
|
15328
13903
|
const parsed = Number.parseInt(value, 10);
|
|
15329
13904
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
@@ -15362,6 +13937,23 @@ function createCliProgram(version) {
|
|
|
15362
13937
|
).option(
|
|
15363
13938
|
"--review-base <ref>",
|
|
15364
13939
|
"review merge base for final-review artifacts"
|
|
13940
|
+
).option(
|
|
13941
|
+
"--issue-number <number>",
|
|
13942
|
+
"issue number for issue-final-review artifacts",
|
|
13943
|
+
(value) => {
|
|
13944
|
+
const parsed = Number.parseInt(value, 10);
|
|
13945
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
13946
|
+
throw new CommanderError(
|
|
13947
|
+
1,
|
|
13948
|
+
"ERR_INVALID_ARG_VALUE",
|
|
13949
|
+
"--issue-number must be a positive integer"
|
|
13950
|
+
);
|
|
13951
|
+
}
|
|
13952
|
+
return parsed;
|
|
13953
|
+
}
|
|
13954
|
+
).option(
|
|
13955
|
+
"--branch-name <name>",
|
|
13956
|
+
"branch name for issue-final-review artifacts"
|
|
15365
13957
|
).option("--no-check-conflict-markers", "skip conflict marker scan").option("--cwd <path>", "target repository directory").action(
|
|
15366
13958
|
(kind, artifactPath, extraPaths, options) => {
|
|
15367
13959
|
const allowedKinds = /* @__PURE__ */ new Set([
|
|
@@ -15373,7 +13965,8 @@ function createCliProgram(version) {
|
|
|
15373
13965
|
"failure-resolution",
|
|
15374
13966
|
"local-prd",
|
|
15375
13967
|
"local-issue",
|
|
15376
|
-
"local-triage"
|
|
13968
|
+
"local-triage",
|
|
13969
|
+
"issue-final-review"
|
|
15377
13970
|
]);
|
|
15378
13971
|
if (!allowedKinds.has(kind)) {
|
|
15379
13972
|
throw new CommanderError(
|
|
@@ -15405,6 +13998,8 @@ function createCliProgram(version) {
|
|
|
15405
13998
|
prdRef: options.prdRef,
|
|
15406
13999
|
checkoutBase: options.checkoutBase,
|
|
15407
14000
|
reviewBase: options.reviewBase,
|
|
14001
|
+
issueNumber: options.issueNumber,
|
|
14002
|
+
branchName: options.branchName,
|
|
15408
14003
|
checkConflictMarkers: options.checkConflictMarkers
|
|
15409
14004
|
});
|
|
15410
14005
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -15886,11 +14481,11 @@ function createCliProgram(version) {
|
|
|
15886
14481
|
return program;
|
|
15887
14482
|
}
|
|
15888
14483
|
async function resolveCliVersion() {
|
|
15889
|
-
if (isPackageVersion("0.0.0-next-
|
|
15890
|
-
return "0.0.0-next-
|
|
14484
|
+
if (isPackageVersion("0.0.0-next-20260613201753")) {
|
|
14485
|
+
return "0.0.0-next-20260613201753";
|
|
15891
14486
|
}
|
|
15892
|
-
if (isReleaseVersion("0.0.0-next-
|
|
15893
|
-
return "0.0.0-next-
|
|
14487
|
+
if (isReleaseVersion("0.0.0-next-20260613201753")) {
|
|
14488
|
+
return "0.0.0-next-20260613201753";
|
|
15894
14489
|
}
|
|
15895
14490
|
try {
|
|
15896
14491
|
const root = repoRoot();
|