Lead identity must key on the space, not the tab label: one host, many teams, each space's first tab named "lead" #770
Open
opened 2026-10-05 11:29:13 +02:00 by ltms
·
4 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#770
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?
Operator requirement, 2026-10-05:
This is not a rename. It inverts the current identity rule
Today the discriminator is the label, and the space is explicitly not consulted. Measured in the code:
herdr/LeadTabScanner.buildTabIndexbuilds one flatMap<String, Entry>keyed by the normalised tab label, merging leads and collaborators into a single global index. A lookup is "a plain map hit" on that label.excludedWorkspaceLabels— a set of spaces never scanned. It is a filter, never part of the key.config/FleetConfig.Leader.workspacedefaults toDEFAULT_WORKSPACE = "fleet", and the javadoc states the intent plainly: all leads share one space so "the operator sees one 'session' with many tabs", and "it tells a lead from a member by the exact tab label".FleetConfigvalidation rejects twofleet.leadersentries that share one exact tab, case-insensitively.So under the new convention every team's lead tab is called
lead, and: the scanner's map has one key for all of them, and config validation refuses the second team outright. The current design has the uniqueness burden on the label, and the requirement removes exactly that uniqueness.The new model moves the burden to the space:
(workspace, "lead")is unique,"lead"alone is not.What has to change
LeadTabScanner's index becomes per-workspace —(workspaceLabel, tabLabel) → Entry, or a per-space index. Every caller that resolves a terminal to a lead follows.excludedWorkspaceLabelsneeds re-examining: under one-space-per-team, a space either belongs to a team or is not ours, and "the sharedfleetspace" stops being the normal case.leadin different spaces, and must still be refused in the same space.Leader.tabPrefixdefaults to"lead:", and validation refuses a member tab template that "could match a lead-tab naming convention". A bareleaddoes not start withlead:, so today's prefix check would not protect the new label. Whatever the new convention is, the member-template check must cover it, or a member tab could be named so that it resolves as a lead.Operational hazard — order matters, and getting it wrong demotes a live lead
A lead is found by its configured tab label. Rename the tab before
fleetd.yamlnames the new label and that pane stops matching any lead, so it resolves as a worker and every orchestration call is refused. Theredeploy-fleetdskill already warns about this for restarts; a rename hits it without any restart.So: change
fleet.leaders.<name>.tabfirst, then rename the tab. My own tab islead: opusright now, so this applies to this very session.I cannot do the config half — writing
fleetd.yamlis refused to me by the permission classifier, and the operator owns that file.Instruction surface and recorded facts that go false
Two things currently documented as true stop being true, and both are load-bearing:
So the change must also update
CLAUDE.md'sfleet_whoami/ fallback-ladder paragraph, theredeploy-fleetdskill's identity check, andwiki/11-Features.md. Under this repo's "the prompt is part of the product" rule that is part of the change, not follow-up.Suggested shape
Land it in two steps so no team is ever half-identified:
(workspace, label)still accepts today'slead: opus. No behaviour change for a single team; the validation and the scanner key move.leadand add the first-tab rule in the launcher, with the config edit ahead of the tab rename.Blocked on nothing except the decision in point 4 and the config edit.
The addressing half is now #771: "with cross spaces communicate, we need to identify the space -> tab (names)".
The two must be decided together. If lead identity keys on
(space, "lead")then(space, tab)is exactly the address, andspacehas to mean the same thing in both places. #771 also turned up one concrete gap that blocks everything else and needs no design decision:fleet_list'spanesrows reportworkspaceId: "w2"but never the space name, althoughherdr workspace listhas it (label: "fleet"). I have delegated that one additive change under #771; it is correct under either identity model.Point 4 here (what "first tab" means for identity) and point 4 of #771 (whether a name address is a task or coordination channel) are the two decisions still open.
Operator decision, 2026-10-05 — point 4 is settled
So the lead tab label is a constant in code, not a config key. Two things follow, and together they answer point 4:
lead. Position is not identity. The operator's words settle this: a name is the contract, and a name is stable while a tab position is not — a tab can be reordered or closed at runtime. "First tab of the space" becomes what the launcher aims for when it creates the tab, and nothing matches on it.fleet.leaders.<name>.tabstops being required. It is not needed to find a lead any more, so it is deprecated.This also removes the config edit the ticket said was blocking, and with it the hazard in the section above: no
fleetd.yamlchange has to land before the code.The one way this change can do real damage
lead/LeadLauncher.leadNameOfmatches a tab withLeader.tabLabel(), andcountLeadsuses it to decide how many leads are already live.ensureLeads()then launches the shortfall.So if
tabLabel()starts returningleadwhile the live tab is still calledlead: opus,countLeadscounts 0 live leads and the daemon launches a second Opus lead into thefleetspace. Two orchestrators on one fleet is the failure this class was written to prevent.Measured now, read-only:
So the live state really is the state that would trip this.
How the cutover stays safe
The accepted labels for a lead become a set per space: the constant
lead, plus the deprecatedtab:while it is still configured. Both the scanner andcountLeadsread that same set.That makes every order safe. Before the rename,
lead: opusstill matches. After it,leadmatches. There is no moment when the live lead matches nothing, and no second lead is ever launched. Deleting thetab:key later shrinks the set to the constant, and that is the whole of step 2.Scope now in flight
One
implementerunit, worktree770-lead-tab-is-a-fixed-contract:config/FleetConfig.Leader— aLEAD_TAB_LABEL = "lead"constant,tab:optional, an accepted-labels accessor.tabLabeltemplate that renders asleadrefused; a collaborator tab namedleadrefused.herdr/LeadTabScanner— the lead index keyed on(space label, tab label). Collaborators keep today's space-agnostic exact match; nothing in the requirement asks to change them.FleetdAssembly— builds the per-space index, and warns once per lead that still carriestab:.lead/LeadLauncher—countLeadscounts per space, against the accepted-labels set.Not in this unit, and tracked separately:
msg/LeadCoordLoop.resolveLocalLead()picks the sole lead when no name matchescoordinator.selfId. That is correct for one team and becomes a guess for several. It needs the space as a discriminator too, and it is a different file and a different decision, so it is not bundled here.Instruction surface — the lead's own half
Two recorded facts go false and both are load-bearing, so under the prompt is part of the product they are part of this change, not follow-up:
CLAUDE.md— "the tab label is the only test; workspace is NOT consulted" becomes exactly wrong.redeploy-fleetdskill, check 4 — it namesfleet.leaders.*.tabas what identity matches.wiki/11-Features.md— one entry for the new convention.The
CLAUDE.mdcanonical block must stay byte-identical with the wiki template, and only the lead can run that check, so this half is mine and not the worker's.The code half is merged and live —
7942378, docs ata3d296fDeployed and verified, not just merged. New jar
2f245a1fec31, pid 23954,fleetd listeningat 13:57:33, no ERROR lines since restart.That last line is the check that mattered.
countLeadsfound the live lead through its legacy label andensureLeads()launched nothing — had it matched the constant alone it would have counted zero and started a second orchestrator into thefleetspace.Points 1, 2, 3 and 5 are done. Point 4 is settled.
1. Identity keys on the space —
LeadTabScannerholds a per-space index;LeadLauncher.leadNameOftakes the space.2. The workspace's role is inverted — it is the team boundary now.
excludedWorkspaceLabelswas left alone deliberately, and the reason is worth recording rather than treating as an omission: production passesSet.of(), and it must stay empty, because populating it would skip the lead's own space. The exclusion mechanism is now redundant rather than wrong — the space is part of the key, so nothing needs filtering out. Removing the parameter would be a separate change with its own test cost, and it buys no behaviour.3. Duplicate validation relaxed and re-tightened — two leaders in different spaces with no
tab:at all are accepted; two in the same space are refused, and the message says why.4. "First tab" is a convention, not identity — settled by the operator's own words. A name is a stable contract; a tab position is mutable state. So the label matches and the launcher creates the tab first in its space. Nothing matches on position, and no validation warns about it.
5. The prefix collision is covered — a member
tabLabeltemplate that can render asleadis refused, and so is a collaborator whosetabislead. The default template{role}: {profile} #{n}cannot render it, so this is a guard against a future override rather than a live problem.Instruction surface — done, and smaller than this ticket predicted
I claimed above that
CLAUDE.md's "the tab label is the only test" sentence would go false. That was wrong, and I measured it rather than acting on it: that sentence is in my own session memory, not in the canonical block. The block's only tab claim is aboutfleet.collaborators.<name>.tab, which this change does not touch. The sync check still printsin sync: True.What actually needed changing, both in
a3d296f:.claude/skills/redeploy-fleetd/SKILL.mdcheck 4 — it told the reader a lead is found byfleet.leaders.*.tab.scripts/redeploy-fleetd.shclosing hint — the same sentence, printed at the end of every redeploy. The shell suite still exits 0.wiki/11-Features.md— one new entry, plus a gotcha added to the existing "Startup refusesplacement: panewhile a lead names a tab" entry, because #775 makes half of it false.Still open on this ticket, and both are the operator's
w2:tYfromlead: opustolead. Renaming a fleet pane's tab is a herdr control-plane action, which invariant 5 reserves. Safe at any time — the accepted-label set means there is no window where the lead matches nothing.tab: "lead: opus"fromfleetd.yaml. Blocked on #775, not on the rename. Dropping that key is what disarmsvalidatePanePlacementAgainstLeadTabs, which this ticket's own change caused. A fix is in flight.Not in this ticket
msg/LeadCoordLoop.resolveLocalLead()still falls back to "the sole lead" when no lead's name matchescoordinator.selfId. Correct for one team, a guess for several. It needs the space as a discriminator and it is a separate decision.Step 2 done — this host is fully migrated
The operator renamed tab
w2:tYtoleadand authorized the config edit.tab: "lead: opus"is out offleetd.yaml. Daemon restarted: pid 47129, jar72801148bc3b(unchanged — config only),fleetd listening16:44:19, no ERROR lines.Order mattered, and I checked it before acting rather than after
Deleting
tab:while the tab was still labelledlead: opuswould have been a self-inflicted outage, so I measured the two states first:acceptedLabelstab:present[lead, lead: opus]lead: opustab:deleted, tab not yet renamed[lead]lead: opusNo match means both failures at once: the lead is demoted and refuses every orchestration call, and
countLeadsreturns 0 againstinstances: 1, soensureLeads()launches a second orchestrator into the same space. So the sequence is strictly rename first, then delete — and the rename is safe at any time, becauseleadis accepted either way.I proved the stripped config was valid before it went live, by loading it through the real
FleetConfig.loadandvalidatePanePlacementAgainstLeadTabsagainst the deployed jar:That is also the first live exercise of #775 and step 2 together: the re-armed guard sees a lead with no
tab:and correctly does not refuse, because no profile is pane-placed.Verified after the restart
My first attempt at that last count said STILL PRESENT, and it was wrong: my log window reached back 60 lines and spanned two boots. The control caught it —
fleet health: detection-onlycounted 2, and there is exactly one per boot. Re-anchored the window between the last twofleetd listeninglines (97362–97396), where both controls read 1 and the deprecation WARN reads 0.fleetd.outcarries no date, so a line-count window is not a boot.tabPrefix: "lead:"stays, and it is not leftoverIt has exactly one reader, and that reader is still doing real work:
FleetConfig.java:2809refuses a membertabLabeltemplate that collides with a lead's tab or starts with that prefix. Withtab:gone,leader.tab()is null and the template check short-circuits, but the prefix check still guards against a future member label likelead: …. Keeping it costs nothing and removing it would drop a live guard.The legacy branch in
acceptedLabels()is deliberately NOT removed yetStep 2 as filed also wanted the legacy label branch deleted. I am leaving it, and the reason is not caution for its own sake:
That branch is what makes the cutover order-free, and this host is not the only host.
fleet01runs its ownfleetdfrom its own checkout with its own config. If that config still setstab:and fleet01 later deploys amainwithout this branch, its lead matches nothing and the same double failure happens there — demoted lead plus a second orchestrator — with nobody watching for it.The deprecation WARN is the migration signal, so the branch should outlive the WARN being acted on everywhere, not the WARN being added. Removing it is a separate ticket, and its real precondition is "every host that deploys this repo has no
tab:in its config", which is a fact about other machines and cannot be checked from here. I am messaging fleet01's lead with the hazard and the required order.Open on this ticket
Nothing on this host. Remaining work is the cross-host migration above, plus the item already noted as out of scope:
msg/LeadCoordLoop.resolveLocalLead()still falls back to "the sole lead" when no name matchescoordinator.selfId, which is a guess once a host runs several teams.