@pourkit/cli 0.0.0-next-20260614002607 → 0.0.0-next-20260614182520

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.
@@ -58,20 +58,20 @@ function createLogger(name, filePath) {
58
58
  );
59
59
  },
60
60
  async close() {
61
- await new Promise((resolve3) => {
61
+ await new Promise((resolve4) => {
62
62
  if (!fileStream) {
63
- resolve3();
63
+ resolve4();
64
64
  return;
65
65
  }
66
66
  const timer = setTimeout(() => {
67
67
  if (!fileStream.destroyed) {
68
68
  fileStream.destroy();
69
69
  }
70
- resolve3();
70
+ resolve4();
71
71
  }, 2e3);
72
72
  fileStream.end(() => {
73
73
  clearTimeout(timer);
74
- resolve3();
74
+ resolve4();
75
75
  });
76
76
  });
77
77
  }
@@ -153,6 +153,7 @@ __export(common_exports, {
153
153
  execJson: () => execJson,
154
154
  parseWorktreeListPorcelain: () => parseWorktreeListPorcelain,
155
155
  readMaybeEnvInt: () => readMaybeEnvInt,
156
+ redactSensitiveValues: () => redactSensitiveValues,
156
157
  repoRelative: () => repoRelative,
157
158
  repoRoot: () => repoRoot,
158
159
  sleep: () => sleep,
@@ -265,7 +266,7 @@ async function execJson(command, args, options = {}) {
265
266
  return JSON.parse(result.stdout);
266
267
  }
267
268
  function sleep(ms) {
268
- return new Promise((resolve3) => setTimeout(resolve3, ms));
269
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
269
270
  }
270
271
  async function execCaptureWithRetry(command, args, options = {}) {
271
272
  const retries = options.retries ?? 3;
@@ -311,7 +312,14 @@ function parseWorktreeListPorcelain(text, branch) {
311
312
  }
312
313
  return null;
313
314
  }
314
- var execFileAsync, TRANSIENT_GH_ERROR, TYPE_LABELS;
315
+ function redactSensitiveValues(input) {
316
+ let redacted = input;
317
+ for (const pattern of TOKEN_LIKE_PATTERNS) {
318
+ redacted = redacted.replace(pattern, "[REDACTED]");
319
+ }
320
+ return redacted;
321
+ }
322
+ var execFileAsync, TRANSIENT_GH_ERROR, TYPE_LABELS, TOKEN_LIKE_PATTERNS;
315
323
  var init_common = __esm({
316
324
  "shared/common.ts"() {
317
325
  "use strict";
@@ -325,189 +333,60 @@ var init_common = __esm({
325
333
  "type:polish",
326
334
  "type:refactor"
327
335
  ];
336
+ TOKEN_LIKE_PATTERNS = [
337
+ /gh[ps]_[A-Za-z0-9]{20,}/g,
338
+ /token[=:_\s][A-Za-z0-9_-]{20,}/gi,
339
+ /secret[=:_\s][A-Za-z0-9_-]{20,}/gi,
340
+ /credential[=:_\s][A-Za-z0-9_-]{20,}/gi,
341
+ /key[=:_\s][A-Za-z0-9_-]{20,}/gi,
342
+ /bearer\s+[A-Za-z0-9_-]{20,}/gi,
343
+ /authorization:\s*Bearer\s+[A-Za-z0-9_-]{20,}/gi,
344
+ /xox[baprs]-[A-Za-z0-9_-]{10,}/g
345
+ ];
328
346
  }
329
347
  });
330
348
 
331
349
  // e2e/run-live-e2e.ts
332
350
  import path8 from "path";
333
- import { existsSync as existsSync14 } from "fs";
334
351
  import { pathToFileURL } from "url";
335
352
  import { mkdir as mkdir5, readFile as readFile4, readdir as readdir2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
336
353
 
337
354
  // commands/issue-run.ts
338
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
339
- import { isAbsolute as isAbsolute3, join as join14 } from "path";
355
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
356
+ import { isAbsolute as isAbsolute4, join as join14 } from "path";
340
357
 
341
358
  // shared/config.ts
342
- import { join } from "path";
343
- import { z } from "zod";
344
- var NonEmptyString = z.string().trim().min(1);
359
+ import { readFileSync } from "fs";
360
+ import { fileURLToPath } from "url";
361
+ import { dirname, isAbsolute, join, normalize, sep, resolve } from "path";
362
+ import Ajv from "ajv";
363
+ var __filename = fileURLToPath(import.meta.url);
364
+ var __dirname = dirname(__filename);
365
+ var SCHEMA_PATH = resolve(__dirname, "../schema/pourkit.schema.json");
366
+ var _schema = null;
367
+ var _validate = null;
368
+ var _ajvErrors = null;
369
+ function getValidator() {
370
+ if (!_validate) {
371
+ const schema = _schema ?? JSON.parse(readFileSync(SCHEMA_PATH, "utf-8"));
372
+ _schema = schema;
373
+ const ajv = new Ajv({ strict: true });
374
+ ajv.addKeyword("x-pourkit-schema-version");
375
+ const validate = ajv.compile(schema);
376
+ _validate = (data) => {
377
+ _ajvErrors = null;
378
+ const valid2 = validate(data);
379
+ if (!valid2) _ajvErrors = validate.errors;
380
+ return valid2;
381
+ };
382
+ }
383
+ return _validate;
384
+ }
345
385
  var DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES = 3;
346
- var OutputRetriesConfigSchema = z.object({
347
- missingOrEmpty: z.number().int().nonnegative().default(DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES)
348
- }).strict();
386
+ var DEFAULT_BRANCH_TEMPLATE = "pourkit/{{issue.number}}/{{issue.slug}}";
349
387
  function resolveMissingOrEmptyOutputRetries(config) {
350
388
  return config?.outputRetries?.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES;
351
389
  }
352
- var StageAgentConfigSchema = z.object({
353
- agent: NonEmptyString,
354
- model: NonEmptyString,
355
- variant: NonEmptyString.optional(),
356
- env: z.record(z.string(), z.string()).optional(),
357
- promptTemplate: NonEmptyString,
358
- outputRetries: OutputRetriesConfigSchema.optional()
359
- }).strict();
360
- var ReviewerConfigSchema = z.object({
361
- agent: NonEmptyString,
362
- model: NonEmptyString,
363
- variant: NonEmptyString.optional(),
364
- env: z.record(z.string(), z.string()).optional(),
365
- promptTemplate: NonEmptyString,
366
- outputRetries: OutputRetriesConfigSchema.optional(),
367
- criteria: z.array(NonEmptyString),
368
- includeReviewHistory: z.boolean().optional(),
369
- passWithNotesRefactorAttempts: z.number().int().nonnegative().optional()
370
- }).strict();
371
- var VerificationCommandSchema = z.object({
372
- command: z.string().nullable().optional(),
373
- label: z.string().optional()
374
- }).strict().refine((d) => d.command && d.command.trim() !== "", {
375
- message: "must have a non-empty command"
376
- }).transform((d) => ({
377
- command: d.command,
378
- label: d.label && d.label.trim() !== "" ? d.label : void 0
379
- }));
380
- var PrdRunModeSchema = z.enum(["github", "local"]);
381
- var QueueConfigSchema = z.object({
382
- loop: z.boolean().optional()
383
- }).strict();
384
- var FailureResolutionConfigSchema = z.object({
385
- agent: NonEmptyString,
386
- model: NonEmptyString,
387
- variant: NonEmptyString.optional(),
388
- env: z.record(z.string(), z.string()).optional(),
389
- promptTemplate: NonEmptyString,
390
- outputRetries: OutputRetriesConfigSchema.optional(),
391
- maxAttemptsPerFailure: z.number().int().positive(),
392
- failureLimits: z.record(z.string(), z.number().int().positive()).optional()
393
- }).strict();
394
- var ReviewRefactorLoopStrategySchema = z.object({
395
- type: z.literal("review-refactor-loop"),
396
- implement: z.object({
397
- builder: StageAgentConfigSchema
398
- }).strict(),
399
- conflictResolution: z.object({
400
- agent: NonEmptyString,
401
- model: NonEmptyString,
402
- variant: NonEmptyString.optional(),
403
- env: z.record(z.string(), z.string()).optional(),
404
- promptTemplate: NonEmptyString,
405
- maxAttempts: z.number().int().positive()
406
- }).strict().optional(),
407
- failureResolution: FailureResolutionConfigSchema,
408
- review: z.object({
409
- reviewer: ReviewerConfigSchema,
410
- refactor: StageAgentConfigSchema,
411
- maxIterations: z.number().int().positive(),
412
- passWithNotesRefactorAttempts: z.number().int().nonnegative().default(2)
413
- }).strict(),
414
- verify: z.object({
415
- commands: z.preprocess(
416
- (v) => Array.isArray(v) ? v : [],
417
- z.array(VerificationCommandSchema).refine((arr) => arr.length > 0, {
418
- message: "must contain at least one command"
419
- })
420
- )
421
- }).strict().optional(),
422
- issueFinalReview: StageAgentConfigSchema.extend({
423
- maxAttempts: z.number().int().positive()
424
- }),
425
- finalize: z.object({
426
- prDescriptionAgent: StageAgentConfigSchema,
427
- maxAttempts: z.number().int().positive()
428
- }).strict(),
429
- prdRun: z.object({
430
- mode: PrdRunModeSchema.optional(),
431
- // Uses promptTemplate (canonical StageAgentConfig field), not prompt as Issue contract may suggest
432
- finalReview: StageAgentConfigSchema
433
- }).strict().optional()
434
- }).strict();
435
- var TargetSerenaConfigSchema = z.object({
436
- enabled: z.boolean().optional(),
437
- required: z.boolean().optional()
438
- }).strict();
439
- var TargetSchema = z.object({
440
- name: NonEmptyString,
441
- baseBranch: z.preprocess(
442
- (v) => typeof v === "string" && v.length > 0 ? v : void 0,
443
- NonEmptyString.default("main")
444
- ),
445
- branchTemplate: z.string().default("pourkit/{{issue.number}}/{{issue.slug}}"),
446
- setupCommands: z.preprocess(
447
- (v) => Array.isArray(v) ? v : void 0,
448
- z.array(VerificationCommandSchema).default([])
449
- ),
450
- autoMerge: z.preprocess(
451
- (v) => typeof v === "boolean" ? v : void 0,
452
- z.boolean().default(true)
453
- ),
454
- queue: QueueConfigSchema.optional(),
455
- serena: TargetSerenaConfigSchema.optional(),
456
- strategy: ReviewRefactorLoopStrategySchema
457
- }).strict();
458
- var LabelsSchema = z.object({
459
- readyForAgent: NonEmptyString,
460
- agentInProgress: NonEmptyString,
461
- blocked: NonEmptyString,
462
- prOpenAwaitingMerge: NonEmptyString,
463
- readyForHuman: NonEmptyString,
464
- needsTriage: NonEmptyString.optional().default("needs-triage")
465
- }).strict();
466
- var SandboxMountSchema = z.object({
467
- hostPath: NonEmptyString,
468
- sandboxPath: NonEmptyString,
469
- readonly: z.boolean().default(false)
470
- }).strict();
471
- var SandboxSchema = z.object({
472
- provider: NonEmptyString,
473
- copyToWorktree: z.array(NonEmptyString).optional(),
474
- mounts: z.array(SandboxMountSchema).optional(),
475
- env: z.record(z.string()).optional(),
476
- idleTimeoutSeconds: z.preprocess((v) => {
477
- if (v === void 0) return void 0;
478
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
479
- return v;
480
- }, z.number().int().positive().optional())
481
- }).strict();
482
- var ChecksSchema = z.object({
483
- requiredLabels: z.array(NonEmptyString),
484
- allowedAuthors: z.array(NonEmptyString),
485
- checksFoundTimeoutSeconds: z.number().int().positive().optional(),
486
- checksCompletionTimeoutSeconds: z.number().int().positive().optional(),
487
- pollIntervalSeconds: z.number().int().positive().optional(),
488
- issueListLimit: z.number().int().positive().optional()
489
- }).strict();
490
- var CleanupConfigSchema = z.object({
491
- enabled: z.boolean().default(true),
492
- worktreeRetentionDays: z.number().int().positive().default(14),
493
- logRetentionDays: z.number().int().positive().default(30)
494
- }).strict();
495
- var SerenaConfigSchema = z.object({
496
- enabled: z.boolean().default(false),
497
- required: z.boolean().default(false),
498
- mcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
499
- sandboxMcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
500
- dataDir: z.string().default(".pourkit/serena/"),
501
- autoStart: z.boolean().default(false)
502
- }).strict();
503
- var PourkitConfigSchema = z.object({
504
- targets: z.array(TargetSchema).min(1),
505
- labels: LabelsSchema,
506
- sandbox: SandboxSchema,
507
- checks: ChecksSchema,
508
- cleanup: CleanupConfigSchema.optional(),
509
- serena: SerenaConfigSchema.default({})
510
- }).strict();
511
390
  var removedFieldReplacements = {
512
391
  "config.implementor": "targets[].strategy.implement.builder",
513
392
  "config.reviewer": "targets[].strategy.review.reviewer",
@@ -571,11 +450,12 @@ function checkRemovedFields(raw) {
571
450
  }
572
451
  }
573
452
  }
574
- function formatZodPath(path9) {
575
- if (path9.length === 0) return "";
453
+ function formatAjvPath(instancePath) {
454
+ if (!instancePath || instancePath === "/") return "";
455
+ const parts = instancePath.split("/").slice(1);
576
456
  let result = "";
577
- for (const segment of path9) {
578
- if (typeof segment === "number") {
457
+ for (const segment of parts) {
458
+ if (/^\d+$/.test(segment)) {
579
459
  result += `[${segment}]`;
580
460
  } else {
581
461
  result += result ? `.${segment}` : segment;
@@ -583,51 +463,172 @@ function formatZodPath(path9) {
583
463
  }
584
464
  return result;
585
465
  }
586
- function formatFirstZodError(err) {
587
- const issue = err.issues[0];
588
- const path9 = formatZodPath(issue.path);
589
- if (path9 === "targets" && (issue.code === "too_small" || issue.code === "invalid_type")) {
590
- return "Config must have at least one target";
466
+ function formatFirstAjvError(errors) {
467
+ const error = errors[0];
468
+ const path9 = formatAjvPath(error.instancePath);
469
+ if (error.keyword === "required") {
470
+ const missingParam = error.params.missingProperty;
471
+ if (missingParam === "targets") {
472
+ return "Config must have at least one target";
473
+ }
474
+ if (path9 === "" && missingParam === "targets") {
475
+ return "Config must have at least one target";
476
+ }
477
+ if (path9) {
478
+ return `${path9} must have required property '${missingParam}'`;
479
+ }
480
+ return `${missingParam} must be an object`;
591
481
  }
592
- if (issue.path.length >= 3 && issue.path[0] === "targets" && typeof issue.path[1] === "number" && issue.path[2] === "name" && issue.code === z.ZodIssueCode.too_small) {
593
- return `Target[${issue.path[1]}] must have a non-empty name`;
482
+ if (error.keyword === "additionalProperties") {
483
+ const additionalProp = error.params.additionalProperty;
484
+ const keyPath = path9 ? `${path9}.${additionalProp}` : additionalProp;
485
+ return `${keyPath} is not supported`;
594
486
  }
595
- switch (issue.code) {
596
- case z.ZodIssueCode.invalid_type: {
597
- if (issue.expected === "object") {
598
- return path9 ? `${path9} must be an object` : "Config must be an object";
599
- }
600
- if (issue.expected === "integer") {
601
- return `${path9} must be an integer`;
602
- }
603
- if (issue.expected === "string") {
604
- return `${path9} must be a string`;
605
- }
606
- if (issue.expected === "number") {
607
- return `${path9} must be a number`;
608
- }
609
- return issue.message;
487
+ if (error.keyword === "minLength") {
488
+ if (path9) {
489
+ return `${path9} must be a non-empty string`;
490
+ }
491
+ return "Config must be a non-empty string";
492
+ }
493
+ if (error.keyword === "const") {
494
+ const allowedValue = error.params.allowedValue;
495
+ return `${path9 || "Config"} must be ${JSON.stringify(allowedValue)}`;
496
+ }
497
+ if (error.keyword === "enum") {
498
+ const allowedValues = error.params.allowedValues;
499
+ return `${path9} must be one of: ${allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
500
+ }
501
+ if (error.keyword === "minimum" || error.keyword === "exclusiveMinimum") {
502
+ const limit = error.params.limit;
503
+ if (limit === 1) {
504
+ return `${path9} must be a positive number`;
505
+ }
506
+ return `${path9} must be at least ${limit}`;
507
+ }
508
+ if (error.keyword === "type") {
509
+ const expected = error.params.type;
510
+ if (path9 === "") {
511
+ return `Config must be an object`;
512
+ }
513
+ if (expected === "object") {
514
+ return `${path9} must be an object`;
515
+ }
516
+ if (expected === "integer") {
517
+ return `${path9} must be an integer`;
518
+ }
519
+ if (expected === "string") {
520
+ return `${path9} must be a string`;
521
+ }
522
+ if (expected === "number") {
523
+ return `${path9} must be a number`;
524
+ }
525
+ return `${path9} must be ${expected === "integer" ? "an integer" : `a ${expected}`}`;
526
+ }
527
+ if (error.keyword === "minItems") {
528
+ return `${path9} must contain at least one item`;
529
+ }
530
+ if (error.keyword === "pattern") {
531
+ return `${path9} has an invalid format`;
532
+ }
533
+ return `${path9 || "Config"} ${error.message || "is invalid"}`;
534
+ }
535
+ function applyOutputRetriesDefaults(retries) {
536
+ if (retries === void 0) return void 0;
537
+ return {
538
+ missingOrEmpty: retries.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES
539
+ };
540
+ }
541
+ function applyStageAgentDefaults(agent) {
542
+ return {
543
+ ...agent,
544
+ outputRetries: applyOutputRetriesDefaults(agent.outputRetries)
545
+ };
546
+ }
547
+ function assertRepoRelativePath(value, location) {
548
+ const normalized = normalize(value);
549
+ if (isAbsolute(value) || isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
550
+ throw new Error(
551
+ `${location} must stay within the repository and be repo-relative; got "${value}"`
552
+ );
553
+ }
554
+ }
555
+ function assertBaseBranch(value, location) {
556
+ if (value.includes("/")) {
557
+ throw new Error(
558
+ `${location} must be a local branch name, not a remote-qualified, tag, ref, or path-like name; got "${value}"`
559
+ );
560
+ }
561
+ if (/^[0-9a-f]{7,40}$/i.test(value)) {
562
+ throw new Error(
563
+ `${location} must be a branch name, not a commit SHA; got "${value}"`
564
+ );
565
+ }
566
+ }
567
+ function assertStageAgentPath(agent, location) {
568
+ if (!agent) return;
569
+ const promptTemplate = agent.promptTemplate;
570
+ if (typeof promptTemplate === "string") {
571
+ assertRepoRelativePath(promptTemplate, `${location}.promptTemplate`);
572
+ }
573
+ }
574
+ function validateConfigSemantics(data) {
575
+ const sandbox = data.sandbox;
576
+ const copyToWorktree = sandbox?.copyToWorktree;
577
+ copyToWorktree?.forEach((entry, index) => {
578
+ assertRepoRelativePath(entry, `sandbox.copyToWorktree[${index}]`);
579
+ });
580
+ const targetNames = /* @__PURE__ */ new Set();
581
+ data.targets.forEach((target, targetIndex) => {
582
+ const name = target.name;
583
+ if (targetNames.has(name)) {
584
+ throw new Error(
585
+ `Duplicate target name "${name}"; target names must be unique`
586
+ );
587
+ }
588
+ targetNames.add(name);
589
+ assertBaseBranch(
590
+ target.baseBranch,
591
+ `targets[${targetIndex}].baseBranch`
592
+ );
593
+ const strategy = target.strategy;
594
+ const implement = strategy.implement;
595
+ const review = strategy.review;
596
+ const finalize = strategy.finalize;
597
+ assertStageAgentPath(
598
+ implement.builder,
599
+ `targets[${targetIndex}].strategy.implement.builder`
600
+ );
601
+ assertStageAgentPath(
602
+ strategy.conflictResolution,
603
+ `targets[${targetIndex}].strategy.conflictResolution`
604
+ );
605
+ assertStageAgentPath(
606
+ strategy.failureResolution,
607
+ `targets[${targetIndex}].strategy.failureResolution`
608
+ );
609
+ assertStageAgentPath(
610
+ review.reviewer,
611
+ `targets[${targetIndex}].strategy.review.reviewer`
612
+ );
613
+ assertStageAgentPath(
614
+ review.refactor,
615
+ `targets[${targetIndex}].strategy.review.refactor`
616
+ );
617
+ assertStageAgentPath(
618
+ strategy.issueFinalReview,
619
+ `targets[${targetIndex}].strategy.issueFinalReview`
620
+ );
621
+ assertStageAgentPath(
622
+ finalize.prDescriptionAgent,
623
+ `targets[${targetIndex}].strategy.finalize.prDescriptionAgent`
624
+ );
625
+ });
626
+ }
627
+ function assertKnownKeys(value, path9, knownKeys) {
628
+ for (const key of Object.keys(value)) {
629
+ if (!knownKeys.includes(key)) {
630
+ throw new Error(`${path9}.${key} is not supported`);
610
631
  }
611
- case z.ZodIssueCode.too_small:
612
- if (issue.type === "string" && issue.minimum === 1) {
613
- return `${path9} must be a non-empty string`;
614
- }
615
- if (issue.type === "array" && issue.minimum === 1) {
616
- return `${path9} must not be empty`;
617
- }
618
- if (issue.type === "number") {
619
- return `${path9} must be a positive number`;
620
- }
621
- return issue.message;
622
- case z.ZodIssueCode.invalid_literal:
623
- return `${path9} must be ${issue.expected}`;
624
- case z.ZodIssueCode.unrecognized_keys:
625
- const keyPath = path9 ? `${path9}.${issue.keys[0]}` : issue.keys[0];
626
- return `${keyPath} is not supported`;
627
- case z.ZodIssueCode.custom:
628
- return path9 ? `${path9} ${issue.message}` : issue.message;
629
- default:
630
- return issue.message;
631
632
  }
632
633
  }
633
634
  function parseConfig(raw) {
@@ -649,6 +650,7 @@ function parseConfig(raw) {
649
650
  "name",
650
651
  "baseBranch",
651
652
  "branchTemplate",
653
+ "prdRun",
652
654
  "setupCommands",
653
655
  "autoMerge",
654
656
  "queue",
@@ -664,9 +666,20 @@ function parseConfig(raw) {
664
666
  `targets[${i}].strategy.conflictResolution has been removed; use targets[${i}].strategy.failureResolution`
665
667
  );
666
668
  }
667
- if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object" && "reconciliation" in strategy.prdRun) {
669
+ if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object") {
670
+ const prdRun = strategy.prdRun;
671
+ if ("reconciliation" in prdRun) {
672
+ throw new Error(
673
+ `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
674
+ );
675
+ }
676
+ if ("finalReview" in prdRun) {
677
+ throw new Error(
678
+ `targets[${i}].strategy.prdRun.finalReview has been removed; PRD-wide Final Review is no longer part of the PRD Run lifecycle. Use targets[${i}].strategy.issueFinalReview instead.`
679
+ );
680
+ }
668
681
  throw new Error(
669
- `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
682
+ `targets[${i}].strategy.prdRun is not supported in the new config; use targets[${i}].prdRun instead.`
670
683
  );
671
684
  }
672
685
  }
@@ -676,67 +689,126 @@ function parseConfig(raw) {
676
689
  "copyToWorktree",
677
690
  "mounts",
678
691
  "env",
679
- "idleTimeoutSeconds"
692
+ "idleTimeoutSeconds",
693
+ "forceRebuild"
680
694
  ]);
681
695
  }
682
- const result = PourkitConfigSchema.safeParse(raw);
683
- if (!result.success) {
684
- throw new Error(formatFirstZodError(result.error));
685
- }
686
- const data = result.data;
687
- const targets = data.targets.map((t) => {
688
- const setupCommands = t.setupCommands?.map((cmd, i) => ({
689
- command: cmd.command,
690
- label: cmd.label ?? `check-${i}`
691
- }));
692
- const verifyCommands = t.strategy.verify?.commands?.map((cmd, i) => ({
693
- command: cmd.command,
694
- label: cmd.label ?? `check-${i}`
695
- }));
696
- return {
697
- name: t.name,
698
- baseBranch: t.baseBranch,
699
- branchTemplate: t.branchTemplate,
700
- setupCommands,
701
- autoMerge: t.autoMerge,
702
- queue: t.queue,
703
- serena: t.serena,
704
- strategy: {
705
- type: "review-refactor-loop",
706
- implement: { builder: t.strategy.implement.builder },
707
- failureResolution: {
708
- agent: t.strategy.failureResolution.agent,
709
- model: t.strategy.failureResolution.model,
710
- promptTemplate: t.strategy.failureResolution.promptTemplate,
711
- outputRetries: t.strategy.failureResolution.outputRetries,
712
- maxAttemptsPerFailure: t.strategy.failureResolution.maxAttemptsPerFailure,
713
- failureLimits: t.strategy.failureResolution.failureLimits
714
- },
715
- review: {
716
- reviewer: t.strategy.review.reviewer,
717
- refactor: t.strategy.review.refactor,
718
- maxIterations: t.strategy.review.maxIterations,
719
- passWithNotesRefactorAttempts: t.strategy.review.passWithNotesRefactorAttempts
720
- },
721
- ...t.strategy.verify ? { verify: { commands: verifyCommands } } : {},
722
- issueFinalReview: t.strategy.issueFinalReview,
723
- finalize: {
724
- prDescriptionAgent: t.strategy.finalize.prDescriptionAgent,
725
- maxAttempts: t.strategy.finalize.maxAttempts
726
- },
727
- ...t.strategy.prdRun ? {
728
- prdRun: {
729
- ...t.strategy.prdRun.mode ? { mode: t.strategy.prdRun.mode } : {},
730
- finalReview: t.strategy.prdRun.finalReview
696
+ const validate = getValidator();
697
+ if (!validate(raw)) {
698
+ throw new Error(
699
+ formatFirstAjvError(
700
+ _ajvErrors ?? [
701
+ {
702
+ instancePath: "",
703
+ message: "validation failed",
704
+ keyword: "error",
705
+ params: {}
731
706
  }
732
- } : {}
733
- }
734
- };
735
- });
707
+ ]
708
+ )
709
+ );
710
+ }
711
+ const data = config;
712
+ validateConfigSemantics(data);
713
+ const targets = data.targets.map(
714
+ (t) => {
715
+ const input = t;
716
+ const strategy = input.strategy;
717
+ const implement = strategy.implement;
718
+ const failureResolution = strategy.failureResolution;
719
+ const review = strategy.review;
720
+ const reviewReviewer = review.reviewer;
721
+ const reviewRefactor = review.refactor;
722
+ const finalize = strategy.finalize;
723
+ const issueFinalReview = strategy.issueFinalReview;
724
+ const setupCommands = input.setupCommands?.map((cmd, i) => ({
725
+ command: cmd.command,
726
+ label: cmd.label ?? `check-${i}`
727
+ }));
728
+ const verifyCommands = strategy.verify?.commands ? strategy.verify.commands : void 0;
729
+ const verifyLabeled = verifyCommands?.map((cmd, i) => ({
730
+ command: cmd.command,
731
+ label: cmd.label ?? `check-${i}`
732
+ }));
733
+ return {
734
+ name: input.name,
735
+ baseBranch: input.baseBranch,
736
+ branchTemplate: input.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE,
737
+ prdRun: input.prdRun,
738
+ setupCommands,
739
+ autoMerge: input.autoMerge !== void 0 ? input.autoMerge : true,
740
+ queue: input.queue,
741
+ serena: input.serena,
742
+ strategy: {
743
+ type: "review-refactor-loop",
744
+ implement: {
745
+ builder: applyStageAgentDefaults(
746
+ implement.builder
747
+ )
748
+ },
749
+ failureResolution: {
750
+ agent: failureResolution.agent,
751
+ model: failureResolution.model,
752
+ variant: failureResolution.variant,
753
+ env: failureResolution.env,
754
+ promptTemplate: failureResolution.promptTemplate,
755
+ outputRetries: applyOutputRetriesDefaults(
756
+ failureResolution.outputRetries
757
+ ),
758
+ maxAttemptsPerFailure: failureResolution.maxAttemptsPerFailure,
759
+ failureLimits: failureResolution.failureLimits
760
+ },
761
+ review: {
762
+ reviewer: {
763
+ agent: reviewReviewer.agent,
764
+ model: reviewReviewer.model,
765
+ variant: reviewReviewer.variant,
766
+ env: reviewReviewer.env,
767
+ promptTemplate: reviewReviewer.promptTemplate,
768
+ outputRetries: applyOutputRetriesDefaults(
769
+ reviewReviewer.outputRetries
770
+ ),
771
+ criteria: reviewReviewer.criteria,
772
+ includeReviewHistory: reviewReviewer.includeReviewHistory,
773
+ passWithNotesRefactorAttempts: reviewReviewer.passWithNotesRefactorAttempts
774
+ },
775
+ refactor: applyStageAgentDefaults(
776
+ reviewRefactor
777
+ ),
778
+ maxIterations: review.maxIterations,
779
+ passWithNotesRefactorAttempts: review.passWithNotesRefactorAttempts ?? 2
780
+ },
781
+ ...verifyLabeled ? { verify: { commands: verifyLabeled } } : {},
782
+ issueFinalReview: {
783
+ ...issueFinalReview,
784
+ maxAttempts: issueFinalReview.maxAttempts,
785
+ outputRetries: applyOutputRetriesDefaults(
786
+ issueFinalReview.outputRetries
787
+ )
788
+ },
789
+ finalize: {
790
+ prDescriptionAgent: applyStageAgentDefaults(
791
+ finalize.prDescriptionAgent
792
+ ),
793
+ maxAttempts: finalize.maxAttempts
794
+ }
795
+ }
796
+ };
797
+ }
798
+ );
799
+ const serenaRaw = data.serena;
800
+ const serenaDefaults = {
801
+ mcpUrl: serenaRaw?.mcpUrl ?? "http://localhost:9121/mcp",
802
+ sandboxMcpUrl: serenaRaw?.sandboxMcpUrl ?? "http://localhost:9121/mcp",
803
+ dataDir: serenaRaw?.dataDir ?? ".pourkit/serena/"
804
+ };
736
805
  const serena = {
737
- ...data.serena,
738
- mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? data.serena.mcpUrl,
739
- sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? data.serena.sandboxMcpUrl
806
+ enabled: serenaRaw?.enabled ?? false,
807
+ required: serenaRaw?.required ?? false,
808
+ mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? serenaDefaults.mcpUrl,
809
+ sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? serenaDefaults.sandboxMcpUrl,
810
+ dataDir: serenaDefaults.dataDir,
811
+ autoStart: serenaRaw?.autoStart ?? false
740
812
  };
741
813
  if (serena.mcpUrl.trim() === "") {
742
814
  throw new Error("POURKIT_SERENA_MCP_URL must be a non-empty string");
@@ -746,84 +818,100 @@ function parseConfig(raw) {
746
818
  "POURKIT_SERENA_SANDBOX_MCP_URL must be a non-empty string"
747
819
  );
748
820
  }
821
+ const checksRaw = data.checks;
822
+ const labelsRaw = data.labels;
823
+ const cleanupRaw = data.cleanup;
824
+ const sandboxRaw = data.sandbox;
749
825
  return {
750
826
  targets,
751
- labels: data.labels,
827
+ labels: {
828
+ readyForAgent: labelsRaw?.readyForAgent ?? "ready-for-agent",
829
+ agentInProgress: labelsRaw?.agentInProgress ?? "agent-in-progress",
830
+ blocked: labelsRaw?.blocked ?? "blocked",
831
+ prOpenAwaitingMerge: labelsRaw?.prOpenAwaitingMerge ?? "pr-open-awaiting-merge",
832
+ readyForHuman: labelsRaw?.readyForHuman ?? "ready-for-human",
833
+ needsTriage: labelsRaw?.needsTriage ?? "needs-triage"
834
+ },
752
835
  sandbox: {
753
- provider: data.sandbox.provider,
754
- copyToWorktree: data.sandbox.copyToWorktree,
755
- mounts: data.sandbox.mounts,
756
- env: data.sandbox.env,
757
- idleTimeoutSeconds: data.sandbox.idleTimeoutSeconds
836
+ provider: sandboxRaw?.provider ?? "docker",
837
+ copyToWorktree: sandboxRaw?.copyToWorktree,
838
+ mounts: sandboxRaw?.mounts ? sandboxRaw.mounts.map((m) => ({
839
+ hostPath: m.hostPath,
840
+ sandboxPath: m.sandboxPath,
841
+ readonly: m.readonly ?? false
842
+ })) : void 0,
843
+ env: sandboxRaw?.env,
844
+ idleTimeoutSeconds: sandboxRaw?.idleTimeoutSeconds,
845
+ forceRebuild: sandboxRaw?.forceRebuild
758
846
  },
759
847
  checks: {
760
- requiredLabels: data.checks.requiredLabels,
761
- allowedAuthors: data.checks.allowedAuthors,
762
- checksFoundTimeoutSeconds: data.checks.checksFoundTimeoutSeconds ?? 60,
763
- checksCompletionTimeoutSeconds: data.checks.checksCompletionTimeoutSeconds ?? 30 * 60,
764
- pollIntervalSeconds: data.checks.pollIntervalSeconds ?? 15,
765
- issueListLimit: data.checks.issueListLimit ?? 50
848
+ requiredLabels: checksRaw?.requiredLabels ?? [],
849
+ allowedAuthors: checksRaw?.allowedAuthors ?? [],
850
+ checksFoundTimeoutSeconds: checksRaw?.checksFoundTimeoutSeconds ?? 60,
851
+ checksCompletionTimeoutSeconds: checksRaw?.checksCompletionTimeoutSeconds ?? 30 * 60,
852
+ pollIntervalSeconds: checksRaw?.pollIntervalSeconds ?? 15,
853
+ issueListLimit: checksRaw?.issueListLimit ?? 50
766
854
  },
767
855
  serena,
768
856
  cleanup: {
769
- enabled: data.cleanup?.enabled ?? true,
770
- worktreeRetentionDays: data.cleanup?.worktreeRetentionDays ?? 14,
771
- logRetentionDays: data.cleanup?.logRetentionDays ?? 30
857
+ enabled: cleanupRaw?.enabled ?? true,
858
+ worktreeRetentionDays: cleanupRaw?.worktreeRetentionDays ?? 14,
859
+ logRetentionDays: cleanupRaw?.logRetentionDays ?? 30
772
860
  }
773
861
  };
774
862
  }
775
- function assertKnownKeys(value, path9, knownKeys) {
776
- for (const key of Object.keys(value)) {
777
- if (!knownKeys.includes(key)) {
778
- throw new Error(`${path9}.${key} is not supported`);
779
- }
780
- }
781
- }
782
863
  function getVerificationCommands(target) {
783
864
  return target.strategy.verify?.commands ?? [];
784
865
  }
785
- async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
786
- const { existsSync: existsSync15 } = await import("fs");
787
- const { mkdir: mkdir6, writeFile: writeFile4, rm: rm3 } = await import("fs/promises");
788
- const { join: pjoin, basename } = await import("path");
789
- const { pathToFileURL: pathToFileURL2 } = await import("url");
790
- const { build } = await import("esbuild");
791
- const configPath = pjoin(repoRoot2, configFileName);
792
- if (!existsSync15(configPath)) {
866
+ var OBSOLETE_CONFIG_PATHS = [
867
+ "pourkit.config.ts",
868
+ "pourkit.config.mjs",
869
+ "pourkit.config.js",
870
+ "pourkit.json"
871
+ ];
872
+ var CANONICAL_CONFIG_PATH = ".pourkit/config.json";
873
+ async function loadRepoConfig(repoRoot2, _configFileName) {
874
+ const { existsSync: existsSync13 } = await import("fs");
875
+ const { join: pjoin } = await import("path");
876
+ for (const obPath of OBSOLETE_CONFIG_PATHS) {
877
+ const fullPath = pjoin(repoRoot2, obPath);
878
+ if (existsSync13(fullPath)) {
879
+ const isRootJson = obPath === "pourkit.json";
880
+ throw new Error(
881
+ isRootJson ? `Found root ${obPath}, but Pourkit config now lives at ${CANONICAL_CONFIG_PATH}. Move the file and update "$schema" to "./schema/pourkit.schema.json".` : `Found ${obPath}, but executable config is no longer supported. Move configuration to ${CANONICAL_CONFIG_PATH} with "$schema": "./schema/pourkit.schema.json".`
882
+ );
883
+ }
884
+ }
885
+ const configPath = pjoin(repoRoot2, CANONICAL_CONFIG_PATH);
886
+ if (!existsSync13(configPath)) {
793
887
  throw new Error(
794
- `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
888
+ `No Pourkit config found at ${CANONICAL_CONFIG_PATH}. Run pourkit init or create ${CANONICAL_CONFIG_PATH} with "$schema": "./schema/pourkit.schema.json".`
795
889
  );
796
890
  }
797
- const tmpDir = pjoin(repoRoot2, ".pourkit", ".tmp", "config");
798
- await mkdir6(tmpDir, { recursive: true });
799
- const tmpFile = pjoin(
800
- tmpDir,
801
- `pourkit-config-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
802
- );
891
+ const { readFile: readFile5 } = await import("fs/promises");
892
+ const raw = await readFile5(configPath, "utf-8");
893
+ let parsed;
803
894
  try {
804
- await build({
805
- entryPoints: [configPath],
806
- bundle: true,
807
- write: false,
808
- platform: "node",
809
- format: "esm",
810
- external: ["node:*"]
811
- }).then(async (result) => {
812
- const output = result.outputFiles[0].text;
813
- await writeFile4(tmpFile, output, "utf-8");
814
- });
815
- const imported = await import(pathToFileURL2(tmpFile).href);
816
- const raw = imported.default;
817
- if (raw === void 0) {
818
- throw new Error("pourkit.config.ts must have a default export");
819
- }
820
- return parseConfig(raw);
821
- } finally {
822
- try {
823
- await rm3(tmpFile, { force: true });
824
- } catch {
825
- }
895
+ parsed = JSON.parse(raw);
896
+ } catch (err) {
897
+ const message = err instanceof SyntaxError ? err.message : String(err);
898
+ throw new Error(`${CANONICAL_CONFIG_PATH}: Invalid JSON \u2014 ${message}`);
899
+ }
900
+ return parseConfig(parsed);
901
+ }
902
+ async function loadConfig(configPath) {
903
+ const { readFile: readFile5 } = await import("fs/promises");
904
+ const ext = configPath.split(".").pop()?.toLowerCase();
905
+ if (ext === "json") {
906
+ const raw = await readFile5(configPath, "utf-8");
907
+ return parseConfig(JSON.parse(raw));
826
908
  }
909
+ if (ext === "mjs" || ext === "js" || ext === "ts") {
910
+ throw new Error(
911
+ `Executable config (${ext}) is no longer supported. Use .pourkit/config.json with "$schema": "./schema/pourkit.schema.json".`
912
+ );
913
+ }
914
+ throw new Error(`Unsupported config format: ${ext}`);
827
915
  }
828
916
  function resolvePromptTemplatePath(repoRoot2, promptTemplate) {
829
917
  if (promptTemplate.includes("/")) {
@@ -872,8 +960,8 @@ function renderTemplate(template, issue) {
872
960
  init_common();
873
961
 
874
962
  // shared/run-context.ts
875
- import { existsSync, readFileSync, readdirSync } from "fs";
876
- import { isAbsolute, join as join2, relative, resolve } from "path";
963
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
964
+ import { isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
877
965
 
878
966
  // commands/run-verification.ts
879
967
  init_common();
@@ -956,13 +1044,6 @@ var STAGE_SECTIONS = {
956
1044
  "branch",
957
1045
  "verification-commands",
958
1046
  "artifacts"
959
- ],
960
- prdReconciliation: [
961
- "issue",
962
- "comments",
963
- "branch",
964
- "verification-commands",
965
- "artifacts"
966
1047
  ]
967
1048
  };
968
1049
  function buildRunContextArtifact(options) {
@@ -1014,11 +1095,17 @@ function buildRunContextMarkdown(options) {
1014
1095
  }
1015
1096
  }
1016
1097
  if (sections.includes("branch")) {
1098
+ const canonicalBaseRef = `origin/${target.baseBranch}`;
1017
1099
  parts.push(
1018
1100
  "## Branch",
1019
1101
  "",
1020
1102
  `- Base: ${target.baseBranch}`,
1103
+ `- Canonical Base Ref: ${canonicalBaseRef}`,
1021
1104
  `- Working Branch: ${branchName}`,
1105
+ "",
1106
+ "Use the canonical base ref for scope checks, commit ranges, and diffs. Do not use a bare PRD/base branch name such as `PRD-0063`; local branches with those names may be stale.",
1107
+ "",
1108
+ "Runner-owned Git operations: do not run destructive history/worktree commands such as `git reset --hard`, `git checkout .`, `git clean`, `git rebase`, `git merge`, or `git branch -f`. Edit files only; the runner owns branch/base movement.",
1022
1109
  ""
1023
1110
  );
1024
1111
  }
@@ -1081,7 +1168,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1081
1168
  `### Parent PRD Content: \`${relative(repoRoot2, parentPrdPath)}\``,
1082
1169
  "",
1083
1170
  "```markdown",
1084
- readFileSync(parentPrdPath, "utf-8").trimEnd(),
1171
+ readFileSync2(parentPrdPath, "utf-8").trimEnd(),
1085
1172
  "```",
1086
1173
  ""
1087
1174
  );
@@ -1108,7 +1195,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1108
1195
  `### Document Content: \`${documentPath}\``,
1109
1196
  "",
1110
1197
  "```markdown",
1111
- readFileSync(absolutePath, "utf-8").trimEnd(),
1198
+ readFileSync2(absolutePath, "utf-8").trimEnd(),
1112
1199
  "```",
1113
1200
  ""
1114
1201
  );
@@ -1133,34 +1220,27 @@ function extractRepoPaths(section) {
1133
1220
  return Array.from(paths);
1134
1221
  }
1135
1222
  function resolveRepoPath(repoRoot2, path9) {
1136
- if (isAbsolute(path9) || path9.includes("\0")) return null;
1137
- const resolved = resolve(repoRoot2, path9);
1223
+ if (isAbsolute2(path9) || path9.includes("\0")) return null;
1224
+ const resolved = resolve2(repoRoot2, path9);
1138
1225
  const repoRelative2 = relative(repoRoot2, resolved);
1139
- if (repoRelative2.startsWith("..") || isAbsolute(repoRelative2)) return null;
1226
+ if (repoRelative2.startsWith("..") || isAbsolute2(repoRelative2)) return null;
1140
1227
  return resolved;
1141
1228
  }
1142
1229
  function findParentPrdPath(repoRoot2, parentRef) {
1143
- const directPath = join2(
1144
- repoRoot2,
1145
- ".pourkit",
1146
- "architecture",
1147
- parentRef,
1148
- "PRD.md"
1149
- );
1150
- if (existsSync(directPath)) return directPath;
1151
- const architectureRoot = join2(repoRoot2, ".pourkit", "architecture");
1152
- if (!existsSync(architectureRoot)) return null;
1153
- return findPrdMirror(architectureRoot, parentRef);
1230
+ const plansRoot = join2(repoRoot2, ".pourkit", "plans");
1231
+ if (!existsSync(plansRoot)) return null;
1232
+ return findPrdInPlans(plansRoot, parentRef);
1154
1233
  }
1155
- function findPrdMirror(directory, parentRef) {
1234
+ function findPrdInPlans(directory, parentRef) {
1156
1235
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1157
1236
  const entryPath = join2(directory, entry.name);
1158
1237
  if (entry.isDirectory()) {
1159
- if (entry.name.startsWith(parentRef)) {
1238
+ const prdNumber = parentRef.match(/^PRD-(\d+)$/)?.[1];
1239
+ if (entry.name.startsWith(parentRef) || prdNumber && entry.name.startsWith(`${prdNumber}-`)) {
1160
1240
  const prdPath = join2(entryPath, "PRD.md");
1161
1241
  if (existsSync(prdPath)) return prdPath;
1162
1242
  }
1163
- const nested = findPrdMirror(entryPath, parentRef);
1243
+ const nested = findPrdInPlans(entryPath, parentRef);
1164
1244
  if (nested) return nested;
1165
1245
  }
1166
1246
  }
@@ -1180,17 +1260,19 @@ function renderCriteria(criteria) {
1180
1260
 
1181
1261
  // shared/prompt-guidance.ts
1182
1262
  var PROTECTED_WORK_RULE = "Do **not** revert, delete, or substantially strip already-landed protected sibling/base work unless the issue explicitly requires those files.";
1263
+ var RUNNER_OWNED_GIT_RULE = "Do **not** move Git history or reset the Worktree. Do not run `git reset --hard`, `git checkout .`, `git clean`, `git rebase`, `git merge`, `git branch -f`, or equivalent destructive history/worktree commands; branch/base changes are runner-owned.";
1183
1264
  function appendProtectedWorkGuidance(promptBody) {
1184
1265
  return `${promptBody}
1185
1266
 
1186
1267
  ## Hard Rule
1187
1268
 
1188
- - ${PROTECTED_WORK_RULE}`;
1269
+ - ${PROTECTED_WORK_RULE}
1270
+ - ${RUNNER_OWNED_GIT_RULE}`;
1189
1271
  }
1190
1272
 
1191
1273
  // shared/worktree-run-state.ts
1192
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1193
- import { dirname, join as join3 } from "path";
1274
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1275
+ import { dirname as dirname2, join as join3 } from "path";
1194
1276
  var WORKTREE_RUN_STATE_PATH = ".pourkit/state.json";
1195
1277
  function readWorktreeRunState(worktreePath) {
1196
1278
  const statePath = join3(worktreePath, WORKTREE_RUN_STATE_PATH);
@@ -1198,7 +1280,7 @@ function readWorktreeRunState(worktreePath) {
1198
1280
  return null;
1199
1281
  }
1200
1282
  try {
1201
- const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
1283
+ const raw = JSON.parse(readFileSync3(statePath, "utf-8"));
1202
1284
  if (isValidWorktreeRunState(raw)) {
1203
1285
  return raw;
1204
1286
  }
@@ -1219,7 +1301,7 @@ function isValidWorktreeRunState(raw) {
1219
1301
  }
1220
1302
  function writeWorktreeRunState(worktreePath, state) {
1221
1303
  const statePath = join3(worktreePath, WORKTREE_RUN_STATE_PATH);
1222
- mkdirSync2(dirname(statePath), { recursive: true });
1304
+ mkdirSync2(dirname2(statePath), { recursive: true });
1223
1305
  writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
1224
1306
  }
1225
1307
  function updateWorktreeRunState(worktreePath, update) {
@@ -1541,12 +1623,12 @@ function validateRecoveryDecision(artifact, allowedDecisions) {
1541
1623
  }
1542
1624
 
1543
1625
  // shared/attempt-log.ts
1544
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
1545
- import { dirname as dirname2, join as join4 } from "path";
1626
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
1627
+ import { dirname as dirname3, join as join4 } from "path";
1546
1628
  var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
1547
1629
  function writeAttemptLog(worktreePath, entry) {
1548
1630
  const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
1549
- mkdirSync3(dirname2(logPath), { recursive: true });
1631
+ mkdirSync3(dirname3(logPath), { recursive: true });
1550
1632
  appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
1551
1633
  }
1552
1634
  function readAttemptLog(worktreePath) {
@@ -1554,7 +1636,7 @@ function readAttemptLog(worktreePath) {
1554
1636
  if (!existsSync3(logPath)) {
1555
1637
  return [];
1556
1638
  }
1557
- const raw = readFileSync3(logPath, "utf-8");
1639
+ const raw = readFileSync4(logPath, "utf-8");
1558
1640
  const lines = raw.split("\n").filter((l) => l.length > 0);
1559
1641
  const entries = [];
1560
1642
  for (const line of lines) {
@@ -1673,7 +1755,7 @@ async function runBaseRefreshAttempt(options) {
1673
1755
  }
1674
1756
 
1675
1757
  // commands/conflict-resolution.ts
1676
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1758
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
1677
1759
  import { join as join8 } from "path";
1678
1760
  init_common();
1679
1761
 
@@ -1838,8 +1920,8 @@ function parseConflictResolutionArtifact(output) {
1838
1920
  // commands/artifact-validation.ts
1839
1921
  import { createHash } from "crypto";
1840
1922
  import { execSync } from "child_process";
1841
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
1842
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
1923
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
1924
+ import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
1843
1925
 
1844
1926
  // pr/review-verdict.ts
1845
1927
  var ReviewVerdictProtocolError = class extends Error {
@@ -1874,15 +1956,15 @@ function parseReviewVerdict(output) {
1874
1956
  import {
1875
1957
  existsSync as existsSync6,
1876
1958
  mkdirSync as mkdirSync6,
1877
- readFileSync as readFileSync6,
1959
+ readFileSync as readFileSync7,
1878
1960
  readdirSync as readdirSync2,
1879
1961
  writeFileSync as writeFileSync3
1880
1962
  } from "fs";
1881
1963
  import { join as join6 } from "path";
1882
1964
 
1883
1965
  // execution/agent-output-retry.ts
1884
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync } from "fs";
1885
- import { dirname as dirname3, join as join5 } from "path";
1966
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync } from "fs";
1967
+ import { dirname as dirname4, join as join5 } from "path";
1886
1968
  async function executeWithMissingOrEmptyArtifactRetry({
1887
1969
  executionProvider,
1888
1970
  executionOptions,
@@ -1942,20 +2024,20 @@ function readArtifactOutput(artifactPath) {
1942
2024
  if (!existsSync4(artifactPath)) {
1943
2025
  return { _tag: "missing", path: artifactPath };
1944
2026
  }
1945
- const output = readFileSync4(artifactPath, "utf-8");
2027
+ const output = readFileSync5(artifactPath, "utf-8");
1946
2028
  if (!output.trim()) {
1947
2029
  return { _tag: "empty", path: artifactPath };
1948
2030
  }
1949
2031
  return { _tag: "content", value: output, path: artifactPath };
1950
2032
  }
1951
2033
  function prepareArtifactPath(artifactPath) {
1952
- mkdirSync4(dirname3(artifactPath), { recursive: true });
2034
+ mkdirSync4(dirname4(artifactPath), { recursive: true });
1953
2035
  rmSync(artifactPath, { recursive: true, force: true });
1954
2036
  }
1955
2037
 
1956
2038
  // shared/effect-services.ts
1957
2039
  import { Context, Effect as Effect3, Layer } from "effect";
1958
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
2040
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
1959
2041
  var GitExecutionError = class extends Error {
1960
2042
  _tag = "GitExecutionError";
1961
2043
  message;
@@ -1971,7 +2053,7 @@ var FileSystemDefault = Layer.succeed(
1971
2053
  FileSystem,
1972
2054
  FileSystem.of({
1973
2055
  readFile: (path9) => Effect3.try({
1974
- try: () => readFileSync5(path9, "utf-8"),
2056
+ try: () => readFileSync6(path9, "utf-8"),
1975
2057
  catch: (error) => new Error(
1976
2058
  `Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
1977
2059
  )
@@ -2195,7 +2277,7 @@ function validateRefactorArtifact(artifactPath, findingIds) {
2195
2277
  `Refactor artifact missing at ${artifactPath}`
2196
2278
  );
2197
2279
  }
2198
- const content = readFileSync6(artifactPath, "utf-8");
2280
+ const content = readFileSync7(artifactPath, "utf-8");
2199
2281
  if (!content.trim()) {
2200
2282
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
2201
2283
  }
@@ -2520,12 +2602,29 @@ A prior review emitted \`NEEDS_HUMAN\` and stopped the agent loop. The issue has
2520
2602
  Before carrying forward old blockers, inspect newer issue comments and the current worktree. Treat prior Reviewer and Refactor Artifacts as historical context, not active findings unless they still apply.
2521
2603
 
2522
2604
  ` : "";
2523
- return `${renderedTemplate}
2605
+ return appendProtectedWorkGuidance(`${renderedTemplate}
2524
2606
 
2525
2607
  ## Shared Run Context
2526
2608
 
2527
2609
  Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
2528
2610
 
2611
+ ## Initial Verification Pass
2612
+
2613
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
2614
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
2615
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
2616
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
2617
+ - 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.
2618
+ - If no verification commands are configured, note that and proceed with normal review.
2619
+
2620
+ ## Scope Evidence Rules
2621
+
2622
+ - Use the Run Context's canonical base ref, for example \`origin/<base>\`, for scope diffs and commit ranges.
2623
+ - Do not use bare PRD/base branch names such as \`PRD-0063\`, \`main\`, or \`dev\` for scope decisions; local branches with those names may be stale.
2624
+ - Only call a file or commit out of scope when it is part of the working branch's delta from the canonical base ref.
2625
+ - Do not recommend reverting accepted sibling/base work. If a file is present or changed on the canonical base ref, it is not this Issue's scope overreach.
2626
+ - If scope evidence is ambiguous, use \`NEEDS_HUMAN\` or ask for a runner/base mismatch decision instead of telling Refactor to revert files.
2627
+
2529
2628
  ${hasCriteriaPlaceholder ? "" : `## Review Criteria
2530
2629
 
2531
2630
  ${criteriaBlock}
@@ -2542,7 +2641,7 @@ End the file with exactly one wrapped verdict token: <verdict>PASS</verdict>, <v
2542
2641
 
2543
2642
  Findings must include an ID column with values in the format R${iteration}.F{findingNumber} (e.g., R${iteration}.F1, R${iteration}.F2) and a Supersedes column referencing the finding ID being superseded (or a hyphen for new findings).
2544
2643
 
2545
- When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`;
2644
+ When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`);
2546
2645
  });
2547
2646
  }
2548
2647
  function renderReviewHistory(reviewHistory) {
@@ -2667,12 +2766,12 @@ function recoverReviewOutputFromLog(logPath) {
2667
2766
  if (!existsSync6(logPath)) {
2668
2767
  return null;
2669
2768
  }
2670
- const logContent = readFileSync6(logPath, "utf-8");
2769
+ const logContent = readFileSync7(logPath, "utf-8");
2671
2770
  return recoverReviewOutputFromString(logContent);
2672
2771
  }
2673
2772
  function readReviewArtifact(artifactPath, logPath) {
2674
2773
  if (existsSync6(artifactPath)) {
2675
- const output = readFileSync6(artifactPath, "utf-8");
2774
+ const output = readFileSync7(artifactPath, "utf-8");
2676
2775
  if (output.trim()) {
2677
2776
  return output;
2678
2777
  }
@@ -3163,194 +3262,6 @@ function inferConventionalType(commitSummaries) {
3163
3262
  return null;
3164
3263
  }
3165
3264
 
3166
- // prd-run/final-review-validation.ts
3167
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3168
- function parseFinalReviewArtifact(artifactPath) {
3169
- if (!existsSync7(artifactPath)) {
3170
- return {
3171
- ok: false,
3172
- reason: "Final Review artifact not found.",
3173
- diagnostics: [`Artifact path: ${artifactPath}`]
3174
- };
3175
- }
3176
- let content;
3177
- try {
3178
- content = readFileSync7(artifactPath, "utf-8");
3179
- } catch (error) {
3180
- return {
3181
- ok: false,
3182
- reason: "Final Review artifact could not be read.",
3183
- diagnostics: [
3184
- error instanceof Error ? error.message : String(error),
3185
- `Artifact path: ${artifactPath}`
3186
- ]
3187
- };
3188
- }
3189
- let parsed;
3190
- try {
3191
- parsed = JSON.parse(content);
3192
- } catch {
3193
- return {
3194
- ok: false,
3195
- reason: "Final Review artifact is not valid JSON.",
3196
- diagnostics: [`Artifact path: ${artifactPath}`]
3197
- };
3198
- }
3199
- const verdict = parsed.verdict;
3200
- if (!verdict || typeof verdict !== "string") {
3201
- return {
3202
- ok: false,
3203
- reason: "Final Review artifact is missing a verdict field.",
3204
- diagnostics: [`Artifact content preview: ${content.slice(0, 300)}`]
3205
- };
3206
- }
3207
- const allowedVerdicts = [
3208
- "pass_no_changes",
3209
- "pass_with_retouch",
3210
- "needs_human_review",
3211
- "blocked"
3212
- ];
3213
- if (!allowedVerdicts.includes(verdict)) {
3214
- return {
3215
- ok: false,
3216
- reason: `Final Review artifact has unsupported verdict "${verdict}".`,
3217
- diagnostics: [`Allowed verdicts: ${allowedVerdicts.join(", ")}`]
3218
- };
3219
- }
3220
- const summary = typeof parsed.summary === "string" ? parsed.summary : typeof parsed.reason === "string" ? parsed.reason : String(verdict);
3221
- const diagnostics = Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [];
3222
- const changedPaths = Array.isArray(parsed.changedPaths) ? parsed.changedPaths : void 0;
3223
- const isAutoSummary = !parsed.summary && !parsed.reason;
3224
- if (verdict === "pass_with_retouch" && (!changedPaths || changedPaths.length === 0) && isAutoSummary) {
3225
- return {
3226
- ok: false,
3227
- reason: 'Final Review artifact with "pass_with_retouch" verdict requires changed paths.',
3228
- diagnostics: [
3229
- "pass_with_retouch verdict must include a changedPaths array in the artifact or provide a summary/reason with enough context for git diff fallback.",
3230
- "Provide changedPaths or a descriptive summary in the artifact."
3231
- ]
3232
- };
3233
- }
3234
- const prdRef = typeof parsed.prdRef === "string" && parsed.prdRef.trim() ? parsed.prdRef.trim() : void 0;
3235
- const stage = typeof parsed.stage === "string" && parsed.stage.trim() ? parsed.stage.trim() : void 0;
3236
- const checkoutBase = typeof parsed.checkoutBase === "string" && parsed.checkoutBase.trim() ? parsed.checkoutBase.trim() : void 0;
3237
- const reviewBase = typeof parsed.reviewBase === "string" && parsed.reviewBase.trim() ? parsed.reviewBase.trim() : void 0;
3238
- return {
3239
- ok: true,
3240
- verdict,
3241
- summary,
3242
- diagnostics,
3243
- changedPaths,
3244
- prdRef,
3245
- stage,
3246
- checkoutBase,
3247
- reviewBase
3248
- };
3249
- }
3250
- function validateFinalReviewArtifactSemanticIds(artifact, context) {
3251
- const errors = [];
3252
- if (!artifact.prdRef) {
3253
- errors.push("Final Review artifact is missing prdRef.");
3254
- } else if (artifact.prdRef !== context.prdRef) {
3255
- errors.push(
3256
- `Final Review artifact prdRef "${artifact.prdRef}" does not match active PRD Run "${context.prdRef}".`
3257
- );
3258
- }
3259
- if (!artifact.stage) {
3260
- errors.push("Final Review artifact is missing stage field.");
3261
- } else if (artifact.stage !== "prdFinalReview") {
3262
- errors.push(
3263
- `Final Review artifact stage "${artifact.stage}" is not "prdFinalReview".`
3264
- );
3265
- }
3266
- if (!artifact.checkoutBase) {
3267
- errors.push("Final Review artifact is missing checkoutBase.");
3268
- } else if (artifact.checkoutBase !== context.prdBranch) {
3269
- errors.push(
3270
- `Final Review artifact checkoutBase "${artifact.checkoutBase}" does not match active PRD Branch "${context.prdBranch}".`
3271
- );
3272
- }
3273
- if (!artifact.reviewBase) {
3274
- errors.push("Final Review artifact is missing reviewBase.");
3275
- } else if (artifact.reviewBase !== context.mergeBase) {
3276
- errors.push(
3277
- `Final Review artifact reviewBase "${artifact.reviewBase}" does not match active merge base "${context.mergeBase}".`
3278
- );
3279
- }
3280
- if (errors.length > 0) {
3281
- return { ok: false, errors, blockedGate: "final-review" };
3282
- }
3283
- return { ok: true };
3284
- }
3285
- function validateFinalReviewRetouchScope(changedPaths) {
3286
- if (!changedPaths || changedPaths.length === 0) {
3287
- return {
3288
- ok: false,
3289
- reason: "Final Review retouch scope validation failed. No changed paths available. Provide changed paths or run Final Review with a summary that includes changed paths.",
3290
- diagnostics: ["Changed paths list is empty or undefined."],
3291
- offendingPaths: []
3292
- };
3293
- }
3294
- const offendingPaths = changedPaths.filter((p) => !isRetouchScopePath(p));
3295
- const allowedPaths = changedPaths.filter((p) => isRetouchScopePath(p));
3296
- if (allowedPaths.length === 0) {
3297
- return {
3298
- ok: false,
3299
- reason: "Final Review retouch scope validation failed. No implementation, test, or Changeset paths found for retouch.",
3300
- diagnostics: [
3301
- "All changed paths are prohibited for retouch.",
3302
- ...changedPaths.map((p) => ` - ${p}`)
3303
- ],
3304
- offendingPaths
3305
- };
3306
- }
3307
- if (offendingPaths.length > 0) {
3308
- return {
3309
- ok: false,
3310
- reason: "Final Review retouch scope validation failed. Prohibited paths cannot be included in retouch PR.",
3311
- diagnostics: [
3312
- "Prohibited paths found:",
3313
- ...offendingPaths.map((p) => ` - ${p}`)
3314
- ],
3315
- offendingPaths
3316
- };
3317
- }
3318
- return { ok: true, changedPaths };
3319
- }
3320
- function validateFinalReviewAgentOutputs(options) {
3321
- const artifact = parseFinalReviewArtifact(options.artifactPath);
3322
- if (!artifact.ok) {
3323
- return { ...artifact, offendingPaths: [] };
3324
- }
3325
- const semanticResult = validateFinalReviewArtifactSemanticIds(
3326
- artifact,
3327
- options.context
3328
- );
3329
- if (!semanticResult.ok) {
3330
- return {
3331
- ok: false,
3332
- reason: "Final Review artifact has mismatched semantic IDs.",
3333
- diagnostics: semanticResult.errors,
3334
- offendingPaths: []
3335
- };
3336
- }
3337
- if (artifact.verdict !== "pass_with_retouch") {
3338
- return { ok: true, artifact };
3339
- }
3340
- const changedPaths = options.changedPaths && options.changedPaths.length > 0 ? options.changedPaths : artifact.changedPaths;
3341
- const retouch = validateFinalReviewRetouchScope(changedPaths ?? []);
3342
- if (!retouch.ok) {
3343
- return retouch;
3344
- }
3345
- return { ok: true, artifact, retouch };
3346
- }
3347
- function isRetouchScopePath(path9) {
3348
- if (path9.startsWith(".pourkit/plans/") || path9 === ".pourkit/CONTEXT.md" || path9.startsWith(".pourkit/docs/") || /^\.pourkit\/prd-runs\/[^/]+\.json$/.test(path9)) {
3349
- return false;
3350
- }
3351
- return true;
3352
- }
3353
-
3354
3265
  // commands/artifact-validation.ts
3355
3266
  var CONFLICT_MARKER_PATTERN = /<<<<<<<|=======|>>>>>>>/m;
3356
3267
  function invalid(options, reason, diagnostics = []) {
@@ -3371,7 +3282,7 @@ function valid(options, diagnostics = []) {
3371
3282
  };
3372
3283
  }
3373
3284
  function readArtifact(options) {
3374
- if (!existsSync8(options.artifactPath)) {
3285
+ if (!existsSync7(options.artifactPath)) {
3375
3286
  return {
3376
3287
  ok: false,
3377
3288
  result: invalid(options, `Artifact missing at ${options.artifactPath}`)
@@ -3448,7 +3359,7 @@ function validateIssueFinalReviewArtifact(parsed, options) {
3448
3359
  for (const p of parsed.changedPaths) {
3449
3360
  const normalized = p.replace(/\\/g, "/");
3450
3361
  const segments = normalized.split("/");
3451
- if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute2(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3362
+ if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute3(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3452
3363
  return {
3453
3364
  ok: false,
3454
3365
  reason: `changedPaths must not contain absolute paths or path traversal: ${p}`,
@@ -3572,7 +3483,7 @@ function validateAgentArtifact(options) {
3572
3483
  if (options.checkConflictMarkers !== false && parsed.status === "resolved") {
3573
3484
  const base = options.worktreePath ?? process.cwd();
3574
3485
  const filesWithMarkers = parsed.files.filter((file) => {
3575
- const filePath = resolve2(base, file);
3486
+ const filePath = resolve3(base, file);
3576
3487
  try {
3577
3488
  return CONFLICT_MARKER_PATTERN.test(
3578
3489
  readFileSync8(filePath, "utf-8")
@@ -3591,29 +3502,6 @@ function validateAgentArtifact(options) {
3591
3502
  }
3592
3503
  return valid(options, [`status: ${parsed.status}`]);
3593
3504
  }
3594
- // QUARANTINED: Retained for Issue Final Review artifact validation via
3595
- // `runPrdRunValidateFinalReviewCommand`. Remove when Issue Final Review
3596
- // is migrated to its own dedicated validator kind.
3597
- case "final-review": {
3598
- if (!options.prdRef || !options.checkoutBase || !options.reviewBase) {
3599
- return invalid(
3600
- options,
3601
- "Final Review artifact validation requires --prd-ref, --checkout-base, and --review-base."
3602
- );
3603
- }
3604
- const result = validateFinalReviewAgentOutputs({
3605
- artifactPath: options.artifactPath,
3606
- context: {
3607
- prdRef: options.prdRef,
3608
- prdBranch: options.checkoutBase,
3609
- mergeBase: options.reviewBase
3610
- }
3611
- });
3612
- if (!result.ok) {
3613
- return invalid(options, result.reason, result.diagnostics);
3614
- }
3615
- return valid(options, [`verdict: ${result.artifact.verdict}`]);
3616
- }
3617
3505
  case "issue-final-review": {
3618
3506
  if (options.issueNumber === void 0 || !options.branchName) {
3619
3507
  return invalid(
@@ -3653,12 +3541,6 @@ function validateAgentArtifact(options) {
3653
3541
  }
3654
3542
  return valid(options, [`decision: ${parsed.json.recoveryDecision}`]);
3655
3543
  }
3656
- case "reconciliation":
3657
- case "planning-manifest":
3658
- return invalid(
3659
- options,
3660
- `Artifact kind "${options.kind}" has been removed from PRD Run validation`
3661
- );
3662
3544
  case "local-prd":
3663
3545
  case "local-issue":
3664
3546
  case "local-triage":
@@ -3944,7 +3826,7 @@ async function canReachMcp(url) {
3944
3826
  return true;
3945
3827
  } catch {
3946
3828
  if (attempt < 9) {
3947
- await new Promise((resolve3) => setTimeout(resolve3, 100));
3829
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
3948
3830
  }
3949
3831
  }
3950
3832
  }
@@ -4565,7 +4447,7 @@ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
4565
4447
 
4566
4448
  // prd-run/local-merge-coordinator.ts
4567
4449
  import { execFileSync as execFileSync2 } from "child_process";
4568
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
4450
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
4569
4451
  import { join as join12 } from "path";
4570
4452
 
4571
4453
  // prd-run/local-branches.ts
@@ -4590,7 +4472,7 @@ function getIssueArtifactPath(repoRoot2, prdId, issueId) {
4590
4472
  }
4591
4473
  function readIssueBranchName(repoRoot2, prdId, issueId) {
4592
4474
  const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
4593
- if (!existsSync10(issuePath)) return null;
4475
+ if (!existsSync9(issuePath)) return null;
4594
4476
  try {
4595
4477
  const content = readFileSync12(issuePath, "utf-8");
4596
4478
  const parsed = JSON.parse(content);
@@ -4602,7 +4484,7 @@ function readIssueBranchName(repoRoot2, prdId, issueId) {
4602
4484
  async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
4603
4485
  const root = repoRoot2 ?? process.cwd();
4604
4486
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
4605
- if (!existsSync10(receiptPath)) return null;
4487
+ if (!existsSync9(receiptPath)) return null;
4606
4488
  try {
4607
4489
  const content = readFileSync12(receiptPath, "utf-8");
4608
4490
  const parsed = JSON.parse(content);
@@ -4617,7 +4499,7 @@ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
4617
4499
  async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
4618
4500
  const root = repoRoot2 ?? process.cwd();
4619
4501
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
4620
- if (existsSync10(receiptPath)) {
4502
+ if (existsSync9(receiptPath)) {
4621
4503
  try {
4622
4504
  const existing = JSON.parse(
4623
4505
  readFileSync12(receiptPath, "utf-8")
@@ -4983,7 +4865,7 @@ function runMergeCoordinator(options) {
4983
4865
  }
4984
4866
 
4985
4867
  // commands/issue-final-review-agent.ts
4986
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
4868
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
4987
4869
  import { join as join13 } from "path";
4988
4870
  var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join13(
4989
4871
  ".pourkit",
@@ -5097,12 +4979,21 @@ async function runIssueFinalReviewAgent(options) {
5097
4979
  }
5098
4980
  function loadIssueFinalReviewPrompt(repoRoot2, promptTemplate) {
5099
4981
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
5100
- const promptBody = existsSync11(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
4982
+ const promptBody = existsSync10(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
5101
4983
  return appendProtectedWorkGuidance(`${promptBody}
5102
4984
 
5103
4985
  ## Shared Run Context
5104
4986
 
5105
- Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
4987
+ Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
4988
+
4989
+ ## Initial Verification Pass
4990
+
4991
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
4992
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
4993
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
4994
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
4995
+ - 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.
4996
+ - If no verification commands are configured, note that and proceed with normal review.`);
5106
4997
  }
5107
4998
 
5108
4999
  // issues/issue-transitions.ts
@@ -5305,7 +5196,7 @@ async function advanceIssueFinalReview(options) {
5305
5196
  "Issue Final Review state is incomplete: missing artifactPath"
5306
5197
  );
5307
5198
  }
5308
- const artifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join14(worktreePath, ifrFromState.artifactPath);
5199
+ const artifactPath = isAbsolute4(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join14(worktreePath, ifrFromState.artifactPath);
5309
5200
  const validation = validateAgentArtifact({
5310
5201
  kind: "issue-final-review",
5311
5202
  artifactPath,
@@ -5337,6 +5228,12 @@ async function advanceIssueFinalReview(options) {
5337
5228
  logger,
5338
5229
  reviewArtifactPath
5339
5230
  });
5231
+ await assertCanonicalBaseAncestor({
5232
+ worktreePath,
5233
+ baseRef: `origin/${target.baseBranch}`,
5234
+ stageName: "Issue Final Review",
5235
+ logger
5236
+ });
5340
5237
  if (result.verdict === "pass") {
5341
5238
  updateWorktreeRunState(worktreePath, {
5342
5239
  issueFinalReview: {
@@ -5561,6 +5458,14 @@ async function startIssueRun(options) {
5561
5458
  };
5562
5459
  }
5563
5460
  if (executionResult.worktreePath) {
5461
+ if (shouldRunBuilder) {
5462
+ await assertCanonicalBaseAncestor({
5463
+ worktreePath: executionResult.worktreePath,
5464
+ baseRef: `origin/${effectiveBaseBranch}`,
5465
+ stageName: "Builder",
5466
+ logger
5467
+ });
5468
+ }
5564
5469
  if (worktreeState) {
5565
5470
  updateWorktreeRunState(executionResult.worktreePath, {
5566
5471
  completedStages: { builder: true }
@@ -5622,6 +5527,12 @@ async function advanceIssueRunReview(options) {
5622
5527
  }
5623
5528
  })
5624
5529
  );
5530
+ await assertCanonicalBaseAncestor({
5531
+ worktreePath: options.worktreePath,
5532
+ baseRef: `origin/${options.target.baseBranch}`,
5533
+ stageName: "Review/Refactor",
5534
+ logger: options.logger
5535
+ });
5625
5536
  updateWorktreeRunState(options.worktreePath, {
5626
5537
  review: {
5627
5538
  lifetimeIterations: reviewResult.lifetimeIterations,
@@ -5685,7 +5596,7 @@ async function completeIssueRun(options) {
5685
5596
  prTitle = finalizerFromState.title;
5686
5597
  prBody = finalizerFromState.body;
5687
5598
  } else if (finalizerFromState.artifactPath) {
5688
- if (!existsSync12(finalizerFromState.artifactPath)) {
5599
+ if (!existsSync11(finalizerFromState.artifactPath)) {
5689
5600
  throw new FinalizerFailure({
5690
5601
  message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
5691
5602
  });
@@ -5748,6 +5659,14 @@ async function completeIssueRun(options) {
5748
5659
  }
5749
5660
  prTitle = finalizerResult.title;
5750
5661
  prBody = finalizerResult.body;
5662
+ if (executionResult.worktreePath) {
5663
+ await assertCanonicalBaseAncestor({
5664
+ worktreePath: executionResult.worktreePath,
5665
+ baseRef: `origin/${effectiveBaseBranch}`,
5666
+ stageName: "Finalizer",
5667
+ logger
5668
+ });
5669
+ }
5751
5670
  }
5752
5671
  prTitle = ensureConventionalPrTitle(
5753
5672
  prTitle,
@@ -6116,18 +6035,12 @@ function getRefactorArtifactDir(artifactPath) {
6116
6035
  }
6117
6036
  async function finalizeWorktreeCommit(options) {
6118
6037
  const { worktreePath, baseRef, title, body, logger } = options;
6119
- await syncRemoteBaseRef(worktreePath, baseRef, logger);
6120
- try {
6121
- await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
6122
- cwd: worktreePath,
6123
- logger,
6124
- label: "git merge-base --is-ancestor"
6125
- });
6126
- } catch {
6127
- throw new Error(
6128
- `Cannot finalize stale worktree: ${baseRef} is not an ancestor of HEAD. Refresh the branch onto the latest target base before creating the final commit.`
6129
- );
6130
- }
6038
+ await assertCanonicalBaseAncestor({
6039
+ worktreePath,
6040
+ baseRef,
6041
+ stageName: "final commit",
6042
+ logger
6043
+ });
6131
6044
  await execCapture("git", ["reset", "--soft", baseRef], {
6132
6045
  cwd: worktreePath,
6133
6046
  logger,
@@ -6144,6 +6057,21 @@ async function finalizeWorktreeCommit(options) {
6144
6057
  label: "git commit"
6145
6058
  });
6146
6059
  }
6060
+ async function assertCanonicalBaseAncestor(options) {
6061
+ const { worktreePath, baseRef, stageName, logger } = options;
6062
+ await syncRemoteBaseRef(worktreePath, baseRef, logger);
6063
+ try {
6064
+ await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
6065
+ cwd: worktreePath,
6066
+ logger,
6067
+ label: "git merge-base --is-ancestor"
6068
+ });
6069
+ } catch {
6070
+ throw new Error(
6071
+ `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.`
6072
+ );
6073
+ }
6074
+ }
6147
6075
  async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
6148
6076
  const remoteBase = parseRemoteBaseRef(baseRef);
6149
6077
  if (!remoteBase) {
@@ -6387,7 +6315,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
6387
6315
  }
6388
6316
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
6389
6317
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
6390
- const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
6318
+ const promptBody = existsSync11(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
6391
6319
  return appendProtectedWorkGuidance(`${promptBody}
6392
6320
 
6393
6321
  ## Shared Run Context
@@ -7384,14 +7312,14 @@ import path6 from "path";
7384
7312
 
7385
7313
  // execution/sandbox-image.ts
7386
7314
  import { createHash as createHash2 } from "crypto";
7387
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
7315
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
7388
7316
  import path5 from "path";
7389
7317
  function sandboxImageName(repoRoot2) {
7390
7318
  const dirName = path5.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
7391
7319
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
7392
7320
  const baseName = sanitized || "local";
7393
7321
  const dockerfilePath = path5.join(repoRoot2, ".sandcastle", "Dockerfile");
7394
- if (!existsSync13(dockerfilePath)) {
7322
+ if (!existsSync12(dockerfilePath)) {
7395
7323
  return `sandcastle:${baseName}`;
7396
7324
  }
7397
7325
  const fingerprint = createHash2("sha256").update(readFileSync15(dockerfilePath)).digest("hex").slice(0, 8);
@@ -7436,11 +7364,11 @@ init_common();
7436
7364
  import { mkdtempSync } from "fs";
7437
7365
  import { writeFile } from "fs/promises";
7438
7366
  import { tmpdir } from "os";
7439
- import { dirname as dirname4, join as join15 } from "path";
7367
+ import { dirname as dirname5, join as join15 } from "path";
7440
7368
  async function writeExecutionArtifacts(worktreePath, artifacts) {
7441
7369
  for (const artifact of artifacts) {
7442
7370
  const filePath = join15(worktreePath, artifact.path);
7443
- await ensureDir(dirname4(filePath));
7371
+ await ensureDir(dirname5(filePath));
7444
7372
  await writeFile(filePath, artifact.content, "utf-8");
7445
7373
  }
7446
7374
  }
@@ -8019,10 +7947,7 @@ function resolveE2EConfigFile(root) {
8019
7947
  if (explicitConfig) {
8020
7948
  return explicitConfig;
8021
7949
  }
8022
- if (existsSync14(path8.join(root, "pourkit.config.ts"))) {
8023
- return "pourkit.config.ts";
8024
- }
8025
- return "pourkit.config.example.ts";
7950
+ return path8.join(".pourkit", "config.json");
8026
7951
  }
8027
7952
  function generateRunId() {
8028
7953
  return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -8390,7 +8315,8 @@ async function runSuccessE2E(options, runId, root, logger, client) {
8390
8315
  if (!options.keep) {
8391
8316
  await persistResources(root, runId, resources);
8392
8317
  }
8393
- const baseConfig = await loadRepoConfig(root, resolveE2EConfigFile(root));
8318
+ const e2eConfigFile = resolveE2EConfigFile(root);
8319
+ const baseConfig = e2eConfigFile === path8.join(".pourkit", "config.json") ? await loadRepoConfig(root) : await loadConfig(path8.resolve(root, e2eConfigFile));
8394
8320
  const profile = resolveProfile(options.fullCheck);
8395
8321
  const config = makeE2EConfig(
8396
8322
  baseConfig,
@@ -8507,7 +8433,8 @@ async function runFailureE2E(options, runId, root, logger) {
8507
8433
  if (!options.keep) {
8508
8434
  await persistResources(root, runId, resources);
8509
8435
  }
8510
- const baseConfig = await loadRepoConfig(root, resolveE2EConfigFile(root));
8436
+ const e2eConfigFile = resolveE2EConfigFile(root);
8437
+ const baseConfig = e2eConfigFile === path8.join(".pourkit", "config.json") ? await loadRepoConfig(root) : await loadConfig(path8.resolve(root, e2eConfigFile));
8511
8438
  const profile = resolveProfile(options.fullCheck);
8512
8439
  const config = makeFailureE2EConfig(
8513
8440
  baseConfig,