For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add CLI commands to forward host ports to a running sandbox using socat.
Architecture: Three new bb tasks (expose, unexpose, exposed) in scripts/sandbox.clj. State tracked in .worktrees/.sandbox-expose/. Uses babashka.process to spawn/kill socat processes.
Tech Stack: Babashka, socat (host dependency), Docker CLI
Files:
scripts/sandbox.clj:10-13Step 1: Add constants after existing defs
Add after line 13 (after port-range-size):
(def expose-state-dir ".worktrees/.sandbox-expose")
(def expose-ports [7777 8888 9999 7888])
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add expose state dir and port constants"
Files:
scripts/sandbox.cljStep 1: Add function after parse-args
Add after parse-args function (after line 41):
(defn get-container-ip
"Get IP address of a running container by name pattern"
[container-name]
(let [result (p/shell {:out :string :continue true}
"docker" "inspect" "-f"
"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
container-name)]
(when (zero? (:exit result))
(str/trim (:out result)))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add get-container-ip helper"
Files:
scripts/sandbox.cljStep 1: Add function after get-container-ip
(defn find-claude-container
"Find the claude container name for a feature, or nil if not running"
[feature]
(let [result (p/shell {:out :string :continue true}
"docker" "ps" "-q" "-f"
(str "name=orcha-sandbox-" feature "-claude"))]
(when (and (zero? (:exit result))
(not (str/blank? (:out result))))
(str/trim (:out result)))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add find-claude-container helper"
Files:
scripts/sandbox.cljStep 1: Add function after find-claude-container
(defn kill-existing-expose!
"Kill any existing socat processes from previous expose"
[]
(let [pids-file (str (fs/path expose-state-dir "pids"))]
(when (fs/exists? pids-file)
(doseq [pid (str/split (slurp pids-file) #"\s+")]
(when-not (str/blank? pid)
(p/shell {:continue true} "kill" pid)))
(fs/delete pids-file))
(when (fs/exists? (str (fs/path expose-state-dir "current")))
(fs/delete (str (fs/path expose-state-dir "current"))))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): add kill-existing-expose! helper"
Files:
scripts/sandbox.cljStep 1: Add expose function after kill-existing-expose!
(defn expose
"Expose sandbox ports to host via socat"
[args]
(let [{:keys [feature]} (parse-args args)]
(when-not feature
(println "Usage: bb sandbox:expose <feature-name>")
(System/exit 1))
(let [container-id (find-claude-container feature)]
(when-not container-id
(println (str "Error: sandbox '" feature "' is not running"))
(System/exit 1))
(let [container-ip (get-container-ip container-id)]
(when-not container-ip
(println "Error: could not get container IP")
(System/exit 1))
;; Kill any existing expose
(kill-existing-expose!)
;; Create state directory
(fs/create-dirs expose-state-dir)
;; Spawn socat processes
(let [pids (for [port expose-ports]
(let [proc (p/process {:out :inherit :err :inherit}
"socat"
(str "TCP-LISTEN:" port ",fork,reuseaddr")
(str "TCP:" container-ip ":" port))]
(.pid (:proc proc))))]
;; Write state files
(spit (str (fs/path expose-state-dir "current")) feature)
(spit (str (fs/path expose-state-dir "pids")) (str/join " " pids))
(println (str "Exposing sandbox '" feature "' on ports: " (str/join ", " expose-ports))))))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): implement expose function"
Files:
scripts/sandbox.cljStep 1: Add unexpose function after expose
(defn unexpose
"Stop exposing sandbox ports"
[_args]
(if (fs/exists? (str (fs/path expose-state-dir "current")))
(let [feature (str/trim (slurp (str (fs/path expose-state-dir "current"))))]
(kill-existing-expose!)
(println (str "Stopped exposing sandbox '" feature "'")))
(println "No sandbox currently exposed")))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): implement unexpose function"
Files:
scripts/sandbox.cljStep 1: Add exposed function after unexpose
(defn exposed
"Show which sandbox is currently exposed"
[_args]
(let [current-file (str (fs/path expose-state-dir "current"))
pids-file (str (fs/path expose-state-dir "pids"))]
(if (and (fs/exists? current-file) (fs/exists? pids-file))
(let [feature (str/trim (slurp current-file))
pids (str/split (slurp pids-file) #"\s+")
alive? (some (fn [pid]
(when-not (str/blank? pid)
(zero? (:exit (p/shell {:continue true :out :string}
"kill" "-0" pid)))))
pids)]
(if alive?
(println (str "Currently exposed: " feature " (ports: " (str/join ", " expose-ports) ")"))
(do
(kill-existing-expose!)
(println "No sandbox currently exposed (stale state cleaned)"))))
(println "No sandbox currently exposed"))))
Step 2: Commit
git add scripts/sandbox.clj
git commit -m "feat(sandbox): implement exposed function"
Files:
bb.edn:111-114Step 1: Add tasks before the closing braces
Add after sandbox:clean task (before line 114):
sandbox:expose
{:doc "Expose sandbox ports to host: bb sandbox:expose <feature-name>"
:requires ([sandbox])
:task (sandbox/expose *command-line-args*)}
sandbox:unexpose
{:doc "Stop exposing sandbox ports: bb sandbox:unexpose"
:requires ([sandbox])
:task (sandbox/unexpose *command-line-args*)}
sandbox:exposed
{:doc "Show which sandbox is currently exposed"
:requires ([sandbox])
:task (sandbox/exposed *command-line-args*)}
Step 2: Commit
git add bb.edn
git commit -m "feat(sandbox): add bb tasks for expose/unexpose/exposed"
Files:
.gitignoreStep 1: Add ignore pattern
Add to .gitignore:
.worktrees/.sandbox-expose/
Step 2: Commit
git add .gitignore
git commit -m "chore: ignore sandbox expose state directory"
Step 1: Verify commands exist
bb tasks | grep sandbox
Expected: should show all sandbox tasks including expose, unexpose, exposed
Step 2: Test exposed with no sandbox
bb sandbox:exposed
Expected: "No sandbox currently exposed"
Step 3: Test expose error case
bb sandbox:expose nonexistent
Expected: "Error: sandbox 'nonexistent' is not running"
Step 4: Test with running sandbox (if available)
If you have a running sandbox, test the full flow:
bb sandbox:expose <feature-name>
bb sandbox:exposed
curl -s http://localhost:8888 || echo "ERP not responding (expected if not started)"
bb sandbox:unexpose
bb sandbox:exposed