Use when the user requests integration testing, feature validation, or test plan execution
日本語の概要は準備中です。原文の説明を表示しています。
Orchestrate AI coding agents (Claude Code, Codex, OpenCode) in isolated sandboxes using the @ai-hero/sandcastle SDK. Use when the user needs to (1) run agents AFK in Docker/Podman containers, (2) build multi-agent pipelines with plan-execute-review patterns, (3) run parallel agents on separate worktrees, (4) create iterative agent loops with maxIterations, (5) extract structured output from agent runs, (6) set up sandcastle in a new or existing project, or (7) write prompt files with template args and shell expressions.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Sandcastle (@ai-hero/sandcastle) orchestrates AI coding agents inside isolated sandbox environments. It manages git worktrees, boots containers, runs agents with structured prompts, collects commits, and merges results back to branches — all from a single run() call.
npm i @ai-hero/sandcastle
npx sandcastle init # scaffolds .sandcastle/ with Dockerfile and prompt templates
npx sandcastle docker build-image # builds the Docker image from .sandcastle/Dockerfile
import { run, claudeCode } from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
const result = await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});
console.log(result.commits); // [{ sha: "abc123" }, ...]
console.log(result.branch); // branch the agent worked on
| Function | Use Case |
|---|---|
run(options) | One-shot agent invocation. Full lifecycle managed automatically. |
createSandbox({ branch, sandbox }) | Reusable sandbox on an explicit branch. Call .run() multiple times. |
createWorktree({ branchStrategy }) | Independent git worktree. Call .run(), .interactive(), or .createSandbox(). |
interactive(options) | Interactive terminal session with an agent. Human-in-the-loop. |
import { claudeCode, codex, opencode, pi } from "@ai-hero/sandcastle";
claudeCode() // default: claude-opus-4-7
claudeCode("claude-sonnet-4-6") // specify model
claudeCode("claude-sonnet-4-6", { effort: "low" }) // with options
codex("o4-mini")
Sandbox providers are subpath imports — not from the main entry point:
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
import { vercel } from "@ai-hero/sandcastle/sandboxes/vercel";
import { daytona } from "@ai-hero/sandcastle/sandboxes/daytona";
import { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
Two categories:
head.merge-to-head.interactive() — run() and createSandbox() reject it.| Strategy | Behavior | Default For |
|---|---|---|
{ type: "head" } | Agent writes directly to host working directory. Bind-mount only. | docker, podman |
{ type: "merge-to-head" } | Temp branch, auto-merged back to HEAD on completion. | vercel, daytona |
{ type: "branch", branch: "feat/x" } | Commits land on a named branch. Optional baseBranch. | — |
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});
Agent runs multiple iterations, checking for a completion signal between each:
await run({
name: "worker",
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
maxIterations: 5,
branchStrategy: { type: "merge-to-head" },
copyToWorktree: ["node_modules"],
hooks: {
sandbox: {
onSandboxReady: [{ command: "npm install" }],
},
},
});
The agent sees a completion signal instruction in the prompt. When it outputs <promise>COMPLETE</promise> (default), iteration stops. Otherwise it runs up to maxIterations.
Reuse one sandbox for implement → review → fix cycles:
const sandbox = await createSandbox({
branch: "feature/my-branch",
sandbox: docker(),
});
await sandbox.run({
agent: claudeCode("claude-sonnet-4-6"),
promptFile: "./.sandcastle/implement.md",
});
await sandbox.run({
agent: claudeCode("claude-opus-4-7"),
promptFile: "./.sandcastle/review.md",
});
await sandbox.close();
Multiple agents work in parallel on separate worktrees, then a merge agent combines results:
const tasks = issues.map(async (issue) => {
const wt = await createWorktree({
branchStrategy: { type: "branch", branch: `fix/${issue.id}` },
});
const result = await wt.run({
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
prompt: `Fix issue: ${issue.title}\n\n${issue.description}`,
});
await wt.close();
return result;
});
const results = await Promise.allSettled(tasks);
Extract typed data from agent runs:
import { Output } from "@ai-hero/sandcastle";
import { z } from "zod";
const result = await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/plan.md",
maxIterations: 1, // required for structured output
output: Output.object({
tag: "plan",
schema: z.object({
issues: z.array(z.object({
title: z.string(),
priority: z.enum(["high", "medium", "low"]),
})),
}),
}),
});
console.log(result.output.issues); // fully typed
The prompt must contain the XML tag (e.g., <plan>) — sandcastle validates this at startup. The agent wraps its structured response in that tag. Also available: Output.string({ tag }) for plain string extraction.
Explore interactively, then automate implementation:
const wt = await createWorktree({
branchStrategy: { type: "branch", branch: "feature/new" },
});
await wt.interactive({
agent: claudeCode("claude-opus-4-7"),
sandbox: noSandbox(), // noSandbox only valid with interactive()
});
await wt.run({
agent: claudeCode("claude-sonnet-4-6"),
sandbox: docker(),
promptFile: "./.sandcastle/implement.md",
});
await wt.close();
await run({
agent: claudeCode(),
sandbox: docker(),
prompt: "Refactor the auth module to use dependency injection",
});
Inline prompts skip all processing — no {{KEY}} substitution, no shell expressions. Passing promptArgs with an inline prompt is an error.
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
promptArgs: { ISSUE_TITLE: "Fix login bug", ISSUE_BODY: "Users can't log in" },
});
Prompt files support two features:
1. Template arguments — {{KEY}} placeholders replaced from promptArgs:
Fix the following issue:
Title: {{ISSUE_TITLE}}
Body: {{ISSUE_BODY}}
2. Shell expressions — !`command` evaluated inside the sandbox before each iteration:
The current test output is:
!`npm test 2>&1 | tail -50`
The current git diff is:
!`git diff`
Shell expressions run per-iteration, so each iteration sees fresh state.
Built-in args (auto-injected, cannot be overridden): {{SOURCE_BRANCH}}, {{TARGET_BRANCH}}.
Processing order: Prompt file resolution → Arg substitution (host, once) → Shell expansion (sandbox, per iteration).
docker({
imageName: "my-custom-image",
containerUid: 1000,
containerGid: 1000,
mounts: [
{ hostPath: "~/.npmrc", sandboxPath: "~/.npmrc", readonly: true },
{ hostPath: "./secrets", sandboxPath: "/app/secrets" },
],
env: { NODE_ENV: "development", CI: "true" },
network: "host",
selinuxLabel: "z",
})
Lifecycle hooks run commands at specific points:
await run({
agent: claudeCode(),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
hooks: {
host: {
onWorktreeReady: [{ command: "cp .env.local .sandcastle/.env" }],
onSandboxReady: [{ command: "echo 'sandbox is up'" }],
},
sandbox: {
onSandboxReady: [
{ command: "npm install", timeoutMs: 120_000 },
{ command: "npm run build", sudo: false },
],
},
},
});
Execution order: copyToWorktree → host.onWorktreeReady (sequential) → sandbox created → host.onSandboxReady + sandbox.onSandboxReady (parallel).
| Parameter | Default |
|---|---|
idleTimeoutSeconds | 600 (10 min) |
| Hook commands | 60s (per-hook timeoutMs) |
| Git setup | 10s |
| Commit collection | 30s |
| Merge to host | 30s |
| Shell expression expansion | 30s |
timeouts.copyToWorktreeMs | 60s |
await run({
completionSignal: "<done>FINISHED</done>", // custom signal
// or: completionSignal: ["<done>FINISHED</done>", "<done>SKIPPED</done>"],
// default: "<promise>COMPLETE</promise>"
});
const controller = new AbortController();
setTimeout(() => controller.abort("timeout"), 300_000);
await run({
agent: claudeCode(),
sandbox: docker(),
prompt: "Fix all lint errors",
signal: controller.signal,
});
Both Sandbox and Worktree support Symbol.asyncDispose:
await using sandbox = await createSandbox({
branch: "feature/x",
sandbox: docker(),
});
// automatically cleaned up when scope exits
Every run() returns:
{
iterations: IterationResult[]; // per-iteration details
completionSignal?: string; // matched signal, or undefined if maxIterations hit
stdout: string; // combined agent output
commits: { sha: string }[]; // commits made by agent
branch: string; // branch agent worked on
logFilePath?: string; // log file path (file logging mode)
preservedWorktreePath?: string; // set when worktree had uncommitted changes
output?: T; // present when Output config was provided
}
Claude Code sessions can be captured and resumed across runs:
import { hostSessionStore, transferSession } from "@ai-hero/sandcastle";
const result = await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/prompt.md",
});
// Resume a prior session in a new run
await run({
agent: claudeCode("claude-opus-4-7"),
sandbox: docker(),
promptFile: "./.sandcastle/continue.md",
resumeSession: "session-id-from-prior-run",
maxIterations: 1, // required when resuming
});
npx sandcastle init # scaffold .sandcastle/ directory
npx sandcastle docker build-image # build Docker image from .sandcastle/Dockerfile
npx sandcastle docker remove-image # remove the Docker image
npx sandcastle podman build-image # build with Podman
npx sandcastle podman remove-image # remove Podman image
sandcastle init offers templates:
run() callcreateSandbox()Sandbox providers are subpath imports — import { docker } from "@ai-hero/sandcastle/sandboxes/docker", never from the main entry point.
noSandbox() only works with interactive() — run() and createSandbox() reject it. AFK work requires real isolation.
head strategy is incompatible with isolated providers — throws at runtime. Use merge-to-head or branch instead.
copyToWorktree is incompatible with head strategy — head mode bind-mounts the host directory directly, no worktree exists.
resumeSession requires maxIterations: 1 — resume applies to iteration 1 only.
Structured output requires maxIterations: 1 — and the XML tag must appear in the prompt text. Sandcastle validates both at startup.
Inline prompts skip all processing — no {{KEY}} substitution, no !`command` expansion. Passing promptArgs with an inline prompt is an error.
Built-in prompt args (SOURCE_BRANCH, TARGET_BRANCH) cannot be overridden — attempting to do so is a runtime error.
Docker UID must match image UID — sandcastle runs a pre-flight check and errors on mismatch. Default is host UID or 1000.
Worktrees are preserved on error — if the agent leaves uncommitted changes or an error occurs, the worktree is not cleaned up. The path is returned in preservedWorktreePath.
See references/api-types.md for full type signatures and references/prompt-system.md for prompt file details.
When asked to build a sandcastle pipeline:
pipeline.ts or .sandcastle/run.ts).sandcastle/*.md prompt templates with {{ARG}} placeholders and !`command` expressions as needed.sandcastle/Dockerfile if not already present (or instruct user to run npx sandcastle init)"pipeline": "npx tsx .sandcastle/run.ts"After generating a pipeline:
@ai-hero/sandcastle is in package.json dependencies{{ARG}} placeholders in prompts have matching promptArgs.sandcastle/Dockerfile exists or sandcastle init was runmaxIterations: 1 and the XML tag appears in the promptまだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Use when the user requests integration testing, feature validation, or test plan execution
日本語の概要は準備中です。原文の説明を表示しています。
Use when the user wants to systematically fix AI code slop — duplicated logic, over-engineering, silent error swallowing, convention drift, cargo-cult patterns, and other LLM-introduced architectural decay — over a specified duration
日本語の概要は準備中です。原文の説明を表示しています。
Produce a researched long-form article from a topic prompt via an orchestrated pipeline - research agent (first-person sources, working-definition gate), narrative-architecture outline, writer/cold-reviewer loop with an explicit ACCEPT/REVISE verdict contract, then a catalog-deslop pass with a regression gate. The orchestrator dispatches subagents only; the writer never judges its own draft. Use when the user says "article factory", "write an article about X", "run the article pipeline", or asks for a researched long-form piece produced end-to-end. For essays and micro posts in the user's own voice without a research stage, use the prose skill instead.
日本語の概要は準備中です。原文の説明を表示しています。
Runs autonomous keep/discard experiments on a codebase to optimize a single metric for a fixed duration, in the style of karpathy/autoresearch. Use when the user says "autoresearch" (optionally with a focus, e.g. "autoresearch the optimizer"), asks to run experiments on a repo overnight, to hill-climb or optimize a metric autonomously, or points at a repo with a karpathy-style program.md.
日本語の概要は準備中です。原文の説明を表示しています。
Create custom modules for [Harbor Boost](https://github.com/av/harbor/tree/main/boost), an optimizing LLM proxy. Use when building Python modules that intercept/transform LLM chat completions—reasoning chains, prompt injection, structured outputs, artifacts, or custom workflows. Triggers on requests to create Boost modules, extend LLM behavior via proxy, or implement chat completion middleware.
日本語の概要は準備中です。原文の説明を表示しています。
Systematically explore and test any software project (CLI, API, Backend, Library, etc.) to find bugs, usability issues, and edge cases. Produces a structured report with full reproduction evidence (exact commands, inputs, logs, and tracebacks) for every issue.
日本語の概要は準備中です。原文の説明を表示しています。