@mu-crew/mu 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ # Agent state from murmur (mu 2.0.0, murmur 1.0.0)
2
+
3
+ Status: approved design, 2026-09-28. Task `crew/mu_status_from_murmur`.
4
+
5
+ ## Problem
6
+
7
+ mu works out what an agent is doing by reading the last 20 lines of its
8
+ pane (`src/detect.ts`), and stores the answer in `agents.status`. This fails
9
+ in three ways:
10
+
11
+ - **Remote workers.** The local pane holds an ssh client that renders a
12
+ nested tmux. `skills/mu/REMOTE_WORKERS.md` records wrong answers in both
13
+ directions in one session: a stall fired at 300 s on a worker that was
14
+ mid-turn, and `mu agent list` showed `needs_input` for a worker that was
15
+ working.
16
+ - **Wrappers.** Each pi wrapper with different chrome has needed its own
17
+ patch (the Braille-spinner fallback).
18
+ - **Stale copies.** The stored status is only as recent as the last
19
+ reconcile, and `mu agent show` needed a fix to re-detect before it printed.
20
+
21
+ murmur's pi extension reports state from inside the agent on its own host,
22
+ and it publishes the state on each pane (`@murmur_pane_state`) and across
23
+ machines (`murmur status --json`). mu already exports the variables that
24
+ murmur uses to recognise mu's workers (`MU_MANAGED_AGENT`, `MU_AGENT_NAME`,
25
+ `MU_WORKSTREAM`).
26
+
27
+ ## Decision
28
+
29
+ **murmur is the answer for agent state.** mu owns the work: the DAG, task
30
+ owners, workspaces and panes. murmur owns what an agent is doing. mu does
31
+ not read pane text to decide agent state, and it does not store agent state.
32
+
33
+ Sources of runtime state, in order:
34
+
35
+ 1. herdr's `paneStatus()` on the herdr backend. This is herdr's own report,
36
+ not scraping.
37
+ 2. On tmux, murmur. The local read is the pane's `@murmur_pane_state` and
38
+ `@murmur_pane_since`. The remote read is `murmur status --json`, matched
39
+ on `agent_name` and `workstream`.
40
+ 3. Otherwise `unknown`, and mu says why.
41
+
42
+ This follows ZEN rules 2 (report state, don't scrape it), 3 (absence means
43
+ absence) and 5 (standalone tools that compose). mu's core needs none of this
44
+ to work: the DAG, `claim`/`close`, `task wait`, workspaces, spawn, send and
45
+ read.
46
+
47
+ ## Scope
48
+
49
+ In:
50
+
51
+ - A state resolver module that replaces every state read from
52
+ `detectPiStatus`:
53
+ - reconcile, including `mu agent list` and `mu state`
54
+ - `mu agent show`
55
+ - `mu agent wait`
56
+ - stall detection in `mu task wait`
57
+ - spawn readiness
58
+ - `computeAgentIdle`
59
+ - the TUI cards and popups
60
+ - Stop writing `agents.status` after the initial insert, and stop reading it
61
+ everywhere.
62
+ - A new `--json` shape for agents: `state` and `source`, with no `status`.
63
+ - `mu doctor` checks for murmur and its pi extension.
64
+ - Updates to the README, the skill, `REMOTE_WORKERS.md`, ARCHITECTURE,
65
+ DECISIONS and ROADMAP.
66
+ - murmur 1.0.0: a contract section in its ARCHITECTURE, and the release.
67
+
68
+ Out:
69
+
70
+ - The two input-timing checks in `send`. `awaitPaneQuiescence` waits out
71
+ pi's re-init modal, which eats the Enter. `isTextStranded` detects a
72
+ prompt that was typed but never submitted. Both are about typing into a
73
+ terminal, not about agent state, and murmur cannot see either. They keep a
74
+ private helper for spinner and work-marker text. It is used only by
75
+ `send`, returns no status, and is not exported.
76
+ - `detectSpawnStartupError` in `src/agents/spawn.ts`. It scans a new pane
77
+ for provider and auth errors (a dud spawn), which is a spawn failure, not
78
+ agent state. It stays for the same reason as the `send` checks.
79
+ - Dropping the `agents.status` column. mu has no migration path: v10 is both
80
+ the minimum and the current schema. The drop waits for the first change
81
+ that needs a v11 anyway, and ROADMAP records it.
82
+ - Reading murmur's SQLite files. Reading them would couple mu to murmur's
83
+ private schema.
84
+ - Any feature added to murmur for mu. murmur only documents what it already
85
+ publishes.
86
+
87
+ ## Design
88
+
89
+ ### Caller's view
90
+
91
+ ```ts
92
+ const states = await readAgentStates(db, agents); // one tmux read, ≤1 murmur call
93
+ const s = states.get(agent); // { state: "busy", source: "murmur", since: 1790581553851 }
94
+ if (s.state === "unknown") hint(s.reason); // "murmur not installed" | ...
95
+ ```
96
+
97
+ `mu agent list --json`:
98
+
99
+ ```json
100
+ { "name": "worker-1", "pane": "%4", "state": "busy", "source": "murmur",
101
+ "since": "2026-09-28T07:45:53Z" }
102
+ { "name": "worker-9", "pane": "%9", "state": "unknown", "source": "none",
103
+ "reason": "murmur not installed" }
104
+ ```
105
+
106
+ ### State resolver (`src/agent-state.ts`, new)
107
+
108
+ ```ts
109
+ type RuntimeState = "busy" | "needs_input" | "needs_permission" | "unknown";
110
+ type StateSource = "murmur" | "herdr" | "none";
111
+ interface StateReading {
112
+ state: RuntimeState;
113
+ source: StateSource;
114
+ since: number | null; // ms; when the state last changed, from the source
115
+ reason?: string; // set when state is unknown
116
+ }
117
+ function readAgentStates(db: Db, agents: AgentRow[]): Promise<Map<AgentRow, StateReading>>;
118
+ ```
119
+
120
+ - It takes a batch of agents, so a whole display costs one read.
121
+ - **herdr backend:** calls `paneStatus()` per pane, as today.
122
+ - **tmux, local:**
123
+ - One `list-panes -a -F '#{pane_id}\t#{@murmur_pane_state}\t#{@murmur_pane_since}'`
124
+ call.
125
+ - A pane with a value is read from murmur. A pane with no value is a
126
+ candidate for the remote read.
127
+ - murmur publishes `@murmur_pane_state` only for a pane its extension has
128
+ claimed (fixed in murmur `3c8c1a7`).
129
+ - **tmux, remote:** the agents with no local value.
130
+ - One `murmur status --json` call, cached per mu process for
131
+ `MURMUR_CACHE_MS` (10 s), so the 1 s poll in `mu agent wait` does not
132
+ start Node every tick. murmur limits its own ssh collect to one per
133
+ 30 s ± 10 s.
134
+ - A row matches when `agent_name` and `workstream` are equal and `local`
135
+ is false.
136
+ - Zero matches gives `unknown`, with reason "murmur has no row". More
137
+ than one match gives `unknown`, with reason "ambiguous: N hosts". A
138
+ peer that murmur reports as stale gives `unknown`, with reason "remote
139
+ snapshot stale".
140
+ - **`since`:**
141
+ - For a local pane, it is `@murmur_pane_since`: the time the pane entered
142
+ its current state (see murmur 1.0.0 below).
143
+ - For a remote row, it is the row's `updated_at`. That is an
144
+ approximation, because it also moves on runtime updates. It is used
145
+ only for remote idle and stall timing, where scraping had no answer at
146
+ all.
147
+ - Only the remote path starts `murmur`. A machine with no remote workers
148
+ never runs it.
149
+ - **When murmur is missing:** no `murmur` on PATH, or the pi extension not
150
+ linked (`~/.pi/agent/extensions/murmur.ts` missing).
151
+ - Every tmux agent gets `unknown`, with reason "murmur not installed" or
152
+ "murmur pi extension not linked".
153
+ - The resolver checks this once per process.
154
+
155
+ ### Mapping
156
+
157
+ | murmur | mu |
158
+ | --- | --- |
159
+ | `working` | `busy` |
160
+ | `idle` | `needs_input` |
161
+ | `done` | `needs_input` |
162
+ | `blocked` | `needs_permission` |
163
+ | `crashed` | `needs_input` |
164
+
165
+ - `done` and `idle` both mean "waiting for a prompt".
166
+ - `blocked` means waiting on a human answer, which is what a permission
167
+ dialog means.
168
+ - For `crashed`, pi is gone but the pane may still be alive. The mapping
169
+ makes waits and stall detection fire. Pane death stays with the reaper.
170
+
171
+ herdr's mapping does not change: `working` gives `busy`; `blocked` gives
172
+ `needs_permission`; `idle`, `done` and `unknown` give `needs_input`.
173
+
174
+ ### Consumers
175
+
176
+ - **reconcile.** It keeps pruning dead panes and refreshing titles. It
177
+ stops detecting and writing status. `report-only` mode stays read-only.
178
+ - **`mu agent list`, `mu state`, `mu agent show`, TUI.** These show
179
+ `state` from the resolver. An `unknown` state shows `?` and a one-line
180
+ hint, printed once per invocation: "agent state needs murmur (see mu
181
+ doctor)".
182
+ - **`mu agent wait`.** It fires on busy → not busy, as today. `unknown`
183
+ never fires. If every watched agent is `unknown` at the first tick, it
184
+ prints the reason once and keeps waiting until `--timeout`.
185
+ - **`mu task wait` stall detection.** The stall timer measures time in
186
+ `needs_input` using `since`. `unknown` never counts as a stall. The rest
187
+ of `task wait` is unchanged: it polls the DB, which is exact.
188
+ - **Spawn readiness.**
189
+ - With murmur present, spawn waits until the pane's `@murmur_pane_state`
190
+ is set, within the existing budget. murmur publishes it right after the
191
+ extension claims the pane.
192
+ - Without murmur, spawn skips the wait and keeps the "pane died on spawn"
193
+ check. The first `send` waits for pi through its input-timing checks.
194
+ - **`computeAgentIdle`.** An agent is idle when it is `needs_input` and
195
+ `since` is older than the threshold, taken from the resolver. It was
196
+ `agents.updated_at`.
197
+ - **`mu doctor`.** It reports murmur on PATH, the version (≥ 1.0.0), the pi
198
+ extension linked, and whether the identity exists. A failure is a
199
+ warning, not an error.
200
+
201
+ ### `agents.status` in 2.0
202
+
203
+ - It is deprecated in place.
204
+ - Inserts write the fixed value `'spawning'` to satisfy `NOT NULL` and the
205
+ CHECK. Nothing updates it and nothing reads it.
206
+ - Mid-spawn is already marked by the `pending:` pane-id placeholder.
207
+ - `free` (from `adopt`) and the "sticky until activity" overwrite rule
208
+ disappear, because nothing overwrites anything.
209
+ - `db.ts` marks the column deprecated.
210
+ - The SDK drops `AgentStatus`, `shouldOverwriteAgentStatus`,
211
+ `updateAgentStatus` and the `detectPiStatus` export. It exports
212
+ `RuntimeState`, `StateReading` and `readAgentStates`.
213
+
214
+ ### murmur 1.0.0
215
+
216
+ 1.0.0 means the interfaces mu reads are stable. A breaking change to them
217
+ needs a major version.
218
+
219
+ - **New: `@murmur_pane_since`.** This is the time (ms since the epoch)
220
+ the pane entered its current `@murmur_pane_state`.
221
+ - It is set in the same tmux command as the state, and only when the
222
+ state changes: `if -F '#{!=:#{@murmur_pane_state},<new>}' 'set -p
223
+ @murmur_pane_since <now>' ; set -p @murmur_pane_state <new>`.
224
+ - The compare-and-set runs inside tmux, so every writer (the extension,
225
+ the collector, `clear`) agrees, and a repeated write of the same state
226
+ leaves the value alone. This was checked on a scratch server:
227
+ `working → working → idle` kept the first time and then moved.
228
+ - It is unset together with the state. It needs no store schema change.
229
+ - **The contract**, listed in the "Contract" section of its ARCHITECTURE:
230
+ - `@murmur_pane_state` and its five values, and `@murmur_pane_since`
231
+ - the `status --json` fields `panes[].{pane, local, agent_name,
232
+ workstream, driver, activity, attention, freshness, updated_at}`
233
+ - the `MU_*` variables murmur reads
234
+ - **Release:** it ships main (the idle-label fix) plus
235
+ `@murmur_pane_since`, with a CHANGELOG entry that says what 1.0 means.
236
+ There is no 0.6.2.
237
+ - **Order:** murmur 1.0.0 first, then mu 2.0.0.
238
+
239
+ ## Cost
240
+
241
+ Measured on the dev machine with 5 local agents:
242
+
243
+ | Read | Before (scrape) | After |
244
+ | --- | --- | --- |
245
+ | local state, 5 agents | 5 × `capture-pane`, 13 ms | 1 × `list-panes`, 2 ms |
246
+ | `agent wait`, per 1 s tick | 5 captures, plus detection in JS | 1 × `list-panes` |
247
+ | local idle and stall timing | a DB timestamp that only moved on reconcile | `@murmur_pane_since`, read in the same call |
248
+ | remote agents | captures that gave wrong answers | `murmur status --json`, 90 ms, cached for 10 s |
249
+
250
+ `mu agent list` and `mu state` take about 0.8 s, and most of that is mu's
251
+ startup (`mu --version` takes 0.30 s). This change removes work from both
252
+ commands. No local path starts a second Node process.
253
+
254
+ ## Testing
255
+
256
+ - **Unit tests** for the resolver, using a fake tmux and a fake `murmur`
257
+ runner:
258
+ - the local value
259
+ - a remote match
260
+ - no match, an ambiguous match and a stale peer
261
+ - murmur missing and the extension not linked
262
+ - the herdr path
263
+ - cache reuse within `MURMUR_CACHE_MS`
264
+ - no `murmur` process started when every agent has a local option
265
+ - **Consumer tests:**
266
+ - `agent wait` never fires on `unknown`
267
+ - stall detection uses `since` and ignores `unknown`
268
+ - spawn readiness with murmur and without it
269
+ - `computeAgentIdle`
270
+ - **Removal checks:**
271
+ - No state path imports the send helper. A grep test in the style of
272
+ murmur's cheap-tick check enforces this.
273
+ - `src/detect.ts` exports no status.
274
+ - **Integration:**
275
+ - With real tmux and a fake `@murmur_pane_state` set with `tmux set -p`:
276
+ `mu agent list --json` shows `source: murmur`.
277
+ - Without the option, it shows `unknown` with its reason.
278
+ - **Live check** on this machine: spawn a worker, send it work, and check
279
+ `mu agent list` for busy and then needs_input, plus `mu agent wait` and
280
+ `mu task wait --on-stall exit`.
281
+ - **Gates:**
282
+ - mu: `npm run typecheck && npm run lint && npm run test`
283
+ - murmur: `npm run check`
284
+
285
+ ## Implementation checklist
286
+
287
+ 1. murmur: publish `@murmur_pane_since` with a compare-and-set in
288
+ `setPaneState`, with tests (set on change, kept on a repeat, unset with
289
+ the state). Add the Contract section to ARCHITECTURE and the 1.0.0
290
+ CHANGELOG entry. Live check, then release 1.0.0 through CI.
291
+ 2. mu: add `src/agent-state.ts` (the resolver, mapping and cache) and its
292
+ unit tests.
293
+ 3. mu: switch reconcile, `agent list/show`, `mu state` and the TUI to the
294
+ resolver. Stop writing status.
295
+ 4. mu: switch `agent wait`, `task wait` stall detection and
296
+ `computeAgentIdle` to the resolver.
297
+ 5. mu: switch spawn readiness to the murmur option, or skip it when murmur
298
+ is missing.
299
+ 6. mu: move the send input-timing checks to a private helper, remove the
300
+ state exports from `src/detect.ts`, and add the grep test.
301
+ 7. mu: add the `--json` shape (`state`, `source`, `since`, `reason`), and
302
+ make the SDK export changes.
303
+ 8. mu: add the `mu doctor` murmur check.
304
+ 9. mu docs:
305
+ - README: "agent state needs murmur"
306
+ - the skill and `REMOTE_WORKERS.md`: drop the "ask murmur by hand" step
307
+ - ARCHITECTURE: rewrite § "Status detection"
308
+ - DECISIONS: this decision and what it rejects
309
+ - ROADMAP: the column drop, and update § murmur
310
+ - VOCABULARY
311
+ 10. mu: CHANGELOG 2.0.0 (breaking: `status` is gone, murmur is needed for
312
+ state); do the live check, then release through CI.
313
+
314
+ ## Rejected
315
+
316
+ - **Scraping as a fallback.** It keeps two answers, one for each source. It
317
+ also keeps the wrapper-patch cycle, and the misreads return whenever
318
+ murmur is absent. `unknown` is honest.
319
+ - **Scraping only for remote workers.** Remote is exactly where scraping is
320
+ wrong.
321
+ - **Storing runtime state in `mu.db`.** The copy goes stale. The data
322
+ belongs in murmur.
323
+ - **Dropping the column now.** It would make this release mu's first
324
+ migration, and the only gain would be deleting an unused column.
325
+ - **Reading murmur's `state.db`.** It couples mu to a private schema.
326
+ - **Making murmur a hard dependency.** It pulls murmur's native
327
+ `better-sqlite3` build into installs that want only the task graph.
328
+ - **Removing the send checks.** They fixed measured failures, such as a
329
+ prompt lost to pi's session-naming modal, and murmur cannot replace them.
330
+
331
+ ## Open questions
332
+
333
+ None.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mu-crew/mu",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "description": "A persistent, observable crew of pi agents running in one tmux or herdr session per workstream, coordinated through a built-in task DAG.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -10,7 +10,8 @@ the traps below it are the part that costs time.
10
10
  **If you read one thing:** on a session-capped host, never leave an
