You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adopt sandcastle's high-level sandbox lifecycle in the execution layer, so .sandcastle/sandbox-runner.ts matches the idiom in the reference run.ts instead of hand-rolling run() + manual teardown. Full design + rationale: .scratch/lean-execution-layer/prd.md.
Goal (end-to-end value)
/afk and /hitl behave identically — one PR per ready issue, auto-merge on green — but the sandbox-execution layer sits on the high-level API (createSandbox + await using + hooks.onSandboxReady + copyToWorktree + promptFile/promptArgs) that @ai-hero/sandcastle@0.10.0 already ships, so the library owns worktree creation and deterministic teardown and we maintain less bespoke lifecycle code. No dependency bump (verified against installed 0.10.0 types).
Acceptance criteria
High-level lifecycle:sandbox-runner.ts uses await using sandbox = await createSandbox({ sandbox: docker({…}), branch, copyToWorktree, hooks }) then sandbox.run({…}). The bespoke SandboxRunner lifecycle/teardown is replaced by scope-bound disposal.
Setup via hook: in-sandbox setup (the local tier's dependency install/build) moves to a declarative hooks.onSandboxReady step, not prompt text.
Prompts as templates: agent prompts move to promptFile.md templates with promptArgs (issue number/title/branch + tier guidance); only genuinely conditional bits stay in a thin pure helper.
MTU stays declarative: network attached via docker({ network: SANDBOX_NETWORK }); ensureSandboxNetwork (creates the --opt mtu=1400 network, ADR-0013) is unchanged — docker() attaches but does not create a custom-MTU network.
Socat stays imperative:resolveDockerHost and its process.env.DOCKER_HOST set (fix: bypass DooD socat proxy so /afk agent turns stream #71) are unchanged — docker() has no socket/host option, so this is not expressible declaratively. The ADR must record this asymmetry so nobody "tidies" it back into the empty-turn bug.
Tiers preserved:claude (claudeCode + OAuth) and local (opencode + Ollama) both run through the shared high-level path; tier selection collapses to choosing the agent provider + imageName/setup hook, not two code paths.
Pure builder seam preserved (the real logic — see Testing): the function that assembles the createSandbox/docker()/agent options stays a pure, exported function returning a plain config object (today buildAgentInput), retyped to the high-level option types.
Regression: full default npm test green, npm run typecheck clean. reduce.ts, the smee/webhook listener, signature verification, issue-source.ts dependency graph, detached/cockpit mode, and credential resolution are not touched.
ADR: new ADR ("Lean execution layer — high-level sandcastle lifecycle") relating to ADR-0006 (afk wraps sandcastle via exec — still true; createSandbox execs under the hood), ADR-0013 (MTU), and the fix: bypass DooD socat proxy so /afk agent turns stream #71 socat fix; CLAUDE.md updated if the runner's contract is documented there.
Testing (highest, single seam — logic in TypeScript)
Reuse the existing seam — the pure builder in sandbox-runner.ts, exercised by sandbox-runner.test.ts (node:test + node:assert/strict). After the refactor it returns the high-level options object; assert on it without spawning Docker:
correct inner image per tier; network set to the MTU network; copyToWorktree contents; onSandboxReady setup command present for the local tier; promptArgs derived from the issue.
Prior art: sandbox-runner.test.ts builder assertions and reduce.test.ts (incl. the resolveDockerHost tests). Live createSandbox/await using round-trip stays behind the gated integration.test.ts (SANDCASTLE_INTEGRATION=1) — the CI sandcastle job is unaffected. Do not add a new seam.
Unchanged (do not touch)
The reactive core and the orchestration topology: webhook-driven, one-issue-per-sandbox, auto-merge (ADR-0005/0006/0007/0008). This adopts the reference's lifecycle idiom, NOT its Planner→parallel→Merger batch model. resolveDockerHost (socat) and ensureSandboxNetwork (MTU creation) stay as shipped.
Constraints (per repo workflow)
Implement with /tdd per criterion, run shell via /exec, stay scoped to the execution layer (sandbox-runner.ts + minimal main.ts call-site changes + prompt templates + ADR), no pushing to main, no new dependencies (target installed @ai-hero/sandcastle@0.10.0).
Adopt sandcastle's high-level sandbox lifecycle in the execution layer, so
.sandcastle/sandbox-runner.tsmatches the idiom in the referencerun.tsinstead of hand-rollingrun()+ manual teardown. Full design + rationale:.scratch/lean-execution-layer/prd.md.Goal (end-to-end value)
/afkand/hitlbehave identically — one PR per ready issue, auto-merge on green — but the sandbox-execution layer sits on the high-level API (createSandbox+await using+hooks.onSandboxReady+copyToWorktree+promptFile/promptArgs) that@ai-hero/sandcastle@0.10.0already ships, so the library owns worktree creation and deterministic teardown and we maintain less bespoke lifecycle code. No dependency bump (verified against installed0.10.0types).Acceptance criteria
sandbox-runner.tsusesawait using sandbox = await createSandbox({ sandbox: docker({…}), branch, copyToWorktree, hooks })thensandbox.run({…}). The bespokeSandboxRunnerlifecycle/teardown is replaced by scope-bound disposal.hooks.onSandboxReadystep, not prompt text.promptFile.mdtemplates withpromptArgs(issue number/title/branch + tier guidance); only genuinely conditional bits stay in a thin pure helper.docker({ network: SANDBOX_NETWORK });ensureSandboxNetwork(creates the--opt mtu=1400network, ADR-0013) is unchanged —docker()attaches but does not create a custom-MTU network.resolveDockerHostand itsprocess.env.DOCKER_HOSTset (fix: bypass DooD socat proxy so /afk agent turns stream #71) are unchanged —docker()has no socket/host option, so this is not expressible declaratively. The ADR must record this asymmetry so nobody "tidies" it back into the empty-turn bug.claude(claudeCode + OAuth) andlocal(opencode + Ollama) both run through the shared high-level path; tier selection collapses to choosing theagentprovider +imageName/setup hook, not two code paths.createSandbox/docker()/agent options stays a pure, exported function returning a plain config object (todaybuildAgentInput), retyped to the high-level option types.npm testgreen,npm run typecheckclean.reduce.ts, the smee/webhook listener, signature verification,issue-source.tsdependency graph, detached/cockpit mode, and credential resolution are not touched.createSandboxexecs under the hood), ADR-0013 (MTU), and the fix: bypass DooD socat proxy so /afk agent turns stream #71 socat fix;CLAUDE.mdupdated if the runner's contract is documented there.Testing (highest, single seam — logic in TypeScript)
Reuse the existing seam — the pure builder in
sandbox-runner.ts, exercised bysandbox-runner.test.ts(node:test+node:assert/strict). After the refactor it returns the high-level options object; assert on it without spawning Docker:networkset to the MTU network;copyToWorktreecontents;onSandboxReadysetup command present for the local tier;promptArgsderived from the issue.Prior art:
sandbox-runner.test.tsbuilder assertions andreduce.test.ts(incl. theresolveDockerHosttests). LivecreateSandbox/await usinground-trip stays behind the gatedintegration.test.ts(SANDCASTLE_INTEGRATION=1) — the CIsandcastlejob is unaffected. Do not add a new seam.Unchanged (do not touch)
The reactive core and the orchestration topology: webhook-driven, one-issue-per-sandbox, auto-merge (ADR-0005/0006/0007/0008). This adopts the reference's lifecycle idiom, NOT its Planner→parallel→Merger batch model.
resolveDockerHost(socat) andensureSandboxNetwork(MTU creation) stay as shipped.Constraints (per repo workflow)
Implement with
/tddper criterion, run shell via/exec, stay scoped to the execution layer (sandbox-runner.ts+ minimalmain.tscall-site changes + prompt templates + ADR), no pushing to main, no new dependencies (target installed@ai-hero/sandcastle@0.10.0).