@ai-hero/sandcastle 0.4.7 → 0.5.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.
Files changed (122) hide show
  1. package/README.md +258 -69
  2. package/dist/AgentProvider.d.ts +9 -0
  3. package/dist/AgentProvider.d.ts.map +1 -1
  4. package/dist/AgentProvider.js +14 -2
  5. package/dist/AgentProvider.js.map +1 -1
  6. package/dist/CopyToWorktree.d.ts +9 -0
  7. package/dist/CopyToWorktree.d.ts.map +1 -0
  8. package/dist/{CopyToWorkspace.js → CopyToWorktree.js} +9 -3
  9. package/dist/CopyToWorktree.js.map +1 -0
  10. package/dist/DockerLifecycle.d.ts +2 -0
  11. package/dist/DockerLifecycle.d.ts.map +1 -1
  12. package/dist/DockerLifecycle.js +7 -0
  13. package/dist/DockerLifecycle.js.map +1 -1
  14. package/dist/ErrorHandler.d.ts.map +1 -1
  15. package/dist/ErrorHandler.js +22 -2
  16. package/dist/ErrorHandler.js.map +1 -1
  17. package/dist/InitService.d.ts +6 -1
  18. package/dist/InitService.d.ts.map +1 -1
  19. package/dist/InitService.js +90 -56
  20. package/dist/InitService.js.map +1 -1
  21. package/dist/MountConfig.d.ts +13 -2
  22. package/dist/MountConfig.d.ts.map +1 -1
  23. package/dist/Orchestrator.d.ts +14 -2
  24. package/dist/Orchestrator.d.ts.map +1 -1
  25. package/dist/Orchestrator.js +56 -12
  26. package/dist/Orchestrator.js.map +1 -1
  27. package/dist/PodmanLifecycle.d.ts +10 -0
  28. package/dist/PodmanLifecycle.d.ts.map +1 -1
  29. package/dist/PodmanLifecycle.js +22 -0
  30. package/dist/PodmanLifecycle.js.map +1 -1
  31. package/dist/PromptPreprocessor.d.ts +2 -2
  32. package/dist/PromptPreprocessor.d.ts.map +1 -1
  33. package/dist/PromptPreprocessor.js +7 -2
  34. package/dist/PromptPreprocessor.js.map +1 -1
  35. package/dist/SandboxFactory.d.ts +13 -11
  36. package/dist/SandboxFactory.d.ts.map +1 -1
  37. package/dist/SandboxFactory.js +59 -41
  38. package/dist/SandboxFactory.js.map +1 -1
  39. package/dist/SandboxLifecycle.d.ts +23 -5
  40. package/dist/SandboxLifecycle.d.ts.map +1 -1
  41. package/dist/SandboxLifecycle.js +88 -18
  42. package/dist/SandboxLifecycle.js.map +1 -1
  43. package/dist/SandboxProvider.d.ts +11 -7
  44. package/dist/SandboxProvider.d.ts.map +1 -1
  45. package/dist/SandboxProvider.js.map +1 -1
  46. package/dist/SessionStore.d.ts +50 -0
  47. package/dist/SessionStore.d.ts.map +1 -0
  48. package/dist/SessionStore.js +120 -0
  49. package/dist/SessionStore.js.map +1 -0
  50. package/dist/WorktreeManager.d.ts +3 -3
  51. package/dist/WorktreeManager.d.ts.map +1 -1
  52. package/dist/WorktreeManager.js +14 -3
  53. package/dist/WorktreeManager.js.map +1 -1
  54. package/dist/createSandbox.d.ts +25 -10
  55. package/dist/createSandbox.d.ts.map +1 -1
  56. package/dist/createSandbox.js +253 -133
  57. package/dist/createSandbox.js.map +1 -1
  58. package/dist/createWorktree.d.ts +121 -0
  59. package/dist/createWorktree.d.ts.map +1 -0
  60. package/dist/createWorktree.js +318 -0
  61. package/dist/createWorktree.js.map +1 -0
  62. package/dist/errors.d.ts +110 -6
  63. package/dist/errors.d.ts.map +1 -1
  64. package/dist/errors.js +41 -3
  65. package/dist/errors.js.map +1 -1
  66. package/dist/index.d.ts +6 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/interactive.d.ts +1 -1
  71. package/dist/interactive.d.ts.map +1 -1
  72. package/dist/interactive.js +63 -46
  73. package/dist/interactive.js.map +1 -1
  74. package/dist/run.d.ts +10 -10
  75. package/dist/run.d.ts.map +1 -1
  76. package/dist/run.js +23 -6
  77. package/dist/run.js.map +1 -1
  78. package/dist/sandboxes/daytona.js +4 -4
  79. package/dist/sandboxes/daytona.js.map +1 -1
  80. package/dist/sandboxes/docker.d.ts +9 -0
  81. package/dist/sandboxes/docker.d.ts.map +1 -1
  82. package/dist/sandboxes/docker.js +35 -5
  83. package/dist/sandboxes/docker.js.map +1 -1
  84. package/dist/sandboxes/no-sandbox.js +4 -4
  85. package/dist/sandboxes/no-sandbox.js.map +1 -1
  86. package/dist/sandboxes/podman.d.ts +9 -0
  87. package/dist/sandboxes/podman.d.ts.map +1 -1
  88. package/dist/sandboxes/podman.js +47 -5
  89. package/dist/sandboxes/podman.js.map +1 -1
  90. package/dist/sandboxes/test-bind-mount.d.ts +17 -0
  91. package/dist/sandboxes/test-bind-mount.d.ts.map +1 -0
  92. package/dist/sandboxes/test-bind-mount.js +91 -0
  93. package/dist/sandboxes/test-bind-mount.js.map +1 -0
  94. package/dist/sandboxes/test-isolated.js +5 -5
  95. package/dist/sandboxes/test-isolated.js.map +1 -1
  96. package/dist/sandboxes/vercel.js +7 -7
  97. package/dist/sandboxes/vercel.js.map +1 -1
  98. package/dist/startSandbox.d.ts +7 -6
  99. package/dist/startSandbox.d.ts.map +1 -1
  100. package/dist/startSandbox.js +38 -19
  101. package/dist/startSandbox.js.map +1 -1
  102. package/dist/syncIn.js +7 -7
  103. package/dist/syncIn.js.map +1 -1
  104. package/dist/syncOut.js +6 -6
  105. package/dist/syncOut.js.map +1 -1
  106. package/dist/templates/parallel-planner/implement-prompt.md +2 -2
  107. package/dist/templates/parallel-planner/main.mts +3 -7
  108. package/dist/templates/parallel-planner/merge-prompt.md +5 -1
  109. package/dist/templates/parallel-planner-with-review/implement-prompt.md +2 -2
  110. package/dist/templates/parallel-planner-with-review/main.mts +29 -63
  111. package/dist/templates/parallel-planner-with-review/merge-prompt.md +5 -1
  112. package/dist/templates/sequential-reviewer/main.mts +4 -4
  113. package/dist/templates/simple-loop/main.mts +9 -7
  114. package/package.json +1 -1
  115. package/dist/CopyToWorkspace.d.ts +0 -8
  116. package/dist/CopyToWorkspace.d.ts.map +0 -1
  117. package/dist/CopyToWorkspace.js.map +0 -1
  118. package/dist/templates/blank/.env.example +0 -5
  119. package/dist/templates/parallel-planner/.env.example +0 -5
  120. package/dist/templates/parallel-planner-with-review/.env.example +0 -5
  121. package/dist/templates/sequential-reviewer/.env.example +0 -5
  122. package/dist/templates/simple-loop/.env.example +0 -5
