@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 CHANGED
@@ -58,20 +58,20 @@ function createLogger(name, filePath) {
58
58
  );
59
59
  },
60
60
  async close() {
61
- await new Promise((resolve2) => {
61
+ await new Promise((resolve3) => {
62
62
  if (!fileStream) {
63
- resolve2();
63
+ resolve3();
64
64
  return;
65
65
  }
66
66
  const timer = setTimeout(() => {
67
67
  if (!fileStream.destroyed) {
68
68
  fileStream.destroy();
69
69
  }
70
- resolve2();
70
+ resolve3();
71
71
  }, 2e3);
72
72
  fileStream.end(() => {
73
73
  clearTimeout(timer);
74
- resolve2();
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((resolve2) => setTimeout(resolve2, ms));
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: existsSync17 } = await import("fs");
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 (!existsSync17(configPath)) {
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 existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1045
- import { isAbsolute, join as join6, resolve } from "path";
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 existsSync4,
1083
+ existsSync as existsSync5,
1079
1084
  mkdirSync as mkdirSync5,
1080
- readFileSync as readFileSync4,
1081
- readdirSync,
1085
+ readFileSync as readFileSync5,
1086
+ readdirSync as readdirSync2,
1082
1087
  writeFileSync as writeFileSync3
1083
1088
  } from "fs";
1084
- import { join as join5 } from "path";
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
- return [
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 existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
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: () => readFileSync3(path9, "utf-8"),
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: () => existsSync3(path9),
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 (!existsSync4(artifactPath)) {
1960
+ if (!existsSync5(artifactPath)) {
1817
1961
  throw new RefactorArtifactValidationError(
1818
1962
  `Refactor artifact missing at ${artifactPath}`
1819
1963
  );
1820
1964
  }
1821
- const content = readFileSync4(artifactPath, "utf-8");
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 = join5(
2122
+ const artifactPathInWorktree = join6(
1978
2123
  ".pourkit",
1979
2124
  ".tmp",
1980
2125
  "reviewers",
1981
2126
  `iteration-${iteration ?? 1}.md`
1982
2127
  );
1983
- const artifactPath = join5(worktreePath, artifactPathInWorktree);
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, validation commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
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 = join5(worktreePath, ".pourkit", ".tmp", "refactors");
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 = join5(refactorsDir, `iteration-${i}.md`);
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 = join5(worktreePath, ".pourkit", ".tmp", "reviewers");
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 = join5(reviewersDir, `iteration-${i}.md`);
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 = join5(
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 (!existsSync4(logPath)) {
2434
+ if (!existsSync5(logPath)) {
2288
2435
  return null;
2289
2436
  }
2290
- const logContent = readFileSync4(logPath, "utf-8");
2437
+ const logContent = readFileSync5(logPath, "utf-8");
2291
2438
  return recoverReviewOutputFromString(logContent);
2292
2439
  }
2293
2440
  function readReviewArtifact(artifactPath, logPath) {
2294
- if (existsSync4(artifactPath)) {
2295
- const output = readFileSync4(artifactPath, "utf-8");
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 (!existsSync4(artifactPath)) {
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 = join5(worktreePath, ".pourkit", ".tmp", "reviewers");
2485
+ const reviewersDir = join6(worktreePath, ".pourkit", ".tmp", "reviewers");
2338
2486
  try {
2339
- const files = readdirSync(reviewersDir);
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 = join5(
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 = join5(
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 = join5(worktreePath, ".pourkit", ".tmp", "reviewers");
2814
+ const dir = join6(worktreePath, ".pourkit", ".tmp", "reviewers");
2665
2815
  yield* fs.mkdir(dir).pipe(Effect2.catchAll(() => Effect2.void));
2666
- yield* fs.writeFile(join5(dir, `iteration-${iteration}.md`), output).pipe(Effect2.catchAll(() => Effect2.void));
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, validation commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
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 existsSync5, readFileSync as readFileSync5 } from "fs";
3092
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
2943
3093
  function parseFinalReviewArtifact(artifactPath) {
2944
- if (!existsSync5(artifactPath)) {
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 = readFileSync5(artifactPath, "utf-8");
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 (!existsSync6(options.artifactPath)) {
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 = readFileSync6(options.artifactPath, "utf-8");
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 = readFileSync6(
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 = resolve(base, file);
3500
+ const filePath = resolve2(base, file);
3213
3501
  try {
3214
3502
  return CONFLICT_MARKER_PATTERN.test(
3215
- readFileSync6(filePath, "utf-8")
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 = resolve(options.repoRoot, options.artifactPath);
3298
- const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve(options.repoRoot, options.latestReviewArtifactPath) : void 0;
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 = resolve(options.repoRoot, options.artifactPath);
3618
+ const resolvedPath = resolve2(options.repoRoot, options.artifactPath);
3308
3619
  const resolvedExtra = (options.extraArgs ?? []).map(
3309
- (p) => isAbsolute(p) ? p : resolve(options.repoRoot, 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 = readFileSync6(path9, "utf-8");
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 existsSync10, readFileSync as readFileSync12 } from "fs";
4056
- import { join as join13 } from "path";
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 existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
4192
- import { dirname as dirname3, join as join7 } from "path";
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 = join7(worktreePath, ATTEMPT_LOG_PATH);
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 = join7(worktreePath, ATTEMPT_LOG_PATH);
4201
- if (!existsSync7(logPath)) {
4511
+ const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
4512
+ if (!existsSync8(logPath)) {
4202
4513
  return [];
4203
4514
  }
4204
- const raw = readFileSync7(logPath, "utf-8");
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 existsSync8, readFileSync as readFileSync8 } from "fs";
4324
- import { join as join8 } from "path";
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 = join8(worktreePath, file);
4640
+ const filePath = join9(worktreePath, file);
4330
4641
  try {
4331
- const content = readFileSync8(filePath, "utf-8");
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((resolve2) => setTimeout(resolve2, 100));
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 readFileSync9 } from "fs";
4663
- import { join as join9 } from "path";
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 = join9(worktreePath, artifactPath);
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 = readFileSync9(fullArtifactPath, "utf-8");
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 join11 } from "path";
4910
- import { readFileSync as readFileSync10 } from "fs";
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 join10 } from "path";
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 = join10(
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 = join11(
5360
+ const artifactPathInWorktree = join12(
5050
5361
  ".pourkit",
5051
5362
  ".tmp",
5052
5363
  "finalizer",
5053
5364
  "agent-output.md"
5054
5365
  );
5055
- const artifactPath = join11(options.worktreePath, artifactPathInWorktree);
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 = readFileSync10(artifactPath, "utf-8");
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 = join11(
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 = join11(worktreePath, ".pourkit", ".tmp", "finalizer");
5535
+ const dir = join12(worktreePath, ".pourkit", ".tmp", "finalizer");
5225
5536
  yield* fs.mkdir(dir).pipe(Effect6.catchAll(() => Effect6.void));
5226
- yield* fs.writeFile(join11(dir, "generated.md"), output).pipe(Effect6.catchAll(() => Effect6.void));
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 existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
5233
- import { join as join12 } from "path";
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 join12(repoRoot2, ".pourkit", "local-prd-runs", prdId);
5630
+ return join13(repoRoot2, ".pourkit", "local-prd-runs", prdId);
5320
5631
  }
5321
5632
  function getMergeReceiptPath(repoRoot2, prdId, issueId) {
5322
- return join12(
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 join12(getLocalStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
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 (!existsSync9(issuePath)) return null;
5644
+ if (!existsSync10(issuePath)) return null;
5334
5645
  try {
5335
- const content = readFileSync11(issuePath, "utf-8");
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 (!existsSync9(receiptPath)) return null;
5656
+ if (!existsSync10(receiptPath)) return null;
5346
5657
  try {
5347
- const content = readFileSync11(receiptPath, "utf-8");
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 (existsSync9(receiptPath)) {
5671
+ if (existsSync10(receiptPath)) {
5361
5672
  try {
5362
5673
  const existing = JSON.parse(
5363
- readFileSync11(receiptPath, "utf-8")
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 = join12(
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
- async function startIssueRun(options) {
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
- force,
5951
- logger
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 (!existsSync10(finalizerFromState.artifactPath)) {
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 = readFileSync12(
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: join13(
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: join13(
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 join13(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
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 = existsSync10(promptPath) ? readFileSync12(promptPath, "utf-8") : promptTemplate;
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 existsSync14,
7980
+ existsSync as existsSync16,
7404
7981
  lstatSync,
7405
7982
  mkdirSync as mkdirSync10,
7406
7983
  mkdtempSync,
7407
- readFileSync as readFileSync15,
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 join18, relative } from "path";
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 existsSync11,
7995
+ existsSync as existsSync13,
7419
7996
  mkdirSync as mkdirSync8,
7420
- readFileSync as readFileSync13,
7421
- readdirSync as readdirSync3,
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 join14 } from "path";
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 (!existsSync11(recordPath)) {
8092
+ if (!existsSync13(recordPath)) {
7516
8093
  return { record: null, diagnostics: [] };
7517
8094
  }
7518
8095
  try {
7519
- const raw = JSON.parse(readFileSync13(recordPath, "utf-8"));
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(readFileSync13(recordPath, "utf-8"));
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 = join14(repoRoot2, PRD_RUN_STATE_DIR);
7546
- if (!existsSync11(stateDir)) {
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 readdirSync3(stateDir, { withFileTypes: true }).sort(
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 = join14(stateDir, entry.name);
8132
+ const recordPath = join16(stateDir, entry.name);
7556
8133
  try {
7557
8134
  const record = PrdRunRecordSchema.parse(
7558
- JSON.parse(readFileSync13(recordPath, "utf-8"))
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 = join14(repoRoot2, PRD_RUN_STATE_DIR);
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 join14(
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 existsSync12 } from "fs";
7766
- import { join as join15 } from "path";
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 join15(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8256
+ return join17(repoRoot2, ".pourkit", "local-prd-runs", prdId);
7800
8257
  }
7801
8258
  function prdArtifactPath(repoRoot2, prdId) {
7802
- return join15(prdStorePath(repoRoot2, prdId), "prd.json");
8259
+ return join17(prdStorePath(repoRoot2, prdId), "prd.json");
7803
8260
  }
7804
8261
  function issueArtifactPath(repoRoot2, prdId, issueId) {
7805
- return join15(prdStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
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 (!existsSync12(prdPath)) {
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 (!existsSync12(issuePath)) {
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 join16 } from "path";
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 readFileSync14, writeFileSync as writeFileSync7 } from "fs";
8068
- import { join as join17 } from "path";
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 { ok: true, issueId, branch };
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 join17(
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 = readFileSync14(
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
- void 0,
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 blockers = parseBlockedBy(issue.body);
8432
- if (blockers.length === 0) {
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 computeFinalReviewMergeBase(repoRoot2, prdRef, baseBranch) {
10561
- const normalized = normalizePrdRunRef(prdRef);
10562
- const fetchResult = spawnSync3(
10563
- "git",
10564
- ["fetch", "origin", baseBranch, normalized],
10565
- {
10566
- cwd: repoRoot2,
10567
- encoding: "utf8"
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
- const fetchDiagnostics = [];
10571
- if (fetchResult.status !== 0) {
10572
- const stderr = fetchResult.stderr?.toString?.() ?? "";
10573
- const stdout = fetchResult.stdout?.toString?.() ?? "";
10574
- if (stderr.trim()) fetchDiagnostics.push(stderr.trim());
10575
- if (stdout.trim()) fetchDiagnostics.push(stdout.trim());
10576
- return {
10577
- ok: false,
10578
- gate: "final-review",
10579
- reason: `Final Review failed to fetch origin/${baseBranch} and origin/${normalized}.`,
10580
- diagnostics: fetchDiagnostics.length > 0 ? fetchDiagnostics : ["git fetch failed"]
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
- const mergeBaseDiagnostics = [];
10595
- if (mergeBaseResult.status !== 0) {
10596
- if (mergeBaseResult.stderr?.toString?.()?.trim()) {
10597
- mergeBaseDiagnostics.push(mergeBaseResult.stderr.toString().trim());
10598
- }
10599
- if (mergeBaseResult.stdout?.toString?.()?.trim()) {
10600
- mergeBaseDiagnostics.push(mergeBaseResult.stdout.toString().trim());
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
- const mergeBase = mergeBaseResult.stdout?.toString?.()?.trim();
10610
- if (!mergeBase) {
9279
+ if (existsSync16(localStoreDir) && !localStoreReady) {
10611
9280
  return {
10612
9281
  ok: false,
10613
- gate: "final-review",
10614
- reason: `Final Review merge-base returned empty result for origin/${baseBranch} and origin/${normalized}.`,
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
- `merge-base stdout was empty for origin/${baseBranch}..origin/${normalized}`
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, mergeBase, diagnostics: fetchDiagnostics };
9291
+ return { ok: true };
10621
9292
  }
10622
- function computeLocalFinalReviewMergeBase(repoRoot2, prdBranch) {
10623
- const mergeBaseResult = spawnSync3("git", ["merge-base", "dev", prdBranch], {
10624
- cwd: repoRoot2,
10625
- encoding: "utf8"
10626
- });
10627
- if (mergeBaseResult.status !== 0) {
10628
- const diagnostics = [];
10629
- if (mergeBaseResult.stderr?.toString?.()?.trim()) {
10630
- diagnostics.push(mergeBaseResult.stderr.toString().trim());
10631
- }
10632
- if (mergeBaseResult.stdout?.toString?.()?.trim()) {
10633
- diagnostics.push(mergeBaseResult.stdout.toString().trim());
10634
- }
10635
- return {
10636
- ok: false,
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
- const mergeBase = mergeBaseResult.stdout?.toString?.()?.trim();
10643
- if (!mergeBase) {
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 { ok: true, mergeBase, diagnostics: [] };
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", "final-review"],
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 = join18(
9438
+ const localStorePath = join20(
10842
9439
  options.repoRoot,
10843
9440
  ".pourkit",
10844
9441
  "local-prd-runs",
10845
9442
  prdRef
10846
9443
  );
10847
- if (existsSync14(localStorePath)) {
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 = join18(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
11362
- if (existsSync14(localStorePath)) {
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 existsSync15, statSync } from "fs";
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 existsSync15(path5.join(root, name));
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 (existsSync15(p)) {
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 (existsSync15(p)) {
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 existsSync15(p) ? p : null;
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 (existsSync15(d)) {
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 (existsSync15(p)) {
11417
+ if (existsSync17(p)) {
12844
11418
  docs.push(p);
12845
11419
  }
12846
11420
  }
12847
11421
  const adrDir = path5.join(root, "docs", "adr");
12848
- if (existsSync15(adrDir)) {
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 (!existsSync15(destPath)) {
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 (!existsSync15(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
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 (existsSync15(destPath)) {
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 (!existsSync15(contextPath) && !merleDestPaths.has(contextPath)) {
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 (!existsSync15(adrGitkeep)) {
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 (existsSync15(srcDocAgents) && !existsSync15(tgtDocAgents)) {
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 (existsSync15(srcPrompts) && !existsSync15(tgtPrompts)) {
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 (existsSync15(tgtSandboxDockerfile)) {
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 (existsSync15(srcSandboxDockerfile)) {
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 (!existsSync15(configTsPath)) {
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 && !existsSync15(path5.join(targetRoot, "AGENTS.md"))) {
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 && !existsSync15(path5.join(targetRoot, "CLAUDE.md"))) {
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 (!existsSync15(gitignoreTarget)) {
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 (!existsSync15(openCodePath)) {
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 (existsSync15(manifestPath)) {
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 (!existsSync15(filePath)) {
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 (existsSync15(op.path)) {
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 (existsSync15(op.path) && !op.destructive) {
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 (existsSync15(op.path)) {
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 (existsSync15(op.path)) {
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 (existsSync15(path5.join(targetRoot, name))) {
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 join20 } from "path";
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 join19 } from "path";
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 = join19(worktreePath, artifact.path);
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 existsSync16, readFileSync as readFileSync16 } from "fs";
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 (!existsSync16(dockerfilePath)) {
13496
+ if (!existsSync18(dockerfilePath)) {
14922
13497
  return `sandcastle:${baseName}`;
14923
13498
  }
14924
- const fingerprint = createHash4("sha256").update(readFileSync16(dockerfilePath)).digest("hex").slice(0, 8);
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 = join20(repoRoot2, ".pourkit", ".tmp", "prompts");
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 = join20(promptsDir, filename);
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 local-triage"
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-20260613012953")) {
15890
- return "0.0.0-next-20260613012953";
14484
+ if (isPackageVersion("0.0.0-next-20260613201753")) {
14485
+ return "0.0.0-next-20260613201753";
15891
14486
  }
15892
- if (isReleaseVersion("0.0.0-next-20260613012953")) {
15893
- return "0.0.0-next-20260613012953";
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();