@pourkit/cli 0.0.0-next-20260601025244 → 0.0.0-next-20260601224754

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
@@ -333,6 +333,426 @@ var init_common = __esm({
333
333
  }
334
334
  });
335
335
 
336
+ // providers/github-client.ts
337
+ var github_client_exports = {};
338
+ __export(github_client_exports, {
339
+ requireGitHubClient: () => requireGitHubClient,
340
+ resolveGitHubRepository: () => resolveGitHubRepository,
341
+ resolveGitHubToken: () => resolveGitHubToken,
342
+ tryCreateGitHubClient: () => tryCreateGitHubClient
343
+ });
344
+ import { Octokit } from "octokit";
345
+ function resolveGitHubToken(env) {
346
+ const token = env.POURKIT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
347
+ if (!token) {
348
+ throw new Error(
349
+ "GitHub token is required. Set POURKIT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN."
350
+ );
351
+ }
352
+ return token;
353
+ }
354
+ async function resolveGitHubRepository(options) {
355
+ if (options?.repository) {
356
+ const parts = options.repository.split("/");
357
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
358
+ throw new Error(
359
+ `Invalid repository format: "${options.repository}". Expected "owner/repo".`
360
+ );
361
+ }
362
+ return { owner: parts[0], repo: parts[1] };
363
+ }
364
+ const env = options?.env ?? process.env;
365
+ const envRepo = env.GITHUB_REPOSITORY;
366
+ if (envRepo) {
367
+ const parts = envRepo.split("/");
368
+ if (parts.length === 2 && parts[0] && parts[1]) {
369
+ return { owner: parts[0], repo: parts[1] };
370
+ }
371
+ throw new Error(
372
+ `Invalid repository format: "${envRepo}". Expected "owner/repo".`
373
+ );
374
+ }
375
+ const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
376
+ const cwd = options?.cwd;
377
+ try {
378
+ const result = await execCapture2("git", ["remote", "get-url", "origin"], {
379
+ cwd
380
+ });
381
+ const remote = result.stdout.trim();
382
+ const match = remote.match(REMOTE_PATTERN);
383
+ if (match) {
384
+ return { owner: match[1], repo: match[2] };
385
+ }
386
+ } catch {
387
+ }
388
+ throw new Error(
389
+ "Could not resolve GitHub repository. Set GITHUB_REPOSITORY env var or ensure a valid 'origin' remote exists."
390
+ );
391
+ }
392
+ async function requireGitHubClient(options) {
393
+ const env = options?.env ?? process.env;
394
+ const token = resolveGitHubToken(env);
395
+ const repo = await resolveGitHubRepository(options);
396
+ const octokit = new Octokit({ auth: token });
397
+ return { octokit, ...repo };
398
+ }
399
+ async function tryCreateGitHubClient(options) {
400
+ const env = options?.env ?? process.env;
401
+ let token;
402
+ try {
403
+ token = resolveGitHubToken(env);
404
+ } catch {
405
+ return {
406
+ ok: false,
407
+ reason: "missing-token",
408
+ message: "GitHub token is not configured."
409
+ };
410
+ }
411
+ let repo;
412
+ try {
413
+ repo = await resolveGitHubRepository(options);
414
+ } catch (e) {
415
+ const message = e instanceof Error ? e.message : String(e);
416
+ if (message.includes("Invalid repository format")) {
417
+ return { ok: false, reason: "invalid-repository", message };
418
+ }
419
+ return { ok: false, reason: "missing-repository", message };
420
+ }
421
+ const octokit = new Octokit({ auth: token });
422
+ return { ok: true, client: { octokit, ...repo } };
423
+ }
424
+ var REMOTE_PATTERN;
425
+ var init_github_client = __esm({
426
+ "providers/github-client.ts"() {
427
+ "use strict";
428
+ REMOTE_PATTERN = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/;
429
+ }
430
+ });
431
+
432
+ // execution/execution-provider.ts
433
+ import { writeFile } from "fs/promises";
434
+ import { dirname as dirname3, join as join7 } from "path";
435
+ async function writeExecutionArtifacts(worktreePath, artifacts) {
436
+ for (const artifact of artifacts) {
437
+ const filePath = join7(worktreePath, artifact.path);
438
+ await ensureDir(dirname3(filePath));
439
+ await writeFile(filePath, artifact.content, "utf-8");
440
+ }
441
+ }
442
+ var init_execution_provider = __esm({
443
+ "execution/execution-provider.ts"() {
444
+ "use strict";
445
+ init_common();
446
+ }
447
+ });
448
+
449
+ // execution/sandbox-image.ts
450
+ import { createHash } from "crypto";
451
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
452
+ import path5 from "path";
453
+ function sandboxImageName(repoRoot2) {
454
+ const dirName = path5.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
455
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
456
+ const baseName = sanitized || "local";
457
+ const dockerfilePath = path5.join(repoRoot2, ".sandcastle", "Dockerfile");
458
+ if (!existsSync5(dockerfilePath)) {
459
+ return `sandcastle:${baseName}`;
460
+ }
461
+ const fingerprint = createHash("sha256").update(readFileSync5(dockerfilePath)).digest("hex").slice(0, 8);
462
+ return `sandcastle:${baseName}-${fingerprint}`;
463
+ }
464
+ var init_sandbox_image = __esm({
465
+ "execution/sandbox-image.ts"() {
466
+ "use strict";
467
+ }
468
+ });
469
+
470
+ // execution/sandbox-image-build.ts
471
+ import path6 from "path";
472
+ async function ensureSandboxImageBuilt(repoRoot2, options) {
473
+ const imageName = sandboxImageName(repoRoot2);
474
+ const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
475
+ if (!options?.force) {
476
+ try {
477
+ await execCapture("docker", ["image", "inspect", imageName]);
478
+ return;
479
+ } catch {
480
+ }
481
+ }
482
+ const buildArgs = ["build", "-t", imageName, "-f", dockerfilePath];
483
+ if (options?.force) {
484
+ buildArgs.push("--pull", "--no-cache");
485
+ }
486
+ buildArgs.push(repoRoot2);
487
+ await execCapture("docker", buildArgs);
488
+ }
489
+ var init_sandbox_image_build = __esm({
490
+ "execution/sandbox-image-build.ts"() {
491
+ "use strict";
492
+ init_common();
493
+ init_sandbox_image();
494
+ }
495
+ });
496
+
497
+ // execution/sandcastle-existing-worktree.ts
498
+ async function createSandboxFromExistingWorktree(options) {
499
+ const sandcastleEntryUrl = import.meta.resolve("@ai-hero/sandcastle");
500
+ const createSandboxUrl = new URL("./createSandbox.js", sandcastleEntryUrl);
501
+ const sandcastleCreateSandbox = await import(createSandboxUrl.href);
502
+ return sandcastleCreateSandbox.createSandboxFromWorktree(options);
503
+ }
504
+ var init_sandcastle_existing_worktree = __esm({
505
+ "execution/sandcastle-existing-worktree.ts"() {
506
+ "use strict";
507
+ }
508
+ });
509
+
510
+ // execution/sandbox-options.ts
511
+ function buildSandboxOptions(repoRoot2, sandbox) {
512
+ const mounts = [];
513
+ if (sandbox.mounts !== void 0) {
514
+ mounts.push(...sandbox.mounts);
515
+ }
516
+ return {
517
+ imageName: sandboxImageName(repoRoot2),
518
+ ...mounts.length > 0 ? { mounts } : {},
519
+ ...sandbox.env !== void 0 ? { env: sandbox.env } : {},
520
+ ...sandbox.idleTimeoutSeconds !== void 0 ? { idleTimeoutSeconds: sandbox.idleTimeoutSeconds } : {}
521
+ };
522
+ }
523
+ var init_sandbox_options = __esm({
524
+ "execution/sandbox-options.ts"() {
525
+ "use strict";
526
+ init_sandbox_image();
527
+ }
528
+ });
529
+
530
+ // execution/opencode-config.ts
531
+ function isSerenaEligibleStage(stage) {
532
+ return stage === "builder" || stage === "refactor";
533
+ }
534
+ function buildSerenaOpenCodeConfig(stage, serena) {
535
+ if (!serena?.available || !isSerenaEligibleStage(stage)) {
536
+ return void 0;
537
+ }
538
+ return {
539
+ mcp: {
540
+ serena: {
541
+ type: "remote",
542
+ url: serena.sandboxMcpUrl,
543
+ enabled: true
544
+ }
545
+ }
546
+ };
547
+ }
548
+ var init_opencode_config = __esm({
549
+ "execution/opencode-config.ts"() {
550
+ "use strict";
551
+ }
552
+ });
553
+
554
+ // execution/sandcastle-execution.ts
555
+ var sandcastle_execution_exports = {};
556
+ __export(sandcastle_execution_exports, {
557
+ SandcastleExecutionProvider: () => SandcastleExecutionProvider
558
+ });
559
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
560
+ import { join as join8 } from "path";
561
+ import { createWorktree, opencode } from "@ai-hero/sandcastle";
562
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
563
+ function resolveSandboxProvider(provider, dockerFactory) {
564
+ if (provider === "docker") {
565
+ return dockerFactory;
566
+ }
567
+ throw new Error(`Unsupported sandbox provider: ${provider}`);
568
+ }
569
+ function sanitizeBranch(branchName) {
570
+ return branchName.replace(/[^A-Za-z0-9._-]/g, "-");
571
+ }
572
+ function savePromptToFile(repoRoot2, stage, iteration, prompt) {
573
+ const promptsDir = join8(repoRoot2, ".pourkit", ".tmp", "prompts");
574
+ mkdirSync4(promptsDir, { recursive: true });
575
+ const timestamp2 = Date.now();
576
+ const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
577
+ const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
578
+ const filePath = join8(promptsDir, filename);
579
+ writeFileSync2(filePath, prompt, "utf-8");
580
+ }
581
+ var SandcastleExecutionProvider, SandcastleExecutionSession;
582
+ var init_sandcastle_execution = __esm({
583
+ "execution/sandcastle-execution.ts"() {
584
+ "use strict";
585
+ init_execution_provider();
586
+ init_sandbox_image_build();
587
+ init_sandcastle_existing_worktree();
588
+ init_sandbox_options();
589
+ init_opencode_config();
590
+ SandcastleExecutionProvider = class {
591
+ async createSession() {
592
+ return new SandcastleExecutionSession();
593
+ }
594
+ async execute(options) {
595
+ const session = await this.createSession();
596
+ try {
597
+ return await session.execute(options);
598
+ } finally {
599
+ await session.close();
600
+ }
601
+ }
602
+ };
603
+ SandcastleExecutionSession = class {
604
+ sandboxHandle = null;
605
+ worktreeHandle = null;
606
+ async close() {
607
+ try {
608
+ await this.sandboxHandle?.close?.();
609
+ } finally {
610
+ this.sandboxHandle = null;
611
+ }
612
+ }
613
+ async execute(options) {
614
+ const {
615
+ stage,
616
+ iteration,
617
+ agent,
618
+ model,
619
+ prompt,
620
+ target,
621
+ repoRoot: repoRoot2,
622
+ branchName,
623
+ worktreePath,
624
+ baseRef,
625
+ sandbox,
626
+ autoApprove = false,
627
+ timeoutMs,
628
+ artifacts = [],
629
+ logger
630
+ } = options;
631
+ const stageLabel = iteration !== void 0 ? `${stage}:${iteration}` : stage;
632
+ logger.step(
633
+ "sandcastle",
634
+ `[${stageLabel}] running agent "${agent}" with model "${model}"`
635
+ );
636
+ try {
637
+ const env = {};
638
+ if (autoApprove) {
639
+ env.OPENCODE_AUTO_APPROVE = "true";
640
+ }
641
+ const serenaConfig = buildSerenaOpenCodeConfig(stage, options.serena);
642
+ if (serenaConfig) {
643
+ env.OPENCODE_CONFIG_CONTENT = JSON.stringify(serenaConfig);
644
+ }
645
+ const logPath = `${repoRoot2}/.pourkit/logs/${sanitizeBranch(branchName)}-${Date.now()}.log`;
646
+ await ensureSandboxImageBuilt(repoRoot2, { force: sandbox.forceRebuild });
647
+ try {
648
+ savePromptToFile(repoRoot2, stage, iteration, prompt);
649
+ } catch {
650
+ }
651
+ const agentProvider = opencode(model, { env, agent });
652
+ const sandboxOptions = buildSandboxOptions(repoRoot2, sandbox);
653
+ const sandboxProvider = resolveSandboxProvider(sandbox.provider, docker);
654
+ const activeSandbox = await this.getOrCreateSandbox({
655
+ repoRoot: repoRoot2,
656
+ branchName,
657
+ baseBranch: baseRef ?? target.baseBranch,
658
+ worktreePath,
659
+ sandboxProvider: sandboxProvider(sandboxOptions),
660
+ setupCommands: target.setupCommands ?? [],
661
+ copyToWorktree: sandbox.copyToWorktree ?? []
662
+ });
663
+ await writeExecutionArtifacts(activeSandbox.worktreePath, artifacts);
664
+ const result = await activeSandbox.run({
665
+ agent: agentProvider,
666
+ prompt,
667
+ maxIterations: 1,
668
+ name: stageLabel,
669
+ logging: {
670
+ type: "file",
671
+ path: logPath,
672
+ onAgentStreamEvent: (event) => {
673
+ if (event.type === "text") {
674
+ logger.raw(event.message);
675
+ } else if (event.type === "toolCall") {
676
+ logger.raw(`${event.name}(${event.formattedArgs})`);
677
+ }
678
+ }
679
+ },
680
+ completionSignal: "<promise>COMPLETE</promise>",
681
+ ...timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {},
682
+ ...sandboxOptions.idleTimeoutSeconds ? { idleTimeoutSeconds: sandboxOptions.idleTimeoutSeconds } : {}
683
+ });
684
+ const commits = result.commits.map((c) => c.sha);
685
+ const resultBranch = result.branch ?? activeSandbox.branch ?? branchName;
686
+ logger.kv("SANDBOX_SUCCESS", "true");
687
+ logger.kv("COMMITS_CREATED", String(commits.length));
688
+ logger.kv("WORKTREE_BRANCH", resultBranch);
689
+ if (result.logFilePath) {
690
+ logger.kv("LOG_FILE", result.logFilePath);
691
+ }
692
+ return {
693
+ success: true,
694
+ branch: resultBranch,
695
+ worktreePath: activeSandbox.worktreePath,
696
+ commits,
697
+ logPath
698
+ };
699
+ } catch (error) {
700
+ logger.step(
701
+ "error",
702
+ `Sandcastle failed: ${error instanceof Error ? error.message : String(error)}`
703
+ );
704
+ return {
705
+ success: false,
706
+ branch: "",
707
+ worktreePath: "",
708
+ commits: [],
709
+ logPath: null,
710
+ error: error instanceof Error ? error.message : String(error)
711
+ };
712
+ }
713
+ }
714
+ async getOrCreateSandbox(options) {
715
+ if (this.sandboxHandle) {
716
+ return this.sandboxHandle;
717
+ }
718
+ const hooks = {
719
+ sandbox: {
720
+ onSandboxReady: options.setupCommands.map((command) => ({
721
+ command: command.command
722
+ }))
723
+ }
724
+ };
725
+ if (options.worktreePath) {
726
+ this.sandboxHandle = await createSandboxFromExistingWorktree({
727
+ branch: options.branchName,
728
+ hostRepoDir: options.repoRoot,
729
+ worktreePath: options.worktreePath,
730
+ sandbox: options.sandboxProvider,
731
+ ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {},
732
+ hooks
733
+ });
734
+ return this.sandboxHandle;
735
+ }
736
+ this.worktreeHandle = await createWorktree({
737
+ cwd: options.repoRoot,
738
+ branchStrategy: {
739
+ type: "branch",
740
+ branch: options.branchName,
741
+ baseBranch: options.baseBranch
742
+ },
743
+ ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {}
744
+ });
745
+ this.sandboxHandle = await this.worktreeHandle.createSandbox({
746
+ sandbox: options.sandboxProvider,
747
+ ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {},
748
+ hooks
749
+ });
750
+ return this.sandboxHandle;
751
+ }
752
+ };
753
+ }
754
+ });
755
+
336
756
  // cli.ts
337
757
  import path8 from "path";
338
758
  import { realpathSync } from "fs";
@@ -997,8 +1417,8 @@ async function cleanupRepository(options) {
997
1417
  }
998
1418
 
999
1419
  // commands/issue-run.ts
1000
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1001
- import { join as join10 } from "path";
1420
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
1421
+ import { join as join12 } from "path";
1002
1422
 
1003
1423
  // pr/templates.ts
1004
1424
  init_common();
@@ -1265,19 +1685,39 @@ function invalidateAfterBaseRefresh(state) {
1265
1685
  import { Effect as Effect2 } from "effect";
1266
1686
 
1267
1687
  // shared/effect-runtime.ts
1268
- import { Effect } from "effect";
1688
+ import { Effect, Exit } from "effect";
1689
+ import { UnknownException } from "effect/Cause";
1269
1690
  var initialized = false;
1270
1691
  function initializeEffectRuntime() {
1271
1692
  if (initialized) return;
1272
1693
  initialized = true;
1273
1694
  }
1274
1695
  function runEffect(program) {
1696
+ ensureEffectRuntime();
1697
+ return Effect.runPromiseExit(program);
1698
+ }
1699
+ function ensureEffectRuntime() {
1275
1700
  if (!initialized) {
1276
- throw new Error(
1277
- "Effect runtime not initialized. Call initializeEffectRuntime() at CLI startup."
1278
- );
1701
+ initializeEffectRuntime();
1279
1702
  }
1280
- return Effect.runPromiseExit(program);
1703
+ }
1704
+ function unwrapError(error) {
1705
+ if (error instanceof UnknownException && error.error !== void 0) {
1706
+ return error.error;
1707
+ }
1708
+ return error;
1709
+ }
1710
+ function runEffectAndMapExit(program) {
1711
+ return runEffect(program).then((exit) => {
1712
+ if (Exit.isSuccess(exit)) return exit.value;
1713
+ const cause = exit.cause;
1714
+ if (cause._tag === "Fail") {
1715
+ const error = unwrapError(cause.error);
1716
+ throw error instanceof Error ? error : new Error(String(error));
1717
+ }
1718
+ const defect = cause._tag === "Die" ? cause.defect : cause._tag === "Interrupt" ? "interrupted" : `unhandled cause: ${cause._tag}`;
1719
+ throw defect instanceof Error ? defect : new Error(`Unexpected defect: ${String(defect)}`);
1720
+ });
1281
1721
  }
1282
1722
 
1283
1723
  // failure-resolution/types.ts
@@ -1336,7 +1776,25 @@ var SafetyFailure = class extends Error {
1336
1776
  this.message = args.message;
1337
1777
  }
1338
1778
  };
