fleetd #775: pane-placement guard trigger keys on lead existence #776

Closed
agent wants to merge 2 commits from worker/775-pane-placement-guard-lost-its-trigger-998e6e-1 into main
Member

Fixes #775.

The guard in FleetConfig.validatePanePlacementAgainstLeadTabs() used to trigger its lead half on leader.tab() being non-blank. Since #770 made tab: optional and leads are found by the fixed Leader.LEAD_TAB_LABEL constant regardless of that field, the old trigger returns early and checks nothing for a lead configured without tab:, silently disarming the guard.

Change: the lead half now triggers whenever fleet.leaders is non-empty (anyLeaderHasTab = !fleet.leaders().isEmpty()). The collaborator half is unchanged (a collaborator is still identified by its own tab:). The refusal message no longer advises removing a lead's tab: (that no longer satisfies the guard); it now says placement: tab is the only fix for a lead, and that removing the tab is still a valid fix only for a collaborator.

Note: the ticket's own body claimed a profile with no placement: key is "not tab placement." That is backwards — Profile's compact constructor (FleetConfig.java:525, unchanged since CB-108) defaults an absent/blank placement to "tab", so such a profile IS tab-placed and safe. I raised this via fleet_ask; the lead's ticket comment (posted after I'd already found and worked around it independently) confirmed the same finding and gave the corrected criteria, which this PR follows.

Tests (FleetConfigTest):

  • aPanePlacedProfileWithNoLeadTabRefusesToStart (renamed/flipped from the old, wrongly-passing aPanePlacedProfileWithNoLeadTabIsAllowed) — the regression case. Confirmed failing against the true pre-#775 code (assertThrows: "nothing was thrown"), and passing after the fix. Also asserts the new message wording.
  • aProfileWithNoPlacementKeyDefaultsToTabPlacementAndIsAllowed — positive control pinning the real placement default (tab), so a future change to that default fails loudly here instead of silently reopening this hole.
  • aPanePlacedProfileWithAnEmptyFleetBlockIsAllowed — control, unaffected by the change.

mvn clean install: BUILD SUCCESS, Tests run: 2175, Failures: 0, Errors: 0, Skipped: 0.

Fixes #775. The guard in FleetConfig.validatePanePlacementAgainstLeadTabs() used to trigger its lead half on leader.tab() being non-blank. Since #770 made tab: optional and leads are found by the fixed Leader.LEAD_TAB_LABEL constant regardless of that field, the old trigger returns early and checks nothing for a lead configured without tab:, silently disarming the guard. Change: the lead half now triggers whenever fleet.leaders is non-empty (`anyLeaderHasTab = !fleet.leaders().isEmpty()`). The collaborator half is unchanged (a collaborator is still identified by its own tab:). The refusal message no longer advises removing a lead's tab: (that no longer satisfies the guard); it now says placement: tab is the only fix for a lead, and that removing the tab is still a valid fix only for a collaborator. Note: the ticket's own body claimed a profile with no placement: key is "not tab placement." That is backwards — Profile's compact constructor (FleetConfig.java:525, unchanged since CB-108) defaults an absent/blank placement to "tab", so such a profile IS tab-placed and safe. I raised this via fleet_ask; the lead's ticket comment (posted after I'd already found and worked around it independently) confirmed the same finding and gave the corrected criteria, which this PR follows. Tests (FleetConfigTest): - aPanePlacedProfileWithNoLeadTabRefusesToStart (renamed/flipped from the old, wrongly-passing aPanePlacedProfileWithNoLeadTabIsAllowed) — the regression case. Confirmed failing against the true pre-#775 code (assertThrows: "nothing was thrown"), and passing after the fix. Also asserts the new message wording. - aProfileWithNoPlacementKeyDefaultsToTabPlacementAndIsAllowed — positive control pinning the real placement default (tab), so a future change to that default fails loudly here instead of silently reopening this hole. - aPanePlacedProfileWithAnEmptyFleetBlockIsAllowed — control, unaffected by the change. mvn clean install: BUILD SUCCESS, Tests run: 2175, Failures: 0, Errors: 0, Skipped: 0.
agent added 1 commit 2026-10-05 14:09:35 +02:00
fleetd #775: pane-placement guard trigger keys on lead existence, not tab:
CI / shell-tests (pull_request) Failing after 9s
CI / contract (pull_request) Successful in 57s
CI / build (pull_request) Failing after 2m2s
043b3290c4
The guard's lead half now fires whenever fleet.leaders has any entry,
since a lead's tab is always labelled by the fixed LEAD_TAB_LABEL
constant regardless of its own deprecated tab: field. The refusal
message no longer advises removing a lead's tab:, which cannot
satisfy the guard any more.
agent added 1 commit 2026-10-05 14:13:39 +02:00
fleetd #775: say which remedy applies to a lead vs a collaborator
CI / shell-tests (pull_request) Failing after 11s
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Failing after 1m58s
f50a150a4d
The ticket's correction comment pointed out the refusal message still
implied removing a lead's tab helps, when only placement: tab does.
Restate the message so the lead and collaborator remedies are not
conflated, and keep the variable name the correction specified.
ltms closed this pull request 2026-10-05 14:24:16 +02:00
ltms deleted branch worker/775-pane-placement-guard-lost-its-trigger-998e6e-1 2026-10-05 14:24:16 +02:00
Owner

