@martintrojer/murmur 0.2.1 → 0.2.2

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/ARCHITECTURE.md CHANGED
@@ -354,7 +354,7 @@ where the system interpreter is least yours to touch.
354
354
  "murmur_snapshot": 1,
355
355
  "host_id": "1d2ee96e-3a94-41b2-90fa-5f1ee2f04276", // from identity.json
356
356
  "display_name": "mtrojer-mac",
357
- "murmur_version": "0.2.1", // read from package.json, never restated
357
+ "murmur_version": "0.2.2", // read from package.json, never restated
358
358
  "generated_at": 1788105698997, // this node's clock at build time
359
359
  "panes": [
360
360
  {
@@ -611,6 +611,83 @@ rendering a picker needs targets, and only ones it can reach. Identity is
611
611
  node's `host_id` and display name, which `peer add` records immediately rather
612
612
  than throwing away.
613
613
 
614
+ Asymmetry is design with an unreported consequence: **if B does not peer A, B's
615
+ picker cannot see A's agents, and no local surface can say so.** From A
616
+ everything reads healthy, because from A it is.
617
+
618
+ `murmur doctor` surveys each peer over ssh and reports it. Two calls per peer —
619
+ `murmur export` for the `host_id`, `murmur peer list --json` for the roster —
620
+ because a snapshot structurally cannot carry a roster (below) and `peer list`
621
+ carries no `host_id`. Identity is compared on `host_id` only: names are local
622
+ handles and `hostname` can be a container id, so comparing by name would report
623
+ naming drift as asymmetry and miss genuine duplicates.
624
+
625
+ Asymmetry is *reported*, never called a fault, and exits 0. Flagging the normal
626
+ case — the laptop and the NAT'd server above — would train the operator to ignore
627
+ the command. Only a duplicate `host_id` and a snapshot-version mismatch are
628
+ problems, because both are murmur behaving provably wrongly. Skew is not
629
+ recomputed: `doctor` calls the same `versionCell` `peer list` uses.
630
+
631
+ "Island" is scoped to one hop and says so: *no peer that this node surveyed peers
632
+ this host*, not "no node in the fleet". `doctor` asks its own peers and stops —
633
+ multi-hop survey is a crawler, needing loop detection, a depth bound and a story
634
+ for a node reachable from B but not from here, all to report on machines this
635
+ node cannot fix anyway.
636
+
637
+ `doctor --topology` adds the fact hub advice needs and murmur otherwise lacks:
638
+ who can reach whom. One `ssh A "ssh B true"` per ordered pair — a bare `true`, so
639
+ it measures transport alone, since "unreachable" and "reachable but murmur
640
+ missing" have different fixes.
641
+
642
+ Which node *can* hub is then a set intersection over the matrix, not a
643
+ preference. A spoke counts only when reachability is proven in **both**
644
+ directions, since the spoke collects from the hub and the hub from the spoke.
645
+ When no node qualifies, nothing is recommended and the partition is reported.
646
+
647
+ Measured while building it: on the author's fleet two peers reach nothing, no
648
+ peer can resolve this node's own display name, and no whole-fleet hub exists. A
649
+ recommendation would have been wrong.
650
+
651
+ A failed probe is not a negative. It becomes `unreachable` only when the target
652
+ is demonstrably up — it answered this node's survey seconds earlier — and
653
+ otherwise it is `unknown`, because a firewall and a sleeping laptop are
654
+ indistinguishable from one dial and hub advice built on the confusion would flip
655
+ between runs as machines sleep. Nothing a survey learns is written to the store:
656
+ it is a diagnostic that reaches out, not state murmur caches, and caching it
657
+ would make `peer list` report facts no collect established.
658
+
659
+ **The snapshot carries no peer roster, deliberately.** Publishing one would be
660
+ the obvious way to make the fleet self-describing, and it is refused: a snapshot's
661
+ contract is "my panes, complete and authoritative", which is exactly what makes
662
+ "absent from a snapshot means absent" true. A roster is neither complete nor
663
+ authoritative — it is one node's local configuration, and its truth lives on the
664
+ node being described rather than the one describing. Putting membership into that
665
+ document would also make every membership question a format question, needing a
666
+ version bump and a coordinated fleet upgrade, which is a heavy price so soon
667
+ after 0.2.1 given that `parseSnapshot` rejects a mismatched version outright.
668
+ Surveying over ssh needs no format change and no fleet-wide upgrade, and it is
669
+ honest about what it is: a question asked now, not a fact murmur stores.
670
+
671
+ **Only `collect` could ever be relayed, which is why there is no broker.** Three
672
+ surfaces need a path to a pane's host, and they are not alike:
673
+
674
+ | surface | needs |
675
+ |---|---|
676
+ | collect | `ssh <target> murmur export` |
677
+ | glance (the picker's preview) | `ssh <target> tmux capture-pane` |
678
+ | jump | `ssh <target>`, interactively |
679
+
680
+ Glance and jump are inherently point-to-point: a captured pane and an interactive
681
+ session cannot be served by a third party holding neither. So a relay could only
682
+ deduplicate `collect` — and `collect` is the cheap one, ~400 bytes per pane and
683
+ ~1.2KB per snapshot, measured.
684
+
685
+ A broker would therefore dedupe the cheapest of the three, leave both interactive
686
+ paths untouched, and add a daemon, a port and a second auth story to do it. That
687
+ is why gossip and a NATS-style bus lost: they address payload, and payload was
688
+ never the problem. The cost was one forked ssh per peer per repaint, fixed by
689
+ throttling — see the collect floor in limitation 3.
690
+
614
691
  **Zero knobs.** Every exported setting is one the user can get wrong invisibly.
615
692
  Two are irreducible: `peers` (only the operator knows their fleet) and `theme`.
616
693
  Collection concurrency, deadlines and the staleness threshold are constants. The
@@ -639,83 +716,38 @@ deferral is the main reason murmur is small.
639
716
 
640
717
  ## Why not something else
641
718
 
642
- Investigated against herdr 0.8.2, a T3 Code checkout, and `mu`, all built and
643
- run locally rather than judged from their READMEs.
644
-
645
- | | multi-machine view | pi support | state source |
646
- | --- | --- | --- | --- |
647
- | herdr | no 1:1, planned, blocked | yes | screen-scraped |
648
- | T3 Code | no "unbuilt" by its own docs | no — 14-method adapter | driven |
649
- | mu | state sync yes, agents no | yes | reported |
650
- | **murmur** | **yes** | **yes, in-process** | **reported from inside** |
651
-
652
- ### herdr
653
-
654
- A Rust terminal multiplexer built for coding agents: workspaces, tabs, panes, a
655
- per-pane `idle/working/blocked/done` sidebar, a socket API. Evaluated as a tmux
656
- replacement and rejected on its own terms.
657
-
658
- On multi-machine it is strictly 1:1. `--remote <target>` takes a single target,
659
- no subcommand has a `--host` flag, and `--remote` *replaces* the view rather
660
- than adding to it: two machines means two sessions and a full switch between
661
- them. Multi-client is the maintainer's stated top priority, gated behind a
662
- long-running server/client refactor.
663
-
664
- Waiting would not help. The scope is one client attaching to multiple *herdr*
665
- servers, so every machine must run herdr including machines where you cannot
666
- choose the multiplexer. And herdr detects state by matching terminal output, so
667
- adopting it trades reporting from inside the agent for screen-scraping.
668
-
669
- Taken from it: integration installs that write hooks into each agent's own
670
- config directory (`murmur link pi`), reusing one authenticated connection, and
671
- its own stated non-goals: no merged PTYs across machines, no moving work between
672
- hosts, host as a lightweight label. Rejected: the always-present sidebar, not
673
- for its ~4 columns but because it is fixed to one edge and cannot become a
674
- horizontal strip, while a status row is overhead already paid.
675
-
676
- ### T3 Code
677
-
678
- An "agent harness control surface": a server owning agent sessions plus web,
679
- desktop and mobile clients over one RPC WebSocket.
680
-
681
- Its remote access is well ahead of herdr: direct ws/wss, bearer pairing, relay
682
- tunnels, mesh-VPN serve and desktop-managed SSH, all shipped. But the aggregated
683
- view is unbuilt by its own internals docs — multiple live connections exist, a
684
- fused cross-machine overview does not.
685
-
686
- It also does not support pi, and adding it is expensive: a provider needs a
687
- driver plus a fourteen-method adapter, and the reference implementation is over
688
- 1700 lines. murmur's adapter problem is smaller structurally: T3 Code drives an
689
- agent it does not live inside, while murmur's extension runs *in process* and
690
- calls the store directly.
691
-
692
- Taken from it: the rule that *remoteness is expressed at the connection layer,
693
- never by splitting the runtime* (murmur's channel seam is exactly this),
694
- transport is not an identity, and environment identity as a stable UUID rather
695
- than a hostname.
696
-
697
- ### mu
698
-
699
- An agent orchestrator: workstreams, a task DAG, agents in panes, isolated
700
- workspaces. It solved machine identity and cross-machine sync for its own
701
- problem, which is a genuinely harder one: mu replicates *writes* from many
702
- nodes, so it needs an op-log and per-peer watermarks.
703
-
704
- Two ideas taken directly. Sync is ambient rather than a daemon: every invocation
705
- syncs before the verb, and no watcher outlives the command. And sync never fails
706
- a command — every ambient entry point is total, and a dead peer warns and
707
- returns.
708
-
709
- One idea deliberately not taken: mu's generic replicated KV. A generic op-log
710
- needs conflict resolution, which single-writer-per-node caching skips entirely,
711
- and a murmur with `put`/`del` over arbitrary entities would just *be* mu. The
712
- same reasoning is why murmur ships no log of its own — nothing here is authored
713
- by two parties, so there is nothing to merge.
714
-
715
- **Relationship:** murmur observes, mu orchestrates. Merging is plausible later,
716
- since murmur would give mu global agent addressing and remote observation. But
717
- remote *orchestration* is much harder than remote observation, and none of it is
718
- murmur's problem.
719
+ Version numbers and roadmap status are deliberately absent here: they were true
720
+ on the day they were checked and are the first thing to rot. What follows is the
721
+ structural difference, which does not move.
722
+
723
+ **Terminal multiplexers built for agents** (herdr and similar) own the panes and
724
+ infer state by matching terminal output. Two consequences: every machine must run
725
+ that multiplexer, which is not a choice you always have, and state is scraped
726
+ rather than reported. murmur takes tmux as given and has the agent report from
727
+ inside its own process.
728
+
729
+ **Harness control surfaces** (T3 Code and similar) run a server that owns agent
730
+ sessions, with clients over RPC. Their remote *access* is strong. The structural
731
+ cost is the adapter: driving an agent you do not live inside needs a per-harness
732
+ driver, where murmur's extension runs in-process and calls the store directly.
733
+
734
+ **Orchestrators** (`mu`) place work and replicate writes from many nodes, so they
735
+ need an op-log, watermarks and conflict resolution. murmur observes and never
736
+ places work, so single-writer-per-node caching removes all of it. **murmur
737
+ observes, mu orchestrates.** Merging is plausible murmur would give mu global
738
+ agent addressing but remote orchestration is a much harder problem than remote
739
+ observation, and it is not murmur's.
740
+
741
+ Taken from these tools, and worth stating because each is load-bearing above:
742
+ integration installs that write hooks into each agent's own config (`murmur link
743
+ pi`); reusing one authenticated connection; remoteness expressed at the
744
+ connection layer rather than by splitting the runtime; identity as a stable UUID
745
+ rather than a hostname; and ambient sync with no daemon, where every invocation
746
+ syncs before the verb and no watcher outlives the command.
747
+
748
+ Deliberately not taken: a generic replicated KV. A generic op-log needs conflict
749
+ resolution, which single-writer caching skips entirely, and a murmur with
750
+ `put`/`del` over arbitrary entities would just *be* mu.
719
751
 
720
752
  ## The extension's lifecycle assumptions
721
753
 
@@ -871,9 +903,25 @@ each was cheaper to accept than to solve:
871
903
  only peer names and targets salvaged. A node serving the old event format is
872
904
  reported as reachable-but-broken, which is the honest description: the two
873
905
  cannot federate.
874
- 3. **No incremental sync.** Every collect transfers each peer's whole snapshot.
875
- It is bounded by live pane count, so it is small — but it is O(panes) per
876
- tick rather than O(changes).
906
+ 3. **No incremental sync.** Every collect transfers each peer's whole snapshot,
907
+ so it is O(panes) rather than O(changes) — bounded by live pane count, and
908
+ small in absolute terms: ~400 bytes per pane and ~1.2KB for a whole snapshot,
909
+ measured. It is no longer *per tick*, though, and that was the part that
910
+ mattered. Fetch rate used to be tied to redraw rate, because `murmur status`
911
+ collects and tmux re-runs it every `status-interval`; the cost is quadratic in
912
+ a mesh and multiplied again per attached client, since the interval fires per
913
+ client. An ambient collect now skips a peer attempted within
914
+ `COLLECT_FLOOR_MS` ± `COLLECT_JITTER_MS / 2` (30s ± 10s), measured at 1.08s →
915
+ 0.05s for `murmur status` against four real peers. Keyed on
916
+ `last_attempt_at`, not `fetched_at`: the expensive peer is the one that does
917
+ *not* answer, since it holds a forked ssh until `ConnectTimeout`, and keying
918
+ on success would exempt exactly the sleeping laptops the floor exists to stop
919
+ hammering. The jitter is drawn per peer per call and is what stops the floor
920
+ becoming a synchroniser — a bare floor makes a fleet converge on hitting one
921
+ machine in the same instant, permanently. Only ambient callers pass a floor:
922
+ `murmur collect` is a person asking, and the picker is a keypress, including
923
+ its `^r` refresh, where a floor would be a refresh key that silently does
924
+ nothing.
877
925
  4. **No nested agents.** A second live process in one pane is refused and
878
926
  reports nothing. A pane shows at most one agent.
879
927
  5. **No remote liveness inference.** `owner_pid` never crosses the wire, so a
package/CHANGELOG.md CHANGED
@@ -3,6 +3,104 @@
3
3
  Notable changes per release. Written for someone deciding whether to upgrade,
4
4
  so it says what changed for a user rather than listing every commit.
5
5
 
6
+ ## 0.2.2
7
+
8
+ **The picker's name column shows `murmur`, not `hacking/murmur`.** 0.2.1 fixed
9
+ *which* source a name comes from; this fixes how the session name is rendered
10
+ once it gets there. Session names are conventionally paths — that is how `tms`
11
+ and similar tools name them — and the last segment is the part that identifies
12
+ the work, so that is what the name column shows now.
13
+
14
+ That fallback is hit more often than it looks: an agent has no name from a richer
15
+ source unless mu set one or you ran pi's `/name`, because pi's own auto-namer runs
16
+ when a session closes. So a live hand-started pi — exactly the agent you are
17
+ looking at — fell through to the session name. The full path is not lost; it is
18
+ still in the stream column beside the name, so the row is strictly more
19
+ informative than before at the same width. Names you or mu chose are never
20
+ shortened.
21
+
22
+ **`murmur status` no longer reaches your peers on every status-bar repaint.**
23
+ Collection was driven by the tmux status bar — `status` collects, and tmux re-runs
24
+ it every `status-interval` — so fetch rate was tied to redraw rate. On a
25
+ four-peer node `murmur status` took 1.08s; it now takes 0.05s. If your status bar
26
+ felt sluggish, or you saw a lot of ssh processes on a machine doing nothing in
27
+ particular, that is this.
28
+
29
+ An ambient collect now skips a peer attempted in the last 30s (±10s of jitter, so
30
+ a fleet does not converge on hitting one machine at the same instant). Commands
31
+ you run yourself are unaffected: `murmur collect` fetches every peer, and so does
32
+ the picker, including its `^r` refresh. The visible trade is that a peer's data
33
+ can be up to ~40s old in the status bar rather than one tick old; the staleness
34
+ threshold is well above that, so a reachable peer still reads as fresh.
35
+
36
+ **New: `murmur doctor` reports what only a fleet-wide view can see.** Membership
37
+ is per node, so peering a machine does not mean it peers you — and if it does
38
+ not, its picker cannot see your agents. No local surface could tell you that,
39
+ because from your node everything reads healthy. `doctor` surveys each peer over
40
+ ssh and names it, along with duplicate hosts configured twice, snapshot-version
41
+ skew, one machine known under different names, and peers it could not survey at
42
+ all.
43
+
44
+ ```
45
+ $ murmur doctor
46
+ Surveyed 4 peers, 4 answered.
47
+ 5 observations, nothing broken.
48
+
49
+ One-way peering
50
+ These do not peer this node, so their pickers cannot see its agents.
51
+ bubba does not peer mtrojer-mac
52
+ gardenpc does not peer mtrojer-mac
53
+
54
+ Do this
55
+ ssh bubba murmur peer add mtrojer-mac
56
+ ssh gardenpc murmur peer add mtrojer-mac
57
+ ```
58
+
59
+ It is read-only — nothing is written to the store and there is no `--fix`, since
60
+ every repair runs on another machine. Repairs are printed for you to run. Exit
61
+ status is 0 for observations and 1 only for a real problem, so it is safe in a
62
+ script, and asymmetry is an observation: reachability is *meant* to be
63
+ one-directional in places, and a check that failed on the normal case would be a
64
+ check you learn to ignore. `--json` gives the finding list with machine-readable
65
+ severity.
66
+
67
+ Worth knowing before you act on its advice: the suggested commands name your node
68
+ by the name it calls itself, and that name may not resolve from the peer's side.
69
+ On the author's fleet none of them could resolve it, so each suggestion needed an
70
+ address the far host can reach.
71
+
72
+ **New: `murmur doctor --topology` computes which fleet shapes are actually
73
+ possible.** Opt-in, because it costs one ssh dial per ordered pair (4 peers is
74
+ 20) where the survey costs one per peer.
75
+
76
+ ```
77
+ $ murmur doctor --topology
78
+ Reachability 20 ordered pairs probed across 5 nodes
79
+ REACHES CANNOT REACH
80
+ mtrojer-mac all 4 -
81
+ bubba - mtrojer-mac gardenpc linuxpc macmini
82
+ linuxpc gardenpc macmini mtrojer-mac bubba
83
+ macmini bubba gardenpc linuxpc mtrojer-mac
84
+
85
+ Hub linuxpc serves {linuxpc, macmini}, leaves out mtrojer-mac, bubba, gardenpc
86
+ ```
87
+
88
+ When no node can hub the fleet it recommends nothing and reports the partition,
89
+ which is the useful half — a hub half the fleet cannot reach is worse than no
90
+ hub. When one exists it prints the commands to build it, and states what a star
91
+ costs: spokes see the hub and the hub sees everyone, but spokes do not see each
92
+ other. A pair whose target was not demonstrably up is `unknown` rather than
93
+ unreachable, so a sleeping laptop is never reported as a firewall.
94
+
95
+ **The doctor report is tables, not prose.** Findings are grouped by kind with the
96
+ shared consequence stated once per group instead of once per row, and every
97
+ suggested command is collected under one deduplicated `Do this` block rather than
98
+ scattered through the text. The widest line went 129 columns to 78.
99
+
100
+ No upgrade coordination needed for any of this: the snapshot format is unchanged,
101
+ and `doctor` works against peers running 0.2.1. A peer too old for `peer list
102
+ --json` is reported as needing an upgrade rather than as broken.
103
+
6
104
  ## 0.2.1
7
105
 
8
106
  **Agents are named by their tmux session, not by whatever process tmux found
package/README.md CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  **Every coding agent you have running, on every machine, in one list.**
4
4
 
5
- You have agents on your laptop, your desktop, and a box somewhere else. One of
6
- them is blocked waiting on you right now. Which one?
5
+ You have agents on your laptop, your desktop, and a box somewhere else. One is
6
+ blocked waiting on you. Which one?
7
7
 
8
- Today you find out by walking the machines. Attach here, glance there, try to
9
- remember where you left that session. murmur answers the question in one
10
- keystroke, then jumps you to the agent on whichever machine it turns out to be.
8
+ Without murmur you walk the machines to find out. murmur answers in one
9
+ keystroke and jumps you to the agent, wherever it is running.
11
10
 
12
11
  ```
13
12
  state agent workstream host age / flags
@@ -17,53 +16,33 @@ keystroke, then jumps you to the agent on whichever machine it turns out to be.
17
16
  · idle worker-2 infra here crew
18
17
  ```
19
18
 
20
- Pick a row and press enter. Local agents are a window switch away; remote ones
21
- open over ssh. The preview beside the list shows the last few lines the agent
22
- printed, so you can tell "waiting on me" from "still thinking" without going
23
- there at all.
24
-
25
- ## Why it exists
26
-
27
- The tools in this space are excellent at one machine and stop there.
28
- [herdr](https://herdr.dev) is strictly one client to one server, and its
29
- multi-client work is blocked behind an unfinished refactor. T3 Code has better
30
- remote *access* than anything else here, but lists the aggregated view as
31
- unbuilt in its own docs. Both infer agent state by matching terminal output.
32
-
33
- murmur takes a different bet. The agent reports its own state from inside the
34
- process, and the machines exchange nothing more complicated than "here is
35
- everything I currently know". Knowing what is happening is the hard part, and reporting
36
- it from inside the agent is what makes it reliable.
19
+ Pick a row, press enter. Local agents are a window switch; remote ones open over
20
+ ssh. The preview shows the last lines the agent printed, so you can tell "waiting
21
+ on me" from "still thinking" without going there.
37
22
 
38
23
  ## What it is
39
24
 
40
- - **A state layer over tmux:** tmux keeps owning your panes. murmur owns the
41
- answer to "what is every agent doing right now".
42
- - **Reported state, not scraped:** a pi extension reports from inside the agent.
43
- Nothing screen-scrapes, and a crash is detected from a pid rather than guessed
44
- from output.
45
- - **Current state only:** each node publishes one complete snapshot of what its
46
- panes are doing right now. No history, no log, nothing to replay — which is
47
- why a peer's whole answer can be replaced in one write and absence means
48
- absence.
49
- - **No daemon, no listening socket, no master:** peers are pulled over ssh when
50
- you run a command. Every node can aggregate; none is special.
51
- - **Fast with one machine:** it replaced a local-only script and got quicker
52
- doing it, ~50 ms to first paint against 250 ms. Configuring zero peers is the
53
- common case, and nothing about it is degraded.
25
+ - **A state layer over tmux.** tmux owns your panes; murmur owns the answer to
26
+ "what is every agent doing right now".
27
+ - **Reported state, not scraped.** A pi extension reports from inside the agent,
28
+ so a crash is detected from a pid rather than guessed from output.
29
+ - **Current state only.** Each node publishes one complete snapshot. No history
30
+ and nothing to replay, which is why a peer's answer is replaced in one write
31
+ and absence means absence.
32
+ - **No daemon, no socket, no master.** Peers are pulled over ssh when you run a
33
+ command. Every node aggregates; none is special.
34
+ - **Fast with one machine.** ~50 ms to first paint. Zero peers is the common
35
+ case and nothing about it is degraded.
54
36
 
55
37
  ## What it is not
56
38
 
57
- It does not orchestrate. It observes and connects, and never places work; that
58
- is [`mu`](https://github.com/martintrojer/mu)'s job.
59
-
60
- It is not a remote terminal. You can glance at a remote pane or jump to it, but
61
- there is no frame streaming and no resize negotiation, which is most of why it
62
- stays small.
63
-
64
- It does not replace your multiplexer. See
65
- [ARCHITECTURE.md](ARCHITECTURE.md#why-not-something-else) for the comparison
66
- against herdr, T3 Code and `mu`.
39
+ - **Not an orchestrator.** It observes and connects, never places work. That is
40
+ [`mu`](https://github.com/martintrojer/mu)'s job.
41
+ - **Not a remote terminal.** Glance at a pane or jump to it; there is no frame
42
+ streaming or resize negotiation. That deferral is most of why murmur is small.
43
+ - **Not a multiplexer.** tmux stays. See
44
+ [ARCHITECTURE.md](ARCHITECTURE.md#why-not-something-else) for how murmur
45
+ relates to adjacent tools.
67
46
 
68
47
  ## Requirements
69
48
 
@@ -180,8 +159,68 @@ picker to a key:
180
159
  murmur peer list # your peers, and when each was last seen
181
160
  murmur peer list --all # also ssh hosts that could become peers
182
161
  murmur peer add devbox # an ssh target; identity is discovered
162
+ murmur doctor # survey each peer over ssh: what only a fleet view shows
163
+ murmur doctor --topology # also probe who can reach whom, and compute hub options
164
+ ```
165
+
166
+ `peer list` reads local state: what you configured, and when you last heard from
167
+ it. `doctor` dials out and asks each peer about *itself* — the only way to see
168
+ what no local surface can: **membership is per node, so peering a machine does
169
+ not mean it peers you.** If it does not, its picker cannot see your agents.
170
+
171
+ `doctor` writes nothing and repairs nothing; it prints the commands. Exit 0 for
172
+ observations, 1 only for a real problem, so it is safe in a script.
173
+
174
+ ```
175
+ $ murmur doctor
176
+ Surveyed 4 peers, 4 answered.
177
+ 5 observations, nothing broken.
178
+
179
+ Not visible to the fleet
180
+ mtrojer-mac none of the 4 surveyed peers can see this node
181
+
182
+ One-way peering
183
+ These do not peer this node, so their pickers cannot see its agents.
184
+ bubba does not peer mtrojer-mac
185
+ gardenpc does not peer mtrojer-mac
186
+
187
+ Do this
188
+ ssh bubba murmur peer add mtrojer-mac
189
+ ssh gardenpc murmur peer add mtrojer-mac
190
+ ```
191
+
192
+ Suggestions name this node as it calls itself, and that name may not resolve from
193
+ the peer's side — hence "check", not "run for you". `peer add` accepts a target
194
+ that does not answer yet and discovers identity on the first collect, so trying
195
+ costs nothing.
196
+
197
+ `--topology` is opt-in: it costs one dial per ordered pair (4 peers is 20) where
198
+ the survey costs one per peer. Plain `doctor` answers "is my fleet mutual?";
199
+ `--topology` answers "what shapes are possible here?", which you ask once while
200
+ setting up.
201
+
202
+ ```
203
+ $ murmur doctor --topology
204
+ Reachability 20 ordered pairs probed across 5 nodes
205
+ REACHES CANNOT REACH
206
+ mtrojer-mac all 4 -
207
+ bubba - mtrojer-mac gardenpc linuxpc macmini
208
+ linuxpc gardenpc macmini mtrojer-mac bubba
209
+ macmini bubba gardenpc linuxpc mtrojer-mac
210
+
211
+ Hub linuxpc serves {linuxpc, macmini}, leaves out mtrojer-mac, bubba, gardenpc
183
212
  ```
184
213
 
214
+ Which node *can* hub is arithmetic on that matrix, not a preference. When none
215
+ qualifies, nothing is recommended and the partition is reported — a hub half the
216
+ fleet cannot reach is worse than no hub. A pair whose target was not
217
+ demonstrably up reads `unknown`, not unreachable: a sleeping laptop and a
218
+ firewall need different fixes.
219
+
220
+ A star also costs something the output states every time it names one: spokes see
221
+ the hub and the hub sees every spoke, but **spokes do not see each other**, since
222
+ `export` publishes local panes only.
223
+
185
224
  Nodes being asleep or switched off is the normal state of a fleet, so nothing
186
225
  warns about it on a polling path: `murmur status` and `murmur pick` stay silent
187
226
  whatever the peers are doing. `murmur peer list` has a LAST SEEN column, and
@@ -192,35 +231,31 @@ could not reach.
192
231
  bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
193
232
  ```
194
233
 
195
- In the picker: `^r` refreshes, `^p` cycles the preview, and `^u` clears the
196
- filter. `M-b` / `M-w` / `M-d` / `M-x` filter to blocked,
197
- running, done or crashed, and `M-a` toggles orchestrated agents in and out of
198
- the list. Alt rather than ctrl because `^b` is tmux's own prefix, which a popup
199
- never receives. Typing matches the agent name, its workstream or tmux
200
- session, and its host, as literal substrings rather than scattered characters.
234
+ In the picker: `^r` refreshes, `^p` cycles the preview, `^u` clears the filter.
235
+ `M-b` / `M-w` / `M-d` / `M-x` filter to blocked, running, done or crashed; `M-a`
236
+ toggles orchestrated agents. Alt rather than ctrl because `^b` is tmux's prefix,
237
+ which a popup never receives. Typing matches agent name, workstream or tmux
238
+ session, and host, as literal substrings.
201
239
 
202
240
  `murmur status` prints per-state counts for a status bar. Everything else is
203
241
  `--help`.
204
242
 
205
243
  ## Jumping to a remote agent
206
244
 
207
- A remote jump opens the `ssh -t <host> tmux attach` in a local tmux session of
208
- its own, named after the peer with a trailing `~`. That session sets two options
209
- on itself, and both are why the jump does not feel like nested tmux:
245
+ A remote jump runs `ssh -t <host> tmux attach` in its own local tmux session,
246
+ named after the peer with a trailing `~`. That session sets two options on
247
+ itself, which is why it does not feel like nested tmux:
210
248
 
211
- - `status off` — no local status bar, so the remote's own bar is the only one on
212
- screen and the jump reads as a full-screen ssh.
213
- - `prefix None` — no local prefix at all, so `^b` goes straight to the remote.
214
- No `^b b`, and no second prefix to learn.
249
+ - `status off` — the remote's bar is the only one on screen.
250
+ - `prefix None` — `^b` goes straight to the remote. No `^b b` to learn.
215
251
 
216
- Both are per-session, so your other sessions keep their prefix and status bar.
217
- When you leave the remote — inner `^b d`, the remote session ending, or the ssh
218
- dropping the wrapper returns you to the exact window you jumped from and
219
- disappears. Jumping to the same host twice reuses the one session.
252
+ Both are per-session, so your other sessions are unaffected. Leaving the remote
253
+ (inner `^b d`, the session ending, or the ssh dropping) returns you to the window
254
+ you jumped from and destroys the wrapper. Jumping to the same host twice reuses
255
+ one session.
220
256
 
221
- The tradeoff: while you are inside the wrapper, the local tmux has no prefix, so
222
- you cannot reach it. If you want an escape hatch that does not involve the
223
- remote, bind one key in the root table:
257
+ The tradeoff: inside the wrapper the local tmux has no prefix, so you cannot
258
+ reach it. For an escape hatch, bind one key in the root table:
224
259
 
225
260
  ```tmux
226
261
  # Alt-Escape detaches out of a murmur wrapper session, and does nothing
@@ -235,7 +270,7 @@ already full-screen, and you land back at your shell prompt on exit.
235
270
 
236
271
  ## Status
237
272
 
238
- **0.2.1.** In daily use on one machine and verified across two over real ssh.
273
+ **0.2.2.** In daily use on one machine and verified across two over real ssh.
239
274
  It is new and not battle-tested. The known gaps and the accepted limitations are
240
275
  listed at the end of [ARCHITECTURE.md](ARCHITECTURE.md#known-gaps).
241
276
 
@@ -246,9 +281,9 @@ shows each peer's version for exactly this. Upgrade the fleet together.
246
281
 
247
282
  ## Documentation
248
283
 
249
- [ARCHITECTURE.md](ARCHITECTURE.md) explains how it works: the three independent
250
- facts the whole model rests on, why it exists rather than the alternatives, what
251
- it deliberately cannot do, and what is unfinished.
284
+ [ARCHITECTURE.md](ARCHITECTURE.md): the three independent facts the model rests
285
+ on, the design choices and what they cost, what murmur deliberately cannot do,
286
+ and what is unfinished.
252
287
 
253
288
  ---
254
289