1339
- var SUPPORTED_DECISIONS = /* @__PURE__ */ new Set([
1779
+ var ReviewerFailure = class extends Error {
1780
+ _tag = "ReviewerFailure";
1781
+ message;
1782
+ constructor(args) {
1783
+ super(args.message);
1784
+ this.name = "ReviewerFailure";
1785
+ this.message = args.message;
1786
+ }
1787
+ };
1788
+ var FinalizerFailure = class extends Error {
1789
+ _tag = "FinalizerFailure";
1790
+ message;
1791
+ constructor(args) {
1792
+ super(args.message);
1793
+ this.name = "FinalizerFailure";
1794
+ this.message = args.message;
1795
+ }
1796
+ };
1797
+ var SUPPORTED_DECISIONS = /* @__PURE__ */ new Set([
1340
1798
  "RETRY_STAGE",
1341
1799
  "HANDOFF_TO_HUMAN",
1342
1800
  "FAIL_RUN"
@@ -2140,8 +2598,7 @@ async function writeRecoveryAttempt(worktreePath, outcome, fingerprint, summary,
2140
2598
  }
2141
2599
 
2142
2600
  // commands/pr-description-agent.ts
2143
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "fs";
2144
- import { dirname as dirname3, join as join8 } from "path";
2601
+ import { dirname as dirname4, join as join10 } from "path";
2145
2602
 
2146
2603
  // pr/pr-description.ts
2147
2604
  var CONVENTIONAL_TITLE_PATTERN = /^(feat|fix|perf|refactor|docs|test|chore|ci|build)(\([^)]+\))?!?:\s+\S/;
@@ -2229,68 +2686,363 @@ function inferConventionalType(commitSummaries) {
2229
2686
 
2230
2687
  // pr/pr-description-context.ts
2231
2688
  init_common();
2232
- import { join as join7 } from "path";
2689
+ import { join as join9 } from "path";
2233
2690
  import { readFile } from "fs/promises";
2234
- async function collectFinalizerContext(options) {
2235
- const { targetBase, branchName, worktreePath, reviewArtifactPath, logger } = options;
2236
- const commits = await collectCommitRange(
2237
- targetBase,
2238
- branchName,
2239
- worktreePath,
2240
- logger
2241
- );
2242
- const reviewArtifact = reviewArtifactPath ? await readReviewArtifact(reviewArtifactPath) : "(no review artifact provided)";
2243
- return {
2244
- commits,
2245
- reviewArtifact,
2246
- targetBase,
2247
- branchName
2248
- };
2249
- }
2250
- async function collectCommitRange(targetBase, branchName, worktreePath, logger) {
2251
- const result = await execCapture(
2252
- "git",
2253
- [
2254
- "log",
2255
- `${remoteTargetBase(targetBase)}..${branchName}`,
2256
- "--oneline",
2257
- "--no-decorate"
2258
- ],
2259
- { cwd: worktreePath, logger, label: "git log" }
2260
- );
2261
- const commits = result.stdout.trim();
2262
- if (!commits) {
2263
- logger.step(
2264
- "warn",
2265
- `No commits found between ${targetBase} and ${branchName}, proceeding with empty commit range`
2266
- );
2691
+ import { Effect as Effect4 } from "effect";
2692
+
2693
+ // shared/effect-services.ts
2694
+ import { Context, Effect as Effect3, Layer } from "effect";
2695
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3, rmSync } from "fs";
2696
+ var GitExecutionError = class extends Error {
2697
+ _tag = "GitExecutionError";
2698
+ message;
2699
+ constructor(args) {
2700
+ super(args.message);
2701
+ this.name = "GitExecutionError";
2702
+ this.message = args.message;
2267
2703
  }
2268
- return commits;
2704
+ };
2705
+ var FileSystem = class extends Context.Tag("FileSystem")() {
2706
+ };
2707
+ var FileSystemDefault = Layer.succeed(
2708
+ FileSystem,
2709
+ FileSystem.of({
2710
+ readFile: (path9) => Effect3.try({
2711
+ try: () => readFileSync6(path9, "utf-8"),
2712
+ catch: (error) => new Error(
2713
+ `Failed to read file ${path9}: ${error instanceof Error ? error.message : String(error)}`
2714
+ )
2715
+ }),
2716
+ writeFile: (path9, content) => Effect3.try({
2717
+ try: () => writeFileSync3(path9, content, "utf-8"),
2718
+ catch: (error) => new Error(
2719
+ `Failed to write file ${path9}: ${error instanceof Error ? error.message : String(error)}`
2720
+ )
2721
+ }),
2722
+ exists: (path9) => Effect3.try({
2723
+ try: () => existsSync6(path9),
2724
+ catch: (error) => new Error(
2725
+ `Failed to check existence of ${path9}: ${error instanceof Error ? error.message : String(error)}`
2726
+ )
2727
+ }),
2728
+ mkdir: (path9) => Effect3.try({
2729
+ try: () => mkdirSync5(path9, { recursive: true }),
2730
+ catch: (error) => new Error(
2731
+ `Failed to create directory ${path9}: ${error instanceof Error ? error.message : String(error)}`
2732
+ )
2733
+ }),
2734
+ remove: (path9) => Effect3.try({
2735
+ try: () => rmSync(path9, { recursive: true, force: true }),
2736
+ catch: (error) => new Error(
2737
+ `Failed to remove ${path9}: ${error instanceof Error ? error.message : String(error)}`
2738
+ )
2739
+ })
2740
+ })
2741
+ );
2742
+ var FileSystemTest = Layer.succeed(
2743
+ FileSystem,
2744
+ FileSystem.of({
2745
+ readFile: (path9) => Effect3.succeed(`test content for ${path9}`),
2746
+ writeFile: (_path, _content) => Effect3.void,
2747
+ exists: (_path) => Effect3.succeed(true),
2748
+ mkdir: (_path) => Effect3.void,
2749
+ remove: (_path) => Effect3.void
2750
+ })
2751
+ );
2752
+ var GitOperations = class extends Context.Tag("GitOperations")() {
2753
+ };
2754
+ var GitOperationsDefault = Layer.succeed(
2755
+ GitOperations,
2756
+ GitOperations.of({
2757
+ exec: (args, options) => Effect3.tryPromise({
2758
+ try: async () => {
2759
+ const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
2760
+ const result = await execCapture2("git", args, {
2761
+ cwd: options?.cwd
2762
+ });
2763
+ return { stdout: result.stdout, stderr: result.stderr };
2764
+ },
2765
+ catch: (error) => new GitExecutionError({
2766
+ message: error instanceof Error ? error.message : `git exec failed: ${String(error)}`
2767
+ })
2768
+ }),
2769
+ revParse: (ref, cwd) => Effect3.tryPromise({
2770
+ try: async () => {
2771
+ const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
2772
+ const result = await execCapture2("git", ["rev-parse", ref], {
2773
+ cwd
2774
+ });
2775
+ return result.stdout.trim();
2776
+ },
2777
+ catch: (error) => new GitExecutionError({
2778
+ message: error instanceof Error ? error.message : `git rev-parse failed: ${String(error)}`
2779
+ })
2780
+ }),
2781
+ fetch: (remote, branch, cwd) => Effect3.tryPromise({
2782
+ try: async () => {
2783
+ const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
2784
+ await execCapture2("git", ["fetch", remote, branch], { cwd });
2785
+ },
2786
+ catch: (error) => new GitExecutionError({
2787
+ message: error instanceof Error ? error.message : `git fetch failed: ${String(error)}`
2788
+ })
2789
+ }),
2790
+ mergeBaseIsAncestor: (baseRef, cwd) => Effect3.tryPromise({
2791
+ try: async () => {
2792
+ const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
2793
+ await execCapture2(
2794
+ "git",
2795
+ ["merge-base", "--is-ancestor", baseRef, "HEAD"],
2796
+ { cwd }
2797
+ );
2798
+ return true;
2799
+ },
2800
+ catch: (error) => new GitExecutionError({
2801
+ message: error instanceof Error ? error.message : `git merge-base failed: ${String(error)}`
2802
+ })
2803
+ }).pipe(
2804
+ Effect3.catchIf(
2805
+ (error) => error instanceof GitExecutionError && error.message.includes("merge-base --is-ancestor") && error.message.includes("exit code: 1"),
2806
+ () => Effect3.succeed(false)
2807
+ )
2808
+ )
2809
+ })
2810
+ );
2811
+ var GitOperationsTest = Layer.succeed(
2812
+ GitOperations,
2813
+ GitOperations.of({
2814
+ exec: (_args, _options) => Effect3.succeed({ stdout: "", stderr: "" }),
2815
+ revParse: (_ref, _cwd) => Effect3.succeed("abc123def456"),
2816
+ fetch: (_remote, _branch, _cwd) => Effect3.void,
2817
+ mergeBaseIsAncestor: (_baseRef, _cwd) => Effect3.succeed(true)
2818
+ })
2819
+ );
2820
+ var GitHubApiClient = class extends Context.Tag("GitHubApiClient")() {
2821
+ };
2822
+ var GitHubApiClientDefault = Layer.succeed(
2823
+ GitHubApiClient,
2824
+ GitHubApiClient.of({
2825
+ getIssue: (owner, repo, issueNumber) => Effect3.tryPromise({
2826
+ try: async () => {
2827
+ const { requireGitHubClient: requireGitHubClient2 } = await Promise.resolve().then(() => (init_github_client(), github_client_exports));
2828
+ const client = await requireGitHubClient2();
2829
+ return await client.octokit.rest.issues.get({
2830
+ owner,
2831
+ repo,
2832
+ issue_number: issueNumber
2833
+ });
2834
+ },
2835
+ catch: (error) => new Error(
2836
+ error instanceof Error ? error.message : `GitHub API getIssue failed: ${String(error)}`
2837
+ )
2838
+ }),
2839
+ createIssueComment: (owner, repo, issueNumber, body) => Effect3.tryPromise({
2840
+ try: async () => {
2841
+ const { requireGitHubClient: requireGitHubClient2 } = await Promise.resolve().then(() => (init_github_client(), github_client_exports));
2842
+ const client = await requireGitHubClient2();
2843
+ return await client.octokit.rest.issues.createComment({
2844
+ owner,
2845
+ repo,
2846
+ issue_number: issueNumber,
2847
+ body
2848
+ });
2849
+ },
2850
+ catch: (error) => new Error(
2851
+ error instanceof Error ? error.message : `GitHub API createIssueComment failed: ${String(error)}`
2852
+ )
2853
+ }),
2854
+ updateIssue: (owner, repo, issueNumber, update) => Effect3.tryPromise({
2855
+ try: async () => {
2856
+ const { requireGitHubClient: requireGitHubClient2 } = await Promise.resolve().then(() => (init_github_client(), github_client_exports));
2857
+ const client = await requireGitHubClient2();
2858
+ return await client.octokit.rest.issues.update({
2859
+ owner,
2860
+ repo,
2861
+ issue_number: issueNumber,
2862
+ ...update
2863
+ });
2864
+ },
2865
+ catch: (error) => new Error(
2866
+ error instanceof Error ? error.message : `GitHub API updateIssue failed: ${String(error)}`
2867
+ )
2868
+ })
2869
+ })
2870
+ );
2871
+ var GitHubApiClientTest = Layer.succeed(
2872
+ GitHubApiClient,
2873
+ GitHubApiClient.of({
2874
+ getIssue: (_owner, _repo, _issueNumber) => Effect3.succeed({ data: { title: "Test Issue", state: "open" } }),
2875
+ createIssueComment: (_owner, _repo, _issueNumber, _body) => Effect3.succeed({ data: { id: 1 } }),
2876
+ updateIssue: (_owner, _repo, _issueNumber, _update) => Effect3.succeed({ data: { number: _issueNumber } })
2877
+ })
2878
+ );
2879
+ var ExecutionProvider = class extends Context.Tag("ExecutionProvider")() {
2880
+ };
2881
+ var ExecutionProviderDefault = Layer.succeed(
2882
+ ExecutionProvider,
2883
+ ExecutionProvider.of({
2884
+ execute: (options) => Effect3.tryPromise({
2885
+ try: async () => {
2886
+ const { SandcastleExecutionProvider: SandcastleExecutionProvider2 } = await Promise.resolve().then(() => (init_sandcastle_execution(), sandcastle_execution_exports));
2887
+ const provider = new SandcastleExecutionProvider2();
2888
+ return await provider.execute(options);
2889
+ },
2890
+ catch: (error) => new Error(
2891
+ error instanceof Error ? error.message : `Execution failed: ${String(error)}`
2892
+ )
2893
+ })
2894
+ })
2895
+ );
2896
+ var ExecutionProviderTest = Layer.succeed(
2897
+ ExecutionProvider,
2898
+ ExecutionProvider.of({
2899
+ execute: (_options) => Effect3.succeed({
2900
+ success: true,
2901
+ branch: "test-branch",
2902
+ worktreePath: _options.worktreePath ?? "/tmp/worktree",
2903
+ commits: ["abc123"],
2904
+ logPath: "/tmp/test-log"
2905
+ })
2906
+ })
2907
+ );
2908
+ var AttemptLogService = class extends Context.Tag("AttemptLogService")() {
2909
+ };
2910
+ var AttemptLogServiceDefault = Layer.succeed(
2911
+ AttemptLogService,
2912
+ AttemptLogService.of({
2913
+ writeEntry: (worktreePath, entry) => Effect3.try({
2914
+ try: () => writeAttemptLog(worktreePath, entry),
2915
+ catch: (error) => new Error(
2916
+ `Failed to write attempt log: ${error instanceof Error ? error.message : String(error)}`
2917
+ )
2918
+ }),
2919
+ readEntries: (worktreePath) => Effect3.try({
2920
+ try: () => readAttemptLog(worktreePath),
2921
+ catch: (error) => new Error(
2922
+ `Failed to read attempt log: ${error instanceof Error ? error.message : String(error)}`
2923
+ )
2924
+ })
2925
+ })
2926
+ );
2927
+ var AttemptLogServiceTest = Layer.succeed(
2928
+ AttemptLogService,
2929
+ AttemptLogService.of({
2930
+ writeEntry: (_worktreePath, _entry) => Effect3.void,
2931
+ readEntries: (_worktreePath) => Effect3.succeed([])
2932
+ })
2933
+ );
2934
+ var WorktreeRunStateService = class extends Context.Tag(
2935
+ "WorktreeRunStateService"
2936
+ )() {
2937
+ };
2938
+ var WorktreeRunStateServiceDefault = Layer.succeed(
2939
+ WorktreeRunStateService,
2940
+ WorktreeRunStateService.of({
2941
+ read: (worktreePath) => Effect3.try({
2942
+ try: () => readWorktreeRunState(worktreePath),
2943
+ catch: (error) => new Error(
2944
+ `Failed to read worktree run state: ${error instanceof Error ? error.message : String(error)}`
2945
+ )
2946
+ }),
2947
+ write: (worktreePath, state) => Effect3.try({
2948
+ try: () => writeWorktreeRunState(worktreePath, state),
2949
+ catch: (error) => new Error(
2950
+ `Failed to write worktree run state: ${error instanceof Error ? error.message : String(error)}`
2951
+ )
2952
+ }),
2953
+ update: (worktreePath, update) => Effect3.try({
2954
+ try: () => updateWorktreeRunState(worktreePath, update),
2955
+ catch: (error) => new Error(
2956
+ `Failed to update worktree run state: ${error instanceof Error ? error.message : String(error)}`
2957
+ )
2958
+ })
2959
+ })
2960
+ );
2961
+ var WorktreeRunStateServiceTestTimestamp = "2026-01-01T00:00:00.000Z";
2962
+ var WorktreeRunStateServiceTest = Layer.succeed(
2963
+ WorktreeRunStateService,
2964
+ WorktreeRunStateService.of({
2965
+ read: (_worktreePath) => Effect3.succeed({
2966
+ issueNumber: 0,
2967
+ targetName: "test",
2968
+ branchName: "test-branch",
2969
+ baseBranch: "main",
2970
+ createdAt: WorktreeRunStateServiceTestTimestamp,
2971
+ updatedAt: WorktreeRunStateServiceTestTimestamp,
2972
+ completedStages: {},
2973
+ review: {
2974
+ lifetimeIterations: 0
2975
+ }
2976
+ }),
2977
+ write: (_worktreePath, _state) => Effect3.void,
2978
+ update: (_worktreePath, _update) => Effect3.void
2979
+ })
2980
+ );
2981
+
2982
+ // pr/pr-description-context.ts
2983
+ function collectFinalizerContextEffect(options) {
2984
+ return Effect4.gen(function* () {
2985
+ const git = yield* GitOperations;
2986
+ const fs = yield* FileSystem;
2987
+ const remoteBase = remoteTargetBase(options.targetBase);
2988
+ const result = yield* git.exec(
2989
+ [
2990
+ "log",
2991
+ `${remoteBase}..${options.branchName}`,
2992
+ "--oneline",
2993
+ "--no-decorate"
2994
+ ],
2995
+ { cwd: options.worktreePath }
2996
+ ).pipe(
2997
+ Effect4.catchAll(
2998
+ (error) => Effect4.fail(
2999
+ new Error(`Failed to collect commit range: ${error.message}`)
3000
+ )
3001
+ )
3002
+ );
3003
+ const commits = result.stdout.trim();
3004
+ if (!commits) {
3005
+ options.logger.step(
3006
+ "warn",
3007
+ `No commits found between ${options.targetBase} and ${options.branchName}, proceeding with empty commit range`
3008
+ );
3009
+ }
3010
+ let reviewArtifact;
3011
+ if (options.reviewArtifactPath) {
3012
+ const content = yield* fs.readFile(options.reviewArtifactPath).pipe(
3013
+ Effect4.catchIf(
3014
+ (error) => error.message.includes("ENOENT") || error.message.includes("no such file"),
3015
+ () => Effect4.fail(
3016
+ new Error(
3017
+ `Review artifact not found at ${options.reviewArtifactPath}. Ensure the review stage completed before running finalizer generation.`
3018
+ )
3019
+ )
3020
+ )
3021
+ );
3022
+ if (!content.trim()) {
3023
+ return yield* Effect4.fail(
3024
+ new Error(
3025
+ `Review artifact at ${options.reviewArtifactPath} is empty. Ensure the review stage produced output before running finalizer generation.`
3026
+ )
3027
+ );
3028
+ }
3029
+ reviewArtifact = content;
3030
+ } else {
3031
+ reviewArtifact = "(no review artifact provided)";
3032
+ }
3033
+ return {
3034
+ commits,
3035
+ reviewArtifact,
3036
+ targetBase: options.targetBase,
3037
+ branchName: options.branchName
3038
+ };
3039
+ });
2269
3040
  }
2270
3041
  function remoteTargetBase(targetBase) {
2271
3042
  return targetBase.includes("/") ? targetBase : `origin/${targetBase}`;
2272
3043
  }
2273
- async function readReviewArtifact(artifactPath) {
2274
- let content;
2275
- try {
2276
- content = await readFile(artifactPath, "utf-8");
2277
- } catch (error) {
2278
- if (error.code === "ENOENT") {
2279
- throw new Error(
2280
- `Review artifact not found at ${artifactPath}. Ensure the review stage completed before running finalizer generation.`
2281
- );
2282
- }
2283
- throw error;
2284
- }
2285
- if (!content.trim()) {
2286
- throw new Error(
2287
- `Review artifact at ${artifactPath} is empty. Ensure the review stage produced output before running finalizer generation.`
2288
- );
2289
- }
2290
- return content;
2291
- }
2292
3044
  function buildFinalizerPrompt(context, promptTemplate) {
2293
- const artifactPathInWorktree = join7(
3045
+ const artifactPathInWorktree = join9(
2294
3046
  ".pourkit",
2295
3047
  ".tmp",
2296
3048
  "finalizer",
@@ -2342,133 +3094,182 @@ Write your finalizer output to: ${artifactPathInWorktree}`;
2342
3094
  }
2343
3095
 
2344
3096
  // commands/pr-description-agent.ts
2345
- async function runFinalizerAgent(options) {
2346
- const {
2347
- executionProvider,
2348
- config,
2349
- target,
2350
- issue,
2351
- builderBranch,
2352
- worktreePath,
2353
- reviewArtifactPath,
2354
- repoRoot: repoRoot2,
2355
- logger
2356
- } = options;
2357
- const strategy = target.strategy;
2358
- const finalizer = strategy.finalize.prDescriptionAgent;
2359
- const context = await collectFinalizerContext({
2360
- targetBase: target.baseBranch,
2361
- branchName: builderBranch,
2362
- worktreePath,
2363
- reviewArtifactPath,
2364
- logger
2365
- });
2366
- const resolvedPrompt = loadFinalizerPrompt(
2367
- repoRoot2,
2368
- finalizer.promptTemplate
3097
+ import { Effect as Effect5, Layer as Layer2 } from "effect";
3098
+ function bridgeExecutionProvider(ep) {
3099
+ return Layer2.succeed(
3100
+ ExecutionProvider,
3101
+ ExecutionProvider.of({
3102
+ execute: (opts) => Effect5.tryPromise({
3103
+ try: () => ep.execute(opts),
3104
+ catch: (e) => new Error(
3105
+ e instanceof Error ? e.message : `Execution failed: ${String(e)}`
3106
+ )
3107
+ })
3108
+ })
2369
3109
  );
2370
- const prompt = buildFinalizerPrompt(context, resolvedPrompt);
2371
- const artifactPathInWorktree = join8(
3110
+ }
3111
+ function runFinalizerAgent(options) {
3112
+ const { executionProvider } = options;
3113
+ const artifactPathInWorktree = join10(
2372
3114
  ".pourkit",
2373
3115
  ".tmp",
2374
3116
  "finalizer",
2375
3117
  "agent-output.md"
2376
3118
  );
2377
- const artifactPath = join8(worktreePath, artifactPathInWorktree);
2378
- prepareArtifactPath(artifactPath);
2379
- let output = "";
2380
- let parsed;
2381
- let lastValidationError;
2382
- for (let attempt = 1; attempt <= strategy.finalize.maxAttempts; attempt++) {
2383
- logger.step(
2384
- "info",
2385
- `Running finalizer agent (${attempt}/${strategy.finalize.maxAttempts})`
3119
+ const artifactPath = join10(options.worktreePath, artifactPathInWorktree);
3120
+ const program = Effect5.gen(function* () {
3121
+ const exec = yield* ExecutionProvider;
3122
+ const fs = yield* FileSystem;
3123
+ const context = yield* collectFinalizerContextEffect({
3124
+ targetBase: options.target.baseBranch,
3125
+ branchName: options.builderBranch,
3126
+ worktreePath: options.worktreePath,
3127
+ reviewArtifactPath: options.reviewArtifactPath,
3128
+ logger: options.logger
3129
+ }).pipe(
3130
+ Effect5.catchAll(
3131
+ (error) => Effect5.fail(
3132
+ new FinalizerFailure({
3133
+ message: `Failed to collect finalizer context: ${error.message}`
3134
+ })
3135
+ )
3136
+ )
2386
3137
  );
2387
- const executionResult = await executionProvider.execute({
2388
- stage: "finalizer",
2389
- agent: finalizer.agent,
2390
- model: finalizer.model,
2391
- prompt,
2392
- target,
2393
- repoRoot: repoRoot2,
2394
- branchName: builderBranch,
2395
- sandbox: config.sandbox,
2396
- autoApprove: true,
2397
- artifactPath: artifactPathInWorktree,
2398
- worktreePath,
2399
- artifacts: [
2400
- buildRunContextArtifact({
2401
- issue,
2402
- target,
2403
- branchName: builderBranch,
2404
- reviewerCriteria: strategy.review.reviewer.criteria,
2405
- sections: STAGE_SECTIONS.finalizer
2406
- })
2407
- ],
2408
- logger
2409
- });
2410
- if (!executionResult.success) {
2411
- throw new Error(
2412
- `Finalizer agent execution failed: ${executionResult.error}`
3138
+ const strategy = options.target.strategy;
3139
+ const finalizer = strategy.finalize.prDescriptionAgent;
3140
+ const resolvedPrompt = yield* loadFinalizerPromptEffect(
3141
+ options.repoRoot,
3142
+ finalizer.promptTemplate,
3143
+ fs
3144
+ );
3145
+ const prompt = buildFinalizerPrompt(context, resolvedPrompt);
3146
+ yield* prepareArtifactPathEffect(artifactPath, fs);
3147
+ let output = "";
3148
+ let parsed;
3149
+ let lastValidationError;
3150
+ for (let attempt = 1; attempt <= strategy.finalize.maxAttempts; attempt++) {
3151
+ options.logger.step(
3152
+ "info",
3153
+ `Running finalizer agent (${attempt}/${strategy.finalize.maxAttempts})`
2413
3154
  );
2414
- }
2415
- try {
2416
- output = readAgentOutput(artifactPath);
2417
- parsed = parsePrDescription(output);
2418
- lastValidationError = void 0;
2419
- break;
2420
- } catch (error) {
2421
- lastValidationError = error instanceof PrDescriptionProtocolError ? new Error(`Finalizer protocol error: ${error.message}`) : error instanceof Error ? error : new Error(String(error));
2422
- if (attempt === strategy.finalize.maxAttempts) {
2423
- break;
3155
+ const executionResult = yield* exec.execute({
3156
+ stage: "finalizer",
3157
+ agent: finalizer.agent,
3158
+ model: finalizer.model,
3159
+ prompt,
3160
+ target: options.target,
3161
+ repoRoot: options.repoRoot,
3162
+ branchName: options.builderBranch,
3163
+ sandbox: options.config.sandbox,
3164
+ autoApprove: true,
3165
+ artifactPath: artifactPathInWorktree,
3166
+ worktreePath: options.worktreePath,
3167
+ artifacts: [
3168
+ buildRunContextArtifact({
3169
+ issue: options.issue,
3170
+ target: options.target,
3171
+ branchName: options.builderBranch,
3172
+ reviewerCriteria: strategy.review.reviewer.criteria,
3173
+ sections: STAGE_SECTIONS.finalizer
3174
+ })
3175
+ ],
3176
+ logger: options.logger
3177
+ });
3178
+ if (!executionResult.success) {
3179
+ return yield* Effect5.fail(
3180
+ new FinalizerFailure({
3181
+ message: `Finalizer agent execution failed: ${executionResult.error}`
3182
+ })
3183
+ );
3184
+ }
3185
+ const readResult = yield* readAgentOutputEffect(artifactPath, fs);
3186
+ if (readResult._tag === "content") {
3187
+ output = readResult.value;
3188
+ try {
3189
+ parsed = parsePrDescription(output);
3190
+ lastValidationError = void 0;
3191
+ break;
3192
+ } catch (error) {
3193
+ lastValidationError = error instanceof PrDescriptionProtocolError ? new Error(`Finalizer protocol error: ${error.message}`) : error instanceof Error ? error : new Error(String(error));
3194
+ if (attempt === strategy.finalize.maxAttempts) {
3195
+ break;
3196
+ }
3197
+ yield* prepareArtifactPathEffect(artifactPath, fs);
3198
+ }
3199
+ } else if (readResult._tag === "empty") {
3200
+ lastValidationError = new Error(
3201
+ `Finalizer agent produced empty output at ${artifactPath}`
3202
+ );
3203
+ if (attempt === strategy.finalize.maxAttempts) {
3204
+ break;
3205
+ }
3206
+ yield* prepareArtifactPathEffect(artifactPath, fs);
3207
+ } else {
3208
+ lastValidationError = new Error(
3209
+ `Finalizer agent did not produce output at ${artifactPath}`
3210
+ );
3211
+ if (attempt === strategy.finalize.maxAttempts) {
3212
+ break;
3213
+ }
3214
+ yield* prepareArtifactPathEffect(artifactPath, fs);
2424
3215
  }
2425
- prepareArtifactPath(artifactPath);
2426
3216
  }
2427
- }
2428
- if (!parsed) {
2429
- throw lastValidationError ?? new Error("Finalizer validation failed");
2430
- }
2431
- await persistGeneratedArtifact(worktreePath, output);
2432
- const title = ensureConventionalPrTitle(parsed.title, context.commits);
2433
- logger.step("info", "Finalizer output generated successfully");
2434
- return {
2435
- title,
2436
- body: parsed.body,
2437
- artifactPath
2438
- };
3217
+ if (!parsed) {
3218
+ return yield* Effect5.fail(
3219
+ new FinalizerFailure({
3220
+ message: lastValidationError?.message ?? "Finalizer validation failed"
3221
+ })
3222
+ );
3223
+ }
3224
+ yield* persistGeneratedArtifactEffect(options.worktreePath, output, fs);
3225
+ options.logger.step("info", "Finalizer output generated successfully");
3226
+ return {
3227
+ title: ensureConventionalPrTitle(parsed.title, context.commits),
3228
+ body: parsed.body,
3229
+ artifactPath
3230
+ };
3231
+ });
3232
+ const executionLayer = bridgeExecutionProvider(executionProvider);
3233
+ return program.pipe(
3234
+ Effect5.provide(
3235
+ Layer2.mergeAll(executionLayer, FileSystemDefault, GitOperationsDefault)
3236
+ )
3237
+ );
2439
3238
  }
2440
- function loadFinalizerPrompt(repoRoot2, promptTemplate) {
2441
- const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
2442
- if (existsSync5(promptPath)) {
2443
- return readFileSync5(promptPath, "utf-8");
2444
- }
2445
- return promptTemplate;
3239
+ function loadFinalizerPromptEffect(repoRoot2, promptTemplate, fs) {
3240
+ return Effect5.gen(function* () {
3241
+ const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
3242
+ const exists = yield* fs.exists(promptPath).pipe(Effect5.orDie);
3243
+ if (exists) {
3244
+ return yield* fs.readFile(promptPath).pipe(Effect5.orDie);
3245
+ }
3246
+ return promptTemplate;
3247
+ });
2446
3248
  }
2447
- function prepareArtifactPath(artifactPath) {
2448
- mkdirSync4(dirname3(artifactPath), { recursive: true });
2449
- if (existsSync5(artifactPath)) {
2450
- rmSync(artifactPath);
2451
- }
3249
+ function prepareArtifactPathEffect(artifactPath, fs) {
3250
+ return Effect5.gen(function* () {
3251
+ yield* fs.mkdir(dirname4(artifactPath)).pipe(Effect5.orDie);
3252
+ const exists = yield* fs.exists(artifactPath).pipe(Effect5.orDie);
3253
+ if (exists) {
3254
+ yield* fs.remove(artifactPath).pipe(Effect5.orDie);
3255
+ }
3256
+ });
2452
3257
  }
2453
- function readAgentOutput(artifactPath) {
2454
- if (!existsSync5(artifactPath)) {
2455
- throw new Error(
2456
- `Finalizer agent did not produce output at ${artifactPath}`
2457
- );
2458
- }
2459
- const output = readFileSync5(artifactPath, "utf-8");
2460
- if (!output.trim()) {
2461
- throw new Error(`Finalizer agent produced empty output at ${artifactPath}`);
2462
- }
2463
- return output;
3258
+ function readAgentOutputEffect(artifactPath, fs) {
3259
+ return Effect5.gen(function* () {
3260
+ const exists = yield* fs.exists(artifactPath).pipe(Effect5.orDie);
3261
+ if (!exists) return { _tag: "missing" };
3262
+ const output = yield* fs.readFile(artifactPath).pipe(Effect5.catchAll(() => Effect5.succeed("")));
3263
+ if (!output.trim()) return { _tag: "empty" };
3264
+ return { _tag: "content", value: output };
3265
+ });
2464
3266
  }
2465
- async function persistGeneratedArtifact(worktreePath, output) {
2466
- try {
2467
- const dir = join8(worktreePath, ".pourkit", ".tmp", "finalizer");
2468
- mkdirSync4(dir, { recursive: true });
2469
- writeFileSync2(join8(dir, "generated.md"), output, "utf-8");
2470
- } catch {
2471
- }
3267
+ function persistGeneratedArtifactEffect(worktreePath, output, fs) {
3268
+ return Effect5.gen(function* () {
3269
+ const dir = join10(worktreePath, ".pourkit", ".tmp", "finalizer");
3270
+ yield* fs.mkdir(dir).pipe(Effect5.catchAll(() => Effect5.void));
3271
+ yield* fs.writeFile(join10(dir, "generated.md"), output).pipe(Effect5.catchAll(() => Effect5.void));
3272
+ });
2472
3273
  }
2473
3274
 
2474
3275
  // pr/pr-body.ts
@@ -2651,7 +3452,15 @@ function secondsRemaining(deadline, observedAt) {
2651
3452
  }
2652
3453
 
2653
3454
  // issues/merge-coordinator.ts
2654
- async function runMergeCoordinator(options) {
3455
+ import { Effect as Effect6, Either } from "effect";
3456
+ import { UnknownException as UnknownException2 } from "effect/Cause";
3457
+ function unwrapError2(error) {
3458
+ if (error instanceof UnknownException2 && error.error !== void 0) {
3459
+ return error.error;
3460
+ }
3461
+ return error;
3462
+ }
3463
+ function runMergeCoordinator(options) {
2655
3464
  const {
2656
3465
  prProvider,
2657
3466
  logger,
@@ -2662,56 +3471,70 @@ async function runMergeCoordinator(options) {
2662
3471
  } = options;
2663
3472
  const method = options.method ?? "squash";
2664
3473
  const waitForTargetGreen = options.waitForTargetGreen ?? true;
2665
- try {
2666
- await prProvider.waitForPrChecks(prNumber, checkWaitOptions);
2667
- } catch (error) {
2668
- return {
2669
- stage: "merge",
2670
- merged: false,
2671
- error: error instanceof Error ? error : new Error(String(error))
2672
- };
2673
- }
2674
- try {
2675
- await prProvider.mergePr(prNumber, {
2676
- method,
2677
- matchHeadCommit
2678
- });
2679
- } catch (error) {
2680
- return {
2681
- stage: "merge",
2682
- merged: false,
2683
- error: error instanceof Error ? error : new Error(String(error))
2684
- };
2685
- }
2686
- if (waitForTargetGreen) {
2687
- try {
2688
- await waitForBranchChecks(prProvider, logger, {
2689
- branchName: targetBranch,
2690
- checksFoundTimeoutMs: checkWaitOptions.checksFoundTimeoutMs,
2691
- checksCompletionTimeoutMs: checkWaitOptions.checksCompletionTimeoutMs,
2692
- pollIntervalMs: checkWaitOptions.pollIntervalMs
2693
- });
2694
- } catch (error) {
3474
+ return Effect6.gen(function* () {
3475
+ const checksEither = yield* Effect6.either(
3476
+ Effect6.tryPromise(
3477
+ () => prProvider.waitForPrChecks(prNumber, checkWaitOptions)
3478
+ )
3479
+ );
3480
+ if (Either.isLeft(checksEither)) {
3481
+ const rawError = checksEither.left;
3482
+ const error = unwrapError2(rawError);
2695
3483
  return {
2696
- stage: "target-green",
2697
- merged: true,
3484
+ stage: "merge",
3485
+ merged: false,
2698
3486
  error: error instanceof Error ? error : new Error(String(error))
2699
3487
  };
2700
3488
  }
2701
- }
2702
- return { stage: "completed", merged: true };
3489
+ const mergeEither = yield* Effect6.either(
3490
+ Effect6.tryPromise(
3491
+ () => prProvider.mergePr(prNumber, { method, matchHeadCommit })
3492
+ )
3493
+ );
3494
+ if (Either.isLeft(mergeEither)) {
3495
+ const rawError = mergeEither.left;
3496
+ const error = unwrapError2(rawError);
3497
+ return {
3498
+ stage: "merge",
3499
+ merged: false,
3500
+ error: error instanceof Error ? error : new Error(String(error))
3501
+ };
3502
+ }
3503
+ if (waitForTargetGreen) {
3504
+ const targetEither = yield* Effect6.either(
3505
+ Effect6.tryPromise(
3506
+ () => waitForBranchChecks(prProvider, logger, {
3507
+ branchName: targetBranch,
3508
+ checksFoundTimeoutMs: checkWaitOptions.checksFoundTimeoutMs,
3509
+ checksCompletionTimeoutMs: checkWaitOptions.checksCompletionTimeoutMs,
3510
+ pollIntervalMs: checkWaitOptions.pollIntervalMs
3511
+ })
3512
+ )
3513
+ );
3514
+ if (Either.isLeft(targetEither)) {
3515
+ const rawError = targetEither.left;
3516
+ const error = unwrapError2(rawError);
3517
+ return {
3518
+ stage: "target-green",
3519
+ merged: true,
3520
+ error: error instanceof Error ? error : new Error(String(error))
3521
+ };
3522
+ }
3523
+ }
3524
+ return { stage: "completed", merged: true };
3525
+ });
2703
3526
  }
2704
3527
 
2705
3528
  // commands/review.ts
2706
3529
  import {
2707
- existsSync as existsSync6,
2708
- mkdirSync as mkdirSync5,
2709
- readFileSync as readFileSync6,
3530
+ existsSync as existsSync7,
3531
+ mkdirSync as mkdirSync6,
3532
+ readFileSync as readFileSync7,
2710
3533
  readdirSync,
2711
3534
  rmSync as rmSync2,
2712
- writeFileSync as writeFileSync3
3535
+ writeFileSync as writeFileSync4
2713
3536
  } from "fs";
2714
- import { dirname as dirname4, join as join9 } from "path";
3537
+ import { dirname as dirname5, join as join11 } from "path";
2715
3538
 
2716
3539
  // pr/review-verdict.ts
2717
3540
  var ReviewVerdictProtocolError = class extends Error {
@@ -2743,6 +3566,7 @@ function parseReviewVerdict(output) {
2743
3566
  }
2744
3567
 
2745
3568
  // commands/review.ts
3569
+ import { Effect as Effect7, Layer as Layer3 } from "effect";
2746
3570
  var ReviewArtifactValidationError = class extends Error {
2747
3571
  constructor(message) {
2748
3572
  super(message);
@@ -2793,12 +3617,12 @@ function extractLatestFindingIds(reviewOutput, iteration) {
2793
3617
  return ids;
2794
3618
  }
2795
3619
  function validateRefactorArtifact(artifactPath, findingIds) {
2796
- if (!existsSync6(artifactPath)) {
3620
+ if (!existsSync7(artifactPath)) {
2797
3621
  throw new RefactorArtifactValidationError(
2798
3622
  `Refactor artifact missing at ${artifactPath}`
2799
3623
  );
2800
3624
  }
2801
- const content = readFileSync6(artifactPath, "utf-8");
3625
+ const content = readFileSync7(artifactPath, "utf-8");
2802
3626
  if (!content.trim()) {
2803
3627
  throw new RefactorArtifactValidationError("Refactor artifact is empty");
2804
3628
  }
@@ -2897,7 +3721,7 @@ function validateReviewArtifact(output, verdict, iteration, priorRefactorArtifac
2897
3721
  );
2898
3722
  }
2899
3723
  const supersedesCell = cells[1];
2900
- if (supersedesCell !== "-" && supersedesCell !== "n/a" && !supersedesIdRegex.test(supersedesCell)) {
3724
+ if (supersedesCell !== "-" && !supersedesIdRegex.test(supersedesCell)) {
2901
3725
  throw new ReviewArtifactValidationError(
2902
3726
  `Supersedes must be a hyphen for new findings or a valid finding ID, got: ${supersedesCell}`
2903
3727
  );
@@ -2916,16 +3740,25 @@ function validateReviewArtifact(output, verdict, iteration, priorRefactorArtifac
2916
3740
  }
2917
3741
  }
2918
3742
  if (priorRefactorArtifactsProvided) {
2919
- if (!output.includes("## Prior Refactor Response Assessment")) {
3743
+ const verdictIndex = output.indexOf("<verdict>");
3744
+ const assessmentIndex = output.indexOf(
3745
+ "## Prior Refactor Response Assessment"
3746
+ );
3747
+ if (assessmentIndex === -1) {
2920
3748
  throw new ReviewArtifactValidationError(
2921
3749
  "Prior Refactor Artifacts were provided but the review is missing a ## Prior Refactor Response Assessment section"
2922
3750
  );
2923
3751
  }
3752
+ if (verdictIndex !== -1 && assessmentIndex > verdictIndex) {
3753
+ throw new ReviewArtifactValidationError(
3754
+ "## Prior Refactor Response Assessment must appear before <verdict>"
3755
+ );
3756
+ }
2924
3757
  }
2925
3758
  }
2926
- async function runReviewCommand(options) {
3759
+ function runReviewCommandEffect(options) {
2927
3760
  const {
2928
- executionProvider,
3761
+ executionProvider: _executionProvider,
2929
3762
  config,
2930
3763
  target,
2931
3764
  issue,
@@ -2941,87 +3774,145 @@ async function runReviewCommand(options) {
2941
3774
  } = options;
2942
3775
  const reviewer = target.strategy.review.reviewer;
2943
3776
  if (!reviewer) {
2944
- throw new Error("No reviewer config found");
3777
+ return Effect7.fail(
3778
+ new ReviewerFailure({ message: "No reviewer config found" })
3779
+ );
2945
3780
  }
2946
- const artifactPathInWorktree = join9(
3781
+ const artifactPathInWorktree = join11(
2947
3782
  ".pourkit",
2948
3783
  ".tmp",
2949
3784
  "reviewers",
2950
3785
  `iteration-${iteration ?? 1}.md`
2951
3786
  );
2952
- const artifactPath = join9(worktreePath, artifactPathInWorktree);
2953
- prepareReviewArtifactPath(artifactPath);
2954
- const prompt = buildReviewerPrompt(
2955
- repoRoot2,
2956
- reviewer.promptTemplate,
2957
- reviewer.criteria,
2958
- artifactPathInWorktree,
2959
- iteration ?? 1,
2960
- reviewHistory,
2961
- priorRefactorArtifacts,
2962
- humanHandoffResolved,
2963
- priorReviewerArtifacts
2964
- );
2965
- logger.step("info", "Running reviewer");
2966
- const executionResult = await executionProvider.execute({
2967
- stage: "reviewer",
2968
- iteration,
2969
- agent: reviewer.agent,
2970
- model: reviewer.model,
2971
- prompt,
2972
- target,
2973
- repoRoot: repoRoot2,
2974
- branchName: builderBranch,
2975
- sandbox: config.sandbox,
2976
- autoApprove: true,
2977
- artifactPath: artifactPathInWorktree,
2978
- worktreePath,
2979
- artifacts: [
2980
- buildRunContextArtifact({
2981
- issue,
2982
- target,
2983
- branchName: builderBranch,
2984
- reviewerCriteria: reviewer.criteria,
2985
- sections: STAGE_SECTIONS.reviewer
2986
- })
2987
- ],
2988
- logger
2989
- });
2990
- if (!executionResult.success) {
2991
- throw new Error(`Reviewer execution failed: ${executionResult.error}`);
2992
- }
2993
- const output = readReviewArtifact2(artifactPath, executionResult.logPath);
2994
- let verdict;
2995
- try {
2996
- verdict = parseReviewVerdict(output);
2997
- } catch (error) {
2998
- if (error instanceof ReviewVerdictProtocolError) {
2999
- throw new Error(`Review protocol error: ${error.message}`);
3787
+ const artifactPath = join11(worktreePath, artifactPathInWorktree);
3788
+ return Effect7.gen(function* () {
3789
+ const exec = yield* ExecutionProvider;
3790
+ const fs = yield* FileSystem;
3791
+ const prompt = yield* buildReviewerPromptEffect(
3792
+ repoRoot2,
3793
+ reviewer.promptTemplate,
3794
+ reviewer.criteria,
3795
+ artifactPathInWorktree,
3796
+ iteration ?? 1,
3797
+ fs,
3798
+ reviewHistory,
3799
+ priorRefactorArtifacts,
3800
+ humanHandoffResolved,
3801
+ priorReviewerArtifacts
3802
+ );
3803
+ yield* prepareReviewArtifactPathEffect(artifactPath, fs);
3804
+ logger.step("info", "Running reviewer");
3805
+ const executionResult = yield* exec.execute({
3806
+ stage: "reviewer",
3807
+ iteration,
3808
+ agent: reviewer.agent,
3809
+ model: reviewer.model,
3810
+ prompt,
3811
+ target,
3812
+ repoRoot: repoRoot2,
3813
+ branchName: builderBranch,
3814
+ sandbox: config.sandbox,
3815
+ autoApprove: true,
3816
+ artifactPath: artifactPathInWorktree,
3817
+ worktreePath,
3818
+ artifacts: [
3819
+ buildRunContextArtifact({
3820
+ issue,
3821
+ target,
3822
+ branchName: builderBranch,
3823
+ reviewerCriteria: reviewer.criteria,
3824
+ sections: STAGE_SECTIONS.reviewer
3825
+ })
3826
+ ],
3827
+ logger
3828
+ });
3829
+ if (!executionResult.success) {
3830
+ return yield* Effect7.fail(
3831
+ new ReviewerFailure({
3832
+ message: `Reviewer execution failed: ${executionResult.error}`
3833
+ })
3834
+ );
3000
3835
  }
3001
- throw error;
3002
- }
3003
- logger.step("info", `Review verdict: ${verdict}`);
3004
- validateReviewArtifact(
3005
- output,
3006
- verdict,
3007
- iteration ?? 1,
3008
- !!priorRefactorArtifacts
3836
+ const output = yield* readReviewArtifactEffect(
3837
+ artifactPath,
3838
+ executionResult.logPath,
3839
+ fs
3840
+ );
3841
+ const parsedVerdict = yield* Effect7.try({
3842
+ try: () => parseReviewVerdict(output),
3843
+ catch: (error) => {
3844
+ if (error instanceof ReviewVerdictProtocolError) {
3845
+ return new ReviewerFailure({
3846
+ message: `Review protocol error: ${error.message}`
3847
+ });
3848
+ }
3849
+ throw error;
3850
+ }
3851
+ }).pipe(
3852
+ Effect7.catchAll(
3853
+ (error) => error instanceof ReviewerFailure ? Effect7.fail(error) : Effect7.die(error)
3854
+ )
3855
+ );
3856
+ logger.step("info", `Review verdict: ${parsedVerdict}`);
3857
+ try {
3858
+ validateReviewArtifact(
3859
+ output,
3860
+ parsedVerdict,
3861
+ iteration ?? 1,
3862
+ !!priorRefactorArtifacts
3863
+ );
3864
+ } catch (error) {
3865
+ if (error instanceof ReviewArtifactValidationError) {
3866
+ return yield* Effect7.fail(
3867
+ new ReviewerFailure({ message: error.message })
3868
+ );
3869
+ }
3870
+ yield* Effect7.die(error);
3871
+ }
3872
+ return { verdict: parsedVerdict, output, artifactPath };
3873
+ });
3874
+ }
3875
+ function bridgeExecutionProvider2(ep) {
3876
+ return Layer3.succeed(
3877
+ ExecutionProvider,
3878
+ ExecutionProvider.of({
3879
+ execute: (opts) => Effect7.tryPromise({
3880
+ try: () => ep.execute(opts),
3881
+ catch: (e) => new Error(
3882
+ e instanceof Error ? e.message : `Execution failed: ${String(e)}`
3883
+ )
3884
+ })
3885
+ })
3009
3886
  );
3010
- return { verdict, output, artifactPath };
3011
3887
  }
3012
- function buildReviewerPrompt(repoRoot2, promptTemplate, criteria, artifactPathInWorktree, iteration, reviewHistory = [], priorRefactorArtifacts, humanHandoffResolved, priorReviewerArtifacts) {
3013
- const criteriaBlock = renderReviewCriteria(repoRoot2, criteria);
3014
- const { content: renderedTemplate, hasCriteriaPlaceholder } = loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock);
3015
- const priorRefactorBlock = priorRefactorArtifacts ? `${priorRefactorArtifacts}` : "";
3016
- const priorReviewerBlock = priorReviewerArtifacts ? `${priorReviewerArtifacts}` : "";
3017
- const humanHandoffBoundary = humanHandoffResolved ? `## Human-Resolved Handoff Boundary
3888
+ function buildReviewerPromptEffect(repoRoot2, promptTemplate, criteria, artifactPathInWorktree, iteration, fs, reviewHistory = [], priorRefactorArtifacts, humanHandoffResolved, priorReviewerArtifacts) {
3889
+ return Effect7.gen(function* () {
3890
+ const criteriaBlock = yield* renderReviewCriteriaEffect(
3891
+ repoRoot2,
3892
+ criteria,
3893
+ fs
3894
+ );
3895
+ const { content: renderedTemplate, hasCriteriaPlaceholder } = yield* loadReviewerPromptTemplateEffect(
3896
+ repoRoot2,
3897
+ promptTemplate,
3898
+ criteriaBlock,
3899
+ fs
3900
+ );
3901
+ const priorRefactorBlock = priorRefactorArtifacts ? `${priorRefactorArtifacts}` : "";
3902
+ const priorRefactorProtocolReminder = priorRefactorArtifacts ? `## Prior Refactor Protocol Reminder
3903
+
3904
+ MANDATORY: because this prompt contains ## Prior Refactor Artifacts, your artifact must contain ## Prior Refactor Response Assessment exactly, before verdict. Section headings must match the names defined in this protocol exactly.
3905
+
3906
+ ` : "";
3907
+ const priorReviewerBlock = priorReviewerArtifacts ? `${priorReviewerArtifacts}` : "";
3908
+ const humanHandoffBoundary = humanHandoffResolved ? `## Human-Resolved Handoff Boundary
3018
3909
 
3019
3910
  A prior review emitted \`NEEDS_HUMAN\` and stopped the agent loop. The issue has since been moved back to \`ready-for-agent\`.
3020
3911
 
3021
3912
  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.
3022
3913
 
3023
3914
  ` : "";
3024
- return `${renderedTemplate}
3915
+ return `${renderedTemplate}
3025
3916
 
3026
3917
  ## Shared Run Context
3027
3918
 
@@ -3031,7 +3922,7 @@ ${hasCriteriaPlaceholder ? "" : `## Review Criteria
3031
3922
 
3032
3923
  ${criteriaBlock}
3033
3924
 
3034
- `}${humanHandoffBoundary}${priorReviewerBlock}${renderReviewHistory(reviewHistory)}${priorRefactorBlock}## Output
3925
+ `}${humanHandoffBoundary}${priorReviewerBlock}${renderReviewHistory(reviewHistory)}${priorRefactorBlock}${priorRefactorProtocolReminder}## Output
3035
3926
 
3036
3927
  Write your review to: ${artifactPathInWorktree}
3037
3928
 
@@ -3042,6 +3933,7 @@ End the file with exactly one wrapped verdict token: <verdict>PASS</verdict>, <v
3042
3933
  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).
3043
3934
 
3044
3935
  When verdict is NEEDS_HUMAN, include Human Handoff Summary and Human Handoff Reason sections before the final verdict token.`;
3936
+ });
3045
3937
  }
3046
3938
  function renderReviewHistory(reviewHistory) {
3047
3939
  if (reviewHistory.length === 0) {
@@ -3055,150 +3947,147 @@ ${entry.trimEnd()}`).join("\n\n")}
3055
3947
 
3056
3948
  `;
3057
3949
  }
3058
- function renderPriorRefactorArtifacts(worktreePath, currentIteration) {
3059
- const refactorsDir = join9(worktreePath, ".pourkit", ".tmp", "refactors");
3060
- if (!existsSync6(refactorsDir)) {
3061
- return "";
3062
- }
3063
- const files = readdirSync(refactorsDir);
3064
- const iterationFiles = [];
3065
- for (const file of files) {
3066
- const match = file.match(/^iteration-(\d+)\.md$/);
3067
- if (match) {
3068
- const num = parseInt(match[1], 10);
3069
- if (num < currentIteration) {
3070
- const filePath = join9(refactorsDir, file);
3071
- try {
3072
- const content = readFileSync6(filePath, "utf-8");
3073
- if (content.trim()) {
3074
- iterationFiles.push({ num, content });
3075
- }
3076
- } catch {
3077
- }
3078
- }
3079
- }
3080
- }
3081
- if (iterationFiles.length === 0) {
3082
- return "";
3083
- }
3084
- iterationFiles.sort((a, b) => a.num - b.num);
3085
- const iterationsBlocks = iterationFiles.map((f) => `### Refactor Iteration ${f.num}
3950
+ function renderPriorRefactorArtifactsEffect(worktreePath, currentIteration) {
3951
+ return Effect7.gen(function* () {
3952
+ const fb = yield* FileSystem;
3953
+ const refactorsDir = join11(worktreePath, ".pourkit", ".tmp", "refactors");
3954
+ const dirExists = yield* fb.exists(refactorsDir).pipe(Effect7.catchAll(() => Effect7.succeed(false)));
3955
+ if (!dirExists) return "";
3956
+ const iterationFiles = [];
3957
+ for (let i = 0; i < currentIteration; i++) {
3958
+ const filePath = join11(refactorsDir, `iteration-${i}.md`);
3959
+ const fileExists = yield* fb.exists(filePath).pipe(Effect7.catchAll(() => Effect7.succeed(false)));
3960
+ if (!fileExists) continue;
3961
+ const content = yield* fb.readFile(filePath).pipe(Effect7.catchAll(() => Effect7.succeed("")));
3962
+ if (content.trim()) {
3963
+ iterationFiles.push({ num: i, content });
3964
+ }
3965
+ }
3966
+ if (iterationFiles.length === 0) return "";
3967
+ iterationFiles.sort((a, b) => a.num - b.num);
3968
+ const iterationsBlocks = iterationFiles.map((f) => `### Refactor Iteration ${f.num}
3086
3969
 
3087
3970
  ${f.content.trimEnd()}`).join("\n\n");
3088
- return `## Prior Refactor Artifacts
3971
+ return `## Prior Refactor Artifacts
3089
3972
 
3090
3973
  Treat these as conversational context, not source of truth. Inspect the current code independently.
3091
3974
 
3092
3975
  ${iterationsBlocks}
3093
3976
 
3094
3977
  `;
3978
+ });
3095
3979
  }
3096
- function renderPriorReviewerArtifacts(worktreePath, currentIteration) {
3097
- const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
3098
- if (!existsSync6(reviewersDir)) {
3099
- return "";
3100
- }
3101
- const files = readdirSync(reviewersDir);
3102
- const iterationFiles = [];
3103
- for (const file of files) {
3104
- const match = file.match(/^iteration-(\d+)\.md$/);
3105
- if (match) {
3106
- const num = parseInt(match[1], 10);
3107
- if (num < currentIteration) {
3108
- const filePath = join9(reviewersDir, file);
3109
- try {
3110
- const content = readFileSync6(filePath, "utf-8");
3111
- if (content.trim()) {
3112
- iterationFiles.push({ num, content });
3113
- }
3114
- } catch {
3115
- }
3116
- }
3117
- }
3118
- }
3119
- if (iterationFiles.length === 0) {
3120
- return "";
3121
- }
3122
- iterationFiles.sort((a, b) => a.num - b.num);
3123
- const iterationsBlocks = iterationFiles.map((f) => `### Reviewer Iteration ${f.num}
3980
+ function renderPriorReviewerArtifactsEffect(worktreePath, currentIteration) {
3981
+ return Effect7.gen(function* () {
3982
+ const fb = yield* FileSystem;
3983
+ const reviewersDir = join11(worktreePath, ".pourkit", ".tmp", "reviewers");
3984
+ const dirExists = yield* fb.exists(reviewersDir).pipe(Effect7.catchAll(() => Effect7.succeed(false)));
3985
+ if (!dirExists) return "";
3986
+ const iterationFiles = [];
3987
+ for (let i = 0; i < currentIteration; i++) {
3988
+ const filePath = join11(reviewersDir, `iteration-${i}.md`);
3989
+ const fileExists = yield* fb.exists(filePath).pipe(Effect7.catchAll(() => Effect7.succeed(false)));
3990
+ if (!fileExists) continue;
3991
+ const content = yield* fb.readFile(filePath).pipe(Effect7.catchAll(() => Effect7.succeed("")));
3992
+ if (content.trim()) {
3993
+ iterationFiles.push({ num: i, content });
3994
+ }
3995
+ }
3996
+ if (iterationFiles.length === 0) return "";
3997
+ iterationFiles.sort((a, b) => a.num - b.num);
3998
+ const iterationsBlocks = iterationFiles.map((f) => `### Reviewer Iteration ${f.num}
3124
3999
 
3125
4000
  ${f.content.trimEnd()}`).join("\n\n");
3126
- return `## Prior Reviewer Artifacts
4001
+ return `## Prior Reviewer Artifacts
3127
4002
 
3128
4003
  A prior review was resolved by a human. These artifacts are historical context from before the handoff. Treat them as background, not active findings.
3129
4004
 
3130
4005
  ${iterationsBlocks}
3131
4006
 
3132
4007
  `;
4008
+ });
3133
4009
  }
3134
- function loadReviewerPromptTemplate(repoRoot2, promptTemplate, criteriaBlock) {
3135
- const promptTemplatePath = resolvePromptTemplatePath(
3136
- repoRoot2,
3137
- promptTemplate
3138
- );
3139
- const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
3140
- const hasCriteriaPlaceholder = promptBody.includes("{{REVIEW_CRITERIA}}");
3141
- return {
3142
- content: promptBody.replace(/\{\{REVIEW_CRITERIA\}\}/g, criteriaBlock),
3143
- hasCriteriaPlaceholder
3144
- };
3145
- }
3146
- function renderReviewCriteria(repoRoot2, criteria) {
3147
- return criteria.map((criterion) => {
3148
- const snippetPath = join9(
4010
+ function loadReviewerPromptTemplateEffect(repoRoot2, promptTemplate, criteriaBlock, fs) {
4011
+ return Effect7.gen(function* () {
4012
+ const promptTemplatePath = resolvePromptTemplatePath(
3149
4013
  repoRoot2,
3150
- ".pourkit",
3151
- "prompts",
3152
- `reviewer-${criterion}.snippet.md`
4014
+ promptTemplate
3153
4015
  );
3154
- if (existsSync6(snippetPath)) {
3155
- return readFileSync6(snippetPath, "utf-8").trimEnd();
3156
- }
3157
- return `- ${criterion}`;
3158
- }).join("\n\n");
4016
+ const exists = yield* fs.exists(promptTemplatePath).pipe(Effect7.orDie);
4017
+ const promptBody = exists ? yield* fs.readFile(promptTemplatePath).pipe(Effect7.orDie) : promptTemplate;
4018
+ const hasCriteriaPlaceholder = promptBody.includes("{{REVIEW_CRITERIA}}");
4019
+ return {
4020
+ content: promptBody.replace(/\{\{REVIEW_CRITERIA\}\}/g, criteriaBlock),
4021
+ hasCriteriaPlaceholder
4022
+ };
4023
+ });
3159
4024
  }
3160
- function prepareReviewArtifactPath(artifactPath) {
3161
- mkdirSync5(dirname4(artifactPath), { recursive: true });
3162
- if (existsSync6(artifactPath)) {
3163
- rmSync2(artifactPath);
3164
- }
4025
+ function renderReviewCriteriaEffect(repoRoot2, criteria, fs) {
4026
+ return Effect7.gen(function* () {
4027
+ const parts = [];
4028
+ for (const criterion of criteria) {
4029
+ const snippetPath = join11(
4030
+ repoRoot2,
4031
+ ".pourkit",
4032
+ "prompts",
4033
+ `reviewer-${criterion}.snippet.md`
4034
+ );
4035
+ const exists = yield* fs.exists(snippetPath).pipe(Effect7.orDie);
4036
+ if (exists) {
4037
+ const content = yield* fs.readFile(snippetPath).pipe(Effect7.orDie);
4038
+ parts.push(content.trimEnd());
4039
+ } else {
4040
+ parts.push(`- ${criterion}`);
4041
+ }
4042
+ }
4043
+ return parts.join("\n\n");
4044
+ });
3165
4045
  }
3166
- function recoverReviewOutputFromLog(logPath) {
3167
- if (!existsSync6(logPath)) {
3168
- return null;
3169
- }
3170
- const logContent = readFileSync6(logPath, "utf-8");
4046
+ function recoverReviewOutputFromString(logContent) {
3171
4047
  const startIndex = logContent.indexOf("## Findings");
3172
- if (startIndex === -1) {
3173
- return null;
3174
- }
4048
+ if (startIndex === -1) return null;
3175
4049
  const verdictMatch = logContent.slice(startIndex).match(
3176
4050
  /<verdict>(PASS|PASS_WITH_NOTES|NEEDS_REFACTOR|FAIL|NEEDS_HUMAN)<\/verdict>/
3177
4051
  );
3178
- if (!verdictMatch || verdictMatch.index === void 0) {
3179
- return null;
3180
- }
4052
+ if (!verdictMatch || verdictMatch.index === void 0) return null;
3181
4053
  const recoveredOutput = logContent.slice(startIndex, startIndex + verdictMatch.index + verdictMatch[0].length).trim();
3182
4054
  return recoveredOutput.length > 0 ? recoveredOutput : null;
3183
4055
  }
3184
- function readReviewArtifact2(artifactPath, logPath) {
3185
- if (existsSync6(artifactPath)) {
3186
- const output = readFileSync6(artifactPath, "utf-8");
3187
- if (output.trim()) {
3188
- return output;
4056
+ function prepareReviewArtifactPathEffect(artifactPath, fs) {
4057
+ return Effect7.gen(function* () {
4058
+ yield* fs.mkdir(dirname5(artifactPath)).pipe(Effect7.orDie);
4059
+ const exists = yield* fs.exists(artifactPath).pipe(Effect7.orDie);
4060
+ if (exists) {
4061
+ yield* fs.remove(artifactPath).pipe(Effect7.orDie);
3189
4062
  }
3190
- }
3191
- const recoveredOutput = logPath ? recoverReviewOutputFromLog(logPath) : null;
3192
- if (recoveredOutput) {
3193
- writeFileSync3(artifactPath, recoveredOutput, "utf-8");
3194
- return recoveredOutput;
3195
- }
3196
- if (!existsSync6(artifactPath)) {
3197
- throw new Error(`Reviewer did not produce output at ${artifactPath}`);
3198
- }
3199
- throw new Error(`Reviewer produced empty output at ${artifactPath}`);
4063
+ });
4064
+ }
4065
+ function readReviewArtifactEffect(artifactPath, logPath, fs) {
4066
+ return Effect7.gen(function* () {
4067
+ const artifactExists = yield* fs.exists(artifactPath).pipe(Effect7.orDie);
4068
+ if (artifactExists) {
4069
+ const output = yield* fs.readFile(artifactPath).pipe(Effect7.orDie);
4070
+ if (output.trim()) return output;
4071
+ }
4072
+ if (logPath) {
4073
+ const logExists = yield* fs.exists(logPath).pipe(Effect7.orDie);
4074
+ if (logExists) {
4075
+ const logContent = yield* fs.readFile(logPath).pipe(Effect7.orDie);
4076
+ const recovered = recoverReviewOutputFromString(logContent);
4077
+ if (recovered) {
4078
+ yield* fs.writeFile(artifactPath, recovered).pipe(Effect7.orDie);
4079
+ return recovered;
4080
+ }
4081
+ }
4082
+ }
4083
+ return yield* Effect7.fail(
4084
+ new ReviewerFailure({
4085
+ message: artifactExists ? `Reviewer produced empty output at ${artifactPath}` : `Reviewer did not produce output at ${artifactPath}`
4086
+ })
4087
+ );
4088
+ });
3200
4089
  }
3201
- async function runReviewWithRefactorLoop(options) {
4090
+ function runReviewWithRefactorLoop(options) {
3202
4091
  const {
3203
4092
  executionProvider,
3204
4093
  config,
@@ -3215,247 +4104,325 @@ async function runReviewWithRefactorLoop(options) {
3215
4104
  const strategy = target.strategy;
3216
4105
  const reviewer = strategy.review.reviewer;
3217
4106
  if (!reviewer) {
3218
- throw new Error("No reviewer config found");
4107
+ return Effect7.die(new Error("No reviewer config found"));
3219
4108
  }
3220
4109
  const refactorer = strategy.review.refactor;
3221
4110
  if (!refactorer) {
3222
- throw new Error("No refactorer config found");
4111
+ return Effect7.die(new Error("No refactorer config found"));
3223
4112
  }
3224
4113
  const maxIterations = strategy.review.maxIterations;
3225
- const passWithNotesRefactorAttempts = strategy.review.passWithNotesRefactorAttempts;
3226
- let resolvedStartingIteration = startingLifetimeIteration;
3227
- {
3228
- const reviewersDir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
3229
- try {
3230
- const files = readdirSync(reviewersDir);
3231
- let maxExistingIteration = 0;
3232
- for (const file of files) {
3233
- const match = file.match(/^iteration-(\d+)\.md$/);
3234
- if (match) {
3235
- const num = parseInt(match[1], 10);
3236
- if (num > maxExistingIteration) {
3237
- maxExistingIteration = num;
3238
- }
3239
- }
3240
- }
3241
- if (maxExistingIteration > resolvedStartingIteration) {
3242
- resolvedStartingIteration = maxExistingIteration;
3243
- }
3244
- } catch {
3245
- }
3246
- }
3247
- const accumulatedRefactorPaths = [];
3248
- let iteration = 0;
3249
- let lastResult = null;
3250
- const reviewHistory = [];
3251
- let passWithNotesRefactorAttemptsRemaining = passWithNotesRefactorAttempts;
3252
- const priorReviewerArtifacts = humanHandoffResolved ? renderPriorReviewerArtifacts(
3253
- worktreePath,
3254
- resolvedStartingIteration + 1
3255
- ) || void 0 : void 0;
3256
- while (iteration < maxIterations) {
3257
- iteration++;
3258
- const lifetimeIteration = resolvedStartingIteration + iteration;
3259
- logger.step("info", `Review iteration ${lifetimeIteration}`);
3260
- const priorRefactorArtifacts = renderPriorRefactorArtifacts(
3261
- worktreePath,
3262
- lifetimeIteration
3263
- );
3264
- const reviewResult = await runReviewCommand({
3265
- executionProvider,
3266
- config,
3267
- target,
3268
- issue,
3269
- builderBranch,
3270
- worktreePath,
3271
- repoRoot: repoRoot2,
3272
- logger,
3273
- iteration: lifetimeIteration,
3274
- priorRefactorArtifacts: priorRefactorArtifacts || void 0,
3275
- humanHandoffResolved,
3276
- priorReviewerArtifacts,
3277
- reviewHistory: reviewer.includeReviewHistory && reviewHistory.length > 0 ? [...reviewHistory] : void 0
3278
- });
3279
- lastResult = reviewResult;
3280
- reviewHistory.push(reviewResult.output);
3281
- await persistIterationArtifact(
3282
- worktreePath,
3283
- reviewResult.output,
3284
- lifetimeIteration
3285
- );
3286
- if (reviewResult.verdict === "PASS") {
3287
- return {
3288
- verdict: reviewResult.verdict,
3289
- output: reviewResult.output,
3290
- artifactPath: reviewResult.artifactPath,
3291
- iterations: iteration,
3292
- lifetimeIterations: lifetimeIteration,
3293
- exhaustedMaxIterations: false,
3294
- refactorCompletedForLastReview: false,
3295
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
3296
- };
3297
- }
3298
- if (reviewResult.verdict === "PASS_WITH_NOTES" && passWithNotesRefactorAttemptsRemaining === 0) {
3299
- logger.step(
3300
- "info",
3301
- "PASS_WITH_NOTES refactor attempts exhausted, treating as PASS"
3302
- );
3303
- return {
3304
- verdict: "PASS",
3305
- output: reviewResult.output,
3306
- artifactPath: reviewResult.artifactPath,
3307
- iterations: iteration,
3308
- lifetimeIterations: lifetimeIteration,
3309
- exhaustedMaxIterations: false,
3310
- refactorCompletedForLastReview: false,
3311
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
3312
- };
3313
- }
3314
- if (reviewResult.verdict === "PASS_WITH_NOTES") {
3315
- passWithNotesRefactorAttemptsRemaining--;
3316
- logger.step(
3317
- "info",
3318
- `PASS_WITH_NOTES refactor attempts remaining: ${passWithNotesRefactorAttemptsRemaining}`
3319
- );
3320
- }
3321
- if (reviewResult.verdict === "NEEDS_HUMAN") {
3322
- logger.step("info", "NEEDS_HUMAN verdict, stopping review loop");
3323
- return {
3324
- verdict: "NEEDS_HUMAN",
3325
- output: reviewResult.output,
3326
- artifactPath: reviewResult.artifactPath,
3327
- iterations: iteration,
3328
- lifetimeIterations: lifetimeIteration,
3329
- exhaustedMaxIterations: false,
3330
- refactorCompletedForLastReview: false,
3331
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
3332
- };
4114
+ const passWithNotesRefactorAttempts = strategy.review.passWithNotesRefactorAttempts;
4115
+ let resolvedStartingIteration = startingLifetimeIteration;
4116
+ {
4117
+ const reviewersDir = join11(worktreePath, ".pourkit", ".tmp", "reviewers");
4118
+ try {
4119
+ const files = readdirSync(reviewersDir);
4120
+ let maxExistingIteration = 0;
4121
+ for (const file of files) {
4122
+ const match = file.match(/^iteration-(\d+)\.md$/);
4123
+ if (match) {
4124
+ const num = parseInt(match[1], 10);
4125
+ if (num > maxExistingIteration) {
4126
+ maxExistingIteration = num;
4127
+ }
4128
+ }
4129
+ }
4130
+ if (maxExistingIteration > resolvedStartingIteration) {
4131
+ resolvedStartingIteration = maxExistingIteration;
4132
+ }
4133
+ } catch {
3333
4134
  }
3334
- if (reviewResult.verdict === "NEEDS_REFACTOR" || reviewResult.verdict === "PASS_WITH_NOTES" || reviewResult.verdict === "FAIL") {
3335
- logger.step("info", "Running refactor agent");
3336
- const refactorArtifactPathInWorktree = join9(
3337
- ".pourkit",
3338
- ".tmp",
3339
- "refactors",
3340
- `iteration-${lifetimeIteration}.md`
3341
- );
3342
- const refactorPrompt = buildRefactorPrompt(
3343
- repoRoot2,
3344
- refactorer.promptTemplate,
3345
- reviewResult.output,
3346
- refactorArtifactPathInWorktree
3347
- );
3348
- const refactorResult = await executionProvider.execute({
3349
- stage: "refactor",
3350
- iteration: lifetimeIteration,
3351
- agent: refactorer.agent,
3352
- model: refactorer.model,
3353
- prompt: refactorPrompt,
3354
- target,
3355
- repoRoot: repoRoot2,
3356
- branchName: builderBranch,
3357
- sandbox: config.sandbox,
3358
- autoApprove: true,
3359
- artifactPath: refactorArtifactPathInWorktree,
3360
- worktreePath,
3361
- artifacts: [
3362
- buildRunContextArtifact({
3363
- issue,
3364
- target,
3365
- branchName: builderBranch,
3366
- reviewerCriteria: reviewer.criteria,
3367
- sections: STAGE_SECTIONS.refactor
3368
- })
3369
- ],
3370
- ...serena ? { serena } : {},
3371
- logger
3372
- });
3373
- if (!refactorResult.success) {
3374
- logger.step(
3375
- "warn",
3376
- "Refactor execution failed, transitioning to ready-for-human"
4135
+ }
4136
+ const program = Effect7.gen(function* () {
4137
+ const exec = yield* ExecutionProvider;
4138
+ const fs = yield* FileSystem;
4139
+ const priorReviewerArtifacts = humanHandoffResolved ? (yield* renderPriorReviewerArtifactsEffect(
4140
+ worktreePath,
4141
+ resolvedStartingIteration + 1
4142
+ )) || void 0 : void 0;
4143
+ const initialState = {
4144
+ done: false,
4145
+ iteration: 0,
4146
+ lastResult: null,
4147
+ reviewHistory: [],
4148
+ passWithNotesRefactorAttemptsRemaining: passWithNotesRefactorAttempts,
4149
+ accumulatedRefactorPaths: [],
4150
+ doneResult: null
4151
+ };
4152
+ const finalState = yield* Effect7.iterate(initialState, {
4153
+ while: (s) => !s.done && s.iteration < maxIterations,
4154
+ body: (s) => Effect7.gen(function* () {
4155
+ const iteration = s.iteration + 1;
4156
+ const lifetimeIteration = resolvedStartingIteration + iteration;
4157
+ logger.step("info", `Review iteration ${lifetimeIteration}`);
4158
+ const priorRefactorArtifactsStr = yield* renderPriorRefactorArtifactsEffect(
4159
+ worktreePath,
4160
+ lifetimeIteration
3377
4161
  );
3378
- return {
3379
- verdict: "FAIL",
3380
- output: reviewResult.output,
3381
- artifactPath: reviewResult.artifactPath,
3382
- iterations: iteration,
3383
- lifetimeIterations: resolvedStartingIteration + iteration,
3384
- exhaustedMaxIterations: false,
3385
- refactorCompletedForLastReview: false,
3386
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
3387
- };
3388
- }
3389
- const latestFindingIds = extractLatestFindingIds(
3390
- reviewResult.output,
3391
- lifetimeIteration
3392
- );
3393
- const refactorArtifactPath = join9(
3394
- worktreePath,
3395
- refactorArtifactPathInWorktree
3396
- );
3397
- try {
3398
- validateRefactorArtifact(refactorArtifactPath, latestFindingIds);
3399
- } catch (error) {
3400
- if (error instanceof RefactorArtifactValidationError) {
4162
+ const reviewResult = yield* runReviewCommandEffect({
4163
+ executionProvider: null,
4164
+ config,
4165
+ target,
4166
+ issue,
4167
+ builderBranch,
4168
+ worktreePath,
4169
+ repoRoot: repoRoot2,
4170
+ logger,
4171
+ iteration: lifetimeIteration,
4172
+ priorRefactorArtifacts: priorRefactorArtifactsStr || void 0,
4173
+ humanHandoffResolved,
4174
+ priorReviewerArtifacts,
4175
+ reviewHistory: reviewer.includeReviewHistory && s.reviewHistory.length > 0 ? [...s.reviewHistory] : void 0
4176
+ });
4177
+ const updatedHistory = [...s.reviewHistory, reviewResult.output];
4178
+ yield* persistIterationArtifactEffect(
4179
+ worktreePath,
4180
+ reviewResult.output,
4181
+ lifetimeIteration,
4182
+ fs
4183
+ );
4184
+ if (reviewResult.verdict === "PASS") {
4185
+ return {
4186
+ ...s,
4187
+ done: true,
4188
+ iteration,
4189
+ lastResult: reviewResult,
4190
+ reviewHistory: updatedHistory,
4191
+ doneResult: {
4192
+ verdict: reviewResult.verdict,
4193
+ output: reviewResult.output,
4194
+ artifactPath: reviewResult.artifactPath,
4195
+ iterations: iteration,
4196
+ lifetimeIterations: lifetimeIteration,
4197
+ exhaustedMaxIterations: false,
4198
+ refactorCompletedForLastReview: false,
4199
+ refactorArtifactPaths: s.accumulatedRefactorPaths.length > 0 ? [...s.accumulatedRefactorPaths] : void 0
4200
+ }
4201
+ };
4202
+ }
4203
+ if (reviewResult.verdict === "PASS_WITH_NOTES" && s.passWithNotesRefactorAttemptsRemaining === 0) {
3401
4204
  logger.step(
3402
- "warn",
3403
- `Refactor artifact validation failed: ${error.message}`
4205
+ "info",
4206
+ "PASS_WITH_NOTES refactor attempts exhausted, treating as PASS"
3404
4207
  );
3405
4208
  return {
3406
- verdict: "FAIL",
3407
- output: reviewResult.output,
3408
- artifactPath: reviewResult.artifactPath,
3409
- iterations: iteration,
3410
- lifetimeIterations: resolvedStartingIteration + iteration,
3411
- exhaustedMaxIterations: false,
3412
- refactorCompletedForLastReview: false,
3413
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
4209
+ ...s,
4210
+ done: true,
4211
+ iteration,
4212
+ lastResult: reviewResult,
4213
+ reviewHistory: updatedHistory,
4214
+ doneResult: {
4215
+ verdict: "PASS",
4216
+ output: reviewResult.output,
4217
+ artifactPath: reviewResult.artifactPath,
4218
+ iterations: iteration,
4219
+ lifetimeIterations: lifetimeIteration,
4220
+ exhaustedMaxIterations: false,
4221
+ refactorCompletedForLastReview: false,
4222
+ refactorArtifactPaths: s.accumulatedRefactorPaths.length > 0 ? [...s.accumulatedRefactorPaths] : void 0
4223
+ }
3414
4224
  };
3415
4225
  }
3416
- throw error;
3417
- }
3418
- accumulatedRefactorPaths.push(refactorArtifactPath);
3419
- if (options.onRefactorProgress) {
3420
- await options.onRefactorProgress({
3421
- lifetimeIterations: resolvedStartingIteration + iteration,
3422
- lastVerdict: reviewResult.verdict,
3423
- lastArtifactPath: reviewResult.artifactPath,
3424
- refactorArtifactPath
3425
- });
3426
- }
4226
+ let updatedPassWithNotesRefactorAttemptsRemaining = s.passWithNotesRefactorAttemptsRemaining;
4227
+ if (reviewResult.verdict === "PASS_WITH_NOTES") {
4228
+ updatedPassWithNotesRefactorAttemptsRemaining--;
4229
+ logger.step(
4230
+ "info",
4231
+ `PASS_WITH_NOTES refactor attempts remaining: ${updatedPassWithNotesRefactorAttemptsRemaining}`
4232
+ );
4233
+ }
4234
+ if (reviewResult.verdict === "NEEDS_HUMAN") {
4235
+ logger.step("info", "NEEDS_HUMAN verdict, stopping review loop");
4236
+ return {
4237
+ ...s,
4238
+ done: true,
4239
+ iteration,
4240
+ lastResult: reviewResult,
4241
+ reviewHistory: updatedHistory,
4242
+ doneResult: {
4243
+ verdict: "NEEDS_HUMAN",
4244
+ output: reviewResult.output,
4245
+ artifactPath: reviewResult.artifactPath,
4246
+ iterations: iteration,
4247
+ lifetimeIterations: lifetimeIteration,
4248
+ exhaustedMaxIterations: false,
4249
+ refactorCompletedForLastReview: false,
4250
+ refactorArtifactPaths: s.accumulatedRefactorPaths.length > 0 ? [...s.accumulatedRefactorPaths] : void 0
4251
+ }
4252
+ };
4253
+ }
4254
+ if (reviewResult.verdict === "NEEDS_REFACTOR" || reviewResult.verdict === "PASS_WITH_NOTES" || reviewResult.verdict === "FAIL") {
4255
+ logger.step("info", "Running refactor agent");
4256
+ const refactorArtifactPathInWorktree = join11(
4257
+ ".pourkit",
4258
+ ".tmp",
4259
+ "refactors",
4260
+ `iteration-${lifetimeIteration}.md`
4261
+ );
4262
+ const refactorPrompt = yield* buildRefactorPromptEffect(
4263
+ repoRoot2,
4264
+ refactorer.promptTemplate,
4265
+ reviewResult.output,
4266
+ refactorArtifactPathInWorktree,
4267
+ fs
4268
+ );
4269
+ const refactorResult = yield* exec.execute({
4270
+ stage: "refactor",
4271
+ iteration: lifetimeIteration,
4272
+ agent: refactorer.agent,
4273
+ model: refactorer.model,
4274
+ prompt: refactorPrompt,
4275
+ target,
4276
+ repoRoot: repoRoot2,
4277
+ branchName: builderBranch,
4278
+ sandbox: config.sandbox,
4279
+ autoApprove: true,
4280
+ artifactPath: refactorArtifactPathInWorktree,
4281
+ worktreePath,
4282
+ artifacts: [
4283
+ buildRunContextArtifact({
4284
+ issue,
4285
+ target,
4286
+ branchName: builderBranch,
4287
+ reviewerCriteria: reviewer.criteria,
4288
+ sections: STAGE_SECTIONS.refactor
4289
+ })
4290
+ ],
4291
+ ...serena ? { serena } : {},
4292
+ logger
4293
+ });
4294
+ if (!refactorResult.success) {
4295
+ logger.step(
4296
+ "warn",
4297
+ "Refactor execution failed, transitioning to ready-for-human"
4298
+ );
4299
+ return {
4300
+ ...s,
4301
+ done: true,
4302
+ iteration,
4303
+ lastResult: reviewResult,
4304
+ reviewHistory: updatedHistory,
4305
+ doneResult: {
4306
+ verdict: "FAIL",
4307
+ output: reviewResult.output,
4308
+ artifactPath: reviewResult.artifactPath,
4309
+ iterations: iteration,
4310
+ lifetimeIterations: resolvedStartingIteration + iteration,
4311
+ exhaustedMaxIterations: false,
4312
+ refactorCompletedForLastReview: false,
4313
+ refactorArtifactPaths: s.accumulatedRefactorPaths.length > 0 ? [...s.accumulatedRefactorPaths] : void 0
4314
+ }
4315
+ };
4316
+ }
4317
+ const latestFindingIds = extractLatestFindingIds(
4318
+ reviewResult.output,
4319
+ lifetimeIteration
4320
+ );
4321
+ const refactorArtifactPath = join11(
4322
+ worktreePath,
4323
+ refactorArtifactPathInWorktree
4324
+ );
4325
+ try {
4326
+ validateRefactorArtifact(refactorArtifactPath, latestFindingIds);
4327
+ } catch (error) {
4328
+ if (error instanceof RefactorArtifactValidationError) {
4329
+ logger.step(
4330
+ "warn",
4331
+ `Refactor artifact validation failed: ${error.message}`
4332
+ );
4333
+ return {
4334
+ ...s,
4335
+ done: true,
4336
+ iteration,
4337
+ lastResult: reviewResult,
4338
+ reviewHistory: updatedHistory,
4339
+ doneResult: {
4340
+ verdict: "FAIL",
4341
+ output: reviewResult.output,
4342
+ artifactPath: reviewResult.artifactPath,
4343
+ iterations: iteration,
4344
+ lifetimeIterations: resolvedStartingIteration + iteration,
4345
+ exhaustedMaxIterations: false,
4346
+ refactorCompletedForLastReview: false,
4347
+ refactorArtifactPaths: s.accumulatedRefactorPaths.length > 0 ? [...s.accumulatedRefactorPaths] : void 0
4348
+ }
4349
+ };
4350
+ }
4351
+ yield* Effect7.die(error);
4352
+ }
4353
+ const updatedAccumulated = [
4354
+ ...s.accumulatedRefactorPaths,
4355
+ refactorArtifactPath
4356
+ ];
4357
+ if (options.onRefactorProgress) {
4358
+ yield* Effect7.promise(
4359
+ () => Promise.resolve(
4360
+ options.onRefactorProgress({
4361
+ lifetimeIterations: resolvedStartingIteration + iteration,
4362
+ lastVerdict: reviewResult.verdict,
4363
+ lastArtifactPath: reviewResult.artifactPath,
4364
+ refactorArtifactPath
4365
+ })
4366
+ )
4367
+ );
4368
+ }
4369
+ return {
4370
+ done: false,
4371
+ iteration,
4372
+ lastResult: reviewResult,
4373
+ reviewHistory: updatedHistory,
4374
+ passWithNotesRefactorAttemptsRemaining: updatedPassWithNotesRefactorAttemptsRemaining,
4375
+ accumulatedRefactorPaths: updatedAccumulated,
4376
+ doneResult: null
4377
+ };
4378
+ }
4379
+ return {
4380
+ done: false,
4381
+ iteration,
4382
+ lastResult: reviewResult,
4383
+ reviewHistory: updatedHistory,
4384
+ passWithNotesRefactorAttemptsRemaining: updatedPassWithNotesRefactorAttemptsRemaining,
4385
+ accumulatedRefactorPaths: s.accumulatedRefactorPaths,
4386
+ doneResult: null
4387
+ };
4388
+ })
4389
+ });
4390
+ if (finalState.done && finalState.doneResult) {
4391
+ return finalState.doneResult;
3427
4392
  }
3428
- }
3429
- logger.step("warn", `Max review iterations (${maxIterations}) exhausted`);
3430
- return {
3431
- verdict: "FAIL",
3432
- output: lastResult?.output ?? "",
3433
- artifactPath: lastResult?.artifactPath ?? "",
3434
- iterations: iteration,
3435
- lifetimeIterations: resolvedStartingIteration + iteration,
3436
- exhaustedMaxIterations: true,
3437
- refactorCompletedForLastReview: true,
3438
- refactorArtifactPaths: accumulatedRefactorPaths.length > 0 ? accumulatedRefactorPaths : void 0
3439
- };
3440
- }
3441
- async function writeArtifact(worktreePath, filename, output) {
3442
- try {
3443
- const dir = join9(worktreePath, ".pourkit", ".tmp", "reviewers");
3444
- mkdirSync5(dir, { recursive: true });
3445
- writeFileSync3(join9(dir, filename), output, "utf-8");
3446
- } catch {
3447
- }
4393
+ logger.step("warn", `Max review iterations (${maxIterations}) exhausted`);
4394
+ return {
4395
+ verdict: "FAIL",
4396
+ output: finalState.lastResult?.output ?? "",
4397
+ artifactPath: finalState.lastResult?.artifactPath ?? "",
4398
+ iterations: finalState.iteration,
4399
+ lifetimeIterations: resolvedStartingIteration + finalState.iteration,
4400
+ exhaustedMaxIterations: true,
4401
+ refactorCompletedForLastReview: true,
4402
+ refactorArtifactPaths: finalState.accumulatedRefactorPaths.length > 0 ? [...finalState.accumulatedRefactorPaths] : void 0
4403
+ };
4404
+ });
4405
+ const executionLayer = bridgeExecutionProvider2(executionProvider);
4406
+ return program.pipe(
4407
+ Effect7.provide(Layer3.merge(executionLayer, FileSystemDefault))
4408
+ );
3448
4409
  }
3449
- async function persistIterationArtifact(worktreePath, output, iteration) {
3450
- await writeArtifact(worktreePath, `iteration-${iteration}.md`, output);
4410
+ function persistIterationArtifactEffect(worktreePath, output, iteration, fs) {
4411
+ return Effect7.gen(function* () {
4412
+ const dir = join11(worktreePath, ".pourkit", ".tmp", "reviewers");
4413
+ yield* fs.mkdir(dir).pipe(Effect7.catchAll(() => Effect7.void));
4414
+ yield* fs.writeFile(join11(dir, `iteration-${iteration}.md`), output).pipe(Effect7.catchAll(() => Effect7.void));
4415
+ });
3451
4416
  }
3452
- function buildRefactorPrompt(repoRoot2, promptTemplate, latestReview, artifactPathInWorktree) {
3453
- const promptTemplatePath = resolvePromptTemplatePath(
3454
- repoRoot2,
3455
- promptTemplate
3456
- );
3457
- const promptBody = existsSync6(promptTemplatePath) ? readFileSync6(promptTemplatePath, "utf-8") : promptTemplate;
3458
- return appendProtectedWorkGuidance(`${promptBody}
4417
+ function buildRefactorPromptEffect(repoRoot2, promptTemplate, latestReview, artifactPathInWorktree, fs) {
4418
+ return Effect7.gen(function* () {
4419
+ const promptTemplatePath = resolvePromptTemplatePath(
4420
+ repoRoot2,
4421
+ promptTemplate
4422
+ );
4423
+ const exists = yield* fs.exists(promptTemplatePath).pipe(Effect7.orDie);
4424
+ const promptBody = exists ? yield* fs.readFile(promptTemplatePath).pipe(Effect7.orDie) : promptTemplate;
4425
+ return appendProtectedWorkGuidance(`${promptBody}
3459
4426
 
3460
4427
  ## Shared Run Context
3461
4428
 
@@ -3470,6 +4437,7 @@ ${latestReview.trimEnd()}
3470
4437
  Write your refactor artifact to: ${artifactPathInWorktree}
3471
4438
 
3472
4439
  When you are done, finish with <promise>COMPLETE</promise>.`);
4440
+ });
3473
4441
  }
3474
4442
 
3475
4443
  // issues/issue-transitions.ts
@@ -3874,22 +4842,24 @@ async function startIssueRun(options) {
3874
4842
  }
3875
4843
  async function advanceIssueRunReview(options) {
3876
4844
  const accumulatedRefactorPaths = [];
3877
- const reviewResult = await runReviewWithRefactorLoop({
3878
- ...options,
3879
- onRefactorProgress: async (progress) => {
3880
- if (progress.refactorArtifactPath) {
3881
- accumulatedRefactorPaths.push(progress.refactorArtifactPath);
3882
- }
3883
- updateWorktreeRunState(options.worktreePath, {
3884
- review: {
3885
- lifetimeIterations: progress.lifetimeIterations,
3886
- lastVerdict: progress.lastVerdict,
3887
- lastArtifactPath: progress.lastArtifactPath,
3888
- refactorCompletedForLastReview: true
4845
+ const reviewResult = await runEffectAndMapExit(
4846
+ runReviewWithRefactorLoop({
4847
+ ...options,
4848
+ onRefactorProgress: (progress) => {
4849
+ if (progress.refactorArtifactPath) {
4850
+ accumulatedRefactorPaths.push(progress.refactorArtifactPath);
3889
4851
  }
3890
- });
3891
- }
3892
- });
4852
+ updateWorktreeRunState(options.worktreePath, {
4853
+ review: {
4854
+ lifetimeIterations: progress.lifetimeIterations,
4855
+ lastVerdict: progress.lastVerdict,
4856
+ lastArtifactPath: progress.lastArtifactPath,
4857
+ refactorCompletedForLastReview: true
4858
+ }
4859
+ });
4860
+ }
4861
+ })
4862
+ );
3893
4863
  updateWorktreeRunState(options.worktreePath, {
3894
4864
  review: {
3895
4865
  lifetimeIterations: reviewResult.lifetimeIterations,
@@ -3948,12 +4918,12 @@ async function completeIssueRun(options) {
3948
4918
  prTitle = finalizerFromState.title;
3949
4919
  prBody = finalizerFromState.body;
3950
4920
  } else if (finalizerFromState.artifactPath) {
3951
- if (!existsSync7(finalizerFromState.artifactPath)) {
3952
- throw new Error(
3953
- `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
3954
- );
4921
+ if (!existsSync8(finalizerFromState.artifactPath)) {
4922
+ throw new FinalizerFailure({
4923
+ message: `Finalizer artifact missing at ${finalizerFromState.artifactPath}`
4924
+ });
3955
4925
  }
3956
- const artifactContent = readFileSync7(
4926
+ const artifactContent = readFileSync8(
3957
4927
  finalizerFromState.artifactPath,
3958
4928
  "utf-8"
3959
4929
  );
@@ -3961,22 +4931,24 @@ async function completeIssueRun(options) {
3961
4931
  prTitle = parsed.title;
3962
4932
  prBody = parsed.body;
3963
4933
  } else {
3964
- throw new Error(
3965
- "Finalizer state is incomplete: missing title, body, and artifactPath"
3966
- );
4934
+ throw new FinalizerFailure({
4935
+ message: "Finalizer state is incomplete: missing title, body, and artifactPath"
4936
+ });
3967
4937
  }
3968
4938
  } else {
3969
- finalizerResult = await runFinalizerAgent({
3970
- executionProvider,
3971
- config,
3972
- target,
3973
- issue,
3974
- builderBranch: branchName,
3975
- worktreePath: executionResult.worktreePath,
3976
- reviewArtifactPath,
3977
- repoRoot: ROOT,
3978
- logger
3979
- });
4939
+ finalizerResult = await runEffectAndMapExit(
4940
+ runFinalizerAgent({
4941
+ executionProvider,
4942
+ config,
4943
+ target,
4944
+ issue,
4945
+ builderBranch: branchName,
4946
+ worktreePath: executionResult.worktreePath,
4947
+ reviewArtifactPath,
4948
+ repoRoot: ROOT,
4949
+ logger
4950
+ })
4951
+ );
3980
4952
  prTitle = finalizerResult.title;
3981
4953
  prBody = finalizerResult.body;
3982
4954
  }
@@ -4086,16 +5058,18 @@ async function completeIssueRun(options) {
4086
5058
  await issueProvider.addLabels(issueNumber, [
4087
5059
  config.labels.prOpenAwaitingMerge
4088
5060
  ]);
4089
- const coordinatorResult = await runMergeCoordinator({
4090
- prProvider,
4091
- logger,
4092
- prNumber: pr.number,
4093
- targetBranch: target.baseBranch,
4094
- matchHeadCommit: pr.headRefOid,
4095
- checkWaitOptions,
4096
- autoMerge: true,
4097
- pr
4098
- });
5061
+ const coordinatorResult = await runEffectAndMapExit(
5062
+ runMergeCoordinator({
5063
+ prProvider,
5064
+ logger,
5065
+ prNumber: pr.number,
5066
+ targetBranch: target.baseBranch,
5067
+ matchHeadCommit: pr.headRefOid,
5068
+ checkWaitOptions,
5069
+ autoMerge: true,
5070
+ pr
5071
+ })
5072
+ );
4099
5073
  if (coordinatorResult.stage === "merge") {
4100
5074
  throw coordinatorResult.error;
4101
5075
  }
@@ -4266,7 +5240,10 @@ async function guardFinalCommitContent(options) {
4266
5240
  label: "git diff --cached secret guard"
4267
5241
  });
4268
5242
  assertNoSecretLikeContent([
4269
- { source: "staged diff", content: stagedDiff.stdout },
5243
+ {
5244
+ source: "staged diff",
5245
+ content: extractAddedDiffContent(stagedDiff.stdout)
5246
+ },
4270
5247
  { source: "commit title", content: title },
4271
5248
  { source: "commit body", content: body }
4272
5249
  ]);
@@ -4279,11 +5256,17 @@ async function guardFinalPublishContent(options) {
4279
5256
  label: "git diff final secret guard"
4280
5257
  });
4281
5258
  assertNoSecretLikeContent([
4282
- { source: "final diff", content: finalDiff.stdout },
5259
+ {
5260
+ source: "final diff",
5261
+ content: extractAddedDiffContent(finalDiff.stdout)
5262
+ },
4283
5263
  { source: "PR title", content: title },
4284
5264
  { source: "PR body", content: body }
4285
5265
  ]);
4286
5266
  }
5267
+ function extractAddedDiffContent(diff) {
5268
+ return diff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1)).join("\n");
5269
+ }
4287
5270
  function assertNoSecretLikeContent(inputs) {
4288
5271
  const findings = inputs.flatMap(
4289
5272
  ({ source, content }) => findSecretLikeContent(source, content ?? "")
@@ -4561,7 +5544,7 @@ async function resolveIssueWorktree(root, branchName, baseBranch, logger) {
4561
5544
  return { mode: "new", branchName, baseRef };
4562
5545
  }
4563
5546
  function issueWorktreePath(root, branchName) {
4564
- return join10(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
5547
+ return join12(root, ".sandcastle", "worktrees", branchName.replace(/\//g, "-"));
4565
5548
  }
4566
5549
  function resolveRegisteredIssueWorktreePath(worktreeListPorcelain, root, branchName) {
4567
5550
  const branchWorktreePath = parseWorktreeListPorcelain(
@@ -4589,7 +5572,7 @@ async function syncTargetBranch(root, baseBranch, logger) {
4589
5572
  }
4590
5573
  function loadBuilderPrompt(repoRoot2, promptTemplate) {
4591
5574
  const promptPath = resolvePromptTemplatePath(repoRoot2, promptTemplate);
4592
- const promptBody = existsSync7(promptPath) ? readFileSync7(promptPath, "utf-8") : promptTemplate;
5575
+ const promptBody = existsSync8(promptPath) ? readFileSync8(promptPath, "utf-8") : promptTemplate;
4593
5576
  return appendProtectedWorkGuidance(`${promptBody}
4594
5577
 
4595
5578
  ## Shared Run Context
@@ -4907,7 +5890,18 @@ async function runIssueCommand(options) {
4907
5890
  reviewArtifactPath
4908
5891
  });
4909
5892
  } catch (error) {
4910
- if (!(error instanceof HumanHandoffStop)) {
5893
+ if (error instanceof HumanHandoffStop) {
5894
+ throw error;
5895
+ }
5896
+ if (error instanceof Error && "_tag" in error) {
5897
+ await failIssueRun({
5898
+ issueProvider,
5899
+ issueNumber,
5900
+ config,
5901
+ logger,
5902
+ error: `[${error._tag}] ${error.message}`
5903
+ });
5904
+ } else {
4911
5905
  await failIssueRun({
4912
5906
  issueProvider,
4913
5907
  issueNumber,
@@ -5013,6 +6007,7 @@ async function runIssueCreateCommand(args, issueProvider, logger) {
5013
6007
 
5014
6008
  // commands/queue.ts
5015
6009
  init_common();
6010
+ import { Effect as Effect8 } from "effect";
5016
6011
 
5017
6012
  // issues/select-issue.ts
5018
6013
  init_common();
@@ -5125,6 +6120,13 @@ async function anyBlockerStillOpen(refs, getIssueState) {
5125
6120
  }
5126
6121
 
5127
6122
  // commands/queue.ts
6123
+ var QueueProviderError = class extends Error {
6124
+ _tag = "QueueProviderError";
6125
+ constructor(message) {
6126
+ super(message);
6127
+ this.name = "QueueProviderError";
6128
+ }
6129
+ };
5128
6130
  function issueDataToCandidate(issue) {
5129
6131
  return {
5130
6132
  number: issue.number,
@@ -5133,72 +6135,93 @@ function issueDataToCandidate(issue) {
5133
6135
  createdAt: issue.createdAt ? issue.createdAt.toISOString() : (/* @__PURE__ */ new Date(0)).toISOString()
5134
6136
  };
5135
6137
  }
5136
- async function selectNextQueueIssue(options) {
6138
+ function selectNextQueueIssue(options) {
5137
6139
  const { config, issueProvider, logger, prdRef } = options;
5138
6140
  logger.step("info", "Loading candidate issues from provider");
5139
- const candidates = await issueProvider.listCandidates();
5140
- if (candidates.length === 0) {
5141
- logger.step("warn", "No candidate issues found");
5142
- return {
5143
- ok: false,
5144
- reason: "No candidate issues found.",
5145
- code: "no-candidates"
5146
- };
5147
- }
5148
- logger.raw(`Found ${candidates.length} candidate(s):`);
5149
- for (const c of candidates) {
5150
- logger.raw(` #${c.number}: ${c.title} [${c.labels.join(", ")}]`);
5151
- }
5152
- const scopedCandidates = prdRef ? candidates.filter((issue) => {
5153
- const parsed = parseStackedIssue(issue.title, issue.body);
5154
- for (const warning of parsed.warnings) {
5155
- logger.raw(` #${issue.number}: ${warning}`);
6141
+ return Effect8.gen(function* () {
6142
+ const candidates = yield* Effect8.tryPromise({
6143
+ try: () => issueProvider.listCandidates(),
6144
+ catch: (e) => {
6145
+ if (e instanceof Error) {
6146
+ return new QueueProviderError(e.message);
6147
+ }
6148
+ throw e;
6149
+ }
6150
+ });
6151
+ if (candidates.length === 0) {
6152
+ logger.step("warn", "No candidate issues found");
6153
+ return {
6154
+ ok: false,
6155
+ reason: "No candidate issues found.",
6156
+ code: "no-candidates"
6157
+ };
5156
6158
  }
5157
- if (!parsed.parentRef) {
5158
- logger.raw(
5159
- ` #${issue.number}: no parent found, excluding from PRD scope`
6159
+ logger.raw(`Found ${candidates.length} candidate(s):`);
6160
+ for (const c of candidates) {
6161
+ logger.raw(` #${c.number}: ${c.title} [${c.labels.join(", ")}]`);
6162
+ }
6163
+ const scopedCandidates = prdRef ? candidates.filter((issue) => {
6164
+ const parsed = parseStackedIssue(issue.title, issue.body);
6165
+ for (const warning of parsed.warnings) {
6166
+ logger.raw(` #${issue.number}: ${warning}`);
6167
+ }
6168
+ if (!parsed.parentRef) {
6169
+ logger.raw(
6170
+ ` #${issue.number}: no parent found, excluding from PRD scope`
6171
+ );
6172
+ return false;
6173
+ }
6174
+ const matches = parsed.parentRef === prdRef;
6175
+ if (!matches) {
6176
+ logger.raw(
6177
+ ` #${issue.number}: parent ${parsed.parentRef} does not match ${prdRef}`
6178
+ );
6179
+ }
6180
+ return matches;
6181
+ }) : candidates;
6182
+ if (prdRef) {
6183
+ logger.step(
6184
+ "info",
6185
+ `PRD-scoped selection: ${scopedCandidates.length} of ${candidates.length} candidate(s) match ${prdRef}`
5160
6186
  );
5161
- return false;
5162
6187
  }
5163
- const matches = parsed.parentRef === prdRef;
5164
- if (!matches) {
5165
- logger.raw(
5166
- ` #${issue.number}: parent ${parsed.parentRef} does not match ${prdRef}`
6188
+ if (prdRef && scopedCandidates.length === 0) {
6189
+ logger.step("warn", `No candidate issues found for ${prdRef}.`);
6190
+ return {
6191
+ ok: false,
6192
+ reason: `No candidate issues found for ${prdRef}.`,
6193
+ code: "no-candidates"
6194
+ };
6195
+ }
6196
+ const candidateIssues = scopedCandidates.map(issueDataToCandidate);
6197
+ const selection = selectIssue(candidateIssues, {
6198
+ blockedLabel: config.labels.blocked,
6199
+ agentInProgressLabel: config.labels.agentInProgress
6200
+ });
6201
+ if (!selection.ok) {
6202
+ logger.step("warn", `No runnable issue: ${selection.reason}`);
6203
+ return {
6204
+ ok: false,
6205
+ reason: selection.reason,
6206
+ code: "no-runnable"
6207
+ };
6208
+ }
6209
+ const selected = candidates.find(
6210
+ (c) => c.number === selection.issue.number
6211
+ );
6212
+ if (!selected) {
6213
+ return yield* Effect8.die(
6214
+ new Error(
6215
+ `Selected issue #${selection.issue.number} not found in candidate list`
6216
+ )
5167
6217
  );
5168
6218
  }
5169
- return matches;
5170
- }) : candidates;
5171
- if (prdRef) {
5172
6219
  logger.step(
5173
6220
  "info",
5174
- `PRD-scoped selection: ${scopedCandidates.length} of ${candidates.length} candidate(s) match ${prdRef}`
6221
+ `Selected issue #${selected.number}: ${selected.title}`
5175
6222
  );
5176
- }
5177
- if (prdRef && scopedCandidates.length === 0) {
5178
- logger.step("warn", `No candidate issues found for ${prdRef}.`);
5179
- return {
5180
- ok: false,
5181
- reason: `No candidate issues found for ${prdRef}.`,
5182
- code: "no-candidates"
5183
- };
5184
- }
5185
- const candidateIssues = scopedCandidates.map(issueDataToCandidate);
5186
- const selection = selectIssue(candidateIssues, {
5187
- blockedLabel: config.labels.blocked,
5188
- agentInProgressLabel: config.labels.agentInProgress
6223
+ return { ok: true, issue: selected };
5189
6224
  });
5190
- if (!selection.ok) {
5191
- logger.step("warn", `No runnable issue: ${selection.reason}`);
5192
- return { ok: false, reason: selection.reason, code: "no-runnable" };
5193
- }
5194
- const selected = candidates.find((c) => c.number === selection.issue.number);
5195
- if (!selected) {
5196
- throw new Error(
5197
- `Selected issue #${selection.issue.number} not found in candidate list`
5198
- );
5199
- }
5200
- logger.step("info", `Selected issue #${selected.number}: ${selected.title}`);
5201
- return { ok: true, issue: selected };
5202
6225
  }
5203
6226
  function makeReconcileDeps(options) {
5204
6227
  const transitions = createIssueTransitions(
@@ -5231,17 +6254,35 @@ function makeReconcileDeps(options) {
5231
6254
  readyLabel: options.config.labels.readyForAgent
5232
6255
  };
5233
6256
  }
5234
- async function reconcileBlocked(options) {
5235
- const blocked = await options.issueProvider.listBlockedIssues();
5236
- if (blocked.length > 0) {
5237
- options.logger.step(
5238
- "info",
5239
- `Reconciling ${blocked.length} blocked issue(s)`
5240
- );
5241
- await reconcileBlockedIssues(blocked, makeReconcileDeps(options));
5242
- }
6257
+ function reconcileBlockedEffect(options) {
6258
+ return Effect8.gen(function* () {
6259
+ const blocked = yield* Effect8.tryPromise({
6260
+ try: () => options.issueProvider.listBlockedIssues(),
6261
+ catch: (e) => {
6262
+ if (e instanceof Error) {
6263
+ return new QueueProviderError(e.message);
6264
+ }
6265
+ throw e;
6266
+ }
6267
+ });
6268
+ if (blocked.length > 0) {
6269
+ options.logger.step(
6270
+ "info",
6271
+ `Reconciling ${blocked.length} blocked issue(s)`
6272
+ );
6273
+ yield* Effect8.tryPromise({
6274
+ try: () => reconcileBlockedIssues(blocked, makeReconcileDeps(options)),
6275
+ catch: (e) => {
6276
+ if (e instanceof Error) {
6277
+ return new QueueProviderError(e.message);
6278
+ }
6279
+ throw e;
6280
+ }
6281
+ });
6282
+ }
6283
+ });
5243
6284
  }
5244
- async function runOneQueueIssue(options) {
6285
+ function runOneQueueIssueEffect(options) {
5245
6286
  const {
5246
6287
  targetName,
5247
6288
  config,
@@ -5253,47 +6294,57 @@ async function runOneQueueIssue(options) {
5253
6294
  repoRoot: repoRoot2,
5254
6295
  prdRef
5255
6296
  } = options;
5256
- const outcome = await selectNextQueueIssue({
5257
- config,
5258
- issueProvider,
5259
- logger,
5260
- prdRef
5261
- });
5262
- if (!outcome.ok) {
5263
- return { selected: null, reason: outcome.reason, code: outcome.code };
5264
- }
5265
- const { issue: selected } = outcome;
5266
- const runResult = await runIssueCommand({
5267
- issueNumber: selected.number,
5268
- targetName,
5269
- config,
5270
- issueProvider,
5271
- prProvider,
5272
- executionProvider,
5273
- force,
5274
- logger,
5275
- repoRoot: repoRoot2
6297
+ return Effect8.gen(function* () {
6298
+ const outcome = yield* selectNextQueueIssue({
6299
+ config,
6300
+ issueProvider,
6301
+ logger,
6302
+ prdRef
6303
+ });
6304
+ if (!outcome.ok) {
6305
+ return {
6306
+ selected: null,
6307
+ reason: outcome.reason,
6308
+ code: outcome.code
6309
+ };
6310
+ }
6311
+ const { issue: selected } = outcome;
6312
+ const runResult = yield* Effect8.tryPromise({
6313
+ try: () => runIssueCommand({
6314
+ issueNumber: selected.number,
6315
+ targetName,
6316
+ config,
6317
+ issueProvider,
6318
+ prProvider,
6319
+ executionProvider,
6320
+ force,
6321
+ logger,
6322
+ repoRoot: repoRoot2
6323
+ }),
6324
+ catch: (e) => {
6325
+ throw e;
6326
+ }
6327
+ });
6328
+ logger.raw("Issue completed successfully:");
6329
+ logger.raw(` Branch: ${runResult.branchName}`);
6330
+ if (runResult.noOp) {
6331
+ logger.raw(" Status: no-op (closed without PR)");
6332
+ } else {
6333
+ logger.raw(` PR Title: ${runResult.prTitle}`);
6334
+ logger.raw(` PR Number: ${runResult.prNumber}`);
6335
+ logger.raw(` PR URL: ${runResult.prUrl}`);
6336
+ }
6337
+ logger.raw(` Target: ${runResult.target.name}`);
6338
+ return { selected, runResult };
5276
6339
  });
5277
- logger.raw("Issue completed successfully:");
5278
- logger.raw(` Branch: ${runResult.branchName}`);
5279
- if (runResult.noOp) {
5280
- logger.raw(" Status: no-op (closed without PR)");
5281
- } else {
5282
- logger.raw(` PR Title: ${runResult.prTitle}`);
5283
- logger.raw(` PR Number: ${runResult.prNumber}`);
5284
- logger.raw(` PR URL: ${runResult.prUrl}`);
5285
- }
5286
- logger.raw(` Target: ${runResult.target.name}`);
5287
- return { selected, runResult };
5288
6340
  }
5289
- async function runQueue(options) {
5290
- return runOneQueueIssue(options);
6341
+ function runQueue(options) {
6342
+ return runOneQueueIssueEffect(options);
5291
6343
  }
5292
- async function runQueueLoop(options) {
5293
- const results = [];
5294
- while (true) {
5295
- await reconcileBlocked(options);
5296
- const outcome = await runOneQueueIssue(options);
6344
+ function runQueueLoopEffect(options, results) {
6345
+ return Effect8.gen(function* () {
6346
+ yield* reconcileBlockedEffect(options);
6347
+ const outcome = yield* runOneQueueIssueEffect(options);
5297
6348
  if (outcome.selected === null) {
5298
6349
  return {
5299
6350
  drained: true,
@@ -5304,18 +6355,29 @@ async function runQueueLoop(options) {
5304
6355
  code: "drained"
5305
6356
  };
5306
6357
  }
5307
- results.push(outcome);
5308
- const processedIssue = await options.issueProvider.fetchIssue(
5309
- outcome.selected.number
5310
- );
6358
+ const newResults = [...results, outcome];
6359
+ const processedIssue = yield* Effect8.tryPromise({
6360
+ try: () => options.issueProvider.fetchIssue(outcome.selected.number),
6361
+ catch: (e) => {
6362
+ if (e instanceof Error) {
6363
+ return new QueueProviderError(e.message);
6364
+ }
6365
+ throw e;
6366
+ }
6367
+ });
5311
6368
  if (processedIssue.state === "closed") {
5312
- await reconcileBlocked(options);
6369
+ yield* reconcileBlockedEffect(options);
5313
6370
  }
5314
- }
6371
+ return yield* runQueueLoopEffect(options, newResults);
6372
+ });
6373
+ }
6374
+ function runQueueLoop(options) {
6375
+ return runQueueLoopEffect(options, []);
5315
6376
  }
5316
6377
 
5317
6378
  // commands/queue-run.ts
5318
6379
  async function runQueueCommand(options) {
6380
+ initializeEffectRuntime();
5319
6381
  const queueOptions = {
5320
6382
  targetName: options.targetName,
5321
6383
  config: options.config,
@@ -5328,9 +6390,9 @@ async function runQueueCommand(options) {
5328
6390
  prdRef: options.prdRef
5329
6391
  };
5330
6392
  if (!options.loop) {
5331
- return runQueue(queueOptions);
6393
+ return runEffectAndMapExit(runQueue(queueOptions));
5332
6394
  }
5333
- return runQueueLoop(queueOptions);
6395
+ return runEffectAndMapExit(runQueueLoop(queueOptions));
5334
6396
  }
5335
6397
 
5336
6398
  // commands/pr-create.ts
@@ -5700,16 +6762,18 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
5700
6762
  matchHeadCommit: pr.headRefOid
5701
6763
  });
5702
6764
  } else {
5703
- const coordinatorResult = await runMergeCoordinator({
5704
- prProvider,
5705
- logger: ownLogger,
5706
- prNumber: pr.number,
5707
- targetBranch: pr.baseRefName,
5708
- matchHeadCommit: pr.headRefOid,
5709
- checkWaitOptions,
5710
- method: options.method,
5711
- waitForTargetGreen: options.targetGreen
5712
- });
6765
+ const coordinatorResult = await runEffectAndMapExit(
6766
+ runMergeCoordinator({
6767
+ prProvider,
6768
+ logger: ownLogger,
6769
+ prNumber: pr.number,
6770
+ targetBranch: pr.baseRefName,
6771
+ matchHeadCommit: pr.headRefOid,
6772
+ checkWaitOptions,
6773
+ method: options.method,
6774
+ waitForTargetGreen: options.targetGreen
6775
+ })
6776
+ );
5713
6777
  if (coordinatorResult.stage !== "completed") {
5714
6778
  throw coordinatorResult.error;
5715
6779
  }
@@ -5732,105 +6796,21 @@ async function runPrMergeCommand(args, logger, prProvider, config) {
5732
6796
  }
5733
6797
 
5734
6798
  // commands/init.ts
5735
- import { existsSync as existsSync8, statSync } from "fs";
6799
+ init_github_client();
6800
+ import { existsSync as existsSync9, statSync } from "fs";
5736
6801
  import {
5737
6802
  copyFile,
5738
6803
  mkdir as mkdir4,
5739
6804
  readFile as readFile4,
5740
6805
  readdir,
5741
6806
  rename,
5742
- writeFile
6807
+ writeFile as writeFile2
5743
6808
  } from "fs/promises";
5744
- import { createHash, randomUUID } from "crypto";
5745
- import path5 from "path";
6809
+ import { createHash as createHash2, randomUUID } from "crypto";
6810
+ import path7 from "path";
5746
6811
  import { execFile as execFile2 } from "child_process";
5747
6812
  import { promisify as promisify2 } from "util";
5748
6813
  import { confirm, isCancel, log, select, text } from "@clack/prompts";
5749
-
5750
- // providers/github-client.ts
5751
- import { Octokit } from "octokit";
5752
- var REMOTE_PATTERN = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/;
5753
- function resolveGitHubToken(env) {
5754
- const token = env.POURKIT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
5755
- if (!token) {
5756
- throw new Error(
5757
- "GitHub token is required. Set POURKIT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN."
5758
- );
5759
- }
5760
- return token;
5761
- }
5762
- async function resolveGitHubRepository(options) {
5763
- if (options?.repository) {
5764
- const parts = options.repository.split("/");
5765
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
5766
- throw new Error(
5767
- `Invalid repository format: "${options.repository}". Expected "owner/repo".`
5768
- );
5769
- }
5770
- return { owner: parts[0], repo: parts[1] };
5771
- }
5772
- const env = options?.env ?? process.env;
5773
- const envRepo = env.GITHUB_REPOSITORY;
5774
- if (envRepo) {
5775
- const parts = envRepo.split("/");
5776
- if (parts.length === 2 && parts[0] && parts[1]) {
5777
- return { owner: parts[0], repo: parts[1] };
5778
- }
5779
- throw new Error(
5780
- `Invalid repository format: "${envRepo}". Expected "owner/repo".`
5781
- );
5782
- }
5783
- const { execCapture: execCapture2 } = await Promise.resolve().then(() => (init_common(), common_exports));
5784
- const cwd = options?.cwd;
5785
- try {
5786
- const result = await execCapture2("git", ["remote", "get-url", "origin"], {
5787
- cwd
5788
- });
5789
- const remote = result.stdout.trim();
5790
- const match = remote.match(REMOTE_PATTERN);
5791
- if (match) {
5792
- return { owner: match[1], repo: match[2] };
5793
- }
5794
- } catch {
5795
- }
5796
- throw new Error(
5797
- "Could not resolve GitHub repository. Set GITHUB_REPOSITORY env var or ensure a valid 'origin' remote exists."
5798
- );
5799
- }
5800
- async function requireGitHubClient(options) {
5801
- const env = options?.env ?? process.env;
5802
- const token = resolveGitHubToken(env);
5803
- const repo = await resolveGitHubRepository(options);
5804
- const octokit = new Octokit({ auth: token });
5805
- return { octokit, ...repo };
5806
- }
5807
- async function tryCreateGitHubClient(options) {
5808
- const env = options?.env ?? process.env;
5809
- let token;
5810
- try {
5811
- token = resolveGitHubToken(env);
5812
- } catch {
5813
- return {
5814
- ok: false,
5815
- reason: "missing-token",
5816
- message: "GitHub token is not configured."
5817
- };
5818
- }
5819
- let repo;
5820
- try {
5821
- repo = await resolveGitHubRepository(options);
5822
- } catch (e) {
5823
- const message = e instanceof Error ? e.message : String(e);
5824
- if (message.includes("Invalid repository format")) {
5825
- return { ok: false, reason: "invalid-repository", message };
5826
- }
5827
- return { ok: false, reason: "missing-repository", message };
5828
- }
5829
- const octokit = new Octokit({ auth: token });
5830
- return { ok: true, client: { octokit, ...repo } };
5831
- }
5832
-
5833
- // commands/init.ts
5834
6814
  var execFileAsync2 = promisify2(execFile2);
5835
6815
  var NO_TOKEN_LABEL_PROVISIONING_WARNING = "Skipped GitHub label provisioning because no GitHub token was provided.";
5836
6816
  var ALLOWED_VERIFICATION_SCRIPTS = [
@@ -5967,7 +6947,7 @@ function generateConfigTemplate(options) {
5967
6947
  labels: maybeLabels
5968
6948
  } = options;
5969
6949
  const labels = maybeLabels ?? DEFAULT_RUNNER_LABELS;
5970
- const relPath = path5.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
6950
+ const relPath = path7.relative(targetRoot, sourceRoot).replace(/\\/g, "/");
5971
6951
  const importPath = relPath || ".";
5972
6952
  const setupCommand = `${packageManager} install`;
5973
6953
  let setupSection;
@@ -6215,7 +7195,7 @@ _Avoid_: Alternative terms
6215
7195
  `;
6216
7196
  }
6217
7197
  async function generateManagedAgentInstructions(options) {
6218
- const sourcePath = path5.join(options.sourceRoot, "AGENTS.md");
7198
+ const sourcePath = path7.join(options.sourceRoot, "AGENTS.md");
6219
7199
  try {
6220
7200
  return await readFile4(sourcePath, "utf-8");
6221
7201
  } catch {
@@ -6245,7 +7225,7 @@ async function walkDir(dir) {
6245
7225
  const files = [];
6246
7226
  const entries = await readdir(dir, { withFileTypes: true });
6247
7227
  for (const entry of entries) {
6248
- const full = path5.join(dir, entry.name);
7228
+ const full = path7.join(dir, entry.name);
6249
7229
  if (entry.isDirectory()) {
6250
7230
  files.push(...await walkDir(full));
6251
7231
  } else {
@@ -6256,10 +7236,10 @@ async function walkDir(dir) {
6256
7236
  }
6257
7237
  async function computeFileChecksum(filePath) {
6258
7238
  const content = await readFile4(filePath);
6259
- return createHash("sha256").update(content).digest("hex");
7239
+ return createHash2("sha256").update(content).digest("hex");
6260
7240
  }
6261
7241
  function lockfileExists(root, name) {
6262
- return existsSync8(path5.join(root, name));
7242
+ return existsSync9(path7.join(root, name));
6263
7243
  }
6264
7244
  function detectPackageManager(root) {
6265
7245
  if (lockfileExists(root, "pnpm-lock.yaml")) return "pnpm";
@@ -6302,8 +7282,8 @@ async function discoverLocalSource(sourcePath) {
6302
7282
  }
6303
7283
  async function discoverReadme(root) {
6304
7284
  for (const name of ["README.md", "readme.md"]) {
6305
- const p = path5.join(root, name);
6306
- if (existsSync8(p)) {
7285
+ const p = path7.join(root, name);
7286
+ if (existsSync9(p)) {
6307
7287
  return p;
6308
7288
  }
6309
7289
  }
@@ -6312,25 +7292,25 @@ async function discoverReadme(root) {
6312
7292
  async function discoverAgentFiles(root) {
6313
7293
  const files = [];
6314
7294
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
6315
- const p = path5.join(root, name);
6316
- if (existsSync8(p)) {
7295
+ const p = path7.join(root, name);
7296
+ if (existsSync9(p)) {
6317
7297
  files.push(p);
6318
7298
  }
6319
7299
  }
6320
7300
  return files;
6321
7301
  }
6322
7302
  async function discoverMerlleState(root) {
6323
- const p = path5.join(root, ".pourkit", "state.json");
6324
- return existsSync8(p) ? p : null;
7303
+ const p = path7.join(root, ".pourkit", "state.json");
7304
+ return existsSync9(p) ? p : null;
6325
7305
  }
6326
7306
  async function discoverAgentSkills(root) {
6327
7307
  const dirs = [
6328
- path5.join(root, ".agents", "skills"),
6329
- path5.join(root, ".opencode", "skills")
7308
+ path7.join(root, ".agents", "skills"),
7309
+ path7.join(root, ".opencode", "skills")
6330
7310
  ];
6331
7311
  const found = [];
6332
7312
  for (const d of dirs) {
6333
- if (existsSync8(d)) {
7313
+ if (existsSync9(d)) {
6334
7314
  found.push(d);
6335
7315
  }
6336
7316
  }
@@ -6339,17 +7319,17 @@ async function discoverAgentSkills(root) {
6339
7319
  async function discoverRootDomainDocs(root) {
6340
7320
  const docs = [];
6341
7321
  for (const name of ["CONTEXT.md", "CONTEXT-MAP.md"]) {
6342
- const p = path5.join(root, name);
6343
- if (existsSync8(p)) {
7322
+ const p = path7.join(root, name);
7323
+ if (existsSync9(p)) {
6344
7324
  docs.push(p);
6345
7325
  }
6346
7326
  }
6347
- const adrDir = path5.join(root, "docs", "adr");
6348
- if (existsSync8(adrDir)) {
7327
+ const adrDir = path7.join(root, "docs", "adr");
7328
+ if (existsSync9(adrDir)) {
6349
7329
  const entries = await readdir(adrDir, { withFileTypes: true });
6350
7330
  for (const entry of entries) {
6351
7331
  if (entry.isFile() && entry.name.endsWith(".md")) {
6352
- docs.push(path5.join(adrDir, entry.name));
7332
+ docs.push(path7.join(adrDir, entry.name));
6353
7333
  }
6354
7334
  }
6355
7335
  }
@@ -6437,7 +7417,7 @@ async function planInit(options) {
6437
7417
  for (const f of agentFiles) {
6438
7418
  if (sourceRoot) {
6439
7419
  const agentFileMode = options.conflictPolicy?.agentFile ?? "both";
6440
- const basename = path5.basename(f);
7420
+ const basename = path7.basename(f);
6441
7421
  if (agentFileMode === "skip" || agentFileMode === "agents" && basename !== "AGENTS.md" || agentFileMode === "claude" && basename !== "CLAUDE.md") {
6442
7422
  operations.push({
6443
7423
  kind: "skip",
@@ -6463,7 +7443,7 @@ async function planInit(options) {
6463
7443
  kind: "skip",
6464
7444
  path: f,
6465
7445
  ownership: "project-owned",
6466
- reason: `Existing agent file: ${path5.basename(f)}`,
7446
+ reason: `Existing agent file: ${path7.basename(f)}`,
6467
7447
  requiresConfirmation: false,
6468
7448
  destructive: false
6469
7449
  });
@@ -6486,15 +7466,15 @@ async function planInit(options) {
6486
7466
  if (s.includes(".opencode") && options.legacySkills) {
6487
7467
  const skillFiles = await walkDir(s);
6488
7468
  for (const file of skillFiles) {
6489
- const relPath = path5.relative(s, file);
6490
- const destPath = path5.join(targetRoot, ".agents", "skills", relPath);
6491
- if (!existsSync8(destPath)) {
7469
+ const relPath = path7.relative(s, file);
7470
+ const destPath = path7.join(targetRoot, ".agents", "skills", relPath);
7471
+ if (!existsSync9(destPath)) {
6492
7472
  operations.push({
6493
7473
  kind: "copy",
6494
7474
  sourcePath: file,
6495
7475
  path: destPath,
6496
7476
  ownership: "project-owned",
6497
- reason: `Migrate legacy skill: ${path5.join(".opencode/skills", relPath)}`,
7477
+ reason: `Migrate legacy skill: ${path7.join(".opencode/skills", relPath)}`,
6498
7478
  requiresConfirmation: false,
6499
7479
  destructive: false
6500
7480
  });
@@ -6551,7 +7531,7 @@ async function planInit(options) {
6551
7531
  });
6552
7532
  }
6553
7533
  if (sourceRoot) {
6554
- if (!existsSync8(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
7534
+ if (!existsSync9(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
6555
7535
  warnings.push(
6556
7536
  `--from-local path does not exist or is not a directory: ${sourceRoot}`
6557
7537
  );
@@ -6612,17 +7592,17 @@ async function planInit(options) {
6612
7592
  continue;
6613
7593
  }
6614
7594
  const targetDirName = ".agents/skills";
6615
- const targetPath = path5.join(targetRoot, targetDirName);
7595
+ const targetPath = path7.join(targetRoot, targetDirName);
6616
7596
  const skillFiles = await walkDir(s);
6617
7597
  for (const file of skillFiles) {
6618
- const relPath = path5.relative(s, file);
6619
- const destPath = path5.join(targetPath, relPath);
7598
+ const relPath = path7.relative(s, file);
7599
+ const destPath = path7.join(targetPath, relPath);
6620
7600
  if (plannedSkillDests.has(destPath)) {
6621
7601
  operations.push({
6622
7602
  kind: "skip",
6623
7603
  path: destPath,
6624
7604
  ownership: "project-owned",
6625
- reason: `Skill destination conflict, skipping source copy: ${path5.join(targetDirName, relPath)}`,
7605
+ reason: `Skill destination conflict, skipping source copy: ${path7.join(targetDirName, relPath)}`,
6626
7606
  requiresConfirmation: false,
6627
7607
  destructive: false,
6628
7608
  conflict: "destination already planned"
@@ -6635,7 +7615,7 @@ async function planInit(options) {
6635
7615
  sourcePath: file,
6636
7616
  path: destPath,
6637
7617
  ownership: "copied-customizable",
6638
- reason: `Copy skill: ${path5.join(targetDirName, relPath)}`,
7618
+ reason: `Copy skill: ${path7.join(targetDirName, relPath)}`,
6639
7619
  requiresConfirmation: false,
6640
7620
  destructive: false,
6641
7621
  checksum
@@ -6647,7 +7627,7 @@ async function planInit(options) {
6647
7627
  operations.push({
6648
7628
  kind: "copy",
6649
7629
  sourcePath: srcReadme,
6650
- path: path5.join(targetRoot, "README.md"),
7630
+ path: path7.join(targetRoot, "README.md"),
6651
7631
  ownership: "project-owned",
6652
7632
  reason: "Copy README.md from source",
6653
7633
  requiresConfirmation: false,
@@ -6658,8 +7638,8 @@ async function planInit(options) {
6658
7638
  const rootDocs = await discoverRootDomainDocs(targetRoot);
6659
7639
  const merleDestPaths = /* @__PURE__ */ new Set();
6660
7640
  for (const docPath of rootDocs) {
6661
- const relPath = path5.relative(targetRoot, docPath);
6662
- const destPath = path5.join(targetRoot, ".pourkit", relPath);
7641
+ const relPath = path7.relative(targetRoot, docPath);
7642
+ const destPath = path7.join(targetRoot, ".pourkit", relPath);
6663
7643
  merleDestPaths.add(destPath);
6664
7644
  if (docsMigration === "skip") {
6665
7645
  operations.push({
@@ -6670,7 +7650,7 @@ async function planInit(options) {
6670
7650
  requiresConfirmation: false,
6671
7651
  destructive: false
6672
7652
  });
6673
- } else if (existsSync8(destPath)) {
7653
+ } else if (existsSync9(destPath)) {
6674
7654
  operations.push({
6675
7655
  kind: "skip",
6676
7656
  path: destPath,
@@ -6706,7 +7686,7 @@ async function planInit(options) {
6706
7686
  let hasPackageJson = true;
6707
7687
  try {
6708
7688
  const pkgContent = await readFile4(
6709
- path5.join(targetRoot, "package.json"),
7689
+ path7.join(targetRoot, "package.json"),
6710
7690
  "utf-8"
6711
7691
  );
6712
7692
  const pkg = JSON.parse(pkgContent);
@@ -6727,8 +7707,8 @@ async function planInit(options) {
6727
7707
  } catch {
6728
7708
  }
6729
7709
  }
6730
- const contextPath = path5.join(targetRoot, ".pourkit", "CONTEXT.md");
6731
- if (!existsSync8(contextPath) && !merleDestPaths.has(contextPath)) {
7710
+ const contextPath = path7.join(targetRoot, ".pourkit", "CONTEXT.md");
7711
+ if (!existsSync9(contextPath) && !merleDestPaths.has(contextPath)) {
6732
7712
  operations.push({
6733
7713
  kind: "create",
6734
7714
  path: contextPath,
@@ -6739,14 +7719,14 @@ async function planInit(options) {
6739
7719
  content: generateContextScaffold()
6740
7720
  });
6741
7721
  }
6742
- const adrGitkeep = path5.join(
7722
+ const adrGitkeep = path7.join(
6743
7723
  targetRoot,
6744
7724
  ".pourkit",
6745
7725
  "docs",
6746
7726
  "adr",
6747
7727
  ".gitkeep"
6748
7728
  );
6749
- if (!existsSync8(adrGitkeep)) {
7729
+ if (!existsSync9(adrGitkeep)) {
6750
7730
  operations.push({
6751
7731
  kind: "create",
6752
7732
  path: adrGitkeep,
@@ -6756,16 +7736,16 @@ async function planInit(options) {
6756
7736
  destructive: false
6757
7737
  });
6758
7738
  }
6759
- const srcDocAgents = path5.join(sourceRoot, ".pourkit", "docs", "agents");
6760
- const tgtDocAgents = path5.join(targetRoot, ".pourkit", "docs", "agents");
6761
- if (existsSync8(srcDocAgents) && !existsSync8(tgtDocAgents)) {
7739
+ const srcDocAgents = path7.join(sourceRoot, ".pourkit", "docs", "agents");
7740
+ const tgtDocAgents = path7.join(targetRoot, ".pourkit", "docs", "agents");
7741
+ if (existsSync9(srcDocAgents) && !existsSync9(tgtDocAgents)) {
6762
7742
  const docFiles = await walkDir(srcDocAgents);
6763
7743
  for (const file of docFiles) {
6764
- const relPath = path5.relative(srcDocAgents, file);
7744
+ const relPath = path7.relative(srcDocAgents, file);
6765
7745
  if (relPath === "triage-labels.md") {
6766
7746
  operations.push({
6767
7747
  kind: "create",
6768
- path: path5.join(tgtDocAgents, relPath),
7748
+ path: path7.join(tgtDocAgents, relPath),
6769
7749
  ownership: "managed",
6770
7750
  reason: "Init triage labels doc",
6771
7751
  requiresConfirmation: false,
@@ -6780,7 +7760,7 @@ async function planInit(options) {
6780
7760
  operations.push({
6781
7761
  kind: "copy",
6782
7762
  sourcePath: file,
6783
- path: path5.join(tgtDocAgents, relPath),
7763
+ path: path7.join(tgtDocAgents, relPath),
6784
7764
  ownership: "managed",
6785
7765
  reason: `Copy agent doc: ${relPath}`,
6786
7766
  requiresConfirmation: false,
@@ -6789,17 +7769,17 @@ async function planInit(options) {
6789
7769
  });
6790
7770
  }
6791
7771
  }
6792
- const srcPrompts = path5.join(sourceRoot, ".pourkit", "prompts");
6793
- const tgtPrompts = path5.join(targetRoot, ".pourkit", "prompts");
6794
- if (existsSync8(srcPrompts) && !existsSync8(tgtPrompts)) {
7772
+ const srcPrompts = path7.join(sourceRoot, ".pourkit", "prompts");
7773
+ const tgtPrompts = path7.join(targetRoot, ".pourkit", "prompts");
7774
+ if (existsSync9(srcPrompts) && !existsSync9(tgtPrompts)) {
6795
7775
  const promptFiles = await walkDir(srcPrompts);
6796
7776
  for (const file of promptFiles) {
6797
- const relPath = path5.relative(srcPrompts, file);
7777
+ const relPath = path7.relative(srcPrompts, file);
6798
7778
  const checksum = await computeFileChecksum(file);
6799
7779
  operations.push({
6800
7780
  kind: "copy",
6801
7781
  sourcePath: file,
6802
- path: path5.join(tgtPrompts, relPath),
7782
+ path: path7.join(tgtPrompts, relPath),
6803
7783
  ownership: "managed",
6804
7784
  reason: `Copy prompt: ${relPath}`,
6805
7785
  requiresConfirmation: false,
@@ -6808,17 +7788,17 @@ async function planInit(options) {
6808
7788
  });
6809
7789
  }
6810
7790
  }
6811
- const srcSandboxDockerfile = path5.join(
7791
+ const srcSandboxDockerfile = path7.join(
6812
7792
  sourceRoot,
6813
7793
  ".sandcastle",
6814
7794
  "Dockerfile"
6815
7795
  );
6816
- const tgtSandboxDockerfile = path5.join(
7796
+ const tgtSandboxDockerfile = path7.join(
6817
7797
  targetRoot,
6818
7798
  ".sandcastle",
6819
7799
  "Dockerfile"
6820
7800
  );
6821
- if (existsSync8(tgtSandboxDockerfile)) {
7801
+ if (existsSync9(tgtSandboxDockerfile)) {
6822
7802
  operations.push({
6823
7803
  kind: "skip",
6824
7804
  path: tgtSandboxDockerfile,
@@ -6827,7 +7807,7 @@ async function planInit(options) {
6827
7807
  requiresConfirmation: false,
6828
7808
  destructive: false
6829
7809
  });
6830
- } else if (existsSync8(srcSandboxDockerfile)) {
7810
+ } else if (existsSync9(srcSandboxDockerfile)) {
6831
7811
  const checksum = await computeFileChecksum(srcSandboxDockerfile);
6832
7812
  operations.push({
6833
7813
  kind: "copy",
@@ -6840,8 +7820,8 @@ async function planInit(options) {
6840
7820
  checksum
6841
7821
  });
6842
7822
  }
6843
- const configTsPath = path5.join(targetRoot, "pourkit.config.ts");
6844
- if (!existsSync8(configTsPath)) {
7823
+ const configTsPath = path7.join(targetRoot, "pourkit.config.ts");
7824
+ if (!existsSync9(configTsPath)) {
6845
7825
  const verifyCommands = inferVerificationCommands(
6846
7826
  packageScripts,
6847
7827
  pm || "npm"
@@ -6878,10 +7858,10 @@ async function planInit(options) {
6878
7858
  const hasExistingAgents = operations.some(
6879
7859
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("AGENTS.md")
6880
7860
  );
6881
- if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync8(path5.join(targetRoot, "AGENTS.md"))) {
7861
+ if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync9(path7.join(targetRoot, "AGENTS.md"))) {
6882
7862
  operations.push({
6883
7863
  kind: "create",
6884
- path: path5.join(targetRoot, "AGENTS.md"),
7864
+ path: path7.join(targetRoot, "AGENTS.md"),
6885
7865
  ownership: "managed",
6886
7866
  reason: "Init AGENTS.md with Pourkit managed block",
6887
7867
  requiresConfirmation: false,
@@ -6894,10 +7874,10 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
6894
7874
  const hasExistingClaude = operations.some(
6895
7875
  (op) => (op.kind === "skip" || op.kind === "update") && op.path?.endsWith("CLAUDE.md")
6896
7876
  );
6897
- if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync8(path5.join(targetRoot, "CLAUDE.md"))) {
7877
+ if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync9(path7.join(targetRoot, "CLAUDE.md"))) {
6898
7878
  operations.push({
6899
7879
  kind: "create",
6900
- path: path5.join(targetRoot, "CLAUDE.md"),
7880
+ path: path7.join(targetRoot, "CLAUDE.md"),
6901
7881
  ownership: "managed",
6902
7882
  reason: "Init CLAUDE.md with Pourkit managed block",
6903
7883
  requiresConfirmation: false,
@@ -6907,9 +7887,9 @@ ${managedAgentContent}${MANAGED_BLOCK_END}
6907
7887
  `
6908
7888
  });
6909
7889
  }
6910
- const gitignoreTarget = path5.join(targetRoot, ".gitignore");
7890
+ const gitignoreTarget = path7.join(targetRoot, ".gitignore");
6911
7891
  const gitignoreContent = generateGitignoreBlock();
6912
- if (!existsSync8(gitignoreTarget)) {
7892
+ if (!existsSync9(gitignoreTarget)) {
6913
7893
  operations.push({
6914
7894
  kind: "create",
6915
7895
  path: gitignoreTarget,
@@ -6932,8 +7912,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
6932
7912
  content: gitignoreContent
6933
7913
  });
6934
7914
  }
6935
- const openCodePath = path5.join(targetRoot, "opencode.json");
6936
- if (!existsSync8(openCodePath)) {
7915
+ const openCodePath = path7.join(targetRoot, "opencode.json");
7916
+ if (!existsSync9(openCodePath)) {
6937
7917
  operations.push({
6938
7918
  kind: "create",
6939
7919
  path: openCodePath,
@@ -6989,8 +7969,8 @@ ${gitignoreContent}${MANAGED_BLOCK_END}
6989
7969
  });
6990
7970
  }
6991
7971
  }
6992
- const manifestPath = path5.join(targetRoot, ".pourkit", "manifest.json");
6993
- if (existsSync8(manifestPath)) {
7972
+ const manifestPath = path7.join(targetRoot, ".pourkit", "manifest.json");
7973
+ if (existsSync9(manifestPath)) {
6994
7974
  operations.push({
6995
7975
  kind: "skip",
6996
7976
  path: manifestPath,
@@ -7300,7 +8280,7 @@ async function promptForInitChoices(plan, current) {
7300
8280
  }
7301
8281
  async function writeFileAtomic(filePath, content) {
7302
8282
  const tmpPath = `${filePath}.tmp.${randomUUID()}`;
7303
- await writeFile(tmpPath, content, "utf-8");
8283
+ await writeFile2(tmpPath, content, "utf-8");
7304
8284
  await rename(tmpPath, filePath);
7305
8285
  }
7306
8286
  var MANAGED_BLOCK_BEGIN = "<!-- BEGIN POURKIT MANAGED BLOCK -->";
@@ -7309,8 +8289,8 @@ async function updateManagedBlock(filePath, content) {
7309
8289
  const blockContent = `${MANAGED_BLOCK_BEGIN}
7310
8290
  ${content}${MANAGED_BLOCK_END}
7311
8291
  `;
7312
- if (!existsSync8(filePath)) {
7313
- const dir = path5.dirname(filePath);
8292
+ if (!existsSync9(filePath)) {
8293
+ const dir = path7.dirname(filePath);
7314
8294
  await mkdir4(dir, { recursive: true });
7315
8295
  await writeFileAtomic(filePath, blockContent);
7316
8296
  return;
@@ -7328,17 +8308,17 @@ ${content}${MANAGED_BLOCK_END}
7328
8308
  }
7329
8309
  }
7330
8310
  async function writeManifest(plan, sourceMeta, agentFiles, packageManager) {
7331
- const manifestDir = path5.join(plan.targetRoot, ".pourkit");
7332
- const manifestPath = path5.join(manifestDir, "manifest.json");
8311
+ const manifestDir = path7.join(plan.targetRoot, ".pourkit");
8312
+ const manifestPath = path7.join(manifestDir, "manifest.json");
7333
8313
  const assets = {};
7334
8314
  for (const op of plan.operations) {
7335
8315
  if (!op.path) continue;
7336
8316
  if (op.kind !== "create" && op.kind !== "copy" && op.kind !== "update" && op.kind !== "move")
7337
8317
  continue;
7338
8318
  if (op.requiresConfirmation) continue;
7339
- const relPath = path5.relative(plan.targetRoot, op.path);
8319
+ const relPath = path7.relative(plan.targetRoot, op.path);
7340
8320
  if (relPath === ".pourkit/manifest.json") continue;
7341
- if (existsSync8(op.path)) {
8321
+ if (existsSync9(op.path)) {
7342
8322
  const sha256 = await computeFileChecksum(op.path);
7343
8323
  assets[relPath] = {
7344
8324
  ownership: op.ownership || "managed",
@@ -7383,11 +8363,11 @@ async function applyInitPlan(plan, options) {
7383
8363
  skipped++;
7384
8364
  continue;
7385
8365
  }
7386
- if (existsSync8(op.path) && !op.destructive) {
8366
+ if (existsSync9(op.path) && !op.destructive) {
7387
8367
  skipped++;
7388
8368
  continue;
7389
8369
  }
7390
- const dir = path5.dirname(op.path);
8370
+ const dir = path7.dirname(op.path);
7391
8371
  await mkdir4(dir, { recursive: true });
7392
8372
  await writeFileAtomic(op.path, op.content ?? "");
7393
8373
  applied++;
@@ -7398,7 +8378,7 @@ async function applyInitPlan(plan, options) {
7398
8378
  skipped++;
7399
8379
  continue;
7400
8380
  }
7401
- if (existsSync8(op.path)) {
8381
+ if (existsSync9(op.path)) {
7402
8382
  skipped++;
7403
8383
  continue;
7404
8384
  }
@@ -7406,7 +8386,7 @@ async function applyInitPlan(plan, options) {
7406
8386
  if (srcStat.isDirectory()) {
7407
8387
  skipped++;
7408
8388
  } else {
7409
- const dir = path5.dirname(op.path);
8389
+ const dir = path7.dirname(op.path);
7410
8390
  await mkdir4(dir, { recursive: true });
7411
8391
  await copyFile(op.sourcePath, op.path);
7412
8392
  applied++;
@@ -7427,11 +8407,11 @@ async function applyInitPlan(plan, options) {
7427
8407
  skipped++;
7428
8408
  continue;
7429
8409
  }
7430
- if (existsSync8(op.path)) {
8410
+ if (existsSync9(op.path)) {
7431
8411
  skipped++;
7432
8412
  continue;
7433
8413
  }
7434
- const dir = path5.dirname(op.path);
8414
+ const dir = path7.dirname(op.path);
7435
8415
  await mkdir4(dir, { recursive: true });
7436
8416
  await rename(op.sourcePath, op.path);
7437
8417
  applied++;
@@ -7562,13 +8542,13 @@ async function applyInitFromSource(options) {
7562
8542
  let manifestWritten = false;
7563
8543
  if (result.errors.length === 0) {
7564
8544
  const manifestSkipped = plan.operations.some(
7565
- (op) => op.kind === "skip" && op.path === path5.join(targetRoot, ".pourkit", "manifest.json")
8545
+ (op) => op.kind === "skip" && op.path === path7.join(targetRoot, ".pourkit", "manifest.json")
7566
8546
  );
7567
8547
  if (!manifestSkipped) {
7568
8548
  const agentFiles = [];
7569
8549
  for (const name of ["AGENTS.md", "CLAUDE.md"]) {
7570
- if (existsSync8(path5.join(targetRoot, name))) {
7571
- agentFiles.push(path5.join(targetRoot, name));
8550
+ if (existsSync9(path7.join(targetRoot, name))) {
8551
+ agentFiles.push(path7.join(targetRoot, name));
7572
8552
  }
7573
8553
  }
7574
8554
  const pm = detectPackageManager(targetRoot);
@@ -8382,290 +9362,9 @@ function formatChecks2(checks) {
8382
9362
  }
8383
9363
 
8384
9364
  // cli.ts
9365
+ init_github_client();
8385
9366
  init_common();
8386
-
8387
- // execution/sandcastle-execution.ts
8388
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
8389
- import { join as join12 } from "path";
8390
- import { createWorktree, opencode } from "@ai-hero/sandcastle";
8391
- import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
8392
-
8393
- // execution/execution-provider.ts
8394
- init_common();
8395
- import { writeFile as writeFile2 } from "fs/promises";
8396
- import { dirname as dirname5, join as join11 } from "path";
8397
- async function writeExecutionArtifacts(worktreePath, artifacts) {
8398
- for (const artifact of artifacts) {
8399
- const filePath = join11(worktreePath, artifact.path);
8400
- await ensureDir(dirname5(filePath));
8401
- await writeFile2(filePath, artifact.content, "utf-8");
8402
- }
8403
- }
8404
-
8405
- // execution/sandbox-image-build.ts
8406
- init_common();
8407
- import path7 from "path";
8408
-
8409
- // execution/sandbox-image.ts
8410
- import { createHash as createHash2 } from "crypto";
8411
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
8412
- import path6 from "path";
8413
- function sandboxImageName(repoRoot2) {
8414
- const dirName = path6.basename(repoRoot2.replace(/[\\/]+$/, "")) || "local";
8415
- const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
8416
- const baseName = sanitized || "local";
8417
- const dockerfilePath = path6.join(repoRoot2, ".sandcastle", "Dockerfile");
8418
- if (!existsSync9(dockerfilePath)) {
8419
- return `sandcastle:${baseName}`;
8420
- }
8421
- const fingerprint = createHash2("sha256").update(readFileSync8(dockerfilePath)).digest("hex").slice(0, 8);
8422
- return `sandcastle:${baseName}-${fingerprint}`;
8423
- }
8424
-
8425
- // execution/sandbox-image-build.ts
8426
- async function ensureSandboxImageBuilt(repoRoot2, options) {
8427
- const imageName = sandboxImageName(repoRoot2);
8428
- const dockerfilePath = path7.join(repoRoot2, ".sandcastle", "Dockerfile");
8429
- if (!options?.force) {
8430
- try {
8431
- await execCapture("docker", ["image", "inspect", imageName]);
8432
- return;
8433
- } catch {
8434
- }
8435
- }
8436
- const buildArgs = ["build", "-t", imageName, "-f", dockerfilePath];
8437
- if (options?.force) {
8438
- buildArgs.push("--pull", "--no-cache");
8439
- }
8440
- buildArgs.push(repoRoot2);
8441
- await execCapture("docker", buildArgs);
8442
- }
8443
-
8444
- // execution/sandcastle-existing-worktree.ts
8445
- async function createSandboxFromExistingWorktree(options) {
8446
- const sandcastleEntryUrl = import.meta.resolve("@ai-hero/sandcastle");
8447
- const createSandboxUrl = new URL("./createSandbox.js", sandcastleEntryUrl);
8448
- const sandcastleCreateSandbox = await import(createSandboxUrl.href);
8449
- return sandcastleCreateSandbox.createSandboxFromWorktree(options);
8450
- }
8451
-
8452
- // execution/sandbox-options.ts
8453
- function buildSandboxOptions(repoRoot2, sandbox) {
8454
- const mounts = [];
8455
- if (sandbox.mounts !== void 0) {
8456
- mounts.push(...sandbox.mounts);
8457
- }
8458
- return {
8459
- imageName: sandboxImageName(repoRoot2),
8460
- ...mounts.length > 0 ? { mounts } : {},
8461
- ...sandbox.env !== void 0 ? { env: sandbox.env } : {},
8462
- ...sandbox.idleTimeoutSeconds !== void 0 ? { idleTimeoutSeconds: sandbox.idleTimeoutSeconds } : {}
8463
- };
8464
- }
8465
-
8466
- // execution/opencode-config.ts
8467
- function isSerenaEligibleStage(stage) {
8468
- return stage === "builder" || stage === "refactor";
8469
- }
8470
- function buildSerenaOpenCodeConfig(stage, serena) {
8471
- if (!serena?.available || !isSerenaEligibleStage(stage)) {
8472
- return void 0;
8473
- }
8474
- return {
8475
- mcp: {
8476
- serena: {
8477
- type: "remote",
8478
- url: serena.sandboxMcpUrl,
8479
- enabled: true
8480
- }
8481
- }
8482
- };
8483
- }
8484
-
8485
- // execution/sandcastle-execution.ts
8486
- var SandcastleExecutionProvider = class {
8487
- async createSession() {
8488
- return new SandcastleExecutionSession();
8489
- }
8490
- async execute(options) {
8491
- const session = await this.createSession();
8492
- try {
8493
- return await session.execute(options);
8494
- } finally {
8495
- await session.close();
8496
- }
8497
- }
8498
- };
8499
- var SandcastleExecutionSession = class {
8500
- sandboxHandle = null;
8501
- worktreeHandle = null;
8502
- async close() {
8503
- try {
8504
- await this.sandboxHandle?.close?.();
8505
- } finally {
8506
- this.sandboxHandle = null;
8507
- }
8508
- }
8509
- async execute(options) {
8510
- const {
8511
- stage,
8512
- iteration,
8513
- agent,
8514
- model,
8515
- prompt,
8516
- target,
8517
- repoRoot: repoRoot2,
8518
- branchName,
8519
- worktreePath,
8520
- baseRef,
8521
- sandbox,
8522
- autoApprove = false,
8523
- timeoutMs,
8524
- artifacts = [],
8525
- logger
8526
- } = options;
8527
- const stageLabel = iteration !== void 0 ? `${stage}:${iteration}` : stage;
8528
- logger.step(
8529
- "sandcastle",
8530
- `[${stageLabel}] running agent "${agent}" with model "${model}"`
8531
- );
8532
- try {
8533
- const env = {};
8534
- if (autoApprove) {
8535
- env.OPENCODE_AUTO_APPROVE = "true";
8536
- }
8537
- const serenaConfig = buildSerenaOpenCodeConfig(stage, options.serena);
8538
- if (serenaConfig) {
8539
- env.OPENCODE_CONFIG_CONTENT = JSON.stringify(serenaConfig);
8540
- }
8541
- const logPath = `${repoRoot2}/.pourkit/logs/${sanitizeBranch(branchName)}-${Date.now()}.log`;
8542
- await ensureSandboxImageBuilt(repoRoot2, { force: sandbox.forceRebuild });
8543
- try {
8544
- savePromptToFile(repoRoot2, stage, iteration, prompt);
8545
- } catch {
8546
- }
8547
- const agentProvider = opencode(model, { env, agent });
8548
- const sandboxOptions = buildSandboxOptions(repoRoot2, sandbox);
8549
- const sandboxProvider = resolveSandboxProvider(sandbox.provider, docker);
8550
- const activeSandbox = await this.getOrCreateSandbox({
8551
- repoRoot: repoRoot2,
8552
- branchName,
8553
- baseBranch: baseRef ?? target.baseBranch,
8554
- worktreePath,
8555
- sandboxProvider: sandboxProvider(sandboxOptions),
8556
- setupCommands: target.setupCommands ?? [],
8557
- copyToWorktree: sandbox.copyToWorktree ?? []
8558
- });
8559
- await writeExecutionArtifacts(activeSandbox.worktreePath, artifacts);
8560
- const result = await activeSandbox.run({
8561
- agent: agentProvider,
8562
- prompt,
8563
- maxIterations: 1,
8564
- name: stageLabel,
8565
- logging: {
8566
- type: "file",
8567
- path: logPath,
8568
- onAgentStreamEvent: (event) => {
8569
- if (event.type === "text") {
8570
- logger.raw(event.message);
8571
- } else if (event.type === "toolCall") {
8572
- logger.raw(`${event.name}(${event.formattedArgs})`);
8573
- }
8574
- }
8575
- },
8576
- completionSignal: "<promise>COMPLETE</promise>",
8577
- ...timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {},
8578
- ...sandboxOptions.idleTimeoutSeconds ? { idleTimeoutSeconds: sandboxOptions.idleTimeoutSeconds } : {}
8579
- });
8580
- const commits = result.commits.map((c) => c.sha);
8581
- const resultBranch = result.branch ?? activeSandbox.branch ?? branchName;
8582
- logger.kv("SANDBOX_SUCCESS", "true");
8583
- logger.kv("COMMITS_CREATED", String(commits.length));
8584
- logger.kv("WORKTREE_BRANCH", resultBranch);
8585
- if (result.logFilePath) {
8586
- logger.kv("LOG_FILE", result.logFilePath);
8587
- }
8588
- return {
8589
- success: true,
8590
- branch: resultBranch,
8591
- worktreePath: activeSandbox.worktreePath,
8592
- commits,
8593
- logPath
8594
- };
8595
- } catch (error) {
8596
- logger.step(
8597
- "error",
8598
- `Sandcastle failed: ${error instanceof Error ? error.message : String(error)}`
8599
- );
8600
- return {
8601
- success: false,
8602
- branch: "",
8603
- worktreePath: "",
8604
- commits: [],
8605
- logPath: null,
8606
- error: error instanceof Error ? error.message : String(error)
8607
- };
8608
- }
8609
- }
8610
- async getOrCreateSandbox(options) {
8611
- if (this.sandboxHandle) {
8612
- return this.sandboxHandle;
8613
- }
8614
- const hooks = {
8615
- sandbox: {
8616
- onSandboxReady: options.setupCommands.map((command) => ({
8617
- command: command.command
8618
- }))
8619
- }
8620
- };
8621
- if (options.worktreePath) {
8622
- this.sandboxHandle = await createSandboxFromExistingWorktree({
8623
- branch: options.branchName,
8624
- hostRepoDir: options.repoRoot,
8625
- worktreePath: options.worktreePath,
8626
- sandbox: options.sandboxProvider,
8627
- ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {},
8628
- hooks
8629
- });
8630
- return this.sandboxHandle;
8631
- }
8632
- this.worktreeHandle = await createWorktree({
8633
- cwd: options.repoRoot,
8634
- branchStrategy: {
8635
- type: "branch",
8636
- branch: options.branchName,
8637
- baseBranch: options.baseBranch
8638
- },
8639
- ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {}
8640
- });
8641
- this.sandboxHandle = await this.worktreeHandle.createSandbox({
8642
- sandbox: options.sandboxProvider,
8643
- ...options.copyToWorktree.length > 0 ? { copyToWorktree: options.copyToWorktree } : {},
8644
- hooks
8645
- });
8646
- return this.sandboxHandle;
8647
- }
8648
- };
8649
- function resolveSandboxProvider(provider, dockerFactory) {
8650
- if (provider === "docker") {
8651
- return dockerFactory;
8652
- }
8653
- throw new Error(`Unsupported sandbox provider: ${provider}`);
8654
- }
8655
- function sanitizeBranch(branchName) {
8656
- return branchName.replace(/[^A-Za-z0-9._-]/g, "-");
8657
- }
8658
- function savePromptToFile(repoRoot2, stage, iteration, prompt) {
8659
- const promptsDir = join12(repoRoot2, ".pourkit", ".tmp", "prompts");
8660
- mkdirSync6(promptsDir, { recursive: true });
8661
- const timestamp2 = Date.now();
8662
- const iterationSuffix = iteration !== void 0 ? `-iteration-${iteration}` : "";
8663
- const filename = `${stage}${iterationSuffix}-${timestamp2}.md`;
8664
- const filePath = join12(promptsDir, filename);
8665
- writeFileSync4(filePath, prompt, "utf-8");
8666
- }
8667
-
8668
- // cli.ts
9367
+ init_sandcastle_execution();
8669
9368
  function normalizePrdRef(ref) {
8670
9369
  const normalized = ref.trim().toUpperCase();
8671
9370
  if (!/^PRD-\d+$/.test(normalized)) {
@@ -9092,11 +9791,11 @@ function createCliProgram(version) {
9092
9791
  return program;
9093
9792
  }
9094
9793
  async function resolveCliVersion() {
9095
- if (isPackageVersion("0.0.0-next-20260601025244")) {
9096
- return "0.0.0-next-20260601025244";
9794
+ if (isPackageVersion("0.0.0-next-20260601224754")) {
9795
+ return "0.0.0-next-20260601224754";
9097
9796
  }
9098
- if (isReleaseVersion("0.0.0-next-20260601025244")) {
9099
- return "0.0.0-next-20260601025244";
9797
+ if (isReleaseVersion("0.0.0-next-20260601224754")) {
9798
+ return "0.0.0-next-20260601224754";
9100
9799
  }
9101
9800
  try {
9102
9801
  const root = repoRoot();