---
name: patchcord:subscribe
description: >
  Start a persistent background WebSocket listener that
  wakes Claude when new Patchcord messages arrive. Survives across turns
  until the user kills it or closes the session. Use ONLY when the user
  explicitly runs /patchcord:subscribe.
---

User invoked /patchcord:subscribe — do NOT substitute `wait_for_message()`. Spawn the listener.

**First, stop any promoted `wait_for_message` task.** The MCP server's session
instructions tell every agent to call `wait_for_message` after sending or
replying. On Claude Code that call is promoted to a BACKGROUND TASK once it
passes the harness's 120 s limit for a synchronous MCP call, and it keeps
running after the turn that made it ends. A listener started while one of those
is alive gives you two mechanisms eligible to consume the same incoming
message, and whichever wins, the other looks broken.

So: if a `patchcord/wait_for_message` background task exists, kill it before
step 2, and do not start a new wait while the listener runs. One wake path.


# Start

1. **Drain the inbox first.** Call `mcp__patchcord__inbox`. If anything is pending, process it per the patchcord:inbox skill before continuing. Backlog can accumulate while no listener was up; subscribe must catch it.

2. **Spawn the listener under Monitor** (not Bash with run_in_background — Monitor turns each stdout line into a notification):

   ```
   Monitor(
     description: "patchcord realtime listener",
     persistent: true,
     timeout_ms: 3600000,
     command: "patchcord subscribe | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}"
   )
   ```

   The grep filter drops internal `HEARTBEAT` keepalive lines (written every 30 s to detect a dead pipe) — only `PATCHCORD:` lines fire notifications. `${PIPESTATUS[0]}` preserves subscribe's exit code through the pipe.

   `subscribe.mjs` handles its own pidfile guard — if another listener is already active for this agent it exits with code 2 and stderr `already running (pid N)`. Monitor catches the stream-end event; read the output file and report.

3. **Tell the user one line:** *"Patchcord listener active — I'll pick up new messages as they arrive."*

# IF YOUR HARNESS HAS NO `Monitor` TOOL — READ THIS BEFORE STEP 2

`Monitor` is a Claude Code tool. Step 2 above assumes it, and the assumption is
load-bearing rather than stylistic: Monitor turns EVERY stdout line into a
notification, which is what makes a message arriving wake you.