11
11
  attach pane open — it holds the only ssh channel and silently breaks
12
12
  `git fetch`, `murmur collect` and every other ssh. Attach to look, then
13
- `mu agent close`. See § Never leave an attach pane open.
13
+ `mu agent close`. See § Never leave an attach pane open. This guidance
14
+ concerns interactive attachment, not mu's automatic agent-state reads.
14
15
 
15
16
  ---
16
17
 
@@ -19,8 +20,8 @@ attach pane open — it holds the only ssh channel and silently breaks
19
20
  **The pane is local, the process is remote.** `mu agent spawn
20
21
  --command 'ssh <host> -t "..."'` starts an ordinary tmux pane whose
21
22
  foreground process happens to be ssh. Everything mu does is
22
- pane-shaped, so `mu agent send`, `mu agent read`, status detection and
23
- the reaper all work unchanged, across the network, with no mu changes.
23
+ pane-shaped, so `mu agent send`, `mu agent read`, and the reaper work
24
+ unchanged across the network. murmur reports the remote process's agent state.
24
25
 
25
26
  **One orchestrator DB; panes may be remote.** All state stays on your
26
27
  machine. Do not run a second mu on the host to "coordinate": ownership
@@ -65,8 +66,8 @@ the only ssh channel**. While it is open, `git fetch`, `git push`,
65
66
  `Permission denied (keyboard-interactive)` error.
