@ai-hero/sandcastle 0.6.4 → 0.6.6

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 (227) hide show
  1. package/README.md +125 -54
  2. package/dist/{MountConfig.d.ts → MountConfig-CmXclHA5.d.ts} +3 -2
  3. package/dist/{SandboxProvider.d.ts → SandboxProvider-EkSMuBp8.d.ts} +25 -39
  4. package/dist/chunk-72UVAC7B.js +99 -0
  5. package/dist/chunk-72UVAC7B.js.map +1 -0
  6. package/dist/chunk-BIWNFKGV.js +22 -0
  7. package/dist/chunk-BIWNFKGV.js.map +1 -0
  8. package/dist/chunk-NGBM7T3E.js +76 -0
  9. package/dist/chunk-NGBM7T3E.js.map +1 -0
  10. package/dist/chunk-Q5W3WQVU.js +25569 -0
  11. package/dist/chunk-Q5W3WQVU.js.map +1 -0
  12. package/dist/chunk-UPDEQ2U7.js +362 -0
  13. package/dist/chunk-UPDEQ2U7.js.map +1 -0
  14. package/dist/chunk-Z7O2WNRU.js +26934 -0
  15. package/dist/chunk-Z7O2WNRU.js.map +1 -0
  16. package/dist/index.d.ts +920 -22
  17. package/dist/index.js +3212 -9
  18. package/dist/index.js.map +1 -1
  19. package/dist/main.d.ts +0 -2
  20. package/dist/main.js +19256 -13
  21. package/dist/main.js.map +1 -1
  22. package/dist/mountUtils-CCA-bbpK.d.ts +25 -0
  23. package/dist/sandboxes/daytona.d.ts +8 -5
  24. package/dist/sandboxes/daytona.js +118 -124
  25. package/dist/sandboxes/daytona.js.map +1 -1
  26. package/dist/sandboxes/docker.d.ts +10 -8
  27. package/dist/sandboxes/docker.js +8 -255
  28. package/dist/sandboxes/docker.js.map +1 -1
  29. package/dist/sandboxes/no-sandbox.d.ts +7 -4
  30. package/dist/sandboxes/no-sandbox.js +6 -114
  31. package/dist/sandboxes/no-sandbox.js.map +1 -1
  32. package/dist/sandboxes/podman.d.ts +10 -8
  33. package/dist/sandboxes/podman.js +287 -297
  34. package/dist/sandboxes/podman.js.map +1 -1
  35. package/dist/sandboxes/vercel.d.ts +7 -4
  36. package/dist/sandboxes/vercel.js +144 -165
  37. package/dist/sandboxes/vercel.js.map +1 -1
  38. package/package.json +15 -14
  39. package/dist/AgentProvider.d.ts +0 -134
  40. package/dist/AgentProvider.d.ts.map +0 -1
  41. package/dist/AgentProvider.js +0 -647
  42. package/dist/AgentProvider.js.map +0 -1
  43. package/dist/AgentStreamEmitter.d.ts +0 -36
  44. package/dist/AgentStreamEmitter.d.ts.map +0 -1
  45. package/dist/AgentStreamEmitter.js +0 -21
  46. package/dist/AgentStreamEmitter.js.map +0 -1
  47. package/dist/CopyToWorktree.d.ts +0 -15
  48. package/dist/CopyToWorktree.d.ts.map +0 -1
  49. package/dist/CopyToWorktree.js +0 -60
  50. package/dist/CopyToWorktree.js.map +0 -1
  51. package/dist/Display.d.ts +0 -58
  52. package/dist/Display.d.ts.map +0 -1
  53. package/dist/Display.js +0 -142
  54. package/dist/Display.js.map +0 -1
  55. package/dist/DockerLifecycle.d.ts +0 -54
  56. package/dist/DockerLifecycle.d.ts.map +0 -1
  57. package/dist/DockerLifecycle.js +0 -123
  58. package/dist/DockerLifecycle.js.map +0 -1
  59. package/dist/EnvResolver.d.ts +0 -11
  60. package/dist/EnvResolver.d.ts.map +0 -1
  61. package/dist/EnvResolver.js +0 -63
  62. package/dist/EnvResolver.js.map +0 -1
  63. package/dist/ErrorHandler.d.ts +0 -15
  64. package/dist/ErrorHandler.d.ts.map +0 -1
  65. package/dist/ErrorHandler.js +0 -85
  66. package/dist/ErrorHandler.js.map +0 -1
  67. package/dist/InitService.d.ts +0 -63
  68. package/dist/InitService.d.ts.map +0 -1
  69. package/dist/InitService.js +0 -733
  70. package/dist/InitService.js.map +0 -1
  71. package/dist/MountConfig.d.ts.map +0 -1
  72. package/dist/MountConfig.js +0 -7
  73. package/dist/MountConfig.js.map +0 -1
  74. package/dist/Orchestrator.d.ts +0 -56
  75. package/dist/Orchestrator.d.ts.map +0 -1
  76. package/dist/Orchestrator.js +0 -293
  77. package/dist/Orchestrator.js.map +0 -1
  78. package/dist/Output.d.ts +0 -107
  79. package/dist/Output.d.ts.map +0 -1
  80. package/dist/Output.js +0 -95
  81. package/dist/Output.js.map +0 -1
  82. package/dist/PodmanLifecycle.d.ts +0 -17
  83. package/dist/PodmanLifecycle.d.ts.map +0 -1
  84. package/dist/PodmanLifecycle.js +0 -45
  85. package/dist/PodmanLifecycle.js.map +0 -1
  86. package/dist/PromptArgumentSubstitution.d.ts +0 -32
  87. package/dist/PromptArgumentSubstitution.d.ts.map +0 -1
  88. package/dist/PromptArgumentSubstitution.js +0 -104
  89. package/dist/PromptArgumentSubstitution.js.map +0 -1
  90. package/dist/PromptPreprocessor.d.ts +0 -15
  91. package/dist/PromptPreprocessor.d.ts.map +0 -1
  92. package/dist/PromptPreprocessor.js +0 -55
  93. package/dist/PromptPreprocessor.js.map +0 -1
  94. package/dist/PromptResolver.d.ts +0 -21
  95. package/dist/PromptResolver.d.ts.map +0 -1
  96. package/dist/PromptResolver.js +0 -27
  97. package/dist/PromptResolver.js.map +0 -1
  98. package/dist/RecoveryMessage.d.ts +0 -15
  99. package/dist/RecoveryMessage.d.ts.map +0 -1
  100. package/dist/RecoveryMessage.js +0 -81
  101. package/dist/RecoveryMessage.js.map +0 -1
  102. package/dist/SandboxFactory.d.ts +0 -90
  103. package/dist/SandboxFactory.d.ts.map +0 -1
  104. package/dist/SandboxFactory.js +0 -324
  105. package/dist/SandboxFactory.js.map +0 -1
  106. package/dist/SandboxLifecycle.d.ts +0 -65
  107. package/dist/SandboxLifecycle.d.ts.map +0 -1
  108. package/dist/SandboxLifecycle.js +0 -296
  109. package/dist/SandboxLifecycle.js.map +0 -1
  110. package/dist/SandboxProvider.d.ts.map +0 -1
  111. package/dist/SandboxProvider.js +0 -28
  112. package/dist/SandboxProvider.js.map +0 -1
  113. package/dist/SessionStore.d.ts +0 -110
  114. package/dist/SessionStore.d.ts.map +0 -1
  115. package/dist/SessionStore.js +0 -330
  116. package/dist/SessionStore.js.map +0 -1
  117. package/dist/TextDeltaBuffer.d.ts +0 -24
  118. package/dist/TextDeltaBuffer.d.ts.map +0 -1
  119. package/dist/TextDeltaBuffer.js +0 -68
  120. package/dist/TextDeltaBuffer.js.map +0 -1
  121. package/dist/WorktreeManager.d.ts +0 -79
  122. package/dist/WorktreeManager.d.ts.map +0 -1
  123. package/dist/WorktreeManager.js +0 -283
  124. package/dist/WorktreeManager.js.map +0 -1
  125. package/dist/boundedTail.d.ts +0 -48
  126. package/dist/boundedTail.d.ts.map +0 -1
  127. package/dist/boundedTail.js +0 -64
  128. package/dist/boundedTail.js.map +0 -1
  129. package/dist/cli.d.ts +0 -30
  130. package/dist/cli.d.ts.map +0 -1
  131. package/dist/cli.js +0 -309
  132. package/dist/cli.js.map +0 -1
  133. package/dist/createSandbox.d.ts +0 -154
  134. package/dist/createSandbox.d.ts.map +0 -1
  135. package/dist/createSandbox.js +0 -476
  136. package/dist/createSandbox.js.map +0 -1
  137. package/dist/createWorktree.d.ts +0 -154
  138. package/dist/createWorktree.d.ts.map +0 -1
  139. package/dist/createWorktree.js +0 -391
  140. package/dist/createWorktree.js.map +0 -1
  141. package/dist/errors.d.ts +0 -227
  142. package/dist/errors.d.ts.map +0 -1
  143. package/dist/errors.js +0 -81
  144. package/dist/errors.js.map +0 -1
  145. package/dist/extractStructuredOutput.d.ts +0 -23
  146. package/dist/extractStructuredOutput.d.ts.map +0 -1
  147. package/dist/extractStructuredOutput.js +0 -102
  148. package/dist/extractStructuredOutput.js.map +0 -1
  149. package/dist/index.d.ts.map +0 -1
  150. package/dist/interactive.d.ts +0 -74
  151. package/dist/interactive.d.ts.map +0 -1
  152. package/dist/interactive.js +0 -279
  153. package/dist/interactive.js.map +0 -1
  154. package/dist/main.d.ts.map +0 -1
  155. package/dist/mergeProviderEnv.d.ts +0 -13
  156. package/dist/mergeProviderEnv.d.ts.map +0 -1
  157. package/dist/mergeProviderEnv.js +0 -23
  158. package/dist/mergeProviderEnv.js.map +0 -1
  159. package/dist/mountUtils.d.ts +0 -146
  160. package/dist/mountUtils.d.ts.map +0 -1
  161. package/dist/mountUtils.js +0 -301
  162. package/dist/mountUtils.js.map +0 -1
  163. package/dist/raceAbortSignal.d.ts +0 -18
  164. package/dist/raceAbortSignal.d.ts.map +0 -1
  165. package/dist/raceAbortSignal.js +0 -32
  166. package/dist/raceAbortSignal.js.map +0 -1
  167. package/dist/resolveCwd.d.ts +0 -24
  168. package/dist/resolveCwd.d.ts.map +0 -1
  169. package/dist/resolveCwd.js +0 -32
  170. package/dist/resolveCwd.js.map +0 -1
  171. package/dist/resumePrecheck.d.ts +0 -26
  172. package/dist/resumePrecheck.d.ts.map +0 -1
  173. package/dist/resumePrecheck.js +0 -40
  174. package/dist/resumePrecheck.js.map +0 -1
  175. package/dist/run.d.ts +0 -216
  176. package/dist/run.d.ts.map +0 -1
  177. package/dist/run.js +0 -313
  178. package/dist/run.js.map +0 -1
  179. package/dist/sandboxExec.d.ts +0 -12
  180. package/dist/sandboxExec.d.ts.map +0 -1
  181. package/dist/sandboxExec.js +0 -26
  182. package/dist/sandboxExec.js.map +0 -1
  183. package/dist/sandboxes/daytona.d.ts.map +0 -1
  184. package/dist/sandboxes/docker.d.ts.map +0 -1
  185. package/dist/sandboxes/no-sandbox.d.ts.map +0 -1
  186. package/dist/sandboxes/podman.d.ts.map +0 -1
  187. package/dist/sandboxes/test-bind-mount.d.ts +0 -17
  188. package/dist/sandboxes/test-bind-mount.d.ts.map +0 -1
  189. package/dist/sandboxes/test-bind-mount.js +0 -92
  190. package/dist/sandboxes/test-bind-mount.js.map +0 -1
  191. package/dist/sandboxes/test-isolated.d.ts +0 -17
  192. package/dist/sandboxes/test-isolated.d.ts.map +0 -1
  193. package/dist/sandboxes/test-isolated.js +0 -98
  194. package/dist/sandboxes/test-isolated.js.map +0 -1
  195. package/dist/sandboxes/vercel.d.ts.map +0 -1
  196. package/dist/shutdownRegistry.d.ts +0 -30
  197. package/dist/shutdownRegistry.d.ts.map +0 -1
  198. package/dist/shutdownRegistry.js +0 -73
  199. package/dist/shutdownRegistry.js.map +0 -1
  200. package/dist/startSandbox.d.ts +0 -50
  201. package/dist/startSandbox.d.ts.map +0 -1
  202. package/dist/startSandbox.js +0 -117
  203. package/dist/startSandbox.js.map +0 -1
  204. package/dist/syncIn.d.ts +0 -24
  205. package/dist/syncIn.d.ts.map +0 -1
  206. package/dist/syncIn.js +0 -107
  207. package/dist/syncIn.js.map +0 -1
  208. package/dist/syncOut.d.ts +0 -27
  209. package/dist/syncOut.d.ts.map +0 -1
  210. package/dist/syncOut.js +0 -271
  211. package/dist/syncOut.js.map +0 -1
  212. package/dist/templates.d.ts +0 -2
  213. package/dist/templates.d.ts.map +0 -1
  214. package/dist/templates.js +0 -26
  215. package/dist/templates.js.map +0 -1
  216. package/dist/terminalCleanup.d.ts +0 -30
  217. package/dist/terminalCleanup.d.ts.map +0 -1
  218. package/dist/terminalCleanup.js +0 -37
  219. package/dist/terminalCleanup.js.map +0 -1
  220. package/dist/testSandbox.d.ts +0 -8
  221. package/dist/testSandbox.d.ts.map +0 -1
  222. package/dist/testSandbox.js +0 -109
  223. package/dist/testSandbox.js.map +0 -1
  224. package/dist/testSetup.d.ts +0 -2
  225. package/dist/testSetup.d.ts.map +0 -1
  226. package/dist/testSetup.js +0 -29
  227. package/dist/testSetup.js.map +0 -1
