#!/usr/bin/env bash
# sandbox — the Neuron STACK sandbox: assemble a WHOLE stack of repos into ONE
# combined worktree workspace, wired so they build/run TOGETHER, on an isolated
# clean base — so an agent (or Will) can work on the full stack at once instead of
# one repo at a time.
#
# It is the multi-repo generalisation of `nsbx` (this same directory): where
# `nsbx dev` stands up ONE repo's worktree + an isolated engram, `sandbox` stands
# up EVERY constituent repo of a named stack as sibling git worktrees under a
# single workspace, mirroring their on-disk relative layout so the cross-repo
# `../foundation/el` imports resolve to the SANDBOX copy — never the live tree.
#
#   sandbox el-stack     <name>      # elc compiler + EL language + framework + tooling (+ consumers)
#   sandbox neuron-stack <name>      # runtime/soul + engram + app/UI (the full product)
#   sandbox list                     # list assembled stack workspaces
#   sandbox status <profile> <name>  # inspect one
#   sandbox build  <profile> <name>  # run the workspace's combined build.sh
#   sandbox down   <profile> <name>  # tear down: remove every worktree, drop the workspace
#
# RAILS (always):
#   * worktrees fork off each repo's COMMITTED HEAD -> a clean, reproducible base
#     (the dirty state of the live checkout is deliberately NOT carried in).
#   * the workspace lives at a PERSISTENT path (never /tmp — ablated on compaction).
#   * NEVER touches the live soul/engram (:7770 / :8742). It only creates git
#     worktrees + a build script; bringing up an isolated engram is delegated,
#     opt-in, to `nsbx` (which already guards the live store & ports).
#   * idempotent & safe: refuses to clobber an existing workspace; teardown removes
#     worktrees through their origin repo and prunes — branches are kept by default.
#   * own-the-core: pure bash + git worktree. No new dependencies.
set -uo pipefail

# ---------------------------------------------------------------- constants ----
# Root that holds all the peer repos (neuron, foundation/el, products/*, ...).
DEV_ROOT="${NEURON_DEV_ROOT:-$HOME/Development/neuron-technologies}"
# Where assembled stack workspaces live (persistent; sibling to el-worktrees/).
STACK_ROOT="${NSBX_STACK_ROOT:-$DEV_ROOT/stack-worktrees}"
EL_REPO_REL="foundation/el"
LIVE_ENGRAM_PORT=8742          # live engram  — sandbox must never bind it
LIVE_SOUL_PORT=7770            # live soul    — sandbox must never bind it
# nsbx (single-repo isolated-engram tool) lives next to this script.
NSBX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/nsbx"

C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_CYN=$'\033[36m'; C_DIM=$'\033[2m'; C_BLD=$'\033[1m'; C_0=$'\033[0m'

# ---------------------------------------------------------------- helpers ------
die(){ printf '%serror:%s %s\n' "$C_RED" "$C_0" "$*" >&2; exit 1; }
log(){ printf '%s==>%s %s\n' "$C_BLD" "$C_0" "$*" >&2; }
info(){ printf '    %s\n' "$*" >&2; }
ok(){   printf '    %s%s%s\n' "$C_GRN" "$*" "$C_0" >&2; }
warn(){ printf '    %s%s%s\n' "$C_YEL" "$*" "$C_0" >&2; }
need(){ command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }

# ---------------------------------------------------------------- profiles -----
# profile_repos <profile> : emit one line per constituent repo:
#   <relpath-under-DEV_ROOT> | <required|optional> | <role>
# The relpath is preserved INSIDE the workspace, so all cross-repo `../foundation/el`
# references resolve to the sandbox copy automatically (mirrored-layout wiring).
profile_repos(){
  case "$1" in
    el-stack)
      # The compiler+language+framework+tooling are all ONE repo (foundation/el).
      # Its downstream SDK consumers (forge, dharma) + the language-faculty POC come
      # along so a change to elc can be proven end-to-end across the kit.
      cat <<'EOF'
