@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.
@@ -31,8 +31,8 @@ programmatic SDK are thin facades over the same core modules.
31
31
  │ │ mux/ │ │ schema │ │ jj │ │ capture │ │
32
32
  │ │ tmux │ │ queries │ │ sapling │ │ apply/sync │ │
33
33
  │ │ herdr │ │ tracks │ │ git │ │ doctor │ │
34
- │ │ detect │ │ claim │ │ none │ │ │ │
35
- │ │ state │ │ │ │ │ │ │ │
34
+ │ │ state │ │ claim │ │ none │ │ │ │
35
+ │ │ sources │ │ │ │ │ │ │ │
36
36
  │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬────────┘ │
37
37
  └───────┼─────────────┼─────────────┼─────────────────┼───────────┘
38
38
  ▼ ▼ ▼ ▼
@@ -111,8 +111,38 @@ it mu is just an agent runner.
111
111
  `ROI = impact / effort` drives prioritization.
112
112
  - **One edge type**: `blocks`. `A → B` means A must close before B can
113
113
  start. Multiple edge types create ambiguity that defeats the purpose.
114
- - **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED`. Postponed or
115
- won't-do rationale belongs in notes; `CLOSED` alone satisfies blockers.
114
+ - **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED`. Status alone
115
+ decides edge satisfaction: any `CLOSED` task satisfies its blockers.
116
+ - **Substate** qualifies status and never touches edges:
117
+ `OPEN/todo|parked`, `IN_PROGRESS/active`,
118
+ `CLOSED/done|wontfix|duplicate|superseded`. Rule: **store intent,
119
+ derive graph facts** — ready and blocked stay derived from edges.
120
+ `OPEN/parked` is excluded from `ready` but stays in `goals`.
121
+
122
+ #### Substate integrity: deferred FK + apply-time pair repair
123
+
124
+ The legal pairs live in the `task_substates` lookup table, seeded from
125
+ code on every open (machine-local, never synced). `tasks` has a
126
+ composite FK `(status, substate) → task_substates`,
127
+ `DEFERRABLE INITIALLY DEFERRED`, which is the only guard on the pair.
128
+ It is checked at COMMIT, so apply's one-field-at-a-time UPDATEs pass as
129
+ long as the pair is legal when the transaction ends. That is why
130
+ `applyOp` wraps each task put in `db.transaction` (and undo already runs
131
+ in one): in autocommit the first per-field UPDATE would fail the FK.
132
+
133
+ Every lifecycle write sets `status` and `substate` in one UPDATE, so
134
+ one op carries both under one HLC. A pair can still split across ops:
135
+ a status-only op from a v10 peer, an unknown substate from a newer
136
+ peer, or a concurrent close and park on two machines. Before commit,
137
+ `repairTaskPair` (`src/apply.ts`) sets the substate to the one implied
138
+ by the newest op in the LOG that writes a substate, resolved against
139
+ the row's current status, and falls back to the status default when
140
+ that pair is illegal. Substate is therefore NOT per-field LWW.
141
+ Reading the log instead of the row makes the result a pure function of
142
+ the op set plus the row's status, so every peer converges on the same
143
+ row whatever the arrival order. For the same reason the repair records
144
+ **no op**: each peer computes the identical repair from the identical
145
+ log. Recording one would only add a second writer racing the first.
116
146
  - **Notes** are append-only per task; survive across LLM sessions and
117
147
  agent restarts. The fix for context loss at the *task* level rather
118
148
  than the agent level.
@@ -121,7 +151,7 @@ it mu is just an agent runner.
121
151
 
122
152
  | View | Returns |
123
153
  | --------- | ---------------------------------------------------------------------- |
124
- | `ready` | OPEN tasks with no unresolved blockers — work that can start *now* |
154
+ | `ready` | `OPEN/todo` tasks with no unresolved blockers — work that can start *now* |
125
155
  | `blocked` | OPEN tasks waiting on something |
126
156
  | `goals` | Tasks with no dependents — graph endpoints |
127
157
 
@@ -291,17 +321,16 @@ That difference is expressed as a **capability**, not a name check:
291
321
  | --- | --- | --- |
292
322
  | `NewWindowOptions.command` etc. | carries the command | **refused** if non-empty |
293
323
  | `startAgentInPane()` | not implemented | implemented |
294
- | liveness / readiness wait | mu polls scrollback | the mux blocks until ready |
324
+ | liveness / readiness wait | mu checks pane life, startup errors, then murmur readiness when available | the mux blocks until ready |
295
325
 
296
326
  `spawnAgent` branches on `mux.startAgentInPane !== undefined`. When it
297
- is absent (tmux) the command rides along on the creation verb and the
298
- path is exactly what it always was. When present, mu creates the pane
299
- bare, then calls `startAgentInPane` — and **skips**
300
- `awaitSpawnLiveness` / `awaitSpawnReadiness`, because such a backend
301
- returns only once it has itself detected the agent and judged it ready
302
- for input, which is strictly stronger than what scrollback polling can
303
- prove. `MU_SPAWN_LIVENESS_MS` / `MU_SPAWN_READINESS_MS` are therefore
304
- tmux-tier knobs.
327
+ is absent (tmux), the command rides along on the creation verb. The
328
+ liveness check verifies that the pane survived and scans for startup
329
+ errors; when murmur is available, readiness also waits for the pane's
330
+ `@murmur_pane_state`. Without murmur, readiness is skipped and the first
331
+ send uses its input-timing checks. When `startAgentInPane` is present,
332
+ mu creates the pane bare, calls it, and skips the tmux checks because
333
+ the backend returns only after the agent is ready.
305
334
 
306
335
  The creation verbs **refuse** a non-empty command on a backend that
307
336
  cannot honour it rather than dropping it silently: a dropped command
@@ -407,7 +436,7 @@ src/cli/tui/
407
436
  ├── tab-strip-layout.ts # pure window-around-active layout helper
408
437
  ├── help.tsx # ?/F1 keymap overlay (scrollable on short panes)
