Sandbox Existing Branches Implementation Plan

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


Task 1: Update worktree-base constant

Files:

Step 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"

Task 2: Add get-commit-info function

Files:

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"

Task 3: Add branch-exists? helper

Files:

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"

Task 4: Add get-branch-sha helper

Files:

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"

Task 5: Add classify-branch function

Files:

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"

Task 6: Add prompt-yn function

Files:

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"

Task 7: Add prompt-choice function

Files:

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"

Task 8: Add format-commit-line helper

Files:

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"

Task 9: Add confirm-branch function

Files:

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"

Task 10: Add prepare-branch function

Files:

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"

Task 11: Update start function

Files:

Step 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"

Task 12: Update clean function

Files:

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"

Task 13: Manual end-to-end test

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


Task 14: Update CLAUDE.md documentation

Files:

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:

Connect editor to nREPL:

cat .worktrees/<branch-name>/.nrepl-port
# Connect to localhost:<port>

Inside sandbox, start Integrant system with (go) in nREPL.

Requirements:


**Step 2: Commit**

```bash
git add CLAUDE.md
git commit -m "docs: update sandbox documentation for branch detection"