A bridge-mounted herdr pane should be addressable without a config edit and a restart #743

Open
opened 2026-10-05 05:00:33 +02:00 by ltms · 7 comments
Owner

What the operator asked

Measured today while two interactive sessions on this host tried to exchange one message over the
bridge. The operator's words:

now we have vms with the fleet mcp too, lets try to talk with each other via fleet's channel

and then, on being told it needs a fleet.collaborators entry:

I thought fleet channel now default for all herdr agent tabs with fleet mcp? why do we need to
configure?

because every new herdr tab with agent session asking for a reconfigure -> this is nonsense

What happens today

A herdr pane that mounts the bridge MCP is authenticated, so READ and METRICS work — that is
why fleet_whoami answers and fleet_list returns loopHealth and capacity. It is addressable
by nobody and can address nobody, because both send gates are keyed on being named in config:

  • Authz.java:115 — SEND needs the caller to be primary, architect or collaborator. An observer
    is none of those.
  • Fleetd.java:238-240 — a target is deliverable only if it is an MCP-connected member, a lead, or
    a collaborator. An observer is in none of those three maps.

So an unconfigured pane can read the fleet and nothing else. Two such panes cannot exchange a
single message, in either direction.

Adding the entry also needs a daemon restart: FleetdAssembly.java:271-275 builds a plain
LinkedHashMap of collaborator tab labels once and hands it to LeadTabScanner, which matches
every later scan against that frozen map. ConfigRef.changedSplitKeys does report this correctly,
so nobody is silently misled — the cost is real, not hidden.

The split this misses

Gating authority is right. If mounting the bridge granted SEND, then anyone who mounts it
could task anyone, and the operator loses the say in who tasks whom. CLAUDE.md already states the
principle: "Being named buys you a channel, not authority."

But being addressable is not authority. Today the two are welded together, and that is what the
operator is objecting to. A pane that can be sent to has gained nothing it could abuse: it still
cannot spawn, stop, drain, poll a ticket, or send. Requiring a config edit and a daemon restart
before a lead can say one word to a pane the operator opened by hand buys no safety.

Proposal, for discussion

Separate the two:

  1. Make an observer a valid SEND target — add the observer terminals to the deliverability
    check at Fleetd.java:238-240. A lead could then message any bridge-mounted pane with no
    config at all. The pane still holds only READ/METRICS plus REPLY/ASK for itself, so it
    gains no authority. It would need a route to answer; REPLY on its own pane may already be
    enough, and that wants checking rather than assuming.
  2. Decide whether an observer may SEND back to the lead that messaged it. This is the real
    design question, and it is where the authority line actually sits. A reply-only right — may
    answer a lead that opened the exchange, may not open one — would cover the operator's case
    without handing out SEND.
  3. Make fleet.collaborators hot. Even if 1 and 2 are rejected, the restart is indefensible on
    its own. The scanner re-reads herdr tabs every scanIntervalSeconds already; only the
    label→entry map is frozen. Reading it through the same live supplier shape that
    architects/developers/reviewers already use would drop the restart.

Item 3 stands alone and is the small one. Items 1 and 2 need a decision on the authority line
before any code.

Related

  • fleetd #705 — gave an unplaced pane the observer floor instead of demoting it to worker.
    This ticket is the next question that floor raises: an observer is now correctly described, and
    correctly unable to do anything.
  • fleetd #669 — added fleet.collaborators, the named-peer mechanism this ticket argues is too
    heavy for the common case.
  • fleetd #333 — the reason the frozen-vs-hot reporting exists, and why item 3 is cheap: the
    reporting is already correct, only the freeze needs lifting.
## What the operator asked Measured today while two interactive sessions on this host tried to exchange one message over the bridge. The operator's words: > now we have vms with the fleet mcp too, lets try to talk with each other via fleet's channel and then, on being told it needs a `fleet.collaborators` entry: > I thought fleet channel now default for all herdr agent tabs with fleet mcp? why do we need to > configure? > > because every new herdr tab with agent session asking for a reconfigure -> this is nonsense ## What happens today A herdr pane that mounts the bridge MCP is authenticated, so `READ` and `METRICS` work — that is why `fleet_whoami` answers and `fleet_list` returns `loopHealth` and `capacity`. It is addressable by nobody and can address nobody, because **both** send gates are keyed on being named in config: - `Authz.java:115` — `SEND` needs the caller to be primary, architect or collaborator. An observer is none of those. - `Fleetd.java:238-240` — a target is deliverable only if it is an MCP-connected member, a lead, or a collaborator. An observer is in none of those three maps. So an unconfigured pane can read the fleet and nothing else. Two such panes cannot exchange a single message, in either direction. Adding the entry also needs a **daemon restart**: `FleetdAssembly.java:271-275` builds a plain `LinkedHashMap` of collaborator tab labels once and hands it to `LeadTabScanner`, which matches every later scan against that frozen map. `ConfigRef.changedSplitKeys` does report this correctly, so nobody is silently misled — the cost is real, not hidden. ## The split this misses Gating **authority** is right. If mounting the bridge granted `SEND`, then anyone who mounts it could task anyone, and the operator loses the say in who tasks whom. `CLAUDE.md` already states the principle: "Being named buys you a channel, not authority." But **being addressable is not authority.** Today the two are welded together, and that is what the operator is objecting to. A pane that can be sent to has gained nothing it could abuse: it still cannot spawn, stop, drain, poll a ticket, or send. Requiring a config edit and a daemon restart before a lead can say one word to a pane the operator opened by hand buys no safety. ## Proposal, for discussion Separate the two: 1. **Make an observer a valid `SEND` target** — add the observer terminals to the deliverability check at `Fleetd.java:238-240`. A lead could then message any bridge-mounted pane with no config at all. The pane still holds only `READ`/`METRICS` plus `REPLY`/`ASK` for itself, so it gains no authority. It would need a route to answer; `REPLY` on its own pane may already be enough, and that wants checking rather than assuming. 2. **Decide whether an observer may `SEND` back to the lead that messaged it.** This is the real design question, and it is where the authority line actually sits. A reply-only right — may answer a lead that opened the exchange, may not open one — would cover the operator's case without handing out `SEND`. 3. **Make `fleet.collaborators` hot.** Even if 1 and 2 are rejected, the restart is indefensible on its own. The scanner re-reads herdr tabs every `scanIntervalSeconds` already; only the label→entry map is frozen. Reading it through the same live supplier shape that `architects`/`developers`/`reviewers` already use would drop the restart. Item 3 stands alone and is the small one. Items 1 and 2 need a decision on the authority line before any code. ## Related - fleetd #705 — gave an unplaced pane the `observer` floor instead of demoting it to `worker`. This ticket is the next question that floor raises: an observer is now correctly described, and correctly unable to do anything. - fleetd #669 — added `fleet.collaborators`, the named-peer mechanism this ticket argues is too heavy for the common case. - fleetd #333 — the reason the frozen-vs-hot reporting exists, and why item 3 is cheap: the reporting is already correct, only the freeze needs lifting.
Author
Owner

