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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -153,6 +153,7 @@ __export(common_exports, {
153
153
  execJson: () => execJson,
154
154
  parseWorktreeListPorcelain: () => parseWorktreeListPorcelain,
155
155
  readMaybeEnvInt: () => readMaybeEnvInt,
156
+ redactSensitiveValues: () => redactSensitiveValues,
156
157
  repoRelative: () => repoRelative,
157
158
  repoRoot: () => repoRoot,
158
159
  sleep: () => sleep,
@@ -265,7 +266,7 @@ async function execJson(command, args, options = {}) {
265
266
  return JSON.parse(result.stdout);
266
267
  }
267
268
  function sleep(ms) {
268
- return new Promise((resolve3) => setTimeout(resolve3, ms));
269
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
269
270
  }
270
271
  async function execCaptureWithRetry(command, args, options = {}) {
271
272
  const retries = options.retries ?? 3;
@@ -311,7 +312,14 @@ function parseWorktreeListPorcelain(text2, branch) {
311
312
  }
312
313
  return null;
313
314
  }
314
- var execFileAsync, TRANSIENT_GH_ERROR, TYPE_LABELS;
315
+ function redactSensitiveValues(input) {
316
+ let redacted = input;
317
+ for (const pattern of TOKEN_LIKE_PATTERNS) {
318
+ redacted = redacted.replace(pattern, "[REDACTED]");
319
+ }
320
+ return redacted;
321
+ }
322
+ var execFileAsync, TRANSIENT_GH_ERROR, TYPE_LABELS, TOKEN_LIKE_PATTERNS;
315
323
  var init_common = __esm({
316
324
  "shared/common.ts"() {
317
325
  "use strict";
@@ -325,6 +333,16 @@ var init_common = __esm({
325
333
  "type:polish",
326
334
  "type:refactor"
327
335
  ];
336
+ TOKEN_LIKE_PATTERNS = [
337
+ /gh[ps]_[A-Za-z0-9]{20,}/g,
338
+ /token[=:_\s][A-Za-z0-9_-]{20,}/gi,
339
+ /secret[=:_\s][A-Za-z0-9_-]{20,}/gi,
340
+ /credential[=:_\s][A-Za-z0-9_-]{20,}/gi,
341
+ /key[=:_\s][A-Za-z0-9_-]{20,}/gi,
342
+ /bearer\s+[A-Za-z0-9_-]{20,}/gi,
343
+ /authorization:\s*Bearer\s+[A-Za-z0-9_-]{20,}/gi,
344
+ /xox[baprs]-[A-Za-z0-9_-]{10,}/g
345
+ ];
328
346
  }
329
347
  });
330
348
 
@@ -335,175 +353,37 @@ import { pathToFileURL } from "url";
335
353
  import { Command, Option, CommanderError } from "commander";
336
354
 
337
355
  // shared/config.ts
338
- import { join } from "path";
339
- import { z } from "zod";
340
- var NonEmptyString = z.string().trim().min(1);
356
+ import { readFileSync } from "fs";
357
+ import { fileURLToPath } from "url";
358
+ import { dirname, isAbsolute, join, normalize, sep, resolve } from "path";
359
+ import Ajv from "ajv";
360
+ var __filename = fileURLToPath(import.meta.url);
361
+ var __dirname = dirname(__filename);
362
+ var SCHEMA_PATH = resolve(__dirname, "../schema/pourkit.schema.json");
363
+ var _schema = null;
364
+ var _validate = null;
365
+ var _ajvErrors = null;
366
+ function getValidator() {
367
+ if (!_validate) {
368
+ const schema = _schema ?? JSON.parse(readFileSync(SCHEMA_PATH, "utf-8"));
369
+ _schema = schema;
370
+ const ajv = new Ajv({ strict: true });
371
+ ajv.addKeyword("x-pourkit-schema-version");
372
+ const validate = ajv.compile(schema);
373
+ _validate = (data) => {
374
+ _ajvErrors = null;
375
+ const valid2 = validate(data);
376
+ if (!valid2) _ajvErrors = validate.errors;
377
+ return valid2;
378
+ };
379
+ }
380
+ return _validate;
381
+ }
341
382
  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();
383
+ var DEFAULT_BRANCH_TEMPLATE = "pourkit/{{issue.number}}/{{issue.slug}}";
345
384
  function resolveMissingOrEmptyOutputRetries(config) {
346
385
  return config?.outputRetries?.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES;
347
386
  }
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
387
  var removedFieldReplacements = {
508
388
  "config.implementor": "targets[].strategy.implement.builder",
509
389
  "config.reviewer": "targets[].strategy.review.reviewer",
@@ -567,11 +447,12 @@ function checkRemovedFields(raw) {
567
447
  }
568
448
  }
569
449
  }
570
- function formatZodPath(path9) {
571
- if (path9.length === 0) return "";
450
+ function formatAjvPath(instancePath) {
451
+ if (!instancePath || instancePath === "/") return "";
452
+ const parts = instancePath.split("/").slice(1);
572
453
  let result = "";
573
- for (const segment of path9) {
574
- if (typeof segment === "number") {
454
+ for (const segment of parts) {
455
+ if (/^\d+$/.test(segment)) {
575
456
  result += `[${segment}]`;
576
457
  } else {
577
458
  result += result ? `.${segment}` : segment;
@@ -579,51 +460,172 @@ function formatZodPath(path9) {
579
460
  }
580
461
  return result;
581
462
  }
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";
463
+ function formatFirstAjvError(errors) {
464
+ const error = errors[0];
465
+ const path9 = formatAjvPath(error.instancePath);
466
+ if (error.keyword === "required") {
467
+ const missingParam = error.params.missingProperty;
468
+ if (missingParam === "targets") {
469
+ return "Config must have at least one target";
470
+ }
471
+ if (path9 === "" && missingParam === "targets") {
472
+ return "Config must have at least one target";
473
+ }
474
+ if (path9) {
475
+ return `${path9} must have required property '${missingParam}'`;
476
+ }
477
+ return `${missingParam} must be an object`;
587
478
  }
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`;
479
+ if (error.keyword === "additionalProperties") {
480
+ const additionalProp = error.params.additionalProperty;
481
+ const keyPath = path9 ? `${path9}.${additionalProp}` : additionalProp;
482
+ return `${keyPath} is not supported`;
590
483
  }
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;
484
+ if (error.keyword === "minLength") {
485
+ if (path9) {
486
+ return `${path9} must be a non-empty string`;
487
+ }
488
+ return "Config must be a non-empty string";
489
+ }
490
+ if (error.keyword === "const") {
491
+ const allowedValue = error.params.allowedValue;
492
+ return `${path9 || "Config"} must be ${JSON.stringify(allowedValue)}`;
493
+ }
494
+ if (error.keyword === "enum") {
495
+ const allowedValues = error.params.allowedValues;
496
+ return `${path9} must be one of: ${allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
497
+ }
498
+ if (error.keyword === "minimum" || error.keyword === "exclusiveMinimum") {
499
+ const limit = error.params.limit;
500
+ if (limit === 1) {
501
+ return `${path9} must be a positive number`;
502
+ }
503
+ return `${path9} must be at least ${limit}`;
504
+ }
505
+ if (error.keyword === "type") {
506
+ const expected = error.params.type;
507
+ if (path9 === "") {
508
+ return `Config must be an object`;
509
+ }
510
+ if (expected === "object") {
511
+ return `${path9} must be an object`;
512
+ }
513
+ if (expected === "integer") {
514
+ return `${path9} must be an integer`;
515
+ }
516
+ if (expected === "string") {
517
+ return `${path9} must be a string`;
518
+ }
519
+ if (expected === "number") {
520
+ return `${path9} must be a number`;
521
+ }
522
+ return `${path9} must be ${expected === "integer" ? "an integer" : `a ${expected}`}`;
523
+ }
524
+ if (error.keyword === "minItems") {
525
+ return `${path9} must contain at least one item`;
526
+ }
527
+ if (error.keyword === "pattern") {
528
+ return `${path9} has an invalid format`;
529
+ }
530
+ return `${path9 || "Config"} ${error.message || "is invalid"}`;
531
+ }
532
+ function applyOutputRetriesDefaults(retries) {
533
+ if (retries === void 0) return void 0;
534
+ return {
535
+ missingOrEmpty: retries.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES
536
+ };
537
+ }
538
+ function applyStageAgentDefaults(agent) {
539
+ return {
540
+ ...agent,
541
+ outputRetries: applyOutputRetriesDefaults(agent.outputRetries)
542
+ };
543
+ }
544
+ function assertRepoRelativePath(value, location) {
545
+ const normalized = normalize(value);
546
+ if (isAbsolute(value) || isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
547
+ throw new Error(
548
+ `${location} must stay within the repository and be repo-relative; got "${value}"`
549
+ );
550
+ }
551
+ }
552
+ function assertBaseBranch(value, location) {
553
+ if (value.includes("/")) {
554
+ throw new Error(
555
+ `${location} must be a local branch name, not a remote-qualified, tag, ref, or path-like name; got "${value}"`
556
+ );
557
+ }
558
+ if (/^[0-9a-f]{7,40}$/i.test(value)) {
559
+ throw new Error(
560
+ `${location} must be a branch name, not a commit SHA; got "${value}"`
561
+ );
562
+ }
563
+ }
564
+ function assertStageAgentPath(agent, location) {
565
+ if (!agent) return;
566
+ const promptTemplate = agent.promptTemplate;
567
+ if (typeof promptTemplate === "string") {
568
+ assertRepoRelativePath(promptTemplate, `${location}.promptTemplate`);
569
+ }
570
+ }
571
+ function validateConfigSemantics(data) {
572
+ const sandbox = data.sandbox;
573
+ const copyToWorktree = sandbox?.copyToWorktree;
574
+ copyToWorktree?.forEach((entry, index) => {
575
+ assertRepoRelativePath(entry, `sandbox.copyToWorktree[${index}]`);
576
+ });
577
+ const targetNames = /* @__PURE__ */ new Set();
578
+ data.targets.forEach((target, targetIndex) => {
579
+ const name = target.name;
580
+ if (targetNames.has(name)) {
581
+ throw new Error(
582
+ `Duplicate target name "${name}"; target names must be unique`
583
+ );
584
+ }
585
+ targetNames.add(name);
586
+ assertBaseBranch(
587
+ target.baseBranch,
588
+ `targets[${targetIndex}].baseBranch`
589
+ );
590
+ const strategy = target.strategy;
591
+ const implement = strategy.implement;
592
+ const review = strategy.review;
593
+ const finalize = strategy.finalize;
594
+ assertStageAgentPath(
595
+ implement.builder,
596
+ `targets[${targetIndex}].strategy.implement.builder`
597
+ );
598
+ assertStageAgentPath(
599
+ strategy.conflictResolution,
600
+ `targets[${targetIndex}].strategy.conflictResolution`
601
+ );
602
+ assertStageAgentPath(
603
+ strategy.failureResolution,
604
+ `targets[${targetIndex}].strategy.failureResolution`
605
+ );
606
+ assertStageAgentPath(
607
+ review.reviewer,
608
+ `targets[${targetIndex}].strategy.review.reviewer`
609
+ );
610
+ assertStageAgentPath(
611
+ review.refactor,
612
+ `targets[${targetIndex}].strategy.review.refactor`
613
+ );
614
+ assertStageAgentPath(
615
+ strategy.issueFinalReview,
616
+ `targets[${targetIndex}].strategy.issueFinalReview`
617
+ );
618
+ assertStageAgentPath(
619
+ finalize.prDescriptionAgent,
620
+ `targets[${targetIndex}].strategy.finalize.prDescriptionAgent`
621
+ );
622
+ });
623
+ }
624
+ function assertKnownKeys(value, path9, knownKeys) {
625
+ for (const key of Object.keys(value)) {
626
+ if (!knownKeys.includes(key)) {
627
+ throw new Error(`${path9}.${key} is not supported`);
606
628
  }
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
629
  }
628
630
  }
629
631
  function parseConfig(raw) {
@@ -645,6 +647,7 @@ function parseConfig(raw) {
645
647
  "name",
646
648
  "baseBranch",
647
649
  "branchTemplate",
650
+ "prdRun",
648
651
  "setupCommands",
649
652
  "autoMerge",
650
653
  "queue",
@@ -660,9 +663,20 @@ function parseConfig(raw) {
660
663
  `targets[${i}].strategy.conflictResolution has been removed; use targets[${i}].strategy.failureResolution`
661
664
  );
662
665
  }
663
- if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object" && "reconciliation" in strategy.prdRun) {
666
+ if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object") {
667
+ const prdRun = strategy.prdRun;
668
+ if ("reconciliation" in prdRun) {
669
+ throw new Error(
670
+ `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
671
+ );
672
+ }
673
+ if ("finalReview" in prdRun) {
674
+ throw new Error(
675
+ `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.`
676
+ );
677
+ }
664
678
  throw new Error(
665
- `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
679
+ `targets[${i}].strategy.prdRun is not supported in the new config; use targets[${i}].prdRun instead.`
666
680
  );
667
681
  }
668
682
  }
@@ -672,67 +686,126 @@ function parseConfig(raw) {
672
686
  "copyToWorktree",
673
687
  "mounts",
674
688
  "env",
675
- "idleTimeoutSeconds"
689
+ "idleTimeoutSeconds",
690
+ "forceRebuild"
676
691
  ]);
677
692
  }
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
693
+ const validate = getValidator();
694
+ if (!validate(raw)) {
695
+ throw new Error(
696
+ formatFirstAjvError(
697
+ _ajvErrors ?? [
698
+ {
699
+ instancePath: "",
700
+ message: "validation failed",
701
+ keyword: "error",
702
+ params: {}
727
703
  }
728
- } : {}
729
- }
730
- };
731
- });
704
+ ]
705
+ )
706
+ );
707
+ }
708
+ const data = config;
709
+ validateConfigSemantics(data);
710
+ const targets = data.targets.map(
711
+ (t) => {
712
+ const input = t;
713
+ const strategy = input.strategy;
714
+ const implement = strategy.implement;
715
+ const failureResolution = strategy.failureResolution;
716
+ const review = strategy.review;
717
+ const reviewReviewer = review.reviewer;
718
+ const reviewRefactor = review.refactor;
719
+ const finalize = strategy.finalize;
720
+ const issueFinalReview = strategy.issueFinalReview;
721
+ const setupCommands = input.setupCommands?.map((cmd, i) => ({
722
+ command: cmd.command,
723
+ label: cmd.label ?? `check-${i}`
724
+ }));
725
+ const verifyCommands = strategy.verify?.commands ? strategy.verify.commands : void 0;
726
+ const verifyLabeled = verifyCommands?.map((cmd, i) => ({
727
+ command: cmd.command,
728
+ label: cmd.label ?? `check-${i}`
729
+ }));
730
+ return {
731
+ name: input.name,
732
+ baseBranch: input.baseBranch,
733
+ branchTemplate: input.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE,
734
+ prdRun: input.prdRun,
735
+ setupCommands,
736
+ autoMerge: input.autoMerge !== void 0 ? input.autoMerge : true,
737
+ queue: input.queue,
738
+ serena: input.serena,
739
+ strategy: {
740
+ type: "review-refactor-loop",
741
+ implement: {
742
+ builder: applyStageAgentDefaults(
743
+ implement.builder
744
+ )
745
+ },
746
+ failureResolution: {
747
+ agent: failureResolution.agent,
748
+ model: failureResolution.model,
749
+ variant: failureResolution.variant,
750
+ env: failureResolution.env,
751
+ promptTemplate: failureResolution.promptTemplate,
752
+ outputRetries: applyOutputRetriesDefaults(
753
+ failureResolution.outputRetries
754
+ ),
755
+ maxAttemptsPerFailure: failureResolution.maxAttemptsPerFailure,
756
+ failureLimits: failureResolution.failureLimits
757
+ },
758
+ review: {
759
+ reviewer: {
760
+ agent: reviewReviewer.agent,
761
+ model: reviewReviewer.model,
762
+ variant: reviewReviewer.variant,
763
+ env: reviewReviewer.env,
764
+ promptTemplate: reviewReviewer.promptTemplate,
765
+ outputRetries: applyOutputRetriesDefaults(
766
+ reviewReviewer.outputRetries
767
+ ),
768
+ criteria: reviewReviewer.criteria,
769
+ includeReviewHistory: reviewReviewer.includeReviewHistory,
770
+ passWithNotesRefactorAttempts: reviewReviewer.passWithNotesRefactorAttempts
771
+ },
772
+ refactor: applyStageAgentDefaults(
773
+ reviewRefactor
774
+ ),
775
+ maxIterations: review.maxIterations,
776
+ passWithNotesRefactorAttempts: review.passWithNotesRefactorAttempts ?? 2
777
+ },
778
+ ...verifyLabeled ? { verify: { commands: verifyLabeled } } : {},
779
+ issueFinalReview: {
780
+ ...issueFinalReview,
781
+ maxAttempts: issueFinalReview.maxAttempts,
782
+ outputRetries: applyOutputRetriesDefaults(
783
+ issueFinalReview.outputRetries
784
+ )
785
+ },
786
+ finalize: {
787
+ prDescriptionAgent: applyStageAgentDefaults(
788
+ finalize.prDescriptionAgent
789
+ ),
790
+ maxAttempts: finalize.maxAttempts
791
+ }
792
+ }
793
+ };
794
+ }
795
+ );
796
+ const serenaRaw = data.serena;
797
+ const serenaDefaults = {
798
+ mcpUrl: serenaRaw?.mcpUrl ?? "http://localhost:9121/mcp",
799
+ sandboxMcpUrl: serenaRaw?.sandboxMcpUrl ?? "http://localhost:9121/mcp",
800
+ dataDir: serenaRaw?.dataDir ?? ".pourkit/serena/"
801
+ };
732
802
  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