66
67
 
67
68
  **Attach to look, then close immediately.** `mu agent close <name>`
68
- detaches without stopping a detached-tmux agent. Poll with `murmur
69
- collect` + `murmur status`; use mule for orchestrator commands because
69
+ detaches without stopping a detached-tmux agent. mu reads agent state
70
+ through murmur automatically; use mule for orchestrator commands because
70
71
  its separate ControlPath cannot contend with the attach pane. See
71
72
  § When the host limits concurrent sessions for diagnosis and the
72
73
  remote-agent setup.
@@ -140,39 +141,30 @@ Local and remote agents mix freely in one workstream. The DAG, tracks,
140
141
  — task status is a row in YOUR database, written by you, so a wait on
141
142
  it is exact.
142
143
 
143
- **Agent STATUS is different, and this is the one place "nothing
144
- changes" is false.** `busy` / `needs_input` / `idle` come from reading
145
- the local pane's scrollback, which for a remote worker is a nested tmux
146
- rendered over ssh. What you get is redraw lag and quiet periods that
147
- look like idleness, so anything derived from status is unreliable here:
144
+ **Agent state comes from murmur.** Its extension runs inside pi on the
145
+ host and reports state without reading the nested tmux screen. mu reads
146
+ murmur's remote rows and caches them for 10 seconds. Remote rows can lag
147
+ by murmur's collect floor (30 seconds ± 10 seconds) plus that cache;
148
+ their `since` value is the row's `updated_at`, so idle and stall age is
149
+ approximate.
148
150
 
