From 2f516e8a7560926618fa5aab5e97f460689af190 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 23 Feb 2026 20:00:54 -0800 Subject: [PATCH] update CLAUDE.md to reflect Commander 14, new vm/open/squash/merge behaviors, binary caching, and container provisioning details --- CLAUDE.md | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b863456..0e7e034 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ - **Runtime**: [Bun](https://bun.sh) (not Node.js — use `bun` for everything) - **Language**: TypeScript (strict, ESNext, bundler module resolution) -- **CLI parsing**: [Commander.js](https://github.com/tj/commander.js) 13.x +- **CLI parsing**: [Commander.js](https://github.com/tj/commander.js) 14.x - **Containers**: Apple Container (`brew install container`) - **Entry point**: `src/cli.ts` (shebang: `#!/usr/bin/env bun`, runs directly without compilation) @@ -38,22 +38,24 @@ src/ spinner.ts # CLI progress spinner (braille frames) commands/ new.ts # Create session, derive branch name from prompt - open.ts # Re-enter existing session + open.ts # Re-enter existing session (always uses --continue) close.ts # Remove worktree and clean up session list.ts # Show all active sessions with status save.ts # Stage all changes and commit merge.ts # Merge branch into main with conflict resolution + squash.ts # Squash-merge branch into main (AI commit message) and close rebase.ts # Rebase branch onto main with conflict resolution review.ts # Launch grumpy code review with Claude diff.ts # Show uncommitted changes or full branch diff show.ts # Show prompt and full diff for a branch log.ts # Show commits on branch not on main dir.ts # Print worktree path for a session + edit.ts # Open a session file in $EDITOR (with path escape check) shell.ts # Open fish shell in container cleanup.ts # Remove stale sessions - vm.ts # VM subcommands (create, start, stop, destroy, status, info) + vm.ts # VM subcommands (create, start, stop, destroy, status, info, uncache) completions.ts # Fish shell completions generator - helpers.ts # Shared: requireSession, resolveConflicts, saveChanges + helpers.ts # Shared: requireSession, resolveConflicts, mergeAndClose, saveChanges ``` ## Architecture @@ -77,11 +79,15 @@ Each module has a single responsibility. No classes — only exported async func - Image: `ubuntu:24.04` - User: `ubuntu` -- Mounts: `~/dev` and `~/.sandlot` from host -- Provisioned once on first use: installs `curl git neofetch fish unzip`, Bun, Claude Code, git identity, API key helper, activity tracking hook +- Memory limit: 4G +- Mounts: `~/dev` **read-only** at `/host`, `~/.sandlot` read-write at `/sandlot` +- Host symlinks: creates `~/dev` → `/host` and `~/.sandlot` → `/sandlot` inside the container so host-absolute worktree paths resolve correctly +- `containerPath()` in `vm.ts` translates host paths to container paths (`~/.sandlot/…` → `/sandlot/…`, `~/dev/…` → `/host/…`) +- Provisioned once on first use: apt installs `curl git fish unzip`, then installs Bun, Claude Code, neofetch, and Neovim +- Binary caching: after first install, binaries are cached in `~/.sandlot/.cache/` (bun, claude, neofetch, nvim.tar.gz) — subsequent `vm create` uses cached copies, skipping downloads - API key: read from `~/.env` on host (`ANTHROPIC_API_KEY=...`), written to a temp file and copied as `~/.claude/api-key-helper.sh` in the container (never passed as a process argument) - Claude settings: `skipDangerousModePermissionPrompt: true`, activity tracking hooks (`UserPromptSubmit` / `Stop`) in container -- Also writes `~/.claude.json` with `hasCompletedOnboarding: true` to skip first-run prompts +- Also writes `~/.claude.json` with `hasCompletedOnboarding: true` and `effortCalloutDismissed: true` ## Shell Command Pattern @@ -131,12 +137,20 @@ Always use `.nothrow()` for commands that may fail non-fatally. Use `.quiet()` t - `vm.exec()` prepends `export PATH=$HOME/.local/bin:$PATH` so `claude` binary is found - `vm.claude()` uses `Bun.spawn` with `stdin/stdout/stderr: "inherit"` for interactive TTY; in print mode (`-p`), captures stdout via pipe and returns the output +- `vm.claude()` runs Claude with `--dangerously-skip-permissions`, `--model claude-opus-4-6`, and `--append-system-prompt` (system prompt describes the container environment) +- `vm.claude()` retry logic: if exit code is non-zero and `--continue` was used, retries without `--continue` - `vm.claudePipe()` writes input to a temp file in `~/.sandlot/`, pipes it to `claude -p` inside the container, and returns the result — used for commit message generation and conflict resolution - `vm.isClaudeActive()` reads activity marker files written by the in-container `sandlot-activity` hook script - Branch creation in `createWorktree()` handles three cases: local branch, remote branch (tracks origin), new branch from HEAD - `sandlot new` accepts a prompt instead of a branch name — derives a 2-word branch name via the Anthropic API (Haiku), falling back to simple text munging +- `sandlot open` always passes `continue: true` to `vm.claude()` to resume the previous conversation - `sandlot save` uses `vm.claudePipe()` to generate commit messages from the staged diff -- `sandlot merge` and `sandlot rebase` use `vm.claudePipe()` to resolve merge conflicts automatically +- `sandlot merge`, `sandlot squash`, and `sandlot rebase` use `vm.claudePipe()` to resolve merge conflicts automatically +- `sandlot squash` generates an AI commit message for the squash commit via `claudePipe()`; falls back to `"squash "` +- `sandlot merge` and `sandlot squash` both delegate to `mergeAndClose()` in `helpers.ts`, which merges, resolves conflicts, commits, and then calls `closeAction()` to clean up +- `sandlot list` discovers missing session prompts by parsing Claude's `history.jsonl` from inside the container +- `sandlot list` shows four status icons: idle (dim `◌`), active (cyan `◯`), dirty/unsaved (yellow `◎`), saved (green `●`) - `sandlot new` and `sandlot open` auto-save changes when Claude exits (disable with `--no-save`) -- Default behavior (no subcommand): shows `list` if sessions exist, otherwise shows help +- `sandlot close` has a hidden `rm` alias +- Default behavior (no subcommand): always runs `list` (which prints "No active sessions." if empty) - `.sandlot/` should be in the repo's `.gitignore`