Crux cockpit
A scientific-method lab notebook your AI agent drives — research as a tree of questions and falsifiable hypotheses in plain markdown, with an LLM literature wiki, a browser GUI cockpit, and autoresearch.
npx -y skills add mehdiforoozandeh/crux --skill crux-cockpitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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. Works wherever the agent runs: local machine, VS Code Remote-SSH (drives the port forward itself via `code --openExternal`), or a plain SSH terminal (hands the user the exact `ssh -L` tunnel command). 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.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.9 KB, as published. Nobody here has run it
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 / focus one question —
plus the review queue, node detail with rendered markdown, and a hypothesis's linked report
and figures; live-refreshing, zero writes). Either panel can go full-screen ([ / ],
Esc restores). 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 on the
machine you run on — the one thing you can't do for them is start a local ssh -L
tunnel (plain-SSH contexts only; see Local vs remote).
The engine rides in the crux skill: ENGINE = <crux skill>/scaffold/crux.py — the
crux skill is this skill's sibling directory in your skills dir (in a repo clone,
skills/crux). If that engine file is missing, the crux skill isn't installed: install
it first (npx skills add mehdiforoozandeh/crux --all, or the repo's ./install.sh)
before continuing.
Ground rules (load-bearing)
- Never report an unverified URL. Before telling the user the cockpit is up, both
/and/snapshot.jsonmust 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-openand deliver the URL as a clickable link. The user opens it when ready. One carve-out: in VS Code Remote-SSH the sanctioned way to create the port forward is an open call (code --openExternal— see the ladder below); there, opening the user's browser is the mechanism, not a discourtesy. - 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):
VAULT="$(cd <vault> && pwd -P)" # -P: physical path — a symlinked route must hash/match identically
VID="$(python3 -c 'import hashlib,sys;print(hashlib.sha256(sys.argv[1].encode()).hexdigest()[:12])' "$VAULT")"
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. kill is asynchronous: if you're about to relaunch on a pinned port,
first wait until lsof -nP -iTCP:<port> -sTCP:LISTEN returns nothing — a still-dying
listener makes the pinned relaunch fail with "cannot bind".)
(No lsof on the box? Minimal cluster images sometimes lack it — and lsof: command not found exits non-zero, which reads exactly like "no match / port free". Never let a
missing tool pass a check. Stdlib substitutes: a port is free iff
python3 -c 'import socket;socket.socket().bind(("127.0.0.1",<port>))' succeeds, and on
Linux a PID's cwd is readlink /proc/<pid>/cwd.)
4 — Launch, backgrounded, log captured. Run from inside the vault so the engine
resolves it; never block your shell on the server. Remote? Pin first — if the session
classifies as any remote per Local vs remote (SSH markers, or the tunnel-remote
probes: $CODESPACES / $REMOTE_CONTAINERS / /.dockerenv), derive $PORT from the
pin block below before launching, and append --port "$PORT" to this line:
LOG="${TMPDIR:-/tmp}/crux-cockpit-$VID.log"
cd "$VAULT" && nohup python3 "$ENGINE" serve --no-open >"$LOG" 2>&1 & # remote: + --port "$PORT"
(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.)
Running remotely? (any remote classification — SSH-marked or tunnel-based; see
Local vs remote below.) Pin the port with --port, and make the pin outlive your session — an auto-picked port
drifts on an always-fresh relaunch (8787 → 8789), silently breaking a tunnel or forward
the user already has open, and "remember it in conversation" dies with the session:
PIN="$HOME/.cache/crux-cockpit/$VID.port" # $VID from step 3
mkdir -p "${PIN%/*}"
PORT="$(cat "$PIN" 2>/dev/null)"
if [ -z "$PORT" ]; then
PORT=8787
while lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; do PORT=$((PORT+1)); done
echo "$PORT" >"$PIN"
fi
Launch with --port "$PORT". If a recorded pin is busy at launch even after the step-3
kill loop, a foreign process took it: re-pick, update the pin file, and tell the user
their tunnel/forward target changed. Truly local launches keep auto-pick — no pin file.
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:
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:
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"). First classify the context (next section): on a local machine the URL is the whole story; in any remote context your curl proved the server is up on the remote — the report must also carry whatever gets the user's local browser to it.
Local vs remote — where the browser lives
The server always binds 127.0.0.1 on the machine you're running on. Step 6 proves
it's serving there; whether the user's browser can reach it is a separate question.
Classify from your environment before reporting:
SSH markers = any of $SSH_CONNECTION, $SSH_CLIENT, $SSH_TTY. VS Code markers =
$TERM_PROGRAM = vscode or $VSCODE_IPC_HOOK_CLI set.
| your env | context | what reaches the user |
|---|---|---|
| none of the SSH markers set | local — your machine is their machine (after ruling out the tunnel-based remotes below) | the URL, as-is |
| SSH markers plus VS Code markers | VS Code Remote-SSH | same URL, once the port is forwarded — you drive the forward (below) |
| SSH markers, no VS Code markers | plain SSH terminal | nothing yet — they must open a tunnel; you hand them the command |
-
Local — report the URL; done.
-
VS Code Remote-SSH — create the forward yourself; never rely on auto-forwarding. It degrades silently, especially on shared hosts (HPC login nodes): once >20 ports have ever auto-forwarded, VS Code flips
remote.autoForwardPortsSourcetohybridand stops detecting; ports already listening when VS Code connected are never auto-forwarded; and your launch writes the banner to a log file, so the output-watcher never sees the URL. The ladder, in order:-
Drive it from the remote CLI (zero user action):
[ -n "$VSCODE_IPC_HOOK_CLI" ] && command -v code >/dev/null && code --openExternal "$URL"This asks the VS Code client to open the URL; the client creates the port forward itself and opens the user's local default browser — it works even with auto-forwarding disabled. (
"$BROWSER" "$URL"is the same mechanism — VS Code points$BROWSERat a helper that calls--openExternal.) Tell the user what happened: "your browser just opened the cockpit; the port is forwarded." You cannot confirm the forward from the remote side, so ask once — "did it open?". A failed guard counts the same as a "no". VS Code forks (Cursor, Windsurf, …) set the same env markers and ship the same remote CLI under their own binary name — ifcodeisn't on PATH, try the fork's name (cursor,windsurf) with the same--openExternalflag before leaving this rung. -
Cmd/Ctrl-click in the integrated terminal. First
echo "$URL"in the terminal so a terminal-rendered link exists — a localhost URL clicked in the terminal forwards on the fly (remote.forwardOnOpen, default on). A URL clicked in a chat/webview panel does NOT forward — it opens the local browser on an unforwarded port and fails. Say this explicitly; it is the classic trap. -
Manual forward: Ports panel (the tab next to TERMINAL — or Cmd/Ctrl+Shift+P → Ports: Focus on Ports View) → Forward a Port →
<port>. -
The filled-in
ssh -Lone-liner (SSH remotes only — skip this rung in tunnel-based remotes, which have no SSH endpoint; there, rung 3 is the last resort).
Move down a rung whenever the current rung's guard fails or the user says it didn't work — and deliver rungs 2–4 together in one message, not one at a time.
-
-
Plain SSH — the tunnel must be started from their local machine; you cannot create it from the remote side. Give them the exact command, filled in:
ssh -L <port>:localhost:<port> <user>@<host> # run on your LOCAL machine; keep it openthen the same
http://localhost:<port>works in their local browser. Fill<user>fromwhoamiand<host>fromhostname -f; if that's not the name they actually SSH to (common on clusters), field 3 of$SSH_CONNECTIONis the server IP — or simply ask "what do you type to connect here?". But when the typed name ≠ this machine, don't put it after-L. Two cluster cases: you're on a compute node behind a login node, or the typed name is a round-robin alias (cedar.computecanada.ca-style) that may land the tunnel on a different login node than the one running the server. Either way, jump through the typed name and pin the far end to this host:ssh -J <typed-name> -L <port>:localhost:<port> $(hostname -f). Two things to say out loud: run it locally, and keep that terminal open while browsing. (serve's own banner prints a VS Code-flavoured forwarding hint even here — your tunnel instructions supersede it; don't relay it.)
In any remote context, word the report so verified and reachable aren't conflated: "serving on the remote and verified there; it opens in your browser once the forward/tunnel is up."
Tunnel-based remotes: VS Code markers, no SSH markers. Not every remote arrives over
SSH — GitHub Codespaces, VS Code Tunnels, and Dev Containers set the VS Code markers but
no SSH_* at all, and localhost-as-is does not reach the user there. Rule them out
before calling a session local: $CODESPACES = true (Codespaces), $REMOTE_CONTAINERS
= true or /.dockerenv exists (a container; podman writes /run/.containerenv
instead). Any of those → use the VS Code ladder:
rung 1 (code --openExternal) works over tunnels exactly as over SSH; there is no
ssh -L rung, so the Ports panel is the true last resort. WSL ($WSL_DISTRO_NAME set)
is the benign case — WSL2 forwards localhost to Windows by default, so the URL usually
works as-is; if it doesn't, same ladder.
When the markers lie. tmux/screen can strip or preserve stale env ($SSH_* gone, or
a $VSCODE_IPC_HOOK_CLI left over from an earlier VS Code session); mosh sets no SSH vars
at all. Concrete probes when the table's answer smells wrong: $SSH_CLIENT often survives
where $SSH_CONNECTION didn't; $TMUX set means the env may predate the current
connection; who am i shows the connecting host for SSH logins. When still unsure, ask
one question — "are you SSH'd into this machine?" — and classify from the answer. And
because a stale VS Code marker misroutes a plain-SSH user, every SSH-remote report
includes the filled-in ssh -L one-liner as the last-resort fallback, even in VS Code
mode. (Tunnel-based remotes are the exception — no SSH endpoint exists, so omit it and
make the Ports panel the last resort there.)
Other editors and agents — nothing above may leak into the local flow. Everything remote-specific (pin files, the forwarding ladder, tunnel commands, opening a browser) is gated on the env markers. In their absence — Claude Code desktop, Codex, Cursor or Copilot on the user's own machine, any plain local terminal — the flow is exactly the local one: auto-picked port, no pin file, report the verified URL, never open a browser. VS Code forks over SSH (Cursor, Windsurf) set the same markers and carry the same remote-CLI mechanism under their own binary name; each ladder rung degrades to the next if a fork lacks a piece.
Status · stop · restart
- Status ("is the cockpit running?") — enumerate
pgrep -f 'crux\.py (serve|gui|ui|cockpit)'; for each PID report its vault (cwd vialsof -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:LISTENreturns 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: acrux: error:line means the vault didn't resolve (wrong dir) or a pinned--portwas busy. Fix and relaunch. - URL answers but
/snapshot.json500s → the vault is structurally broken; runpython3 "$ENGINE" validatein the vault and surface the findings. - Zombie from a crashed session holding a port → the step-3 kill loop clears it; on local (auto-pick) launches a survivor you shouldn't kill (another vault's server) never blocks a new launch. On pinned remote launches it does block — that's the step-4 re-pick rule: new port, update the pin file, tell the user the forward/tunnel target changed.