{"_id":"@ecology91/sandcastle","_rev":"9-9b45656495aa7a43eef55c225eeeee9f","name":"@ecology91/sandcastle","dist-tags":{"latest":"0.5.17"},"versions":{"0.5.9":{"name":"@ecology91/sandcastle","version":"0.5.9","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.9","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"c300fbac50e6898175393f2fd443a25f6952fe00","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.9.tgz","fileCount":222,"integrity":"sha512-thJTzQrfjDKYMq/IGUtyIWY1WdfecUlq0ZS5g0HQv02gQreRe2eLeV7hNq29GzZYcwwJskEEJyQG3AJ94w2aNA==","signatures":[{"sig":"MEUCIFS1//vOFtidHCmzyWSokg19Qf2Ln1EW+4AVIyPal7BEAiEAwuVdENpc/6TUI5gHgmzYUvPJdMmyZM+PnkoV3OZDOxM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":810979},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"af7d5c8d49fcdb8ccdc5e1555581399a4c836edd","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.11.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.9_1777977609557_0.6737329274089234","host":"s3://npm-registry-packages-npm-production"}},"0.5.10":{"name":"@ecology91/sandcastle","version":"0.5.10","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.10","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"762192addc771b885e6242fa027864ea2f8f07a0","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.10.tgz","fileCount":222,"integrity":"sha512-+s1sXi317RTkXsh5JG/KzdIJ8hQaNbltWD7PZcyEjBdsnLFis2jolZ1VNq4WwW+5RGxKIGja2Q+5zF0x6kCJGg==","signatures":[{"sig":"MEUCIG4wCrTQ+x6yB0CQPuE4YN1OV8AhDEB86x+mDCn0AAzcAiEAhBDHOrIMpfD/BnRtc7w9uY1UnbS8I78xyTRgylBezj8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":814908},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"4d1b97be452c59b179942e18cab644c326e6a159","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.11.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.10_1778013532790_0.8708240322974379","host":"s3://npm-registry-packages-npm-production"}},"0.5.11":{"name":"@ecology91/sandcastle","version":"0.5.11","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.11","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"6491a9dcc152b10ba99485f8b947b00afb8c35ad","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.11.tgz","fileCount":222,"integrity":"sha512-28wMCs0QYw9saz5b38vBumuJLsQ2nRmKWHDov9SOuIghaJKKYNATVm2RrtddULFmxkk+Jz2Rb3NUHMiMOTcJkg==","signatures":[{"sig":"MEQCIGhv9fIspjw1ikmxbXkx9pQ7pRmHrKNq5e3wCECm2EifAiB+6KVHKBXa75LRZ+0nUyMiATwZPf32XYYzdUb1BtLbbA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":831624},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.11.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.11_1778202082698_0.898557482338376","host":"s3://npm-registry-packages-npm-production"}},"0.5.12":{"name":"@ecology91/sandcastle","version":"0.5.12","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.12","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"45ce7413cf9c2d740fa89b230ad71d384d49fdf3","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.12.tgz","fileCount":222,"integrity":"sha512-oX/hSBX9ROgSp4hRUQjuOdOGWLBITwYPVKuiPP8WdfHTb0Nxa8Uqwy1sNKfBRdpfivtBCNgertiq+LZ8a4A6kg==","signatures":[{"sig":"MEQCIHSB1ugINHJFEtGzJsIrM1nSmbF3Oyo/CF9ITM3dW0J2AiBvKRCaTkBjsDim3PRnn+w5IUOwT8VWyn2VQdAHLMEANg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":831760},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"2eaf4266d4871bec8ba8f2d404876fdf71cd4753","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.11.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.12_1778230089819_0.5633966124772591","host":"s3://npm-registry-packages-npm-production"}},"0.5.13":{"name":"@ecology91/sandcastle","version":"0.5.13","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.13","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"19cf5bf60f8c764a3e28de9fb8ed71ad3c598cae","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.13.tgz","fileCount":222,"integrity":"sha512-BM/nUO8sry0vyqIm7RQp786rkApj732rBW45XQACvpxdP055SAyC3Zfg/gK9J+5gvrWlebsSozeoY8XwCpAB2w==","signatures":[{"sig":"MEUCIB4cdLa89wSU8hgdNnWq8wVMj6iblMiHu8+DWtvnxc5UAiEAv2Pj9zT6oTxcA2sqKmoWpYPHxKFN1j681GXnA+RNUZY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":839592},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"96ebb8edb2c67c9586b1dfdc220a45da07e269e2","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.14.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.13_1778383632221_0.8785618753191484","host":"s3://npm-registry-packages-npm-production"}},"0.5.14":{"name":"@ecology91/sandcastle","version":"0.5.14","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.14","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"c4fddef8ff4b66ed18c63d5f44840efc40f119bb","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.14.tgz","fileCount":222,"integrity":"sha512-/tfzKm/tXTMFd/Am6qwPakvC0p/p9lbg8uP3MrcN9H1moj3pxwP3QGFsPQG5FdUwFpLXDt+s3xeCTPm3GfNO0A==","signatures":[{"sig":"MEYCIQDXgNvwsC39XF0oydRfBlNJIt38ca4Dhqd/0oxdXbyH5QIhAKaVyVubTu9HPSN5+b4i3FfOv4l/T+W2yMJNFGWtK6kP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":861343},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"1f5e8509522e76071c0326faf662f2db19b45e6f","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.14.0","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.14_1778473056469_0.2198469882895242","host":"s3://npm-registry-packages-npm-production"}},"0.5.15":{"name":"@ecology91/sandcastle","version":"0.5.15","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.15","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"ef8788d6a05e35e20284e78141a5ecffb28ded87","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.15.tgz","fileCount":222,"integrity":"sha512-urU35jHzCHeVIiIEGt80atBxa5Fn/o3rAf1uaNGnaldPO415WqO9+oHyD1VOiKe6UaGjMAoLR79cs/c064NJ8Q==","signatures":[{"sig":"MEQCIEKT8X2hOQ//EGkMlryAtRgOJV4ZzHJDnwHeuAiSSpZ0AiBYPmsxRBWE9bx9rtJPleFIuXXD6qRuJreoWhW6tmffMg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":862879},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"7ed3580e6a341fe3da3d2d015bdf2638c0515fc1","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.14.1","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.14.1","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.15_1778738616999_0.07820283612361156","host":"s3://npm-registry-packages-npm-production"}},"0.5.16":{"name":"@ecology91/sandcastle","version":"0.5.16","keywords":["cli","sandbox","docker","ai","agent"],"license":"MIT","_id":"@ecology91/sandcastle@0.5.16","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"homepage":"https://github.com/ecology9191/sandcastle#readme","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"bin":{"sandcastle":"dist/main.js"},"dist":{"shasum":"b2465a3f0100094e544c2f006afb547574bed277","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.16.tgz","fileCount":222,"integrity":"sha512-ZHY6153Rjnn1AbZahpbEihK6GdD4MhggE+OKN+A4Yqm3TUsHSDKQS6rT1A48GYWlWU7QXQ43jp0Wcd93yziWgQ==","signatures":[{"sig":"MEUCIQDd5IIbxifVKhdLNuoh84EVVPOb+v5yW8grhot3n/gg0gIgfMMC2fI6L2mZmjLOKfRs7iHW42D2GHGuuSw+gr34KHQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":877387},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./sandboxes/docker":{"types":"./dist/sandboxes/docker.d.ts","import":"./dist/sandboxes/docker.js"},"./sandboxes/podman":{"types":"./dist/sandboxes/podman.d.ts","import":"./dist/sandboxes/podman.js"},"./sandboxes/vercel":{"types":"./dist/sandboxes/vercel.d.ts","import":"./dist/sandboxes/vercel.js"},"./sandboxes/daytona":{"types":"./dist/sandboxes/daytona.d.ts","import":"./dist/sandboxes/daytona.js"},"./sandboxes/no-sandbox":{"types":"./dist/sandboxes/no-sandbox.d.ts","import":"./dist/sandboxes/no-sandbox.js"}},"gitHead":"94c0af52450a1402868a435d3164dbe32b375fe2","scripts":{"test":"vitest run","build":"tsgo --project tsconfig.build.json","format":"prettier --write .","prepare":"husky","release":"changeset publish","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","typecheck":"tsgo --noEmit","sandcastle":"npm run build && tsx .sandcastle/run.ts","test:watch":"vitest","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","format:check":"prettier --check .","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"repository":{"url":"git+https://github.com/ecology9191/sandcastle.git","type":"git"},"_npmVersion":"11.14.1","description":"CLI for orchestrating AI agents in isolated sandbox environments","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"_nodeVersion":"24.15.0","dependencies":{"effect":"^3.20.0","@effect/cli":"^0.74.0","@clack/prompts":"^1.1.0","@effect/printer":"^0.48.0","@effect/platform":"^0.95.0","@effect/printer-ansi":"^0.48.0","@effect/platform-node":"^0.105.0"},"_hasShrinkwrap":false,"packageManager":"npm@10.9.2","devDependencies":{"tsx":"^4.21.0","husky":"^9.1.7","vitest":"^3.2.0","prettier":"^3.5.3","@types/node":"^25.5.0","lint-staged":"^15.5.1","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@changesets/cli":"^2.30.0","@typescript/native-preview":"^7.0.0-dev.20260317.1"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@daytona/sdk":{"optional":true},"@vercel/sandbox":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/sandcastle_0.5.16_1779319232055_0.012887993783788243","host":"s3://npm-registry-packages-npm-production"}},"0.5.17":{"name":"@ecology91/sandcastle","version":"0.5.17","description":"CLI for orchestrating AI agents in isolated sandbox environments","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"},"./sandboxes/docker":{"import":"./dist/sandboxes/docker.js","types":"./dist/sandboxes/docker.d.ts"},"./sandboxes/vercel":{"import":"./dist/sandboxes/vercel.js","types":"./dist/sandboxes/vercel.d.ts"},"./sandboxes/podman":{"import":"./dist/sandboxes/podman.js","types":"./dist/sandboxes/podman.d.ts"},"./sandboxes/daytona":{"import":"./dist/sandboxes/daytona.js","types":"./dist/sandboxes/daytona.d.ts"},"./sandboxes/no-sandbox":{"import":"./dist/sandboxes/no-sandbox.js","types":"./dist/sandboxes/no-sandbox.d.ts"}},"bin":{"sandcastle":"dist/main.js"},"scripts":{"build":"tsgo --project tsconfig.build.json","postbuild":"rm -rf dist/templates && cp -r src/templates dist/templates","test":"vitest run","test:watch":"vitest","typecheck":"tsgo --noEmit","format":"prettier --write .","format:check":"prettier --check .","prepare":"husky","release":"changeset publish","sandcastle":"npm run build && tsx .sandcastle/run.ts","test-podman":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-podman.ts","test-vercel":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-vercel.ts","test-interactive":"npm run build && tsx --env-file=.sandcastle/.env .sandcastle/test-interactive.ts"},"keywords":["cli","sandbox","docker","ai","agent"],"packageManager":"npm@10.9.2","repository":{"type":"git","url":"git+https://github.com/ecology9191/sandcastle.git"},"license":"MIT","devDependencies":{"@changesets/cli":"^2.30.0","@daytona/sdk":"^0.171.0","@effect/vitest":"^0.28.0","@types/node":"^25.5.0","@typescript/native-preview":"^7.0.0-dev.20260317.1","husky":"^9.1.7","lint-staged":"^15.5.1","prettier":"^3.5.3","tsx":"^4.21.0","vitest":"^3.2.0"},"dependencies":{"@clack/prompts":"^1.1.0","@effect/cli":"^0.74.0","@effect/platform":"^0.95.0","@effect/platform-node":"^0.105.0","@effect/printer":"^0.48.0","@effect/printer-ansi":"^0.48.0","effect":"^3.20.0"},"peerDependencies":{"@daytona/sdk":"^0.164.0","@vercel/sandbox":">=1.0.0"},"peerDependenciesMeta":{"@vercel/sandbox":{"optional":true},"@daytona/sdk":{"optional":true}},"lint-staged":{"*.{ts,tsx,js,jsx,json,md}":"prettier --write"},"gitHead":"924afbe739e484f0ee5d175aa57c28c8d8e65d05","_id":"@ecology91/sandcastle@0.5.17","bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"homepage":"https://github.com/ecology9191/sandcastle#readme","_nodeVersion":"24.15.0","_npmVersion":"11.14.1","dist":{"integrity":"sha512-LxexlO6ZR3e/6BrZFC6Oqz4ODdtFRGFftyvdtgMlkaww40AWBkEsV5f9Blppjdb6m8MEeq2TPvyqiqUWKlwjMA==","shasum":"db14efe2ca5665651dabdb524243434c28e76b65","tarball":"https://registry.npmjs.org/@ecology91/sandcastle/-/sandcastle-0.5.17.tgz","fileCount":226,"unpackedSize":893675,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDkbKvwaH4z3h6ogz8ecDaXXUUh+uFNY/INJ8jRrZL/fgIgQxU9sr8j+GVKesgrOR9EbeIKFHI5kJRph8AD3Y6F6eU="}]},"_npmUser":{"name":"ecology91","email":"ecology91@proton.me"},"directories":{},"maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sandcastle_0.5.17_1779337597236_0.5787729071258945"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-05T10:40:09.407Z","modified":"2026-05-21T04:26:37.588Z","0.5.9":"2026-05-05T10:40:09.726Z","0.5.10":"2026-05-05T20:38:52.991Z","0.5.11":"2026-05-08T01:01:22.859Z","0.5.12":"2026-05-08T08:48:09.990Z","0.5.13":"2026-05-10T03:27:12.384Z","0.5.14":"2026-05-11T04:17:36.687Z","0.5.15":"2026-05-14T06:03:37.217Z","0.5.16":"2026-05-20T23:20:32.290Z","0.5.17":"2026-05-21T04:26:37.484Z"},"bugs":{"url":"https://github.com/ecology9191/sandcastle/issues"},"license":"MIT","homepage":"https://github.com/ecology9191/sandcastle#readme","keywords":["cli","sandbox","docker","ai","agent"],"repository":{"type":"git","url":"git+https://github.com/ecology9191/sandcastle.git"},"description":"CLI for orchestrating AI agents in isolated sandbox environments","maintainers":[{"name":"ecology91","email":"ecology91@proton.me"}],"readme":"<div align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://res.cloudinary.com/total-typescript/image/upload/v1775033787/readme-sandcastle-ondark_2x.png\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"https://res.cloudinary.com/total-typescript/image/upload/v1775033787/readme-sandcastle-onlight_2x.png\">\n    <img alt=\"Sandcastle\" src=\"https://res.cloudinary.com/total-typescript/image/upload/v1775033787/readme-sandcastle-onlight_2x.png\" height=\"200\" style=\"margin-bottom: 20px;\">\n  </picture>\n</div>\n\n## What Is Sandcastle?\n\nA TypeScript library for orchestrating AI coding agents in isolated sandboxes:\n\n1. You invoke agents with a single `sandcastle.run()`.\n2. Sandcastle handles sandboxing the agent with a configurable branch strategy.\n3. The commits made on the branches get merged back.\n\nSandcastle is provider-agnostic — it ships with built-in providers for Docker, Podman, and Vercel, and you can create your own. Great for parallelizing multiple AFK agents, creating review pipelines, or even just orchestrating your own agents.\n\n## Prerequisites\n\n- [Git](https://git-scm.com/)\n- A sandbox provider — Sandcastle needs an isolated environment to run agents in. Built-in options:\n  - [Docker Desktop](https://www.docker.com/) — most common for local development\n  - [Podman](https://podman.io/) — rootless alternative to Docker\n  - [Vercel](https://vercel.com/) — cloud-based Firecracker microVMs via `@vercel/sandbox`\n  - Or [create your own](#custom-sandbox-providers) using `createBindMountSandboxProvider` or `createIsolatedSandboxProvider`\n\n## Quick start\n\n1. Install the package:\n\n```bash\nnpm install --save-dev @ecology91/sandcastle\n```\n\n2. Run `sandcastle init`. This scaffolds a `.sandcastle` directory with all the files needed.\n\n```bash\nnpx sandcastle init\n```\n\n3. Edit `.sandcastle/.env` and fill in the values from `.sandcastle/.env.example`. The generated `main.ts`/`main.mts` also reads `SANDCASTLE_CODING_HARNESS` and `SANDCASTLE_MODEL` from this file so you can switch harnesses or models without editing the orchestration script. If you want to use your Claude subscription instead of an API key, see [#191](https://github.com/mattpocock/sandcastle/issues/191).\n\n```bash\ncp .sandcastle/.env.example .sandcastle/.env\n```\n\n4. Run the `.sandcastle/main.ts` (or `main.mts`) file with `npx tsx`\n\n```bash\nnpx tsx .sandcastle/main.ts\n```\n\n```typescript\n// 3. Run the agent via the JS API\nimport { run, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(), // or podman(), vercel(), or your own provider\n  promptFile: \".sandcastle/prompt.md\",\n});\n```\n\n## Sandbox Providers\n\nSandcastle 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:\n\n| Provider   | Import path                                  | Type       | Accepted by                                   |\n| ---------- | -------------------------------------------- | ---------- | --------------------------------------------- |\n| Docker     | `@ecology91/sandcastle/sandboxes/docker`     | Bind-mount | `run()`, `createSandbox()`, `interactive()`   |\n| Podman     | `@ecology91/sandcastle/sandboxes/podman`     | Bind-mount | `run()`, `createSandbox()`, `interactive()`   |\n| Vercel     | `@ecology91/sandcastle/sandboxes/vercel`     | Isolated   | `run()`, `createSandbox()`, `interactive()`   |\n| No-sandbox | `@ecology91/sandcastle/sandboxes/no-sandbox` | None       | `interactive()`, `wt.interactive()` (default) |\n\nWorktree 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.\n\n```typescript\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\nimport { podman } from \"@ecology91/sandcastle/sandboxes/podman\";\nimport { vercel } from \"@ecology91/sandcastle/sandboxes/vercel\";\nimport { noSandbox } from \"@ecology91/sandcastle/sandboxes/no-sandbox\";\n\n// Docker, Podman, and Vercel are interchangeable in run() and createSandbox():\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(),\n  prompt: \"...\",\n});\n\n// No-sandbox runs the agent directly on the host — interactive() only:\nawait interactive({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: noSandbox(),\n  prompt: \"...\", // optional — omit to launch the TUI with no initial prompt\n  cwd: \"/path/to/other-repo\", // optional — defaults to process.cwd()\n});\n```\n\nYou can also [create your own provider](#custom-sandbox-providers) using `createBindMountSandboxProvider` or `createIsolatedSandboxProvider`.\n\n## API\n\nSandcastle exports a programmatic `run()` function for use in scripts, CI pipelines, or custom tooling. The examples below use `docker()`, but any `SandboxProvider` works in its place.\n\n```typescript\nimport { run, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nconst result = await run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(),\n  promptFile: \".sandcastle/prompt.md\",\n});\n\nconsole.log(result.iterations.length); // number of iterations executed\nconsole.log(result.iterations); // per-iteration results with optional sessionId\nconsole.log(result.commits); // array of { sha } for commits created\nconsole.log(result.branch); // target branch name\n```\n\n### All options\n\n```typescript\nimport { run, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nconst result = await run({\n  // Agent provider — required. Pass a model string to claudeCode().\n  // Optional second arg for provider-specific options like effort level.\n  agent: claudeCode(\"claude-opus-4-6\", { effort: \"high\" }),\n\n  // Sandbox provider — required. Any SandboxProvider works (docker, podman, vercel, or custom).\n  // Provider-specific config (like imageName, mounts) lives inside the provider factory call.\n  sandbox: docker({\n    imageName: \"sandcastle:local\",\n    // Optional: mount host directories into the sandbox (e.g. package manager caches)\n    // hostPath supports absolute, tilde-expanded (~), and relative paths (resolved from cwd).\n    // sandboxPath supports absolute and relative paths (resolved from the sandbox repo directory).\n    mounts: [\n      { hostPath: \"~/.npm\", sandboxPath: \"/home/agent/.npm\", readonly: true },\n      { hostPath: \"data\", sandboxPath: \"data\" }, // mounts <cwd>/data → <sandbox-repo>/data\n    ],\n    // Optional: provider-level env vars merged at launch time\n    env: { DOCKER_SPECIFIC: \"value\" },\n    // Optional: attach container to Docker network(s) — string or string[]\n    network: \"my-network\",\n  }),\n\n  // Host repo directory — replaces process.cwd() as the anchor for\n  // .sandcastle/ artifacts (worktrees, logs, env, patches) and git operations.\n  // Relative paths resolve against process.cwd(). Defaults to process.cwd().\n  cwd: \"../other-repo\",\n\n  // Branch strategy — controls how the agent's changes relate to branches.\n  // Defaults to { type: \"head\" } for bind-mount and { type: \"merge-to-head\" } for isolated providers.\n  branchStrategy: { type: \"branch\", branch: \"agent/fix-42\" },\n\n  // Prompt source — provide one of these, not both.\n  // Note: promptFile resolves against process.cwd(), NOT cwd.\n  promptFile: \".sandcastle/prompt.md\", // path to a prompt file\n  // prompt: \"Fix issue #42 in this repo\", // OR an inline prompt string\n\n  // Values substituted for {{KEY}} placeholders in the prompt.\n  promptArgs: {\n    ISSUE_NUMBER: \"42\",\n  },\n\n  // Maximum number of agent iterations to run before stopping. Default: 1\n  maxIterations: 5,\n\n  // Display name for this run, shown as a prefix in log output.\n  name: \"fix-issue-42\",\n\n  // Lifecycle hooks grouped by where they run: host or sandbox.\n  hooks: {\n    host: {\n      onWorktreeReady: [{ command: \"cp .env.example .env\" }],\n      onSandboxReady: [{ command: \"echo setup done\" }],\n    },\n    sandbox: {\n      onSandboxReady: [{ command: \"npm install\" }],\n    },\n  },\n\n  // Host-relative file paths to copy into the sandbox before the container starts.\n  // Not supported with branchStrategy: { type: \"head\" }.\n  copyToWorktree: [\".env\"],\n\n  // Override default timeouts for built-in lifecycle steps.\n  // Unset keys keep their defaults.\n  timeouts: {\n    copyToWorktreeMs: 120_000, // default: 60_000\n  },\n\n  // How to record progress. Default: write to a file under .sandcastle/logs/\n  logging: {\n    type: \"file\",\n    path: \".sandcastle/logs/my-run.log\",\n    // Optional: forward the agent's output stream to your own observability system.\n    // Fires for each text chunk and tool call the agent produces. Errors thrown\n    // by the callback are swallowed so a broken forwarder cannot kill the run.\n    onAgentStreamEvent: (event) => {\n      // event is { type: \"text\" | \"toolCall\", iteration, timestamp, ... }\n      myLogger.info(event);\n    },\n  },\n  // logging: { type: \"stdout\" }, // OR render an interactive UI in the terminal\n\n  // String (or array of strings) the agent emits to end the iteration loop early.\n  // Default: \"<promise>COMPLETE</promise>\"\n  completionSignal: \"<promise>COMPLETE</promise>\",\n\n  // Idle timeout in seconds — resets whenever the agent produces output. Default: 600 (10 minutes)\n  idleTimeoutSeconds: 600,\n});\n\nconsole.log(result.iterations.length); // number of iterations executed\nconsole.log(result.completionSignal); // matched signal string, or undefined if none fired\nconsole.log(result.commits); // array of { sha } for commits created\nconsole.log(result.branch); // target branch name\n```\n\n### Terminal Output Mode\n\nBy default, file logging writes durable run logs under `.sandcastle/logs/` and prints a `tail -f` hint. Set `SANDCASTLE_TERMINAL_OUTPUT=verbose` in `.sandcastle/.env` to keep those file logs and also mirror automated `run()` and reusable `sandbox.run()` progress to the terminal with prefixed lifecycle, hook, sync, merge, commit collection, and parsed agent stream output. Use `off` or omit the key for log-file-only behavior.\n\nVerbose terminal output is additive to log-to-file mode. It does not change `logging: { type: \"stdout\" }`, and `interactive()`, `sandbox.interactive()`, and `wt.interactive()` are excluded from the verbose renderer.\n\n### `createSandbox()` — reusable sandbox\n\nUse `createSandbox()` when you need to run multiple agents (or multiple rounds of the same agent) inside a single sandbox. It creates the sandbox once, and you call `sandbox.run()` as many times as you need. This avoids repeated container startup costs and keeps all runs on the same branch.\n\nUse `run()` instead when you only need a single one-shot invocation — it handles sandbox lifecycle automatically.\n\n#### Basic single-run usage\n\n```typescript\nimport { createSandbox, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nawait using sandbox = await createSandbox({\n  branch: \"agent/fix-42\",\n  sandbox: docker(),\n});\n\nconst result = await sandbox.run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  prompt: \"Fix issue #42 in this repo.\",\n});\n\nconsole.log(result.commits); // [{ sha: \"abc123\" }]\n```\n\n#### Multi-run implement-then-review\n\n```typescript\nimport { createSandbox, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nawait using sandbox = await createSandbox({\n  branch: \"agent/fix-42\",\n  sandbox: docker(),\n  hooks: { sandbox: { onSandboxReady: [{ command: \"npm install\" }] } },\n});\n\n// Step 1: implement\nconst implResult = await sandbox.run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  promptFile: \".sandcastle/implement.md\",\n  maxIterations: 5,\n});\n\n// Step 2: review on the same branch, same container\nconst reviewResult = await sandbox.run({\n  agent: claudeCode(\"claude-sonnet-4-6\"),\n  prompt: \"Review the changes and fix any issues.\",\n});\n```\n\nCommits from all `run()` calls accumulate on the same branch. The sandbox container stays alive between runs, so installed dependencies and build artifacts persist.\n\n#### Automatic cleanup with `await using`\n\n`await using` calls `sandbox.close()` automatically when the block exits. If the sandbox has uncommitted changes, the worktree is preserved on disk; if clean, both container and worktree are removed.\n\n#### Manual `close()` with `CloseResult`\n\n```typescript\nconst sandbox = await createSandbox({\n  branch: \"agent/fix-42\",\n  sandbox: docker(),\n});\n// ... run agents ...\nconst closeResult = await sandbox.close();\nif (closeResult.preservedWorktreePath) {\n  console.log(`Worktree preserved at ${closeResult.preservedWorktreePath}`);\n}\n```\n\n#### `CreateSandboxOptions`\n\n| Option                    | Type            | Default         | Description                                                          |\n| ------------------------- | --------------- | --------------- | -------------------------------------------------------------------- |\n| `branch`                  | string          | —               | **Required.** Explicit branch for the sandbox                        |\n| `sandbox`                 | SandboxProvider | —               | **Required.** Sandbox provider (e.g. `docker()`, `podman()`)         |\n| `cwd`                     | string          | `process.cwd()` | Host repo directory — relative paths resolve against `process.cwd()` |\n| `hooks`                   | SandboxHooks    | —               | Lifecycle hooks (`host.*`, `sandbox.*`) — run once at creation time  |\n| `copyToWorktree`          | string[]        | —               | Host-relative file paths to copy into the sandbox at creation time   |\n| `timeouts`                | Timeouts        | —               | Override default timeouts (e.g. `{ copyToWorktreeMs: 120_000 }`)     |\n| `gitCredentialGuardrails` | boolean         | `true`          | Strip GitHub and git credential env from reusable sandbox startup    |\n\n#### `Sandbox`\n\n| Property / Method       | Type                                                               | Description                                  |\n| ----------------------- | ------------------------------------------------------------------ | -------------------------------------------- |\n| `branch`                | string                                                             | The branch the sandbox is on                 |\n| `worktreePath`          | string                                                             | Host path to the worktree                    |\n| `run(options)`          | `(SandboxRunOptions) => Promise<SandboxRunResult>`                 | Invoke an agent inside the existing sandbox  |\n| `interactive(options)`  | `(SandboxInteractiveOptions) => Promise<SandboxInteractiveResult>` | Launch an interactive session in the sandbox |\n| `close()`               | `() => Promise<CloseResult>`                                       | Tear down the container and sandbox          |\n| `[Symbol.asyncDispose]` | `() => Promise<void>`                                              | Auto teardown via `await using`              |\n\n#### `SandboxRunOptions`\n\n| Option               | Type               | Default                       | Description                                                         |\n| -------------------- | ------------------ | ----------------------------- | ------------------------------------------------------------------- |\n| `agent`              | AgentProvider      | —                             | **Required.** Agent provider (e.g. `claudeCode(\"claude-opus-4-6\")`) |\n| `prompt`             | string             | —                             | Inline prompt (mutually exclusive with `promptFile`)                |\n| `promptFile`         | string             | —                             | Path to prompt file (mutually exclusive with `prompt`)              |\n| `promptArgs`         | PromptArgs         | —                             | Key-value map for `{{KEY}}` placeholder substitution                |\n| `maxIterations`      | number             | `1`                           | Maximum iterations to run                                           |\n| `completionSignal`   | string \\| string[] | `<promise>COMPLETE</promise>` | String(s) the agent emits to stop the iteration loop early          |\n| `idleTimeoutSeconds` | number             | `600`                         | Idle timeout in seconds — resets on each agent output event         |\n| `name`               | string             | —                             | Display name for the run                                            |\n| `logging`            | object             | file (auto-generated)         | `{ type: 'file', path }` or `{ type: 'stdout' }`                    |\n| `signal`             | AbortSignal        | —                             | Cancels the run when aborted; handle stays usable afterward         |\n\n#### `SandboxRunResult`\n\n| Field              | Type                | Description                                                        |\n| ------------------ | ------------------- | ------------------------------------------------------------------ |\n| `iterations`       | `IterationResult[]` | Per-iteration results (use `.length` for the count)                |\n| `completionSignal` | string?             | The matched completion signal string, or `undefined` if none fired |\n| `stdout`           | string              | Combined final assistant text from all iterations                  |\n| `commits`          | `{ sha }[]`         | Commits created during the run                                     |\n| `logFilePath`      | string?             | Path to the log file (only when logging to a file)                 |\n\n#### `CloseResult`\n\n| Field                   | Type    | Description                                                              |\n| ----------------------- | ------- | ------------------------------------------------------------------------ |\n| `preservedWorktreePath` | string? | Host path to the preserved worktree, set when it had uncommitted changes |\n\n### `createWorktree()` — independent worktree lifecycle\n\nUse `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.\n\nOnly `branch` and `merge-to-head` strategies are accepted; `head` is a compile-time type error since it means no worktree.\n\nPass `cwd` to target a repo other than `process.cwd()`. Relative paths resolve against `process.cwd()`; absolute paths pass through. A `CwdError` is thrown if the path does not exist or is not a directory.\n\n```typescript\nimport { createWorktree } from \"@ecology91/sandcastle\";\n\nawait using wt = await createWorktree({\n  branchStrategy: { type: \"branch\", branch: \"agent/fix-42\" },\n  copyToWorktree: [\"node_modules\"],\n  cwd: \"/path/to/other-repo\", // optional — defaults to process.cwd()\n});\n\nconsole.log(wt.worktreePath); // host path to the worktree\nconsole.log(wt.branch); // \"agent/fix-42\"\n\n// Run an interactive session in the worktree (defaults to noSandbox)\nawait wt.interactive({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  prompt: \"Explore the codebase and understand the bug.\",\n});\n\n// Run an AFK agent in the worktree (sandbox is required)\nconst result = await wt.run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker({ imageName: \"sandcastle:myrepo\" }),\n  prompt: \"Fix issue #42.\",\n  maxIterations: 3,\n});\nconsole.log(result.commits); // commits made during the run\n\n// Create a long-lived sandbox from the worktree\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\nawait using sandbox = await wt.createSandbox({\n  sandbox: docker(),\n  hooks: { sandbox: { onSandboxReady: [{ command: \"npm install\" }] } },\n});\n\n// sandbox.close() tears down the container only — the worktree stays\nawait sandbox.close();\n\n// wt.close() cleans up the worktree\n```\n\n`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.\n\n**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.\n\n#### `CreateWorktreeOptions`\n\n| Option           | Type                   | Default | Description                                                               |\n| ---------------- | ---------------------- | ------- | ------------------------------------------------------------------------- |\n| `branchStrategy` | WorktreeBranchStrategy | —       | **Required.** `{ type: \"branch\", branch }` or `{ type: \"merge-to-head\" }` |\n| `copyToWorktree` | string[]               | —       | Host-relative file paths to copy into the worktree at creation time       |\n| `timeouts`       | Timeouts               | —       | Override default timeouts (e.g. `{ copyToWorktreeMs: 120_000 }`)          |\n\n#### `Worktree`\n\n| Property / Method        | Type                                                                  | Description                                         |\n| ------------------------ | --------------------------------------------------------------------- | --------------------------------------------------- |\n| `branch`                 | string                                                                | The branch the worktree is on                       |\n| `worktreePath`           | string                                                                | Host path to the worktree                           |\n| `run(options)`           | `(options: WorktreeRunOptions) => Promise<WorktreeRunResult>`         | Run an AFK agent in the worktree (sandbox required) |\n| `interactive(options)`   | `(options: WorktreeInteractiveOptions) => Promise<InteractiveResult>` | Run an interactive agent session in the worktree    |\n| `createSandbox(options)` | `(options: WorktreeCreateSandboxOptions) => Promise<Sandbox>`         | Create a long-lived sandbox backed by this worktree |\n| `close()`                | `() => Promise<CloseResult>`                                          | Clean up the worktree (preserves if dirty)          |\n| `[Symbol.asyncDispose]`  | `() => Promise<void>`                                                 | Auto cleanup via `await using`                      |\n\n#### `WorktreeInteractiveOptions`\n\n| Option       | Type                   | Default       | Description                                                                                       |\n| ------------ | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------- |\n| `agent`      | AgentProvider          | —             | **Required.** Agent provider                                                                      |\n| `sandbox`    | AnySandboxProvider     | `noSandbox()` | Sandbox provider (defaults to no sandbox)                                                         |\n| `prompt`     | string                 | —             | Inline prompt (mutually exclusive with `promptFile`)                                              |\n| `promptFile` | string                 | —             | Path to prompt file                                                                               |\n| `name`       | string                 | —             | Optional session name                                                                             |\n| `hooks`      | SandboxHooks           | —             | Lifecycle hooks (`host.*`, `sandbox.*`)                                                           |\n| `promptArgs` | PromptArgs             | —             | Key-value map for `{{KEY}}` placeholder substitution                                              |\n| `env`        | Record<string, string> | —             | Environment variables to inject into the sandbox                                                  |\n| `signal`     | AbortSignal            | —             | Cancel the session when aborted. The worktree is preserved on disk. Rejects with `signal.reason`. |\n\n#### `WorktreeRunOptions`\n\n| Option               | Type                   | Default | Description                                                                                                                         |\n| -------------------- | ---------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `agent`              | AgentProvider          | —       | **Required.** Agent provider                                                                                                        |\n| `sandbox`            | SandboxProvider        | —       | **Required.** Sandbox provider (AFK agents must be sandboxed)                                                                       |\n| `prompt`             | string                 | —       | Inline prompt (mutually exclusive with `promptFile`)                                                                                |\n| `promptFile`         | string                 | —       | Path to prompt file                                                                                                                 |\n| `maxIterations`      | number                 | 1       | Maximum iterations to run                                                                                                           |\n| `completionSignal`   | string \\| string[]     | —       | Substring(s) to stop the iteration loop early                                                                                       |\n| `idleTimeoutSeconds` | number                 | 600     | Idle timeout in seconds                                                                                                             |\n| `name`               | string                 | —       | Optional run name                                                                                                                   |\n| `logging`            | LoggingOption          | file    | Logging mode                                                                                                                        |\n| `hooks`              | SandboxHooks           | —       | Lifecycle hooks (`host.*`, `sandbox.*`)                                                                                             |\n| `promptArgs`         | PromptArgs             | —       | Key-value map for `{{KEY}}` placeholder substitution                                                                                |\n| `env`                | Record<string, string> | —       | Environment variables to inject into the sandbox                                                                                    |\n| `resumeSession`      | string                 | —       | Resume a prior Claude Code session by ID. Incompatible with `maxIterations > 1`. Session file must exist on host.                   |\n| `signal`             | AbortSignal            | —       | Cancel the run when aborted. Kills the in-flight agent subprocess; the worktree is preserved on disk. Rejects with `signal.reason`. |\n\n#### `WorktreeRunResult`\n\n| Property           | Type                | Description                                            |\n| ------------------ | ------------------- | ------------------------------------------------------ |\n| `iterations`       | `IterationResult[]` | Per-iteration results (use `.length` for the count)    |\n| `completionSignal` | string              | The matched completion signal, or undefined            |\n| `stdout`           | string              | Combined final assistant text from all iterations      |\n| `commits`          | { sha: string }[]   | List of commits made by the agent during the run       |\n| `branch`           | string              | The branch name the agent worked on                    |\n| `logFilePath`      | string              | Path to the log file, if logging was drained to a file |\n\n#### `WorktreeCreateSandboxOptions`\n\n| Option                    | Type            | Default | Description                                                         |\n| ------------------------- | --------------- | ------- | ------------------------------------------------------------------- |\n| `sandbox`                 | SandboxProvider | —       | **Required.** Sandbox provider (e.g. `docker()`)                    |\n| `hooks`                   | SandboxHooks    | —       | Lifecycle hooks (`host.*`, `sandbox.*`)                             |\n| `copyToWorktree`          | string[]        | —       | Host-relative file paths to copy into the worktree at creation time |\n| `timeouts`                | Timeouts        | —       | Override default timeouts (e.g. `{ copyToWorktreeMs: 120_000 }`)    |\n| `gitCredentialGuardrails` | boolean         | `true`  | Strip GitHub and git credential env from reusable sandbox startup   |\n\n## How it works\n\nSandcastle uses a **branch strategy** configured on the sandbox provider to control how the agent's changes relate to branches. There are three strategies:\n\n- **Head** (`{ type: \"head\" }`) — The agent writes directly to the host working directory. No worktree, no branch indirection. This is the default for bind-mount providers like `docker()`.\n- **Merge-to-head** (`{ type: \"merge-to-head\" }`) — Sandcastle creates a temporary branch in a git worktree. The agent works on the temp branch, and changes are merged back to HEAD when done. The temp branch is cleaned up after merge.\n- **Branch** (`{ type: \"branch\", branch: \"foo\" }`) — Commits land on an explicitly named branch in a git worktree.\n\nFor bind-mount providers (like Docker), the worktree directory is bind-mounted into the container — the agent writes directly to the host filesystem through the mount, so no sync is needed.\n\nFrom your point of view, you just configure `branchStrategy: { type: 'branch', branch: 'foo' }` on `run()`, and get a commit on branch `foo` once it's complete. All 100% local.\n\n## Prompts\n\nSandcastle uses a flexible prompt system. You write the prompt, and the engine executes it — no opinions about workflow, task management, or context sources are imposed.\n\n### Prompt resolution\n\nYou must provide exactly one of:\n\n1. `prompt: \"inline string\"` — pass an inline prompt directly via `RunOptions`\n2. `promptFile: \"./path/to/prompt.md\"` — point to a specific file via `RunOptions`\n\n`prompt` and `promptFile` are mutually exclusive — providing both is an error. If neither is provided, `run()` throws an error asking you to supply one.\n\n**Inline prompts (`prompt: \"...\"`) are passed to the agent literally.** No `{{KEY}}` substitution, no `` !`command` `` expansion, no built-in `{{SOURCE_BRANCH}}` / `{{TARGET_BRANCH}}` injection. If you need values interpolated into an inline prompt, build the string in JavaScript (`` `Work on ${branch}…` ``). Passing `promptArgs` alongside an inline prompt is an error — switch to `promptFile` to use substitution.\n\nThe substitution and expansion features below apply **only** to prompts sourced from `promptFile`.\n\n> **Convention**: `sandcastle init` scaffolds `.sandcastle/prompt.md` and all templates explicitly reference it via `promptFile: \".sandcastle/prompt.md\"`. This is a convention, not an automatic fallback — Sandcastle does not read `.sandcastle/prompt.md` unless you pass it as `promptFile`.\n\n### Dynamic context with `` !`command` ``\n\nUse `` !`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.\n\nCommands run **inside the sandbox** after `sandbox.onSandboxReady` hooks complete, so they see the same repo state the agent sees (including installed dependencies).\n\n```markdown\n# Open issues\n\n!`gh issue list --state open --label Sandcastle --json number,title,body,comments,labels --limit 20`\n\n# Recent commits\n\n!`git log --oneline -10`\n```\n\nIf any command exits with a non-zero code, the run fails immediately with an error.\n\n### Prompt arguments with `{{KEY}}`\n\nUse `{{KEY}}` placeholders in your prompt to inject values from the `promptArgs` option. This is useful for reusing the same prompt file across multiple runs with different parameters.\n\n```typescript\nimport { run } from \"@ecology91/sandcastle\";\n\nawait run({\n  promptFile: \"./my-prompt.md\",\n  promptArgs: { ISSUE_NUMBER: 42, PRIORITY: \"high\" },\n});\n```\n\nIn the prompt file:\n\n```markdown\nWork on issue #{{ISSUE_NUMBER}} (priority: {{PRIORITY}}).\n```\n\nPrompt argument substitution runs on the host before shell expression expansion, so `{{KEY}}` placeholders inside `` !`command` `` expressions are replaced first:\n\n```markdown\n!`gh issue view {{ISSUE_NUMBER}} --json body -q .body`\n```\n\nA `{{KEY}}` placeholder with no matching prompt argument is an error. Unused prompt arguments produce a warning.\n\n`` !`command` `` expansion only runs on shell blocks written in the prompt file itself. Any `` !`…` `` pattern that appears inside an argument value is treated as inert text — it won't be executed against the host shell. This makes it safe to pass user-authored content (issue titles, PR descriptions, docs excerpts) through `promptArgs`.\n\n### Built-in prompt arguments\n\nSandcastle automatically injects two built-in prompt arguments into every prompt:\n\n| Placeholder         | Value                                                             |\n| ------------------- | ----------------------------------------------------------------- |\n| `{{SOURCE_BRANCH}}` | The branch the agent works on (determined by the branch strategy) |\n| `{{TARGET_BRANCH}}` | The host's active branch at `run()` time                          |\n\nUse them in your prompt without passing them via `promptArgs`:\n\n```markdown\nYou are working on {{SOURCE_BRANCH}}. When diffing, compare against {{TARGET_BRANCH}}.\n```\n\nPassing `SOURCE_BRANCH` or `TARGET_BRANCH` in `promptArgs` is an error — built-in prompt arguments cannot be overridden.\n\n### Early termination with `<promise>COMPLETE</promise>`\n\nWhen the agent outputs `<promise>COMPLETE</promise>`, the orchestrator stops the iteration loop early. This is a convention you document in your prompt for the agent to follow — the engine never injects it.\n\nThis is useful for task-based workflows where the agent should stop once it has finished, rather than running all remaining iterations.\n\nYou can override the default signal by passing `completionSignal` to `run()`. It accepts a single string or an array of strings:\n\n```ts\nawait run({\n  // ...\n  completionSignal: \"DONE\",\n});\n\n// Or pass multiple signals — the loop stops on the first match:\nawait run({\n  // ...\n  completionSignal: [\"TASK_COMPLETE\", \"TASK_ABORTED\"],\n});\n```\n\nTell 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`.\n\n### Templates\n\n`sandcastle init` prompts you to choose a sandbox provider (Docker or Podman), a backlog manager (GitHub Issues or Beads), and a template, which scaffolds a ready-to-use prompt and `main.mts` suited to a specific workflow. If your project's `package.json` has `\"type\": \"module\"`, the file will be named `main.ts` instead. Five templates are available:\n\n| Template                       | Description                                                               |\n| ------------------------------ | ------------------------------------------------------------------------- |\n| `blank`                        | Bare scaffold — write your own prompt and orchestration                   |\n| `simple-loop`                  | Picks backlog issues one by one and closes them                           |\n| `sequential-reviewer`          | Implements issues one by one, with a code review step after each          |\n| `parallel-planner`             | Plans parallelizable issues, executes on separate branches, then merges   |\n| `parallel-planner-with-review` | Plans parallelizable issues, executes with per-branch review, then merges |\n\nSelect a template during `sandcastle init` when prompted, or re-run init in a fresh repo to try a different one.\n\nThe issue-driving templates (`simple-loop`, `sequential-reviewer`, `parallel-planner`, and `parallel-planner-with-review`) stop before planning or running agent work when the selected backlog manager reports open human-gated issues. GitHub Issues uses the `ready-for-human` label. Beads uses `bd list --label ready-for-human --status open,deferred --json --limit 0`, so deferred HITL issues stop autonomous runs instead of being hidden by `bd ready`.\n\n#### Parallel planner safety behavior\n\nThe `parallel-planner` and `parallel-planner-with-review` templates load task context on the host before each implementer agent starts. The planner output stays small - each task includes only its ID, title, and branch - then the scaffold runs the selected backlog manager's view command and passes that deterministic output to the implementer as `{{TASK_CONTEXT}}`.\n\nIf the task context command fails or returns empty output, that task's implementer does not start. The surrounding `Promise.allSettled` loop logs the rejected task as a failed issue and continues with the other planned tasks.\n\nMerge eligibility is deliberately conservative. In `parallel-planner`, a branch reaches the merge prompt only when the implementer run fulfilled, emitted the configured completion signal, and produced commits. In `parallel-planner-with-review`, the implementer must fulfill, emit the completion signal, and produce commits; the reviewer must also fulfill and emit the completion signal. Reviewer commits are optional.\n\nSkipped branches are printed in the operator output instead of being silently ignored. Commits without a completion signal are reported as `Skipped incomplete branch ...`, completed runs with no commits are reported as skipped with no merge, missing reviewer completion is reported as `Skipped incomplete review ...`, and a cycle with no eligible branches prints `No merge-eligible branches. Nothing to merge.` The merge prompt receives only merge-eligible issue IDs for closure; the scaffold does not add automatic programmatic issue closure.\n\n## CLI commands\n\n### `sandcastle init`\n\nScaffolds or refreshes the `.sandcastle/` config directory and builds the container image. This is the first command you run in a new repo, and it is safe to re-run after updating Sandcastle. 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.\n\n| Option         | Required | Default                      | Description                                                          |\n| -------------- | -------- | ---------------------------- | -------------------------------------------------------------------- |\n| `--image-name` | No       | `sandcastle:<repo-dir-name>` | Docker image name                                                    |\n| `--agent`      | No       | Interactive prompt           | Agent to use (`claude-code`, `pi`, `codex`, `opencode`)              |\n| `--model`      | No       | Agent's default model        | Model to use (e.g. `claude-sonnet-4-6`). Defaults to agent's default |\n| `--template`   | No       | Interactive prompt           | Template to scaffold (e.g. `blank`, `simple-loop`)                   |\n\nCreates the following files:\n\n```\n.sandcastle/\n├── Dockerfile      # Sandbox environment (customize as needed)\n├── prompt.md       # Agent instructions\n├── .env.example    # Token placeholders\n└── .gitignore      # Ignores .env, logs/\n```\n\nWhen `.sandcastle/` already exists, init refreshes generated template files such as `main.ts`/`main.mts` and `.env.example`, creates missing scaffold files, and preserves existing sandbox provider files such as `Dockerfile`/`Containerfile`, `.gitignore`, and runtime artifacts such as logs and worktrees. Existing prompt markdown files are only replaced when you confirm the prompt overwrite question.\n\n### `sandcastle docker build-image`\n\nRebuilds the Docker image from an existing `.sandcastle/` directory. Use this after modifying the Dockerfile.\n\n| Option         | Required | Default                      | Description                                                                       |\n| -------------- | -------- | ---------------------------- | --------------------------------------------------------------------------------- |\n| `--image-name` | No       | `sandcastle:<repo-dir-name>` | Docker image name                                                                 |\n| `--dockerfile` | No       | —                            | Path to a custom Dockerfile (build context will be the current working directory) |\n\n### `sandcastle docker remove-image`\n\nRemoves the Docker image.\n\n| Option         | Required | Default                      | Description       |\n| -------------- | -------- | ---------------------------- | ----------------- |\n| `--image-name` | No       | `sandcastle:<repo-dir-name>` | Docker image name |\n\n### `sandcastle podman build-image`\n\nBuilds the Podman image from an existing `.sandcastle/` directory. Use this after modifying the Containerfile.\n\n| Option            | Required | Default                      | Description                                                                          |\n| ----------------- | -------- | ---------------------------- | ------------------------------------------------------------------------------------ |\n| `--image-name`    | No       | `sandcastle:<repo-dir-name>` | Podman image name                                                                    |\n| `--containerfile` | No       | —                            | Path to a custom Containerfile (build context will be the current working directory) |\n\n### `sandcastle podman remove-image`\n\nRemoves the Podman image.\n\n| Option         | Required | Default                      | Description       |\n| -------------- | -------- | ---------------------------- | ----------------- |\n| `--image-name` | No       | `sandcastle:<repo-dir-name>` | Podman image name |\n\n### `RunOptions`\n\n| Option               | Type               | Default                       | Description                                                                                                                                                     |\n| -------------------- | ------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `agent`              | AgentProvider      | —                             | **Required.** Agent provider (e.g. `claudeCode(\"claude-opus-4-6\")`, `pi(\"claude-sonnet-4-6\")`, `codex(\"gpt-5.4-mini\")`, `opencode(\"opencode/big-pickle\")`)      |\n| `sandbox`            | SandboxProvider    | —                             | **Required.** Sandbox provider (e.g. `docker()`, `podman()`, `docker({ imageName: \"sandcastle:local\" })`)                                                       |\n| `cwd`                | string             | `process.cwd()`               | Host repo directory — anchor for `.sandcastle/` artifacts and git operations. Relative paths resolve against `process.cwd()`.                                   |\n| `prompt`             | string             | —                             | Inline prompt (mutually exclusive with `promptFile`)                                                                                                            |\n| `promptFile`         | string             | —                             | Path to prompt file (mutually exclusive with `prompt`). Resolves against `process.cwd()`, **not** `cwd`.                                                        |\n| `maxIterations`      | number             | `1`                           | Maximum iterations to run                                                                                                                                       |\n| `hooks`              | SandboxHooks       | —                             | Lifecycle hooks (`host.*`, `sandbox.*`)                                                                                                                         |\n| `name`               | string             | —                             | Display name for the run, shown as a prefix in log output                                                                                                       |\n| `promptArgs`         | PromptArgs         | —                             | Key-value map for `{{KEY}}` placeholder substitution                                                                                                            |\n| `branchStrategy`     | BranchStrategy     | per-provider default          | Branch strategy: `{ type: 'head' }`, `{ type: 'merge-to-head' }`, or `{ type: 'branch', branch: '…' }`                                                          |\n| `copyToWorktree`     | string[]           | —                             | Host-relative file paths to copy into the sandbox before start (not supported with `branchStrategy: { type: 'head' }`)                                          |\n| `logging`            | object             | file (auto-generated)         | `{ type: 'file', path }` or `{ type: 'stdout' }`                                                                                                                |\n| `completionSignal`   | string \\| string[] | `<promise>COMPLETE</promise>` | String or array of strings the agent emits to stop the iteration loop early                                                                                     |\n| `idleTimeoutSeconds` | number             | `600`                         | Idle timeout in seconds — resets on each agent output event                                                                                                     |\n| `resumeSession`      | string             | —                             | Resume a prior Claude Code session by ID. Incompatible with `maxIterations > 1`. Session file must exist on host.                                               |\n| `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`. |\n| `timeouts`           | Timeouts           | —                             | Override default timeouts for built-in lifecycle steps. Currently supports `{ copyToWorktreeMs?: number }` (default: 60 000).                                   |\n\nOpenCode runs inside the sandbox container, so host `opencode login` state is not available there. Set `OPENCODE_API_KEY` in `.sandcastle/.env` or pass it via `opencode(model, { env: { OPENCODE_API_KEY } })`.\n\nOpenCode also enables no-push guardrails by default. Sandcastle gives OpenCode an explicit `OPENCODE_PERMISSION` policy that allows normal autonomous work but denies `git push`, git remote creation/mutation, and GitHub repo/PR/release creation commands. Guarded OpenCode runs do not use `--dangerously-skip-permissions`.\n\nWhen those guardrails are active, Sandcastle strips GitHub and git credential material from the sandbox environment, including `GH_TOKEN`, `GITHUB_TOKEN`, token-like GitHub env vars, git credential helper env, git config injection env, and git askpass hooks. User mounts that expose `~/.config/gh`, `~/.git-credentials`, `~/.gitconfig`, or git credential-manager paths are rejected. Mounting `~/.ssh` is still allowed.\n\nAuthenticated `gh` issue workflows are intentionally unavailable inside guarded OpenCode sandboxes. Use Beads, unauthenticated public GitHub reads, or an explicit `opencode(model, { gitRemoteGuardrails: false })` opt-out if you accept the remote-push risk.\n\n### `RunResult`\n\n| Field              | Type                | Description                                                        |\n| ------------------ | ------------------- | ------------------------------------------------------------------ |\n| `iterations`       | `IterationResult[]` | Per-iteration results (use `.length` for the count)                |\n| `completionSignal` | string?             | The matched completion signal string, or `undefined` if none fired |\n| `stdout`           | string              | Final assistant text from all iterations                           |\n| `commits`          | `{ sha }[]`         | Commits created during the run                                     |\n| `branch`           | string              | Target branch name                                                 |\n| `logFilePath`      | string?             | Path to the log file (only when logging to a file)                 |\n\n### `IterationResult`\n\n| Field             | Type              | Description                                                                                                                         |\n| ----------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `sessionId`       | string?           | Claude Code session ID from the init line, or `undefined` for non-Claude agents                                                     |\n| `sessionFilePath` | string?           | Absolute host path to the captured session JSONL, or `undefined` when capture is off                                                |\n| `usage`           | `IterationUsage`? | Token usage snapshot from the last assistant message, or `undefined` when capture is off or provider does not support usage parsing |\n\n### `IterationUsage`\n\n| Field                      | Type   | Description                                |\n| -------------------------- | ------ | ------------------------------------------ |\n| `inputTokens`              | number | Input tokens consumed                      |\n| `cacheCreationInputTokens` | number | Tokens used to create prompt cache entries |\n| `cacheReadInputTokens`     | number | Tokens read from prompt cache              |\n| `outputTokens`             | number | Output tokens generated                    |\n\n### Session capture\n\nAfter 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.\n\nSession 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.\n\n### Session resume\n\nPass `resumeSession` to `run()` to continue a prior Claude Code conversation inside a new sandbox:\n\n```typescript\nconst result = await run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(),\n  prompt: \"Continue where you left off\",\n  resumeSession: \"abc-123-def\",\n});\n```\n\nBefore 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.\n\nConstraints:\n\n- `resumeSession` is incompatible with `maxIterations > 1` (throws before sandbox creation).\n- The session file must exist at `~/.claude/projects/<encoded-path>/sessions/<id>.jsonl` (throws before sandbox creation).\n- Only iteration 1 receives the resume flag; subsequent iterations (if any) start fresh.\n- Non-Claude agent providers ignore `resumeSession`.\n\n### `ClaudeCodeOptions`\n\nThe `claudeCode()` factory accepts an optional second argument for provider-specific options:\n\n```typescript\nagent: claudeCode(\"claude-opus-4-6\", { effort: \"high\" });\n```\n\n| Option            | Type                                         | Default | Description                                               |\n| ----------------- | -------------------------------------------- | ------- | --------------------------------------------------------- |\n| `effort`          | `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"max\"` | —       | Claude Code reasoning effort level (`max` is Opus only)   |\n| `env`             | `Record<string, string>`                     | `{}`    | Environment variables injected by this agent provider     |\n| `captureSessions` | `boolean`                                    | `true`  | Capture agent session JSONL to host for `claude --resume` |\n\n### `CodexOptions`\n\nThe `codex()` factory accepts an optional second argument for provider-specific options:\n\n```typescript\nagent: codex(\"gpt-5.4\", { effort: \"high\" });\n```\n\n| Option   | Type                                           | Default | Description                                               |\n| -------- | ---------------------------------------------- | ------- | --------------------------------------------------------- |\n| `effort` | `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` | —       | Codex reasoning effort level via `model_reasoning_effort` |\n| `env`    | `Record<string, string>`                       | `{}`    | Environment variables injected by this agent provider     |\n\n### Provider `env`\n\nBoth **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:\n\n```typescript\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\", {\n    env: { ANTHROPIC_API_KEY: \"sk-ant-...\" },\n  }),\n  sandbox: docker({\n    env: { DOCKER_SPECIFIC_VAR: \"value\" },\n  }),\n  prompt: \"Fix issue #42\",\n});\n```\n\n**Merge rules:**\n\n- Provider env (agent + sandbox) overrides `.sandcastle/.env` resolver output for shared keys\n- Agent provider env and sandbox provider env **must not overlap** — if they share any key, `run()` throws an error\n- When `env` is not provided, it defaults to `{}`\n\nEnvironment variables are also resolved automatically from `.sandcastle/.env` and `process.env` — no need to pass them to the API. The required variables depend on the **agent provider** (see `sandcastle init` output for details).\n\n## Custom Sandbox Providers\n\nSandcastle ships with built-in providers for Docker, Podman, and Vercel, but you can create your own. A sandbox provider tells Sandcastle how to execute commands in an isolated environment. There are two kinds:\n\n- **Bind-mount** — the sandbox can mount a host directory. Sandcastle creates a worktree on the host and the provider mounts it in. No file sync needed. Use this for Docker, Podman, or any local container runtime.\n- **Isolated** — the sandbox has its own filesystem (e.g. a cloud VM). The provider handles syncing code in and out via `copyIn` and `copyFileOut`. Use this when the sandbox cannot access the host filesystem.\n\n### The sandbox handle contract\n\nBoth provider types return a **sandbox handle** from their `create()` function. The handle exposes:\n\n| Method         | Required   | Description                                                                  |\n| -------------- | ---------- | ---------------------------------------------------------------------------- |\n| `exec`         | Both       | Run a command, optionally streaming stdout line-by-line via `options.onLine` |\n| `close`        | Both       | Tear down the sandbox                                                        |\n| `copyFileIn`   | Bind-mount | Copy a single file from the host into the sandbox                            |\n| `copyFileOut`  | Both       | Copy a single file from the sandbox to the host                              |\n| `copyIn`       | Isolated   | Copy a file or directory from the host into the sandbox                      |\n| `worktreePath` | Both       | Absolute path to the repo directory inside the sandbox                       |\n\n### `ExecResult`\n\nEvery `exec` call returns an `ExecResult`:\n\n```typescript\ninterface ExecResult {\n  readonly stdout: string;\n  readonly stderr: string;\n  readonly exitCode: number;\n}\n```\n\n### Bind-mount provider example\n\nA minimal bind-mount provider that shells out to local processes (no container):\n\n```typescript\nimport {\n  createBindMountSandboxProvider,\n  type BindMountCreateOptions,\n  type BindMountSandboxHandle,\n  type ExecResult,\n} from \"@ecology91/sandcastle\";\nimport { execFile, spawn } from \"node:child_process\";\nimport { copyFile as fsCopyFile, mkdir as fsMkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { createInterface } from \"node:readline\";\n\nconst localProcess = () =>\n  createBindMountSandboxProvider({\n    name: \"local-process\",\n    create: async (\n      options: BindMountCreateOptions,\n    ): Promise<BindMountSandboxHandle> => {\n      const worktreePath = options.worktreePath;\n\n      return {\n        worktreePath,\n\n        exec: (\n          command: string,\n          opts?: { onLine?: (line: string) => void; cwd?: string },\n        ): Promise<ExecResult> => {\n          if (opts?.onLine) {\n            const onLine = opts.onLine;\n            return new Promise((resolve, reject) => {\n              const proc = spawn(\"sh\", [\"-c\", command], {\n                cwd: opts?.cwd ?? worktreePath,\n                stdio: [\"ignore\", \"pipe\", \"pipe\"],\n              });\n\n              const stdoutChunks: string[] = [];\n              const stderrChunks: string[] = [];\n\n              const rl = createInterface({ input: proc.stdout! });\n              rl.on(\"line\", (line) => {\n                stdoutChunks.push(line);\n                onLine(line); // forward each line to Sandcastle\n              });\n\n              proc.stderr!.on(\"data\", (chunk: Buffer) => {\n                stderrChunks.push(chunk.toString());\n              });\n\n              proc.on(\"error\", (err) => reject(err));\n              proc.on(\"close\", (code) => {\n                resolve({\n                  stdout: stdoutChunks.join(\"\\n\"),\n                  stderr: stderrChunks.join(\"\"),\n                  exitCode: code ?? 0,\n                });\n              });\n            });\n          }\n\n          return new Promise((resolve, reject) => {\n            execFile(\n              \"sh\",\n              [\"-c\", command],\n              { cwd: opts?.cwd ?? worktreePath, maxBuffer: 10 * 1024 * 1024 },\n              (error, stdout, stderr) => {\n                if (error && error.code === undefined) {\n                  reject(new Error(`exec failed: ${error.message}`));\n                } else {\n                  resolve({\n                    stdout: stdout.toString(),\n                    stderr: stderr.toString(),\n                    exitCode: typeof error?.code === \"number\" ? error.code : 0,\n                  });\n                }\n              },\n            );\n          });\n        },\n\n        copyFileIn: async (hostPath: string, sandboxPath: string) => {\n          await fsMkdir(dirname(sandboxPath), { recursive: true });\n          await fsCopyFile(hostPath, sandboxPath);\n        },\n\n        copyFileOut: async (sandboxPath: string, hostPath: string) => {\n          await fsMkdir(dirname(hostPath), { recursive: true });\n          await fsCopyFile(sandboxPath, hostPath);\n        },\n\n        close: async () => {\n          // nothing to tear down for a local process\n        },\n      };\n    },\n  });\n```\n\n### Isolated provider example\n\nA minimal isolated provider using a temp directory:\n\n```typescript\nimport {\n  createIsolatedSandboxProvider,\n  type IsolatedSandboxHandle,\n  type ExecResult,\n} from \"@ecology91/sandcastle\";\nimport { execFile, spawn } from \"node:child_process\";\nimport { copyFile, mkdir, mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { createInterface } from \"node:readline\";\n\nconst tempDir = () =>\n  createIsolatedSandboxProvider({\n    name: \"temp-dir\",\n    create: async (): Promise<IsolatedSandboxHandle> => {\n      const root = await mkdtemp(join(tmpdir(), \"sandbox-\"));\n      const worktreePath = join(root, \"workspace\");\n      await mkdir(worktreePath, { recursive: true });\n\n      return {\n        worktreePath,\n\n        exec: (\n          command: string,\n          opts?: { onLine?: (line: string) => void; cwd?: string },\n        ): Promise<ExecResult> => {\n          if (opts?.onLine) {\n            const onLine = opts.onLine;\n            return new Promise((resolve, reject) => {\n              const proc = spawn(\"sh\", [\"-c\", command], {\n                cwd: opts?.cwd ?? worktreePath,\n                stdio: [\"ignore\", \"pipe\", \"pipe\"],\n              });\n\n              const stdoutChunks: string[] = [];\n              const stderrChunks: string[] = [];\n\n              const rl = createInterface({ input: proc.stdout! });\n              rl.on(\"line\", (line) => {\n                stdoutChunks.push(line);\n                onLine(line);\n              });\n\n              proc.stderr!.on(\"data\", (chunk: Buffer) => {\n                stderrChunks.push(chunk.toString());\n              });\n\n              proc.on(\"error\", (err) => reject(err));\n              proc.on(\"close\", (code) => {\n                resolve({\n                  stdout: stdoutChunks.join(\"\\n\"),\n                  stderr: stderrChunks.join(\"\"),\n                  exitCode: code ?? 0,\n                });\n              });\n            });\n          }\n\n          return new Promise((resolve, reject) => {\n            execFile(\n              \"sh\",\n              [\"-c\", command],\n              { cwd: opts?.cwd ?? worktreePath, maxBuffer: 10 * 1024 * 1024 },\n              (error, stdout, stderr) => {\n                if (error && error.code === undefined) {\n                  reject(new Error(`exec failed: ${error.message}`));\n                } else {\n                  resolve({\n                    stdout: stdout.toString(),\n                    stderr: stderr.toString(),\n                    exitCode: typeof error?.code === \"number\" ? error.code : 0,\n                  });\n                }\n              },\n            );\n          });\n        },\n\n        copyIn: async (hostPath: string, sandboxPath: string) => {\n          const info = await stat(hostPath);\n          if (info.isDirectory()) {\n            await cp(hostPath, sandboxPath, { recursive: true });\n          } else {\n            await mkdir(dirname(sandboxPath), { recursive: true });\n            await copyFile(hostPath, sandboxPath);\n          }\n        },\n\n        copyFileOut: async (sandboxPath: string, hostPath: string) => {\n          await mkdir(dirname(hostPath), { recursive: true });\n          await copyFile(sandboxPath, hostPath);\n        },\n\n        close: async () => {\n          await rm(root, { recursive: true, force: true });\n        },\n      };\n    },\n  });\n```\n\n### Branch strategies\n\nA branch strategy controls where the agent's commits land. Configure it when constructing the provider:\n\n| Strategy        | Behavior                                                                 | Bind-mount | Isolated  |\n| --------------- | ------------------------------------------------------------------------ | ---------- | --------- |\n| `head`          | Agent writes directly to the host working directory. No worktree created | Default    | N/A       |\n| `merge-to-head` | Sandcastle creates a temp branch, merges back to HEAD when done          | Supported  | Default   |\n| `branch`        | Commits land on an explicit named branch you provide                     | Supported  | Supported |\n\n**When to use each:**\n\n- **`head`** — fast iteration during development. No branch indirection, no merge step. Only works with bind-mount providers since the agent needs direct host filesystem access.\n- **`merge-to-head`** — safe default for automation. The agent works on a throwaway branch; if something goes wrong, HEAD is untouched. Use this for CI or unattended runs.\n- **`branch`** — when you want commits on a specific branch (e.g. for a PR). Pass `{ type: \"branch\", branch: \"agent/fix-42\" }`.\n\nBranch strategy is now configured on `run()`, not on the provider:\n\n```typescript\nimport { run, claudeCode } from \"@ecology91/sandcastle\";\nimport { docker } from \"@ecology91/sandcastle/sandboxes/docker\";\n\n// head — direct write, bind-mount only (default for bind-mount providers)\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(),\n  prompt: \"…\",\n});\n// merge-to-head — temp branch, merge back (default for isolated providers)\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: tempDir(),\n  prompt: \"…\",\n});\n// branch — explicit named branch\nawait run({\n  agent: claudeCode(\"claude-opus-4-6\"),\n  sandbox: docker(),\n  branchStrategy: { t","readmeFilename":"README.md"}