crux-cockpit · v1.0 · 2026-07-11 · sha256 041eb504e5c5b00e
crux-cockpit v1.0A
Immutable. This exact content is served forever at /api/v1/blob/041eb504e5c5b00e.
---
name: crux-cockpit
description: >-
Launch and manage the crux cockpit — the read-only browser GUI over a crux research
vault (`crux serve`). This skill runs it beginning to finish: locate the vault, start
the server fresh on localhost, verify it actually serves, and hand the user one
clickable URL — plus status / stop / restart, and a setup-or-demo path when no vault
exists yet. Use when a crux user wants to see their vault in a browser. Triggers:
"open the cockpit", "launch the crux GUI", "show me the tree in a browser", "serve
the vault", "is the cockpit running", "stop the cockpit", "restart the cockpit",
crux gui, crux cockpit.
license: MIT
metadata:
author: Mehdi Foroozandeh
version: "1.0"
requires: "the crux skill (drives its engine at skills/crux/scaffold/)"
notice: "Playbook only — operates the bundled crux engine's `serve` verb; no engine changes, no third-party code."
---
# crux-cockpit — launch the crux GUI, beginning to finish
The **cockpit** is crux's read-only browser GUI: `crux serve` boots a stdlib HTTP server
on `127.0.0.1`, serves the `webui/` frontend, and exposes the vault as `/snapshot.json`
(status-colored tree — pan / zoom / collapse / search / re-orient — plus the review queue
and node detail; live-refreshing, zero writes). The engine does the serving; **you (the
agent) do the operating**: find the vault, launch clean, verify, hand over one URL, and
manage the server's lifecycle. The user should never have to touch a terminal for any of it.
The engine rides in the **crux** skill: `ENGINE = <crux skill>/scaffold/crux.py`.
## Ground rules (load-bearing)
- **Never report an unverified URL.** Before telling the user the cockpit is up, both `/`
and `/snapshot.json` must have answered 200 from *your* curl. A URL you haven't verified
does not leave your mouth.
- **URL only — never pop a browser.** Always launch with `--no-open` and deliver the URL
as a clickable link. The user opens it when ready.
- **Always fresh, per vault.** A launch first kills any cockpit already serving *that
vault* — and never touches cockpits serving *other* vaults (several can coexist on
different ports).
- **The cockpit is read-only.** No route writes; every mutation stays in the agent/CLI.
Never present it as an editor.
- **Surface engine-drift warnings.** If the launch log warns that the vault was stamped by
a different engine version, relay that warning to the user verbatim.
## Launch protocol
**1 — Locate the vault.** The nearest directory at-or-above cwd containing `.crux.yaml`.
If the user named a vault, use that. If more than one vault is plausible in the project,
ask which — don't guess.
**2 — No vault anywhere?** Offer two paths and let the user pick:
- **Set up a real vault** — hand off to the **crux** skill's setup interview (seed outline
→ approve → `init --from`), then come back here.
- **A disposable demo vault** — so they can explore the GUI in seconds:
`python3 <crux skill>/scaffold/selftest.py --keep <tmpdir>/crux-demo-vault`
(build it *outside* their repo, e.g. under `$TMPDIR`, and say clearly that it's
throwaway sample data).
**3 — Kill stale servers for this vault** (fresh-start rule, scoped by process cwd):
```bash
VAULT="$(cd <vault> && pwd)"
for pid in $(pgrep -f 'crux\.py (serve|gui|ui|cockpit)'); do
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p')"
case "$cwd" in "$VAULT"|"$VAULT"/*) kill "$pid" ;; esac
done
```
(The pgrep pattern can also match a wrapper shell — e.g. a `zsh -c … crux.py serve …`
parent. Its cwd is the same vault, so the loop kills it too; that's correct, it dies
with its child.)
**4 — Launch, backgrounded, log captured.** Run from inside the vault so the engine
resolves it; never block your shell on the server:
```bash
LOG="${TMPDIR:-/tmp}/crux-cockpit-$(basename "$VAULT").log"
cd "$VAULT" && nohup python3 "$ENGINE" serve --no-open >"$LOG" 2>&1 &
```
(If your harness has a native run-in-background facility, prefer it — same command, same
captured log. And if shell state doesn't persist between your commands, re-derive
`$VAULT`/`$LOG`/`$URL` in each call rather than assuming the variables survive.)
**5 — Parse the URL from the banner.** The server prints exactly one line carrying it
(`crux cockpit (read-only) → http://localhost:<port>`; port auto-picked from 8787, or pin
one with `--port`). Poll the log rather than racing it:
```bash
for i in 1 2 3 4 5 6 7 8 9 10; do
URL="$(grep -o 'http://localhost:[0-9]*' "$LOG" | head -1)"
[ -n "$URL" ] && break; sleep 0.5
done
```
(No URL after ~5s → the server died on startup; see **Failure modes**.)
**6 — Verify before reporting.** Both routes, from your own shell:
```bash
curl -fsS "$URL/" >/dev/null
curl -fsS "$URL/snapshot.json" | python3 -c \
'import json,sys; d=json.load(sys.stdin); print(d["project"]["title"], "·", len(d["nodes"]), "nodes")'
```
If either fails: read `$LOG`, diagnose, fix, re-verify. Do **not** hand over the URL on a
failed check.
**7 — Report.** One message: the clickable URL, which vault it serves (project title +
node count from step 6), that it's read-only, and how to end it ("say *stop the cockpit*
when you're done"). If the banner carried a VS Code / Remote-SSH port-forwarding hint
(it appears automatically in those contexts), relay it.
## Status · stop · restart
- **Status** ("is the cockpit running?") — enumerate `pgrep -f 'crux\.py (serve|gui|ui|cockpit)'`;
for each PID report its vault (cwd via `lsof -a -p <pid> -d cwd -Fn`) and port
(`lsof -a -p <pid> -iTCP -sTCP:LISTEN -P`), and confirm the URL still answers before
calling it alive. Count only PIDs that actually hold a LISTEN port — a wrapper shell can
match the pattern while listening on nothing. No servers → say so plainly.
- **Stop** ("stop/close the cockpit") — kill that vault's PID(s) (step-3 loop), then confirm:
process gone (`kill -0 <pid>` fails) and port freed
(`lsof -nP -iTCP:<port> -sTCP:LISTEN` returns nothing). Report it stopped. If servers for
several vaults are running and the user didn't say which, ask.
- **Restart** — just run the launch protocol; the fresh-start rule *is* the restart.
## Failure modes worth knowing
- **Banner never appears / process exits immediately** → read `$LOG`: a `crux: error:` line
means the vault didn't resolve (wrong dir) or a pinned `--port` was busy. Fix and relaunch.
- **URL answers but `/snapshot.json` 500s** → the vault is structurally broken; run
`python3 "$ENGINE" validate` in the vault and surface the findings.
- **Zombie from a crashed session** holding a port → the step-3 kill loop clears it; ports
are auto-picked, so a survivor you *shouldn't* kill (another vault's server) never blocks
a new launch.