@pourkit/cli 0.0.0-next-20260622201317 → 0.0.0-next-20260622235247

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
@@ -3740,9 +3740,6 @@ function shellQuote(value) {
3740
3740
  return `'${value.replace(/'/g, `'"'"'`)}'`;
3741
3741
  }
3742
3742
 
3743
- // commands/issue-run.ts
3744
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
3745
-
3746
3743
  // pr/templates.ts
3747
3744
  init_common();
3748
3745
  function renderBranchName(template, issue) {
@@ -4959,9 +4956,9 @@ async function assertCanonicalBaseAncestor(options) {
4959
4956
  }
4960
4957
  }
4961
4958
 
4962
- // commands/pr-description-agent.ts
4963
- import { join as join11 } from "path";
4964
- import { readFileSync as readFileSync12 } from "fs";
4959
+ // issues/pr-description-agent.ts
4960
+ import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
4961
+ import { isAbsolute as isAbsolute4, join as join12 } from "path";
4965
4962
 
4966
4963
  // pr/pr-description-context.ts
4967
4964
  init_common();
@@ -5083,204 +5080,323 @@ Write your finalizer output to: ${artifactPathInWorktree}
5083
5080
  Before handoff, run: pourkit validate-artifact finalizer ${artifactPathInWorktree}`;
5084
5081
  }
5085
5082
 
5086
- // commands/pr-description-agent.ts
5087
- import { Effect as Effect7, Layer as Layer3 } from "effect";
5088
- function bridgeExecutionProvider2(ep) {
5089
- return Layer3.succeed(
5090
- ExecutionProvider,
5091
- ExecutionProvider.of({
5092
- execute: (opts) => Effect7.tryPromise({
5093
- try: () => ep.execute(opts),
5094
- catch: (e) => new Error(
5095
- e instanceof Error ? e.message : `Execution failed: ${String(e)}`
5096
- )
5097
- })
5098
- })
5099
- );
5100
- }
5101
- function runFinalizerAgent(options) {
5102
- const { executionProvider } = options;
5103
- const artifactPathInWorktree = join11(
5104
- ".pourkit",
5105
- ".tmp",
5106
- "finalizer",
5107
- "agent-output.md"
5108
- );
5109
- const artifactPath = join11(options.worktreePath, artifactPathInWorktree);
5110
- const program = Effect7.gen(function* () {
5111
- const fs = yield* FileSystem;
5112
- const context = yield* collectFinalizerContextEffect({
5113
- targetBase: options.targetBaseBranch,
5114
- branchName: options.builderBranch,
5115
- worktreePath: options.worktreePath,
5116
- reviewArtifactPath: options.reviewArtifactPath,
5117
- logger: options.logger
5118
- }).pipe(
5119
- Effect7.catchAll(
5120
- (error) => Effect7.fail(
5121
- new FinalizerFailure({
5122
- message: `Failed to collect finalizer context: ${error.message}`
5123
- })
5124
- )
5125
- )
5126
- );
5127
- const strategy = options.target.strategy;
5128
- const finalizer = strategy.finalize.prDescriptionAgent;
5129
- const resolvedPrompt = yield* loadFinalizerPromptEffect(
5130
- options.repoRoot,
5131
- finalizer.promptTemplate,
5132
- fs
5133
- );
5134
- const prompt = buildFinalizerPrompt(context, resolvedPrompt);
5135
- const result = yield* runPrDescriptionFinalizerCore({
5136
- executionProvider: options.executionProvider,
5137
- config: options.config,
5138
- target: options.target,
5139
- finalizer,
5140
- maxAttempts: strategy.finalize.maxAttempts,
5083
+ // issues/issue-final-review.ts
5084
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
5085
+ import { isAbsolute as isAbsolute3, join as join11 } from "path";
5086
+ init_common();
5087
+ var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join11(
5088
+ ".pourkit",
5089
+ ".tmp",
5090
+ "issue-final-review",
5091
+ "agent-output.json"
5092
+ );
5093
+ async function runIssueFinalReview(options) {
5094
+ const {
5095
+ executionProvider,
5096
+ target,
5097
+ issue,
5098
+ parentPrdIssue,
5099
+ builderBranch,
5100
+ worktreePath,
5101
+ repoRoot: repoRoot2,
5102
+ logger
5103
+ } = options;
5104
+ const artifactPathInWorktree = ISSUE_FINAL_REVIEW_ARTIFACT_PATH;
5105
+ const artifactPath = join11(worktreePath, artifactPathInWorktree);
5106
+ const ifrFromState = options.worktreeState?.issueFinalReview;
5107
+ if (ifrFromState?.completed && ifrFromState.verdict === "pass") {
5108
+ if (!ifrFromState.artifactPath) {
5109
+ throw new Error(
5110
+ "Issue Final Review state is incomplete: missing artifactPath"
5111
+ );
5112
+ }
5113
+ const cachedArtifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join11(worktreePath, ifrFromState.artifactPath);
5114
+ const validation2 = validateAgentArtifact({
5115
+ kind: "issue-final-review",
5116
+ artifactPath: cachedArtifactPath,
5117
+ issueNumber: issue.number,
5118
+ branchName: builderBranch
5119
+ });
5120
+ if (!validation2.ok) {
5121
+ throw new Error(
5122
+ `Issue Final Review state artifact is invalid: ${validation2.reason}`
5123
+ );
5124
+ }
5125
+ return {
5126
+ status: "skipped",
5127
+ verdict: "pass",
5128
+ artifactPath: cachedArtifactPath,
5129
+ selfRetouched: ifrFromState.selfRetouched ?? false,
5130
+ changedPaths: ifrFromState.changedPaths ?? [],
5131
+ verificationPassed: ifrFromState.verificationPassed ?? false
5132
+ };
5133
+ }
5134
+ const strategy = target.strategy;
5135
+ const issueFinalReview = strategy.issueFinalReview;
5136
+ const agent = issueFinalReview;
5137
+ const prompt = loadIssueFinalReviewPrompt({
5138
+ repoRoot: repoRoot2,
5139
+ promptTemplate: agent.promptTemplate,
5140
+ validationCommand: `pourkit validate-artifact issue-final-review ${artifactPathInWorktree} --issue-number ${issue.number} --branch-name ${builderBranch} --base-ref origin/${target.baseBranch}`,
5141
+ reviewArtifactPath: options.reviewArtifactPath,
5142
+ reviewVerdict: options.reviewVerdict
5143
+ });
5144
+ const entry = await executeWithMissingOrEmptyArtifactRetry({
5145
+ executionProvider,
5146
+ missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(agent),
5147
+ logger,
5148
+ runningMessage: () => "Running Issue Final Review agent",
5149
+ retryMessage: (attempt, total) => `Retrying Issue Final Review after empty output (${attempt}/${total})`,
5150
+ executionOptions: {
5151
+ stage: "issueFinalReview",
5152
+ agent: agent.agent,
5153
+ model: agent.model,
5154
+ variant: agent.variant,
5155
+ env: agent.env,
5141
5156
  prompt,
5142
- branchName: options.builderBranch,
5143
- repoRoot: options.repoRoot,
5144
- worktreePath: options.worktreePath,
5145
- artifactPathInWorktree,
5146
- commitSummaries: context.commits,
5157
+ target,
5158
+ repoRoot: repoRoot2,
5159
+ branchName: builderBranch,
5160
+ sandbox: options.config.sandbox,
5161
+ autoApprove: true,
5162
+ artifactPath: artifactPathInWorktree,
5163
+ worktreePath,
5147
5164
  artifacts: [
5148
5165
  buildRunContextArtifact({
5149
- issue: options.issue,
5150
- target: options.target,
5151
- branchName: options.builderBranch,
5166
+ issue,
5167
+ parentPrdIssue,
5168
+ target,
5169
+ branchName: builderBranch,
5170
+ repoRoot: repoRoot2,
5152
5171
  reviewerCriteria: strategy.review.reviewer.criteria,
5153
- sections: STAGE_SECTIONS.finalizer
5172
+ sections: STAGE_SECTIONS.issueFinalReview
5154
5173
  })
5155
5174
  ],
5156
- logger: options.logger,
5157
- ...options.memory ? { memory: options.memory } : {}
5158
- });
5159
- const output = readFileSync12(artifactPath, "utf-8");
5160
- yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
5161
- return result;
5175
+ ...options.memory ? { memory: options.memory } : {},
5176
+ logger
5177
+ }
5162
5178
  });
5163
- const executionLayer = bridgeExecutionProvider2(executionProvider);
5164
- return program.pipe(
5165
- Effect7.provide(
5166
- Layer3.mergeAll(executionLayer, FileSystemDefault, GitOperationsDefault)
5167
- )
5179
+ const executionResult = entry.executionResult;
5180
+ if (!executionResult.success) {
5181
+ throw new Error(
5182
+ `Issue Final Review agent execution failed: ${executionResult.error}`
5183
+ );
5184
+ }
5185
+ let content;
5186
+ if (entry.artifact._tag === "content") {
5187
+ content = entry.artifact.value;
5188
+ } else if (entry.artifact._tag === "empty") {
5189
+ throw new Error(
5190
+ `Issue Final Review agent produced empty output at ${artifactPath}`
5191
+ );
5192
+ } else {
5193
+ throw new Error(
5194
+ `Issue Final Review agent did not produce output at ${artifactPath}`
5195
+ );
5196
+ }
5197
+ const validation = validateAgentArtifact({
5198
+ kind: "issue-final-review",
5199
+ artifactPath,
5200
+ issueNumber: issue.number,
5201
+ branchName: builderBranch
5202
+ });
5203
+ if (!validation.ok) {
5204
+ throw new Error(
5205
+ `Issue Final Review artifact validation failed: ${validation.reason}`
5206
+ );
5207
+ }
5208
+ const parsed = JSON.parse(content);
5209
+ const verdict = parsed.verdict;
5210
+ const baseRef = `origin/${target.baseBranch}`;
5211
+ await assertCanonicalBaseAncestor2({
5212
+ worktreePath,
5213
+ baseRef,
5214
+ stageName: "Issue Final Review",
5215
+ logger
5216
+ });
5217
+ if (verdict === "pass") {
5218
+ const changedPaths = await completeIssueFinalReviewChangedPaths({
5219
+ worktreePath,
5220
+ baseRef,
5221
+ declaredChangedPaths: extractChangedPaths(parsed),
5222
+ logger
5223
+ });
5224
+ updateWorktreeRunState(worktreePath, {
5225
+ issueFinalReview: {
5226
+ completed: true,
5227
+ verdict: "pass",
5228
+ artifactPath,
5229
+ selfRetouched: parsed.selfRetouched === true,
5230
+ changedPaths,
5231
+ verificationPassed: parsed.verification?.passed === true
5232
+ }
5233
+ });
5234
+ return {
5235
+ status: "passed",
5236
+ verdict: "pass",
5237
+ artifactPath,
5238
+ selfRetouched: parsed.selfRetouched === true,
5239
+ changedPaths,
5240
+ verificationPassed: parsed.verification?.passed === true
5241
+ };
5242
+ }
5243
+ if (verdict === "needs_human_review") {
5244
+ return {
5245
+ status: "needs_human_review",
5246
+ verdict: "needs_human_review",
5247
+ artifactPath,
5248
+ needsHumanReason: parsed.needsHumanReason,
5249
+ selfRetouched: parsed.selfRetouched === true,
5250
+ changedPaths: extractChangedPaths(parsed)
5251
+ };
5252
+ }
5253
+ throw new Error(
5254
+ `Unknown Issue Final Review verdict: ${JSON.stringify(verdict)}`
5168
5255
  );
5169
5256
  }
5170
- function runPrDescriptionFinalizerCore(options) {
5171
- const artifactPath = join11(
5172
- options.worktreePath,
5173
- options.artifactPathInWorktree
5174
- );
5175
- return Effect7.gen(function* () {
5176
- let parsed;
5177
- let lastValidationError;
5178
- for (let protocolAttempt = 1; protocolAttempt <= options.maxAttempts; protocolAttempt++) {
5179
- const retryResult = yield* Effect7.tryPromise({
5180
- try: () => executeWithMissingOrEmptyArtifactRetry({
5181
- executionProvider: options.executionProvider,
5182
- missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(
5183
- options.finalizer
5184
- ),
5185
- logger: options.logger,
5186
- runningMessage: () => `Running finalizer agent (${protocolAttempt}/${options.maxAttempts})`,
5187
- retryMessage: (attempt, total) => `Retrying finalizer after empty output (${attempt}/${total})`,
5188
- executionOptions: {
5189
- stage: "finalizer",
5190
- agent: options.finalizer.agent,
5191
- model: options.finalizer.model,
5192
- variant: options.finalizer.variant,
5193
- env: options.finalizer.env,
5194
- prompt: options.prompt,
5195
- target: options.target,
5196
- repoRoot: options.repoRoot,
5197
- branchName: options.branchName,
5198
- sandbox: options.config.sandbox,
5199
- autoApprove: true,
5200
- artifactPath: options.artifactPathInWorktree,
5201
- worktreePath: options.worktreePath,
5202
- artifacts: options.artifacts,
5203
- baseRef: options.baseRef,
5204
- checkoutBase: options.checkoutBase,
5205
- reviewBase: options.reviewBase,
5206
- ...options.memory ? { memory: options.memory } : {},
5207
- logger: options.logger
5208
- }
5209
- }),
5210
- catch: (error) => new FinalizerFailure({
5211
- message: error instanceof Error ? error.message : String(error)
5212
- })
5213
- });
5214
- const executionResult = retryResult.executionResult;
5215
- if (!executionResult.success) {
5216
- return yield* Effect7.fail(
5217
- new FinalizerFailure({
5218
- message: `Finalizer agent execution failed: ${executionResult.error}`
5219
- })
5220
- );
5221
- }
5222
- if (retryResult.artifact._tag === "content") {
5223
- const validation = validateAgentArtifact({
5224
- kind: "finalizer",
5225
- artifactPath
5226
- });
5227
- if (validation.ok) {
5228
- try {
5229
- parsed = parsePrDescription(retryResult.artifact.value);
5230
- lastValidationError = void 0;
5231
- break;
5232
- } catch (error) {
5233
- lastValidationError = error instanceof PrDescriptionProtocolError ? new Error(`Finalizer protocol error: ${error.message}`) : error instanceof Error ? error : new Error(String(error));
5234
- }
5235
- } else {
5236
- lastValidationError = new Error(
5237
- `Finalizer protocol error: ${validation.reason}`
5238
- );
5239
- if (protocolAttempt === options.maxAttempts) break;
5240
- }
5241
- } else if (retryResult.artifact._tag === "empty") {
5242
- lastValidationError = new Error(
5243
- `Finalizer agent produced empty output at ${artifactPath}`
5244
- );
5245
- break;
5246
- } else {
5247
- lastValidationError = new Error(
5248
- `Finalizer agent did not produce output at ${artifactPath}`
5249
- );
5250
- break;
5251
- }
5252
- }
5253
- if (!parsed) {
5254
- return yield* Effect7.fail(
5255
- new FinalizerFailure({
5256
- message: lastValidationError?.message ?? "Finalizer validation failed"
5257
- })
5258
- );
5259
- }
5260
- options.logger.step("info", "Finalizer output generated successfully");
5261
- return {
5262
- title: ensureConventionalPrTitle(parsed.title, options.commitSummaries),
5263
- body: parsed.body,
5264
- artifactPath
5265
- };
5257
+ async function assertCanonicalBaseAncestor2(options) {
5258
+ const { worktreePath, baseRef, stageName, logger } = options;
5259
+ await syncRemoteBaseRef2(worktreePath, baseRef, logger);
5260
+ try {
5261
+ await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
5262
+ cwd: worktreePath,
5263
+ logger,
5264
+ label: "git merge-base --is-ancestor"
5265
+ });
5266
+ } catch {
5267
+ throw new Error(
5268
+ `Cannot continue after ${stageName}: ${baseRef} is not an ancestor of HEAD. An agent may have moved the issue branch behind the canonical base; refresh the branch onto the latest target base before continuing.`
5269
+ );
5270
+ }
5271
+ }
5272
+ async function syncRemoteBaseRef2(worktreePath, baseRef, logger) {
5273
+ const remoteBase = parseRemoteBaseRef2(baseRef);
5274
+ if (!remoteBase) {
5275
+ return;
5276
+ }
5277
+ await execCapture("git", ["fetch", remoteBase.remote, remoteBase.branch], {
5278
+ cwd: worktreePath,
5279
+ logger,
5280
+ label: "git fetch target"
5266
5281
  });
5267
5282
  }
5268
- function loadFinalizerPromptEffect(repoRoot2, promptTemplate, fs) {
5269
- return Effect7.gen(function* () {
5270
- const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
5271
- const exists = yield* fs.exists(promptPath).pipe(Effect7.orDie);
5272
- if (exists) {
5273
- return yield* fs.readFile(promptPath).pipe(Effect7.orDie);
5283
+ function parseRemoteBaseRef2(baseRef) {
5284
+ const [remote, ...branchParts] = baseRef.split("/");
5285
+ const branch = branchParts.join("/");
5286
+ if (!remote || !branch) {
5287
+ return null;
5288
+ }
5289
+ return { remote, branch };
5290
+ }
5291
+ async function completeIssueFinalReviewChangedPaths(options) {
5292
+ const actualChangedPaths = await listIssueFinalReviewChangedPaths2(options);
5293
+ const declared = new Set(options.declaredChangedPaths.map(normalizeGitPath2));
5294
+ const missing = actualChangedPaths.filter((path9) => !declared.has(path9));
5295
+ if (missing.length === 0) {
5296
+ return actualChangedPaths;
5297
+ }
5298
+ options.logger.step(
5299
+ "warn",
5300
+ `Issue Final Review pass omitted ${missing.length} changed file(s) from changedFiles; using git inventory for finalization state.`
5301
+ );
5302
+ options.logger.kv("MISSING_CHANGED_FILES", missing.join(", "));
5303
+ return actualChangedPaths;
5304
+ }
5305
+ async function listIssueFinalReviewChangedPaths2(options) {
5306
+ const trackedDiff = await execCapture(
5307
+ "git",
5308
+ ["diff-index", "--name-only", options.baseRef, "--"],
5309
+ {
5310
+ cwd: options.worktreePath,
5311
+ logger: options.logger,
5312
+ label: "git diff-index Issue Final Review paths"
5274
5313
  }
5275
- return promptTemplate;
5276
- });
5314
+ );
5315
+ const untrackedFiles = await execCapture(
5316
+ "git",
5317
+ ["ls-files", "--others", "--exclude-standard"],
5318
+ {
5319
+ cwd: options.worktreePath,
5320
+ logger: options.logger,
5321
+ label: "git ls-files Issue Final Review paths"
5322
+ }
5323
+ );
5324
+ return Array.from(
5325
+ /* @__PURE__ */ new Set([
5326
+ ...parseGitPathList2(trackedDiff.stdout),
5327
+ ...parseGitPathList2(untrackedFiles.stdout)
5328
+ ])
5329
+ ).filter(isIssueFinalReviewAuditedPath2);
5277
5330
  }
5278
- function persistGeneratedArtifactEffect(worktreePath, output, fs) {
5279
- return Effect7.gen(function* () {
5280
- const dir = join11(worktreePath, ".pourkit", ".tmp", "finalizer");
5281
- yield* fs.mkdir(dir).pipe(Effect7.catchAll(() => Effect7.void));
5282
- yield* fs.writeFile(join11(dir, "generated.md"), output).pipe(Effect7.catchAll(() => Effect7.void));
5283
- });
5331
+ function parseGitPathList2(output) {
5332
+ return output.split(/\r?\n/).map(normalizeGitPath2).filter((path9) => path9.length > 0);
5333
+ }
5334
+ function normalizeGitPath2(path9) {
5335
+ return path9.replace(/^"|"$/g, "").replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
5336
+ }
5337
+ function isIssueFinalReviewAuditedPath2(path9) {
5338
+ return path9 !== "" && path9 !== WORKTREE_RUN_STATE_PATH && !path9.startsWith(".pourkit/.tmp/") && !path9.startsWith(".pourkit/logs/");
5339
+ }
5340
+ function extractChangedPaths(parsed) {
5341
+ return Array.isArray(parsed.changedFiles) ? parsed.changedFiles.map(
5342
+ (file) => file && typeof file === "object" ? file.path : void 0
5343
+ ).filter((path9) => typeof path9 === "string") : [];
5344
+ }
5345
+ function loadIssueFinalReviewPrompt(options) {
5346
+ const {
5347
+ repoRoot: repoRoot2,
5348
+ promptTemplate,
5349
+ validationCommand,
5350
+ reviewArtifactPath,
5351
+ reviewVerdict
5352
+ } = options;
5353
+ const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
5354
+ const promptBody = existsSync11(promptPath) ? readFileSync12(promptPath, "utf-8") : promptTemplate;
5355
+ let prompt = appendProtectedWorkGuidance(`${promptBody}
5356
+
5357
+ ## Shared Run Context
5358
+
5359
+ Read the selected issue requirements, planning context (including plan and PRD content when present), comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
5360
+
5361
+ ## Initial Verification Pass
5362
+
5363
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
5364
+ - Before reviewing code, diffs, artifacts, or prior findings, run the verification wrapper from the Worktree.
5365
+ - Do not substitute direct underlying commands unless no wrapper path can run; if you must use direct commands, explain why in \`verification.summary\`.
5366
+ - If a configured verification command fails because of fixable Issue work, self-retouch the Worktree, then rerun all configured verification commands.
5367
+ - Do not return \`pass\` while any configured verification command is failing.
5368
+ - If a command cannot run because the environment is missing required setup, dependencies, or scripts outside agent control, treat it as a human handoff blocker.
5369
+ - If no verification commands are configured, note that and proceed with normal review.
5370
+
5371
+ ## Changed Files Audit
5372
+
5373
+ - Read the Branch section of ${RUN_CONTEXT_PATH_IN_WORKTREE} and use its Canonical Base Ref for the changed-file inventory.
5374
+ - Before writing \`changedFiles\`, run \`git diff-index --name-only <canonical-base-ref> --\` from the Worktree to enumerate tracked files changed by the Issue.
5375
+ - Also run \`git ls-files --others --exclude-standard\` from the Worktree to enumerate untracked files changed by the Issue.
5376
+ - Treat the union of those command outputs, excluding only \`.pourkit/.tmp/**\`, \`.pourkit/logs/**\`, and \`.pourkit/state.json\`, as the required \`changedFiles\` inventory.
5377
+ - Include every file from that inventory in \`changedFiles\`, including generated files, managed operating assets, docs, prompts, schema files, and large batches.
5378
+ - If any file from the inventory is out-of-scope, mark it \`out-of-scope\` and either self-retouch it away or return \`needs_human_review\`; do not omit it.
5379
+ - Do not return \`pass\` until \`changedFiles\` covers the full inventory.
5380
+
5381
+ ## Required Artifact Validation
5382
+
5383
+ - Before handoff, run exactly: \`${validationCommand}\`
5384
+ - This validation checks both the artifact shape and \`changedFiles\` coverage against the canonical Git diff.
5385
+ - If validation fails, fix the artifact or safely retouch the worktree as needed, then rerun the same command.
5386
+ - Do not finish until the validation command passes.`);
5387
+ if (reviewVerdict) {
5388
+ prompt += `
5389
+
5390
+ ## Prior Review Context
5391
+
5392
+ - Reviewer Artifact Path: ${reviewArtifactPath}
5393
+ - Prior Review Verdict: ${reviewVerdict}`;
5394
+ if (reviewVerdict === "PASS_WITH_NOTES") {
5395
+ prompt += `
5396
+ - This Issue passed the prior Reviewer with notes. Read the Reviewer Artifact and inspect its notes before making your final pass decision.`;
5397
+ }
5398
+ }
5399
+ return prompt;
5284
5400
  }