409
438
  ├── use-popup-filter.tsx # shared '/' substring filter hook + applyFilter + FilterPrompt
410
- ├── use-status-filter.tsx # task-status toggles (o/i/c) for task-list popups
439
+ ├── use-status-filter.tsx # status toggles (o/i/c) + substate toggles (p/w) for task-list popups
411
440
  ├── use-notes-drill.ts # shared notes-drill memo (5 task popups consume it)
412
441
  ├── use-popup-action-queue.ts # consume mouse PopupAction queue once per render
413
442
  ├── cards/ # 10 dashboard glance cards (one slot each)
@@ -579,15 +608,12 @@ surrogate INTEGER ids once and then stay on ids. See
579
608
 
580
609
  ## Reconciliation
581
610
 
582
- `mu agent list` always reconciles the registry against tmux reality before
583
- returning. Three steps, in order:
611
+ `mu agent list` always reconciles the registry against mux reality before
612
+ returning. Two steps, in order:
584
613
 
585
614
  1. **Prune ghosts.** For each `agents` row, if its `pane_id` no longer
586
- exists in tmux, delete the row.
587
- 2. **Detect status from scrollback.** For each surviving agent, capture
588
- the pane and run the per-CLI detector. Update `agents.status` if
589
- the detected value differs from the stored one.
590
- 3. **Surface orphans.** For each tmux pane in the workstream's session
615
+ exists in the mux, delete the row.
616
+ 2. **Surface orphans.** For each pane in the workstream's mux session
591
617
  that has no matching `agents` row but whose pane title looks like
592
618
  an agent name, add it to the orphans list. **Do not auto-adopt** —
593
619
  `mu agent list` shows orphans under a separate "(orphans)" section and
@@ -596,19 +622,21 @@ returning. Three steps, in order:
596
622
  `src/reconcile.ts` is the only implementation. Key properties:
597
623
 
598
624
  - **Reality wins**: the mux is the source of truth for what panes