foundation/el      | required | elc + elb compiler, el_runtime, engram source, el-ui framework, elp/ql/ide/epm tooling
engram-language    | optional | language-faculty reference POC (Python) — ported into el/elp
foundation/forge   | optional | downstream SDK consumer — `make build` (imprint forge CLI)
foundation/dharma  | optional | downstream SDK consumer — CGI provenance registry
EOF
      ;;
    neuron-stack)
      # The full product: substrate (el) + soul + UI. Engram is NOT a separate repo
      # (its source lives in foundation/el/engram/src/server.el).
      cat <<'EOF'
foundation/el      | required | substrate: elc + el_runtime + engram source + elp NLG the soul imports
neuron             | required | the soul (:7770) + engram build; soul.el imports ../foundation/el/elp/src/elp.el
products/NeuronUI  | required | the app/UI (Kotlin/Compose desktop client; bundles the soul binary)
products/web       | optional | marketing site + interactive soul-demo
EOF
      ;;
    *) return 1;;
  esac
}

is_profile(){ profile_repos "$1" >/dev/null 2>&1; }

ws_dir(){ printf '%s/%s-%s' "$STACK_ROOT" "$1" "$2"; }        # <root>/<profile>-<name>
ws_branch(){ printf 'sandbox/%s-%s' "$1" "$2"; }             # branch name used in each repo
manifest(){ printf '%s/.stack-manifest.json' "$1"; }         # <ws>/.stack-manifest.json

