diff --git a/README.md b/README.md index 17262ec4..61e131fa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/scripts/workbench b/scripts/workbench new file mode 100755 index 00000000..227b1672 --- /dev/null +++ b/scripts/workbench @@ -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