sandlot/SPEC.md
Chris Wanstrath 5d7bf302f2 Add sandlot CLI for branch-based dev with worktrees and Apple containers
Implements all 7 commands: new, save, push, list, open, stop, rm.
Uses Commander for CLI parsing, Anthropic SDK for AI commit messages
and merge conflict resolution, and Apple container for VMs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:15:29 -08:00

6.1 KiB

sandlot

A CLI for branch-based development using git worktrees and Apple containers. Each branch gets its own worktree and isolated VM. Commits are auto-summarized by Claude. Merging cleans everything up.

Concepts

Sandlot is a thin workflow layer over three things: git worktrees, Apple containers, and the Claude API. The idea is that spinning up a branch should give you a fully isolated environment — filesystem and runtime — with zero setup. When you're done, you merge and everything tears down.

A sandlot session is a (worktree, VM) pair tied to a branch. Sessions are created with sandlot <branch> and destroyed on merge.

Tech Stack

Setup

Prerequisites

  • macOS on Apple Silicon
  • Bun installed
  • Git installed
  • container installed and available on PATH

Install

git clone https://github.com/your/sandlot
cd sandlot
bun install
bun link

This makes sandlot available globally.

Configure

Set your Anthropic API key:

export ANTHROPIC_API_KEY=sk-ant-...

Add to your shell profile (.zshrc, .bashrc, etc.) to persist it.

CLI

sandlot new <branch>

Create a new session. This:

  1. Checks out the branch if it exists (local or remote), or creates a new branch from current HEAD
  2. Creates a git worktree at .sandlot/<branch>/ (relative to the repo root)
  3. Boots an Apple container VM mapped to that worktree
  4. Drops you into the VM shell
$ sandlot new fix-POST
Creating worktree at .sandlot/fix-POST/
Booting VM...
root@fix-POST:~#

sandlot save [message]

Stage and commit all changes. If a message is provided, use it. If not, generate one with Claude.

  1. Runs git add . in the worktree
  2. If no message provided: diffs staged changes against the last commit, sends the diff to Claude API (claude-sonnet-4-20250514) to generate a commit message
  3. Commits with the message
  4. Pushes the branch to origin

If there are no changes, does nothing.

The AI-generated commit message is a single-line summary (≤72 chars) followed by an optional body with more detail if the diff is substantial.

$ sandlot save
Staged 3 files
Commit: Fix POST handler to validate request body before processing
Pushed fix-POST → origin/fix-POST

$ sandlot save "wip: rough cut of validation"
Staged 3 files
Commit: wip: rough cut of validation
Pushed fix-POST → origin/fix-POST

sandlot push <target>

Push the current session's branch into <target>, then tear everything down.

  1. Checks out <target> in the main working tree
  2. Merges the session branch (fast-forward if possible, merge commit otherwise)
  3. Pushes <target> to origin
  4. Stops and removes the VM
  5. Removes the worktree
  6. Deletes the local branch
  7. Deletes the remote branch

If there are uncommitted changes in the worktree, prompts to sandlot save first.

If the merge has conflicts:

  1. Collects all conflicted files
  2. For each file, sends the full conflict diff (ours, theirs, and base) to the Claude API
  3. Claude resolves the conflict and returns the merged file
  4. Writes the resolved file and stages it
  5. Shows a summary of what Claude chose and why
  6. Prompts for confirmation before committing the merge

If you reject Claude's resolution, drops you into the main working tree to resolve manually. Run sandlot push <target> again to complete cleanup.

$ sandlot push main
Pushing fix-POST → main...
Pushed main → origin/main
Stopped VM fix-POST
Removed worktree .sandlot/fix-POST/
Deleted branch fix-POST (local + remote)

With conflicts:

$ sandlot push main
Pushing fix-POST → main...
2 conflicts found. Resolving with Claude...

  src/handlers/post.ts
    ✓ Kept the new validation logic from fix-POST,
      preserved the logging added in main

  src/routes.ts
    ✓ Combined route definitions from both branches

Accept Claude's resolutions? [Y/n] y
Committed merge
Pushed main → origin/main
Stopped VM fix-POST
Removed worktree .sandlot/fix-POST/
Deleted branch fix-POST (local + remote)

sandlot list

Show all active sessions.

$ sandlot list
BRANCH          VM STATUS    WORKTREE
fix-POST        running      .sandlot/fix-POST/
refactor-auth   stopped      .sandlot/refactor-auth/

sandlot open <branch>

Re-enter an existing session's VM. If the VM is stopped, boots it first.

$ sandlot open fix-POST
Booting VM...
root@fix-POST:~#

sandlot stop <branch>

Stop a session's VM without destroying it. The worktree and branch remain.

sandlot rm <branch>

Tear down a session without merging. Stops the VM, removes the worktree, deletes the local branch. Does not touch the remote branch.

Configuration

Optional sandlot.json at the repo root:

{
  "vm": {
    "cpus": 4,
    "memory": "8GB",
    "image": "ubuntu:24.04",
    "mounts": {
      "/path/to/shared/deps": "/deps"
    }
  },
  "ai": {
    "model": "claude-sonnet-4-20250514"
  }
}

State

Sandlot tracks sessions in .sandlot/state.json at the repo root:

{
  "sessions": {
    "fix-POST": {
      "branch": "fix-POST",
      "worktree": ".sandlot/fix-POST",
      "vm_id": "container-abc123",
      "created_at": "2026-02-16T10:30:00Z",
      "status": "running"
    }
  }
}

.sandlot/ should be added to .gitignore.

Edge Cases

  • Nested sandlot calls: sandlot save and sandlot push detect the current session from the working directory or a SANDLOT_BRANCH env var set when entering the VM.
  • Stale VMs: If a VM crashes, sandlot open should detect the dead VM and reboot it.
  • Multiple repos: State is per-repo. No global daemon.
  • Branch name conflicts: If .sandlot/<branch>/ already exists as a directory but the session state is missing, prompt to clean up or recover.

Non-Goals

  • Not a CI/CD tool. No pipelines, no test runners.
  • Not a replacement for git. All git state lives in the real repo. Sandlot is sugar on top.
  • No multi-user collaboration features. This is a single-developer workflow tool.