599
- exist. The DB records what we last *observed*. Reconciliation closes
600
- the gap on every `mu agent list`.
601
- - **Status detection is backend-dependent**, expressed as the optional
602
- `MuxBackend.paneStatus?()`. A backend that omits it (tmux) means "ask
603
- the detector": `src/detect.ts` scrapes scrollback (`busy` /
604
- `needs_input` / `needs_permission` via a known pi marker, with a
605
- Braille-spinner fallback for other CLIs). A backend that implements it
606
- (herdr) classifies panes natively across every agent kind it
607
- recognises, and mu takes its word — `src/detect.ts` is bypassed
608
- entirely on that backend. herdr's `working` → `busy`, `blocked` →
609
- `needs_permission`, `idle` / `done` / `unknown` → `needs_input`.
610
- `unknown` must never become `free`: herdr documents that it does not
611
- prove completion, and no detector of either kind may mint `free`.
625
+ exist. Reconciliation closes the gap on every `mu agent list`.
626
+ - **Agent state is reported, not scraped or persisted.** `src/agent-state.ts`
627
+ reads herdr's `paneStatus()` on herdr. On tmux, one `list-panes -a`
628
+ reads `@murmur_pane_state` and `@murmur_pane_since` for every local
629
+ agent. Only agents without a local option use `murmur status --json`;
630
+ that result is cached for 10 seconds. See murmur's
631
+ [stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
632
+ - **Mapping:** murmur `working` → `busy`, `blocked` →
633
+ `needs_permission`, and `idle` / `done` / `crashed` → `needs_input`.
634
+ herdr supplies mu's runtime states directly.
635
+ - **Absence is `unknown`.** Readings carry reasons such as `murmur not
636
+ installed`, `murmur pi extension not linked`, `murmur has no row`,
637
+ `ambiguous: N hosts`, `remote snapshot stale`, and `pane gone`.
638
+ - **`agents.status` is deprecated.** Inserts write `spawning` to satisfy
639
+ the schema. No code updates or reads the column as runtime state.
612
640
  - **No silent adoption**: orphans are reported, never claimed.
613
641
  - **`mu doctor` calls the same routine** and reports counts.
614
642
 
@@ -627,7 +655,7 @@ separately below.
627
655
 
628
656
  | Module | Responsibility |
629
657
  | --------------------- | ----------------------------------------------------------------------------------------- |
630
- | `src/db.ts` | Connection (better-sqlite3, WAL), **schema v10** (10 tables + 3 views), `resolveWorkstreamId`. Installs capture on every writable open. Owns `SYNCED_ENTITIES` / `MACHINE_LOCAL_ENTITIES` / `PORTABLE_TABLES` / `MACHINE_LOCAL_TABLES`. Refuses a pre-v10 DB (exit 4). |
658
+ | `src/db.ts` | Connection (better-sqlite3, WAL), **schema v11** (11 tables incl. the `task_substates` lookup + 3 views), `resolveWorkstreamId`. Installs capture on every writable open. Owns `SYNCED_ENTITIES` / `MACHINE_LOCAL_ENTITIES` / `PORTABLE_TABLES` / `MACHINE_LOCAL_TABLES`. Refuses a pre-v11 DB (`SchemaTooOldError`) or a newer one (`SchemaTooNewError`), both exit 4. |
631
659
  | `src/hlc.ts` | The **HLC** (VOCABULARY § HLC), serialized as sortable TEXT `<wall_ms:15>.<counter:6>.<machine_id>`. `nextHlc` / `receiveHlc` / `compareHlc` / `parseHlc` / `formatHlc`. Clock state lives in `machine_identity`. |
632
660
  | `src/capture.ts` | **Op capture**: builds the triggers that record every write to a portable table as an op in the same transaction. |
633
661
  | `src/apply.ts` | **The apply path** — capture's counterpart: given one op, local or from a peer, make the tables reflect it. Also owns `reprojectDeferredOps` ([§ ambient sync hook](#the-ambient-sync-hook)). |
@@ -645,10 +673,10 @@ separately below.
645
673
  | `src/disk-recon.ts` | **Disk↔DB reconciliation** — the only module that reads the STATE DIR and compares it to the DB, in both directions: a `vcs_workspaces` row whose path is gone (`ws-rows`), a dir with no row (`ws-dirs`, reusing `listAllOrphanWorkspaces`), plus residue nothing references (`ws-empty` / `db-copies` / `exports` / `locks`). Emits `FleetHazard` so doctor's renderer, `--json` and the TUI card need no new shape. Default tier is `readdir` + `stat` at depth 2 (~1ms); `measureWorkspaceUsage` recurses for bytes and is `--disk` only, because its cost scales with the checkouts. **Report-only by construction** — every finding carries its cleanup command and the module runs none of them. |
646
674
  | `src/op-context.ts` | The **op context** seam: `withOpContext(db, {intent, actor, group}, fn)` labels every op in a scope, restored in a `finally`. Nested scopes inherit the group, which puts a cascade under one `mu undo`. `withCaptureSuppressed` is the echo guard. |
647
675
  | `src/mux.ts` | **Mux backend hub** — re-exports `src/mux/*`, same shape as `src/vcs.ts`. The public `MuxBackend` surface every call site imports. |
648
- | `src/mux/*.ts` | **Multiplexer backends**: `types.ts` (the `MuxBackend` interface + `MuxError` / `PaneNotFoundError`), `detect.ts` (`MU_MUX` → `HERDR_ENV` → `$TMUX` → `PATH` ladder + test seam), `tmux.ts`, `herdr.ts`. The backend owns topology, the send protocol, capture, **pane id** validation (tmux `%15` vs herdr `w1:p1`), the identity fallback, and optionally native pane status (`paneStatus?()`) and agent start (`startAgentInPane?()`) — so no global pane-id regex and no per-call-site branching. The send protocol is where the two diverge most: tmux needs a six-step paste/Enter dance to survive a modal swallowing the Enter, while herdr's `agent prompt --wait` is one atomic call. |
676
+ | `src/mux/*.ts` | **Multiplexer backends**: `types.ts` (the `MuxBackend` interface + `MuxError` / `PaneNotFoundError`), `detect.ts` (`MU_MUX` → `HERDR_ENV` → `$TMUX` → `PATH` ladder + test seam), `tmux.ts`, `herdr.ts`, and `input-timing.ts`. The backend owns topology, capture, **pane id** validation (tmux `%15` vs herdr `w1:p1`), identity fallback, and optional native agent state (`paneStatus?()`) and agent start (`startAgentInPane?()`). `input-timing.ts` reads pane text only to decide when tmux input can be submitted; it does not report agent state. |
649
677
  | `src/tmux.ts` | Re-export of the tmux backend, kept for genuinely tmux-only concerns: the `MU_TMUX_SOCKET` test seam and the shared `sleep` / `setSleepForTests` poll seam. Everything else imports `src/mux.ts` and goes through `activeMux()`. |
650
- | `src/detect.ts` | Pi status detector (`busy` / `needs_input` / `needs_permission`) + Braille-spinner fallback. Used when the mux cannot classify panes itself — i.e. always on tmux, never on herdr. |
651
- | `src/reconcile.ts` | Ghost prune + status detect + orphan surface; "reality wins" |
678
+ | `src/agent-state.ts` | Runtime agent-state resolver. Reads herdr's native state or murmur's local pane options and remote JSON; maps source states, returns `unknown` with a reason, and caches remote murmur reads for 10 seconds. |
679
+ | `src/reconcile.ts` | Ghost prune + orphan surface; "reality wins" |
652
680
  | `src/agents.ts` | Hub: CRUD + send / read / list / close + liveness + reaper. Re-exports `src/agents/*`; pane-title composition (`composeAgentTitle`) lives here. |
653
681
  | `src/agents/*.ts` | Agent-lifecycle internals: `spawn.ts` (spawn, CLI resolution, liveness wait *or* backend `startAgentInPane`, pane create-or-reuse, rollback), `spawn-lock.ts` (per-session lock around topology+finalize), `wait.ts`, `adopt.ts`, `kick.ts` (signal a wedged pane's pgid), `errors.ts`. |
654
682
  | `src/dag.ts` | Shared DAG read/render helpers: `loadFullDag` plus pure `renderForest` / `renderTaskTree`, reused by `mu task tree` and the TUI DAG popup. |
@@ -660,7 +688,7 @@ separately below.
660
688
  | `src/log-render.ts` | **The ONE op → prose formatter.** `renderOp` maps an intent (+ key + payload fields) to `{verb, subject, detail}`; plus `renderOpLine`, `opSubject`, `parseOpKey`. Pure and colour-free, so CLI and TUI share one phrasing. |
661
689
  | `src/vcs/*.ts` | One backend per file (`git.ts`, `jj.ts`, `sl.ts`, `none.ts`) plus `types.ts` (the `VcsBackend` interface), `helpers.ts`, and `index.ts` (detection precedence `jj` → `sl` → `git` → none; `backendByName`). |
662
690
  | `src/workspace/*.ts` | Per-agent VCS workspaces, a registry on top of `vcs.ts`: `core.ts` (row shapes, paths, errors), `crud.ts`, `decorate.ts` (staleness + dirty), `orphans.ts`. |
663
- | `src/glyphs.ts` | **The ONE glyph vocabulary.** `AGENT_STATUS_GLYPH` + `agentStatusGlyph()` for agent status, the `GLYPH` record for every other state symbol (blocked, dirty, stale, ok/warn/fail, filter toggles), and `superscriptDigit` for TUI card-header keys. All classic Nerd Font `nf-fa-*` — single-codepoint and one cell wide, so `cli-table3` columns line up. Keyed by MEANING, so re-pointing a symbol is a one-line edit. `busy` matches murmur's dash `running`: same panes, same symbol. |
691
+ | `src/glyphs.ts` | **The ONE glyph vocabulary.** `AGENT_STATE_GLYPH` + `agentStateGlyph()` for runtime agent state, the `GLYPH` record for every other state symbol (blocked, dirty, stale, ok/warn/fail, filter toggles), and `superscriptDigit` for TUI card-header keys. All classic Nerd Font `nf-fa-*` — single-codepoint and one cell wide, so `cli-table3` columns line up. |
664
692
  | `src/output.ts` | NextStep type + `printNextSteps` + `errorNextSteps` plumbing for self-documenting output |
665
693
  | `src/shell-quote.ts` | `shellQuote` — POSIX single-quoting for tokens interpolated into copy-pasteable next-step hints. |
666
694
  | `src/state.ts` | SDK seam for `mu state`: `loadWorkstreamSnapshotFast` (pure SQL, TUI 1s tick), `loadWorkstreamSnapshotSlow` (subprocesses), `mergeSnapshotFastSlow`, `loadWorkstreamSnapshot`. Also parses exact `REMOTE:` task-note inventory and `REMOTE_BASE:` dispatch metadata. Opt-in: `withDirty`, `withDoctor`, `withRecentCommits`, `withAllTasks`. |
@@ -670,7 +698,7 @@ separately below.
670
698
  | `src/cli/tui/*.tsx` | The interactive ink TUI, lazy-imported by `src/cli/state.ts`. **The only place ink/react are imported** — a ROADMAP pledge. Per-file roles: [§ TUI architecture](#tui-architecture). |
671
699
  | `src/cli/tasks/*.ts` | Sub-cluster of the `mu task` namespace: `queries.ts` (list/next/owned-by + `mu me tasks` / `mu me next`), `lifecycle.ts` (close/open), `edit.ts`, `edges.ts` (+ delete cascade preview), `claim.ts`, `tree.ts`, `wire.ts` (Commander glue). |
672
700
  | `src/index.ts` | SDK entrypoint (re-exports) |
673
- | `skills/mu/SKILL.md` | Bundled skill teaching the LLM the model + verb list + jq pipelines |
701
+ | `skills/mu/SKILL.md` | Bundled skill teaching the LLM the orchestration model and operational traps; `mu --help` owns the verb reference. |
674
702
 
675
703
  ### What the rows above leave out
676
704
 
@@ -756,9 +784,8 @@ DB must never sit in `MU_SYNC_DIR`:
756
784
  better-sqlite3's `db.transaction(fn)()` wrapper.
757
785
  4. **Executes the operation** — agent ops shell out to tmux (and to
758
786
  jj/sl/git for workspaces); task ops are pure SQL.
759
- 5. **Reconciles with reality** — for read-paths that need accuracy
760
- (`mu agent list`, state views), queries tmux for live pane
761
- state and updates the DB (ghost prune + status detect).
787
+ 5. **Reconciles with reality** — read paths prune ghosts and surface
788
+ orphans; runtime agent state is read from murmur or herdr without a DB write.
762
789
  6. **Records ops** — automatically, per [§ The ops log](#the-ops-log-read-this-first);
763
790
  machine-local changes go through `emitEvent`. `mu log --tail`
764
791
  subscribers see the new ops on the next 1-second poll.
@@ -773,7 +800,6 @@ The extension points. A new impl of each is small.
773
800
  | Seam | Add a new impl by... |
774
801
  | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
775
802
  | `VcsBackend` | Implementing `detect / createWorkspace / freeWorkspace / isClean / commitsBehind / rebaseTo / commitsSinceBase / recentCommits / showCommit` (~80–150 LOC; jj/sl/git/none are working examples) |
776
- | Per-CLI `Detector` | Adding patterns to `detectPiStatus` (vanilla pi `to interrupt)`; pi-meta + every TUI wrapper covered by Braille spinner glyph fallback `[\u2800-\u28FF]`) |
777
803
  | New typed verb | An SDK function in the relevant `src/*.ts`; a `cmd<Verb>` in the matching `src/cli/<namespace>.ts`; one commander block in `buildProgram()`, wrapped in `handle()` and routed through `printNextSteps` |
778
804
  | New schema migration| Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts` and mirror the shape in `CURRENT_SCHEMA`. Keep startup migration-free; extend the retained fresh-target sidecar only when an old released schema needs a bridge. Recipe: [scripts/README.md](../scripts/README.md) |
779
805
  | New syncable field | Add the column to a portable table, extend the capture trigger's changed-column comparison, confirm the apply path writes it. The UPDATE trigger MUST emit only changed columns — a full-row payload silently regresses field merge to row-level LWW |
package/docs/ROADMAP.md CHANGED
@@ -85,41 +85,6 @@ These have a clear design but haven't yet hit promotion criterion
85
85
  1 (friction in ≥2 real workflows). They earn implementation when
86
86
  real use surfaces them.
87
87
 
88
- ### Per-CLI status detection (claude, codex, …)
89
-
90
- **Partly superseded on the herdr backend**, which classifies panes
91
- natively across the agent kinds it knows — mu takes its word there and
92
- skips scrollback scraping entirely. This item stays live for the tmux
93
- backend, where mu has to do its own detecting.
94
-
95
- mu is a pi orchestrator. The Braille-spinner fallback catches every
96
- TUI wrapper using standard spinner glyphs (U+2800–U+28FF), so
97
- pi-meta + solo + many vanilla TUIs (claude, codex) work without a
98
- per-CLI detector.
99
-
100
- For patterns the spinner fallback misses (permission prompts,
101
- specific busy markers), a per-CLI `Detector` registry keyed by
102
- CLI name (~50 LOC per CLI) is the obvious shape. Promote when a
103
- real specific-prompt-misclassification surfaces.
104
-
105
- Pattern sketch:
106
-
107
- | CLI | Busy patterns | Permission patterns |
108
- | ------ | -------------------------------------- | --------------------------------------------------------- |
109
- | Claude | `to interrupt`, `\(.*[↑↓].*tokens\)` | `Allow once`, `Allow for this session`, `Esc to cancel` |
110
- | Codex | `esc to interrupt)`, `to cancel` | `enter to confirm`, `enter to submit \| esc to cancel` |
111
- | Pi | (well-known mu-defined marker) | (well-known mu-defined marker) — shipped |
112
-
113
- Critical subtleties any new detector must keep:
114
-
115
- - **Tail-window extraction**: take last ~100 lines, strip trailing
116
- blanks, then take last ~20. Already implemented for pi in
117
- `src/detect.ts`; the registry version factors it out.
118
- - **Permission detection uses a narrower window than busy
119
- detection** to prevent already-answered prompts re-triggering.
120
- - **Permission overrides busy** — if a permission prompt is
121
- visible, agent is `NeedsPermission`, not `Busy`.
122
-
123
88
  ### A third mux backend (zellij, wezterm, kitty)
124
89
 
125
90
  **Not promoted.** tmux and herdr are two real implementations, which
@@ -152,76 +117,22 @@ Listed so we don't pretend they're settled.
152
117
 
153
118
  ---
154
119
 
155
- ## murmur, and remote agents (noted 2026-08-28, revised 2026-09-02)
156
-
157
- [murmur](https://github.com/mu-crew/murmur) is a sibling tool at
158
- `~/hacking/murmur`, on npm as `@mu-crew/murmur`: every coding
159
- agent on every machine in one attention-sorted list you can jump
160
- from. It is **built and in daily use** — the original note here
161
- described it as four design documents, and two of its conclusions
162
- moved since:
163
-
164
- - **Current state only, not an event log.** Each node publishes one
165
- complete snapshot; a peer's answer is replaced in one write and
166
- absence means absence. No history, watermarks or HLC. murmur's own
167
- architecture says a generic `put`/`del` op-log "would simply *be*
168
- `mu`" — so the two differ in substrate now, not just scope.
169
- - **No control channel, ever.** murmur observes and jumps; "not a
170
- remote terminal" is a stated non-goal. The phase-2 prompt/abort
171
- surface this note once counted on will not come from there.
172
-
173
- **What it would give mu**, both prerequisites for `mu spawn --on
174
- <host>`: **global agent addressing** (`agents.pane_id` is
175
- machine-local; murmur's `(host_id, agent_id)` is not, and identity is
176
- kept separate from `session/window/pane` so it survives a move), and
177
- **remote observation** (read the fold, get agents on other machines
178
- for free — including ones mu never spawned). It would not give mu the
179
- control channel; remote *driving* is unbuilt in both tools.
180
-
181
- **The cheap seam already works.** murmur's pi extension reads
182
- `MU_MANAGED_AGENT` / `MU_AGENT_NAME` / `MU_WORKSTREAM` / `MU_ROLE`
183
- off the pane and marks such agents `orchestrated` — "crew" — hidden
184
- unless `blocked` or `crashed`, since a supervisor consumes anything
185
- else. mu already exports the first three (`src/agents/spawn.ts`,
186
- `paneEnv`) and did nothing. Zero lines: the baseline any tighter
187
- coupling has to beat. `MU_ROLE` is the one murmur reads and mu does
188
- NOT set, so a crew row's role column is always null — a one-line fix
189
- if that column ever proves useful, not a reason to couple further.
190
-
191
- **No conflict with the sync design.** § Rejected sync substrates and
192
- § Explicitly rejected both stand: murmur is a **separate binary**
193
- owning the ssh egress, and if mu ever consumes it, it does so by
194
- reading a local store, never by growing a network stack. A merge must
195
- keep the mu half transport-free. murmur independently reached mu's
196
- two conclusions from mu's own code: ambient rather than daemon (a
197
- collect rides ordinary invocations, rate-limited with jitter so the
198
- status bar cannot drive an ssh fan-out) and sync must never fail a
199
- command (every peer error caught and categorised).
200
-
201
- **Why not merge now.** Remote observation is the easy half and is now
202
- shipped; orchestration is the hard half and murmur removes none of
203
- it. Repos and credentials must exist on the target, worktree setup is
204
- a local filesystem operation, cherry-pick handoff assumes artifacts
205
- move between machines, and "which node runs this task" is a scheduler
206
- question mu does not have.
207
-
208
- **The step that tests it** is now available, not scheduled: make mu a
209
- *client* — drop its own status tracking and read murmur's snapshot.
210
- Small change, real experiment. But `paneStatus()` / `src/detect.ts`
211
- is not hurting, and trading a working local detector for a
212
- cross-machine dependency needs a reason from real use (promotion
213
- criterion 1), not the mere existence of the alternative. Keep the
214
- *observe* vs *orchestrate* seam in mind even then — it is what lets
215
- murmur watch agents mu never spawned, and a merge should say why it
216
- beats that.
217
-
218
- **Doors already open**, free: same language and runtime (TypeScript
219
- on npm, since pi is an npm package), `(host_id, agent_id)` addressing
220
- mu would need anyway, and the `MU_*` env contract.
221
-
222
- **Do not** start the merge, add ssh to mu, or fold murmur's schema
223
- into `mu.db` on the strength of this note. It records an intention
224
- and its preconditions.
120
+ ## murmur and remote agents
121
+
122
+ [murmur](https://github.com/mu-crew/murmur) is a separate binary for
123
+ current agent state across machines. It owns collection and ssh egress;
124
+ mu remains transport-free and owns the task DAG, claims, workspaces, and
125
+ panes.
126
+
127
+ **Done in 2.0:** mu reads murmur for agent state. See
128
+ [Agent state from murmur](specs/2026-09-28-agent-state-from-murmur.md).
129
+
130
+ ---
131
+
132
+ ### Next schema change
133
+
134
+ Drop the deprecated `agents.status` column at the first v11 migration.
135
+ In 2.0 it remains for schema compatibility and always contains `spawning`.
225
136
 
226
137
  ---
227
138
 
@@ -275,3 +186,7 @@ Listed so we don't rediscover them.
275
186
  - **A "hosted" mu** — your machine is the deployment.
276
187
  - **Anthropomorphic agent names (`alice`, `bob`)** — use
277
188
  role-based names (`worker-1`, `reviewer-1`).
189
+ - **Pane scraping as an agent-state fallback** — absence of a state source is `unknown`, not a guessed state.
190
+ - **Storing runtime agent state in `mu.db`** — the copy goes stale; murmur or herdr owns the current reading.
191
+ - **Reading murmur's `state.db`** — use murmur's stable public contract, not its private schema.
192
+ - **murmur as a hard dependency** — installs that only use the task graph do not need murmur or its native dependencies.
@@ -8,7 +8,7 @@ A practical, copy-pasteable tour of mu. Terms are canonical — see
8
8
  > **Status:** 1.0 (pre-release). Typed verbs span 7 namespaces
9
9
  > (`workstream`, `agent`, `task`, `workspace`, `log`, `me`, `db`) plus
10
10
  > bare top-level verbs (`state`, `doctor`, `sql`, `undo`, `sync`,
11
- > `rebuild`). Every verb accepts `--json`. Schema v10. See
11
+ > `rebuild`). Every verb accepts `--json`. Schema v11. See
12
12
  > [CHANGELOG.md](../CHANGELOG.md).
13
13
 
14
14
  **In a hurry? Start at [§ 0. Common scenarios](#0-common-scenarios).**
@@ -240,6 +240,7 @@ environment
240
240
  $TMUX : set
241
241
  $TMUX_PANE : %21
242
242
  $MU_SESSION : auth
243
+ agent state : ok agent state from murmur 1.0.0
243
244
 
244
245
  db
245
246
  path : /tmp/mu-play.db
@@ -385,11 +386,12 @@ table/key/field is the reproduction. See
385
386
 
386
387
  ### 0.6 Upgrading an older DB
387
388
 
388
- `mu` refuses to open a pre-v10 DB (`SchemaTooOldError`, exit 4) and
389
- leaves the file alone. The retained `scripts/migrate.ts` sidecar
390
- auto-detects v7, v8, or v9 and writes a fresh v10 DB. It never migrates
391
- in place. Legacy `REJECTED` / `DEFERRED` tasks become `OPEN` with a
392
- migration note while their original op payloads remain unchanged.
389
+ `mu` refuses to open a pre-v11 DB (`SchemaTooOldError`, exit 4) or a DB
390
+ newer than it understands (`SchemaTooNewError`, exit 4) and leaves the
391
+ file alone. The retained `scripts/migrate.ts` sidecar auto-detects v7,
392
+ v8, v9, or v10 and writes a fresh v11 DB. It never migrates in place.
393
+ Legacy `REJECTED` tasks become `CLOSED/wontfix` and `DEFERRED` tasks
394
+ become `OPEN/parked`; their original op payloads remain unchanged.
393
395
  Pre-1.0 archives restore as live workstreams under their original
394
396
  `source_workstream` names unless you pass `--drop-archives`.
395
397
 
@@ -469,11 +471,11 @@ alias mu="node $PWD/dist/cli.js"
469
471
  See [README.md § Install](../README.md#install) for the full set of
470
472
  install patterns.
471
473
 
472
- mu requires a terminal multiplexer. tmux ≥ 3.0 is the complete
473
- backend and what the rest of this guide assumes; herdr is supported
474
- for topology but not yet for spawn/send/read (see
475
- [§ 20](#20-multiplexer-backends-tmux-and-herdr)). Make sure you're
476
- inside a session before proceeding:
474
+ mu requires a terminal multiplexer. tmux ≥ 3.0 is the backend the rest
475
+ of this guide assumes; herdr also supports spawn, send, and read (see
476
+ [§ 20](#20-multiplexer-backends-tmux-and-herdr)). On tmux, install
477
+ murmur for runtime agent state. Make sure you're inside a session before
478
+ proceeding:
477
479
 
478
480
  ```bash
479
481
  tmux # if you're not already in one
@@ -1042,7 +1044,7 @@ convention):
1042
1044
  | Slot | Card | Toggle | Popup | Content |
1043
1045
  | ---- | ------------- | ------ | --------- | ---------------------------------------------------- |
1044
1046
  | 0 | Commits | `0` | `Shift+0` | Recent project-root commits (git / jj / sl) |
1045
- | 1 | Agents | `1` | `Shift+1` | Active agents + status + cli + role |
1047
+ | 1 | Agents | `1` | `Shift+1` | Active agents + runtime state + cli + role |
1046
1048
  | 2 | Tracks | `2` | `Shift+2` | Parallel tracks (union-find clusters) |
1047
1049
  | 3 | Ready (Tasks) | `3` | `Shift+3` | Ready-to-claim tasks (no open blockers) |
1048
1050
  | 4 | Activity log | `4` | `Shift+4` | Recent ops rendered as prose |
@@ -1207,6 +1209,7 @@ for the newly active workstream.
1207
1209
  | filter | `Esc` | cancel (clear query) |
1208
1210
  | filter | `Enter` | commit (keep filter, return to nav) |
1209
1211
  | task popup| `o` / `i` / `c` | toggle OPEN / IN_PROGRESS / CLOSED |
1212
+ | task popup| `p` / `w` | toggle OPEN/parked / closed-not-done (DAG + All-tasks) |
1210
1213
  | All-tasks | `b` | blocked filter cycle (all → only → hide) |
1211
1214
  | All-tasks | `s` | cycle sort key (roi → recency → age → id) |
1212
1215
  | git-show | `t` | launch `tuicr -r <sha>` (alt-screen handoff) |
@@ -1226,7 +1229,7 @@ and recoverable from any shell.
1226
1229
 
1227
1230
  ## 6. Spawn a crew
1228
1231
 
1229
- For a demo with live status detection, spawn pi agents:
1232
+ For a demo with live agent state, install and link murmur, then spawn pi agents:
1230
1233
 
1231
1234
  ```bash
1232
1235
  mu agent spawn worker-1 --workstream auth-refactor # default --cli is pi
@@ -1250,7 +1253,7 @@ What just happened:
1250
1253
  session
1251
1254
  3. mu set the pane title to `worker-1` via `tmux select-pane -T worker-1`
1252
1255
  — **this is the claim protocol identity**
1253
- 4. mu inserted a row in `agents` with `pane_id=%15`, `status=spawning`
1256
+ 4. mu inserted a row in `agents` with `pane_id=%15`; the deprecated `status` column remains `spawning`
1254
1257
 
1255
1258
  If the DB insert fails after the pane was created, mu kills the pane
1256
1259
  to avoid leaking. If the same name was already taken, mu rejects
@@ -1615,13 +1618,30 @@ enforce these — they're for the agents reading them.
1615
1618
  ## 12. Close out a task
1616
1619
 
1617
1620
  ```bash
1618
- mu task close design # OPEN/IN_PROGRESS → CLOSED
1621
+ mu task close design # OPEN/IN_PROGRESS → CLOSED/done
1619
1622
  mu task close umbrella --if-ready # close ONLY if every blocker
1620
1623
  # is CLOSED; else no-op + list
1621
1624
  # the still-blocking ids
1625
+ mu task close spike --as wontfix --why "superseded by the v2 design"
1622
1626
  mu task open design # CLOSED → OPEN (e.g. closed by mistake)
1627
+ mu task park polish --why "after the release" # OPEN → OPEN/parked
1628
+ mu task unpark polish # OPEN/parked → OPEN/todo
1623
1629
  ```
1624
1630
 
1631
+ Every task carries a **substate** next to its status, rendered as a pair.
1632
+ `OPEN` is `todo` or `parked`; `IN_PROGRESS` is `active`; `CLOSED` is
1633
+ `done`, `wontfix`, `duplicate`, or `superseded`. `--as` picks the closed
1634
+ substate. Anything but `done` requires `--why`, stored as a note in the
1635
+ same transaction. Any `CLOSED/*` satisfies a blocker, so a non-`done`
1636
+ close prints the dependents it unblocked.
1637
+
1638
+ To keep dependents waiting, park the task instead of closing it. A
1639
+ parked task leaves `mu task next` and the ready set, and `mu task claim`
1640
+ refuses it without `--force`. It stays in goals, and a track whose non-closed
1641
+ tasks are all parked is marked `(parked)`. Park refuses `IN_PROGRESS`
1642
+ (release first) and `CLOSED` (open first). Filter with
1643
+ `mu task list --substate parked`.
1644
+
1625
1645
  Both are idempotent (closing an already-CLOSED task prints a no-op and
1626
1646
  exits 0). Owner is left intact — use `mu task release <id>` to clear
1627
1647
  ownership when an agent bails mid-flight. `IN_PROGRESS` flips back to
@@ -1703,7 +1723,7 @@ mu sql "SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY t
1703
1723
  ### `mu agent wait`: the task-less counterpart to `mu task wait`
1704
1724
 
1705
1725
  Scratch helpers usually own no task, so `mu task wait` has nothing to
1706
- watch. `mu agent wait` blocks on the agent's runtime status: an agent
1726
+ watch. `mu agent wait` blocks on the agent's runtime state: an agent
1707
1727
  **fires** when it goes **busy → any other state**. It must be observed
1708
1728
  busy first, so an already-idle agent does NOT fire instantly — you're
1709
1729
  waiting for *this* work to finish. Replaces `sleep` polling loops.
@@ -1719,8 +1739,8 @@ Mirrors `mu task wait`'s shape: `--any`/`--first` fire on the first
1719
1739
  agent (default: all must finish); `--first` prints the firing agent's
1720
1740
  ref; `--json` carries `nextSteps`; refs may be qualified
1721
1741
  `<workstream>/<name>`. Exit codes: `0` met, `5` timeout, `6` a watched
1722
- agent's pane died. Status detection is pi-only (a non-pi pane always
1723
- reads `needs_input`, so it never goes busy and the wait times out).
1742
+ agent's pane died. On tmux, murmur supplies the state. `unknown` never
1743
+ fires the wait; run `mu doctor` for the missing-source reason.
1724
1744
 
1725
1745
  **Don't reach for this verb to catch a worker that asked a question.**
1726
1746
  It fires on `busy → needs_input`, so it does detect the case — but it
@@ -1843,9 +1863,10 @@ For unattended waits, always pass `--on-stall exit`; otherwise the default
1843
1863
  warning leaves the wait polling. Two orthogonal flags govern the behaviour:
1844
1864
 
1845
1865
  * `--stuck-after <seconds>` — the **trigger**. An IN_PROGRESS task
1846
- whose owner has been in `needs_input` for `>= N` seconds is marked
1847
- as needing attention. Default `300` (5 min); pass `0` to disable
1848
- detection entirely (no warn AND no exit).
1866
+ whose owner has been in `needs_input` for `>= N` seconds according
1867
+ to the state source is marked as needing attention. `unknown` never
1868
+ counts. Default `300` (5 min); pass `0` to disable detection entirely
1869
+ (no warn AND no exit).
1849
1870
  * `--on-stall <action>` — the **action** when the trigger fires.
1850
1871
  Two values:
1851
1872
  * `warn` (default) — yellow attention warning to stderr (deduped per
@@ -1954,14 +1975,11 @@ You killed it from another tmux client, or its CLI crashed:
1954
1975
  mu agent list # worker-1's row prunes itself (ghost detected)
1955
1976
  ```
1956
1977
 
1957
- Reconciliation runs on every `mu agent list` / `mu`. Three steps:
1978
+ Reconciliation runs on every `mu agent list` / `mu`. Two steps:
1958
1979
 
1959
1980
  1. **Prune ghost rows** — DB row whose `pane_id` no longer exists in
1960
- tmux gets deleted
1961
- 2. **Detect status from scrollback** — for survivors, capture the
1962
- pane and re-derive status (busy / needs_input / needs_permission /
1963
- spawning) per the pi-status detector
1964
- 3. **Surface orphan panes** — panes in the workstream's session whose
1981
+ the mux gets deleted
1982
+ 2. **Surface orphan panes** — panes in the workstream's session whose
1965
1983
  `pane.command` looks like an agent CLI but that aren't in the
1966
1984
  registry. **Not** auto-adopted; mu lists them under "Orphan panes"
1967
1985
  with the `mu agent adopt <pane-id>` hint
@@ -2367,26 +2385,29 @@ winner, and the newer HLC takes it.
2367
2385
 
2368
2386
  ---
2369
2387
 
2370
- ## 15.7 Upgrading a v7, v8, or v9 DB
2388
+ ## 15.7 Upgrading a v7, v8, v9, or v10 DB
2371
2389
 
2372
- `mu` refuses to write a pre-v10 DB and leaves it untouched. Use the
2390
+ `mu` refuses to write a pre-v11 DB and leaves it untouched. Use the
2373
2391
  single retained sidecar against a backup:
2374
2392
 
2375
2393
  ```bash
2376
2394
  DB=${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}
2377
2395
  BACKUP="$HOME/mu-old-backup-$(date +%Y%m%d-%H%M%S).db"
2378
2396
  sqlite3 "$DB" ".backup '$BACKUP'"
2379
- npx tsx scripts/migrate.ts "$BACKUP" --out "${DB}.v10"
2380
- MU_DB_PATH="${DB}.v10" mu doctor --deep
2381
- mv "$DB" "${DB}.old-kept" && mv "${DB}.v10" "$DB"
2397
+ npx tsx scripts/migrate.ts "$BACKUP" --out "${DB}.v11"
2398
+ MU_DB_PATH="${DB}.v11" mu doctor --deep
2399
+ mv "$DB" "${DB}.old-kept" && mv "${DB}.v11" "$DB"
2382
2400
  mu doctor
2383
2401
  ```
2384
2402
 
2385
- The script auto-detects v7, v8, or v9, opens it read-only, compares its
2386
- SHA-256 before and after, and never overwrites a target unless `--force`
2387
- is explicit. v9 history is retained unchanged; legacy statuses normalize
2388
- only while projecting into v10, with one durable migration note per
2389
- currently affected task. Valid v9 agents, workspaces, owners, peer
2403
+ The script auto-detects v7, v8, v9, or v10, opens it read-only, compares
2404
+ its SHA-256 before and after, and never overwrites a target unless
2405
+ `--force` is explicit. v9/v10 history is retained unchanged; legacy
2406
+ statuses map onto substates (`REJECTED` → `CLOSED/wontfix`, `DEFERRED` →
2407
+ `OPEN/parked`) while projecting into v11, and no migration notes are
2408
+ written. Where an undo or a note hid the legacy status, the script
2409
+ records a `migrate.substate` op; `--recover <db>` reruns that recovery
2410
+ on an already-migrated DB. Valid v9 agents, workspaces, owners, peer
2390
2411
  watermarks, and machine identity carry across, but pane ids and absolute
2391
2412
  workspace paths cannot be proven live until `mu doctor` reconciles them.
2392
2413
  The v7/v8 path keeps the older conservative omissions for agents and
@@ -2449,7 +2470,7 @@ rm -f /tmp/mu-demo.db
2449
2470
  set on spawn. Two agents cannot claim the same task.
2450
2471
 
2451
2472
  Everything else (`mu sql`, send/read, the bracketed-paste protocol,
2452
- ghost reconciliation) is plumbing in service of those three.
2473
+ agent-state sources, ghost reconciliation) is plumbing in service of those three.
2453
2474
 
2454
2475
  ---
2455
2476
 
@@ -2461,7 +2482,6 @@ in real use:
2461
2482
 
2462
2483
  | Want | Workaround | Status |
2463
2484
  | --------------------------------------------- | ----------------------------------------------------------------------- | ------------- |
2464
- | Multi-CLI status detection (per-CLI prompts) | Braille spinner fallback covers pi/pi-meta + every TUI wrapper using standard spinner glyphs. Per-CLI permission-prompt patterns are pi-only. | partially shipped |
2465
2485
  | Pi extension (typed tools, HUD, wakeups) | `mu state --tui` (interactive) covers the dashboard use-case; plain `mu state` (static) is the `watch` / `tmux display-popup` / `status-right` substrate. Other extension tools deferred. | partially shipped |
2466
2486
  | Markdown agent-definition discovery | Spawn accepts `--cli` and `--command` directly; no template registry | dropped |
2467
2487
  <!-- doc-cli-drift:skip-start -->
@@ -2504,7 +2524,8 @@ your platform.
2504
2524
 
2505
2525
  mu drives exactly one multiplexer per invocation. tmux is the
2506
2526
  incumbent; [herdr](https://github.com/herdrdev/herdr) is the second
2507
- backend, and spawn, send, read and status detection all work on it. The
2527
+ backend. Spawn, send, and read work on both; agent state comes from herdr
2528
+ on herdr and from murmur on tmux. The
2508
2529
  remaining gaps are narrow and listed under
2509
2530
  [Known limits on herdr](#known-limits-on-herdr) — the notable one is
2510
2531
  that `mu agent kick` is Linux-only there.
@@ -2554,7 +2575,7 @@ The backend is resolved once per process and cached.
2554
2575
  | Attach hint | `tmux a -t mu-<ws>` | `herdr session attach mu-<ws>` |
2555
2576
  | Pane borders | 4-side border showing agent name + status glyph | no-op — herdr owns its pane chrome; mu-managed panes carry the label instead |
2556
2577
  | Layout | `select-layout` | no-op — herdr splits are explicit, geometry is yours |
2557
- | Status detection | scrollback scraping (`src/detect.ts`) | herdr classifies panes natively via `paneStatus()`; the scraper is bypassed |
2578
+ | Agent state | murmur pane options for local agents; `murmur status --json` for remote agents | herdr reports state via `paneStatus()` |
2558
2579
  | Focus | mu creates detached | `--no-focus` on every mutating call, always. `detached: false` still gets you a detached workspace; run `herdr workspace focus` yourself. |
2559
2580
  | Isolation seam | `MU_TMUX_SOCKET` (`-L <name>`) | `MU_HERDR_SESSION` (`--session <name>`, its own socket) |
2560
2581
  | `mu agent send` / `read` | six-step paste/Enter protocol | one atomic `agent prompt --wait` |