@snappedly-tools/shipyard 0.5.0 → 0.7.0-staging.36081473933

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.
Files changed (89) hide show
  1. package/README.md +64 -117
  2. package/dist/MountConfig-BHnKnA4h.d.ts +145 -0
  3. package/dist/{chunk-WAXEMZUV.js → chunk-44I2BL6E.js} +225 -33
  4. package/dist/chunk-44I2BL6E.js.map +1 -0
  5. package/dist/{chunk-56TDSWFU.js → chunk-JI3HDDMS.js} +3 -3
  6. package/dist/{chunk-56TDSWFU.js.map → chunk-JI3HDDMS.js.map} +1 -1
  7. package/dist/{chunk-FDYOTN55.js → chunk-L6PX5QTU.js} +253 -878
  8. package/dist/chunk-L6PX5QTU.js.map +1 -0
  9. package/dist/index.d.ts +117 -97
  10. package/dist/index.js +349 -353
  11. package/dist/index.js.map +1 -1
  12. package/dist/main.js +285 -153
  13. package/dist/main.js.map +1 -1
  14. package/dist/sandboxes/docker.d.ts +1 -3
  15. package/dist/sandboxes/docker.js +2 -4
  16. package/dist/templates/parallel-planner/block-scope.sh +160 -0
  17. package/dist/templates/parallel-planner/conflict-prompt.md +22 -0
  18. package/dist/templates/parallel-planner/handoff.sh +175 -0
  19. package/dist/templates/parallel-planner/implement-prompt.md +10 -56
  20. package/dist/templates/parallel-planner/main.mts +493 -189
  21. package/dist/templates/parallel-planner/merge-prompt.md +9 -21
  22. package/dist/templates/parallel-planner/plan-prompt.md +5 -31
  23. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  24. package/dist/templates/parallel-planner/select-issues.mjs +788 -0
  25. package/dist/templates/parallel-planner/setup.sh +51 -0
  26. package/dist/templates/parallel-planner/spec-wave-prompt.md +16 -0
  27. package/dist/templates/parallel-planner/template.json +1 -1
  28. package/dist/templates/parallel-planner/triage-prompt.md +5 -0
  29. package/dist/templates/parallel-planner/verify-triage.sh +19 -0
  30. package/dist/templates/parallel-planner-with-review/block-scope.sh +160 -0
  31. package/dist/templates/parallel-planner-with-review/conflict-prompt.md +22 -0
  32. package/dist/templates/parallel-planner-with-review/handoff.sh +175 -0
  33. package/dist/templates/parallel-planner-with-review/implement-prompt.md +10 -56
  34. package/dist/templates/parallel-planner-with-review/main.mts +533 -207
  35. package/dist/templates/parallel-planner-with-review/merge-prompt.md +9 -21
  36. package/dist/templates/parallel-planner-with-review/plan-prompt.md +5 -31
  37. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  38. package/dist/templates/parallel-planner-with-review/review-prompt.md +11 -49
  39. package/dist/templates/parallel-planner-with-review/select-issues.mjs +788 -0
  40. package/dist/templates/parallel-planner-with-review/setup.sh +51 -0
  41. package/dist/templates/parallel-planner-with-review/spec-wave-prompt.md +16 -0
  42. package/dist/templates/parallel-planner-with-review/template.json +1 -1
  43. package/dist/templates/parallel-planner-with-review/triage-prompt.md +5 -0
  44. package/dist/templates/parallel-planner-with-review/verify-triage.sh +19 -0
  45. package/dist/templates/sequential-reviewer/block-scope.sh +160 -0
  46. package/dist/templates/sequential-reviewer/handoff.sh +175 -0
  47. package/dist/templates/sequential-reviewer/implement-prompt.md +12 -46
  48. package/dist/templates/sequential-reviewer/main.mts +248 -103
  49. package/dist/templates/sequential-reviewer/review-prompt.md +11 -49
  50. package/dist/templates/sequential-reviewer/select-issues.mjs +788 -0
  51. package/dist/templates/sequential-reviewer/setup.sh +51 -0
  52. package/dist/templates/sequential-reviewer/template.json +1 -1
  53. package/dist/templates/sequential-reviewer/triage-prompt.md +5 -0
  54. package/dist/templates/sequential-reviewer/verify-triage.sh +19 -0
  55. package/dist/templates/shared/block-scope.sh +160 -0
  56. package/dist/templates/shared/handoff.sh +175 -0
  57. package/dist/templates/shared/select-issues.mjs +788 -0
  58. package/dist/templates/shared/setup.sh +51 -0
  59. package/dist/templates/shared/triage-prompt.md +5 -0
  60. package/dist/templates/shared/verify-triage.sh +19 -0
  61. package/dist/templates/simple-loop/block-scope.sh +160 -0
  62. package/dist/templates/simple-loop/handoff.sh +175 -0
  63. package/dist/templates/simple-loop/main.mts +239 -45
  64. package/dist/templates/simple-loop/prompt.md +12 -46
  65. package/dist/templates/simple-loop/select-issues.mjs +788 -0
  66. package/dist/templates/simple-loop/setup.sh +51 -0
  67. package/dist/templates/simple-loop/template.json +1 -1
  68. package/dist/templates/simple-loop/triage-prompt.md +5 -0
  69. package/dist/templates/simple-loop/verify-triage.sh +19 -0
  70. package/package.json +1 -17
  71. package/dist/MountConfig-bZoCs4Dd.d.ts +0 -26
  72. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  73. package/dist/chunk-ACD46ZM4.js +0 -136
  74. package/dist/chunk-ACD46ZM4.js.map +0 -1
  75. package/dist/chunk-FDYOTN55.js.map +0 -1
  76. package/dist/chunk-KMGNFXKN.js +0 -38
  77. package/dist/chunk-KMGNFXKN.js.map +0 -1
  78. package/dist/chunk-SOJTAJTF.js +0 -78
  79. package/dist/chunk-SOJTAJTF.js.map +0 -1
  80. package/dist/chunk-WAXEMZUV.js.map +0 -1
  81. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  82. package/dist/sandboxes/no-sandbox.js +0 -4
  83. package/dist/sandboxes/no-sandbox.js.map +0 -1
  84. package/dist/sandboxes/vercel.d.ts +0 -104
  85. package/dist/sandboxes/vercel.js +0 -166
  86. package/dist/sandboxes/vercel.js.map +0 -1
  87. package/dist/templates/blank/main.mts +0 -13
  88. package/dist/templates/blank/prompt.md +0 -12
  89. package/dist/templates/blank/template.json +0 -4
package/dist/index.js CHANGED
@@ -1,36 +1,25 @@
1
- import { CODEX_MODELS, resolveEnv, formatErrorMessage, buildLogDirectoryName } from './chunk-56TDSWFU.js';
2
- export { CODEX_MODELS, CODEX_REASONING_EFFORTS } from './chunk-56TDSWFU.js';
3
- import { CwdError, resolveCwd, getCurrentBranch, generateTempBranchName, assertNoSymlinkComponents, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, CLI_NAME, PRODUCT_NAME, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, patchGitMountsForWindows, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, registerShutdown, SandboxFactory, PromptError, CONFIG_DIR, LOGS_DIR, AgentError, SessionCaptureError, PromptExpansionTimeoutError, SilentDisplay, AgentIdleTimeoutError } from './chunk-FDYOTN55.js';
4
- import { shellQuote, toSessionTransferHandle } from './chunk-KMGNFXKN.js';
5
- export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-KMGNFXKN.js';
6
- import { noSandbox } from './chunk-SOJTAJTF.js';
7
- import './chunk-ACD46ZM4.js';
1
+ import { CODEX_MODELS, resolveEnv, formatErrorMessage, buildLogDirectoryName } from './chunk-JI3HDDMS.js';
2
+ export { CODEX_MODELS, CODEX_REASONING_EFFORTS } from './chunk-JI3HDDMS.js';
3
+ import { CwdError, resolveCwd, getCurrentBranch, generateTempBranchName, assertNoSymlinkComponents, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, CLI_NAME, PRODUCT_NAME, pruneStale, create, runHostHooks, startSandbox, remove, makeSandboxFromHandle, withSandboxLifecycle, hasUncommittedChanges, shellQuote, registerShutdown, toSessionTransferHandle, SandboxFactory, syncOut, PromptError, CONFIG_DIR, LOGS_DIR, AgentError, SessionCaptureError, PromptExpansionTimeoutError, assertExcludesRepositoryRunner, resolveSafeRelativePath, CopyToWorktreeError, withTimeout, CopyToWorktreeTimeoutError, SilentDisplay, AgentIdleTimeoutError } from './chunk-L6PX5QTU.js';
4
+ export { createIsolatedSandboxProvider } from './chunk-L6PX5QTU.js';
8
5
  import { NodeContext, NodeFileSystem } from '@effect/platform-node';
