@ai-hero/sandcastle 0.9.0 → 0.12.0

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/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from 'node:module';
2
- import { NodeContext_exports, NodeFileSystem_exports, formatErrorMessage } from './chunk-52CIJF45.js';
3
- import { Context_exports, CwdError, Effect_exports, resolveCwd, getCurrentBranch, generateTempBranchName, Layer_exports, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, patchGitMountsForWindows, registerShutdown, SandboxFactory, PromptError, FileSystem_exports, SessionCaptureError, Clock_exports, Duration_exports, Option_exports, PromptExpansionTimeoutError, Deferred_exports, AgentError, Ref_exports, SilentDisplay, AgentIdleTimeoutError, Fiber_exports } from './chunk-5VM5QZ26.js';
2
+ import { NodeContext_exports, NodeFileSystem_exports, formatErrorMessage } from './chunk-DJRHWPEH.js';
3
+ import { Context_exports, CwdError, Effect_exports, resolveCwd, getCurrentBranch, generateTempBranchName, Layer_exports, FileDisplay, ClackDisplay, WorktreeDockerSandboxFactory, SandboxConfig, Display, pruneStale, create, copyToWorktree, runHostHooks, startSandbox, resolveGitMounts, patchGitMountsForWindows, SANDBOX_REPO_DIR, remove, makeSandboxFromHandle, syncOut, withSandboxLifecycle, hasUncommittedChanges, registerShutdown, SandboxFactory, PromptError, FileSystem_exports, SessionCaptureError, Clock_exports, Duration_exports, Option_exports, PromptExpansionTimeoutError, Deferred_exports, AgentError, Ref_exports, SilentDisplay, AgentIdleTimeoutError, Fiber_exports } from './chunk-VOG34SRF.js';
4
4
  export { createBindMountSandboxProvider, createIsolatedSandboxProvider } from './chunk-BIWNFKGV.js';
5
- import { noSandbox } from './chunk-72UVAC7B.js';
5
+ import { noSandbox } from './chunk-62WN33RK.js';
6
6
  import './chunk-NGBM7T3E.js';
7
+ import { mkdirSync, appendFileSync } from 'fs';
7
8
  import path, { join, posix, dirname, relative } from 'path';
8
9
  import { styleText } from 'util';
9
10
  import * as clack from '@clack/prompts';