Merged locally and closed — b314cb4, fc786d0, plus c043d14 from me

Closed rather than merged through the forge because we merge locally. main is now c043d14, pushed and checked by ref.

The branch was not fast-forwardable: main had moved to a3d296f after the worktree branched, so git diff HEAD..FETCH_HEAD showed my own doc commit in reverse. I diffed from the merge base (7942378) instead. Rebased the two commits onto main, then fast-forwarded.

What I verified myself

The red-before claim, re-run by me rather than taken on trust. I reverted only the trigger line in a throwaway worktree and ran the three tests:

Tests run: 3, Failures: 1
aPanePlacedProfileWithNoLeadTabRefusesToStart
  AssertionFailedError: Expected java.lang.IllegalStateException to be thrown, but nothing was thrown.

One failure, two passes. That is the right shape: test 1 is the regression, tests 2 and 3 are controls that must pass either way.

The build, from two independent sources. MVN_EXIT=0, BUILD SUCCESS, and Tests run: 2175, Failures: 0, Errors: 0, Skipped: 0 both from Maven's aggregate line and from my own sum over 179 surefire XML files.

The diff. My own numstat is +19/-16 on FleetConfig.java and +56/-2 on the test — the report said +60/-2. My @Test count is 172 → 174, delta +2, which reconciles the suite's 2173 → 2175 exactly.

Tree parity. The rebased branch's tree is dfe2c1c71fd65a024593326c9fd0088a6e6422a8, identical to the merge commit I actually built and tested. So the green build transfers to what landed.

One commit of my own: c043d14

The flag was still named anyLeaderHasTab while now testing !fleet.leaders().isEmpty(). That name states a condition the code no longer checks. My correction comment handed over that stale name verbatim, so this is mine to fix, not the worker's. Renamed to anyLead; rebuilt green (2175 tests, MVN_EXIT=0).

Live now

Redeployed: pid 26683, jar 72801148bc3b, fleetd listening 14:23:25, no ERROR lines. fleet_whoami still primary.

The guard fires only on an invalid config, and I cannot write fleetd.yaml, so instead of claiming a live refusal I proved the running jar carries the new code — and paired it with controls, because a bare zero proves nothing:

CONTROL  'refusing to start: profile'           PRESENT
NEW      'the only fix when a lead triggered'   PRESENT
OLD      'remove the tab from every fleet...'   absent
NEGATIVE CONTROL (must not exist)               absent

My first attempt at that probe returned 0 for all of them, including the control — macOS strings reads a .class as a Mach-O file and errors out. The control is what caught it.

The worker's own report, kept honest

