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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,20 +58,20 @@ function createLogger(name, filePath) {
58
58
  );
59
59
  },
60
60
  async close() {
61
- await new Promise((resolve3) => {
61
+ await new Promise((resolve4) => {
62
62
  if (!fileStream) {
63
- resolve3();
63
+ resolve4();
64
64
  return;
65
65
  }
66
66
  const timer = setTimeout(() => {
67
67
  if (!fileStream.destroyed) {
68
68
  fileStream.destroy();
69
69
  }
70
- resolve3();
70
+ resolve4();
71
71
  }, 2e3);
72
72
  fileStream.end(() => {
73
73
  clearTimeout(timer);
74
- resolve3();
74
+ resolve4();
75
75
  });
76
76
  });
77
77
  }
@@ -265,7 +265,7 @@ async function execJson(command, args, options = {}) {
265
265
  return JSON.parse(result.stdout);
266
266
  }
267
267
  function sleep(ms) {
268
- return new Promise((resolve3) => setTimeout(resolve3, ms));
268
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
269
269
  }
270
270
  async function execCaptureWithRetry(command, args, options = {}) {
271
271
  const retries = options.retries ?? 3;
@@ -330,184 +330,45 @@ var init_common = __esm({
330
330
 
331
331
  // e2e/run-live-e2e.ts
332
332
  import path8 from "path";
333
- import { existsSync as existsSync14 } from "fs";
334
333
  import { pathToFileURL } from "url";
335
334
  import { mkdir as mkdir5, readFile as readFile4, readdir as readdir2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
336
335
 
337
336
  // commands/issue-run.ts
338
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
339
- import { isAbsolute as isAbsolute3, join as join14 } from "path";
337
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
338
+ import { isAbsolute as isAbsolute4, join as join14 } from "path";
340
339
 
341
340
  // shared/config.ts
342
- import { join } from "path";
343
- import { z } from "zod";
344
- var NonEmptyString = z.string().trim().min(1);
341
+ import { readFileSync } from "fs";
342
+ import { fileURLToPath } from "url";
343
+ import { dirname, isAbsolute, join, normalize, sep, resolve } from "path";
344
+ import Ajv from "ajv";
345
+ var __filename = fileURLToPath(import.meta.url);
346
+ var __dirname = dirname(__filename);
347
+ var SCHEMA_PATH = resolve(__dirname, "../schema/pourkit.schema.json");
348
+ var _schema = null;
349
+ var _validate = null;
350
+ var _ajvErrors = null;
351
+ function getValidator() {
352
+ if (!_validate) {
353
+ const schema = _schema ?? JSON.parse(readFileSync(SCHEMA_PATH, "utf-8"));
354
+ _schema = schema;
355
+ const ajv = new Ajv({ strict: true });
356
+ ajv.addKeyword("x-pourkit-schema-version");
357
+ const validate = ajv.compile(schema);
358
+ _validate = (data) => {
359
+ _ajvErrors = null;
360
+ const valid2 = validate(data);
361
+ if (!valid2) _ajvErrors = validate.errors;
362
+ return valid2;
363
+ };
364
+ }
365
+ return _validate;
366
+ }
345
367
  var DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES = 3;
346
- var OutputRetriesConfigSchema = z.object({
347
- missingOrEmpty: z.number().int().nonnegative().default(DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES)
348
- }).strict();
368
+ var DEFAULT_BRANCH_TEMPLATE = "pourkit/{{issue.number}}/{{issue.slug}}";
349
369
  function resolveMissingOrEmptyOutputRetries(config) {
350
370
  return config?.outputRetries?.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES;
351
371
  }
352
- var StageAgentConfigSchema = z.object({
353
- agent: NonEmptyString,
354
- model: NonEmptyString,
355
- variant: NonEmptyString.optional(),
356
- env: z.record(z.string(), z.string()).optional(),
357
- promptTemplate: NonEmptyString,
358
- outputRetries: OutputRetriesConfigSchema.optional()
359
- }).strict();
360
- var ReviewerConfigSchema = z.object({
361
- agent: NonEmptyString,
362
- model: NonEmptyString,
363
- variant: NonEmptyString.optional(),
364
- env: z.record(z.string(), z.string()).optional(),
365
- promptTemplate: NonEmptyString,
366
- outputRetries: OutputRetriesConfigSchema.optional(),
367
- criteria: z.array(NonEmptyString),
368
- includeReviewHistory: z.boolean().optional(),
369
- passWithNotesRefactorAttempts: z.number().int().nonnegative().optional()
370
- }).strict();
371
- var VerificationCommandSchema = z.object({
372
- command: z.string().nullable().optional(),
373
- label: z.string().optional()
374
- }).strict().refine((d) => d.command && d.command.trim() !== "", {
375
- message: "must have a non-empty command"
376
- }).transform((d) => ({
377
- command: d.command,
378
- label: d.label && d.label.trim() !== "" ? d.label : void 0
379
- }));
380
- var PrdRunModeSchema = z.enum(["github", "local"]);
381
- var QueueConfigSchema = z.object({
382
- loop: z.boolean().optional()
383
- }).strict();
384
- var FailureResolutionConfigSchema = z.object({
385
- agent: NonEmptyString,
386
- model: NonEmptyString,
387
- variant: NonEmptyString.optional(),
388
- env: z.record(z.string(), z.string()).optional(),
389
- promptTemplate: NonEmptyString,
390
- outputRetries: OutputRetriesConfigSchema.optional(),
391
- maxAttemptsPerFailure: z.number().int().positive(),
392
- failureLimits: z.record(z.string(), z.number().int().positive()).optional()
393
- }).strict();
394
- var ReviewRefactorLoopStrategySchema = z.object({
395
- type: z.literal("review-refactor-loop"),
396
- implement: z.object({
397
- builder: StageAgentConfigSchema
398
- }).strict(),
399
- conflictResolution: z.object({
400
- agent: NonEmptyString,
401
- model: NonEmptyString,
402
- variant: NonEmptyString.optional(),
403
- env: z.record(z.string(), z.string()).optional(),
404
- promptTemplate: NonEmptyString,
405
- maxAttempts: z.number().int().positive()
406
- }).strict().optional(),
407
- failureResolution: FailureResolutionConfigSchema,
408
- review: z.object({
409
- reviewer: ReviewerConfigSchema,
410
- refactor: StageAgentConfigSchema,
411
- maxIterations: z.number().int().positive(),
412
- passWithNotesRefactorAttempts: z.number().int().nonnegative().default(2)
413
- }).strict(),
414
- verify: z.object({
415
- commands: z.preprocess(
416
- (v) => Array.isArray(v) ? v : [],
417
- z.array(VerificationCommandSchema).refine((arr) => arr.length > 0, {
418
- message: "must contain at least one command"
419
- })
420
- )
421
- }).strict().optional(),
422
- issueFinalReview: StageAgentConfigSchema.extend({
423
- maxAttempts: z.number().int().positive()
424
- }),
425
- finalize: z.object({
426
- prDescriptionAgent: StageAgentConfigSchema,
427
- maxAttempts: z.number().int().positive()
428
- }).strict(),
429
- prdRun: z.object({
430
- mode: PrdRunModeSchema.optional(),
431
- // Uses promptTemplate (canonical StageAgentConfig field), not prompt as Issue contract may suggest
432
- finalReview: StageAgentConfigSchema
433
- }).strict().optional()
434
- }).strict();
435
- var TargetSerenaConfigSchema = z.object({
436
- enabled: z.boolean().optional(),
437
- required: z.boolean().optional()
438
- }).strict();
439
- var TargetSchema = z.object({
440
- name: NonEmptyString,
441
- baseBranch: z.preprocess(
442
- (v) => typeof v === "string" && v.length > 0 ? v : void 0,
443
- NonEmptyString.default("main")
444
- ),
445
- branchTemplate: z.string().default("pourkit/{{issue.number}}/{{issue.slug}}"),
446
- setupCommands: z.preprocess(
447
- (v) => Array.isArray(v) ? v : void 0,
448
- z.array(VerificationCommandSchema).default([])
449
- ),
450
- autoMerge: z.preprocess(
451
- (v) => typeof v === "boolean" ? v : void 0,
452
- z.boolean().default(true)
453
- ),
454
- queue: QueueConfigSchema.optional(),
455
- serena: TargetSerenaConfigSchema.optional(),
456
- strategy: ReviewRefactorLoopStrategySchema
457
- }).strict();
458
- var LabelsSchema = z.object({
459
- readyForAgent: NonEmptyString,
460
- agentInProgress: NonEmptyString,
461
- blocked: NonEmptyString,
462
- prOpenAwaitingMerge: NonEmptyString,
463
- readyForHuman: NonEmptyString,
464
- needsTriage: NonEmptyString.optional().default("needs-triage")
465
- }).strict();
466
- var SandboxMountSchema = z.object({
467
- hostPath: NonEmptyString,
468
- sandboxPath: NonEmptyString,
469
- readonly: z.boolean().default(false)
470
- }).strict();
471
- var SandboxSchema = z.object({
472
- provider: NonEmptyString,
473
- copyToWorktree: z.array(NonEmptyString).optional(),
474
- mounts: z.array(SandboxMountSchema).optional(),
475
- env: z.record(z.string()).optional(),
476
- idleTimeoutSeconds: z.preprocess((v) => {
477
- if (v === void 0) return void 0;
478
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
479
- return v;
480
- }, z.number().int().positive().optional())
481
- }).strict();
482
- var ChecksSchema = z.object({
483
- requiredLabels: z.array(NonEmptyString),
484
- allowedAuthors: z.array(NonEmptyString),
485
- checksFoundTimeoutSeconds: z.number().int().positive().optional(),
486
- checksCompletionTimeoutSeconds: z.number().int().positive().optional(),
487
- pollIntervalSeconds: z.number().int().positive().optional(),
488
- issueListLimit: z.number().int().positive().optional()
489
- }).strict();
490
- var CleanupConfigSchema = z.object({
491
- enabled: z.boolean().default(true),
492
- worktreeRetentionDays: z.number().int().positive().default(14),
493
- logRetentionDays: z.number().int().positive().default(30)
494
- }).strict();
495
- var SerenaConfigSchema = z.object({
496
- enabled: z.boolean().default(false),
497
- required: z.boolean().default(false),
498
- mcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
499
- sandboxMcpUrl: NonEmptyString.default("http://localhost:9121/mcp"),
500
- dataDir: z.string().default(".pourkit/serena/"),
501
- autoStart: z.boolean().default(false)
502
- }).strict();
503
- var PourkitConfigSchema = z.object({
504
- targets: z.array(TargetSchema).min(1),
505
- labels: LabelsSchema,
506
- sandbox: SandboxSchema,
507
- checks: ChecksSchema,
508
- cleanup: CleanupConfigSchema.optional(),
509
- serena: SerenaConfigSchema.default({})
510
- }).strict();
511
372
  var removedFieldReplacements = {
512
373
  "config.implementor": "targets[].strategy.implement.builder",
513
374
  "config.reviewer": "targets[].strategy.review.reviewer",
@@ -571,11 +432,12 @@ function checkRemovedFields(raw) {
571
432
  }
572
433
  }
573
434
  }
574
- function formatZodPath(path9) {
575
- if (path9.length === 0) return "";
435
+ function formatAjvPath(instancePath) {
436
+ if (!instancePath || instancePath === "/") return "";
437
+ const parts = instancePath.split("/").slice(1);
576
438
  let result = "";
577
- for (const segment of path9) {
578
- if (typeof segment === "number") {
439
+ for (const segment of parts) {
440
+ if (/^\d+$/.test(segment)) {
579
441
  result += `[${segment}]`;
580
442
  } else {
581
443
  result += result ? `.${segment}` : segment;
@@ -583,51 +445,172 @@ function formatZodPath(path9) {
583
445
  }
584
446
  return result;
585
447
  }
586
- function formatFirstZodError(err) {
587
- const issue = err.issues[0];
588
- const path9 = formatZodPath(issue.path);
589
- if (path9 === "targets" && (issue.code === "too_small" || issue.code === "invalid_type")) {
590
- return "Config must have at least one target";
448
+ function formatFirstAjvError(errors) {
449
+ const error = errors[0];
450
+ const path9 = formatAjvPath(error.instancePath);
451
+ if (error.keyword === "required") {
452
+ const missingParam = error.params.missingProperty;
453
+ if (missingParam === "targets") {
454
+ return "Config must have at least one target";
455
+ }
456
+ if (path9 === "" && missingParam === "targets") {
457
+ return "Config must have at least one target";
458
+ }
459
+ if (path9) {
460
+ return `${path9} must have required property '${missingParam}'`;
461
+ }
462
+ return `${missingParam} must be an object`;
591
463
  }
592
- if (issue.path.length >= 3 && issue.path[0] === "targets" && typeof issue.path[1] === "number" && issue.path[2] === "name" && issue.code === z.ZodIssueCode.too_small) {
593
- return `Target[${issue.path[1]}] must have a non-empty name`;
464
+ if (error.keyword === "additionalProperties") {
465
+ const additionalProp = error.params.additionalProperty;
466
+ const keyPath = path9 ? `${path9}.${additionalProp}` : additionalProp;
467
+ return `${keyPath} is not supported`;
594
468
  }
595
- switch (issue.code) {
596
- case z.ZodIssueCode.invalid_type: {
597
- if (issue.expected === "object") {
598
- return path9 ? `${path9} must be an object` : "Config must be an object";
599
- }
600
- if (issue.expected === "integer") {
601
- return `${path9} must be an integer`;
602
- }
603
- if (issue.expected === "string") {
604
- return `${path9} must be a string`;
605
- }
606
- if (issue.expected === "number") {
607
- return `${path9} must be a number`;
608
- }
609
- return issue.message;
469
+ if (error.keyword === "minLength") {
470
+ if (path9) {
471
+ return `${path9} must be a non-empty string`;
472
+ }
473
+ return "Config must be a non-empty string";
474
+ }
475
+ if (error.keyword === "const") {
476
+ const allowedValue = error.params.allowedValue;
477
+ return `${path9 || "Config"} must be ${JSON.stringify(allowedValue)}`;
478
+ }
479
+ if (error.keyword === "enum") {
480
+ const allowedValues = error.params.allowedValues;
481
+ return `${path9} must be one of: ${allowedValues.map((v) => JSON.stringify(v)).join(", ")}`;
482
+ }
483
+ if (error.keyword === "minimum" || error.keyword === "exclusiveMinimum") {
484
+ const limit = error.params.limit;
485
+ if (limit === 1) {
486
+ return `${path9} must be a positive number`;
487
+ }
488
+ return `${path9} must be at least ${limit}`;
489
+ }
490
+ if (error.keyword === "type") {
491
+ const expected = error.params.type;
492
+ if (path9 === "") {
493
+ return `Config must be an object`;
494
+ }
495
+ if (expected === "object") {
496
+ return `${path9} must be an object`;
497
+ }
498
+ if (expected === "integer") {
499
+ return `${path9} must be an integer`;
500
+ }
501
+ if (expected === "string") {
502
+ return `${path9} must be a string`;
503
+ }
504
+ if (expected === "number") {
505
+ return `${path9} must be a number`;
506
+ }
507
+ return `${path9} must be ${expected === "integer" ? "an integer" : `a ${expected}`}`;
508
+ }
509
+ if (error.keyword === "minItems") {
510
+ return `${path9} must contain at least one item`;
511
+ }
512
+ if (error.keyword === "pattern") {
513
+ return `${path9} has an invalid format`;
514
+ }
515
+ return `${path9 || "Config"} ${error.message || "is invalid"}`;
516
+ }
517
+ function applyOutputRetriesDefaults(retries) {
518
+ if (retries === void 0) return void 0;
519
+ return {
520
+ missingOrEmpty: retries.missingOrEmpty ?? DEFAULT_MISSING_OR_EMPTY_OUTPUT_RETRIES
521
+ };
522
+ }
523
+ function applyStageAgentDefaults(agent) {
524
+ return {
525
+ ...agent,
526
+ outputRetries: applyOutputRetriesDefaults(agent.outputRetries)
527
+ };
528
+ }
529
+ function assertRepoRelativePath(value, location) {
530
+ const normalized = normalize(value);
531
+ if (isAbsolute(value) || isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
532
+ throw new Error(
533
+ `${location} must stay within the repository and be repo-relative; got "${value}"`
534
+ );
535
+ }
536
+ }
537
+ function assertBaseBranch(value, location) {
538
+ if (value.includes("/")) {
539
+ throw new Error(
540
+ `${location} must be a local branch name, not a remote-qualified, tag, ref, or path-like name; got "${value}"`
541
+ );
542
+ }
543
+ if (/^[0-9a-f]{7,40}$/i.test(value)) {
544
+ throw new Error(
545
+ `${location} must be a branch name, not a commit SHA; got "${value}"`
546
+ );
547
+ }
548
+ }
549
+ function assertStageAgentPath(agent, location) {
550
+ if (!agent) return;
551
+ const promptTemplate = agent.promptTemplate;
552
+ if (typeof promptTemplate === "string") {
553
+ assertRepoRelativePath(promptTemplate, `${location}.promptTemplate`);
554
+ }
555
+ }
556
+ function validateConfigSemantics(data) {
557
+ const sandbox = data.sandbox;
558
+ const copyToWorktree = sandbox?.copyToWorktree;
559
+ copyToWorktree?.forEach((entry, index) => {
560
+ assertRepoRelativePath(entry, `sandbox.copyToWorktree[${index}]`);
561
+ });
562
+ const targetNames = /* @__PURE__ */ new Set();
563
+ data.targets.forEach((target, targetIndex) => {
564
+ const name = target.name;
565
+ if (targetNames.has(name)) {
566
+ throw new Error(
567
+ `Duplicate target name "${name}"; target names must be unique`
568
+ );
569
+ }
570
+ targetNames.add(name);
571
+ assertBaseBranch(
572
+ target.baseBranch,
573
+ `targets[${targetIndex}].baseBranch`
574
+ );
575
+ const strategy = target.strategy;
576
+ const implement = strategy.implement;
577
+ const review = strategy.review;
578
+ const finalize = strategy.finalize;
579
+ assertStageAgentPath(
580
+ implement.builder,
581
+ `targets[${targetIndex}].strategy.implement.builder`
582
+ );
583
+ assertStageAgentPath(
584
+ strategy.conflictResolution,
585
+ `targets[${targetIndex}].strategy.conflictResolution`
586
+ );
587
+ assertStageAgentPath(
588
+ strategy.failureResolution,
589
+ `targets[${targetIndex}].strategy.failureResolution`
590
+ );
591
+ assertStageAgentPath(
592
+ review.reviewer,
593
+ `targets[${targetIndex}].strategy.review.reviewer`
594
+ );
595
+ assertStageAgentPath(
596
+ review.refactor,
597
+ `targets[${targetIndex}].strategy.review.refactor`
598
+ );
599
+ assertStageAgentPath(
600
+ strategy.issueFinalReview,
601
+ `targets[${targetIndex}].strategy.issueFinalReview`
602
+ );
603
+ assertStageAgentPath(
604
+ finalize.prDescriptionAgent,
605
+ `targets[${targetIndex}].strategy.finalize.prDescriptionAgent`
606
+ );
607
+ });
608
+ }
609
+ function assertKnownKeys(value, path9, knownKeys) {
610
+ for (const key of Object.keys(value)) {
611
+ if (!knownKeys.includes(key)) {
612
+ throw new Error(`${path9}.${key} is not supported`);
610
613
  }
611
- case z.ZodIssueCode.too_small:
612
- if (issue.type === "string" && issue.minimum === 1) {
613
- return `${path9} must be a non-empty string`;
614
- }
615
- if (issue.type === "array" && issue.minimum === 1) {
616
- return `${path9} must not be empty`;
617
- }
618
- if (issue.type === "number") {
619
- return `${path9} must be a positive number`;
620
- }
621
- return issue.message;
622
- case z.ZodIssueCode.invalid_literal:
623
- return `${path9} must be ${issue.expected}`;
624
- case z.ZodIssueCode.unrecognized_keys:
625
- const keyPath = path9 ? `${path9}.${issue.keys[0]}` : issue.keys[0];
626
- return `${keyPath} is not supported`;
627
- case z.ZodIssueCode.custom:
628
- return path9 ? `${path9} ${issue.message}` : issue.message;
629
- default:
630
- return issue.message;
631
614
  }
632
615
  }
633
616
  function parseConfig(raw) {
@@ -649,6 +632,7 @@ function parseConfig(raw) {
649
632
  "name",
650
633
  "baseBranch",
651
634
  "branchTemplate",
635
+ "prdRun",
652
636
  "setupCommands",
653
637
  "autoMerge",
654
638
  "queue",
@@ -664,9 +648,20 @@ function parseConfig(raw) {
664
648
  `targets[${i}].strategy.conflictResolution has been removed; use targets[${i}].strategy.failureResolution`
665
649
  );
666
650
  }
667
- if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object" && "reconciliation" in strategy.prdRun) {
651
+ if (strategy && typeof strategy === "object" && strategy.prdRun && typeof strategy.prdRun === "object") {
652
+ const prdRun = strategy.prdRun;
653
+ if ("reconciliation" in prdRun) {
654
+ throw new Error(
655
+ `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
656
+ );
657
+ }
658
+ if ("finalReview" in prdRun) {
659
+ throw new Error(
660
+ `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.`
661
+ );
662
+ }
668
663
  throw new Error(
669
- `targets[${i}].strategy.prdRun.reconciliation has been removed; PRD Run no longer invokes Architect reconciliation.`
664
+ `targets[${i}].strategy.prdRun is not supported in the new config; use targets[${i}].prdRun instead.`
670
665
  );
671
666
  }
672
667
  }
@@ -676,67 +671,126 @@ function parseConfig(raw) {
676
671
  "copyToWorktree",
677
672
  "mounts",
678
673
  "env",
679
- "idleTimeoutSeconds"
674
+ "idleTimeoutSeconds",
675
+ "forceRebuild"
680
676
  ]);
681
677
  }
682
- const result = PourkitConfigSchema.safeParse(raw);
683
- if (!result.success) {
684
- throw new Error(formatFirstZodError(result.error));
685
- }
686
- const data = result.data;
687
- const targets = data.targets.map((t) => {
688
- const setupCommands = t.setupCommands?.map((cmd, i) => ({
689
- command: cmd.command,
690
- label: cmd.label ?? `check-${i}`
691
- }));
692
- const verifyCommands = t.strategy.verify?.commands?.map((cmd, i) => ({
693
- command: cmd.command,
694
- label: cmd.label ?? `check-${i}`
695
- }));
696
- return {
697
- name: t.name,
698
- baseBranch: t.baseBranch,
699
- branchTemplate: t.branchTemplate,
700
- setupCommands,
701
- autoMerge: t.autoMerge,
702
- queue: t.queue,
703
- serena: t.serena,
704
- strategy: {
705
- type: "review-refactor-loop",
706
- implement: { builder: t.strategy.implement.builder },
707
- failureResolution: {
708
- agent: t.strategy.failureResolution.agent,
709
- model: t.strategy.failureResolution.model,
710
- promptTemplate: t.strategy.failureResolution.promptTemplate,
711
- outputRetries: t.strategy.failureResolution.outputRetries,
712
- maxAttemptsPerFailure: t.strategy.failureResolution.maxAttemptsPerFailure,
713
- failureLimits: t.strategy.failureResolution.failureLimits
714
- },
715
- review: {
716
- reviewer: t.strategy.review.reviewer,
717
- refactor: t.strategy.review.refactor,
718
- maxIterations: t.strategy.review.maxIterations,
719
- passWithNotesRefactorAttempts: t.strategy.review.passWithNotesRefactorAttempts
720
- },
721
- ...t.strategy.verify ? { verify: { commands: verifyCommands } } : {},
722
- issueFinalReview: t.strategy.issueFinalReview,
723
- finalize: {
724
- prDescriptionAgent: t.strategy.finalize.prDescriptionAgent,
725
- maxAttempts: t.strategy.finalize.maxAttempts
726
- },
727
- ...t.strategy.prdRun ? {
728
- prdRun: {
729
- ...t.strategy.prdRun.mode ? { mode: t.strategy.prdRun.mode } : {},
730
- finalReview: t.strategy.prdRun.finalReview
678
+ const validate = getValidator();
679
+ if (!validate(raw)) {
680
+ throw new Error(
681
+ formatFirstAjvError(
682
+ _ajvErrors ?? [
683
+ {
684
+ instancePath: "",
685
+ message: "validation failed",
686
+ keyword: "error",
687
+ params: {}
731
688
  }
732
- } : {}
733
- }
734
- };
735
- });
689
+ ]
690
+ )
691
+ );
692
+ }
693
+ const data = config;
694
+ validateConfigSemantics(data);
695
+ const targets = data.targets.map(
696
+ (t) => {
697
+ const input = t;
698
+ const strategy = input.strategy;
699
+ const implement = strategy.implement;
700
+ const failureResolution = strategy.failureResolution;
701
+ const review = strategy.review;
702
+ const reviewReviewer = review.reviewer;
703
+ const reviewRefactor = review.refactor;
704
+ const finalize = strategy.finalize;
705
+ const issueFinalReview = strategy.issueFinalReview;
706
+ const setupCommands = input.setupCommands?.map((cmd, i) => ({
707
+ command: cmd.command,
708
+ label: cmd.label ?? `check-${i}`
709
+ }));
710
+ const verifyCommands = strategy.verify?.commands ? strategy.verify.commands : void 0;
711
+ const verifyLabeled = verifyCommands?.map((cmd, i) => ({
712
+ command: cmd.command,
713
+ label: cmd.label ?? `check-${i}`
714
+ }));
715
+ return {
716
+ name: input.name,
717
+ baseBranch: input.baseBranch,
718
+ branchTemplate: input.branchTemplate ?? DEFAULT_BRANCH_TEMPLATE,
719
+ prdRun: input.prdRun,
720
+ setupCommands,
721
+ autoMerge: input.autoMerge !== void 0 ? input.autoMerge : true,
722
+ queue: input.queue,
723
+ serena: input.serena,
724
+ strategy: {
725
+ type: "review-refactor-loop",
726
+ implement: {
727
+ builder: applyStageAgentDefaults(
728
+ implement.builder
729
+ )
730
+ },
731
+ failureResolution: {
732
+ agent: failureResolution.agent,
733
+ model: failureResolution.model,
734
+ variant: failureResolution.variant,
735
+ env: failureResolution.env,
736
+ promptTemplate: failureResolution.promptTemplate,
737
+ outputRetries: applyOutputRetriesDefaults(
738
+ failureResolution.outputRetries
739
+ ),
740
+ maxAttemptsPerFailure: failureResolution.maxAttemptsPerFailure,
741
+ failureLimits: failureResolution.failureLimits
742
+ },
743
+ review: {
744
+ reviewer: {
745
+ agent: reviewReviewer.agent,
746
+ model: reviewReviewer.model,
747
+ variant: reviewReviewer.variant,
748
+ env: reviewReviewer.env,
749
+ promptTemplate: reviewReviewer.promptTemplate,
750
+ outputRetries: applyOutputRetriesDefaults(
751
+ reviewReviewer.outputRetries
752
+ ),
753
+ criteria: reviewReviewer.criteria,
754
+ includeReviewHistory: reviewReviewer.includeReviewHistory,
755
+ passWithNotesRefactorAttempts: reviewReviewer.passWithNotesRefactorAttempts
756
+ },
757
+ refactor: applyStageAgentDefaults(
758
+ reviewRefactor
759
+ ),
760
+ maxIterations: review.maxIterations,
761
+ passWithNotesRefactorAttempts: review.passWithNotesRefactorAttempts ?? 2
762
+ },
763
+ ...verifyLabeled ? { verify: { commands: verifyLabeled } } : {},
764
+ issueFinalReview: {
765
+ ...issueFinalReview,
766
+ maxAttempts: issueFinalReview.maxAttempts,
767
+ outputRetries: applyOutputRetriesDefaults(
768
+ issueFinalReview.outputRetries
769
+ )
770
+ },
771
+ finalize: {
772
+ prDescriptionAgent: applyStageAgentDefaults(
773
+ finalize.prDescriptionAgent
774
+ ),
775
+ maxAttempts: finalize.maxAttempts
776
+ }
777
+ }
778
+ };
779
+ }
780
+ );
781
+ const serenaRaw = data.serena;
782
+ const serenaDefaults = {
783
+ mcpUrl: serenaRaw?.mcpUrl ?? "http://localhost:9121/mcp",
784
+ sandboxMcpUrl: serenaRaw?.sandboxMcpUrl ?? "http://localhost:9121/mcp",
785
+ dataDir: serenaRaw?.dataDir ?? ".pourkit/serena/"
786
+ };
736
787
  const serena = {
737
- ...data.serena,
738
- mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? data.serena.mcpUrl,
739
- sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? data.serena.sandboxMcpUrl
788
+ enabled: serenaRaw?.enabled ?? false,
789
+ required: serenaRaw?.required ?? false,
790
+ mcpUrl: process.env.POURKIT_SERENA_MCP_URL ?? serenaDefaults.mcpUrl,
791
+ sandboxMcpUrl: process.env.POURKIT_SERENA_SANDBOX_MCP_URL ?? serenaDefaults.sandboxMcpUrl,
792
+ dataDir: serenaDefaults.dataDir,
793
+ autoStart: serenaRaw?.autoStart ?? false
740
794
  };
741
795
  if (serena.mcpUrl.trim() === "") {
742
796
  throw new Error("POURKIT_SERENA_MCP_URL must be a non-empty string");
@@ -746,84 +800,100 @@ function parseConfig(raw) {
746
800
  "POURKIT_SERENA_SANDBOX_MCP_URL must be a non-empty string"
747
801
  );
748
802
  }
803
+ const checksRaw = data.checks;
804
+ const labelsRaw = data.labels;
805
+ const cleanupRaw = data.cleanup;
806
+ const sandboxRaw = data.sandbox;
749
807
  return {
750
808
  targets,
751
- labels: data.labels,
809
+ labels: {
810
+ readyForAgent: labelsRaw?.readyForAgent ?? "ready-for-agent",
811
+ agentInProgress: labelsRaw?.agentInProgress ?? "agent-in-progress",
812
+ blocked: labelsRaw?.blocked ?? "blocked",
813
+ prOpenAwaitingMerge: labelsRaw?.prOpenAwaitingMerge ?? "pr-open-awaiting-merge",
814
+ readyForHuman: labelsRaw?.readyForHuman ?? "ready-for-human",
815
+ needsTriage: labelsRaw?.needsTriage ?? "needs-triage"
816
+ },
752
817
  sandbox: {
753
- provider: data.sandbox.provider,
754
- copyToWorktree: data.sandbox.copyToWorktree,
755
- mounts: data.sandbox.mounts,
756
- env: data.sandbox.env,
757
- idleTimeoutSeconds: data.sandbox.idleTimeoutSeconds
818
+ provider: sandboxRaw?.provider ?? "docker",
819
+ copyToWorktree: sandboxRaw?.copyToWorktree,
820
+ mounts: sandboxRaw?.mounts ? sandboxRaw.mounts.map((m) => ({
821
+ hostPath: m.hostPath,
822
+ sandboxPath: m.sandboxPath,
823
+ readonly: m.readonly ?? false
824
+ })) : void 0,
825
+ env: sandboxRaw?.env,
826
+ idleTimeoutSeconds: sandboxRaw?.idleTimeoutSeconds,
827
+ forceRebuild: sandboxRaw?.forceRebuild
758
828
  },
759
829
  checks: {
760
- requiredLabels: data.checks.requiredLabels,
761
- allowedAuthors: data.checks.allowedAuthors,
762
- checksFoundTimeoutSeconds: data.checks.checksFoundTimeoutSeconds ?? 60,
763
- checksCompletionTimeoutSeconds: data.checks.checksCompletionTimeoutSeconds ?? 30 * 60,
764
- pollIntervalSeconds: data.checks.pollIntervalSeconds ?? 15,
765
- issueListLimit: data.checks.issueListLimit ?? 50
830
+ requiredLabels: checksRaw?.requiredLabels ?? [],
831
+ allowedAuthors: checksRaw?.allowedAuthors ?? [],
832
+ checksFoundTimeoutSeconds: checksRaw?.checksFoundTimeoutSeconds ?? 60,
833
+ checksCompletionTimeoutSeconds: checksRaw?.checksCompletionTimeoutSeconds ?? 30 * 60,
834
+ pollIntervalSeconds: checksRaw?.pollIntervalSeconds ?? 15,
835
+ issueListLimit: checksRaw?.issueListLimit ?? 50
766
836
  },
767
837
  serena,
768
838
  cleanup: {
769
- enabled: data.cleanup?.enabled ?? true,
770
- worktreeRetentionDays: data.cleanup?.worktreeRetentionDays ?? 14,
771
- logRetentionDays: data.cleanup?.logRetentionDays ?? 30
839
+ enabled: cleanupRaw?.enabled ?? true,
840
+ worktreeRetentionDays: cleanupRaw?.worktreeRetentionDays ?? 14,
841
+ logRetentionDays: cleanupRaw?.logRetentionDays ?? 30
772
842
  }
773
843
  };
774
844
  }
775
- function assertKnownKeys(value, path9, knownKeys) {
776
- for (const key of Object.keys(value)) {
777
- if (!knownKeys.includes(key)) {
778
- throw new Error(`${path9}.${key} is not supported`);
779
- }
780
- }
781
- }
782
845
  function getVerificationCommands(target) {
783
846
  return target.strategy.verify?.commands ?? [];
784
847
  }
785
- async function loadRepoConfig(repoRoot2, configFileName = "pourkit.config.ts") {
786
- const { existsSync: existsSync15 } = await import("fs");
787
- const { mkdir: mkdir6, writeFile: writeFile4, rm: rm3 } = await import("fs/promises");
788
- const { join: pjoin, basename } = await import("path");
789
- const { pathToFileURL: pathToFileURL2 } = await import("url");
790
- const { build } = await import("esbuild");
791
- const configPath = pjoin(repoRoot2, configFileName);
792
- if (!existsSync15(configPath)) {
848
+ var OBSOLETE_CONFIG_PATHS = [
849
+ "pourkit.config.ts",
850
+ "pourkit.config.mjs",
851
+ "pourkit.config.js",
852
+ "pourkit.json"
853
+ ];
854
+ var CANONICAL_CONFIG_PATH = ".pourkit/config.json";
855
+ async function loadRepoConfig(repoRoot2, _configFileName) {
856
+ const { existsSync: existsSync14 } = await import("fs");
857
+ const { join: pjoin } = await import("path");
858
+ for (const obPath of OBSOLETE_CONFIG_PATHS) {
859
+ const fullPath = pjoin(repoRoot2, obPath);
860
+ if (existsSync14(fullPath)) {
861
+ const isRootJson = obPath === "pourkit.json";
862
+ throw new Error(
863
+ 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".`
864
+ );
865
+ }
866
+ }
867
+ const configPath = pjoin(repoRoot2, CANONICAL_CONFIG_PATH);
868
+ if (!existsSync14(configPath)) {
793
869
  throw new Error(
794
- `No config file found at ${configPath}. Create a ${configFileName} that exports a default PourkitConfig.`
870
+ `No Pourkit config found at ${CANONICAL_CONFIG_PATH}. Run pourkit init or create ${CANONICAL_CONFIG_PATH} with "$schema": "./schema/pourkit.schema.json".`
795
871
  );
796
872
  }
797
- const tmpDir = pjoin(repoRoot2, ".pourkit", ".tmp", "config");
798
- await mkdir6(tmpDir, { recursive: true });
799
- const tmpFile = pjoin(
800
- tmpDir,
801
- `pourkit-config-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
802
- );
873
+ const { readFile: readFile5 } = await import("fs/promises");
874
+ const raw = await readFile5(configPath, "utf-8");
875
+ let parsed;
803
876
  try {
804
- await build({
805
- entryPoints: [configPath],
806
- bundle: true,
807
- write: false,
808
- platform: "node",
809
- format: "esm",
810
- external: ["node:*"]
811
- }).then(async (result) => {
812
- const output = result.outputFiles[0].text;
813
- await writeFile4(tmpFile, output, "utf-8");
814
- });
815
- const imported = await import(pathToFileURL2(tmpFile).href);
816
- const raw = imported.default;
817
- if (raw === void 0) {
818
- throw new Error("pourkit.config.ts must have a default export");
819
- }
820
- return parseConfig(raw);
821
- } finally {
822
- try {
823
- await rm3(tmpFile, { force: true });
824
- } catch {
825
- }
877
+ parsed = JSON.parse(raw);
878
+ } catch (err) {
879
+ const message = err instanceof SyntaxError ? err.message : String(err);
880
+ throw new Error(`${CANONICAL_CONFIG_PATH}: Invalid JSON \u2014 ${message}`);
881
+ }
882
+ return parseConfig(parsed);
883
+ }
884
+ async function loadConfig(configPath) {
885
+ const { readFile: readFile5 } = await import("fs/promises");
886
+ const ext = configPath.split(".").pop()?.toLowerCase();
887
+ if (ext === "json") {
888
+ const raw = await readFile5(configPath, "utf-8");
889
+ return parseConfig(JSON.parse(raw));
890
+ }
891
+ if (ext === "mjs" || ext === "js" || ext === "ts") {
892
+ throw new Error(
893
+ `Executable config (${ext}) is no longer supported. Use .pourkit/config.json with "$schema": "./schema/pourkit.schema.json".`
894
+ );
826
895
  }
896
+ throw new Error(`Unsupported config format: ${ext}`);
827
897
  }
828
898
  function resolvePromptTemplatePath(repoRoot2, promptTemplate) {
829
899
  if (promptTemplate.includes("/")) {
@@ -872,8 +942,8 @@ function renderTemplate(template, issue) {
872
942
  init_common();
873
943
 
874
944
  // shared/run-context.ts
875
- import { existsSync, readFileSync, readdirSync } from "fs";
876
- import { isAbsolute, join as join2, relative, resolve } from "path";
945
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
946
+ import { isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
877
947
 
878
948
  // commands/run-verification.ts
879
949
  init_common();
@@ -1014,11 +1084,17 @@ function buildRunContextMarkdown(options) {
1014
1084
  }
1015
1085
  }
1016
1086
  if (sections.includes("branch")) {
1087
+ const canonicalBaseRef = `origin/${target.baseBranch}`;
1017
1088
  parts.push(
1018
1089
  "## Branch",
1019
1090
  "",
1020
1091
  `- Base: ${target.baseBranch}`,
1092
+ `- Canonical Base Ref: ${canonicalBaseRef}`,
1021
1093
  `- Working Branch: ${branchName}`,
1094
+ "",
1095
+ "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.",
1096
+ "",
1097
+ "Runner-owned Git operations: do not run destructive history/worktree commands such as `git reset --hard`, `git checkout .`, `git clean`, `git rebase`, `git merge`, or `git branch -f`. Edit files only; the runner owns branch/base movement.",
1022
1098
  ""
1023
1099
  );
1024
1100
  }
@@ -1081,7 +1157,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1081
1157
  `### Parent PRD Content: \`${relative(repoRoot2, parentPrdPath)}\``,
1082
1158
  "",
1083
1159
  "```markdown",
1084
- readFileSync(parentPrdPath, "utf-8").trimEnd(),
1160
+ readFileSync2(parentPrdPath, "utf-8").trimEnd(),
1085
1161
  "```",
1086
1162
  ""
1087
1163
  );
@@ -1108,7 +1184,7 @@ function renderPrdContext(issue, parentPrdIssue, repoRoot2) {
1108
1184
  `### Document Content: \`${documentPath}\``,
1109
1185
  "",
1110
1186
  "```markdown",
1111
- readFileSync(absolutePath, "utf-8").trimEnd(),
1187
+ readFileSync2(absolutePath, "utf-8").trimEnd(),
1112
1188
  "```",
1113
1189
  ""
1114
1190
  );
@@ -1133,10 +1209,10 @@ function extractRepoPaths(section) {
1133
1209
  return Array.from(paths);
1134
1210
  }
1135
1211
  function resolveRepoPath(repoRoot2, path9) {
1136
- if (isAbsolute(path9) || path9.includes("\0")) return null;
1137
- const resolved = resolve(repoRoot2, path9);
1212
+ if (isAbsolute2(path9) || path9.includes("\0")) return null;
1213
+ const resolved = resolve2(repoRoot2, path9);
1138
1214
  const repoRelative2 = relative(repoRoot2, resolved);
1139
- if (repoRelative2.startsWith("..") || isAbsolute(repoRelative2)) return null;
1215
+ if (repoRelative2.startsWith("..") || isAbsolute2(repoRelative2)) return null;
1140
1216
  return resolved;
1141
1217
  }
1142
1218
  function findParentPrdPath(repoRoot2, parentRef) {
@@ -1180,17 +1256,19 @@ function renderCriteria(criteria) {
1180
1256
 
1181
1257
  // shared/prompt-guidance.ts
1182
1258
  var PROTECTED_WORK_RULE = "Do **not** revert, delete, or substantially strip already-landed protected sibling/base work unless the issue explicitly requires those files.";
1259
+ var RUNNER_OWNED_GIT_RULE = "Do **not** move Git history or reset the Worktree. Do not run `git reset --hard`, `git checkout .`, `git clean`, `git rebase`, `git merge`, `git branch -f`, or equivalent destructive history/worktree commands; branch/base changes are runner-owned.";
1183
1260
  function appendProtectedWorkGuidance(promptBody) {
1184
1261
  return `${promptBody}
1185
1262
 
1186
1263
  ## Hard Rule
1187
1264
 
1188
- - ${PROTECTED_WORK_RULE}`;
1265
+ - ${PROTECTED_WORK_RULE}
1266
+ - ${RUNNER_OWNED_GIT_RULE}`;
1189
1267
  }
1190
1268
 
1191
1269
  // shared/worktree-run-state.ts
1192
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1193
- import { dirname, join as join3 } from "path";
1270
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1271
+ import { dirname as dirname2, join as join3 } from "path";
1194
1272
  var WORKTREE_RUN_STATE_PATH = ".pourkit/state.json";
1195
1273
  function readWorktreeRunState(worktreePath) {
1196
1274
  const statePath = join3(worktreePath, WORKTREE_RUN_STATE_PATH);
@@ -1198,7 +1276,7 @@ function readWorktreeRunState(worktreePath) {
1198
1276
  return null;
1199
1277
  }
1200
1278
  try {
1201
- const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
1279
+ const raw = JSON.parse(readFileSync3(statePath, "utf-8"));
1202
1280
  if (isValidWorktreeRunState(raw)) {
1203
1281
  return raw;
1204
1282
  }
@@ -1219,7 +1297,7 @@ function isValidWorktreeRunState(raw) {
1219
1297
  }
1220
1298
  function writeWorktreeRunState(worktreePath, state) {
1221
1299
  const statePath = join3(worktreePath, WORKTREE_RUN_STATE_PATH);
1222
- mkdirSync2(dirname(statePath), { recursive: true });
1300
+ mkdirSync2(dirname2(statePath), { recursive: true });
1223
1301
  writeFileSync(statePath, JSON.stringify(state, null, 2), "utf-8");
1224
1302
  }
1225
1303
  function updateWorktreeRunState(worktreePath, update) {
@@ -1541,12 +1619,12 @@ function validateRecoveryDecision(artifact, allowedDecisions) {
1541
1619
  }
1542
1620
 
1543
1621
  // shared/attempt-log.ts
1544
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
1545
- import { dirname as dirname2, join as join4 } from "path";
1622
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4 } from "fs";
1623
+ import { dirname as dirname3, join as join4 } from "path";
1546
1624
  var ATTEMPT_LOG_PATH = ".pourkit/attempt-log.jsonl";
1547
1625
  function writeAttemptLog(worktreePath, entry) {
1548
1626
  const logPath = join4(worktreePath, ATTEMPT_LOG_PATH);
1549
- mkdirSync3(dirname2(logPath), { recursive: true });
1627
+ mkdirSync3(dirname3(logPath), { recursive: true });
1550
1628
  appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8");
1551
1629
  }
1552
1630
  function readAttemptLog(worktreePath) {
@@ -1554,7 +1632,7 @@ function readAttemptLog(worktreePath) {
1554
1632
  if (!existsSync3(logPath)) {
1555
1633
  return [];
1556
1634
  }
1557
- const raw = readFileSync3(logPath, "utf-8");
1635
+ const raw = readFileSync4(logPath, "utf-8");
1558
1636
  const lines = raw.split("\n").filter((l) => l.length > 0);
1559
1637
  const entries = [];
1560
1638
  for (const line of lines) {
@@ -1673,7 +1751,7 @@ async function runBaseRefreshAttempt(options) {
1673
1751
  }
1674
1752
 
1675
1753
  // commands/conflict-resolution.ts
1676
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1754
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
1677
1755
  import { join as join8 } from "path";
1678
1756
  init_common();
1679
1757
 
@@ -1838,8 +1916,8 @@ function parseConflictResolutionArtifact(output) {
1838
1916
  // commands/artifact-validation.ts
1839
1917
  import { createHash } from "crypto";
1840
1918
  import { execSync } from "child_process";
1841
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
1842
- import { isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
1919
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
1920
+ import { isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
1843
1921
 
1844
1922
  // pr/review-verdict.ts
1845
1923
  var ReviewVerdictProtocolError = class extends Error {
@@ -1874,15 +1952,15 @@ function parseReviewVerdict(output) {
1874
1952
  import {
1875
1953
  existsSync as existsSync6,
1876
1954
  mkdirSync as mkdirSync6,
1877
- readFileSync as readFileSync6,
1955
+ readFileSync as readFileSync7,
1878
1956
  readdirSync as readdirSync2,
1879
1957
  writeFileSync as writeFileSync3
1880
1958
  } from "fs";
1881
1959
  import { join as join6 } from "path";
1882
1960
 
1883
1961
  // execution/agent-output-retry.ts
1884
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync } from "fs";
1885
- import { dirname as dirname3, join as join5 } from "path";
1962
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync } from "fs";
1963
+ import { dirname as dirname4, join as join5 } from "path";
1886
1964
  async function executeWithMissingOrEmptyArtifactRetry({
1887
1965
  executionProvider,
1888
1966
  executionOptions,
@@ -1942,20 +2020,20 @@ function readArtifactOutput(artifactPath) {
1942
2020
  if (!existsSync4(artifactPath)) {
1943
2021
  return { _tag: "missing", path: artifactPath };
1944
2022
  }
1945
- const output = readFileSync4(artifactPath, "utf-8");
2023
+ const output = readFileSync5(artifactPath, "utf-8");
1946
2024
  if (!output.trim()) {
1947
2025
  return { _tag: "empty", path: artifactPath };
1948
2026
  }
1949
2027
  return { _tag: "content", value: output, path: artifactPath };
1950
2028
  }
1951
2029
  function prepareArtifactPath(artifactPath) {
1952
- mkdirSync4(dirname3(artifactPath), { recursive: true });
2030
+ mkdirSync4(dirname4(artifactPath), { recursive: true });
1953
2031
  rmSync(artifactPath, { recursive: true, force: true });
1954
2032
  }
1955
2033
 
1956
2034
  // shared/effect-services.ts
1957
2035
  import { Context, Effect as Effect3, Layer } from "effect";
1958
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
2036
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
1959
2037
  var GitExecutionError = class extends Error {
1960
2038
  _tag = "GitExecutionError";
1961
2039
  message;
@@ -1971,7 +2049,7 @@ var FileSystemDefault = Layer.succeed(
1971
2049
  FileSystem,
1972
2050
  FileSystem.of({
1973
2051
  readFile: (path9) => Effect3.try({
1974
- try: () => readFileSync5(path9, "utf-8"),
2052
+ try: () => readFileSync6(path9, "utf-8"),
1975
2053
  catch: (error) => new Error(
1976
2054
  `Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
1977
2055
  )
@@ -2195,7 +2273,7 @@ function validateRefactorArtifact(artifactPath, findingIds) {
2195
2273
  `Refactor artifact missing at ${artifactPath}`
2196
2274
  );
2197
2275
  }
2198
- const content = readFileSync6(artifactPath, "utf-8");
2276
+ const content = readFileSync7(artifactPath, "utf-8");
2199
2277
  if (!content.trim()) {
2200
2278
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
2201
2279
  }
@@ -2520,12 +2598,29 @@ A prior review emitted \`NEEDS_HUMAN\` and stopped the agent loop. The issue has
2520
2598
  Before carrying forward old blockers, inspect newer issue comments and the current worktree. Treat prior Reviewer and Refactor Artifacts as historical context, not active findings unless they still apply.
2521
2599
 
2522
2600
  ` : "";
2523
- return `${renderedTemplate}
2601
+ return appendProtectedWorkGuidance(`${renderedTemplate}
2524
2602
 
2525
2603
  ## Shared Run Context
2526
2604
 
2527
2605
  Read the selected issue requirements, PRD context, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
2528
2606
 
2607
+ ## Initial Verification Pass
2608
+
2609
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
2610
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
2611
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
2612
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
2613
+ - 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.
2614
+ - If no verification commands are configured, note that and proceed with normal review.
2615
+
2616
+ ## Scope Evidence Rules
2617
+
2618
+ - Use the Run Context's canonical base ref, for example \`origin/<base>\`, for scope diffs and commit ranges.
2619
+ - 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.
2620
+ - Only call a file or commit out of scope when it is part of the working branch's delta from the canonical base ref.
2621
+ - 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.
2622
+ - If scope evidence is ambiguous, use \`NEEDS_HUMAN\` or ask for a runner/base mismatch decision instead of telling Refactor to revert files.
2623
+
2529
2624
  ${hasCriteriaPlaceholder ? "" : `## Review Criteria
2530
2625
 
2531
2626
  ${criteriaBlock}
@@ -2542,7 +2637,7 @@ End the file with exactly one wrapped verdict token: <verdict>PASS</verdict>, <v
2542
2637
 
2543
2638
  Findings must include an ID column with values in the format R${iteration}.F{findingNumber} (e.g., R${iteration}.F1, R${iteration}.F2) and a Supersedes column referencing the finding ID being superseded (or a hyphen for new findings).
2544
2639
 
2545
- When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`;
2640
+ When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`);
2546
2641
  });
2547
2642
  }
2548
2643
  function renderReviewHistory(reviewHistory) {
@@ -2667,12 +2762,12 @@ function recoverReviewOutputFromLog(logPath) {
2667
2762
  if (!existsSync6(logPath)) {
2668
2763
  return null;
2669
2764
  }
2670
- const logContent = readFileSync6(logPath, "utf-8");
2765
+ const logContent = readFileSync7(logPath, "utf-8");
2671
2766
  return recoverReviewOutputFromString(logContent);
2672
2767
  }
2673
2768
  function readReviewArtifact(artifactPath, logPath) {
2674
2769
  if (existsSync6(artifactPath)) {
2675
- const output = readFileSync6(artifactPath, "utf-8");
2770
+ const output = readFileSync7(artifactPath, "utf-8");
2676
2771
  if (output.trim()) {
2677
2772
  return output;
2678
2773
  }
@@ -3164,7 +3259,7 @@ function inferConventionalType(commitSummaries) {
3164
3259
  }
3165
3260
 
3166
3261
  // prd-run/final-review-validation.ts
3167
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3262
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
3168
3263
  function parseFinalReviewArtifact(artifactPath) {
3169
3264
  if (!existsSync7(artifactPath)) {
3170
3265
  return {
@@ -3175,7 +3270,7 @@ function parseFinalReviewArtifact(artifactPath) {
3175
3270
  }
3176
3271
  let content;
3177
3272
  try {
3178
- content = readFileSync7(artifactPath, "utf-8");
3273
+ content = readFileSync8(artifactPath, "utf-8");
3179
3274
  } catch (error) {
3180
3275
  return {
3181
3276
  ok: false,
@@ -3378,7 +3473,7 @@ function readArtifact(options) {
3378
3473
  };
3379
3474
  }
3380
3475
  try {
3381
- const content = readFileSync8(options.artifactPath, "utf-8");
3476
+ const content = readFileSync9(options.artifactPath, "utf-8");
3382
3477
  if (!content.trim()) {
3383
3478
  return { ok: false, result: invalid(options, "Artifact is empty") };
3384
3479
  }
@@ -3448,7 +3543,7 @@ function validateIssueFinalReviewArtifact(parsed, options) {
3448
3543
  for (const p of parsed.changedPaths) {
3449
3544
  const normalized = p.replace(/\\/g, "/");
3450
3545
  const segments = normalized.split("/");
3451
- if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute2(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3546
+ if (normalized.trim() === "" || normalized === "." || normalized === ".." || isAbsolute3(p) || normalized.startsWith("/") || segments.some((segment) => segment === "..")) {
3452
3547
  return {
3453
3548
  ok: false,
3454
3549
  reason: `changedPaths must not contain absolute paths or path traversal: ${p}`,
@@ -3549,7 +3644,7 @@ function validateAgentArtifact(options) {
3549
3644
  case "refactor": {
3550
3645
  let findingIds = options.findingIds ?? [];
3551
3646
  if (findingIds.length === 0 && options.latestReviewArtifactPath) {
3552
- const latestReview = readFileSync8(
3647
+ const latestReview = readFileSync9(
3553
3648
  options.latestReviewArtifactPath,
3554
3649
  "utf-8"
3555
3650
  );
@@ -3572,10 +3667,10 @@ function validateAgentArtifact(options) {
3572
3667
  if (options.checkConflictMarkers !== false && parsed.status === "resolved") {
3573
3668
  const base = options.worktreePath ?? process.cwd();
3574
3669
  const filesWithMarkers = parsed.files.filter((file) => {
3575
- const filePath = resolve2(base, file);
3670
+ const filePath = resolve3(base, file);
3576
3671
  try {
3577
3672
  return CONFLICT_MARKER_PATTERN.test(
3578
- readFileSync8(filePath, "utf-8")
3673
+ readFileSync9(filePath, "utf-8")
3579
3674
  );
3580
3675
  } catch {
3581
3676
  return false;
@@ -3686,7 +3781,7 @@ async function hasUnresolvedConflictMarkers(worktreePath, files) {
3686
3781
  for (const file of files) {
3687
3782
  const filePath = join8(worktreePath, file);
3688
3783
  try {
3689
- const content = readFileSync9(filePath, "utf-8");
3784
+ const content = readFileSync10(filePath, "utf-8");
3690
3785
  if (CONFLICT_MARKER_PATTERN2.test(content)) {
3691
3786
  return true;
3692
3787
  }
@@ -3944,7 +4039,7 @@ async function canReachMcp(url) {
3944
4039
  return true;
3945
4040
  } catch {
3946
4041
  if (attempt < 9) {
3947
- await new Promise((resolve3) => setTimeout(resolve3, 100));
4042
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
3948
4043
  }
3949
4044
  }
3950
4045
  }
@@ -3995,7 +4090,7 @@ async function prepareSerenaForTarget(options) {
3995
4090
  }
3996
4091
 
3997
4092
  // failure-resolution/failure-resolution-agent.ts
3998
- import { readFileSync as readFileSync10 } from "fs";
4093
+ import { readFileSync as readFileSync11 } from "fs";
3999
4094
  import { join as join9 } from "path";
4000
4095
 
4001
4096
  // failure-resolution/recovery-policy.ts
@@ -4149,7 +4244,7 @@ async function runFailureResolutionAgent(options) {
4149
4244
  }
4150
4245
  let artifact;
4151
4246
  try {
4152
- const md = readFileSync10(fullArtifactPath, "utf-8");
4247
+ const md = readFileSync11(fullArtifactPath, "utf-8");
4153
4248
  const validation2 = validateAgentArtifact({
4154
4249
  kind: "failure-resolution",
4155
4250
  artifactPath: fullArtifactPath,
@@ -4243,7 +4338,7 @@ async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary,
4243
4338
 
4244
4339
  // commands/pr-description-agent.ts
4245
4340
  import { join as join11 } from "path";
4246
- import { readFileSync as readFileSync11 } from "fs";
4341
+ import { readFileSync as readFileSync12 } from "fs";
4247
4342
 
4248
4343
  // pr/pr-description-context.ts
4249
4344
  init_common();
@@ -4437,7 +4532,7 @@ function runFinalizerAgent(options) {
4437
4532
  ],
4438
4533
  logger: options.logger
4439
4534
  });
4440
- const output = readFileSync11(artifactPath, "utf-8");
4535
+ const output = readFileSync12(artifactPath, "utf-8");
4441
4536
  yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
4442
4537
  return result;
4443
4538
  });
@@ -4565,7 +4660,7 @@ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
4565
4660
 
4566
4661
  // prd-run/local-merge-coordinator.ts
4567
4662
  import { execFileSync as execFileSync2 } from "child_process";
4568
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
4663
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
4569
4664
  import { join as join12 } from "path";
4570
4665
 
4571
4666
  // prd-run/local-branches.ts
@@ -4592,7 +4687,7 @@ function readIssueBranchName(repoRoot2, prdId, issueId) {
4592
4687
  const issuePath = getIssueArtifactPath(repoRoot2, prdId, issueId);
4593
4688
  if (!existsSync10(issuePath)) return null;
4594
4689
  try {
4595
- const content = readFileSync12(issuePath, "utf-8");
4690
+ const content = readFileSync13(issuePath, "utf-8");
4596
4691
  const parsed = JSON.parse(content);
4597
4692
  return typeof parsed.branchName === "string" && parsed.branchName ? parsed.branchName : null;
4598
4693
  } catch {
@@ -4604,7 +4699,7 @@ async function hasLocalIssueMergeReceipt(prdId, issueId, repoRoot2) {
4604
4699
  const receiptPath = getMergeReceiptPath(root, prdId, issueId);
4605
4700
  if (!existsSync10(receiptPath)) return null;
4606
4701
  try {
4607
- const content = readFileSync12(receiptPath, "utf-8");
4702
+ const content = readFileSync13(receiptPath, "utf-8");
4608
4703
  const parsed = JSON.parse(content);
4609
4704
  if (typeof parsed.prdId === "string" && typeof parsed.issueId === "string" && typeof parsed.stage === "string" && typeof parsed.sourceBranch === "string" && typeof parsed.localPrdBranch === "string" && typeof parsed.mergeCommit === "string" && typeof parsed.completedAt === "string") {
4610
4705
  return parsed;
@@ -4620,7 +4715,7 @@ async function squashMergeLocalIssue(prdId, issueId, input, repoRoot2) {
4620
4715
  if (existsSync10(receiptPath)) {
4621
4716
  try {
4622
4717
  const existing = JSON.parse(
4623
- readFileSync12(receiptPath, "utf-8")
4718
+ readFileSync13(receiptPath, "utf-8")
4624
4719
  );
4625
4720
  if (existing.prdId === prdId && existing.issueId === issueId && existing.mergeCommit) {
4626
4721
  return {
@@ -4983,7 +5078,7 @@ function runMergeCoordinator(options) {
4983
5078
  }
4984
5079
 
4985
5080
  // commands/issue-final-review-agent.ts
4986
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
5081
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
4987
5082
  import { join as join13 } from "path";
4988
5083
  var ISSUE_FINAL_REVIEW_ARTIFACT_PATH = join13(
4989
5084
  ".pourkit",
@@ -5097,12 +5192,21 @@ async function runIssueFinalReviewAgent(options) {
5097
5192
  }
5098
5193
  function loadIssueFinalReviewPrompt(repoRoot2, promptTemplate) {
5099
5194
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
5100
- const promptBody = existsSync11(promptPath) ? readFileSync13(promptPath, "utf-8") : promptTemplate;
5195
+ const promptBody = existsSync11(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
5101
5196
  return appendProtectedWorkGuidance(`${promptBody}
5102
5197
 
5103
5198
  ## Shared Run Context
5104
5199
 
5105
- Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}`);
5200
+ Read the selected issue requirements, PRD context, comments, branch context, verification commands, and artifact paths from: ${RUN_CONTEXT_PATH_IN_WORKTREE}
5201
+
5202
+ ## Initial Verification Pass
5203
+
5204
+ - First read ${RUN_CONTEXT_PATH_IN_WORKTREE} only far enough to identify the configured verification commands.
5205
+ - Before reviewing code, diffs, artifacts, or prior findings, run each configured verification command yourself from the Worktree.
5206
+ - Run the commands exactly as configured. Do not substitute narrower commands unless the configured command cannot run.
5207
+ - If a configured command fails, keep reviewing after recording the failure details; use the failure output as review evidence.
5208
+ - 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.
5209
+ - If no verification commands are configured, note that and proceed with normal review.`);
5106
5210
  }
5107
5211
 
5108
5212
  // issues/issue-transitions.ts
@@ -5305,7 +5409,7 @@ async function advanceIssueFinalReview(options) {
5305
5409
  "Issue Final Review state is incomplete: missing artifactPath"
5306
5410
  );
5307
5411
  }
5308
- const artifactPath = isAbsolute3(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join14(worktreePath, ifrFromState.artifactPath);
5412
+ const artifactPath = isAbsolute4(ifrFromState.artifactPath) ? ifrFromState.artifactPath : join14(worktreePath, ifrFromState.artifactPath);
5309
5413
  const validation = validateAgentArtifact({
5310
5414
  kind: "issue-final-review",
5311
5415
  artifactPath,
@@ -5337,6 +5441,12 @@ async function advanceIssueFinalReview(options) {
5337
5441
  logger,
5338
5442
  reviewArtifactPath
5339
5443
  });
5444
+ await assertCanonicalBaseAncestor({
5445
+ worktreePath,
5446
+ baseRef: `origin/${target.baseBranch}`,
5447
+ stageName: "Issue Final Review",
5448
+ logger
5449
+ });
5340
5450
  if (result.verdict === "pass") {
5341
5451
  updateWorktreeRunState(worktreePath, {
5342
5452
  issueFinalReview: {
@@ -5561,6 +5671,14 @@ async function startIssueRun(options) {
5561
5671
  };
5562
5672
  }
5563
5673
  if (executionResult.worktreePath) {
5674
+ if (shouldRunBuilder) {
5675
+ await assertCanonicalBaseAncestor({
5676
+ worktreePath: executionResult.worktreePath,
5677
+ baseRef: `origin/${effectiveBaseBranch}`,
5678
+ stageName: "Builder",
5679
+ logger
5680
+ });
5681
+ }
5564
5682
  if (worktreeState) {
5565
5683
  updateWorktreeRunState(executionResult.worktreePath, {
5566
5684
  completedStages: { builder: true }
@@ -5622,6 +5740,12 @@ async function advanceIssueRunReview(options) {
5622
5740
  }
5623
5741
  })
5624
5742
  );
5743
+ await assertCanonicalBaseAncestor({
5744
+ worktreePath: options.worktreePath,
5745
+ baseRef: `origin/${options.target.baseBranch}`,
5746
+ stageName: "Review/Refactor",
5747
+ logger: options.logger
5748
+ });
5625
5749
  updateWorktreeRunState(options.worktreePath, {
5626
5750
  review: {
5627
5751
  lifetimeIterations: reviewResult.lifetimeIterations,
@@ -5690,7 +5814,7 @@ async function completeIssueRun(options) {
5690
5814
  message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
5691
5815
  });
5692
5816
  }
5693
- const artifactContent = readFileSync14(
5817
+ const artifactContent = readFileSync15(
5694
5818
  finalizerFromState.artifactPath,
5695
5819
  "utf-8"
5696
5820
  );
@@ -5748,6 +5872,14 @@ async function completeIssueRun(options) {
5748
5872
  }
5749
5873
  prTitle = finalizerResult.title;
5750
5874
  prBody = finalizerResult.body;
5875
+ if (executionResult.worktreePath) {
5876
+ await assertCanonicalBaseAncestor({
5877
+ worktreePath: executionResult.worktreePath,
5878
+ baseRef: `origin/${effectiveBaseBranch}`,
5879
+ stageName: "Finalizer",
5880
+ logger
5881
+ });
5882
+ }
5751
5883
  }
5752
5884
  prTitle = ensureConventionalPrTitle(
5753
5885
  prTitle,
@@ -6116,18 +6248,12 @@ function getRefactorArtifactDir(artifactPath) {
6116
6248
  }
6117
6249
  async function finalizeWorktreeCommit(options) {
6118
6250
  const { worktreePath, baseRef, title, body, logger } = options;
6119
- await syncRemoteBaseRef(worktreePath, baseRef, logger);
6120
- try {
6121
- await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
6122
- cwd: worktreePath,
6123
- logger,
6124
- label: "git merge-base --is-ancestor"
6125
- });
6126
- } catch {
6127
- throw new Error(
6128
- `Cannot finalize stale worktree: ${baseRef} is not an ancestor of HEAD. Refresh the branch onto the latest target base before creating the final commit.`
6129
- );
6130
- }
6251
+ await assertCanonicalBaseAncestor({
6252
+ worktreePath,
6253
+ baseRef,
6254
+ stageName: "final commit",
6255
+ logger
6256
+ });
6131
6257
  await execCapture("git", ["reset", "--soft", baseRef], {
6132
6258
  cwd: worktreePath,
6133
6259
  logger,
@@ -6144,6 +6270,21 @@ async function finalizeWorktreeCommit(options) {
6144
6270
  label: "git commit"
6145
6271
  });
6146
6272
  }
6273
+ async function assertCanonicalBaseAncestor(options) {
6274
+ const { worktreePath, baseRef, stageName, logger } = options;
6275
+ await syncRemoteBaseRef(worktreePath, baseRef, logger);
6276
+ try {
6277
+ await execCapture("git", ["merge-base", "--is-ancestor", baseRef, "HEAD"], {
6278
+ cwd: worktreePath,
6279
+ logger,
6280
+ label: "git merge-base --is-ancestor"
6281
+ });
6282
+ } catch {
6283
+ throw new Error(
6284
+ `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.`
6285
+ );
6286
+ }
6287
+ }
6147
6288
  async function syncRemoteBaseRef(worktreePath, baseRef, logger) {
6148
6289
  const remoteBase = parseRemoteBaseRef(baseRef);
6149
6290
  if (!remoteBase) {
@@ -6387,7 +6528,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
6387
6528
  }
6388
6529
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
6389
6530
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
6390
- const promptBody = existsSync12(promptPath) ? readFileSync14(promptPath, "utf-8") : promptTemplate;
6531
+ const promptBody = existsSync12(promptPath) ? readFileSync15(promptPath, "utf-8") : promptTemplate;
6391
6532
  return appendProtectedWorkGuidance(`${promptBody}
6392
6533
 
6393
6534
  ## Shared Run Context
@@ -7384,7 +7525,7 @@ import path6 from "path";
7384
7525
 
7385
7526
  // execution/sandbox-image.ts
7386
7527
  import { createHash as createHash2 } from "crypto";
7387
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
7528
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
7388
7529
  import path5 from "path";
7389
7530
  function sandboxImageName(repoRoot2) {
7390
7531
  const dirName = path5.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
@@ -7394,7 +7535,7 @@ function sandboxImageName(repoRoot2) {
7394
7535
  if (!existsSync13(dockerfilePath)) {
7395
7536
  return `sandcastle:${baseName}`;
7396
7537
  }
7397
- const fingerprint = createHash2("sha256").update(readFileSync15(dockerfilePath)).digest("hex").slice(0, 8);
7538
+ const fingerprint = createHash2("sha256").update(readFileSync16(dockerfilePath)).digest("hex").slice(0, 8);
7398
7539
  return `sandcastle:${baseName}-${fingerprint}`;
7399
7540
  }
7400
7541
 
@@ -7436,11 +7577,11 @@ init_common();
7436
7577
  import { mkdtempSync } from "fs";
7437
7578
  import { writeFile } from "fs/promises";
7438
7579
  import { tmpdir } from "os";
7439
- import { dirname as dirname4, join as join15 } from "path";
7580
+ import { dirname as dirname5, join as join15 } from "path";
7440
7581
  async function writeExecutionArtifacts(worktreePath, artifacts) {
7441
7582
  for (const artifact of artifacts) {
7442
7583
  const filePath = join15(worktreePath, artifact.path);
7443
- await ensureDir(dirname4(filePath));
7584
+ await ensureDir(dirname5(filePath));
7444
7585
  await writeFile(filePath, artifact.content, "utf-8");
7445
7586
  }
7446
7587
  }
@@ -8019,10 +8160,7 @@ function resolveE2EConfigFile(root) {
8019
8160
  if (explicitConfig) {
8020
8161
  return explicitConfig;
8021
8162
  }
8022
- if (existsSync14(path8.join(root, "pourkit.config.ts"))) {
8023
- return "pourkit.config.ts";
8024
- }
8025
- return "pourkit.config.example.ts";
8163
+ return path8.join(".pourkit", "config.json");
8026
8164
  }
8027
8165
  function generateRunId() {
8028
8166
  return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -8390,7 +8528,8 @@ async function runSuccessE2E(options, runId, root, logger, client) {
8390
8528
  if (!options.keep) {
8391
8529
  await persistResources(root, runId, resources);
8392
8530
  }
8393
- const baseConfig = await loadRepoConfig(root, resolveE2EConfigFile(root));
8531
+ const e2eConfigFile = resolveE2EConfigFile(root);
8532
+ const baseConfig = e2eConfigFile === path8.join(".pourkit", "config.json") ? await loadRepoConfig(root) : await loadConfig(path8.resolve(root, e2eConfigFile));
8394
8533
  const profile = resolveProfile(options.fullCheck);
8395
8534
  const config = makeE2EConfig(
8396
8535
  baseConfig,
@@ -8507,7 +8646,8 @@ async function runFailureE2E(options, runId, root, logger) {
8507
8646
  if (!options.keep) {
8508
8647
  await persistResources(root, runId, resources);
8509
8648
  }
8510
- const baseConfig = await loadRepoConfig(root, resolveE2EConfigFile(root));
8649
+ const e2eConfigFile = resolveE2EConfigFile(root);
8650
+ const baseConfig = e2eConfigFile === path8.join(".pourkit", "config.json") ? await loadRepoConfig(root) : await loadConfig(path8.resolve(root, e2eConfigFile));
8511
8651
  const profile = resolveProfile(options.fullCheck);
8512
8652
  const config = makeFailureE2EConfig(
8513
8653
  baseConfig,