@@ -177,7 +178,7 @@ var TextDeltaBuffer = class {
177
178
 
178
179
  // src/Orchestrator.ts
179
180
  var IDLE_WARNING_INTERVAL_MS = 6e4;
180
- var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal) => Effect_exports.gen(function* () {
181
+ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, completionTimeoutMs, completionSignals, onText, onToolCall, onRawLine, onIdleWarning, onCompletionTimeout, idleWarningIntervalMs = IDLE_WARNING_INTERVAL_MS, resumeSession, forkSession, signal) => Effect_exports.gen(function* () {
181
182
  let resultText = "";
182
183
  let sessionId;
183
184
  let usage;
@@ -256,6 +257,10 @@ var invokeAgent = (sandbox, sandboxRepoDir, prompt, provider, idleTimeoutMs, com
256
257
  });
257
258
  const execResult = yield* sandbox.exec(printCmd.command, {
258
259
  onLine: (line) => {
260
+ try {
261
+ onRawLine(line);
262
+ } catch {
263
+ }
259
264
  for (const parsed of provider.parseStreamLine(line)) {
260
265
  if (parsed.type === "text") {
261
266
  onText(parsed.text);
@@ -367,7 +372,8 @@ var orchestrate = (options) => {
367
372
  hostWorktreePath,
368
373
  applyToHost,
369
374
  signal: options.signal,
370
- timeouts: options.timeouts
375
+ timeouts: options.timeouts,
376
+ keepSourceBranch: options.keepSourceBranch
371
377
  },
372
378
  sandbox,
373
379
  (ctx) => Effect_exports.gen(function* () {
@@ -395,7 +401,7 @@ var orchestrate = (options) => {
395
401
  );
396
402
  yield* display.status(label("Agent started"), "success");
397
403
  const textBuffer = new TextDeltaBuffer((chunk) => {
398
- Effect_exports.runPromise(display.text(chunk));
404
+ Effect_exports.runPromise(display.textChunk(chunk));
399
405
  Effect_exports.runPromise(
400
406
  streamEmitter.emit({
401
407
  type: "text",
@@ -421,6 +427,16 @@ var orchestrate = (options) => {
421
427
  })
422
428
  );
423
429
  };
430
+ const onRawLine = (line) => {
431
+ Effect_exports.runPromise(
432
+ streamEmitter.emit({
433
+ type: "raw",
434
+ line,
435
+ iteration: i,
436
+ timestamp: /* @__PURE__ */ new Date()
437
+ })
438
+ );
439
+ };
424
440
  const onIdleWarning = (minutes) => {
425
441
  const msg = minutes === 1 ? "Agent idle for 1 minute" : `Agent idle for ${minutes} minutes`;
426
442
  Effect_exports.runPromise(display.status(label(msg), "warn"));
@@ -449,6 +465,7 @@ var orchestrate = (options) => {
449
465
  completionSignals,
450
466
  onText,
451
467
  onToolCall,
468
+ onRawLine,
452
469
  onIdleWarning,
453
470
  onCompletionTimeout,
454
471
  options._idleWarningIntervalMs,
@@ -735,20 +752,30 @@ var Output = {
735
752
  * Declare an object-typed structured output extracted from an XML tag in
736
753
  * the agent's stdout. The tag contents are JSON-parsed (with fence-aware
737
754
  * unwrapping) and validated against the provided Standard Schema validator.
755
+ *
756
+ * Set `maxRetries` to have `run()` automatically resume the failed session
757
+ * and ask the agent to re-emit corrected output when extraction or
758
+ * validation fails. Default: `0` (no retries).
738
759
  */
739
760
  object: (opts) => ({
740
761
  _tag: "object",
741
762
  tag: opts.tag,
742
- schema: opts.schema
763
+ schema: opts.schema,
764
+ maxRetries: opts.maxRetries
743
765
  }),
744
766
  /**
745
767
  * Declare a string-typed structured output extracted from an XML tag in
746
768
  * the agent's stdout. The tag contents are whitespace-trimmed and returned
747
769
  * as a plain string — no JSON parsing, no schema validation.
770
+ *
771
+ * Set `maxRetries` to have `run()` automatically resume the failed session
772
+ * and ask the agent to re-emit corrected output when extraction fails.
773
+ * Default: `0` (no retries).
748
774
  */
749
775
  string: (opts) => ({
750
776
  _tag: "string",
751
- tag: opts.tag
777
+ tag: opts.tag,
778
+ maxRetries: opts.maxRetries
752
779
  })
753
780
  };
754
781
  var StructuredOutputError = class extends Error {
@@ -854,6 +881,24 @@ var unwrapFences = (text2) => {
854
881
  };
855
882
 
856
883
  // src/run.ts
884
+ var buildStructuredOutputRetryFeedback = (error, retriesRemaining) => {
885
+ const raw = error.rawMatched === void 0 ? "(no matching tag was emitted)" : error.rawMatched;
886
+ const cause = error.cause === void 0 ? "(no parser detail)" : typeof error.cause === "string" ? error.cause : JSON.stringify(error.cause, null, 2);
887
+ return `Your previous response did not produce valid structured output.
888
+
889
+ Retries remaining after this attempt: ${retriesRemaining}.
890
+
891
+ Problem:
892
+ ${error.message}
893
+
894
+ Parser detail:
895
+ ${cause}
896
+
897
+ Previous matched output:
898
+ ${raw}
899
+
900
+ Emit only a corrected <${error.tag}> block. Do not change files or run commands.`;
901
+ };
857
902
  var DEFAULT_MAX_ITERATIONS = 1;
858
903
  var sanitizeBranchForFilename = (branch) => branch.replace(/[/\\:*?"<>|]/g, "-");
859
904
  var printFileDisplayStartup = (options) => {
@@ -896,6 +941,40 @@ var formatContextWindowSize = (usage) => {
896
941
  return `${Math.ceil(total / 1e3)}k`;
897
942
  };
898
943
  var buildContextWindowLines = (iterations) => iterations.filter((it) => it.usage !== void 0).map((it) => `Context window: ${formatContextWindowSize(it.usage)}`);
944
+ var buildAgentStreamHandler = (logging) => {
945
+ const userHandler = logging.type === "file" ? logging.onAgentStreamEvent : void 0;
946
+ const verboseSink = logging.verbose ? buildVerboseRawLineSink(logging) : void 0;
947
+ if (!userHandler && !verboseSink) return void 0;
948
+ return (event) => {
949
+ if (userHandler) {
950
+ try {
951
+ userHandler(event);
952
+ } catch {
953
+ }
954
+ }
955
+ if (verboseSink && event.type === "raw") {
956
+ verboseSink(event.line);
957
+ }
958
+ };
959
+ };
960
+ var buildVerboseRawLineSink = (logging) => {
961
+ if (logging.type === "file") {
962
+ const logPath = logging.path;
963
+ try {
964
+ mkdirSync(path.dirname(logPath), { recursive: true });
965
+ } catch {
966
+ }
967
+ return (line) => {
968
+ try {
969
+ appendFileSync(logPath, line + "\n");
970
+ } catch {
971
+ }
972
+ };
973
+ }
974
+ return (line) => {
975
+ process.stdout.write(line + "\n");
976
+ };
977
+ };
899
978
  async function run(options) {
900
979
  options.signal?.throwIfAborted();
901
980
  const {
@@ -932,6 +1011,17 @@ async function run(options) {
932
1011
  "output requires maxIterations to be 1. Structured output is only supported for single-iteration runs."
933
1012
  );
934
1013
  }
1014
+ const outputMaxRetries = options.output?.maxRetries ?? 0;
1015
+ if (outputMaxRetries < 0 || !Number.isInteger(outputMaxRetries)) {
1016
+ throw new Error(
1017
+ `output.maxRetries must be a non-negative integer. Received: ${outputMaxRetries}`
1018
+ );
1019
+ }
1020
+ if (outputMaxRetries > 0 && !provider.sessionStorage) {
1021
+ throw new Error(
1022
+ `output.maxRetries requires an agent provider that supports session resumption. The "${provider.name}" provider does not. Use claudeCode, codex, or pi, or set maxRetries to 0.`
1023
+ );
1024
+ }
935
1025
  const branch = branchStrategy.type === "branch" ? branchStrategy.branch : void 0;
936
1026
  const hostRepoDir = await Effect_exports.runPromise(
937
1027
  resolveCwd(options.cwd).pipe(Effect_exports.provide(NodeContext_exports.layer))
@@ -1013,7 +1103,7 @@ async function run(options) {
1013
1103
  )
1014
1104
  );
1015
1105
  const streamEmitterLayer = agentStreamEmitterLayer(
1016
- resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
1106
+ buildAgentStreamHandler(resolvedLogging)
1017
1107
  );
1018
1108
  const runLayer = Layer_exports.mergeAll(
1019
1109
  factoryLayer,
@@ -1133,18 +1223,41 @@ async function run(options) {
1133
1223
  };
1134
1224
  if (options.output) {
1135
1225
  const lastIteration = baseResult.iterations.at(-1);
1136
- const output = await extractStructuredOutput(
1137
- baseResult.stdout,
1138
- options.output,
1139
- {
1140
- commits: baseResult.commits,
1141
- branch: baseResult.branch,
1142
- preservedWorktreePath: baseResult.preservedWorktreePath,
1143
- sessionId: lastIteration?.sessionId,
1144
- sessionFilePath: lastIteration?.sessionFilePath
1226
+ try {
1227
+ const output = await extractStructuredOutput(
1228
+ baseResult.stdout,
1229
+ options.output,
1230
+ {
1231
+ commits: baseResult.commits,
1232
+ branch: baseResult.branch,
1233
+ preservedWorktreePath: baseResult.preservedWorktreePath,
1234
+ sessionId: lastIteration?.sessionId,
1235
+ sessionFilePath: lastIteration?.sessionFilePath
1236
+ }
1237
+ );
1238
+ return { ...baseResult, output };
1239
+ } catch (error) {
1240
+ if (error instanceof StructuredOutputError && outputMaxRetries > 0 && error.sessionId !== void 0) {
1241
+ const retriesRemainingAfter = outputMaxRetries - 1;
1242
+ const retryOutput = {
1243
+ ...options.output,
1244
+ maxRetries: retriesRemainingAfter
1245
+ };
1246
+ return run({
1247
+ ...options,
1248
+ prompt: buildStructuredOutputRetryFeedback(
1249
+ error,
1250
+ retriesRemainingAfter
1251
+ ),
1252
+ promptFile: void 0,
1253
+ promptArgs: void 0,
1254
+ resumeSession: error.sessionId,
1255
+ forkSession: false,
1256
+ output: retryOutput
1257
+ });
1145
1258
  }
1146
- );
1147
- return { ...baseResult, output };
1259
+ throw error;
1260
+ }
1148
1261
  }
1149
1262
  return baseResult;
1150
1263
  }
@@ -1313,14 +1426,20 @@ var interactive = async (options) => {
1313
1426
  return startResult.handle;
1314
1427
  } else {
1315
1428
  const gitPath = join(hostRepoDir, ".git");
1316
- const gitMounts = yield* resolveGitMounts(gitPath);
1429
+ const rawGitMounts = yield* resolveGitMounts(gitPath);
1430
+ const worktreeOrRepoPath = isHeadMode ? hostRepoDir : worktreeInfo.path;
1431
+ const gitMounts = yield* patchGitMountsForWindows(
1432
+ rawGitMounts,
1433
+ worktreeOrRepoPath,
1434
+ SANDBOX_REPO_DIR
1435
+ );
1317
1436
  const startResult = yield* d.taskLog(
1318
1437
  "Starting sandbox",
1319
1438
  () => startSandbox({
1320
1439
  provider: sandboxProvider,
1321
1440
  hostRepoDir,
1322
1441
  env: effectiveEnv,
1323
- worktreeOrRepoPath: isHeadMode ? hostRepoDir : worktreeInfo.path,
1442
+ worktreeOrRepoPath,
1324
1443
  gitMounts,
1325
1444
  repoDir: SANDBOX_REPO_DIR
1326
1445
  })
@@ -1442,9 +1561,12 @@ var buildSandboxHandle = (ctx, close) => {
1442
1561
  sandboxRepoDir,
1443
1562
  sandbox,
1444
1563
  providerHandle,
1564
+ bindMountHandle,
1445
1565
  applyToHost,
1446
- timeouts
1566
+ timeouts,
1567
+ branchStrategy
1447
1568
  } = ctx;
1569
+ const mergeToHead = branchStrategy?.type === "merge-to-head";
1448
1570
  const sandboxHandle = {
1449
1571
  branch,
1450
1572
  worktreePath,
@@ -1456,6 +1578,24 @@ var buildSandboxHandle = (ctx, close) => {
1456
1578
  promptFile,
1457
1579
  maxIterations = 1
1458
1580
  } = runOptions;
1581
+ if (runOptions.resumeSession && maxIterations > 1) {
1582
+ throw new Error(
1583
+ "resumeSession cannot be combined with maxIterations > 1. Resume applies to iteration 1 only; multi-iteration resume semantics are not supported."
1584
+ );
1585
+ }
1586
+ if (runOptions.forkSession && !runOptions.resumeSession) {
1587
+ throw new Error(
1588
+ "forkSession requires resumeSession. Use sandboxRunResult.fork(prompt) to fork the most recent captured session."
1589
+ );
1590
+ }
1591
+ if (runOptions.resumeSession) {
1592
+ await assertResumeSessionExists({
1593
+ provider,
1594
+ sandboxTag: ctx.providerTag,
1595
+ hostRepoDir,
1596
+ resumeSession: runOptions.resumeSession
1597
+ });
1598
+ }
1459
1599
  const resolved = await Effect_exports.runPromise(
1460
1600
  resolvePrompt({ prompt, promptFile }).pipe(
1461
1601
  Effect_exports.provide(NodeContext_exports.layer)
@@ -1514,7 +1654,8 @@ var buildSandboxHandle = (ctx, close) => {
1514
1654
  {
1515
1655
  hostWorktreePath: worktreePath,
1516
1656
  sandboxRepoPath: sandboxRepoDir,
1517
- applyToHost
1657
+ applyToHost,
1658
+ bindMountHandle
1518
1659
  },
1519
1660
  sandbox
1520
1661
  ).pipe(
@@ -1525,7 +1666,7 @@ var buildSandboxHandle = (ctx, close) => {
1525
1666
  )
1526
1667
  });
1527
1668
  const streamEmitterLayer = agentStreamEmitterLayer(
1528
- resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
1669
+ buildAgentStreamHandler(resolvedLogging)
1529
1670
  );
1530
1671
  const runLayer = Layer_exports.mergeAll(
1531
1672
  reuseFactoryLayer,
@@ -1542,15 +1683,18 @@ var buildSandboxHandle = (ctx, close) => {
1542
1683
  hostRepoDir,
1543
1684
  iterations: maxIterations,
1544
1685
  prompt: resolvedPrompt,
1545
- branch,
1686
+ branch: mergeToHead ? void 0 : branch,
1546
1687
  provider,
1547
1688
  completionSignal: runOptions.completionSignal,
1548
1689
  idleTimeoutSeconds: runOptions.idleTimeoutSeconds,
1549
1690
  completionTimeoutSeconds: runOptions.completionTimeoutSeconds,
1550
1691
  name: runOptions.name,
1692
+ resumeSession: runOptions.resumeSession,
1693
+ forkSession: runOptions.forkSession,
1551
1694
  signal: runOptions.signal,
1552
1695
  skipPromptExpansion: isInlinePrompt,
1553
- timeouts
1696
+ timeouts,
1697
+ keepSourceBranch: mergeToHead
1554
1698
  });
1555
1699
  const completion = buildCompletionMessage(
1556
1700
  orchestrateResult.completionSignal,
@@ -1569,13 +1713,38 @@ var buildSandboxHandle = (ctx, close) => {
1569
1713
  runOptions.signal?.throwIfAborted();
1570
1714
  throw error;
1571
1715
  }
1572
- return {
1716
+ const baseResult = {
1573
1717
  iterations: result.iterations,
1574
1718
  completionSignal: result.completionSignal,
1575
1719
  stdout: result.stdout,
1576
1720
  commits: result.commits,
1577
1721
  logFilePath: resolvedLogging.type === "file" ? resolvedLogging.path : void 0
1578
1722
  };
1723
+ const lastIteration = result.iterations.at(-1);
1724
+ if (provider.sessionStorage && lastIteration?.sessionId) {
1725
+ const capturedSessionId = lastIteration.sessionId;
1726
+ return {
1727
+ ...baseResult,
1728
+ resume: (nextPrompt, resumeOptions) => sandboxHandle.run({
1729
+ ...runOptions,
1730
+ ...resumeOptions,
1731
+ prompt: nextPrompt,
1732
+ promptFile: void 0,
1733
+ maxIterations: 1,
1734
+ resumeSession: capturedSessionId
1735
+ }),
1736
+ fork: (nextPrompt, forkOptions) => sandboxHandle.run({
1737
+ ...runOptions,
1738
+ ...forkOptions,
1739
+ prompt: nextPrompt,
1740
+ promptFile: void 0,
1741
+ maxIterations: 1,
1742
+ resumeSession: capturedSessionId,
1743
+ forkSession: true
1744
+ })
1745
+ };
1746
+ }
1747
+ return baseResult;
1579
1748
  },
1580
1749
  interactive: async (interactiveOptions) => {
1581
1750
  interactiveOptions.signal?.throwIfAborted();
@@ -1624,10 +1793,11 @@ var buildSandboxHandle = (ctx, close) => {
1624
1793
  {
1625
1794
  hostRepoDir,
1626
1795
  sandboxRepoDir,
1627
- branch,
1796
+ branch: mergeToHead ? void 0 : branch,
1628
1797
  hostWorktreePath: worktreePath,
1629
1798
  applyToHost,
1630
- timeouts
1799
+ timeouts,
1800
+ keepSourceBranch: mergeToHead
1631
1801
  },
1632
1802
  sandbox,
1633
1803
  (ctx2) => Effect_exports.gen(function* () {
@@ -1686,6 +1856,13 @@ var buildSandboxHandle = (ctx, close) => {
1686
1856
  exitCode: lifecycleResult.result
1687
1857
  };
1688
1858
  },
1859
+ exec: async (command, options) => {
1860
+ const mergedOptions = { cwd: sandboxRepoDir, ...options };
1861
+ if (providerHandle) {
1862
+ return providerHandle.exec(command, mergedOptions);
1863
+ }
1864
+ return Effect_exports.runPromise(sandbox.exec(command, mergedOptions));
1865
+ },
1689
1866
  close: async () => close(),
1690
1867
  [Symbol.asyncDispose]: async () => {
1691
1868
  await sandboxHandle.close();
@@ -1713,6 +1890,7 @@ var createSandboxFromWorktree = async (options) => {
1713
1890
  if (isTestMode) {
1714
1891
  sandbox = options._test.buildSandbox(worktreePath);
1715
1892
  sandboxRepoDir = worktreePath;
1893
+ providerHandle = options._test.bindMountHandle;
1716
1894
  } else {
1717
1895
  const resolvedEnv = await Effect_exports.runPromise(
1718
1896
  resolveEnv(hostRepoDir).pipe(Effect_exports.provide(NodeContext_exports.layer))
@@ -1744,16 +1922,7 @@ var createSandboxFromWorktree = async (options) => {
1744
1922
  Effect_exports.catchAll(() => Effect_exports.succeed([])),
1745
1923
  // Patch git mounts for Windows worktree compatibility (ADR-0006)
1746
1924
  Effect_exports.flatMap(
1747
- (gitMounts) => Effect_exports.tryPromise({
1748
- try: () => patchGitMountsForWindows(
1749
- gitMounts,
1750
- worktreePath,
1751
- SANDBOX_REPO_DIR
1752
- ),
1753
- catch: (e) => new Error(
1754
- `Failed to patch git mounts: ${e instanceof Error ? e.message : String(e)}`
1755
- )
1756
- })
1925
+ (gitMounts) => patchGitMountsForWindows(gitMounts, worktreePath, SANDBOX_REPO_DIR)
1757
1926
  ),
1758
1927
  Effect_exports.flatMap(
1759
1928
  (gitMounts) => startSandbox({
@@ -1798,6 +1967,7 @@ var createSandboxFromWorktree = async (options) => {
1798
1967
  }
1799
1968
  const applyToHost = isIsolated && providerHandle ? () => syncOut(worktreePath, providerHandle) : () => Effect_exports.void;
1800
1969
  let closed = false;
1970
+ const bindMountHandle = options.sandbox.tag === "bind-mount" ? providerHandle : void 0;
1801
1971
  return buildSandboxHandle(
1802
1972
  {
1803
1973
  branch,
@@ -1806,8 +1976,11 @@ var createSandboxFromWorktree = async (options) => {
1806
1976
  sandboxRepoDir,
1807
1977
  sandbox,
1808
1978
  providerHandle,
1979
+ bindMountHandle,
1980
+ providerTag: options.sandbox.tag,
1809
1981
  applyToHost,
1810
- timeouts: options.timeouts
1982
+ timeouts: options.timeouts,
1983
+ branchStrategy: options.branchStrategy
1811
1984
  },
1812
1985
  async () => {
1813
1986
  if (closed) return { preservedWorktreePath: void 0 };
@@ -1852,6 +2025,7 @@ var createSandbox = async (options) => {
1852
2025
  if (isTestMode) {
1853
2026
  sandbox2 = options._test.buildSandbox(worktreePath2);
1854
2027
  sandboxRepoDir2 = worktreePath2;
2028
+ providerHandle2 = options._test.bindMountHandle;
1855
2029
  } else {
1856
2030
  const resolvedEnv = yield* resolveEnv(hostRepoDir2);
1857
2031
  const env = mergeProviderEnv({
@@ -1875,16 +2049,11 @@ var createSandbox = async (options) => {
1875
2049
  Effect_exports.catchAll(() => Effect_exports.succeed([])),
1876
2050
  // Patch git mounts for Windows worktree compatibility (ADR-0006)
1877
2051
  Effect_exports.flatMap(
1878
- (gitMounts) => Effect_exports.tryPromise({
1879
- try: () => patchGitMountsForWindows(
1880
- gitMounts,
1881
- worktreePath2,
1882
- SANDBOX_REPO_DIR
1883
- ),
1884
- catch: (e) => new Error(
1885
- `Failed to patch git mounts: ${e instanceof Error ? e.message : String(e)}`
1886
- )
1887
- })
2052
+ (gitMounts) => patchGitMountsForWindows(
2053
+ gitMounts,
2054
+ worktreePath2,
2055
+ SANDBOX_REPO_DIR
2056
+ )
1888
2057
  ),
1889
2058
  Effect_exports.flatMap(
1890
2059
  (gitMounts) => startSandbox({
@@ -1969,6 +2138,7 @@ Worktree preserved at ${worktreePath}`);
1969
2138
  })
1970
2139
  );
1971
2140
  };
2141
+ const bindMountHandle = options.sandbox.tag === "bind-mount" ? providerHandle : void 0;
1972
2142
  return buildSandboxHandle(
1973
2143
  {
1974
2144
  branch,
@@ -1977,6 +2147,8 @@ Worktree preserved at ${worktreePath}`);
1977
2147
  sandboxRepoDir,
1978
2148
  sandbox,
1979
2149
  providerHandle,
2150
+ bindMountHandle,
2151
+ providerTag: options.sandbox.tag,
1980
2152
  applyToHost,
1981
2153
  timeouts: options.timeouts
1982
2154
  },
@@ -1989,6 +2161,7 @@ Worktree preserved at ${worktreePath}`);
1989
2161
  var createWorktree = async (options) => {
1990
2162
  const branch = options.branchStrategy.type === "branch" ? options.branchStrategy.branch : void 0;
1991
2163
  const baseBranch = options.branchStrategy.type === "branch" ? options.branchStrategy.baseBranch : void 0;
2164
+ const isMergeToHead = options.branchStrategy.type === "merge-to-head";
1992
2165
  const { hostRepoDir, worktreeInfo } = await Effect_exports.gen(function* () {
1993
2166
  const hostRepoDir2 = yield* resolveCwd(options.cwd);
1994
2167
  yield* pruneStale(hostRepoDir2).pipe(
@@ -2094,7 +2267,12 @@ var createWorktree = async (options) => {
2094
2267
  handle = startResult.handle;
2095
2268
  } else {
2096
2269
  const gitPath = join(hostRepoDir, ".git");
2097
- const gitMounts = yield* resolveGitMounts(gitPath);
2270
+ const rawGitMounts = yield* resolveGitMounts(gitPath);
2271
+ const gitMounts = yield* patchGitMountsForWindows(
2272
+ rawGitMounts,
2273
+ worktreeInfo.path,
2274
+ SANDBOX_REPO_DIR
2275
+ );
2098
2276
  const startResult = yield* d.taskLog(
2099
2277
  "Starting sandbox",
2100
2278
  () => startSandbox({
@@ -2123,10 +2301,14 @@ var createWorktree = async (options) => {
2123
2301
  hostRepoDir,
2124
2302
  sandboxRepoDir: worktreePath,
2125
2303
  hooks,
2126
- branch: worktreeInfo.branch,
2304
+ // merge-to-head: pass `undefined` so the lifecycle records the
2305
+ // host's current branch and merges the worktree's commits back into
2306
+ // it. branch strategy: pin to the worktree's branch.
2307
+ branch: isMergeToHead ? void 0 : worktreeInfo.branch,
2127
2308
  hostWorktreePath: worktreeInfo.path,
2128
2309
  applyToHost,
2129
- timeouts: options.timeouts
2310
+ timeouts: options.timeouts,
2311
+ keepSourceBranch: isMergeToHead
2130
2312
  },
2131
2313
  sandbox,
2132
2314
  (ctx) => Effect_exports.gen(function* () {
@@ -2254,7 +2436,12 @@ var createWorktree = async (options) => {
2254
2436
  sandboxRepoDir = startResult.worktreePath;
2255
2437
  } else {
2256
2438
  const gitPath = join(hostRepoDir, ".git");
2257
- const gitMounts = yield* resolveGitMounts(gitPath);
2439
+ const rawGitMounts = yield* resolveGitMounts(gitPath);
2440
+ const gitMounts = yield* patchGitMountsForWindows(
2441
+ rawGitMounts,
2442
+ worktreeInfo.path,
2443
+ SANDBOX_REPO_DIR
2444
+ );
2258
2445
  const startResult = yield* startSandbox({
2259
2446
  provider: sandboxProvider,
2260
2447
  hostRepoDir,
@@ -2288,12 +2475,14 @@ var createWorktree = async (options) => {
2288
2475
  NodeFileSystem_exports.layer
2289
2476
  );
2290
2477
  })() : ClackDisplay.layer;
2478
+ const bindMountHandle = sandboxProvider.tag === "bind-mount" ? handle : void 0;
2291
2479
  const reuseFactoryLayer = Layer_exports.succeed(SandboxFactory, {
2292
2480
  withSandbox: (makeEffect) => makeEffect(
2293
2481
  {
2294
2482
  hostWorktreePath: worktreeInfo.path,
2295
2483
  sandboxRepoPath: sandboxRepoDir,
2296
- applyToHost
2484
+ applyToHost,
2485
+ bindMountHandle
2297
2486
  },
2298
2487
  sandbox
2299
2488
  ).pipe(
@@ -2304,7 +2493,7 @@ var createWorktree = async (options) => {
2304
2493
  )
2305
2494
  });
2306
2495
  const streamEmitterLayer = agentStreamEmitterLayer(
2307
- resolvedLogging.type === "file" ? resolvedLogging.onAgentStreamEvent : void 0
2496
+ buildAgentStreamHandler(resolvedLogging)
2308
2497
  );
2309
2498
  const runLayer = Layer_exports.mergeAll(
2310
2499
  reuseFactoryLayer,
@@ -2319,7 +2508,10 @@ var createWorktree = async (options) => {
2319
2508
  iterations: maxIterations,
2320
2509
  hooks,
2321
2510
  prompt: resolvedPrompt,
2322
- branch: worktreeInfo.branch,
2511
+ // merge-to-head: pass `undefined` so the lifecycle records the host's
2512
+ // current branch and routes through the merge step. branch strategy:
2513
+ // pin to the worktree's branch so commits stay there.
2514
+ branch: isMergeToHead ? void 0 : worktreeInfo.branch,
2323
2515
  provider,
2324
2516
  completionSignal: opts.completionSignal,
2325
2517
  idleTimeoutSeconds: opts.idleTimeoutSeconds,
@@ -2328,7 +2520,8 @@ var createWorktree = async (options) => {
2328
2520
  resumeSession: opts.resumeSession,
2329
2521
  signal: opts.signal,
2330
2522
  skipPromptExpansion: isInlinePrompt,
2331
- timeouts: options.timeouts
2523
+ timeouts: options.timeouts,
2524
+ keepSourceBranch: isMergeToHead
2332
2525
  });
2333
2526
  const completion = buildCompletionMessage(
2334
2527
  orchestrateResult.completionSignal,
@@ -2378,6 +2571,7 @@ var createWorktree = async (options) => {
2378
2571
  hooks: opts.hooks,
2379
2572
  copyToWorktree: opts.copyToWorktree,
2380
2573
  timeouts: opts.timeouts,
2574
+ branchStrategy: options.branchStrategy,
2381
2575
  _test: opts._test
2382
2576
  });
2383
2577
  };