Skip to content

Lean execution layer: adopt sandcastle's high-level sandbox lifecycle #72

Description

@lsfera

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 unchangeddocker() 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 unchangeddocker() 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions