Skip to main content

MUXSPECT_JS

Constant MUXSPECT_JS 

Source
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.