# ================================================================ up ===========
cmd_up(){
  local profile="$1"; shift
  local name="" branch="" base_override="" minimal=0
  [ $# -gt 0 ] && [ "${1#-}" = "$1" ] && { name="$1"; shift; } || die "usage: sandbox $profile <name> [--minimal] [--branch B] [--base REF]"
  while [ $# -gt 0 ]; do case "$1" in
    --minimal)       minimal=1; shift;;
    --branch)        branch="$2"; shift 2;;
    --base)          base_override="$2"; shift 2;;
    *) die "unknown flag: $1";;
  esac; done
  need git
  is_profile "$profile" || die "unknown profile: $profile (try: el-stack | neuron-stack)"

  local ws; ws="$(ws_dir "$profile" "$name")"
  [ -n "$branch" ] || branch="$(ws_branch "$profile" "$name")"

  # -------- pre-flight (fail before creating anything) --------
  case "$ws" in /tmp/*|/private/tmp/*|/var/tmp/*)
    die "refusing workspace under a temp dir ($ws) — temp dirs are ablated on compaction; set NSBX_STACK_ROOT to a persistent path";;
  esac
  [ -e "$ws" ] && die "workspace already exists: $ws  (sandbox down $profile $name first)"

  # resolve + validate every repo, and pick a base sha per repo, BEFORE touching disk
  local -a rels roles bases origins wts
  local line rel role_extra role req origin base wt
  while IFS= read -r line; do
    [ -z "${line// }" ] && continue
    rel="$(printf '%s' "$line"  | cut -d'|' -f1 | xargs)"
    req="$(printf '%s' "$line"  | cut -d'|' -f2 | xargs)"
    role="$(printf '%s' "$line" | cut -d'|' -f3- | sed 's/^ *//')"
    [ "$minimal" -eq 1 ] && [ "$req" = "optional" ] && continue
    origin="$DEV_ROOT/$rel"
    git -C "$origin" rev-parse --git-dir >/dev/null 2>&1 || {
      [ "$req" = "required" ] && die "required repo missing or not a git repo: $origin"
      warn "skipping optional repo (missing): $rel"; continue; }
    if [ -n "$base_override" ]; then base="$base_override"; else base="$(git -C "$origin" rev-parse HEAD)"; fi
    wt="$ws/$rel"
    [ -e "$wt" ] && die "target worktree path already exists: $wt"
    rels+=("$rel"); roles+=("$role"); origins+=("$origin"); bases+=("$base"); wts+=("$wt")
  done < <(profile_repos "$profile")
  [ "${#rels[@]}" -gt 0 ] || die "no repos resolved for profile $profile"

  log "assembling '$profile' workspace '$name'"
  info "workspace: $ws"
  info "branch:    $branch   (created in each repo, off its committed HEAD)"
  mkdir -p "$ws"

  # -------- create a worktree per repo (mirrored relpath layout) --------
  local i n="${#rels[@]}"
  SB_DONE_WTS=(); SB_DONE_ORIGINS=()
  for ((i=0; i<n; i++)); do
    rel="${rels[$i]}"; origin="${origins[$i]}"; base="${bases[$i]}"; wt="${wts[$i]}"
    mkdir -p "$(dirname "$wt")"
    local gerr
    if git -C "$origin" show-ref --verify --quiet "refs/heads/$branch"; then
      gerr="$(git -C "$origin" worktree add "$wt" "$branch" 2>&1)" \
        || { _rollback; die "git worktree add failed for $rel (existing branch $branch):"$'\n'"    $gerr"; }
    else
      gerr="$(git -C "$origin" worktree add -b "$branch" "$wt" "$base" 2>&1)" \
        || { _rollback; die "git worktree add -b $branch failed for $rel (base $base):"$'\n'"    $gerr"; }
    fi
    SB_DONE_WTS+=("$wt"); SB_DONE_ORIGINS+=("$origin")
    ok "worktree: $rel  ->  ${wt#$ws/}  (branch $branch @ ${base:0:9})"
  done

  local el_ws="$ws/$EL_REPO_REL"
  _write_env      "$ws" "$profile" "$name" "$branch" "$el_ws"
  _write_manifest "$ws" "$profile" "$name" "$branch"
  _write_build    "$ws" "$profile" "$el_ws"
  _write_readme   "$ws" "$profile" "$name" "$branch" "$el_ws"

  # -------- summary --------
  echo >&2
  printf '%s  STACK WORKSPACE READY — %s / %s%s\n' "$C_BLD" "$profile" "$name" "$C_0" >&2
  printf '   %-11s %s\n' "workspace" "$ws" >&2
  printf '   %-11s %s\n' "branch"    "$branch (in each repo)" >&2
  printf '   %-11s %s\n' "repos"     "$n worktrees, mirrored layout" >&2
  echo >&2
  info "get in:      cd $ws && source .stack-env"
  info "build all:   sandbox build $profile $name        # (or: cd $ws && ./build.sh)"
  if [ "$profile" = "neuron-stack" ]; then
    info "isolated engram (opt-in, via nsbx):"
    info "  nsbx create $profile-$name --source $el_ws && nsbx up $profile-$name"
  fi
  info "tear down:   sandbox down $profile $name          # removes all worktrees; branches kept"
}

