diff --git a/.claude/scripts/cockpit.test.sh b/.claude/scripts/cockpit.test.sh index f47ba88..d39a981 100755 --- a/.claude/scripts/cockpit.test.sh +++ b/.claude/scripts/cockpit.test.sh @@ -43,11 +43,23 @@ work="$(mktemp -d "${TMPDIR:-/tmp}/cockpit-test.XXXXXX")" # cockpit-serve.sh in the background -- declared here so the SAME EXIT trap # cleans it up no matter where in the script a later assertion fails. server_pid="" +# alias_npm_pid/alias_node_pid: same idea, for the `npm run cockpit` alias +# smoke (section 5b) -- npm wraps the real node server behind npm -> sh -> +# node, so cleanup needs to reach the actual node PID too, not just npm's. +alias_npm_pid="" +alias_node_pid="" cleanup() { if [ -n "$server_pid" ]; then kill "$server_pid" >/dev/null 2>&1 || true wait "$server_pid" 2>/dev/null || true fi + if [ -n "$alias_node_pid" ]; then + kill "$alias_node_pid" >/dev/null 2>&1 || true + fi + if [ -n "$alias_npm_pid" ]; then + kill "$alias_npm_pid" >/dev/null 2>&1 || true + wait "$alias_npm_pid" 2>/dev/null || true + fi rm -rf "$work" } trap cleanup EXIT @@ -430,6 +442,72 @@ tmp_after=0 for f in $tmp_glob; do [ -e "$f" ] && tmp_after=$((tmp_after + 1)); done check "cockpit-serve.sh does not leak its temp HTML file after exit (3a-followup fix)" [ "$tmp_after" -eq "$tmp_before" ] +# --------------------------------------------------------------------------- +# 5b. `pnpm cockpit` alias smoke (issue #90 review fix): exercises the ROOT +# package.json's `scripts.cockpit` entry itself -- not cockpit-serve.sh +# directly -- via `npm run cockpit`. npm (not pnpm) is used here +# deliberately: npm ships with node, so it needs no extra toolchain +# (corepack/pnpm) in CI, and it reads the SAME scripts.cockpit entry pnpm +# would resolve, so this validates the alias wiring end-to-end. Same +# offline --fixtures seam and 127.0.0.1-only contract as section 5's +# cockpit-serve.sh smoke, on a fresh free port, against the SAME +# fixtures dir used above. +# --------------------------------------------------------------------------- +# shellcheck source=resolve-roots.sh +. "$script_dir/resolve-roots.sh" + +check "root package.json has a cockpit script wired to cockpit-serve.sh" node -e ' + const p = require(process.argv[1]); + const s = p.scripts && p.scripts.cockpit; + if (typeof s !== "string" || s.indexOf("cockpit-serve.sh") === -1) { + console.error("scripts.cockpit =", s); + process.exit(1); + } +' "$root/package.json" + +alias_port="$(node -e ' + const s = require("net").createServer(); + s.listen(0, "127.0.0.1", () => { console.log(s.address().port); s.close(); }); +')" + +# --prefix (rather than a `cd`) so the script'"'"'s own relative path +# (.claude/scripts/cockpit-serve.sh) resolves against the repo root +# regardless of this test'"'"'s own cwd. +alias_log="$work/npm-run-cockpit.log" +: > "$alias_log" +npm --prefix "$root" run cockpit -- "$alias_port" --fixtures "$work/fixtures" >"$alias_log" 2>&1 & +alias_npm_pid=$! + +alias_ready=0 +for _ in $(seq 1 50); do + grep -q "cockpit serving" "$alias_log" 2>/dev/null && { alias_ready=1; break; } + kill -0 "$alias_npm_pid" 2>/dev/null || break # npm exited early -- stop polling + sleep 0.2 +done +check "npm run cockpit (the scripts.cockpit alias) prints a startup line within the readiness timeout" [ "$alias_ready" -eq 1 ] + +alias_resp="$(curl -s -o - -w '%{http_code}' "http://127.0.0.1:$alias_port/" 2>/dev/null)" +alias_code="${alias_resp: -3}" +alias_body="${alias_resp%???}" +check "npm run cockpit: GET / returns HTTP 200" [ "$alias_code" = "200" ] +check "npm run cockpit: GET / serves the dashboard (issues section present)" bash -c 'printf "%s" "$1" | grep -qF '"'"'
sh -> (bash exec) node, so +# killing only npm's own PID can leave the node process (and the listening +# port) orphaned. Find the actual PID bound to the port and kill it +# directly (lsof, falling back to ss if lsof isn't installed), then npm's +# own PID as a best-effort belt-and-braces cleanup. +if command -v lsof >/dev/null 2>&1; then + alias_node_pid="$(lsof -ti tcp:"$alias_port" 2>/dev/null | head -1)" +elif command -v ss >/dev/null 2>&1; then + alias_node_pid="$(ss -ltnp 2>/dev/null | grep ":$alias_port " | grep -oP 'pid=\K[0-9]+' | head -1)" +fi +[ -n "$alias_node_pid" ] && kill "$alias_node_pid" >/dev/null 2>&1 || true +kill "$alias_npm_pid" >/dev/null 2>&1 || true +wait "$alias_npm_pid" 2>/dev/null || true +alias_node_pid="" +alias_npm_pid="" + # --------------------------------------------------------------------------- # 6. Worker inspector endpoint (GET /api/worker//, issue #70): # event timeline + latest breadcrumbs + live worktree forensics, entirely diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ed8c4aa..d2e89b2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -63,6 +63,10 @@ worker worktrees. Run it, then open `.claude/state/cockpit.html`. No server, no script header for usage (`--fixtures`, `GATES_FILE` override) and `docs/COCKPIT_EVALUATION.md` for the design rationale. +For a live, auto-refreshing view, `.claude/scripts/cockpit-serve.sh` wraps the same renderer behind a small +HTTP server (127.0.0.1 only, node built-ins only). Run it via `pnpm cockpit` (default port 8090), or +`pnpm cockpit -- ` to override the port. + ### Concurrent config-write safety This template fans out to **parallel worktree-isolated workers**, and upstream [anthropics/claude-code#29217](https://github.com/anthropics/claude-code/issues/29217) reported that diff --git a/package.json b/package.json new file mode 100644 index 0000000..52f28dd --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "recode", + "version": "0.1.0", + "private": true, + "description": "Orchestrated multi-agent Claude Code harness — root package.json is a thin alias for the cockpit dashboard (no dependencies, node built-ins only).", + "scripts": { + "cockpit": "bash .claude/scripts/cockpit-serve.sh" + }, + "engines": { + "node": ">=20" + }, + "packageManager": "pnpm@10.34.4" +}