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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -58,20 +58,20 @@ function createLogger(name, filePath) {
58
58
  );
59
59
  },
60
60
  async close() {
61
- await new Promise((resolve3) => {
61
+ await new Promise((resolve5) => {
62
62
  if (!fileStream) {
63
- resolve3();
63
+ resolve5();
64
64
  return;
65
65
  }
66
66
  const timer = setTimeout(() => {
67
67
  if (!fileStream.destroyed) {
68
68
  fileStream.destroy();
69
69
  }
70
- resolve3();
70
+ resolve5();
71
71
  }, 2e3);
72
72
  fileStream.end(() => {
73
73
  clearTimeout(timer);
74
- resolve3();
74
+ resolve5();
75
75
  });
76
76
  });
77
77
  }
@@ -265,7 +265,7 @@ async function execJson(command, args, options = {}) {
265
265
  return JSON.parse(result.stdout);
266
266
  }
267
267
  function sleep(ms) {
268
- return new Promise((resolve3) => setTimeout(resolve3, ms));
268
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
269
269
  }
270
270
  async function execCaptureWithRetry(command, args, options = {}) {
271
271
  const retries = options.retries ?? 3;
@@ -335,175 +335,37 @@ import { pathToFileURL } from "url";
335
335
  import { Command, Option, CommanderError } from "commander";
336
336
 
337
337
  // shared/config.ts
338
- import { join } from "path";
339
- import { z } from "zod";
340
- var NonEmptyString = z.string().trim().min(1);
338
+ import { readFileSync } from "fs";
339
+ import { fileURLToPath } from "url";
340
+ import { dirname, isAbsolute, join, normalize, sep, resolve } from "path";
341
+ import Ajv from "ajv";
342
+ var __filename = fileURLToPath(import.meta.url);
343
+ var __dirname = dirname(__filename);
344
+ var SCHEMA_PATH = resolve(__dirname, "../schema/pourkit.schema.json");
345
+ var _schema = null;
346
+ var _validate = null;
347
+ var _ajvErrors = null;
348
+ function getValidator() {
349
+ if (!_validate) {
350
+ const schema = _schema ?? JSON.parse(readFileSync(SCHEMA_PATH, "utf-8"));
351
+ _schema = schema;
352
+ const ajv = new Ajv({ strict: true });
353
+ ajv.addKeyword("x-pourkit-schema-version");
354
+ const validate = ajv.compile(schema);
355
+ _validate = (data) => {
356
+ _ajvErrors = null;
357
+ const valid2 = validate(data);
358
+ if (!valid2) _ajvErrors = validate.errors;
359
+ return valid2;
360
+ };
361
+ }
362
+ return _validate;
363
+ }
341
364
  var DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES = 3;
342
- var OutputRetriesConfigSchema = z.object({
343
- missingOrEmpty: z.number().int().nonnegative().default(DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES)
344
- }).strict();
365
+ var DEFAULT_BRANCH_TEMPLATE = "pourkit/{{issue.number}}/{{issue.slug}}";
345
366
  function resolveMissingOrEmptyOutputRetries(config) {
346
367
  return config?.outputRetries?.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES;
347
368
  }
348
- var StageAgentConfigSchema = z.object({
349
- agent: NonEmptyString,
350
- model: NonEmptyString,
351
- variant: NonEmptyString.optional(),
352
- env: z.record(z.string(), z.string()).optional(),
353
- promptTemplate: NonEmptyString,
354
- outputRetries: OutputRetriesConfigSchema.optional()
355
- }).strict();
356
- var ReviewerConfigSchema = z.object({
357
- agent: NonEmptyString,
358
- model: NonEmptyString,
359
- variant: NonEmptyString.optional(),
360
- env: z.record(z.string(), z.string()).optional(),
361
- promptTemplate: NonEmptyString,
362
- outputRetries: OutputRetriesConfigSchema.optional(),
363
- criteria: z.array(NonEmptyString),
364
- includeReviewHistory: z.boolean().optional(),
365
- passWithNotesRefactorAttempts: z.number().int().nonnegative().optional()
366
- }).strict();
367
- var VerificationCommandSchema = z.object({
368
- command: z.string().nullable().optional(),
369
- label: z.string().optional()
370
- }).strict().refine((d) => d.command && d.command.trim() !== "", {
371
- message: "must have a non-empty command"
372
- }).transform((d) => ({
373
- command: d.command,
374
- label: d.label && d.label.trim() !== "" ? d.label : void 0
375
- }));
376
- var PrdRunModeSchema = z.enum(["github", "local"]);
377
- var QueueConfigSchema = z.object({
378
- loop: z.boolean().optional()
379
- }).strict();
380
- var FailureResolutionConfigSchema = z.object({
381
- agent: NonEmptyString,
382
- model: NonEmptyString,
383
- variant: NonEmptyString.optional(),
384
- env: z.record(z.string(), z.string()).optional(),
385
- promptTemplate: NonEmptyString,
386
- outputRetries: OutputRetriesConfigSchema.optional(),
387
- maxAttemptsPerFailure: z.number().int().positive(),
388
- failureLimits: z.record(z.string(), z.number().int().positive()).optional()
389
- }).strict();
390
- var ReviewRefactorLoopStrategySchema = z.object({
391
- type: z.literal("review-refactor-loop"),
392
- implement: z.object({
393
- builder: StageAgentConfigSchema
394
- }).strict(),
395
- conflictResolution: z.object({
396
- agent: NonEmptyString,
397
- model: NonEmptyString,
398
- variant: NonEmptyString.optional(),
399
- env: z.record(z.string(), z.string()).optional(),
400
- promptTemplate: NonEmptyString,
401
- maxAttempts: z.number().int().positive()
402
- }).strict().optional(),
403
- failureResolution: FailureResolutionConfigSchema,
404
- review: z.object({
405
- reviewer: ReviewerConfigSchema,
406
- refactor: StageAgentConfigSchema,
407
- maxIterations: z.number().int().positive(),
408
- passWithNotesRefactorAttempts: z.number().int().nonnegative().default(2)
409
- }).strict(),
410
- verify: z.object({
411
- commands: z.preprocess(
412
- (v) => Array.isArray(v) ? v : [],
413
- z.array(VerificationCommandSchema).refine((arr) => arr.length > 0, {
414
- message: "must contain at least one command"
415
- })
416
- )
417
- }).strict().optional(),
418
- issueFinalReview: StageAgentConfigSchema.extend({
419
- maxAttempts: z.number().int().positive()
420
- }),
421
- finalize: z.object({
422
- prDescriptionAgent: StageAgentConfigSchema,
423
- maxAttempts: z.number().int().positive()
424
- }).strict(),
425
- prdRun: z.object({
426
- mode: PrdRunModeSchema.optional(),
427
- // Uses promptTemplate (canonical StageAgentConfig field), not prompt as Issue contract may suggest
428
- finalReview: StageAgentConfigSchema
429
- }).strict().optional()
430
- }).strict();
431
- var TargetSerenaConfigSchema = z.object({
432
- enabled: z.boolean().optional(),
433
- required: z.boolean().optional()
434
- }).strict();
435
- var TargetSchema = z.object({
436
- name: NonEmptyString,
437
- baseBranch: z.preprocess(
438
- (v) => typeof v === "string" && v.length > 0 ? v : void 0,
439
- NonEmptyString.default("main")
440
- ),
441
- branchTemplate: z.string().default("pourkit/{{issue.number}}/{{issue.slug}}"),
442
- setupCommands: z.preprocess(
443
- (v) => Array.isArray(v) ? v : void 0,
444
- z.array(VerificationCommandSchema).default([])
445
- ),
446
- autoMerge: z.preprocess(
447
- (v) => typeof v === "boolean" ? v : void 0,
448
- z.boolean().default(true)
449
- ),
450
- queue: QueueConfigSchema.optional(),
451
- serena: TargetSerenaConfigSchema.optional(),
452
- strategy: ReviewRefactorLoopStrategySchema
453
- }).strict();
454
- var LabelsSchema = z.object({
455
- readyForAgent: NonEmptyString,
456
- agentInProgress: NonEmptyString,
457
- blocked: NonEmptyString,
458
- prOpenAwaitingMerge: NonEmptyString,
459
- readyForHuman: NonEmptyString,
460
- needsTriage: NonEmptyString.optional().default("needs-triage")
461
- }).strict();
462
- var SandboxMountSchema = z.object({
463
- hostPath: NonEmptyString,
464
- sandboxPath: NonEmptyString,
465
- readonly: z.boolean().default(false)
466
- }).strict();
467
- var SandboxSchema = z.object({
468
- provider: NonEmptyString,
469
- copyToWorktree: z.array(NonEmptyString).optional(),
470
- mounts: z.array(SandboxMountSchema).optional(),
471
- env: z.record(z.string()).optional(),
472
- idleTimeoutSeconds: z.preprocess((v) => {
473
- if (v === void 0) return void 0;
474
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
475
- return v;
476
- }, z.number().int().positive().optional())
477
- }).strict();
478
- var ChecksSchema = z.object({
479
- requiredLabels: z.array(NonEmptyString),
480
- allowedAuthors: z.array(NonEmptyString),
481
- checksFoundTimeoutSeconds: z.number().int().positive().optional(),
482
- checksCompletionTimeoutSeconds: z.number().int().positive().optional(),
483
- pollIntervalSeconds: z.number().int().positive().optional(),
484
- issueListLimit: z.number().int().positive().optional()
485
- }).strict();
486
- var CleanupConfigSchema = z.object({
487
- enabled: z.boolean().default(true),
488
- worktreeRetentionDays: z.number().int().positive().default(14),
489
- logRetentionDays: z.number().int().positive().default(30)
490
- }).strict();
491
- var SerenaConfigSchema = z.object({
492
- enabled: z.boolean().default(false),
493
- required: z.boolean().default(false),
494
- mcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
495
- sandboxMcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
496
- dataDir: z.string().default(".pourkit/serena/"),
497
- autoStart: z.boolean().default(false)
498
- }).strict();
499
- var PourkitConfigSchema = z.object({
500
- targets: z.array(TargetSchema).min(1),
501
- labels: LabelsSchema,
502
- sandbox: SandboxSchema,
503
- checks: ChecksSchema,
504
- cleanup: CleanupConfigSchema.optional(),
505
- serena: SerenaConfigSchema.default({})
506
- }).strict();
507
369
  var removedFieldReplacements = {
508
370
  "config.implementor": "targets[].strategy.implement.builder",
509
371
  "config.reviewer": "targets[].strategy.review.reviewer",
@@ -567,11 +429,12 @@ function checkRemovedFields(raw) {
567
429
  }
568
430
  }
569
431
  }
570
- function formatZodPath(path9) {
571
- if (path9.length === 0) return "";
432
+ function formatAjvPath(instancePath) {
433
+ if (!instancePath || instancePath === "/") return "";
434
+ const parts = instancePath.split("/").slice(1);
572
435
  let result = "";
573
- for (const segment of path9) {
574
- if (typeof segment === "number") {
436
+ for (const segment of parts) {
437
+ if (/^\d+$/.test(segment)) {
575
438
  result += `[${segment}]`;
576
439
  } else {
577
440
  result += result ? `.${segment}` : segment;
@@ -579,51 +442,172 @@ function formatZodPath(path9) {
579
442
  }
580
443
  return result;
581
444
  }
582
- function formatFirstZodError(err) {
583
- const issue = err.issues[0];
584
- const path9 = formatZodPath(issue.path);
585
- if (path9 === "targets" && (issue.code === "too_small" || issue.code === "invalid_type")) {
586
- return "Config must have at least one target";
445
+ function formatFirstAjvError(errors) {
446
+ const error = errors[0];
447
+ const path9 = formatAjvPath(error.instancePath);
448
+ if (error.keyword === "required") {
449
+ const missingParam = error.params.missingProperty;
450
+ if (missingParam === "targets") {
451
+ return "Config must have at least one target";
452
+ }
453
+ if (path9 === "" && missingParam === "targets") {
454
+ return "Config must have at least one target";
455
+ }
456
+ if (path9) {
457
+ return `${path9} must have required property '${missingParam}'`;
458
+ }
459
+ return `${missingParam} must be an object`;
587
460
  }
588
- if (issue.path.length >= 3 && issue.path[0] === "targets" && typeof issue.path[1] === "number" && issue.path[2] === "name" && issue.code === z.ZodIssueCode.too_small) {
589
- return `Target[${issue.path[1]}] must have a non-empty name`;
461
+ if (error.keyword === "additionalProperties") {
462
+ const additionalProp = error.params.additionalProperty;
463
+ const keyPath = path9 ? `${path9}.${additionalProp}` : additionalProp;
464
+ return `${keyPath} is not supported`;
590
465
  }
591
- switch (issue.code) {
592
- case z.ZodIssueCode.invalid_type: {
593
- if (issue.expected === "object") {
594
- return path9 ? `${path9} must be an object` : "Config must be an object";
595
- }
596
- if (issue.expected === "integer") {
597
- return `${path9} must be an integer`;
598
- }
599
- if (issue.expected === "string") {
600
- return `${path9} must be a string`;
601
- }
602
- if (issue.expected === "number") {
603
- return `${path9} must be a number`;
604
- }
605
- return issue.message;
466
+ if (error.keyword === "minLength") {
467
+ if (path9) {
468
+ return `${path9} must be a non-empty string`;
469
+ }
470
+ return "Config must be a non-empty string";
471
+ }
472
+ if (error.keyword === "const") {
473
+ const allowedValue = error.params.allowedValue;
474
+ return `${path9 || "Config"} must be ${JSON.stringify(allowedValue)}`;
475
+ }
476
+ if (error.keyword === "enum") {
477
+ const allowedValues = error.params.allowedValues;
478
+ return `${path9} must be one of: ${allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
479
+ }
480
+ if (error.keyword === "minimum" || error.keyword === "exclusiveMinimum") {
481
+ const limit = error.params.limit;
482
+ if (limit === 1) {
483
+ return `${path9} must be a positive number`;
484
+ }
485
+ return `${path9} must be at least ${limit}`;
486
+ }
487
+ if (error.keyword === "type") {
488
+ const expected = error.params.type;
489
+ if (path9 === "") {
490
+ return `Config must be an object`;
491
+ }
492
+ if (expected === "object") {
493
+ return `${path9} must be an object`;
494
+ }
495
+ if (expected === "integer") {
496
+ return `${path9} must be an integer`;
497
+ }
498
+ if (expected === "string") {
499
+ return `${path9} must be a string`;
500
+ }
501
+ if (expected === "number") {
502
+ return `${path9} must be a number`;
503
+ }
504
+ return `${path9} must be ${expected === "integer" ? "an integer" : `a ${expected}`}`;
505
+ }
506
+ if (error.keyword === "minItems") {
507
+ return `${path9} must contain at least one item`;
508
+ }
509
+ if (error.keyword === "pattern") {
510
+ return `${path9} has an invalid format`;
511
+ }
512
+ return `${path9 || "Config"} ${error.message || "is invalid"}`;
513
+ }
514
+ function applyOutputRetriesDefaults(retries) {
515
+ if (retries === void 0) return void 0;
516
+ return {
517
+ missingOrEmpty: retries.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES
518
+ };
519
+ }
520
+ function applyStageAgentDefaults(agent) {
521
+ return {
522
+ ...agent,
523
+ outputRetries: applyOutputRetriesDefaults(agent.outputRetries)
524
+ };
525
+ }
526
+ function assertRepoRelativePath(value, location) {
527
+ const normalized = normalize(value);
528
+ if (isAbsolute(value) || isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
529
+ throw new Error(
530
+ `${location} must stay within the repository and be repo-relative; got "${value}"`
531
+ );
532
+ }
533
+ }
534
+ function assertBaseBranch(value, location) {
535
+ if (value.includes("/")) {
536
+ throw new Error(
537
+ `${location} must be a local branch name, not a remote-qualified, tag, ref, or path-like name; got "${value}"`
538
+ );
539
+ }
540
+ if (/^[0-9a-f]{7,40}$/i.test(value)) {
541
+ throw new Error(
542
+ `${location} must be a branch name, not a commit SHA; got "${value}"`
543
+ );
544
+ }
545
+ }
546
+ function assertStageAgentPath(agent, location) {
547
+ if (!agent) return;
548
+ const promptTemplate = agent.promptTemplate;
549
+ if (typeof promptTemplate === "string") {
550
+ assertRepoRelativePath(promptTemplate, `${location}.promptTemplate`);
551
+ }
552
+ }
553
+ function validateConfigSemantics(data) {
554
+ const sandbox = data.sandbox;
555
+ const copyToWorktree = sandbox?.copyToWorktree;
556
+ copyToWorktree?.forEach((entry, index) => {
557
+ assertRepoRelativePath(entry, `sandbox.copyToWorktree[${index}]`);
558
+ });
559
+ const targetNames = /* @__PURE__ */ new Set();
560
+ data.targets.forEach((target, targetIndex) => {
561
+ const name = target.name;
562
+ if (targetNames.has(name)) {
563
+ throw new Error(
564
+ `Duplicate target name "${name}"; target names must be unique`
565
+ );
566
+ }
567
+ targetNames.add(name);
568
+ assertBaseBranch(
569
+ target.baseBranch,
570
+ `targets[${targetIndex}].baseBranch`
571
+ );
572
+ const strategy = target.strategy;
573
+ const implement = strategy.implement;
574
+ const review = strategy.review;
575
+ const finalize = strategy.finalize;
576
+ assertStageAgentPath(
577
+ implement.builder,
578
+ `targets[${targetIndex}].strategy.implement.builder`
579
+ );
580
+ assertStageAgentPath(
581
+ strategy.conflictResolution,
582
+ `targets[${targetIndex}].strategy.conflictResolution`
583
+ );
584
+ assertStageAgentPath(
585
+ strategy.failureResolution,
586
+ `targets[${targetIndex}].strategy.failureResolution`
587
+ );
588
+ assertStageAgentPath(
589
+ review.reviewer,
590
+ `targets[${targetIndex}].strategy.review.reviewer`
591
+ );
592
+ assertStageAgentPath(
593
+ review.refactor,
594
+ `targets[${targetIndex}].strategy.review.refactor`
595
+ );
596
+ assertStageAgentPath(
597
+ strategy.issueFinalReview,
598
+ `targets[${targetIndex}].strategy.issueFinalReview`
599
+ );
600
+ assertStageAgentPath(
601
+ finalize.prDescriptionAgent,
602
+ `targets[${targetIndex}].strategy.finalize.prDescriptionAgent`
603
+ );
604
+ });
605
+ }
606
+ function assertKnownKeys(value, path9, knownKeys) {
607
+ for (const key of Object.keys(value)) {
608
+ if (!knownKeys.includes(key)) {
609
+ throw new Error(`${path9}.${key} is not supported`);
606
610
  }
607
- case z.ZodIssueCode.too_small:
608
- if (issue.type === "string" && issue.minimum === 1) {
609
- return `${path9} must be a non-empty string`;
610
- }
611
- if (issue.type === "array" && issue.minimum === 1) {
612
- return `${path9} must not be empty`;
613
- }
614
- if (issue.type === "number") {
615
- return `${path9} must be a positive number`;
616
- }
617
- return issue.message;
618
- case z.ZodIssueCode.invalid_literal:
619
- return `${path9} must be ${issue.expected}`;
620
- case z.ZodIssueCode.unrecognized_keys:
621
- const keyPath = path9 ? `${path9}.${issue.keys[0]}` : issue.keys[0];
622
- return `${keyPath} is not supported`;
623
- case z.ZodIssueCode.custom:
624
- return path9 ? `${path9} ${issue.message}` : issue.message;
625
- default:
626
- return issue.message;
627
611
  }
628
612
  }
629
613
  function parseConfig(raw) {
@@ -645,6 +629,7 @@ function parseConfig(raw) {
645
629
  "name",
646
630
  "baseBranch",
647
631
  "branchTemplate",
632
+ "prdRun",
648
633
  "setupCommands",
649
634
  "autoMerge",
650
635
  "queue",
@@ -660,9 +645,20 @@ function parseConfig(raw) {
660
645
  `targets[${i}].strategy.conflictResolution has been removed; use targets[${i}].strategy.failureResolution`
661
646
  );
662
647
  }
663
- if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object" && "reconciliation" in strategy.prdRun) {
648
+ if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object") {
649
+ const prdRun = strategy.prdRun;
650
+ if ("reconciliation" in prdRun) {
651
+ throw new Error(
652
+ `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
653
+ );
654
+ }
655
+ if ("finalReview" in prdRun) {
656
+ throw new Error(
657
+ `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.`
658
+ );
659
+ }
664
660
  throw new Error(
665
- `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
661
+ `targets[${i}].strategy.prdRun is not supported in the new config; use targets[${i}].prdRun instead.`
666
662
  );
667
663
  }
668
664
  }
@@ -672,67 +668,126 @@ function parseConfig(raw) {
672
668
  "copyToWorktree",
673
669
  "mounts",
674
670
  "env",
675
- "idleTimeoutSeconds"
671
+ "idleTimeoutSeconds",
672
+ "forceRebuild"
676
673
  ]);
677
674
  }
678
- const result = PourkitConfigSchema.safeParse(raw);
679
- if (!result.success) {
680
- throw new Error(formatFirstZodError(result.error));
681
- }
682
- const data = result.data;
683
- const targets = data.targets.map((t) => {
684
- const setupCommands = t.setupCommands?.map((cmd, i) => ({
685
- command: cmd.command,
686
- label: cmd.label ?? `check-${i}`
687
- }));
688
- const verifyCommands = t.strategy.verify?.commands?.map((cmd, i) => ({
689
- command: cmd.command,
690
- label: cmd.label ?? `check-${i}`
691
- }));
692
- return {
693
- name: t.name,
694
- baseBranch: t.baseBranch,
695
- branchTemplate: t.branchTemplate,
696
- setupCommands,
697
- autoMerge: t.autoMerge,
698
- queue: t.queue,
699
- serena: t.serena,
700
- strategy: {
701
- type: "review-refactor-loop",
702
- implement: { builder: t.strategy.implement.builder },
703
- failureResolution: {
704
- agent: t.strategy.failureResolution.agent,
705
- model: t.strategy.failureResolution.model,
706
- promptTemplate: t.strategy.failureResolution.promptTemplate,
707
- outputRetries: t.strategy.failureResolution.outputRetries,
708
- maxAttemptsPerFailure: t.strategy.failureResolution.maxAttemptsPerFailure,
709
- failureLimits: t.strategy.failureResolution.failureLimits
710
- },
711
- review: {
712
- reviewer: t.strategy.review.reviewer,
713
- refactor: t.strategy.review.refactor,
714
- maxIterations: t.strategy.review.maxIterations,
715
- passWithNotesRefactorAttempts: t.strategy.review.passWithNotesRefactorAttempts
716
- },
717
- ...t.strategy.verify ? { verify: { commands: verifyCommands } } : {},
718
- issueFinalReview: t.strategy.issueFinalReview,
719
- finalize: {
720
- prDescriptionAgent: t.strategy.finalize.prDescriptionAgent,
721
- maxAttempts: t.strategy.finalize.maxAttempts
722
- },
723
- ...t.strategy.prdRun ? {
724
- prdRun: {
725
- ...t.strategy.prdRun.mode ? { mode: t.strategy.prdRun.mode } : {},
726
- finalReview: t.strategy.prdRun.finalReview
675
+ const validate = getValidator();
676
+ if (!validate(raw)) {
677
+ throw new Error(
678
+ formatFirstAjvError(
679
+ _ajvErrors ?? [
680
+ {
681
+ instancePath: "",
682
+ message: "validation failed",
683
+ keyword: "error",
684
+ params: {}
727
685
  }
728
- } : {}
729
- }
730
- };
731
- });
686
+ ]
687
+ )
688
+ );
689
+ }
690
+ const data = config;
691
+ validateConfigSemantics(data);
692
+ const targets = data.targets.map(
693
+ (t) => {
694
+ const input = t;
695
+ const strategy = input.strategy;
696
+ const implement = strategy.implement;
697
+ const failureResolution = strategy.failureResolution;
698
+ const review = strategy.review;
699
+ const reviewReviewer = review.reviewer;
700
+ const reviewRefactor = review.refactor;
701
+ const finalize = strategy.finalize;
702
+ const issueFinalReview = strategy.issueFinalReview;
703
+ const setupCommands = input.setupCommands?.map((cmd, i) => ({
704
+ command: cmd.command,
705
+ label: cmd.label ?? `check-${i}`
706
+ }));
707
+ const verifyCommands = strategy.verify?.commands ? strategy.verify.commands : void 0;
708
+ const verifyLabeled = verifyCommands?.map((cmd, i) => ({
709
+ command: cmd.command,
710
+ label: cmd.label ?? `check-${i}`
711
+ }));
712
+ return {
713
+ name: input.name,
714
+ baseBranch: input.baseBranch,
715
+ branchTemplate: input.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE,
716
+ prdRun: input.prdRun,
717
+ setupCommands,
718
+ autoMerge: input.autoMerge !== void 0 ? input.autoMerge : true,
719
+ queue: input.queue,
720
+ serena: input.serena,
721
+ strategy: {
722
+ type: "review-refactor-loop",
723
+ implement: {
724
+ builder: applyStageAgentDefaults(
725
+ implement.builder
726
+ )
727
+ },
728
+ failureResolution: {
729
+ agent: failureResolution.agent,
730
+ model: failureResolution.model,
731
+ variant: failureResolution.variant,
732
+ env: failureResolution.env,
733
+ promptTemplate: failureResolution.promptTemplate,
734
+ outputRetries: applyOutputRetriesDefaults(
735
+ failureResolution.outputRetries
736
+ ),
737
+ maxAttemptsPerFailure: failureResolution.maxAttemptsPerFailure,
738
+ failureLimits: failureResolution.failureLimits
739
+ },
740
+ review: {
741
+ reviewer: {
742
+ agent: reviewReviewer.agent,
743
+ model: reviewReviewer.model,
744
+ variant: reviewReviewer.variant,
745
+ env: reviewReviewer.env,
746
+ promptTemplate: reviewReviewer.promptTemplate,
747
+ outputRetries: applyOutputRetriesDefaults(
748
+ reviewReviewer.outputRetries
749
+ ),
750
+ criteria: reviewReviewer.criteria,
751
+ includeReviewHistory: reviewReviewer.includeReviewHistory,
752
+ passWithNotesRefactorAttempts: reviewReviewer.passWithNotesRefactorAttempts
753
+ },
754
+ refactor: applyStageAgentDefaults(
755
+ reviewRefactor
756
+ ),
757
+ maxIterations: review.maxIterations,
758
+ passWithNotesRefactorAttempts: review.passWithNotesRefactorAttempts ?? 2
759
+ },
760
+ ...verifyLabeled ? { verify: { commands: verifyLabeled } } : {},
761
+ issueFinalReview: {
762
+ ...issueFinalReview,
763
+ maxAttempts: issueFinalReview.maxAttempts,
764
+ outputRetries: applyOutputRetriesDefaults(
765
+ issueFinalReview.outputRetries
766
+ )
767
+ },
768
+ finalize: {
769
+ prDescriptionAgent: applyStageAgentDefaults(
770
+ finalize.prDescriptionAgent
771
+ ),
772
+ maxAttempts: finalize.maxAttempts
773
+ }
774
+ }
775
+ };
776
+ }
777
+ );
778
+ const serenaRaw = data.serena;
779
+ const serenaDefaults = {
780
+ mcpUrl: serenaRaw?.mcpUrl ?? "http://localhost:9121/mcp",
781
+ sandboxMcpUrl: serenaRaw?.sandboxMcpUrl ?? "http://localhost:9121/mcp",
782
+ dataDir: serenaRaw?.dataDir ?? ".pourkit/serena/"
783
+ };
732
784
  const serena = {
733
- ...data.serena,
734
- mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? data.serena.mcpUrl,
735
- sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? data.serena.sandboxMcpUrl
785
+ enabled: serenaRaw?.enabled ?? false,
786
+ required: serenaRaw?.required ?? false,
787
+ mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? serenaDefaults.mcpUrl,
788
+ sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? serenaDefaults.sandboxMcpUrl,
789
+ dataDir: serenaDefaults.dataDir,
790
+ autoStart: serenaRaw?.autoStart ?? false
736
791
  };
737
792
  if (serena.mcpUrl.trim() === "") {
738
793
  throw new Error("POURKIT_SERENA_MCP_URL must be a non-empty string");
@@ -742,39 +797,48 @@ function parseConfig(raw) {
742
797
  "POURKIT_SERENA_SANDBOX_MCP_URL must be a non-empty string"
743
798
  );
744
799
  }
800
+ const checksRaw = data.checks;
801
+ const labelsRaw = data.labels;
802
+ const cleanupRaw = data.cleanup;
803
+ const sandboxRaw = data.sandbox;
745
804
  return {
746
805
  targets,
747
- labels: data.labels,
806
+ labels: {
807
+ readyForAgent: labelsRaw?.readyForAgent ?? "ready-for-agent",
808
+ agentInProgress: labelsRaw?.agentInProgress ?? "agent-in-progress",
809
+ blocked: labelsRaw?.blocked ?? "blocked",
810
+ prOpenAwaitingMerge: labelsRaw?.prOpenAwaitingMerge ?? "pr-open-awaiting-merge",
811
+ readyForHuman: labelsRaw?.readyForHuman ?? "ready-for-human",
812
+ needsTriage: labelsRaw?.needsTriage ?? "needs-triage"
813
+ },
748
814
  sandbox: {
749
- provider: data.sandbox.provider,
750
- copyToWorktree: data.sandbox.copyToWorktree,
751
- mounts: data.sandbox.mounts,
752
- env: data.sandbox.env,
753
- idleTimeoutSeconds: data.sandbox.idleTimeoutSeconds
815
+ provider: sandboxRaw?.provider ?? "docker",
816
+ copyToWorktree: sandboxRaw?.copyToWorktree,
817
+ mounts: sandboxRaw?.mounts ? sandboxRaw.mounts.map((m) => ({
818
+ hostPath: m.hostPath,
819
+ sandboxPath: m.sandboxPath,
820
+ readonly: m.readonly ?? false
821
+ })) : void 0,
822
+ env: sandboxRaw?.env,
823
+ idleTimeoutSeconds: sandboxRaw?.idleTimeoutSeconds,
824
+ forceRebuild: sandboxRaw?.forceRebuild
754
825
  },
755
826
  checks: {
756
- requiredLabels: data.checks.requiredLabels,
757
- allowedAuthors: data.checks.allowedAuthors,
758
- checksFoundTimeoutSeconds: data.checks.checksFoundTimeoutSeconds ?? 60,
759
- checksCompletionTimeoutSeconds: data.checks.checksCompletionTimeoutSeconds ?? 30 * 60,
760
- pollIntervalSeconds: data.checks.pollIntervalSeconds ?? 15,
761
- issueListLimit: data.checks.issueListLimit ?? 50
827
+ requiredLabels: checksRaw?.requiredLabels ?? [],
828
+ allowedAuthors: checksRaw?.allowedAuthors ?? [],
829
+ checksFoundTimeoutSeconds: checksRaw?.checksFoundTimeoutSeconds ?? 60,
830
+ checksCompletionTimeoutSeconds: checksRaw?.checksCompletionTimeoutSeconds ?? 30 * 60,
831
+ pollIntervalSeconds: checksRaw?.pollIntervalSeconds ?? 15,
832
+ issueListLimit: checksRaw?.issueListLimit ?? 50
762
833
  },
763
834
  serena,
764
835
  cleanup: {
765
- enabled: data.cleanup?.enabled ?? true,
766
- worktreeRetentionDays: data.cleanup?.worktreeRetentionDays ?? 14,
767
- logRetentionDays: data.cleanup?.logRetentionDays ?? 30
836
+ enabled: cleanupRaw?.enabled ?? true,
837
+ worktreeRetentionDays: cleanupRaw?.worktreeRetentionDays ?? 14,
838
+ logRetentionDays: cleanupRaw?.logRetentionDays ?? 30
768
839
  }
769
840
  };
770
841
  }
771
- function assertKnownKeys(value, path9, knownKeys) {
772
- for (const key of Object.keys(value)) {
773
- if (!knownKeys.includes(key)) {
774
- throw new Error(`${path9}.${key} is not supported`);
775
- }
776
- }
777
- }
778
842
  function getVerificationCommands(target) {
779
843
  return target.strategy.verify?.commands ?? [];
780
844
  }
@@ -782,7 +846,7 @@ function resolvePrdRunMode(target, opts) {
782
846
  if (opts?.localOverride === true) {
783
847
  return { mode: "local", source: "cli-override", targetName: target.name };
784
848
  }
785
- const configMode = target.strategy.prdRun?.mode;
849
+ const configMode = target.prdRun?.mode;
786
850
  if (configMode) {
787
851
  return {
788
852
  mode: configMode,
@@ -792,48 +856,41 @@ function resolvePrdRunMode(target, opts) {
792
856
  }
793
857
  return { mode: "github", source: "default", targetName: target.name };
794
858
  }
795
- async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
796
- const { existsSync: existsSync19 } = await import("fs");
797
- const { mkdir: mkdir6, writeFile: writeFile4, rm } = await import("fs/promises");
798
- const { join: pjoin, basename } = await import("path");
799
- const { pathToFileURL: pathToFileURL2 } = await import("url");
800
- const { build } = await import("esbuild");
801
- const configPath = pjoin(repoRoot2, configFileName);
802
- if (!existsSync19(configPath)) {
859
+ var OBSOLETE_CONFIG_PATHS = [
860
+ "pourkit.config.ts",
861
+ "pourkit.config.mjs",
862
+ "pourkit.config.js",
863
+ "pourkit.json"
864
+ ];
865
+ var CANONICAL_CONFIG_PATH = ".pourkit/config.json";
866
+ async function loadRepoConfig(repoRoot2, _configFileName) {
867
+ const { existsSync: existsSync20 } = await import("fs");
868
+ const { join: pjoin } = await import("path");
869
+ for (const obPath of OBSOLETE_CONFIG_PATHS) {
870
+ const fullPath = pjoin(repoRoot2, obPath);
871
+ if (existsSync20(fullPath)) {
872
+ const isRootJson = obPath === "pourkit.json";
873
+ throw new Error(
874
+ 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".`
875
+ );
876
+ }
877
+ }
878
+ const configPath = pjoin(repoRoot2, CANONICAL_CONFIG_PATH);
879
+ if (!existsSync20(configPath)) {
803
880
  throw new Error(
804
- `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
881
+ `No Pourkit config found at ${CANONICAL_CONFIG_PATH}. Run pourkit init or create ${CANONICAL_CONFIG_PATH} with "$schema": "./schema/pourkit.schema.json".`
805
882
  );
806
883
  }
807
- const tmpDir = pjoin(repoRoot2, ".pourkit", ".tmp", "config");
808
- await mkdir6(tmpDir, { recursive: true });
809
- const tmpFile = pjoin(
810
- tmpDir,
811
- `pourkit-config-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
812
- );
884
+ const { readFile: readFile7 } = await import("fs/promises");
885
+ const raw = await readFile7(configPath, "utf-8");
886
+ let parsed;
813
887
  try {
814
- await build({
815
- entryPoints: [configPath],
816
- bundle: true,
817
- write: false,
818
- platform: "node",
819
- format: "esm",
820
- external: ["node:*"]
821
- }).then(async (result) => {
822
- const output = result.outputFiles[0].text;
823
- await writeFile4(tmpFile, output, "utf-8");
824
- });
825
- const imported = await import(pathToFileURL2(tmpFile).href);
826
- const raw = imported.default;
827
- if (raw === void 0) {
828
- throw new Error("pourkit.config.ts must have a default export");
829
- }
830
- return parseConfig(raw);
831
- } finally {
832
- try {
833
- await rm(tmpFile, { force: true });
834
- } catch {
835
- }
888
+ parsed = JSON.parse(raw);
889
+ } catch (err) {
890
+ const message = err instanceof SyntaxError ? err.message : String(err);
891
+ throw new Error(`${CANONICAL_CONFIG_PATH}: Invalid JSON \u2014 ${message}`);
836
892
  }
893
+ return parseConfig(parsed);
837
894
  }
838
895
  function resolvePromptTemplatePath(repoRoot2, promptTemplate) {
839
896
  if (promptTemplate.includes("/")) {
@@ -872,8 +929,8 @@ function resolveTarget(config, explicitTarget) {
872
929
  init_common();
873
930
 
874
931
  // shared/worktree-run-state.ts
875
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
876
- import { dirname, join as join2 } from "path";
932
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
933
+ import { dirname as dirname2, join as join2 } from "path";
877
934
  var WORKTREE_RUN_STATE_PATH = ".pourkit/state.json";
878
935
  function readWorktreeRunState(worktreePath) {
879
936
  const statePath = join2(worktreePath, WORKTREE_RUN_STATE_PATH);
@@ -881,7 +938,7 @@ function readWorktreeRunState(worktreePath) {
881
938
  return null;
882
939
  }
883
940
  try {
884
- const raw = JSON.parse(readFileSync(statePath, "utf-8"));
941
+ const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
885
942
  if (isValidWorktreeRunState(raw)) {
886
943
  return raw;
887
944
  }
@@ -902,7 +959,7 @@ function isValidWorktreeRunState(raw) {
902
959
  }
903
960
  function writeWorktreeRunState(worktreePath, state) {
904
961
  const statePath = join2(worktreePath, WORKTREE_RUN_STATE_PATH);
905
- mkdirSync2(dirname(statePath), { recursive: true });
962
+ mkdirSync2(dirname2(statePath), { recursive: true });
906
963
  writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
907
964
  }
908
965
  function updateWorktreeRunState(worktreePath, update) {
@@ -1049,8 +1106,8 @@ async function cleanupRepository(options) {
1049
1106
  // commands/artifact-validation.ts
1050
1107
  import { createHash } from "crypto";
1051
1108
  import { execSync } from "child_process";
1052
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
1053
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
1109
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
1110
+ import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
1054
1111
 
1055
1112
  // pr/review-verdict.ts
1056
1113
  var ReviewVerdictProtocolError = class extends Error {
@@ -1085,15 +1142,15 @@ function parseReviewVerdict(output) {
1085
1142
  import {
1086
1143
  existsSync as existsSync5,
1087
1144
  mkdirSync as mkdirSync5,
1088
- readFileSync as readFileSync5,
1145
+ readFileSync as readFileSync6,
1089
1146
  readdirSync as readdirSync2,
1090
1147
  writeFileSync as writeFileSync3
1091
1148
  } from "fs";
1092
1149
  import { join as join6 } from "path";
1093
1150
 
1094
1151
  // execution/agent-output-retry.ts
1095
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync } from "fs";
1096
- import { dirname as dirname2, join as join4 } from "path";
1152
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync } from "fs";
1153
+ import { dirname as dirname3, join as join4 } from "path";
1097
1154
  async function executeWithMissingOrEmptyArtifactRetry({
1098
1155
  executionProvider,
1099
1156
  executionOptions,
@@ -1153,20 +1210,20 @@ function readArtifactOutput(artifactPath) {
1153
1210
  if (!existsSync2(artifactPath)) {
1154
1211
  return { _tag: "missing", path: artifactPath };
1155
1212
  }
1156
- const output = readFileSync2(artifactPath, "utf-8");
1213
+ const output = readFileSync3(artifactPath, "utf-8");
1157
1214
  if (!output.trim()) {
1158
1215
  return { _tag: "empty", path: artifactPath };
1159
1216
  }
1160
1217
  return { _tag: "content", value: output, path: artifactPath };
1161
1218
  }
1162
1219
  function prepareArtifactPath(artifactPath) {
1163
- mkdirSync3(dirname2(artifactPath), { recursive: true });
1220
+ mkdirSync3(dirname3(artifactPath), { recursive: true });
1164
1221
  rmSync(artifactPath, { recursive: true, force: true });
1165
1222
  }
1166
1223
 
1167
1224
  // shared/run-context.ts
1168
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
1169
- import { isAbsolute, join as join5, relative, resolve } from "path";
1225
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "fs";
1226
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
1170
1227
 
1171
1228
  // commands/run-verification.ts
1172
1229
  init_common();
@@ -1372,11 +1429,17 @@ function buildRunContextMarkdown(options) {
1372
1429
  }
1373
1430
  }
1374
1431
  if (sections.includes("branch")) {
1432
+ const canonicalBaseRef = `origin/${target.baseBranch}`;
1375
1433
  parts.push(
1376
1434
  "## Branch",
1377
1435
  "",
1378
1436
  `- Base: ${target.baseBranch}`,
1437
+ `- Canonical Base Ref: ${canonicalBaseRef}`,
1379
1438
  `- Working Branch: ${branchName}`,
1439
+ "",
1440
+ "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.",
1441
+ "",
1442
+ "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.",
1380
1443
  ""
1381
1444
  );
1382
1445
  }
@@ -1439,7 +1502,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1439
1502
  `### Parent PRD Content: \`${relative(repoRoot2, parentPrdPath)}\``,
1440
1503
  "",
1441
1504
  "```markdown",
1442
- readFileSync3(parentPrdPath, "utf-8").trimEnd(),
1505
+ readFileSync4(parentPrdPath, "utf-8").trimEnd(),
1443
1506
  "```",
1444
1507
  ""
1445
1508
  );
@@ -1466,7 +1529,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1466
1529
  `### Document Content: \`${documentPath}\``,
1467
1530
  "",
1468
1531
  "```markdown",
1469
- readFileSync3(absolutePath, "utf-8").trimEnd(),
1532
+ readFileSync4(absolutePath, "utf-8").trimEnd(),
1470
1533
  "```",
1471
1534
  ""
1472
1535
  );
@@ -1491,10 +1554,10 @@ function extractRepoPaths(section) {
1491
1554
  return Array.from(paths);
1492
1555
  }
1493
1556
  function resolveRepoPath(repoRoot2, path9) {
1494
- if (isAbsolute(path9) || path9.includes("\0")) return null;
1495
- const resolved = resolve(repoRoot2, path9);
1557
+ if (isAbsolute2(path9) || path9.includes("\0")) return null;
1558
+ const resolved = resolve2(repoRoot2, path9);
1496
1559
  const repoRelative2 = relative(repoRoot2, resolved);
1497
- if (repoRelative2.startsWith("..") || isAbsolute(repoRelative2)) return null;
1560
+ if (repoRelative2.startsWith("..") || isAbsolute2(repoRelative2)) return null;
1498
1561
  return resolved;
1499
1562
  }
1500
1563
  function findParentPrdPath(repoRoot2, parentRef) {
@@ -1538,17 +1601,19 @@ function renderCriteria(criteria) {
1538
1601
 
1539
1602
  // shared/prompt-guidance.ts
1540
1603
  var PROTECTED_WORK_RULE = "Do **not** revert, delete, or substantially strip already-landed protected sibling/base work unless the issue explicitly requires those files.";
1604
+ 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.";
1541
1605
  function appendProtectedWorkGuidance(promptBody) {
1542
1606
  return `${promptBody}
1543
1607
 
1544
1608
  ## Hard Rule
1545
1609
 
1546
- - ${PROTECTED_WORK_RULE}`;
1610
+ - ${PROTECTED_WORK_RULE}
1611
+ - ${RUNNER_OWNED_GIT_RULE}`;
1547
1612
  }
1548
1613
 
1549
1614
  // shared/effect-services.ts
1550
1615
  import { Context, Effect, Layer } from "effect";
1551
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
1616
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
1552
1617
  var GitExecutionError = class extends Error {
1553
1618
  _tag = "GitExecutionError";
1554
1619
  message;
@@ -1564,7 +1629,7 @@ var FileSystemDefault = Layer.succeed(
1564
1629
  FileSystem,
1565
1630
  FileSystem.of({
1566
1631
  readFile: (path9) => Effect.try({
1567
- try: () => readFileSync4(path9, "utf-8"),
1632
+ try: () => readFileSync5(path9, "utf-8"),
1568
1633
  catch: (error) => new Error(
1569
1634
  `Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
1570
1635
  )
@@ -1965,7 +2030,7 @@ function validateRefactorArtifact(artifactPath, findingIds) {
1965
2030
  `Refactor artifact missing at ${artifactPath}`
1966
2031
  );
1967
2032
  }
1968
- const content = readFileSync5(artifactPath, "utf-8");
2033
+ const content = readFileSync6(artifactPath, "utf-8");
1969
2034
  if (!content.trim()) {
1970
2035
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
1971
2036
  }
@@ -2290,12 +2355,29 @@ A prior review emitted \`NEEDS_HUMAN\` and stopped the agent loop. The issue has
2290
2355
  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.
2291
2356
 
2292
2357
  ` : "";
2293
- return `${renderedTemplate}
2358
+ return appendProtectedWorkGuidance(`${renderedTemplate}
2294
2359
 
2295
2360
  ## Shared Run Context
2296
2361
 
2297
2362
  Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
2298
2363
 
2364
+ ## Initial Verification Pass
2365
+
2366
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
2367
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
2368
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
2369
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
2370
+ - 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.
2371
+ - If no verification commands are configured, note that and proceed with normal review.
2372
+
2373
+ ## Scope Evidence Rules
2374
+
2375
+ - Use the Run Context's canonical base ref, for example \`origin/<base>\`, for scope diffs and commit ranges.
2376
+ - 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.
2377
+ - Only call a file or commit out of scope when it is part of the working branch's delta from the canonical base ref.
2378
+ - 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.
2379
+ - If scope evidence is ambiguous, use \`NEEDS_HUMAN\` or ask for a runner/base mismatch decision instead of telling Refactor to revert files.
2380
+
2299
2381
  ${hasCriteriaPlaceholder ? "" : `## Review Criteria
2300
2382
 
2301
2383
  ${criteriaBlock}
@@ -2312,7 +2394,7 @@ End the file with exactly one wrapped verdict token: <verdict>PASS</verdict>, <v
2312
2394
 
2313
2395
  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).
2314
2396
 
2315
- When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`;
2397
+ When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`);
2316
2398
  });
2317
2399
  }
2318
2400
  function renderReviewHistory(reviewHistory) {
@@ -2437,12 +2519,12 @@ function recoverReviewOutputFromLog(logPath) {
2437
2519
  if (!existsSync5(logPath)) {
2438
2520
  return null;
2439
2521
  }
2440
- const logContent = readFileSync5(logPath, "utf-8");
2522
+ const logContent = readFileSync6(logPath, "utf-8");
2441
2523
  return recoverReviewOutputFromString(logContent);
2442
2524
  }
2443
2525
  function readReviewArtifact(artifactPath, logPath) {
2444
2526
  if (existsSync5(artifactPath)) {
2445
- const output = readFileSync5(artifactPath, "utf-8");
2527
+ const output = readFileSync6(artifactPath, "utf-8");
2446
2528
  if (output.trim()) {
2447
2529
  return output;
2448
2530
  }
@@ -3092,7 +3174,7 @@ function parseConflictResolutionArtifact(output) {
3092
3174
  }
3093
3175
 
3094
3176
  // prd-run/final-review-validation.ts
3095
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3177
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
3096
3178
  function parseFinalReviewArtifact(artifactPath) {
3097
3179
  if (!existsSync6(artifactPath)) {
3098
3180
  return {
@@ -3103,7 +3185,7 @@ function parseFinalReviewArtifact(artifactPath) {
3103
3185
  }
3104
3186
  let content;
3105
3187
  try {
3106
- content = readFileSync6(artifactPath, "utf-8");
3188
+ content = readFileSync7(artifactPath, "utf-8");
3107
3189
  } catch (error) {
3108
3190
  return {
3109
3191
  ok: false,
@@ -3306,7 +3388,7 @@ function readArtifact(options) {
3306
3388
  };
3307
3389
  }
3308
3390
  try {
3309
- const content = readFileSync7(options.artifactPath, "utf-8");
3391
+ const content = readFileSync8(options.artifactPath, "utf-8");
3310
3392
  if (!content.trim()) {
3311
3393
  return { ok: false, result: invalid(options, "Artifact is empty") };
3312
3394
  }
@@ -3376,7 +3458,7 @@ function validateIssueFinalReviewArtifact(parsed, options) {
3376
3458
  for (const p of parsed.changedPaths) {
3377
3459
  const normalized = p.replace(/\\/g, "/");
3378
3460
  const segments = normalized.split("/");
3379
- if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute2(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3461
+ if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute3(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3380
3462
  return {
3381
3463
  ok: false,
3382
3464
  reason: `changedPaths must not contain absolute paths or path traversal: ${p}`,
@@ -3477,7 +3559,7 @@ function validateAgentArtifact(options) {
3477
3559
  case "refactor": {
3478
3560
  let findingIds = options.findingIds ?? [];
3479
3561
  if (findingIds.length === 0 && options.latestReviewArtifactPath) {
3480
- const latestReview = readFileSync7(
3562
+ const latestReview = readFileSync8(
3481
3563
  options.latestReviewArtifactPath,
3482
3564
  "utf-8"
3483
3565
  );
@@ -3500,10 +3582,10 @@ function validateAgentArtifact(options) {
3500
3582
  if (options.checkConflictMarkers !== false && parsed.status === "resolved") {
3501
3583
  const base = options.worktreePath ?? process.cwd();
3502
3584
  const filesWithMarkers = parsed.files.filter((file) => {
3503
- const filePath = resolve2(base, file);
3585
+ const filePath = resolve3(base, file);
3504
3586
  try {
3505
3587
  return CONFLICT_MARKER_PATTERN.test(
3506
- readFileSync7(filePath, "utf-8")
3588
+ readFileSync8(filePath, "utf-8")
3507
3589
  );
3508
3590
  } catch {
3509
3591
  return false;
@@ -3608,8 +3690,8 @@ function validateAgentArtifact(options) {
3608
3690
  }
3609
3691
  }
3610
3692
  function runValidateArtifactCommand(options) {
3611
- const artifactPath = resolve2(options.repoRoot, options.artifactPath);
3612
- const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve2(options.repoRoot, options.latestReviewArtifactPath) : void 0;
3693
+ const artifactPath = resolve3(options.repoRoot, options.artifactPath);
3694
+ const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve3(options.repoRoot, options.latestReviewArtifactPath) : void 0;
3613
3695
  return validateAgentArtifact({
3614
3696
  ...options,
3615
3697
  artifactPath,
@@ -3618,9 +3700,9 @@ function runValidateArtifactCommand(options) {
3618
3700
  });
3619
3701
  }
3620
3702
  function runLocalValidateArtifactCommand(options) {
3621
- const resolvedPath = resolve2(options.repoRoot, options.artifactPath);
3703
+ const resolvedPath = resolve3(options.repoRoot, options.artifactPath);
3622
3704
  const resolvedExtra = (options.extraArgs ?? []).map(
3623
- (p) => isAbsolute2(p) ? p : resolve2(options.repoRoot, p)
3705
+ (p) => isAbsolute3(p) ? p : resolve3(options.repoRoot, p)
3624
3706
  );
3625
3707
  let localResult;
3626
3708
  switch (options.kind) {
@@ -3667,7 +3749,7 @@ function runLocalValidateArtifactCommand(options) {
3667
3749
  }
3668
3750
  function readLocalArtifact(path9, failureCode) {
3669
3751
  try {
3670
- const raw = readFileSync7(path9, "utf-8");
3752
+ const raw = readFileSync8(path9, "utf-8");
3671
3753
  const parsed = JSON.parse(raw);
3672
3754
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3673
3755
  return {
@@ -4366,8 +4448,8 @@ function validateLocalTriage(prdPath, issuePaths) {
4366
4448
  }
4367
4449
 
4368
4450
  // commands/issue-run.ts
4369
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
4370
- import { isAbsolute as isAbsolute3, join as join15 } from "path";
4451
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4452
+ import { isAbsolute as isAbsolute4, join as join15 } from "path";
4371
4453
 
4372
4454
  // pr/templates.ts
4373
4455
  init_common();
@@ -4502,12 +4584,12 @@ function runEffectAndMapExit(program) {
4502
4584
  }
4503
4585
 
4504
4586
  // shared/attempt-log.ts
4505
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "fs";
4506
- import { dirname as dirname3, join as join8 } from "path";
4587
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9 } from "fs";
4588
+ import { dirname as dirname4, join as join8 } from "path";
4507
4589
  var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
4508
4590
  function writeAttemptLog(worktreePath, entry) {
4509
4591
  const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
4510
- mkdirSync6(dirname3(logPath), { recursive: true });
4592
+ mkdirSync6(dirname4(logPath), { recursive: true });
4511
4593
  appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
4512
4594
  }
4513
4595
  function readAttemptLog(worktreePath) {
@@ -4515,7 +4597,7 @@ function readAttemptLog(worktreePath) {
4515
4597
  if (!existsSync8(logPath)) {
4516
4598
  return [];
4517
4599
  }
4518
- const raw = readFileSync8(logPath, "utf-8");
4600
+ const raw = readFileSync9(logPath, "utf-8");
4519
4601
  const lines = raw.split("\n").filter((l) => l.length > 0);
4520
4602
  const entries = [];
4521
4603
  for (const line of lines) {
@@ -4634,7 +4716,7 @@ async function runBaseRefreshAttempt(options) {
4634
4716
  }
4635
4717
 
4636
4718
  // commands/conflict-resolution.ts
4637
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
4719
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
4638
4720
  import { join as join9 } from "path";
4639
4721
  init_common();
4640
4722
  var CONFLICT_MARKER_PATTERN2 = /<<<<<<<|=======|>>>>>>>/m;
@@ -4642,7 +4724,7 @@ async function hasUnresolvedConflictMarkers(worktreePath, files) {
4642
4724
  for (const file of files) {
4643
4725
  const filePath = join9(worktreePath, file);
4644
4726
  try {
4645
- const content = readFileSync9(filePath, "utf-8");
4727
+ const content = readFileSync10(filePath, "utf-8");
4646
4728
  if (CONFLICT_MARKER_PATTERN2.test(content)) {
4647
4729
  return true;
4648
4730
  }
@@ -4922,7 +5004,7 @@ async function canReachMcp(url) {
4922
5004
  return true;
4923
5005
  } catch {
4924
5006
  if (attempt < 9) {
4925
- await new Promise((resolve3) => setTimeout(resolve3, 100));
5007
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
4926
5008
  }
4927
5009
  }
4928
5010
  }
@@ -4973,7 +5055,7 @@ async function prepareSerenaForTarget(options) {
4973
5055
  }
4974
5056
 
4975
5057
  // failure-resolution/failure-resolution-agent.ts
4976
- import { readFileSync as readFileSync10 } from "fs";
5058
+ import { readFileSync as readFileSync11 } from "fs";
4977
5059
  import { join as join10 } from "path";
4978
5060
 
4979
5061
  // failure-resolution/recovery-policy.ts
@@ -5127,7 +5209,7 @@ async function runFailureResolutionAgent(options) {
5127
5209
  }
5128
5210
  let artifact;
5129
5211
  try {
5130
- const md = readFileSync10(fullArtifactPath, "utf-8");
5212
+ const md = readFileSync11(fullArtifactPath, "utf-8");
5131
5213
  const validation2 = validateAgentArtifact({
5132
5214
  kind: "failure-resolution",
5133
5215
  artifactPath: fullArtifactPath,
@@ -5221,7 +5303,7 @@ async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary,
5221
5303
 
5222
5304
  // commands/pr-description-agent.ts
5223
5305
  import { join as join12 } from "path";
5224
- import { readFileSync as readFileSync11 } from "fs";
5306
+ import { readFileSync as readFileSync12 } from "fs";
5225
5307
 
5226
5308
  // pr/pr-description-context.ts
5227
5309
  init_common();
@@ -5415,7 +5497,7 @@ function runFinalizerAgent(options) {
5415
5497
  ],
5416
5498
  logger: options.logger
5417
5499
  });
5418
- const output = readFileSync11(artifactPath, "utf-8");
5500
+ const output = readFileSync12(artifactPath, "utf-8");
5419
5501
  yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
5420
5502
  return result;
5421
5503
  });
@@ -5543,7 +5625,7 @@ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
5543
5625
 
5544
5626
  // prd-run/local-merge-coordinator.ts
5545
5627
  import { execFileSync as execFileSync2 } from "child_process";
5546
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
5628
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
5547
5629
  import { join as join13 } from "path";
5548
5630
 
5549
5631
  // prd-run/local-branches.ts
@@ -5646,7 +5728,7 @@ function readIssueBranchName(repoRoot2, prdId, issueId) {
5646
5728
  const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
5647
5729
  if (!existsSync10(issuePath)) return null;
5648
5730
  try {
5649
- const content = readFileSync12(issuePath, "utf-8");
5731
+ const content = readFileSync13(issuePath, "utf-8");
5650
5732
  const parsed = JSON.parse(content);
5651
5733
  return typeof parsed.branchName === "string" && parsed.branchName ? parsed.branchName : null;
5652
5734
  } catch {
@@ -5658,7 +5740,7 @@ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
5658
5740
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
5659
5741
  if (!existsSync10(receiptPath)) return null;
5660
5742
  try {
5661
- const content = readFileSync12(receiptPath, "utf-8");
5743
+ const content = readFileSync13(receiptPath, "utf-8");
5662
5744
  const parsed = JSON.parse(content);
5663
5745
  if (typeof parsed.prdId === "string" && typeof parsed.issueId === "string" && typeof parsed.stage === "string" && typeof parsed.sourceBranch === "string" && typeof parsed.localPrdBranch === "string" && typeof parsed.mergeCommit === "string" && typeof parsed.completedAt === "string") {
5664
5746
  return parsed;
@@ -5674,7 +5756,7 @@ async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
5674
5756
  if (existsSync10(receiptPath)) {
5675
5757
  try {
5676
5758
  const existing = JSON.parse(
5677
- readFileSync12(receiptPath, "utf-8")
5759
+ readFileSync13(receiptPath, "utf-8")
5678
5760
  );
5679
5761
  if (existing.prdId === prdId && existing.issueId === issueId && existing.mergeCommit) {
5680
5762
  return {
@@ -6083,7 +6165,7 @@ function runMergeCoordinator(options) {
6083
6165
  }
6084
6166
 
6085
6167
  // commands/issue-final-review-agent.ts
6086
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
6168
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
6087
6169
  import { join as join14 } from "path";
6088
6170
  var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join14(
6089
6171
  ".pourkit",
@@ -6197,12 +6279,21 @@ async function runIssueFinalReviewAgent(options) {
6197
6279
  }
6198
6280
  function loadIssueFinalReviewPrompt(repoRoot2, promptTemplate) {
6199
6281
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
6200
- const promptBody = existsSync11(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
6282
+ const promptBody = existsSync11(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
6201
6283
  return appendProtectedWorkGuidance(`${promptBody}
6202
6284
 
6203
6285
  ## Shared Run Context
6204
6286
 
6205
- Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
6287
+ Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
6288
+
6289
+ ## Initial Verification Pass
6290
+
6291
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
6292
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
6293
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
6294
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
6295
+ - 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.
6296
+ - If no verification commands are configured, note that and proceed with normal review.`);
6206
6297
  }
6207
6298
 
6208
6299
  // issues/issue-transitions.ts
@@ -6405,7 +6496,7 @@ async function advanceIssueFinalReview(options) {
6405
6496
  "Issue Final Review state is incomplete: missing artifactPath"
6406
6497
  );
6407
6498
  }
6408
- const artifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join15(worktreePath, ifrFromState.artifactPath);
6499
+ const artifactPath = isAbsolute4(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join15(worktreePath, ifrFromState.artifactPath);
6409
6500
  const validation = validateAgentArtifact({
6410
6501
  kind: "issue-final-review",
6411
6502
  artifactPath,
@@ -6437,6 +6528,12 @@ async function advanceIssueFinalReview(options) {
6437
6528
  logger,
6438
6529
  reviewArtifactPath
6439
6530
  });
6531
+ await assertCanonicalBaseAncestor({
6532
+ worktreePath,
6533
+ baseRef: `origin/${target.baseBranch}`,
6534
+ stageName: "Issue Final Review",
6535
+ logger
6536
+ });
6440
6537
  if (result.verdict === "pass") {
6441
6538
  updateWorktreeRunState(worktreePath, {
6442
6539
  issueFinalReview: {
@@ -6661,6 +6758,14 @@ async function startIssueRun(options) {
6661
6758
  };
6662
6759
  }
6663
6760
  if (executionResult.worktreePath) {
6761
+ if (shouldRunBuilder) {
6762
+ await assertCanonicalBaseAncestor({
6763
+ worktreePath: executionResult.worktreePath,
6764
+ baseRef: `origin/${effectiveBaseBranch}`,
6765
+ stageName: "Builder",
6766
+ logger
6767
+ });
6768
+ }
6664
6769
  if (worktreeState) {
6665
6770
  updateWorktreeRunState(executionResult.worktreePath, {
6666
6771
  completedStages: { builder: true }
@@ -6722,6 +6827,12 @@ async function advanceIssueRunReview(options) {
6722
6827
  }
6723
6828
  })
6724
6829
  );
6830
+ await assertCanonicalBaseAncestor({
6831
+ worktreePath: options.worktreePath,
6832
+ baseRef: `origin/${options.target.baseBranch}`,
6833
+ stageName: "Review/Refactor",
6834
+ logger: options.logger
6835
+ });
6725
6836
  updateWorktreeRunState(options.worktreePath, {
6726
6837
  review: {
6727
6838
  lifetimeIterations: reviewResult.lifetimeIterations,
@@ -6790,7 +6901,7 @@ async function completeIssueRun(options) {
6790
6901
  message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
6791
6902
  });
6792
6903
  }
6793
- const artifactContent = readFileSync14(
6904
+ const artifactContent = readFileSync15(
6794
6905
  finalizerFromState.artifactPath,
6795
6906
  "utf-8"
6796
6907
  );
@@ -6848,6 +6959,14 @@ async function completeIssueRun(options) {
6848
6959
  }
6849
6960
  prTitle = finalizerResult.title;
6850
6961
  prBody = finalizerResult.body;
6962
+ if (executionResult.worktreePath) {
6963
+ await assertCanonicalBaseAncestor({
6964
+ worktreePath: executionResult.worktreePath,
6965
+ baseRef: `origin/${effectiveBaseBranch}`,
6966
+ stageName: "Finalizer",
6967
+ logger
6968
+ });
6969
+ }
6851
6970
  }
6852
6971
  prTitle = ensureConventionalPrTitle(
6853
6972
  prTitle,
@@ -7216,18 +7335,12 @@ function getRefactorArtifactDir(artifactPath) {
7216
7335
  }
7217
7336
  async function finalizeWorktreeCommit(options) {
7218
7337
  const { worktreePath, baseRef, title, body, logger } = options;
7219
- await syncRemoteBaseRef(worktreePath, baseRef, logger);
7220
- try {
7221
- await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
7222
- cwd: worktreePath,
7223
- logger,
7224
- label: "git merge-base --is-ancestor"
7225
- });
7226
- } catch {
7227
- throw new Error(
7228
- `Cannot finalize stale worktree: ${baseRef} is not an ancestor of HEAD. Refresh the branch onto the latest target base before creating the final commit.`
7229
- );
7230
- }
7338
+ await assertCanonicalBaseAncestor({
7339
+ worktreePath,
7340
+ baseRef,
7341
+ stageName: "final commit",
7342
+ logger
7343
+ });
7231
7344
  await execCapture("git", ["reset", "--soft", baseRef], {
7232
7345
  cwd: worktreePath,
7233
7346
  logger,
@@ -7244,6 +7357,21 @@ async function finalizeWorktreeCommit(options) {
7244
7357
  label: "git commit"
7245
7358
  });
7246
7359
  }
7360
+ async function assertCanonicalBaseAncestor(options) {
7361
+ const { worktreePath, baseRef, stageName, logger } = options;
7362
+ await syncRemoteBaseRef(worktreePath, baseRef, logger);
7363
+ try {
7364
+ await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
7365
+ cwd: worktreePath,
7366
+ logger,
7367
+ label: "git merge-base --is-ancestor"
7368
+ });
7369
+ } catch {
7370
+ throw new Error(
7371
+ `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.`
7372
+ );
7373
+ }
7374
+ }
7247
7375
  async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
7248
7376
  const remoteBase = parseRemoteBaseRef(baseRef);
7249
7377
  if (!remoteBase) {
@@ -7487,7 +7615,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
7487
7615
  }
7488
7616
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
7489
7617
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
7490
- const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
7618
+ const promptBody = existsSync12(promptPath) ? readFileSync15(promptPath, "utf-8") : promptTemplate;
7491
7619
  return appendProtectedWorkGuidance(`${promptBody}
7492
7620
 
7493
7621
  ## Shared Run Context
@@ -7984,12 +8112,12 @@ import {
7984
8112
  lstatSync,
7985
8113
  mkdirSync as mkdirSync10,
7986
8114
  mkdtempSync,
7987
- readFileSync as readFileSync17,
8115
+ readFileSync as readFileSync18,
7988
8116
  realpathSync,
7989
8117
  rmSync as rmSync3
7990
8118
  } from "fs";
7991
8119
  import { spawnSync as spawnSync3 } from "child_process";
7992
- import { dirname as dirname4, join as join20, relative as relative2 } from "path";
8120
+ import { dirname as dirname5, join as join20, relative as relative2 } from "path";
7993
8121
  import { tmpdir } from "os";
7994
8122
  import { Match, pipe } from "effect";
7995
8123
 
@@ -7997,20 +8125,20 @@ import { Match, pipe } from "effect";
7997
8125
  import {
7998
8126
  existsSync as existsSync13,
7999
8127
  mkdirSync as mkdirSync8,
8000
- readFileSync as readFileSync15,
8128
+ readFileSync as readFileSync16,
8001
8129
  readdirSync as readdirSync4,
8002
8130
  writeFileSync as writeFileSync5
8003
8131
  } from "fs";
8004
8132
  import { mkdir as mkdir4, readFile as readFile4, writeFile } from "fs/promises";
8005
8133
  import { join as join16 } from "path";
8006
- import { z as z2 } from "zod";
8134
+ import { z } from "zod";
8007
8135
  var PRD_RUN_STATE_DIR = ".pourkit/prd-runs";
8008
- var PrdRunRecordSchema = z2.object({
8009
- prdRef: z2.string().regex(
8136
+ var PrdRunRecordSchema = z.object({
8137
+ prdRef: z.string().regex(
8010
8138
  /^PRD-\d{4}$/,
8011
8139
  "PRD ref must use four-digit format (e.g., PRD-0052)"
8012
8140
  ),
8013
- status: z2.enum([
8141
+ status: z.enum([
8014
8142
  "blocked",
8015
8143
  "starting",
8016
8144
  "running",
@@ -8022,61 +8150,61 @@ var PrdRunRecordSchema = z2.object({
8022
8150
  "complete",
8023
8151
  "completed_local_branch"
8024
8152
  ]),
8025
- updatedAt: z2.string().min(1),
8026
- mode: z2.enum(["github", "local"]).optional(),
8027
- blockedGate: z2.enum(["receipt-check", "branch-state", "git", "queue", "final-review"]).optional(),
8028
- targetName: z2.string().min(1).optional(),
8029
- prdBranch: z2.string().min(1).optional(),
8030
- blockedReason: z2.string().min(1).optional(),
8031
- diagnostics: z2.array(z2.string()).optional(),
8032
- offendingPaths: z2.array(z2.string()).optional(),
8033
- start: z2.object({
8034
- status: z2.enum(["started", "succeeded", "adopted", "reused"]),
8035
- targetName: z2.string().min(1),
8036
- prdBranch: z2.string().min(1),
8037
- startBaseBranch: z2.string().min(1),
8038
- startBaseCommit: z2.string().min(1).optional(),
8039
- branchAction: z2.enum(["created", "reused", "adopted"]).optional(),
8040
- startedAt: z2.string().min(1).optional(),
8041
- queueStartedAt: z2.string().min(1).optional(),
8042
- queueDrainedAt: z2.string().min(1).optional(),
8043
- queueProcessedCount: z2.number().int().nonnegative().optional(),
8044
- queueCommand: z2.literal("queue-run").optional(),
8045
- refreshReceipts: z2.tuple([]).optional()
8153
+ updatedAt: z.string().min(1),
8154
+ mode: z.enum(["github", "local"]).optional(),
8155
+ blockedGate: z.enum(["receipt-check", "branch-state", "git", "queue", "final-review"]).optional(),
8156
+ targetName: z.string().min(1).optional(),
8157
+ prdBranch: z.string().min(1).optional(),
8158
+ blockedReason: z.string().min(1).optional(),
8159
+ diagnostics: z.array(z.string()).optional(),
8160
+ offendingPaths: z.array(z.string()).optional(),
8161
+ start: z.object({
8162
+ status: z.enum(["started", "succeeded", "adopted", "reused"]),
8163
+ targetName: z.string().min(1),
8164
+ prdBranch: z.string().min(1),
8165
+ startBaseBranch: z.string().min(1),
8166
+ startBaseCommit: z.string().min(1).optional(),
8167
+ branchAction: z.enum(["created", "reused", "adopted"]).optional(),
8168
+ startedAt: z.string().min(1).optional(),
8169
+ queueStartedAt: z.string().min(1).optional(),
8170
+ queueDrainedAt: z.string().min(1).optional(),
8171
+ queueProcessedCount: z.number().int().nonnegative().optional(),
8172
+ queueCommand: z.literal("queue-run").optional(),
8173
+ refreshReceipts: z.tuple([]).optional()
8046
8174
  }).strict().optional(),
8047
- finalReview: z2.object({
8048
- status: z2.enum([
8175
+ finalReview: z.object({
8176
+ status: z.enum([
8049
8177
  "started",
8050
8178
  "succeeded",
8051
8179
  "blocked",
8052
8180
  "needs_human_review",
8053
8181
  "final_reviewed"
8054
8182
  ]),
8055
- targetName: z2.string().min(1),
8056
- prdBranch: z2.string().min(1),
8057
- mergeBase: z2.string().min(1).optional(),
8058
- verdict: z2.enum([
8183
+ targetName: z.string().min(1),
8184
+ prdBranch: z.string().min(1),
8185
+ mergeBase: z.string().min(1).optional(),
8186
+ verdict: z.enum([
8059
8187
  "pass_no_changes",
8060
8188
  "pass_with_retouch",
8061
8189
  "needs_human_review",
8062
8190
  "blocked"
8063
8191
  ]).optional(),
8064
- artifactPath: z2.string().min(1).optional(),
8065
- diagnostics: z2.array(z2.string()).optional(),
8066
- reviewedAt: z2.string().min(1).optional(),
8067
- retouchPrNumber: z2.number().int().positive().optional(),
8068
- retouchPrUrl: z2.string().url().optional(),
8069
- retouchMergeCommit: z2.string().min(1).optional(),
8070
- autoMerge: z2.boolean().optional(),
8071
- changedPaths: z2.array(z2.string()).optional()
8192
+ artifactPath: z.string().min(1).optional(),
8193
+ diagnostics: z.array(z.string()).optional(),
8194
+ reviewedAt: z.string().min(1).optional(),
8195
+ retouchPrNumber: z.number().int().positive().optional(),
8196
+ retouchPrUrl: z.string().url().optional(),
8197
+ retouchMergeCommit: z.string().min(1).optional(),
8198
+ autoMerge: z.boolean().optional(),
8199
+ changedPaths: z.array(z.string()).optional()
8072
8200
  }).strict().optional(),
8073
- scopeChanges: z2.array(
8074
- z2.object({
8075
- issueNumber: z2.number().int().positive(),
8076
- decision: z2.literal("accepted_scope_change"),
8077
- reason: z2.string().min(1),
8078
- acceptedBy: z2.string().min(1),
8079
- acceptedAt: z2.string().min(1)
8201
+ scopeChanges: z.array(
8202
+ z.object({
8203
+ issueNumber: z.number().int().positive(),
8204
+ decision: z.literal("accepted_scope_change"),
8205
+ reason: z.string().min(1),
8206
+ acceptedBy: z.string().min(1),
8207
+ acceptedAt: z.string().min(1)
8080
8208
  }).strict()
8081
8209
  ).optional()
8082
8210
  }).strict();
@@ -8097,12 +8225,12 @@ function readPrdRun(repoRoot2, prdRef) {
8097
8225
  return { record: null, diagnostics: [] };
8098
8226
  }
8099
8227
  try {
8100
- const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
8228
+ const raw = JSON.parse(readFileSync16(recordPath, "utf-8"));
8101
8229
  const parsed = normalizeLegacyPrdRunRecord(PrdRunRecordSchema.parse(raw));
8102
8230
  return { record: parsed, diagnostics: [] };
8103
8231
  } catch (error) {
8104
8232
  try {
8105
- const raw = JSON.parse(readFileSync15(recordPath, "utf-8"));
8233
+ const raw = JSON.parse(readFileSync16(recordPath, "utf-8"));
8106
8234
  if (raw && typeof raw === "object" && raw.start && typeof raw.start === "object" && raw.start.startBaseBranch === void 0) {
8107
8235
  return {
8108
8236
  record: raw,
@@ -8136,7 +8264,7 @@ function listPrdRuns(repoRoot2) {
8136
8264
  const recordPath = join16(stateDir, entry.name);
8137
8265
  try {
8138
8266
  const record = normalizeLegacyPrdRunRecord(
8139
- PrdRunRecordSchema.parse(JSON.parse(readFileSync15(recordPath, "utf-8")))
8267
+ PrdRunRecordSchema.parse(JSON.parse(readFileSync16(recordPath, "utf-8")))
8140
8268
  );
8141
8269
  records.push(record);
8142
8270
  } catch (error) {
@@ -8168,40 +8296,40 @@ function normalizeLegacyPrdRunRecord(record) {
8168
8296
  offendingPaths: void 0
8169
8297
  };
8170
8298
  }
8171
- var LocalPrdRunRecordSchema = z2.object({
8172
- prdId: z2.string().regex(
8299
+ var LocalPrdRunRecordSchema = z.object({
8300
+ prdId: z.string().regex(
8173
8301
  /^PRD-\d{4}$/,
8174
8302
  "PRD id must use four-digit format (e.g., PRD-0052)"
8175
8303
  ),
8176
- createdAt: z2.string().min(1),
8177
- receipts: z2.object({
8178
- start: z2.object({
8179
- startedAt: z2.string().min(1),
8180
- branch: z2.string().min(1),
8181
- baseCommit: z2.string().min(1)
8304
+ createdAt: z.string().min(1),
8305
+ receipts: z.object({
8306
+ start: z.object({
8307
+ startedAt: z.string().min(1),
8308
+ branch: z.string().min(1),
8309
+ baseCommit: z.string().min(1)
8182
8310
  }).strict().optional(),
8183
- queue: z2.object({ completedAt: z2.string().min(1) }).strict().optional(),
8184
- finalReview: z2.object({
8185
- completedAt: z2.string().min(1),
8186
- targetName: z2.string().optional(),
8187
- prdBranch: z2.string().optional(),
8188
- mergeBase: z2.string().optional(),
8189
- verdict: z2.enum([
8311
+ queue: z.object({ completedAt: z.string().min(1) }).strict().optional(),
8312
+ finalReview: z.object({
8313
+ completedAt: z.string().min(1),
8314
+ targetName: z.string().optional(),
8315
+ prdBranch: z.string().optional(),
8316
+ mergeBase: z.string().optional(),
8317
+ verdict: z.enum([
8190
8318
  "pass_no_changes",
8191
8319
  "pass_with_retouch",
8192
8320
  "needs_human_review",
8193
8321
  "blocked"
8194
8322
  ]).optional(),
8195
- diagnostics: z2.array(z2.string()).optional(),
8196
- artifactPath: z2.string().optional()
8323
+ diagnostics: z.array(z.string()).optional(),
8324
+ artifactPath: z.string().optional()
8197
8325
  }).strict().optional(),
8198
- complete: z2.object({
8199
- completedAt: z2.string().min(1),
8200
- branch: z2.string().min(1),
8201
- stages: z2.array(z2.string())
8326
+ complete: z.object({
8327
+ completedAt: z.string().min(1),
8328
+ branch: z.string().min(1),
8329
+ stages: z.array(z.string())
8202
8330
  }).strict().optional()
8203
8331
  }).strict(),
8204
- metadata: z2.record(z2.unknown())
8332
+ metadata: z.record(z.unknown())
8205
8333
  }).strict();
8206
8334
  function getRecordPath(repoRoot2, prdRef) {
8207
8335
  return join16(
@@ -8213,20 +8341,20 @@ function getRecordPath(repoRoot2, prdRef) {
8213
8341
 
8214
8342
  // prd-run/evidence-packet.ts
8215
8343
  import { createHash as createHash2 } from "crypto";
8216
- import { z as z3 } from "zod";
8344
+ import { z as z2 } from "zod";
8217
8345
  var PRD_REF_REGEX2 = /^PRD-\d{3,4}$/;
8218
- var StageSchema = z3.enum(["prdFinalReview", "prdReconciliation"]);
8219
- var EvidencePacketSchema = z3.object({
8220
- prdRef: z3.string().regex(PRD_REF_REGEX2),
8221
- prdBranch: z3.string().min(1),
8222
- mergeBase: z3.string().min(6),
8223
- planningManifestPath: z3.string().min(1),
8224
- planningManifestFacts: z3.object({
8225
- parentPrdIssueUrl: z3.string().min(1),
8226
- childIssueCount: z3.number().int().nonnegative()
8346
+ var StageSchema = z2.enum(["prdFinalReview", "prdReconciliation"]);
8347
+ var EvidencePacketSchema = z2.object({
8348
+ prdRef: z2.string().regex(PRD_REF_REGEX2),
8349
+ prdBranch: z2.string().min(1),
8350
+ mergeBase: z2.string().min(6),
8351
+ planningManifestPath: z2.string().min(1),
8352
+ planningManifestFacts: z2.object({
8353
+ parentPrdIssueUrl: z2.string().min(1),
8354
+ childIssueCount: z2.number().int().nonnegative()
8227
8355
  }),
8228
8356
  stage: StageSchema,
8229
- stageReceipts: z3.record(z3.string(), z3.unknown())
8357
+ stageReceipts: z2.record(z2.string(), z2.unknown())
8230
8358
  }).strict();
8231
8359
 
8232
8360
  // commands/prd-run.ts
@@ -8437,7 +8565,7 @@ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
8437
8565
  import { join as join18 } from "path";
8438
8566
 
8439
8567
  // prd-run/local-queue-loop.ts
8440
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
8568
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync7 } from "fs";
8441
8569
  import { join as join19 } from "path";
8442
8570
 
8443
8571
  // prd-run/local-issue-run.ts
@@ -8574,7 +8702,7 @@ function getIssueArtifactPath2(repoRoot2, prdId, issueId) {
8574
8702
  }
8575
8703
  function readIssueArtifact(repoRoot2, prdId, issueId) {
8576
8704
  try {
8577
- const content = readFileSync16(
8705
+ const content = readFileSync17(
8578
8706
  getIssueArtifactPath2(repoRoot2, prdId, issueId),
8579
8707
  "utf-8"
8580
8708
  );
@@ -9316,7 +9444,7 @@ function validateLocalStartStore(repoRoot2, prdRef) {
9316
9444
  if (existsSync16(localStoreDir)) {
9317
9445
  const localStorePath = join20(localStoreDir, "prd.json");
9318
9446
  try {
9319
- const content = JSON.parse(readFileSync17(localStorePath, "utf8"));
9447
+ const content = JSON.parse(readFileSync18(localStorePath, "utf8"));
9320
9448
  localStoreReady = content?.id === prdRef && content?.kind === "prd";
9321
9449
  } catch {
9322
9450
  localStoreReady = false;
@@ -11115,135 +11243,120 @@ function inferVerificationCommands(scripts, pm) {
11115
11243
  }
11116
11244
  function generateConfigTemplate(options) {
11117
11245
  const {
11118
- sourceRoot,
11119
- targetRoot,
11120
- packageManager,
11121
11246
  baseBranch,
11247
+ packageManager,
11122
11248
  verificationCommands,
11123
11249
  hasPackageJson = true,
11124
11250
  labels: maybeLabels
11125
11251
  } = options;
11126
11252
  const labels = maybeLabels ?? DEFAULT_RUNNER_LABELS;
11127
- const relPath = path5.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
11128
- const importPath = relPath || ".";
11129
11253
  const setupCommand = `${packageManager} install`;
11130
- let setupSection;
11131
- if (hasPackageJson) {
11132
- setupSection = [
11133
- " setupCommands: [",
11134
- ` { command: "${setupCommand}", label: "install" },`,
11135
- " ],"
11136
- ].join("\n");
11137
- } else {
11138
- setupSection = "";
11139
- }
11140
- let verifySection;
11141
- if (verificationCommands.length > 0) {
11142
- const cmdLines = verificationCommands.map((vc) => ` { command: "${vc.command}", label: "${vc.label}" }`).join(",\n");
11143
- verifySection = [
11144
- " verify: {",
11145
- " commands: [",
11146
- cmdLines,
11147
- " ],",
11148
- " },"
11149
- ].join("\n");
11150
- } else {
11151
- verifySection = [
11152
- " // verify: {",
11153
- " // commands: [",
11154
- " // No matching scripts found in package.json.",
11155
- " // ],",
11156
- " // },"
11157
- ].join("\n");
11158
- }
11159
- return `import { definePourkitConfig } from "${importPath}/pourkit/shared/config";
11160
- import type { PourkitConfig } from "${importPath}/pourkit/shared/config";
11161
-
11162
- export default definePourkitConfig({
11163
- targets: [
11164
- {
11165
- name: "default",
11166
- baseBranch: "${baseBranch}",
11167
- branchTemplate: "pourkit/{{issue.number}}/{{issue.slug}}",
11168
- autoMerge: false,
11169
- ${setupSection}
11170
- strategy: {
11171
- type: "review-refactor-loop",
11172
- implement: {
11173
- builder: {
11174
- agent: "pourkit-builder",
11175
- model: "opencode-go/deepseek-v4-flash",
11176
- promptTemplate: ".pourkit/prompts/builder.prompt.md",
11177
- },
11178
- },
11179
- review: {
11180
- reviewer: {
11181
- agent: "pourkit-reviewer",
11182
- model: "opencode-go/deepseek-v4-pro",
11183
- promptTemplate: ".pourkit/prompts/reviewer.prompt.md",
11184
- criteria: ["correctness", "scope", "tests", "quality"],
11185
- },
11186
- refactor: {
11187
- agent: "pourkit-refactor",
11188
- model: "opencode-go/qwen3.6-plus",
11189
- promptTemplate: ".pourkit/prompts/refactor.prompt.md",
11190
- },
11191
- maxIterations: 3,
11192
- passWithNotesRefactorAttempts: 2,
11254
+ const target = {
11255
+ name: "default",
11256
+ baseBranch,
11257
+ branchTemplate: "pourkit/{{issue.number}}/{{issue.slug}}",
11258
+ autoMerge: false,
11259
+ strategy: {
11260
+ type: "review-refactor-loop",
11261
+ implement: {
11262
+ builder: {
11263
+ agent: "pourkit-builder",
11264
+ model: "opencode-go/deepseek-v4-flash",
11265
+ promptTemplate: ".pourkit/prompts/builder.prompt.md"
11266
+ }
11267
+ },
11268
+ failureResolution: {
11269
+ agent: "pourkit-failure-resolution-agent",
11270
+ model: "opencode-go/mimo-v2.5",
11271
+ promptTemplate: ".pourkit/prompts/failure-resolution.prompt.md",
11272
+ maxAttemptsPerFailure: 1
11273
+ },
11274
+ review: {
11275
+ reviewer: {
11276
+ agent: "pourkit-reviewer",
11277
+ model: "opencode-go/deepseek-v4-pro",
11278
+ promptTemplate: ".pourkit/prompts/reviewer.prompt.md",
11279
+ criteria: ["correctness", "scope", "tests", "quality"]
11193
11280
  },
11194
- ${verifySection}
11195
- finalize: {
11196
- prDescriptionAgent: {
11197
- agent: "pourkit-pr-description",
11198
- model: "opencode-go/deepseek-v4-flash",
11199
- promptTemplate: ".pourkit/prompts/pr-description.prompt.md",
11200
- },
11201
- maxAttempts: 2,
11281
+ refactor: {
11282
+ agent: "pourkit-refactor",
11283
+ model: "opencode-go/qwen3.6-plus",
11284
+ promptTemplate: ".pourkit/prompts/refactor.prompt.md"
11202
11285
  },
11286
+ maxIterations: 3,
11287
+ passWithNotesRefactorAttempts: 2
11203
11288
  },
11204
- },
11205
- ],
11206
- labels: {
11207
- readyForAgent: ${JSON.stringify(labels.readyForAgent)},
11208
- agentInProgress: ${JSON.stringify(labels.agentInProgress)},
11209
- blocked: ${JSON.stringify(labels.blocked)},
11210
- prOpenAwaitingMerge: ${JSON.stringify(labels.prOpenAwaitingMerge)},
11211
- readyForHuman: ${JSON.stringify(labels.readyForHuman)},
11212
- },
11213
- sandbox: {
11214
- provider: "docker",
11215
- copyToWorktree: ["node_modules"],
11216
- mounts: [
11217
- {
11218
- hostPath: "~/.local/share/opencode",
11219
- sandboxPath: "/home/agent/.local/share/opencode",
11220
- readonly: false,
11289
+ issueFinalReview: {
11290
+ agent: "pourkit-reviewer",
11291
+ model: "opencode-go/deepseek-v4-pro",
11292
+ promptTemplate: ".pourkit/prompts/issue-final-review.prompt.md",
11293
+ maxAttempts: 3
11221
11294
  },
11222
- {
11223
- hostPath: "~/.config/opencode",
11224
- sandboxPath: "/home/agent/.config/opencode",
11225
- readonly: true,
11295
+ finalize: {
11296
+ prDescriptionAgent: {
11297
+ agent: "pourkit-pr-description",
11298
+ model: "opencode-go/deepseek-v4-flash",
11299
+ promptTemplate: ".pourkit/prompts/pr-description.prompt.md"
11300
+ },
11301
+ maxAttempts: 2
11302
+ }
11303
+ }
11304
+ };
11305
+ if (hasPackageJson) {
11306
+ target.setupCommands = [
11307
+ { command: setupCommand, label: "install" }
11308
+ ];
11309
+ }
11310
+ if (verificationCommands.length > 0) {
11311
+ target.strategy.verify = {
11312
+ commands: verificationCommands
11313
+ };
11314
+ }
11315
+ const config = {
11316
+ $schema: "./schema/pourkit.schema.json",
11317
+ schemaVersion: 1,
11318
+ targets: [target],
11319
+ labels: {
11320
+ readyForAgent: labels.readyForAgent,
11321
+ agentInProgress: labels.agentInProgress,
11322
+ blocked: labels.blocked,
11323
+ prOpenAwaitingMerge: labels.prOpenAwaitingMerge,
11324
+ readyForHuman: labels.readyForHuman
11325
+ },
11326
+ sandbox: {
11327
+ provider: "docker",
11328
+ copyToWorktree: ["node_modules"],
11329
+ mounts: [
11330
+ {
11331
+ hostPath: "~/.local/share/opencode",
11332
+ sandboxPath: "/home/agent/.local/share/opencode",
11333
+ readonly: false
11334
+ },
11335
+ {
11336
+ hostPath: "~/.config/opencode",
11337
+ sandboxPath: "/home/agent/.config/opencode",
11338
+ readonly: true
11339
+ }
11340
+ ],
11341
+ env: {
11342
+ HOME: "/home/agent",
11343
+ XDG_DATA_HOME: "/home/agent/.local/share",
11344
+ XDG_CONFIG_HOME: "/home/agent/.config",
11345
+ XDG_STATE_HOME: "/home/agent/.local/state",
11346
+ XDG_CACHE_HOME: "/home/agent/.cache"
11226
11347
  },
11227
- ],
11228
- env: {
11229
- HOME: "/home/agent",
11230
- XDG_DATA_HOME: "/home/agent/.local/share",
11231
- XDG_CONFIG_HOME: "/home/agent/.config",
11232
- XDG_STATE_HOME: "/home/agent/.local/state",
11233
- XDG_CACHE_HOME: "/home/agent/.cache",
11348
+ idleTimeoutSeconds: 300
11234
11349
  },
11235
- idleTimeoutSeconds: 300,
11236
- },
11237
- checks: {
11238
- requiredLabels: [],
11239
- allowedAuthors: [],
11240
- checksFoundTimeoutSeconds: 60,
11241
- checksCompletionTimeoutSeconds: 1800,
11242
- pollIntervalSeconds: 15,
11243
- issueListLimit: 50,
11244
- },
11245
- });
11246
- `;
11350
+ checks: {
11351
+ requiredLabels: [],
11352
+ allowedAuthors: [],
11353
+ checksFoundTimeoutSeconds: 60,
11354
+ checksCompletionTimeoutSeconds: 1800,
11355
+ pollIntervalSeconds: 15,
11356
+ issueListLimit: 50
11357
+ }
11358
+ };
11359
+ return JSON.stringify(config, null, 2) + "\n";
11247
11360
  }
11248
11361
  function generateOpenCodeConfig(existingConfig = {}) {
11249
11362
  const { $schema, ...rest } = existingConfig;
@@ -11976,15 +12089,13 @@ async function planInit(options) {
11976
12089
  checksum
11977
12090
  });
11978
12091
  }
11979
- const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
11980
- if (!existsSync17(configTsPath)) {
12092
+ const configJsonPath = path5.join(targetRoot, ".pourkit", "config.json");
12093
+ if (!existsSync17(configJsonPath)) {
11981
12094
  const verifyCommands = inferVerificationCommands(
11982
12095
  packageScripts,
11983
12096
  pm || "npm"
11984
12097
  );
11985
12098
  const configContent = generateConfigTemplate({
11986
- targetRoot,
11987
- sourceRoot,
11988
12099
  packageManager: pm || "npm",
11989
12100
  baseBranch,
11990
12101
  verificationCommands: verifyCommands,
@@ -11993,9 +12104,9 @@ async function planInit(options) {
11993
12104
  });
11994
12105
  operations.push({
11995
12106
  kind: "create",
11996
- path: configTsPath,
12107
+ path: configJsonPath,
11997
12108
  ownership: "managed",
11998
- reason: "Init pourkit.config.ts template",
12109
+ reason: "Init .pourkit/config.json template",
11999
12110
  requiresConfirmation: false,
12000
12111
  destructive: false,
12001
12112
  content: configContent
@@ -12003,13 +12114,85 @@ async function planInit(options) {
12003
12114
  } else {
12004
12115
  operations.push({
12005
12116
  kind: "skip",
12006
- path: configTsPath,
12117
+ path: configJsonPath,
12007
12118
  ownership: "project-owned",
12008
- reason: "Existing pourkit.config.ts (project-owned)",
12119
+ reason: "Existing .pourkit/config.json (project-owned)",
12009
12120
  requiresConfirmation: false,
12010
12121
  destructive: false
12011
12122
  });
12012
12123
  }
12124
+ const schemaJsonPath = path5.join(
12125
+ targetRoot,
12126
+ ".pourkit",
12127
+ "schema",
12128
+ "pourkit.schema.json"
12129
+ );
12130
+ const srcSchemaJson = path5.join(
12131
+ sourceRoot,
12132
+ "pourkit",
12133
+ "schema",
12134
+ "pourkit.schema.json"
12135
+ );
12136
+ if (existsSync17(srcSchemaJson)) {
12137
+ const checksum = await computeFileChecksum(srcSchemaJson);
12138
+ if (!existsSync17(schemaJsonPath)) {
12139
+ operations.push({
12140
+ kind: "copy",
12141
+ sourcePath: srcSchemaJson,
12142
+ path: schemaJsonPath,
12143
+ ownership: "managed",
12144
+ reason: "Copy schema: pourkit.schema.json",
12145
+ requiresConfirmation: false,
12146
+ destructive: false,
12147
+ checksum
12148
+ });
12149
+ } else {
12150
+ operations.push({
12151
+ kind: "skip",
12152
+ path: schemaJsonPath,
12153
+ ownership: "project-owned",
12154
+ reason: "Existing .pourkit/schema/pourkit.schema.json (project-owned)",
12155
+ requiresConfirmation: false,
12156
+ destructive: false
12157
+ });
12158
+ }
12159
+ }
12160
+ const schemaHashPath = path5.join(
12161
+ targetRoot,
12162
+ ".pourkit",
12163
+ "schema",
12164
+ "pourkit.schema.hash"
12165
+ );
12166
+ const srcSchemaHash = path5.join(
12167
+ sourceRoot,
12168
+ "pourkit",
12169
+ "schema",
12170
+ "pourkit.schema.hash"
12171
+ );
12172
+ if (existsSync17(srcSchemaHash)) {
12173
+ const checksum = await computeFileChecksum(srcSchemaHash);
12174
+ if (!existsSync17(schemaHashPath)) {
12175
+ operations.push({
12176
+ kind: "copy",
12177
+ sourcePath: srcSchemaHash,
12178
+ path: schemaHashPath,
12179
+ ownership: "managed",
12180
+ reason: "Copy schema hash: pourkit.schema.hash",
12181
+ requiresConfirmation: false,
12182
+ destructive: false,
12183
+ checksum
12184
+ });
12185
+ } else {
12186
+ operations.push({
12187
+ kind: "skip",
12188
+ path: schemaHashPath,
12189
+ ownership: "project-owned",
12190
+ reason: "Existing .pourkit/schema/pourkit.schema.hash (project-owned)",
12191
+ requiresConfirmation: false,
12192
+ destructive: false
12193
+ });
12194
+ }
12195
+ }
12013
12196
  const agentFileMode = options.conflictPolicy?.agentFile ?? "both";
12014
12197
  const hasExistingAgents = operations.some(
12015
12198
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
@@ -12994,6 +13177,165 @@ async function runSerenaStatusCommand(options) {
12994
13177
  logSerenaSidecarStatus("Serena sidecar status", status);
12995
13178
  }
12996
13179
 
13180
+ // commands/config-schema.ts
13181
+ import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
13182
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
13183
+ import { resolve as resolve4, dirname as dirname6 } from "path";
13184
+ import { fileURLToPath as fileURLToPath2 } from "url";
13185
+ import Ajv2 from "ajv";
13186
+ var __filename2 = fileURLToPath2(import.meta.url);
13187
+ var __dirname2 = dirname6(__filename2);
13188
+ var PACKAGED_SCHEMA_PATH = resolve4(
13189
+ __dirname2,
13190
+ "../schema/pourkit.schema.json"
13191
+ );
13192
+ var PACKAGED_HASH_PATH = resolve4(__dirname2, "../schema/pourkit.schema.hash");
13193
+ var _schemaValidator = null;
13194
+ var _schemaErrors = null;
13195
+ function getSchemaValidator() {
13196
+ if (!_schemaValidator) {
13197
+ const schema = JSON.parse(readFileSync19(PACKAGED_SCHEMA_PATH, "utf-8"));
13198
+ const ajv = new Ajv2({ strict: true });
13199
+ ajv.addKeyword("x-pourkit-schema-version");
13200
+ const validate = ajv.compile(schema);
13201
+ _schemaValidator = (data) => {
13202
+ _schemaErrors = null;
13203
+ const valid2 = validate(data);
13204
+ if (!valid2) _schemaErrors = validate.errors;
13205
+ return valid2;
13206
+ };
13207
+ }
13208
+ return _schemaValidator;
13209
+ }
13210
+ function readPackagedHash() {
13211
+ return readFileSync19(PACKAGED_HASH_PATH, "utf-8");
13212
+ }
13213
+ function localSchemaDir(repoRoot2) {
13214
+ return resolve4(repoRoot2, ".pourkit/schema");
13215
+ }
13216
+ async function runDoctorCommand(options) {
13217
+ const repoRootPath = options.cwd ?? process.cwd();
13218
+ const schemaDir = localSchemaDir(repoRootPath);
13219
+ const localSchemaPath = resolve4(schemaDir, "pourkit.schema.json");
13220
+ const localHashPath = resolve4(schemaDir, "pourkit.schema.hash");
13221
+ const localSchemaExists = existsSync18(localSchemaPath);
13222
+ const localHashExists = existsSync18(localHashPath);
13223
+ let packagedHash = null;
13224
+ try {
13225
+ packagedHash = readPackagedHash();
13226
+ } catch {
13227
+ }
13228
+ let localHashContent = null;
13229
+ if (localHashExists) {
13230
+ try {
13231
+ localHashContent = await readFile6(localHashPath, "utf-8");
13232
+ } catch {
13233
+ }
13234
+ }
13235
+ const schema = !localSchemaExists ? "missing" : !packagedHash || !localHashContent ? "missing" : localHashContent.trim() === packagedHash.trim() ? "fresh" : "stale";
13236
+ const hash = !localHashExists ? "missing" : !packagedHash ? "missing" : localHashContent && localHashContent.trim() === packagedHash.trim() ? "fresh" : "stale";
13237
+ const overall = schema === "fresh" && hash === "fresh" ? "fresh" : schema === "missing" || hash === "missing" ? "missing" : "stale";
13238
+ const configPath = resolve4(repoRootPath, ".pourkit/config.json");
13239
+ let configValidation;
13240
+ if (existsSync18(configPath)) {
13241
+ try {
13242
+ const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
13243
+ const validate = getSchemaValidator();
13244
+ const valid2 = validate(raw);
13245
+ if (valid2) {
13246
+ configValidation = { ok: true, errors: [] };
13247
+ } else {
13248
+ const errors = _schemaErrors;
13249
+ configValidation = {
13250
+ ok: false,
13251
+ errors: (errors ?? []).map(
13252
+ (e) => `${e.instancePath || "(root)"}: ${e.message ?? "validation failed"}`
13253
+ )
13254
+ };
13255
+ }
13256
+ } catch (err) {
13257
+ const msg = err instanceof SyntaxError ? err.message : String(err);
13258
+ configValidation = {
13259
+ ok: false,
13260
+ errors: [`.pourkit/config.json: ${msg}`]
13261
+ };
13262
+ }
13263
+ } else {
13264
+ configValidation = {
13265
+ ok: false,
13266
+ errors: [".pourkit/config.json not found"]
13267
+ };
13268
+ }
13269
+ const obsoleteConfigs = [
13270
+ "pourkit.config.ts",
13271
+ "pourkit.config.mjs",
13272
+ "pourkit.config.js",
13273
+ "pourkit.json"
13274
+ ].filter((p) => existsSync18(resolve4(repoRootPath, p)));
13275
+ let recommendation = null;
13276
+ if (!configValidation.ok || overall !== "fresh" || obsoleteConfigs.length > 0) {
13277
+ const parts = [];
13278
+ if (obsoleteConfigs.length > 0) {
13279
+ parts.push(
13280
+ `Found obsolete config files: ${obsoleteConfigs.join(", ")}. Move configuration to .pourkit/config.json with "$schema": "./schema/pourkit.schema.json".`
13281
+ );
13282
+ }
13283
+ if (!configValidation.ok) {
13284
+ parts.push(
13285
+ "Config validation failed; fix errors in .pourkit/config.json."
13286
+ );
13287
+ }
13288
+ if (overall !== "fresh") {
13289
+ parts.push(
13290
+ 'Run "pourkit config sync-schema" to update local schema assets.'
13291
+ );
13292
+ }
13293
+ recommendation = parts.join(" ");
13294
+ }
13295
+ return {
13296
+ configValidation,
13297
+ obsoleteConfigs,
13298
+ schemaAssets: { schema, hash, overall },
13299
+ recommendation
13300
+ };
13301
+ }
13302
+ async function runConfigSyncSchemaCommand(options) {
13303
+ const repoRootPath = options.cwd ?? process.cwd();
13304
+ const schemaDir = localSchemaDir(repoRootPath);
13305
+ await mkdir6(schemaDir, { recursive: true });
13306
+ const packagedSchema = await readFile6(PACKAGED_SCHEMA_PATH, "utf-8");
13307
+ const packagedHash = await readFile6(PACKAGED_HASH_PATH, "utf-8");
13308
+ const localSchemaPath = resolve4(schemaDir, "pourkit.schema.json");
13309
+ const localHashPath = resolve4(schemaDir, "pourkit.schema.hash");
13310
+ let schemaWritten = false;
13311
+ let hashWritten = false;
13312
+ if (existsSync18(localSchemaPath)) {
13313
+ const existing = await readFile6(localSchemaPath, "utf-8");
13314
+ if (existing !== packagedSchema) {
13315
+ await writeFile3(localSchemaPath, packagedSchema, "utf-8");
13316
+ schemaWritten = true;
13317
+ }
13318
+ } else {
13319
+ await writeFile3(localSchemaPath, packagedSchema, "utf-8");
13320
+ schemaWritten = true;
13321
+ }
13322
+ if (existsSync18(localHashPath)) {
13323
+ const existing = await readFile6(localHashPath, "utf-8");
13324
+ if (existing !== packagedHash) {
13325
+ await writeFile3(localHashPath, packagedHash, "utf-8");
13326
+ hashWritten = true;
13327
+ }
13328
+ } else {
13329
+ await writeFile3(localHashPath, packagedHash, "utf-8");
13330
+ hashWritten = true;
13331
+ }
13332
+ return {
13333
+ changed: schemaWritten || hashWritten,
13334
+ schemaWritten,
13335
+ hashWritten
13336
+ };
13337
+ }
13338
+
12997
13339
  // providers/github-provider.ts
12998
13340
  var GitHubIssueProvider = class {
12999
13341
  client;
@@ -13531,14 +13873,14 @@ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
13531
13873
  // execution/execution-provider.ts
13532
13874
  init_common();
13533
13875
  import { mkdtempSync as mkdtempSync2 } from "fs";
13534
- import { writeFile as writeFile3 } from "fs/promises";
13876
+ import { writeFile as writeFile4 } from "fs/promises";
13535
13877
  import { tmpdir as tmpdir2 } from "os";
13536
- import { dirname as dirname5, join as join21 } from "path";
13878
+ import { dirname as dirname7, join as join21 } from "path";
13537
13879
  async function writeExecutionArtifacts(worktreePath, artifacts) {
13538
13880
  for (const artifact of artifacts) {
13539
13881
  const filePath = join21(worktreePath, artifact.path);
13540
- await ensureDir(dirname5(filePath));
13541
- await writeFile3(filePath, artifact.content, "utf-8");
13882
+ await ensureDir(dirname7(filePath));
13883
+ await writeFile4(filePath, artifact.content, "utf-8");
13542
13884
  }
13543
13885
  }
13544
13886
 
@@ -13548,17 +13890,17 @@ import path7 from "path";
13548
13890
 
13549
13891
  // execution/sandbox-image.ts
13550
13892
  import { createHash as createHash4 } from "crypto";
13551
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
13893
+ import { existsSync as existsSync19, readFileSync as readFileSync20 } from "fs";
13552
13894
  import path6 from "path";
13553
13895
  function sandboxImageName(repoRoot2) {
13554
13896
  const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
13555
13897
  const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
13556
13898
  const baseName = sanitized || "local";
13557
13899
  const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
13558
- if (!existsSync18(dockerfilePath)) {
13900
+ if (!existsSync19(dockerfilePath)) {
13559
13901
  return `sandcastle:${baseName}`;
13560
13902
  }
13561
- const fingerprint = createHash4("sha256").update(readFileSync18(dockerfilePath)).digest("hex").slice(0, 8);
13903
+ const fingerprint = createHash4("sha256").update(readFileSync20(dockerfilePath)).digest("hex").slice(0, 8);
13562
13904
  return `sandcastle:${baseName}-${fingerprint}`;
13563
13905
  }
13564
13906
 
@@ -14418,6 +14760,26 @@ function createCliProgram(version) {
14418
14760
  await runInitCommand(initOptions);
14419
14761
  }
14420
14762
  );
14763
+ program.command("doctor").description("Report Config Schema drift without mutating files").option("--cwd <path>", "target repository directory").action(async (options) => {
14764
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
14765
+ const report = await runDoctorCommand({ cwd: targetRepoRoot });
14766
+ console.log(JSON.stringify(report, null, 2));
14767
+ if (!report.configValidation.ok || report.obsoleteConfigs.length > 0 || report.schemaAssets.overall !== "fresh") {
14768
+ process.exitCode = 1;
14769
+ }
14770
+ });
14771
+ const configCommand = program.command("config").description("Config management commands");
14772
+ configCommand.command("sync-schema").description("Copy packaged Config Schema assets to .pourkit/schema/").option("--cwd <path>", "target repository directory").action(async (options) => {
14773
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
14774
+ const result = await runConfigSyncSchemaCommand({ cwd: targetRepoRoot });
14775
+ if (result.changed) {
14776
+ console.log(
14777
+ `Schema assets updated (schema: ${result.schemaWritten ? "written" : "unchanged"}, hash: ${result.hashWritten ? "written" : "unchanged"}).`
14778
+ );
14779
+ } else {
14780
+ console.log("Schema assets are up to date.");
14781
+ }
14782
+ });
14421
14783
  const serena = program.command("serena").description("Serena baseline and sidecar commands");
14422
14784
  serena.command("init").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
14423
14785
  await runSerenaInitCommand({
@@ -14543,11 +14905,11 @@ function createCliProgram(version) {
14543
14905
  return program;
14544
14906
  }
14545
14907
  async function resolveCliVersion() {
14546
- if (isPackageVersion("0.0.0-next-20260614002607")) {
14547
- return "0.0.0-next-20260614002607";
14908
+ if (isPackageVersion("0.0.0-next-20260614074434")) {
14909
+ return "0.0.0-next-20260614074434";
14548
14910
  }
14549
- if (isReleaseVersion("0.0.0-next-20260614002607")) {
14550
- return "0.0.0-next-20260614002607";
14911
+ if (isReleaseVersion("0.0.0-next-20260614074434")) {
14912
+ return "0.0.0-next-20260614074434";
14551
14913
  }
14552
14914
  try {
14553
14915
  const root = repoRoot();