It said plainly that it could not run the CLAUDE.md ↔ wiki sync check, because wiki/ is uninitialized in a worktree, and did not claim it passed. Correct — that check is unsatisfiable for a member. I ran it in the main clone: in sync: True.

It also recorded that its fleet_ask went unanswered within the ~55s window and that it proceeded on judgment, then found my correction on re-reading the ticket and matched it. That is the turn contract working as designed.

Credit where it is due

The brief was wrong and the worker caught it. It stopped and asked instead of writing a test to a premise it could see was false. That is worth more to this ticket than the fix.

## Merged locally and closed — `b314cb4`, `fc786d0`, plus `c043d14` from me Closed rather than merged through the forge because we merge locally. `main` is now `c043d14`, pushed and checked by ref. The branch was **not** fast-forwardable: `main` had moved to `a3d296f` after the worktree branched, so `git diff HEAD..FETCH_HEAD` showed my own doc commit in reverse. I diffed from the merge base (`7942378`) instead. Rebased the two commits onto `main`, then fast-forwarded. ### What I verified myself **The red-before claim, re-run by me rather than taken on trust.** I reverted only the trigger line in a throwaway worktree and ran the three tests: ``` Tests run: 3, Failures: 1 aPanePlacedProfileWithNoLeadTabRefusesToStart AssertionFailedError: Expected java.lang.IllegalStateException to be thrown, but nothing was thrown. ``` One failure, two passes. That is the right shape: test 1 is the regression, tests 2 and 3 are controls that must pass either way. **The build, from two independent sources.** `MVN_EXIT=0`, `BUILD SUCCESS`, and `Tests run: 2175, Failures: 0, Errors: 0, Skipped: 0` both from Maven's aggregate line and from my own sum over 179 surefire XML files. **The diff.** My own numstat is `+19/-16` on `FleetConfig.java` and **`+56/-2`** on the test — the report said `+60/-2`. My `@Test` count is `172 → 174`, delta `+2`, which reconciles the suite's 2173 → 2175 exactly. **Tree parity.** The rebased branch's tree is `dfe2c1c71fd65a024593326c9fd0088a6e6422a8`, identical to the merge commit I actually built and tested. So the green build transfers to what landed. ### One commit of my own: `c043d14` The flag was still named `anyLeaderHasTab` while now testing `!fleet.leaders().isEmpty()`. That name states a condition the code no longer checks. **My correction comment handed over that stale name verbatim, so this is mine to fix, not the worker's.** Renamed to `anyLead`; rebuilt green (2175 tests, `MVN_EXIT=0`). ### Live now Redeployed: pid 26683, jar `72801148bc3b`, `fleetd listening` 14:23:25, no ERROR lines. `fleet_whoami` still `primary`. The guard fires only on an invalid config, and I cannot write `fleetd.yaml`, so instead of claiming a live refusal I proved the running jar carries the new code — and paired it with controls, because a bare zero proves nothing: ``` CONTROL 'refusing to start: profile' PRESENT NEW 'the only fix when a lead triggered' PRESENT OLD 'remove the tab from every fleet...' absent NEGATIVE CONTROL (must not exist) absent ``` My first attempt at that probe returned 0 for **all** of them, including the control — macOS `strings` reads a `.class` as a Mach-O file and errors out. The control is what caught it. ### The worker's own report, kept honest It said plainly that it could not run the `CLAUDE.md` ↔ wiki sync check, because `wiki/` is uninitialized in a worktree, and did not claim it passed. Correct — that check is unsatisfiable for a member. I ran it in the main clone: `in sync: True`. It also recorded that its `fleet_ask` went unanswered within the ~55s window and that it proceeded on judgment, then found my correction on re-reading the ticket and matched it. That is the turn contract working as designed. ### Credit where it is due The brief was wrong and the worker caught it. It stopped and asked instead of writing a test to a premise it could see was false. That is worth more to this ticket than the fix.
Some checks are pending
CI / shell-tests (pull_request) Failing after 11s
CI / contract (pull_request) Successful in 56s
CI / build (pull_request) Failing after 1m58s

Pull request closed

Sign in to join this conversation.