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
No Branch/Tag Specified
main
worker/759-authz-comment-and-role-list-e3f0e5-5
worker/756-758-observer-pane-discovery-7e6ffd-1
worker/759-role-model-comments-5d8409-3
worker/743-pane-discovery-ad5b75-5
worker/743-observer-send-4706db-6
worker/749-edge-baseline-28d1a0-3
worker/748-dead-comment-refs-f42ac5-4
worker/737-9c61d3-4
worker/737-a263f3-3
worker/737-20d1d9-1
worker/737-b038f7-2
worker/726-unit2-75cb13-4
worker/737-owner-key-ff061f-10
worker/736-presence-forget-f35144-9
worker/705-observer-14c258-6
worker/722-024c34-5
worker/726-ea34a0-2
worker/726-10cbf0-1
worker/729-5961c6-3
worker/727-ee14ed-3
worker/719-bdd95e-4
worker/702-4f5c7f-2
worker/715-5c43fc-1
worker/721-70f9ea-5
worker/718-99362b-2
worker/task-15-af0d10-12
worker/task-16-50a702-13
worker/task-12-4d0479-9
worker/task-13-823ce2-10
worker/705-ticket-owner-af9928-8
worker/703-list-collaborators-9c06c2-7
worker/669-example-truth-0b303d-6
worker/669-collab-deliverability-9ba859-3
worker/669-collab-reload-report-2a21bd-4
worker/669-7e80a6-1
worker/669-unit-d-efbbd7-1
worker/669-1b786a-1
worker/669-1d1d9f-1
worker/692-4afb9d-2
worker/689-02fced-13
worker/693-cf23fa-14
worker/677-fix-lead-collision-f69073-12
worker/638-fix-overmask-dbb1bf-11
worker/675-5b7478-4
worker/669-unit-a-70cc8f-3
worker/677-8cdaaf-5
worker/638-a7b391-1
worker/683-4536d6-2
worker/651-a75bbe-8
worker/680-20607d-7
worker/664-c12e95-3
worker/668-08534d-4
worker/672-0f2469-2
worker/670-7d1022-1
worker/661-ac7c28-2
worker/664-37fb9b-3
worker/663-remove-3arg-read-3f6783-1
worker/659-remove-dead-backcompat-ba5e6f-1
worker/637-revision-60a488-23
worker/656-redact-regression-tests-892903-19
worker/637-context-gauge-threshold-466eb5-16
worker/639-redact-line-numbers-de4ac4-17
worker/641-set-reformat-guard-6f96a4-18
worker/642-herdr-guard-scope-5de0e4-15
worker/650-javadoc-scope-95f3b3-14
worker/612-01e9f7-13
worker/612-a-r4-quarantine-outage-7ab0e8-5
worker/612-a-r9-r11-capacity-coverage-peers-cfcc79-7
worker/612-a-r10-loophealth-ccc872-8
worker/612-a-r12-turnregistrar-9e3bb7-9
worker/612-a-r5-leadconfigdir-9e70cf-6
lead/config-edit-redact-anchor-wording
worker/config-edit-seam-ca8dc1-1
worker/612-r67-630-lifecycle-290b8d-3
worker/629-625-ports-seams-da7d5d-4
worker/612-r12-exhaustion-f37cd7-1
worker/612-r38-amqp-24b083-2
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#743
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
and then, on being told it needs a
fleet.collaboratorsentry:What happens today
A herdr pane that mounts the bridge MCP is authenticated, so
READandMETRICSwork — that iswhy
fleet_whoamianswers andfleet_listreturnsloopHealthandcapacity. It is addressableby nobody and can address nobody, because both send gates are keyed on being named in config:
Authz.java:115—SENDneeds the caller to be primary, architect or collaborator. An observeris none of those.
Fleetd.java:238-240— a target is deliverable only if it is an MCP-connected member, a lead, ora 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-275builds a plainLinkedHashMapof collaborator tab labels once and hands it toLeadTabScanner, which matchesevery later scan against that frozen map.
ConfigRef.changedSplitKeysdoes 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 itcould task anyone, and the operator loses the say in who tasks whom.
CLAUDE.mdalready states theprinciple: "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:
SENDtarget — add the observer terminals to the deliverabilitycheck at
Fleetd.java:238-240. A lead could then message any bridge-mounted pane with noconfig at all. The pane still holds only
READ/METRICSplusREPLY/ASKfor itself, so itgains no authority. It would need a route to answer;
REPLYon its own pane may already beenough, and that wants checking rather than assuming.
SENDback to the lead that messaged it. This is the realdesign 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.fleet.collaboratorshot. Even if 1 and 2 are rejected, the restart is indefensible onits own. The scanner re-reads herdr tabs every
scanIntervalSecondsalready; only thelabel→entry map is frozen. Reading it through the same live supplier shape that
architects/developers/reviewersalready 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
observerfloor instead of demoting it toworker.This ticket is the next question that floor raises: an observer is now correctly described, and
correctly unable to do anything.
fleet.collaborators, the named-peer mechanism this ticket argues is tooheavy for the common case.
reporting is already correct, only the freeze needs lifting.
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 —
— is false. An observer is in the first map.
FleetMcp.markTrackedCallerPresent(
mcp/FleetMcp.java:845-849) enrols it: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, andFleetd.deliverableTotestspresence.isPresent(target)first. So any pane that has made a singlefleet_*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
opuslead to an observer pane (term_65cfe2c6afc7776), withfleet.collaboratorsabsent fromfleetd.yaml—fleet_listreportedcollaborators: []throughout.
The observer answered both with
fleet_reply, which resolved each ticket and was collected withfleet_poll.REPLYis gated oncaller.ownsSession(targetSession), and a pane owns itself, sothat 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.collaboratorshot is not on the path to anything, because the config is not neededfor 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.
SENDrequires primary, architect or collaborator(
auth/Authz.java:115), so the channel is one-way: a lead may reach any bridge-mounted pane, andthat 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.
The operator has answered item 2, and two of the three items need restating
Operator, 2026-10-05, after the
opus↔vmsexchange succeeded: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:deliverableTotestspresence.isPresent(target)first, so an observer that has made anyfleet_*call is already a valid target. Proven live: threefleet_sendcalls from leadopusto observerterm_65cfe2c6afc7776landed and were each answered withfleet_reply, withfleet.collaboratorsabsent andfleet_listreportingcollaborators: [].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:
MemberPresenceis in memory, so a daemon restart drops it. An observer stops being addressable until it makes one morefleet_*call. Re-enrolment is pull, not push — the lead cannot do it for them.What the goal actually asks for
Both
trinotesandankiare unconfigured panes, so both are observers. Neither is a lead. So the ask is observer → observerSEND, which is refused atAuthz.java:115(SENDneeds primary, architect or collaborator). That is the only missing piece.The different Claude accounts are not an obstacle and never were.
CallerResolverresolves 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"removesTASK_READbut addsSEND, 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:
fleet_sendarrives 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.REPLY?REPLYresolves a blockedfleet_sendand 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.collaboratorshot (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
trinoteswas not running when the goal was given.ListAgentsshowed onlyanki,vmsand 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
ListAgentsnamed after its worktree directory, while an opencode member does not. Measured today with two architects spawned in the same minute —spawning claude profile=opusshowed up as904536-1-90;spawning opencode profile=soldid not appear. So a row inListAgentsproves nothing about a pane's fleet role.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:fleet.collaboratorsentry, nofleetd.yamledit, no daemon restartfleet_*tools were still deferred and unloaded — it had toToolSearchthem to answer{"role":"observer","sessionId":"term_65d106559d7e43"}on the first trySo 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—contextExtractorruns on every MCP request,initializeincluded(the comment there already says so: "its MCP initialize is the reliable 'the agent is up'
signal"), and calls
markTrackedCallerPresentmcp/FleetMcp.java:850— that guards on the role, and marks presence for a spawned memberor the unconfigured-pane floor
Fleetd.java:239—deliverableTotestspresence.isPresent(target)first, before the leadand collaborator maps
So connecting the server is the enrolment. And
Authz.java:135gatesREPLY/ASKon owning yourown 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:115refuses anobserver
SEND, which looks like "an observer cannot take part".Item status
SENDtargetSEND?Authz.java:115keepsSENDto primary, architect or collaborator, so pane → pane is refused.fleet.collaboratorshotItem 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"addsSEND, so anyunconfigured 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 rowwiki/7-Use-Cases.md— the portable block, regenerated fromCLAUDE.mdso the two cannot driftCLAUDE.md— a lead intent→tool row for an unconfigured pane (commit291dc02)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:
fleet_listnorListAgentsis a roster of what you can reach.fleet_listshowsconfigured and spawned roles;
ListAgentsshows only sessions that registered a Claude sessionid.
trinoteswas absent from both and answered anyway. I reported it as "not running" offListAgentsalone — wrong.CLAUDE_CONFIG_DIR. The four~/.ccs/instances/*dirs carryfleet; plain~/.claude.jsondoes not.Item 3 (pane discovery) — PR #753 verified, merging after one fix
Read this if you are working the observer-
SENDhalf of this ticket — there is an obligation for you at the bottom.What I verified myself
Merged the branch onto
mainat291dc02in a throwaway worktree. No conflict.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:
PaneLocator.java:75islead == member ? List.of(lead) : List.of(lead, member).Tab.labelreally can be null (Tab.java:21,asText(null)), andpaneRowguardsa.tabId() == nullbefore the lookup, so theMap.of()null-key trap inPaneSource.none()is unreachable.One fix going back before merge
A reviewer found that
rest/FleetApp.java:449runs the label scan as the first statement inside thetrythatworkers.list()shares. So aHerdrExceptionfromworkspace.list/tab.listnow turns a previously-successfulGET /agentsinto the error envelope.I then found the same shape with a worse consequence: in
FleetMcp.listFleet, the labelSupplieris read inside atrywhosecatch (HerdrException)returnserror("herdr error listing the fleet: …")for the whole call. A tab-label hiccup would cost the callerleads,members,capacityandcoordinator— arrays that never neededworkspace.listat 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
panesVisibleTois the first offleet_list's five visibility flags with no call-site pin. I replaced it with the literaltruein the handler and all 2122 tests passed; the same mutation onleadsVisibleTokilled, so the probe works. The established mechanism is a source-text test, whichCLAUDE.mdrule 4 now forbids, so closing it needs a design decision — #755 proposes oneauth.mode: tokenend-to-end test pinning all five by behaviour.The obligation for the observer-
SENDchangepanesVisibleToisisPrimary() || isArchitect() || isCollaborator(). Its stated reason is that a worker or an observer "can neverSENDat all", so a tab label and acwdwould 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 toherdr 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:
panesVisibleToto include an observer, filtered — only the rows that observer may actuallySENDto, and withoutcwd. 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.Do not simply leave it unaddressed. I reached this independently of the reviewer I put on the authorization dimension, which returned
NO ISSUEon #753 and assigned the obligation the same way: "If observer-to-observerSENDships, 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.
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: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-reportswiped first; every mutation restored and confirmed byte-identical withgit diffafterwards.M1 —
FleetMcp.paneRows,tabLabelsOrEmpty(panes)→panes.tabLabels().get():Run with a positive control in the same invocation:
listReportsAPaneRowWithItsTabLabelAndSendableSessionIdpassed 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 insideworkers.list()'s try:Both regression tests are real. The second instance in
FleetMcp.listFleet— which costleads/members/capacity/coordinator, not just the labels — is fixed and pinned.The gap:
GET /agents'labelfield has no positive pin at allM3 — I replaced the whole label merge with an empty map,
final Map<String, String> tabLabels = Map.of();inFleetApp.agents: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
/agentslabel assertion in the whole test tree is the newassertTrue(agents.get(0).get("label").isNull(), ...)atFleetAppTest:241, which asserts the degraded reading. It passes when the scan fails, and it also passes when the merge was never written.fleet_list'spanesrow does have a positive pin (listReportsAPaneRowWithItsTabLabelAndSendableSessionIdasserts"label":"trinotes"), soPaneLocator.tabLabelsByTabIdis covered; theFleetAppcaller 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'labelis now load-bearing documentation. The fallback recipe that this ticket put into the user-scopeCLAUDE.md, the repoCLAUDE.mdintent→tool table andwiki/11-Features.mdall tell a future session to read a pane's terminal id by joiningherdr tab listtoGET /agentsontab_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 #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:Matches the worker's claim exactly.
The deletion is surgical
git diff 001367d^ 001367dtouches one file and removes 29 lines: thetheSendHandlerActuallyAttributesAnObserversContentmethod and its javadoc, nothing else. No import left dangling that the other sixFiles.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
attributeIfObserverbeing 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:Full suite:
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 diffconfirmed empty after the mutation.Why this unit existed at all
Rule 4 of the code-quality policy (
CLAUDE.md, shipped in291dc02) 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.
FleetMcpObserverSendDeliveryTestis now the in-repo worked example that #755 should follow.Status: both PRs merged and live. One half of the goal is done, one is not.
Merged into
mainas02eff4cand pushed. Redeployed withscripts/redeploy-fleetd.sh --yes: daemon pid 9796, jarb0b40b6ccd4b,/healthz200, a freshfleetd listeningline at 09:52:45, no ERROR lines since the restart, andfleet_whoamistill answersprimarywith itsleaderkey. PRs #753 and #754 closed as locally merged.Done and proven live.
fleet_listnow returns thepanesarray, 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 /agentscarries thelabeltoo. An observer mayfleet_sendto another observer and nothing else, with the sender's daemon-resolved terminal prefixed to the text.Not done: an observer cannot discover another observer.
panesVisibleTois 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 — widenpanesVisibleToto an observer, filtered to the rows it may actually send to and withoutcwd. It should land after #756, because that ticket fixes therolefield the filter would otherwise be read against.Two things the merge surfaced, both filed rather than left in my head:
panes[].rolereportsobserverfor a pane bound to a configured architect slot, while the SEND gate refuses that same pane as an architect. The neighbouringdeliverablefield got this right by callingFleetd.deliverableToinstead of re-deriving it;rolere-derives.MemberPresenceis an in-memory set with no boot rebuild. All six observer panes readdeliverable: falseright 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
MessageServiceTesttests —aTicketTerminalPushFailureDoesNotVanishSilently:2574(expected: <true> but was: <false>) andasyncSendRecordsOwnershipOnAcceptance: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 ofMessageServiceTestgreen, 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, andgit log 291dc02..HEADshows neither merged PR touchedMessageServiceor 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.mdblock 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 "cannotfleet_sendback". All three fixed,wiki/7-Use-Cases.mdregenerated from the repo copy programmatically so the two cannot drift (in sync: True), andwiki/11-Features.mdcarries the corrected gotchas plus a new entry for thepanesarray.