@pourkit/cli 0.0.0-next-20260607223610 → 0.0.0-next-20260608221925

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
@@ -376,6 +376,7 @@ var VerificationCommandSchema = z.object({
376
376
  command: d.command,
377
377
  label: d.label && d.label.trim() !== "" ? d.label : void 0
378
378
  }));
379
+ var PrdRunModeSchema = z.enum(["github", "local"]);
379
380
  var QueueConfigSchema = z.object({
380
381
  loop: z.boolean().optional()
381
382
  }).strict();
@@ -422,6 +423,7 @@ var ReviewRefactorLoopStrategySchema = z.object({
422
423
  maxAttempts: z.number().int().positive()
423
424
  }).strict(),
424
425
  prdRun: z.object({
426
+ mode: PrdRunModeSchema.optional(),
425
427
  // Uses promptTemplate (canonical StageAgentConfig field), not prompt as Issue contract may suggest
426
428
  finalReview: StageAgentConfigSchema,
427
429
  reconciliation: StageAgentConfigSchema.optional()
@@ -715,6 +717,7 @@ function parseConfig(raw) {
715
717
  },
716
718
  ...t.strategy.prdRun ? {
717
719
  prdRun: {
720
+ ...t.strategy.prdRun.mode ? { mode: t.strategy.prdRun.mode } : {},
718
721
  finalReview: t.strategy.prdRun.finalReview,
719
722
  ...t.strategy.prdRun.reconciliation ? { reconciliation: t.strategy.prdRun.reconciliation } : {}
720
723
  }
@@ -771,14 +774,28 @@ function assertKnownKeys(value, path9, knownKeys) {
771
774
  function getVerificationCommands(target) {
772
775
  return target.strategy.verify?.commands ?? [];
773
776
  }
777
+ function resolvePrdRunMode(target, opts) {
778
+ if (opts?.localOverride === true) {
779
+ return { mode: "local", source: "cli-override", targetName: target.name };
780
+ }
781
+ const configMode = target.strategy.prdRun?.mode;
782
+ if (configMode) {
783
+ return {
784
+ mode: configMode,
785
+ source: "target-config",
786
+ targetName: target.name
787
+ };
788
+ }
789
+ return { mode: "github", source: "default", targetName: target.name };
790
+ }
774
791
  async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
775
- const { existsSync: existsSync19 } = await import("fs");
792
+ const { existsSync: existsSync20 } = await import("fs");
776
793
  const { mkdir: mkdir6, writeFile: writeFile4, rm } = await import("fs/promises");
777
794
  const { join: pjoin, basename } = await import("path");
778
795
  const { pathToFileURL: pathToFileURL2 } = await import("url");
779
796
  const { build } = await import("esbuild");
780
797
  const configPath = pjoin(repoRoot2, configFileName);
781
- if (!existsSync19(configPath)) {
798
+ if (!existsSync20(configPath)) {
782
799
  throw new Error(
783
800
  `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
784
801
  );
@@ -1142,6 +1159,77 @@ function prepareArtifactPath(artifactPath) {
1142
1159
  rmSync(artifactPath, { recursive: true, force: true });
1143
1160
  }
1144
1161
 
1162
+ // commands/run-verification.ts
1163
+ init_common();
1164
+ function buildRunVerificationCommand(target) {
1165
+ return `pourkit run-verification --target ${target.name}`;
1166
+ }
1167
+ async function runVerificationCommand(command, cwd, logger) {
1168
+ try {
1169
+ const result = await execCapture("bash", ["-lc", command.command], {
1170
+ cwd,
1171
+ logger,
1172
+ label: command.label
1173
+ });
1174
+ return {
1175
+ label: command.label,
1176
+ command: command.command,
1177
+ ok: true,
1178
+ stdout: result.stdout,
1179
+ stderr: result.stderr
1180
+ };
1181
+ } catch (error) {
1182
+ return {
1183
+ label: command.label,
1184
+ command: command.command,
1185
+ ok: false,
1186
+ error: error instanceof Error ? error.message : String(error)
1187
+ };
1188
+ }
1189
+ }
1190
+ async function runVerificationCommands(options) {
1191
+ const commands = getVerificationCommands(options.target);
1192
+ const diagnostics = [];
1193
+ if (commands.length === 0) {
1194
+ diagnostics.push(
1195
+ `No verification commands configured for target "${options.target.name}".`
1196
+ );
1197
+ return {
1198
+ ok: true,
1199
+ target: options.target.name,
1200
+ commands: [],
1201
+ diagnostics
1202
+ };
1203
+ }
1204
+ const results = [];
1205
+ for (const command of commands) {
1206
+ const result = await runVerificationCommand(
1207
+ command,
1208
+ options.cwd,
1209
+ options.logger
1210
+ );
1211
+ results.push(result);
1212
+ if (!result.ok) {
1213
+ diagnostics.push(`Verification failed for "${command.label}".`);
1214
+ return {
1215
+ ok: false,
1216
+ target: options.target.name,
1217
+ commands: results,
1218
+ diagnostics
1219
+ };
1220
+ }
1221
+ }
1222
+ diagnostics.push(
1223
+ `Verification passed for target "${options.target.name}".`
1224
+ );
1225
+ return {
1226
+ ok: true,
1227
+ target: options.target.name,
1228
+ commands: results,
1229
+ diagnostics
1230
+ };
1231
+ }
1232
+
1145
1233
  // shared/run-context.ts
1146
1234
  var RUN_CONTEXT_PATH_IN_WORKTREE = ".pourkit/.tmp/run-context.md";
1147
1235
  var ALL_RUN_CONTEXT_SECTIONS = [
@@ -1268,6 +1356,7 @@ function buildRunContextMarkdown(options) {
1268
1356
  if (sections.includes("verification-commands")) {
1269
1357
  parts.push(
1270
1358
  ...renderCommandList(
1359
+ target,
1271
1360
  getVerificationCommands(target),
1272
1361
  "Verification Commands"
1273
1362
  )
@@ -1289,13 +1378,26 @@ function buildRunContextMarkdown(options) {
1289
1378
  }
1290
1379
  return parts.join("\n");
1291
1380
  }
1292
- function renderCommandList(commands, heading) {
1381
+ function renderCommandList(target, commands, heading) {
1293
1382
  if (commands.length === 0) {
1294
- return [`## ${heading}`, "", "(none)", ""];
1383
+ return [
1384
+ `## ${heading}`,
1385
+ "",
1386
+ `Run this command from the repository root: \`${buildRunVerificationCommand(target)}\``,
1387
+ "",
1388
+ "Underlying project commands:",
1389
+ "",
1390
+ "(none configured)",
1391
+ ""
1392
+ ];
1295
1393
  }
1296
1394
  return [
1297
1395
  `## ${heading}`,
1298
1396
  "",
1397
+ `Run this command from the repository root: \`${buildRunVerificationCommand(target)}\``,
1398
+ "",
1399
+ "Underlying project commands:",
1400
+ "",
1299
1401
  "Run these commands from the repository root exactly as written. Do not substitute equivalent scripts from nested package.json files.",
1300
1402
  "",
1301
1403
  ...commands.map((command) => `- ${command.label}: \`${command.command}\``),
@@ -2079,7 +2181,7 @@ ${criteriaBlock}
2079
2181
 
2080
2182
  Write your review to: ${artifactPathInWorktree}
2081
2183
 
2082
- Before handoff, run: npm run pourkit:validate-reviewer -- ${artifactPathInWorktree} --iteration ${iteration}${priorRefactorArtifacts ? " --prior-refactor-artifacts" : ""}
2184
+ Before handoff, run: pourkit validate-artifact reviewer ${artifactPathInWorktree} --iteration ${iteration}${priorRefactorArtifacts ? " --prior-refactor-artifacts" : ""}
2083
2185
 
2084
2186
  Do not provide a separate chat response. The runner only reads the file above.
2085
2187
 
@@ -2615,7 +2717,7 @@ ${latestReview.trimEnd()}
2615
2717
 
2616
2718
  Write your refactor artifact to: ${artifactPathInWorktree}
2617
2719
 
2618
- Before handoff, run: npm run pourkit:validate-refactor -- ${artifactPathInWorktree} --iteration ${iteration}${findingArgs}
2720
+ Before handoff, run: pourkit validate-artifact refactor ${artifactPathInWorktree} --iteration ${iteration}${findingArgs}
2619
2721
 
2620
2722
  When you are done, finish with <promise>COMPLETE</promise>.`);
2621
2723
  });
@@ -4879,8 +4981,8 @@ function validateLocalTriage(prdPath, issuePaths) {
4879
4981
  }
4880
4982
 
4881
4983
  // commands/issue-run.ts
4882
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
4883
- import { join as join13 } from "path";
4984
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
4985
+ import { join as join14 } from "path";
4884
4986
 
4885
4987
  // pr/templates.ts
4886
4988
  init_common();
@@ -5581,7 +5683,7 @@ async function runFailureResolutionAgent(options) {
5581
5683
  "",
5582
5684
  "Allowed decisions: " + packet.allowedDecisions.join(", "),
5583
5685
  "",
5584
- `Before handoff, run: npm run pourkit:validate-failure-resolution -- ${artifactPath} ${packet.allowedDecisions.map((decision) => `--allowed-decision ${decision}`).join(" ")}`
5686
+ `Before handoff, run: pourkit validate-artifact failure-resolution ${artifactPath} ${packet.allowedDecisions.map((decision) => `--allowed-decision ${decision}`).join(" ")}`
5585
5687
  ].join("\n");
5586
5688
  const retryResult = await executeWithMissingOrEmptyArtifactRetry({
5587
5689
  executionProvider,
@@ -5853,7 +5955,7 @@ Rules:
5853
5955
 
5854
5956
  Write your finalizer output to: ${artifactPathInWorktree}
5855
5957
 
5856
- Before handoff, run: npm run pourkit:validate-finalizer -- ${artifactPathInWorktree}`;
5958
+ Before handoff, run: pourkit validate-artifact finalizer ${artifactPathInWorktree}`;
5857
5959
  }
5858
5960
 
5859
5961
  // commands/pr-description-agent.ts
@@ -6054,110 +6156,444 @@ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
6054
6156
  });
6055
6157
  }
6056
6158
 
6057
- // pr/pr-body.ts
6058
- import { readFile as readFile2 } from "fs/promises";
6059
- var DEFAULT_MANUAL_PR_BODY = `## Summary
6060
-
6061
- - Summarize why this branch exists.
6062
-
6063
- ## Changes
6159
+ // prd-run/local-merge-coordinator.ts
6160
+ import { execFileSync as execFileSync2 } from "child_process";
6161
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
6162
+ import { join as join13 } from "path";
6064
6163
 
6065
- - Summarize the final net change in this branch.`;
6066
- async function buildPrBody(input) {
6067
- const { defaultBody, options } = input;
6068
- let bodyText = await resolveBodyText(defaultBody, options);
6069
- bodyText = appendClosingRef(bodyText, options.issue);
6070
- return bodyText;
6164
+ // prd-run/local-branches.ts
6165
+ import { execFileSync, spawnSync as spawnSync2 } from "child_process";
6166
+ function getLocalPrdBranchName(prdId) {
6167
+ return `local/${prdId}`;
6071
6168
  }
6072
- function ensureClosingRefs(body, issue) {
6073
- const stripped = stripClosingRefs(body || "");
6074
- if (issue === void 0) {
6075
- return stripped;
6169
+ function materializeLocalPrdBranch(prdId, startBaseCommit, repoRoot2) {
6170
+ const branch = getLocalPrdBranchName(prdId);
6171
+ const root = repoRoot2 ?? process.cwd();
6172
+ const branchExists = (() => {
6173
+ try {
6174
+ return spawnSync2("git", ["rev-parse", "--verify", "--quiet", branch], {
6175
+ cwd: root,
6176
+ encoding: "utf8",
6177
+ stdio: "pipe"
6178
+ }).status === 0;
6179
+ } catch {
6180
+ return false;
6181
+ }
6182
+ })();
6183
+ if (branchExists) {
6184
+ const currentCommit = spawnSync2("git", ["rev-parse", branch], {
6185
+ cwd: root,
6186
+ encoding: "utf8",
6187
+ stdio: "pipe"
6188
+ }).stdout?.toString?.().trim() ?? "";
6189
+ if (currentCommit !== startBaseCommit) {
6190
+ return {
6191
+ ok: false,
6192
+ failureCode: "branch_commit_mismatch",
6193
+ message: `Local PRD branch ${branch} exists at commit ${currentCommit} but expected commit ${startBaseCommit}. The branch has diverged from the start base. To resolve, delete the local branch and rerun start, or manually reset it to the expected commit.`
6194
+ };
6195
+ }
6196
+ return {
6197
+ ok: true,
6198
+ created: false
6199
+ };
6076
6200
  }
6077
- if (!stripped) {
6078
- return `Closes #${issue}`;
6201
+ const branchResult = spawnSync2("git", ["branch", branch, startBaseCommit], {
6202
+ cwd: root,
6203
+ encoding: "utf8",
6204
+ stdio: "pipe"
6205
+ });
6206
+ if (branchResult.status !== 0) {
6207
+ const stderr = branchResult.stderr?.toString?.() ?? "unknown error";
6208
+ return {
6209
+ ok: false,
6210
+ failureCode: "branch_creation_failed",
6211
+ message: `Failed to create local PRD branch ${branch} at ${startBaseCommit}: ${stderr}`
6212
+ };
6079
6213
  }
6080
- return `${stripped}
6081
-
6082
- Closes #${issue}`;
6083
- }
6084
- function stripClosingRefs(body) {
6085
- const linePattern = /^[ \t]*[-*+]\s+(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#\d+\s*$/gim;
6086
- let result = body.replace(linePattern, "");
6087
- const inlinePattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#\d+/gi;
6088
- result = result.replace(inlinePattern, "").trimEnd();
6089
- return result;
6214
+ return { ok: true, created: true };
6090
6215
  }
6091
- async function resolveBodyText(defaultBody, options) {
6092
- if (options.body && options.bodyFile) {
6093
- throw new Error("--body and --body-file cannot be used together");
6216
+ var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["dev", "next", "main"]);
6217
+ var LOCAL_BRANCH_PATTERN = /^local\/PRD-\d{4}(\/(I-\d{2}(-[a-z0-9-]+)?)?)?$/;
6218
+ function validateLocalBranchName(name) {
6219
+ if (!name || name.length === 0) {
6220
+ return {
6221
+ ok: false,
6222
+ failureCode: "invalid_format",
6223
+ message: "Branch name is empty."
6224
+ };
6094
6225
  }
6095
- if (options.body !== void 0) {
6096
- return options.body;
6226
+ if (isProtectedBranch(name)) {
6227
+ return {
6228
+ ok: false,
6229
+ failureCode: "protected_branch",
6230
+ message: `Branch "${name}" is protected.`
6231
+ };
6097
6232
  }
6098
- if (options.bodyFile !== void 0) {
6099
- return await readFile2(options.bodyFile, "utf-8");
6233
+ if (!LOCAL_BRANCH_PATTERN.test(name)) {
6234
+ return {
6235
+ ok: false,
6236
+ failureCode: "invalid_format",
6237
+ message: `Branch "${name}" does not match the local branch pattern.`
6238
+ };
6100
6239
  }
6101
- return defaultBody;
6240
+ return { ok: true };
6102
6241
  }
6103
- function appendClosingRef(body, issue) {
6104
- if (issue === void 0) {
6105
- return body;
6242
+ function isProtectedBranch(name) {
6243
+ return PROTECTED_BRANCHES.has(name);
6244
+ }
6245
+ function hasRemoteBackedCollision(localName, repoRoot2) {
6246
+ const match = localName.match(/^local\/(PRD-\d{4})/);
6247
+ if (!match) {
6248
+ return Promise.resolve({
6249
+ ok: false,
6250
+ failureCode: "invalid_format",
6251
+ message: `Cannot extract PRD ref from "${localName}".`
6252
+ });
6106
6253
  }
6107
- const existingRefs = extractClosingRefs(body);
6108
- if (existingRefs.has(issue)) {
6109
- return body;
6254
+ const prdRef = match[1];
6255
+ try {
6256
+ execFileSync(
6257
+ "git",
6258
+ ["show-ref", "--verify", "--quiet", `refs/heads/${prdRef}`],
6259
+ {
6260
+ cwd: repoRoot2 ?? process.cwd(),
6261
+ encoding: "utf8",
6262
+ stdio: "pipe"
6263
+ }
6264
+ );
6265
+ return Promise.resolve({
6266
+ ok: false,
6267
+ failureCode: "remote_backed_collision",
6268
+ message: `Branch "${prdRef}" exists locally.`
6269
+ });
6270
+ } catch {
6110
6271
  }
6111
- return `${body}
6272
+ try {
6273
+ execFileSync(
6274
+ "git",
6275
+ ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${prdRef}`],
6276
+ {
6277
+ cwd: repoRoot2 ?? process.cwd(),
6278
+ encoding: "utf8",
6279
+ stdio: "pipe"
6280
+ }
6281
+ );
6282
+ return Promise.resolve({
6283
+ ok: false,
6284
+ failureCode: "remote_backed_collision",
6285
+ message: `Remote branch "origin/${prdRef}" exists.`
6286
+ });
6287
+ } catch {
6288
+ }
6289
+ return Promise.resolve({ ok: true });
6290
+ }
6112
6291
 
6113
- Closes #${issue}`;
6292
+ // prd-run/local-merge-coordinator.ts
6293
+ function getLocalStorePath(repoRoot2, prdId) {
6294
+ return join13(repoRoot2, ".pourkit", "local-prd-runs", prdId);
6114
6295
  }
6115
- function extractClosingRefs(body) {
6116
- const refs = /* @__PURE__ */ new Set();
6117
- const pattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#(\d+)/gi;
6118
- let match;
6119
- while ((match = pattern.exec(body)) !== null) {
6120
- refs.add(parseInt(match[1], 10));
6296
+ function getMergeReceiptPath(repoRoot2, prdId, issueId) {
6297
+ return join13(
6298
+ getLocalStorePath(repoRoot2, prdId),
6299
+ "merge-receipts",
6300
+ `${issueId}.json`
6301
+ );
6302
+ }
6303
+ function getIssueArtifactPath(repoRoot2, prdId, issueId) {
6304
+ return join13(getLocalStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
6305
+ }
6306
+ function readIssueBranchName(repoRoot2, prdId, issueId) {
6307
+ const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
6308
+ if (!existsSync9(issuePath)) return null;
6309
+ try {
6310
+ const content = readFileSync11(issuePath, "utf-8");
6311
+ const parsed = JSON.parse(content);
6312
+ return typeof parsed.branchName === "string" && parsed.branchName ? parsed.branchName : null;
6313
+ } catch {
6314
+ return null;
6121
6315
  }
6122
- return refs;
6123
6316
  }
6124
-
6125
- // issues/target-green.ts
6126
- init_common();
6127
- var RED_CHECK_CONCLUSIONS = /* @__PURE__ */ new Set([
6128
- "FAILURE",
6129
- "CANCELLED",
6130
- "TIMED_OUT",
6131
- "STARTUP_FAILURE",
6132
- "ACTION_REQUIRED",
6133
- "STALE"
6134
- ]);
6135
- async function waitForBranchChecks(prProvider, logger, options) {
6136
- const checksFoundTimeoutMs = options.checksFoundTimeoutMs ?? 60 * 1e3;
6137
- const checksCompletionTimeoutMs = options.checksCompletionTimeoutMs ?? 5 * 60 * 1e3;
6138
- const pollIntervalMs = options.pollIntervalMs ?? 15 * 1e3;
6139
- const stableHeadMs = options.stableHeadMs ?? pollIntervalMs;
6140
- let lastHeadSha = "";
6141
- let headStableSince = 0;
6142
- let checksFoundDeadline = 0;
6143
- let checksCompletionDeadline = 0;
6144
- let checksDiscovered = false;
6145
- logger.step("wait", `waiting for ${options.branchName} to be green`);
6146
- while (true) {
6147
- const observedAt = Date.now();
6148
- const status = await prProvider.getBranchStatus(options.branchName);
6149
- if (status.headSha !== lastHeadSha) {
6150
- logger.step(
6151
- "info",
6152
- `branch head changed to ${status.headSha.substring(0, 7)}`
6153
- );
6154
- lastHeadSha = status.headSha;
6155
- headStableSince = observedAt;
6156
- if (checksFoundDeadline === 0) {
6157
- checksFoundDeadline = observedAt + checksFoundTimeoutMs;
6158
- }
6159
- checksCompletionDeadline = 0;
6160
- checksDiscovered = false;
6317
+ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
6318
+ const root = repoRoot2 ?? process.cwd();
6319
+ const receiptPath = getMergeReceiptPath(root, prdId, issueId);
6320
+ if (!existsSync9(receiptPath)) return null;
6321
+ try {
6322
+ const content = readFileSync11(receiptPath, "utf-8");
6323
+ const parsed = JSON.parse(content);
6324
+ 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") {
6325
+ return parsed;
6326
+ }
6327
+ return null;
6328
+ } catch {
6329
+ return null;
6330
+ }
6331
+ }
6332
+ async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
6333
+ const root = repoRoot2 ?? process.cwd();
6334
+ const receiptPath = getMergeReceiptPath(root, prdId, issueId);
6335
+ if (existsSync9(receiptPath)) {
6336
+ try {
6337
+ const existing = JSON.parse(
6338
+ readFileSync11(receiptPath, "utf-8")
6339
+ );
6340
+ if (existing.prdId === prdId && existing.issueId === issueId && existing.mergeCommit) {
6341
+ return {
6342
+ ok: false,
6343
+ failureCode: "already_merged",
6344
+ repairGuidance: `Issue ${issueId} was already merged into ${existing.localPrdBranch}. Merge commit: ${existing.mergeCommit}`
6345
+ };
6346
+ }
6347
+ return {
6348
+ ok: false,
6349
+ failureCode: "invalid_receipt",
6350
+ repairGuidance: `Merge receipt for ${issueId} belongs to different prd/issue. Remove it manually: ${receiptPath}`
6351
+ };
6352
+ } catch {
6353
+ return {
6354
+ ok: false,
6355
+ failureCode: "invalid_receipt",
6356
+ repairGuidance: `Merge receipt for ${issueId} is corrupted. Remove it manually: ${receiptPath}`
6357
+ };
6358
+ }
6359
+ }
6360
+ const sourceBranch = input?.sourceBranch ?? readIssueBranchName(root, prdId, issueId);
6361
+ if (!sourceBranch) {
6362
+ return {
6363
+ ok: false,
6364
+ failureCode: "not_found",
6365
+ repairGuidance: `No source branch found for issue ${issueId} under PRD ${prdId}. Ensure the issue artifact exists with a valid branchName field.`
6366
+ };
6367
+ }
6368
+ try {
6369
+ execFileSync2(
6370
+ "git",
6371
+ ["show-ref", "--verify", "--quiet", `refs/heads/${sourceBranch}`],
6372
+ { cwd: root, encoding: "utf8", stdio: "pipe" }
6373
+ );
6374
+ } catch {
6375
+ return {
6376
+ ok: false,
6377
+ failureCode: "not_found",
6378
+ repairGuidance: `Source branch "${sourceBranch}" does not exist locally. Check that the branch was created and not deleted.`
6379
+ };
6380
+ }
6381
+ const targetBranch = getLocalPrdBranchName(prdId);
6382
+ try {
6383
+ execFileSync2(
6384
+ "git",
6385
+ ["show-ref", "--verify", "--quiet", `refs/heads/${targetBranch}`],
6386
+ { cwd: root, encoding: "utf8", stdio: "pipe" }
6387
+ );
6388
+ } catch {
6389
+ return {
6390
+ ok: false,
6391
+ failureCode: "not_found",
6392
+ repairGuidance: `Local PRD branch "${targetBranch}" does not exist. Run \`prd-run start\` first to create it.`
6393
+ };
6394
+ }
6395
+ try {
6396
+ execFileSync2("git", ["checkout", targetBranch], {
6397
+ cwd: root,
6398
+ encoding: "utf8",
6399
+ stdio: "pipe"
6400
+ });
6401
+ let preMergeHead;
6402
+ try {
6403
+ preMergeHead = execFileSync2("git", ["rev-parse", "HEAD"], {
6404
+ cwd: root,
6405
+ encoding: "utf8",
6406
+ stdio: "pipe"
6407
+ }).trim();
6408
+ } catch {
6409
+ return {
6410
+ ok: false,
6411
+ failureCode: "merge_error",
6412
+ repairGuidance: "Failed to read current HEAD before merge."
6413
+ };
6414
+ }
6415
+ execFileSync2("git", ["merge", "--squash", sourceBranch], {
6416
+ cwd: root,
6417
+ encoding: "utf8",
6418
+ stdio: "pipe"
6419
+ });
6420
+ if (input) {
6421
+ execFileSync2(
6422
+ "git",
6423
+ ["commit", "-m", input.finalizerTitle, "-m", input.finalizerBody],
6424
+ { cwd: root, encoding: "utf8", stdio: "pipe" }
6425
+ );
6426
+ } else {
6427
+ execFileSync2(
6428
+ "git",
6429
+ ["commit", "-m", `Squash merge ${sourceBranch} into ${targetBranch}`],
6430
+ { cwd: root, encoding: "utf8", stdio: "pipe" }
6431
+ );
6432
+ }
6433
+ const revParseResult = execFileSync2("git", ["rev-parse", "HEAD"], {
6434
+ cwd: root,
6435
+ encoding: "utf8",
6436
+ stdio: "pipe"
6437
+ });
6438
+ const mergeCommit = revParseResult.trim();
6439
+ const receipt = {
6440
+ prdId,
6441
+ issueId,
6442
+ stage: "child-issue",
6443
+ sourceBranch,
6444
+ localPrdBranch: targetBranch,
6445
+ mergeCommit,
6446
+ finalizerArtifactPath: input?.finalizerArtifactPath ?? "",
6447
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
6448
+ };
6449
+ try {
6450
+ const receiptsDir = join13(
6451
+ root,
6452
+ ".pourkit",
6453
+ "local-prd-runs",
6454
+ prdId,
6455
+ "merge-receipts"
6456
+ );
6457
+ mkdirSync7(receiptsDir, { recursive: true });
6458
+ writeFileSync4(receiptPath, JSON.stringify(receipt, null, 2), "utf-8");
6459
+ } catch {
6460
+ try {
6461
+ execFileSync2("git", ["reset", "--hard", preMergeHead], {
6462
+ cwd: root,
6463
+ encoding: "utf8",
6464
+ stdio: "pipe"
6465
+ });
6466
+ } catch {
6467
+ }
6468
+ return {
6469
+ ok: false,
6470
+ failureCode: "receipt_error",
6471
+ repairGuidance: "Failed to write merge receipt. Check disk space and permissions on .pourkit/local-prd-runs/. The merge commit has been rolled back."
6472
+ };
6473
+ }
6474
+ return { ok: true, receipt };
6475
+ } catch (error) {
6476
+ const message = error instanceof Error ? error.message : String(error);
6477
+ if (message.toLowerCase().includes("conflict")) {
6478
+ return {
6479
+ ok: false,
6480
+ failureCode: "conflict",
6481
+ repairGuidance: "Merge conflict during squash. Resolve conflicts in the working tree, then retry."
6482
+ };
6483
+ }
6484
+ const stderr = error instanceof Error && "stderr" in error ? error.stderr : void 0;
6485
+ return {
6486
+ ok: false,
6487
+ failureCode: "merge_error",
6488
+ repairGuidance: stderr ? `Merge failed: ${stderr}` : `Merge failed: ${message}`
6489
+ };
6490
+ }
6491
+ }
6492
+
6493
+ // pr/pr-body.ts
6494
+ import { readFile as readFile2 } from "fs/promises";
6495
+ var DEFAULT_MANUAL_PR_BODY = `## Summary
6496
+
6497
+ - Summarize why this branch exists.
6498
+
6499
+ ## Changes
6500
+
6501
+ - Summarize the final net change in this branch.`;
6502
+ async function buildPrBody(input) {
6503
+ const { defaultBody, options } = input;
6504
+ let bodyText = await resolveBodyText(defaultBody, options);
6505
+ bodyText = appendClosingRef(bodyText, options.issue);
6506
+ return bodyText;
6507
+ }
6508
+ function ensureClosingRefs(body, issue) {
6509
+ const stripped = stripClosingRefs(body || "");
6510
+ if (issue === void 0) {
6511
+ return stripped;
6512
+ }
6513
+ if (!stripped) {
6514
+ return `Closes #${issue}`;
6515
+ }
6516
+ return `${stripped}
6517
+
6518
+ Closes #${issue}`;
6519
+ }
6520
+ function stripClosingRefs(body) {
6521
+ const linePattern = /^[ \t]*[-*+]\s+(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#\d+\s*$/gim;
6522
+ let result = body.replace(linePattern, "");
6523
+ const inlinePattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#\d+/gi;
6524
+ result = result.replace(inlinePattern, "").trimEnd();
6525
+ return result;
6526
+ }
6527
+ async function resolveBodyText(defaultBody, options) {
6528
+ if (options.body && options.bodyFile) {
6529
+ throw new Error("--body and --body-file cannot be used together");
6530
+ }
6531
+ if (options.body !== void 0) {
6532
+ return options.body;
6533
+ }
6534
+ if (options.bodyFile !== void 0) {
6535
+ return await readFile2(options.bodyFile, "utf-8");
6536
+ }
6537
+ return defaultBody;
6538
+ }
6539
+ function appendClosingRef(body, issue) {
6540
+ if (issue === void 0) {
6541
+ return body;
6542
+ }
6543
+ const existingRefs = extractClosingRefs(body);
6544
+ if (existingRefs.has(issue)) {
6545
+ return body;
6546
+ }
6547
+ return `${body}
6548
+
6549
+ Closes #${issue}`;
6550
+ }
6551
+ function extractClosingRefs(body) {
6552
+ const refs = /* @__PURE__ */ new Set();
6553
+ const pattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*:?\s*#(\d+)/gi;
6554
+ let match;
6555
+ while ((match = pattern.exec(body)) !== null) {
6556
+ refs.add(parseInt(match[1], 10));
6557
+ }
6558
+ return refs;
6559
+ }
6560
+
6561
+ // issues/target-green.ts
6562
+ init_common();
6563
+ var RED_CHECK_CONCLUSIONS = /* @__PURE__ */ new Set([
6564
+ "FAILURE",
6565
+ "CANCELLED",
6566
+ "TIMED_OUT",
6567
+ "STARTUP_FAILURE",
6568
+ "ACTION_REQUIRED",
6569
+ "STALE"
6570
+ ]);
6571
+ async function waitForBranchChecks(prProvider, logger, options) {
6572
+ const checksFoundTimeoutMs = options.checksFoundTimeoutMs ?? 60 * 1e3;
6573
+ const checksCompletionTimeoutMs = options.checksCompletionTimeoutMs ?? 5 * 60 * 1e3;
6574
+ const pollIntervalMs = options.pollIntervalMs ?? 15 * 1e3;
6575
+ const stableHeadMs = options.stableHeadMs ?? pollIntervalMs;
6576
+ let lastHeadSha = "";
6577
+ let headStableSince = 0;
6578
+ let checksFoundDeadline = 0;
6579
+ let checksCompletionDeadline = 0;
6580
+ let checksDiscovered = false;
6581
+ logger.step("wait", `waiting for ${options.branchName} to be green`);
6582
+ while (true) {
6583
+ const observedAt = Date.now();
6584
+ const status = await prProvider.getBranchStatus(options.branchName);
6585
+ if (status.headSha !== lastHeadSha) {
6586
+ logger.step(
6587
+ "info",
6588
+ `branch head changed to ${status.headSha.substring(0, 7)}`
6589
+ );
6590
+ lastHeadSha = status.headSha;
6591
+ headStableSince = observedAt;
6592
+ if (checksFoundDeadline === 0) {
6593
+ checksFoundDeadline = observedAt + checksFoundTimeoutMs;
6594
+ }
6595
+ checksCompletionDeadline = 0;
6596
+ checksDiscovered = false;
6161
6597
  }
6162
6598
  if (status.checks.length > 0 && !checksDiscovered) {
6163
6599
  checksDiscovered = true;
@@ -6786,44 +7222,75 @@ async function completeIssueRun(options) {
6786
7222
  let prTitle = issue.title;
6787
7223
  let prBody;
6788
7224
  let finalizerResult;
7225
+ const isLocalModeForFinalizer = options.prdRunMode?.mode === "local";
6789
7226
  const finalizerFromState = worktreeState?.finalizer?.completed ? worktreeState.finalizer : null;
6790
7227
  if (finalizerFromState) {
6791
- if (finalizerFromState.title && finalizerFromState.body) {
6792
- prTitle = finalizerFromState.title;
6793
- prBody = finalizerFromState.body;
6794
- } else if (finalizerFromState.artifactPath) {
6795
- if (!existsSync9(finalizerFromState.artifactPath)) {
7228
+ try {
7229
+ if (finalizerFromState.title && finalizerFromState.body) {
7230
+ prTitle = finalizerFromState.title;
7231
+ prBody = finalizerFromState.body;
7232
+ } else if (finalizerFromState.artifactPath) {
7233
+ if (!existsSync10(finalizerFromState.artifactPath)) {
7234
+ throw new FinalizerFailure({
7235
+ message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
7236
+ });
7237
+ }
7238
+ const artifactContent = readFileSync12(
7239
+ finalizerFromState.artifactPath,
7240
+ "utf-8"
7241
+ );
7242
+ const parsed = parsePrDescription(artifactContent);
7243
+ prTitle = parsed.title;
7244
+ prBody = parsed.body;
7245
+ } else {
6796
7246
  throw new FinalizerFailure({
6797
- message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
7247
+ message: "Finalizer state is incomplete: missing title, body, and artifactPath"
6798
7248
  });
6799
7249
  }
6800
- const artifactContent = readFileSync11(
6801
- finalizerFromState.artifactPath,
6802
- "utf-8"
6803
- );
6804
- const parsed = parsePrDescription(artifactContent);
6805
- prTitle = parsed.title;
6806
- prBody = parsed.body;
6807
- } else {
6808
- throw new FinalizerFailure({
6809
- message: "Finalizer state is incomplete: missing title, body, and artifactPath"
6810
- });
7250
+ } catch (error) {
7251
+ if (isLocalModeForFinalizer) {
7252
+ return {
7253
+ branchName,
7254
+ target,
7255
+ issue,
7256
+ noOp: false,
7257
+ mode: "local",
7258
+ failureCode: "finalizer_failed",
7259
+ repairGuidance: error instanceof Error ? error.message : "Finalizer state is invalid"
7260
+ };
7261
+ }
7262
+ throw error;
6811
7263
  }
6812
7264
  } else {
6813
- finalizerResult = await runEffectAndMapExit(
6814
- runFinalizerAgent({
6815
- executionProvider,
6816
- config,
6817
- target,
6818
- issue,
6819
- builderBranch: branchName,
6820
- targetBaseBranch: effectiveBaseBranch,
6821
- worktreePath: executionResult.worktreePath,
6822
- reviewArtifactPath,
6823
- repoRoot: ROOT,
6824
- logger
6825
- })
6826
- );
7265
+ try {
7266
+ finalizerResult = await runEffectAndMapExit(
7267
+ runFinalizerAgent({
7268
+ executionProvider,
7269
+ config,
7270
+ target,
7271
+ issue,
7272
+ builderBranch: branchName,
7273
+ targetBaseBranch: effectiveBaseBranch,
7274
+ worktreePath: executionResult.worktreePath,
7275
+ reviewArtifactPath,
7276
+ repoRoot: ROOT,
7277
+ logger
7278
+ })
7279
+ );
7280
+ } catch (error) {
7281
+ if (isLocalModeForFinalizer) {
7282
+ return {
7283
+ branchName,
7284
+ target,
7285
+ issue,
7286
+ noOp: false,
7287
+ mode: "local",
7288
+ failureCode: "finalizer_failed",
7289
+ repairGuidance: error instanceof Error ? error.message : "Finalizer execution failed"
7290
+ };
7291
+ }
7292
+ throw error;
7293
+ }
6827
7294
  prTitle = finalizerResult.title;
6828
7295
  prBody = finalizerResult.body;
6829
7296
  }
@@ -6868,6 +7335,113 @@ async function completeIssueRun(options) {
6868
7335
  });
6869
7336
  }
6870
7337
  }
7338
+ const isLocalMode = options.prdRunMode?.mode === "local";
7339
+ if (isLocalMode) {
7340
+ const parsed = parseStackedIssue(issue.title, issue.body);
7341
+ const prdId = parsed.parentRef;
7342
+ if (!prdId) {
7343
+ return {
7344
+ branchName,
7345
+ target,
7346
+ issue,
7347
+ noOp: false,
7348
+ mode: "local",
7349
+ failureCode: "finalizer_failed",
7350
+ repairGuidance: "Local mode requires a PRD reference (## Parent) in the issue body or title. Add the parent PRD reference and retry."
7351
+ };
7352
+ }
7353
+ const finalizerFromWorktreeState = worktreeState?.finalizer;
7354
+ if (finalizerFromWorktreeState && !finalizerFromWorktreeState.completed) {
7355
+ return {
7356
+ branchName,
7357
+ target,
7358
+ issue,
7359
+ noOp: false,
7360
+ mode: "local",
7361
+ failureCode: "finalizer_failed",
7362
+ repairGuidance: "Finalizer did not complete on previous run. Re-run the issue finalizer and try again."
7363
+ };
7364
+ }
7365
+ if (!prTitle || !finalBody) {
7366
+ return {
7367
+ branchName,
7368
+ target,
7369
+ issue,
7370
+ noOp: false,
7371
+ mode: "local",
7372
+ failureCode: "finalizer_failed",
7373
+ repairGuidance: "Finalizer produced empty title or body. Re-run the issue finalizer and try again."
7374
+ };
7375
+ }
7376
+ const existingReceipt = await hasLocalIssueMergeReceipt(
7377
+ prdId,
7378
+ `issue-${issueNumber}`,
7379
+ ROOT
7380
+ );
7381
+ if (existingReceipt) {
7382
+ return {
7383
+ branchName,
7384
+ target,
7385
+ issue,
7386
+ noOp: false,
7387
+ mode: "local",
7388
+ failureCode: "already_merged",
7389
+ localPrdBranch: existingReceipt.localPrdBranch,
7390
+ mergeCommit: existingReceipt.mergeCommit,
7391
+ receiptPath: join14(
7392
+ ROOT,
7393
+ ".pourkit",
7394
+ "local-prd-runs",
7395
+ prdId,
7396
+ "merge-receipts",
7397
+ `issue-${issueNumber}.json`
7398
+ )
7399
+ };
7400
+ }
7401
+ const mergeResult = await squashMergeLocalIssue(
7402
+ prdId,
7403
+ `issue-${issueNumber}`,
7404
+ {
7405
+ finalizerTitle: prTitle,
7406
+ finalizerBody: finalBody,
7407
+ finalizerArtifactPath: worktreeState?.finalizer?.artifactPath ?? "",
7408
+ sourceBranch: branchName
7409
+ },
7410
+ ROOT
7411
+ );
7412
+ if (!mergeResult.ok) {
7413
+ return {
7414
+ branchName,
7415
+ target,
7416
+ issue,
7417
+ noOp: false,
7418
+ mode: "local",
7419
+ failureCode: mergeResult.failureCode,
7420
+ repairGuidance: mergeResult.repairGuidance ?? `Local squash-merge failed: ${mergeResult.failureCode}`
7421
+ };
7422
+ }
7423
+ await issueProvider.removeLabel(
7424
+ issueNumber,
7425
+ config.labels.agentInProgress
7426
+ );
7427
+ return {
7428
+ branchName,
7429
+ target,
7430
+ issue,
7431
+ noOp: false,
7432
+ mode: "local",
7433
+ localPrdBranch: getLocalPrdBranchName(prdId),
7434
+ mergeCommit: mergeResult.receipt.mergeCommit,
7435
+ receiptPath: join14(
7436
+ ROOT,
7437
+ ".pourkit",
7438
+ "local-prd-runs",
7439
+ prdId,
7440
+ "merge-receipts",
7441
+ `issue-${issueNumber}.json`
7442
+ )
7443
+ };
7444
+ }
6871
7445
  let pr;
6872
7446
  if (worktreeState?.pr?.merged) {
6873
7447
  mergeCompleted = true;
@@ -7309,7 +7883,7 @@ async function resolveIssueWorktree(root, branchName, baseBranch, logger) {
7309
7883
  return { mode: "new", branchName, baseRef };
7310
7884
  }
7311
7885
  function issueWorktreePath(root, branchName) {
7312
- return join13(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
7886
+ return join14(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
7313
7887
  }
7314
7888
  function resolveRegisteredIssueWorktreePath(worktreeListPorcelain, root, branchName) {
7315
7889
  const branchWorktreePath = parseWorktreeListPorcelain(
@@ -7337,7 +7911,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
7337
7911
  }
7338
7912
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
7339
7913
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
7340
- const promptBody = existsSync9(promptPath) ? readFileSync11(promptPath, "utf-8") : promptTemplate;
7914
+ const promptBody = existsSync10(promptPath) ? readFileSync12(promptPath, "utf-8") : promptTemplate;
7341
7915
  return appendProtectedWorkGuidance(`${promptBody}
7342
7916
 
7343
7917
  ## Shared Run Context
@@ -7780,29 +8354,29 @@ async function runIssueCreateCommand(args, issueProvider, logger) {
7780
8354
  // commands/prd-run.ts
7781
8355
  import {
7782
8356
  cpSync,
7783
- existsSync as existsSync16,
8357
+ existsSync as existsSync17,
7784
8358
  lstatSync,
7785
- mkdirSync as mkdirSync10,
8359
+ mkdirSync as mkdirSync11,
7786
8360
  mkdtempSync,
7787
- readdirSync as readdirSync6,
7788
- readFileSync as readFileSync15,
8361
+ readdirSync as readdirSync5,
8362
+ readFileSync as readFileSync17,
7789
8363
  realpathSync,
7790
8364
  rmSync as rmSync3
7791
8365
  } from "fs";
7792
- import { spawnSync as spawnSync2 } from "child_process";
7793
- import { dirname as dirname5, join as join19, relative as relative2 } from "path";
8366
+ import { spawnSync as spawnSync3 } from "child_process";
8367
+ import { dirname as dirname5, join as join21, relative as relative2 } from "path";
7794
8368
  import { tmpdir } from "os";
7795
8369
 
7796
8370
  // prd-run/state.ts
7797
8371
  import {
7798
- existsSync as existsSync10,
7799
- mkdirSync as mkdirSync7,
7800
- readFileSync as readFileSync12,
8372
+ existsSync as existsSync11,
8373
+ mkdirSync as mkdirSync8,
8374
+ readFileSync as readFileSync13,
7801
8375
  readdirSync as readdirSync4,
7802
- writeFileSync as writeFileSync4
8376
+ writeFileSync as writeFileSync5
7803
8377
  } from "fs";
7804
8378
  import { mkdir as mkdir4, readFile as readFile4, writeFile } from "fs/promises";
7805
- import { join as join14 } from "path";
8379
+ import { join as join15 } from "path";
7806
8380
  import { z as z3 } from "zod";
7807
8381
  var PRD_RUN_STATE_DIR = ".pourkit/prd-runs";
7808
8382
  var PrdRunRecordSchema = z3.object({
@@ -7820,9 +8394,11 @@ var PrdRunRecordSchema = z3.object({
7820
8394
  "waiting_for_integration",
7821
8395
  "finalizing",
7822
8396
  "final_reviewed",
7823
- "complete"
8397
+ "complete",
8398
+ "completed_local_branch"
7824
8399
  ]),
7825
8400
  updatedAt: z3.string().min(1),
8401
+ mode: z3.enum(["github", "local"]).optional(),
7826
8402
  blockedGate: z3.enum([
7827
8403
  "manifest-location",
7828
8404
  "manifest-readiness",
@@ -7946,12 +8522,12 @@ function normalizePrdRunRef2(ref) {
7946
8522
  function readPrdRun(repoRoot2, prdRef) {
7947
8523
  const normalized = normalizePrdRunRef2(prdRef);
7948
8524
  const recordPath = getRecordPath(repoRoot2, normalized);
7949
- if (!existsSync10(recordPath)) {
8525
+ if (!existsSync11(recordPath)) {
7950
8526
  return { record: null, diagnostics: [] };
7951
8527
  }
7952
8528
  try {
7953
8529
  const parsed = PrdRunRecordSchema.parse(
7954
- JSON.parse(readFileSync12(recordPath, "utf-8"))
8530
+ JSON.parse(readFileSync13(recordPath, "utf-8"))
7955
8531
  );
7956
8532
  return { record: parsed, diagnostics: [] };
7957
8533
  } catch (error) {
@@ -7965,8 +8541,8 @@ function readPrdRun(repoRoot2, prdRef) {
7965
8541
  }
7966
8542
  }
7967
8543
  function listPrdRuns(repoRoot2) {
7968
- const stateDir = join14(repoRoot2, PRD_RUN_STATE_DIR);
7969
- if (!existsSync10(stateDir)) {
8544
+ const stateDir = join15(repoRoot2, PRD_RUN_STATE_DIR);
8545
+ if (!existsSync11(stateDir)) {
7970
8546
  return { records: [], diagnostics: [] };
7971
8547
  }
7972
8548
  const records = [];
@@ -7975,10 +8551,10 @@ function listPrdRuns(repoRoot2) {
7975
8551
  (left, right) => left.name.localeCompare(right.name)
7976
8552
  )) {
7977
8553
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
7978
- const recordPath = join14(stateDir, entry.name);
8554
+ const recordPath = join15(stateDir, entry.name);
7979
8555
  try {
7980
8556
  const record = PrdRunRecordSchema.parse(
7981
- JSON.parse(readFileSync12(recordPath, "utf-8"))
8557
+ JSON.parse(readFileSync13(recordPath, "utf-8"))
7982
8558
  );
7983
8559
  records.push(record);
7984
8560
  } catch (error) {
@@ -7991,10 +8567,10 @@ function listPrdRuns(repoRoot2) {
7991
8567
  }
7992
8568
  function writePrdRunRecord(repoRoot2, record) {
7993
8569
  const normalized = normalizePrdRunRef2(record.prdRef);
7994
- const stateDir = join14(repoRoot2, PRD_RUN_STATE_DIR);
8570
+ const stateDir = join15(repoRoot2, PRD_RUN_STATE_DIR);
7995
8571
  const recordPath = getRecordPath(repoRoot2, normalized);
7996
- mkdirSync7(stateDir, { recursive: true });
7997
- writeFileSync4(
8572
+ mkdirSync8(stateDir, { recursive: true });
8573
+ writeFileSync5(
7998
8574
  recordPath,
7999
8575
  JSON.stringify({ ...record, prdRef: normalized }, null, 2),
8000
8576
  "utf-8"
@@ -8031,8 +8607,8 @@ var LocalPrdRunRecordSchema = z3.object({
8031
8607
  }),
8032
8608
  metadata: z3.record(z3.unknown())
8033
8609
  });
8034
- function getLocalStorePath(repoRoot2, prdId) {
8035
- return join14(
8610
+ function getLocalStorePath2(repoRoot2, prdId) {
8611
+ return join15(
8036
8612
  repoRoot2,
8037
8613
  LOCAL_PRD_RUN_STATE_DIR,
8038
8614
  `${normalizePrdRunRef2(prdId)}.json`
@@ -8040,8 +8616,8 @@ function getLocalStorePath(repoRoot2, prdId) {
8040
8616
  }
8041
8617
  async function readLocalPrdRun(repoRoot2, prdId) {
8042
8618
  const normalized = normalizePrdRunRef2(prdId);
8043
- const recordPath = getLocalStorePath(repoRoot2, normalized);
8044
- if (!existsSync10(recordPath)) {
8619
+ const recordPath = getLocalStorePath2(repoRoot2, normalized);
8620
+ if (!existsSync11(recordPath)) {
8045
8621
  return null;
8046
8622
  }
8047
8623
  try {
@@ -8053,16 +8629,16 @@ async function readLocalPrdRun(repoRoot2, prdId) {
8053
8629
  }
8054
8630
  async function writeLocalPrdRunRecord(repoRoot2, prdId, record) {
8055
8631
  const normalized = normalizePrdRunRef2(prdId);
8056
- const storeDir = join14(repoRoot2, LOCAL_PRD_RUN_STATE_DIR);
8632
+ const storeDir = join15(repoRoot2, LOCAL_PRD_RUN_STATE_DIR);
8057
8633
  await mkdir4(storeDir, { recursive: true });
8058
8634
  await writeFile(
8059
- getLocalStorePath(repoRoot2, normalized),
8635
+ getLocalStorePath2(repoRoot2, normalized),
8060
8636
  JSON.stringify({ ...record, prdId: normalized }, null, 2),
8061
8637
  "utf-8"
8062
8638
  );
8063
8639
  }
8064
8640
  function getRecordPath(repoRoot2, prdRef) {
8065
- return join14(
8641
+ return join15(
8066
8642
  repoRoot2,
8067
8643
  PRD_RUN_STATE_DIR,
8068
8644
  `${normalizePrdRunRef2(prdRef)}.json`
@@ -8195,9 +8771,9 @@ function redactSensitiveValues(input) {
8195
8771
  }
8196
8772
 
8197
8773
  // prd-run/final-review-validation.ts
8198
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
8774
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
8199
8775
  function parseFinalReviewArtifact(artifactPath) {
8200
- if (!existsSync11(artifactPath)) {
8776
+ if (!existsSync12(artifactPath)) {
8201
8777
  return {
8202
8778
  ok: false,
8203
8779
  reason: "Final Review artifact not found.",
@@ -8206,7 +8782,7 @@ function parseFinalReviewArtifact(artifactPath) {
8206
8782
  }
8207
8783
  let content;
8208
8784
  try {
8209
- content = readFileSync13(artifactPath, "utf-8");
8785
+ content = readFileSync14(artifactPath, "utf-8");
8210
8786
  } catch (error) {
8211
8787
  return {
8212
8788
  ok: false,
@@ -8386,8 +8962,8 @@ function isRetouchScopePath(path9) {
8386
8962
  init_common();
8387
8963
 
8388
8964
  // prd-run/local-artifacts.ts
8389
- import { existsSync as existsSync12 } from "fs";
8390
- import { join as join15 } from "path";
8965
+ import { existsSync as existsSync13 } from "fs";
8966
+ import { join as join16 } from "path";
8391
8967
  var REQUIRED_PRD_FIELDS = [
8392
8968
  "schemaVersion",
8393
8969
  "kind",
@@ -8420,13 +8996,13 @@ var REQUIRED_ISSUE_FIELDS = [
8420
8996
  "githubProjection"
8421
8997
  ];
8422
8998
  function prdStorePath(repoRoot2, prdId) {
8423
- return join15(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8999
+ return join16(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8424
9000
  }
8425
9001
  function prdArtifactPath(repoRoot2, prdId) {
8426
- return join15(prdStorePath(repoRoot2, prdId), "prd.json");
9002
+ return join16(prdStorePath(repoRoot2, prdId), "prd.json");
8427
9003
  }
8428
9004
  function issueArtifactPath(repoRoot2, prdId, issueId) {
8429
- return join15(prdStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
9005
+ return join16(prdStorePath(repoRoot2, prdId), "issues", `${issueId}.json`);
8430
9006
  }
8431
9007
  function hasRequiredFields(data, requiredFields) {
8432
9008
  for (const field of requiredFields) {
@@ -8439,7 +9015,7 @@ function hasRequiredFields(data, requiredFields) {
8439
9015
  async function resolveLocalPrdArtifact(prdId, repoRoot2) {
8440
9016
  const root = repoRoot2 ?? process.cwd();
8441
9017
  const prdPath = prdArtifactPath(root, prdId);
8442
- if (!existsSync12(prdPath)) {
9018
+ if (!existsSync13(prdPath)) {
8443
9019
  return {
8444
9020
  ok: false,
8445
9021
  failureCode: "missing_prd_artifact",
@@ -8484,7 +9060,7 @@ async function resolveLocalIssueArtifacts(prdId, repoRoot2) {
8484
9060
  const issues = [];
8485
9061
  for (const childId of childIssueIds) {
8486
9062
  const issuePath = issueArtifactPath(root, prdId, childId);
8487
- if (!existsSync12(issuePath)) {
9063
+ if (!existsSync13(issuePath)) {
8488
9064
  return {
8489
9065
  ok: false,
8490
9066
  failureCode: "missing_child_issue",
@@ -8620,97 +9196,36 @@ async function getRunnableLocalIssues(prdId, repoRoot2) {
8620
9196
  }
8621
9197
 
8622
9198
  // prd-run/local-final-review.ts
8623
- import { execFileSync as execFileSync2, execSync as execSync2 } from "child_process";
8624
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync5 } from "fs";
8625
- import { join as join16 } from "path";
8626
-
8627
- // prd-run/local-branches.ts
8628
- import { execFileSync } from "child_process";
8629
- function getLocalPrdBranchName(prdId) {
8630
- return `local/${prdId}`;
8631
- }
8632
- var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["dev", "next", "main"]);
8633
- var LOCAL_BRANCH_PATTERN = /^local\/PRD-\d{4}(\/(I-\d{2}(-[a-z0-9-]+)?)?)?$/;
8634
- function validateLocalBranchName(name) {
8635
- if (!name || name.length === 0) {
8636
- return {
8637
- ok: false,
8638
- failureCode: "invalid_format",
8639
- message: "Branch name is empty."
8640
- };
8641
- }
8642
- if (isProtectedBranch(name)) {
8643
- return {
8644
- ok: false,
8645
- failureCode: "protected_branch",
8646
- message: `Branch "${name}" is protected.`
8647
- };
8648
- }
8649
- if (!LOCAL_BRANCH_PATTERN.test(name)) {
8650
- return {
8651
- ok: false,
8652
- failureCode: "invalid_format",
8653
- message: `Branch "${name}" does not match the local branch pattern.`
8654
- };
8655
- }
8656
- return { ok: true };
9199
+ import { execFileSync as execFileSync3, execSync as execSync2 } from "child_process";
9200
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
9201
+ import { join as join17 } from "path";
9202
+ function getLocalStorePath3(repoRoot2, prdId) {
9203
+ return join17(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8657
9204
  }
8658
- function isProtectedBranch(name) {
8659
- return PROTECTED_BRANCHES.has(name);
9205
+ function getFinalReviewReceiptPath(repoRoot2, prdId) {
9206
+ return join17(getLocalStorePath3(repoRoot2, prdId), "final-review-receipt.json");
8660
9207
  }
8661
- function hasRemoteBackedCollision(localName, repoRoot2) {
8662
- const match = localName.match(/^local\/(PRD-\d{4})/);
8663
- if (!match) {
8664
- return Promise.resolve({
8665
- ok: false,
8666
- failureCode: "invalid_format",
8667
- message: `Cannot extract PRD ref from "${localName}".`
8668
- });
8669
- }
8670
- const prdRef = match[1];
8671
- try {
8672
- execFileSync(
8673
- "git",
8674
- ["show-ref", "--verify", "--quiet", `refs/heads/${prdRef}`],
8675
- {
8676
- cwd: repoRoot2 ?? process.cwd(),
8677
- encoding: "utf8",
8678
- stdio: "pipe"
8679
- }
8680
- );
8681
- return Promise.resolve({
8682
- ok: false,
8683
- failureCode: "remote_backed_collision",
8684
- message: `Branch "${prdRef}" exists locally.`
8685
- });
8686
- } catch {
8687
- }
8688
- try {
8689
- execFileSync(
8690
- "git",
8691
- ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${prdRef}`],
8692
- {
8693
- cwd: repoRoot2 ?? process.cwd(),
9208
+ function computeLocalMergeBase(repoRoot2, prdBranch) {
9209
+ const candidates = ["dev", "main", "master", "HEAD"];
9210
+ for (const base of candidates) {
9211
+ try {
9212
+ const result = execFileSync3("git", ["merge-base", base, prdBranch], {
9213
+ cwd: repoRoot2,
8694
9214
  encoding: "utf8",
8695
9215
  stdio: "pipe"
9216
+ });
9217
+ const mergeBase = result.trim();
9218
+ if (mergeBase && mergeBase.length >= 6) {
9219
+ return { ok: true, mergeBase };
8696
9220
  }
8697
- );
8698
- return Promise.resolve({
8699
- ok: false,
8700
- failureCode: "remote_backed_collision",
8701
- message: `Remote branch "origin/${prdRef}" exists.`
8702
- });
8703
- } catch {
9221
+ } catch {
9222
+ continue;
9223
+ }
8704
9224
  }
8705
- return Promise.resolve({ ok: true });
8706
- }
8707
-
8708
- // prd-run/local-final-review.ts
8709
- function getLocalStorePath2(repoRoot2, prdId) {
8710
- return join16(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8711
- }
8712
- function getFinalReviewReceiptPath(repoRoot2, prdId) {
8713
- return join16(getLocalStorePath2(repoRoot2, prdId), "final-review-receipt.json");
9225
+ return {
9226
+ ok: false,
9227
+ reason: "Could not compute merge base against any known base branch (dev/main/master/HEAD)."
9228
+ };
8714
9229
  }
8715
9230
  async function runLocalFinalReview(prdId, options) {
8716
9231
  const root = options?.repoRoot ?? process.cwd();
@@ -8724,6 +9239,34 @@ async function runLocalFinalReview(prdId, options) {
8724
9239
  };
8725
9240
  }
8726
9241
  const branch = getLocalPrdBranchName(prdId);
9242
+ const mergeBaseResult = computeLocalMergeBase(root, branch);
9243
+ if (!mergeBaseResult.ok) {
9244
+ return {
9245
+ ok: false,
9246
+ failureCode: "merge_base_failed",
9247
+ message: mergeBaseResult.reason
9248
+ };
9249
+ }
9250
+ const semanticResult = validateFinalReviewArtifactSemanticIds(
9251
+ {
9252
+ prdRef: prdId,
9253
+ stage: "prdFinalReview",
9254
+ checkoutBase: branch,
9255
+ reviewBase: mergeBaseResult.mergeBase
9256
+ },
9257
+ {
9258
+ prdRef: prdId,
9259
+ prdBranch: branch,
9260
+ mergeBase: mergeBaseResult.mergeBase
9261
+ }
9262
+ );
9263
+ if (!semanticResult.ok) {
9264
+ return {
9265
+ ok: false,
9266
+ failureCode: "invalid_artifact_semantics",
9267
+ message: semanticResult.errors.join("; ")
9268
+ };
9269
+ }
8727
9270
  const failures = [];
8728
9271
  for (const cmd of commands) {
8729
9272
  try {
@@ -8744,7 +9287,7 @@ async function runLocalFinalReview(prdId, options) {
8744
9287
  });
8745
9288
  verdict = "pass_with_retouch";
8746
9289
  } catch {
8747
- verdict = "pass";
9290
+ verdict = "pass_no_changes";
8748
9291
  }
8749
9292
  } else if (failures.length === commands.length) {
8750
9293
  verdict = "blocked";
@@ -8762,13 +9305,14 @@ async function runLocalFinalReview(prdId, options) {
8762
9305
  }
8763
9306
  }
8764
9307
  const receipt = {
8765
- reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
9308
+ reviewedTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
8766
9309
  verdict,
8767
- branch
9310
+ prdBranch: branch,
9311
+ mergeBase: mergeBaseResult.mergeBase
8768
9312
  };
8769
9313
  const receiptPath = getFinalReviewReceiptPath(root, prdId);
8770
- mkdirSync8(getLocalStorePath2(root, prdId), { recursive: true });
8771
- writeFileSync5(receiptPath, JSON.stringify(receipt, null, 2), "utf-8");
9314
+ mkdirSync9(getLocalStorePath3(root, prdId), { recursive: true });
9315
+ writeFileSync6(receiptPath, JSON.stringify(receipt, null, 2), "utf-8");
8772
9316
  const result = { ok: true, verdict, receipt };
8773
9317
  if (failures.length > 0) {
8774
9318
  result.message = failures.map(
@@ -8782,7 +9326,7 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8782
9326
  const targetBranch = `local/${prdId}`;
8783
9327
  const retouchBranch = `local/${prdId}/retouch`;
8784
9328
  try {
8785
- execFileSync2(
9329
+ execFileSync3(
8786
9330
  "git",
8787
9331
  ["show-ref", "--verify", "--quiet", `refs/heads/${retouchBranch}`],
8788
9332
  {
@@ -8795,7 +9339,7 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8795
9339
  return;
8796
9340
  }
8797
9341
  try {
8798
- execFileSync2(
9342
+ execFileSync3(
8799
9343
  "git",
8800
9344
  ["show-ref", "--verify", "--quiet", `refs/heads/${targetBranch}`],
8801
9345
  {
@@ -8808,18 +9352,18 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8808
9352
  return;
8809
9353
  }
8810
9354
  try {
8811
- execFileSync2("git", ["checkout", targetBranch], {
9355
+ execFileSync3("git", ["checkout", targetBranch], {
8812
9356
  cwd: root,
8813
9357
  encoding: "utf8",
8814
9358
  stdio: "pipe"
8815
9359
  });
8816
- execFileSync2("git", ["merge", "--squash", retouchBranch], {
9360
+ execFileSync3("git", ["merge", "--squash", retouchBranch], {
8817
9361
  cwd: root,
8818
9362
  encoding: "utf8",
8819
9363
  stdio: "pipe"
8820
9364
  });
8821
9365
  try {
8822
- execFileSync2("git", ["diff", "--cached", "--quiet"], {
9366
+ execFileSync3("git", ["diff", "--cached", "--quiet"], {
8823
9367
  cwd: root,
8824
9368
  encoding: "utf8",
8825
9369
  stdio: "pipe"
@@ -8827,7 +9371,7 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8827
9371
  return;
8828
9372
  } catch {
8829
9373
  }
8830
- execFileSync2(
9374
+ execFileSync3(
8831
9375
  "git",
8832
9376
  ["commit", "-m", `Squash merge ${retouchBranch} into ${targetBranch}`],
8833
9377
  {
@@ -8836,6 +9380,26 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8836
9380
  stdio: "pipe"
8837
9381
  }
8838
9382
  );
9383
+ const mergeCommit = execFileSync3("git", ["rev-parse", "HEAD"], {
9384
+ cwd: root,
9385
+ encoding: "utf8",
9386
+ stdio: "pipe"
9387
+ }).trim();
9388
+ const changedPathsResult = execFileSync3(
9389
+ "git",
9390
+ ["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"],
9391
+ {
9392
+ cwd: root,
9393
+ encoding: "utf8",
9394
+ stdio: "pipe"
9395
+ }
9396
+ );
9397
+ const changedPaths = changedPathsResult.split(/\r?\n/).filter(Boolean);
9398
+ return {
9399
+ mergeCommit,
9400
+ changedPaths,
9401
+ reviewedTimestamp: (/* @__PURE__ */ new Date()).toISOString()
9402
+ };
8839
9403
  } catch (error) {
8840
9404
  const message = error instanceof Error ? error.message : String(error);
8841
9405
  if (message.toLowerCase().includes("conflict")) {
@@ -8846,53 +9410,27 @@ async function squashFinalReviewRetouch(prdId, repoRoot2) {
8846
9410
  }
8847
9411
 
8848
9412
  // prd-run/local-reconciliation.ts
8849
- import { execFileSync as execFileSync3 } from "child_process";
8850
- import {
8851
- existsSync as existsSync14,
8852
- mkdirSync as mkdirSync9,
8853
- readFileSync as readFileSync14,
8854
- readdirSync as readdirSync5,
8855
- writeFileSync as writeFileSync6
8856
- } from "fs";
8857
- import { join as join17 } from "path";
8858
- function getLocalStorePath3(repoRoot2, prdId) {
8859
- return join17(repoRoot2, ".pourkit", "local-prd-runs", prdId);
9413
+ import { execFileSync as execFileSync4 } from "child_process";
9414
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
9415
+ import { join as join18 } from "path";
9416
+ function getLocalStorePath4(repoRoot2, prdId) {
9417
+ return join18(repoRoot2, ".pourkit", "local-prd-runs", prdId);
8860
9418
  }
8861
9419
  function getFinalReviewReceiptPath2(repoRoot2, prdId) {
8862
- return join17(getLocalStorePath3(repoRoot2, prdId), "final-review-receipt.json");
9420
+ return join18(getLocalStorePath4(repoRoot2, prdId), "final-review-receipt.json");
8863
9421
  }
8864
9422
  function getHandoffArtifactPath(repoRoot2, prdId) {
8865
- return join17(repoRoot2, ".pourkit", ".tmp", "reconciliation", `${prdId}.json`);
8866
- }
8867
- function getCompletionsDir(repoRoot2, prdId) {
8868
- const initiativesRoot = join17(
8869
- repoRoot2,
8870
- ".pourkit",
8871
- "architecture",
8872
- "initiatives"
8873
- );
8874
- if (existsSync14(initiativesRoot)) {
8875
- const dirs = readdirSync5(initiativesRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join17(initiativesRoot, e.name));
8876
- for (const dir of dirs) {
8877
- const prdsDir = join17(dir, "prds");
8878
- if (!existsSync14(prdsDir)) continue;
8879
- const prdDirs = readdirSync5(prdsDir, { withFileTypes: true }).filter(
8880
- (e) => e.isDirectory() && e.name.startsWith(prdId.toLowerCase())
8881
- );
8882
- if (prdDirs.length > 0) return join17(dir, "completions");
8883
- }
8884
- }
8885
- return join17(getLocalStorePath3(repoRoot2, prdId), "completions");
9423
+ return join18(repoRoot2, ".pourkit", ".tmp", "reconciliation", `${prdId}.json`);
8886
9424
  }
8887
9425
  function getReconciliationReceiptPath(repoRoot2, prdId) {
8888
- return join17(
8889
- getLocalStorePath3(repoRoot2, prdId),
9426
+ return join18(
9427
+ getLocalStorePath4(repoRoot2, prdId),
8890
9428
  "reconciliation-receipt.json"
8891
9429
  );
8892
9430
  }
8893
9431
  async function runLocalReconciliation(prdId, options) {
8894
9432
  const root = options?.repoRoot ?? process.cwd();
8895
- if (!existsSync14(getFinalReviewReceiptPath2(root, prdId))) {
9433
+ if (!existsSync15(getFinalReviewReceiptPath2(root, prdId))) {
8896
9434
  return {
8897
9435
  ok: false,
8898
9436
  failureCode: "missing_final_review",
@@ -8900,7 +9438,7 @@ async function runLocalReconciliation(prdId, options) {
8900
9438
  };
8901
9439
  }
8902
9440
  try {
8903
- execFileSync3("pourkit-architect", ["reconcile", prdId], {
9441
+ execFileSync4("pourkit-architect", ["reconcile", prdId], {
8904
9442
  cwd: root,
8905
9443
  encoding: "utf8",
8906
9444
  stdio: "pipe"
@@ -8916,7 +9454,7 @@ async function runLocalReconciliation(prdId, options) {
8916
9454
  }
8917
9455
  let handoff;
8918
9456
  try {
8919
- const content = readFileSync14(getHandoffArtifactPath(root, prdId), "utf-8");
9457
+ const content = readFileSync15(getHandoffArtifactPath(root, prdId), "utf-8");
8920
9458
  handoff = JSON.parse(content);
8921
9459
  } catch {
8922
9460
  return {
@@ -8925,67 +9463,39 @@ async function runLocalReconciliation(prdId, options) {
8925
9463
  message: "Handoff artifact validation failed. Check Architect reconciliation output."
8926
9464
  };
8927
9465
  }
8928
- if (handoff.result === "blocked") {
8929
- return {
8930
- ok: false,
8931
- failureCode: "reconciliation_blocked",
8932
- message: "Architect reconciliation is blocked."
8933
- };
8934
- }
8935
- if (handoff.result === "changes_produced" && (!Array.isArray(handoff.changedPlanningPaths) || handoff.changedPlanningPaths.length === 0)) {
9466
+ const validResults = [
9467
+ "no_changes_needed",
9468
+ "changes_produced",
9469
+ "blocked"
9470
+ ];
9471
+ if (!validResults.includes(
9472
+ handoff.result
9473
+ )) {
9474
+ return {
9475
+ ok: false,
9476
+ failureCode: "invalid_handoff",
9477
+ message: "Handoff artifact validation failed. Check Architect reconciliation output."
9478
+ };
9479
+ }
9480
+ if (handoff.result === "blocked") {
9481
+ return {
9482
+ ok: false,
9483
+ failureCode: "reconciliation_blocked",
9484
+ message: "Architect reconciliation is blocked."
9485
+ };
9486
+ }
9487
+ if (handoff.result === "changes_produced" && (!Array.isArray(handoff.changedPlanningPaths) || handoff.changedPlanningPaths.length === 0)) {
8936
9488
  return {
8937
9489
  ok: false,
8938
9490
  failureCode: "invalid_handoff",
8939
9491
  message: "Handoff artifact validation failed. Check Architect reconciliation output."
8940
9492
  };
8941
9493
  }
8942
- if (handoff.result === "changes_produced") {
8943
- try {
8944
- const completionsDir = getCompletionsDir(root, prdId);
8945
- mkdirSync9(completionsDir, { recursive: true });
8946
- const existing = existsSync14(completionsDir) ? readdirSync5(completionsDir).filter((f) => f.endsWith(".md")) : [];
8947
- const nextNum = String(existing.length + 1).padStart(3, "0");
8948
- const filename = `${nextNum}-prd-${prdId.replace("PRD-", "").toLowerCase()}-reconciliation-completion.md`;
8949
- const filepath = join17(completionsDir, filename);
8950
- const changedPaths = Array.isArray(handoff.changedPlanningPaths) ? handoff.changedPlanningPaths : [];
8951
- const summary = typeof handoff.summary === "string" ? handoff.summary : "";
8952
- const lines = [
8953
- `# ${prdId} Reconciliation Completion`,
8954
- "",
8955
- `Result: ${handoff.result}`
8956
- ];
8957
- if (summary) lines.push(`Summary: ${summary}`);
8958
- lines.push("", "## Changed Planning Paths", "");
8959
- if (changedPaths.length > 0) {
8960
- lines.push(...changedPaths.map((p) => `- ${p}`));
8961
- } else {
8962
- lines.push("- (none)");
8963
- }
8964
- lines.push("");
8965
- writeFileSync6(filepath, lines.join("\n"), "utf-8");
8966
- execFileSync3("git", ["add", filepath], {
8967
- cwd: root,
8968
- encoding: "utf8",
8969
- stdio: "pipe"
8970
- });
8971
- execFileSync3(
8972
- "git",
8973
- ["commit", "-m", `docs: ${prdId} reconciliation completion`],
8974
- { cwd: root, encoding: "utf8", stdio: "pipe" }
8975
- );
8976
- } catch {
8977
- return {
8978
- ok: false,
8979
- failureCode: "completion_commit_failed",
8980
- message: "Failed to commit completion files."
8981
- };
8982
- }
8983
- }
8984
9494
  if (handoff.result === "changes_produced") {
8985
9495
  try {
8986
9496
  const targetBranch = getLocalPrdBranchName(prdId);
8987
9497
  try {
8988
- execFileSync3(
9498
+ execFileSync4(
8989
9499
  "git",
8990
9500
  ["show-ref", "--verify", "--quiet", `refs/heads/${targetBranch}`],
8991
9501
  { cwd: root, encoding: "utf8", stdio: "pipe" }
@@ -8994,17 +9504,17 @@ async function runLocalReconciliation(prdId, options) {
8994
9504
  return {
8995
9505
  ok: false,
8996
9506
  failureCode: "squash_merge_failed",
8997
- message: "Squash merge into local PRD Branch failed."
9507
+ message: `Local PRD Branch "${targetBranch}" does not exist. Run prd-run start first to create it.`
8998
9508
  };
8999
9509
  }
9000
- execFileSync3("git", ["checkout", targetBranch], {
9510
+ execFileSync4("git", ["checkout", targetBranch], {
9001
9511
  cwd: root,
9002
9512
  encoding: "utf8",
9003
9513
  stdio: "pipe"
9004
9514
  });
9005
9515
  const sourceBranch = `local/${prdId}/reconciliation`;
9006
9516
  try {
9007
- execFileSync3(
9517
+ execFileSync4(
9008
9518
  "git",
9009
9519
  ["show-ref", "--verify", "--quiet", `refs/heads/${sourceBranch}`],
9010
9520
  {
@@ -9017,27 +9527,46 @@ async function runLocalReconciliation(prdId, options) {
9017
9527
  return {
9018
9528
  ok: false,
9019
9529
  failureCode: "squash_merge_failed",
9020
- message: "Squash merge into local PRD Branch failed."
9530
+ message: `Reconciliation source branch "${sourceBranch}" does not exist. Re-run reconciliation.`
9021
9531
  };
9022
9532
  }
9023
- execFileSync3("git", ["merge", "--squash", sourceBranch], {
9533
+ execFileSync4("git", ["merge", "--squash", sourceBranch], {
9024
9534
  cwd: root,
9025
9535
  encoding: "utf8",
9026
9536
  stdio: "pipe"
9027
9537
  });
9028
9538
  try {
9029
- execFileSync3("git", ["diff", "--cached", "--quiet"], {
9539
+ execFileSync4("git", ["diff", "--cached", "--quiet"], {
9030
9540
  cwd: root,
9031
9541
  encoding: "utf8",
9032
9542
  stdio: "pipe"
9033
9543
  });
9034
9544
  } catch {
9035
- execFileSync3(
9545
+ execFileSync4(
9036
9546
  "git",
9037
9547
  ["commit", "-m", `Squash merge ${sourceBranch} into ${targetBranch}`],
9038
9548
  { cwd: root, encoding: "utf8", stdio: "pipe" }
9039
9549
  );
9040
9550
  }
9551
+ } catch (error) {
9552
+ const message = error instanceof Error ? error.message : "unknown error";
9553
+ return {
9554
+ ok: false,
9555
+ failureCode: "squash_merge_failed",
9556
+ message: `Squash merge into local PRD Branch failed: ${message}`
9557
+ };
9558
+ }
9559
+ }
9560
+ const prdBranch = getLocalPrdBranchName(prdId);
9561
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9562
+ let mergeCommit;
9563
+ if (handoff.result === "changes_produced") {
9564
+ try {
9565
+ mergeCommit = execFileSync4("git", ["rev-parse", "HEAD"], {
9566
+ cwd: root,
9567
+ encoding: "utf8",
9568
+ stdio: "pipe"
9569
+ }).trim();
9041
9570
  } catch {
9042
9571
  return {
9043
9572
  ok: false,
@@ -9046,23 +9575,26 @@ async function runLocalReconciliation(prdId, options) {
9046
9575
  };
9047
9576
  }
9048
9577
  }
9049
- const branch = getLocalPrdBranchName(prdId);
9050
- const receipt = {
9051
- reconciledAt: (/* @__PURE__ */ new Date()).toISOString(),
9052
- branch
9578
+ const changedPaths = Array.isArray(handoff.changedPlanningPaths) ? handoff.changedPlanningPaths : [];
9579
+ const entry = {
9580
+ result: handoff.result ?? "blocked",
9581
+ prdBranch,
9582
+ ...changedPaths.length > 0 ? { changedPlanningPaths: changedPaths } : {},
9583
+ ...mergeCommit ? { mergeCommit } : {},
9584
+ reconciledTimestamp: now
9053
9585
  };
9054
- mkdirSync9(getLocalStorePath3(root, prdId), { recursive: true });
9055
- writeFileSync6(
9586
+ mkdirSync10(getLocalStorePath4(root, prdId), { recursive: true });
9587
+ writeFileSync7(
9056
9588
  getReconciliationReceiptPath(root, prdId),
9057
- JSON.stringify(receipt, null, 2),
9589
+ JSON.stringify(entry, null, 2),
9058
9590
  "utf-8"
9059
9591
  );
9060
- return { ok: true, receipt };
9592
+ return { ok: true, receipt: entry };
9061
9593
  }
9062
9594
 
9063
9595
  // prd-run/local-prepare.ts
9064
- import { existsSync as existsSync15 } from "fs";
9065
- import { join as join18 } from "path";
9596
+ import { existsSync as existsSync16 } from "fs";
9597
+ import { join as join19 } from "path";
9066
9598
  var VALID_TRIAGE_LABELS2 = /* @__PURE__ */ new Set([
9067
9599
  "needs-triage",
9068
9600
  "ready-for-agent",
@@ -9080,8 +9612,8 @@ function fail(gate, failureCode, repairGuidance, gates) {
9080
9612
  async function validateLocalPrepareGates(prdId, repoRoot2) {
9081
9613
  const root = repoRoot2 ?? process.cwd();
9082
9614
  const gates = {};
9083
- const storePath = join18(root, ".pourkit", "local-prd-runs", prdId);
9084
- if (!existsSync15(storePath)) {
9615
+ const storePath = join19(root, ".pourkit", "local-prd-runs", prdId);
9616
+ if (!existsSync16(storePath)) {
9085
9617
  return fail(
9086
9618
  "store_shape",
9087
9619
  "local_prepare_store_shape_failed",
@@ -9202,6 +9734,297 @@ async function validateLocalPrepareGates(prdId, repoRoot2) {
9202
9734
  return { ok: true, gates };
9203
9735
  }
9204
9736
 
9737
+ // prd-run/local-queue-loop.ts
9738
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
9739
+ import { join as join20 } from "path";
9740
+
9741
+ // prd-run/local-issue-run.ts
9742
+ import { execFileSync as execFileSync5 } from "child_process";
9743
+ init_common();
9744
+ async function createLocalIssueBranch(prdId, issueId, repoRoot2) {
9745
+ const root = repoRoot2 ?? process.cwd();
9746
+ const issuesResult = await resolveLocalIssueArtifacts(prdId, root);
9747
+ if (!issuesResult.ok) {
9748
+ throw new Error(
9749
+ `Cannot create local issue branch: ${issuesResult.message}`
9750
+ );
9751
+ }
9752
+ const issue = issuesResult.data.find((i) => i.id === issueId);
9753
+ if (!issue) {
9754
+ throw new Error(`Issue "${issueId}" not found under PRD "${prdId}"`);
9755
+ }
9756
+ const slug = slugify(issue.title);
9757
+ const branchName = `local/${prdId}/${issueId}-${slug}`;
9758
+ const validation = validateLocalBranchName(branchName);
9759
+ if (!validation.ok) {
9760
+ throw new Error(
9761
+ `Invalid local issue branch name "${branchName}": ${validation.message}`
9762
+ );
9763
+ }
9764
+ const localPrdBranch = `local/${prdId}`;
9765
+ try {
9766
+ execFileSync5(
9767
+ "git",
9768
+ ["show-ref", "--verify", "--quiet", `refs/heads/${localPrdBranch}`],
9769
+ {
9770
+ cwd: root,
9771
+ encoding: "utf8",
9772
+ stdio: "pipe"
9773
+ }
9774
+ );
9775
+ } catch {
9776
+ throw new Error(
9777
+ `Local PRD branch "${localPrdBranch}" does not exist. Create it first.`
9778
+ );
9779
+ }
9780
+ let branchExists = false;
9781
+ try {
9782
+ execFileSync5(
9783
+ "git",
9784
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
9785
+ {
9786
+ cwd: root,
9787
+ encoding: "utf8",
9788
+ stdio: "pipe"
9789
+ }
9790
+ );
9791
+ branchExists = true;
9792
+ } catch {
9793
+ }
9794
+ if (!branchExists) {
9795
+ execFileSync5("git", ["checkout", "-b", branchName, localPrdBranch], {
9796
+ cwd: root,
9797
+ encoding: "utf8",
9798
+ stdio: "pipe"
9799
+ });
9800
+ }
9801
+ return { branchName, issue };
9802
+ }
9803
+ async function runLocalIssue(prdId, issueId, builder, repoRoot2) {
9804
+ try {
9805
+ const root = repoRoot2 ?? process.cwd();
9806
+ const { branchName: branch, issue } = await createLocalIssueBranch(
9807
+ prdId,
9808
+ issueId,
9809
+ root
9810
+ );
9811
+ if (builder) {
9812
+ const result = await builder.execute({
9813
+ prdId,
9814
+ issueId,
9815
+ branch,
9816
+ bodyMarkdown: issue.bodyMarkdown
9817
+ });
9818
+ if (!result.ok) {
9819
+ return {
9820
+ ok: false,
9821
+ issueId,
9822
+ branch,
9823
+ failureCode: result.error ?? "Builder execution failed"
9824
+ };
9825
+ }
9826
+ }
9827
+ return { ok: true, issueId, branch };
9828
+ } catch (error) {
9829
+ return {
9830
+ ok: false,
9831
+ issueId,
9832
+ branch: "",
9833
+ failureCode: error instanceof Error ? error.message : String(error)
9834
+ };
9835
+ }
9836
+ }
9837
+
9838
+ // prd-run/local-queue-loop.ts
9839
+ var CHILD_CLEANUP_LABELS = ["agent-in-progress", "pr-open-awaiting-merge"];
9840
+ function getIssueArtifactPath2(repoRoot2, prdId, issueId) {
9841
+ return join20(
9842
+ repoRoot2,
9843
+ ".pourkit",
9844
+ "local-prd-runs",
9845
+ prdId,
9846
+ "issues",
9847
+ `${issueId}.json`
9848
+ );
9849
+ }
9850
+ function readIssueArtifact(repoRoot2, prdId, issueId) {
9851
+ try {
9852
+ const content = readFileSync16(
9853
+ getIssueArtifactPath2(repoRoot2, prdId, issueId),
9854
+ "utf-8"
9855
+ );
9856
+ return JSON.parse(content);
9857
+ } catch {
9858
+ return null;
9859
+ }
9860
+ }
9861
+ function writeIssueArtifact(repoRoot2, prdId, issue) {
9862
+ writeFileSync8(
9863
+ getIssueArtifactPath2(repoRoot2, prdId, issue.id),
9864
+ JSON.stringify(issue, null, 2),
9865
+ "utf-8"
9866
+ );
9867
+ }
9868
+ async function reconcileLocalBlockedIssues(prdId, repoRoot2) {
9869
+ const root = repoRoot2 ?? process.cwd();
9870
+ const issuesResult = await resolveLocalIssueArtifacts(prdId, root);
9871
+ if (!issuesResult.ok) return;
9872
+ const issues = issuesResult.data;
9873
+ for (const issue of issues) {
9874
+ const isBlocked = issue.status === "blocked";
9875
+ if (!isBlocked) continue;
9876
+ if (issue.dependencyIssueIds.length === 0) continue;
9877
+ const depResults = await Promise.all(
9878
+ issue.dependencyIssueIds.map(
9879
+ (depId) => hasLocalIssueMergeReceipt(prdId, depId, root)
9880
+ )
9881
+ );
9882
+ if (!depResults.every(Boolean)) continue;
9883
+ const updated = readIssueArtifact(root, prdId, issue.id);
9884
+ if (!updated) continue;
9885
+ updated.blockedReason = null;
9886
+ updated.labels = updated.labels.filter((l) => l !== "blocked");
9887
+ if (!updated.labels.includes("ready-for-agent")) {
9888
+ updated.labels.push("ready-for-agent");
9889
+ }
9890
+ if (hasValidLabels(updated.labels)) {
9891
+ updated.status = "ready-for-agent";
9892
+ } else {
9893
+ updated.status = "needs-triage";
9894
+ updated.labels = updated.labels.filter((l) => l !== "ready-for-agent");
9895
+ if (!updated.labels.includes("needs-triage")) {
9896
+ updated.labels.push("needs-triage");
9897
+ }
9898
+ }
9899
+ updated.receipts.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9900
+ writeIssueArtifact(root, prdId, updated);
9901
+ }
9902
+ }
9903
+ async function cleanupChildGitHubIssue(issue, issueProvider) {
9904
+ const issueNumber = issue.githubProjection.issueNumber;
9905
+ if (!issueNumber) return { ok: true };
9906
+ if (!issueProvider) {
9907
+ return {
9908
+ ok: false,
9909
+ failureCode: "missing_issue_provider",
9910
+ repairGuidance: "Issue provider is not available. Cannot close child GitHub Issue or remove labels. Configure a GitHub Issue provider to enable cleanup parity."
9911
+ };
9912
+ }
9913
+ for (const label of CHILD_CLEANUP_LABELS) {
9914
+ try {
9915
+ await issueProvider.removeLabel(issueNumber, label);
9916
+ } catch {
9917
+ }
9918
+ }
9919
+ try {
9920
+ await issueProvider.closeIssue(issueNumber);
9921
+ } catch {
9922
+ }
9923
+ return { ok: true };
9924
+ }
9925
+ async function runLocalQueueLoop(prdId, repoRoot2, issueProvider) {
9926
+ const root = repoRoot2 ?? process.cwd();
9927
+ const completedIssues = [];
9928
+ const blockedIssues = [];
9929
+ await reconcileLocalBlockedIssues(prdId, root);
9930
+ for (; ; ) {
9931
+ const runnable = await getRunnableLocalIssues(prdId, root);
9932
+ if (runnable.length === 0) {
9933
+ const issuesResult = await resolveLocalIssueArtifacts(prdId, root);
9934
+ if (issuesResult.ok) {
9935
+ for (const issue2 of issuesResult.data) {
9936
+ if (issue2.status === "blocked") {
9937
+ blockedIssues.push(issue2.id);
9938
+ }
9939
+ }
9940
+ }
9941
+ break;
9942
+ }
9943
+ const issue = runnable[0];
9944
+ const runResult = await runLocalIssue(prdId, issue.id, void 0, root);
9945
+ if (!runResult.ok) {
9946
+ return {
9947
+ ok: false,
9948
+ completedIssues,
9949
+ blockedIssues,
9950
+ failureCode: runResult.failureCode ?? `Issue ${issue.id} failed`,
9951
+ repairGuidance: `Local issue run failed for ${issue.id}. Check the issue artifact and branch state.`,
9952
+ blockedGate: "queue"
9953
+ };
9954
+ }
9955
+ const mergeResult = await squashMergeLocalIssue(
9956
+ prdId,
9957
+ issue.id,
9958
+ void 0,
9959
+ root
9960
+ );
9961
+ if (!mergeResult.ok) {
9962
+ if (mergeResult.failureCode === "already_merged") {
9963
+ const alreadyMergedArtifact = readIssueArtifact(root, prdId, issue.id);
9964
+ if (alreadyMergedArtifact) {
9965
+ alreadyMergedArtifact.status = "complete";
9966
+ alreadyMergedArtifact.receipts.completedAt = mergeResult.receipt?.completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
9967
+ alreadyMergedArtifact.receipts.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9968
+ writeIssueArtifact(root, prdId, alreadyMergedArtifact);
9969
+ const cleanupResult = await cleanupChildGitHubIssue(
9970
+ alreadyMergedArtifact,
9971
+ issueProvider
9972
+ );
9973
+ if (!cleanupResult.ok) {
9974
+ return {
9975
+ ok: false,
9976
+ completedIssues,
9977
+ blockedIssues,
9978
+ failureCode: cleanupResult.failureCode ?? "child_cleanup_failed",
9979
+ repairGuidance: cleanupResult.repairGuidance ?? `Child GitHub Issue cleanup failed for ${issue.id}.`,
9980
+ blockedGate: "queue"
9981
+ };
9982
+ }
9983
+ }
9984
+ completedIssues.push(issue.id);
9985
+ await reconcileLocalBlockedIssues(prdId, root);
9986
+ continue;
9987
+ }
9988
+ return {
9989
+ ok: false,
9990
+ completedIssues,
9991
+ blockedIssues,
9992
+ failureCode: mergeResult.failureCode ?? `Merge of ${issue.id} failed`,
9993
+ repairGuidance: mergeResult.repairGuidance,
9994
+ blockedGate: "queue"
9995
+ };
9996
+ }
9997
+ const completedArtifact = readIssueArtifact(root, prdId, issue.id);
9998
+ if (completedArtifact) {
9999
+ completedArtifact.status = "complete";
10000
+ completedArtifact.receipts.completedAt = mergeResult.receipt?.completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
10001
+ completedArtifact.receipts.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
10002
+ writeIssueArtifact(root, prdId, completedArtifact);
10003
+ const cleanupResult = await cleanupChildGitHubIssue(
10004
+ completedArtifact,
10005
+ issueProvider
10006
+ );
10007
+ if (!cleanupResult.ok) {
10008
+ return {
10009
+ ok: false,
10010
+ completedIssues,
10011
+ blockedIssues,
10012
+ failureCode: cleanupResult.failureCode ?? "child_cleanup_failed",
10013
+ repairGuidance: cleanupResult.repairGuidance ?? `Child GitHub Issue cleanup failed for ${issue.id}.`,
10014
+ blockedGate: "queue"
10015
+ };
10016
+ }
10017
+ }
10018
+ completedIssues.push(issue.id);
10019
+ await reconcileLocalBlockedIssues(prdId, root);
10020
+ }
10021
+ return {
10022
+ ok: true,
10023
+ completedIssues,
10024
+ blockedIssues
10025
+ };
10026
+ }
10027
+
9205
10028
  // commands/queue.ts
9206
10029
  init_common();
9207
10030
  import { Effect as Effect8 } from "effect";
@@ -9507,6 +10330,7 @@ function runOneQueueIssueEffect(options) {
9507
10330
  }
9508
10331
  const { issue: selected } = outcome;
9509
10332
  const baseBranchOverride = options.queueRunContext?.prdBranch;
10333
+ const prdRunMode = options.prdRunMode ?? options.queueRunContext?.prdRunMode;
9510
10334
  const runResult = yield* Effect8.tryPromise({
9511
10335
  try: () => runIssueCommand({
9512
10336
  issueNumber: selected.number,
@@ -9518,7 +10342,8 @@ function runOneQueueIssueEffect(options) {
9518
10342
  force,
9519
10343
  logger,
9520
10344
  repoRoot: repoRoot2,
9521
- ...baseBranchOverride ? { baseBranchOverride } : {}
10345
+ ...baseBranchOverride ? { baseBranchOverride } : {},
10346
+ ...prdRunMode ? { prdRunMode } : {}
9522
10347
  }),
9523
10348
  catch: (e) => {
9524
10349
  if (e instanceof Error) return e;
@@ -9529,6 +10354,10 @@ function runOneQueueIssueEffect(options) {
9529
10354
  logger.raw(` Branch: ${runResult.branchName}`);
9530
10355
  if (runResult.noOp) {
9531
10356
  logger.raw(" Status: no-op (closed without PR)");
10357
+ } else if (runResult.mode === "local") {
10358
+ logger.raw(` Local PRD Branch: ${runResult.localPrdBranch}`);
10359
+ logger.raw(` Merge Commit: ${runResult.mergeCommit}`);
10360
+ logger.raw(` Receipt: ${runResult.receiptPath}`);
9532
10361
  } else {
9533
10362
  logger.raw(` PR Title: ${runResult.prTitle}`);
9534
10363
  logger.raw(` PR Number: ${runResult.prNumber}`);
@@ -9588,7 +10417,8 @@ async function runQueueCommand(options) {
9588
10417
  logger: options.logger,
9589
10418
  repoRoot: options.repoRoot,
9590
10419
  prdRef: options.prdRef,
9591
- queueRunContext: options.queueRunContext
10420
+ queueRunContext: options.queueRunContext,
10421
+ prdRunMode: options.queueRunContext?.prdRunMode
9592
10422
  };
9593
10423
  if (!options.loop) {
9594
10424
  return runEffectAndMapExit(runQueue(queueOptions));
@@ -9717,7 +10547,7 @@ function validateChangedPlanningPaths(paths, options) {
9717
10547
  continue;
9718
10548
  }
9719
10549
  if (options?.repoRoot) {
9720
- const fullPath = join19(options.repoRoot, p);
10550
+ const fullPath = join21(options.repoRoot, p);
9721
10551
  try {
9722
10552
  if (lstatSync(fullPath).isSymbolicLink()) {
9723
10553
  rejected.push(p);
@@ -9742,7 +10572,7 @@ function assessStaleSucceededReconciliationReceipt(options) {
9742
10572
  if (!receipt || receipt.status !== "succeeded" || receipt.prNumber || receipt.mergeCommit) {
9743
10573
  return { mode: "none" };
9744
10574
  }
9745
- const observedDiff = collectObservedReconciliationPlanningDiff({
10575
+ const observedDiff = collectObservedReconciliationDirtyPaths({
9746
10576
  worktreeCwd: options.worktreeCwd,
9747
10577
  ignoredPrdRunRecordRef: options.prdRef
9748
10578
  });
@@ -9753,23 +10583,14 @@ function assessStaleSucceededReconciliationReceipt(options) {
9753
10583
  offendingPaths: []
9754
10584
  };
9755
10585
  }
9756
- if (observedDiff.rejectedPaths.length > 0) {
9757
- return {
9758
- mode: "blocked",
9759
- diagnostics: [
9760
- "Reconciliation worktree contains unpublished changes outside safe planning scope."
9761
- ],
9762
- offendingPaths: observedDiff.rejectedPaths
9763
- };
9764
- }
9765
- if (observedDiff.changedPlanningPaths.length === 0) {
10586
+ if (observedDiff.dirtyPaths.length === 0) {
9766
10587
  return { mode: "none" };
9767
10588
  }
9768
10589
  return {
9769
10590
  mode: "recoverable",
9770
- changedPlanningPaths: observedDiff.changedPlanningPaths,
10591
+ dirtyPaths: observedDiff.dirtyPaths,
9771
10592
  diagnostics: [
9772
- "Recovered stale reconciliation success receipt from unpublished planning changes in worktree."
10593
+ "Recovered stale reconciliation success receipt from dirty reconciliation worktree."
9773
10594
  ]
9774
10595
  };
9775
10596
  }
@@ -9837,6 +10658,12 @@ async function validateFinalReviewChildCompleteness(repoRoot2, prdRef, record, i
9837
10658
  );
9838
10659
  continue;
9839
10660
  }
10661
+ if (!issueData || typeof issueData.state !== "string") {
10662
+ incompleteChildren.push(
10663
+ `${child.id} (#${child.number}): failed to fetch issue state`
10664
+ );
10665
+ continue;
10666
+ }
9840
10667
  if (issueData.state === "closed") {
9841
10668
  continue;
9842
10669
  }
@@ -9893,7 +10720,7 @@ function canRetryFinalReviewBlock(record) {
9893
10720
  }
9894
10721
  function loadFinalReviewPrompt(repoRoot2, promptTemplate) {
9895
10722
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
9896
- return existsSync16(promptPath) ? readFileSync15(promptPath, "utf-8") : promptTemplate;
10723
+ return existsSync17(promptPath) ? readFileSync17(promptPath, "utf-8") : promptTemplate;
9897
10724
  }
9898
10725
  function formatVerificationCommands(commands) {
9899
10726
  if (commands.length === 0) return "No verification commands configured.";
@@ -9906,12 +10733,14 @@ function buildFinalReviewPrompt(options) {
9906
10733
  `Worktree checkout base: ${options.evidencePacket.prdBranch}`,
9907
10734
  `Review only this range: ${options.evidencePacket.mergeBase}..HEAD`,
9908
10735
  "Do not compare against current target branch HEAD; the merge base is the review baseline.",
9909
- `Before handoff, run: npm run pourkit:validate-final-review -- ${options.evidencePacket.prdRef} --checkout-base ${options.evidencePacket.prdBranch} --review-base ${options.evidencePacket.mergeBase}`,
10736
+ `Before handoff, run: pourkit prd-run validate-final-review ${options.evidencePacket.prdRef} --checkout-base ${options.evidencePacket.prdBranch} --review-base ${options.evidencePacket.mergeBase}`,
9910
10737
  "Fix any validation failures before handing off.",
9911
10738
  "",
9912
- "## Target Verification Commands",
10739
+ "## Verification",
10740
+ "",
10741
+ `Run this command before handoff when retouch changes are made: pourkit run-verification --target ${options.targetName}`,
9913
10742
  "",
9914
- "Run these commands before handoff when retouch changes are made:",
10743
+ "Underlying project commands:",
9915
10744
  formatVerificationCommands(options.verificationCommands),
9916
10745
  "",
9917
10746
  "Evidence Packet (do not infer PRD context from local state files):",
@@ -9938,14 +10767,14 @@ function buildReconciliationPrompt(options) {
9938
10767
  options.artifactPath,
9939
10768
  "",
9940
10769
  "Self-validation command:",
9941
- `validate-artifact reconciliation ${options.artifactPath}`
10770
+ `pourkit validate-artifact reconciliation ${options.artifactPath}`
9942
10771
  ].join("\n");
9943
10772
  }
9944
10773
  function buildReconciliationBranchName(prdRef) {
9945
10774
  return `${normalizePrdRunRef2(prdRef)}-reconciliation`;
9946
10775
  }
9947
10776
  function reconciliationWorktreePath(repoRoot2, branchName) {
9948
- return join19(repoRoot2, ".sandcastle", "worktrees", branchName);
10777
+ return join21(repoRoot2, ".sandcastle", "worktrees", branchName);
9949
10778
  }
9950
10779
  function reconciliationReceiptWorktreePath(branchName) {
9951
10780
  return `.sandcastle/worktrees/${branchName}`;
@@ -9957,7 +10786,7 @@ function buildFinalReviewBranchName(prdRef) {
9957
10786
  return `pourkit/${normalizePrdRunRef2(prdRef).toLowerCase()}-final-review`;
9958
10787
  }
9959
10788
  function finalReviewWorktreePath(repoRoot2, branchName) {
9960
- return join19(
10789
+ return join21(
9961
10790
  repoRoot2,
9962
10791
  ".sandcastle",
9963
10792
  "worktrees",
@@ -9980,7 +10809,7 @@ function listFinalReviewChangedPaths(worktreePath, mergeBase) {
9980
10809
  }
9981
10810
  paths.add(path9);
9982
10811
  };
9983
- const diffResult = spawnSync2(
10812
+ const diffResult = spawnSync3(
9984
10813
  "git",
9985
10814
  ["diff", "--name-only", mergeBase, "--", "."],
9986
10815
  {
@@ -9991,7 +10820,7 @@ function listFinalReviewChangedPaths(worktreePath, mergeBase) {
9991
10820
  for (const path9 of diffResult.stdout.split(/\r?\n/).filter(Boolean)) {
9992
10821
  addPath(path9);
9993
10822
  }
9994
- const statusResult = spawnSync2(
10823
+ const statusResult = spawnSync3(
9995
10824
  "git",
9996
10825
  ["status", "--porcelain", "--untracked-files=all"],
9997
10826
  {
@@ -10012,7 +10841,7 @@ function buildFinalReviewFinalizerPrompt(options) {
10012
10841
  options.repoRoot,
10013
10842
  options.promptTemplate
10014
10843
  );
10015
- const promptBody = existsSync16(promptPath) ? readFileSync15(promptPath, "utf-8") : options.promptTemplate;
10844
+ const promptBody = existsSync17(promptPath) ? readFileSync17(promptPath, "utf-8") : options.promptTemplate;
10016
10845
  return [
10017
10846
  "# Final Review Retouch PR Finalizer",
10018
10847
  "",
@@ -10046,7 +10875,7 @@ function buildFinalReviewFinalizerPrompt(options) {
10046
10875
  }
10047
10876
  async function runFinalReviewPrFinalizer(options) {
10048
10877
  const finalizer = options.target.strategy.finalize.prDescriptionAgent;
10049
- const artifactPathInWorktree = join19(
10878
+ const artifactPathInWorktree = join21(
10050
10879
  ".pourkit",
10051
10880
  ".tmp",
10052
10881
  "finalizer",
@@ -10092,7 +10921,7 @@ function ensureFinalReviewWorktree(options) {
10092
10921
  options.repoRoot,
10093
10922
  options.branchName
10094
10923
  );
10095
- const listResult = spawnSync2("git", ["worktree", "list", "--porcelain"], {
10924
+ const listResult = spawnSync3("git", ["worktree", "list", "--porcelain"], {
10096
10925
  cwd: options.repoRoot,
10097
10926
  encoding: "utf8"
10098
10927
  });
@@ -10120,15 +10949,15 @@ function ensureFinalReviewWorktree(options) {
10120
10949
  }
10121
10950
  return { ok: true, worktreePath: registeredPath };
10122
10951
  }
10123
- if (existsSync16(worktreePath)) {
10952
+ if (existsSync17(worktreePath)) {
10124
10953
  return {
10125
10954
  ok: false,
10126
10955
  reason: "Final Review worktree path exists but is not registered with git.",
10127
10956
  diagnostics: [`stale worktree path: ${worktreePath}`]
10128
10957
  };
10129
10958
  }
10130
- mkdirSync10(dirname5(worktreePath), { recursive: true });
10131
- const branchResult = spawnSync2(
10959
+ mkdirSync11(dirname5(worktreePath), { recursive: true });
10960
+ const branchResult = spawnSync3(
10132
10961
  "git",
10133
10962
  ["branch", "-f", options.branchName, `origin/${options.checkoutBase}`],
10134
10963
  { cwd: options.repoRoot, encoding: "utf8" }
@@ -10140,7 +10969,7 @@ function ensureFinalReviewWorktree(options) {
10140
10969
  diagnostics: collectSpawnDiagnostics(branchResult, "git branch failed")
10141
10970
  };
10142
10971
  }
10143
- const addResult = spawnSync2(
10972
+ const addResult = spawnSync3(
10144
10973
  "git",
10145
10974
  ["worktree", "add", worktreePath, options.branchName],
10146
10975
  { cwd: options.repoRoot, encoding: "utf8" }
@@ -10182,7 +11011,7 @@ async function createOrReuseFinalReviewRetouchPr(options) {
10182
11011
  if (existingPr && existingPr.state === "OPEN") {
10183
11012
  return existingPr;
10184
11013
  }
10185
- const worktreePath = mkdtempSync(join19(tmpdir(), "pourkit-retouch-"));
11014
+ const worktreePath = mkdtempSync(join21(tmpdir(), "pourkit-retouch-"));
10186
11015
  try {
10187
11016
  runGitOrThrow(
10188
11017
  options.repoRoot,
@@ -10200,9 +11029,9 @@ async function createOrReuseFinalReviewRetouchPr(options) {
10200
11029
  "create retouch branch"
10201
11030
  );
10202
11031
  for (const changedFile of options.changedPaths) {
10203
- const sourcePath = join19(options.sourceWorktreePath, changedFile);
10204
- const targetPath = join19(worktreePath, changedFile);
10205
- mkdirSync10(dirname5(targetPath), { recursive: true });
11032
+ const sourcePath = join21(options.sourceWorktreePath, changedFile);
11033
+ const targetPath = join21(worktreePath, changedFile);
11034
+ mkdirSync11(dirname5(targetPath), { recursive: true });
10206
11035
  cpSync(sourcePath, targetPath, { recursive: true });
10207
11036
  }
10208
11037
  runGitOrThrow(
@@ -10216,13 +11045,13 @@ async function createOrReuseFinalReviewRetouchPr(options) {
10216
11045
  "commit retouch diff"
10217
11046
  );
10218
11047
  } finally {
10219
- spawnSync2("git", ["worktree", "remove", "--force", worktreePath], {
11048
+ spawnSync3("git", ["worktree", "remove", "--force", worktreePath], {
10220
11049
  cwd: options.repoRoot,
10221
11050
  encoding: "utf8"
10222
11051
  });
10223
11052
  rmSync3(worktreePath, { recursive: true, force: true });
10224
11053
  }
10225
- const pushResult = spawnSync2(
11054
+ const pushResult = spawnSync3(
10226
11055
  "git",
10227
11056
  ["push", "--force", "origin", `${branchName}:refs/heads/${branchName}`],
10228
11057
  {
@@ -10556,6 +11385,7 @@ async function runPrdRunFinalReviewCommand(options) {
10556
11385
  prompt: buildFinalReviewPrompt({
10557
11386
  repoRoot: options.repoRoot,
10558
11387
  promptTemplate: finalReviewConfig.promptTemplate,
11388
+ targetName,
10559
11389
  evidencePacket,
10560
11390
  verificationCommands
10561
11391
  }),
@@ -10631,7 +11461,7 @@ async function runPrdRunFinalReviewCommand(options) {
10631
11461
  };
10632
11462
  }
10633
11463
  const resolvedWorktreePath = executionResult.worktreePath;
10634
- const artifactPath = join19(
11464
+ const artifactPath = join21(
10635
11465
  resolvedWorktreePath,
10636
11466
  ".pourkit/final-review-artifact.json"
10637
11467
  );
@@ -11215,7 +12045,7 @@ async function runPrdRunFinalReviewCommand(options) {
11215
12045
  function runPrdRunValidateFinalReviewCommand(options) {
11216
12046
  const prdRef = normalizePrdRunRef2(options.prdRef);
11217
12047
  const checkoutBase = options.checkoutBase ?? prdRef;
11218
- const artifactPath = options.artifactPath ? options.artifactPath : join19(options.repoRoot, ".pourkit", "final-review-artifact.json");
12048
+ const artifactPath = options.artifactPath ? options.artifactPath : join21(options.repoRoot, ".pourkit", "final-review-artifact.json");
11219
12049
  const artifact = parseFinalReviewArtifact(artifactPath);
11220
12050
  if (!artifact.ok) {
11221
12051
  return {
@@ -11229,7 +12059,7 @@ function runPrdRunValidateFinalReviewCommand(options) {
11229
12059
  }
11230
12060
  let changedPaths = options.changedPaths;
11231
12061
  if (artifact.verdict === "pass_with_retouch" && (!changedPaths || changedPaths.length === 0) && (!artifact.changedPaths || artifact.changedPaths.length === 0)) {
11232
- const diffResult = spawnSync2(
12062
+ const diffResult = spawnSync3(
11233
12063
  "git",
11234
12064
  ["diff", "--name-only", options.reviewBase, "HEAD", "--", "."],
11235
12065
  { cwd: options.repoRoot, encoding: "utf8" }
@@ -11381,12 +12211,12 @@ async function runReconcilePreflightAndSetup(options) {
11381
12211
  );
11382
12212
  const worktreeCwd = worktreePath;
11383
12213
  const existingReconciliationReceipt = record.reconciliation;
11384
- const existingLocalBranch = spawnSync2(
12214
+ const existingLocalBranch = spawnSync3(
11385
12215
  "git",
11386
12216
  ["rev-parse", "--verify", "--quiet", reconciliationBranchName],
11387
12217
  { cwd: options.repoRoot, encoding: "utf8" }
11388
12218
  ).status === 0;
11389
- const existingWorktree = spawnSync2(
12219
+ const existingWorktree = spawnSync3(
11390
12220
  "git",
11391
12221
  ["worktree", "list", "--porcelain"],
11392
12222
  { cwd: options.repoRoot, encoding: "utf8" }
@@ -11439,7 +12269,7 @@ async function runReconcilePreflightAndSetup(options) {
11439
12269
  branchAction: "blocked"
11440
12270
  };
11441
12271
  }
11442
- const resetResult = spawnSync2(
12272
+ const resetResult = spawnSync3(
11443
12273
  "git",
11444
12274
  ["reset", "--hard", `origin/${prdBranch}`],
11445
12275
  { cwd: worktreeCwd, encoding: "utf8" }
@@ -11482,7 +12312,7 @@ async function runReconcilePreflightAndSetup(options) {
11482
12312
  );
11483
12313
  return { ok: false, diagnostics, branchAction: "blocked" };
11484
12314
  }
11485
- const adoptionCheck = spawnSync2(
12315
+ const adoptionCheck = spawnSync3(
11486
12316
  "git",
11487
12317
  [
11488
12318
  "merge-base",
@@ -11523,14 +12353,14 @@ async function runReconcilePreflightAndSetup(options) {
11523
12353
  );
11524
12354
  return { ok: false, diagnostics, branchAction: "blocked" };
11525
12355
  }
11526
- if (existsSync16(worktreePath)) {
12356
+ if (existsSync17(worktreePath)) {
11527
12357
  diagnostics.push(
11528
12358
  "Reconciliation worktree path exists but is not registered with git.",
11529
12359
  `stale worktree path: ${worktreePath}`
11530
12360
  );
11531
12361
  return { ok: false, diagnostics, branchAction: "blocked" };
11532
12362
  }
11533
- const branchResult = spawnSync2(
12363
+ const branchResult = spawnSync3(
11534
12364
  "git",
11535
12365
  ["branch", "-f", reconciliationBranchName, `origin/${prdBranch}`],
11536
12366
  { cwd: options.repoRoot, encoding: "utf8" }
@@ -11544,14 +12374,14 @@ async function runReconcilePreflightAndSetup(options) {
11544
12374
  );
11545
12375
  return { ok: false, diagnostics, branchAction: "blocked" };
11546
12376
  }
11547
- mkdirSync10(dirname5(worktreePath), { recursive: true });
11548
- const addResult = spawnSync2(
12377
+ mkdirSync11(dirname5(worktreePath), { recursive: true });
12378
+ const addResult = spawnSync3(
11549
12379
  "git",
11550
12380
  ["worktree", "add", worktreePath, reconciliationBranchName],
11551
12381
  { cwd: options.repoRoot, encoding: "utf8" }
11552
12382
  );
11553
12383
  if (addResult.status !== 0) {
11554
- spawnSync2("git", ["branch", "-D", reconciliationBranchName], {
12384
+ spawnSync3("git", ["branch", "-D", reconciliationBranchName], {
11555
12385
  cwd: options.repoRoot,
11556
12386
  encoding: "utf8"
11557
12387
  });
@@ -11599,7 +12429,7 @@ function buildReconciliationPrBody(options) {
11599
12429
  function commitAndPushReconciliationChanges(options) {
11600
12430
  const diagnostics = [];
11601
12431
  const existingPaths = options.changedPlanningPaths.filter((p) => {
11602
- return existsSync16(join19(options.worktreeCwd, p));
12432
+ return existsSync17(join21(options.worktreeCwd, p));
11603
12433
  });
11604
12434
  if (existingPaths.length === 0) {
11605
12435
  diagnostics.push(
@@ -11607,7 +12437,7 @@ function commitAndPushReconciliationChanges(options) {
11607
12437
  );
11608
12438
  return { ok: true, diagnostics: [...diagnostics] };
11609
12439
  }
11610
- const addResult = spawnSync2("git", ["add", "--", ...existingPaths], {
12440
+ const addResult = spawnSync3("git", ["add", "--", ...existingPaths], {
11611
12441
  cwd: options.worktreeCwd,
11612
12442
  encoding: "utf8"
11613
12443
  });
@@ -11624,7 +12454,7 @@ function commitAndPushReconciliationChanges(options) {
11624
12454
  };
11625
12455
  }
11626
12456
  const commitTitle = `docs: reconcile ${normalizePrdRunRef2(options.prdRef)} architecture`;
11627
- const commitResult = spawnSync2("git", ["commit", "-m", commitTitle], {
12457
+ const commitResult = spawnSync3("git", ["commit", "-m", commitTitle], {
11628
12458
  cwd: options.worktreeCwd,
11629
12459
  encoding: "utf8"
11630
12460
  });
@@ -11648,7 +12478,7 @@ function commitAndPushReconciliationChanges(options) {
11648
12478
  };
11649
12479
  }
11650
12480
  }
11651
- const pushResult = spawnSync2(
12481
+ const pushResult = spawnSync3(
11652
12482
  "git",
11653
12483
  [
11654
12484
  "push",
@@ -11742,7 +12572,7 @@ async function runPrdRunReconcileCommand(options) {
11742
12572
  const record = preflightResult.record;
11743
12573
  const prdBranch = record?.prdBranch ?? prdRef;
11744
12574
  const mergeBase = preflightResult.mergeBase;
11745
- const worktreeCwd = preflightResult.worktreePath ? join19(options.repoRoot, preflightResult.worktreePath) : options.repoRoot;
12575
+ const worktreeCwd = preflightResult.worktreePath ? join21(options.repoRoot, preflightResult.worktreePath) : options.repoRoot;
11746
12576
  const staleSucceededRecovery = assessStaleSucceededReconciliationReceipt({
11747
12577
  receipt: record?.reconciliation,
11748
12578
  worktreeCwd,
@@ -12044,7 +12874,7 @@ async function runPrdRunReconcileCommand(options) {
12044
12874
  baseRef: prdBranch,
12045
12875
  checkoutBase,
12046
12876
  reviewBase: mergeBase,
12047
- worktreePath: join19(options.repoRoot, preflightResult.worktreePath),
12877
+ worktreePath: join21(options.repoRoot, preflightResult.worktreePath),
12048
12878
  sandbox: options.config.sandbox,
12049
12879
  artifactPath,
12050
12880
  logger: options.logger
@@ -12145,7 +12975,7 @@ async function runPrdRunReconcileCommand(options) {
12145
12975
  offendingPaths: []
12146
12976
  };
12147
12977
  }
12148
- const fullArtifactPath = join19(executionResult.worktreePath, artifactPath);
12978
+ const fullArtifactPath = join21(executionResult.worktreePath, artifactPath);
12149
12979
  let artifactContent;
12150
12980
  try {
12151
12981
  artifactContent = JSON.parse(retryResult.artifact.value);
@@ -12252,11 +13082,11 @@ async function runPrdRunReconcileCommand(options) {
12252
13082
  const artifactResult = artifactContent.result ?? "changes_produced";
12253
13083
  const artifactChangedPaths = artifactContent.changedPlanningPaths ?? [];
12254
13084
  const artifactNoChangeRationale = artifactContent.noChangeRationale;
12255
- const gitStatusResult = spawnSync2("git", ["status", "--short"], {
13085
+ const gitStatusResult = spawnSync3("git", ["status", "--short"], {
12256
13086
  cwd: worktreeCwd,
12257
13087
  encoding: "utf8"
12258
13088
  });
12259
- const gitDiffResult = spawnSync2("git", ["diff", "--name-status"], {
13089
+ const gitDiffResult = spawnSync3("git", ["diff", "--name-status"], {
12260
13090
  cwd: worktreeCwd,
12261
13091
  encoding: "utf8"
12262
13092
  });
@@ -12267,11 +13097,27 @@ async function runPrdRunReconcileCommand(options) {
12267
13097
  gitStatusShortLines,
12268
13098
  gitDiffNameStatusLines
12269
13099
  });
13100
+ const observedWorktreeDiff = collectObservedReconciliationDirtyPaths({
13101
+ worktreeCwd,
13102
+ ignoredPrdRunRecordRef: prdRef
13103
+ });
12270
13104
  let outcomeResult;
12271
13105
  let outcomeDiagnostics = [];
12272
13106
  let outcomeChangedPaths = [];
12273
13107
  let outcomeRationale;
12274
- if (artifactResult === "no_changes_needed") {
13108
+ if (!observedWorktreeDiff.ok) {
13109
+ outcomeResult = "blocked";
13110
+ outcomeDiagnostics.push(...observedWorktreeDiff.diagnostics);
13111
+ } else if (observedWorktreeDiff.dirtyPaths.length > 0) {
13112
+ outcomeResult = "changes_produced";
13113
+ outcomeChangedPaths = [...observedWorktreeDiff.dirtyPaths];
13114
+ outcomeRationale = void 0;
13115
+ if (artifactResult === "no_changes_needed") {
13116
+ outcomeDiagnostics.push(
13117
+ "Dirty reconciliation worktree observed after agent run; treating reconciliation result as changes_produced."
13118
+ );
13119
+ }
13120
+ } else if (artifactResult === "no_changes_needed") {
12275
13121
  outcomeResult = "no_changes_needed";
12276
13122
  outcomeChangedPaths = [];
12277
13123
  outcomeRationale = artifactNoChangeRationale ?? "No changes needed.";
@@ -12292,11 +13138,11 @@ async function runPrdRunReconcileCommand(options) {
12292
13138
  }
12293
13139
  if (staleSucceededRecovery.mode === "recoverable" && artifactResult === "no_changes_needed") {
12294
13140
  outcomeResult = "changes_produced";
12295
- outcomeChangedPaths = [...staleSucceededRecovery.changedPlanningPaths];
13141
+ outcomeChangedPaths = [...staleSucceededRecovery.dirtyPaths];
12296
13142
  outcomeRationale = void 0;
12297
13143
  outcomeDiagnostics.push(...staleSucceededRecovery.diagnostics);
12298
13144
  }
12299
- if (!pathValidation.ok && outcomeResult !== "blocked") {
13145
+ if (!pathValidation.ok && outcomeResult !== "blocked" && observedWorktreeDiff.ok && observedWorktreeDiff.dirtyPaths.length === 0) {
12300
13146
  outcomeResult = "blocked";
12301
13147
  outcomeDiagnostics.push(
12302
13148
  `Unsafe changed planning paths: ${(pathValidation.rejected ?? []).join(", ")}`
@@ -12993,7 +13839,7 @@ async function runPrdRunReconcileCommand(options) {
12993
13839
  }
12994
13840
  function computeFinalReviewMergeBase(repoRoot2, prdRef) {
12995
13841
  const normalized = normalizePrdRunRef2(prdRef);
12996
- const fetchResult = spawnSync2("git", ["fetch", "origin", "dev", normalized], {
13842
+ const fetchResult = spawnSync3("git", ["fetch", "origin", "dev", normalized], {
12997
13843
  cwd: repoRoot2,
12998
13844
  encoding: "utf8"
12999
13845
  });
@@ -13013,7 +13859,7 @@ function computeFinalReviewMergeBase(repoRoot2, prdRef) {
13013
13859
  if (fetchResult.stderr?.toString?.()?.trim()) {
13014
13860
  fetchDiagnostics.push(fetchResult.stderr.toString().trim());
13015
13861
  }
13016
- const mergeBaseResult = spawnSync2(
13862
+ const mergeBaseResult = spawnSync3(
13017
13863
  "git",
13018
13864
  ["merge-base", "origin/dev", `origin/${normalized}`],
13019
13865
  {
@@ -13156,26 +14002,26 @@ function auditPrd047Implementation(repoRoot2, prdRef) {
13156
14002
  const blockerBugs = [];
13157
14003
  const blockersFixed = [];
13158
14004
  const nonBlockers = [];
13159
- const prdMirrorPath = join19(repoRoot2, PRD_047_PRD_MIRROR_PATH);
13160
- const completionsDir = join19(repoRoot2, dirname5(PRD_047_ESCAPE_HATCH_PATH));
14005
+ const prdMirrorPath = join21(repoRoot2, PRD_047_PRD_MIRROR_PATH);
14006
+ const completionsDir = join21(repoRoot2, dirname5(PRD_047_ESCAPE_HATCH_PATH));
13161
14007
  let escapeHatchPresent = false;
13162
- if (existsSync16(completionsDir)) {
13163
- const files = readdirSync6(completionsDir);
14008
+ if (existsSync17(completionsDir)) {
14009
+ const files = readdirSync5(completionsDir);
13164
14010
  escapeHatchPresent = files.some(
13165
14011
  (f) => f.endsWith(".md") && (f.includes("prd-047") || f.includes("prd-reconciliation"))
13166
14012
  );
13167
14013
  }
13168
14014
  if (!escapeHatchPresent) {
13169
- escapeHatchPresent = existsSync16(join19(repoRoot2, PRD_047_ESCAPE_HATCH_PATH));
14015
+ escapeHatchPresent = existsSync17(join21(repoRoot2, PRD_047_ESCAPE_HATCH_PATH));
13170
14016
  }
13171
- const prdMirrorExists = existsSync16(prdMirrorPath);
14017
+ const prdMirrorExists = existsSync17(prdMirrorPath);
13172
14018
  if (!prdMirrorExists) {
13173
14019
  nonBlockers.push(
13174
14020
  `PRD-0047 PRD mirror not found at ${PRD_047_PRD_MIRROR_PATH}. Cannot verify scope alignment from published mirrors.`
13175
14021
  );
13176
14022
  }
13177
14023
  if (prdMirrorExists) {
13178
- const mirrorContent = readFileSync15(prdMirrorPath, "utf-8");
14024
+ const mirrorContent = readFileSync17(prdMirrorPath, "utf-8");
13179
14025
  const hasReconcileCommand = mirrorContent.includes("prd-run reconcile");
13180
14026
  const hasEscapeHatch = mirrorContent.includes("escape hatch");
13181
14027
  if (!hasReconcileCommand) {
@@ -13189,16 +14035,16 @@ function auditPrd047Implementation(repoRoot2, prdRef) {
13189
14035
  );
13190
14036
  }
13191
14037
  }
13192
- const childIssueDir = join19(repoRoot2, PRD_047_CHILD_ISSUE_DIR);
13193
- const childIssuesExist = existsSync16(childIssueDir);
14038
+ const childIssueDir = join21(repoRoot2, PRD_047_CHILD_ISSUE_DIR);
14039
+ const childIssuesExist = existsSync17(childIssueDir);
13194
14040
  if (!childIssuesExist) {
13195
14041
  nonBlockers.push(
13196
14042
  `PRD-0047 child Issue mirrors not found at ${PRD_047_CHILD_ISSUE_DIR}.`
13197
14043
  );
13198
14044
  }
13199
- const manifestPath = join19(repoRoot2, PRD_047_MANIFEST_PATH);
13200
- if (existsSync16(manifestPath)) {
13201
- const manifestContent = readFileSync15(manifestPath, "utf-8");
14045
+ const manifestPath = join21(repoRoot2, PRD_047_MANIFEST_PATH);
14046
+ if (existsSync17(manifestPath)) {
14047
+ const manifestContent = readFileSync17(manifestPath, "utf-8");
13202
14048
  if (!manifestContent.includes("ready_for_prepare")) {
13203
14049
  nonBlockers.push(
13204
14050
  "Planning Artifact Manifest is not marked as ready_for_prepare."
@@ -13287,6 +14133,17 @@ function runPrdRunAuditCommand(options) {
13287
14133
  const prdRef = normalizePrdRunRef2(options.prdRef);
13288
14134
  return auditPrd047Implementation(options.repoRoot, prdRef);
13289
14135
  }
14136
+ async function closeParentGitHubIssue(prdRef, repoRoot2, issueProvider) {
14137
+ const prdResult = await resolveLocalPrdArtifact(prdRef, repoRoot2);
14138
+ const parentIssueNumber = prdResult.ok ? prdResult.data.githubProjection.issueNumber : null;
14139
+ if (!parentIssueNumber) return { closed: false, issueNumber: null };
14140
+ try {
14141
+ await issueProvider.closeIssue(parentIssueNumber);
14142
+ return { closed: true, issueNumber: parentIssueNumber };
14143
+ } catch {
14144
+ return { closed: false, issueNumber: parentIssueNumber };
14145
+ }
14146
+ }
13290
14147
  async function runPrdRunLaunchCommand(options) {
13291
14148
  const prdRef = normalizePrdRunRef2(options.prdRef);
13292
14149
  const attempted = [];
@@ -13298,10 +14155,11 @@ async function runPrdRunLaunchCommand(options) {
13298
14155
  const terminal = /* @__PURE__ */ new Set([
13299
14156
  "waiting_for_integration",
13300
14157
  "finalizing",
13301
- "complete"
14158
+ "complete",
14159
+ "completed_local_branch"
13302
14160
  ]);
13303
14161
  if (terminal.has(existingRecord.record.status)) {
13304
- return {
14162
+ const result = {
13305
14163
  prdRef,
13306
14164
  status: existingRecord.record.status,
13307
14165
  attempted: [],
@@ -13311,10 +14169,38 @@ async function runPrdRunLaunchCommand(options) {
13311
14169
  `PRD Run ${prdRef} is already in status "${existingRecord.record.status}".`
13312
14170
  ]
13313
14171
  };
14172
+ if (existingRecord.record.status === "completed_local_branch" && existingRecord.record.prdBranch) {
14173
+ result.prdBranch = existingRecord.record.prdBranch;
14174
+ }
14175
+ return result;
13314
14176
  }
13315
14177
  }
13316
14178
  if (existingRecord.record) {
13317
14179
  const status = existingRecord.record.status;
14180
+ const resolvedLaunchMode = options.config ? (() => {
14181
+ try {
14182
+ const target = resolveTarget(options.config, options.targetName);
14183
+ return resolvePrdRunMode(target).mode;
14184
+ } catch {
14185
+ return void 0;
14186
+ }
14187
+ })() : void 0;
14188
+ if (resolvedLaunchMode && existingRecord.record.mode && existingRecord.record.mode !== resolvedLaunchMode) {
14189
+ return {
14190
+ prdRef,
14191
+ status: "blocked",
14192
+ attempted: [],
14193
+ skipped: ["prepare", "start", "queue", "final-review", "reconcile"],
14194
+ resumed: [],
14195
+ diagnostics: [
14196
+ `Recorded mode: ${existingRecord.record.mode}`,
14197
+ `Resolved mode: ${resolvedLaunchMode}`
14198
+ ],
14199
+ blockedGate: "branch-state",
14200
+ blockedReason: `PRD Run ${prdRef} mode mismatch: recorded "${existingRecord.record.mode}" but resolved "${resolvedLaunchMode}". Resolve by using the correct target or --local-override.`,
14201
+ offendingPaths: []
14202
+ };
14203
+ }
13318
14204
  if (status === "drained") {
13319
14205
  skipped.push("prepare", "start", "queue");
13320
14206
  resumed.push("final-review");
@@ -13442,17 +14328,59 @@ async function runPrdRunLaunchCommand(options) {
13442
14328
  }
13443
14329
  diagnostics.push(...startResult.diagnostics);
13444
14330
  attempted.push("queue");
14331
+ if (startResult.status === "drained" && options.issueProvider && existsSync17(join21(options.repoRoot, ".pourkit", "local-prd-runs", prdRef))) {
14332
+ await closeParentGitHubIssue(
14333
+ prdRef,
14334
+ options.repoRoot,
14335
+ options.issueProvider
14336
+ );
14337
+ }
13445
14338
  }
13446
14339
  let finalReviewResult;
13447
14340
  if (!skipped.includes("final-review")) {
13448
14341
  attempted.push("final-review");
13449
- const localStorePath = join19(
14342
+ const localStorePath2 = join21(
13450
14343
  options.repoRoot,
13451
14344
  ".pourkit",
13452
14345
  "local-prd-runs",
13453
14346
  prdRef
13454
14347
  );
13455
- if (existsSync16(localStorePath)) {
14348
+ if (existsSync17(localStorePath2)) {
14349
+ const prdRecord = readPrdRun(options.repoRoot, prdRef);
14350
+ if (prdRecord.record && prdRecord.record.status !== "drained" && !canRetryFinalReviewBlock(prdRecord.record)) {
14351
+ const reason = `Final Review blocked: Queue drain not completed. Status is "${prdRecord.record.status}". Finish all child Issues before Final Review.`;
14352
+ writePrdRunRecord(options.repoRoot, {
14353
+ prdRef,
14354
+ status: "blocked",
14355
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14356
+ blockedGate: "final-review",
14357
+ blockedReason: reason,
14358
+ diagnostics: [
14359
+ `Current status: ${prdRecord.record.status}. Queue drain receipt is missing. Run child Issues through queue-run before Final Review.`
14360
+ ],
14361
+ targetName: options.targetName,
14362
+ offendingPaths: []
14363
+ });
14364
+ return {
14365
+ prdRef,
14366
+ status: "blocked",
14367
+ attempted,
14368
+ skipped: [
14369
+ ...skipped.includes("prepare") ? ["prepare"] : [],
14370
+ ...skipped.includes("start") ? ["queue"] : [],
14371
+ "reconcile"
14372
+ ],
14373
+ resumed,
14374
+ diagnostics: [
14375
+ `Current status: ${prdRecord.record.status}. Queue drain receipt is missing. Run child Issues through queue-run before Final Review.`
14376
+ ],
14377
+ blockedGate: "final-review",
14378
+ blockedReason: reason,
14379
+ offendingPaths: [],
14380
+ prepare: prepareResult,
14381
+ start: startResult
14382
+ };
14383
+ }
13456
14384
  const targetConfig = options.config?.targets?.find(
13457
14385
  (t) => t.name === options.targetName
13458
14386
  );
@@ -13491,9 +14419,48 @@ async function runPrdRunLaunchCommand(options) {
13491
14419
  start: startResult
13492
14420
  };
13493
14421
  }
14422
+ if (localResult.verdict === "blocked" || localResult.verdict === "needs_human_review") {
14423
+ const reason = localResult.message ?? `Local Final Review verdict: ${localResult.verdict}`;
14424
+ writePrdRunRecord(options.repoRoot, {
14425
+ prdRef,
14426
+ status: "blocked",
14427
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14428
+ blockedGate: "final-review",
14429
+ blockedReason: reason,
14430
+ diagnostics: [reason],
14431
+ targetName: options.targetName,
14432
+ offendingPaths: []
14433
+ });
14434
+ return {
14435
+ prdRef,
14436
+ status: "blocked",
14437
+ attempted,
14438
+ skipped: [
14439
+ ...skipped.includes("prepare") ? ["prepare"] : [],
14440
+ ...skipped.includes("start") ? ["queue"] : [],
14441
+ "reconcile"
14442
+ ],
14443
+ resumed,
14444
+ diagnostics: [reason],
14445
+ blockedGate: "final-review",
14446
+ blockedReason: reason,
14447
+ offendingPaths: [],
14448
+ prepare: prepareResult,
14449
+ start: startResult
14450
+ };
14451
+ }
14452
+ let retouchMergeCommit;
14453
+ let changedPaths;
13494
14454
  if (localResult.verdict === "pass_with_retouch") {
13495
14455
  try {
13496
- await squashFinalReviewRetouch(prdRef, options.repoRoot);
14456
+ const evidence = await squashFinalReviewRetouch(
14457
+ prdRef,
14458
+ options.repoRoot
14459
+ );
14460
+ if (evidence) {
14461
+ retouchMergeCommit = evidence.mergeCommit;
14462
+ changedPaths = evidence.changedPaths;
14463
+ }
13497
14464
  } catch (error) {
13498
14465
  const reason = error instanceof Error ? error.message : String(error);
13499
14466
  writePrdRunRecord(options.repoRoot, {
@@ -13525,14 +14492,16 @@ async function runPrdRunLaunchCommand(options) {
13525
14492
  };
13526
14493
  }
13527
14494
  }
13528
- const mappedVerdict = localResult.verdict === "pass" ? "pass_no_changes" : localResult.verdict;
13529
14495
  const receipt = {
13530
14496
  status: "final_reviewed",
13531
14497
  targetName: options.targetName,
13532
14498
  prdBranch: `local/${prdRef}`,
14499
+ mergeBase: localResult.receipt?.mergeBase,
13533
14500
  diagnostics: [],
13534
14501
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
13535
- verdict: mappedVerdict
14502
+ verdict: localResult.verdict ?? "pass_no_changes",
14503
+ retouchMergeCommit,
14504
+ changedPaths
13536
14505
  };
13537
14506
  const existingRecord2 = readPrdRun(options.repoRoot, prdRef);
13538
14507
  writePrdRunRecord(options.repoRoot, {
@@ -13607,17 +14576,100 @@ async function runPrdRunLaunchCommand(options) {
13607
14576
  let reconcileResult;
13608
14577
  if (!skipped.includes("reconcile")) {
13609
14578
  attempted.push("reconcile");
13610
- reconcileResult = await runPrdRunReconcileCommand({
13611
- repoRoot: options.repoRoot,
13612
- prdRef,
13613
- targetName: options.targetName,
13614
- config: options.config,
13615
- logger: options.logger,
13616
- executionProvider: options.executionProvider,
13617
- prProvider: options.prProvider,
13618
- autoMerge: options.autoMerge
13619
- });
13620
- if (reconcileResult.status === "blocked") {
14579
+ const localStorePath2 = join21(
14580
+ options.repoRoot,
14581
+ ".pourkit",
14582
+ "local-prd-runs",
14583
+ prdRef
14584
+ );
14585
+ if (existsSync17(localStorePath2)) {
14586
+ const prdRecord = readPrdRun(options.repoRoot, prdRef);
14587
+ const finalReviewReceipt = prdRecord.record?.finalReview;
14588
+ const hasCompletedFinalReview = finalReviewReceipt?.status === "final_reviewed" || finalReviewReceipt?.status === "succeeded";
14589
+ if (!hasCompletedFinalReview) {
14590
+ const reason = `Reconciliation blocked: Final Review not completed. Run Final Review before reconciliation.`;
14591
+ return {
14592
+ prdRef,
14593
+ status: "blocked",
14594
+ attempted,
14595
+ skipped: [...skipped],
14596
+ resumed,
14597
+ diagnostics: [reason],
14598
+ blockedGate: "reconciliation",
14599
+ blockedReason: reason,
14600
+ offendingPaths: [],
14601
+ prepare: prepareResult,
14602
+ start: startResult,
14603
+ finalReview: finalReviewResult
14604
+ };
14605
+ }
14606
+ const localResult = await runLocalReconciliation(prdRef, {
14607
+ repoRoot: options.repoRoot
14608
+ });
14609
+ if (!localResult.ok) {
14610
+ return {
14611
+ prdRef,
14612
+ status: "blocked",
14613
+ attempted,
14614
+ skipped: [...skipped],
14615
+ resumed,
14616
+ diagnostics: localResult.message ? [localResult.message] : [],
14617
+ blockedGate: "reconciliation",
14618
+ blockedReason: localResult.message ?? "Local reconciliation blocked.",
14619
+ offendingPaths: [],
14620
+ prepare: prepareResult,
14621
+ start: startResult,
14622
+ finalReview: finalReviewResult
14623
+ };
14624
+ }
14625
+ const receipt = {
14626
+ status: "succeeded",
14627
+ result: localResult.receipt?.result,
14628
+ changedPlanningPaths: localResult.receipt?.changedPlanningPaths,
14629
+ targetName: options.targetName,
14630
+ prdBranch: localResult.receipt?.prdBranch ?? `local/${prdRef}`,
14631
+ mergeCommit: localResult.receipt?.mergeCommit,
14632
+ reconciledAt: localResult.receipt?.reconciledTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
14633
+ autoMerge: false,
14634
+ agentIdentity: "pourkit-architect",
14635
+ noChange: localResult.receipt?.result === "no_changes_needed",
14636
+ source: "dev",
14637
+ target: localResult.receipt?.prdBranch ?? `local/${prdRef}`
14638
+ };
14639
+ const existingRecord2 = readPrdRun(options.repoRoot, prdRef);
14640
+ writePrdRunRecord(options.repoRoot, {
14641
+ prdRef,
14642
+ status: "completed_local_branch",
14643
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14644
+ targetName: options.targetName,
14645
+ prdBranch: `local/${prdRef}`,
14646
+ manifestPath: existingRecord2.record?.manifestPath,
14647
+ planning: existingRecord2.record?.planning,
14648
+ start: existingRecord2.record?.start,
14649
+ finalReview: existingRecord2.record?.finalReview,
14650
+ reconciliation: receipt,
14651
+ scopeChanges: existingRecord2.record?.scopeChanges
14652
+ });
14653
+ reconcileResult = {
14654
+ prdRef,
14655
+ status: "succeeded",
14656
+ reconciliation: receipt,
14657
+ diagnostics: []
14658
+ };
14659
+ diagnostics.push("Local reconciliation completed.");
14660
+ } else {
14661
+ reconcileResult = await runPrdRunReconcileCommand({
14662
+ repoRoot: options.repoRoot,
14663
+ prdRef,
14664
+ targetName: options.targetName,
14665
+ config: options.config,
14666
+ logger: options.logger,
14667
+ executionProvider: options.executionProvider,
14668
+ prProvider: options.prProvider,
14669
+ autoMerge: options.autoMerge
14670
+ });
14671
+ }
14672
+ if (reconcileResult && reconcileResult.status === "blocked") {
13621
14673
  return {
13622
14674
  prdRef,
13623
14675
  status: "blocked",
@@ -13634,7 +14686,9 @@ async function runPrdRunLaunchCommand(options) {
13634
14686
  reconcile: reconcileResult
13635
14687
  };
13636
14688
  }
13637
- diagnostics.push(...reconcileResult.diagnostics);
14689
+ if (reconcileResult) {
14690
+ diagnostics.push(...reconcileResult.diagnostics);
14691
+ }
13638
14692
  }
13639
14693
  const currentRecord = readPrdRun(options.repoRoot, prdRef).record;
13640
14694
  const reconciliationReceipt = reconcileResult && reconcileResult.status === "succeeded" ? reconcileResult.reconciliation : currentRecord?.reconciliation;
@@ -13717,6 +14771,29 @@ async function runPrdRunLaunchCommand(options) {
13717
14771
  reconcile: reconcileResult
13718
14772
  };
13719
14773
  }
14774
+ const localStorePath = join21(
14775
+ options.repoRoot,
14776
+ ".pourkit",
14777
+ "local-prd-runs",
14778
+ prdRef
14779
+ );
14780
+ if (existsSync17(localStorePath)) {
14781
+ return {
14782
+ prdRef,
14783
+ status: "completed_local_branch",
14784
+ prdBranch: `local/${prdRef}`,
14785
+ attempted,
14786
+ skipped,
14787
+ resumed,
14788
+ diagnostics: [
14789
+ `Local PRD Run ${prdRef} completed. Branch: local/${prdRef}.`
14790
+ ],
14791
+ prepare: prepareResult,
14792
+ start: startResult,
14793
+ finalReview: finalReviewResult,
14794
+ reconcile: reconcileResult
14795
+ };
14796
+ }
13720
14797
  writePrdRunRecord(options.repoRoot, {
13721
14798
  prdRef,
13722
14799
  status: "waiting_for_integration",
@@ -14046,7 +15123,7 @@ async function runLocalStartCommand(prdId, repoRoot2) {
14046
15123
  const branch = `local/${normalizedRef}`;
14047
15124
  const branchExists = (() => {
14048
15125
  try {
14049
- return spawnSync2("git", ["rev-parse", "--verify", "--quiet", branch], {
15126
+ return spawnSync3("git", ["rev-parse", "--verify", "--quiet", branch], {
14050
15127
  cwd: root,
14051
15128
  encoding: "utf8",
14052
15129
  stdio: "pipe"
@@ -14066,7 +15143,7 @@ async function runLocalStartCommand(prdId, repoRoot2) {
14066
15143
  message: `Branch ${branch} already exists without a matching start receipt. Remove it manually or use a different PRD ID.`
14067
15144
  };
14068
15145
  }
14069
- const branchResult = spawnSync2("git", ["branch", branch], {
15146
+ const branchResult = spawnSync3("git", ["branch", branch], {
14070
15147
  cwd: root,
14071
15148
  encoding: "utf8",
14072
15149
  stdio: "pipe"
@@ -14081,7 +15158,7 @@ async function runLocalStartCommand(prdId, repoRoot2) {
14081
15158
  }
14082
15159
  const baseCommit = (() => {
14083
15160
  try {
14084
- return spawnSync2("git", ["rev-parse", "HEAD"], {
15161
+ return spawnSync3("git", ["rev-parse", "HEAD"], {
14085
15162
  cwd: root,
14086
15163
  encoding: "utf8",
14087
15164
  stdio: "pipe"
@@ -14203,6 +15280,17 @@ async function runLocalLaunchCommand(prdId, repoRoot2) {
14203
15280
  };
14204
15281
  return { ok: false, failedStage: "finalReview", stages };
14205
15282
  }
15283
+ if (result.verdict === "blocked" || result.verdict === "needs_human_review") {
15284
+ stages.finalReview = {
15285
+ ok: false,
15286
+ stage: "finalReview",
15287
+ failureCode: result.verdict === "blocked" ? "final_review_blocked" : "needs_human_review",
15288
+ repairabilityClass: "manual",
15289
+ reason: result.message ?? `Final Review verdict: ${result.verdict}`,
15290
+ repairGuidance: `Fix final review failures and rerun \`pourkit prd-run launch --local ${normalizedRef}\`.`
15291
+ };
15292
+ return { ok: false, failedStage: "finalReview", stages };
15293
+ }
14206
15294
  if (result.verdict === "pass_with_retouch") {
14207
15295
  try {
14208
15296
  await squashFinalReviewRetouch(normalizedRef, root);
@@ -14322,6 +15410,32 @@ async function runPrdRunStartCommand(options) {
14322
15410
  );
14323
15411
  }
14324
15412
  const existingRecord = readPrdRun(options.repoRoot, prdRef);
15413
+ const resolvedMode = options.config ? (() => {
15414
+ try {
15415
+ const target = resolveTarget(options.config, targetName);
15416
+ return resolvePrdRunMode(target).mode;
15417
+ } catch {
15418
+ return void 0;
15419
+ }
15420
+ })() : void 0;
15421
+ if (resolvedMode && existingRecord.record?.mode && existingRecord.record.mode !== resolvedMode) {
15422
+ const failure = {
15423
+ ok: false,
15424
+ gate: "branch-state",
15425
+ reason: `PRD Run ${prdRef} mode mismatch: recorded "${existingRecord.record.mode}" but resolved "${resolvedMode}". Resolve by using the correct target or --local-override.`,
15426
+ diagnostics: [
15427
+ `Recorded mode: ${existingRecord.record.mode}`,
15428
+ `Resolved mode: ${resolvedMode}`
15429
+ ],
15430
+ offendingPaths: []
15431
+ };
15432
+ persistBlockedPrdRunStartRecord(options.repoRoot, prdRef, failure, {
15433
+ manifestPath: existingRecord.record?.manifestPath,
15434
+ planning: existingRecord.record?.planning,
15435
+ targetName
15436
+ });
15437
+ return buildBlockedStartResult(prdRef, failure);
15438
+ }
14325
15439
  if (existingRecord.record?.start) {
14326
15440
  const fetchResult2 = fetchOriginDev(options.repoRoot);
14327
15441
  if (!fetchResult2.ok) {
@@ -14357,7 +15471,8 @@ async function runPrdRunStartCommand(options) {
14357
15471
  ...reusedStart
14358
15472
  },
14359
15473
  {
14360
- targetName
15474
+ targetName,
15475
+ mode: resolvedMode
14361
15476
  }
14362
15477
  );
14363
15478
  return await processStartResult(
@@ -14460,6 +15575,7 @@ async function runPrdRunStartCommand(options) {
14460
15575
  },
14461
15576
  {
14462
15577
  targetName,
15578
+ mode: resolvedMode,
14463
15579
  manifestPath: existingRecord.record?.manifestPath,
14464
15580
  planning: existingRecord.record?.planning
14465
15581
  }
@@ -14619,6 +15735,7 @@ async function runPrdRunStartCommand(options) {
14619
15735
  },
14620
15736
  {
14621
15737
  targetName,
15738
+ mode: resolvedMode,
14622
15739
  manifestPath: existingRecord.record?.manifestPath ?? manifest?.manifestPath,
14623
15740
  planning: existingRecord.record?.planning
14624
15741
  }
@@ -14701,6 +15818,68 @@ async function runPrdRunStartCommand(options) {
14701
15818
  });
14702
15819
  return buildBlockedStartResult(prdRef, failure);
14703
15820
  }
15821
+ if (resolvedMode === "local") {
15822
+ const localBranchResult = materializeLocalPrdBranch(
15823
+ prdRef,
15824
+ fetchResult.startBaseCommit,
15825
+ options.repoRoot
15826
+ );
15827
+ if (!localBranchResult.ok) {
15828
+ const failure = {
15829
+ ok: false,
15830
+ gate: "branch-state",
15831
+ reason: localBranchResult.message,
15832
+ diagnostics: [localBranchResult.message],
15833
+ offendingPaths: []
15834
+ };
15835
+ persistBlockedPrdRunStartRecord(options.repoRoot, prdRef, failure, {
15836
+ manifestPath: existingRecord.record?.manifestPath ?? manifest?.manifestPath,
15837
+ planning: existingRecord.record?.planning,
15838
+ targetName
15839
+ });
15840
+ return buildBlockedStartResult(prdRef, failure);
15841
+ }
15842
+ const localStoreDir = join21(
15843
+ options.repoRoot,
15844
+ ".pourkit",
15845
+ "local-prd-runs",
15846
+ prdRef
15847
+ );
15848
+ let localStoreReady = false;
15849
+ if (existsSync17(localStoreDir)) {
15850
+ const localStorePath = join21(localStoreDir, "prd.json");
15851
+ try {
15852
+ const content = JSON.parse(readFileSync17(localStorePath, "utf8"));
15853
+ localStoreReady = content?.id === prdRef && content?.kind === "prd";
15854
+ } catch {
15855
+ localStoreReady = false;
15856
+ }
15857
+ }
15858
+ const prereqs = {
15859
+ localBranchName: getLocalPrdBranchName(prdRef),
15860
+ localBranchExists: !localBranchResult.created,
15861
+ localBranchCommit: fetchResult.startBaseCommit,
15862
+ localStoreReady
15863
+ };
15864
+ if (existsSync17(localStoreDir) && !prereqs.localStoreReady) {
15865
+ const failure = {
15866
+ ok: false,
15867
+ gate: "branch-state",
15868
+ 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.`,
15869
+ diagnostics: [
15870
+ `Expected store path: .pourkit/local-prd-runs/${prdRef}/prd.json`,
15871
+ `Expected PRD ID: ${prdRef}`
15872
+ ],
15873
+ offendingPaths: []
15874
+ };
15875
+ persistBlockedPrdRunStartRecord(options.repoRoot, prdRef, failure, {
15876
+ manifestPath: existingRecord.record?.manifestPath ?? manifest?.manifestPath,
15877
+ planning: existingRecord.record?.planning,
15878
+ targetName
15879
+ });
15880
+ return buildBlockedStartResult(prdRef, failure);
15881
+ }
15882
+ }
14704
15883
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
14705
15884
  const start = {
14706
15885
  status: "started",
@@ -14728,6 +15907,7 @@ async function runPrdRunStartCommand(options) {
14728
15907
  updatedAt,
14729
15908
  targetName,
14730
15909
  start,
15910
+ mode: resolvedMode,
14731
15911
  manifestPath: existingRecord.record?.manifestPath ?? manifest?.manifestPath ?? void 0,
14732
15912
  planning: existingRecord.record?.planning
14733
15913
  });
@@ -14751,26 +15931,100 @@ async function processStartResult(startResult, options) {
14751
15931
  start.queueStartedAt = (/* @__PURE__ */ new Date()).toISOString();
14752
15932
  start.queueCommand = "queue-run";
14753
15933
  const existingRecord = readPrdRun(repoRoot2, prdRef);
15934
+ let resolvedMode;
15935
+ try {
15936
+ const target = options.config ? resolveTarget(options.config, start.targetName) : null;
15937
+ resolvedMode = target?.strategy ? resolvePrdRunMode(target) : void 0;
15938
+ } catch {
15939
+ resolvedMode = void 0;
15940
+ }
15941
+ const modeForRecord = resolvedMode?.mode;
14754
15942
  writePrdRunRecord(repoRoot2, {
14755
15943
  prdRef,
14756
15944
  status: "running",
14757
15945
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14758
15946
  targetName: start.targetName,
14759
15947
  start,
15948
+ mode: modeForRecord,
14760
15949
  manifestPath: existingRecord.record?.manifestPath,
14761
15950
  planning: existingRecord.record?.planning
14762
15951
  });
14763
- const localStorePath = join19(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
14764
- if (existsSync16(localStorePath)) {
14765
- const localIssues = await getRunnableLocalIssues(prdRef, repoRoot2);
15952
+ const localStorePath = join21(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
15953
+ if (existsSync17(localStorePath)) {
15954
+ const queueResult = await runLocalQueueLoop(
15955
+ prdRef,
15956
+ repoRoot2,
15957
+ options.issueProvider
15958
+ );
15959
+ if (!queueResult.ok) {
15960
+ const failureCode = queueResult.failureCode ?? "queue_error";
15961
+ const reason = queueResult.repairGuidance ?? `Local queue loop blocked: ${failureCode}`;
15962
+ const diagnostics = [
15963
+ `Local queue loop failed with failureCode "${failureCode}".`,
15964
+ ...queueResult.repairGuidance ? [`Repair guidance: ${queueResult.repairGuidance}`] : []
15965
+ ];
15966
+ writePrdRunRecord(repoRoot2, {
15967
+ prdRef,
15968
+ status: "blocked",
15969
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15970
+ targetName: start.targetName,
15971
+ start,
15972
+ mode: modeForRecord,
15973
+ manifestPath: existingRecord.record?.manifestPath,
15974
+ planning: existingRecord.record?.planning,
15975
+ blockedGate: "queue",
15976
+ blockedReason: reason,
15977
+ diagnostics,
15978
+ offendingPaths: []
15979
+ });
15980
+ return {
15981
+ prdRef,
15982
+ status: "blocked",
15983
+ blockedGate: "queue",
15984
+ blockedReason: reason,
15985
+ diagnostics,
15986
+ offendingPaths: []
15987
+ };
15988
+ }
15989
+ if (queueResult.blockedIssues.length > 0) {
15990
+ const reason = `Queue processed complete but ${queueResult.blockedIssues.length} blocked child issue(s) remain.`;
15991
+ const diagnostics = [
15992
+ reason,
15993
+ `Blocked issues: ${queueResult.blockedIssues.join(", ")}`,
15994
+ "Resolve blocked children before parent close."
15995
+ ];
15996
+ writePrdRunRecord(repoRoot2, {
15997
+ prdRef,
15998
+ status: "blocked",
15999
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16000
+ targetName: start.targetName,
16001
+ start,
16002
+ mode: modeForRecord,
16003
+ manifestPath: existingRecord.record?.manifestPath,
16004
+ planning: existingRecord.record?.planning,
16005
+ blockedGate: "queue",
16006
+ blockedReason: reason,
16007
+ diagnostics,
16008
+ offendingPaths: []
16009
+ });
16010
+ return {
16011
+ prdRef,
16012
+ status: "blocked",
16013
+ blockedGate: "queue",
16014
+ blockedReason: reason,
16015
+ diagnostics,
16016
+ offendingPaths: []
16017
+ };
16018
+ }
14766
16019
  start.queueDrainedAt = (/* @__PURE__ */ new Date()).toISOString();
14767
- start.queueProcessedCount = localIssues.length;
16020
+ start.queueProcessedCount = queueResult.completedIssues.length + queueResult.blockedIssues.length;
14768
16021
  writePrdRunRecord(repoRoot2, {
14769
16022
  prdRef,
14770
16023
  status: "drained",
14771
16024
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14772
16025
  targetName: start.targetName,
14773
16026
  start,
16027
+ mode: modeForRecord,
14774
16028
  manifestPath: existingRecord.record?.manifestPath,
14775
16029
  planning: existingRecord.record?.planning
14776
16030
  });
@@ -14795,7 +16049,8 @@ async function processStartResult(startResult, options) {
14795
16049
  prdRef,
14796
16050
  queueRunContext: {
14797
16051
  prdRef,
14798
- prdBranch: start.prdBranch
16052
+ prdBranch: start.prdBranch,
16053
+ prdRunMode: resolvedMode
14799
16054
  }
14800
16055
  });
14801
16056
  if (outcome.selected === null && outcome.code === "drained") {
@@ -14812,6 +16067,7 @@ async function processStartResult(startResult, options) {
14812
16067
  diagnostics,
14813
16068
  targetName: start.targetName,
14814
16069
  start,
16070
+ mode: existingRecord.record?.mode,
14815
16071
  manifestPath: existingRecord.record?.manifestPath,
14816
16072
  planning: existingRecord.record?.planning,
14817
16073
  offendingPaths: []
@@ -14833,6 +16089,7 @@ async function processStartResult(startResult, options) {
14833
16089
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14834
16090
  targetName: start.targetName,
14835
16091
  start,
16092
+ mode: existingRecord.record?.mode,
14836
16093
  manifestPath: existingRecord.record?.manifestPath,
14837
16094
  planning: existingRecord.record?.planning
14838
16095
  });
@@ -14854,6 +16111,7 @@ async function processStartResult(startResult, options) {
14854
16111
  diagnostics,
14855
16112
  targetName: start.targetName,
14856
16113
  start,
16114
+ mode: existingRecord.record?.mode,
14857
16115
  manifestPath: existingRecord.record?.manifestPath,
14858
16116
  planning: existingRecord.record?.planning,
14859
16117
  offendingPaths: []
@@ -14878,6 +16136,7 @@ async function processStartResult(startResult, options) {
14878
16136
  diagnostics: outcomeDiagnostics,
14879
16137
  targetName: start.targetName,
14880
16138
  start,
16139
+ mode: existingRecord.record?.mode,
14881
16140
  manifestPath: existingRecord.record?.manifestPath,
14882
16141
  planning: existingRecord.record?.planning,
14883
16142
  offendingPaths: []
@@ -14902,6 +16161,7 @@ async function processStartResult(startResult, options) {
14902
16161
  diagnostics,
14903
16162
  targetName: start.targetName,
14904
16163
  start,
16164
+ mode: existingRecord.record?.mode,
14905
16165
  manifestPath: existingRecord.record?.manifestPath,
14906
16166
  planning: existingRecord.record?.planning,
14907
16167
  offendingPaths: []
@@ -14933,12 +16193,13 @@ function persistStartingPrdRunRecord(repoRoot2, prdRef, existingRecord, start, c
14933
16193
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14934
16194
  targetName: context.targetName,
14935
16195
  start,
16196
+ mode: context.mode,
14936
16197
  manifestPath: context.manifestPath ?? existingRecord.record?.manifestPath,
14937
16198
  planning: context.planning ?? existingRecord.record?.planning
14938
16199
  });
14939
16200
  }
14940
16201
  function inspectRemotePrdBranch(repoRoot2, prdRef) {
14941
- const result = spawnSync2("git", ["ls-remote", "--heads", "origin", prdRef], {
16202
+ const result = spawnSync3("git", ["ls-remote", "--heads", "origin", prdRef], {
14942
16203
  cwd: repoRoot2,
14943
16204
  encoding: "utf8"
14944
16205
  });
@@ -14963,7 +16224,7 @@ function inspectRemotePrdBranch(repoRoot2, prdRef) {
14963
16224
  }
14964
16225
  function ensureAdoptableExistingPrdBranch(repoRoot2, prdRef, prepareMergeCommit) {
14965
16226
  const branchRef = `refs/remotes/origin/${prdRef}`;
14966
- const result = spawnSync2(
16227
+ const result = spawnSync3(
14967
16228
  "git",
14968
16229
  ["merge-base", "--is-ancestor", prepareMergeCommit, branchRef],
14969
16230
  {
@@ -15093,7 +16354,7 @@ async function createOrReusePlanningPr(options) {
15093
16354
  });
15094
16355
  }
15095
16356
  function fetchOriginDev(repoRoot2) {
15096
- const fetchResult = spawnSync2("git", ["fetch", "origin", "dev"], {
16357
+ const fetchResult = spawnSync3("git", ["fetch", "origin", "dev"], {
15097
16358
  cwd: repoRoot2,
15098
16359
  encoding: "utf8"
15099
16360
  });
@@ -15110,7 +16371,7 @@ function fetchOriginDev(repoRoot2) {
15110
16371
  offendingPaths: []
15111
16372
  };
15112
16373
  }
15113
- const revParseResult = spawnSync2("git", ["rev-parse", "origin/dev"], {
16374
+ const revParseResult = spawnSync3("git", ["rev-parse", "origin/dev"], {
15114
16375
  cwd: repoRoot2,
15115
16376
  encoding: "utf8"
15116
16377
  });
@@ -15130,7 +16391,7 @@ function fetchOriginDev(repoRoot2) {
15130
16391
  return { ok: true, startBaseCommit: revParseResult.stdout.trim() };
15131
16392
  }
15132
16393
  function fetchPrdBranch(repoRoot2, prdRef) {
15133
- const result = spawnSync2("git", ["fetch", "origin", prdRef], {
16394
+ const result = spawnSync3("git", ["fetch", "origin", prdRef], {
15134
16395
  cwd: repoRoot2,
15135
16396
  encoding: "utf8"
15136
16397
  });
@@ -15150,7 +16411,7 @@ function fetchPrdBranch(repoRoot2, prdRef) {
15150
16411
  return { ok: true };
15151
16412
  }
15152
16413
  function ensureAncestorGuard(repoRoot2, prepareMergeCommit, startBaseRef, startBaseCommit) {
15153
- const result = spawnSync2(
16414
+ const result = spawnSync3(
15154
16415
  "git",
15155
16416
  ["merge-base", "--is-ancestor", prepareMergeCommit, startBaseRef],
15156
16417
  {
@@ -15177,7 +16438,7 @@ function ensureAncestorGuard(repoRoot2, prepareMergeCommit, startBaseRef, startB
15177
16438
  return { ok: true };
15178
16439
  }
15179
16440
  async function ensurePrdBranchPublished(repoRoot2, prdRef, startBaseCommit) {
15180
- const pushResult = spawnSync2(
16441
+ const pushResult = spawnSync3(
15181
16442
  "git",
15182
16443
  ["push", "origin", `${startBaseCommit}:refs/heads/${prdRef}`],
15183
16444
  {
@@ -15230,7 +16491,7 @@ function buildPlanningPrBody(options) {
15230
16491
  ].join("\n");
15231
16492
  }
15232
16493
  function runGitOrThrow(cwd, args, label) {
15233
- const result = spawnSync2("git", args, { cwd, encoding: "utf8" });
16494
+ const result = spawnSync3("git", args, { cwd, encoding: "utf8" });
15234
16495
  if (result.status !== 0) {
15235
16496
  throw new Error(
15236
16497
  `Failed to ${label}: ${[
@@ -15242,7 +16503,7 @@ function runGitOrThrow(cwd, args, label) {
15242
16503
  }
15243
16504
  }
15244
16505
  function validatePrepareAutoMergeDevCheckout(repoRoot2) {
15245
- const branchResult = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
16506
+ const branchResult = spawnSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
15246
16507
  cwd: repoRoot2,
15247
16508
  encoding: "utf8"
15248
16509
  });
@@ -15273,7 +16534,7 @@ function validatePrepareAutoMergeDevCheckout(repoRoot2) {
15273
16534
  function syncLocalDevToOriginDev(repoRoot2) {
15274
16535
  const branchResult = validatePrepareAutoMergeDevCheckout(repoRoot2);
15275
16536
  if (!branchResult.ok) return branchResult;
15276
- const fetchResult = spawnSync2("git", ["fetch", "origin", "dev"], {
16537
+ const fetchResult = spawnSync3("git", ["fetch", "origin", "dev"], {
15277
16538
  cwd: repoRoot2,
15278
16539
  encoding: "utf8"
15279
16540
  });
@@ -15289,7 +16550,7 @@ function syncLocalDevToOriginDev(repoRoot2) {
15289
16550
  offendingPaths: []
15290
16551
  };
15291
16552
  }
15292
- const resetResult = spawnSync2("git", ["reset", "--hard", "origin/dev"], {
16553
+ const resetResult = spawnSync3("git", ["reset", "--hard", "origin/dev"], {
15293
16554
  cwd: repoRoot2,
15294
16555
  encoding: "utf8"
15295
16556
  });
@@ -15333,14 +16594,14 @@ function isArchitecturePlanningDiffPath(path9) {
15333
16594
  return path9.startsWith(".pourkit/architecture/") || path9 === ".pourkit/CONTEXT.md" || path9.startsWith(".pourkit/docs/");
15334
16595
  }
15335
16596
  async function ensurePlanningBranchPublished(repoRoot2, branchName, changedFiles) {
15336
- const remoteResult = spawnSync2("git", ["remote", "get-url", "origin"], {
16597
+ const remoteResult = spawnSync3("git", ["remote", "get-url", "origin"], {
15337
16598
  cwd: repoRoot2,
15338
16599
  encoding: "utf8"
15339
16600
  });
15340
16601
  if (remoteResult.status !== 0) {
15341
16602
  return;
15342
16603
  }
15343
- const worktreePath = mkdtempSync(join19(tmpdir(), "pourkit-planning-"));
16604
+ const worktreePath = mkdtempSync(join21(tmpdir(), "pourkit-planning-"));
15344
16605
  try {
15345
16606
  runGitOrThrow(repoRoot2, ["fetch", "origin", "dev"], "fetch origin/dev");
15346
16607
  runGitOrThrow(
@@ -15354,9 +16615,9 @@ async function ensurePlanningBranchPublished(repoRoot2, branchName, changedFiles
15354
16615
  "create planning branch"
15355
16616
  );
15356
16617
  for (const changedFile of changedFiles) {
15357
- const sourcePath = join19(repoRoot2, changedFile);
15358
- const targetPath = join19(worktreePath, changedFile);
15359
- mkdirSync10(dirname5(targetPath), { recursive: true });
16618
+ const sourcePath = join21(repoRoot2, changedFile);
16619
+ const targetPath = join21(worktreePath, changedFile);
16620
+ mkdirSync11(dirname5(targetPath), { recursive: true });
15360
16621
  cpSync(sourcePath, targetPath, { recursive: true });
15361
16622
  }
15362
16623
  runGitOrThrow(
@@ -15370,13 +16631,13 @@ async function ensurePlanningBranchPublished(repoRoot2, branchName, changedFiles
15370
16631
  "commit planning diff"
15371
16632
  );
15372
16633
  } finally {
15373
- spawnSync2("git", ["worktree", "remove", "--force", worktreePath], {
16634
+ spawnSync3("git", ["worktree", "remove", "--force", worktreePath], {
15374
16635
  cwd: repoRoot2,
15375
16636
  encoding: "utf8"
15376
16637
  });
15377
16638
  rmSync3(worktreePath, { recursive: true, force: true });
15378
16639
  }
15379
- const pushResult = spawnSync2(
16640
+ const pushResult = spawnSync3(
15380
16641
  "git",
15381
16642
  ["push", "--force", "origin", `${branchName}:refs/heads/${branchName}`],
15382
16643
  {
@@ -15512,7 +16773,7 @@ function validateStartPrepareReceipt(prdRef, record, options = {}) {
15512
16773
  function validateStartArtifactExistenceOnOriginDev(repoRoot2, manifest) {
15513
16774
  for (const artifactPath of listManifestArtifactPaths(repoRoot2, manifest)) {
15514
16775
  const repoRelativePath = toRepoRelativePath2(repoRoot2, artifactPath);
15515
- const result = spawnSync2(
16776
+ const result = spawnSync3(
15516
16777
  "git",
15517
16778
  ["show", `origin/dev:${repoRelativePath}`],
15518
16779
  {
@@ -15538,7 +16799,7 @@ function validateStartArtifactExistenceOnOriginDev(repoRoot2, manifest) {
15538
16799
  return { ok: true };
15539
16800
  }
15540
16801
  function collectGitChangedFiles(repoRoot2, ignoredPrdRunRecordRef) {
15541
- const result = spawnSync2("git", ["status", "--porcelain=v1", "-uall"], {
16802
+ const result = spawnSync3("git", ["status", "--porcelain=v1", "-uall"], {
15542
16803
  cwd: repoRoot2,
15543
16804
  encoding: "utf8"
15544
16805
  });
@@ -15592,12 +16853,12 @@ function collectGitChangedFiles(repoRoot2, ignoredPrdRunRecordRef) {
15592
16853
  changedFiles: Array.from(new Set(changedFiles))
15593
16854
  };
15594
16855
  }
15595
- function collectObservedReconciliationPlanningDiff(options) {
15596
- const statusResult = spawnSync2("git", ["status", "--porcelain=v1", "-uall"], {
16856
+ function collectObservedReconciliationDirtyPaths(options) {
16857
+ const statusResult = spawnSync3("git", ["status", "--porcelain=v1", "-uall"], {
15597
16858
  cwd: options.worktreeCwd,
15598
16859
  encoding: "utf8"
15599
16860
  });
15600
- const diffResult = spawnSync2("git", ["diff", "--name-status"], {
16861
+ const diffResult = spawnSync3("git", ["diff", "--name-status"], {
15601
16862
  cwd: options.worktreeCwd,
15602
16863
  encoding: "utf8"
15603
16864
  });
@@ -15625,37 +16886,19 @@ function collectObservedReconciliationPlanningDiff(options) {
15625
16886
  )
15626
16887
  )
15627
16888
  );
15628
- const intrinsicallyRejectedPaths = candidatePaths.filter(
15629
- (path9) => !isAllowedPlanningPath(path9) || isBlockedPlanningPath(path9) || hasDirectoryTraversal(path9)
15630
- );
15631
- const allowedCandidatePaths = candidatePaths.filter(
15632
- (path9) => !intrinsicallyRejectedPaths.includes(path9)
15633
- );
15634
- const validation = validateChangedPlanningPaths(allowedCandidatePaths, {
15635
- repoRoot: options.worktreeCwd,
15636
- gitStatusShortLines,
15637
- gitDiffNameStatusLines
15638
- });
15639
- const rejectedPaths = Array.from(
15640
- /* @__PURE__ */ new Set([
15641
- ...intrinsicallyRejectedPaths,
15642
- ...validation.ok ? [] : validation.rejected ?? []
15643
- ])
15644
- ).sort();
15645
16889
  return {
15646
16890
  ok: true,
15647
- changedPlanningPaths: allowedCandidatePaths.filter((path9) => !rejectedPaths.includes(path9)).sort(),
15648
- rejectedPaths
16891
+ dirtyPaths: candidatePaths.sort()
15649
16892
  };
15650
16893
  }
15651
16894
  function gitRefExists(repoRoot2, ref) {
15652
- return spawnSync2("git", ["rev-parse", "--verify", "--quiet", ref], {
16895
+ return spawnSync3("git", ["rev-parse", "--verify", "--quiet", ref], {
15653
16896
  cwd: repoRoot2,
15654
16897
  encoding: "utf8"
15655
16898
  }).status === 0;
15656
16899
  }
15657
16900
  function collectGitDiffNames(repoRoot2, baseRef, headRef) {
15658
- const result = spawnSync2("git", ["diff", "--name-only", baseRef, headRef], {
16901
+ const result = spawnSync3("git", ["diff", "--name-only", baseRef, headRef], {
15659
16902
  cwd: repoRoot2,
15660
16903
  encoding: "utf8"
15661
16904
  });
@@ -15670,7 +16913,7 @@ function parseGitStatusLine(line) {
15670
16913
  return { status, path: path9.replace(/^"|"$/g, "") };
15671
16914
  }
15672
16915
  function validateManifestArtifactExistence(repoRoot2, manifest) {
15673
- const missingPaths = listManifestArtifactPaths(repoRoot2, manifest).filter((path9) => !existsSync16(path9)).map((path9) => toRepoRelativePath2(repoRoot2, path9));
16916
+ const missingPaths = listManifestArtifactPaths(repoRoot2, manifest).filter((path9) => !existsSync17(path9)).map((path9) => toRepoRelativePath2(repoRoot2, path9));
15674
16917
  if (missingPaths.length > 0) {
15675
16918
  return {
15676
16919
  ok: false,
@@ -16086,7 +17329,7 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
16086
17329
  }
16087
17330
 
16088
17331
  // commands/init.ts
16089
- import { existsSync as existsSync17, statSync } from "fs";
17332
+ import { existsSync as existsSync18, statSync } from "fs";
16090
17333
  import {
16091
17334
  copyFile,
16092
17335
  mkdir as mkdir5,
@@ -16589,7 +17832,7 @@ async function computeFileChecksum(filePath) {
16589
17832
  return createHash3("sha256").update(content).digest("hex");
16590
17833
  }
16591
17834
  function lockfileExists(root, name) {
16592
- return existsSync17(path5.join(root, name));
17835
+ return existsSync18(path5.join(root, name));
16593
17836
  }
16594
17837
  function detectPackageManager(root) {
16595
17838
  if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
@@ -16633,7 +17876,7 @@ async function discoverLocalSource(sourcePath) {
16633
17876
  async function discoverReadme(root) {
16634
17877
  for (const name of ["README.md", "readme.md"]) {
16635
17878
  const p = path5.join(root, name);
16636
- if (existsSync17(p)) {
17879
+ if (existsSync18(p)) {
16637
17880
  return p;
16638
17881
  }
16639
17882
  }
@@ -16643,7 +17886,7 @@ async function discoverAgentFiles(root) {
16643
17886
  const files = [];
16644
17887
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
16645
17888
  const p = path5.join(root, name);
16646
- if (existsSync17(p)) {
17889
+ if (existsSync18(p)) {
16647
17890
  files.push(p);
16648
17891
  }
16649
17892
  }
@@ -16651,7 +17894,7 @@ async function discoverAgentFiles(root) {
16651
17894
  }
16652
17895
  async function discoverMerlleState(root) {
16653
17896
  const p = path5.join(root, ".pourkit", "state.json");
16654
- return existsSync17(p) ? p : null;
17897
+ return existsSync18(p) ? p : null;
16655
17898
  }
16656
17899
  async function discoverAgentSkills(root) {
16657
17900
  const dirs = [
@@ -16660,7 +17903,7 @@ async function discoverAgentSkills(root) {
16660
17903
  ];
16661
17904
  const found = [];
16662
17905
  for (const d of dirs) {
16663
- if (existsSync17(d)) {
17906
+ if (existsSync18(d)) {
16664
17907
  found.push(d);
16665
17908
  }
16666
17909
  }
@@ -16670,12 +17913,12 @@ async function discoverRootDomainDocs(root) {
16670
17913
  const docs = [];
16671
17914
  for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
16672
17915
  const p = path5.join(root, name);
16673
- if (existsSync17(p)) {
17916
+ if (existsSync18(p)) {
16674
17917
  docs.push(p);
16675
17918
  }
16676
17919
  }
16677
17920
  const adrDir = path5.join(root, "docs", "adr");
16678
- if (existsSync17(adrDir)) {
17921
+ if (existsSync18(adrDir)) {
16679
17922
  const entries = await readdir(adrDir, { withFileTypes: true });
16680
17923
  for (const entry of entries) {
16681
17924
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -16818,7 +18061,7 @@ async function planInit(options) {
16818
18061
  for (const file of skillFiles) {
16819
18062
  const relPath = path5.relative(s, file);
16820
18063
  const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
16821
- if (!existsSync17(destPath)) {
18064
+ if (!existsSync18(destPath)) {
16822
18065
  operations.push({
16823
18066
  kind: "copy",
16824
18067
  sourcePath: file,
@@ -16881,7 +18124,7 @@ async function planInit(options) {
16881
18124
  });
16882
18125
  }
16883
18126
  if (sourceRoot) {
16884
- if (!existsSync17(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
18127
+ if (!existsSync18(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
16885
18128
  warnings.push(
16886
18129
  `--from-local path does not exist or is not a directory: ${sourceRoot}`
16887
18130
  );
@@ -17000,7 +18243,7 @@ async function planInit(options) {
17000
18243
  requiresConfirmation: false,
17001
18244
  destructive: false
17002
18245
  });
17003
- } else if (existsSync17(destPath)) {
18246
+ } else if (existsSync18(destPath)) {
17004
18247
  operations.push({
17005
18248
  kind: "skip",
17006
18249
  path: destPath,
@@ -17058,7 +18301,7 @@ async function planInit(options) {
17058
18301
  }
17059
18302
  }
17060
18303
  const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
17061
- if (!existsSync17(contextPath) && !merleDestPaths.has(contextPath)) {
18304
+ if (!existsSync18(contextPath) && !merleDestPaths.has(contextPath)) {
17062
18305
  operations.push({
17063
18306
  kind: "create",
17064
18307
  path: contextPath,
@@ -17076,7 +18319,7 @@ async function planInit(options) {
17076
18319
  "adr",
17077
18320
  ".gitkeep"
17078
18321
  );
17079
- if (!existsSync17(adrGitkeep)) {
18322
+ if (!existsSync18(adrGitkeep)) {
17080
18323
  operations.push({
17081
18324
  kind: "create",
17082
18325
  path: adrGitkeep,
@@ -17088,7 +18331,7 @@ async function planInit(options) {
17088
18331
  }
17089
18332
  const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
17090
18333
  const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
17091
- if (existsSync17(srcDocAgents) && !existsSync17(tgtDocAgents)) {
18334
+ if (existsSync18(srcDocAgents) && !existsSync18(tgtDocAgents)) {
17092
18335
  const docFiles = await walkDir(srcDocAgents);
17093
18336
  for (const file of docFiles) {
17094
18337
  const relPath = path5.relative(srcDocAgents, file);
@@ -17121,7 +18364,7 @@ async function planInit(options) {
17121
18364
  }
17122
18365
  const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
17123
18366
  const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
17124
- if (existsSync17(srcPrompts) && !existsSync17(tgtPrompts)) {
18367
+ if (existsSync18(srcPrompts) && !existsSync18(tgtPrompts)) {
17125
18368
  const promptFiles = await walkDir(srcPrompts);
17126
18369
  for (const file of promptFiles) {
17127
18370
  const relPath = path5.relative(srcPrompts, file);
@@ -17148,7 +18391,7 @@ async function planInit(options) {
17148
18391
  ".sandcastle",
17149
18392
  "Dockerfile"
17150
18393
  );
17151
- if (existsSync17(tgtSandboxDockerfile)) {
18394
+ if (existsSync18(tgtSandboxDockerfile)) {
17152
18395
  operations.push({
17153
18396
  kind: "skip",
17154
18397
  path: tgtSandboxDockerfile,
@@ -17157,7 +18400,7 @@ async function planInit(options) {
17157
18400
  requiresConfirmation: false,
17158
18401
  destructive: false
17159
18402
  });
17160
- } else if (existsSync17(srcSandboxDockerfile)) {
18403
+ } else if (existsSync18(srcSandboxDockerfile)) {
17161
18404
  const checksum = await computeFileChecksum(srcSandboxDockerfile);
17162
18405
  operations.push({
17163
18406
  kind: "copy",
@@ -17171,7 +18414,7 @@ async function planInit(options) {
17171
18414
  });
17172
18415
  }
17173
18416
  const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
17174
- if (!existsSync17(configTsPath)) {
18417
+ if (!existsSync18(configTsPath)) {
17175
18418
  const verifyCommands = inferVerificationCommands(
17176
18419
  packageScripts,
17177
18420
  pm || "npm"
@@ -17208,7 +18451,7 @@ async function planInit(options) {
17208
18451
  const hasExistingAgents = operations.some(
17209
18452
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
17210
18453
  );
17211
- if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync17(path5.join(targetRoot, "AGENTS.md"))) {
18454
+ if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync18(path5.join(targetRoot, "AGENTS.md"))) {
17212
18455
  operations.push({
17213
18456
  kind: "create",
17214
18457
  path: path5.join(targetRoot, "AGENTS.md"),
@@ -17224,7 +18467,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
17224
18467
  const hasExistingClaude = operations.some(
17225
18468
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
17226
18469
  );
17227
- if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync17(path5.join(targetRoot, "CLAUDE.md"))) {
18470
+ if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync18(path5.join(targetRoot, "CLAUDE.md"))) {
17228
18471
  operations.push({
17229
18472
  kind: "create",
17230
18473
  path: path5.join(targetRoot, "CLAUDE.md"),
@@ -17239,7 +18482,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
17239
18482
  }
17240
18483
  const gitignoreTarget = path5.join(targetRoot, ".gitignore");
17241
18484
  const gitignoreContent = generateGitignoreBlock();
17242
- if (!existsSync17(gitignoreTarget)) {
18485
+ if (!existsSync18(gitignoreTarget)) {
17243
18486
  operations.push({
17244
18487
  kind: "create",
17245
18488
  path: gitignoreTarget,
@@ -17263,7 +18506,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
17263
18506
  });
17264
18507
  }
17265
18508
  const openCodePath = path5.join(targetRoot, "opencode.json");
17266
- if (!existsSync17(openCodePath)) {
18509
+ if (!existsSync18(openCodePath)) {
17267
18510
  operations.push({
17268
18511
  kind: "create",
17269
18512
  path: openCodePath,
@@ -17320,7 +18563,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
17320
18563
  }
17321
18564
  }
17322
18565
  const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
17323
- if (existsSync17(manifestPath)) {
18566
+ if (existsSync18(manifestPath)) {
17324
18567
  operations.push({
17325
18568
  kind: "skip",
17326
18569
  path: manifestPath,
@@ -17639,7 +18882,7 @@ async function updateManagedBlock(filePath, content) {
17639
18882
  const blockContent = `${MANAGED_BLOCK_BEGIN}
17640
18883
  ${content}${MANAGED_BLOCK_END}
17641
18884
  `;
17642
- if (!existsSync17(filePath)) {
18885
+ if (!existsSync18(filePath)) {
17643
18886
  const dir = path5.dirname(filePath);
17644
18887
  await mkdir5(dir, { recursive: true });
17645
18888
  await writeFileAtomic(filePath, blockContent);
@@ -17668,7 +18911,7 @@ async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
17668
18911
  if (op.requiresConfirmation) continue;
17669
18912
  const relPath = path5.relative(plan.targetRoot, op.path);
17670
18913
  if (relPath === ".pourkit/manifest.json") continue;
17671
- if (existsSync17(op.path)) {
18914
+ if (existsSync18(op.path)) {
17672
18915
  const sha256 = await computeFileChecksum(op.path);
17673
18916
  assets[relPath] = {
17674
18917
  ownership: op.ownership || "managed",
@@ -17713,7 +18956,7 @@ async function applyInitPlan(plan, options) {
17713
18956
  skipped++;
17714
18957
  continue;
17715
18958
  }
17716
- if (existsSync17(op.path) && !op.destructive) {
18959
+ if (existsSync18(op.path) && !op.destructive) {
17717
18960
  skipped++;
17718
18961
  continue;
17719
18962
  }
@@ -17728,7 +18971,7 @@ async function applyInitPlan(plan, options) {
17728
18971
  skipped++;
17729
18972
  continue;
17730
18973
  }
17731
- if (existsSync17(op.path)) {
18974
+ if (existsSync18(op.path)) {
17732
18975
  skipped++;
17733
18976
  continue;
17734
18977
  }
@@ -17757,7 +19000,7 @@ async function applyInitPlan(plan, options) {
17757
19000
  skipped++;
17758
19001
  continue;
17759
19002
  }
17760
- if (existsSync17(op.path)) {
19003
+ if (existsSync18(op.path)) {
17761
19004
  skipped++;
17762
19005
  continue;
17763
19006
  }
@@ -17897,7 +19140,7 @@ async function applyInitFromSource(options) {
17897
19140
  if (!manifestSkipped) {
17898
19141
  const agentFiles = [];
17899
19142
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
17900
- if (existsSync17(path5.join(targetRoot, name))) {
19143
+ if (existsSync18(path5.join(targetRoot, name))) {
17901
19144
  agentFiles.push(path5.join(targetRoot, name));
17902
19145
  }
17903
19146
  }
@@ -18716,8 +19959,8 @@ function formatChecks2(checks) {
18716
19959
  init_common();
18717
19960
 
18718
19961
  // execution/sandcastle-execution.ts
18719
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync7 } from "fs";
18720
- import { join as join21 } from "path";
19962
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "fs";
19963
+ import { join as join23 } from "path";
18721
19964
  import { createWorktree, opencode } from "@ai-hero/sandcastle";
18722
19965
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
18723
19966
 
@@ -18726,10 +19969,10 @@ init_common();
18726
19969
  import { mkdtempSync as mkdtempSync2 } from "fs";
18727
19970
  import { writeFile as writeFile3 } from "fs/promises";
18728
19971
  import { tmpdir as tmpdir2 } from "os";
18729
- import { dirname as dirname6, join as join20 } from "path";
19972
+ import { dirname as dirname6, join as join22 } from "path";
18730
19973
  async function writeExecutionArtifacts(worktreePath, artifacts) {
18731
19974
  for (const artifact of artifacts) {
18732
- const filePath = join20(worktreePath, artifact.path);
19975
+ const filePath = join22(worktreePath, artifact.path);
18733
19976
  await ensureDir(dirname6(filePath));
18734
19977
  await writeFile3(filePath, artifact.content, "utf-8");
18735
19978
  }
@@ -18741,17 +19984,17 @@ import path7 from "path";
18741
19984
 
18742
19985
  // execution/sandbox-image.ts
18743
19986
  import { createHash as createHash4 } from "crypto";
18744
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
19987
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
18745
19988
  import path6 from "path";
18746
19989
  function sandboxImageName(repoRoot2) {
18747
19990
  const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
18748
19991
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
18749
19992
  const baseName = sanitized || "local";
18750
19993
  const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
18751
- if (!existsSync18(dockerfilePath)) {
19994
+ if (!existsSync19(dockerfilePath)) {
18752
19995
  return `sandcastle:${baseName}`;
18753
19996
  }
18754
- const fingerprint = createHash4("sha256").update(readFileSync16(dockerfilePath)).digest("hex").slice(0, 8);
19997
+ const fingerprint = createHash4("sha256").update(readFileSync18(dockerfilePath)).digest("hex").slice(0, 8);
18755
19998
  return `sandcastle:${baseName}-${fingerprint}`;
18756
19999
  }
18757
20000
 
@@ -18901,11 +20144,7 @@ var SandcastleExecutionSession = class {
18901
20144
  type: "file",
18902
20145
  path: logPath,
18903
20146
  onAgentStreamEvent: (event) => {
18904
- if (event.type === "text") {
18905
- logger.raw(event.message);
18906
- } else if (event.type === "toolCall") {
18907
- logger.raw(`${event.name}(${event.formattedArgs})`);
18908
- }
20147
+ logger.raw(formatAgentStreamEvent(event));
18909
20148
  }
18910
20149
  },
18911
20150
  completionSignal: "<promise>COMPLETE</promise>",
@@ -18990,14 +20229,74 @@ function resolveSandboxProvider(provider, dockerFactory) {
18990
20229
  function sanitizeBranch(branchName) {
18991
20230
  return branchName.replace(/[^A-Za-z0-9._-]/g, "-");
18992
20231
  }
20232
+ function formatAgentStreamEvent(event) {
20233
+ if (event.type === "text") {
20234
+ return JSON.stringify({ type: "text", textCount: event.message.length });
20235
+ }
20236
+ const parsedArgs = parseToolCallArgs(event.formattedArgs);
20237
+ if (!parsedArgs.ok) {
20238
+ return JSON.stringify({
20239
+ type: "toolCall",
20240
+ name: event.name,
20241
+ argsTextCount: event.formattedArgs.length
20242
+ });
20243
+ }
20244
+ return JSON.stringify({
20245
+ type: "toolCall",
20246
+ name: event.name,
20247
+ args: summarizeToolCallArgs(event.name, parsedArgs.value)
20248
+ });
20249
+ }
20250
+ function parseToolCallArgs(formattedArgs) {
20251
+ try {
20252
+ return { ok: true, value: JSON.parse(formattedArgs) };
20253
+ } catch {
20254
+ return { ok: false };
20255
+ }
20256
+ }
20257
+ var SUMMARIZED_TOOL_FIELDS = {
20258
+ write: { content: "contentCount" },
20259
+ edit: { oldString: "oldCount", newString: "newCount" }
20260
+ };
20261
+ var SUMMARIZED_TOOL_ARRAY_FIELDS = {
20262
+ todowrite: { todos: "todoCount" }
20263
+ };
20264
+ function summarizeToolCallArgs(name, args) {
20265
+ if (!isPlainObject(args)) {
20266
+ return args;
20267
+ }
20268
+ const fields = SUMMARIZED_TOOL_FIELDS[name.toLowerCase()];
20269
+ const arrayFields = SUMMARIZED_TOOL_ARRAY_FIELDS[name.toLowerCase()];
20270
+ if (!fields && !arrayFields) {
20271
+ return args;
20272
+ }
20273
+ const summarizedArgs = {};
20274
+ for (const [key, value] of Object.entries(args)) {
20275
+ const mappedKey = fields?.[key];
20276
+ if (mappedKey && typeof value === "string") {
20277
+ summarizedArgs[mappedKey] = value.length;
20278
+ continue;
20279
+ }
20280
+ const mappedArrayKey = arrayFields?.[key];
20281
+ if (mappedArrayKey && Array.isArray(value)) {
20282
+ summarizedArgs[mappedArrayKey] = value.length;
20283
+ continue;
20284
+ }
20285
+ summarizedArgs[key] = value;
20286
+ }
20287
+ return summarizedArgs;
20288
+ }
20289
+ function isPlainObject(value) {
20290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20291
+ }
18993
20292
  function savePromptToFile(repoRoot2, stage, iteration, prompt) {
18994
- const promptsDir = join21(repoRoot2, ".pourkit", ".tmp", "prompts");
18995
- mkdirSync11(promptsDir, { recursive: true });
20293
+ const promptsDir = join23(repoRoot2, ".pourkit", ".tmp", "prompts");
20294
+ mkdirSync12(promptsDir, { recursive: true });
18996
20295
  const timestamp2 = Date.now();
18997
20296
  const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
18998
20297
  const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
18999
- const filePath = join21(promptsDir, filename);
19000
- writeFileSync7(filePath, prompt, "utf-8");
20298
+ const filePath = join23(promptsDir, filename);
20299
+ writeFileSync9(filePath, prompt, "utf-8");
19001
20300
  }
19002
20301
 
19003
20302
  // cli.ts
@@ -19066,6 +20365,35 @@ function createCliProgram(version) {
19066
20365
  const program = new Command();
19067
20366
  program.name("pourkit").version(version).exitOverride().description("AI-driven issue-to-PR workflow for GitHub repositories.");
19068
20367
  const issueCommand = program.command("issue");
20368
+ program.command("run-verification").description("Run configured verification commands for a target").option("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
20369
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
20370
+ const logPath = path8.join(
20371
+ targetRepoRoot,
20372
+ ".pourkit",
20373
+ "logs",
20374
+ "run-verification.log"
20375
+ );
20376
+ const logger = createLogger("pourkit", logPath);
20377
+ let failed = false;
20378
+ try {
20379
+ const config = await loadRepoConfig(targetRepoRoot);
20380
+ const target = resolveTarget(config, options.target);
20381
+ const result = await runVerificationCommands({
20382
+ target,
20383
+ cwd: targetRepoRoot,
20384
+ logger
20385
+ });
20386
+ console.log(JSON.stringify(result, null, 2));
20387
+ failed = !result.ok;
20388
+ } catch (error) {
20389
+ await handleError(logger, error);
20390
+ } finally {
20391
+ await logger.close();
20392
+ }
20393
+ if (failed) {
20394
+ process.exit(1);
20395
+ }
20396
+ });
19069
20397
  program.command("validate-artifact").description("Validate an agent handoff artifact").argument(
19070
20398
  "<kind>",
19071
20399
  "artifact kind: reviewer, refactor, finalizer, conflict-resolution, failure-resolution, reconciliation, planning-manifest, local-prd, local-issue, local-triage, or local-prepare"
@@ -19841,11 +21169,11 @@ function createCliProgram(version) {
19841
21169
  return program;
19842
21170
  }
19843
21171
  async function resolveCliVersion() {
19844
- if (isPackageVersion("0.0.0-next-20260607223610")) {
19845
- return "0.0.0-next-20260607223610";
21172
+ if (isPackageVersion("0.0.0-next-20260608221925")) {
21173
+ return "0.0.0-next-20260608221925";
19846
21174
  }
19847
- if (isReleaseVersion("0.0.0-next-20260607223610")) {
19848
- return "0.0.0-next-20260607223610";
21175
+ if (isReleaseVersion("0.0.0-next-20260608221925")) {
21176
+ return "0.0.0-next-20260608221925";
19849
21177
  }
19850
21178
  try {
19851
21179
  const root = repoRoot();