Correction: the premise of this issue is wrong. An observer is already deliverable.

I wrote the body from a code reading I had not finished. The central claim —

Fleetd.java:238-240 — a target is deliverable only if it is an MCP-connected member, a lead, or
a collaborator. An observer is in none of those three maps.

— is false. An observer is in the first map. FleetMcp.markTrackedCallerPresent
(mcp/FleetMcp.java:845-849) enrols it:

static void markTrackedCallerPresent(Principal caller, MemberPresence presence) {
    if (caller.isSpawnedMember() || caller.isObserver()) {
        presence.markPresent(caller.terminal());
    }
}

Its own javadoc names the three cases it covers as "a worker, an architect, or the
unconfigured-pane floor
". It is called from the request path at mcp/FleetMcp.java:448, and
Fleetd.deliverableTo tests presence.isPresent(target) first. So any pane that has made a single
fleet_* call is an addressable target, with no configuration.

I read the two gates and never read what fills the first one.

Measured, both directions

Two sends from the opus lead to an observer pane (term_65cfe2c6afc7776), with
fleet.collaborators absent from fleetd.yaml — fleet_list reported collaborators: []
throughout.

05:20:53.271  async send task-41558b-3 -> term_65cfe2c6afc7776
05:21:00.850  push: starting reminder loop for lead term_65c806b784dc28   (reply in: 7.5s)
05:21:56.497  async send task-41558b-4 -> term_65cfe2c6afc7776
05:22:09.107  push: starting reminder loop for lead term_65c806b784dc28   (reply in: 12.6s)

The observer answered both with fleet_reply, which resolved each ticket and was collected with
fleet_poll. REPLY is gated on caller.ownsSession(targetSession), and a pane owns itself, so
that direction needed nothing either.

One measurement trap worth recording. The observer first reported that a human had pasted the
message, which would have made the status change worthless as evidence. It also said, correctly,
that it could not tell — herdr injects text as a paste, so an injection and a human paste are
identical from inside the session. The timings above settle it: a 7.5-second round trip rules out a
person relaying a long message and a second model composing a prose reply. The operator was also
asked not to relay the second message, and it arrived regardless. So the "pasted" report was the
observer's inference, and its own caveat was the more reliable half of what it said. I initially
took the conclusion over the caveat.

What actually remains

Items 1 and 3 of the proposal are void — there is nothing to add to the deliverability check, and
making fleet.collaborators hot is not on the path to anything, because the config is not needed
for this at all. The restart cost is real but it buys a different feature.

The one live question is item 2, and it is narrower than the body says:

An observer cannot open an exchange. SEND requires primary, architect or collaborator
(auth/Authz.java:115), so the channel is one-way: a lead may reach any bridge-mounted pane, and
that pane may answer, but it cannot initiate. Whether answering is sufficient, or an unconfigured
pane should be able to open a send to a lead, is the only thing left to decide. That is a genuine
authority question rather than a missing wire, and the operator's objection — that a config edit
per pane is nonsense — is already satisfied for everything except initiation.

Why this got filed wrong

The operator pushed back on the config route twice before I re-read the code. I had a reading that
explained the symptom, so I stopped looking, and each push produced a better workaround instead of a
re-measurement. The shape is the one already recorded on #705 and #726: a true-sounding cause that
fits the symptom ends the search before the real mechanism is found. Here the "cause" was not even
true — it was a map membership I asserted without checking the writer.

## Correction: the premise of this issue is wrong. An observer is already deliverable. I wrote the body from a code reading I had not finished. The central claim — > `Fleetd.java:238-240` — a target is deliverable only if it is an MCP-connected member, a lead, or > a collaborator. An observer is in none of those three maps. — is false. An observer **is** in the first map. `FleetMcp.markTrackedCallerPresent` (`mcp/FleetMcp.java:845-849`) enrols it: ```java static void markTrackedCallerPresent(Principal caller, MemberPresence presence) { if (caller.isSpawnedMember() || caller.isObserver()) { presence.markPresent(caller.terminal()); } } ``` Its own javadoc names the three cases it covers as "a worker, an architect, or **the unconfigured-pane floor**". It is called from the request path at `mcp/FleetMcp.java:448`, and `Fleetd.deliverableTo` tests `presence.isPresent(target)` first. So any pane that has made a single `fleet_*` call is an addressable target, with no configuration. I read the two gates and never read what fills the first one. ## Measured, both directions Two sends from the `opus` lead to an observer pane (`term_65cfe2c6afc7776`), with `fleet.collaborators` absent from `fleetd.yaml` — `fleet_list` reported `collaborators: []` throughout. ``` 05:20:53.271 async send task-41558b-3 -> term_65cfe2c6afc7776 05:21:00.850 push: starting reminder loop for lead term_65c806b784dc28 (reply in: 7.5s) 05:21:56.497 async send task-41558b-4 -> term_65cfe2c6afc7776 05:22:09.107 push: starting reminder loop for lead term_65c806b784dc28 (reply in: 12.6s) ``` The observer answered both with `fleet_reply`, which resolved each ticket and was collected with `fleet_poll`. `REPLY` is gated on `caller.ownsSession(targetSession)`, and a pane owns itself, so that direction needed nothing either. **One measurement trap worth recording.** The observer first reported that a human had pasted the message, which would have made the status change worthless as evidence. It also said, correctly, that it *could not tell* — herdr injects text as a paste, so an injection and a human paste are identical from inside the session. The timings above settle it: a 7.5-second round trip rules out a person relaying a long message and a second model composing a prose reply. The operator was also asked not to relay the second message, and it arrived regardless. So the "pasted" report was the observer's inference, and its own caveat was the more reliable half of what it said. I initially took the conclusion over the caveat. ## What actually remains Items 1 and 3 of the proposal are void — there is nothing to add to the deliverability check, and making `fleet.collaborators` hot is not on the path to anything, because the config is not needed for this at all. The restart cost is real but it buys a different feature. The one live question is item 2, and it is narrower than the body says: **An observer cannot open an exchange.** `SEND` requires primary, architect or collaborator (`auth/Authz.java:115`), so the channel is one-way: a lead may reach any bridge-mounted pane, and that pane may answer, but it cannot initiate. Whether answering is sufficient, or an unconfigured pane should be able to open a send to a lead, is the only thing left to decide. That is a genuine authority question rather than a missing wire, and the operator's objection — that a config edit per pane is nonsense — is already satisfied for everything except initiation. ## Why this got filed wrong The operator pushed back on the config route twice before I re-read the code. I had a reading that explained the symptom, so I stopped looking, and each push produced a better workaround instead of a re-measurement. The shape is the one already recorded on #705 and #726: a true-sounding cause that fits the symptom ends the search before the real mechanism is found. Here the "cause" was not even true — it was a map membership I asserted without checking the writer.
Author
Owner