803
+ enabled: serenaRaw?.enabled ?? false,
804
+ required: serenaRaw?.required ?? false,
805
+ mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? serenaDefaults.mcpUrl,
806
+ sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? serenaDefaults.sandboxMcpUrl,
807
+ dataDir: serenaDefaults.dataDir,
808
+ autoStart: serenaRaw?.autoStart ?? false
736
809
  };
737
810
  if (serena.mcpUrl.trim() === "") {
738
811
  throw new Error("POURKIT_SERENA_MCP_URL must be a non-empty string");
@@ -742,39 +815,48 @@ function parseConfig(raw) {
742
815
  "POURKIT_SERENA_SANDBOX_MCP_URL must be a non-empty string"
743
816
  );
744
817
  }
818
+ const checksRaw = data.checks;
819
+ const labelsRaw = data.labels;
820
+ const cleanupRaw = data.cleanup;
821
+ const sandboxRaw = data.sandbox;
745
822
  return {
746
823
  targets,
747
- labels: data.labels,
824
+ labels: {
825
+ readyForAgent: labelsRaw?.readyForAgent ?? "ready-for-agent",
826
+ agentInProgress: labelsRaw?.agentInProgress ?? "agent-in-progress",
827
+ blocked: labelsRaw?.blocked ?? "blocked",
828
+ prOpenAwaitingMerge: labelsRaw?.prOpenAwaitingMerge ?? "pr-open-awaiting-merge",
829
+ readyForHuman: labelsRaw?.readyForHuman ?? "ready-for-human",
830
+ needsTriage: labelsRaw?.needsTriage ?? "needs-triage"
831
+ },
748
832
  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
833
+ provider: sandboxRaw?.provider ?? "docker",
834
+ copyToWorktree: sandboxRaw?.copyToWorktree,
835
+ mounts: sandboxRaw?.mounts ? sandboxRaw.mounts.map((m) => ({
836
+ hostPath: m.hostPath,
837
+ sandboxPath: m.sandboxPath,
838
+ readonly: m.readonly ?? false
839
+ })) : void 0,
840
+ env: sandboxRaw?.env,
841
+ idleTimeoutSeconds: sandboxRaw?.idleTimeoutSeconds,
842
+ forceRebuild: sandboxRaw?.forceRebuild
754
843
  },
755
844
  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
845
+ requiredLabels: checksRaw?.requiredLabels ?? [],
846
+ allowedAuthors: checksRaw?.allowedAuthors ?? [],
847
+ checksFoundTimeoutSeconds: checksRaw?.checksFoundTimeoutSeconds ?? 60,
848
+ checksCompletionTimeoutSeconds: checksRaw?.checksCompletionTimeoutSeconds ?? 30 * 60,
849
+ pollIntervalSeconds: checksRaw?.pollIntervalSeconds ?? 15,
850
+ issueListLimit: checksRaw?.issueListLimit ?? 50
762
851
  },
763
852
  serena,
764
853
  cleanup: {
765
- enabled: data.cleanup?.enabled ?? true,
766
- worktreeRetentionDays: data.cleanup?.worktreeRetentionDays ?? 14,
767
- logRetentionDays: data.cleanup?.logRetentionDays ?? 30
854
+ enabled: cleanupRaw?.enabled ?? true,
855
+ worktreeRetentionDays: cleanupRaw?.worktreeRetentionDays ?? 14,
856
+ logRetentionDays: cleanupRaw?.logRetentionDays ?? 30
768
857
  }
769
858
  };
770
859
  }
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
860
  function getVerificationCommands(target) {
779
861
  return target.strategy.verify?.commands ?? [];
780
862
  }
@@ -782,7 +864,7 @@ function resolvePrdRunMode(target, opts) {
782
864
  if (opts?.localOverride === true) {
783
865
  return { mode: "local", source: "cli-override", targetName: target.name };
784
866
  }
785
- const configMode = target.strategy.prdRun?.mode;
867
+ const configMode = target.prdRun?.mode;
786
868
  if (configMode) {
787
869
  return {
788
870
  mode: configMode,
@@ -792,48 +874,41 @@ function resolvePrdRunMode(target, opts) {
792
874
  }
793
875
  return { mode: "github", source: "default", targetName: target.name };
794
876
  }
795
- async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
877
+ var OBSOLETE_CONFIG_PATHS = [
878
+ "pourkit.config.ts",
879
+ "pourkit.config.mjs",
880
+ "pourkit.config.js",
881
+ "pourkit.json"
882
+ ];
883
+ var CANONICAL_CONFIG_PATH = ".pourkit/config.json";
884
+ async function loadRepoConfig(repoRoot2, _configFileName) {
796
885
  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);
886
+ const { join: pjoin } = await import("path");
887
+ for (const obPath of OBSOLETE_CONFIG_PATHS) {
888
+ const fullPath = pjoin(repoRoot2, obPath);
889
+ if (existsSync19(fullPath)) {
890
+ const isRootJson = obPath === "pourkit.json";
891
+ throw new Error(
892
+ 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".`
893
+ );
894
+ }
895
+ }
896
+ const configPath = pjoin(repoRoot2, CANONICAL_CONFIG_PATH);
802
897
  if (!existsSync19(configPath)) {
803
898
  throw new Error(
804
- `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
899
+ `No Pourkit config found at ${CANONICAL_CONFIG_PATH}. Run pourkit init or create ${CANONICAL_CONFIG_PATH} with "$schema": "./schema/pourkit.schema.json".`
805
900
  );
806
901
  }
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
- );
902
+ const { readFile: readFile7 } = await import("fs/promises");
903
+ const raw = await readFile7(configPath, "utf-8");
904
+ let parsed;
813
905
  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
- }
906
+ parsed = JSON.parse(raw);
907
+ } catch (err) {
908
+ const message = err instanceof SyntaxError ? err.message : String(err);
909
+ throw new Error(`${CANONICAL_CONFIG_PATH}: Invalid JSON \u2014 ${message}`);
836
910
  }
911
+ return parseConfig(parsed);
837
912
  }
838
913
  function resolvePromptTemplatePath(repoRoot2, promptTemplate) {
839
914
  if (promptTemplate.includes("/")) {
@@ -872,8 +947,8 @@ function resolveTarget(config, explicitTarget) {
872
947
  init_common();
873
948
 
874
949
  // shared/worktree-run-state.ts
875
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
876
- import { dirname, join as join2 } from "path";
950
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
951
+ import { dirname as dirname2, join as join2 } from "path";
877
952
  var WORKTREE_RUN_STATE_PATH = ".pourkit/state.json";
878
953
  function readWorktreeRunState(worktreePath) {
879
954
  const statePath = join2(worktreePath, WORKTREE_RUN_STATE_PATH);
@@ -881,7 +956,7 @@ function readWorktreeRunState(worktreePath) {
881
956
  return null;
882
957
  }
883
958
  try {
884
- const raw = JSON.parse(readFileSync(statePath, "utf-8"));
959
+ const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
885
960
  if (isValidWorktreeRunState(raw)) {
886
961
  return raw;
887
962
  }
@@ -902,7 +977,7 @@ function isValidWorktreeRunState(raw) {
902
977
  }
903
978
  function writeWorktreeRunState(worktreePath, state) {
904
979
  const statePath = join2(worktreePath, WORKTREE_RUN_STATE_PATH);
905
- mkdirSync2(dirname(statePath), { recursive: true });
980
+ mkdirSync2(dirname2(statePath), { recursive: true });
906
981
  writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
907
982
  }
908
983
  function updateWorktreeRunState(worktreePath, update) {
@@ -1049,8 +1124,8 @@ async function cleanupRepository(options) {
1049
1124
  // commands/artifact-validation.ts
1050
1125
  import { createHash } from "crypto";
1051
1126
  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";
1127
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
1128
+ import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
1054
1129
 
1055
1130
  // pr/review-verdict.ts
1056
1131
  var ReviewVerdictProtocolError = class extends Error {
@@ -1085,15 +1160,15 @@ function parseReviewVerdict(output) {
1085
1160
  import {
1086
1161
  existsSync as existsSync5,
1087
1162
  mkdirSync as mkdirSync5,
1088
- readFileSync as readFileSync5,
1163
+ readFileSync as readFileSync6,
1089
1164
  readdirSync as readdirSync2,
1090
1165
  writeFileSync as writeFileSync3
1091
1166
  } from "fs";
1092
1167
  import { join as join6 } from "path";
1093
1168
 
1094
1169
  // 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";
1170
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync } from "fs";
1171
+ import { dirname as dirname3, join as join4 } from "path";
1097
1172
  async function executeWithMissingOrEmptyArtifactRetry({
1098
1173
  executionProvider,
1099
1174
  executionOptions,
@@ -1153,20 +1228,20 @@ function readArtifactOutput(artifactPath) {
1153
1228
  if (!existsSync2(artifactPath)) {
1154
1229
  return { _tag: "missing", path: artifactPath };
1155
1230
  }
1156
- const output = readFileSync2(artifactPath, "utf-8");
1231
+ const output = readFileSync3(artifactPath, "utf-8");
1157
1232
  if (!output.trim()) {
1158
1233
  return { _tag: "empty", path: artifactPath };
1159
1234
  }
1160
1235
  return { _tag: "content", value: output, path: artifactPath };
1161
1236
  }
1162
1237
  function prepareArtifactPath(artifactPath) {
1163
- mkdirSync3(dirname2(artifactPath), { recursive: true });
1238
+ mkdirSync3(dirname3(artifactPath), { recursive: true });
1164
1239
  rmSync(artifactPath, { recursive: true, force: true });
1165
1240
  }
1166
1241
 
1167
1242
  // 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";
1243
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "fs";
1244
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
1170
1245
 
1171
1246
  // commands/run-verification.ts
1172
1247
  init_common();
@@ -1314,13 +1389,6 @@ var STAGE_SECTIONS = {
1314
1389
  "branch",
1315
1390
  "verification-commands",
1316
1391
  "artifacts"
1317
- ],
1318
- prdReconciliation: [
1319
- "issue",
1320
- "comments",
1321
- "branch",
1322
- "verification-commands",
1323
- "artifacts"
1324
1392
  ]
1325
1393
  };
1326
1394
  function buildRunContextArtifact(options) {
@@ -1372,11 +1440,17 @@ function buildRunContextMarkdown(options) {
1372
1440
  }
1373
1441
  }
1374
1442
  if (sections.includes("branch")) {
1443
+ const canonicalBaseRef = `origin/${target.baseBranch}`;
1375
1444
  parts.push(
1376
1445
  "## Branch",
1377
1446
  "",
1378
1447
  `- Base: ${target.baseBranch}`,
1448
+ `- Canonical Base Ref: ${canonicalBaseRef}`,
1379
1449
  `- Working Branch: ${branchName}`,
1450
+ "",
1451
+ "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.",
1452
+ "",
1453
+ "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
1454
  ""
1381
1455
  );
1382
1456
  }
@@ -1439,7 +1513,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1439
1513
  `### Parent PRD Content: \`${relative(repoRoot2, parentPrdPath)}\``,
1440
1514
  "",
1441
1515
  "```markdown",
1442
- readFileSync3(parentPrdPath, "utf-8").trimEnd(),
1516
+ readFileSync4(parentPrdPath, "utf-8").trimEnd(),
1443
1517
  "```",
1444
1518
  ""
1445
1519
  );
@@ -1466,7 +1540,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1466
1540
  `### Document Content: \`${documentPath}\``,
1467
1541
  "",
1468
1542
  "```markdown",
1469
- readFileSync3(absolutePath, "utf-8").trimEnd(),
1543
+ readFileSync4(absolutePath, "utf-8").trimEnd(),
1470
1544
  "```",
1471
1545
  ""
1472
1546
  );
@@ -1491,34 +1565,27 @@ function extractRepoPaths(section) {
1491
1565
  return Array.from(paths);
1492
1566
  }
1493
1567
  function resolveRepoPath(repoRoot2, path9) {
1494
- if (isAbsolute(path9) || path9.includes("\0")) return null;
1495
- const resolved = resolve(repoRoot2, path9);
1568
+ if (isAbsolute2(path9) || path9.includes("\0")) return null;
1569
+ const resolved = resolve2(repoRoot2, path9);
1496
1570
  const repoRelative2 = relative(repoRoot2, resolved);
1497
- if (repoRelative2.startsWith("..") || isAbsolute(repoRelative2)) return null;
1571
+ if (repoRelative2.startsWith("..") || isAbsolute2(repoRelative2)) return null;
1498
1572
  return resolved;
1499
1573
  }
1500
1574
  function findParentPrdPath(repoRoot2, parentRef) {
1501
- const directPath = join5(
1502
- repoRoot2,
1503
- ".pourkit",
1504
- "architecture",
1505
- parentRef,
1506
- "PRD.md"
1507
- );
1508
- if (existsSync3(directPath)) return directPath;
1509
- const architectureRoot = join5(repoRoot2, ".pourkit", "architecture");
1510
- if (!existsSync3(architectureRoot)) return null;
1511
- return findPrdMirror(architectureRoot, parentRef);
1575
+ const plansRoot = join5(repoRoot2, ".pourkit", "plans");
1576
+ if (!existsSync3(plansRoot)) return null;
1577
+ return findPrdInPlans(plansRoot, parentRef);
1512
1578
  }
1513
- function findPrdMirror(directory, parentRef) {
1579
+ function findPrdInPlans(directory, parentRef) {
1514
1580
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
1515
1581
  const entryPath = join5(directory, entry.name);
1516
1582
  if (entry.isDirectory()) {
1517
- if (entry.name.startsWith(parentRef)) {
1583
+ const prdNumber = parentRef.match(/^PRD-(\d+)$/)?.[1];
1584
+ if (entry.name.startsWith(parentRef) || prdNumber && entry.name.startsWith(`${prdNumber}-`)) {
1518
1585
  const prdPath = join5(entryPath, "PRD.md");
1519
1586
  if (existsSync3(prdPath)) return prdPath;
1520
1587
  }
1521
- const nested = findPrdMirror(entryPath, parentRef);
1588
+ const nested = findPrdInPlans(entryPath, parentRef);
1522
1589
  if (nested) return nested;
1523
1590
  }
1524
1591
  }
@@ -1538,17 +1605,19 @@ function renderCriteria(criteria) {
1538
1605
 
1539
1606
  // shared/prompt-guidance.ts
1540
1607
  var PROTECTED_WORK_RULE = "Do **not** revert, delete, or substantially strip already-landed protected sibling/base work unless the issue explicitly requires those files.";
1608
+ 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
1609
  function appendProtectedWorkGuidance(promptBody) {
1542
1610
  return `${promptBody}
1543
1611
 
1544
1612
  ## Hard Rule
1545
1613
 
1546
- - ${PROTECTED_WORK_RULE}`;
1614
+ - ${PROTECTED_WORK_RULE}
1615
+ - ${RUNNER_OWNED_GIT_RULE}`;
1547
1616
  }
1548
1617
 
1549
1618
  // shared/effect-services.ts
1550
1619
  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";
1620
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
1552
1621
  var GitExecutionError = class extends Error {
1553
1622
  _tag = "GitExecutionError";
1554
1623
  message;
@@ -1564,7 +1633,7 @@ var FileSystemDefault = Layer.succeed(
1564
1633
  FileSystem,
1565
1634
  FileSystem.of({
1566
1635
  readFile: (path9) => Effect.try({
1567
- try: () => readFileSync4(path9, "utf-8"),
1636
+ try: () => readFileSync5(path9, "utf-8"),
1568
1637
  catch: (error) => new Error(
1569
1638
  `Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
1570
1639
  )
@@ -1965,7 +2034,7 @@ function validateRefactorArtifact(artifactPath, findingIds) {
1965
2034
  `Refactor artifact missing at ${artifactPath}`
1966
2035
  );
1967
2036
  }
1968
- const content = readFileSync5(artifactPath, "utf-8");
2037
+ const content = readFileSync6(artifactPath, "utf-8");
1969
2038
  if (!content.trim()) {
1970
2039
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
1971
2040
  }
@@ -2290,12 +2359,29 @@ A prior review emitted \`NEEDS_HUMAN\` and stopped the agent loop. The issue has
2290
2359
  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
2360
 
2292
2361
  ` : "";
2293
- return `${renderedTemplate}
2362
+ return appendProtectedWorkGuidance(`${renderedTemplate}
2294
2363
 
2295
2364
  ## Shared Run Context
2296
2365
 
2297
2366
  Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
2298
2367
 
2368
+ ## Initial Verification Pass
2369
+
2370
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
2371
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
2372
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
2373
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
2374
+ - 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.
2375
+ - If no verification commands are configured, note that and proceed with normal review.
2376
+
2377
+ ## Scope Evidence Rules
2378
+
2379
+ - Use the Run Context's canonical base ref, for example \`origin/<base>\`, for scope diffs and commit ranges.
2380
+ - 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.
2381
+ - Only call a file or commit out of scope when it is part of the working branch's delta from the canonical base ref.
2382
+ - 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.
2383
+ - If scope evidence is ambiguous, use \`NEEDS_HUMAN\` or ask for a runner/base mismatch decision instead of telling Refactor to revert files.
2384
+
2299
2385
  ${hasCriteriaPlaceholder ? "" : `## Review Criteria
2300
2386
 
2301
2387
  ${criteriaBlock}
@@ -2312,7 +2398,7 @@ End the file with exactly one wrapped verdict token: <verdict>PASS</verdict>, <v
2312
2398
 
2313
2399
  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
2400
 
2315
- When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`;
2401
+ When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`);
2316
2402
  });
2317
2403
  }
