const MUXSPECT_JS: &str = "#!/usr/bin/env node\n// Copyright 2026, AgentMux Corp.\n// SPDX-License-Identifier: Apache-2.0\n//\n// muxspect \u{2014} live introspection into the CURRENT running AgentMux instance\'s\n// process/turn state. Sibling to muxlog (history) \u{2014} muxspect answers \"what is\n// this instance doing right now,\" not \"what happened.\"\n//\n// Phase 1 of docs/specs/SPEC_MUXSPECT_LIVE_INTROSPECTION_TOOL_2026_08_01.md:\n// queries the instance the caller is ALREADY inside (via $AGENTMUX_LOCAL_URL /\n// $AGENTMUX_AUTH_KEY, inherited from the environment exactly the way\n// agentmux-mcp already does for every other /api/v1/* route \u{2014} no new IPC, no\n// new auth scheme). Cross-instance querying (`muxspect targets` / `-i <other>`)\n// is Phase 2, not implemented here.\n//\n// Deployed by agentmux-srv next to muxlog.mjs (~/.agentmux/shell/muxspect.mjs);\n// the bash/zsh/pwsh/fish `muxspect` functions delegate here. Run standalone\n// with `node muxspect.mjs ...` (works from any subshell, unlike the shell\n// function) \u{2014} but only from within a pane whose environment already carries\n// $AGENTMUX_LOCAL_URL/$AGENTMUX_AUTH_KEY (an agent pane, or any shell pane for\n// the URL \u{2014} the auth key specifically is only injected for agent-CLI-type\n// controllers today, see crates/srv/src/server/agent_handlers/input.rs).\n\nimport { pathToFileURL } from \"node:url\";\n\nimport { agentmuxFetch, readAgentmuxEnv } from \"./lib/muxclient.mjs\";\n\nconst USAGE = `muxspect \u{2014} live process/turn-state introspection for the current AgentMux instance\n\nUsage:\n muxspect same as \'muxspect list\'\n muxspect list [--json] summary of every controller-backed block\n muxspect describe <block_id> [--json]\n full detail for one block (process status,\n controller status, OS process tree)\n muxspect watch <block_id> poll \'describe\' every 2s until Ctrl+C\n muxspect dock <block_id> [--json]\n Activity Dock ToolNode status snapshot for\n one block \u{2014} diagnoses stuck \'running\'\n entries (SPEC_MUXSPECT_DOCK_DIAGNOSIS_\n AND_REMEDIATION_2026_08_06.md)\n muxspect dock clear <block_id> <node_id>\n force-clear one stuck dock entry, live, in\n whatever renderer currently has that block\n open \u{2014} no pane reload needed. muxspect\'s\n only mutating command; see the spec above.\n muxspect conversations [--json] every agent\'s most recent activity across\n host, cross-channel, LAN, and connected WAN\n in one glance \u{2014} host/cross-channel entries\n include a last-message preview; LAN/WAN\n are liveness-only for now\n (SPEC_MUXSPECT_CROSS_TIER_CONVERSATION_\n VISIBILITY_2026_08_21.md Phase A)\n muxspect conversation <agent> [--json]\n read the tail transcript of one agent by\n name \u{2014} resolves host first, then\n cross-channel (other AgentMux channels on\n this same host); does not reach LAN/WAN\n muxspect find <block_id_or_agent> [--json]\n which running instance(s), if any, have a\n controller/dispatch matching this block id\n or agent name \u{2014} checks this instance first\n (ANY controller type), then every other\n channel via the shared reactive registry\n (agent-registered blocks ONLY on the\n cross-channel tier \u{2014} a non-agent controller,\n e.g. a plain shell, in another channel isn\'t\n findable by block_id there). No forwarded\n call unless a channel actually matches. A\n UUID-shaped query is sent as block_id;\n anything else as agent name\n (SPEC_MUXSPECT_CROSS_INSTANCE_FIND_2026_08_22.md,\n Ext 4 of REPORT_MUXSPECT_MUXLOG_CROSS_CHANNEL_\n INSPECTION_2026_08_22.md)\n muxspect verify-sender <agent_name> [--json]\n is an agent named <agent_name> currently\n REGISTERED, and via what tier (spawner /\n host / cross-channel / lan / wan)?\n Registry-liveness only \u{2014} NOT cryptographic\n sender verification; does not replace\n checking a JEKT\'s own TRUST=/SIG= fields\n (SPEC_MUXSPECT_VERIFY_SENDER_2026_08_21.md).\n Exits non-zero unless the verdict is\n \'found\'.\n muxspect layout [tab_id] [--json]\n the PERSISTED layout tree for every tab (or\n one), as an indented outline: direction,\n size, minimized/magnified flags, block ids \u{2014}\n plus the layout doctor\'s verdict on each\n tree. Answers \"what does the layout actually\n look like right now\" without reading\n db_layout out of SQLite by hand. Note it\n reports what is PERSISTED, which can lag\n what the frontend is currently rendering.\n Exits non-zero if any tree has violations.\n muxspect work [state] [--json] the Muxqueue backlog \u{2014} the shared work queue\n any agent can add to and any agent can claim\n (REPORT_UNIVERSAL_AGENT_WORK_QUEUE_2026_09_01.md).\n Optional state filter: open | claimed | done |\n failed | cancelled. Shows who holds what, how\n many attempts each item has burned, and the\n result/reason recorded on it. Answers \"what is\n outstanding, and is anything stuck\" without\n claiming anything yourself \u{2014} this command is\n strictly read-only. Shows the 500\n most-recently-updated items and says so\n explicitly when there may be more, rather\n than presenting a truncated view as the\n whole backlog.\n muxspect migrations [--json] the migration doctor for THIS instance\'s\n data dir: every migration\'s applied/pending\n state and, for applied ones, the same\n post-condition verdict\n \'agentmux-srv migrate --verify\' gives\n (ok / MISMATCH / error / n/a) \u{2014} without\n stopping the app or hunting for the srv\n binary. A MISMATCH is \"recorded as applied\n but the data isn\'t there\", the failure the\n hardening spec was written about. Read-only.\n Exits 3 on any MISMATCH or error, matching\n the CLI (docs/incident/RUNBOOK_MIGRATION_\n RECOVERY_2026_09_07.md \u{a7}4).\n muxspect help this message\n\nRequires $AGENTMUX_LOCAL_URL and $AGENTMUX_AUTH_KEY in the environment.\n\nIMPORTANT (reagent P1 on PR #2380): the bare \'muxspect\' shell FUNCTION is\nonly defined in INTERACTIVE terminal panes (it\'s sourced from the shell\nrcfile), but those panes only get $AGENTMUX_LOCAL_URL, not\n$AGENTMUX_AUTH_KEY \u{2014} so the function loads there but auth fails. Agent-CLI\ntool calls (the primary intended use \u{2014} an agent inspecting its own running\ninstance) get BOTH env vars, but tool-spawned shells don\'t source the\nrcfile, so the \'muxspect\' function isn\'t defined there either. Until a\nPhase 2 fix wires this up properly, the ONLY invocation that reliably works\ntoday is calling the deployed core directly by path (same caveat muxlog\nitself documents for tool-spawned subshells):\n node ~/.agentmux/shell/muxspect.mjs list\nSee docs/specs/SPEC_MUXSPECT_LIVE_INTROSPECTION_TOOL_2026_08_01.md for the\nfull story and the planned fix.`;\n\n/// How many queue items `muxspect work` asks for. The server clamps to 500, so\n/// this is the practical ceiling. Threaded into renderWork rather than being\n/// compared against a second hardcoded literal there \u{2014} that duplication is how\n/// a truncation warning silently stops matching the request that caused it.\nconst WORK_LIST_LIMIT = 500;\n\nfunction fail(msg) {\n console.error(`muxspect: ${msg}`);\n process.exit(1);\n}\n\nfunction requireEnv() {\n const { url, authKey } = readAgentmuxEnv();\n if (!url || !authKey) {\n fail(\n \"$AGENTMUX_LOCAL_URL / $AGENTMUX_AUTH_KEY not set \u{2014} run this from inside an AgentMux agent pane.\\n\" +\n \"muxspect only queries the instance you\'re already running in; it can\'t discover or guess another one.\"\n );\n }\n return { url, authKey };\n}\n\n// Ext 5 of docs/reports/REPORT_MUXSPECT_MUXLOG_CROSS_CHANNEL_INSPECTION_2026_08_22.md:\n// this session hit `muxspect conversations` 404ing because the running srv\n// predated that route, with nothing saying so \u{2014} every response now carries\n// an `x-agentmux-srv-version` header (server/mod.rs\'s version_header\n// middleware) stamping the instance\'s own version. Printed to STDERR (not\n// stdout) on every call so it never pollutes piped/parsed `--json` output,\n// and folded into the error message specifically for a 404 \u{2014} the one status\n// a version mismatch is most likely to explain.\nexport function logSrvVersion(resp) {\n const v = resp.headers.get(\"x-agentmux-srv-version\");\n if (v) console.error(`[srv v${v}]`);\n return v;\n}\n\nasync function apiGet(url, authKey, urlPath) {\n let resp;\n try {\n ({ resp } = await agentmuxFetch(url, authKey, urlPath));\n } catch (e) {\n fail(`could not reach ${url} \u{2014} instance unreachable (${e.message})`);\n }\n const srvVersion = logSrvVersion(resp);\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n const versionHint =\n resp.status === 404 && srvVersion\n ? ` (this instance is running srv v${srvVersion} \u{2014} a 404 here may mean it predates this command; check you\'re not talking to a stale build)`\n : \"\";\n fail(`request failed (${resp.status}): ${body || resp.statusText}${versionHint}`);\n }\n return resp.json();\n}\n\n// `dock clear` is muxspect\'s first mutating call \u{2014} see\n// docs/specs/SPEC_MUXSPECT_DOCK_DIAGNOSIS_AND_REMEDIATION_2026_08_06.md \u{a7}2.\n// Same shape as apiGet (same auth header, same fail()-on-non-2xx contract,\n// including on the handler\'s 404 \"no such node\" response \u{2014} from the CLI\'s\n// perspective, \"the clear didn\'t happen\" is a failure worth a non-zero exit).\nasync function apiPost(url, authKey, urlPath, body) {\n let resp;\n try {\n ({ resp } = await agentmuxFetch(url, authKey, urlPath, { method: \"POST\", body }));\n } catch (e) {\n fail(`could not reach ${url} \u{2014} instance unreachable (${e.message})`);\n }\n const srvVersion = logSrvVersion(resp);\n if (!resp.ok) {\n const respBody = await resp.text().catch(() => \"\");\n const versionHint =\n resp.status === 404 && srvVersion\n ? ` (this instance is running srv v${srvVersion} \u{2014} a 404 here may mean it predates this command; check you\'re not talking to a stale build)`\n : \"\";\n fail(`request failed (${resp.status}): ${respBody || resp.statusText}${versionHint}`);\n }\n return resp.json();\n}\n\nfunction ageString(lastComputedMs) {\n if (!lastComputedMs) return \"unknown\";\n const ageMs = Date.now() - lastComputedMs;\n if (ageMs < 0) return \"0s\"; // clock skew \u{2014} never print a negative age\n if (ageMs < 1000) return `${ageMs}ms`;\n if (ageMs < 60_000) return `${Math.round(ageMs / 1000)}s`;\n return `${Math.round(ageMs / 60_000)}m`;\n}\n\nfunction renderList(data) {\n const blocks = data.blocks ?? [];\n if (blocks.length === 0) {\n console.log(\"(no controller-backed blocks in this instance)\");\n return;\n }\n // \"pane_type\" reflects static pane classification, NOT live turn\n // activity \u{2014} that\'s what `lifecycle` is for (`lifecycle_from` maps\n // turn_active=true straight to Lifecycle::Running; see\n // crates/srv/src/broker/process.rs). A column literally named \"turn\"\n // showing pane-type instead of turn state was reviewed as misleading\n // (codex P2 on PR #2380) \u{2014} `list`\'s ProcessStatus rows don\'t carry\n // BlockControllerRuntimeStatus::turn_active at all (that needs\n // `describe`), so this renames rather than fetching it at extra cost.\n //\n // Uses the server-computed `is_agent` field, NOT the raw\n // `is_agent_pane` flag \u{2014} subprocess/persistent/acp controllers can\n // report `is_agent_pane: false` even though those controller types are\n // always agents; reimplementing that classification rule in JS instead\n // of reusing ProcessStatus::is_agent() mislabeled exactly those three\n // types as \"term\" (codex P2 on PR #2380, second round).\n // \"last_error\" \u{2014} the last thing that happened to this block was an\n // unrecovered spawn/execution error (muxspect_handlers.rs\'s\n // last_error_frame). Distinct from lifecycle/confidence (liveness): a\n // block can be `lifecycle: unknown` for perfectly healthy reasons\n // (idle, never opened) \u{2014} this column is what disambiguates \"idle\"\n // from \"wedged, fix available\" at a glance, which is the whole point\n // (docs/reports/REPORT_MUXSPECT_SPAWN_REFUSAL_DIAGNOSIS_EXTENSION_2026_08_03.md \u{a7}3.2).\n const rows = blocks.map((b) => ({\n block_id: b.block_id,\n type: b.controller_type || \"?\",\n lifecycle: b.lifecycle,\n pane_type: b.is_agent ? \"agent\" : \"term\",\n confidence: b.liveness_confidence,\n age: ageString(b.last_computed_ms),\n last_error: b.last_error ? `yes (${ageString(b.last_error.written_ms)})` : \"-\",\n }));\n const cols = [\"block_id\", \"type\", \"lifecycle\", \"pane_type\", \"confidence\", \"age\", \"last_error\"];\n const widths = Object.fromEntries(\n cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => String(r[c]).length))])\n );\n const line = (r) => cols.map((c) => String(r[c]).padEnd(widths[c])).join(\" \");\n console.log(line(Object.fromEntries(cols.map((c) => [c, c]))));\n for (const r of rows) console.log(line(r));\n console.log(`\\n(as of query time \u{2014} each row\'s own \'age\' shows how stale its computed status is)`);\n}\n\nfunction renderDescribe(data) {\n const ps = data.process_status ?? {};\n const cs = data.controller_status;\n console.log(`block_id: ${data.block_id}`);\n console.log(`lifecycle: ${ps.lifecycle ?? \"unknown\"} (computed ${ageString(ps.last_computed_ms)} ago)`);\n console.log(`controller_type: ${ps.controller_type || \"(no controller)\"}`);\n console.log(`is_agent: ${data.is_agent ?? false} (raw is_agent_pane: ${ps.is_agent_pane ?? false})`);\n console.log(`liveness_confidence: ${ps.liveness_confidence ?? \"none\"}`);\n console.log(`tracking_confidence: ${data.tracking_confidence}`);\n if (cs) {\n console.log(`\\ncontroller status:`);\n console.log(` shellprocstatus: ${cs.shellprocstatus || \"(none)\"}`);\n console.log(` shellprocexitcode: ${cs.shellprocexitcode}`);\n console.log(` turn_active: ${cs.turn_active ?? false}`);\n console.log(` spawn_ts_ms: ${cs.spawn_ts_ms ?? \"(never spawned)\"}`);\n } else {\n console.log(`\\ncontroller status: (no controller for this block_id)`);\n }\n const processes = data.processes ?? [];\n console.log(`\\nprocesses (${processes.length}):`);\n if (processes.length === 0) {\n console.log(` (none tracked)`);\n } else {\n for (const p of processes) {\n console.log(` pid=${p.pid} rss=${Math.round(p.rss_bytes / 1024)}KB ${p.command || \"(unknown command)\"}`);\n }\n }\n // The actual \"why\" for a block with no live controller/process \u{2014} see\n // muxspect_handlers.rs\'s last_error_frame. Printed last, since it\'s\n // usually the answer someone\'s here for precisely when everything\n // above it is empty.\n if (data.last_error) {\n console.log(`\\nlast_error:`);\n console.log(` message: ${data.last_error.message}`);\n console.log(` source: ${data.last_error.source}`);\n console.log(` age: ${ageString(data.last_error.written_ms)}`);\n } else {\n console.log(`\\nlast_error: (none)`);\n }\n}\n\n// Like ageString, but for a duration already expressed in ms (the `dock`\n// endpoint returns `age_ms` directly, not a timestamp to diff against\n// Date.now() \u{2014} using ageString here would silently compute a bogus value).\nfunction msToAge(ms) {\n if (ms < 1000) return `${ms}ms`;\n if (ms < 60_000) return `${Math.round(ms / 1000)}s`;\n return `${Math.round(ms / 60_000)}m`;\n}\n\n/**\n * `verify-sender` tier 0 \u{2014} zero round-trip, checked before any network\n * call. A `task dev` instance\'s own `AGENTMUX_CHANNEL` (e.g.\n * \"dev-agenta-background-task-dashboard-intelligence-6c345e93dbc777e1\")\n * and `AGENTMUX_RUNTIME_MODE` (\"dev:agenta-background-task-dashboard-\n * intelligence\") encode which dev-build/worktree slug this instance was\n * launched from.\n *\n * IMPORTANT \u{2014} this is a NAMING-CONVENTION HEURISTIC, not an attested\n * identity: `AGENTMUX_CHANNEL`/`AGENTMUX_RUNTIME_MODE` are derived from\n * whatever branch/slug name was used to create the `task dev` instance,\n * not from any launcher-signed or otherwise authenticated \"spawned by\"\n * record. A dev branch coincidentally or deliberately named e.g.\n * `agenta-unrelated-work` would make a claimed sender named `AgentA`\n * match here even if AgentA never spawned this instance (codex review on\n * PR #2702, P1). Treat a `tier: \"spawner\"` result as a coordination hint\n * for the common case (a task-dev instance checking the agent whose\n * worktree it\'s obviously running from), NOT as proof \u{2014} same caveat as\n * every other tier this command reports (see [`checkSpawnerTier`]\'s\n * caller for why this command has no `trust`/`verified` field at all). A\n * real fix would need the launcher to inject an authenticated\n * `AGENTMUX_SPAWNED_BY` value at instance-creation time rather than this\n * inferring it from a name string; out of scope here \u{2014} see\n * docs/specs/SPEC_MUXSPECT_VERIFY_SENDER_2026_08_21.md\'s non-goals.\n *\n * Returns `null` (fall through to the network tiers) when neither env var\n * is set, or set but doesn\'t match `name`. Exported (pure) for\n * muxspect.test.mjs.\n */\nexport function checkSpawnerTier(name, env) {\n const needle = name.toLowerCase();\n const channel = env.AGENTMUX_CHANNEL || \"\";\n const runtimeMode = env.AGENTMUX_RUNTIME_MODE || \"\";\n const channelMatch = channel.toLowerCase().match(/^dev-([a-z0-9]+)-/);\n const runtimeMatch = runtimeMode.toLowerCase().match(/^dev:([a-z0-9]+)-/);\n const spawner = channelMatch?.[1] ?? runtimeMatch?.[1];\n if (!spawner || spawner !== needle) return null;\n return { name, status: \"found\", tier: \"spawner\", channel: channel || undefined };\n}\n\n// `verify-sender` reports REGISTRY LIVENESS only \u{2014} does an agent named X\n// currently exist in the discovery data. It performs NO cryptographic\n// check and is NOT the JEKT protocol\'s own sender-authentication\n// mechanism, which is per-message, automatic, and already computes a real\n// `TRUST=`/`SIG=` value on every delivered jekt (HMAC-SHA256 host tier /\n// Ed25519 LAN tier / pinned Ed25519 reagent WAN \u{2014} see this repo\'s root\n// CLAUDE.md, \"Is a jekt\'s sender identity actually verified?\"). A\n// `status: \"found\"` result here means \"an agent by this name is\n// currently registered\" \u{2014} it does NOT mean a specific JEKT claiming\n// FROM=X was actually sent by X. Deliberately no `trust`/`verified` field\n// in the output for exactly this reason (reagentx-workflow review on PR\n// #2702, P1 \u{2014} an earlier version reused `host-verified`/`network-claimed`,\n// which collided with that real, stronger, cryptographic guarantee).\nfunction renderVerifySender(data) {\n console.log(`sender: ${data.name}`);\n console.log(`status: ${data.status}`);\n if (data.tier) console.log(`tier: ${data.tier}`);\n if (data.last_seen_ms !== undefined && data.last_seen_ms !== null) {\n console.log(`last_seen: ${ageString(data.last_seen_ms)} ago`);\n }\n if (data.channel) console.log(`channel: ${data.channel}`);\n if (data.local_url) console.log(`local_url: ${data.local_url}`);\n if (data.status === \"not_found\") {\n console.log(`\\nno agent named \'${data.name}\' found on any tier (spawner, host, cross-channel, lan, wan).`);\n } else if (data.status === \"stale\") {\n console.log(`\\nfound but heartbeat is stale.`);\n }\n console.log(`\\nNote: this is a registry-liveness check, not cryptographic sender verification.`);\n console.log(`Check the JEKT\'s own TRUST=/SIG= fields for that (see CLAUDE.md\'s JEKT section).`);\n}\n\n// `conversations` \u{2014} see SPEC_MUXSPECT_CROSS_TIER_CONVERSATION_VISIBILITY_\n// 2026_08_21.md Phase A. host/cross-channel rows carry a preview + turn\n// state; lan/wan rows are liveness-only (no remote-read protocol yet for\n// those tiers) \u{2014} rendered with a \"(remote \u{2014} use \'conversation <agent>\' once\n// supported)\" placeholder rather than blank cells, so the gap is obvious\n// rather than looking like missing data.\nfunction renderConversations(data) {\n const agents = data.agents ?? [];\n if (agents.length === 0) {\n console.log(\"(no agents found on any tier)\");\n return;\n }\n const rows = agents.map((a) => ({\n name: a.name,\n tier: a.tier,\n turn: a.turn_active === true ? \"active\" : a.turn_active === false ? \"idle\" : \"?\",\n activity: a.last_activity_ms ? `${ageString(a.last_activity_ms)} ago` : \"-\",\n preview: a.remote_fetch_required\n ? \"(remote \u{2014} use \'conversation <agent>\' once supported)\"\n : (a.last_message_preview ?? \"(no output yet)\"),\n }));\n const cols = [\"name\", \"tier\", \"turn\", \"activity\"];\n const widths = Object.fromEntries(\n cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => String(r[c]).length))])\n );\n const line = (r) => cols.map((c) => String(r[c]).padEnd(widths[c])).join(\" \");\n console.log(line(Object.fromEntries(cols.map((c) => [c, c]))));\n for (const r of rows) {\n console.log(line(r));\n console.log(` ${r.preview}`);\n }\n}\n\n// `conversation <agent>` \u{2014} reuses the same `/agentmux/reactive/transcript`\n// response `GetAgentTranscript` returns, just rendered for a human. `tier`\n// is present as of Phase A (host | cross-channel); older srv builds\n// predating this change won\'t send it \u{2014} printed only when present.\nfunction renderConversation(data) {\n console.log(`agent: ${data.agent}`);\n if (data.tier) console.log(`tier: ${data.tier}`);\n if (data.channel) console.log(`channel: ${data.channel}`);\n console.log(`turn_active: ${data.turn_active ?? false}`);\n const lines = data.lines ?? [];\n console.log(`\\n--- transcript tail (${lines.length}${data.truncated ? \", truncated\" : \"\"}) ---`);\n for (const l of lines) console.log(l);\n}\n\n// `find <block_id_or_agent>` \u{2014} Ext 4 of REPORT_MUXSPECT_MUXLOG_CROSS_CHANNEL_\n// INSPECTION_2026_08_22.md / SPEC_MUXSPECT_CROSS_INSTANCE_FIND_2026_08_22.md.\n// `results` is empty when nothing matched anywhere \u{2014} a legitimate \"not\n// found on this host, in any known channel\" answer, not an error.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n// `found: false` (cross-channel only) means the shared registry pointed at\n// a channel, but the forwarded describe\'s own lifecycle came back \"unknown\"\n// \u{2014} the remote ProcessBroker has no controller for this block_id at all,\n// i.e. the registry entry was fresh enough to survive the staleness filter\n// srv-side but the block itself is already gone. Shown, not hidden \u{2014} but\n// not counted toward \"genuinely found\" for the summary line or exit code.\nfunction renderFind(data) {\n const { block_id, agent } = data.query ?? {};\n console.log(`query: ${block_id ? `block_id=${block_id}` : `agent=${agent}`}`);\n const results = data.results ?? [];\n const live = results.filter((r) => r.found !== false);\n if (results.length === 0) {\n console.log(\"\\nnot found on this host, in any known channel.\");\n return;\n }\n if (live.length === 0) {\n console.log(`\\nnot found \u{2014} ${results.length} registry match${results.length === 1 ? \"\" : \"es\"} pointed at a channel, but the block is already gone there.`);\n } else {\n console.log(`\\nfound in ${live.length} place${live.length === 1 ? \"\" : \"s\"}:`);\n }\n for (const r of results) {\n const staleNote = r.found === false ? \" \u{2014} gone (registry match, but remote lifecycle is \'unknown\')\" : \"\";\n console.log(`\\n--- ${r.tier}${r.channel ? ` (${r.channel})` : \"\"}${staleNote} ---`);\n console.log(` block_id: ${r.block_id}`);\n if (r.agent_id) console.log(` agent_id: ${r.agent_id}`);\n if (r.tier === \"host\" && r.process_status) {\n console.log(` lifecycle: ${r.process_status.lifecycle ?? \"unknown\"}`);\n console.log(` controller_type: ${r.process_status.controller_type ?? \"(none)\"}`);\n }\n if (r.tier === \"cross-channel\") {\n if (r.describe) {\n const ps = r.describe.process_status ?? {};\n console.log(` lifecycle: ${ps.lifecycle ?? \"unknown\"}`);\n console.log(` controller_type: ${ps.controller_type ?? \"(none)\"}`);\n } else {\n console.log(` (matched in the shared registry, but the forwarded describe call didn\'t respond in time \u{2014} channel may be busy or gone)`);\n }\n }\n }\n}\n\nfunction renderDock(data) {\n console.log(`block_id: ${data.block_id}`);\n const nodes = data.nodes ?? [];\n if (nodes.length === 0) {\n console.log(\"(no tracked dock nodes for this block)\");\n return;\n }\n const rows = nodes.map((n) => ({\n node_id: n.node_id,\n tool: n.tool_name,\n status: n.status,\n age: msToAge(n.age_ms),\n stuck: n.stuck ? \"STUCK?\" : \"\",\n bg: n.run_in_background ? \"bg\" : \"\",\n }));\n const cols = [\"node_id\", \"tool\", \"status\", \"age\", \"stuck\", \"bg\"];\n const widths = Object.fromEntries(\n cols.map((c) => [c, Math.max(c.length, ...rows.map((r) => String(r[c]).length))])\n );\n const line = (r) => cols.map((c) => String(r[c]).padEnd(widths[c])).join(\" \");\n console.log(line(Object.fromEntries(cols.map((c) => [c, c]))));\n for (const r of rows) console.log(line(r));\n if (rows.some((r) => r.stuck)) {\n console.log(`\\nSTUCK? nodes: \'running\' past the promotion threshold with nothing srv-side backing this block.`);\n console.log(`Clear one with: muxspect dock clear ${data.block_id} <node_id>`);\n }\n // issue #2518: a \'bg\' row\'s status is the RAW ToolNode status, terminal\n // (\"success\") within ~a second \u{2014} it can NEVER show STUCK? here even if\n // the real dock row has been showing \'running\' for hours, because the\n // srv has no visibility into whether this node\'s <task-notification>\n // ever arrived (that reclassification happens entirely client-side).\n // A long-`age` \'bg\' row with status=success is worth checking by hand\n // in the actual UI even though this table reads clean.\n if (rows.some((r) => r.bg)) {\n console.log(`\\nbg rows: backgrounded launches \u{2014} STUCK? never applies to these (server can\'t see the dock\'s own <task-notification> tracking). Check the live UI by hand if one looks old.`);\n }\n}\n\n/**\n * Render `muxspect work` \u{2014} the Muxqueue backlog.\n *\n * Exported (pure apart from console output) for muxspect.test.mjs, same as\n * renderLayout below, so the failure paths are tested rather than asserted.\n */\n// NOTE on error handling, and a pattern that does NOT apply here (reagent P2 on\n// PR #2903): `renderLayout` below carries an explicit `if (data.error)` branch\n// because ITS handler deliberately returns 200-with-{error} for a store\n// failure. `/agentmux/work` does not \u{2014} `handle_work_list` returns HTTP 500 with\n// {\"error\": ...}, which `apiGet` already turns into\n// `fail(\"request failed (500): {...}\")` and a non-zero exit, printing the\n// server\'s own message.\n//\n// An earlier revision of this function copied that branch anyway. It was\n// unreachable via the real endpoint, and it had a unit test asserting a\n// response shape production never produces \u{2014} which is worse than having no\n// branch at all, because it looks like the failure mode is covered. Removed\n// rather than made reachable: the 500 path is already correct and informative.\nexport function renderWork(data, stateFilter, limit) {\n const items = data.items ?? [];\n if (!items.length) {\n // Distinguish \"nothing at all\" from \"nothing MATCHING\", so a filtered\n // query never reads as an empty queue.\n console.log(stateFilter ? `no ${stateFilter} items` : \"queue is empty\");\n return;\n }\n\n const now = Date.now();\n const isExpired = (it) =>\n it.state === \"claimed\" && it.claim_expires && it.claim_expires <= now;\n // An expired lease does NOT always mean \"returns to the pool\". The reaper\n // parks an item whose attempts are already spent as `failed` instead of\n // reopening it, so promising a comeback for those is exactly the kind of\n // false reassurance this command exists to prevent (Codex P2 on PR #2903 \u{2014}\n // the same over-promise made in WorkRelease\'s acknowledgement on #2902,\n // repeated here in the renderer).\n const isDoomed = (it) => isExpired(it) && (it.attempts ?? 0) >= (it.max_attempts ?? 0);\n\n console.log(`${items.length} item(s)${stateFilter ? ` (state=${stateFilter})` : \"\"}\\n`);\n for (const it of items) {\n const holder = it.claimed_by ? ` held-by=${it.claimed_by}` : \"\";\n const attempts = `${it.attempts ?? 0}/${it.max_attempts ?? 0}`;\n // A claimed row whose lease is already in the past is the interesting\n // pathology: nobody is working it, and it stays invisible to `list`\n // until the next claim reaps it. Call that out rather than making the\n // reader compare timestamps by eye.\n let expired = \"\";\n if (isDoomed(it)) {\n expired = \" LEASE EXPIRED, ATTEMPTS SPENT (will be parked as failed, NOT reoffered)\";\n } else if (isExpired(it)) {\n expired = \" LEASE EXPIRED (returns to the pool on the next claim)\";\n }\n console.log(`${it.id} ${it.state}${holder} attempts=${attempts}${expired}`);\n console.log(` ${it.title}`);\n if (it.kind) console.log(` kind=${it.kind}`);\n if (it.target_agent) console.log(` target-agent=${it.target_agent}`);\n if (it.target_group) console.log(` target-group=${it.target_group}`);\n if (it.not_before && it.not_before > now) {\n console.log(` deferred until ${new Date(it.not_before).toISOString()}`);\n }\n // The completion trace for a done item, the reason for a\n // failed/released one \u{2014} the field WorkComplete calls the only record\n // that the work happened.\n if (it.result) console.log(` -> ${it.result}`);\n console.log(\"\");\n }\n\n const reclaimable = items.filter((it) => isExpired(it) && !isDoomed(it)).length;\n const doomed = items.filter(isDoomed).length;\n if (reclaimable) {\n console.log(\n `${reclaimable} item(s) hold an EXPIRED lease \u{2014} their claimant is gone. ` +\n `They return to the pool the next time any agent calls WorkClaim.`,\n );\n }\n if (doomed) {\n console.log(\n `${doomed} item(s) hold an EXPIRED lease AND have spent every attempt \u{2014} ` +\n `the next reap parks them as failed. They will NOT be offered again; ` +\n `re-enqueue if the work still matters.`,\n );\n }\n\n // The caller asked for `limit` items and got exactly that many, so there\n // are probably more (Codex P2 on PR #2903). Silence here would make a\n // truncated view look like the complete backlog \u{2014} the specific failure this\n // command exists to avoid, since `work` is meant to answer \"what is\n // outstanding\" and older open items sort last under `updated_at DESC`.\n if (limit && items.length >= limit) {\n console.log(\n `\\nShowing the ${items.length} most recently updated item(s) \u{2014} there may be MORE. ` +\n `Older open work and older expired claims sort last and are cut off here. ` +\n `Narrow with a state filter (muxspect work open) or use --json against the ` +\n `API directly for a full sweep.`,\n );\n }\n}\n\n/**\n * Render `muxspect migrations` \u{2014} the migration doctor report\n * (`GET /api/v1/muxspect/migrations`, the same producer as\n * `agentmux-srv migrate --verify`). One line per migration in registry\n * order; a MISMATCH or error is shouted, not left for the reader to spot in\n * a column, because that row IS the answer to \"why is my data missing\".\n *\n * Exported for muxspect.test.mjs. Pure apart from console output.\n */\nexport function renderMigrations(data) {\n const rows = data.migrations ?? [];\n const s = data.summary ?? {};\n console.log(`data dir: ${data.data_dir ?? \"?\"}`);\n console.log(`${rows.length} migration(s) \u{2014} ${s.applied ?? 0} applied, ${s.pending ?? 0} pending` +\n (s.unknown ? `, ${s.unknown} UNKNOWN (tracking store unreadable)` : \"\") + \"\\n\");\n for (const m of rows) {\n // A verdict only exists for applied migrations; pending rows have\n // nothing to verify yet and say so by omission.\n let verdict = \"\";\n if (m.verify === \"MISMATCH\") verdict = \" MISMATCH \u{2014} recorded applied, effect is NOT there\";\n else if (m.verify === \"error\") verdict = \" ERROR \u{2014} check could not run\";\n else if (m.verify) verdict = ` verify=${m.verify}`;\n console.log(`${m.id} [${m.scope}] ${m.state}${verdict}`);\n if (m.detail) console.log(` ${m.detail}`);\n }\n for (const err of data.tracking_errors ?? []) {\n console.log(`\\ntracking state unreadable: ${err}`);\n }\n console.log(\n `\\nverify: ${s.checked ?? 0} applied checked \u{2014} ${s.ok ?? 0} ok, ${s.mismatch ?? 0} mismatch, ` +\n `${s.error ?? 0} error, ${s.not_verifiable ?? 0} not verifiable`,\n );\n if ((s.mismatch ?? 0) > 0 || (s.error ?? 0) > 0) {\n console.log(\n \"At least one applied migration failed its post-condition. This is the \" +\n \"\\\"marker says done, data isn\'t there\\\" shape \u{2014} see \" +\n \"docs/incident/RUNBOOK_MIGRATION_RECOVERY_2026_09_07.md \u{a7}4-\u{a7}5 before touching anything. \" +\n \"Nothing here has been changed; this command is read-only.\",\n );\n }\n}\n\n/**\n * Render `muxspect layout` \u{2014} the persisted pane tree per tab as an indented\n * outline, plus the layout doctor\'s verdict.\n *\n * Exported (pure apart from console output) for muxspect.test.mjs, so the\n * failure paths are testable rather than asserted.\n */\nexport function renderLayout(data) {\n // A whole-request failure (the store couldn\'t be read at all) comes back\n // as 200 + {error} with NO `layouts` key \u{2014} see handle_muxspect_layout for\n // why it isn\'t a 4xx. Without this branch it fell through to \"no layouts\n // found\" and exit 0, reporting success for exactly the on-disk failure\n // this command exists to surface (reagent P1 on PR #2856).\n if (data.error) {\n console.error(`layout: ${data.error}`);\n return;\n }\n const layouts = data.layouts ?? [];\n if (!layouts.length) {\n console.log(\"no layouts found\");\n return;\n }\n for (const l of layouts) {\n if (l.error) {\n console.log(`tab ${l.tab_name ?? l.tab_id} \\u2716 ${l.error}`);\n continue;\n }\n const verdict = l.healthy ? \"healthy\" : `${l.violations.length} violation(s)`;\n console.log(\n `tab ${l.tab_name ?? l.tab_id} (${l.leaf_count} pane(s), ` +\n `${l.minimized_leaf_count} minimized) ${verdict}`\n );\n for (const v of l.violations ?? []) console.log(` ! ${v}`);\n for (const n of l.nodes ?? []) {\n const indent = \" \".repeat(n.depth + 1);\n const flags = [];\n if (n.locked) flags.push(\"minimized\");\n // Only meaningful on a branch \u{2014} a leaf\'s own locked flag already\n // says it, and repeating it there would be noise.\n if (n.kind === \"branch\" && n.effectively_minimized) flags.push(\"all-minimized\");\n if (n.id === l.magnified_node_id) flags.push(\"magnified\");\n const tail = flags.length ? ` [${flags.join(\", \")}]` : \"\";\n const label =\n n.kind === \"leaf\"\n ? `leaf ${n.block_id ? n.block_id.slice(0, 8) : \"(no block)\"}`\n : `${n.flex_direction} (${n.child_count})`;\n console.log(`${indent}${label} size=${n.size}${tail}`);\n }\n console.log(\"\");\n }\n}\n\n/**\n * Split argv into `{ cmd, sub, blockId, nodeId, json, help }`,\n * flags-and-positionals separated regardless of ordering. Flags can\n * legally appear before OR after the positional args (both \'muxspect\n * describe --json <id>\' and \'muxspect --json describe <id>\' are natural to\n * type) \u{2014} filtering them out first, then reading command/args\n * positionally, is what makes both orders work; indexing into the raw,\n * flag-interspersed argv (the original implementation) silently ran\n * \'list\' instead of \'describe\' \u{2014} or picked up a flag string as the\n * block_id \u{2014} depending on where the flag landed (reagent P2 on PR #2380).\n *\n * `dock clear <block_id> <node_id>` doesn\'t fit the flat `{cmd, blockId}`\n * shape the other subcommands use (three positionals after the flag\n * filter, not two) \u{2014} `sub` disambiguates `dock <block_id>` (read) from\n * `dock clear <block_id> <node_id>` (write) so `main()` doesn\'t have to\n * re-parse positional[1] itself.\n *\n * Exported (pure, no I/O) for muxspect.test.mjs.\n */\nexport function parseArgs(argv) {\n const json = argv.includes(\"--json\");\n const help = argv.includes(\"--help\") || argv.includes(\"-h\");\n const positional = argv.filter((a) => !a.startsWith(\"-\"));\n const cmd = positional[0] ?? \"list\";\n let sub, blockId, nodeId;\n if (cmd === \"dock\" && positional[1] === \"clear\") {\n sub = \"clear\";\n blockId = positional[2];\n nodeId = positional[3];\n } else {\n blockId = positional[1];\n }\n return { cmd, sub, blockId, nodeId, json, help };\n}\n\nasync function main() {\n const { cmd, sub, blockId, nodeId, json, help } = parseArgs(process.argv.slice(2));\n\n if (cmd === \"help\" || help) {\n console.log(USAGE);\n return;\n }\n\n // Handled before the unconditional requireEnv() below \u{2014} tier 0\n // (checkSpawnerTier) is a pure env-var check that shouldn\'t need\n // $AGENTMUX_LOCAL_URL/$AGENTMUX_AUTH_KEY at all; only the fallback to\n // the srv-backed tiers (1-4) needs them.\n if (cmd === \"verify-sender\") {\n const senderName = blockId;\n if (!senderName) fail(\"verify-sender requires an agent name \u{2014} \'muxspect verify-sender <agent_name>\'\");\n const spawnerVerdict = checkSpawnerTier(senderName, process.env);\n const data = spawnerVerdict ?? (await (async () => {\n const { url, authKey } = requireEnv();\n return apiGet(url, authKey, `/api/v1/muxspect/verify-sender?name=${encodeURIComponent(senderName)}`);\n })());\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderVerifySender(data);\n if (data.status !== \"found\") process.exitCode = 1;\n return;\n }\n\n const { url, authKey } = requireEnv();\n\n if (cmd === \"list\") {\n const data = await apiGet(url, authKey, \"/api/v1/muxspect/list\");\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderList(data);\n return;\n }\n\n if (cmd === \"work\") {\n // The sole positional arg is an optional state filter. Validated here\n // rather than passed through blindly: a typo like `muxspect work opne`\n // would otherwise return an empty list that reads exactly like \"the\n // queue is empty\", which is the wrong answer to a different question.\n const stateFilter = blockId ?? \"\";\n const VALID = [\"open\", \"claimed\", \"done\", \"failed\", \"cancelled\"];\n if (stateFilter && !VALID.includes(stateFilter)) {\n fail(`unknown state \'${stateFilter}\' \u{2014} expected one of: ${VALID.join(\", \")}`);\n }\n const data = await apiGet(\n url,\n authKey,\n `/agentmux/work?state=${encodeURIComponent(stateFilter)}&limit=${WORK_LIST_LIMIT}`,\n );\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderWork(data, stateFilter, WORK_LIST_LIMIT);\n return;\n }\n\n if (cmd === \"migrations\") {\n const data = await apiGet(url, authKey, \"/api/v1/muxspect/migrations\");\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderMigrations(data);\n // Mirror `migrate --verify`\'s exit code so scripts can gate on it the\n // same way whether they ran the CLI or this.\n if (data.exit_code) process.exitCode = data.exit_code;\n return;\n }\n\n if (cmd === \"conversations\") {\n const data = await apiGet(url, authKey, \"/api/v1/muxspect/conversations\");\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderConversations(data);\n return;\n }\n\n if (cmd === \"conversation\") {\n const agentName = blockId;\n if (!agentName) fail(\"conversation requires an agent name \u{2014} \'muxspect conversation <agent>\'\");\n const data = await apiGet(url, authKey, `/agentmux/reactive/transcript?agent=${encodeURIComponent(agentName)}`);\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderConversation(data);\n return;\n }\n\n if (cmd === \"find\") {\n const query = blockId; // parseArgs puts the sole positional arg here\n if (!query) fail(\"find requires a block_id or agent name \u{2014} \'muxspect find <block_id_or_agent>\'\");\n const isBlockId = UUID_RE.test(query);\n const params = isBlockId ? `block_id=${encodeURIComponent(query)}` : `agent=${encodeURIComponent(query)}`;\n const data = await apiGet(url, authKey, `/api/v1/muxspect/find?${params}`);\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderFind(data);\n // A cross-channel result with found:false (registry match, but the\n // remote lifecycle reads \"unknown\" \u{2014} the block is already gone\n // there) doesn\'t count as a genuine find for exit-code purposes.\n const genuinelyFound = (data.results ?? []).some((r) => r.found !== false);\n if (!genuinelyFound) process.exitCode = 1;\n return;\n }\n\n if (cmd === \"dock\" && sub === \"clear\") {\n if (!blockId || !nodeId) fail(\"dock clear requires a block_id and node_id \u{2014} \'muxspect dock clear <block_id> <node_id>\'\");\n const data = await apiPost(url, authKey, \"/api/v1/muxspect/dock/clear\", { block_id: blockId, node_id: nodeId });\n if (json) console.log(JSON.stringify(data, null, 2));\n else console.log(`cleared: node ${nodeId} in block ${blockId}`);\n return;\n }\n\n if (cmd === \"dock\") {\n if (!blockId) fail(\"dock requires a block_id \u{2014} \'muxspect dock <block_id>\'\");\n const data = await apiGet(url, authKey, `/api/v1/muxspect/dock?block_id=${encodeURIComponent(blockId)}`);\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderDock(data);\n return;\n }\n\n if (cmd === \"layout\") {\n // `blockId` is muxspect\'s generic first positional; for this command\n // it is an optional tab id.\n const qs = blockId ? `?tab_id=${encodeURIComponent(blockId)}` : \"\";\n const data = await apiGet(url, authKey, `/api/v1/muxspect/layout${qs}`);\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderLayout(data);\n // A tree with violations is a real finding \u{2014} exit non-zero so a\n // scripted caller notices without parsing stdout. `data.error` is the\n // whole-request failure (no `layouts` key at all); without it the\n // command exited 0 on a store-read failure.\n if (data.error || (data.layouts ?? []).some((l) => l.violations?.length || l.error)) {\n process.exitCode = 1;\n }\n return;\n }\n\n if (cmd === \"describe\") {\n if (!blockId) fail(\"describe requires a block_id \u{2014} \'muxspect describe <block_id>\'\");\n const data = await apiGet(url, authKey, `/api/v1/muxspect/describe?block_id=${encodeURIComponent(blockId)}`);\n if (json) console.log(JSON.stringify(data, null, 2));\n else renderDescribe(data);\n return;\n }\n\n if (cmd === \"watch\") {\n if (!blockId) fail(\"watch requires a block_id \u{2014} \'muxspect watch <block_id>\'\");\n console.log(`watching ${blockId} \u{2014} Ctrl+C to stop\\n`);\n for (;;) {\n const data = await apiGet(url, authKey, `/api/v1/muxspect/describe?block_id=${encodeURIComponent(blockId)}`);\n console.log(`--- ${new Date().toISOString()} ---`);\n renderDescribe(data);\n console.log(\"\");\n await new Promise((r) => setTimeout(r, 2000));\n }\n }\n\n fail(`unknown command \'${cmd}\' \u{2014} \'muxspect help\' for usage`);\n}\n\n// Only run when executed directly (`node muxspect.mjs ...`) \u{2014} importing this\n// module (muxspect.test.mjs imports `parseArgs`) must not trigger a live\n// network call / `process.exit`.\nif (import.meta.url === pathToFileURL(process.argv[1] ?? \"\").href) {\n main().catch((e) => fail(e?.message ?? String(e)));\n}\n";Expand description
Shared muxspect core (Node) — muxlog’s live-state sibling. Deployed once
at <shell>/muxspect.mjs; every shell’s muxspect function delegates to
it. See docs/specs/SPEC_MUXSPECT_LIVE_INTROSPECTION_TOOL_2026_08_01.md.