The operator has answered item 2, and two of the three items need restating

Operator, 2026-10-05, after the opus ↔ vms exchange succeeded:

if you scan this herdr sessions, you will find "trinotes, anki" tabs -> they are not able to talk to each other - after finish with apply coding practice, please work with the /goal that allow them to communicate (they are on different claude account sessions) without any extra config needed for fleetd

Scheduling: this is queued behind #748 (the code-quality decision), by the operator's own order. No code yet.

Item 1 is already satisfied — this ticket's body is wrong about it

The body says an observer "is addressable by nobody" and proposes adding observer terminals to the deliverability check at Fleetd.java:238-240. That change is not needed. Measured 2026-10-05:

// mcp/FleetMcp.java:852-856, called from the request path at FleetMcp.java:448
static void markTrackedCallerPresent(Principal caller, MemberPresence presence) {
    if (caller.isSpawnedMember() || caller.isObserver()) {   // observer included
        presence.markPresent(caller.terminal());
    }
}

deliverableTo tests presence.isPresent(target) first, so an observer that has made any fleet_* call is already a valid target. Proven live: three fleet_send calls from lead opus to observer term_65cfe2c6afc7776 landed and were each answered with fleet_reply, with fleet.collaborators absent and fleet_list reporting collaborators: [].

So item 1 and the reply half of item 2 are done, with no config. I read deliverableTo's three disjuncts when writing the body and never read what fills the first one.

One operational caveat that is easy to miss: MemberPresence is in memory, so a daemon restart drops it. An observer stops being addressable until it makes one more fleet_* call. Re-enrolment is pull, not push — the lead cannot do it for them.

What the goal actually asks for

Both trinotes and anki are unconfigured panes, so both are observers. Neither is a lead. So the ask is observer → observer SEND, which is refused at Authz.java:115 (SEND needs primary, architect or collaborator). That is the only missing piece.

The different Claude accounts are not an obstacle and never were. CallerResolver resolves from the connection — the OS reports the pid, herdr owns the pid→pane map — and consults no Claude account at any rung. Cross-account messaging needs nothing extra.

The constraint that makes this a design decision, not a one-line grant

#705 option 3 was rejected in writing for granting exactly this. Its words: defaulting unconfigured panes to COLLABORATOR "removes TASK_READ but adds SEND, so any unconfigured pane could inject text into a lead's pane. A trade, not a strict improvement."

And #705 was closed today, with the reasoning that an observer holds no SEND. So a broad grant here re-opens a ticket that was closed hours earlier. Whoever picks this up must not do that by accident.

The narrow form avoids the stated hazard: observer → observer only. Never observer → lead, never observer → worker. The #705 objection named injection into a lead's pane; a right that cannot target a lead does not carry that consequence. That restriction is the design, and it has to be enforced at the authorization gate — a convention in a brief will not hold.

Two further questions for whoever designs it, neither answered here:

  1. A fleet_send arrives as a paste, so the receiving agent cannot distinguish an injection from its own user typing — measured yesterday, and the receiving session was right to say it could not tell. From a trusted lead that is acceptable. From an arbitrary peer pane, any pane could make text appear in another as if the user wrote it. Decide whether injected text must be marked at the boundary, and verify on the sender's side, never by asking the receiver.
  2. Does observer → observer need a reply route distinct from REPLY? REPLY resolves a blocked fleet_send and is gated on owning your own pane. Whether that composes for a peer-to-peer exchange with no ticket owner wants checking rather than assuming — the same mistake as item 1 above.

Item 3 stands, and does not deliver this goal

Making fleet.collaborators hot (FleetdAssembly.java:271-275) is still worth doing and still independent. But it cannot satisfy this goal, because the goal forbids config entirely — a hot config edit is still a config edit.

Not measured

trinotes was not running when the goal was given. ListAgents showed only anki, vms and one architect pane. So I have not confirmed trinotes is a herdr pane, has the bridge mounted, or resolves as an observer. All three need checking before designing for it.

Also worth knowing for that check: a Claude Code member appears in ListAgents named after its worktree directory, while an opencode member does not. Measured today with two architects spawned in the same minute — spawning claude profile=opus showed up as 904536-1-90; spawning opencode profile=sol did not appear. So a row in ListAgents proves nothing about a pane's fleet role.