2318
2404
  function renderReviewHistory(reviewHistory) {
@@ -2437,12 +2523,12 @@ function recoverReviewOutputFromLog(logPath) {
2437
2523
  if (!existsSync5(logPath)) {
2438
2524
  return null;
2439
2525
  }
2440
- const logContent = readFileSync5(logPath, "utf-8");
2526
+ const logContent = readFileSync6(logPath, "utf-8");
2441
2527
  return recoverReviewOutputFromString(logContent);
2442
2528
  }
2443
2529
  function readReviewArtifact(artifactPath, logPath) {
2444
2530
  if (existsSync5(artifactPath)) {
2445
- const output = readFileSync5(artifactPath, "utf-8");
2531
+ const output = readFileSync6(artifactPath, "utf-8");
2446
2532
  if (output.trim()) {
2447
2533
  return output;
2448
2534
  }
@@ -3091,194 +3177,6 @@ function parseConflictResolutionArtifact(output) {
3091
3177
  };
3092
3178
  }
3093
3179
 
3094
- // prd-run/final-review-validation.ts
3095
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3096
- function parseFinalReviewArtifact(artifactPath) {
3097
- if (!existsSync6(artifactPath)) {
3098
- return {
3099
- ok: false,
3100
- reason: "Final Review artifact not found.",
3101
- diagnostics: [`Artifact path: ${artifactPath}`]
3102
- };
3103
- }
3104
- let content;
3105
- try {
3106
- content = readFileSync6(artifactPath, "utf-8");
3107
- } catch (error) {
3108
- return {
3109
- ok: false,
3110
- reason: "Final Review artifact could not be read.",
3111
- diagnostics: [
3112
- error instanceof Error ? error.message : String(error),
3113
- `Artifact path: ${artifactPath}`
3114
- ]
3115
- };
3116
- }
3117
- let parsed;
3118
- try {
3119
- parsed = JSON.parse(content);
3120
- } catch {
3121
- return {
3122
- ok: false,
3123
- reason: "Final Review artifact is not valid JSON.",
3124
- diagnostics: [`Artifact path: ${artifactPath}`]
3125
- };
3126
- }
3127
- const verdict = parsed.verdict;
3128
- if (!verdict || typeof verdict !== "string") {
3129
- return {
3130
- ok: false,
3131
- reason: "Final Review artifact is missing a verdict field.",
3132
- diagnostics: [`Artifact content preview: ${content.slice(0, 300)}`]
3133
- };
3134
- }
3135
- const allowedVerdicts = [
3136
- "pass_no_changes",
3137
- "pass_with_retouch",
3138
- "needs_human_review",
3139
- "blocked"
3140
- ];
3141
- if (!allowedVerdicts.includes(verdict)) {
3142
- return {
3143
- ok: false,
3144
- reason: `Final Review artifact has unsupported verdict "${verdict}".`,
3145
- diagnostics: [`Allowed verdicts: ${allowedVerdicts.join(", ")}`]
3146
- };
3147
- }
3148
- const summary = typeof parsed.summary === "string" ? parsed.summary : typeof parsed.reason === "string" ? parsed.reason : String(verdict);
3149
- const diagnostics = Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [];
3150
- const changedPaths = Array.isArray(parsed.changedPaths) ? parsed.changedPaths : void 0;
3151
- const isAutoSummary = !parsed.summary && !parsed.reason;
3152
- if (verdict === "pass_with_retouch" && (!changedPaths || changedPaths.length === 0) && isAutoSummary) {
3153
- return {
3154
- ok: false,
3155
- reason: 'Final Review artifact with "pass_with_retouch" verdict requires changed paths.',
3156
- diagnostics: [
3157
- "pass_with_retouch verdict must include a changedPaths array in the artifact or provide a summary/reason with enough context for git diff fallback.",
3158
- "Provide changedPaths or a descriptive summary in the artifact."
3159
- ]
3160
- };
3161
- }
3162
- const prdRef = typeof parsed.prdRef === "string" && parsed.prdRef.trim() ? parsed.prdRef.trim() : void 0;
3163
- const stage = typeof parsed.stage === "string" && parsed.stage.trim() ? parsed.stage.trim() : void 0;
3164
- const checkoutBase = typeof parsed.checkoutBase === "string" && parsed.checkoutBase.trim() ? parsed.checkoutBase.trim() : void 0;
3165
- const reviewBase = typeof parsed.reviewBase === "string" && parsed.reviewBase.trim() ? parsed.reviewBase.trim() : void 0;
3166
- return {
3167
- ok: true,
3168
- verdict,
3169
- summary,
3170
- diagnostics,
3171
- changedPaths,
3172
- prdRef,
3173
- stage,
3174
- checkoutBase,
3175
- reviewBase
3176
- };
3177
- }
3178
- function validateFinalReviewArtifactSemanticIds(artifact, context) {
3179
- const errors = [];
3180
- if (!artifact.prdRef) {
3181
- errors.push("Final Review artifact is missing prdRef.");
3182
- } else if (artifact.prdRef !== context.prdRef) {
3183
- errors.push(
3184
- `Final Review artifact prdRef "${artifact.prdRef}" does not match active PRD Run "${context.prdRef}".`
3185
- );
3186
- }
3187
- if (!artifact.stage) {
3188
- errors.push("Final Review artifact is missing stage field.");
3189
- } else if (artifact.stage !== "prdFinalReview") {
3190
- errors.push(
3191
- `Final Review artifact stage "${artifact.stage}" is not "prdFinalReview".`
3192
- );
3193
- }
3194
- if (!artifact.checkoutBase) {
3195
- errors.push("Final Review artifact is missing checkoutBase.");
3196
- } else if (artifact.checkoutBase !== context.prdBranch) {
3197
- errors.push(
3198
- `Final Review artifact checkoutBase "${artifact.checkoutBase}" does not match active PRD Branch "${context.prdBranch}".`
3199
- );
3200
- }
3201
- if (!artifact.reviewBase) {
3202
- errors.push("Final Review artifact is missing reviewBase.");
3203
- } else if (artifact.reviewBase !== context.mergeBase) {
3204
- errors.push(
3205
- `Final Review artifact reviewBase "${artifact.reviewBase}" does not match active merge base "${context.mergeBase}".`
3206
- );
3207
- }
3208
- if (errors.length > 0) {
3209
- return { ok: false, errors, blockedGate: "final-review" };
3210
- }
3211
- return { ok: true };
3212
- }
3213
- function validateFinalReviewRetouchScope(changedPaths) {
3214
- if (!changedPaths || changedPaths.length === 0) {
3215
- return {
3216
- ok: false,
3217
- reason: "Final Review retouch scope validation failed. No changed paths available. Provide changed paths or run Final Review with a summary that includes changed paths.",
3218
- diagnostics: ["Changed paths list is empty or undefined."],
3219
- offendingPaths: []
3220
- };
3221
- }
3222
- const offendingPaths = changedPaths.filter((p) => !isRetouchScopePath(p));
3223
- const allowedPaths = changedPaths.filter((p) => isRetouchScopePath(p));
3224
- if (allowedPaths.length === 0) {
3225
- return {
3226
- ok: false,
3227
- reason: "Final Review retouch scope validation failed. No implementation, test, or Changeset paths found for retouch.",
3228
- diagnostics: [
3229
- "All changed paths are prohibited for retouch.",
3230
- ...changedPaths.map((p) => ` - ${p}`)
3231
- ],
3232
- offendingPaths
3233
- };
3234
- }
3235
- if (offendingPaths.length > 0) {
3236
- return {
3237
- ok: false,
3238
- reason: "Final Review retouch scope validation failed. Prohibited paths cannot be included in retouch PR.",
3239
- diagnostics: [
3240
- "Prohibited paths found:",
3241
- ...offendingPaths.map((p) => ` - ${p}`)
3242
- ],
3243
- offendingPaths
3244
- };
3245
- }
3246
- return { ok: true, changedPaths };
3247
- }
3248
- function validateFinalReviewAgentOutputs(options) {
3249
- const artifact = parseFinalReviewArtifact(options.artifactPath);
3250
- if (!artifact.ok) {
3251
- return { ...artifact, offendingPaths: [] };
3252
- }
3253
- const semanticResult = validateFinalReviewArtifactSemanticIds(
3254
- artifact,
3255
- options.context
3256
- );
3257
- if (!semanticResult.ok) {
3258
- return {
3259
- ok: false,
3260
- reason: "Final Review artifact has mismatched semantic IDs.",
3261
- diagnostics: semanticResult.errors,
3262
- offendingPaths: []
3263
- };
3264
- }
3265
- if (artifact.verdict !== "pass_with_retouch") {
3266
- return { ok: true, artifact };
3267
- }
3268
- const changedPaths = options.changedPaths && options.changedPaths.length > 0 ? options.changedPaths : artifact.changedPaths;
3269
- const retouch = validateFinalReviewRetouchScope(changedPaths ?? []);
3270
- if (!retouch.ok) {
3271
- return retouch;
3272
- }
3273
- return { ok: true, artifact, retouch };
3274
- }
3275
- function isRetouchScopePath(path9) {
3276
- if (path9.startsWith(".pourkit/plans/") || path9 === ".pourkit/CONTEXT.md" || path9.startsWith(".pourkit/docs/") || /^\.pourkit\/prd-runs\/[^/]+\.json$/.test(path9)) {
3277
- return false;
3278
- }
3279
- return true;
3280
- }
3281
-
3282
3180
  // commands/artifact-validation.ts
3283
3181
  var CONFLICT_MARKER_PATTERN = /<<<<<<<|=======|>>>>>>>/m;
3284
3182
  function invalid(options, reason, diagnostics = []) {
@@ -3299,7 +3197,7 @@ function valid(options, diagnostics = []) {
3299
3197
  };
3300
3198
  }
3301
3199
  function readArtifact(options) {
3302
- if (!existsSync7(options.artifactPath)) {
3200
+ if (!existsSync6(options.artifactPath)) {
3303
3201
  return {
3304
3202
  ok: false,
3305
3203
  result: invalid(options, `Artifact missing at ${options.artifactPath}`)
@@ -3376,7 +3274,7 @@ function validateIssueFinalReviewArtifact(parsed, options) {
3376
3274
  for (const p of parsed.changedPaths) {
3377
3275
  const normalized = p.replace(/\\/g, "/");
3378
3276
  const segments = normalized.split("/");
3379
- if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute2(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3277
+ if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute3(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3380
3278
  return {
3381
3279
  ok: false,
3382
3280
  reason: `changedPaths must not contain absolute paths or path traversal: ${p}`,
@@ -3500,7 +3398,7 @@ function validateAgentArtifact(options) {
3500
3398
  if (options.checkConflictMarkers !== false && parsed.status === "resolved") {
3501
3399
  const base = options.worktreePath ?? process.cwd();
3502
3400
  const filesWithMarkers = parsed.files.filter((file) => {
3503
- const filePath = resolve2(base, file);
3401
+ const filePath = resolve3(base, file);
3504
3402
  try {
3505
3403
  return CONFLICT_MARKER_PATTERN.test(
3506
3404
  readFileSync7(filePath, "utf-8")
@@ -3519,29 +3417,6 @@ function validateAgentArtifact(options) {
3519
3417
  }
3520
3418
  return valid(options, [`status: ${parsed.status}`]);
3521
3419
  }
3522
- // QUARANTINED: Retained for Issue Final Review artifact validation via
3523
- // `runPrdRunValidateFinalReviewCommand`. Remove when Issue Final Review
3524
- // is migrated to its own dedicated validator kind.
3525
- case "final-review": {
3526
- if (!options.prdRef || !options.checkoutBase || !options.reviewBase) {
3527
- return invalid(
3528
- options,
3529
- "Final Review artifact validation requires --prd-ref, --checkout-base, and --review-base."
3530
- );
3531
- }
3532
- const result = validateFinalReviewAgentOutputs({
3533
- artifactPath: options.artifactPath,
3534
- context: {
3535
- prdRef: options.prdRef,
3536
- prdBranch: options.checkoutBase,
3537
- mergeBase: options.reviewBase
3538
- }
3539
- });
3540
- if (!result.ok) {
3541
- return invalid(options, result.reason, result.diagnostics);
3542
- }
3543
- return valid(options, [`verdict: ${result.artifact.verdict}`]);
3544
- }
3545
3420
  case "issue-final-review": {
3546
3421
  if (options.issueNumber === void 0 || !options.branchName) {
3547
3422
  return invalid(
@@ -3581,12 +3456,6 @@ function validateAgentArtifact(options) {
3581
3456
  }
3582
3457
  return valid(options, [`decision: ${parsed.json.recoveryDecision}`]);
3583
3458
  }
3584
- case "reconciliation":
3585
- case "planning-manifest":
3586
- return invalid(
3587
- options,
3588
- `Artifact kind "${options.kind}" has been removed from PRD Run validation`
3589
- );
3590
3459
  case "local-prd":
3591
3460
  case "local-issue":
3592
3461
  case "local-triage":
@@ -3608,8 +3477,8 @@ function validateAgentArtifact(options) {
3608
3477
  }
3609
3478
  }
3610
3479
  function runValidateArtifactCommand(options) {
3611
- const artifactPath = resolve2(options.repoRoot, options.artifactPath);
3612
- const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve2(options.repoRoot, options.latestReviewArtifactPath) : void 0;
3480
+ const artifactPath = resolve3(options.repoRoot, options.artifactPath);
3481
+ const latestReviewArtifactPath = options.latestReviewArtifactPath ? resolve3(options.repoRoot, options.latestReviewArtifactPath) : void 0;
3613
3482
  return validateAgentArtifact({
3614
3483
  ...options,
3615
3484
  artifactPath,
@@ -3618,9 +3487,9 @@ function runValidateArtifactCommand(options) {
3618
3487
  });
3619
3488
  }
3620
3489
  function runLocalValidateArtifactCommand(options) {
3621
- const resolvedPath = resolve2(options.repoRoot, options.artifactPath);
3490
+ const resolvedPath = resolve3(options.repoRoot, options.artifactPath);
3622
3491
  const resolvedExtra = (options.extraArgs ?? []).map(
3623
- (p) => isAbsolute2(p) ? p : resolve2(options.repoRoot, p)
3492
+ (p) => isAbsolute3(p) ? p : resolve3(options.repoRoot, p)
3624
3493
  );
3625
3494
  let localResult;
3626
3495
  switch (options.kind) {
@@ -4366,8 +4235,8 @@ function validateLocalTriage(prdPath, issuePaths) {
4366
4235
  }
4367
4236
 
4368
4237
  // commands/issue-run.ts
4369
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
4370
- import { isAbsolute as isAbsolute3, join as join15 } from "path";
4238
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
4239
+ import { isAbsolute as isAbsolute4, join as join15 } from "path";
4371
4240
 
4372
4241
  // pr/templates.ts
4373
4242
  init_common();
@@ -4502,17 +4371,17 @@ function runEffectAndMapExit(program) {
4502
4371
  }
4503
4372
 
4504
4373
  // 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";
4374
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "fs";
4375
+ import { dirname as dirname4, join as join8 } from "path";
4507
4376
  var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
4508
4377
  function writeAttemptLog(worktreePath, entry) {
4509
4378
  const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
4510
- mkdirSync6(dirname3(logPath), { recursive: true });
4379
+ mkdirSync6(dirname4(logPath), { recursive: true });
4511
4380
  appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
4512
4381
  }
4513
4382
  function readAttemptLog(worktreePath) {
4514
4383
  const logPath = join8(worktreePath, ATTEMPT_LOG_PATH);
4515
- if (!existsSync8(logPath)) {
4384
+ if (!existsSync7(logPath)) {
4516
4385
  return [];
4517
4386
  }
4518
4387
  const raw = readFileSync8(logPath, "utf-8");
@@ -4634,7 +4503,7 @@ async function runBaseRefreshAttempt(options) {
4634
4503
  }
4635
4504
 
4636
4505
  // commands/conflict-resolution.ts
4637
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
4506
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
4638
4507
  import { join as join9 } from "path";
4639
4508
  init_common();
4640
4509
  var CONFLICT_MARKER_PATTERN2 = /<<<<<<<|=======|>>>>>>>/m;
@@ -4922,7 +4791,7 @@ async function canReachMcp(url) {
4922
4791
  return true;
4923
4792
  } catch {
4924
4793
  if (attempt < 9) {
4925
- await new Promise((resolve3) => setTimeout(resolve3, 100));
4794
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
4926
4795
  }
4927
4796
  }
4928
4797
  }
@@ -5543,7 +5412,7 @@ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
5543
5412
 
5544
5413
  // prd-run/local-merge-coordinator.ts
5545
5414
  import { execFileSync as execFileSync2 } from "child_process";
5546
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
5415
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
5547
5416
  import { join as join13 } from "path";
5548
5417
 
5549
5418
  // prd-run/local-branches.ts
@@ -5644,7 +5513,7 @@ function getIssueArtifactPath(repoRoot2, prdId, issueId) {
5644
5513
  }
5645
5514
  function readIssueBranchName(repoRoot2, prdId, issueId) {
5646
5515
  const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
5647
- if (!existsSync10(issuePath)) return null;
5516
+ if (!existsSync9(issuePath)) return null;
5648
5517
  try {
5649
5518
  const content = readFileSync12(issuePath, "utf-8");
5650
5519
  const parsed = JSON.parse(content);
@@ -5656,7 +5525,7 @@ function readIssueBranchName(repoRoot2, prdId, issueId) {
5656
5525
  async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
5657
5526
  const root = repoRoot2 ?? process.cwd();
5658
5527
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
5659
- if (!existsSync10(receiptPath)) return null;
5528
+ if (!existsSync9(receiptPath)) return null;
5660
5529
  try {
5661
5530
  const content = readFileSync12(receiptPath, "utf-8");
5662
5531
  const parsed = JSON.parse(content);
@@ -5671,7 +5540,7 @@ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
5671
5540
  async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
5672
5541
  const root = repoRoot2 ?? process.cwd();
5673
5542
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
5674
- if (existsSync10(receiptPath)) {
5543
+ if (existsSync9(receiptPath)) {
5675
5544
  try {
5676
5545
  const existing = JSON.parse(
5677
5546
  readFileSync12(receiptPath, "utf-8")
@@ -6083,7 +5952,7 @@ function runMergeCoordinator(options) {
6083
5952
  }
6084
5953
 
6085
5954
  // commands/issue-final-review-agent.ts
6086
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
5955
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
6087
5956
  import { join as join14 } from "path";
6088
5957
  var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join14(
6089
5958
  ".pourkit",
@@ -6197,12 +6066,21 @@ async function runIssueFinalReviewAgent(options) {
6197
6066
  }
6198
6067
  function loadIssueFinalReviewPrompt(repoRoot2, promptTemplate) {
6199
6068
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
6200
- const promptBody = existsSync11(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
6069
+ const promptBody = existsSync10(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
6201
6070
  return appendProtectedWorkGuidance(`${promptBody}
6202
6071
 
6203
6072
  ## Shared Run Context
6204
6073
 
6205
- Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
6074
+ Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
6075
+
6076
+ ## Initial Verification Pass
6077
+
6078
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
6079
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
6080
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
6081
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
6082
+ - 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.
6083
+ - If no verification commands are configured, note that and proceed with normal review.`);
6206
6084
  }
6207
6085
 
6208
6086
  // issues/issue-transitions.ts
@@ -6405,7 +6283,7 @@ async function advanceIssueFinalReview(options) {
6405
6283
  "Issue Final Review state is incomplete: missing artifactPath"
6406
6284
  );
6407
6285
  }
6408
- const artifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join15(worktreePath, ifrFromState.artifactPath);
6286
+ const artifactPath = isAbsolute4(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join15(worktreePath, ifrFromState.artifactPath);
6409
6287
  const validation = validateAgentArtifact({
6410
6288
  kind: "issue-final-review",
6411
6289
  artifactPath,
@@ -6437,6 +6315,12 @@ async function advanceIssueFinalReview(options) {
6437
6315
  logger,
6438
6316
  reviewArtifactPath
6439
6317
  });
6318
+ await assertCanonicalBaseAncestor({
6319
+ worktreePath,
6320
+ baseRef: `origin/${target.baseBranch}`,
6321
+ stageName: "Issue Final Review",
6322
+ logger
6323
+ });
6440
6324
  if (result.verdict === "pass") {
6441
6325
  updateWorktreeRunState(worktreePath, {
6442
6326
  issueFinalReview: {
@@ -6661,6 +6545,14 @@ async function startIssueRun(options) {
6661
6545
  };
6662
6546
  }
6663
6547
  if (executionResult.worktreePath) {
6548
+ if (shouldRunBuilder) {
6549
+ await assertCanonicalBaseAncestor({
6550
+ worktreePath: executionResult.worktreePath,
6551
+ baseRef: `origin/${effectiveBaseBranch}`,
6552
+ stageName: "Builder",
6553
+ logger
6554
+ });
6555
+ }
6664
6556
  if (worktreeState) {
6665
6557
  updateWorktreeRunState(executionResult.worktreePath, {
6666
6558
  completedStages: { builder: true }
@@ -6722,6 +6614,12 @@ async function advanceIssueRunReview(options) {
6722
6614
  }
6723
6615
  })
6724
6616
  );
6617
+ await assertCanonicalBaseAncestor({
6618
+ worktreePath: options.worktreePath,
6619
+ baseRef: `origin/${options.target.baseBranch}`,
6620
+ stageName: "Review/Refactor",
6621
+ logger: options.logger
6622
+ });
6725
6623
  updateWorktreeRunState(options.worktreePath, {
6726
6624
  review: {
6727
6625
  lifetimeIterations: reviewResult.lifetimeIterations,
@@ -6785,7 +6683,7 @@ async function completeIssueRun(options) {
6785
6683
  prTitle = finalizerFromState.title;
6786
6684
  prBody = finalizerFromState.body;
6787
6685
  } else if (finalizerFromState.artifactPath) {
6788
- if (!existsSync12(finalizerFromState.artifactPath)) {
6686
+ if (!existsSync11(finalizerFromState.artifactPath)) {
6789
6687
  throw new FinalizerFailure({
6790
6688
  message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
6791
6689
  });
@@ -6848,6 +6746,14 @@ async function completeIssueRun(options) {
6848
6746
  }
6849
6747
  prTitle = finalizerResult.title;
6850
6748
  prBody = finalizerResult.body;
6749
+ if (executionResult.worktreePath) {
6750
+ await assertCanonicalBaseAncestor({
6751
+ worktreePath: executionResult.worktreePath,
6752
+ baseRef: `origin/${effectiveBaseBranch}`,
6753
+ stageName: "Finalizer",
6754
+ logger
6755
+ });
6756
+ }
6851
6757
  }
6852
6758
  prTitle = ensureConventionalPrTitle(
6853
6759
  prTitle,
@@ -7216,18 +7122,12 @@ function getRefactorArtifactDir(artifactPath) {
7216
7122
  }
7217
7123
  async function finalizeWorktreeCommit(options) {
7218
7124
  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
- }
7125
+ await assertCanonicalBaseAncestor({
7126
+ worktreePath,
7127
+ baseRef,
7128
+ stageName: "final commit",
7129
+ logger
7130
+ });
7231
7131
  await execCapture("git", ["reset", "--soft", baseRef], {
7232
7132
  cwd: worktreePath,
7233
7133
  logger,
@@ -7244,6 +7144,21 @@ async function finalizeWorktreeCommit(options) {
7244
7144
  label: "git commit"
7245
7145
  });
7246
7146
  }
7147
+ async function assertCanonicalBaseAncestor(options) {
7148
+ const { worktreePath, baseRef, stageName, logger } = options;
7149
+ await syncRemoteBaseRef(worktreePath, baseRef, logger);
7150
+ try {
7151
+ await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
7152
+ cwd: worktreePath,
7153
+ logger,
7154
+ label: "git merge-base --is-ancestor"
7155
+ });
7156
+ } catch {
7157
+ throw new Error(
7158
+ `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.`
7159
+ );
7160
+ }
7161
+ }
7247
7162
  async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
7248
7163
  const remoteBase = parseRemoteBaseRef(baseRef);
7249
7164
  if (!remoteBase) {
@@ -7487,7 +7402,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
7487
7402
  }
7488
7403
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
7489
7404
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
7490
- const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
7405
+ const promptBody = existsSync11(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
7491
7406
  return appendProtectedWorkGuidance(`${promptBody}
7492
7407
 
7493
7408
  ## Shared Run Context
@@ -7978,24 +7893,14 @@ async function runIssueCreateCommand(args, issueProvider, logger) {
7978
7893
  }
7979
7894
 
7980
7895
  // commands/prd-run.ts
7981
- import {
7982
- cpSync,
7983
- existsSync as existsSync16,
7984
- lstatSync,
7985
- mkdirSync as mkdirSync10,
7986
- mkdtempSync,
7987
- readFileSync as readFileSync17,
7988
- realpathSync,
7989
- rmSync as rmSync3
7990
- } from "fs";
7896
+ import { existsSync as existsSync15, lstatSync, readFileSync as readFileSync18, realpathSync } from "fs";
7991
7897
  import { spawnSync as spawnSync3 } from "child_process";
7992
- import { dirname as dirname4, join as join20, relative as relative2 } from "path";
7993
- import { tmpdir } from "os";
7898
+ import { join as join19, relative as relative2 } from "path";
7994
7899
  import { Match, pipe } from "effect";
7995
7900
 
7996
7901
  // prd-run/state.ts
7997
7902
  import {
7998
- existsSync as existsSync13,
7903
+ existsSync as existsSync12,
7999
7904
  mkdirSync as mkdirSync8,
8000
7905
  readFileSync as readFileSync15,
8001
7906
  readdirSync as readdirSync4,
@@ -8003,14 +7908,14 @@ import {
8003
7908
  } from "fs";
8004
7909
  import { mkdir as mkdir4, readFile as readFile4, writeFile } from "fs/promises";
8005
7910
  import { join as join16 } from "path";
8006
- import { z as z2 } from "zod";
7911
+ import { z } from "zod";
8007
7912
  var PRD_RUN_STATE_DIR = ".pourkit/prd-runs";
8008
- var PrdRunRecordSchema = z2.object({
8009
- prdRef: z2.string().regex(
7913
+ var PrdRunRecordSchema = z.object({
7914
+ prdRef: z.string().regex(
8010
7915
  /^PRD-\d{4}$/,
8011
7916
  "PRD ref must use four-digit format (e.g., PRD-0052)"
8012
7917
  ),
8013
- status: z2.enum([
7918
+ status: z.enum([
8014
7919
  "blocked",
8015
7920
  "starting",
8016
7921
  "running",
@@ -8022,61 +7927,61 @@ var PrdRunRecordSchema = z2.object({
8022
7927
  "complete",
8023
7928
  "completed_local_branch"
8024
7929
  ]),
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()
7930
+ updatedAt: z.string().min(1),
7931
+ mode: z.enum(["github", "local"]).optional(),
7932
+ blockedGate: z.enum(["receipt-check", "branch-state", "git", "queue", "final-review"]).optional(),
7933
+ targetName: z.string().min(1).optional(),
7934
+ prdBranch: z.string().min(1).optional(),
7935
+ blockedReason: z.string().min(1).optional(),
7936
+ diagnostics: z.array(z.string()).optional(),
7937
+ offendingPaths: z.array(z.string()).optional(),
7938
+ start: z.object({
7939
+ status: z.enum(["started", "succeeded", "adopted", "reused"]),
7940
+ targetName: z.string().min(1),
7941
+ prdBranch: z.string().min(1),
7942
+ startBaseBranch: z.string().min(1),
7943
+ startBaseCommit: z.string().min(1).optional(),
7944
+ branchAction: z.enum(["created", "reused", "adopted"]).optional(),
7945
+ startedAt: z.string().min(1).optional(),
7946
+ queueStartedAt: z.string().min(1).optional(),
7947
+ queueDrainedAt: z.string().min(1).optional(),
7948
+ queueProcessedCount: z.number().int().nonnegative().optional(),
7949
+ queueCommand: z.literal("queue-run").optional(),
7950
+ refreshReceipts: z.tuple([]).optional()
8046
7951
  }).strict().optional(),
8047
- finalReview: z2.object({
8048
- status: z2.enum([
7952
+ finalReview: z.object({
7953
+ status: z.enum([
8049
7954
  "started",
8050
7955
  "succeeded",
8051
7956
  "blocked",
8052
7957
  "needs_human_review",
8053
7958
  "final_reviewed"
8054
7959
  ]),
8055
- targetName: z2.string().min(1),
8056
- prdBranch: z2.string().min(1),
8057
- mergeBase: z2.string().min(1).optional(),
8058
- verdict: z2.enum([
7960
+ targetName: z.string().min(1),
7961
+ prdBranch: z.string().min(1),
7962
+ mergeBase: z.string().min(1).optional(),
7963
+ verdict: z.enum([
8059
7964
  "pass_no_changes",
8060
7965
  "pass_with_retouch",
8061
7966
  "needs_human_review",
8062
7967
  "blocked"
8063
7968
  ]).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()
7969
+ artifactPath: z.string().min(1).optional(),
7970
+ diagnostics: z.array(z.string()).optional(),
7971
+ reviewedAt: z.string().min(1).optional(),
7972
+ retouchPrNumber: z.number().int().positive().optional(),
7973
+ retouchPrUrl: z.string().url().optional(),
7974
+ retouchMergeCommit: z.string().min(1).optional(),
7975
+ autoMerge: z.boolean().optional(),
7976
+ changedPaths: z.array(z.string()).optional()
8072
7977
  }).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)
7978
+ scopeChanges: z.array(
7979
+ z.object({
7980
+ issueNumber: z.number().int().positive(),
7981
+ decision: z.literal("accepted_scope_change"),
7982
+ reason: z.string().min(1),
7983
+ acceptedBy: z.string().min(1),
7984
+ acceptedAt: z.string().min(1)
8080
7985
  }).strict()
8081
7986
  ).optional()
8082
7987
  }).strict();
@@ -8093,7 +7998,7 @@ function normalizePrdRunRef(ref) {
8093
7998
  function readPrdRun(repoRoot2, prdRef) {
8094
7999
  const normalized = normalizePrdRunRef(prdRef);
8095
8000
  const recordPath = getRecordPath(repoRoot2, normalized);
8096
- if (!existsSync13(recordPath)) {
8001
+ if (!existsSync12(recordPath)) {
8097
8002
  return { record: null, diagnostics: [] };
8098
8003
  }
8099
8004
  try {
@@ -8124,7 +8029,7 @@ function readPrdRun(repoRoot2, prdRef) {
8124
8029
  }
8125
8030
  function listPrdRuns(repoRoot2) {
8126
8031
  const stateDir = join16(repoRoot2, PRD_RUN_STATE_DIR);
8127
- if (!existsSync13(stateDir)) {
8032
+ if (!existsSync12(stateDir)) {
8128
8033
  return { records: [], diagnostics: [] };
8129
8034
  }
8130
8035
  const records = [];
@@ -8168,40 +8073,40 @@ function normalizeLegacyPrdRunRecord(record) {
8168
8073
  offendingPaths: void 0
8169
8074
  };
8170
8075
  }
8171
- var LocalPrdRunRecordSchema = z2.object({
8172
- prdId: z2.string().regex(
8076
+ var LocalPrdRunRecordSchema = z.object({
8077
+ prdId: z.string().regex(
8173
8078
  /^PRD-\d{4}$/,
8174
8079
  "PRD id must use four-digit format (e.g., PRD-0052)"
8175
8080
  ),
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)
8081
+ createdAt: z.string().min(1),
8082
+ receipts: z.object({
8083
+ start: z.object({
8084
+ startedAt: z.string().min(1),
8085
+ branch: z.string().min(1),
8086
+ baseCommit: z.string().min(1)
8182
8087
  }).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([
8088
+ queue: z.object({ completedAt: z.string().min(1) }).strict().optional(),
8089
+ finalReview: z.object({
8090
+ completedAt: z.string().min(1),
8091
+ targetName: z.string().optional(),
8092
+ prdBranch: z.string().optional(),
8093
+ mergeBase: z.string().optional(),
8094
+ verdict: z.enum([
8190
8095
  "pass_no_changes",
8191
8096
  "pass_with_retouch",
8192
8097
  "needs_human_review",
8193
8098
  "blocked"
8194
8099
  ]).optional(),
8195
- diagnostics: z2.array(z2.string()).optional(),
8196
- artifactPath: z2.string().optional()
8100
+ diagnostics: z.array(z.string()).optional(),
8101
+ artifactPath: z.string().optional()
8197
8102
  }).strict().optional(),
8198
- complete: z2.object({
8199
- completedAt: z2.string().min(1),
8200
- branch: z2.string().min(1),
8201
- stages: z2.array(z2.string())
8103
+ complete: z.object({
8104
+ completedAt: z.string().min(1),
8105
+ branch: z.string().min(1),
8106
+ stages: z.array(z.string())
8202
8107
  }).strict().optional()
8203
8108
  }).strict(),
8204
- metadata: z2.record(z2.unknown())
8109
+ metadata: z.record(z.unknown())
8205
8110
  }).strict();
8206
8111
  function getRecordPath(repoRoot2, prdRef) {
8207
8112
  return join16(
@@ -8211,29 +8116,12 @@ function getRecordPath(repoRoot2, prdRef) {
8211
8116
  );
8212
8117
  }
8213
8118
 
8214
- // prd-run/evidence-packet.ts
8215
- import { createHash as createHash2 } from "crypto";
8216
- import { z as z3 } from "zod";
8217
- 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()
8227
- }),
8228
- stage: StageSchema,
8229
- stageReceipts: z3.record(z3.string(), z3.unknown())
8230
- }).strict();
8231
-
8232
- // commands/prd-run.ts
8233
- init_common();
8119
+ // prd-run/local-queue-loop.ts
8120
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
8121
+ import { join as join18 } from "path";
8234
8122
 
8235
8123
  // prd-run/local-artifacts.ts
8236
- import { existsSync as existsSync14 } from "fs";
8124
+ import { existsSync as existsSync13 } from "fs";
8237
8125
  import { join as join17 } from "path";
8238
8126
  var REQUIRED_PRD_FIELDS = [
8239
8127
  "schemaVersion",
@@ -8286,7 +8174,7 @@ function hasRequiredFields(data, requiredFields) {
8286
8174
  async function resolveLocalPrdArtifact(prdId, repoRoot2) {
8287
8175
  const root = repoRoot2 ?? process.cwd();
8288
8176
  const prdPath = prdArtifactPath(root, prdId);
8289
- if (!existsSync14(prdPath)) {
8177
+ if (!existsSync13(prdPath)) {
8290
8178
  return {
8291
8179
  ok: false,
8292
8180
  failureCode: "missing_prd_artifact",
@@ -8331,7 +8219,7 @@ async function resolveLocalIssueArtifacts(prdId, repoRoot2) {
8331
8219
  const issues = [];
8332
8220
  for (const childId of childIssueIds) {
8333
8221
  const issuePath = issueArtifactPath(root, prdId, childId);
8334
- if (!existsSync14(issuePath)) {
8222
+ if (!existsSync13(issuePath)) {
8335
8223
  return {
8336
8224
  ok: false,
8337
8225
  failureCode: "missing_child_issue",
@@ -8348,100 +8236,33 @@ async function resolveLocalIssueArtifacts(prdId, repoRoot2) {
8348
8236
  message: `Failed to read or parse Issue artifact at ${issuePath}: ${result.message}`
8349
8237
  };
8350
8238
  }
8351
- const missing = hasRequiredFields(
8352
- result.data,
8353
- REQUIRED_ISSUE_FIELDS
8354
- );
8355
- if (missing) {
8356
- return {
8357
- ok: false,
8358
- failureCode: "invalid_artifact",
8359
- missingId: childId,
8360
- message: `Issue artifact at ${issuePath} is missing required field: ${missing}`
8361
- };
8362
- }
8363
- if (result.data.kind !== "issue") {
8364
- return {
8365
- ok: false,
8366
- failureCode: "invalid_artifact",
8367
- missingId: childId,
8368
- message: `Issue artifact at ${issuePath} has kind "${result.data.kind}", expected "issue"`
8369
- };
8370
- }
8371
- issues.push(result.data);
8372
- }
8373
- return { ok: true, data: issues };
8374
- }
8375
-
8376
- // prd-run/local-issue-adapter.ts
8377
- var VALID_TRIAGE_LABELS = /* @__PURE__ */ new Set([
8378
- "needs-triage",
8379
- "ready-for-agent",
8380
- "blocked",
8381
- "ready-for-human",
8382
- "wontfix"
8383
- ]);
8384
- function countTypeLabels(labels) {
8385
- return labels.filter((l) => l.startsWith("type:")).length;
8386
- }
8387
- function countTriageLabels(labels) {
8388
- return labels.filter((l) => VALID_TRIAGE_LABELS.has(l)).length;
8389
- }
8390
- function countCategoryLabels(labels) {
8391
- const triageCount = countTriageLabels(labels);
8392
- const typeCount = countTypeLabels(labels);
8393
- return labels.length - triageCount - typeCount;
8394
- }
8395
- function hasValidLabels(labels) {
8396
- if (countTriageLabels(labels) !== 1) return false;
8397
- if (!labels.includes("ready-for-agent")) return false;
8398
- if (countTypeLabels(labels) !== 1) return false;
8399
- if (countCategoryLabels(labels) !== 1) return false;
8400
- return true;
8401
- }
8402
- function isIssueBlockedByDependencies(issue, issues) {
8403
- const blockers = [];
8404
- for (const depId of issue.dependencyIssueIds) {
8405
- const dep = issues.find((i) => i.id === depId);
8406
- if (!dep || dep.receipts.completedAt === null) {
8407
- blockers.push(depId);
8408
- }
8409
- }
8410
- return blockers;
8411
- }
8412
- async function getRunnableLocalIssues(prdId, repoRoot2) {
8413
- const issuesResult = await resolveLocalIssueArtifacts(prdId, repoRoot2);
8414
- if (!issuesResult.ok) return [];
8415
- const issues = issuesResult.data;
8416
- const runnable = [];
8417
- for (const issue of issues) {
8418
- if (!hasValidLabels(issue.labels)) continue;
8419
- if (issue.status !== "ready-for-agent") continue;
8420
- const blockers = isIssueBlockedByDependencies(issue, issues);
8421
- runnable.push({
8422
- id: issue.id,
8423
- title: issue.title,
8424
- labels: issue.labels,
8425
- blockedBy: blockers.length > 0 ? blockers : void 0
8426
- });
8239
+ const missing = hasRequiredFields(
8240
+ result.data,
8241
+ REQUIRED_ISSUE_FIELDS
8242
+ );
8243
+ if (missing) {
8244
+ return {
8245
+ ok: false,
8246
+ failureCode: "invalid_artifact",
8247
+ missingId: childId,
8248
+ message: `Issue artifact at ${issuePath} is missing required field: ${missing}`
8249
+ };
8250
+ }
8251
+ if (result.data.kind !== "issue") {
8252
+ return {
8253
+ ok: false,
8254
+ failureCode: "invalid_artifact",
8255
+ missingId: childId,
8256
+ message: `Issue artifact at ${issuePath} has kind "${result.data.kind}", expected "issue"`
8257
+ };
8258
+ }
8259
+ issues.push(result.data);
8427
8260
  }
8428
- const filtered = runnable.filter(
8429
- (i) => !i.blockedBy || i.blockedBy.length === 0
8430
- );
8431
- return filtered;
8261
+ return { ok: true, data: issues };
8432
8262
  }
8433
8263
 
8434
- // prd-run/local-final-review.ts
8435
- import { execFileSync as execFileSync3, execSync as execSync2 } from "child_process";
8436
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "fs";
8437
- import { join as join18 } from "path";
8438
-
8439
- // prd-run/local-queue-loop.ts
8440
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
8441
- import { join as join19 } from "path";
8442
-
8443
8264
  // prd-run/local-issue-run.ts
8444
- import { execFileSync as execFileSync4 } from "child_process";
8265
+ import { execFileSync as execFileSync3 } from "child_process";
8445
8266
  init_common();
8446
8267
  async function createLocalIssueBranch(prdId, issueId, repoRoot2) {
8447
8268
  const root = repoRoot2 ?? process.cwd();
@@ -8465,7 +8286,7 @@ async function createLocalIssueBranch(prdId, issueId, repoRoot2) {
8465
8286
  }
8466
8287
  const localPrdBranch = `local/${prdId}`;
8467
8288
  try {
8468
- execFileSync4(
8289
+ execFileSync3(
8469
8290
  "git",
8470
8291
  ["show-ref", "--verify", "--quiet", `refs/heads/${localPrdBranch}`],
8471
8292
  {
@@ -8481,7 +8302,7 @@ async function createLocalIssueBranch(prdId, issueId, repoRoot2) {
8481
8302
  }
8482
8303
  let branchExists = false;
8483
8304
  try {
8484
- execFileSync4(
8305
+ execFileSync3(
8485
8306
  "git",
8486
8307
  ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
8487
8308
  {
@@ -8494,7 +8315,7 @@ async function createLocalIssueBranch(prdId, issueId, repoRoot2) {
8494
8315
  } catch {
8495
8316
  }
8496
8317
  if (!branchExists) {
8497
- execFileSync4("git", ["checkout", "-b", branchName, localPrdBranch], {
8318
+ execFileSync3("git", ["checkout", "-b", branchName, localPrdBranch], {
8498
8319
  cwd: root,
8499
8320
  encoding: "utf8",
8500
8321
  stdio: "pipe"
@@ -8559,11 +8380,69 @@ async function runLocalIssue(prdId, issueId, builder, repoRoot2) {
8559
8380
  }
8560
8381
  }
8561
8382
 
8383
+ // prd-run/local-issue-adapter.ts
8384
+ var VALID_TRIAGE_LABELS = /* @__PURE__ */ new Set([
8385
+ "needs-triage",
8386
+ "ready-for-agent",
8387
+ "blocked",
8388
+ "ready-for-human",
8389
+ "wontfix"
8390
+ ]);
8391
+ function countTypeLabels(labels) {
8392
+ return labels.filter((l) => l.startsWith("type:")).length;
8393
+ }
8394
+ function countTriageLabels(labels) {
8395
+ return labels.filter((l) => VALID_TRIAGE_LABELS.has(l)).length;
8396
+ }
8397
+ function countCategoryLabels(labels) {
8398
+ const triageCount = countTriageLabels(labels);
8399
+ const typeCount = countTypeLabels(labels);
8400
+ return labels.length - triageCount - typeCount;
8401
+ }
8402
+ function hasValidLabels(labels) {
8403
+ if (countTriageLabels(labels) !== 1) return false;
8404
+ if (!labels.includes("ready-for-agent")) return false;
8405
+ if (countTypeLabels(labels) !== 1) return false;
8406
+ if (countCategoryLabels(labels) !== 1) return false;
8407
+ return true;
8408
+ }
8409
+ function isIssueBlockedByDependencies(issue, issues) {
8410
+ const blockers = [];
8411
+ for (const depId of issue.dependencyIssueIds) {
8412
+ const dep = issues.find((i) => i.id === depId);
8413
+ if (!dep || dep.receipts.completedAt === null) {
8414
+ blockers.push(depId);
8415
+ }
8416
+ }
8417
+ return blockers;
8418
+ }
8419
+ async function getRunnableLocalIssues(prdId, repoRoot2) {
8420
+ const issuesResult = await resolveLocalIssueArtifacts(prdId, repoRoot2);
8421
+ if (!issuesResult.ok) return [];
8422
+ const issues = issuesResult.data;
8423
+ const runnable = [];
8424
+ for (const issue of issues) {
8425
+ if (!hasValidLabels(issue.labels)) continue;
8426
+ if (issue.status !== "ready-for-agent") continue;
8427
+ const blockers = isIssueBlockedByDependencies(issue, issues);
8428
+ runnable.push({
8429
+ id: issue.id,
8430
+ title: issue.title,
8431
+ labels: issue.labels,
8432
+ blockedBy: blockers.length > 0 ? blockers : void 0
8433
+ });
8434
+ }
8435
+ const filtered = runnable.filter(
8436
+ (i) => !i.blockedBy || i.blockedBy.length === 0
8437
+ );
8438
+ return filtered;
8439
+ }
8440
+
8562
8441
  // prd-run/local-queue-loop.ts
8563
8442
  var CHILD_CLEANUP_LABELS = ["agent-in-progress", "pr-open-awaiting-merge"];
8564
8443
  var LOCAL_ISSUE_ID_REGEX = /^I-(\d+)$/i;
8565
8444
  function getIssueArtifactPath2(repoRoot2, prdId, issueId) {
8566
- return join19(
8445
+ return join18(
8567
8446
  repoRoot2,
8568
8447
  ".pourkit",
8569
8448
  "local-prd-runs",
@@ -8584,7 +8463,7 @@ function readIssueArtifact(repoRoot2, prdId, issueId) {
8584
8463
  }
8585
8464
  }
8586
8465
  function writeIssueArtifact(repoRoot2, prdId, issue) {
8587
- writeFileSync7(
8466
+ writeFileSync6(
8588
8467
  getIssueArtifactPath2(repoRoot2, prdId, issue.id),
8589
8468
  JSON.stringify(issue, null, 2),
8590
8469
  "utf-8"
@@ -9250,6 +9129,9 @@ async function runQueueCommand(options) {
9250
9129
  return runEffectAndMapExit(runQueueLoop(queueOptions));
9251
9130
  }
9252
9131
 
9132
+ // prd-run/final-review-validation.ts
9133
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
9134
+
9253
9135
  // commands/prd-run.ts
9254
9136
  function planLaunchResume(record) {
9255
9137
  if (!record) return { attempted: [], skipped: [], resumed: [] };
@@ -9311,18 +9193,18 @@ function planLaunchResume(record) {
9311
9193
  );
9312
9194
  }
9313
9195
  function validateLocalStartStore(repoRoot2, prdRef) {
9314
- const localStoreDir = join20(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
9196
+ const localStoreDir = join19(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
9315
9197
  let localStoreReady = false;
9316
- if (existsSync16(localStoreDir)) {
9317
- const localStorePath = join20(localStoreDir, "prd.json");
9198
+ if (existsSync15(localStoreDir)) {
9199
+ const localStorePath = join19(localStoreDir, "prd.json");
9318
9200
  try {
9319
- const content = JSON.parse(readFileSync17(localStorePath, "utf8"));
9201
+ const content = JSON.parse(readFileSync18(localStorePath, "utf8"));
9320
9202
  localStoreReady = content?.id === prdRef && content?.kind === "prd";
9321
9203
  } catch {
9322
9204
  localStoreReady = false;
9323
9205
  }
9324
9206
  }
9325
- if (existsSync16(localStoreDir) && !localStoreReady) {
9207
+ if (existsSync15(localStoreDir) && !localStoreReady) {
9326
9208
  return {
9327
9209
  ok: false,
9328
9210
  gate: "branch-state",
@@ -9486,13 +9368,13 @@ async function runPrdRunLaunchCommand(options) {
9486
9368
  attempted.push("queue");
9487
9369
  }
9488
9370
  const currentRecord = readPrdRun(options.repoRoot, prdRef).record;
9489
- const localStorePath = join20(
9371
+ const localStorePath = join19(
9490
9372
  options.repoRoot,
9491
9373
  ".pourkit",
9492
9374
  "local-prd-runs",
9493
9375
  prdRef
9494
9376
  );
9495
- if (existsSync16(localStorePath)) {
9377
+ if (existsSync15(localStorePath)) {
9496
9378
  writePrdRunRecord(options.repoRoot, {
9497
9379
  prdRef,
9498
9380
  status: "completed_local_branch",
@@ -10003,8 +9885,8 @@ async function processStartResult(startResult, options) {
10003
9885
  start,
10004
9886
  mode: modeForRecord
10005
9887
  });
10006
- const localStorePath = join20(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
10007
- if (existsSync16(localStorePath)) {
9888
+ const localStorePath = join19(repoRoot2, ".pourkit", "local-prd-runs", prdRef);
9889
+ if (existsSync15(localStorePath)) {
10008
9890
  const queueResult = await runLocalQueueLoop(
10009
9891
  prdRef,
10010
9892
  repoRoot2,
@@ -10892,7 +10774,7 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
10892
10774
  }
10893
10775
 
10894
10776
  // commands/init.ts
10895
- import { existsSync as existsSync17, statSync } from "fs";
10777
+ import { existsSync as existsSync16, statSync } from "fs";
10896
10778
  import {
10897
10779
  copyFile,
10898
10780
  mkdir as mkdir5,
@@ -10901,7 +10783,7 @@ import {
10901
10783
  rename,
10902
10784
  writeFile as writeFile2
10903
10785
  } from "fs/promises";
10904
- import { createHash as createHash3, randomUUID } from "crypto";
10786
+ import { createHash as createHash2, randomUUID } from "crypto";
10905
10787
  import path5 from "path";
10906
10788
  import { execFile as execFile2 } from "child_process";
10907
10789
  import { promisify as promisify2 } from "util";
@@ -11115,135 +10997,120 @@ function inferVerificationCommands(scripts, pm) {
11115
10997
  }
11116
10998
  function generateConfigTemplate(options) {
11117
10999
  const {
11118
- sourceRoot,
11119
- targetRoot,
11120
- packageManager,
11121
11000
  baseBranch,
11001
+ packageManager,
11122
11002
  verificationCommands,
11123
11003
  hasPackageJson = true,
11124
11004
  labels: maybeLabels
11125
11005
  } = options;
11126
11006
  const labels = maybeLabels ?? DEFAULT_RUNNER_LABELS;
11127
- const relPath = path5.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
11128
- const importPath = relPath || ".";
11129
11007
  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,
11008
+ const target = {
11009
+ name: "default",
11010
+ baseBranch,
11011
+ branchTemplate: "pourkit/{{issue.number}}/{{issue.slug}}",
11012
+ autoMerge: false,
11013
+ strategy: {
11014
+ type: "review-refactor-loop",
11015
+ implement: {
11016
+ builder: {
11017
+ agent: "pourkit-builder",
11018
+ model: "opencode-go/deepseek-v4-flash",
11019
+ promptTemplate: ".pourkit/prompts/builder.prompt.md"
11020
+ }
11021
+ },
11022
+ failureResolution: {
11023
+ agent: "pourkit-failure-resolution-agent",
11024
+ model: "opencode-go/mimo-v2.5",
11025
+ promptTemplate: ".pourkit/prompts/failure-resolution.prompt.md",
11026
+ maxAttemptsPerFailure: 1
11027
+ },
11028
+ review: {
11029
+ reviewer: {
11030
+ agent: "pourkit-reviewer",
11031
+ model: "opencode-go/deepseek-v4-pro",
11032
+ promptTemplate: ".pourkit/prompts/reviewer.prompt.md",
11033
+ criteria: ["correctness", "scope", "tests", "quality"]
11193
11034
  },
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,
11035
+ refactor: {
11036
+ agent: "pourkit-refactor",
11037
+ model: "opencode-go/qwen3.6-plus",
11038
+ promptTemplate: ".pourkit/prompts/refactor.prompt.md"
11202
11039
  },
11040
+ maxIterations: 3,
11041
+ passWithNotesRefactorAttempts: 2
11203
11042
  },
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,
11043
+ issueFinalReview: {
11044
+ agent: "pourkit-reviewer",
11045
+ model: "opencode-go/deepseek-v4-pro",
11046
+ promptTemplate: ".pourkit/prompts/issue-final-review.prompt.md",
11047
+ maxAttempts: 3
11221
11048
  },
11222
- {
11223
- hostPath: "~/.config/opencode",
11224
- sandboxPath: "/home/agent/.config/opencode",
11225
- readonly: true,
11049
+ finalize: {
11050
+ prDescriptionAgent: {
11051
+ agent: "pourkit-pr-description",
11052
+ model: "opencode-go/deepseek-v4-flash",
11053
+ promptTemplate: ".pourkit/prompts/pr-description.prompt.md"
11054
+ },
11055
+ maxAttempts: 2
11056
+ }
11057
+ }
11058
+ };
11059
+ if (hasPackageJson) {
11060
+ target.setupCommands = [
11061
+ { command: setupCommand, label: "install" }
11062
+ ];
11063
+ }
11064
+ if (verificationCommands.length > 0) {
11065
+ target.strategy.verify = {
11066
+ commands: verificationCommands
11067
+ };
11068
+ }
11069
+ const config = {
11070
+ $schema: "./schema/pourkit.schema.json",
11071
+ schemaVersion: 1,
11072
+ targets: [target],
11073
+ labels: {
11074
+ readyForAgent: labels.readyForAgent,
11075
+ agentInProgress: labels.agentInProgress,
11076
+ blocked: labels.blocked,
11077
+ prOpenAwaitingMerge: labels.prOpenAwaitingMerge,
11078
+ readyForHuman: labels.readyForHuman
11079
+ },
11080
+ sandbox: {
11081
+ provider: "docker",
11082
+ copyToWorktree: ["node_modules"],
11083
+ mounts: [
11084
+ {
11085
+ hostPath: "~/.local/share/opencode",
11086
+ sandboxPath: "/home/agent/.local/share/opencode",
11087
+ readonly: false
11088
+ },
11089
+ {
11090
+ hostPath: "~/.config/opencode",
11091
+ sandboxPath: "/home/agent/.config/opencode",
11092
+ readonly: true
11093
+ }
11094
+ ],
11095
+ env: {
11096
+ HOME: "/home/agent",
11097
+ XDG_DATA_HOME: "/home/agent/.local/share",
11098
+ XDG_CONFIG_HOME: "/home/agent/.config",
11099
+ XDG_STATE_HOME: "/home/agent/.local/state",
11100
+ XDG_CACHE_HOME: "/home/agent/.cache"
11226
11101
  },
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",
11102
+ idleTimeoutSeconds: 300
11234
11103
  },
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
- `;
11104
+ checks: {
11105
+ requiredLabels: [],
11106
+ allowedAuthors: [],
11107
+ checksFoundTimeoutSeconds: 60,
11108
+ checksCompletionTimeoutSeconds: 1800,
11109
+ pollIntervalSeconds: 15,
11110
+ issueListLimit: 50
11111
+ }
11112
+ };
11113
+ return JSON.stringify(config, null, 2) + "\n";
11247
11114
  }
11248
11115
  function generateOpenCodeConfig(existingConfig = {}) {
11249
11116
  const { $schema, ...rest } = existingConfig;
@@ -11392,10 +11259,10 @@ async function walkDir(dir) {
11392
11259
  }
11393
11260
  async function computeFileChecksum(filePath) {
11394
11261
  const content = await readFile5(filePath);
11395
- return createHash3("sha256").update(content).digest("hex");
11262
+ return createHash2("sha256").update(content).digest("hex");
11396
11263
  }
11397
11264
  function lockfileExists(root, name) {
11398
- return existsSync17(path5.join(root, name));
11265
+ return existsSync16(path5.join(root, name));
11399
11266
  }
11400
11267
  function detectPackageManager(root) {
11401
11268
  if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
@@ -11439,7 +11306,7 @@ async function discoverLocalSource(sourcePath) {
11439
11306
  async function discoverReadme(root) {
11440
11307
  for (const name of ["README.md", "readme.md"]) {
11441
11308
  const p = path5.join(root, name);
11442
- if (existsSync17(p)) {
11309
+ if (existsSync16(p)) {
11443
11310
  return p;
11444
11311
  }
11445
11312
  }
@@ -11449,7 +11316,7 @@ async function discoverAgentFiles(root) {
11449
11316
  const files = [];
11450
11317
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
11451
11318
  const p = path5.join(root, name);
11452
- if (existsSync17(p)) {
11319
+ if (existsSync16(p)) {
11453
11320
  files.push(p);
11454
11321
  }
11455
11322
  }
@@ -11457,7 +11324,7 @@ async function discoverAgentFiles(root) {
11457
11324
  }
11458
11325
  async function discoverMerlleState(root) {
11459
11326
  const p = path5.join(root, ".pourkit", "state.json");
11460
- return existsSync17(p) ? p : null;
11327
+ return existsSync16(p) ? p : null;
11461
11328
  }
11462
11329
  async function discoverAgentSkills(root) {
11463
11330
  const dirs = [
@@ -11466,7 +11333,7 @@ async function discoverAgentSkills(root) {
11466
11333
  ];
11467
11334
  const found = [];
11468
11335
  for (const d of dirs) {
11469
- if (existsSync17(d)) {
11336
+ if (existsSync16(d)) {
11470
11337
  found.push(d);
11471
11338
  }
11472
11339
  }
@@ -11476,12 +11343,12 @@ async function discoverRootDomainDocs(root) {
11476
11343
  const docs = [];
11477
11344
  for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
11478
11345
  const p = path5.join(root, name);
11479
- if (existsSync17(p)) {
11346
+ if (existsSync16(p)) {
11480
11347
  docs.push(p);
11481
11348
  }
11482
11349
  }
11483
11350
  const adrDir = path5.join(root, "docs", "adr");
11484
- if (existsSync17(adrDir)) {
11351
+ if (existsSync16(adrDir)) {
11485
11352
  const entries = await readdir(adrDir, { withFileTypes: true });
11486
11353
  for (const entry of entries) {
11487
11354
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -11624,7 +11491,7 @@ async function planInit(options) {
11624
11491
  for (const file of skillFiles) {
11625
11492
  const relPath = path5.relative(s, file);
11626
11493
  const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
11627
- if (!existsSync17(destPath)) {
11494
+ if (!existsSync16(destPath)) {
11628
11495
  operations.push({
11629
11496
  kind: "copy",
11630
11497
  sourcePath: file,
@@ -11687,7 +11554,7 @@ async function planInit(options) {
11687
11554
  });
11688
11555
  }
11689
11556
  if (sourceRoot) {
11690
- if (!existsSync17(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
11557
+ if (!existsSync16(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
11691
11558
  warnings.push(
11692
11559
  `--from-local path does not exist or is not a directory: ${sourceRoot}`
11693
11560
  );
@@ -11806,7 +11673,7 @@ async function planInit(options) {
11806
11673
  requiresConfirmation: false,
11807
11674
  destructive: false
11808
11675
  });
11809
- } else if (existsSync17(destPath)) {
11676
+ } else if (existsSync16(destPath)) {
11810
11677
  operations.push({
11811
11678
  kind: "skip",
11812
11679
  path: destPath,
@@ -11864,7 +11731,7 @@ async function planInit(options) {
11864
11731
  }
11865
11732
  }
11866
11733
  const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
11867
- if (!existsSync17(contextPath) && !merleDestPaths.has(contextPath)) {
11734
+ if (!existsSync16(contextPath) && !merleDestPaths.has(contextPath)) {
11868
11735
  operations.push({
11869
11736
  kind: "create",
11870
11737
  path: contextPath,
@@ -11882,7 +11749,7 @@ async function planInit(options) {
11882
11749
  "adr",
11883
11750
  ".gitkeep"
11884
11751
  );
11885
- if (!existsSync17(adrGitkeep)) {
11752
+ if (!existsSync16(adrGitkeep)) {
11886
11753
  operations.push({
11887
11754
  kind: "create",
11888
11755
  path: adrGitkeep,
@@ -11894,7 +11761,7 @@ async function planInit(options) {
11894
11761
  }
11895
11762
  const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
11896
11763
  const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
11897
- if (existsSync17(srcDocAgents) && !existsSync17(tgtDocAgents)) {
11764
+ if (existsSync16(srcDocAgents) && !existsSync16(tgtDocAgents)) {
11898
11765
  const docFiles = await walkDir(srcDocAgents);
11899
11766
  for (const file of docFiles) {
11900
11767
  const relPath = path5.relative(srcDocAgents, file);
@@ -11927,7 +11794,7 @@ async function planInit(options) {
11927
11794
  }
11928
11795
  const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
11929
11796
  const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
11930
- if (existsSync17(srcPrompts) && !existsSync17(tgtPrompts)) {
11797
+ if (existsSync16(srcPrompts) && !existsSync16(tgtPrompts)) {
11931
11798
  const promptFiles = await walkDir(srcPrompts);
11932
11799
  for (const file of promptFiles) {
11933
11800
  const relPath = path5.relative(srcPrompts, file);
@@ -11954,7 +11821,7 @@ async function planInit(options) {
11954
11821
  ".sandcastle",
11955
11822
  "Dockerfile"
11956
11823
  );
11957
- if (existsSync17(tgtSandboxDockerfile)) {
11824
+ if (existsSync16(tgtSandboxDockerfile)) {
11958
11825
  operations.push({
11959
11826
  kind: "skip",
11960
11827
  path: tgtSandboxDockerfile,
@@ -11963,7 +11830,7 @@ async function planInit(options) {
11963
11830
  requiresConfirmation: false,
11964
11831
  destructive: false
11965
11832
  });
11966
- } else if (existsSync17(srcSandboxDockerfile)) {
11833
+ } else if (existsSync16(srcSandboxDockerfile)) {
11967
11834
  const checksum = await computeFileChecksum(srcSandboxDockerfile);
11968
11835
  operations.push({
11969
11836
  kind: "copy",
@@ -11976,15 +11843,13 @@ async function planInit(options) {
11976
11843
  checksum
11977
11844
  });
11978
11845
  }
11979
- const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
11980
- if (!existsSync17(configTsPath)) {
11846
+ const configJsonPath = path5.join(targetRoot, ".pourkit", "config.json");
11847
+ if (!existsSync16(configJsonPath)) {
11981
11848
  const verifyCommands = inferVerificationCommands(
11982
11849
  packageScripts,
11983
11850
  pm || "npm"
11984
11851
  );
11985
11852
  const configContent = generateConfigTemplate({
11986
- targetRoot,
11987
- sourceRoot,
11988
11853
  packageManager: pm || "npm",
11989
11854
  baseBranch,
11990
11855
  verificationCommands: verifyCommands,
@@ -11993,9 +11858,9 @@ async function planInit(options) {
11993
11858
  });
11994
11859
  operations.push({
11995
11860
  kind: "create",
11996
- path: configTsPath,
11861
+ path: configJsonPath,
11997
11862
  ownership: "managed",
11998
- reason: "Init pourkit.config.ts template",
11863
+ reason: "Init .pourkit/config.json template",
11999
11864
  requiresConfirmation: false,
12000
11865
  destructive: false,
12001
11866
  content: configContent
@@ -12003,18 +11868,90 @@ async function planInit(options) {
12003
11868
  } else {
12004
11869
  operations.push({
12005
11870
  kind: "skip",
12006
- path: configTsPath,
11871
+ path: configJsonPath,
12007
11872
  ownership: "project-owned",
12008
- reason: "Existing pourkit.config.ts (project-owned)",
11873
+ reason: "Existing .pourkit/config.json (project-owned)",
12009
11874
  requiresConfirmation: false,
12010
11875
  destructive: false
12011
11876
  });
12012
11877
  }
11878
+ const schemaJsonPath = path5.join(
11879
+ targetRoot,
11880
+ ".pourkit",
11881
+ "schema",
11882
+ "pourkit.schema.json"
11883
+ );
11884
+ const srcSchemaJson = path5.join(
11885
+ sourceRoot,
11886
+ "pourkit",
11887
+ "schema",
11888
+ "pourkit.schema.json"
11889
+ );
11890
+ if (existsSync16(srcSchemaJson)) {
11891
+ const checksum = await computeFileChecksum(srcSchemaJson);
11892
+ if (!existsSync16(schemaJsonPath)) {
11893
+ operations.push({
11894
+ kind: "copy",
11895
+ sourcePath: srcSchemaJson,
11896
+ path: schemaJsonPath,
11897
+ ownership: "managed",
11898
+ reason: "Copy schema: pourkit.schema.json",
11899
+ requiresConfirmation: false,
11900
+ destructive: false,
11901
+ checksum
11902
+ });
11903
+ } else {
11904
+ operations.push({
11905
+ kind: "skip",
11906
+ path: schemaJsonPath,
11907
+ ownership: "project-owned",
11908
+ reason: "Existing .pourkit/schema/pourkit.schema.json (project-owned)",
11909
+ requiresConfirmation: false,
11910
+ destructive: false
11911
+ });
11912
+ }
11913
+ }
11914
+ const schemaHashPath = path5.join(
11915
+ targetRoot,
11916
+ ".pourkit",
11917
+ "schema",
11918
+ "pourkit.schema.hash"
11919
+ );
11920
+ const srcSchemaHash = path5.join(
11921
+ sourceRoot,
11922
+ "pourkit",
11923
+ "schema",
11924
+ "pourkit.schema.hash"
11925
+ );
11926
+ if (existsSync16(srcSchemaHash)) {
11927
+ const checksum = await computeFileChecksum(srcSchemaHash);
11928
+ if (!existsSync16(schemaHashPath)) {
11929
+ operations.push({
11930
+ kind: "copy",
11931
+ sourcePath: srcSchemaHash,
11932
+ path: schemaHashPath,
11933
+ ownership: "managed",
11934
+ reason: "Copy schema hash: pourkit.schema.hash",
11935
+ requiresConfirmation: false,
11936
+ destructive: false,
11937
+ checksum
11938
+ });
11939
+ } else {
11940
+ operations.push({
11941
+ kind: "skip",
11942
+ path: schemaHashPath,
11943
+ ownership: "project-owned",
11944
+ reason: "Existing .pourkit/schema/pourkit.schema.hash (project-owned)",
11945
+ requiresConfirmation: false,
11946
+ destructive: false
11947
+ });
11948
+ }
11949
+ }
12013
11950
  const agentFileMode = options.conflictPolicy?.agentFile ?? "both";
12014
11951
  const hasExistingAgents = operations.some(
12015
11952
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
12016
11953
  );
12017
- if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync17(path5.join(targetRoot, "AGENTS.md"))) {
11954
+ if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync16(path5.join(targetRoot, "AGENTS.md"))) {
12018
11955
  operations.push({
12019
11956
  kind: "create",
12020
11957
  path: path5.join(targetRoot, "AGENTS.md"),
@@ -12030,7 +11967,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
12030
11967
  const hasExistingClaude = operations.some(
12031
11968
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
12032
11969
  );
12033
- if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync17(path5.join(targetRoot, "CLAUDE.md"))) {
11970
+ if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync16(path5.join(targetRoot, "CLAUDE.md"))) {
12034
11971
  operations.push({
12035
11972
  kind: "create",
12036
11973
  path: path5.join(targetRoot, "CLAUDE.md"),
@@ -12045,7 +11982,7 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
12045
11982
  }
12046
11983
  const gitignoreTarget = path5.join(targetRoot, ".gitignore");
12047
11984
  const gitignoreContent = generateGitignoreBlock();
12048
- if (!existsSync17(gitignoreTarget)) {
11985
+ if (!existsSync16(gitignoreTarget)) {
12049
11986
  operations.push({
12050
11987
  kind: "create",
12051
11988
  path: gitignoreTarget,
@@ -12069,7 +12006,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
12069
12006
  });
12070
12007
  }
12071
12008
  const openCodePath = path5.join(targetRoot, "opencode.json");
12072
- if (!existsSync17(openCodePath)) {
12009
+ if (!existsSync16(openCodePath)) {
12073
12010
  operations.push({
12074
12011
  kind: "create",
12075
12012
  path: openCodePath,
@@ -12126,7 +12063,7 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
12126
12063
  }
12127
12064
  }
12128
12065
  const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
12129
- if (existsSync17(manifestPath)) {
12066
+ if (existsSync16(manifestPath)) {
12130
12067
  operations.push({
12131
12068
  kind: "skip",
12132
12069
  path: manifestPath,
@@ -12445,7 +12382,7 @@ async function updateManagedBlock(filePath, content) {
12445
12382
  const blockContent = `${MANAGED_BLOCK_BEGIN}
12446
12383
  ${content}${MANAGED_BLOCK_END}
12447
12384
  `;
12448
- if (!existsSync17(filePath)) {
12385
+ if (!existsSync16(filePath)) {
12449
12386
  const dir = path5.dirname(filePath);
12450
12387
  await mkdir5(dir, { recursive: true });
12451
12388
  await writeFileAtomic(filePath, blockContent);
@@ -12474,7 +12411,7 @@ async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
12474
12411
  if (op.requiresConfirmation) continue;
12475
12412
  const relPath = path5.relative(plan.targetRoot, op.path);
12476
12413
  if (relPath === ".pourkit/manifest.json") continue;
12477
- if (existsSync17(op.path)) {
12414
+ if (existsSync16(op.path)) {
12478
12415
  const sha256 = await computeFileChecksum(op.path);
12479
12416
  assets[relPath] = {
12480
12417
  ownership: op.ownership || "managed",
@@ -12519,7 +12456,7 @@ async function applyInitPlan(plan, options) {
12519
12456
  skipped++;
12520
12457
  continue;
12521
12458
  }
12522
- if (existsSync17(op.path) && !op.destructive) {
12459
+ if (existsSync16(op.path) && !op.destructive) {
12523
12460
  skipped++;
12524
12461
  continue;
12525
12462
  }
@@ -12534,7 +12471,7 @@ async function applyInitPlan(plan, options) {
12534
12471
  skipped++;
12535
12472
  continue;
12536
12473
  }
12537
- if (existsSync17(op.path)) {
12474
+ if (existsSync16(op.path)) {
12538
12475
  skipped++;
12539
12476
  continue;
12540
12477
  }
@@ -12563,7 +12500,7 @@ async function applyInitPlan(plan, options) {
12563
12500
  skipped++;
12564
12501
  continue;
12565
12502
  }
12566
- if (existsSync17(op.path)) {
12503
+ if (existsSync16(op.path)) {
12567
12504
  skipped++;
12568
12505
  continue;
12569
12506
  }
@@ -12703,7 +12640,7 @@ async function applyInitFromSource(options) {
12703
12640
  if (!manifestSkipped) {
12704
12641
  const agentFiles = [];
12705
12642
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
12706
- if (existsSync17(path5.join(targetRoot, name))) {
12643
+ if (existsSync16(path5.join(targetRoot, name))) {
12707
12644
  agentFiles.push(path5.join(targetRoot, name));
12708
12645
  }
12709
12646
  }
@@ -12994,6 +12931,165 @@ async function runSerenaStatusCommand(options) {
12994
12931
  logSerenaSidecarStatus("Serena sidecar status", status);
12995
12932
  }
12996
12933
 
12934
+ // commands/config-schema.ts
12935
+ import { readFileSync as readFileSync19, existsSync as existsSync17 } from "fs";
12936
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
12937
+ import { resolve as resolve4, dirname as dirname5 } from "path";
12938
+ import { fileURLToPath as fileURLToPath2 } from "url";
12939
+ import Ajv2 from "ajv";
12940
+ var __filename2 = fileURLToPath2(import.meta.url);
12941
+ var __dirname2 = dirname5(__filename2);
12942
+ var PACKAGED_SCHEMA_PATH = resolve4(
12943
+ __dirname2,
12944
+ "../schema/pourkit.schema.json"
12945
+ );
12946
+ var PACKAGED_HASH_PATH = resolve4(__dirname2, "../schema/pourkit.schema.hash");
12947
+ var _schemaValidator = null;
12948
+ var _schemaErrors = null;
12949
+ function getSchemaValidator() {
12950
+ if (!_schemaValidator) {
12951
+ const schema = JSON.parse(readFileSync19(PACKAGED_SCHEMA_PATH, "utf-8"));
12952
+ const ajv = new Ajv2({ strict: true });
12953
+ ajv.addKeyword("x-pourkit-schema-version");
12954
+ const validate = ajv.compile(schema);
12955
+ _schemaValidator = (data) => {
12956
+ _schemaErrors = null;
12957
+ const valid2 = validate(data);
12958
+ if (!valid2) _schemaErrors = validate.errors;
12959
+ return valid2;
12960
+ };
12961
+ }
12962
+ return _schemaValidator;
12963
+ }
12964
+ function readPackagedHash() {
12965
+ return readFileSync19(PACKAGED_HASH_PATH, "utf-8");
12966
+ }
12967
+ function localSchemaDir(repoRoot2) {
12968
+ return resolve4(repoRoot2, ".pourkit/schema");
12969
+ }
12970
+ async function runDoctorCommand(options) {
12971
+ const repoRootPath = options.cwd ?? process.cwd();
12972
+ const schemaDir = localSchemaDir(repoRootPath);
12973
+ const localSchemaPath = resolve4(schemaDir, "pourkit.schema.json");
12974
+ const localHashPath = resolve4(schemaDir, "pourkit.schema.hash");
12975
+ const localSchemaExists = existsSync17(localSchemaPath);
12976
+ const localHashExists = existsSync17(localHashPath);
12977
+ let packagedHash = null;
12978
+ try {
12979
+ packagedHash = readPackagedHash();
12980
+ } catch {
12981
+ }
12982
+ let localHashContent = null;
12983
+ if (localHashExists) {
12984
+ try {
12985
+ localHashContent = await readFile6(localHashPath, "utf-8");
12986
+ } catch {
12987
+ }
12988
+ }
12989
+ const schema = !localSchemaExists ? "missing" : !packagedHash || !localHashContent ? "missing" : localHashContent.trim() === packagedHash.trim() ? "fresh" : "stale";
12990
+ const hash = !localHashExists ? "missing" : !packagedHash ? "missing" : localHashContent && localHashContent.trim() === packagedHash.trim() ? "fresh" : "stale";
12991
+ const overall = schema === "fresh" && hash === "fresh" ? "fresh" : schema === "missing" || hash === "missing" ? "missing" : "stale";
12992
+ const configPath = resolve4(repoRootPath, ".pourkit/config.json");
12993
+ let configValidation;
12994
+ if (existsSync17(configPath)) {
12995
+ try {
12996
+ const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
12997
+ const validate = getSchemaValidator();
12998
+ const valid2 = validate(raw);
12999
+ if (valid2) {
13000
+ configValidation = { ok: true, errors: [] };
13001
+ } else {
13002
+ const errors = _schemaErrors;
13003
+ configValidation = {
13004
+ ok: false,
13005
+ errors: (errors ?? []).map(
13006
+ (e) => `${e.instancePath || "(root)"}: ${e.message ?? "validation failed"}`
13007
+ )
13008
+ };
13009
+ }
13010
+ } catch (err) {
13011
+ const msg = err instanceof SyntaxError ? err.message : String(err);
13012
+ configValidation = {
13013
+ ok: false,
13014
+ errors: [`.pourkit/config.json: ${msg}`]
13015
+ };
13016
+ }
13017
+ } else {
13018
+ configValidation = {
13019
+ ok: false,
13020
+ errors: [".pourkit/config.json not found"]
13021
+ };
13022
+ }
13023
+ const obsoleteConfigs = [
13024
+ "pourkit.config.ts",
13025
+ "pourkit.config.mjs",
13026
+ "pourkit.config.js",
13027
+ "pourkit.json"
13028
+ ].filter((p) => existsSync17(resolve4(repoRootPath, p)));
13029
+ let recommendation = null;
13030
+ if (!configValidation.ok || overall !== "fresh" || obsoleteConfigs.length > 0) {
13031
+ const parts = [];
13032
+ if (obsoleteConfigs.length > 0) {
13033
+ parts.push(
13034
+ `Found obsolete config files: ${obsoleteConfigs.join(", ")}. Move configuration to .pourkit/config.json with "$schema": "./schema/pourkit.schema.json".`
13035
+ );
13036
+ }
13037
+ if (!configValidation.ok) {
13038
+ parts.push(
13039
+ "Config validation failed; fix errors in .pourkit/config.json."
13040
+ );
13041
+ }
13042
+ if (overall !== "fresh") {
13043
+ parts.push(
13044
+ 'Run "pourkit config sync-schema" to update local schema assets.'
13045
+ );
13046
+ }
13047
+ recommendation = parts.join(" ");
13048
+ }
13049
+ return {
13050
+ configValidation,
13051
+ obsoleteConfigs,
13052
+ schemaAssets: { schema, hash, overall },
13053
+ recommendation
13054
+ };
13055
+ }
13056
+ async function runConfigSyncSchemaCommand(options) {
13057
+ const repoRootPath = options.cwd ?? process.cwd();
13058
+ const schemaDir = localSchemaDir(repoRootPath);
13059
+ await mkdir6(schemaDir, { recursive: true });
13060
+ const packagedSchema = await readFile6(PACKAGED_SCHEMA_PATH, "utf-8");
13061
+ const packagedHash = await readFile6(PACKAGED_HASH_PATH, "utf-8");
13062
+ const localSchemaPath = resolve4(schemaDir, "pourkit.schema.json");
13063
+ const localHashPath = resolve4(schemaDir, "pourkit.schema.hash");
13064
+ let schemaWritten = false;
13065
+ let hashWritten = false;
13066
+ if (existsSync17(localSchemaPath)) {
13067
+ const existing = await readFile6(localSchemaPath, "utf-8");
13068
+ if (existing !== packagedSchema) {
13069
+ await writeFile3(localSchemaPath, packagedSchema, "utf-8");
13070
+ schemaWritten = true;
13071
+ }
13072
+ } else {
13073
+ await writeFile3(localSchemaPath, packagedSchema, "utf-8");
13074
+ schemaWritten = true;
13075
+ }
13076
+ if (existsSync17(localHashPath)) {
13077
+ const existing = await readFile6(localHashPath, "utf-8");
13078
+ if (existing !== packagedHash) {
13079
+ await writeFile3(localHashPath, packagedHash, "utf-8");
13080
+ hashWritten = true;
13081
+ }
13082
+ } else {
13083
+ await writeFile3(localHashPath, packagedHash, "utf-8");
13084
+ hashWritten = true;
13085
+ }
13086
+ return {
13087
+ changed: schemaWritten || hashWritten,
13088
+ schemaWritten,
13089
+ hashWritten
13090
+ };
13091
+ }
13092
+
12997
13093
  // providers/github-provider.ts
12998
13094
  var GitHubIssueProvider = class {
12999
13095
  client;
@@ -13523,22 +13619,22 @@ function formatChecks2(checks) {
13523
13619
  init_common();
13524
13620
 
13525
13621
  // execution/sandcastle-execution.ts
13526
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync8 } from "fs";
13527
- import { join as join22 } from "path";
13622
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
13623
+ import { join as join21 } from "path";
13528
13624
  import { createWorktree, opencode } from "@ai-hero/sandcastle";
13529
13625
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
13530
13626
 
13531
13627
  // execution/execution-provider.ts
13532
13628
  init_common();
13533
- import { mkdtempSync as mkdtempSync2 } from "fs";
13534
- import { writeFile as writeFile3 } from "fs/promises";
13535
- import { tmpdir as tmpdir2 } from "os";
13536
- import { dirname as dirname5, join as join21 } from "path";
13629
+ import { mkdtempSync } from "fs";
13630
+ import { writeFile as writeFile4 } from "fs/promises";
13631
+ import { tmpdir } from "os";
13632
+ import { dirname as dirname6, join as join20 } from "path";
13537
13633
  async function writeExecutionArtifacts(worktreePath, artifacts) {
13538
13634
  for (const artifact of artifacts) {
13539
- const filePath = join21(worktreePath, artifact.path);
13540
- await ensureDir(dirname5(filePath));
13541
- await writeFile3(filePath, artifact.content, "utf-8");
13635
+ const filePath = join20(worktreePath, artifact.path);
13636
+ await ensureDir(dirname6(filePath));
13637
+ await writeFile4(filePath, artifact.content, "utf-8");
13542
13638
  }
13543
13639
  }
13544
13640
 
@@ -13547,8 +13643,8 @@ init_common();
13547
13643
  import path7 from "path";
13548
13644
 
13549
13645
  // execution/sandbox-image.ts
13550
- import { createHash as createHash4 } from "crypto";
13551
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
13646
+ import { createHash as createHash3 } from "crypto";
13647
+ import { existsSync as existsSync18, readFileSync as readFileSync20 } from "fs";
13552
13648
  import path6 from "path";
13553
13649
  function sandboxImageName(repoRoot2) {
13554
13650
  const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
@@ -13558,7 +13654,7 @@ function sandboxImageName(repoRoot2) {
13558
13654
  if (!existsSync18(dockerfilePath)) {
13559
13655
  return `sandcastle:${baseName}`;
13560
13656
  }
13561
- const fingerprint = createHash4("sha256").update(readFileSync18(dockerfilePath)).digest("hex").slice(0, 8);
13657
+ const fingerprint = createHash3("sha256").update(readFileSync20(dockerfilePath)).digest("hex").slice(0, 8);
13562
13658
  return `sandcastle:${baseName}-${fingerprint}`;
13563
13659
  }
13564
13660
 
@@ -13854,13 +13950,13 @@ function isPlainObject(value) {
13854
13950
  return typeof value === "object" && value !== null && !Array.isArray(value);
13855
13951
  }
13856
13952
  function savePromptToFile(repoRoot2, stage, iteration, prompt) {
13857
- const promptsDir = join22(repoRoot2, ".pourkit", ".tmp", "prompts");
13858
- mkdirSync11(promptsDir, { recursive: true });
13953
+ const promptsDir = join21(repoRoot2, ".pourkit", ".tmp", "prompts");
13954
+ mkdirSync9(promptsDir, { recursive: true });
13859
13955
  const timestamp2 = Date.now();
13860
13956
  const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
13861
13957
  const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
13862
- const filePath = join22(promptsDir, filename);
13863
- writeFileSync8(filePath, prompt, "utf-8");
13958
+ const filePath = join21(promptsDir, filename);
13959
+ writeFileSync7(filePath, prompt, "utf-8");
13864
13960
  }
13865
13961
 
13866
13962
  // cli.ts
@@ -13960,7 +14056,7 @@ function createCliProgram(version) {
13960
14056
  });
13961
14057
  program.command("validate-artifact").description("Validate an agent handoff artifact").argument(
13962
14058
  "<kind>",
13963
- "artifact kind: reviewer, refactor, finalizer, conflict-resolution, final-review (retained for Issue Final Review validation), failure-resolution, local-prd, local-issue, local-triage, or issue-final-review"
14059
+ "artifact kind: reviewer, refactor, finalizer, conflict-resolution, failure-resolution, local-prd, local-issue, local-triage, or issue-final-review"
13964
14060
  ).argument("<artifactPath>", "artifact path to validate").argument("[extraPaths...]", "additional artifact paths (for local-triage)").option("--iteration <number>", "review/refactor iteration", (value) => {
13965
14061
  const parsed = Number.parseInt(value, 10);
13966
14062
  if (!Number.isInteger(parsed) || parsed < 1) {
@@ -13993,12 +14089,6 @@ function createCliProgram(version) {
13993
14089
  return previous;
13994
14090
  },
13995
14091
  []
13996
- ).option("--prd-ref <ref>", "PRD ref for final-review artifacts").option(
13997
- "--checkout-base <ref>",
13998
- "checkout base / PRD branch for final-review artifacts"
13999
- ).option(
14000
- "--review-base <ref>",
14001
- "review merge base for final-review artifacts"
14002
14092
  ).option(
14003
14093
  "--issue-number <number>",
14004
14094
  "issue number for issue-final-review artifacts",
@@ -14023,7 +14113,6 @@ function createCliProgram(version) {
14023
14113
  "refactor",
14024
14114
  "finalizer",
14025
14115
  "conflict-resolution",
14026
- "final-review",
14027
14116
  "failure-resolution",
14028
14117
  "local-prd",
14029
14118
  "local-issue",
@@ -14057,9 +14146,6 @@ function createCliProgram(version) {
14057
14146
  findingIds: options.findingId,
14058
14147
  latestReviewArtifactPath: options.latestReviewArtifact,
14059
14148
  allowedDecisions: options.allowedDecision,
14060
- prdRef: options.prdRef,
14061
- checkoutBase: options.checkoutBase,
14062
- reviewBase: options.reviewBase,
14063
14149
  issueNumber: options.issueNumber,
14064
14150
  branchName: options.branchName,
14065
14151
  checkConflictMarkers: options.checkConflictMarkers
@@ -14418,6 +14504,26 @@ function createCliProgram(version) {
14418
14504
  await runInitCommand(initOptions);
14419
14505
  }
14420
14506
  );
14507
+ program.command("doctor").description("Report Config Schema drift without mutating files").option("--cwd <path>", "target repository directory").action(async (options) => {
14508
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
14509
+ const report = await runDoctorCommand({ cwd: targetRepoRoot });
14510
+ console.log(JSON.stringify(report, null, 2));
14511
+ if (!report.configValidation.ok || report.obsoleteConfigs.length > 0 || report.schemaAssets.overall !== "fresh") {
14512
+ process.exitCode = 1;
14513
+ }
14514
+ });
14515
+ const configCommand = program.command("config").description("Config management commands");
14516
+ configCommand.command("sync-schema").description("Copy packaged Config Schema assets to .pourkit/schema/").option("--cwd <path>", "target repository directory").action(async (options) => {
14517
+ const targetRepoRoot = options.cwd ? repoRoot(options.cwd) : repoRoot();
14518
+ const result = await runConfigSyncSchemaCommand({ cwd: targetRepoRoot });
14519
+ if (result.changed) {
14520
+ console.log(
14521
+ `Schema assets updated (schema: ${result.schemaWritten ? "written" : "unchanged"}, hash: ${result.hashWritten ? "written" : "unchanged"}).`
14522
+ );
14523
+ } else {
14524
+ console.log("Schema assets are up to date.");
14525
+ }
14526
+ });
14421
14527
  const serena = program.command("serena").description("Serena baseline and sidecar commands");
14422
14528
  serena.command("init").requiredOption("--target <name>", "target name").option("--cwd <path>", "target repository directory").action(async (options) => {
14423
14529
  await runSerenaInitCommand({
@@ -14543,11 +14649,11 @@ function createCliProgram(version) {
14543
14649
  return program;
14544
14650
  }
14545
14651
  async function resolveCliVersion() {
14546
- if (isPackageVersion("0.0.0-next-20260614002607")) {
14547
- return "0.0.0-next-20260614002607";
14652
+ if (isPackageVersion("0.0.0-next-20260614182520")) {
14653
+ return "0.0.0-next-20260614182520";
14548
14654
  }
14549
- if (isReleaseVersion("0.0.0-next-20260614002607")) {
14550
- return "0.0.0-next-20260614002607";
14655
+ if (isReleaseVersion("0.0.0-next-20260614182520")) {
14656
+ return "0.0.0-next-20260614182520";
14551
14657
  }
14552
14658
  try {
14553
14659
  const root = repoRoot();