@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 +1958 -1259
- package/dist/cli.js.map +1 -1
- package/dist/e2e/run-live-e2e.js +1681 -802
- package/dist/e2e/run-live-e2e.js.map +1 -1
- package/package.json +1 -1
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
|
|
1001
|
-
import { join as
|
|
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
|
-
|
|
1277
|
-
"Effect runtime not initialized. Call initializeEffectRuntime() at CLI startup."
|
|
1278
|
-
);
|
|
1701
|
+
initializeEffectRuntime();
|
|
1279
1702
|
}
|
|
1280
|
-
|
|
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
|
|
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 {
|
|
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
|
|
2689
|
+
import { join as join9 } from "path";
|
|
2233
2690
|
import { readFile } from "fs/promises";
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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
|
-
|
|
2371
|
-
|
|
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 =
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
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
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
);
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
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
|
-
|
|
2466
|
-
|
|
2467
|
-
const dir =
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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: "
|
|
2697
|
-
merged:
|
|
3484
|
+
stage: "merge",
|
|
3485
|
+
merged: false,
|
|
2698
3486
|
error: error instanceof Error ? error : new Error(String(error))
|
|
2699
3487
|
};
|
|
2700
3488
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
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
|
|
2708
|
-
mkdirSync as
|
|
2709
|
-
readFileSync as
|
|
3530
|
+
existsSync as existsSync7,
|
|
3531
|
+
mkdirSync as mkdirSync6,
|
|
3532
|
+
readFileSync as readFileSync7,
|
|
2710
3533
|
readdirSync,
|
|
2711
3534
|
rmSync as rmSync2,
|
|
2712
|
-
writeFileSync as
|
|
3535
|
+
writeFileSync as writeFileSync4
|
|
2713
3536
|
} from "fs";
|
|
2714
|
-
import { dirname as
|
|
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 (!
|
|
3620
|
+
if (!existsSync7(artifactPath)) {
|
|
2797
3621
|
throw new RefactorArtifactValidationError(
|
|
2798
3622
|
`Refactor artifact missing at ${artifactPath}`
|
|
2799
3623
|
);
|
|
2800
3624
|
}
|
|
2801
|
-
const content =
|
|
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 !== "-" &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3777
|
+
return Effect7.fail(
|
|
3778
|
+
new ReviewerFailure({ message: "No reviewer config found" })
|
|
3779
|
+
);
|
|
2945
3780
|
}
|
|
2946
|
-
const artifactPathInWorktree =
|
|
3781
|
+
const artifactPathInWorktree = join11(
|
|
2947
3782
|
".pourkit",
|
|
2948
3783
|
".tmp",
|
|
2949
3784
|
"reviewers",
|
|
2950
3785
|
`iteration-${iteration ?? 1}.md`
|
|
2951
3786
|
);
|
|
2952
|
-
const artifactPath =
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
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
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
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
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
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
|
-
|
|
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
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
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
|
-
|
|
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
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
-
|
|
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
|
|
3135
|
-
|
|
3136
|
-
|
|
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
|
-
|
|
3151
|
-
"prompts",
|
|
3152
|
-
`reviewer-${criterion}.snippet.md`
|
|
4014
|
+
promptTemplate
|
|
3153
4015
|
);
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
}
|
|
3157
|
-
return
|
|
3158
|
-
|
|
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
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
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
|
|
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
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4107
|
+
return Effect7.die(new Error("No reviewer config found"));
|
|
3219
4108
|
}
|
|
3220
4109
|
const refactorer = strategy.review.refactor;
|
|
3221
4110
|
if (!refactorer) {
|
|
3222
|
-
|
|
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 =
|
|
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
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
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
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
if (
|
|
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
|
-
"
|
|
3403
|
-
|
|
4205
|
+
"info",
|
|
4206
|
+
"PASS_WITH_NOTES refactor attempts exhausted, treating as PASS"
|
|
3404
4207
|
);
|
|
3405
4208
|
return {
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
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
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
};
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
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
|
-
|
|
3450
|
-
|
|
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
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
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
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
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 (!
|
|
3952
|
-
throw new
|
|
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 =
|
|
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
|
|
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
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
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
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
6138
|
+
function selectNextQueueIssue(options) {
|
|
5137
6139
|
const { config, issueProvider, logger, prdRef } = options;
|
|
5138
6140
|
logger.step("info", "Loading candidate issues from provider");
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
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
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
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
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
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
|
-
|
|
5290
|
-
return
|
|
6341
|
+
function runQueue(options) {
|
|
6342
|
+
return runOneQueueIssueEffect(options);
|
|
5291
6343
|
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
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
|
|
5308
|
-
const processedIssue =
|
|
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
|
-
|
|
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
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
7239
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
6260
7240
|
}
|
|
6261
7241
|
function lockfileExists(root, name) {
|
|
6262
|
-
return
|
|
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 =
|
|
6306
|
-
if (
|
|
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 =
|
|
6316
|
-
if (
|
|
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 =
|
|
6324
|
-
return
|
|
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
|
-
|
|
6329
|
-
|
|
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 (
|
|
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 =
|
|
6343
|
-
if (
|
|
7322
|
+
const p = path7.join(root, name);
|
|
7323
|
+
if (existsSync9(p)) {
|
|
6344
7324
|
docs.push(p);
|
|
6345
7325
|
}
|
|
6346
7326
|
}
|
|
6347
|
-
const adrDir =
|
|
6348
|
-
if (
|
|
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(
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
6490
|
-
const destPath =
|
|
6491
|
-
if (!
|
|
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: ${
|
|
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 (!
|
|
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 =
|
|
7595
|
+
const targetPath = path7.join(targetRoot, targetDirName);
|
|
6616
7596
|
const skillFiles = await walkDir(s);
|
|
6617
7597
|
for (const file of skillFiles) {
|
|
6618
|
-
const relPath =
|
|
6619
|
-
const destPath =
|
|
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: ${
|
|
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: ${
|
|
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:
|
|
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 =
|
|
6662
|
-
const destPath =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
6731
|
-
if (!
|
|
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 =
|
|
7722
|
+
const adrGitkeep = path7.join(
|
|
6743
7723
|
targetRoot,
|
|
6744
7724
|
".pourkit",
|
|
6745
7725
|
"docs",
|
|
6746
7726
|
"adr",
|
|
6747
7727
|
".gitkeep"
|
|
6748
7728
|
);
|
|
6749
|
-
if (!
|
|
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 =
|
|
6760
|
-
const tgtDocAgents =
|
|
6761
|
-
if (
|
|
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 =
|
|
7744
|
+
const relPath = path7.relative(srcDocAgents, file);
|
|
6765
7745
|
if (relPath === "triage-labels.md") {
|
|
6766
7746
|
operations.push({
|
|
6767
7747
|
kind: "create",
|
|
6768
|
-
path:
|
|
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:
|
|
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 =
|
|
6793
|
-
const tgtPrompts =
|
|
6794
|
-
if (
|
|
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 =
|
|
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:
|
|
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 =
|
|
7791
|
+
const srcSandboxDockerfile = path7.join(
|
|
6812
7792
|
sourceRoot,
|
|
6813
7793
|
".sandcastle",
|
|
6814
7794
|
"Dockerfile"
|
|
6815
7795
|
);
|
|
6816
|
-
const tgtSandboxDockerfile =
|
|
7796
|
+
const tgtSandboxDockerfile = path7.join(
|
|
6817
7797
|
targetRoot,
|
|
6818
7798
|
".sandcastle",
|
|
6819
7799
|
"Dockerfile"
|
|
6820
7800
|
);
|
|
6821
|
-
if (
|
|
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 (
|
|
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 =
|
|
6844
|
-
if (!
|
|
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 && !
|
|
7861
|
+
if ((agentFileMode === "agents" || agentFileMode === "both") && !hasExistingAgents && !existsSync9(path7.join(targetRoot, "AGENTS.md"))) {
|
|
6882
7862
|
operations.push({
|
|
6883
7863
|
kind: "create",
|
|
6884
|
-
path:
|
|
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 && !
|
|
7877
|
+
if ((agentFileMode === "claude" || agentFileMode === "both") && !hasExistingClaude && !existsSync9(path7.join(targetRoot, "CLAUDE.md"))) {
|
|
6898
7878
|
operations.push({
|
|
6899
7879
|
kind: "create",
|
|
6900
|
-
path:
|
|
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 =
|
|
7890
|
+
const gitignoreTarget = path7.join(targetRoot, ".gitignore");
|
|
6911
7891
|
const gitignoreContent = generateGitignoreBlock();
|
|
6912
|
-
if (!
|
|
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 =
|
|
6936
|
-
if (!
|
|
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 =
|
|
6993
|
-
if (
|
|
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
|
|
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 (!
|
|
7313
|
-
const dir =
|
|
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 =
|
|
7332
|
-
const manifestPath =
|
|
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 =
|
|
8319
|
+
const relPath = path7.relative(plan.targetRoot, op.path);
|
|
7340
8320
|
if (relPath === ".pourkit/manifest.json") continue;
|
|
7341
|
-
if (
|
|
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 (
|
|
8366
|
+
if (existsSync9(op.path) && !op.destructive) {
|
|
7387
8367
|
skipped++;
|
|
7388
8368
|
continue;
|
|
7389
8369
|
}
|
|
7390
|
-
const dir =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
8410
|
+
if (existsSync9(op.path)) {
|
|
7431
8411
|
skipped++;
|
|
7432
8412
|
continue;
|
|
7433
8413
|
}
|
|
7434
|
-
const dir =
|
|
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 ===
|
|
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 (
|
|
7571
|
-
agentFiles.push(
|
|
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-
|
|
9096
|
-
return "0.0.0-next-
|
|
9794
|
+
if (isPackageVersion("0.0.0-next-20260601224754")) {
|
|
9795
|
+
return "0.0.0-next-20260601224754";
|
|
9097
9796
|
}
|
|
9098
|
-
if (isReleaseVersion("0.0.0-next-
|
|
9099
|
-
return "0.0.0-next-
|
|
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();
|