## The operator has answered item 2, and two of the three items need restating Operator, 2026-10-05, after the `opus` ↔ `vms` exchange succeeded: > if you scan this herdr sessions, you will find "trinotes, anki" tabs -> they are not able to talk to each other - after finish with apply coding practice, please work with the /goal that allow them to communicate (they are on different claude account sessions) without any extra config needed for fleetd **Scheduling: this is queued behind #748 (the code-quality decision), by the operator's own order. No code yet.** ### Item 1 is already satisfied — this ticket's body is wrong about it The body says an observer "is addressable by nobody" and proposes adding observer terminals to the deliverability check at `Fleetd.java:238-240`. **That change is not needed.** Measured 2026-10-05: ```java // mcp/FleetMcp.java:852-856, called from the request path at FleetMcp.java:448 static void markTrackedCallerPresent(Principal caller, MemberPresence presence) { if (caller.isSpawnedMember() || caller.isObserver()) { // observer included presence.markPresent(caller.terminal()); } } ``` `deliverableTo` tests `presence.isPresent(target)` first, so an observer that has made any `fleet_*` call is already a valid target. Proven live: three `fleet_send` calls from lead `opus` to observer `term_65cfe2c6afc7776` landed and were each answered with `fleet_reply`, with `fleet.collaborators` absent and `fleet_list` reporting `collaborators: []`. So item 1 and the reply half of item 2 are **done**, with no config. I read `deliverableTo`'s three disjuncts when writing the body and never read what fills the first one. One operational caveat that is easy to miss: `MemberPresence` is in memory, so a daemon restart drops it. An observer stops being addressable until it makes one more `fleet_*` call. Re-enrolment is pull, not push — the lead cannot do it for them. ### What the goal actually asks for Both `trinotes` and `anki` are unconfigured panes, so both are observers. Neither is a lead. So the ask is **observer → observer `SEND`**, which is refused at `Authz.java:115` (`SEND` needs primary, architect or collaborator). That is the only missing piece. The different Claude accounts are **not** an obstacle and never were. `CallerResolver` resolves from the connection — the OS reports the pid, herdr owns the pid→pane map — and consults no Claude account at any rung. Cross-account messaging needs nothing extra. ### The constraint that makes this a design decision, not a one-line grant **#705 option 3 was rejected in writing for granting exactly this.** Its words: defaulting unconfigured panes to `COLLABORATOR` "removes `TASK_READ` but **adds** `SEND`, so any unconfigured pane could inject text into a lead's pane. A trade, not a strict improvement." And #705 was **closed today**, with the reasoning that an observer holds no `SEND`. So a broad grant here re-opens a ticket that was closed hours earlier. Whoever picks this up must not do that by accident. The narrow form avoids the stated hazard: **observer → observer only. Never observer → lead, never observer → worker.** The #705 objection named injection into a *lead's* pane; a right that cannot target a lead does not carry that consequence. That restriction is the design, and it has to be enforced at the authorization gate — a convention in a brief will not hold. Two further questions for whoever designs it, neither answered here: 1. **A `fleet_send` arrives as a paste**, so the receiving agent cannot distinguish an injection from its own user typing — measured yesterday, and the receiving session was right to say it could not tell. From a trusted lead that is acceptable. From an arbitrary peer pane, any pane could make text appear in another as if the user wrote it. Decide whether injected text must be marked at the boundary, and verify on the **sender's** side, never by asking the receiver. 2. **Does observer → observer need a reply route distinct from `REPLY`?** `REPLY` resolves a blocked `fleet_send` and is gated on owning your own pane. Whether that composes for a peer-to-peer exchange with no ticket owner wants checking rather than assuming — the same mistake as item 1 above. ### Item 3 stands, and does not deliver this goal Making `fleet.collaborators` hot (`FleetdAssembly.java:271-275`) is still worth doing and still independent. But it cannot satisfy this goal, because the goal forbids config entirely — a hot config edit is still a config edit. ### Not measured `trinotes` was **not running** when the goal was given. `ListAgents` showed only `anki`, `vms` and one architect pane. So I have not confirmed trinotes is a herdr pane, has the bridge mounted, or resolves as an observer. All three need checking before designing for it. Also worth knowing for that check: a **Claude Code** member appears in `ListAgents` named after its worktree directory, while an opencode member does not. Measured today with two architects spawned in the same minute — `spawning claude profile=opus` showed up as `904536-1-90`; `spawning opencode profile=sol` did not appear. So a row in `ListAgents` proves nothing about a pane's fleet role.
Author
Owner

Measured 2026-10-05 on a real pane, and documented. Item 1 needs no code change — it already works.

What I measured

Sent to trinotes (term_65d106559d7e43), a tab a person opened by hand:

  • no fleet.collaborators entry, no fleetd.yaml edit, no daemon restart
  • its fleet_* tools were still deferred and unloaded — it had to ToolSearch them to answer
  • it replied {"role":"observer","sessionId":"term_65d106559d7e43"} on the first try

So lead → unconfigured pane, and the pane answering, both already work with zero config.

Why it works, read from the code

Delivery is gated on presence, not on SEND:

  • mcp/FleetMcp.java:436 — contextExtractor runs on every MCP request, initialize included
    (the comment there already says so: "its MCP initialize is the reliable 'the agent is up'
    signal"
    ), and calls markTrackedCallerPresent
  • mcp/FleetMcp.java:850 — that guards on the role, and marks presence for a spawned member
    or the unconfigured-pane floor
  • Fleetd.java:239 — deliverableTo tests presence.isPresent(target) first, before the lead
    and collaborator maps

So connecting the server is the enrolment. And Authz.java:135 gates REPLY/ASK on owning your
own pane, which every pane does. Receiving and answering are simply not SEND.

That is why reading the role table alone hides this capability — Authz.java:115 refuses an
observer SEND, which looks like "an observer cannot take part".

Item status

Item Status
1 — make an observer a valid SEND target already true. The issue body says this needs a code change; it does not.
2 — may an observer SEND? still open. Authz.java:115 keeps SEND to primary, architect or collaborator, so pane → pane is refused.
3 — make fleet.collaborators hot still open and independent. Worth doing, but it does not deliver the no-config goal.

Item 2 is the only gap. It remains a design decision rather than a one-line grant: #705 option 3
was refused in writing because defaulting unplaced panes to COLLABORATOR "adds SEND, so any
unconfigured pane could inject text into a lead's pane". A grant here must be observer → observer
only
, enforced at the gate, or it silently re-opens a ticket closed on 2026-10-05. The second
open question is that a delivered message arrives as a paste, so the receiver cannot tell it from
its own user typing — if injected text must be marked, that has to be verified on the sender's
side.

Where this is now documented

  • wiki/11-Features.md — new entry "Message a hand-opened pane with no config at all" + index row
  • wiki/7-Use-Cases.md — the portable block, regenerated from CLAUDE.md so the two cannot drift
  • CLAUDE.md — a lead intent→tool row for an unconfigured pane (commit 291dc02)
  • docs/MCP-Contract.md §2 — the enrolment side of the deliverability gate, next to the existing
    "a spawned member is not deliverable until it has mounted the MCP" bullet, which is the same
    gate read the other way

One correction worth recording

The user-scope instruction file said the fleet has no route to an interactive session unless an
operator registers it as a collaborator. That was false, and it was load-bearing: a session reading
it concludes the exchange is impossible and stops looking, which is exactly what happened here
before the measurement. Fixed.