5285
5401
 
5286
5402
  // pr/pr-body.ts
@@ -5351,44 +5467,315 @@ function extractClosingRefs(body) {
5351
5467
  return refs;
5352
5468
  }
5353
5469
 
5354
- // issues/github-issue-publication.ts
5355
- init_common();
5356
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
5357
- import { isAbsolute as isAbsolute3, join as join12 } from "path";
5358
-
5359
- // issues/issue-transitions.ts
5360
- function createIssueTransitions(deps, labels) {
5361
- return {
5362
- async removeBlocked(issueNumber) {
5363
- await deps.removeLabel(issueNumber, labels.blocked);
5364
- },
5365
- async addReadyForAgent(issueNumber) {
5366
- const issue = await deps.fetchIssue(issueNumber);
5367
- if (!issue.labels.includes(labels.readyForAgent)) {
5368
- await deps.addLabels(issueNumber, [labels.readyForAgent]);
5369
- }
5370
- },
5371
- async moveToNeedsTriage(issueNumber) {
5372
- if (deps.updateLabels) {
5373
- await deps.updateLabels(
5374
- issueNumber,
5375
- [labels.blocked, labels.readyForAgent],
5376
- [labels.needsTriage]
5377
- );
5378
- } else {
5379
- await deps.removeLabel(issueNumber, labels.blocked);
5380
- const issue = await deps.fetchIssue(issueNumber);
5381
- if (issue.labels.includes(labels.readyForAgent)) {
5382
- await deps.removeLabel(issueNumber, labels.readyForAgent);
5383
- }
5384
- await deps.addLabels(issueNumber, [labels.needsTriage]);
5385
- }
5386
- },
5387
- async moveToReadyForHuman(issueNumber) {
5388
- try {
5389
- await deps.removeLabel(issueNumber, labels.agentInProgress);
5390
- } catch {
5391
- }
5470
+ // issues/pr-description-agent.ts
5471
+ import { Effect as Effect7, Layer as Layer3 } from "effect";
5472
+ function bridgeExecutionProvider2(ep) {
5473
+ return Layer3.succeed(
5474
+ ExecutionProvider,
5475
+ ExecutionProvider.of({
5476
+ execute: (opts) => Effect7.tryPromise({
5477
+ try: () => ep.execute(opts),
5478
+ catch: (e) => new Error(
5479
+ e instanceof Error ? e.message : `Execution failed: ${String(e)}`
5480
+ )
5481
+ })
5482
+ })
5483
+ );
5484
+ }
5485
+ function loadFinalizerPromptEffect(repoRoot2, promptTemplate, fs) {
5486
+ return Effect7.gen(function* () {
5487
+ const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
5488
+ const exists = yield* fs.exists(promptPath).pipe(Effect7.orDie);
5489
+ if (exists) {
5490
+ return yield* fs.readFile(promptPath).pipe(Effect7.orDie);
5491
+ }
5492
+ return promptTemplate;
5493
+ });
5494
+ }
5495
+ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
5496
+ return Effect7.gen(function* () {
5497
+ const dir = join12(worktreePath, ".pourkit", ".tmp", "finalizer");
5498
+ yield* fs.mkdir(dir).pipe(Effect7.catchAll(() => Effect7.void));
5499
+ yield* fs.writeFile(join12(dir, "generated.md"), output).pipe(Effect7.catchAll(() => Effect7.void));
5500
+ });
5501
+ }
5502
+ function runPrDescriptionAgent(options) {
5503
+ const { executionProvider } = options;
5504
+ const artifactPathInWorktree = join12(
5505
+ ".pourkit",
5506
+ ".tmp",
5507
+ "finalizer",
5508
+ "agent-output.md"
5509
+ );
5510
+ const artifactPath = join12(options.worktreePath, artifactPathInWorktree);
5511
+ const finalizerFromState = options.worktreeState?.finalizer?.completed ? options.worktreeState.finalizer : null;
5512
+ if (finalizerFromState) {
5513
+ return Effect7.gen(function* () {
5514
+ if (!finalizerFromState.artifactPath) {
5515
+ return yield* Effect7.fail(
5516
+ new FinalizerFailure({
5517
+ message: "Finalizer artifact missing: artifactPath not set in completed finalizer state"
5518
+ })
5519
+ );
5520
+ }
5521
+ const resolvedArtifactPath = isAbsolute4(finalizerFromState.artifactPath) ? finalizerFromState.artifactPath : join12(options.worktreePath, finalizerFromState.artifactPath);
5522
+ if (!existsSync12(resolvedArtifactPath)) {
5523
+ return yield* Effect7.fail(
5524
+ new FinalizerFailure({
5525
+ message: `Finalizer artifact missing: the artifact does not exist at \`${resolvedArtifactPath}\``
5526
+ })
5527
+ );
5528
+ }
5529
+ const validation = validateAgentArtifact({
5530
+ kind: "finalizer",
5531
+ artifactPath: resolvedArtifactPath
5532
+ });
5533
+ if (!validation.ok) {
5534
+ return yield* Effect7.fail(
5535
+ new FinalizerFailure({
5536
+ message: `Finalizer state artifact is invalid: ${validation.reason}`
5537
+ })
5538
+ );
5539
+ }
5540
+ if (!finalizerFromState.title || !finalizerFromState.body) {
5541
+ return yield* Effect7.fail(
5542
+ new FinalizerFailure({
5543
+ message: "Finalizer state is incomplete: missing stored title or body"
5544
+ })
5545
+ );
5546
+ }
5547
+ return {
5548
+ status: "skipped",
5549
+ title: finalizerFromState.title,
5550
+ body: finalizerFromState.body,
5551
+ artifactPath: resolvedArtifactPath
5552
+ };
5553
+ });
5554
+ }
5555
+ const program = Effect7.gen(function* () {
5556
+ const fs = yield* FileSystem;
5557
+ const context = yield* collectFinalizerContextEffect({
5558
+ targetBase: options.targetBaseBranch,
5559
+ branchName: options.builderBranch,
5560
+ worktreePath: options.worktreePath,
5561
+ reviewArtifactPath: options.reviewArtifactPath,
5562
+ logger: options.logger
5563
+ }).pipe(
5564
+ Effect7.catchAll(
5565
+ (error) => Effect7.fail(
5566
+ new FinalizerFailure({
5567
+ message: `Failed to collect finalizer context: ${error.message}`
5568
+ })
5569
+ )
5570
+ )
5571
+ );
5572
+ const strategy = options.target.strategy;
5573
+ const finalizer = strategy.finalize.prDescriptionAgent;
5574
+ const resolvedPrompt = yield* loadFinalizerPromptEffect(
5575
+ options.repoRoot,
5576
+ finalizer.promptTemplate,
5577
+ fs
5578
+ );
5579
+ const prompt = buildFinalizerPrompt(context, resolvedPrompt);
5580
+ const result = yield* runPrDescriptionFinalizerCore({
5581
+ executionProvider: options.executionProvider,
5582
+ config: options.config,
5583
+ target: options.target,
5584
+ finalizer,
5585
+ maxAttempts: strategy.finalize.maxAttempts,
5586
+ prompt,
5587
+ branchName: options.builderBranch,
5588
+ repoRoot: options.repoRoot,
5589
+ worktreePath: options.worktreePath,
5590
+ artifactPathInWorktree,
5591
+ commitSummaries: context.commits,
5592
+ artifacts: [
5593
+ buildRunContextArtifact({
5594
+ issue: options.issue,
5595
+ target: options.target,
5596
+ branchName: options.builderBranch,
5597
+ reviewerCriteria: strategy.review.reviewer.criteria,
5598
+ sections: STAGE_SECTIONS.finalizer
5599
+ })
5600
+ ],
5601
+ logger: options.logger,
5602
+ ...options.memory ? { memory: options.memory } : {}
5603
+ });
5604
+ const output = readFileSync13(artifactPath, "utf-8");
5605
+ yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
5606
+ const body = ensureClosingRefs(result.body, options.issue.number);
5607
+ yield* Effect7.tryPromise({
5608
+ try: () => assertCanonicalBaseAncestor2({
5609
+ worktreePath: options.worktreePath,
5610
+ baseRef: `origin/${options.targetBaseBranch}`,
5611
+ stageName: "Finalizer",
5612
+ logger: options.logger
5613
+ }),
5614
+ catch: (error) => new FinalizerFailure({
5615
+ message: error instanceof Error ? error.message : String(error)
5616
+ })
5617
+ });
5618
+ yield* Effect7.sync(() => {
5619
+ updateWorktreeRunState(options.worktreePath, {
5620
+ finalizer: {
5621
+ completed: true,
5622
+ artifactPath: result.artifactPath,
5623
+ title: result.title,
5624
+ body
5625
+ }
5626
+ });
5627
+ });
5628
+ return {
5629
+ status: "completed",
5630
+ title: result.title,
5631
+ body,
5632
+ artifactPath: result.artifactPath
5633
+ };
5634
+ });
5635
+ const executionLayer = bridgeExecutionProvider2(executionProvider);
5636
+ return program.pipe(
5637
+ Effect7.provide(
5638
+ Layer3.mergeAll(executionLayer, FileSystemDefault, GitOperationsDefault)
5639
+ )
5640
+ );
5641
+ }
5642
+ function runPrDescriptionFinalizerCore(options) {
5643
+ const artifactPath = join12(
5644
+ options.worktreePath,
5645
+ options.artifactPathInWorktree
5646
+ );
5647
+ return Effect7.gen(function* () {
5648
+ let parsed;
5649
+ let lastValidationError;
5650
+ for (let protocolAttempt = 1; protocolAttempt <= options.maxAttempts; protocolAttempt++) {
5651
+ const retryResult = yield* Effect7.tryPromise({
5652
+ try: () => executeWithMissingOrEmptyArtifactRetry({
5653
+ executionProvider: options.executionProvider,
5654
+ missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(
5655
+ options.finalizer
5656
+ ),
5657
+ logger: options.logger,
5658
+ runningMessage: () => `Running finalizer agent (${protocolAttempt}/${options.maxAttempts})`,
5659
+ retryMessage: (attempt, total) => `Retrying finalizer after empty output (${attempt}/${total})`,
5660
+ executionOptions: {
5661
+ stage: "finalizer",
5662
+ agent: options.finalizer.agent,
5663
+ model: options.finalizer.model,
5664
+ variant: options.finalizer.variant,
5665
+ env: options.finalizer.env,
5666
+ prompt: options.prompt,
5667
+ target: options.target,
5668
+ repoRoot: options.repoRoot,
5669
+ branchName: options.branchName,
5670
+ sandbox: options.config.sandbox,
5671
+ autoApprove: true,
5672
+ artifactPath: options.artifactPathInWorktree,
5673
+ worktreePath: options.worktreePath,
5674
+ artifacts: options.artifacts,
5675
+ baseRef: options.baseRef,
5676
+ checkoutBase: options.checkoutBase,
5677
+ reviewBase: options.reviewBase,
5678
+ ...options.memory ? { memory: options.memory } : {},
5679
+ logger: options.logger
5680
+ }
5681
+ }),
5682
+ catch: (error) => new FinalizerFailure({
5683
+ message: error instanceof Error ? error.message : String(error)
5684
+ })
5685
+ });
5686
+ const executionResult = retryResult.executionResult;
5687
+ if (!executionResult.success) {
5688
+ return yield* Effect7.fail(
5689
+ new FinalizerFailure({
5690
+ message: `Finalizer agent execution failed: ${executionResult.error}`
5691
+ })
5692
+ );
5693
+ }
5694
+ if (retryResult.artifact._tag === "content") {
5695
+ const validation = validateAgentArtifact({
5696
+ kind: "finalizer",
5697
+ artifactPath
5698
+ });
5699
+ if (validation.ok) {
5700
+ try {
5701
+ parsed = parsePrDescription(retryResult.artifact.value);
5702
+ lastValidationError = void 0;
5703
+ break;
5704
+ } catch (error) {
5705
+ lastValidationError = error instanceof PrDescriptionProtocolError ? new Error(`Finalizer protocol error: ${error.message}`) : error instanceof Error ? error : new Error(String(error));
5706
+ }
5707
+ } else {
5708
+ lastValidationError = new Error(
5709
+ `Finalizer protocol error: ${validation.reason}`
5710
+ );
5711
+ if (protocolAttempt === options.maxAttempts) break;
5712
+ }
5713
+ } else if (retryResult.artifact._tag === "empty") {
5714
+ lastValidationError = new Error(
5715
+ `Finalizer agent produced empty output at ${artifactPath}`
5716
+ );
5717
+ break;
5718
+ } else {
5719
+ lastValidationError = new Error(
5720
+ `Finalizer agent did not produce output at ${artifactPath}`
5721
+ );
5722
+ break;
5723
+ }
5724
+ }
5725
+ if (!parsed) {
5726
+ return yield* Effect7.fail(
5727
+ new FinalizerFailure({
5728
+ message: lastValidationError?.message ?? "Finalizer validation failed"
5729
+ })
5730
+ );
5731
+ }
5732
+ options.logger.step("info", "Finalizer output generated successfully");
5733
+ return {
5734
+ title: ensureConventionalPrTitle(parsed.title, options.commitSummaries),
5735
+ body: parsed.body,
5736
+ artifactPath
5737
+ };
5738
+ });
5739
+ }
5740
+
5741
+ // issues/github-issue-publication.ts
5742
+ init_common();
5743
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
5744
+ import { isAbsolute as isAbsolute5, join as join13 } from "path";
5745
+
5746
+ // issues/issue-transitions.ts
5747
+ function createIssueTransitions(deps, labels) {
5748
+ return {
5749
+ async removeBlocked(issueNumber) {
5750
+ await deps.removeLabel(issueNumber, labels.blocked);
5751
+ },
5752
+ async addReadyForAgent(issueNumber) {
5753
+ const issue = await deps.fetchIssue(issueNumber);
5754
+ if (!issue.labels.includes(labels.readyForAgent)) {
5755
+ await deps.addLabels(issueNumber, [labels.readyForAgent]);
5756
+ }
5757
+ },
5758
+ async moveToNeedsTriage(issueNumber) {
5759
+ if (deps.updateLabels) {
5760
+ await deps.updateLabels(
5761
+ issueNumber,
5762
+ [labels.blocked, labels.readyForAgent],
5763
+ [labels.needsTriage]
5764
+ );
5765
+ } else {
5766
+ await deps.removeLabel(issueNumber, labels.blocked);
5767
+ const issue = await deps.fetchIssue(issueNumber);
5768
+ if (issue.labels.includes(labels.readyForAgent)) {
5769
+ await deps.removeLabel(issueNumber, labels.readyForAgent);
5770
+ }
5771
+ await deps.addLabels(issueNumber, [labels.needsTriage]);
5772
+ }
5773
+ },
5774
+ async moveToReadyForHuman(issueNumber) {
5775
+ try {
5776
+ await deps.removeLabel(issueNumber, labels.agentInProgress);
5777
+ } catch {
5778
+ }
5392
5779
  try {
5393
5780
  await deps.removeLabel(issueNumber, labels.readyForAgent);
5394
5781
  } catch {
@@ -5829,11 +6216,11 @@ function readIssueFinalReviewArtifact(worktreeState, worktreePath) {
5829
6216
  if (!configuredPath) {
5830
6217
  return null;
5831
6218
  }
5832
- const artifactPath = isAbsolute3(configuredPath) ? configuredPath : join12(worktreePath, configuredPath);
5833
- if (!existsSync11(artifactPath)) return null;
6219
+ const artifactPath = isAbsolute5(configuredPath) ? configuredPath : join13(worktreePath, configuredPath);
6220
+ if (!existsSync13(artifactPath)) return null;
5834
6221
  let parsed;
5835
6222
  try {
5836
- parsed = JSON.parse(readFileSync13(artifactPath, "utf-8"));
6223
+ parsed = JSON.parse(readFileSync14(artifactPath, "utf-8"));
5837
6224
  } catch {
5838
6225
  return null;
5839
6226
  }
@@ -5893,520 +6280,201 @@ function buildIssueFinalReviewPrComment(artifact) {
5893
6280
  `| ${formatPath(file.path)} | ${formatLabel(file.scopeVerdict)} | ${formatBoolean(file.revertFlag)} | ${formatTableCell(file.change)} |`
5894
6281
  );
5895
6282
  }
5896
- lines.push("");
5897
- }
5898
- }
5899
- if (typeof artifact.selfRetouched === "boolean" || artifact.outOfScopeFiles || artifact.badReverts) {
5900
- lines.push("### Scope And Reverts", "");
5901
- if (typeof artifact.selfRetouched === "boolean") {
5902
- lines.push(
5903
- `- **Self-retouched:** ${formatBoolean(artifact.selfRetouched)}`
5904
- );
5905
- }
5906
- if (artifact.outOfScopeFiles) {
5907
- lines.push(
5908
- `- **Out-of-scope files:** ${formatPathList(artifact.outOfScopeFiles)}`
5909
- );
5910
- }
5911
- if (artifact.badReverts) {
5912
- lines.push(
5913
- `- **Bad reverts:** ${formatCount(artifact.badReverts.length)}`
5914
- );
5915
- }
5916
- lines.push("");
5917
- }
5918
- return lines.join("\n").trimEnd();
5919
- }
5920
- function formatLabel(value) {
5921
- return typeof value === "string" && value.trim() !== "" ? value.trim().replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : "Unknown";
5922
- }
5923
- function formatBoolean(value) {
5924
- return value === true ? "Yes" : value === false ? "No" : "Unknown";
5925
- }
5926
- function formatPath(value) {
5927
- return typeof value === "string" && value.trim() !== "" ? `\`${value.trim()}\`` : "Unknown";
5928
- }
5929
- function formatPathList(paths) {
5930
- return paths.length > 0 ? paths.map((path9) => `\`${path9}\``).join(", ") : "None";
5931
- }
5932
- function formatCount(count) {
5933
- return count === 0 ? "None" : String(count);
5934
- }
5935
- function formatTableCell(value) {
5936
- return typeof value === "string" && value.trim() !== "" ? value.trim().replace(/\s+/g, " ").replace(/\|/g, "\\|") : "Unknown";
5937
- }
5938
- async function performCleanupAndClosure(options, pr) {
5939
- await options.issueProvider.removeLabel(
5940
- options.issue.number,
5941
- options.labels.agentInProgress
5942
- );
5943
- await options.issueProvider.removeLabel(
5944
- options.issue.number,
5945
- options.labels.prOpenAwaitingMerge
5946
- );
5947
- let childCloseSucceeded = false;
5948
- try {
5949
- await options.issueProvider.closeIssue(options.issue.number);
5950
- childCloseSucceeded = true;
5951
- } catch (error) {
5952
- options.logger.step(
5953
- "warn",
5954
- `Issue #${options.issue.number} could not be closed: ${error instanceof Error ? error.message : String(error)}`
5955
- );
5956
- }
5957
- if (childCloseSucceeded) {
5958
- await maybeCloseParentAfterChildCompletion(
5959
- options.issueProvider,
5960
- options.issue.number,
5961
- options.logger
5962
- );
5963
- }
5964
- return {
5965
- status: "merged",
5966
- prNumber: pr.number,
5967
- prUrl: pr.url,
5968
- prTitle: options.prTitle,
5969
- prBody: options.prBody
5970
- };
5971
- }
5972
- function assertRecordedMergedPrDetails(state) {
5973
- if (typeof state.pr?.number === "number" && typeof state.pr.url === "string") {
5974
- return;
5975
- }
5976
- throw new Error(
5977
- "Lifecycle invariant: resumed merged GitHub Issue Publication requires recorded PR number and URL"
5978
- );
5979
- }
5980
- function makeIssueTransitions(issueProvider, labels) {
5981
- return createIssueTransitions(
5982
- {
5983
- fetchIssue: issueProvider.fetchIssue.bind(issueProvider),
5984
- addLabels: issueProvider.addLabels.bind(issueProvider),
5985
- removeLabel: issueProvider.removeLabel.bind(issueProvider),
5986
- closeIssue: issueProvider.closeIssue.bind(issueProvider)
5987
- },
5988
- {
5989
- blocked: labels.blocked,
5990
- readyForAgent: labels.readyForAgent,
5991
- needsTriage: labels.needsTriage,
5992
- agentInProgress: labels.agentInProgress,
5993
- readyForHuman: labels.readyForHuman,
5994
- prOpenAwaitingMerge: labels.prOpenAwaitingMerge
5995
- }
5996
- );
5997
- }
5998
- async function guardFinalPublishContent(options) {
5999
- const {
6000
- worktreePath,
6001
- baseRef,
6002
- title,
6003
- body,
6004
- secretLikeContentDetectionEnabled,
6005
- logger
6006
- } = options;
6007
- if (!secretLikeContentDetectionEnabled) {
6008
- return;
6009
- }
6010
- const finalDiff = await execCapture("git", ["diff", `${baseRef}...HEAD`], {
6011
- cwd: worktreePath,
6012
- logger,
6013
- label: "git diff final secret guard"
6014
- });
6015
- assertNoSecretLikeContent([
6016
- {
6017
- source: "final diff",
6018
- content: extractAddedDiffContent(finalDiff.stdout)
6019
- },
6020
- { source: "PR title", content: title },
6021
- { source: "PR body", content: body }
6022
- ]);
6023
- }
6024
- function extractAddedDiffContent(diff) {
6025
- return diff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1)).join("\n");
6026
- }
6027
- function assertNoSecretLikeContent(inputs) {
6028
- const findings = inputs.flatMap(
6029
- ({ source, content }) => findSecretLikeContent(source, content ?? "")
6030
- );
6031
- if (findings.length === 0) {
6032
- return;
6033
- }
6034
- const summary = findings.slice(0, 5).map((finding) => `${finding.source}: ${finding.kind}`).join("; ");
6035
- throw new Error(
6036
- `Secret-like content detected before finalization (${summary}). Redact it before committing, pushing, or creating a PR.`
6037
- );
6038
- }
6039
- function findSecretLikeContent(source, content) {
6040
- const findings = [];
6041
- const patterns = [
6042
- {
6043
- kind: "private key block",
6044
- regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/gi
6045
- },
6046
- {
6047
- kind: "GitHub token",
6048
- regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g
6049
- },
6050
- {
6051
- kind: "GitHub fine-grained token",
6052
- regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g
6053
- },
6054
- { kind: "npm token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g },
6055
- { kind: "OpenAI key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
6056
- { kind: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
6057
- {
6058
- kind: "authorization bearer token",
6059
- regex: /\bAuthorization\s*:\s*Bearer\s+[^\s`'\"]{12,}/gi
6060
- },
6061
- {
6062
- kind: "secret assignment",
6063
- regex: /\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|DATABASE[_-]?URL)[A-Z0-9_]*\s*[:=]\s*["']?[^\s"'`]{8,}/gi
6064
- }
6065
- ];
6066
- for (const { kind, regex } of patterns) {
6067
- for (const match of content.matchAll(regex)) {
6068
- if (isAllowedSecretPlaceholder(match[0]) || isAllowedDocumentationTokenReference(match[0])) {
6069
- continue;
6070
- }
6071
- findings.push({ source, kind });
6072
- break;
6283
+ lines.push("");
6073
6284
  }
6074
6285
  }
6075
- return findings;
6076
- }
6077
- function isAllowedSecretPlaceholder(value) {
6078
- const normalized = value.toLowerCase();
6079
- return [
6080
- "<redacted>",
6081
- "<example",
6082
- "example-",
6083
- "dummy-",
6084
- "test-",
6085
- "placeholder",
6086
- "xxxxx"
6087
- ].some((allowed) => normalized.includes(allowed));
6088
- }
6089
- function isAllowedDocumentationTokenReference(value) {
6090
- return /^token\s*:\s*["']?<verdict>/i.test(value);
6091
- }
6092
-
6093
- // issues/issue-final-review.ts
6094
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
6095
- import { isAbsolute as isAbsolute4, join as join13 } from "path";
6096
- init_common();
6097
- var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join13(
6098
- ".pourkit",
6099
- ".tmp",
6100
- "issue-final-review",
6101
- "agent-output.json"
6102
- );
6103
- async function runIssueFinalReview(options) {
6104
- const {
6105
- executionProvider,
6106
- target,
6107
- issue,
6108
- parentPrdIssue,
6109
- builderBranch,
6110
- worktreePath,
6111
- repoRoot: repoRoot2,
6112
- logger
6113
- } = options;
6114
- const artifactPathInWorktree = ISSUE_FINAL_REVIEW_ARTIFACT_PATH;
6115
- const artifactPath = join13(worktreePath, artifactPathInWorktree);
6116
- const ifrFromState = options.worktreeState?.issueFinalReview;
6117
- if (ifrFromState?.completed && ifrFromState.verdict === "pass") {
6118
- if (!ifrFromState.artifactPath) {
6119
- throw new Error(
6120
- "Issue Final Review state is incomplete: missing artifactPath"
6286
+ if (typeof artifact.selfRetouched === "boolean" || artifact.outOfScopeFiles || artifact.badReverts) {
6287
+ lines.push("### Scope And Reverts", "");
6288
+ if (typeof artifact.selfRetouched === "boolean") {
6289
+ lines.push(
6290
+ `- **Self-retouched:** ${formatBoolean(artifact.selfRetouched)}`
6121
6291
  );
6122
6292
  }
6123
- const cachedArtifactPath = isAbsolute4(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join13(worktreePath, ifrFromState.artifactPath);
6124
- const validation2 = validateAgentArtifact({
6125
- kind: "issue-final-review",
6126
- artifactPath: cachedArtifactPath,
6127
- issueNumber: issue.number,
6128
- branchName: builderBranch
6129
- });
6130
- if (!validation2.ok) {
6131
- throw new Error(
6132
- `Issue Final Review state artifact is invalid: ${validation2.reason}`
6293
+ if (artifact.outOfScopeFiles) {
6294
+ lines.push(
6295
+ `- **Out-of-scope files:** ${formatPathList(artifact.outOfScopeFiles)}`
6133
6296
  );
6134
6297
  }
6135
- return {
6136
- status: "skipped",
6137
- verdict: "pass",
6138
- artifactPath: cachedArtifactPath,
6139
- selfRetouched: ifrFromState.selfRetouched ?? false,
6140
- changedPaths: ifrFromState.changedPaths ?? [],
6141
- verificationPassed: ifrFromState.verificationPassed ?? false
6142
- };
6143
- }
6144
- const strategy = target.strategy;
6145
- const issueFinalReview = strategy.issueFinalReview;
6146
- const agent = issueFinalReview;
6147
- const prompt = loadIssueFinalReviewPrompt({
6148
- repoRoot: repoRoot2,
6149
- promptTemplate: agent.promptTemplate,
6150
- validationCommand: `pourkit validate-artifact issue-final-review ${artifactPathInWorktree} --issue-number ${issue.number} --branch-name ${builderBranch} --base-ref origin/${target.baseBranch}`,
6151
- reviewArtifactPath: options.reviewArtifactPath,
6152
- reviewVerdict: options.reviewVerdict
6153
- });
6154
- const entry = await executeWithMissingOrEmptyArtifactRetry({
6155
- executionProvider,
6156
- missingOrEmptyRetries: resolveMissingOrEmptyOutputRetries(agent),
6157
- logger,
6158
- runningMessage: () => "Running Issue Final Review agent",
6159
- retryMessage: (attempt, total) => `Retrying Issue Final Review after empty output (${attempt}/${total})`,
6160
- executionOptions: {
6161
- stage: "issueFinalReview",
6162
- agent: agent.agent,
6163
- model: agent.model,
6164
- variant: agent.variant,
6165
- env: agent.env,
6166
- prompt,
6167
- target,
6168
- repoRoot: repoRoot2,
6169
- branchName: builderBranch,
6170
- sandbox: options.config.sandbox,
6171
- autoApprove: true,
6172
- artifactPath: artifactPathInWorktree,
6173
- worktreePath,
6174
- artifacts: [
6175
- buildRunContextArtifact({
6176
- issue,
6177
- parentPrdIssue,
6178
- target,
6179
- branchName: builderBranch,
6180
- repoRoot: repoRoot2,
6181
- reviewerCriteria: strategy.review.reviewer.criteria,
6182
- sections: STAGE_SECTIONS.issueFinalReview
6183
- })
6184
- ],
6185
- ...options.memory ? { memory: options.memory } : {},
6186
- logger
6298
+ if (artifact.badReverts) {
6299
+ lines.push(
6300
+ `- **Bad reverts:** ${formatCount(artifact.badReverts.length)}`
6301
+ );
6187
6302
  }
6188
- });
6189
- const executionResult = entry.executionResult;
6190
- if (!executionResult.success) {
6191
- throw new Error(
6192
- `Issue Final Review agent execution failed: ${executionResult.error}`
6193
- );
6303
+ lines.push("");
6194
6304
  }
6195
- let content;
6196
- if (entry.artifact._tag === "content") {
6197
- content = entry.artifact.value;
6198
- } else if (entry.artifact._tag === "empty") {
6199
- throw new Error(
6200
- `Issue Final Review agent produced empty output at ${artifactPath}`
6201
- );
6202
- } else {
6203
- throw new Error(
6204
- `Issue Final Review agent did not produce output at ${artifactPath}`
6305
+ return lines.join("\n").trimEnd();
6306
+ }
6307
+ function formatLabel(value) {
6308
+ return typeof value === "string" && value.trim() !== "" ? value.trim().replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : "Unknown";
6309
+ }
6310
+ function formatBoolean(value) {
6311
+ return value === true ? "Yes" : value === false ? "No" : "Unknown";
6312
+ }
6313
+ function formatPath(value) {
6314
+ return typeof value === "string" && value.trim() !== "" ? `\`${value.trim()}\`` : "Unknown";
6315
+ }
6316
+ function formatPathList(paths) {
6317
+ return paths.length > 0 ? paths.map((path9) => `\`${path9}\``).join(", ") : "None";
6318
+ }
6319
+ function formatCount(count) {
6320
+ return count === 0 ? "None" : String(count);
6321
+ }
6322
+ function formatTableCell(value) {
6323
+ return typeof value === "string" && value.trim() !== "" ? value.trim().replace(/\s+/g, " ").replace(/\|/g, "\\|") : "Unknown";
6324
+ }
6325
+ async function performCleanupAndClosure(options, pr) {
6326
+ await options.issueProvider.removeLabel(
6327
+ options.issue.number,
6328
+ options.labels.agentInProgress
6329
+ );
6330
+ await options.issueProvider.removeLabel(
6331
+ options.issue.number,
6332
+ options.labels.prOpenAwaitingMerge
6333
+ );
6334
+ let childCloseSucceeded = false;
6335
+ try {
6336
+ await options.issueProvider.closeIssue(options.issue.number);
6337
+ childCloseSucceeded = true;
6338
+ } catch (error) {
6339
+ options.logger.step(
6340
+ "warn",
6341
+ `Issue #${options.issue.number} could not be closed: ${error instanceof Error ? error.message : String(error)}`
6205
6342
  );
6206
6343
  }
6207
- const validation = validateAgentArtifact({
6208
- kind: "issue-final-review",
6209
- artifactPath,
6210
- issueNumber: issue.number,
6211
- branchName: builderBranch
6212
- });
6213
- if (!validation.ok) {
6214
- throw new Error(
6215
- `Issue Final Review artifact validation failed: ${validation.reason}`
6344
+ if (childCloseSucceeded) {
6345
+ await maybeCloseParentAfterChildCompletion(
6346
+ options.issueProvider,
6347
+ options.issue.number,
6348
+ options.logger
6216
6349
  );
6217
6350
  }
6218
- const parsed = JSON.parse(content);
6219
- const verdict = parsed.verdict;
6220
- const baseRef = `origin/${target.baseBranch}`;
6221
- await assertCanonicalBaseAncestor2({
6222
- worktreePath,
6223
- baseRef,
6224
- stageName: "Issue Final Review",
6225
- logger
6226
- });
6227
- if (verdict === "pass") {
6228
- const changedPaths = await completeIssueFinalReviewChangedPaths({
6229
- worktreePath,
6230
- baseRef,
6231
- declaredChangedPaths: extractChangedPaths(parsed),
6232
- logger
6233
- });
6234
- updateWorktreeRunState(worktreePath, {
6235
- issueFinalReview: {
6236
- completed: true,
6237
- verdict: "pass",
6238
- artifactPath,
6239
- selfRetouched: parsed.selfRetouched === true,
6240
- changedPaths,
6241
- verificationPassed: parsed.verification?.passed === true
6242
- }
6243
- });
6244
- return {
6245
- status: "passed",
6246
- verdict: "pass",
6247
- artifactPath,
6248
- selfRetouched: parsed.selfRetouched === true,
6249
- changedPaths,
6250
- verificationPassed: parsed.verification?.passed === true
6251
- };
6252
- }
6253
- if (verdict === "needs_human_review") {
6254
- return {
6255
- status: "needs_human_review",
6256
- verdict: "needs_human_review",
6257
- artifactPath,
6258
- needsHumanReason: parsed.needsHumanReason,
6259
- selfRetouched: parsed.selfRetouched === true,
6260
- changedPaths: extractChangedPaths(parsed)
6261
- };
6351
+ return {
6352
+ status: "merged",
6353
+ prNumber: pr.number,
6354
+ prUrl: pr.url,
6355
+ prTitle: options.prTitle,
6356
+ prBody: options.prBody
6357
+ };
6358
+ }
6359
+ function assertRecordedMergedPrDetails(state) {
6360
+ if (typeof state.pr?.number === "number" && typeof state.pr.url === "string") {
6361
+ return;
6262
6362
  }
6263
6363
  throw new Error(
6264
- `Unknown Issue Final Review verdict: ${JSON.stringify(verdict)}`
6364
+ "Lifecycle invariant: resumed merged GitHub Issue Publication requires recorded PR number and URL"
6365
+ );
6366
+ }
6367
+ function makeIssueTransitions(issueProvider, labels) {
6368
+ return createIssueTransitions(
6369
+ {
6370
+ fetchIssue: issueProvider.fetchIssue.bind(issueProvider),
6371
+ addLabels: issueProvider.addLabels.bind(issueProvider),
6372
+ removeLabel: issueProvider.removeLabel.bind(issueProvider),
6373
+ closeIssue: issueProvider.closeIssue.bind(issueProvider)
6374
+ },
6375
+ {
6376
+ blocked: labels.blocked,
6377
+ readyForAgent: labels.readyForAgent,
6378
+ needsTriage: labels.needsTriage,
6379
+ agentInProgress: labels.agentInProgress,
6380
+ readyForHuman: labels.readyForHuman,
6381
+ prOpenAwaitingMerge: labels.prOpenAwaitingMerge
6382
+ }
6265
6383
  );
6266
6384
  }
6267
- async function assertCanonicalBaseAncestor2(options) {
6268
- const { worktreePath, baseRef, stageName, logger } = options;
6269
- await syncRemoteBaseRef2(worktreePath, baseRef, logger);
6270
- try {
6271
- await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
6272
- cwd: worktreePath,
6273
- logger,
6274
- label: "git merge-base --is-ancestor"
6275
- });
6276
- } catch {
6277
- throw new Error(
6278
- `Cannot continue after ${stageName}: ${baseRef} is not an ancestor of HEAD. An agent may have moved the issue branch behind the canonical base; refresh the branch onto the latest target base before continuing.`
6279
- );
6280
- }
6281
- }
6282
- async function syncRemoteBaseRef2(worktreePath, baseRef, logger) {
6283
- const remoteBase = parseRemoteBaseRef2(baseRef);
6284
- if (!remoteBase) {
6385
+ async function guardFinalPublishContent(options) {
6386
+ const {
6387
+ worktreePath,
6388
+ baseRef,
6389
+ title,
6390
+ body,
6391
+ secretLikeContentDetectionEnabled,
6392
+ logger
6393
+ } = options;
6394
+ if (!secretLikeContentDetectionEnabled) {
6285
6395
  return;
6286
6396
  }
6287
- await execCapture("git", ["fetch", remoteBase.remote, remoteBase.branch], {
6397
+ const finalDiff = await execCapture("git", ["diff", `${baseRef}...HEAD`], {
6288
6398
  cwd: worktreePath,
6289
6399
  logger,
6290
- label: "git fetch target"
6400
+ label: "git diff final secret guard"
6291
6401
  });
6402
+ assertNoSecretLikeContent([
6403
+ {
6404
+ source: "final diff",
6405
+ content: extractAddedDiffContent(finalDiff.stdout)
6406
+ },
6407
+ { source: "PR title", content: title },
6408
+ { source: "PR body", content: body }
6409
+ ]);
6292
6410
  }
6293
- function parseRemoteBaseRef2(baseRef) {
6294
- const [remote, ...branchParts] = baseRef.split("/");
6295
- const branch = branchParts.join("/");
6296
- if (!remote || !branch) {
6297
- return null;
6298
- }
6299
- return { remote, branch };
6411
+ function extractAddedDiffContent(diff) {
6412
+ return diff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1)).join("\n");
6300
6413
  }
6301
- async function completeIssueFinalReviewChangedPaths(options) {
6302
- const actualChangedPaths = await listIssueFinalReviewChangedPaths2(options);
6303
- const declared = new Set(options.declaredChangedPaths.map(normalizeGitPath2));
6304
- const missing = actualChangedPaths.filter((path9) => !declared.has(path9));
6305
- if (missing.length === 0) {
6306
- return actualChangedPaths;
6414
+ function assertNoSecretLikeContent(inputs) {
6415
+ const findings = inputs.flatMap(
6416
+ ({ source, content }) => findSecretLikeContent(source, content ?? "")
6417
+ );
6418
+ if (findings.length === 0) {
6419
+ return;
6307
6420
  }
6308
- options.logger.step(
6309
- "warn",
6310
- `Issue Final Review pass omitted ${missing.length} changed file(s) from changedFiles; using git inventory for finalization state.`
6421
+ const summary = findings.slice(0, 5).map((finding) => `${finding.source}: ${finding.kind}`).join("; ");
6422
+ throw new Error(
6423
+ `Secret-like content detected before finalization (${summary}). Redact it before committing, pushing, or creating a PR.`
6311
6424
  );
6312
- options.logger.kv("MISSING_CHANGED_FILES", missing.join(", "));
6313
- return actualChangedPaths;
6314
6425
  }
6315
- async function listIssueFinalReviewChangedPaths2(options) {
6316
- const trackedDiff = await execCapture(
6317
- "git",
6318
- ["diff-index", "--name-only", options.baseRef, "--"],
6426
+ function findSecretLikeContent(source, content) {
6427
+ const findings = [];
6428
+ const patterns = [
6319
6429
  {
6320
- cwd: options.worktreePath,
6321
- logger: options.logger,
6322
- label: "git diff-index Issue Final Review paths"
6323
- }
6324
- );
6325
- const untrackedFiles = await execCapture(
6326
- "git",
6327
- ["ls-files", "--others", "--exclude-standard"],
6430
+ kind: "private key block",
6431
+ regex: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/gi
6432
+ },
6328
6433
  {
6329
- cwd: options.worktreePath,
6330
- logger: options.logger,
6331
- label: "git ls-files Issue Final Review paths"
6434
+ kind: "GitHub token",
6435
+ regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g
6436
+ },
6437
+ {
6438
+ kind: "GitHub fine-grained token",
6439
+ regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g
6440
+ },
6441
+ { kind: "npm token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g },
6442
+ { kind: "OpenAI key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
6443
+ { kind: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
6444
+ {
6445
+ kind: "authorization bearer token",
6446
+ regex: /\bAuthorization\s*:\s*Bearer\s+[^\s`'\"]{12,}/gi
6447
+ },
6448
+ {
6449
+ kind: "secret assignment",
6450
+ regex: /\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|DATABASE[_-]?URL)[A-Z0-9_]*\s*[:=]\s*["']?[^\s"'`]{8,}/gi
6332
6451
  }
6333
- );
6334
- return Array.from(
6335
- /* @__PURE__ */ new Set([
6336
- ...parseGitPathList2(trackedDiff.stdout),
6337
- ...parseGitPathList2(untrackedFiles.stdout)
6338
- ])
6339
- ).filter(isIssueFinalReviewAuditedPath2);
6340
- }
6341
- function parseGitPathList2(output) {
6342
- return output.split(/\r?\n/).map(normalizeGitPath2).filter((path9) => path9.length > 0);
6343
- }
6344
- function normalizeGitPath2(path9) {
6345
- return path9.replace(/^"|"$/g, "").replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
6346
- }
6347
- function isIssueFinalReviewAuditedPath2(path9) {
6348
- return path9 !== "" && path9 !== WORKTREE_RUN_STATE_PATH && !path9.startsWith(".pourkit/.tmp/") && !path9.startsWith(".pourkit/logs/");
6349
- }
6350
- function extractChangedPaths(parsed) {
6351
- return Array.isArray(parsed.changedFiles) ? parsed.changedFiles.map(
6352
- (file) => file && typeof file === "object" ? file.path : void 0
6353
- ).filter((path9) => typeof path9 === "string") : [];
6354
- }
6355
- function loadIssueFinalReviewPrompt(options) {
6356
- const {
6357
- repoRoot: repoRoot2,
6358
- promptTemplate,
6359
- validationCommand,
6360
- reviewArtifactPath,
6361
- reviewVerdict
6362
- } = options;
6363
- const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
6364
- const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
6365
- let prompt = appendProtectedWorkGuidance(`${promptBody}
6366
-
6367
- ## Shared Run Context
6368
-
6369
- Read the selected issue requirements, planning context (including plan and PRD content when present), comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
6370
-
6371
- ## Initial Verification Pass
6372
-
6373
- - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
6374
- - Before reviewing code, diffs, artifacts, or prior findings, run the verification wrapper from the Worktree.
6375
- - Do not substitute direct underlying commands unless no wrapper path can run; if you must use direct commands, explain why in \`verification.summary\`.
6376
- - If a configured verification command fails because of fixable Issue work, self-retouch the Worktree, then rerun all configured verification commands.
6377
- - Do not return \`pass\` while any configured verification command is failing.
6378
- - If a command cannot run because the environment is missing required setup, dependencies, or scripts outside agent control, treat it as a human handoff blocker.
6379
- - If no verification commands are configured, note that and proceed with normal review.
6380
-
6381
- ## Changed Files Audit
6382
-
6383
- - Read the Branch section of ${RUN_CONTEXT_PATH_IN_WORKTREE} and use its Canonical Base Ref for the changed-file inventory.
6384
- - Before writing \`changedFiles\`, run \`git diff-index --name-only <canonical-base-ref> --\` from the Worktree to enumerate tracked files changed by the Issue.
6385
- - Also run \`git ls-files --others --exclude-standard\` from the Worktree to enumerate untracked files changed by the Issue.
6386
- - Treat the union of those command outputs, excluding only \`.pourkit/.tmp/**\`, \`.pourkit/logs/**\`, and \`.pourkit/state.json\`, as the required \`changedFiles\` inventory.
6387
- - Include every file from that inventory in \`changedFiles\`, including generated files, managed operating assets, docs, prompts, schema files, and large batches.
6388
- - If any file from the inventory is out-of-scope, mark it \`out-of-scope\` and either self-retouch it away or return \`needs_human_review\`; do not omit it.
6389
- - Do not return \`pass\` until \`changedFiles\` covers the full inventory.
6390
-
6391
- ## Required Artifact Validation
6392
-
6393
- - Before handoff, run exactly: \`${validationCommand}\`
6394
- - This validation checks both the artifact shape and \`changedFiles\` coverage against the canonical Git diff.
6395
- - If validation fails, fix the artifact or safely retouch the worktree as needed, then rerun the same command.
6396
- - Do not finish until the validation command passes.`);
6397
- if (reviewVerdict) {
6398
- prompt += `
6399
-
6400
- ## Prior Review Context
6401
-
6402
- - Reviewer Artifact Path: ${reviewArtifactPath}
6403
- - Prior Review Verdict: ${reviewVerdict}`;
6404
- if (reviewVerdict === "PASS_WITH_NOTES") {
6405
- prompt += `
6406
- - This Issue passed the prior Reviewer with notes. Read the Reviewer Artifact and inspect its notes before making your final pass decision.`;
6452
+ ];
6453
+ for (const { kind, regex } of patterns) {
6454
+ for (const match of content.matchAll(regex)) {
6455
+ if (isAllowedSecretPlaceholder(match[0]) || isAllowedDocumentationTokenReference(match[0])) {
6456
+ continue;
6457
+ }
6458
+ findings.push({ source, kind });
6459
+ break;
6407
6460
  }
6408
6461
  }
6409
- return prompt;
6462
+ return findings;
6463
+ }
6464
+ function isAllowedSecretPlaceholder(value) {
6465
+ const normalized = value.toLowerCase();
6466
+ return [
6467
+ "<redacted>",
6468
+ "<example",
6469
+ "example-",
6470
+ "dummy-",
6471
+ "test-",
6472
+ "placeholder",
6473
+ "xxxxx"
6474
+ ].some((allowed) => normalized.includes(allowed));
6475
+ }
6476
+ function isAllowedDocumentationTokenReference(value) {
6477
+ return /^token\s*:\s*["']?<verdict>/i.test(value);
6410
6478
  }
6411
6479
 
6412
6480
  // issues/issue-worktree-resolution.ts
@@ -6787,9 +6855,11 @@ async function completeIssueRun(options) {
6787
6855
  checksCompletionTimeoutMs: config.checks.checksCompletionTimeoutSeconds * 1e3,
6788
6856
  pollIntervalMs: config.checks.pollIntervalSeconds * 1e3
6789
6857
  };
6790
- const ifrState = executionResult.worktreePath ? readWorktreeRunState(executionResult.worktreePath) ?? worktreeState : worktreeState;
6858
+ const readCurrentWorktreeState = () => executionResult.worktreePath ? readWorktreeRunState(executionResult.worktreePath) ?? worktreeState : worktreeState;
6859
+ const ifrState = readCurrentWorktreeState();
6791
6860
  assertIssueFinalReviewPassed(ifrState);
6792
- if (executionResult.worktreePath && !worktreeState?.finalCommit?.completed && !worktreeState?.pr?.created && !await hasWorktreeChanges(
6861
+ const stateBeforeCompletion = readCurrentWorktreeState();
6862
+ if (executionResult.worktreePath && !stateBeforeCompletion?.finalCommit?.completed && !stateBeforeCompletion?.pr?.created && !await hasWorktreeChanges(
6793
6863
  executionResult.worktreePath,
6794
6864
  `origin/${effectiveBaseBranch}`,
6795
6865
  logger
@@ -6806,92 +6876,33 @@ async function completeIssueRun(options) {
6806
6876
  noOp: true
6807
6877
  };
6808
6878
  }
6809
- let prTitle = issue.title;
6810
- let prBody;
6811
- let finalizerResult;
6812
- const finalizerFromState = worktreeState?.finalizer?.completed ? worktreeState.finalizer : null;
6813
- if (finalizerFromState) {
6814
- try {
6815
- if (finalizerFromState.title && finalizerFromState.body) {
6816
- prTitle = finalizerFromState.title;
6817
- prBody = finalizerFromState.body;
6818
- } else if (finalizerFromState.artifactPath) {
6819
- if (!existsSync13(finalizerFromState.artifactPath)) {
6820
- throw new FinalizerFailure({
6821
- message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
6822
- });
6823
- }
6824
- const artifactContent = readFileSync15(
6825
- finalizerFromState.artifactPath,
6826
- "utf-8"
6827
- );
6828
- const parsed = parsePrDescription(artifactContent);
6829
- prTitle = parsed.title;
6830
- prBody = parsed.body;
6831
- } else {
6832
- throw new FinalizerFailure({
6833
- message: "Finalizer state is incomplete: missing title, body, and artifactPath"
6834
- });
6835
- }
6836
- } catch (error) {
6837
- throw error;
6838
- }
6839
- } else {
6840
- try {
6841
- finalizerResult = await runEffectAndMapExit(
6842
- runFinalizerAgent({
6843
- executionProvider,
6844
- config,
6845
- target,
6846
- issue,
6847
- builderBranch: branchName,
6848
- targetBaseBranch: effectiveBaseBranch,
6849
- worktreePath: executionResult.worktreePath,
6850
- reviewArtifactPath,
6851
- repoRoot: ROOT,
6852
- logger,
6853
- ...startResult.memory ? { memory: startResult.memory } : {}
6854
- })
6855
- );
6856
- } catch (error) {
6857
- throw error;
6858
- }
6859
- prTitle = finalizerResult.title;
6860
- prBody = finalizerResult.body;
6861
- if (executionResult.worktreePath) {
6862
- await assertCanonicalBaseAncestor2({
6863
- worktreePath: executionResult.worktreePath,
6864
- baseRef: `origin/${effectiveBaseBranch}`,
6865
- stageName: "Finalizer",
6866
- logger
6867
- });
6868
- }
6869
- }
6870
- prTitle = ensureConventionalPrTitle(
6871
- prTitle,
6872
- executionResult.commits.join("\n")
6873
- );
6874
- const finalBody = ensureClosingRefs(
6875
- prBody ?? `Closes #${issue.number}`,
6876
- issue.number
6879
+ const currentWorktreeState = readCurrentWorktreeState();
6880
+ const prDescriptionAgentResult = await runEffectAndMapExit(
6881
+ runPrDescriptionAgent({
6882
+ executionProvider,
6883
+ config,
6884
+ target,
6885
+ issue,
6886
+ builderBranch: branchName,
6887
+ targetBaseBranch: effectiveBaseBranch,
6888
+ worktreePath: executionResult.worktreePath,
6889
+ reviewArtifactPath,
6890
+ repoRoot: ROOT,
6891
+ logger,
6892
+ worktreeState: currentWorktreeState,
6893
+ ...startResult.memory ? { memory: startResult.memory } : {}
6894
+ })
6877
6895
  );
6878
- if (!finalizerFromState && executionResult.worktreePath) {
6879
- updateWorktreeRunState(executionResult.worktreePath, {
6880
- finalizer: {
6881
- completed: true,
6882
- artifactPath: finalizerResult.artifactPath,
6883
- title: prTitle,
6884
- body: prBody
6885
- }
6886
- });
6887
- }
6888
- const finalCommitFromState = worktreeState?.finalCommit?.completed;
6896
+ const prTitle = prDescriptionAgentResult.title;
6897
+ const prBody = prDescriptionAgentResult.body;
6898
+ const stateBeforeFinalCommit = readCurrentWorktreeState();
6899
+ const finalCommitFromState = stateBeforeFinalCommit?.finalCommit?.completed;
6889
6900
  if (!finalCommitFromState) {
6890
6901
  await finalizeWorktreeCommit({
6891
6902
  worktreePath: executionResult.worktreePath,
6892
6903
  baseRef: `origin/${effectiveBaseBranch}`,
6893
6904
  title: prTitle,
6894
- body: finalBody,
6905
+ body: prBody,
6895
6906
  logger
6896
6907
  });
6897
6908
  if (executionResult.worktreePath) {
@@ -6919,9 +6930,9 @@ async function completeIssueRun(options) {
6919
6930
  baseBranch: effectiveBaseBranch,
6920
6931
  autoMerge: target.autoMerge !== false,
6921
6932
  worktreePath: executionResult.worktreePath,
6922
- worktreeState,
6933
+ worktreeState: readCurrentWorktreeState(),
6923
6934
  prTitle,
6924
- prBody: finalBody,
6935
+ prBody,
6925
6936
  issueProvider,
6926
6937
  prProvider,
6927
6938
  logger,
@@ -7369,7 +7380,7 @@ import { join as join16 } from "path";
7369
7380
  import {
7370
7381
  existsSync as existsSync14,
7371
7382
  mkdirSync as mkdirSync7,
7372
- readFileSync as readFileSync16,
7383
+ readFileSync as readFileSync15,
7373
7384
  readdirSync as readdirSync3,
7374
7385
  writeFileSync as writeFileSync4
7375
7386
  } from "fs";
@@ -7462,12 +7473,12 @@ function readPrdRun(repoRoot2, prdRef) {
7462
7473
  return { record: null, diagnostics: [] };
7463
7474
  }
7464
7475
  try {
7465
- const raw = JSON.parse(readFileSync16(recordPath, "utf-8"));
7476
+ const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
7466
7477
  const parsed = PrdRunRecordSchema.parse(raw);
7467
7478
  return { record: parsed, diagnostics: [] };
7468
7479
  } catch (error) {
7469
7480
  try {
7470
- const raw = JSON.parse(readFileSync16(recordPath, "utf-8"));
7481
+ const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
7471
7482
  if (raw && typeof raw === "object" && raw.start && typeof raw.start === "object" && raw.start.startBaseBranch === void 0) {
7472
7483
  return {
7473
7484
  record: raw,
@@ -7501,7 +7512,7 @@ function listPrdRuns(repoRoot2) {
7501
7512
  const recordPath = join15(stateDir, entry.name);
7502
7513
  try {
7503
7514
  const record = PrdRunRecordSchema.parse(
7504
- JSON.parse(readFileSync16(recordPath, "utf-8"))
7515
+ JSON.parse(readFileSync15(recordPath, "utf-8"))
7505
7516
  );
7506
7517
  records.push(record);
7507
7518
  } catch (error) {
@@ -9284,7 +9295,7 @@ async function launchGithubQueueDrain(options, prdRef, startReceipt, targetName,
9284
9295
  }
9285
9296
 
9286
9297
  // prd-run/final-review-validation.ts
9287
- import { existsSync as existsSync15, readFileSync as readFileSync17 } from "fs";
9298
+ import { existsSync as existsSync15, readFileSync as readFileSync16 } from "fs";
9288
9299
 
9289
9300
  // commands/prd-run.ts
9290
9301
  async function runPrdRunLaunchCommand(options) {
@@ -12294,7 +12305,7 @@ Init applied: ${result.applied} operations applied, ${result.skipped} skipped.`
12294
12305
  }
12295
12306
 
12296
12307
  // commands/memory-init.ts
12297
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync5 } from "fs";
12308
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
12298
12309
  import { join as join17 } from "path";
12299
12310
  async function runMemoryInitCommand(options) {
12300
12311
  const cwd = options.cwd ?? process.cwd();
@@ -12305,7 +12316,7 @@ async function runMemoryInitCommand(options) {
12305
12316
  message: "No .pourkit/config.json found. Run `pourkit init` first to initialize Pourkit, then run `pourkit memory init`."
12306
12317
  };
12307
12318
  }
12308
- const raw = JSON.parse(readFileSync18(configPath, "utf8"));
12319
+ const raw = JSON.parse(readFileSync17(configPath, "utf8"));
12309
12320
  let alreadyEnabled = false;
12310
12321
  if (Object.prototype.hasOwnProperty.call(raw, "memory")) {
12311
12322
  const m = raw.memory;
@@ -12362,7 +12373,7 @@ ${content}${MANAGED_BLOCK_END}
12362
12373
  writeFileSync5(filePath, blockContent);
12363
12374
  return;
12364
12375
  }
12365
- const existing = readFileSync18(filePath, "utf8");
12376
+ const existing = readFileSync17(filePath, "utf8");
12366
12377
  const beginIdx = existing.indexOf(MANAGED_BLOCK_BEGIN);
12367
12378
  const endIdx = existing.indexOf(MANAGED_BLOCK_END);
12368
12379
  if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
@@ -12491,7 +12502,7 @@ async function runSerenaStatusCommand(options) {
12491
12502
  }
12492
12503
 
12493
12504
  // commands/config-schema.ts
12494
- import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
12505
+ import { readFileSync as readFileSync18, existsSync as existsSync18 } from "fs";
12495
12506
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile2, readdir as readdir2 } from "fs/promises";
12496
12507
  import { resolve as resolve3, dirname as dirname5, relative as relative2 } from "path";
12497
12508
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -12512,7 +12523,7 @@ var _schemaValidator = null;
12512
12523
  var _schemaErrors = null;
12513
12524
  function getSchemaValidator() {
12514
12525
  if (!_schemaValidator) {
12515
- const schema = JSON.parse(readFileSync19(PACKAGED_SCHEMA_PATH, "utf-8"));
12526
+ const schema = JSON.parse(readFileSync18(PACKAGED_SCHEMA_PATH, "utf-8"));
12516
12527
  const ajv = new Ajv2({ strict: true });
12517
12528
  ajv.addKeyword("x-pourkit-schema-version");
12518
12529
  const validate = ajv.compile(schema);
@@ -12526,7 +12537,7 @@ function getSchemaValidator() {
12526
12537
  return _schemaValidator;
12527
12538
  }
12528
12539
  function readPackagedHash() {
12529
- return readFileSync19(PACKAGED_HASH_PATH, "utf-8");
12540
+ return readFileSync18(PACKAGED_HASH_PATH, "utf-8");
12530
12541
  }
12531
12542
  function localSchemaDir(repoRoot2) {
12532
12543
  return resolve3(repoRoot2, ".pourkit/schema");
@@ -13082,7 +13093,7 @@ async function validateMemoryHealth(cwd, memoryConfig) {
13082
13093
  detail: icmPath !== null ? icmPath : "icm not found on PATH"
13083
13094
  });
13084
13095
  const gitignorePath = resolve3(cwd, ".gitignore");
13085
- const gitignoreContent = existsSync18(gitignorePath) ? readFileSync19(gitignorePath, "utf-8") : "";
13096
+ const gitignoreContent = existsSync18(gitignorePath) ? readFileSync18(gitignorePath, "utf-8") : "";
13086
13097
  const hasGitignoreEntry = gitignoreContent.includes(".pourkit/icm/");
13087
13098
  checks.push({
13088
13099
  name: "icm-gitignore",
@@ -13131,7 +13142,7 @@ async function runDoctorCommand(options) {
13131
13142
  let rawMemoryConfig;
13132
13143
  if (existsSync18(configPath)) {
13133
13144
  try {
13134
- const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
13145
+ const raw = JSON.parse(readFileSync18(configPath, "utf-8"));
13135
13146
  rawMemoryConfig = raw.memory;
13136
13147
  const validate = getSchemaValidator();
13137
13148
  const valid2 = validate(raw);
@@ -14370,7 +14381,7 @@ import path7 from "path";
14370
14381
 
14371
14382
  // execution/sandbox-image.ts
14372
14383
  import { createHash as createHash2 } from "crypto";
14373
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
14384
+ import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
14374
14385
  import path6 from "path";
14375
14386
  function sandboxImageName(repoRoot2, installIcm) {
14376
14387
  const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
@@ -14381,7 +14392,7 @@ function sandboxImageName(repoRoot2, installIcm) {
14381
14392
  const base2 = `sandcastle:${baseName}`;
14382
14393
  return installIcm ? `${base2}-icm` : base2;
14383
14394
  }
14384
- const fingerprint = createHash2("sha256").update(readFileSync20(dockerfilePath)).digest("hex").slice(0, 8);
14395
+ const fingerprint = createHash2("sha256").update(readFileSync19(dockerfilePath)).digest("hex").slice(0, 8);
14385
14396
  const base = `sandcastle:${baseName}-${fingerprint}`;
14386
14397
  return installIcm ? `${base}-icm` : base;
14387
14398
  }
@@ -15532,11 +15543,11 @@ function createCliProgram(version) {
15532
15543
  return program;
15533
15544
  }
15534
15545
  async function resolveCliVersion() {
15535
- if (isPackageVersion("0.0.0-next-20260622201317")) {
15536
- return "0.0.0-next-20260622201317";
15546
+ if (isPackageVersion("0.0.0-next-20260622235247")) {
15547
+ return "0.0.0-next-20260622235247";
15537
15548
  }
15538
- if (isReleaseVersion("0.0.0-next-20260622201317")) {
15539
- return "0.0.0-next-20260622201317";
15549
+ if (isReleaseVersion("0.0.0-next-20260622235247")) {
15550
+ return "0.0.0-next-20260622235247";
15540
15551
  }
15541
15552
  try {
15542
15553
  const root = repoRoot();