149
151
  | waiting on | remote? |
150
152
  | --- | --- |
151
153
  | `mu task wait` (task status) | exact — a DB poll, once something closes the task |
152
154
  | the reaper | fires, but see below |
153
- | stall detection | unreliable |
154
- | `mu agent wait --first` | same, it is status-based |
155
+ | stall detection | exact state from murmur; approximate age |
156
+ | `mu agent wait --first` | exact state from murmur |
155
157
 
156
- Measured both directions in one session: a stall fired at 300s against
157
- a worker that was visibly mid-turn, and `mu agent list` showed
158
- `needs_input` for another that was working. Give remote waits a generous
159
- `--timeout`; see `mu task wait --help` for stall controls.
158
+ Without murmur, remote agent state is `unknown`. `mu agent wait` does not
159
+ fire on `unknown`, and `unknown` never counts as a task stall. Run
160
+ `mu doctor` for the missing-source reason.
160
161
 
161
- The reaper is right for a DIRECT spawn — the connection dying really
162
+ The reaper is right for a direct spawn — the connection dying really
162
163
  does kill that agent — and wrong for a detached-tmux one, where the
163
164
  agent outlives the ssh but mu reaps the task anyway. See § A dropped
164
165
  connection reaps the task but NOT the commit.
165
166
 