package/README.md CHANGED
@@ -234,6 +234,12 @@ const result = await run({
234
234
  // Idle timeout in seconds — resets whenever the agent produces output. Default: 600 (10 minutes)
235
235
  idleTimeoutSeconds: 600,
236
236
 
237
+ // Grace window in seconds after the agent emits a completion signal but
238
+ // before its process has exited (a "hanging process" — typically a spawned
239
+ // `gh`/git child or MCP server keeping stdout open). Resets on every
240
+ // subsequent output line so trailing data is still captured. Default: 60
241
+ completionTimeoutSeconds: 60,
242
+
237
243
  // Structured output — extract a typed payload from the agent's stdout.
238
244
  // Requires maxIterations === 1 and the tag must appear in the prompt.
239
245
  // output: Output.object({ tag: "result", schema: z.object({ answer: z.number() }) }),
@@ -341,18 +347,19 @@ if (closeResult.preservedWorktreePath) {
341
347
 
342
348
  #### `SandboxRunOptions`
343
349
 
344
- | Option | Type | Default | Description |
345
- | -------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------- |
346
- | `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`) |
347
- | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
348
- | `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`) |
349
- | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
350
- | `maxIterations` | number | `1` | Maximum iterations to run |
351
- | `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String(s) the agent emits to stop the iteration loop early |
352
- | `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
353
- | `name` | string | | Display name for the run |
354
- | `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
355
- | `signal` | AbortSignal | | Cancels the run when aborted; handle stays usable afterward |
350
+ | Option | Type | Default | Description |
351
+ | -------------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------ |
352
+ | `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`) |
353
+ | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
354
+ | `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`) |
355
+ | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
356
+ | `maxIterations` | number | `1` | Maximum iterations to run |
357
+ | `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String(s) the agent emits to stop the iteration loop early |
358
+ | `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
359
+ | `completionTimeoutSeconds` | number | `60` | Grace window after the completion signal is seen but the agent process hasn't exited |
360
+ | `name` | string | | Display name for the run |
361
+ | `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
362
+ | `signal` | AbortSignal | — | Cancels the run when aborted; handle stays usable afterward |
356
363
 
357
364
  #### `SandboxRunResult`
358
365
 
@@ -459,22 +466,23 @@ await sandbox.close();
459
466
 
460
467
  #### `WorktreeRunOptions`
461
468
 
462
- | Option | Type | Default | Description |
463
- | -------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
464
- | `agent` | AgentProvider | — | **Required.** Agent provider |
465
- | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (AFK agents must be sandboxed) |
466
- | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
467
- | `promptFile` | string | — | Path to prompt file |
468
- | `maxIterations` | number | 1 | Maximum iterations to run |
469
- | `completionSignal` | string \| string[] | — | Substring(s) to stop the iteration loop early |
470
- | `idleTimeoutSeconds` | number | 600 | Idle timeout in seconds |
471
- | `name` | string | | Optional run name |
472
- | `logging` | LoggingOption | file | Logging mode |
473
- | `hooks` | SandboxHooks | | Lifecycle hooks (`host.*`, `sandbox.*`) |
474
- | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
475
- | `env` | Record<string, string> | — | Environment variables to inject into the sandbox |
476
- | `resumeSession` | string | — | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
477
- | `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess; the worktree is preserved on disk. Rejects with `signal.reason`. |
469
+ | Option | Type | Default | Description |
470
+ | -------------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
471
+ | `agent` | AgentProvider | — | **Required.** Agent provider |
472
+ | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (AFK agents must be sandboxed) |
473
+ | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
474
+ | `promptFile` | string | — | Path to prompt file |
475
+ | `maxIterations` | number | 1 | Maximum iterations to run |
476
+ | `completionSignal` | string \| string[] | — | Substring(s) to stop the iteration loop early |
477
+ | `idleTimeoutSeconds` | number | 600 | Idle timeout in seconds |
478
+ | `completionTimeoutSeconds` | number | 60 | Grace window after completion signal is seen but agent process hasn't exited |
479
+ | `name` | string | | Optional run name |
480
+ | `logging` | LoggingOption | file | Logging mode |
481
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
482
+ | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
483
+ | `env` | Record<string, string> | — | Environment variables to inject into the sandbox |
484
+ | `resumeSession` | string | — | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
485
+ | `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess; the worktree is preserved on disk. Rejects with `signal.reason`. |
478
486
 
479
487
  #### `WorktreeRunResult`
480
488
 
@@ -614,6 +622,25 @@ await run({
614
622
 
615
623
  Tell the agent to output your chosen string(s) in the prompt, and the orchestrator will stop when it detects any of them. The matched signal is returned as `result.completionSignal`.
616
624
 
625
+ #### Hanging processes after the completion signal
626
+
627
+ The agent process is expected to exit shortly after emitting the completion signal. When a child it spawned — a `gh`/git subprocess, a long-lived MCP server, etc. — inherits the agent's stdout pipe and keeps it open, the parent process can linger long past its logical end. Sandcastle would otherwise wait for the full `idleTimeoutSeconds` and fail with `AgentIdleTimeoutError`, throwing away the commits the agent already made.
628
+
629
+ Instead, once the completion signal is observed in the output buffer, Sandcastle swaps in a short **completion timeout** (default 60 s). When it expires, the run resolves successfully with a warning that the process was hanging; `result.commits` and `result.completionSignal` are populated as if the process had exited cleanly. The timer resets on every subsequent output line, so trailing data emitted after the signal — token-usage events, terminal `result` events, a structured-output `<tag>` — is still captured.
630
+
631
+ A clean process exit always wins the race, so healthy runs gain zero added latency. The completion timeout only matters when the process hangs.
632
+
633
+ Tune the window with `completionTimeoutSeconds`:
634
+
635
+ ```ts
636
+ await run({
637
+ // ...
638
+ completionTimeoutSeconds: 30, // shorter grace window
639
+ });
640
+ ```
641
+
642
+ This is independent of `idleTimeoutSeconds`. They cover different phases: `idleTimeoutSeconds` runs **before** any signal is seen (genuinely stuck agent → fail); `completionTimeoutSeconds` runs **after** the signal is seen (hanging process → succeed with warning). See [ADR 0019](docs/adr/0019-completion-timeout-for-hanging-process.md).
643
+
617
644
  ### Structured output
618
645
 
619
646
  Use `Output.object()` to extract a typed, schema-validated JSON payload from the agent's stdout. The agent emits its answer inside an XML tag you specify, and Sandcastle parses, validates, and returns it on `result.output`. The schema can be any [Standard Schema](https://standardschema.dev) validator — the examples below use [Zod](https://zod.dev), but Valibot, ArkType, and others work identically. See [ADR 0010](docs/adr/0010-structured-output.md) for design rationale.
@@ -682,6 +709,8 @@ Select a template during `sandcastle init` when prompted, or re-run init in a fr
682
709
 
683
710
  Scaffolds the `.sandcastle/` config directory and builds the container image. This is the first command you run in a new repo. You choose a sandbox provider (Docker or Podman) during init — selecting Podman writes a `Containerfile` instead of `Dockerfile` and uses `sandcastle podman build-image` for the build step.
684
711
 
712
+ Init detects your host package manager (npm, pnpm, yarn, or bun) from a `packageManager` field or lockfile, defaulting to npm. Templates whose `main` file imports a host dependency — the planner templates import [Zod](https://zod.dev) for their `<plan>` output schema — prompt you to install it with that package manager when it isn't already in your `package.json`, so the first `npx tsx .sandcastle/main.ts` doesn't fail with `ERR_MODULE_NOT_FOUND`.
713
+
685
714
  | Option | Required | Default | Description |
686
715
  | -------------- | -------- | ---------------------------- | ---------------------------------------------------------------------------- |
687
716
  | `--image-name` | No | `sandcastle:<repo-dir-name>` | Docker image name |
@@ -738,26 +767,27 @@ Removes the Podman image.
738
767
 
739
768
  ### `RunOptions`
740
769
 
741
- | Option | Type | Default | Description |
742
- | -------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
743
- | `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`, `pi("claude-sonnet-4-6")`, `codex("gpt-5.4-mini")`, `cursor("composer-2")`, `opencode("opencode/big-pickle")`, `copilot("claude-sonnet-4.5")`) |
744
- | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (e.g. `docker()`, `podman()`, `docker({ imageName: "sandcastle:local" })`) |
745
- | `cwd` | string | `process.cwd()` | Host repo directory — anchor for `.sandcastle/` artifacts and git operations. Relative paths resolve against `process.cwd()`. |
746
- | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
747
- | `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`). Resolves against `process.cwd()`, **not** `cwd`. |
748
- | `maxIterations` | number | `1` | Maximum iterations to run |
749
- | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
750
- | `name` | string | — | Display name for the run, shown as a prefix in log output |
751
- | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
752
- | `branchStrategy` | BranchStrategy | per-provider default | Branch strategy: `{ type: 'head' }`, `{ type: 'merge-to-head' }`, or `{ type: 'branch', branch: '…' }` |
753
- | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`) |
754
- | `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
755
- | `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String or array of strings the agent emits to stop the iteration loop early |
756
- | `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
757
- | `resumeSession` | string | | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
758
- | `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess and cancels lifecycle hooks; the worktree is preserved on disk. Rejects with `signal.reason`. |
759
- | `timeouts` | Timeouts | — | Override default timeouts for built-in lifecycle steps: `copyToWorktreeMs` (60 000), `gitSetupMs` (10 000), `commitCollectionMs` (30 000), `mergeToHostMs` (30 000). |
760
- | `output` | OutputDefinition | — | Structured output definition (`Output.object()` or `Output.string(…)`). Requires `maxIterations === 1`. See [Structured output](#structured-output). |
770
+ | Option | Type | Default | Description |
771
+ | -------------------------- | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
772
+ | `agent` | AgentProvider | — | **Required.** Agent provider (e.g. `claudeCode("claude-opus-4-7")`, `pi("claude-sonnet-4-6")`, `codex("gpt-5.4-mini")`, `cursor("composer-2")`, `opencode("opencode/big-pickle")`, `copilot("claude-sonnet-4.5")`) |
773
+ | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (e.g. `docker()`, `podman()`, `docker({ imageName: "sandcastle:local" })`) |
774
+ | `cwd` | string | `process.cwd()` | Host repo directory — anchor for `.sandcastle/` artifacts and git operations. Relative paths resolve against `process.cwd()`. |
775
+ | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
776
+ | `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`). Resolves against `process.cwd()`, **not** `cwd`. |
777
+ | `maxIterations` | number | `1` | Maximum iterations to run |
778
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
779
+ | `name` | string | — | Display name for the run, shown as a prefix in log output |
780
+ | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
781
+ | `branchStrategy` | BranchStrategy | per-provider default | Branch strategy: `{ type: 'head' }`, `{ type: 'merge-to-head' }`, or `{ type: 'branch', branch: '…' }` |
782
+ | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`) |
783
+ | `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
784
+ | `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String or array of strings the agent emits to stop the iteration loop early |
785
+ | `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
786
+ | `completionTimeoutSeconds` | number | `60` | Grace window in seconds after the completion signal is observed but the agent process has not exited (hanging process). See [Hanging processes after the completion signal](#hanging-processes-after-the-completion-signal). |
787
+ | `resumeSession` | string | — | Resume a prior session by ID for agents that support resume. Incompatible with `maxIterations > 1`. Session file must exist on host. |
788
+ | `signal` | AbortSignal | — | Cancel the run when aborted. Kills the in-flight agent subprocess and cancels lifecycle hooks; the worktree is preserved on disk. Rejects with `signal.reason`. |
789
+ | `timeouts` | Timeouts | — | Override default timeouts for built-in lifecycle steps: `copyToWorktreeMs` (60 000), `gitSetupMs` (10 000), `commitCollectionMs` (30 000), `mergeToHostMs` (30 000). |
790
+ | `output` | OutputDefinition | — | Structured output definition (`Output.object(…)` or `Output.string(…)`). Requires `maxIterations === 1`. See [Structured output](#structured-output). |
761
791
 
762
792
  ### `RunResult`
763
793
 
@@ -790,13 +820,13 @@ Removes the Podman image.
790
820
 
791
821
  ### Session capture
792
822
 
793
- After each resumable provider iteration, Sandcastle automatically captures the agent's session file from the sandbox to the host. Claude Code sessions are stored under `~/.claude/projects/<encoded-path>/<session-id>.jsonl`; Codex sessions are stored under `~/.codex/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`. Any provider-specific `cwd` fields are rewritten to match the host repo root, so the provider's native resume command works.
823
+ After each resumable provider iteration, Sandcastle automatically captures the agent's session file from the sandbox to the host. Claude Code sessions are stored under `~/.claude/projects/<encoded-path>/<session-id>.jsonl`; Codex sessions are stored under `~/.codex/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`; Pi sessions are stored under `~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<session-id>.jsonl`. Any provider-specific `cwd` fields are rewritten to match the host repo root, so the provider's native resume command works.
794
824
 
795
- Session capture is enabled by default for `claudeCode()` and `codex()` and can be opted out via `captureSessions: false`. Providers without `sessionStorage` do not attempt capture. Capture failure fails the run.
825
+ Session capture is enabled by default for `claudeCode()`, `codex()`, and `pi()` and can be opted out via `captureSessions: false`. Providers without `sessionStorage` do not attempt capture. Capture failure fails the run.
796
826
 
797
827
  ### Session resume
798
828
 
799
- Pass `resumeSession` to `run()` to continue a prior Claude Code or Codex conversation inside a new sandbox:
829
+ Pass `resumeSession` to `run()` to continue a prior Claude Code, Codex, or Pi conversation inside a new sandbox:
800
830
 
801
831
  ```typescript
802
832
  const result = await run({
@@ -819,9 +849,9 @@ const first = await run({
819
849
  const second = await first.resume?.("Now implement the plan");
820
850
  ```
821
851
 
822
- `resume` is present only on results from resumable providers (Claude Code, Codex) — hence the optional-chaining call.
852
+ `resume` is present only on results from resumable providers (Claude Code, Codex, Pi) — hence the optional-chaining call.
823
853
 
824
- Before the sandbox starts, Sandcastle validates that the session file exists on the host and transfers it into the sandbox with `cwd` fields rewritten to match the sandbox-side path. Claude Code receives `--resume <id>`; Codex receives `codex exec resume <id>` with the prompt piped over stdin.
854
+ Before the sandbox starts, Sandcastle validates that the session file exists on the host and transfers it into the sandbox with `cwd` fields rewritten to match the sandbox-side path. Claude Code receives `--resume <id>`; Codex receives `codex exec resume <id>` with the prompt piped over stdin; Pi receives `--session <id>`.
825
855
 
826
856
  Constraints:
827
857
 
@@ -830,6 +860,33 @@ Constraints:
830
860
  - Only iteration 1 receives the resume flag; subsequent iterations (if any) start fresh.
831
861
  - Providers without resume support reject `resumeSession`.
832
862
 
863
+ ### Session fork
864
+
865
+ `RunResult.fork(prompt, options?)` is the sibling of `.resume()`: it continues from the last captured session but leaves the parent session JSONL untouched and writes the child under a new session id. The mechanism is the agent's native fork flag — `claude --resume <id> --fork-session` for Claude Code, `codex exec fork <id>` for Codex.
866
+
867
+ Fork enables fan-out workflows where a single parent run is the starting point for several independent children:
868
+
869
+ ```typescript
870
+ const parent = await run({
871
+ agent: claudeCode("claude-opus-4-7"),
872
+ sandbox: docker(),
873
+ prompt: "Read the codebase and summarise the data model",
874
+ });
875
+
876
+ const [reviewA, reviewB] = await Promise.all([
877
+ parent.fork?.("Review the migration plan", {
878
+ branchStrategy: { type: "branch", branch: "review-a" },
879
+ }),
880
+ parent.fork?.("Audit the auth layer", {
881
+ branchStrategy: { type: "branch", branch: "review-b" },
882
+ }),
883
+ ]);
884
+ ```
885
+
886
+ **Fork is session-only.** `--fork-session` and `codex exec fork` isolate the agent session JSONL — they do **not** isolate the branch, worktree, or sandbox. Safe concurrent fan-out (`Promise.all([r.fork(a), r.fork(b)])`) requires the caller to give each child a distinct `branch` via `branchStrategy: { type: "branch", branch: "..." }`. The default `head` and `merge-to-head` strategies are **not** safe for concurrent forks: `head` shares the host working directory across all children, and `merge-to-head` races `git merge` against the same HEAD. See [ADR 0018](docs/adr/0018-fork-is-session-only.md).
887
+
888
+ `fork` is present only on results from providers with `sessionStorage` (Claude Code, Codex) — hence the optional-chaining call. The same single-iteration and session-file constraints as `.resume()` apply.
889
+
833
890
  ### `ClaudeCodeOptions`
834
891
 
835
892
  The `claudeCode()` factory accepts an optional second argument for provider-specific options:
@@ -858,6 +915,20 @@ agent: codex("gpt-5.4", { effort: "high" });
858
915
  | `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
859
916
  | `captureSessions` | `boolean` | `true` | Capture Codex rollout JSONL to host for resume |
860
917
 
918
+ ### `PiOptions`
919
+
920
+ The `pi()` factory accepts an optional second argument for provider-specific options:
921
+
922
+ ```typescript
923
+ agent: pi("claude-sonnet-4-6", { thinking: "high" });
924
+ ```
925
+
926
+ | Option | Type | Default | Description |
927
+ | ----------------- | ------------------------------------------------------------------------ | ------- | -------------------------------------------------------- |
928
+ | `thinking` | `"off"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` | — | Pi reasoning effort level via the `--thinking` flag |
929
+ | `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
930
+ | `captureSessions` | `boolean` | `true` | Capture pi session JSONL to host for `pi --session <id>` |
931
+
861
932
  ### Provider `env`
862
933
 
863
934
  Both **agent providers** and **sandbox providers** accept an optional `env: Record<string, string>` in their options. These environment variables are merged with the `.sandcastle/.env` resolver output at launch time:
@@ -1250,7 +1321,7 @@ hooks: {
1250
1321
 
1251
1322
  ```bash
1252
1323
  npm install
1253
- npm run build # Build with tsgo
1324
+ npm run build # Bundle with tsup
1254
1325
  npm test # Run tests with vitest
1255
1326
  npm run typecheck # Type-check
1256
1327
  ```
@@ -4,7 +4,7 @@
4
4
  * Each entry describes a host directory to mount into the sandbox container.
5
5
  */
6
6
  /** A single bind-mount descriptor for docker()/podman() providers. */
7
- export interface MountConfig {
7
+ interface MountConfig {
8
8
  /**
9
9
  * Path on the host. Supports:
10
10
  * - Absolute paths (`/data/cache`)
@@ -22,4 +22,5 @@ export interface MountConfig {
22
22
  /** Mount as read-only. Defaults to `false`. */
23
23
  readonly readonly?: boolean;
24
24
  }
25
- //# sourceMappingURL=MountConfig.d.ts.map
25
+
26
+ export type { MountConfig as M };
@@ -5,20 +5,20 @@
5
5
  * handles worktree creation, git mount resolution, and commit extraction.
6
6
  */
7
7
  /** Result of executing a command inside a sandbox. */
8
- export interface ExecResult {
8
+ interface ExecResult {
9
9
  readonly stdout: string;
10
10
  readonly stderr: string;
11
11
  readonly exitCode: number;
12
12
  }
13
13
  /** Options for interactiveExec — the streams the provider should wire to the spawned process. */
14
- export interface InteractiveExecOptions {
14
+ interface InteractiveExecOptions {
15
15
  readonly stdin: NodeJS.ReadableStream;
16
16
  readonly stdout: NodeJS.WritableStream;
17
17
  readonly stderr: NodeJS.WritableStream;
18
18
  readonly cwd?: string;
19
19
  }
20
20
  /** Handle to a running bind-mount sandbox. */
21
- export interface BindMountSandboxHandle {
21
+ interface BindMountSandboxHandle {
22
22
  /** Absolute path to the worktree inside the sandbox. */
23
23
  readonly worktreePath: string;
24
24
  /**
@@ -56,7 +56,7 @@ export interface BindMountSandboxHandle {
56
56
  close(): Promise<void>;
57
57
  }
58
58
  /** Options passed to a bind-mount provider's `create` function. */
59
- export interface BindMountCreateOptions {
59
+ interface BindMountCreateOptions {
60
60
  /** Host-side path to the worktree directory. */
61
61
  readonly worktreePath: string;
62
62
  /** Host-side path to the original repo root. */
@@ -71,7 +71,7 @@ export interface BindMountCreateOptions {
71
71
  readonly env: Record<string, string>;
72
72
  }
73
73
  /** Configuration for createBindMountSandboxProvider. */
74
- export interface BindMountSandboxProviderConfig {
74
+ interface BindMountSandboxProviderConfig {
75
75
  /** Human-readable name for this provider (e.g. "docker", "podman"). */
76
76
  readonly name: string;
77
77
  /** Environment variables injected by this provider. Merged at launch time. */
@@ -86,7 +86,7 @@ export interface BindMountSandboxProviderConfig {
86
86
  readonly create: (options: BindMountCreateOptions) => Promise<BindMountSandboxHandle>;
87
87
  }
88
88
  /** Handle to a running isolated sandbox (extends bind-mount with file transfer). */
89
- export interface IsolatedSandboxHandle {
89
+ interface IsolatedSandboxHandle {
90
90
  /** Absolute path to the worktree inside the sandbox. */
91
91
  readonly worktreePath: string;
92
92
  /**
@@ -124,12 +124,12 @@ export interface IsolatedSandboxHandle {
124
124
  close(): Promise<void>;
125
125
  }
126
126
  /** Options passed to an isolated provider's `create` function. */
127
- export interface IsolatedCreateOptions {
127
+ interface IsolatedCreateOptions {
128
128
  /** Environment variables to inject into the sandbox. */
129
129
  readonly env: Record<string, string>;
130
130
  }
131
131
  /** Configuration for createIsolatedSandboxProvider. */
132
- export interface IsolatedSandboxProviderConfig {
132
+ interface IsolatedSandboxProviderConfig {
133
133
  /** Human-readable name for this provider (e.g. "daytona", "e2b"). */
134
134
  readonly name: string;
135
135
  /** Environment variables injected by this provider. Merged at launch time. */
@@ -138,9 +138,7 @@ export interface IsolatedSandboxProviderConfig {
138
138
  readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
139
139
  }
140
140
  /** A bind-mount sandbox provider. */
141
- export interface BindMountSandboxProvider {
142
- /** @internal Discriminator for internal dispatch. */
143
- readonly tag: "bind-mount";
141
+ interface BindMountSandboxProvider {
144
142
  /** Human-readable provider name. */
145
143
  readonly name: string;
146
144
  /** Environment variables injected by this provider. */
@@ -150,22 +148,16 @@ export interface BindMountSandboxProvider {
150
148
  * `undefined` when the provider does not declare a sandbox home directory.
151
149
  */
152
150
  readonly sandboxHomedir: string | undefined;
153
- /** @internal Create a sandbox handle. */
154
- readonly create: (options: BindMountCreateOptions) => Promise<BindMountSandboxHandle>;
155
151
  }
156
152
  /** An isolated sandbox provider. */
157
- export interface IsolatedSandboxProvider {
158
- /** @internal Discriminator for internal dispatch. */
159
- readonly tag: "isolated";
153
+ interface IsolatedSandboxProvider {
160
154
  /** Human-readable provider name. */
161
155
  readonly name: string;
162
156
  /** Environment variables injected by this provider. */
163
157
  readonly env: Record<string, string>;
164
- /** @internal Create an isolated sandbox handle. */
165
- readonly create: (options: IsolatedCreateOptions) => Promise<IsolatedSandboxHandle>;
166
158
  }
167
159
  /** Handle to a no-sandbox session — runs commands directly on the host. */
168
- export interface NoSandboxHandle {
160
+ interface NoSandboxHandle {
169
161
  /** Absolute path to the worktree on the host. */
170
162
  readonly worktreePath: string;
171
163
  /**
@@ -194,29 +186,22 @@ export interface NoSandboxHandle {
194
186
  close(): Promise<void>;
195
187
  }
196
188
  /** A no-sandbox provider — runs the agent directly on the host with no container isolation. */
197
- export interface NoSandboxProvider {
198
- /** @internal Discriminator for internal dispatch. */
199
- readonly tag: "none";
189
+ interface NoSandboxProvider {
200
190
  /** Human-readable provider name. */
201
191
  readonly name: string;
202
192
  /** Environment variables injected by this provider. */
203
193
  readonly env: Record<string, string>;
204
- /** @internal Create a no-sandbox handle. */
205
- readonly create: (options: {
206
- readonly worktreePath: string;
207
- readonly env: Record<string, string>;
208
- }) => Promise<NoSandboxHandle>;
209
194
  }
210
195
  /** Head strategy: agent writes directly to host working directory. Bind-mount only. */
211
- export interface HeadBranchStrategy {
196
+ interface HeadBranchStrategy {
212
197
  readonly type: "head";
213
198
  }
214
199
  /** Merge-to-head strategy: temp branch, merge back to HEAD, delete temp branch. */
215
- export interface MergeToHeadBranchStrategy {
200
+ interface MergeToHeadBranchStrategy {
216
201
  readonly type: "merge-to-head";
217
202
  }
218
203
  /** Branch strategy: commits land on an explicit named branch. */
219
- export interface NamedBranchStrategy {
204
+ interface NamedBranchStrategy {
220
205
  readonly type: "branch";
221
206
  readonly branch: string;
222
207
  /**
@@ -228,30 +213,31 @@ export interface NamedBranchStrategy {
228
213
  readonly baseBranch?: string;
229
214
  }
230
215
  /** Branch strategy for bind-mount providers (all three variants). */
231
- export type BindMountBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
216
+ type BindMountBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
232
217
  /** Branch strategy for isolated providers (no head — can't write to host). */
233
- export type IsolatedBranchStrategy = MergeToHeadBranchStrategy | NamedBranchStrategy;
218
+ type IsolatedBranchStrategy = MergeToHeadBranchStrategy | NamedBranchStrategy;
234
219
  /** Branch strategy for no-sandbox providers (all three — same as bind-mount). */
235
- export type NoSandboxBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
220
+ type NoSandboxBranchStrategy = HeadBranchStrategy | MergeToHeadBranchStrategy | NamedBranchStrategy;
236
221
  /** Union of all branch strategy variants. */
237
- export type BranchStrategy = BindMountBranchStrategy | IsolatedBranchStrategy | NoSandboxBranchStrategy;
222
+ type BranchStrategy = BindMountBranchStrategy | IsolatedBranchStrategy | NoSandboxBranchStrategy;
238
223
  /**
239
224
  * A sandbox provider — the pluggable unit that `run()`, `interactive()`, and
240
225
  * `createSandbox()` accept. Tagged for internal dispatch: "bind-mount",
241
226
  * "isolated", or "none". When `NoSandboxProvider` is used, the agent runs
242
227
  * directly on the host with no container isolation — opt in at your own risk.
243
228
  */
244
- export type SandboxProvider = BindMountSandboxProvider | IsolatedSandboxProvider | NoSandboxProvider;
229
+ type SandboxProvider = BindMountSandboxProvider | IsolatedSandboxProvider | NoSandboxProvider;
245
230
  /** @deprecated Use `SandboxProvider` — it now includes `NoSandboxProvider`. */
246
- export type AnySandboxProvider = SandboxProvider;
231
+ type AnySandboxProvider = SandboxProvider;
247
232
  /**
248
233
  * Create a bind-mount sandbox provider from a config object.
249
234
  * The returned provider can be passed to `run()` or `createSandbox()`.
250
235
  */
251
- export declare const createBindMountSandboxProvider: (config: BindMountSandboxProviderConfig) => BindMountSandboxProvider;
236
+ declare const createBindMountSandboxProvider: (config: BindMountSandboxProviderConfig) => BindMountSandboxProvider;
252
237
  /**
253
238
  * Create an isolated sandbox provider from a config object.
254
239
  * The returned provider can be passed to `run()` or `createSandbox()`.
255
240
  */
256
- export declare const createIsolatedSandboxProvider: (config: IsolatedSandboxProviderConfig) => IsolatedSandboxProvider;
257
- //# sourceMappingURL=SandboxProvider.d.ts.map
241
+ declare const createIsolatedSandboxProvider: (config: IsolatedSandboxProviderConfig) => IsolatedSandboxProvider;
242
+
243
+ export { type AnySandboxProvider as A, type BindMountSandboxHandle as B, type ExecResult as E, type HeadBranchStrategy as H, type IsolatedSandboxProvider as I, type MergeToHeadBranchStrategy as M, type NoSandboxProvider as N, type SandboxProvider as S, type BranchStrategy as a, type NamedBranchStrategy as b, type BindMountBranchStrategy as c, type BindMountCreateOptions as d, type BindMountSandboxProvider as e, type BindMountSandboxProviderConfig as f, type InteractiveExecOptions as g, type IsolatedBranchStrategy as h, type IsolatedCreateOptions as i, type IsolatedSandboxHandle as j, type IsolatedSandboxProviderConfig as k, type NoSandboxBranchStrategy as l, type NoSandboxHandle as m, createBindMountSandboxProvider as n, createIsolatedSandboxProvider as o };
@@ -0,0 +1,99 @@
1
+ import { createRequire } from 'node:module';
2
+ import { MAX_TAIL_CHARS, BoundedTail } from './chunk-NGBM7T3E.js';
3
+ import { spawn } from 'child_process';
4
+ import { createInterface } from 'readline';
5
+
6
+ createRequire(import.meta.url);
7
+ var noSandbox = (options) => ({
8
+ tag: "none",
9
+ name: "no-sandbox",
10
+ env: options?.env ?? {},
11
+ create: async (createOptions) => {
12
+ const worktreePath = createOptions.worktreePath;
13
+ const processEnv = { ...process.env, ...createOptions.env };
14
+ const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
15
+ const handle = {
16
+ worktreePath,
17
+ exec: (command, opts) => {
18
+ const cwd = opts?.cwd ?? worktreePath;
19
+ return new Promise((resolve, reject) => {
20
+ const proc = spawn("sh", ["-c", command], {
21
+ cwd,
22
+ env: processEnv,
23
+ stdio: [
24
+ opts?.stdin !== void 0 ? "pipe" : "ignore",
25
+ "pipe",
26
+ "pipe"
27
+ ]
28
+ });
29
+ if (opts?.stdin !== void 0) {
30
+ proc.stdin.write(opts.stdin);
31
+ proc.stdin.end();
32
+ }
33
+ proc.on("error", (error) => {
34
+ reject(new Error(`exec failed: ${error.message}`));
35
+ });
36
+ if (opts?.onLine) {
37
+ const onLine = opts.onLine;
38
+ const stdoutTail = new BoundedTail(maxOutputTailChars, "\n");
39
+ const stderrTail = new BoundedTail(maxOutputTailChars, "");
40
+ const rl = createInterface({ input: proc.stdout });
41
+ rl.on("line", (line) => {
42
+ stdoutTail.push(line);
43
+ onLine(line);
44
+ });
45
+ proc.stderr.on("data", (chunk) => {
46
+ stderrTail.push(chunk.toString());
47
+ });
48
+ proc.on("close", (code) => {
49
+ resolve({
50
+ stdout: stdoutTail.toString(),
51
+ stderr: stderrTail.toString(),
52
+ exitCode: code ?? 0
53
+ });
54
+ });
55
+ } else {
56
+ const stdoutChunks = [];
57
+ const stderrChunks = [];
58
+ proc.stdout.on("data", (chunk) => {
59
+ stdoutChunks.push(chunk.toString());
60
+ });
61
+ proc.stderr.on("data", (chunk) => {
62
+ stderrChunks.push(chunk.toString());
63
+ });
64
+ proc.on("close", (code) => {
65
+ resolve({
66
+ stdout: stdoutChunks.join(""),
67
+ stderr: stderrChunks.join(""),
68
+ exitCode: code ?? 0
69
+ });
70
+ });
71
+ }
72
+ });
73
+ },
74
+ interactiveExec: (args, opts) => {
75
+ return new Promise((resolve, reject) => {
76
+ const [cmd, ...rest] = args;
77
+ const proc = spawn(cmd, rest, {
78
+ cwd: opts.cwd ?? worktreePath,
79
+ env: processEnv,
80
+ stdio: [opts.stdin, opts.stdout, opts.stderr]
81
+ });
82
+ proc.on("error", (error) => {
83
+ reject(new Error(`exec failed: ${error.message}`));
84
+ });
85
+ proc.on("close", (code) => {
86
+ resolve({ exitCode: code ?? 0 });
87
+ });
88
+ });
89
+ },
90
+ close: async () => {
91
+ }
92
+ };
93
+ return handle;
94
+ }
95
+ });
96
+
97
+ export { noSandbox };
98
+ //# sourceMappingURL=chunk-72UVAC7B.js.map
99
+ //# sourceMappingURL=chunk-72UVAC7B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sandboxes/no-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-72UVAC7B.js","sourcesContent":["/**\n * No-sandbox provider — runs the agent directly on the host with no container isolation.\n *\n * Usage:\n * import { noSandbox } from \"sandcastle/sandboxes/no-sandbox\";\n * await interactive({ agent: claudeCode(\"claude-opus-4-7\"), sandbox: noSandbox() });\n *\n * Accepted by `run()`, `interactive()`, and `createSandbox()`. Skips\n * container isolation entirely — the agent executes on the host. Does not\n * pass `--dangerously-skip-permissions` to the agent — the user manages\n * permissions themselves.\n */\n\nimport { spawn, type StdioOptions } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport type {\n NoSandboxProvider,\n NoSandboxHandle,\n ExecResult,\n InteractiveExecOptions,\n} from \"../SandboxProvider.js\";\nimport { BoundedTail, MAX_TAIL_CHARS } from \"../boundedTail.js\";\n\nexport interface NoSandboxOptions {\n /** Environment variables injected by this provider. Merged at launch time. */\n readonly env?: Record<string, string>;\n /**\n * Maximum number of characters of streamed `exec` output retained per stream\n * (stdout and stderr) when an `onLine` callback is supplied (default: 64KiB).\n *\n * Output is delivered live to `onLine` regardless; this only bounds the tail\n * returned in `ExecResult`, preventing a long-running agent's output from\n * overflowing V8's max string length and crashing the run.\n */\n readonly maxOutputTailChars?: number;\n}\n\n/**\n * Create a no-sandbox provider.\n *\n * The returned provider runs the agent directly on the host. All three\n * branch strategies are supported (head, merge-to-head, branch),\n * defaulting to head.\n */\nexport const noSandbox = (options?: NoSandboxOptions): NoSandboxProvider => ({\n tag: \"none\",\n name: \"no-sandbox\",\n env: options?.env ?? {},\n create: async (createOptions): Promise<NoSandboxHandle> => {\n const worktreePath = createOptions.worktreePath;\n const processEnv = { ...process.env, ...createOptions.env };\n const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;\n\n const handle: NoSandboxHandle = {\n worktreePath,\n\n exec: (\n command: string,\n opts?: {\n onLine?: (line: string) => void;\n cwd?: string;\n sudo?: boolean;\n stdin?: string;\n },\n ): Promise<ExecResult> => {\n // sudo is a no-op for no-sandbox — the user is already on the host\n const cwd = opts?.cwd ?? worktreePath;\n\n return new Promise((resolve, reject) => {\n const proc = spawn(\"sh\", [\"-c\", command], {\n cwd,\n env: processEnv,\n stdio: [\n opts?.stdin !== undefined ? \"pipe\" : \"ignore\",\n \"pipe\",\n \"pipe\",\n ],\n });\n\n if (opts?.stdin !== undefined) {\n proc.stdin!.write(opts.stdin);\n proc.stdin!.end();\n }\n\n proc.on(\"error\", (error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n if (opts?.onLine) {\n const onLine = opts.onLine;\n const stdoutTail = new BoundedTail(maxOutputTailChars, \"\\n\");\n const stderrTail = new BoundedTail(maxOutputTailChars, \"\");\n const rl = createInterface({ input: proc.stdout! });\n rl.on(\"line\", (line) => {\n stdoutTail.push(line);\n onLine(line);\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrTail.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutTail.toString(),\n stderr: stderrTail.toString(),\n exitCode: code ?? 0,\n });\n });\n } else {\n const stdoutChunks: string[] = [];\n const stderrChunks: string[] = [];\n proc.stdout!.on(\"data\", (chunk: Buffer) => {\n stdoutChunks.push(chunk.toString());\n });\n proc.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk.toString());\n });\n proc.on(\"close\", (code) => {\n resolve({\n stdout: stdoutChunks.join(\"\"),\n stderr: stderrChunks.join(\"\"),\n exitCode: code ?? 0,\n });\n });\n }\n });\n },\n\n interactiveExec: (\n args: string[],\n opts: InteractiveExecOptions,\n ): Promise<{ exitCode: number }> => {\n return new Promise((resolve, reject) => {\n const [cmd, ...rest] = args;\n const proc = spawn(cmd!, rest, {\n cwd: opts.cwd ?? worktreePath,\n env: processEnv,\n stdio: [opts.stdin, opts.stdout, opts.stderr] as StdioOptions,\n });\n\n proc.on(\"error\", (error: Error) => {\n reject(new Error(`exec failed: ${error.message}`));\n });\n\n proc.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code ?? 0 });\n });\n });\n },\n\n close: async (): Promise<void> => {\n // No-op — no container to tear down\n },\n };\n\n return handle;\n },\n});\n"]}
@@ -0,0 +1,22 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ createRequire(import.meta.url);
4
+
5
+ // src/SandboxProvider.ts
6
+ var createBindMountSandboxProvider = (config) => ({
7
+ tag: "bind-mount",
8
+ name: config.name,
9
+ env: config.env ?? {},
10
+ sandboxHomedir: config.sandboxHomedir,
11
+ create: config.create
12
+ });
13
+ var createIsolatedSandboxProvider = (config) => ({
14
+ tag: "isolated",
15
+ name: config.name,
16
+ env: config.env ?? {},
17
+ create: config.create
18
+ });
19
+
20
+ export { createBindMountSandboxProvider, createIsolatedSandboxProvider };
21
+ //# sourceMappingURL=chunk-BIWNFKGV.js.map
22
+ //# sourceMappingURL=chunk-BIWNFKGV.js.map