diff --git a/tools/neuron-sandbox/Makefile b/tools/neuron-sandbox/Makefile new file mode 100644 index 0000000..f03ef06 --- /dev/null +++ b/tools/neuron-sandbox/Makefile @@ -0,0 +1,64 @@ +# nsbx — Neuron dev-environment Makefile +# --------------------------------------------------------------------------- +# Thin, documented wrappers over the `nsbx` primitive so a newcomer never has to +# memorise the elc/cc build incantation or the sandbox lifecycle. Every target is +# a one-liner over `nsbx`; nothing here reimplements engram logic. +# +# make dev NAME=tim # new branch + worktree + isolated engram, one shot +# make status NAME=tim # inspect it (omit NAME to list all sandboxes) +# make run NAME=tim # poke its API (API=/api/stats by default) +# make test NAME=tim # run the safety rails as checks (nsbx validate) +# make build NAME=tim # compile the worktree's changes into the engram +# make destroy NAME=tim # tear it all down (branch kept) +# +# The isolated engram is ALWAYS a clone of the live store on a NON-default port; +# prod (:8742 / :7770) is untouchable from here. +# --------------------------------------------------------------------------- + +# locate nsbx next to this Makefile, regardless of where make is run from +NSBX := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))nsbx +SBX := dev-$(NAME) +API ?= /api/stats + +.DEFAULT_GOAL := help + +.PHONY: help dev build run test status list destroy bt + +help: ## Show this help + @echo "nsbx dev-environment — one-command isolated Neuron dev setup" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) \ + | awk 'BEGIN{FS=":.*?## "}{printf " make %-22s %s\n", $$1, $$2}' + @echo "" + @echo " Variables: NAME= (required for most) API= (run)" + @echo " BASE= WT= PORT= ARGS=" + +dev: ## Create branch + worktree + isolated engram (NAME=x [BASE=ref PORT=n WT=dir ARGS=...]) + @test -n "$(NAME)" || { echo "usage: make dev NAME="; exit 2; } + $(NSBX) dev $(NAME) $(if $(BASE),--base $(BASE)) $(if $(PORT),--port $(PORT)) $(if $(WT),--worktree $(WT)) $(ARGS) + +build: ## Rebuild the engram from the dev worktree's own source (NAME=x) + @test -n "$(NAME)" || { echo "usage: make build NAME="; exit 2; } + @wt=$$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.neuron/sandboxes/$(SBX)/dev.json')))['worktree'])" 2>/dev/null); \ + test -n "$$wt" || { echo "no dev.json for $(SBX) — run 'make dev NAME=$(NAME)' first"; exit 2; }; \ + $(NSBX) build $(SBX) --source "$$wt" + +bt: build run ## Fast El loop: rebuild the engram from the worktree, then poke it (NAME=x) + +run: ## Poke the isolated engram's API (NAME=x [API=/api/stats]) + @test -n "$(NAME)" || { echo "usage: make run NAME= [API=/path]"; exit 2; } + $(NSBX) run $(SBX) api $(API) + +test: ## Run the safety rails as checks: zero-loss, reboot, RSS, parity, keystones (NAME=x) + @test -n "$(NAME)" || { echo "usage: make test NAME="; exit 2; } + $(NSBX) validate $(SBX) + +status: ## Show one sandbox's status (NAME=x), or list all if NAME is unset + @if [ -n "$(NAME)" ]; then $(NSBX) status $(SBX); else $(NSBX) list; fi + +list: ## List all sandboxes + $(NSBX) list + +destroy: ## Tear down engram + worktree (NAME=x [ARGS=--delete-branch]) + @test -n "$(NAME)" || { echo "usage: make destroy NAME="; exit 2; } + $(NSBX) dev-down $(NAME) $(ARGS) diff --git a/tools/neuron-sandbox/README.md b/tools/neuron-sandbox/README.md index 7af9950..5dd776d 100644 --- a/tools/neuron-sandbox/README.md +++ b/tools/neuron-sandbox/README.md @@ -31,6 +31,64 @@ nsbx destroy # cheap teardown; live untouched That is the whole loop. Sane defaults: stock prod binary, auto-allocated port (`8900+`, never `8742`/`7770`), snapshot of the live store. +## One-command dev onboarding — `nsbx dev` (start here) + +Going from a clone to *coding on the mind* is a single command. It creates a git +**branch**, a persistent git **worktree**, and an **isolated engram** (a clone of the +live store on a non-default port) — and wires the whole worktree to that clone so you +**cannot hit live `:8742` by accident**. + +```bash +make dev NAME=tim # branch wt/tim + worktree + isolated engram, in one shot +cd ~/Development/neuron-technologies/el-worktrees/tim +source .nsbx-env # every ENGRAM_* var now points at YOUR clone + +# edit El in the worktree, then the fast loop: +make build NAME=tim # compile your El change into the isolated engram +make run NAME=tim # poke it (API=/api/stats by default) +make test NAME=tim # run the safety rails as checks +make destroy NAME=tim # tear it all down (branch kept; live untouched) +``` + +**Why this exists:** so provisional/experimental work is built **directly in El against a +throwaway cloned engram** — not prototyped in Python and re-ported later. The El +edit → `make build` → `make run` loop is the path of least resistance; that double-work is +what stranded the translation faculty for weeks. + +### What `nsbx dev ` does, in order + +1. **branch** — `git worktree add -b ` (default prefix `dev/`; a *real named + branch*, never detached HEAD). +2. **worktree** — at a **persistent** path (default `…/el-worktrees/`, override + `NSBX_DEV_WT_ROOT`). It **refuses** `/tmp` — temp dirs are ablated on compaction, which + is the exact "worktree in /tmp + no branch = lost work" failure this designs out. +3. **isolated engram** — `nsbx create` under the hood: clone of the live store + WAL + + config, booted on an auto-allocated port (`8900+`, never `:8742`/`:7770`). Stock prod + binary by default (instant); `--build` compiles the worktree's own runtime instead. +4. **env pin** — writes `.nsbx-env` (+ `.envrc` for direnv) into the worktree exporting + `ENGRAM_URL / ENGRAM_PORT / ENGRAM_DATA_DIR / ENGRAM_API_KEY / NEURON_ENGRAM_URL …` — all + pointing at the clone. Nothing references live. + +``` +nsbx dev [--base REF] [--worktree DIR] [--port N] [--prefix P] [--build] [--no-engram] [--repo R] +nsbx dev-down [--delete-branch] [--repo R] # destroy engram + remove worktree +``` + +### Makefile targets + +| target | does | +|--------|------| +| `make dev NAME=x` | branch + worktree + isolated engram (one shot) | +| `make bt NAME=x` | fast El loop: `build` then `run` | +| `make build NAME=x` | recompile the engram from the worktree's El source | +| `make run NAME=x` | poke the isolated engram (`API=/api/stats`) | +| `make test NAME=x` | rails as checks (`nsbx validate`) | +| `make status [NAME=x]` | inspect one, or `list` all | +| `make destroy NAME=x` | tear down (add `ARGS=--delete-branch` to drop the branch) | + +> Note: if a bare `dev` branch already exists in the repo, git can't create `dev/*` names — +> pass `--prefix wt/` (or delete the stray `dev` branch). The tool surfaces git's exact error. + ## The code-change dev loop (first-class) Run *your changed runtime*, not just the stock binary, against a snapshot: diff --git a/tools/neuron-sandbox/nsbx b/tools/neuron-sandbox/nsbx index 1ca095a..249780a 100755 --- a/tools/neuron-sandbox/nsbx +++ b/tools/neuron-sandbox/nsbx @@ -623,10 +623,181 @@ cmd_status(){ [ -f "$(sdir "$name")/validate.json" ] && { echo "--- last validation ---"; python3 -m json.tool "$(sdir "$name")/validate.json"; } } +# ================================================================ dev ========== +# ONE-COMMAND isolated dev environment. Everything a newcomer (Tim, any agent) +# needs to go from clone -> coding on an isolated running mind, in a single shot: +# 1) a git BRANCH (dev/, or --prefix) +# 2) a git WORKTREE for it, at a visible path they can open + edit +# 3) an ISOLATED engram bound to a NON-default port, on a clone of the live store +# (stock prod binary by default — instant + safe; --build to compile the +# worktree's own runtime instead). Prod :$LIVE_BIND_PORT/:$SOUL_PORT is untouchable. +# +# This is additive sugar over the proven primitives (git worktree + cmd_create). +# It never binds a forbidden port and never touches ~/.neuron/engram (the live store) +# except the same READ-only snapshot cmd_create already performs. +# +# nsbx dev [--repo R] [--base REF] [--worktree DIR] [--port N] +# [--prefix P] [--build] [--no-engram] +cmd_dev(){ + local name="" repo="$EL_REPO" base="" wt="" port="" prefix="dev/" build=0 no_engram=0 + [ $# -gt 0 ] && [ "${1#-}" = "$1" ] && { name="$1"; shift; } || die "usage: nsbx dev [flags]" + while [ $# -gt 0 ]; do case "$1" in + --repo) repo="$2"; shift 2;; + --base) base="$2"; shift 2;; + --worktree|--wt) wt="$2"; shift 2;; + --port) port="$2"; shift 2;; + --prefix) prefix="$2"; shift 2;; + --build) build=1; shift;; + --no-engram) no_engram=1; shift;; + *) die "unknown flag: $1";; + esac; done + need git + git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || die "not a git repo: $repo" + + local branch="${prefix}${name}" + local sbx="dev-${name}" + # default worktree path: a PERSISTENT, git-managed dir — NEVER /tmp (which is + # ablated on compaction). Default root = /el-worktrees, i.e. + # ~/Development/neuron-technologies/el-worktrees/. Override with NSBX_DEV_WT_ROOT. + local wt_root="${NSBX_DEV_WT_ROOT:-$(cd "$(dirname "$(dirname "$repo")")" && pwd -P)/el-worktrees}" + [ -n "$wt" ] || wt="${wt_root}/${name}" + case "$wt" in /tmp/*|/private/tmp/*|/var/tmp/*) + die "refusing worktree under a temp dir ($wt) — temp dirs are ablated on compaction; set NSBX_DEV_WT_ROOT to a persistent path";; + esac + # default base: whatever the repo's working checkout is on now + [ -n "$base" ] || base="$(git -C "$repo" rev-parse --abbrev-ref HEAD 2>/dev/null)" + + # pre-flight (fail before creating anything) + [ "$no_engram" -eq 1 ] || ! mexists "$sbx" || die "engram sandbox '$sbx' already exists (nsbx dev-down $name first)" + [ -e "$wt" ] && die "worktree path already exists: $wt" + if [ -n "$port" ]; then + { [ "$port" = "$LIVE_BIND_PORT" ] || [ "$port" = "$SOUL_PORT" ]; } && die "refusing forbidden port $port (live)" + fi + + log "dev env '$name' (branch=$branch worktree=$wt base=$base)" + + # ---- 1+2) branch + worktree in one shot ---- + local gerr + if git -C "$repo" show-ref --verify --quiet "refs/heads/$branch"; then + info "branch $branch exists — checking it out into a new worktree" + gerr="$(git -C "$repo" worktree add "$wt" "$branch" 2>&1)" \ + || die "git worktree add failed for existing branch $branch:"$'\n'" $gerr" + else + gerr="$(git -C "$repo" worktree add -b "$branch" "$wt" "$base" 2>&1)" \ + || die "git worktree add -b $branch (base $base) failed:"$'\n'" $gerr"$'\n'" (a bare 'dev' branch blocks 'dev/*' names — try --prefix, e.g. nsbx dev $name --prefix wt/)" + fi + ok "worktree ready: $wt (branch $branch)" + + # ---- 3) isolated engram ---- + local eport="(none)" + if [ "$no_engram" -eq 1 ]; then + warn "--no-engram: skipped standing up an engram" + else + if [ "$build" -eq 1 ]; then + log "isolated engram: building the worktree's own runtime" + cmd_create "$sbx" ${port:+--port "$port"} --source "$wt" || die "engram create (--build) failed" + else + log "isolated engram: stock prod binary on a clone of the live store" + cmd_create "$sbx" ${port:+--port "$port"} || die "engram create failed" + fi + eport="$(mget "$sbx" "['port']")" + # record the dev linkage next to the sandbox so dev-down can clean up + python3 - "$(sdir "$sbx")/dev.json" "$name" "$branch" "$wt" "$repo" "$eport" <<'PY' +import json,sys +p,name,branch,wt,repo,port=sys.argv[1:7] +json.dump({"name":name,"branch":branch,"worktree":wt,"repo":repo,"port":int(port)}, + open(p,'w'),indent=2) +PY + # ---- pin the WHOLE worktree to the CLONE ---- + # Every var any El tooling in this worktree might read for an engram target now + # points at the isolated clone. Sourcing .nsbx-env makes hitting live :$LIVE_BIND_PORT + # or ~/.neuron/engram by accident structurally impossible from this shell. + local edata ekey eurl ebin + edata="$(sdir "$sbx")/data"; ekey="sbx-$sbx"; eurl="http://127.0.0.1:$eport"; ebin="$(sdir "$sbx")/bin/engram" + cat > "$wt/.nsbx-env" < source .nsbx-env +export NSBX_NAME="$sbx" +export ENGRAM_URL="$eurl" +export ENGRAM_BIND=":$eport" +export ENGRAM_PORT="$eport" +export ENGRAM_HOST="127.0.0.1" +export ENGRAM_DATA_DIR="$edata" +export ENGRAM_API_KEY="$ekey" +export NEURON_ENGRAM_URL="$eurl" +export NEURON_ENGRAM_KEY="$ekey" +# nsbx run compatibility (same names 'nsbx run' exports) +export SBX_NAME="$sbx" SBX_PORT="$eport" SBX_URL="$eurl" SBX_KEY="$ekey" SBX_DATA="$edata" SBX_BIN="$ebin" +ENV + # direnv users get it automatically on cd; everyone else runs 'source .nsbx-env' + [ -e "$wt/.envrc" ] || printf 'source_env .nsbx-env 2>/dev/null || source .nsbx-env\n' > "$wt/.envrc" + ok "wrote $wt/.nsbx-env (pins this worktree to the clone)" + fi + + # ---- summary ---- + echo >&2 + printf '%s DEV ENV READY — %s%s\n' "$C_BLD" "$name" "$C_0" >&2 + printf ' %-10s %s\n' "worktree" "$wt" >&2 + printf ' %-10s %s\n' "branch" "$branch" >&2 + if [ "$no_engram" -ne 1 ]; then + printf ' %-10s %s\n' "engram" "http://127.0.0.1:$eport (isolated clone; prod :$LIVE_BIND_PORT untouchable)" >&2 + printf ' %-10s %s\n' "sandbox" "$sbx" >&2 + echo >&2 + printf '%s env exported into %s/.nsbx-env (source it -> pinned to the clone):%s\n' "$C_DIM" "$wt" "$C_0" >&2 + grep '^export' "$wt/.nsbx-env" | sed 's/^/ /' >&2 + fi + echo >&2 + info "code in: cd $wt && source .nsbx-env # now every engram var points at the clone" + [ "$no_engram" -ne 1 ] && info "poke it: nsbx run $sbx api /api/stats (or: make run NAME=$name)" + [ "$no_engram" -ne 1 ] && info "edit->test: make build NAME=$name && make run NAME=$name # El change -> clone, seconds" + info "tear down: nsbx dev-down $name (destroys engram + removes worktree; branch kept)" +} + +# nsbx dev-down [--delete-branch] [--repo R] +# Teardown counterpart: destroy the isolated engram, remove the git worktree, +# and (optionally) delete the branch. Live prod is never touched. +cmd_dev_down(){ + local name="" repo="$EL_REPO" del_branch=0 + [ $# -gt 0 ] && [ "${1#-}" = "$1" ] && { name="$1"; shift; } || die "usage: nsbx dev-down [--delete-branch]" + while [ $# -gt 0 ]; do case "$1" in + --repo) repo="$2"; shift 2;; + --delete-branch) del_branch=1; shift;; + *) die "unknown flag: $1";; + esac; done + local sbx="dev-${name}" wt="" branch="dev/${name}" + # recover worktree/branch/repo from the dev linkage if present + if mexists "$sbx" && [ -f "$(sdir "$sbx")/dev.json" ]; then + local dj; dj="$(sdir "$sbx")/dev.json" + wt="$(python3 -c "import json;print(json.load(open('$dj'))['worktree'])" 2>/dev/null)" + branch="$(python3 -c "import json;print(json.load(open('$dj'))['branch'])" 2>/dev/null)" + repo="$(python3 -c "import json;print(json.load(open('$dj'))['repo'])" 2>/dev/null)" + fi + # 1) engram + if mexists "$sbx"; then cmd_destroy "$sbx"; else info "no engram sandbox '$sbx'"; fi + # 2) worktree + if [ -n "$wt" ] && [ -d "$wt" ]; then + log "removing git worktree $wt" + git -C "$repo" worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt" + git -C "$repo" worktree prune 2>/dev/null || true + ok "worktree removed" + else info "no worktree to remove"; fi + # 3) branch (opt-in) + if [ "$del_branch" -eq 1 ]; then + git -C "$repo" branch -D "$branch" 2>/dev/null && ok "deleted branch $branch" || warn "could not delete branch $branch" + else info "branch $branch kept (use --delete-branch to remove)"; fi + ok "dev-down '$name' complete (live untouched)" +} + usage(){ cat >&2 < [--base REF] [--worktree DIR] ONE command onboarding: new branch (dev/) + git + [--port N] [--prefix P] [--build] worktree (persistent, never /tmp) + isolated engram on a + [--no-engram] [--repo R] non-default port. clone -> coding on the mind in one shot. + nsbx dev-down [--delete-branch] [--repo R] teardown: destroy the engram + remove the worktree + (branch kept unless --delete-branch). live untouched. nsbx up [name] [flags…] one command: your private, isolated copy of the mind (creates on first run, starts thereafter; prod untouchable) nsbx create [name] [--port N] [--source DIR | --branch REF [--repo R] | --binary PATH] @@ -647,6 +818,8 @@ EOF main(){ local cmd="${1:-}"; shift || true case "$cmd" in + dev) cmd_dev "$@";; + dev-down) cmd_dev_down "$@";; up) cmd_up "$@";; create) cmd_create "$@";; build) cmd_build "$@";;