feat(workbench): one-command launcher with fail-loud preflight + README quick-start

scripts/workbench — a single robust entrypoint so a fresh clone runs the
Workbench the FIRST time, not sometimes:
- doctor: fail-loud preflight (uv, node>=20, pnpm via corepack, curl) — each
  failure prints the exact fix, never a half-started UI
- setup: idempotent — uv venv + editable CORE install + pnpm frozen install,
  only when actually missing; re-runs instant
- up: health-gated start of the stdlib API (:8765 /health) + Vite UI (:5173),
  reuses already-healthy ports, invokes vite directly so Ctrl+C tears down
  both cleanly (TERM-then-KILL, EXIT trap); logs to /tmp/core-workbench-*.log
- pure-Python backend (numpy) — no Rust build required to run the Workbench
- API_PORT / UI_PORT overridable

Tested end-to-end: up in ~5s (API /health 200, UI 200), clean teardown
frees both ports. README gains a 'CORE Workbench — one command' quick-start
as the most-discoverable visual entry, with the first-time-works guarantees
spelled out for an external evaluator.
This commit is contained in:
Shay 2026-06-13 00:06:31 -07:00
parent 02f02c804a
commit 7044bdcd19
2 changed files with 219 additions and 0 deletions

View file

@ -81,6 +81,53 @@ pytest tests/test_versor_closure.py # the core invariant — must pass fi
pytest tests/ # full suite (~8,337 tests; some pre-existing reds — see docs/test-debt-quarantine.md)
```
### CORE Workbench — the visual operator console (one command)
The Workbench is the audit-native UI over the engine: every screen is
read-only evidence you can deep-link, replay, and verify — *determinism made
felt*, not animated cognition. Bring it up with a single command that runs
its own preflight, installs everything it needs, and only reports "up" once
both servers are actually healthy:
```bash
scripts/workbench # preflight → setup (idempotent) → start API + UI
```
It prints a single URL — open **http://127.0.0.1:5173** — and `Ctrl+C` stops
both servers cleanly. Two sub-commands help when you want to be deliberate:
```bash
scripts/workbench doctor # check tools only (uv, node ≥20, pnpm, curl) — changes nothing
scripts/workbench setup # check + install deps (venv, editable CORE, pnpm) — no servers
```
What it guarantees so it works the *first* time, not *sometimes*:
- **Pure-Python backend** — the API and the full `CognitiveTurnPipeline`
import on the numpy backend; **no Rust build is required** to run the
Workbench.
- **Fail-loud preflight** — a missing tool stops with the exact fix
(e.g. how to install `uv` or enable `pnpm` via corepack), never a
half-started UI.
- **Idempotent setup** — creates a `uv` venv, installs CORE editable, and
runs `pnpm install --frozen-lockfile` only when something is actually
missing; re-runs are instant.
- **Health-gated start** — waits for the API's `/health` and the UI port
before declaring success; surfaces logs (`/tmp/core-workbench-*.log`) if
either fails. Ports are configurable via `API_PORT` / `UI_PORT` and reused
if already healthy.
Prerequisites the preflight checks for you: [`uv`](https://docs.astral.sh/uv/),
Node ≥ 20, and `pnpm` (`corepack enable pnpm`). The UI is React 18 + Vite;
the API is a stdlib HTTP server. Both bind to localhost only.
Inside, eleven routes project one evidence model — Chat, **Trace** (the
canonical turn record), **Replay** (re-run a turn, hash-for-hash), Demos,
Proposals (human ratification), Evals (the `wrong=0` ledger), Runs, Packs,
Vault, Audit, Settings — stitched by the Evidence Chain Rail and addressable
by URL. See [`docs/workbench/`](docs/workbench/) for the design system,
route map, and the mastery roadmap.
### Watch the flywheel turn — one command
For a public-facing reproduction of the core thesis, in **four

172
scripts/workbench Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env bash
#
# CORE Workbench launcher — one command, env-checked, fail-loud.
#
# scripts/workbench # doctor + setup (idempotent) + run both servers
# scripts/workbench up # same as no-arg
# scripts/workbench doctor # preflight checks only, change nothing
# scripts/workbench setup # preflight + install deps, do not start servers
#
# Brings up the Python API (stdlib HTTP, :8765) and the Vite UI (:5173),
# waits until each is actually healthy, then prints the URL. Ctrl+C stops both.
# Override ports with API_PORT / UI_PORT.
#
# The backend runs on pure Python (numpy backend) — no Rust build required.
set -euo pipefail
API_PORT="${API_PORT:-8765}"
UI_PORT="${UI_PORT:-5173}"
API_HOST="127.0.0.1"
UI_HOST="127.0.0.1"
MIN_NODE_MAJOR=20
MIN_PY_MINOR=11 # requires-python >=3.11
# --- resolve repo root from this script's location (works from anywhere) -----
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
VENV="$ROOT/.venv"
UI_DIR="$ROOT/workbench-ui"
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
die() { printf ' \033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
# ---------------------------------------------------------------------------
# Preflight — every failure prints the exact fix.
# ---------------------------------------------------------------------------
doctor() {
bold "Preflight"
command -v uv >/dev/null 2>&1 \
|| die "uv not found. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh"
ok "uv $(uv --version 2>/dev/null | awk '{print $2}')"
command -v node >/dev/null 2>&1 \
|| die "node not found. Install Node >= ${MIN_NODE_MAJOR} (https://nodejs.org or nvm)."
local node_major
node_major="$(node -v | sed 's/^v//' | cut -d. -f1)"
[ "$node_major" -ge "$MIN_NODE_MAJOR" ] \
|| die "node $(node -v) is too old; need >= ${MIN_NODE_MAJOR}. Try: nvm install ${MIN_NODE_MAJOR}"
ok "node $(node -v)"
if ! command -v pnpm >/dev/null 2>&1; then
if command -v corepack >/dev/null 2>&1; then
warn "pnpm not on PATH — enabling via corepack (uses the pinned version)"
corepack enable pnpm >/dev/null 2>&1 || true
fi
command -v pnpm >/dev/null 2>&1 \
|| die "pnpm not found. Enable it: corepack enable pnpm (or: npm i -g pnpm)"
fi
ok "pnpm $(pnpm -v)"
command -v curl >/dev/null 2>&1 || die "curl not found (needed for health checks)."
ok "curl present"
}
# ---------------------------------------------------------------------------
# Setup — idempotent. Only does work when something is actually missing.
# ---------------------------------------------------------------------------
setup() {
bold "Setup"
if [ ! -x "$VENV/bin/python" ]; then
warn "creating Python venv (.venv) with uv"
uv venv "$VENV" >/dev/null
fi
# Confirm the venv Python meets the minimum.
local py_minor
py_minor="$("$VENV/bin/python" -c 'import sys; print(sys.version_info[1])')"
[ "$py_minor" -ge "$MIN_PY_MINOR" ] \
|| die "venv Python is 3.${py_minor}; need >= 3.${MIN_PY_MINOR}. Recreate: rm -rf .venv && uv venv --python 3.12"
ok "Python $("$VENV/bin/python" -c 'import platform; print(platform.python_version())')"
if [ ! -x "$VENV/bin/core" ] || ! "$VENV/bin/python" -c 'import workbench.server' >/dev/null 2>&1; then
warn "installing the CORE package into .venv (editable) — first run only, may take a minute"
(cd "$ROOT" && uv pip install --python "$VENV/bin/python" -e . >/dev/null)
fi
"$VENV/bin/python" -c 'import workbench.server, core.cognition.pipeline' \
|| die "workbench backend failed to import. Re-run: rm -rf .venv && scripts/workbench setup"
ok "CORE backend importable (pure Python, no Rust build needed)"
if [ ! -d "$UI_DIR/node_modules" ]; then
warn "installing UI dependencies (pnpm, frozen lockfile) — first run only"
(cd "$UI_DIR" && pnpm install --frozen-lockfile >/dev/null)
fi
ok "UI dependencies installed"
}
# ---------------------------------------------------------------------------
# Run — health-gated startup of both servers; Ctrl+C stops both.
# ---------------------------------------------------------------------------
port_responds() { curl -fsS "http://$1:$2/health" >/dev/null 2>&1; }
port_open() { curl -fsS -o /dev/null "http://$1:$2" 2>/dev/null; }
BACK_PID=""
FRONT_PID=""
cleanup() {
trap - INT TERM EXIT
for pid in "$FRONT_PID" "$BACK_PID"; do
[ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true
done
sleep 1
for pid in "$FRONT_PID" "$BACK_PID"; do
[ -n "$pid" ] && kill -KILL "$pid" 2>/dev/null || true
done
printf '\n'; bold "Workbench stopped."
}
run() {
bold "Start"
trap cleanup INT TERM EXIT
if port_responds "$API_HOST" "$API_PORT"; then
ok "API already healthy on :$API_PORT (reusing)"
else
"$VENV/bin/core" workbench api --host "$API_HOST" --port "$API_PORT" >/tmp/core-workbench-api.log 2>&1 &
BACK_PID=$!
printf ' … waiting for API on :%s' "$API_PORT"
for _ in $(seq 1 60); do
if port_responds "$API_HOST" "$API_PORT"; then printf '\r'; ok "API healthy on :$API_PORT "; break; fi
kill -0 "$BACK_PID" 2>/dev/null || die "API process exited. Logs: /tmp/core-workbench-api.log"
printf '.'; sleep 1
done
port_responds "$API_HOST" "$API_PORT" || die "API did not become healthy in 60s. Logs: /tmp/core-workbench-api.log"
fi
if port_open "$UI_HOST" "$UI_PORT"; then
ok "UI already serving on :$UI_PORT (reusing)"
else
# Invoke vite directly (not via `pnpm dev`) so FRONT_PID is the server
# itself — a pnpm wrapper would not forward the stop signal to vite.
(cd "$UI_DIR" && exec node_modules/.bin/vite --host "$UI_HOST" --port "$UI_PORT" --strictPort) >/tmp/core-workbench-ui.log 2>&1 &
FRONT_PID=$!
printf ' … waiting for UI on :%s' "$UI_PORT"
for _ in $(seq 1 60); do
if port_open "$UI_HOST" "$UI_PORT"; then printf '\r'; ok "UI serving on :$UI_PORT "; break; fi
kill -0 "$FRONT_PID" 2>/dev/null || die "UI process exited. Logs: /tmp/core-workbench-ui.log"
printf '.'; sleep 1
done
port_open "$UI_HOST" "$UI_PORT" || die "UI did not start in 60s. Logs: /tmp/core-workbench-ui.log"
fi
printf '\n'
bold "CORE Workbench is up:"
printf ' \033[1;36mhttp://%s:%s\033[0m (API: http://%s:%s)\n\n' "$UI_HOST" "$UI_PORT" "$API_HOST" "$API_PORT"
printf ' Press Ctrl+C to stop.\n'
# Stay in the foreground; if either child dies, surface it and exit.
while true; do
[ -n "$BACK_PID" ] && ! kill -0 "$BACK_PID" 2>/dev/null && die "API exited unexpectedly. Logs: /tmp/core-workbench-api.log"
[ -n "$FRONT_PID" ] && ! kill -0 "$FRONT_PID" 2>/dev/null && die "UI exited unexpectedly. Logs: /tmp/core-workbench-ui.log"
sleep 2
done
}
case "${1:-up}" in
doctor) doctor ;;
setup) doctor; setup ;;
up|"") doctor; setup; run ;;
*) die "unknown command '$1' (use: doctor | setup | up)" ;;
esac