@mu-crew/mu 1.6.0 → 3.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.
- package/AGENTS.md +6 -5
- package/README.md +31 -17
- package/dist/cli.js +3582 -2633
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +265 -137
- package/dist/index.js +3128 -2486
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +71 -45
- package/docs/ROADMAP.md +20 -105
- package/docs/USAGE_GUIDE.md +62 -41
- package/docs/VISION.md +11 -21
- package/docs/VOCABULARY.md +33 -40
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +333 -0
- package/docs/specs/2026-09-29-task-substates.md +247 -0
- package/package.json +1 -1
- package/skills/mu/REMOTE_WORKERS.md +31 -38
- package/skills/mu/SKILL.md +26 -25
- package/docs/.m-reflow-5f7exa_b.md +0 -509
- package/docs/.m-reflow-5v_034rw.md +0 -545
- package/docs/.m-reflow-7yaw8ubc.md +0 -513
package/docs/VISION.md
CHANGED
|
@@ -137,19 +137,11 @@ lives behind `MuxBackend` in `src/mux/`. Two implementations, not an
|
|
|
137
137
|
anticipatory abstraction: a third earns its way in on its own
|
|
138
138
|
friction evidence.
|
|
139
139
|
|
|
140
|
-
### 6. Pi-first,
|
|
140
|
+
### 6. Pi-first, with reported agent state
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
backend no other CLI ships with a detector, so an unrecognized pane
|
|
146
|
-
tends to read `needs_input`.
|
|
147
|
-
|
|
148
|
-
A mux that classifies panes itself changes this. The herdr backend
|
|
149
|
-
reports agent state natively across the agent kinds it knows, so on
|
|
150
|
-
that backend mu takes the mux's word and skips scrollback scraping
|
|
151
|
-
entirely. Detection is a property of the **mux backend**, not a fixed
|
|
152
|
-
property of mu.
|
|
142
|
+
mu does not infer agent state from pane text. The herdr backend reports
|
|
143
|
+
state through the mux. On tmux, murmur's pi extension reports state from
|
|
144
|
+
inside the agent process; without murmur, state is `unknown` with a reason.
|
|
153
145
|
|
|
154
146
|
`--cli` and `MU_<UPPER_CLI>_COMMAND` stay useful for swapping the pi
|
|
155
147
|
binary: set `MU_PI_COMMAND=<name>` once and every spawn picks it up.
|
|
@@ -244,9 +236,9 @@ mu does not own:
|
|
|
244
236
|
- **Tool routing decisions.** Pi (and any other CLI you spawn) owns
|
|
245
237
|
tool allowlists, MCP servers, extensions. Mu doesn't proxy or
|
|
246
238
|
inspect them.
|
|
247
|
-
- **Output interpretation.** Mu
|
|
248
|
-
|
|
249
|
-
|
|
239
|
+
- **Output interpretation.** Mu does not parse pane contents for agent
|
|
240
|
+
state, facts, claims, or tool calls. murmur or herdr reports runtime
|
|
241
|
+
agent state.
|
|
250
242
|
The `--evidence` payload is whatever the agent says it is; mu
|
|
251
243
|
records it without interpretation.
|
|
252
244
|
|
|
@@ -344,12 +336,10 @@ orchestrator stays in charge.
|
|
|
344
336
|
`~/.local/state/mu/mu.db`. Cross-machine state moves as
|
|
345
337
|
append-only JSONL segments in a shared folder; you own transport.
|
|
346
338
|
|
|
347
|
-
3. **Pi-first.**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
mu defers to it. Either way we optimize for
|
|
352
|
-
false-negative-then-poll over false-positive-then-act.
|
|
339
|
+
3. **Pi-first.** `--cli pi` is the meaningful default; `--cli` accepts
|
|
340
|
+
other strings as a key for the `MU_<UPPER_CLI>_COMMAND` env var
|
|
341
|
+
resolver. Agent state comes from murmur on tmux and herdr on herdr;
|
|
342
|
+
absence is `unknown` rather than an inferred value.
|
|
353
343
|
|
|
354
344
|
4. **Send is delivered, not acknowledged.** `mu agent send` gets the
|
|
355
345
|
text into the pane and warns loudly when it cannot confirm
|
package/docs/VOCABULARY.md
CHANGED
|
@@ -28,13 +28,16 @@ defined here, fix the doc. If you need a new term, add it here first.
|
|
|
28
28
|
| **remote worker** | A worker whose process and workspace are on another machine while its pane and orchestrator DB remain local. The durable inventory record is a task-note line in the exact `REMOTE: <host>:<path>` shape; pair it with `REMOTE_BASE: <agent>:<sha>` so `mu task claim --for <agent>` can print the one-shot poll-and-close step. `mu state` lists `REMOTE:` lines and `mu state --json` exposes them as `remoteWorkers`. | "remote agent record", "host registry" |
|
|
29
29
|
| **actor** | The party that *caused* a state change. May or may not be a registered worker. Recorded in `ops.actor` for every op. The orchestrator running mu from a top-level shell is an actor but not a worker. | "caller", "author" (only on notes) |
|
|
30
30
|
| **crew** | *Informal* collective noun for the agents in a workstream | (no API surface; prose only) |
|
|
31
|
-
| **task** | A node in the DAG. Has mandatory `impact` and `effort_days`.
|
|
32
|
-
| **task status** |
|
|
31
|
+
| **task** | A node in the DAG. Has mandatory `impact` and `effort_days`. Has a **task status** and a **substate**, shown as a pair (`OPEN/parked`, `CLOSED/wontfix`). | "issue", "ticket", "item" |
|
|
32
|
+
| **task status** | The lifecycle phase, one of 3: **OPEN** = not started; **IN_PROGRESS** = claimed and active; **CLOSED** = ended. Status alone decides edge satisfaction: any `CLOSED/*` satisfies a `--blocked-by` edge. | "state" |
|
|
33
|
+
| **substate** | What the **task status** means for this task. Never null; each status has a default. `OPEN`: `todo` (default), `parked`. `IN_PROGRESS`: `active`. `CLOSED`: `done` (default), `wontfix`, `duplicate`, `superseded`. Rendered as a pair `STATUS/substate`; a default substate renders as the bare status. Valid pairs live in the `task_substates` table. Never affects edges. Refer to a non-`done` close as a *closed substate*. | "resolution", "reason", "sub-status" |
|
|
34
|
+
| **park** | Verb: set an `OPEN` task to `OPEN/parked` with a required `--why` note. A parked task leaves **ready** and `next`, `claim` refuses it without `--force`, and it stays in **goals**. Its dependents stay blocked. Refuses `IN_PROGRESS` (release first) and `CLOSED` (open first). | "defer", "snooze", "shelve" |
|
|
35
|
+
| **unpark** | Verb: return an `OPEN/parked` task to `OPEN/todo`. | "resume", "reactivate" |
|
|
33
36
|
| **task DAG** / **graph** | The directed acyclic graph of tasks. | "task list", "todo", "tree" (it's a DAG, not a tree) |
|
|
34
37
|
| **edge** | A `blocks` relationship between two tasks. The single edge type. `A blocks B` = A must close before B can start. | "dependency" (use only in prose) |
|
|
35
38
|
| **track** | An independent subtree of the DAG identified by parallel-track detection | "branch", "lane" |
|
|
36
39
|
| **diamond merge** | When two tracks share a prerequisite, parallel-track detection collapses them into one track to prevent two agents from colliding on the shared dependency. | "join", "converge" |
|
|
37
|
-
| **ready** | An OPEN task with no unresolved blockers. Exposed as the `ready` SQL view. | "unblocked", "available" |
|
|
40
|
+
| **ready** | An `OPEN/todo` task with no unresolved blockers (parked tasks are excluded). Exposed as the `ready` SQL view. | "unblocked", "available" |
|
|
38
41
|
| **goals** | Tasks with no outgoing blocks-edges (graph endpoints). Exposed as the `goals` SQL view. | "leaves", "targets" |
|
|
39
42
|
| **sort key** | Argument to `mu task list / next / ready --sort <key>`. One of `roi` (impact / effort_days, default for `next` / `ready`), `recency` (`updated_at` DESC — "what did I touch most recently"), `age` (`created_at` ASC — "what's gone stale"), `id` (`local_id` ASC, default for `task list`). The two time-based keys also render an `updated`/`created` relative-time column in the table view. | "order by", "sort by" |
|
|
40
43
|
| **subtree** / **scope** | The set of tasks reachable from a root via blocks-edges | "subgraph" (only for technical descriptions) |
|
|
@@ -46,16 +49,14 @@ defined here, fix the doc. If you need a new term, add it here first.
|
|
|
46
49
|
| **owner** | The **worker** name in `tasks.owner`. Set by claim. NULL when the task is unowned OR was claimed via `--self` (anonymous, attributed via `ops.actor` instead). Owners are **machine-local** — they reference `agents`, so ownership never syncs. | "claimer", "assignee" |
|
|
47
50
|
| **anonymous claim** | A claim made via `--self` where the **actor** isn't a registered **worker**. `tasks.owner` stays NULL; the actor is recorded in `ops.actor` for the auto-emitted `task.claim` op. The orchestrator-doing-direct-work pattern. | "self-claim" (in code; "anonymous claim" in prose), "unowned claim" |
|
|
48
51
|
| **release** | Verb: clear `tasks.owner` | "unclaim", "unassign" |
|
|
49
|
-
| **
|
|
50
|
-
| **status** | Persisted enum on `agents` (busy/needs_input/free/...) | "state" (use only "lifecycle state") |
|
|
51
|
-
| **lifecycle state** | A position in the agent state machine | "state" alone, "phase" |
|
|
52
|
+
| **agent state** | Runtime state reported by the **state source**: `busy`, `needs_input`, `needs_permission`, or `unknown`. It is not persisted in mu. | "agent status", "lifecycle state" |
|
|
52
53
|
| **role** | `full-access` or `read-only` capability flag | "permission" (avoid), "tier" |
|
|
53
54
|
| **persistent** | Agent that stays alive across tasks | "long-lived" (only in prose) |
|
|
54
55
|
| **one-shot** | Agent that exists for a single task and then terminates | "ephemeral", "transient" |
|
|
55
56
|
| **workspace** | A VCS-isolated checkout (jj workspace / sl worktree / git worktree / cp). **In mu, "workspace" is always the VCS sense** — never the mux sense. herdr calls its session-level container a "workspace" too; in mu docs and code that is a **mux session**. | "branch" (it has one but isn't one), "checkout" (only for `none` backend), the herdr sense |
|
|
56
57
|
| **ghost** | An `agents` row whose `pane_id` no longer exists in the **mux**. Pruned by **reconcile**, which runs in `mu agent list` and `mu doctor`. | "dead agent" (a dead agent may still have a pane), "stale row" |
|
|
57
58
|
| **reaper** | The part of reconcile that releases a **ghost**'s tasks: `IN_PROGRESS → OPEN`, owner cleared. Makes `mu task wait` exit 6. | "garbage collector", "janitor" |
|
|
58
|
-
| **needs attention** | The condition `mu task wait --stuck-after` detects: an IN_PROGRESS task whose **owner** has
|
|
59
|
+
| **needs attention** | The condition `mu task wait --stuck-after` detects: an IN_PROGRESS task whose **owner** has been in `needs_input` for at least N seconds according to murmur's `since`. `unknown` never counts. The predicate cannot distinguish a worker that finished without closing, one waiting on an answer, or one at an approval prompt, so every surface points at `mu agent read <owner>`. | "stuck" / "stalled" (the `stuck` JSON field and exit-7 `STALL_DETECTED` keep their names for compatibility), "close discipline", "negligent worker" |
|
|
59
60
|
| **workspace orphan** | A directory under `<state-dir>/workspaces/<workstream>/` with no row in `vcs_workspaces`. Blocks subsequent `--workspace` spawns. Surfaced by `mu workspace orphans -w X`, `mu state -w X`, and `mu doctor`'s `ws-dirs` row. | "stray dir", "leftover workspace" |
|
|
60
61
|
| **stranded** | A **workspace orphan** whose parent *workstream* row is also gone. Reported separately because no workstream-scoped verb can reach it — `mu workspace orphans --all` and `mu doctor` are the only surfaces. | "double orphan", "abandoned" |
|
|
61
62
|
| **missing workspace dir** | The INVERSE of a **workspace orphan**: a `vcs_workspaces` row whose `path` is not on disk (usually a hand-run `rm -rf` where `mu workspace free` was wanted). More dangerous than an orphan, because every read surface reports the row as a healthy workspace and the next send fails inside the VCS backend rather than at the row that lied. `mu doctor`'s `ws-rows` row. | "broken workspace", "dangling row" |
|
|
@@ -64,7 +65,7 @@ defined here, fix the doc. If you need a new term, add it here first.
|
|
|
64
65
|
| **stale workspace** | A workspace whose `parent_ref` is N commits behind the project's default branch HEAD (per the workspace's local refs cache). Rendered as a color-coded `behind` column (green ≤2, yellow 3–9, red ≥10) in `mu workspace list` and `mu state`; ≥10 triggers a one-line warn in `mu state`, `mu task claim --for`, and `mu agent send` (or refusal on the two dispatch verbs with `--strict-staleness`). Pure observation — mu never auto-fetches. | "out of date", "drifting" |
|
|
65
66
|
| **refresh** | `mu workspace refresh <agent>` — rebase the agent's workspace onto a fresh base (default = backend's tracked main; `--from <ref>` overrides) WITHOUT touching the agent or pane. Refuses on dirty WC; surfaces conflicts as exit 5 with a resolve-in-place hint. The `none` backend errors (no VCS to rebase). | "recycle", "reset" (overloaded) |
|
|
66
67
|
| **backend** | Implementation of `MuxBackend` or `VcsBackend`. Always qualify which (**mux backend** / **VCS backend**) when both are in scope. | "driver", "provider" |
|
|
67
|
-
| **
|
|
68
|
+
| **state source** | The system that reports **agent state**: herdr's `paneStatus()` on herdr, or murmur on tmux. mu reads local murmur pane options in one tmux call and uses `murmur status --json` only for remote workers. No source yields `unknown` with a reason. | "scraper" |
|
|
68
69
|
| **op** | The atomic unit of change: one row in the `ops` table, written by a trigger inside the same transaction as the mutation it records. Carries `(hlc, machine_id, group_id, actor, intent, entity, key, op, payload)`. A **semantic partial update** — the payload holds only the columns that actually changed, which is what makes per-field merge free. | "event", "delta", "change" (all overloaded) |
|
|
69
70
|
| **ops log** | The `ops` table: the single append-only record of every change, and the substrate **sync**, **undo**, and history are all queries or replays over. Canonical and ACID because it lives in `mu.db` alongside the tables it records. | "event log", "journal", "WAL" (reserved by SQLite) |
|
|
70
71
|
| **intent** | The semantic label on an **op** (`task.close`, `task.reparent`, `agent.spawn`). Set once per public SDK function via **op context**, not per mutation. `mu log` renders prose from it through one formatter. | "verb" (overloaded by CLI verbs), "action" |
|
|
@@ -165,36 +166,25 @@ window.
|
|
|
165
166
|
|
|
166
167
|
---
|
|
167
168
|
|
|
168
|
-
##
|
|
169
|
-
|
|
170
|
-
### Agent
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`src/glyphs.ts` (`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
All of them are classic Nerd Font `nf-fa-*` (Font Awesome 4)
|
|
188
|
-
codepoints, single-codepoint and one cell wide, so `cli-table3`
|
|
189
|
-
columns line up. `busy` is deliberately the same glyph murmur's dash
|
|
190
|
-
paints for a `running` pane — they drive the same panes, so one symbol
|
|
191
|
-
means one thing on both surfaces. Non-agent state glyphs (blocked,
|
|
192
|
-
dirty, stale, ok/warn/fail, filter toggles) come from the `GLYPH`
|
|
193
|
-
record in the same file.
|
|
194
|
-
|
|
195
|
-
**Source of truth:** the substrate — the **mux backend**, plus the
|
|
196
|
-
**detector** when that backend cannot classify panes itself. The DB is
|
|
197
|
-
a cache; `mu agent list` reconciles on every call.
|
|
169
|
+
## State, lifecycle, and the verbs that touch them
|
|
170
|
+
|
|
171
|
+
### Agent state
|
|
172
|
+
|
|
173
|
+
Agent state is runtime data and is not persisted. The codepoints live in
|
|
174
|
+
`src/glyphs.ts` (`AGENT_STATE_GLYPH`).
|
|
175
|
+
|
|
176
|
+
| Value | Glyph slot | Meaning |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `busy` | nf-fa-play | Actively working |
|
|
179
|
+
| `needs_input` | nf-fa-moon_o | Waiting for input |
|
|
180
|
+
| `needs_permission` | nf-fa-lock | Waiting for a human answer |
|
|
181
|
+
| `unknown` | nf-fa-question_circle | No state source or no usable reading; the reason accompanies the state |
|
|
182
|
+
|
|
183
|
+
The state source is herdr on the herdr backend and murmur on the tmux
|
|
184
|
+
backend. mu maps murmur `working` to `busy`, `blocked` to
|
|
185
|
+
`needs_permission`, and `idle`, `done`, and `crashed` to `needs_input`.
|
|
186
|
+
The retained `agents.status` column is deprecated and always contains
|
|
187
|
+
`spawning`; no runtime read uses it.
|
|
198
188
|
|
|
199
189
|
### Agent lifecycle verbs
|
|
200
190
|
|
|
@@ -211,8 +201,11 @@ a cache; `mu agent list` reconciles on every call.
|
|
|
211
201
|
| Verb | Effect |
|
|
212
202
|
| ------------------------------------- | ----------------------------------------------------- |
|
|
213
203
|
| `mu task add <id> ...` | Creates a new OPEN task. `--note <text>` appends an initial note in the same transaction; `--note-author <name>` overrides the note author. |
|
|
214
|
-
| `mu task close
|
|
215
|
-
| `mu task
|
|
204
|
+
| `mu task close <id> [--as <substate> --why <text>]` | Sets `CLOSED` (default `CLOSED/done`). A non-`done` `--as` requires `--why` and prints the dependents it unblocked. |
|
|
205
|
+
| `mu task open <id>` | Reopens a task to `OPEN/todo`. |
|
|
206
|
+
| `mu task park <id> --why <text>` | `OPEN` → `OPEN/parked`. Leaves ready/next; stays in goals. |
|
|
207
|
+
| `mu task unpark <id>` | `OPEN/parked` → `OPEN/todo`. |
|
|
208
|
+
| `mu task claim <task> [--for <agent>] [--force]` | Atomic: sets `owner`, flips status to `IN_PROGRESS/active`. Refuses a parked task unless `--force`. |
|
|
216
209
|
| `mu task release <task>` | Clears `owner`. Auto-flips `IN_PROGRESS` → `OPEN` (so the task re-enters the ready set); `CLOSED` is preserved. `--reopen` forces `OPEN` from `CLOSED`. |
|
|
217
210
|
| `mu task note <task> "..."` | Appends to `task_notes`. Never edits prior notes. |
|
|
218
211
|
| `mu task notes <task> [--tail N \| --since <iso> \| --since-claim]` | List notes (oldest first). `--tail N` (alias `--last N`) prints last N; `--since <iso>` filters by `created_at`; `--since-claim` auto-resolves to the most recent `task claim` event timestamp. `--since` and `--since-claim` are mutually exclusive. |
|
|
Binary file
|
|
@@ -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.
|