# _rollback : remove any worktrees already created this run (globals set by cmd_up)
SB_DONE_WTS=(); SB_DONE_ORIGINS=()
_rollback(){
  local j
  [ "${#SB_DONE_WTS[@]}" -gt 0 ] && warn "rolling back ${#SB_DONE_WTS[@]} partial worktree(s)"
  for ((j=${#SB_DONE_WTS[@]}-1; j>=0; j--)); do
    git -C "${SB_DONE_ORIGINS[$j]}" worktree remove --force "${SB_DONE_WTS[$j]}" 2>/dev/null || rm -rf "${SB_DONE_WTS[$j]}"
    git -C "${SB_DONE_ORIGINS[$j]}" worktree prune 2>/dev/null || true
  done
}

# ---------------------------------------------------------------- writers ------
_write_env(){
  local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
  local elc_dir="$el_ws/lang/dist/platform"
  cat > "$ws/.stack-env" <<ENV
# stack env for '$profile/$name' — SOURCE this to work the whole stack together.
# Pins EL_REPO + PATH at the SANDBOX copy of foundation/el, so elc/elb/runtime and
# every cross-repo ../foundation/el import resolve INSIDE this workspace.
# The live mind (:$LIVE_ENGRAM_PORT engram / :$LIVE_SOUL_PORT soul) is deliberately NOT referenced.
export STACK_NAME="$profile-$name"
export STACK_PROFILE="$profile"
export STACK_ROOT_WS="$ws"
export EL_REPO="$el_ws"
export PATH="$elc_dir:\$PATH"          # elc, elb (darwin/linux prebuilt) from the sandbox el
ENV
  if [ "$profile" = "neuron-stack" ]; then
    cat >> "$ws/.stack-env" <<ENV
export NEURON_REPO="$ws/neuron"
export NEURONUI_REPO="$ws/products/NeuronUI"
# engram/soul are NOT bound here — bring up an ISOLATED engram via nsbx when needed
# (nsbx guards the live store & refuses ports :$LIVE_ENGRAM_PORT/:$LIVE_SOUL_PORT):
#   nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
#   nsbx status $profile-$name        # prints the isolated engram URL to point the soul at
ENV
  fi
  # direnv convenience
  [ -e "$ws/.envrc" ] || printf 'source_env .stack-env 2>/dev/null || source .stack-env\n' > "$ws/.envrc"
}

_write_manifest(){
  local ws="$1" profile="$2" name="$3" branch="$4"
  # emit worktree records from git's own worktree list, filtered to this workspace
  python3 - "$ws" "$profile" "$name" "$branch" "$DEV_ROOT" <<'PY'
import json, os, subprocess, sys
ws, profile, name, branch, dev = sys.argv[1:6]
repos = []
for rel in sorted(os.listdir(ws)) if False else []:
    pass
# discover worktrees by walking one level of relpaths we created
def git(root, *a):
    return subprocess.run(["git","-C",root,*a], capture_output=True, text=True).stdout.strip()
for dirpath, dirnames, filenames in os.walk(ws):
    if ".git" in filenames or ".git" in dirnames:
        rel = os.path.relpath(dirpath, ws)
        toplevel = git(dirpath, "rev-parse", "--show-toplevel")
        common   = git(dirpath, "rev-parse", "--git-common-dir")
        origin   = os.path.realpath(os.path.join(common, ".."))
        head     = git(dirpath, "rev-parse", "HEAD")
        repos.append({"rel": rel, "worktree": dirpath, "origin": origin,
                      "branch": branch, "head": head})
        dirnames[:] = []  # don't descend into a repo
repos.sort(key=lambda r: r["rel"])
json.dump({"profile": profile, "name": name, "branch": branch,
           "workspace": ws, "repos": repos},
          open(os.path.join(ws, ".stack-manifest.json"), "w"), indent=2)
PY
}

_write_build(){
  local ws="$1" profile="$2" el_ws="$3"
  cat > "$ws/build.sh" <<'BUILD'
#!/usr/bin/env bash
# build.sh — build the assembled stack together, in dependency order.
# Generated by `sandbox`. Run from the workspace root (it sources .stack-env).
set -uo pipefail
cd "$(dirname "$0")"; source ./.stack-env
say(){ printf '\033[1m==>\033[0m %s\n' "$*"; }
ok(){  printf '    \033[32m%s\033[0m\n' "$*"; }
bad(){ printf '    \033[31m%s\033[0m\n' "$*"; }

# locate an elc that runs on THIS machine (darwin-arm64 / linux-amd64), from the sandbox el
find_elc(){
  local d="$EL_REPO/lang/dist/platform"
  case "$(uname -s)-$(uname -m)" in
    Darwin-arm64) echo "$d/elc-darwin-arm64";;
    Linux-x86_64) echo "$d/elc-linux-amd64";;
    *)            echo "$d/elc";;
  esac
}
ELC="$(find_elc)"; [ -x "$ELC" ] || ELC="$EL_REPO/lang/dist/platform/elc"
say "elc: $ELC"
[ -x "$ELC" ] && ok "$("$ELC" 2>&1 | head -1 || echo present)" || { bad "elc not executable"; exit 1; }

# canonical runtime C to link (CI-published release copy; ~8 copies exist in-tree)
RT="$EL_REPO/lang/releases/v1.0.0-20260501"
[ -f "$RT/el_runtime.c" ] || RT="$EL_REPO/lang/el-compiler/runtime"
[ -f "$RT/el_runtime.c" ] && ok "el_runtime: $RT/el_runtime.c" || bad "no el_runtime.c found under $EL_REPO/lang"
BUILD

  if [ "$profile" = "el-stack" ]; then
    cat >> "$ws/build.sh" <<'BUILD'

# ---- EL STACK: prove elc + the el stuff (incl. the el-ui framework) build together ----
say "el-ui framework present: $EL_REPO/ui"
[ -d "$EL_REPO/ui" ] && ok "framework dir present ($(ls "$EL_REPO/ui" | tr '\n' ' '))" || bad "no ui/ dir"

# end-to-end compiler proof: elc compiles a real, substantial stack source to C,
# then cc links it against the runtime -> a working native binary.
B="$(mktemp -d)"
say "elc end-to-end: compile engram/src/server.el and link a native binary"
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/x.c" 2>"$B/elc.err"; then
  ok "elc -> C ($(wc -c <"$B/x.c" | tr -d ' ') bytes)"
  if cc -std=c11 -O2 -w -I "$RT" -o "$B/x" "$B/x.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
    ok "cc link ok -> native binary $(ls -lh "$B/x" | awk '{print $5}')  (elc + runtime build together)"
  else
    bad "cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/      /'
  fi
else
  bad "elc compile failed:"; sed 's/^/      /' "$B/elc.err" | head
fi

# optional downstream consumer: forge builds on the SDK (make build) — proves the
# freshly-assembled el SDK still compiles a real downstream repo.
FORGE="$STACK_ROOT_WS/foundation/forge"
if [ -f "$FORGE/Makefile" ]; then
  say "downstream consumer: foundation/forge (make build)"
  ( cd "$FORGE" && EL_REPO="$EL_REPO" PATH="$EL_REPO/lang/dist/platform:$PATH" make build ) \
    && ok "forge built against the sandbox SDK" || bad "forge build failed (see above)"
fi
say "el-stack build complete"
BUILD
  else
    cat >> "$ws/build.sh" <<'BUILD'

# ---- 1) engram (elc engram/src/server.el -> cc engram.c el_runtime.c), from the sandbox el ----
say "build engram from $EL_REPO/engram/src/server.el"
B="$(mktemp -d)"
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/engram.c" 2>"$B/elc.err"; then
  ok "elc -> engram.c ($(wc -c <"$B/engram.c" | tr -d ' ') bytes)"
  if cc -std=c11 -O2 -w -I "$RT" -o "$B/engram" \
        "$B/engram.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
    ok "engram binary built: $(ls -lh "$B/engram" | awk '{print $5}')"
  else
    bad "engram cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/      /'
  fi
else
  bad "engram elc transpile failed:"; sed 's/^/      /' "$B/elc.err"
fi

# ---- 2) soul (imports ../foundation/el/elp/src/elp.el — resolves to SANDBOX el) ----
say "soul present + cross-repo import resolves inside the sandbox"
[ -f "$NEURON_REPO/soul.el" ] && ok "neuron/soul.el present" || bad "no soul.el"
if [ -f "$EL_REPO/elp/src/elp.el" ]; then
  ok "../foundation/el/elp/src/elp.el resolves -> $EL_REPO/elp/src/elp.el (sandbox copy)"
else
  bad "elp NLG source missing under sandbox el"
fi
# soul is a heavy single-TU compile; prove elc parses it rather than a full link
if "$ELC" "$NEURON_REPO/soul.el" > "$B/soul.c" 2>"$B/soul.err"; then
  ok "elc compiled soul.el -> $(wc -c <"$B/soul.c" | tr -d ' ') bytes of C (cross-repo imports resolved)"
else
  bad "soul.el elc compile failed:"; sed 's/^/      /' "$B/soul.err" | head
fi

# ---- 3) UI (present + buildable; gradle/JDK21 is heavy so we don't run it here) ----
say "app/UI present + buildable"
if [ -f "$NEURONUI_REPO/build.sh" ] || [ -f "$NEURONUI_REPO/gradlew" ]; then
  ok "NeuronUI build entry present (./build.sh / ./gradlew — needs JDK21; run: cd $NEURONUI_REPO && ./gradlew run)"
else
  bad "no NeuronUI build entry"
fi
say "neuron-stack build complete (engram compiled, soul compiled, UI present & buildable)"
BUILD
  fi
  chmod +x "$ws/build.sh"
}

