#770 made a lead's tab: optional, which silently disarms validatePanePlacementAgainstLeadTabs
#775
Closed
opened 2026-10-05 14:00:20 +02:00 by ltms
·
2 comments
No Branch/Tag Specified
main
worker/799-a76336-9
worker/796-a7911a-5
worker/778-e301b0-1
worker/791-fleet-plugin-0-3-0-9e25b0-2
worker/790-observer-to-lead-send-523d45-1
worker/788-eb7dd0-1
worker/782-styling-probe-4ceb10-9
worker/778-12988a-4
worker/782-18f8bc-5
worker/780-faf58b-3
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#775
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?
Found while writing the Features entry for #770, by re-reading the existing entry "Startup refuses
placement: panewhile a lead names a tab". Latent on this host, not live. Reachable by exactly the config edit #770 tells operators to make.The guard
FleetConfig.validatePanePlacementAgainstLeadTabs()refuses startup when a profile places members by pane while a lead or collaborator names a tab. Its trigger:Why #770 breaks it
Before #770,
tab:was required on every lead, soanyLeaderHasTabwas true whenever any lead existed. The trigger was a reliable proxy for "a lead has a labelled tab".After #770,
tab:is optional and deprecated, and the lead tab is the fixed constantLeader.LEAD_TAB_LABEL = "lead". So a lead still has a labelled tab — but the field the guard reads is gone. The proxy stopped tracking the thing it stood for.Config that disarms it, which is the config #770 recommends:
What it costs
The guard's own refusal message says what it protects:
A pane-placed member splits the currently focused tab, so where it lands depends on what the operator was looking at. Landing in the
leadtab grants that memberfleet_spawn,fleet_stop,fleet_drainandfleet_handoverover the whole fleet, intermittently and by accident of focus.Worse than an explicit
placement: paneplacementunset is therefore not tab placement. So after thetab:key is dropped, a profile that merely omitsplacement:is a pane-placer with no guard watching. It does not take a deliberateplacement: pane.Reachability here — latent, measured 2026-10-05
So nothing is broken in the running fleet. It is two config edits away, and one of them is the
tab:deletion #770 asks for. Do not deletetab:fromfleetd.yamluntil this lands.Fix
Make the trigger read the thing, not the proxy. Every lead now has a labelled tab by construction, so:
That is stronger and simpler than the current test, and it cannot drift again when another field changes. The refusal message needs a matching reword: it currently says "or remove the tab from every fleet.leaders and fleet.collaborators entry", and removing
tab:is no longer a way to satisfy it for a lead — onlyplacement: tabis.The lesson, not the instance
A guard whose trigger is a proxy for the condition it protects goes quiet when the proxy changes, with a green build and no refusal to notice. This one had no test that configured a lead without
tab:and a pane-placed profile, because before #770 that config could not exist.The same entry predicted this shape from a different direction: "a new tab-attached role must widen this validator in the same change, because a guard written for one registry is silent about the next one." Here it was not a new registry. It was the same registry losing the field the guard read. So the rule is wider than it was written: any change to what makes a tab a lead's tab must re-check this validator.
Related: #770 (cause), #661 / PR #667 (the guard).
CORRECTION TO THE BRIEF — read this before you commit
The worker on this unit was right and my brief was wrong. It raised the point as a
fleet_ask, the ~55s window expired before my answer landed, and a send cannot reach it while it is working. So the correction is here, which is the channel that does reach it. This supersedes the brief on the one point below; everything else in the brief stands.What my brief got backwards
I wrote that
Profile.tabPlacement()is"tab".equals(placement), so "a profile with noplacement:key at all is already not tab placement". That is the opposite of what the code does. Measured, not recalled:An unset
placement:defaults to"tab", sotabPlacement()returnstrueand such a profile is tab-placed — safe, and the guard is right not to fire on it.git log -Sdates that line at2e138a1(2026-08-25, CB-634).This narrows the hazard, it does not remove it. The defect needs a profile with an explicit
placement: pane, not merely an omitted key. The live config has none — all eight profiles setplacement: tab— so #775 stays latent today, as the ticket already said.One route I had left implicit is closed, and nothing here needs to cover it:
rejectUnknownPlacement(yaml)is called unconditionally atFleetConfig.java:1956, before the record is parsed, and refuses anyplacement:outside{tab, pane}. So a typo likeplacement: tabbis refused at load whatever the leads configure.Criterion #2 is replaced by two tests
2a — the real defect. Red before the fix, green after.
A lead with no
tab:at all plus a profile with an explicitplacement: panemust be refused. TodayanyLeaderHasTabis false, the method returns early, and that config starts. That is the whole of #775.2b — a positive control pinning the default I got wrong.
A lead with no
tab:plus a profile with noplacement:key must be allowed. Assert the allowed outcome, name the test so the reason is visible, and give the assertion a one-line message: an unsetplacementdefaults totab, so the profile is tab-placed and nothing should fire.2b is worth more than the test it replaces. It makes the fact the worker found into a build check: if someone later changes that default from
tabtopane, 2b goes red and names the reason, instead of this hole widening again in silence.The refusal message is a hard criterion, not a nicety
The current text ends "or remove the tab from every fleet.leaders and fleet.collaborators entry." After #770 that advice is actively harmful for a lead. Removing
tab:is not an escape from the guard — it is what disarms the guard, and the lead's tab is still labelledleadafterwards. The message must offerplacement: tabas the only remedy for a lead, and may keep the remove-the-tab remedy for a collaborator, where it is still true. Say which half applies to which.Unchanged
The fix is still
boolean anyLeaderHasTab = !fleet.leaders().isEmpty();. Scope is still that one method, its message and its tests. Do not touchProfile's placement default — the worker was right to refuse that as out of scope.Record the premise error in your
fleet_reply: what the brief claimed, what the code does, and which criterion you replaced. I want it in the record rather than hidden inside a passing test.The lesson, which outlives this ticket
This is the second time in two tickets that a claim of mine about
placementwas wrong in the same direction — the first was theIT-suffix regex, this is the default. Both came from reading a method body ("tab".equals(placement)) without reading the constructor that feeds it. An accessor does not tell you the field's domain; the compact constructor does. For a record, the defaulting is the contract, and it lives somewhere else in the file.Fixed, merged and live —
b314cb4+fc786d0+c043d14mainisc043d14. Daemon redeployed: pid 26683, jar72801148bc3b,fleetd listening14:23:25, no ERROR lines.fleet_whoamistillprimary.The lead half of the trigger now reads the registry:
A configured lead is a labelled tab, whether or not it says so in YAML. The collaborator half still reads its own
tab:, which stays correct — a collaborator with no tab feeds nothing intoLeadTabScanner.The refusal message no longer sends the operator in a circle. It now says
placement: tabis the only fix when a lead triggered it, and keeps the remove-the-tab remedy for a collaborator, where that remedy still works.Verification I did myself
I re-ran the red-before claim rather than trusting it. Reverting only the trigger line in a throwaway worktree:
Full build
MVN_EXIT=0, 2175 tests / 0 failures, confirmed from Maven's own line and from my own sum over 179 surefire XML files.@Testdelta+2(172 → 174), reconciling 2173 → 2175. Tree parity between the rebased branch and the merge I tested:dfe2c1c7…on both sides.The guard only fires on an invalid config and I cannot write
fleetd.yaml, so rather than claim a live refusal I proved the running jar holds the new code, with controls either side:My brief was wrong, and the worker caught it
I wrote that a profile with no
placement:key is already not tab-placed. The opposite is true —FleetConfig.java:525defaults an unsetplacementto"tab". I had read the accessortabPlacement()and never the compact constructor 321 lines above it.So this hazard always needed an explicit
placement: pane. Real, and narrower than I filed it.The worker refused to write a test to a premise it could see was false, raised a
fleet_ask, got no answer inside the ~55s window, proceeded on its own judgment, and then found my ticket correction on the mandatory re-read and matched it. That is the turn contract doing exactly its job, and it is the reason this ticket has a correct positive-control test instead of a wrong one.That control is the lasting part: a test now asserts that a profile with no
placement:key is allowed, naming the default as the reason. If the default ever changes, it fails loudly instead of reopening this hole in silence.One commit is mine, not the worker's
c043d14renamesanyLeaderHasTabtoanyLead. The flag testedleader.tab()and was named for it; it now tests whether any lead exists at all. My correction comment handed the worker that stale name verbatim, so the fix is mine. Rebuilt green.Unblocked:
tab:can now be deletedThis was the only thing blocking #770 step 2.
tab: "lead: opus"can come out offleetd.yamlwhenever the operator wants, and the legacy-label branch inLeader.acceptedLabels()can be removed after that. The rename of tabw2:tYtoleadis independent and also safe at any time.Docs
wiki/11-Features.md(2012fff): the entry's What now describes the registry trigger and the role-split remedy; the "trigger is disarmed" gotcha is replaced by the fix and the two tests; and the "do not deletetab:" blocker is cleared from the #770 entry.CLAUDE.md↔ wiki sync check:in sync: True.Lesson for the next ticket
An accessor shows you a comparison, not a field's domain. For a Java record the compact constructor holds every default and normalization, so the constructor is the contract. A one-line getter looks like the whole truth precisely because there is nothing in it to warn you. Read the constructor before asserting what a field can hold — especially when the claim is in the negative ("if it is not set, then…"), because that is a claim about the default.