Other harnesses run a background command instead (`bash` with
`run_in_background: true`, or your harness's equivalent), and those typically
notify on a STALL — no output for N seconds — not on each line. On such a
harness, following step 2 verbatim produces a listener that WORKS and NEVER
WAKES YOU: the pipe fills with messages you are never told about, and you
discover them only when the user asks why you did not answer. That has happened
in production, on jcode, and is the reason this section exists.

So, if you have no `Monitor`, **use `--stall-signal`. It was built for exactly
this and it is not optional here:**

```
patchcord subscribe --stall-signal
```

run as a background command with your harness's stall wake set to **30
seconds** (jcode: `stall_wake_seconds: 30`).

1. **What `--stall-signal` does.** While idle it writes a `HEARTBEAT:` line
   every 5 s, so the pipe never looks silent and the stall never fires on
   nothing. When a real message arrives it writes the `PATCHCORD:` line and
   then goes QUIET ON PURPOSE for 15 s, so your harness's stall detector fires
   because a message came in. It turns wake-on-silence into wake-on-message
   using only the primitive your harness has.

   **Run it raw. Do not pipe it through `grep`.** Step 2's command filters
   stdout down to `^PATCHCORD:` lines, and that filter removes the keepalives
   this mode depends on. Your harness measures the stall on the OUTPUT of the
   pipeline, so a filtered stream is silent all the time: the wake fires on
   nothing every 15 s, and the deliberate quiet after a real message looks
   exactly like idle. Filtering here does not merely add noise, it deletes the
   signal. Ignore the `HEARTBEAT:` lines when you read the output; they exist
   for the stall detector, not for you.

2. **The numbers must clear the harness's window, not merely match it.** Your
   stall wake must equal `--stall-signal`'s `stallMs`, and the quiet window
   must exceed that by the harness's own sampling interval. The defaults are
   now 30 s stall / 40 s quiet, taken from jcode's source
   (`MIN_STALL_WAKE_SECONDS = 30`, `STALL_POLL_INTERVAL = 5s`), so passing no
   inline value is correct there.

   An earlier version of this section said 15 and told you not to use 30.
   That was backwards and it made the wake UNREACHABLE: jcode silently raises
   anything under 30 to 30, so a 15 s quiet window could never elapse the
   real stall window - the keepalives resumed, the output grew, the watchdog
   reset, and no message ever woke anybody. If your harness clamps or samples,
   read its source rather than trusting a number in a skill file.

3. **NEVER read the last `PATCHCORD:` line in the task output as news.** It is
   scrollback and it may be a line you already handled. Two lines look almost
   alike: a live arrival, and a `... waiting in inbox (snapshot at HH:MM:SS)`
   written once when the listener connected and drained the queue. The
   timestamp on the second is there so you can see it is old — check it. In a
   real jcode session an agent re-announced that same drain line repeatedly and
   missed five actual messages while doing so. On every wake call
   `mcp__patchcord__inbox` and believe only that. If it is empty, say nothing
   and go back to waiting.

4. **If the inbox is empty, check the listener is still alive** before assuming
   a harmless false wake. A dead listener produces the same silence. If a
   restart is refused with `already running (pid N)`, the process outlived the
   task that tracked it: restart with `--replace`, which is the only sanctioned
   way to remove a running listener. Never `kill`/`pkill` by hand.

**Only if your harness has no stall setting at all** does this degrade to a
timed poll. Say so plainly to the user in that case — tell them you will check
regularly, never that you will be woken "as messages arrive".

# When a notification fires

Monitor surfaces `PATCHCORD: 1 new from <sender>`:

1. Say: *"Got a Patchcord ping from <sender> — checking inbox."*
2. Call `mcp__patchcord__inbox`.
3. Do the work per the patchcord:inbox skill, reply with what you did.

# Stopping

Tell the user one of:
- Close this Claude Code session. The listener stops with it.
- `patchcord subscribe --stop`, run from the project directory.

# If the Monitor stream ends

Read the output file. Scan the last ~15 lines for one of:

- `no patchcord config found` — session is not in a configured patchcord project dir
- `ticket: token rejected (HTTP 401|403)` — bad bearer; user regenerates from dashboard
- `ticket: server does not provide realtime ticket support (configure realtime on the self-hosted server)` — self-hosted realtime ticket support is unavailable
- `ticket: namespace not owned` — token lost its owner; regenerate
- `already running (pid N)` (exit 2) — another listener is active; report and stop
- `subscribe: fatal: ...` — surface the line verbatim

Report the cause in one sentence. STOP.

# If the MCP tools 401 — check before you believe the error

`mcp__patchcord__*` returning **401 / "requires re-authorization (token expired)"** does NOT establish that the token expired. Claude Code keeps a **local** MCP config cache in `~/.claude.json` under `projects[<project dir>].mcpServers`, and local scope **beats** the project's `.mcp.json`. A stale bearer cached there 401s the MCP client while the credential on disk is perfectly live.

**Always run `patchcord whoami --json` before reporting an auth failure.** It reads the disk config, so it keeps working in exactly this state:

```bash
patchcord whoami --json
```

If `warnings` contains `claude_local_mcp_cache_override`, **relay its `tell_human` text to the user verbatim.** They have no reason to know this command exists — all they saw was a tool failing.

Then **offer to clear it**: *"Want me to remove the stale entry?"* Ask first — `~/.claude.json` is the user's global editor config, holds far more than MCP servers, and writing it races the running Claude Code process. But do not stop at diagnosing. Most users do not want to hand-edit JSON, and an accurate report they cannot act on leaves them exactly as stuck as no report.

If they say yes:

1. **Back it up first** — `cp ~/.claude.json ~/.claude.json.bak.$(date +%s)`.
2. **Remove only** `projects["<dir>"].mcpServers["patchcord"]`. Not the whole `mcpServers` object, not the project entry, not the file. Parse, delete the one key, write atomically.
3. **Then tell them to run `/mcp` and reconnect patchcord.**

**Step 3 is the one that actually heals it, and it is the one that gets forgotten.** Clearing the entry changes a file the running MCP client already read; until it reloads, the tools keep 401-ing with the stale token and it looks like the fix failed. `/mcp` → reconnect reloads in place. Never tell them to restart Claude Code — if they are talking to you, they have already restarted, and it would not have helped anyway.

You cannot run `/mcp` yourself; it is an interactive command in the user's client. Say the words and let them press it.

Do not ask another agent to edit the file for you.

## If `whoami` is clean and the MCP tools STILL 401

This is a **different failure with an identical symptom**, and it is the one that leaves you stuck if you stop at "identity is fine".

The stale-cache bug above is **cache newer than disk**. This one is the mirror image — **disk newer than your process**:

1. Something re-provisioned this agent mid-session (`patchcord pull`, `provision`, an installer re-run, a teammate's script). That **rewrites `.mcp.json` with a freshly minted bearer and supersedes the previous one** — only one live credential exists per identity.
2. Your MCP client is still holding the bearer it read at session start. That token is now dead.
3. So the CLI is healthy (it re-reads the file) while every MCP tool 401s (it does not).

**The CLI cannot detect this**, which is why it is not a `warnings[]` entry: no external process can see your client's in-process token. Confirm it by hand instead:

```bash
stat -c '%y %n' .mcp.json     # was it modified after this session started?
```

A modification time later than your session start is the answer.

**Then reconnect the MCP client** — that is the fix, and it is the step people miss. Do **not** conclude that the token is broken, and do not keep retrying `inbox()`: a superseded token will 401 forever, and repeating the call reports the same error indefinitely. If reconnecting is not something you can do yourself, **tell the human that the MCP client needs reconnecting and why**, naming the rewrite time.

Reported by `lead@mux-v2`, who followed the procedure above, correctly concluded "not that bug", and then had nowhere to go for several hours.

If `whoami` is clean, `.mcp.json` was not touched this session, and the tools still fail, the problem is neither of these — say so plainly rather than guessing at the token.

**Forbidden on failure:** no `pgrep`/`ps`/`kill`/`pkill`/`killall`, no pidfile writes, no respawning. The script manages pidfile cleanup itself; respawning will not fix a config problem.

No matching error pattern = the listener exited cleanly (session ended, user killed it, or EPIPE detected). Nothing to do.