package/README.md CHANGED
@@ -65,14 +65,16 @@ await run({
65
65
 
66
66
  ## Sandbox Providers
67
67
 
68
- Sandcastle uses a `SandboxProvider` to create isolated environments. The `sandbox` option on `run()` and `createSandbox()` accepts any provider. A no-sandbox option is also available for `interactive()` only. Built-in providers:
68
+ Sandcastle uses a `SandboxProvider` to create isolated environments. The `sandbox` option on `run()` and `createSandbox()` accepts any provider. A no-sandbox option is also available for `interactive()` and `wt.interactive()`. Built-in providers:
69
69
 
70
- | Provider | Import path | Type | Accepted by |
71
- | ---------- | ------------------------------------------ | ---------- | ------------------------------------------- |
72
- | Docker | `@ai-hero/sandcastle/sandboxes/docker` | Bind-mount | `run()`, `createSandbox()`, `interactive()` |
73
- | Podman | `@ai-hero/sandcastle/sandboxes/podman` | Bind-mount | `run()`, `createSandbox()`, `interactive()` |
74
- | Vercel | `@ai-hero/sandcastle/sandboxes/vercel` | Isolated | `run()`, `createSandbox()`, `interactive()` |
75
- | No-sandbox | `@ai-hero/sandcastle/sandboxes/no-sandbox` | None | `interactive()` only |
70
+ | Provider | Import path | Type | Accepted by |
71
+ | ---------- | ------------------------------------------ | ---------- | --------------------------------------------- |
72
+ | Docker | `@ai-hero/sandcastle/sandboxes/docker` | Bind-mount | `run()`, `createSandbox()`, `interactive()` |
73
+ | Podman | `@ai-hero/sandcastle/sandboxes/podman` | Bind-mount | `run()`, `createSandbox()`, `interactive()` |
74
+ | Vercel | `@ai-hero/sandcastle/sandboxes/vercel` | Isolated | `run()`, `createSandbox()`, `interactive()` |
75
+ | No-sandbox | `@ai-hero/sandcastle/sandboxes/no-sandbox` | None | `interactive()`, `wt.interactive()` (default) |
76
+
77
+ Worktree methods (`wt.run()`, `wt.interactive()`, `wt.createSandbox()`) accept the same providers as their top-level counterparts. `wt.interactive()` defaults to `noSandbox()` when no sandbox is specified.
76
78
 
77
79
  ```typescript
78
80
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -91,7 +93,7 @@ await run({
91
93
  await interactive({
92
94
  agent: claudeCode("claude-opus-4-6"),
93
95
  sandbox: noSandbox(),
94
- prompt: "...",
96
+ prompt: "...", // optional — omit to launch the TUI with no initial prompt
95
97
  });
96
98
  ```
97
99
 
@@ -111,7 +113,8 @@ const result = await run({
111
113
  promptFile: ".sandcastle/prompt.md",
112
114
  });
113
115
 
114
- console.log(result.iterationsRun); // number of iterations executed
116
+ console.log(result.iterations.length); // number of iterations executed
117
+ console.log(result.iterations); // per-iteration results with optional sessionId
115
118
  console.log(result.commits); // array of { sha } for commits created
116
119
  console.log(result.branch); // target branch name
117
120
  ```
@@ -132,11 +135,16 @@ const result = await run({
132
135
  sandbox: docker({
133
136
  imageName: "sandcastle:local",
134
137
  // Optional: mount host directories into the sandbox (e.g. package manager caches)
138
+ // hostPath supports absolute, tilde-expanded (~), and relative paths (resolved from cwd).
139
+ // sandboxPath supports absolute and relative paths (resolved from the sandbox repo directory).
135
140
  mounts: [
136
141
  { hostPath: "~/.npm", sandboxPath: "/home/agent/.npm", readonly: true },
142
+ { hostPath: "data", sandboxPath: "data" }, // mounts <cwd>/data → <sandbox-repo>/data
137
143
  ],
138
144
  // Optional: provider-level env vars merged at launch time
139
145
  env: { DOCKER_SPECIFIC: "value" },
146
+ // Optional: attach container to Docker network(s) — string or string[]
147
+ network: "my-network",
140
148
  }),
141
149
 
142
150
  // Branch strategy — controls how the agent's changes relate to branches.
@@ -158,15 +166,20 @@ const result = await run({
158
166
  // Display name for this run, shown as a prefix in log output.
159
167
  name: "fix-issue-42",
160
168
 
161
- // Lifecycle hooks arrays of shell commands run in parallel inside the sandbox.
169
+ // Lifecycle hooks grouped by where they run: host or sandbox.
162
170
  hooks: {
163
- // Runs after the sandbox is ready.
164
- onSandboxReady: [{ command: "npm install" }],
171
+ host: {
172
+ onWorktreeReady: [{ command: "cp .env.example .env" }],
173
+ onSandboxReady: [{ command: "echo setup done" }],
174
+ },
175
+ sandbox: {
176
+ onSandboxReady: [{ command: "npm install" }],
177
+ },
165
178
  },
166
179
 
167
180
  // Host-relative file paths to copy into the sandbox before the container starts.
168
181
  // Not supported with branchStrategy: { type: "head" }.
169
- copyToWorkspace: [".env"],
182
+ copyToWorktree: [".env"],
170
183
 
171
184
  // How to record progress. Default: write to a file under .sandcastle/logs/
172
185
  logging: { type: "file", path: ".sandcastle/logs/my-run.log" },
@@ -180,7 +193,7 @@ const result = await run({
180
193
  idleTimeoutSeconds: 600,
181
194
  });
182
195
 
183
- console.log(result.iterationsRun); // number of iterations executed
196
+ console.log(result.iterations.length); // number of iterations executed
184
197
  console.log(result.completionSignal); // matched signal string, or undefined if none fired
185
198
  console.log(result.commits); // array of { sha } for commits created
186
199
  console.log(result.branch); // target branch name
@@ -220,7 +233,7 @@ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
220
233
  await using sandbox = await createSandbox({
221
234
  branch: "agent/fix-42",
222
235
  sandbox: docker(),
223
- hooks: { onSandboxReady: [{ command: "npm install" }] },
236
+ hooks: { sandbox: { onSandboxReady: [{ command: "npm install" }] } },
224
237
  });
225
238
 
226
239
  // Step 1: implement
@@ -263,8 +276,8 @@ if (closeResult.preservedWorktreePath) {
263
276
  | -------------------------- | --------------- | ------- | ------------------------------------------------------------------------ |
264
277
  | `branch` | string | — | **Required.** Explicit branch for the sandbox |
265
278
  | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (e.g. `docker()`, `podman()`) |
266
- | `hooks` | object | — | Lifecycle hooks (`onSandboxReady`) — run once at creation time |
267
- | `copyToWorkspace` | string[] | — | Host-relative file paths to copy into the sandbox at creation time |
279
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) — run once at creation time |
280
+ | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the sandbox at creation time |
268
281
  | `throwOnDuplicateWorktree` | boolean | `true` | When `false`, reuse an existing worktree instead of failing on collision |
269
282
 
270
283
  #### `Sandbox`
@@ -293,13 +306,13 @@ if (closeResult.preservedWorktreePath) {
293
306
 
294
307
  #### `SandboxRunResult`
295
308
 
296
- | Field | Type | Description |
297
- | ------------------ | ----------- | ------------------------------------------------------------------ |
298
- | `iterationsRun` | number | Number of iterations executed |
299
- | `completionSignal` | string? | The matched completion signal string, or `undefined` if none fired |
300
- | `stdout` | string | Combined agent output from all iterations |
301
- | `commits` | `{ sha }[]` | Commits created during the run |
302
- | `logFilePath` | string? | Path to the log file (only when logging to a file) |
309
+ | Field | Type | Description |
310
+ | ------------------ | ------------------- | ------------------------------------------------------------------ |
311
+ | `iterations` | `IterationResult[]` | Per-iteration results (use `.length` for the count) |
312
+ | `completionSignal` | string? | The matched completion signal string, or `undefined` if none fired |
313
+ | `stdout` | string | Combined agent output from all iterations |
314
+ | `commits` | `{ sha }[]` | Commits created during the run |
315
+ | `logFilePath` | string? | Path to the log file (only when logging to a file) |
303
316
 
304
317
  #### `CloseResult`
305
318
 
@@ -307,6 +320,124 @@ if (closeResult.preservedWorktreePath) {
307
320
  | ----------------------- | ------- | ------------------------------------------------------------------------ |
308
321
  | `preservedWorktreePath` | string? | Host path to the preserved worktree, set when it had uncommitted changes |
309
322
 
323
+ ### `createWorktree()` — independent worktree lifecycle
324
+
325
+ Use `createWorktree()` when you need a worktree (git worktree) as an independent, first-class concept — separate from any sandbox. This is useful when you want to run an interactive session first and then hand the same worktree to a sandboxed AFK agent.
326
+
327
+ Only `branch` and `merge-to-head` strategies are accepted; `head` is a compile-time type error since it means no worktree.
328
+
329
+ ```typescript
330
+ import { createWorktree } from "@ai-hero/sandcastle";
331
+
332
+ await using wt = await createWorktree({
333
+ branchStrategy: { type: "branch", branch: "agent/fix-42" },
334
+ copyToWorktree: ["node_modules"],
335
+ });
336
+
337
+ console.log(wt.worktreePath); // host path to the worktree
338
+ console.log(wt.branch); // "agent/fix-42"
339
+
340
+ // Run an interactive session in the worktree (defaults to noSandbox)
341
+ await wt.interactive({
342
+ agent: claudeCode("claude-opus-4-6"),
343
+ prompt: "Explore the codebase and understand the bug.",
344
+ });
345
+
346
+ // Run an AFK agent in the worktree (sandbox is required)
347
+ const result = await wt.run({
348
+ agent: claudeCode("claude-opus-4-6"),
349
+ sandbox: docker({ imageName: "sandcastle:myrepo" }),
350
+ prompt: "Fix issue #42.",
351
+ maxIterations: 3,
352
+ });
353
+ console.log(result.commits); // commits made during the run
354
+
355
+ // Create a long-lived sandbox from the worktree
356
+ import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
357
+
358
+ await using sandbox = await wt.createSandbox({
359
+ sandbox: docker(),
360
+ hooks: { sandbox: { onSandboxReady: [{ command: "npm install" }] } },
361
+ });
362
+
363
+ // sandbox.close() tears down the container only — the worktree stays
364
+ await sandbox.close();
365
+
366
+ // wt.close() cleans up the worktree
367
+ ```
368
+
369
+ `wt.close()` checks for uncommitted changes: if the worktree is dirty, it's preserved on disk; if clean, it's removed. `await using` calls `close()` automatically. The worktree persists after `run()`, `interactive()`, and `createSandbox()` complete, so you can hand it to another agent or inspect it.
370
+
371
+ **Split ownership**: When a sandbox is created via `wt.createSandbox()`, `sandbox.close()` tears down the container only — the worktree remains. `wt.close()` is responsible for worktree cleanup. This differs from the top-level `createSandbox()`, where `sandbox.close()` owns both container and worktree.
372
+
373
+ #### `CreateWorktreeOptions`
374
+
375
+ | Option | Type | Default | Description |
376
+ | ---------------- | ---------------------- | ------- | ------------------------------------------------------------------------- |
377
+ | `branchStrategy` | WorktreeBranchStrategy | — | **Required.** `{ type: "branch", branch }` or `{ type: "merge-to-head" }` |
378
+ | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the worktree at creation time |
379
+
380
+ #### `Worktree`
381
+
382
+ | Property / Method | Type | Description |
383
+ | ------------------------ | --------------------------------------------------------------------- | --------------------------------------------------- |
384
+ | `branch` | string | The branch the worktree is on |
385
+ | `worktreePath` | string | Host path to the worktree |
386
+ | `run(options)` | `(options: WorktreeRunOptions) => Promise<WorktreeRunResult>` | Run an AFK agent in the worktree (sandbox required) |
387
+ | `interactive(options)` | `(options: WorktreeInteractiveOptions) => Promise<InteractiveResult>` | Run an interactive agent session in the worktree |
388
+ | `createSandbox(options)` | `(options: WorktreeCreateSandboxOptions) => Promise<Sandbox>` | Create a long-lived sandbox backed by this worktree |
389
+ | `close()` | `() => Promise<CloseResult>` | Clean up the worktree (preserves if dirty) |
390
+ | `[Symbol.asyncDispose]` | `() => Promise<void>` | Auto cleanup via `await using` |
391
+
392
+ #### `WorktreeInteractiveOptions`
393
+
394
+ | Option | Type | Default | Description |
395
+ | ------------ | ---------------------- | ------------- | ---------------------------------------------------- |
396
+ | `agent` | AgentProvider | — | **Required.** Agent provider |
397
+ | `sandbox` | AnySandboxProvider | `noSandbox()` | Sandbox provider (defaults to no sandbox) |
398
+ | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
399
+ | `promptFile` | string | — | Path to prompt file |
400
+ | `name` | string | — | Optional session name |
401
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
402
+ | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
403
+ | `env` | Record<string, string> | — | Environment variables to inject into the sandbox |
404
+
405
+ #### `WorktreeRunOptions`
406
+
407
+ | Option | Type | Default | Description |
408
+ | -------------------- | ---------------------- | ------- | ------------------------------------------------------------- |
409
+ | `agent` | AgentProvider | — | **Required.** Agent provider |
410
+ | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (AFK agents must be sandboxed) |
411
+ | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
412
+ | `promptFile` | string | — | Path to prompt file |
413
+ | `maxIterations` | number | 1 | Maximum iterations to run |
414
+ | `completionSignal` | string \| string[] | — | Substring(s) to stop the iteration loop early |
415
+ | `idleTimeoutSeconds` | number | 600 | Idle timeout in seconds |
416
+ | `name` | string | — | Optional run name |
417
+ | `logging` | LoggingOption | file | Logging mode |
418
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
419
+ | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
420
+ | `env` | Record<string, string> | — | Environment variables to inject into the sandbox |
421
+
422
+ #### `WorktreeRunResult`
423
+
424
+ | Property | Type | Description |
425
+ | ------------------ | ------------------- | ------------------------------------------------------ |
426
+ | `iterations` | `IterationResult[]` | Per-iteration results (use `.length` for the count) |
427
+ | `completionSignal` | string | The matched completion signal, or undefined |
428
+ | `stdout` | string | Combined stdout output from all agent iterations |
429
+ | `commits` | { sha: string }[] | List of commits made by the agent during the run |
430
+ | `branch` | string | The branch name the agent worked on |
431
+ | `logFilePath` | string | Path to the log file, if logging was drained to a file |
432
+
433
+ #### `WorktreeCreateSandboxOptions`
434
+
435
+ | Option | Type | Default | Description |
436
+ | ---------------- | --------------- | ------- | ------------------------------------------------------------------- |
437
+ | `sandbox` | SandboxProvider | — | **Required.** Sandbox provider (e.g. `docker()`) |
438
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
439
+ | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the worktree at creation time |
440
+
310
441
  ## How it works
311
442
 
312
443
  Sandcastle uses a **branch strategy** configured on the sandbox provider to control how the agent's changes relate to branches. There are three strategies:
@@ -338,7 +469,7 @@ You must provide exactly one of:
338
469
 
339
470
  Use `` !`command` `` expressions in your prompt to pull in dynamic context. Each expression is replaced with the command's stdout before the prompt is sent to the agent. All expressions in a prompt run **in parallel** for faster expansion.
340
471
 
341
- Commands run **inside the sandbox** after `onSandboxReady` hooks complete, so they see the same repo state the agent sees (including installed dependencies).
472
+ Commands run **inside the sandbox** after `sandbox.onSandboxReady` hooks complete, so they see the same repo state the agent sees (including installed dependencies).
342
473
 
343
474
  ```markdown
344
475
  # Open issues
@@ -501,26 +632,62 @@ Removes the Podman image.
501
632
  | `prompt` | string | — | Inline prompt (mutually exclusive with `promptFile`) |
502
633
  | `promptFile` | string | — | Path to prompt file (mutually exclusive with `prompt`) |
503
634
  | `maxIterations` | number | `1` | Maximum iterations to run |
504
- | `hooks` | object | — | Lifecycle hooks (`onSandboxReady`) |
635
+ | `hooks` | SandboxHooks | — | Lifecycle hooks (`host.*`, `sandbox.*`) |
505
636
  | `name` | string | — | Display name for the run, shown as a prefix in log output |
506
637
  | `promptArgs` | PromptArgs | — | Key-value map for `{{KEY}}` placeholder substitution |
507
638
  | `branchStrategy` | BranchStrategy | per-provider default | Branch strategy: `{ type: 'head' }`, `{ type: 'merge-to-head' }`, or `{ type: 'branch', branch: '…' }` |
508
- | `copyToWorkspace` | string[] | — | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`) |
639
+ | `copyToWorktree` | string[] | — | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`) |
509
640
  | `logging` | object | file (auto-generated) | `{ type: 'file', path }` or `{ type: 'stdout' }` |
510
641
  | `completionSignal` | string \| string[] | `<promise>COMPLETE</promise>` | String or array of strings the agent emits to stop the iteration loop early |
511
642
  | `idleTimeoutSeconds` | number | `600` | Idle timeout in seconds — resets on each agent output event |
512
643
  | `throwOnDuplicateWorktree` | boolean | `true` | When `false`, reuse an existing worktree for the target branch instead of failing on collision |
644
+ | `resumeSession` | string | — | Resume a prior Claude Code session by ID. Incompatible with `maxIterations > 1`. Session file must exist on host. |
513
645
 
514
646
  ### `RunResult`
515
647
 
516
- | Field | Type | Description |
517
- | ------------------ | ----------- | ------------------------------------------------------------------ |
518
- | `iterationsRun` | number | Number of iterations that were executed |
519
- | `completionSignal` | string? | The matched completion signal string, or `undefined` if none fired |
520
- | `stdout` | string | Agent output |
521
- | `commits` | `{ sha }[]` | Commits created during the run |
522
- | `branch` | string | Target branch name |
523
- | `logFilePath` | string? | Path to the log file (only when logging to a file) |
648
+ | Field | Type | Description |
649
+ | ------------------ | ------------------- | ------------------------------------------------------------------ |
650
+ | `iterations` | `IterationResult[]` | Per-iteration results (use `.length` for the count) |
651
+ | `completionSignal` | string? | The matched completion signal string, or `undefined` if none fired |
652
+ | `stdout` | string | Agent output |
653
+ | `commits` | `{ sha }[]` | Commits created during the run |
654
+ | `branch` | string | Target branch name |
655
+ | `logFilePath` | string? | Path to the log file (only when logging to a file) |
656
+
657
+ ### `IterationResult`
658
+
659
+ | Field | Type | Description |
660
+ | ----------------- | ------- | ------------------------------------------------------------------------------------ |
661
+ | `sessionId` | string? | Claude Code session ID from the init line, or `undefined` for non-Claude agents |
662
+ | `sessionFilePath` | string? | Absolute host path to the captured session JSONL, or `undefined` when capture is off |
663
+
664
+ ### Session capture
665
+
666
+ After each Claude Code iteration, Sandcastle automatically captures the agent's session JSONL from the sandbox to the host at `~/.claude/projects/<encoded-path>/sessions/<session-id>.jsonl`. The `cwd` fields inside each JSONL entry are rewritten to match the host repo root, so `claude --resume` works natively.
667
+
668
+ Session capture is enabled by default for `claudeCode()` and can be opted out via `captureSessions: false`. Non-Claude agent providers never attempt capture. Capture failure fails the run.
669
+
670
+ ### Session resume
671
+
672
+ Pass `resumeSession` to `run()` to continue a prior Claude Code conversation inside a new sandbox:
673
+
674
+ ```typescript
675
+ const result = await run({
676
+ agent: claudeCode("claude-opus-4-6"),
677
+ sandbox: docker(),
678
+ prompt: "Continue where you left off",
679
+ resumeSession: "abc-123-def",
680
+ });
681
+ ```
682
+
683
+ 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. The Claude Code agent receives `--resume <id>` on its print command for iteration 1.
684
+
685
+ Constraints:
686
+
687
+ - `resumeSession` is incompatible with `maxIterations > 1` (throws before sandbox creation).
688
+ - The session file must exist at `~/.claude/projects/<encoded-path>/sessions/<id>.jsonl` (throws before sandbox creation).
689
+ - Only iteration 1 receives the resume flag; subsequent iterations (if any) start fresh.
690
+ - Non-Claude agent providers ignore `resumeSession`.
524
691
 
525
692
  ### `ClaudeCodeOptions`
526
693
 
@@ -530,10 +697,11 @@ The `claudeCode()` factory accepts an optional second argument for provider-spec
530
697
  agent: claudeCode("claude-opus-4-6", { effort: "high" });
531
698
  ```
532
699
 
533
- | Option | Type | Default | Description |
534
- | -------- | -------------------------------------------- | ------- | ------------------------------------------------------- |
535
- | `effort` | `"low"` \| `"medium"` \| `"high"` \| `"max"` | — | Claude Code reasoning effort level (`max` is Opus only) |
536
- | `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
700
+ | Option | Type | Default | Description |
701
+ | ----------------- | -------------------------------------------- | ------- | --------------------------------------------------------- |
702
+ | `effort` | `"low"` \| `"medium"` \| `"high"` \| `"max"` | — | Claude Code reasoning effort level (`max` is Opus only) |
703
+ | `env` | `Record<string, string>` | `{}` | Environment variables injected by this agent provider |
704
+ | `captureSessions` | `boolean` | `true` | Capture agent session JSONL to host for `claude --resume` |
537
705
 
538
706
  ### `CodexOptions`
539
707
 
@@ -583,13 +751,14 @@ Sandcastle ships with built-in providers for Docker, Podman, and Vercel, but you
583
751
 
584
752
  Both provider types return a **sandbox handle** from their `create()` function. The handle exposes:
585
753
 
586
- | Method | Required | Description |
587
- | --------------- | -------- | ---------------------------------------------------------------------------- |
588
- | `exec` | Both | Run a command, optionally streaming stdout line-by-line via `options.onLine` |
589
- | `close` | Both | Tear down the sandbox |
590
- | `copyIn` | Isolated | Copy a file or directory from the host into the sandbox |
591
- | `copyOut` | Isolated | Copy a file from the sandbox to the host |
592
- | `workspacePath` | Both | Absolute path to the workspace inside the sandbox |
754
+ | Method | Required | Description |
755
+ | -------------- | ---------- | ---------------------------------------------------------------------------- |
756
+ | `exec` | Both | Run a command, optionally streaming stdout line-by-line via `options.onLine` |
757
+ | `close` | Both | Tear down the sandbox |
758
+ | `copyFileIn` | Bind-mount | Copy a single file from the host into the sandbox |
759
+ | `copyFileOut` | Both | Copy a single file from the sandbox to the host |
760
+ | `copyIn` | Isolated | Copy a file or directory from the host into the sandbox |
761
+ | `worktreePath` | Both | Absolute path to the repo directory inside the sandbox |
593
762
 
594
763
  ### `ExecResult`
595
764
 
@@ -615,6 +784,8 @@ import {
615
784
  type ExecResult,
616
785
  } from "@ai-hero/sandcastle";
617
786
  import { execFile, spawn } from "node:child_process";
787
+ import { copyFile as fsCopyFile, mkdir as fsMkdir } from "node:fs/promises";
788
+ import { dirname } from "node:path";
618
789
  import { createInterface } from "node:readline";
619
790
 
620
791
  const localProcess = () =>
@@ -623,10 +794,10 @@ const localProcess = () =>
623
794
  create: async (
624
795
  options: BindMountCreateOptions,
625
796
  ): Promise<BindMountSandboxHandle> => {
626
- const workspacePath = options.worktreePath;
797
+ const worktreePath = options.worktreePath;
627
798
 
628
799
  return {
629
- workspacePath,
800
+ worktreePath,
630
801
 
631
802
  exec: (
632
803
  command: string,
@@ -636,7 +807,7 @@ const localProcess = () =>
636
807
  const onLine = opts.onLine;
637
808
  return new Promise((resolve, reject) => {
638
809
  const proc = spawn("sh", ["-c", command], {
639
- cwd: opts?.cwd ?? workspacePath,
810
+ cwd: opts?.cwd ?? worktreePath,
640
811
  stdio: ["ignore", "pipe", "pipe"],
641
812
  });
642
813
 
@@ -668,7 +839,7 @@ const localProcess = () =>
668
839
  execFile(
669
840
  "sh",
670
841
  ["-c", command],
671
- { cwd: opts?.cwd ?? workspacePath, maxBuffer: 10 * 1024 * 1024 },
842
+ { cwd: opts?.cwd ?? worktreePath, maxBuffer: 10 * 1024 * 1024 },
672
843
  (error, stdout, stderr) => {
673
844
  if (error && error.code === undefined) {
674
845
  reject(new Error(`exec failed: ${error.message}`));
@@ -684,6 +855,16 @@ const localProcess = () =>
684
855
  });
685
856
  },
686
857
 
858
+ copyFileIn: async (hostPath: string, sandboxPath: string) => {
859
+ await fsMkdir(dirname(sandboxPath), { recursive: true });
860
+ await fsCopyFile(hostPath, sandboxPath);
861
+ },
862
+
863
+ copyFileOut: async (sandboxPath: string, hostPath: string) => {
864
+ await fsMkdir(dirname(hostPath), { recursive: true });
865
+ await fsCopyFile(sandboxPath, hostPath);
866
+ },
867
+
687
868
  close: async () => {
688
869
  // nothing to tear down for a local process
689
870
  },
@@ -713,11 +894,11 @@ const tempDir = () =>
713
894
  name: "temp-dir",
714
895
  create: async (): Promise<IsolatedSandboxHandle> => {
715
896
  const root = await mkdtemp(join(tmpdir(), "sandbox-"));
716
- const workspacePath = join(root, "workspace");
717
- await mkdir(workspacePath, { recursive: true });
897
+ const worktreePath = join(root, "workspace");
898
+ await mkdir(worktreePath, { recursive: true });
718
899
 
719
900
  return {
720
- workspacePath,
901
+ worktreePath,
721
902
 
722
903
  exec: (
723
904
  command: string,
@@ -727,7 +908,7 @@ const tempDir = () =>
727
908
  const onLine = opts.onLine;
728
909
  return new Promise((resolve, reject) => {
729
910
  const proc = spawn("sh", ["-c", command], {
730
- cwd: opts?.cwd ?? workspacePath,
911
+ cwd: opts?.cwd ?? worktreePath,
731
912
  stdio: ["ignore", "pipe", "pipe"],
732
913
  });
733
914
 
@@ -759,7 +940,7 @@ const tempDir = () =>
759
940
  execFile(
760
941
  "sh",
761
942
  ["-c", command],
762
- { cwd: opts?.cwd ?? workspacePath, maxBuffer: 10 * 1024 * 1024 },
943
+ { cwd: opts?.cwd ?? worktreePath, maxBuffer: 10 * 1024 * 1024 },
763
944
  (error, stdout, stderr) => {
764
945
  if (error && error.code === undefined) {
765
946
  reject(new Error(`exec failed: ${error.message}`));
@@ -891,28 +1072,36 @@ Add your project-specific dependencies (e.g., language runtimes, build tools) to
891
1072
 
892
1073
  ### Hooks
893
1074
 
894
- Hooks are arrays of `{ command, sudo? }` objects executed **in parallel** inside the sandbox. All commands within a hook point run concurrently if any command exits with a non-zero code, the operation fails after all commands settle. To enforce ordering between commands, combine them with `&&` in a single command string.
895
-
896
- | Hook | When it runs | Working directory |
897
- | ---------------- | -------------------------- | ---------------------- |
898
- | `onSandboxReady` | After the sandbox is ready | Sandbox repo directory |
899
-
900
- **`onSandboxReady`** runs after the sandbox is ready. Use it for dependency installation or build steps (e.g., `npm install`).
901
-
902
- Set `sudo: true` to run a command with elevated privileges inside the sandbox:
1075
+ Hooks are grouped by **where** they run — `host` (on the developer's machine) or `sandbox` (inside the container):
903
1076
 
904
1077
  ```ts
905
- await run({
906
- hooks: {
1078
+ hooks: {
1079
+ host: {
1080
+ onWorktreeReady: [{ command: "cp .env.example .env" }],
1081
+ onSandboxReady: [{ command: "echo sandbox is up" }],
1082
+ },
1083
+ sandbox: {
907
1084
  onSandboxReady: [
908
- { command: "apt-get install -y ffmpeg", sudo: true },
909
1085
  { command: "npm install" },
1086
+ { command: "apt-get install -y ffmpeg", sudo: true },
910
1087
  ],
911
1088
  },
912
- // ...
913
- });
1089
+ }
914
1090
  ```
915
1091
 
1092
+ | Hook | Runs on | When | Working directory |
1093
+ | ------------------------ | ------- | -------------------------------------------- | ------------------------------------------- |
1094
+ | `host.onWorktreeReady` | Host | After `copyToWorktree`, before sandbox start | Worktree path (host repo root under `head`) |
1095
+ | `host.onSandboxReady` | Host | After sandbox is up | Worktree path (host repo root under `head`) |
1096
+ | `sandbox.onSandboxReady` | Sandbox | After sandbox is up | Sandbox repo directory |
1097
+
1098
+ **Ordering:** `copyToWorktree` -> `host.onWorktreeReady` (sequential) -> sandbox created -> `host.onSandboxReady` + `sandbox.onSandboxReady` (parallel).
1099
+
1100
+ - **Host hooks** accept `{ command: string }` — no `sudo`, no `cwd`. Use `cd` or inline env in the command string.
1101
+ - **Sandbox hooks** accept `{ command: string; sudo?: boolean }` — set `sudo: true` for elevated privileges.
1102
+ - Within each hook point, sandbox hooks run in parallel; host hooks within `onSandboxReady` also run in parallel with sandbox hooks. `host.onWorktreeReady` hooks run sequentially in declared order.
1103
+ - If any hook exits non-zero, setup fails fast.
1104
+
916
1105
  ## Development
917
1106
 
918
1107
  ```bash
@@ -8,16 +8,23 @@ export type ParsedStreamEvent = {
8
8
  type: "tool_call";
9
9
  name: string;
10
10
  args: string;
11
+ } | {
12
+ type: "session_id";
13
+ sessionId: string;
11
14
  };
12
15
  /** Options passed to buildPrintCommand and buildInteractiveArgs. */
13
16
  export interface AgentCommandOptions {
14
17
  readonly prompt: string;
15
18
  readonly dangerouslySkipPermissions: boolean;
19
+ /** When set, the agent should resume the given session ID instead of starting fresh. */
20
+ readonly resumeSession?: string;
16
21
  }
17
22
  export interface AgentProvider {
18
23
  readonly name: string;
19
24
  /** Environment variables injected by this agent provider. Merged at launch time with env resolver and sandbox provider env. */
20
25
  readonly env: Record<string, string>;
26
+ /** When true, session capture is enabled for this provider. Default: true for Claude Code, false for others. */
27
+ readonly captureSessions: boolean;
21
28
  buildPrintCommand(options: AgentCommandOptions): string;
22
29
  buildInteractiveArgs?(options: AgentCommandOptions): string[];
23
30
  parseStreamLine(line: string): ParsedStreamEvent[];
@@ -46,6 +53,8 @@ export interface ClaudeCodeOptions {
46
53
  readonly effort?: "low" | "medium" | "high" | "max";
47
54
  /** Environment variables injected by this agent provider. */
48
55
  readonly env?: Record<string, string>;
56
+ /** When false, session capture is disabled. Default: true. */
57
+ readonly captureSessions?: boolean;
49
58
  }
50
59
  export declare const claudeCode: (model: string, options?: ClaudeCodeOptions | undefined) => AgentProvider;
51
60
  //# sourceMappingURL=AgentProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AgentProvider.d.ts","sourceRoot":"","sources":["../src/AgentProvider.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AA6DtD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC;CAC9C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+HAA+H;IAC/H,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACxD,oBAAoB,CAAC,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAAC;IAC9D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAAC;CACpD;AAED,eAAO,MAAM,aAAa,oBAAoB,CAAC;AA2D/C,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,EAAE,mEAiBb,CAAC;AAwCH,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,KAAK,sEAuBhB,CAAC;AAMH,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,QAAQ,yEAoBnB,CAAC;AAMH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;IACpD,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,UAAU,2EAiCrB,CAAC"}
1
+ {"version":3,"file":"AgentProvider.d.ts","sourceRoot":"","sources":["../src/AgentProvider.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAoE9C,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC;IAC7C,wFAAwF;IACxF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+HAA+H;IAC/H,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,gHAAgH;IAChH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,iBAAiB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACxD,oBAAoB,CAAC,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAAC;IAC9D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAAC;CACpD;AAED,eAAO,MAAM,aAAa,oBAAoB,CAAC;AA2D/C,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,EAAE,mEAkBb,CAAC;AAwCH,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IACtD,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,KAAK,sEAwBhB,CAAC;AAMH,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,QAAQ,yEAqBnB,CAAC;AAMH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;IACpD,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,eAAO,MAAM,UAAU,2EAsCrB,CAAC"}
@@ -46,6 +46,11 @@ const parseStreamJsonLine = (line) => {
46
46
  if (obj.type === "result" && typeof obj.result === "string") {
47
47
  return [{ type: "result", result: obj.result }];
48
48
  }
49
+ if (obj.type === "system" &&
50
+ obj.subtype === "init" &&
51
+ typeof obj.session_id === "string") {
52
+ return [{ type: "session_id", sessionId: obj.session_id }];
53
+ }
49
54
  }
50
55
  catch {
51
56
  // Not valid JSON — skip
@@ -111,6 +116,7 @@ const parsePiStreamLine = (line) => {
111
116
  export const pi = (model, options) => ({
112
117
  name: "pi",
113
118
  env: options?.env ?? {},
119
+ captureSessions: false,
114
120
  buildPrintCommand({ prompt }) {
115
121
  return `pi -p --mode json --no-session --model ${shellEscape(model)} ${shellEscape(prompt)}`;
116
122
  },
@@ -158,6 +164,7 @@ const parseCodexStreamLine = (line) => {
158
164
  export const codex = (model, options) => ({
159
165
  name: "codex",
160
166
  env: options?.env ?? {},
167
+ captureSessions: false,
161
168
  buildPrintCommand({ prompt }) {
162
169
  const effortFlag = options?.effort
163
170
  ? ` -c ${shellEscape(`model_reasoning_effort="${options.effort}"`)}`
@@ -177,6 +184,7 @@ export const codex = (model, options) => ({
177
184
  export const opencode = (model, options) => ({
178
185
  name: "opencode",
179
186
  env: options?.env ?? {},
187
+ captureSessions: false,
180
188
  buildPrintCommand({ prompt }) {
181
189
  return `opencode run --model ${shellEscape(model)} ${shellEscape(prompt)}`;
182
190
  },
@@ -193,12 +201,16 @@ export const opencode = (model, options) => ({
193
201
  export const claudeCode = (model, options) => ({
194
202
  name: "claude-code",
195
203
  env: options?.env ?? {},
196
- buildPrintCommand({ prompt, dangerouslySkipPermissions, }) {
204
+ captureSessions: options?.captureSessions ?? true,
205
+ buildPrintCommand({ prompt, dangerouslySkipPermissions, resumeSession, }) {
197
206
  const skipPerms = dangerouslySkipPermissions
198
207
  ? " --dangerously-skip-permissions"
199
208
  : "";
200
209
  const effortFlag = options?.effort ? ` --effort ${options.effort}` : "";
201
- return `claude --print --verbose${skipPerms} --output-format stream-json --model ${shellEscape(model)}${effortFlag} -p ${shellEscape(prompt)}`;
210
+ const resumeFlag = resumeSession
211
+ ? ` --resume ${shellEscape(resumeSession)}`
212
+ : "";
213
+ return `claude --print --verbose${skipPerms} --output-format stream-json --model ${shellEscape(model)}${effortFlag}${resumeFlag} -p ${shellEscape(prompt)}`;
202
214
  },
203
215
  buildInteractiveArgs({ prompt, dangerouslySkipPermissions, }) {
204
216
  const args = ["claude"];