Two gotchas now written down, because both cost time:

  • Neither fleet_list nor ListAgents is a roster of what you can reach. fleet_list shows
    configured and spawned roles; ListAgents shows only sessions that registered a Claude session
    id. trinotes was absent from both and answered anyway. I reported it as "not running" off
    ListAgents alone — wrong.
  • The target must mount the bridge, and that is per CLAUDE_CONFIG_DIR. The four
    ~/.ccs/instances/* dirs carry fleet; plain ~/.claude.json does not.
Measured 2026-10-05 on a real pane, and documented. **Item 1 needs no code change — it already works.** ## What I measured Sent to `trinotes` (`term_65d106559d7e43`), a tab a person opened by hand: - no `fleet.collaborators` entry, no `fleetd.yaml` edit, no daemon restart - its `fleet_*` tools were still **deferred and unloaded** — it had to `ToolSearch` them to answer - it replied `{"role":"observer","sessionId":"term_65d106559d7e43"}` on the first try So lead → unconfigured pane, and the pane answering, both already work with zero config. ## Why it works, read from the code Delivery is gated on **presence**, not on `SEND`: - `mcp/FleetMcp.java:436` — `contextExtractor` runs on **every** MCP request, `initialize` included (the comment there already says so: *"its MCP initialize is the reliable 'the agent is up' signal"*), and calls `markTrackedCallerPresent` - `mcp/FleetMcp.java:850` — that guards on the **role**, and marks presence for a spawned member *or* the unconfigured-pane floor - `Fleetd.java:239` — `deliverableTo` tests `presence.isPresent(target)` **first**, before the lead and collaborator maps So connecting the server *is* the enrolment. And `Authz.java:135` gates `REPLY`/`ASK` on owning your own pane, which every pane does. Receiving and answering are simply not `SEND`. That is why reading the role table alone hides this capability — `Authz.java:115` refuses an observer `SEND`, which looks like "an observer cannot take part". ## Item status | Item | Status | |---|---| | 1 — make an observer a valid `SEND` target | **already true.** The issue body says this needs a code change; it does not. | | 2 — may an observer `SEND`? | **still open.** `Authz.java:115` keeps `SEND` to primary, architect or collaborator, so pane → pane is refused. | | 3 — make `fleet.collaborators` hot | still open and independent. Worth doing, but it does not deliver the no-config goal. | Item 2 is the only gap. It remains a design decision rather than a one-line grant: #705 option 3 was refused in writing because defaulting unplaced panes to `COLLABORATOR` "adds `SEND`, so any unconfigured pane could inject text into a lead's pane". A grant here must be **observer → observer only**, enforced at the gate, or it silently re-opens a ticket closed on 2026-10-05. The second open question is that a delivered message arrives as a paste, so the receiver cannot tell it from its own user typing — if injected text must be marked, that has to be verified on the **sender's** side. ## Where this is now documented - `wiki/11-Features.md` — new entry *"Message a hand-opened pane with no config at all"* + index row - `wiki/7-Use-Cases.md` — the portable block, regenerated from `CLAUDE.md` so the two cannot drift - `CLAUDE.md` — a lead intent→tool row for an unconfigured pane (commit `291dc02`) - `docs/MCP-Contract.md` §2 — the enrolment side of the deliverability gate, next to the existing *"a spawned member is not deliverable until it has mounted the MCP"* bullet, which is the same gate read the other way ## One correction worth recording The user-scope instruction file said the fleet has **no** route to an interactive session unless an operator registers it as a collaborator. That was false, and it was load-bearing: a session reading it concludes the exchange is impossible and stops looking, which is exactly what happened here before the measurement. Fixed. Two gotchas now written down, because both cost time: - **Neither `fleet_list` nor `ListAgents` is a roster of what you can reach.** `fleet_list` shows configured and spawned roles; `ListAgents` shows only sessions that registered a Claude session id. `trinotes` was absent from **both** and answered anyway. I reported it as "not running" off `ListAgents` alone — wrong. - **The target must mount the bridge, and that is per `CLAUDE_CONFIG_DIR`.** The four `~/.ccs/instances/*` dirs carry `fleet`; plain `~/.claude.json` does not.
Author
Owner

Item 3 (pane discovery) — PR #753 verified, merging after one fix

Read this if you are working the observer-SEND half of this ticket — there is an obligation for you at the bottom.

What I verified myself

Merged the branch onto main at 291dc02 in a throwaway worktree. No conflict.

MVN_EXIT=0
xml=177 tests=2122 failures=0 errors=0 skipped=0

That matches the implementer's reported 2122, and 2122 − 2118 baseline accounts for its 4 new tests.

Two javadoc claims checked in the code rather than trusted:

  • "the scan collapses to one in the single-daemon deployment" — true, PaneLocator.java:75 is lead == member ? List.of(lead) : List.of(lead, member).
  • Tab.label really can be null (Tab.java:21, asText(null)), and paneRow guards a.tabId() == null before the lookup, so the Map.of() null-key trap in PaneSource.none() is unreachable.

One fix going back before merge

A reviewer found that rest/FleetApp.java:449 runs the label scan as the first statement inside the try that workers.list() shares. So a HerdrException from workspace.list/tab.list now turns a previously-successful GET /agents into the error envelope.

I then found the same shape with a worse consequence: in FleetMcp.listFleet, the label Supplier is read inside a try whose catch (HerdrException) returns error("herdr error listing the fleet: …") for the whole call. A tab-label hiccup would cost the caller leads, members, capacity and coordinator — arrays that never needed workspace.list at all.

The label is decoration. Both scans are going best-effort, with tests that fail before the fix.

A test gap, filed separately as #755

panesVisibleTo is the first of fleet_list's five visibility flags with no call-site pin. I replaced it with the literal true in the handler and all 2122 tests passed; the same mutation on leadsVisibleTo killed, so the probe works. The established mechanism is a source-text test, which CLAUDE.md rule 4 now forbids, so closing it needs a design decision — #755 proposes one auth.mode: token end-to-end test pinning all five by behaviour.

The obligation for the observer-SEND change

panesVisibleTo is isPrimary() || isArchitect() || isCollaborator(). Its stated reason is that a worker or an observer "can never SEND at all", so a tab label and a cwd would be useless to them.

Your change falsifies that premise. If an observer may SEND, then as the code stands it can send but cannot discover a target through the bridge — it would have to shell out to herdr tab list. A grant at one gate with the other still shut is a half-plumbed feature.

So your change must do one of these, explicitly:

  1. Widen panesVisibleTo to include an observer, filtered — only the rows that observer may actually SEND to, and without cwd. The original objection was about leaking host shape to a caller that can do nothing with it; a label it may address is not that, and a working directory still is.
  2. Leave the gate shut and say so in the PR — discovery stays outside the bridge, and that is a deliberate limit rather than an oversight.

Do not simply leave it unaddressed. I reached this independently of the reviewer I put on the authorization dimension, which returned NO ISSUE on #753 and assigned the obligation the same way: "If observer-to-observer SEND ships, that change must also widen this gate; otherwise the defect is in that later change, not this PR."

Also note what has not changed and is not yours to re-decide: the #705 constraint still binds. Any grant must be observer → observer, enforced at the gate, never observer → lead.

## Item 3 (pane discovery) — PR #753 verified, merging after one fix **Read this if you are working the observer-`SEND` half of this ticket** — there is an obligation for you at the bottom. ### What I verified myself Merged the branch onto `main` at `291dc02` in a throwaway worktree. No conflict. ``` MVN_EXIT=0 xml=177 tests=2122 failures=0 errors=0 skipped=0 ``` That matches the implementer's reported 2122, and 2122 − 2118 baseline accounts for its 4 new tests. Two javadoc claims checked in the code rather than trusted: - "the scan collapses to one in the single-daemon deployment" — true, `PaneLocator.java:75` is `lead == member ? List.of(lead) : List.of(lead, member)`. - `Tab.label` really can be null (`Tab.java:21`, `asText(null)`), and `paneRow` guards `a.tabId() == null` before the lookup, so the `Map.of()` null-key trap in `PaneSource.none()` is unreachable. ### One fix going back before merge A reviewer found that `rest/FleetApp.java:449` runs the label scan as the first statement inside the `try` that `workers.list()` shares. So a `HerdrException` from `workspace.list`/`tab.list` now turns a previously-successful `GET /agents` into the error envelope. I then found **the same shape with a worse consequence**: in `FleetMcp.listFleet`, the label `Supplier` is read inside a `try` whose `catch (HerdrException)` returns `error("herdr error listing the fleet: …")` for the **whole call**. A tab-label hiccup would cost the caller `leads`, `members`, `capacity` and `coordinator` — arrays that never needed `workspace.list` at all. The label is decoration. Both scans are going best-effort, with tests that fail before the fix. ### A test gap, filed separately as #755 `panesVisibleTo` is the **first of `fleet_list`'s five visibility flags with no call-site pin**. I replaced it with the literal `true` in the handler and all 2122 tests passed; the same mutation on `leadsVisibleTo` killed, so the probe works. The established mechanism is a source-text test, which `CLAUDE.md` rule 4 now forbids, so closing it needs a design decision — #755 proposes one `auth.mode: token` end-to-end test pinning all five by behaviour. ### The obligation for the observer-`SEND` change `panesVisibleTo` is `isPrimary() || isArchitect() || isCollaborator()`. Its stated reason is that a worker or an observer "can never `SEND` at all", so a tab label and a `cwd` would be useless to them. **Your change falsifies that premise.** If an observer may `SEND`, then as the code stands it can send but cannot discover a target through the bridge — it would have to shell out to `herdr tab list`. A grant at one gate with the other still shut is a half-plumbed feature. So your change must do one of these, explicitly: 1. **Widen `panesVisibleTo` to include an observer, filtered** — only the rows that observer may actually `SEND` to, and **without `cwd`**. The original objection was about leaking host shape to a caller that can do nothing with it; a label it may address is not that, and a working directory still is. 2. **Leave the gate shut and say so in the PR** — discovery stays outside the bridge, and that is a deliberate limit rather than an oversight. Do not simply leave it unaddressed. I reached this independently of the reviewer I put on the authorization dimension, which returned `NO ISSUE` on #753 and assigned the obligation the same way: *"If observer-to-observer `SEND` ships, that change must also widen this gate; otherwise the defect is in that later change, not this PR."* Also note what has **not** changed and is not yours to re-decide: the #705 constraint still binds. Any grant must be observer → observer, enforced at the gate, never observer → lead.
Author
Owner

Lead verification of PR #753 (both commits) — plus one gap the fix does not cover

Verified in a throwaway worktree, origin/main (291dc02) + origin/worker/743-pane-discovery-ad5b75-5 (b1d2cb4), clean merge:

MVN_EXIT=0
xml=177 tests=2124 failures=0 errors=0 skipped=0

That matches the worker's claim exactly (2122 baseline + the 2 new regression tests).

I re-ran the worker's revert-dance myself, and both kills reproduce

I did not take the pasted failures on trust. target/surefire-reports wiped first; every mutation restored and confirmed byte-identical with git diff afterwards.

M1 — FleetMcp.paneRows, tabLabelsOrEmpty(panes) → panes.tabLabels().get():

FleetMcpTest  Tests run: 2, Failures: 1
AssertionFailedError: herdr error listing the fleet: herdr error [unavailable]: workspace.list failed ==> expected: not equal but was: <true>

Run with a positive control in the same invocation: listReportsAPaneRowWithItsTabLabelAndSendableSessionId passed under the mutation, so M1 broke only the failure path and the new test is genuinely pinned to it.

M2 — FleetApp.agents, label scan moved back inside workers.list()'s try:

FleetAppTest.agentsStillReportsTheRosterWhenTheLabelScanFails:237
  {"detail":"herdr error [unavailable]: workspace.list failed","error":"herdr_error"} ==> expected: <200> but was: <502>

Both regression tests are real. The second instance in FleetMcp.listFleet — which cost leads/members/capacity/coordinator, not just the labels — is fixed and pinned.

The gap: GET /agents' label field has no positive pin at all

M3 — I replaced the whole label merge with an empty map, final Map<String, String> tabLabels = Map.of(); in FleetApp.agents:

MVN_EXIT=0
xml=177 tests=2124 failures=0 errors=0
classes with failures: []

The entire feature can be deleted and all 2124 tests pass. M1 and M2 killed in this same tree minutes earlier, so the probe works and these files do run — a survivor here is evidence, not a broken harness.

The cause is a one-sided assertion. The only /agents label assertion in the whole test tree is the new assertTrue(agents.get(0).get("label").isNull(), ...) at FleetAppTest:241, which asserts the degraded reading. It passes when the scan fails, and it also passes when the merge was never written. fleet_list's panes row does have a positive pin (listReportsAPaneRowWithItsTabLabelAndSendableSessionId asserts "label":"trinotes"), so PaneLocator.tabLabelsByTabId is covered; the FleetApp caller is not. A test on the seam does not prove the caller.

This is not a defect in the fix — the fix was asked to make a failure best-effort and it does. It is the half the brief did not name.

Why it matters more than a usual coverage hole

GET /agents' label is now load-bearing documentation. The fallback recipe that this ticket put into the user-scope CLAUDE.md, the repo CLAUDE.md intent→tool table and wiki/11-Features.md all tell a future session to read a pane's terminal id by joining herdr tab list to GET /agents on tab_id. If that merge silently returns no labels, the documented route to reach a hand-opened pane stops working and no test fails. That is the exact shape of a note that goes stale as a live restriction.

Delegated as one more commit on this same branch before I merge.

## Lead verification of PR #753 (both commits) — plus one gap the fix does not cover Verified in a throwaway worktree, `origin/main` (291dc02) + `origin/worker/743-pane-discovery-ad5b75-5` (b1d2cb4), clean merge: ``` MVN_EXIT=0 xml=177 tests=2124 failures=0 errors=0 skipped=0 ``` That matches the worker's claim exactly (2122 baseline + the 2 new regression tests). ### I re-ran the worker's revert-dance myself, and both kills reproduce I did not take the pasted failures on trust. `target/surefire-reports` wiped first; every mutation restored and confirmed byte-identical with `git diff` afterwards. **M1** — `FleetMcp.paneRows`, `tabLabelsOrEmpty(panes)` → `panes.tabLabels().get()`: ``` FleetMcpTest Tests run: 2, Failures: 1 AssertionFailedError: herdr error listing the fleet: herdr error [unavailable]: workspace.list failed ==> expected: not equal but was: <true> ``` Run with a **positive control in the same invocation**: `listReportsAPaneRowWithItsTabLabelAndSendableSessionId` passed under the mutation, so M1 broke only the failure path and the new test is genuinely pinned to it. **M2** — `FleetApp.agents`, label scan moved back inside `workers.list()`'s try: ``` FleetAppTest.agentsStillReportsTheRosterWhenTheLabelScanFails:237 {"detail":"herdr error [unavailable]: workspace.list failed","error":"herdr_error"} ==> expected: <200> but was: <502> ``` Both regression tests are real. The second instance in `FleetMcp.listFleet` — which cost `leads`/`members`/`capacity`/`coordinator`, not just the labels — is fixed and pinned. ### The gap: `GET /agents`' `label` field has no positive pin at all **M3** — I replaced the whole label merge with an empty map, `final Map<String, String> tabLabels = Map.of();` in `FleetApp.agents`: ``` MVN_EXIT=0 xml=177 tests=2124 failures=0 errors=0 classes with failures: [] ``` **The entire feature can be deleted and all 2124 tests pass.** M1 and M2 killed in this same tree minutes earlier, so the probe works and these files do run — a survivor here is evidence, not a broken harness. The cause is a one-sided assertion. The only `/agents` label assertion in the whole test tree is the new `assertTrue(agents.get(0).get("label").isNull(), ...)` at `FleetAppTest:241`, which asserts the **degraded** reading. It passes when the scan fails, and it also passes when the merge was never written. `fleet_list`'s `panes` row does have a positive pin (`listReportsAPaneRowWithItsTabLabelAndSendableSessionId` asserts `"label":"trinotes"`), so `PaneLocator.tabLabelsByTabId` is covered; the `FleetApp` **caller** is not. A test on the seam does not prove the caller. This is not a defect in the fix — the fix was asked to make a failure best-effort and it does. It is the half the brief did not name. ### Why it matters more than a usual coverage hole `GET /agents`' `label` is now **load-bearing documentation**. The fallback recipe that this ticket put into the user-scope `CLAUDE.md`, the repo `CLAUDE.md` intent→tool table and `wiki/11-Features.md` all tell a future session to read a pane's terminal id by joining `herdr tab list` to `GET /agents` on `tab_id`. If that merge silently returns no labels, the documented route to reach a hand-opened pane stops working and no test fails. That is the exact shape of a note that goes stale as a live restriction. Delegated as one more commit on this same branch before I merge.
Author
Owner

Lead verification of PR #754 (both commits) — the deletion is safe

Verified in a second throwaway worktree, origin/main (291dc02) + origin/worker/743-observer-send-4706db-6 (001367d), clean merge:

MVN_EXIT=0
xml=178 tests=2130 failures=0 errors=0

Matches the worker's claim exactly.

The deletion is surgical

git diff 001367d^ 001367d touches one file and removes 29 lines: the theSendHandlerActuallyAttributesAnObserversContent method and its javadoc, nothing else. No import left dangling that the other six Files.readString(MCP_SOURCE) tests in that class do not still need.

The real question — does anything still pin the call site?

That test existed to stop attributeIfObserver being correct in isolation while the production call site never calls it. Deleting it is only safe if something else fails when the call site drops the wrapper. I measured it rather than reasoning about it.

Mutation — FleetMcp.java:474:

String content = str(a, "content");          // was: attributeIfObserver(caller, str(a, "content"))

Full suite:

MVN_EXIT=1
xml=178 tests=2130 failures=1 errors=0
KILLED IN: [('dev.ltms.fleet.mcp.FleetMcpObserverSendDeliveryTest', 1)]
FleetMcpObserverSendDeliveryTest.anObserversSendIsAttributedAndReachesTheRealInjector:122
the receiving pane must see the sender's own daemon-resolved terminal, never a raw echo of the
content and never a client-supplied name ==> expected: <[fleet_send from observer term_shell]

Exactly one kill, and it is the behavioural test. The call site is pinned by what the receiving pane actually sees, through a real Jetty server, a real MCP client and the real CallerResolver — not by a string match on source text. That is a strictly better pin than the one removed: it survives a rename, and it breaks if the attribution is wrong rather than merely absent.

Tree restored and git diff confirmed empty after the mutation.

Why this unit existed at all

Rule 4 of the code-quality policy (CLAUDE.md, shipped in 291dc02) says no new source-text test may be added. The deleted test was added in the same ticket that shipped the rule, so it breached a rule that did not exist when the work was briefed. The worker had already, independently, built the better answer — so the policy and the coverage are both satisfied by deleting the weaker of two pins, not by inventing a seam.

That is the opposite outcome from #755, where the one established pattern for pinning a handler call site is banned and no behavioural pin exists yet. FleetMcpObserverSendDeliveryTest is now the in-repo worked example that #755 should follow.

## Lead verification of PR #754 (both commits) — the deletion is safe Verified in a second throwaway worktree, `origin/main` (291dc02) + `origin/worker/743-observer-send-4706db-6` (001367d), clean merge: ``` MVN_EXIT=0 xml=178 tests=2130 failures=0 errors=0 ``` Matches the worker's claim exactly. ### The deletion is surgical `git diff 001367d^ 001367d` touches one file and removes 29 lines: the `theSendHandlerActuallyAttributesAnObserversContent` method and its javadoc, nothing else. No import left dangling that the other six `Files.readString(MCP_SOURCE)` tests in that class do not still need. ### The real question — does anything still pin the call site? That test existed to stop `attributeIfObserver` being correct in isolation while the production call site never calls it. Deleting it is only safe if something else fails when the call site drops the wrapper. I measured it rather than reasoning about it. **Mutation** — `FleetMcp.java:474`: ```java String content = str(a, "content"); // was: attributeIfObserver(caller, str(a, "content")) ``` Full suite: ``` MVN_EXIT=1 xml=178 tests=2130 failures=1 errors=0 KILLED IN: [('dev.ltms.fleet.mcp.FleetMcpObserverSendDeliveryTest', 1)] ``` ``` FleetMcpObserverSendDeliveryTest.anObserversSendIsAttributedAndReachesTheRealInjector:122 the receiving pane must see the sender's own daemon-resolved terminal, never a raw echo of the content and never a client-supplied name ==> expected: <[fleet_send from observer term_shell] ``` **Exactly one kill, and it is the behavioural test.** The call site is pinned by what the receiving pane actually sees, through a real Jetty server, a real MCP client and the real `CallerResolver` — not by a string match on source text. That is a strictly better pin than the one removed: it survives a rename, and it breaks if the attribution is wrong rather than merely absent. Tree restored and `git diff` confirmed empty after the mutation. ### Why this unit existed at all Rule 4 of the code-quality policy (`CLAUDE.md`, shipped in 291dc02) says no new source-text test may be added. The deleted test was added in the same ticket that shipped the rule, so it breached a rule that did not exist when the work was briefed. The worker had already, independently, built the better answer — so the policy and the coverage are both satisfied by deleting the weaker of two pins, not by inventing a seam. That is the opposite outcome from #755, where the one established pattern for pinning a handler call site is banned and no behavioural pin exists yet. `FleetMcpObserverSendDeliveryTest` is now the in-repo worked example that #755 should follow.
Author
Owner

Status: both PRs merged and live. One half of the goal is done, one is not.

Merged into main as 02eff4c and pushed. Redeployed with scripts/redeploy-fleetd.sh --yes: daemon pid 9796, jar b0b40b6ccd4b, /healthz 200, a fresh fleetd listening line at 09:52:45, no ERROR lines since the restart, and fleet_whoami still answers primary with its leader key. PRs #753 and #754 closed as locally merged.

Done and proven live. fleet_list now returns the panes array, and the first call after the redeploy found seven panes — including three I had never managed to enumerate at all (adev, 70-review-approvals, 70-tidy). GET /agents carries the label too. An observer may fleet_send to another observer and nothing else, with the sender's daemon-resolved terminal prefixed to the text.

Not done: an observer cannot discover another observer. panesVisibleTo is primary, architect and collaborator, so a pane that now may send has no way to learn the terminal id to send to. A person can paste one, but pane-to-pane is not self-service yet. That was always the planned third unit — widen panesVisibleTo to an observer, filtered to the rows it may actually send to and without cwd. It should land after #756, because that ticket fixes the role field the filter would otherwise be read against.

Two things the merge surfaced, both filed rather than left in my head:

  • #756 — panes[].role reports observer for a pane bound to a configured architect slot, while the SEND gate refuses that same pane as an architect. The neighbouring deliverable field got this right by calling Fleetd.deliverableTo instead of re-deriving it; role re-derives.
  • #757 — a daemon restart de-enrols every hand-opened pane, because MemberPresence is an in-memory set with no boot rebuild. All six observer panes read deliverable: false right after the redeploy, including the one that took a send on the first try twenty minutes earlier. And a send to such a pane is accepted, then queued for the full 30 minutes with no injector line. This is the limitation that most affects this ticket's own goal, so "reachable with no daemon restart" is a claim about enrolment and not about the lifecycle.

One intermittent failure, not caused by this work. The first redeploy attempt failed its build on two MessageServiceTest tests — aTicketTerminalPushFailureDoesNotVanishSilently:2574 (expected: <true> but was: <false>) and asyncSendRecordsOwnershipOnAcceptance:1222 (NoSuchElementException: No value present). The script left the running daemon untouched, which is what that ordering is for. I then measured rather than assuming: three isolated runs of MessageServiceTest green, and two further full-suite runs green at 2137/0/0 on the same bytes. So it is one failure in three full-suite runs, both assertions timing-dependent, and git log 291dc02..HEAD shows neither merged PR touched MessageService or its test. I am deliberately not naming a cause — the measurement is not stable, so any cause I gave would be a story. Worth its own ticket if it recurs; the log excerpt is saved.

The instruction surface was updated with the code, per this repo's own rule. Three claims in the canonical CLAUDE.md block went false the moment the grant merged — the observer role definition said "never SEND", invariant 3 listed send as "lead, architect, or collaborator", and the hand-opened-pane row said such a pane "cannot fleet_send back". All three fixed, wiki/7-Use-Cases.md regenerated from the repo copy programmatically so the two cannot drift (in sync: True), and wiki/11-Features.md carries the corrected gotchas plus a new entry for the panes array.

## Status: both PRs merged and live. One half of the goal is done, one is not. Merged into `main` as `02eff4c` and pushed. Redeployed with `scripts/redeploy-fleetd.sh --yes`: daemon pid 9796, jar `b0b40b6ccd4b`, `/healthz` 200, a fresh `fleetd listening` line at 09:52:45, no ERROR lines since the restart, and `fleet_whoami` still answers `primary` with its `leader` key. PRs #753 and #754 closed as locally merged. **Done and proven live.** `fleet_list` now returns the `panes` array, and the first call after the redeploy found **seven** panes — including three I had never managed to enumerate at all (`adev`, `70-review-approvals`, `70-tidy`). `GET /agents` carries the `label` too. An observer may `fleet_send` to another observer and nothing else, with the sender's daemon-resolved terminal prefixed to the text. **Not done: an observer cannot discover another observer.** `panesVisibleTo` is primary, architect and collaborator, so a pane that now *may* send has no way to learn the terminal id to send to. A person can paste one, but pane-to-pane is not self-service yet. That was always the planned third unit — widen `panesVisibleTo` to an observer, filtered to the rows it may actually send to and without `cwd`. It should land after #756, because that ticket fixes the `role` field the filter would otherwise be read against. **Two things the merge surfaced, both filed rather than left in my head:** - **#756** — `panes[].role` reports `observer` for a pane bound to a configured architect slot, while the SEND gate refuses that same pane as an architect. The neighbouring `deliverable` field got this right by calling `Fleetd.deliverableTo` instead of re-deriving it; `role` re-derives. - **#757** — a daemon restart de-enrols every hand-opened pane, because `MemberPresence` is an in-memory set with no boot rebuild. All six observer panes read `deliverable: false` right after the redeploy, including the one that took a send on the first try twenty minutes earlier. And a send to such a pane is *accepted*, then queued for the full 30 minutes with no injector line. This is the limitation that most affects this ticket's own goal, so "reachable with no daemon restart" is a claim about enrolment and not about the lifecycle. **One intermittent failure, not caused by this work.** The first redeploy attempt failed its build on two `MessageServiceTest` tests — `aTicketTerminalPushFailureDoesNotVanishSilently:2574` (`expected: <true> but was: <false>`) and `asyncSendRecordsOwnershipOnAcceptance:1222` (`NoSuchElementException: No value present`). The script left the running daemon untouched, which is what that ordering is for. I then measured rather than assuming: three isolated runs of `MessageServiceTest` green, and two further full-suite runs green at 2137/0/0 on the same bytes. So it is one failure in three full-suite runs, both assertions timing-dependent, and `git log 291dc02..HEAD` shows neither merged PR touched `MessageService` or its test. I am deliberately not naming a cause — the measurement is not stable, so any cause I gave would be a story. Worth its own ticket if it recurs; the log excerpt is saved. **The instruction surface was updated with the code**, per this repo's own rule. Three claims in the canonical `CLAUDE.md` block went false the moment the grant merged — the observer role definition said "never SEND", invariant 3 listed send as "lead, architect, or collaborator", and the hand-opened-pane row said such a pane "cannot `fleet_send` back". All three fixed, `wiki/7-Use-Cases.md` regenerated from the repo copy programmatically so the two cannot drift (`in sync: True`), and `wiki/11-Features.md` carries the corrected gotchas plus a new entry for the `panes` array.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#743