9
- import { mkdirSync, chmodSync, appendFileSync, constants } from 'fs';
6
+ import { existsSync, mkdirSync, chmodSync, appendFileSync, constants } from 'fs';
10
7
  import path, { join, posix, dirname, relative } from 'path';
11
8
  import { styleText } from 'util';
12
9
  import { Context, Effect, Layer, Clock, Duration, Option, Deferred, Ref, Fiber } from 'effect';
13
10
  import { FileSystem } from '@effect/platform';
14
11
  import * as clack from '@clack/prompts';
12
+ import { execFile } from 'child_process';
15
13
  import { readdir, access, readFile, mkdir, mkdtemp, writeFile, rm, lstat, open, chmod } from 'fs/promises';
16
14
  import { homedir, tmpdir } from 'os';
17
15
  import { createHmac, timingSafeEqual, createHash, randomUUID } from 'crypto';
18
16
 
19
17
  // src/resumePrecheck.ts
20
18
  var assertResumeSessionExists = async (params) => {
21
- const { provider, sandboxTag, hostRepoDir, resumeSession } = params;
19
+ const { provider, hostRepoDir, resumeSession } = params;
22
20
  if (!provider.sessionStorage) {
23
21
  throw new Error(`${provider.name} does not support resumeSession`);
24
22
  }
25
- if (sandboxTag === "none") {
26
- const found = await provider.sessionStorage.findByIdOnHost(resumeSession);
27
- if (!found.path) {
28
- throw new Error(
29
- `resumeSession "${resumeSession}" not found under ${found.searchedRoot}`
30
- );
31
- }
32
- return;
33
- }
34
23
  const exists = await provider.sessionStorage.existsOnHost(
35
24
  hostRepoDir,
36
25
  resumeSession
@@ -424,7 +413,12 @@ var orchestrate = (options) => {
424
413
  yield* checkAbort();
425
414
  yield* display.status(label(`Iteration ${i}/${iterations}`), "info");
426
415
  const sandboxResult = yield* factory.withSandbox(
427
- ({ hostWorktreePath, sandboxRepoPath, applyToHost, bindMountHandle }, sandbox) => withSandboxLifecycle(
416
+ ({
417
+ hostWorktreePath,
418
+ sandboxRepoPath,
419
+ applyToHost,
420
+ sessionTransferHandle
421
+ }, sandbox) => withSandboxLifecycle(
428
422
  {
429
423
  hostRepoDir,
430
424
  sandboxRepoDir: sandboxRepoPath,
@@ -440,14 +434,14 @@ var orchestrate = (options) => {
440
434
  (ctx) => Effect.gen(function* () {
441
435
  const iterationResumeSession = i === 1 ? options.resumeSession : void 0;
442
436
  const iterationForkSession = i === 1 ? options.forkSession : void 0;
443
- if (iterationResumeSession && bindMountHandle && provider.sessionStorage) {
437
+ if (iterationResumeSession && sessionTransferHandle && provider.sessionStorage) {
444
438
  yield* display.status(label("Resuming session"), "info");
445
439
  yield* Effect.tryPromise({
446
440
  try: () => provider.sessionStorage.resumeIntoSandbox({
447
441
  hostCwd: hostRepoDir,
448
442
  sandboxCwd: ctx.sandboxRepoDir,
449
443
  sessionId: iterationResumeSession,
450
- handle: bindMountHandle
444
+ handle: sessionTransferHandle
451
445
  }),
452
446
  catch: (e) => new SessionCaptureError({
453
447
  message: `Session resume failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -539,14 +533,14 @@ var orchestrate = (options) => {
539
533
  yield* display.status(label("Agent stopped"), "info");
540
534
  let sessionFilePath;
541
535
  let usage = streamUsage;
542
- if (provider.captureSessions && provider.sessionStorage && sessionId && bindMountHandle) {
536
+ if (provider.captureSessions && provider.sessionStorage && sessionId && sessionTransferHandle) {
543
537
  yield* display.status(label("Capturing session"), "info");
544
538
  yield* Effect.tryPromise({
545
539
  try: () => provider.sessionStorage.captureToHost({
546
540
  hostCwd: hostRepoDir,
547
541
  sandboxCwd: ctx.sandboxRepoDir,
548
542
  sessionId,
549
- handle: bindMountHandle
543
+ handle: sessionTransferHandle
550
544
  }),
551
545
  catch: (e) => new SessionCaptureError({
552
546
  message: `Session capture failed: ${e instanceof Error ? e.message : String(e)}`,
@@ -1019,18 +1013,9 @@ async function run(options) {
1019
1013
  agent: provider
1020
1014
  } = options;
1021
1015
  validateMaxIterations(maxIterations);
1022
- const branchStrategy = options.branchStrategy ?? (options.sandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
1023
- const effectiveBranchType = branchStrategy.type;
1024
- if (effectiveBranchType === "head" && options.sandbox.tag === "isolated") {
1025
- throw new Error(
1026
- "head branch strategy is not supported with isolated providers"
1027
- );
1028
- }
1029
- if (effectiveBranchType === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
1030
- throw new Error(
1031
- "copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
1032
- );
1033
- }
1016
+ const branchStrategy = options.branchStrategy ?? {
1017
+ type: "merge-to-head"
1018
+ };
1034
1019
  if (options.resumeSession && maxIterations > 1) {
1035
1020
  throw new Error(
1036
1021
  "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
@@ -1064,7 +1049,6 @@ async function run(options) {
1064
1049
  if (options.resumeSession) {
1065
1050
  await assertResumeSessionExists({
1066
1051
  provider,
1067
- sandboxTag: options.sandbox.tag,
1068
1052
  hostRepoDir,
1069
1053
  resumeSession: options.resumeSession
1070
1054
  });
@@ -1096,8 +1080,8 @@ async function run(options) {
1096
1080
  const currentHostBranch = await Effect.runPromise(
1097
1081
  getCurrentBranch(hostRepoDir)
1098
1082
  );
1099
- const resolvedBranch = effectiveBranchType === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
1100
- const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : void 0;
1083
+ const resolvedBranch = branch ?? generateTempBranchName(options.name);
1084
+ const targetBranch = branchStrategy.type === "merge-to-head" ? currentHostBranch : void 0;
1101
1085
  const resolvedLogging = options.logging ?? {
1102
1086
  type: "file",
1103
1087
  path: buildDefaultLogPath(
@@ -1182,7 +1166,7 @@ async function run(options) {
1182
1166
  builtInArgKeysSet
1183
1167
  );
1184
1168
  }
1185
- const orchestrateBranch = effectiveBranchType === "head" ? currentHostBranch : branch;
1169
+ const orchestrateBranch = branch;
1186
1170
  const orchestrateResult = yield* orchestrate({
1187
1171
  hostRepoDir,
1188
1172
  iterations: maxIterations,
@@ -1333,25 +1317,16 @@ var raceAbortSignal = (effect, signal) => {
1333
1317
  var interactive = async (options) => {
1334
1318
  options.signal?.throwIfAborted();
1335
1319
  const { prompt, promptFile, hooks, agent: provider } = options;
1336
- const resolvedSandbox = options.sandbox ?? noSandbox();
1337
- const branchStrategy = options.branchStrategy ?? (resolvedSandbox.tag === "isolated" ? { type: "merge-to-head" } : { type: "head" });
1338
- if (branchStrategy.type === "head" && resolvedSandbox.tag === "isolated") {
1339
- throw new Error(
1340
- "head branch strategy is not supported with isolated providers"
1341
- );
1342
- }
1343
- if (branchStrategy.type === "head" && options.copyToWorktree && options.copyToWorktree.length > 0) {
1344
- throw new Error(
1345
- "copyToWorktree is not supported with head branch strategy. In head mode the host working directory is bind-mounted directly."
1346
- );
1347
- }
1320
+ const resolvedSandbox = options.sandbox;
1321
+ const branchStrategy = options.branchStrategy ?? {
1322
+ type: "merge-to-head"
1323
+ };
1348
1324
  if (!provider.buildInteractiveArgs) {
1349
1325
  throw new Error(
1350
1326
  `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
1351
1327
  );
1352
1328
  }
1353
1329
  const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
1354
- const isHeadMode = branchStrategy.type === "head";
1355
1330
  const sandboxProvider = resolvedSandbox;
1356
1331
  const inner = Effect.gen(function* () {
1357
1332
  const hostRepoDir = yield* resolveCwd(options.cwd);
@@ -1368,7 +1343,7 @@ var interactive = async (options) => {
1368
1343
  });
1369
1344
  const effectiveEnv = { ...env, ...options.env ?? {} };
1370
1345
  const currentHostBranch = yield* getCurrentBranch(hostRepoDir);
1371
- const resolvedBranch = branchStrategy.type === "head" ? currentHostBranch : branch ?? generateTempBranchName(options.name);
1346
+ const resolvedBranch = branch ?? generateTempBranchName(options.name);
1372
1347
  let substitutedPrompt = rawPrompt;
1373
1348
  if (hasPromptSource && !isInlinePrompt) {
1374
1349
  const userArgs = options.promptArgs ?? {};
@@ -1408,86 +1383,38 @@ var interactive = async (options) => {
1408
1383
  const userArgs = options.promptArgs ?? {};
1409
1384
  yield* validateNoArgsWithInlinePrompt(userArgs);
1410
1385
  }
1411
- const lifecycleBranch = isHeadMode ? currentHostBranch : branch;
1386
+ const lifecycleBranch = branch;
1412
1387
  yield* d.intro(options.name ?? `${CLI_NAME} interactive`);
1413
1388
  yield* d.summary("Interactive Session", {
1414
1389
  Agent: options.name ?? provider.name,
1415
1390
  Sandbox: sandboxProvider.name,
1416
1391
  Branch: resolvedBranch
1417
1392
  });
1418
- let worktreeInfo;
1419
- if (!isHeadMode) {
1420
- worktreeInfo = yield* d.taskLog(
1421
- "Creating worktree",
1422
- () => pruneStale(hostRepoDir).pipe(
1423
- Effect.catchAll(() => Effect.void),
1424
- Effect.andThen(
1425
- branch ? create(hostRepoDir, { branch }) : create(hostRepoDir, { name: options.name })
1426
- )
1393
+ const worktreeInfo = yield* d.taskLog(
1394
+ "Creating worktree",
1395
+ () => pruneStale(hostRepoDir).pipe(
1396
+ Effect.catchAll(() => Effect.void),
1397
+ Effect.andThen(
1398
+ branch ? create(hostRepoDir, { branch }) : create(hostRepoDir, { name: options.name })
1427
1399
  )
1428
- );
1429
- }
1400
+ )
1401
+ );
1430
1402
  const handle = yield* Effect.gen(function* () {
1431
- if (!isHeadMode) {
1432
- if ((sandboxProvider.tag === "bind-mount" || sandboxProvider.tag === "none") && options.copyToWorktree && options.copyToWorktree.length > 0) {
1433
- yield* d.taskLog(
1434
- "Copying files to worktree",
1435
- () => copyToWorktree(
1436
- options.copyToWorktree,
1437
- hostRepoDir,
1438
- worktreeInfo.path,
1439
- options.timeouts?.copyToWorktreeMs
1440
- )
1441
- );
1442
- }
1443
- if (hooks?.host?.onWorktreeReady?.length) {
1444
- yield* runHostHooks(hooks.host.onWorktreeReady, worktreeInfo.path);
1445
- }
1446
- } else if (hooks?.host?.onWorktreeReady?.length) {
1447
- yield* runHostHooks(hooks.host.onWorktreeReady, hostRepoDir);
1448
- }
1449
- if (sandboxProvider.tag === "none") {
1450
- const worktreePath = isHeadMode ? hostRepoDir : worktreeInfo.path;
1451
- return yield* Effect.promise(
1452
- () => sandboxProvider.create({
1453
- worktreePath,
1454
- env: effectiveEnv
1455
- })
1456
- );
1457
- } else if (sandboxProvider.tag === "isolated") {
1458
- const startResult = yield* d.taskLog(
1459
- "Starting sandbox",
1460
- () => startSandbox({
1461
- provider: sandboxProvider,
1462
- hostRepoDir: worktreeInfo.path,
1463
- sourceRepoDir: hostRepoDir,
1464
- env: effectiveEnv,
1465
- copyPaths: options.copyToWorktree
1466
- })
1467
- );
1468
- return startResult.handle;
1469
- } else {
1470
- const gitPath = join(hostRepoDir, ".git");
1471
- const rawGitMounts = yield* resolveGitMounts(gitPath);
1472
- const worktreeOrRepoPath = isHeadMode ? hostRepoDir : worktreeInfo.path;
1473
- const gitMounts = yield* patchGitMountsForWindows(
1474
- rawGitMounts,
1475
- worktreeOrRepoPath,
1476
- SANDBOX_REPO_DIR
1477
- );
1478
- const startResult = yield* d.taskLog(
1479
- "Starting sandbox",
1480
- () => startSandbox({
1481
- provider: sandboxProvider,
1482
- hostRepoDir,
1483
- env: effectiveEnv,
1484
- worktreeOrRepoPath,
1485
- gitMounts,
1486
- repoDir: SANDBOX_REPO_DIR
1487
- })
1488
- );
1489
- return startResult.handle;
1403
+ if (hooks?.host?.onWorktreeReady?.length) {
1404
+ yield* runHostHooks(hooks.host.onWorktreeReady, worktreeInfo.path);
1490
1405
  }
1406
+ const startResult = yield* d.taskLog(
1407
+ "Starting sandbox",
1408
+ () => startSandbox({
1409
+ provider: sandboxProvider,
1410
+ hostRepoDir: worktreeInfo.path,
1411
+ sourceRepoDir: hostRepoDir,
1412
+ env: effectiveEnv,
1413
+ copyPaths: options.copyToWorktree,
1414
+ copyTimeoutMs: options.timeouts?.copyToWorktreeMs
1415
+ })
1416
+ );
1417
+ return startResult.handle;
1491
1418
  }).pipe(
1492
1419
  Effect.tapError(
1493
1420
  () => worktreeInfo ? remove(worktreeInfo.path).pipe(
@@ -1504,14 +1431,14 @@ var interactive = async (options) => {
1504
1431
  const interactiveExecFn = handle.interactiveExec.bind(handle);
1505
1432
  const sandbox = makeSandboxFromHandle(handle);
1506
1433
  const worktreePath = handle.worktreePath;
1507
- const applyToHost = sandboxProvider.tag === "isolated" && worktreeInfo ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
1434
+ const applyToHost = () => syncOut(worktreeInfo.path, handle);
1508
1435
  const lifecycleEffect = withSandboxLifecycle(
1509
1436
  {
1510
1437
  hostRepoDir,
1511
1438
  sandboxRepoDir: worktreePath,
1512
1439
  hooks,
1513
1440
  branch: lifecycleBranch,
1514
- hostWorktreePath: isHeadMode ? hostRepoDir : worktreeInfo?.path,
1441
+ hostWorktreePath: worktreeInfo.path,
1515
1442
  applyToHost,
1516
1443
  timeouts: options.timeouts
1517
1444
  },
@@ -1524,7 +1451,7 @@ var interactive = async (options) => {
1524
1451
  );
1525
1452
  const interactiveArgs = provider.buildInteractiveArgs({
1526
1453
  prompt: fullPrompt,
1527
- dangerouslySkipPermissions: sandboxProvider.tag !== "none"
1454
+ dangerouslySkipPermissions: true
1528
1455
  });
1529
1456
  const result4 = yield* raceAbortSignal(
1530
1457
  Effect.promise(
@@ -1596,6 +1523,15 @@ var interactive = async (options) => {
1596
1523
  }
1597
1524
  return result3;
1598
1525
  };
1526
+ var runRequiredSandboxHook = (sandbox, command, cwd, sudo) => Effect.flatMap(
1527
+ sandbox.exec(command, { cwd, sudo }),
1528
+ (result3) => result3.exitCode === 0 ? Effect.succeed(result3) : Effect.fail(
1529
+ new Error(
1530
+ `Sandbox setup failed (exit ${result3.exitCode}): ${command}
1531
+ ${result3.stderr}`
1532
+ )
1533
+ )
1534
+ );
1599
1535
  var buildSandboxHandle = (ctx, close) => {
1600
1536
  const {
1601
1537
  branch,
@@ -1604,7 +1540,7 @@ var buildSandboxHandle = (ctx, close) => {
1604
1540
  sandboxRepoDir,
1605
1541
  sandbox,
1606
1542
  providerHandle,
1607
- bindMountHandle,
1543
+ sessionTransferHandle,
1608
1544
  applyToHost,
1609
1545
  timeouts,
1610
1546
  branchStrategy
@@ -1635,7 +1571,6 @@ var buildSandboxHandle = (ctx, close) => {
1635
1571
  if (runOptions.resumeSession) {
1636
1572
  await assertResumeSessionExists({
1637
1573
  provider,
1638
- sandboxTag: ctx.providerTag,
1639
1574
  hostRepoDir,
1640
1575
  resumeSession: runOptions.resumeSession
1641
1576
  });
@@ -1706,7 +1641,7 @@ var buildSandboxHandle = (ctx, close) => {
1706
1641
  hostWorktreePath: worktreePath,
1707
1642
  sandboxRepoPath: sandboxRepoDir,
1708
1643
  applyToHost,
1709
- bindMountHandle
1644
+ sessionTransferHandle
1710
1645
  },
1711
1646
  sandbox
1712
1647
  ).pipe(
@@ -1926,24 +1861,13 @@ var buildSandboxHandle = (ctx, close) => {
1926
1861
  var createSandboxFromWorktree = async (options) => {
1927
1862
  const { branch, worktreePath, hostRepoDir } = options;
1928
1863
  const isTestMode = !!options._test?.buildSandbox;
1929
- if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
1930
- await Effect.runPromise(
1931
- copyToWorktree(
1932
- options.copyToWorktree,
1933
- hostRepoDir,
1934
- worktreePath,
1935
- options.timeouts?.copyToWorktreeMs
1936
- )
1937
- );
1938
- }
1939
1864
  let providerHandle;
1940
1865
  let sandbox;
1941
1866
  let sandboxRepoDir;
1942
- const isIsolated = options.sandbox.tag === "isolated";
1943
1867
  if (isTestMode) {
1944
1868
  sandbox = options._test.buildSandbox(worktreePath);
1945
1869
  sandboxRepoDir = worktreePath;
1946
- providerHandle = options._test.bindMountHandle;
1870
+ providerHandle = options._test.sessionTransferHandle;
1947
1871
  } else {
1948
1872
  const resolvedEnv = await Effect.runPromise(
1949
1873
  resolveEnv(hostRepoDir).pipe(Effect.provide(NodeContext.layer))
@@ -1954,42 +1878,15 @@ var createSandboxFromWorktree = async (options) => {
1954
1878
  sandboxProviderEnv: options.sandbox.env
1955
1879
  });
1956
1880
  const provider = options.sandbox;
1957
- let startEffect;
1958
- if (provider.tag === "isolated") {
1959
- startEffect = startSandbox({
1960
- provider,
1961
- hostRepoDir: worktreePath,
1962
- sourceRepoDir: hostRepoDir,
1963
- env,
1964
- copyPaths: options.copyToWorktree
1965
- });
1966
- } else if (provider.tag === "none") {
1967
- startEffect = startSandbox({
1968
- provider,
1969
- hostRepoDir,
1970
- env,
1971
- worktreeOrRepoPath: worktreePath
1972
- });
1973
- } else {
1974
- startEffect = resolveGitMounts(join(hostRepoDir, ".git")).pipe(
1975
- Effect.provide(NodeFileSystem.layer),
1976
- Effect.catchAll(() => Effect.succeed([])),
1977
- // Patch git mounts for Windows worktree compatibility (ADR-0006)
1978
- Effect.flatMap(
1979
- (gitMounts) => patchGitMountsForWindows(gitMounts, worktreePath, SANDBOX_REPO_DIR)
1980
- ),
1981
- Effect.flatMap(
1982
- (gitMounts) => startSandbox({
1983
- provider,
1984
- hostRepoDir,
1985
- env,
1986
- worktreeOrRepoPath: worktreePath,
1987
- gitMounts,
1988
- repoDir: SANDBOX_REPO_DIR
1989
- })
1990
- )
1991
- );
1992
- }
1881
+ const startEffect = startSandbox({
1882
+ provider,
1883
+ hostRepoDir: worktreePath,
1884
+ sourceRepoDir: hostRepoDir,
1885
+ copySourceDir: worktreePath,
1886
+ env,
1887
+ copyPaths: options.copyToWorktree,
1888
+ copyTimeoutMs: options.timeouts?.copyToWorktreeMs
1889
+ });
1993
1890
  const startResult = await Effect.runPromise(startEffect);
1994
1891
  providerHandle = startResult.handle;
1995
1892
  sandbox = startResult.sandbox;
@@ -2004,10 +1901,12 @@ var createSandboxFromWorktree = async (options) => {
2004
1901
  `git config --global --add safe.directory ${shellQuote(sandboxRepoDir)}`
2005
1902
  );
2006
1903
  const sandboxEffects = (sandboxOnReady ?? []).map(
2007
- (hook) => sandbox.exec(hook.command, {
2008
- cwd: sandboxRepoDir,
2009
- sudo: hook.sudo
2010
- })
1904
+ (hook) => runRequiredSandboxHook(
1905
+ sandbox,
1906
+ hook.command,
1907
+ sandboxRepoDir,
1908
+ hook.sudo
1909
+ )
2011
1910
  );
2012
1911
  const allEffects = [...sandboxEffects];
2013
1912
  if (hostOnReady?.length) {
@@ -2016,12 +1915,17 @@ var createSandboxFromWorktree = async (options) => {
2016
1915
  yield* Effect.all(allEffects, {
2017
1916
  concurrency: "unbounded"
2018
1917
  });
2019
- })
1918
+ }).pipe(
1919
+ Effect.onError(
1920
+ () => providerHandle ? Effect.promise(() => providerHandle.close().catch(() => {
1921
+ })) : Effect.void
1922
+ )
1923
+ )
2020
1924
  );
2021
1925
  }
2022
- const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
1926
+ const applyToHost = providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
2023
1927
  let closed = false;
2024
- const bindMountHandle = toSessionTransferHandle(providerHandle);
1928
+ const sessionTransferHandle = toSessionTransferHandle(providerHandle);
2025
1929
  return buildSandboxHandle(
2026
1930
  {
2027
1931
  branch,
@@ -2030,8 +1934,7 @@ var createSandboxFromWorktree = async (options) => {
2030
1934
  sandboxRepoDir,
2031
1935
  sandbox,
2032
1936
  providerHandle,
2033
- bindMountHandle,
2034
- providerTag: options.sandbox.tag,
1937
+ sessionTransferHandle,
2035
1938
  applyToHost,
2036
1939
  timeouts: options.timeouts,
2037
1940
  branchStrategy: options.branchStrategy
@@ -2047,7 +1950,6 @@ var createSandboxFromWorktree = async (options) => {
2047
1950
  var createSandbox = async (options) => {
2048
1951
  const { branch } = options;
2049
1952
  const isTestMode = !!options._test?.buildSandbox;
2050
- const isIsolated = options.sandbox.tag === "isolated";
2051
1953
  const { hostRepoDir, worktreePath, providerHandle, sandbox, sandboxRepoDir } = await Effect.runPromise(
2052
1954
  Effect.gen(function* () {
2053
1955
  const hostRepoDir2 = yield* resolveCwd(options.cwd);
@@ -2059,14 +1961,6 @@ var createSandbox = async (options) => {
2059
1961
  { branch, baseBranch: options.baseBranch }
2060
1962
  );
2061
1963
  const prepared = yield* Effect.gen(function* () {
2062
- if (options.copyToWorktree && options.copyToWorktree.length > 0 && options.sandbox.tag !== "isolated") {
2063
- yield* copyToWorktree(
2064
- options.copyToWorktree,
2065
- hostRepoDir2,
2066
- worktreePath2,
2067
- options.timeouts?.copyToWorktreeMs
2068
- );
2069
- }
2070
1964
  if (options.hooks?.host?.onWorktreeReady?.length) {
2071
1965
  yield* runHostHooks(
2072
1966
  options.hooks.host.onWorktreeReady,
@@ -2079,7 +1973,7 @@ var createSandbox = async (options) => {
2079
1973
  if (isTestMode) {
2080
1974
  sandbox2 = options._test.buildSandbox(worktreePath2);
2081
1975
  sandboxRepoDir2 = worktreePath2;
2082
- providerHandle2 = options._test.bindMountHandle;
1976
+ providerHandle2 = options._test.sessionTransferHandle;
2083
1977
  } else {
2084
1978
  const resolvedEnv = yield* resolveEnv(hostRepoDir2);
2085
1979
  const env = mergeProviderEnv({
@@ -2088,39 +1982,14 @@ var createSandbox = async (options) => {
2088
1982
  sandboxProviderEnv: options.sandbox.env
2089
1983
  });
2090
1984
  const provider = options.sandbox;
2091
- const startResult = yield* provider.tag === "isolated" ? startSandbox({
1985
+ const startResult = yield* startSandbox({
2092
1986
  provider,
2093
1987
  hostRepoDir: worktreePath2,
2094
1988
  sourceRepoDir: hostRepoDir2,
2095
1989
  env,
2096
- copyPaths: options.copyToWorktree
2097
- }) : provider.tag === "none" ? startSandbox({
2098
- provider,
2099
- hostRepoDir: hostRepoDir2,
2100
- env,
2101
- worktreeOrRepoPath: worktreePath2
2102
- }) : resolveGitMounts(join(hostRepoDir2, ".git")).pipe(
2103
- Effect.provide(NodeFileSystem.layer),
2104
- Effect.catchAll(() => Effect.succeed([])),
2105
- // Patch git mounts for Windows worktree compatibility (ADR-0006)
2106
- Effect.flatMap(
2107
- (gitMounts) => patchGitMountsForWindows(
2108
- gitMounts,
2109
- worktreePath2,
2110
- SANDBOX_REPO_DIR
2111
- )
2112
- ),
2113
- Effect.flatMap(
2114
- (gitMounts) => startSandbox({
2115
- provider,
2116
- hostRepoDir: hostRepoDir2,
2117
- env,
2118
- worktreeOrRepoPath: worktreePath2,
2119
- gitMounts,
2120
- repoDir: SANDBOX_REPO_DIR
2121
- })
2122
- )
2123
- );
1990
+ copyPaths: options.copyToWorktree,
1991
+ copyTimeoutMs: options.timeouts?.copyToWorktreeMs
1992
+ });
2124
1993
  providerHandle2 = startResult.handle;
2125
1994
  sandbox2 = startResult.sandbox;
2126
1995
  sandboxRepoDir2 = startResult.worktreePath;
@@ -2133,10 +2002,12 @@ var createSandbox = async (options) => {
2133
2002
  `git config --global --add safe.directory ${shellQuote(sandboxRepoDir2)}`
2134
2003
  );
2135
2004
  const sandboxEffects = (sandboxOnReady ?? []).map(
2136
- (hook) => sandbox2.exec(hook.command, {
2137
- cwd: sandboxRepoDir2,
2138
- sudo: hook.sudo
2139
- })
2005
+ (hook) => runRequiredSandboxHook(
2006
+ sandbox2,
2007
+ hook.command,
2008
+ sandboxRepoDir2,
2009
+ hook.sudo
2010
+ )
2140
2011
  );
2141
2012
  const allEffects = [...sandboxEffects];
2142
2013
  if (hostOnReady?.length) {
@@ -2163,7 +2034,7 @@ var createSandbox = async (options) => {
2163
2034
  return { hostRepoDir: hostRepoDir2, worktreePath: worktreePath2, ...prepared };
2164
2035
  }).pipe(Effect.provide(NodeContext.layer))
2165
2036
  );
2166
- const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
2037
+ const applyToHost = providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect.void;
2167
2038
  let closed = false;
2168
2039
  const forceCleanup = () => {
2169
2040
  console.error(`
@@ -2193,7 +2064,7 @@ Worktree preserved at ${worktreePath}`);
2193
2064
  })
2194
2065
  );
2195
2066
  };
2196
- const bindMountHandle = toSessionTransferHandle(providerHandle);
2067
+ const sessionTransferHandle = toSessionTransferHandle(providerHandle);
2197
2068
  return buildSandboxHandle(
2198
2069
  {
2199
2070
  branch,
@@ -2202,8 +2073,7 @@ Worktree preserved at ${worktreePath}`);
2202
2073
  sandboxRepoDir,
2203
2074
  sandbox,
2204
2075
  providerHandle,
2205
- bindMountHandle,
2206
- providerTag: options.sandbox.tag,
2076
+ sessionTransferHandle,
2207
2077
  applyToHost,
2208
2078
  timeouts: options.timeouts
2209
2079
  },
@@ -2213,6 +2083,82 @@ Worktree preserved at ${worktreePath}`);
2213
2083
  }
2214
2084
  );
2215
2085
  };
2086
+ var COPY_TO_WORKTREE_TIMEOUT_MS = 6e4;
2087
+ var getCopyOnWriteFlags = (platform) => platform === "darwin" ? ["-cR"] : ["-R", "--reflink=auto"];
2088
+ var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
2089
+ const effectiveTimeout = timeoutMs ?? COPY_TO_WORKTREE_TIMEOUT_MS;
2090
+ return Effect.gen(function* () {
2091
+ const cowFlags = getCopyOnWriteFlags(process.platform);
2092
+ for (const relativePath of paths) {
2093
+ let src;
2094
+ let dest;
2095
+ try {
2096
+ assertExcludesRepositoryRunner(relativePath);
2097
+ src = resolveSafeRelativePath(hostRepoDir, relativePath, "copy path");
2098
+ dest = resolveSafeRelativePath(worktreePath, relativePath, "copy path");
2099
+ } catch (error) {
2100
+ return yield* Effect.fail(
2101
+ new CopyToWorktreeError({
2102
+ message: `Refusing unsafe copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
2103
+ path: relativePath,
2104
+ stderr: error instanceof Error ? error.message : String(error),
2105
+ exitCode: null
2106
+ })
2107
+ );
2108
+ }
2109
+ if (!existsSync(src)) {
2110
+ continue;
2111
+ }
2112
+ yield* Effect.tryPromise({
2113
+ try: async () => {
2114
+ await assertNoSymlinkComponents(hostRepoDir, src, "copy source");
2115
+ await assertNoSymlinkComponents(worktreePath, dest, "copy target");
2116
+ },
2117
+ catch: (error) => new CopyToWorktreeError({
2118
+ message: `Refusing symlinked copy path ${relativePath}: ${error instanceof Error ? error.message : String(error)}`,
2119
+ path: relativePath,
2120
+ stderr: error instanceof Error ? error.message : String(error),
2121
+ exitCode: null
2122
+ })
2123
+ });
2124
+ yield* Effect.async((resume) => {
2125
+ execFile("cp", [...cowFlags, src, dest], (error) => {
2126
+ if (error) {
2127
+ execFile("cp", ["-R", src, dest], (fallbackError, _, stderr) => {
2128
+ if (fallbackError) {
2129
+ resume(
2130
+ Effect.fail(
2131
+ new CopyToWorktreeError({
2132
+ message: `Failed to copy ${relativePath} to worktree: ${stderr || fallbackError.message}`,
2133
+ path: relativePath,
2134
+ stderr: stderr || fallbackError.message,
2135
+ exitCode: typeof fallbackError.code === "number" ? fallbackError.code : null
2136
+ })
2137
+ )
2138
+ );
2139
+ } else {
2140
+ resume(Effect.succeed(void 0));
2141
+ }
2142
+ });
2143
+ } else {
2144
+ resume(Effect.succeed(void 0));
2145
+ }
2146
+ });
2147
+ });
2148
+ }
2149
+ }).pipe(
2150
+ withTimeout(
2151
+ effectiveTimeout,
2152
+ () => new CopyToWorktreeTimeoutError({
2153
+ message: `Copying files to worktree timed out after ${effectiveTimeout}ms`,
2154
+ timeoutMs: effectiveTimeout,
2155
+ paths
2156
+ })
2157
+ )
2158
+ );
2159
+ };
2160
+
2161
+ // src/createWorktree.ts
2216
2162
  var createWorktree = async (options) => {
2217
2163
  const branch = options.branchStrategy.type === "branch" ? options.branchStrategy.branch : void 0;
2218
2164
  const baseBranch = options.branchStrategy.type === "branch" ? options.branchStrategy.baseBranch : void 0;
@@ -2259,7 +2205,7 @@ var createWorktree = async (options) => {
2259
2205
  const worktreeInteractive = async (opts) => {
2260
2206
  opts.signal?.throwIfAborted();
2261
2207
  const { prompt, promptFile, hooks, agent: provider } = opts;
2262
- const resolvedSandbox = opts.sandbox ?? noSandbox();
2208
+ const resolvedSandbox = opts.sandbox;
2263
2209
  if (!provider.buildInteractiveArgs) {
2264
2210
  throw new Error(
2265
2211
  `Agent provider "${provider.name}" does not support buildInteractiveArgs, required for interactive sessions.`
@@ -2302,46 +2248,19 @@ var createWorktree = async (options) => {
2302
2248
  Sandbox: resolvedSandbox.name,
2303
2249
  Branch: worktreeInfo.branch
2304
2250
  });
2305
- let handle;
2306
- if (resolvedSandbox.tag === "none") {
2307
- handle = yield* Effect.promise(
2308
- () => resolvedSandbox.create({
2309
- worktreePath: worktreeInfo.path,
2310
- env: effectiveEnv
2311
- })
2312
- );
2313
- } else if (resolvedSandbox.tag === "isolated") {
2314
- const startResult = yield* d.taskLog(
2315
- "Starting sandbox",
2316
- () => startSandbox({
2317
- provider: resolvedSandbox,
2318
- hostRepoDir: worktreeInfo.path,
2319
- sourceRepoDir: hostRepoDir,
2320
- env: effectiveEnv
2321
- })
2322
- );
2323
- handle = startResult.handle;
2324
- } else {
2325
- const gitPath = join(hostRepoDir, ".git");
2326
- const rawGitMounts = yield* resolveGitMounts(gitPath);
2327
- const gitMounts = yield* patchGitMountsForWindows(
2328
- rawGitMounts,
2329
- worktreeInfo.path,
2330
- SANDBOX_REPO_DIR
2331
- );
2332
- const startResult = yield* d.taskLog(
2333
- "Starting sandbox",
2334
- () => startSandbox({
2335
- provider: resolvedSandbox,
2336
- hostRepoDir,
2337
- env: effectiveEnv,
2338
- worktreeOrRepoPath: worktreeInfo.path,
2339
- gitMounts,
2340
- repoDir: SANDBOX_REPO_DIR
2341
- })
2342
- );
2343
- handle = startResult.handle;
2344
- }
2251
+ const startResult = yield* d.taskLog(
2252
+ "Starting sandbox",
2253
+ () => startSandbox({
2254
+ provider: resolvedSandbox,
2255
+ hostRepoDir: worktreeInfo.path,
2256
+ sourceRepoDir: hostRepoDir,
2257
+ copySourceDir: worktreeInfo.path,
2258
+ env: effectiveEnv,
2259
+ copyPaths: options.copyToWorktree,
2260
+ copyTimeoutMs: options.timeouts?.copyToWorktreeMs
2261
+ })
2262
+ );
2263
+ const handle = startResult.handle;
2345
2264
  return yield* Effect.gen(function* () {
2346
2265
  if (!handle.interactiveExec) {
2347
2266
  throw new Error(
@@ -2351,7 +2270,7 @@ var createWorktree = async (options) => {
2351
2270
  const interactiveExecFn = handle.interactiveExec.bind(handle);
2352
2271
  const sandbox = makeSandboxFromHandle(handle);
2353
2272
  const worktreePath = handle.worktreePath;
2354
- const applyToHost = resolvedSandbox.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
2273
+ const applyToHost = () => syncOut(worktreeInfo.path, handle);
2355
2274
  const lifecycleEffect = withSandboxLifecycle(
2356
2275
  {
2357
2276
  hostRepoDir,
@@ -2375,7 +2294,7 @@ var createWorktree = async (options) => {
2375
2294
  );
2376
2295
  const interactiveArgs = provider.buildInteractiveArgs({
2377
2296
  prompt: fullPrompt,
2378
- dangerouslySkipPermissions: resolvedSandbox.tag !== "none"
2297
+ dangerouslySkipPermissions: true
2379
2298
  });
2380
2299
  const result3 = yield* raceAbortSignal(
2381
2300
  Effect.promise(
@@ -2438,7 +2357,6 @@ var createWorktree = async (options) => {
2438
2357
  if (opts.resumeSession) {
2439
2358
  await assertResumeSessionExists({
2440
2359
  provider,
2441
- sandboxTag: sandboxProvider.tag,
2442
2360
  hostRepoDir,
2443
2361
  resumeSession: opts.resumeSession
2444
2362
  });
@@ -2473,47 +2391,19 @@ var createWorktree = async (options) => {
2473
2391
  builtInArgKeysSet
2474
2392
  );
2475
2393
  }
2476
- let handle;
2477
- let sandboxRepoDir;
2478
- if (sandboxProvider.tag === "isolated") {
2479
- const startResult = yield* startSandbox({
2480
- provider: sandboxProvider,
2481
- hostRepoDir: worktreeInfo.path,
2482
- sourceRepoDir: hostRepoDir,
2483
- env: effectiveEnv
2484
- });
2485
- handle = startResult.handle;
2486
- sandboxRepoDir = startResult.worktreePath;
2487
- } else if (sandboxProvider.tag === "none") {
2488
- const startResult = yield* startSandbox({
2489
- provider: sandboxProvider,
2490
- hostRepoDir,
2491
- env: effectiveEnv,
2492
- worktreeOrRepoPath: worktreeInfo.path
2493
- });
2494
- handle = startResult.handle;
2495
- sandboxRepoDir = startResult.worktreePath;
2496
- } else {
2497
- const gitPath = join(hostRepoDir, ".git");
2498
- const rawGitMounts = yield* resolveGitMounts(gitPath);
2499
- const gitMounts = yield* patchGitMountsForWindows(
2500
- rawGitMounts,
2501
- worktreeInfo.path,
2502
- SANDBOX_REPO_DIR
2503
- );
2504
- const startResult = yield* startSandbox({
2505
- provider: sandboxProvider,
2506
- hostRepoDir,
2507
- env: effectiveEnv,
2508
- worktreeOrRepoPath: worktreeInfo.path,
2509
- gitMounts,
2510
- repoDir: SANDBOX_REPO_DIR
2511
- });
2512
- handle = startResult.handle;
2513
- sandboxRepoDir = startResult.worktreePath;
2514
- }
2394
+ const startResult = yield* startSandbox({
2395
+ provider: sandboxProvider,
2396
+ hostRepoDir: worktreeInfo.path,
2397
+ sourceRepoDir: hostRepoDir,
2398
+ copySourceDir: worktreeInfo.path,
2399
+ env: effectiveEnv,
2400
+ copyPaths: options.copyToWorktree,
2401
+ copyTimeoutMs: options.timeouts?.copyToWorktreeMs
2402
+ });
2403
+ const handle = startResult.handle;
2404
+ const sandboxRepoDir = startResult.worktreePath;
2515
2405
  const sandbox = makeSandboxFromHandle(handle);
2516
- const applyToHost = sandboxProvider.tag === "isolated" ? () => syncOut(worktreeInfo.path, handle) : () => Effect.void;
2406
+ const applyToHost = () => syncOut(worktreeInfo.path, handle);
2517
2407
  const resolvedLogging = opts.logging ?? {
2518
2408
  type: "file",
2519
2409
  path: buildDefaultLogPath(
@@ -2546,14 +2436,14 @@ var createWorktree = async (options) => {
2546
2436
  NodeFileSystem.layer
2547
2437
  );
2548
2438
  })() : ClackDisplay.layer;
2549
- const bindMountHandle = toSessionTransferHandle(handle);
2439
+ const sessionTransferHandle = toSessionTransferHandle(handle);
2550
2440
  const reuseFactoryLayer = Layer.succeed(SandboxFactory, {
2551
2441
  withSandbox: (makeEffect) => makeEffect(
2552
2442
  {
2553
2443
  hostWorktreePath: worktreeInfo.path,
2554
2444
  sandboxRepoPath: sandboxRepoDir,
2555
2445
  applyToHost,
2556
- bindMountHandle
2446
+ sessionTransferHandle
2557
2447
  },
2558
2448
  sandbox
2559
2449
  ).pipe(
@@ -2634,13 +2524,28 @@ var createWorktree = async (options) => {
2634
2524
  }
2635
2525
  };
2636
2526
  const worktreeCreateSandbox = async (opts) => {
2527
+ if (opts.copyToWorktree?.length) {
2528
+ await Effect.runPromise(
2529
+ copyToWorktree(
2530
+ opts.copyToWorktree,
2531
+ hostRepoDir,
2532
+ worktreeInfo.path,
2533
+ opts.timeouts?.copyToWorktreeMs ?? options.timeouts?.copyToWorktreeMs
2534
+ )
2535
+ );
2536
+ }
2637
2537
  return createSandboxFromWorktree({
2638
2538
  branch: worktreeInfo.branch,
2639
2539
  worktreePath: worktreeInfo.path,
2640
2540
  hostRepoDir,
2641
2541
  sandbox: opts.sandbox,
2642
2542
  hooks: opts.hooks,
2643
- copyToWorktree: opts.copyToWorktree,
2543
+ copyToWorktree: [
2544
+ .../* @__PURE__ */ new Set([
2545
+ ...options.copyToWorktree ?? [],
2546
+ ...opts.copyToWorktree ?? []
2547
+ ])
2548
+ ],
2644
2549
  timeouts: opts.timeouts,
2645
2550
  branchStrategy: options.branchStrategy,
2646
2551
  _test: opts._test
@@ -3009,8 +2914,7 @@ var makeClaudeSessionStorage = (options) => {
3009
2914
  sandboxProjectsDir
3010
2915
  );
3011
2916
  await writeSandboxFile(handle, sandboxPath, rewritten, "claude-res");
3012
- },
3013
- findByIdOnHost: (id) => findClaudeSessionOnHost(id, hostProjectsDir)
2917
+ }
3014
2918
  };
3015
2919
  };
3016
2920
  var makeCodexSessionStorage = (options) => {
@@ -3048,8 +2952,7 @@ var makeCodexSessionStorage = (options) => {
3048
2952
  const rewritten = transferCodexSession(jsonl, hostCwd, sandboxCwd);
3049
2953
  const target = posix.join(sandboxSessionsDir, located.relativePath);
3050
2954
  await writeSandboxFile(handle, target, rewritten, "codex-res");
3051
- },
3052
- findByIdOnHost: (id) => findCodexSessionOnHost(id, hostSessionsDir)
2955
+ }
3053
2956
  };
3054
2957
  };
3055
2958
  var parseCodexUsage = (usage) => {
@@ -3096,7 +2999,7 @@ var parseCodexStreamLine = (line) => {
3096
2999
  };
3097
3000
  var codex = (model, options) => {
3098
3001
  const modelId = typeof model === "string" ? model : model.model;
3099
- const effort = options?.effort ?? (typeof model === "string" ? CONFIGURED_CODEX_MODEL_EFFORTS.get(modelId) : model.effort);
3002
+ const effort = options?.effort === null ? void 0 : options?.effort ?? (typeof model === "string" ? CONFIGURED_CODEX_MODEL_EFFORTS.get(modelId) : model.effort);
3100
3003
  return {
3101
3004
  name: "codex",
3102
3005
  env: options?.env ?? {},
@@ -3357,7 +3260,18 @@ var createWorkBrief = (input) => {
3357
3260
  "acceptanceCriteria"
3358
3261
  ),
3359
3262
  exclusions: stringArray(input.exclusions, "exclusions"),
3360
- risk: enumValue(input.risk, ["low", "medium", "high", "critical"], "risk"),
3263
+ risk: enumValue(
3264
+ input.risk,
3265
+ ["low", "medium", "high", "critical", "unknown"],
3266
+ "risk"
3267
+ ),
3268
+ ...input.scope === void 0 ? {} : {
3269
+ scope: enumValue(
3270
+ input.scope,
3271
+ ["small", "substantial", "unknown"],
3272
+ "scope"
3273
+ )
3274
+ },
3361
3275
  verification: parseVerification(input.verification),
3362
3276
  unresolvedQuestions: stringArray(
3363
3277
  input.unresolvedQuestions,
@@ -3397,7 +3311,18 @@ var parseWorkBrief = (value) => {
3397
3311
  "acceptanceCriteria"
3398
3312
  ),
3399
3313
  exclusions: stringArray(value.exclusions, "exclusions"),
3400
- risk: enumValue(value.risk, ["low", "medium", "high", "critical"], "risk"),
3314
+ risk: enumValue(
3315
+ value.risk,
3316
+ ["low", "medium", "high", "critical", "unknown"],
3317
+ "risk"
3318
+ ),
3319
+ ...value.scope === void 0 ? {} : {
3320
+ scope: enumValue(
3321
+ value.scope,
3322
+ ["small", "substantial", "unknown"],
3323
+ "scope"
3324
+ )
3325
+ },
3401
3326
  verification: parseVerification(value.verification),
3402
3327
  unresolvedQuestions: stringArray(
3403
3328
  value.unresolvedQuestions,
@@ -3453,6 +3378,28 @@ var parseRepositoryPolicy = (value) => {
3453
3378
  if (!isRecord(value.worker)) {
3454
3379
  throw new ContractValidationError("policy.worker must be an object");
3455
3380
  }
3381
+ const workerModels = value.worker.models === void 0 ? void 0 : (() => {
3382
+ if (value.worker.model !== void 0) {
3383
+ throw new ContractValidationError(
3384
+ "policy.worker.model cannot be combined with policy.worker.models"
3385
+ );
3386
+ }
3387
+ if (!isRecord(value.worker.models)) {
3388
+ throw new ContractValidationError(
3389
+ "policy.worker.models must be an object"
3390
+ );
3391
+ }
3392
+ return {
3393
+ routine: nonEmptyString(
3394
+ value.worker.models.routine,
3395
+ "policy.worker.models.routine"
3396
+ ),
3397
+ strong: nonEmptyString(
3398
+ value.worker.models.strong,
3399
+ "policy.worker.models.strong"
3400
+ )
3401
+ };
3402
+ })();
3456
3403
  if (!Array.isArray(value.checks)) {
3457
3404
  throw new ContractValidationError("policy.checks must be an array");
3458
3405
  }
@@ -3546,7 +3493,7 @@ var parseRepositoryPolicy = (value) => {
3546
3493
  authorization: { required, allowedActors, autoStartRisk },
3547
3494
  worker: {
3548
3495
  provider: nonEmptyString(value.worker.provider, "policy.worker.provider"),
3549
- model: nonEmptyString(value.worker.model, "policy.worker.model"),
3496
+ ...workerModels === void 0 ? { model: nonEmptyString(value.worker.model, "policy.worker.model") } : { models: workerModels },
3550
3497
  sandbox: nonEmptyString(value.worker.sandbox, "policy.worker.sandbox"),
3551
3498
  skillRevision: nonEmptyString(
3552
3499
  value.worker.skillRevision,
@@ -3562,6 +3509,16 @@ var createRepositoryPolicy = (input) => parseRepositoryPolicy({
3562
3509
  contractVersion: WORKFLOW_CONTRACT_VERSION,
3563
3510
  ...input
3564
3511
  });
3512
+ var resolveAgentSelection = (policy, phase2, risk, scope) => {
3513
+ const role = phase2 === "review" && !(risk === "low" && scope === "small") ? "strong" : "routine";
3514
+ const worker = policy.worker;
3515
+ const modelPath = worker.models ? `policy.worker.models.${role}` : "policy.worker.model";
3516
+ return Object.freeze({
3517
+ provider: nonEmptyString(worker.provider, "policy.worker.provider"),
3518
+ model: nonEmptyString(worker.models?.[role] ?? worker.model, modelPath),
3519
+ role
3520
+ });
3521
+ };
3565
3522
  var parseCheckEvidence = (value) => {
3566
3523
  if (!isRecord(value)) {
3567
3524
  throw new ContractValidationError("check evidence must be an object");
@@ -3700,6 +3657,12 @@ var createAssignment = (input) => {
3700
3657
  briefHash: brief.hash,
3701
3658
  policyRevision: input.policy.revision,
3702
3659
  skillRevision: brief.skillRevision,
3660
+ agentSelection: resolveAgentSelection(
3661
+ input.policy,
3662
+ input.phase,
3663
+ brief.risk,
3664
+ brief.scope
3665
+ ),
3703
3666
  base: brief.base,
3704
3667
  head: input.head,
3705
3668
  createdAt: nonEmptyString(input.createdAt, "assignment.createdAt")
@@ -4069,6 +4032,25 @@ var parseAssignment = (value) => {
4069
4032
  throw new Error("Stored assignment base must be an object");
4070
4033
  }
4071
4034
  const head = candidate.head;
4035
+ const agentSelection = candidate.agentSelection;
4036
+ let parsedAgentSelection;
4037
+ if (agentSelection !== void 0) {
4038
+ if (typeof agentSelection !== "object" || agentSelection === null || Array.isArray(agentSelection)) {
4039
+ throw new Error("Stored assignment agent selection must be an object");
4040
+ }
4041
+ const selection = agentSelection;
4042
+ if (selection.role !== "routine" && selection.role !== "strong") {
4043
+ throw new Error("Stored assignment agent selection role is invalid");
4044
+ }
4045
+ parsedAgentSelection = {
4046
+ provider: requiredString(
4047
+ selection.provider,
4048
+ "assignment.agentSelection.provider"
4049
+ ),
4050
+ model: requiredString(selection.model, "assignment.agentSelection.model"),
4051
+ role: selection.role
4052
+ };
4053
+ }
4072
4054
  const revision = (value2, path2) => {
4073
4055
  if (typeof value2 !== "object" || value2 === null || Array.isArray(value2)) {
4074
4056
  throw new Error(`${path2} must be an object`);
@@ -4112,6 +4094,7 @@ var parseAssignment = (value) => {
4112
4094
  candidate.skillRevision,
4113
4095
  "assignment.skillRevision"
4114
4096
  ),
4097
+ ...parsedAgentSelection === void 0 ? {} : { agentSelection: parsedAgentSelection },
4115
4098
  base: revision(base, "assignment.base"),
4116
4099
  head: head === void 0 ? void 0 : revision(head, "assignment.head"),
4117
4100
  createdAt: requiredString(candidate.createdAt, "assignment.createdAt")
@@ -4301,9 +4284,6 @@ var PostgresTransaction = class {
4301
4284
  async one(text2, values, path2) {
4302
4285
  return row(await this.client.query(text2, values), path2);
4303
4286
  }
4304
- async optional(text2, values) {
4305
- return optionalRow(await this.client.query(text2, values));
4306
- }
4307
4287
  async insertEventIfAbsent(event) {
4308
4288
  const inserted = await this.client.query(
4309
4289
  `INSERT INTO shipyard_events (
@@ -6031,11 +6011,23 @@ var cancelledResult = (assignment, response, artifacts, message, kind) => ({
6031
6011
  evidence: response.report.evidence
6032
6012
  })
6033
6013
  });
6014
+ var sameAgentSelection = (left, right) => left.provider === right.provider && left.model === right.model && left.role === right.role;
6034
6015
  var executePhase = async (options) => {
6035
6016
  const assignment = deepFreeze(structuredClone(options.assignment));
6036
6017
  const trusted = deepFreeze(structuredClone(options.trusted));
6037
6018
  const untrusted = deepFreeze(structuredClone(options.untrusted));
6038
6019
  const controls = deepFreeze(structuredClone(options.controls));
6020
+ const resolvedSelection = resolveAgentSelection(
6021
+ trusted.policy,
6022
+ assignment.phase,
6023
+ trusted.brief.risk,
6024
+ trusted.brief.scope
6025
+ );
6026
+ const persistedSelection = assignment.agentSelection;
6027
+ if (persistedSelection !== void 0 && !sameAgentSelection(persistedSelection, resolvedSelection)) {
6028
+ throw new Error("Assignment agent selection does not match trusted policy");
6029
+ }
6030
+ const agentSelection = persistedSelection ?? resolvedSelection;
6039
6031
  const secretValues = /* @__PURE__ */ new Set();
6040
6032
  const controller = new AbortController();
6041
6033
  let timedOut = false;
@@ -6049,6 +6041,7 @@ var executePhase = async (options) => {
6049
6041
  }, options.controls.timeoutSeconds * 1e3);
6050
6042
  const request = {
6051
6043
  assignment,
6044
+ agentSelection,
6052
6045
  trusted,
6053
6046
  untrusted,
6054
6047
  controls,
@@ -6293,6 +6286,23 @@ var executePhase = async (options) => {
6293
6286
  })
6294
6287
  };
6295
6288
  };
6289
+ var resolvePhaseAgent = (options, request) => {
6290
+ if (options.resolveAgent === void 0 && request.trusted.policy.worker.models !== void 0) {
6291
+ throw new Error(
6292
+ "An agent resolver is required when repository policy defines worker models"
6293
+ );
6294
+ }
6295
+ const agent = options.resolveAgent === void 0 ? options.agent : options.resolveAgent(request.agentSelection);
6296
+ if (agent === void 0) {
6297
+ throw new Error("The phase engine has no agent for the selected model");
6298
+ }
6299
+ if (agent.name !== request.agentSelection.provider) {
6300
+ throw new Error(
6301
+ `Resolved agent provider "${agent.name}" does not match repository policy provider "${request.agentSelection.provider}"`
6302
+ );
6303
+ }
6304
+ return agent;
6305
+ };
6296
6306
  var runResultToResponse = (result3, branch) => {
6297
6307
  const commits = result3.commits.map((commit) => commit.sha);
6298
6308
  const headSha = commits.at(-1) ?? "";
@@ -6317,11 +6327,12 @@ var runResultToResponse = (result3, branch) => {
6317
6327
  };
6318
6328
  var createRunPhaseEngineAdapter = (options) => ({
6319
6329
  execute: async (request) => {
6320
- assertToolAllowlistSupport(options.agent);
6330
+ const agent = resolvePhaseAgent(options, request);
6331
+ assertToolAllowlistSupport(agent);
6321
6332
  const branch = request.checkout.branch;
6322
6333
  const baseBranch = request.checkout.immutable ? request.checkout.candidate?.sha ?? request.assignment.base.sha : request.assignment.base.sha;
6323
6334
  const result3 = await options.run({
6324
- agent: options.agent,
6335
+ agent,
6325
6336
  sandbox: options.sandbox,
6326
6337
  cwd: options.cwd,
6327
6338
  prompt: buildPrompt(request),
@@ -6359,7 +6370,8 @@ var sandboxResultToResponse = (result3, branch) => {
6359
6370
  };
6360
6371
  var createCreateSandboxPhaseEngineAdapter = (options) => ({
6361
6372
  execute: async (request) => {
6362
- assertToolAllowlistSupport(options.agent);
6373
+ const agent = resolvePhaseAgent(options, request);
6374
+ assertToolAllowlistSupport(agent);
6363
6375
  const branch = request.checkout.branch;
6364
6376
  const baseBranch = request.checkout.immutable ? request.checkout.candidate?.sha ?? request.assignment.base.sha : request.assignment.base.sha;
6365
6377
  const sandbox = await options.createSandbox({
@@ -6370,7 +6382,7 @@ var createCreateSandboxPhaseEngineAdapter = (options) => ({
6370
6382
  });
6371
6383
  try {
6372
6384
  const result3 = await sandbox.run({
6373
- agent: options.agent,
6385
+ agent,
6374
6386
  prompt: buildPrompt(request),
6375
6387
  toolAllowlist: request.controls.toolAllowlist,
6376
6388
  maxIterations: request.controls.maxIterations,
@@ -9012,22 +9024,6 @@ var GitHubIntegration = class {
9012
9024
  reason: ingest.reason
9013
9025
  };
9014
9026
  }
9015
- async recordRejected(input) {
9016
- const payloadRecord = record(input.payload);
9017
- const delivery = {
9018
- deliveryId: input.deliveryId,
9019
- eventName: input.eventName,
9020
- repository: payloadRecord ? repositoryName(payloadRecord) ?? "unknown" : "unknown",
9021
- senderLogin: payloadRecord ? sender(payloadRecord).login : "unknown",
9022
- receivedAt: this.now(),
9023
- payloadHash: payloadHash(input.body),
9024
- payload: input.payload,
9025
- status: "rejected",
9026
- reason: input.reason
9027
- };
9028
- const stored = await this.options.deliveryStore.recordDeliveryIfAbsent(delivery);
9029
- if (!stored.inserted) return;
9030
- }
9031
9027
  async rejectStored(delivery, reason) {
9032
9028
  await this.options.deliveryStore.updateDelivery({
9033
9029
  ...delivery,
@@ -9952,6 +9948,6 @@ var ReleaseIntegration = class {
9952
9948
  };
9953
9949
  var candidateMatchesApproval = (candidate, approval) => sameCandidate(candidate, approval.candidate);
9954
9950
 
9955
- export { ContractValidationError, CredentialNotAllowedError, CredentialUnavailableError, CwdError2 as CwdError, GitHubIntegration, GitHubPublication, InMemoryCoordinatorStorage, InMemoryGitHubStore, InMemoryReleaseStore, InMemoryRepairBatchStore, InMemoryTriageStore, LeaseBusyError, LeaseLostError, Output, PostgresCoordinatorStorage, ReleaseIntegration, StructuredOutputError, WORKFLOW_CONTRACT_VERSION, WorkflowCoordinator, WorkflowOperator, candidateMatchesApproval, claudeCode, claudeHostSessionPath, claudeSandboxSessionPath, closeSourceIssue, codex, completeSourceIssue, createAssignment, createCreateSandboxPhaseEngineAdapter, createFakePhaseEngineAdapter, createGitHubIssueBrief, createInMemoryArtifactStore, createRepositoryPolicy, createRunPhaseEngineAdapter, createSandbox, createWorkBrief, createWorktree, defaultInvestigator as defaultTriageInvestigator, encodeProjectPath, evaluateHandoffReadiness, executePhase, findClaudeSessionOnHost, findCodexSessionOnHost, interactive, isAuthorizationAllowed, mergeProtectedCandidate, parseCheckEvidence, parsePhaseResult, parseRepositoryPolicy, parseWorkBrief, prepareHumanHandoff, prepareIndependentReview, processHumanReviewDecision, redact2 as redactOperatorText, releaseCandidateKey, requireTransition, resolveHumanReviewDecision, reviewDecisionFor, run, runAuthorizedImplementation, runBoundedRepair, runIndependentReview, runTriage, scheduleBoundedRepair, transferClaudeSession, transferCodexSession, verifyGitHubWebhookSignature };
9951
+ export { ContractValidationError, CredentialNotAllowedError, CredentialUnavailableError, CwdError2 as CwdError, GitHubIntegration, GitHubPublication, InMemoryCoordinatorStorage, InMemoryGitHubStore, InMemoryReleaseStore, InMemoryRepairBatchStore, InMemoryTriageStore, LeaseBusyError, LeaseLostError, Output, PostgresCoordinatorStorage, ReleaseIntegration, StructuredOutputError, WORKFLOW_CONTRACT_VERSION, WorkflowCoordinator, WorkflowOperator, candidateMatchesApproval, claudeCode, claudeHostSessionPath, claudeSandboxSessionPath, closeSourceIssue, codex, completeSourceIssue, createAssignment, createCreateSandboxPhaseEngineAdapter, createFakePhaseEngineAdapter, createGitHubIssueBrief, createInMemoryArtifactStore, createRepositoryPolicy, createRunPhaseEngineAdapter, createSandbox, createWorkBrief, createWorktree, defaultInvestigator as defaultTriageInvestigator, encodeProjectPath, evaluateHandoffReadiness, executePhase, findClaudeSessionOnHost, findCodexSessionOnHost, interactive, isAuthorizationAllowed, mergeProtectedCandidate, parseCheckEvidence, parsePhaseResult, parseRepositoryPolicy, parseWorkBrief, prepareHumanHandoff, prepareIndependentReview, processHumanReviewDecision, redact2 as redactOperatorText, releaseCandidateKey, requireTransition, resolveAgentSelection, resolveHumanReviewDecision, reviewDecisionFor, run, runAuthorizedImplementation, runBoundedRepair, runIndependentReview, runTriage, scheduleBoundedRepair, transferClaudeSession, transferCodexSession, verifyGitHubWebhookSignature };
9956
9952
  //# sourceMappingURL=index.js.map
9957
9953
  //# sourceMappingURL=index.js.map