_write_readme(){
  local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
  cat > "$ws/README.md" <<MD
# $profile / $name — combined stack workspace

Assembled by \`sandbox\`. Every constituent repo is a **git worktree** on branch
\`$branch\`, forked off its origin repo's committed HEAD, laid out at its natural
relative path so cross-repo \`../foundation/el\` imports resolve **inside this
workspace** (the sandbox el), never the live tree.

## Get in
\`\`\`bash
cd $ws
source .stack-env      # EL_REPO + PATH now point at the sandbox el
./build.sh             # build the stack together (or: sandbox build $profile $name)
\`\`\`

## Layout
__STACK_LAYOUT__

## Isolation
- Worktrees only; the live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched.
- To run against an **isolated engram**, delegate to \`nsbx\` (guards the live store/ports):
  \`\`\`bash
  nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
  \`\`\`

## Tear down
\`\`\`bash
sandbox down $profile $name           # remove every worktree; branch '$branch' kept
sandbox down $profile $name --delete-branch
\`\`\`
MD
  # fill the layout list from the manifest without embedding backticks in the heredoc
  python3 - "$ws" <<'PY'
import json, os, sys
ws = sys.argv[1]
d = json.load(open(os.path.join(ws, ".stack-manifest.json")))
lines = ["- `%s`  <- worktree of %s" % (r["rel"], r["origin"]) for r in d["repos"]]
p = os.path.join(ws, "README.md")
txt = open(p).read().replace("__STACK_LAYOUT__", "\n".join(lines))
open(p, "w").write(txt)
PY
}

# ================================================================ down =========
cmd_down(){
  local profile="$1" name="$2"; shift 2 || true
  local del_branch=0
  while [ $# -gt 0 ]; do case "$1" in
    --delete-branch) del_branch=1; shift;;
    *) die "unknown flag: $1";;
  esac; done
  need git
  local ws; ws="$(ws_dir "$profile" "$name")"
  [ -d "$ws" ] || die "no such workspace: $ws"
  local mf; mf="$(manifest "$ws")"
  [ -f "$mf" ] || die "no manifest in $ws (refusing to guess); remove it by hand if intended"
  local branch; branch="$(python3 -c "import json;print(json.load(open('$mf'))['branch'])")"

  log "tearing down '$profile/$name'  ($ws)"
  # remove each worktree through its origin repo
  python3 -c "import json;[print(r['origin']+'\t'+r['worktree']) for r in json.load(open('$mf'))['repos']]" \
  | while IFS=$'\t' read -r origin wt; do
      if [ -d "$wt" ]; then
        git -C "$origin" worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
        git -C "$origin" worktree prune 2>/dev/null || true
        ok "removed worktree: ${wt#$ws/}"
      fi
      if [ "$del_branch" -eq 1 ]; then
        git -C "$origin" branch -D "$branch" 2>/dev/null && ok "deleted branch $branch in ${origin#$DEV_ROOT/}" || true
      fi
    done
  # drop the (now worktree-free) workspace tree
  rm -rf "$ws"
  ok "workspace removed: $ws"
  [ "$del_branch" -eq 1 ] || info "branch '$branch' kept in each repo (use --delete-branch to drop)"
  ok "down '$profile/$name' complete (live untouched)"
}

# ================================================================ build ========
cmd_build(){
  local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
  [ -x "$ws/build.sh" ] || die "no build.sh in $ws (is it assembled? sandbox $profile $name)"
  exec "$ws/build.sh"
}

# ================================================================ list/status ==
cmd_list(){
  [ -d "$STACK_ROOT" ] || { info "no stack workspaces (root $STACK_ROOT absent)"; return 0; }
  local mf found=0
  for mf in "$STACK_ROOT"/*/.stack-manifest.json; do
    [ -f "$mf" ] || continue; found=1
    python3 -c "import json;d=json.load(open('$mf'));print('  %-22s %-8s %2d repos  branch=%s'%(d['profile']+'/'+d['name'],'',len(d['repos']),d['branch']))" 2>/dev/null
  done
  [ "$found" -eq 1 ] || info "no assembled stack workspaces under $STACK_ROOT"
}

cmd_status(){
  local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
  local mf; mf="$(manifest "$ws")"; [ -f "$mf" ] || die "no such workspace: $ws"
  log "stack '$profile/$name'"; info "workspace: $ws"
  python3 - "$mf" <<'PY'
import json,sys,subprocess
d=json.load(open(sys.argv[1]))
print(f"    branch:    {d['branch']}")
for r in d['repos']:
    st=subprocess.run(["git","-C",r["worktree"],"status","--porcelain"],capture_output=True,text=True).stdout
    n=len([l for l in st.splitlines() if l.strip()])
    print(f"    {r['rel']:<20} {r['head'][:9]}  {'clean' if n==0 else str(n)+' changed'}")
PY
}

# ================================================================ usage/main ===
usage(){ cat >&2 <<EOF
${C_BLD}sandbox${C_0} — assemble a WHOLE Neuron stack into one combined worktree workspace,
wired to build together on an isolated clean base. Multi-repo sibling of ${C_BLD}nsbx${C_0}.

  ${C_CYN}sandbox el-stack     <name>${C_0} [--minimal]   elc + EL language + el-ui framework + tooling (+ SDK consumers)
  ${C_CYN}sandbox neuron-stack <name>${C_0} [--minimal]   runtime/soul + engram + app/UI  (the full product)
  ${C_CYN}sandbox build  <profile> <name>${C_0}           build the assembled stack together (runs its build.sh)
  ${C_CYN}sandbox status <profile> <name>${C_0}           inspect one workspace
  ${C_CYN}sandbox list${C_0}                               list assembled workspaces
  ${C_CYN}sandbox down   <profile> <name>${C_0} [--delete-branch]   tear down (remove worktrees; branch kept)

  Flags:  --minimal  only the required repos    --branch B  branch name    --base REF  fork point
  Env:    NSBX_STACK_ROOT (workspace root, default \$DEV_ROOT/stack-worktrees)   NEURON_DEV_ROOT

Each constituent repo becomes a git worktree at its natural relpath inside the
workspace, so cross-repo ../foundation/el imports resolve to the SANDBOX el. The
live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched; isolated-engram
bring-up is delegated to nsbx.
EOF
}

main(){
  local cmd="${1:-}"; shift || true
  case "$cmd" in
    el-stack|neuron-stack) cmd_up "$cmd" "$@";;
    up)        [ $# -ge 1 ] || die "usage: sandbox up <profile> <name>"; local p="$1"; shift; cmd_up "$p" "$@";;
    down)      [ $# -ge 2 ] || die "usage: sandbox down <profile> <name>"; cmd_down "$@";;
    build)     [ $# -ge 2 ] || die "usage: sandbox build <profile> <name>"; cmd_build "$@";;
    status)    [ $# -ge 2 ] || die "usage: sandbox status <profile> <name>"; cmd_status "$@";;
    list|ls)   cmd_list "$@";;
    ""|-h|--help|help) usage;;
    *) die "unknown command: $cmd (try: sandbox help)";;
  esac
}
main "$@"
