For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Enable bb sandbox:start to detect and use existing local/remote branches with interactive confirmation.
Architecture: Add branch detection logic that classifies branch state (none/local/remote/both), prompt functions for user confirmation with commit context, then modify start to use detection before worktree creation. Worktree paths now match branch names directly.
Tech Stack: Babashka, babashka.process (git commands), clojure.string
Files:
scripts/sandbox.clj:10Step 1: Change worktree-base from .worktrees/feature to .worktrees
(def worktree-base ".worktrees")
Step 2: Verify change
Run: bb -e '(require (quote sandbox)) sandbox/worktree-base'
Expected: ".worktrees"
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "refactor(sandbox): change worktree-base to .worktrees"
Files:
scripts/sandbox.clj (add after feature->port)Step 1: Add function to fetch commit info
(defn get-commit-info
"Get commit info for a ref: {:sha :message :author :date}"
[ref]
(let [format "%h|%s|%an|%ar"
result (p/shell {:out :string :continue true}
"git" "log" "-1" (str "--format=" format) ref)]
(when (zero? (:exit result))
(let [[sha message author date] (str/split (str/trim (:out result)) #"\|" 4)]
{:sha sha :message message :author author :date date}))))
Step 2: Test manually
Run: bb -e '(require (quote sandbox)) (sandbox/get-commit-info "HEAD")'
Expected: Map with :sha, :message, :author, :date keys
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add get-commit-info function"
Files:
scripts/sandbox.clj (add after get-commit-info)Step 1: Add function to check if branch exists
(defn branch-exists?
"Check if a branch exists (local or remote)"
[branch-name]
(let [result (p/shell {:out :string :continue true}
"git" "rev-parse" "--verify" branch-name)]
(zero? (:exit result))))
Step 2: Test manually
Run: bb -e '(require (quote sandbox)) [(sandbox/branch-exists? "master") (sandbox/branch-exists? "nonexistent-branch-xyz")]'
Expected: [true false]
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add branch-exists? helper"
Files:
scripts/sandbox.clj (add after branch-exists?)Step 1: Add function to get branch SHA
(defn get-branch-sha
"Get the SHA of a branch, or nil if it doesn't exist"
[branch-name]
(let [result (p/shell {:out :string :continue true}
"git" "rev-parse" branch-name)]
(when (zero? (:exit result))
(str/trim (:out result)))))
Step 2: Test manually
Run: bb -e '(require (quote sandbox)) (sandbox/get-branch-sha "master")'
Expected: A 40-character SHA string
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add get-branch-sha helper"
Files:
scripts/sandbox.clj (add after get-branch-sha)Step 1: Add function to classify branch state
(defn classify-branch
"Classify branch state. Returns {:classification :local-info :remote-info}
Classification is one of: :none, :local-only, :remote-only, :both-same, :both-diverged"
[branch-name]
(let [local-sha (get-branch-sha branch-name)
remote-sha (get-branch-sha (str "origin/" branch-name))
local-info (when local-sha (get-commit-info branch-name))
remote-info (when remote-sha (get-commit-info (str "origin/" branch-name)))]
{:classification (cond
(and (nil? local-sha) (nil? remote-sha)) :none
(and local-sha (nil? remote-sha)) :local-only
(and (nil? local-sha) remote-sha) :remote-only
(= local-sha remote-sha) :both-same
:else :both-diverged)
:local-info local-info
:remote-info remote-info}))
Step 2: Test manually
Run: bb -e '(require (quote sandbox)) (sandbox/classify-branch "master")'
Expected: Map with :classification (likely :both-same or :local-only) and info maps
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add classify-branch function"
Files:
scripts/sandbox.clj (add after classify-branch)Step 1: Add Y/n prompt function
(defn prompt-yn
"Prompt user for Y/n confirmation. Returns true if yes, false if no."
[message]
(print (str message " [Y/n] "))
(flush)
(let [input (str/trim (or (read-line) ""))]
(not (str/starts-with? (str/lower-case input) "n"))))
Step 2: Commit (can't easily test interactive prompts)
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add prompt-yn function"
Files:
scripts/sandbox.clj (add after prompt-yn)Step 1: Add multiple choice prompt function
(defn prompt-choice
"Prompt user for a numbered choice. Returns the choice number (1-indexed) or nil if cancelled."
[options]
(doseq [[i option] (map-indexed vector options)]
(println (str " " (inc i) ") " option)))
(print "\nChoice: ")
(flush)
(let [input (str/trim (or (read-line) ""))]
(when-let [n (parse-long input)]
(when (and (>= n 1) (<= n (count options)))
n))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add prompt-choice function"
Files:
scripts/sandbox.clj (add after prompt-choice)Step 1: Add function to format commit info as single line
(defn format-commit-line
"Format commit info as: sha - message (author, date)"
[{:keys [sha message author date]}]
(str " " sha " - " message " (" author ", " date ")"))
Step 2: Test manually
Run: bb -e '(require (quote sandbox)) (sandbox/format-commit-line {:sha "abc123" :message "Fix bug" :author "John" :date "2 days ago"})'
Expected: " abc123 - Fix bug (John, 2 days ago)"
Step 3: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add format-commit-line helper"
Files:
scripts/sandbox.clj (add after format-commit-line)Step 1: Add main confirmation function that handles all classifications
(defn confirm-branch
"Show confirmation prompt based on branch classification.
Returns {:action :create-new|:use-local|:track-remote|:reset-to-remote|:cancel
:branch <branch-name>}"
[branch-name {:keys [classification local-info remote-info]}]
(case classification
:none
(do
(println (str "No branch '" branch-name "' found."))
(if (prompt-yn (str "Create new branch '" branch-name "' from master?"))
{:action :create-new :branch branch-name}
{:action :cancel}))
:local-only
(do
(println (str "Found local branch '" branch-name "':"))
(println (format-commit-line local-info))
(if (prompt-yn "Use this branch?")
{:action :use-local :branch branch-name}
{:action :cancel}))
:remote-only
(do
(println (str "Found remote branch 'origin/" branch-name "':"))
(println (format-commit-line remote-info))
(if (prompt-yn (str "Create local branch '" branch-name "' tracking remote?"))
{:action :track-remote :branch branch-name}
{:action :cancel}))
:both-same
(do
(println (str "Found branch '" branch-name "' (synced with remote):"))
(println (format-commit-line local-info))
(if (prompt-yn "Use this branch?")
{:action :use-local :branch branch-name}
{:action :cancel}))
:both-diverged
(do
(println (str "Branch '" branch-name "' has diverged from remote:\n"))
(println (str " Local: " (subs (format-commit-line local-info) 2)))
(println (str " Remote: " (subs (format-commit-line remote-info) 2)))
(println "\nWhich version?")
(case (prompt-choice ["Local (keep your changes)"
"Remote (discard local, reset to remote)"
"Cancel"])
1 {:action :use-local :branch branch-name}
2 {:action :reset-to-remote :branch branch-name}
{:action :cancel}))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add confirm-branch function"
Files:
scripts/sandbox.clj (add after confirm-branch)Step 1: Add function to execute git commands based on action
(defn prepare-branch
"Execute git commands to prepare the branch based on action.
Returns the branch name to use for worktree, or nil if cancelled."
[{:keys [action branch]}]
(case action
:cancel
(do
(println "Cancelled.")
nil)
:create-new
(do
(println (str "Creating branch '" branch "' from master..."))
(p/shell "git" "branch" branch "master")
branch)
:use-local
branch
:track-remote
(do
(println (str "Creating local branch '" branch "' tracking origin/" branch "..."))
(p/shell "git" "branch" branch (str "origin/" branch))
branch)
:reset-to-remote
(do
(println (str "Resetting '" branch "' to origin/" branch "..."))
(p/shell "git" "branch" "-f" branch (str "origin/" branch))
branch)))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add prepare-branch function"
Files:
scripts/sandbox.clj:44-78Step 1: Replace the start function with new implementation
(defn start
"Start sandbox for a branch. Detects existing branches and prompts for confirmation."
[args]
(let [{:keys [feature port]} (parse-args args)]
(when-not feature
(println "Usage: bb sandbox:start <branch-name> [--port PORT]")
(System/exit 1))
(let [port (or port (feature->port feature))
worktree-path (str (fs/path worktree-base feature))
abs-worktree-path (str (fs/absolutize worktree-path))]
;; If worktree already exists, just start containers
(if (fs/exists? worktree-path)
(do
(println (str "Reusing existing worktree at " worktree-path))
(spit (str (fs/path worktree-path ".nrepl-port")) (str port))
(println (str "nREPL will be exposed on port " port))
(println "Starting sandbox containers...")
(p/shell {:dir sandbox-dir
:extra-env {"WORKTREE_PATH" abs-worktree-path
"NREPL_HOST_PORT" (str port)
"FEATURE_NAME" feature
"HOME" (System/getenv "HOME")}}
"docker-compose" "run" "--rm" "--service-ports" "claude"
"claude" "--dangerously-skip-permissions"))
;; No worktree - detect branch and confirm
(do
(println "Fetching from origin...")
(p/shell {:out :inherit :err :inherit} "git" "fetch" "origin")
(println)
(let [branch-state (classify-branch feature)
decision (confirm-branch feature branch-state)]
(when-let [branch (prepare-branch decision)]
(println (str "\nCreating worktree at " worktree-path "..."))
(fs/create-dirs (fs/parent worktree-path))
(p/shell "git" "worktree" "add" worktree-path branch)
(spit (str (fs/path worktree-path ".nrepl-port")) (str port))
(println (str "nREPL will be exposed on port " port))
(println "Starting sandbox containers...")
(p/shell {:dir sandbox-dir
:extra-env {"WORKTREE_PATH" abs-worktree-path
"NREPL_HOST_PORT" (str port)
"FEATURE_NAME" feature
"HOME" (System/getenv "HOME")}}
"docker-compose" "run" "--rm" "--service-ports" "claude"
"claude" "--dangerously-skip-permissions"))))))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): update start with branch detection and confirmation"
Files:
scripts/sandbox.clj (the clean function)Step 1: Update clean to handle arbitrary branch names
(defn clean
"Clean up sandbox containers and optionally worktree"
[args]
(let [{:keys [feature worktree]} (parse-args args)]
(when-not feature
(println "Usage: bb sandbox:clean <branch-name> [--worktree]")
(System/exit 1))
(let [worktree-path (str (fs/path worktree-base feature))]
;; Stop and remove containers/volumes
(println (str "Removing containers and volumes for " feature "..."))
(p/shell {:dir sandbox-dir
:extra-env {"FEATURE_NAME" feature}
:continue true}
"docker-compose" "down" "-v")
;; Remove worktree if requested
(when worktree
(when (fs/exists? worktree-path)
(println (str "Removing worktree " worktree-path "..."))
(p/shell "git" "worktree" "remove" "--force" worktree-path))
;; Only delete branch if it's not a standard branch (master, main, develop)
(when-not (#{"master" "main" "develop"} feature)
(println (str "Deleting branch " feature "..."))
(p/shell {:continue true} "git" "branch" "-D" feature)))
(println "Done."))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "refactor(sandbox): update clean for arbitrary branch names"
Step 1: Test with non-existent branch (should prompt to create)
Run: bb sandbox:start test-new-branch-xyz
Expected:
Fetching from origin...
No branch 'test-new-branch-xyz' found.
Create new branch 'test-new-branch-xyz' from master? [Y/n]
Type n to cancel.
Step 2: Test with existing remote branch
Find an existing remote branch: git branch -r | head -5
Run: bb sandbox:start <existing-branch-name>
Expected: Shows remote commit info and asks to create local tracking branch.
Type n to cancel.
Step 3: Clean up test artifacts
Run: git branch -D test-new-branch-xyz 2>/dev/null; rm -rf .worktrees/test-new-branch-xyz
Step 4: Commit any fixes if needed
Files:
CLAUDE.md (Sandbox Development section)Step 1: Update the sandbox documentation
Find the "Sandbox Development" section and update it:
## Sandbox Development
Run Claude Code in an isolated Docker environment with its own postgres and localstack:
```bash
bb sandbox:start <branch-name> # Start sandbox (detects existing branches)
bb sandbox:stop <branch-name> # Stop containers
bb sandbox:list # List running sandboxes
bb sandbox:clean <name> [--worktree] # Clean up containers/volumes (and worktree)
The sandbox:start command:
Each sandbox:
.worktrees/<branch-name>ENVIRON_NAME=sandbox profile for configConnect editor to nREPL:
cat .worktrees/<branch-name>/.nrepl-port
# Connect to localhost:<port>
Inside sandbox, start Integrant system with (go) in nREPL.
Requirements:
orcha group with GID 1600 (for file permission sharing)orcha group
**Step 2: Commit**
```bash
git add CLAUDE.md
git commit -m "docs: update sandbox documentation for branch detection"