166
- **If you need trustworthy remote status, ask murmur, not mu.** Its
167
- extension runs inside pi ON THE HOST and pushes state, so nothing is
168
- scraped and no ssh hop distorts it:
169
-
170
- ```bash
171
- murmur collect && murmur status --json # activity is host-reported
172
- ```
173
-
174
- That is the division worth remembering: mu owns the work, murmur owns
175
- what the agent is doing.
167
+ The division is: mu owns the work; murmur reports what the agent is doing.
176
168
 
177
169
  ### On step 2 — the note is load-bearing
178
170
 
@@ -361,17 +353,18 @@ respawn.
361
353
 
362
354
  ## mu and murmur, and what you lose without it
363
355
 
364
- [murmur](https://github.com/mu-crew/murmur) is optional and
365
- **strictly additive**. Nothing in mu needs it: the DAG, claim/close/
366
- wait, workspaces, spawn/send/read, the reaper, `mu state`, this whole
367
- remote recipe and `git fetch` collection all work with murmur absent.
368
- What you lose is the *view* — cross-machine agent state, the tmux
369
- status pills, `prefix+a`, the attachment hint, and `murmur peer list`
370
- for host reachability. Guard on it (`if it is installed`) rather than
371
- assuming it.
372
-
373
- The division: **mu owns the work, murmur owns what an agent is doing.**
374
- murmur never places work.
356
+ [murmur](https://github.com/mu-crew/murmur) provides agent state for
357
+ mu's tmux backend. Without it, mu reports agent state as `unknown`.
358
+ The DAG, claims, task completion waits, workspaces, spawn, send, read,
359
+ the reaper, this remote recipe, and `git fetch` collection still work.
360
+ Agent-state consumers such as `mu agent wait`, idle flags, and task-stall
361
+ detection wait rather than treating `unknown` as completion or a stall.
362
+
363
+ murmur also provides the tmux status pills, `prefix+a`, attachment hints,
364
+ and `murmur peer list` for host reachability. The division is: **mu owns
365
+ the work; murmur reports what an agent is doing.** murmur never places
366
+ work. Its interfaces used by mu are in murmur's
367
+ [stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
375
368
 
376
369
  | question | ask |
377
370
  | --- | --- |
@@ -32,8 +32,8 @@ both. `--json` exists on every verb:
32
32
  - **agent** — named worker in a pane (you may be one).
33
33
  - **mux** — the multiplexer mu drives: tmux, or herdr. One per
34
34
  invocation. `mu doctor` names the active one; `MU_MUX` forces it.
35
- Spawn, send, read, and status detection work on both; `mu agent
36
- kick` is herdr's remaining gap (Linux-only there).
35
+ Spawn, send, and read work on both. Agent state comes from murmur
36
+ on tmux and from herdr on herdr; `mu agent kick` is Linux-only on herdr.
37
37
  - **task** — DAG node with mandatory `impact` (1–100) and
38
38
  `effort_days`. Status: `OPEN`, `IN_PROGRESS`, `CLOSED`
39
39
  (satisfies `--blocked-by`). Record postponed/wont-do rationale as
@@ -98,8 +98,9 @@ that a refusal with `--strict-staleness`.
98
98
  ### Remote agents
99
99
 
100
100
  Agents can run on another machine: the PANE is local, the PROCESS is
101
- remote (`--command 'ssh <host> -t "..."'`), so `send`, `read`, status
102
- detection and the reaper all keep working unchanged. **One orchestrator
101
+ remote (`--command 'ssh <host> -t "..."'`), so `send`, `read`, and the
102
+ reaper keep working unchanged. murmur reports the remote agent's state.
103
+ **One orchestrator
103
104
  DB; panes may be remote** — never run a second mu on the host, since
104
105
  `tasks.owner_id` is an FK into the machine-local `agents` table and a
105
106
  remote mu could not claim your tasks anyway. You create the remote
@@ -158,7 +159,7 @@ Every turn:
158
159
  `mu task claim <id> -w <ws> --for <agent> --evidence "..."`.
159
160
  If no task exists, `mu task add` first; include initial context with
160
161
  `--note 'REPRO: ...\nSCOPE: ...'` when the title alone is not
161
- enough. Agent status is noisy; task ownership is durable and
162
+ enough. Agent state is runtime observation; task ownership is durable and
162
163
  waitable.
163
164
  4. Send terse instructions: task id, files/notes to read, workspace
164
165
  path, validation command, scope guards, task note contract.
@@ -210,8 +211,8 @@ Every turn:
210
211
  conflict. Refresh workspaces between waves.
211
212
  - Cross-workstream wait/claim uses qualified refs. The owner stays in its own
212
213
  workstream; only task ownership crosses.
213
- - For an idle worker, inspect scrollback, then send a retry or release its task.
214
- `MU_IDLE_THRESHOLD_MS` defaults to 5m.
214
+ - For an idle worker, read the pane to learn why it stopped, then answer,
215
+ retry, or release its task. `MU_IDLE_THRESHOLD_MS` defaults to 5m.
215
216
  - `mu agent kick` targets a wedged foreground subprocess. It refuses to signal
216
217
  the wrapping CLI; close the agent if that is what must stop.
217
218
  - Use `mu agent send`, not raw mux input: mu preserves literal text and confirms
@@ -291,24 +292,18 @@ Convention: `pi_mini` / `pi` / `pi_big`. Use mini for probing,
291
292
  modest for build/edit/refactor, big for design/review/incidents.
292
293
  Discover model strings with `pi --list-models [fuzzy-search]`.
293
294
 
294
- ## Reaper and status limits
295
+ ## Reaper and agent state
295
296
 
296
297
  If an agent pane dies, or `mu agent close` kills it mid-task, owned
297
298
  IN_PROGRESS tasks revert to OPEN with a `[reaper]` note and `task
298
299
  reap` op. No manual release after crashes.
299
300
 
300
- Status detection is heuristic and can lag behind custom `--command`
301
- wrappers. It is weakest for a remote worker, where the scrollback is a
302
- nested tmux rendered over ssh.
303
-
304
- **[murmur](https://github.com/mu-crew/murmur), if installed, is
305
- authoritative there** — its extension pushes state from inside the agent
306
- on the host rather than scraping a pane, and it answers across machines.
307
- Optional and strictly additive: nothing here needs it, and the seam is
308
- the env vars `mu agent spawn` already injects (`MU_MANAGED_AGENT`,
309
- `MU_AGENT_NAME`, `MU_WORKSTREAM`), which murmur reads to mark a pane as
310
- crew. **mu owns the work; murmur owns what an agent is doing.** See
311
- [REMOTE_WORKERS.md](REMOTE_WORKERS.md) § mu and murmur.
301
+ Agent state comes from [murmur](https://github.com/mu-crew/murmur) on
302
+ tmux and from herdr on herdr. `unknown` means no state source; run
303
+ `mu doctor` for the reason. mu needs murmur only for agent state:
304
+ tasks, claims, workspaces, spawn, send, read, and task completion waits
305
+ work without it. **mu owns the work; murmur reports what an agent is
306
+ doing.** See [REMOTE_WORKERS.md](REMOTE_WORKERS.md) § mu and murmur.
312
307
 
313
308
  For high-stakes decisions:
314
309
 
@@ -359,7 +354,7 @@ Sending to a BUSY agent is not delayed — that input queues normally.
359
354
 
360
355
  ## Guardrails
361
356
 
362
- Task ownership outranks scraped status. Coordinate through task notes and the
357
+ Task ownership outranks runtime agent state. Coordinate through task notes and the
363
358
  activity log. Keep edges within one workstream, role-name agents, and reserve
364
359
  the `mu_` task-id prefix. Give workers bounded paths and commands; use `mu
365
360
  agent kick` if a subprocess wedges.