042b8c99ddf17a091ce23fc0ee0e716514d230f1
870 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
042b8c99dd |
fleetd #480 follow-up correction: cover Fleetd.leadRollover(...)'s own wiring behaviourally
Add FleetdLeadRolloverWorkspaceLookupTest, calling the package-private Fleetd.leadRollover(...) factory directly (with a real ConfigRef built from a temp fleetd.yaml, never the gitignored live one) to prove the terminal -> lead-name -> Leader.cwd() lookup it builds actually works: a relative handoverPath resolves against the calling lead's configured cwd; a terminal absent from the live lead-terminal map falls back to user.dir; and the lookup is read live, not snapshotted at construction time (a lead discovered by the tab scan after leadRollover(...) was built still resolves correctly). Proved this closes the gap: mutating the factory's lambda body (String leadName = null;, always "no lead found", which forces the daemon-cwd fallback this ticket exists to fix) left the full 1669-test suite green before this commit. With the new test added, the same one-line mutation now fails 2 of its 3 cases; reverting it goes green again (3/3). Mutation applied/reverted only during verification and is not part of this commit (git diff on Fleetd.java is empty). FleetdLeadRolloverWiringTest's class javadoc corrected: it previously claimed no behavioural test could catch this wiring dropping out, which was true only before this commit and only covered the factory's own body, not its call site. Restated what each test class actually covers: the source- text pin covers the call site's argument list; the new behavioural test covers the lambda's body. |
||
|
|
4bfab6b718 |
fleetd #480 follow-up: resolve a relative leadRollover.handoverPath against the calling lead's workspace
LeadRollover.open() now resolves handoverPath to an absolute path exactly once, against the calling lead's fleet.leaders.<name>.cwd (falling back to the daemon's own user.dir when that lead has none configured), matching the LeadLauncher#launch precedent. PendingRollover stores only the resolved absolute path, so checkHandover's exists/empty/fresh checks, the path handed back to the lead in the fleet_handover open response, and the default bootstrapText sentence all see the same absolute location instead of a value resolved against whatever directory the daemon process happened to start in. FleetConfig.LeadRollover.bootstrapText is no longer defaulted in the compact constructor (it would otherwise still bake in the raw, possibly-relative handoverPath); a new bootstrapTextFor (resolvedHandoverPath) method builds the default sentence from the resolved path instead. Fleetd.leadRollover(...) gains a required liveLeadTerminals parameter to build the terminal to lead-name to Leader.cwd lookup, read live through the existing `leads` supplier and ConfigRef on every call, never off a startup snapshot. |
||
|
|
008a457557 |
docs: fleet_handover is shipped — update the charter table and the handover skill
fleetd #480 merged in #483, #484 and #485, so the instruction surface has to catch up. Per CLAUDE.md's own rule, a code change that silently invalidates the canonical block is an incomplete change. CLAUDE.md + wiki/7-Use-Cases.md: one new intent-to-tool row for fleet_handover. Both copies edited identically; the byte-identical sync check prints True. The row carries the trap rather than just the call: open FIRST, then write the file, then confirm — because confirm refuses the file as stale unless its modified time is later than the open request, so the obvious order fails. .claude/skills/handover/SKILL.md: the skill said "do not call a fleet_handover tool: it does not exist." That was true when it was written this morning and is now false, which is the worst state for an instruction file to be in. Replaced with the two real paths (by hand, or with the tool when leadRollover: is configured), plus a new section 11 giving the three-step order and the five things that surprise a caller — chiefly that `accepted` does not mean the pane has been cleared, and that operatorConfirmed is a report of what a human said, not a confidence level. Section 11 also states plainly that the bootstrap prompt landing in a freshly cleared pane is not yet proven end-to-end, and that the recovery is the manual path — which is why the file is written before confirm, never after. |
||
|
|
9494a6b99a |
Merge #485: fleetd #480 Unit C — the fleet_handover MCP tool
fleet_handover{action: "open"|"confirm"|"cancel"} drives LeadRollover, which #483 and
#484 landed with nothing calling it. Primary-only via a new Authz.Action.HANDOVER, on
the same case line as SPAWN/STOP/DRAIN.
The tool has NO terminal, session or leadTerminal parameter of any kind — the pane is
always callerTerminal(exchange), resolved from the connection. A lead can therefore only
ever roll itself, never another lead. That is charter invariant 3, and it is the second
of the two corrections recorded in LeadRollover's class javadoc.
Registered unconditionally, so the tool surface does not vary with config: with
leadRollover: absent, every action returns a clean NOT_CONFIGURED refusal instead of
failing, and open()'s IllegalStateException (config removed by a hot reload after
construction) is caught and turned into the same refusal. A config-dependent tool set
would have collided with #474's charter tool-surface gate and McpContractDocTest.
Correction round applied before merge, and it is the reason this took two passes.
The unit first shipped with a defaulted 15-argument FleetMcp constructor delegating to
the new 16-argument one with leadRollover = null. I mutated the wiring rather than
reasoning about it: deleting just the leadRollover argument from Fleetd.main's FleetMcp
call compiled with 0 errors and passed all 1659 tests, BUILD SUCCESS — while the live
daemon would have answered NOT_CONFIGURED to every fleet_handover call for ever.
Neither FleetMcpHandoverTest (it builds its own FleetMcp) nor FleetdLeadRolloverWiringTest
(it pins that LeadRollover is constructed, not that it is passed on) could see it.
The worker then found the defect was wider than I had named: all five shorter
constructors (11/12/13/14/15-arg) formed one defaulting chain into the 16-arg one, each
silently supplying another feature's "off" value — leadChannel, outage, leadSeats, peers,
and finally leadRollover. All five are deleted. FleetMcp now has exactly one public
constructor, so every one of those features is compile-enforced at its call site, not
just this one.
Verified by the lead before merge, on PR head merged with current main (0176378):
- CI run 1728 green on
|
||
|
|
eb0557621e |
fleetd #480 correction round: collapse FleetMcp to one required constructor
FleetMcp had a defaulted 15-argument constructor that delegated to the new 16-argument one with an implicit null for leadRollover. Dropping the leadRollover argument from Fleetd.main's FleetMcp(...) call fell back to that shorter overload, compiled fine, and left all 1659 tests green — the live daemon would then answer NOT_CONFIGURED to fleet_handover forever with nothing going red. Delete every overload that could reach the 16-arg constructor with a silently-defaulted leadRollover (11/12/13/14/15-arg forms all chained to it), leaving the 16-arg constructor as FleetMcp's sole public constructor. Update FleetMcpAuthzTest's call site to pass every parameter explicitly (leadChannel null, OutageSource.none(), LeadSeatSource.none(), List.of(), leadRollover null) — Fleetd.java and FleetMcpHandoverTest already called the full form. Proved with mvn -o -q compile: removing the leadRollover argument from Fleetd.main now fails to compile instead of silently defaulting. No behaviour changes — NOT_CONFIGURED refusals are unchanged. |
||
|
|
a0eed6f01b |
Merge #484: fleetd #480 Unit E — a BLOCKED pane is not a settled pane
LeadRollover's two settle waits used AgentStatus#injectable(), which is
IDLE || BLOCKED || DONE. That is the right rule for Injector ("may I deliver a message
without stepping on a live turn") and the wrong one here ("has the turn actually
ended"), because BLOCKED is a live turn that is merely paused — a pane sitting on an
approval prompt.
Path in: the lead calls confirm(); its turn carries on and hits anything needing
approval; herdr reports blocked; within 250ms the deferred continuation reads that as
settled; /clear is typed into an open prompt. That destroys the lead's live context
mid-turn, which is exactly what the turnSettleSeconds gate added in #483 exists to
prevent. The window is turnSettleSeconds, default 20s.
Both waits now require a real turn boundary — IDLE or DONE. AgentStatus#injectable() is
untouched: it is correct for Injector, LeadHeartbeatLoop, LeadCoordLoop, ReplyPushLoop
and HerdrPeerLauncher's two readiness checks, all of which are delivery gates.
Verified by the lead before merge:
- CI run 1726 green on head
|
||
|
|
e2a91e883e |
fleetd #480 Unit E correction: retire "injectable" wording from the log lines
Both waitUntilAtTurnBoundary guard messages still said "never went idle" / "did not become injectable" — the old mental model the rename was meant to retire. Made both say what the code now actually waits for: a turn boundary (IDLE or DONE). |
||
|
|
62646957ea |
fleetd #480 Unit C: wire fleet_handover MCP tool onto LeadRollover
Adds the fleet_handover tool (open/confirm/cancel) as a thin adapter over LeadRollover, registered unconditionally so the charter tool-surface gate sees a stable set regardless of whether leadRollover: is configured. With a null LeadRollover every action degrades to a clean NOT_CONFIGURED refusal instead of throwing. Gated on a new Authz.Action.HANDOVER (primary-only, same as SPAWN/STOP/DRAIN). The caller's own connection-resolved terminal is the only lead identity ever used — the tool's input schema carries no terminal/session/leadTerminal parameter, so a lead can only ever roll itself. Fleetd.main now passes its existing leadRollover local into FleetMcp via a new trailing constructor parameter. |
||
|
|
802c0ab701 |
fleetd #480 Unit E: BLOCKED is not a settled turn boundary
LeadRollover's waitUntilInjectable used AgentStatus#injectable(), which accepts BLOCKED. A BLOCKED pane is paused mid-turn on a prompt, not settled — reusing injectable() let /clear (or the bootstrap text after it) fire into an open approval prompt within the 20s settle window, destroying the lead's live context. Renamed the helper to waitUntilAtTurnBoundary and restricted both waits to IDLE or DONE only, with a comment explaining why this class does not reuse injectable() (it answers "may I deliver", not "has the turn ended"). Added tests for BLOCKED-forever on both waits (zero sends / exactly one send) and for DONE still completing the full roll. |
||
|
|
4bab23e241 |
Merge #483: fleetd #480 Unit A — lead rollover core (config + executor)
Two corrections were applied to the original unit before this merge, and the PR description above still describes the pre-correction shape: 1. confirm() no longer rolls inline. It validates every gate, then hands a one-shot continuation to continuationRunner and returns. confirm() is called BY the lead FROM its own turn, so the pane is WORKING and cannot report injectable until confirm() returns; the old code sent /clear first and then timed out waiting, which destroyed the lead's context and started no fresh session. The continuation waits for the calling turn to settle FIRST (turnSettleSeconds, a new knob), and if that wait times out it sends no /clear at all. 2. The pane to roll comes from the caller's terminal, not PrimaryRegistry. A single-slot lookup let lead X's confirm() clear lead Y's pane. open() records the caller's terminal; confirm() refuses with NOT_YOUR_ROLLOVER on a mismatch. Verified by the lead before merge: - CI run 1722 green on head |
||
|
|
a94262271b |
fleetd #480 correction round: defer the roll, and gate it on caller identity
Two defects found after the fact, both from the original brief, both fixed here. 1. confirm() is called FROM the calling lead's own turn, so its pane is still WORKING and can never report injectable inside that same call. The old confirm() sent /clear before polling for that — the poll always timed out, but only after /clear had already fired and queued, destroying the lead's context with no fresh session ever started and a refusal return that lied about what had happened. Fix: confirm() now only validates and, if every gate passes, hands a one-shot continuation to a new continuationRunner (a real virtual thread in production, Runnable::run in tests) and returns RollDecision.approved() immediately - "scheduled", not "rolled". The continuation itself does the actual work, once the calling turn has ended: wait for the SAME pane to report injectable again (new turnSettleSeconds config key, default 20) - if this never happens, /clear is NEVER sent, at all - then /clear, then wait again (clearSettleSeconds, as before), then bootstrapText. The "no timer/scheduler, only confirm() can roll" invariant is restated precisely in LeadRollover's class javadoc: it is about initiative, not synchronicity - a single-shot continuation of an already-approved confirm() call still satisfies it; a recurring background loop would not. 2. confirm() resolved the pane to clear via PrimaryRegistry.primaryTerminal(), a single-slot lookup that is correct for a background loop with no caller but wrong here: on a daemon with more than one labelled lead tab, lead X's confirm() could clear lead Y's pane, violating the charter's "identity comes from the connection, never an argument" invariant. Fix: open() and confirm() now take the caller's terminal id as a parameter (resolved by the MCP layer from the connection - the later MCP-tool unit must pass it in, never accept it as a request field). confirm() refuses with a new NOT_YOUR_ROLLOVER reason unless it matches the terminal open() recorded. LeadRollover no longer depends on PrimaryRegistry at all. Also: renamed RollResult to RollDecision (rolled -> accepted) to reflect the new meaning - approved and scheduled, not necessarily cleared yet. Added turnSettleSeconds to the leadRollover: config block (documented in fleetd.example.yaml alongside the existing keys) and updated Fleetd.java's leadRollover(...) factory to drop the primaryRegistry parameter, with FleetdLeadRolloverWiringTest's source-text pin updated to match. New tests: turnThatNeverSettlesSendsNoClearAtAll (the branch that matters most - a turn that never ends means /clear is never sent) and aDifferentLeadTerminalCannotConfirmAnotherLeadsRollover (NOT_YOUR_ROLLOVER), plus a settle-after-clear timeout test and an open() input-validation test. LeadRolloverTest: 11 -> 14 tests. |
||
|
|
5c12865c25 |
fleetd #480 Unit A: lead rollover core (config block + executor)
Adds the opt-in leadRollover: config block and LeadRollover, the executor a later unit's MCP tool will call. A lead writes a handover file, then open() records a token and confirm() verifies it (exists, non-empty, fresh) and an operator confirmation before clearing the lead's own pane via /clear (sent directly through AgentControl, bypassing Injector, same as ClaudeCodeLauncher#clearContext) and bootstrapping a fresh session. Nothing but an explicit confirm() call can ever roll a pane - no timer, no heartbeat, no background thread anywhere in this class. Wired into Fleetd.java exactly like LeadHeartbeatLoop: constructed only when leadRollover: is present at startup, and nothing calls it yet - the MCP tool is a separate, later unit. Classified leadRollover: as HOT in ConfigRef (joins placement/ memberCredentials/memberLoginShell/models): the executor holds Supplier<FleetConfig.LeadRollover> and reads every field fresh per call, unlike LeadHeartbeatLoop's frozen final fields. The one caveat: the object's construction is still gated on presence in the startup config snapshot, so a freshly-added block needs a restart before anything exists to call. Tests: LeadRolloverTest (14 cases covering the 6 hard requirements - no object without the config block, only confirm() can roll, missing/empty/ stale handover file each refuse by name, requireOperatorConfirm gating, and the injected wall-clock supplier) and FleetdLeadRolloverWiringTest (source- text pin on Fleetd.main's construction call, mirroring FleetdCompletionResolverWiringTest). Also updated the existing FleetConfigValidateAllTest, FleetConfigWithDefaultsPreservesEveryComponentTest, ConfigRefTopLevelCoverageTest and ConfigRefTopLevelReportingCoverageTest to account for the new record component. |
||
|
|
3f8c956325 |
docs: the handover skill must not promise a tool that does not exist
The skill shipped in #481 described fleetd #480's automation in the present tense: "fleetd ticket #480 lets a lead session hand off to a fresh one". The tool is not built. A session loading the skill would look for a fleet_handover tool it cannot call, and this repo already knows what a confidently wrong instruction costs — a wrong comment stops the reader investigating with a false conclusion, which is worse than no comment. Now says plainly: today the handoff is manual, #480 will automate the same cycle, and do not call fleet_handover because it does not exist. The nine procedure rules are unchanged — only who performs the swap changes when #480 ships, not what the file must contain. My wording to fix, not the worker's: the brief handed them the present tense. |
||
|
|
2051ceeb26 |
Merge #481: the handover skill (fleetd #480 Unit B)
Adds .claude/skills/handover/SKILL.md — the outgoing lead's procedure for writing
the file a fresh lead session inherits.
Reviewed by the lead against the eight requirements in the brief: every number
carries its command, measured-vs-reported is marked, open decisions name their
owner, live hazards, what is not owed, a first section of three re-measurement
commands, a time-and-commit stamp, and a plain statement that the file goes
stale. All eight are present, plus a leave-out section and a template.
Verified by the lead, not taken from the worker's report:
- CLAUDE.md diff is the addendum's skill list only, in the primary-side group.
- The canonical block is byte-identical with the wiki template (sync check True).
- CI run 1718 green on
|
||
|
|
325d0771a4 |
fleetd #480 Unit B: add handover skill for lead session handoff
Adds .claude/skills/handover/SKILL.md, the procedure an outgoing lead follows to write the handover file a fresh lead session inherits when fleetd clears the pane. Registers the new skill in CLAUDE.md's primary-side skills list; no other change to CLAUDE.md. |
||
|
|
7b97aae85b |
docs: move the redeploy procedure out of CLAUDE.md into a skill
CLAUDE.md loads into every session. The daemon-redeploy procedure is needed only when someone redeploys, so it paid for context it did not use: 58 lines, about 918 est. tokens, every session. The procedure now lives in .claude/skills/redeploy-fleetd/SKILL.md, which loads only when invoked. The moved text is byte-identical to what was removed, plus one new paragraph documenting --no-build (scripts/redeploy-fleetd.sh:34,62,266) — the script and the wiki already had that flag, CLAUDE.md never did. CLAUDE.md keeps an 11-line pointer, because two rules must stay resident: a merge is not a deployment, and workers must never redeploy. A session learns it needs the procedure before it needs the skill, then the next line names the skill. Also updates the addendum's primary-side skill list, as this file's own rule for .claude/skills/** changes requires. CLAUDE.md: 34,442 -> 31,591 chars. Canonical block untouched — the wiki sync check still prints True. |
||
|
|
49a404ddf3 |
Merge #474 follow-up: pin main's ConfigRef wiring against the surviving mutation
My battery on the #474 merge found one survivor: reverting Fleetd.java:154 from the three-argument ConfigRef constructor to the plain two-argument one turns the live reload gate off and leaves all 1633 tests green. Both new #474 tests build their own ConfigRef with the method reference, so neither reads what main chose. This adds FleetdConfigRefWiringTest, following the three source-text precedents already in the tree (FleetdBackendQuarantineWiringTest, FleetdLeadSeatWiringTest, FleetdCompletionResolverWiringTest) rather than the weaker sibling pattern that builds the wiring itself. No production change. The worker branched fresh off |
||
|
|
72d6a6878b |
fleetd #474 follow-up: pin main's config wiring against M2
Fleetd.main's own choice of the three-argument ConfigRef constructor (with Fleetd::assertChartersNameOnlyRegisteredTools as extraValidation) was unpinned. Reverting Fleetd.java:154 to the plain two-argument constructor compiled with 0 errors and left the whole suite green, because ConfigRefTest and FleetdConfigRefCharterToolSurfaceWiringTest each build their own ConfigRef directly rather than through main. Adds FleetdConfigRefWiringTest, a source-text check on Fleetd.java following the FleetdBackendQuarantineWiringTest/FleetdLeadSeatWiringTest/ FleetdCompletionResolverWiringTest precedent: asserts the exact three-argument construction is present, asserts the plain two-argument form is absent, and guards against a vacuous pass on a broken/empty source read by first asserting an unrelated anchor is present. |
||
|
|
4466ee0ef2 |
fleetd #474: ConfigRef.reload() runs the charter tool-surface gate too
A charter naming an MCP tool the server does not register refused Fleetd.main at startup but slipped through ConfigRef.reload(), because reload() only ran FleetConfig.validateAll(), which never looks at what a charter's text names. CharterToolSurface stays in the mcp package (config must not depend on it), so ConfigRef now accepts the check as a Consumer<FleetConfig> extraValidation, run inside reload()'s same try/catch as validateAll(). Fleetd.main wires a new package-private adapter, Fleetd.assertChartersNameOnlyRegisteredTools, into both the startup call site and ConfigRef's constructor, so the two call sites can never check different things. Tests: ConfigRefTest (reload refuses/accepts, via a locally-built equivalent consumer since Fleetd's method is package-private to dev.ltms.fleet) and the new FleetdConfigRefCharterToolSurfaceWiringTest (same proof through the exact Fleetd::assertChartersNameOnlyRegisteredTools reference production uses). Verified deleting the new extraValidation.accept(fresh) call site fails both new "refuses" tests by name. (cherry picked from commit |
||
|
|
25ba7f16bb |
Merge #473: fleet_profiles and fleet_list report which attempt a quarantine is on (fleetd #466 item 2)
The escalating cooldown landed in
|
||
|
|
5ba69c9cf2 |
fleetd #393: correct a comment that claimed two tests pin a call they cannot
The comment above the charter writer said flipping that call back to putArray was "proven load-bearing" and pointed at two named tests. I mutated exactly that, on the merge commit, and it SURVIVED at 1618 green - so neither named test covers it, and neither can. The reason is structural, not a missing test: that writer runs first against an empty array, so putArray has nothing to replace and the two idioms are equivalent there. No test can distinguish them. The worker measured the same thing independently and said so in their report; the comment was left over from the pre-fix state, where the mutation being described was a DIFFERENT one (a later writer destroying the charter). A comment that names tests which do not cover the line is worse than no comment. The next person mutates the line, sees green, and concludes the tests are broken. The corrected version states what each cell actually measures: this line is unpinnable and why, the later writers ARE pinned and by which test names, and deleting this line entirely fails the charter-reaches-the-member assertion - the hole that predates #393. |
||
|
|
17052bb515 |
Merge #471: seeded skills reach an opencode member, and instructions[] stops depending on write order (fleetd #393)
memberSkills: copied skill folders into every provisioned worktree's .claude/skills/ and stopped there. Claude Code reads that directory natively; opencode never does. So the feature was INERT for opencode members rather than broken: the copy succeeded, the files were correct, and nothing ever read them. No test failed because there was nothing to fail - the feature worked at the only layer it implemented. An opencode member's only channel for static guidance is the instructions[] array in its generated config. OpenCodeLauncher now adds each seeded skill's SKILL.md there. A folder with no SKILL.md is never delivered and the log names it. The second half is an ordering hazard fleet01 found by reading, and that I then measured. Three writers append to instructions[]: the role charter, the seeded skills, and the IDE rules. The charter used putArray (CREATE-OR-REPLACE) while the other two used withArray (get-or-create). That was safe only because the charter ran first against an empty array - an undeclared constraint that nothing tested. Measured on the earlier merge: making the skills writer use putArray left 1603 tests green while silently deleting the charter entry, so an opencode member would launch with no role contract at all. Worse than the bug being fixed, and invisible. All three writers now use withArray, and three tests pin the array's CONTENTS (never its size - a size assertion passes when putArray swaps two entries for two others) across the combinations that matter: charter-only, charter+ide, charter+skills+ide. WHY NOTHING CAUGHT IT, MEASURED RATHER THAN ASSUMED. fleet01 first said the missing axis was the COMBINATION of writers, then revised that to a stronger claim: that nothing asserted the charter reaches an opencode member at all. I checked the second claim on this merge and it is FALSE. Deleting the charter writer outright fails 6 tests, and 3 of those existed before #393: writesRemoteMcpConfigAndCharterInstructionsWhenMcpUrlSet, roleCharterWithoutMcpOrCustomProviderStillWritesAConfig, and aPinnedEndpointAndTheFleetMcpCoexistInOneConfig. The charter reaching a member was already pinned. So their FIRST diagnosis was the right one. The surviving mutation did not delete the charter write; it made a LATER writer replace the whole array. Every pre-existing test had exactly one writer active, and with one writer putArray and withArray are indistinguishable. The gap was never "is the charter delivered" - it was "are two writers ever active at once", which is the combination axis. Recording this because the stronger claim is the more quotable one and it would have sent the next reader looking for a hole that is not there. One honest residue: the charter writer's own idiom cannot be pinned. Flipping it back to putArray leaves the suite green, and always will, because it runs first against an empty array where the two idioms are equivalent. The edit removes an undeclared constraint for the next person to add a writer; it is not a change any test can detect. The comment in the source claimed two named tests cover it - that was wrong, and the commit after this one corrects it rather than leaving a false claim beside the code. What this does NOT do: opencode has no equivalent of Claude Code's Skill tool, so the content is static system-prompt text present from spawn, not something a member can invoke by name. This closes the DELIVERY gap and cannot close the ACTIVATION gap. That residue is opencode's design. Numbers and my own mutation battery are on the ticket, measured on this merge commit. |
||
|
|
e95ed99bf7 |
fleetd #466 scope item 2: report the quarantine repeat count, not only the seconds
Add BackendQuarantine#status(credentialId) -> Optional<Status>, a single QuarantineState read that answers both remainingSeconds and repeatCount together -- the same "one accessor" pattern CompositePeerLauncher. modelGateState() already uses, so the two facts can never disagree. fleet_profiles/REST GET /profiles and fleet_list's capacity rows (FleetMcp.profilesView/capacityView) now call status() instead of remainingSeconds() and add a "quarantineAttempt" field beside "quarantinedForSeconds": 1 for a first occurrence, 2 for the second in a row, and so on. No change to the escalation, ceiling, or reset logic itself -- this unit is reporting only. |
||
|
|
1477e4358a |
Merge #472: one canonical tool-name set, and charters are checked against it (fleetd #469)
A role charter is free text in config that tells a member which tools to call, and nothing checked that those tools exist. A charter naming bridge_send - a name CB-634 removed - started the daemon cleanly, and the member found out at run time by calling something that was not there. #464 shipped a test for this, but it wrote its own charter into a @TempDir, so nothing anyone put in the real config could fail it. That was a defect in my acceptance criteria, not in that work. Now: FleetTool is one enum of the 11 registered wire names, and every reader goes through it. - FleetMcp's schema builders pass FleetTool.X.wireName() instead of a literal. - FleetMcp's constructor asserts at startup that what it registers with the SDK equals FleetTool.wireNames() exactly, in both directions. - toolAction(String, Map) resolves arbitrary wire input against FleetTool.byWireName() and keeps its run-time throw, which is necessary - network input has no closed compile-time form. It then hands off to authzAction(FleetTool, Map), a switch over the enum with NO default, so a new tool is a compile error at that layer. - CharterToolSurface lives in mcp, not config, and Fleetd.main calls it right after validateAll(). Config must not depend on the MCP server: config loads before the server exists. My brief undercounted the problem and the worker corrected it. I said there were two tool-name inventories plus a test fixture. There were FIVE: the registrations, the authz switch, and three separate source-text scrapes of FleetMcp.java in CharterToolSurfaceTest, FleetMcpAuthzTest and McpContractDocTest - none of which the ticket mentioned. Fixing those three was required, not scope creep: once the literals moved into FleetTool their regexes matched zero names, so one would have failed on its vacuity guard and the other two would have gone quietly vacuous. Inventories after: one. Verified independently on origin/main before accepting the wider diff: three test files did read FleetMcp.java as source text, with a control file at zero to prove the search discriminated. Build number and my own mutation results are on the ticket and the PR, measured on this merge commit rather than on the branch. |
||
|
|
9e4e423ad6 |
fleetd #393 follow-up: remove the instructions[] writer-ordering hazard
OpenCodeLauncher.writeConfig has three writers into the instructions[] array (charter, seeded skills, IDE rules). The charter writer used putArray (create-or-REPLACE) instead of withArray (get-or-create), which "worked" only because it happened to run first against a still-empty array — an undeclared ordering dependency nothing tested. Found by the fleet01 lead and verified on this branch's merge: flipping the skills writer to putArray left the full 1603-test suite green while silently deleting the charter entry, which would launch an opencode member with no role contract at all. Fix: charter's putArray -> withArray (one-word change, behavior-identical today). Add three tests asserting instructions[] CONTENT as an exact ordered list (not size) across writer combinations: charter only, charter + IDE rules, and charter + IDE rules + seeded skills. Mutation testing (see PR body) shows the skills and IDE-rules writers are each independently detectable by name; the charter writer's own mutation is not detectable by any test, because it structurally always runs first against an empty array, so putArray and withArray are equivalent there. |
||
|
|
6c2d6e93cb |
fleetd #469: one canonical FleetTool set backs registration, authz and charter checks
FleetConfig.validateCharters() only checked that a charter key is a role wire name and its text is non-blank; #464's CharterToolSurfaceTest compared charter text against the registered tool surface, but wrote its own charter into a @TempDir fixture, so nothing anyone wrote into the live fleetd.yaml could ever fail it. Add FleetTool, an enum in dev.ltms.fleet.mcp holding the one canonical set of registered tool wire names. FleetMcp's tool schemas now derive their names from it, its constructor asserts at startup that what it actually registers with the SDK equals FleetTool.wireNames() exactly, and its authz dispatch (toolAction/authzAction) resolves the wire string against FleetTool before switching on the enum itself with no default -- adding a tool without pinning its Authz.Action is now a compile error, not just a test gap. Add CharterToolSurface (mcp package, not config -- config loads before the MCP server exists) and call it from Fleetd.main right after cfg.validateAll(), so a charter naming a tool the server does not register refuses the daemon's startup, naming both the charter key and the unknown tool. FleetdStartupValidationTest proves this through Fleetd.main itself against a live-shaped config fixture (bridge_send, CB-634's own removed name). CharterToolSurfaceTest, FleetMcpAuthzTest and McpContractDocTest each kept an independent regex scrape of FleetMcp.java's source for the registered side of their own comparison -- three more copies of the same list nothing tied together. All three now read FleetTool.wireNames() instead. Proved canonical by removal: deleting FleetTool.ACK while ackTool() still referenced it broke mvn compile in two places (FleetMcp.java:940,:1898); registering a schema under a literal not backed by FleetTool ("fleet_ack_v2") failed FleetMcp's new startup assertion in every test that constructs it (12 errors, IllegalStateException at FleetMcp.<init>). Both reverted before this commit. |
||
|
|
789b6a8716 |
Merge #470: the exhaustion quarantine escalates instead of retrying flat (fleetd #466)
A flat 30-minute cooldown suits a backend that is out of capacity for the hour. It does not suit a weekly subscription limit: that keeps reporting exhausted until the window resets, so the daemon retried it roughly 336 times across a week and learned nothing each time. BackendQuarantine now tracks, per credential, how many consecutive exhaustion reports it has seen with no quiet gap between them, and doubles the cooldown each time, capped at 12x the base (about 6 hours at the 1800s default). That is about a dozen attempts a week instead of ~336. Three things on the record because they are judgement calls, not facts: - The reset is a TIME PROXY, not a success signal. Nothing in this codebase reports a spawn success back to this class, so "it started working again" cannot be observed here. A base cooldown of quiet is the best available evidence, and the class doc says that plainly instead of implying the stronger thing. - No automatic probing. That was the operator's design constraint and the implementation respects it: the daemon warns and waits, it never pokes a limited backend to see whether the limit lifted. - The multiplier (2.0) and ceiling (12x) are constants, not config surface, so no new ConfigRef hot/cold/deferred question arises. quarantineCooldownSeconds stays Deferred and is now the BASE of the backoff; fleetd.example.yaml and FleetConfig's javadoc say so. Escalation fires on the exhaustion signal alone. Cooling-off (BackendOutagePolicy, a flat 60s after repeated non-exhaustion errors) is a separate mechanism and deliberately NOT escalated: doing so would turn a transient 5xx storm into a multi-hour outage. The old two-argument constructor is behaviourally unchanged - the same formula with multiplier 1.0 and a ceiling equal to the base, which collapses to the original flat "now + cooldown". Every existing call site keeps its shape. The second commit exists because my own mutation on the first merge found the wiring unpinned: putting Fleetd.main back on the flat constructor left all 1608 tests green, so the factory was pinned and the decision to use it was not. FleetdBackendQuarantineWiringTest closes that, following the five existing *WiringTest files rather than inventing an idiom. It checks source TEXT, and its class doc says so: it does not prove the call executes, and it cannot tell "wrong factory" apart from "renamed the anchor" - both fail the same assertion. That limit is real and recorded rather than papered over. Build number and my own mutation results are on the ticket and the PR, measured on this merge commit rather than on the branch. |
||
|
|
01462c9695 |
fleetd #466 follow-up: pin main's choice of the escalating quarantine factory
BackendQuarantineTest proves BackendQuarantine.withEscalation itself escalates, is ceilinged, and resets. Nothing proved Fleetd.main still calls it: reverting main to the flat two-argument constructor compiled with 0 errors and left the full 1608-test suite green, because every other test builds its own BackendQuarantine directly. Adds FleetdBackendQuarantineWiringTest, a source-text assertion on Fleetd.java (same idiom as FleetdLeadSeatWiringTest and FleetdCompletionResolverWiringTest) pinning that main's declaration is built from BackendQuarantine.withEscalation(...), not `new BackendQuarantine(...)`. Measured: reverting to the flat constructor fails it (expected true, was false); renaming the anchored local variable while keeping behaviour identical also fails it loudly, not silently. The class doc states plainly this checks source text only, not that the call executes or that escalation reaches a real backend. |
||
|
|
8b4ff78546 |
Merge #470: the exhaustion quarantine escalates instead of retrying flat (fleetd #466)
A flat 30-minute cooldown suits a backend that is out of capacity for the hour. It does not suit a weekly subscription limit: that keeps reporting exhausted until the window resets, so the daemon retried it roughly 336 times across a week and learned nothing each time. BackendQuarantine now tracks, per credential, how many consecutive exhaustion reports it has seen with no quiet gap between them, and doubles the cooldown each time, capped at 12x the base (about 6 hours at the 1800s default). That is about a dozen attempts a week instead of ~336. Three things I want on the record because they are judgement calls, not facts: - The reset is a TIME PROXY, not a success signal. Nothing in this codebase reports a spawn success back to this class, so "it started working again" cannot be observed here. A base cooldown of quiet is the best available evidence. The class doc says this plainly rather than implying the stronger thing. - No automatic probing. That was the operator's design constraint and the implementation respects it: the daemon warns and waits, it never pokes a limited backend to see whether the limit lifted. - The multiplier (2.0) and ceiling (12x) are constants, not config surface, so no new ConfigRef hot/cold/deferred classification question arises. quarantineCooldownSeconds stays Deferred and is now the BASE of the backoff; fleetd.example.yaml and FleetConfig's javadoc say so. Escalation fires on the exhaustion signal alone. Cooling-off (BackendOutagePolicy, a flat 60s after repeated non-exhaustion errors) is a separate mechanism and is deliberately NOT escalated: doing so would turn a transient 5xx storm into a multi-hour outage. The old two-argument constructor is behaviourally unchanged - it is the same formula with multiplier 1.0 and a ceiling equal to the base, which collapses to the original flat "now + cooldown". Every existing call site keeps its shape. Build number and my own mutation results are reported in the PR and on the ticket, measured on this merge commit rather than on the branch. |
||
|
|
d4a2cd720c |
fleetd #393: deliver memberSkills to opencode members, and stop overclaiming seeding success
GitWorktrees.seedSkills copies memberSkills:-seeded skill folders into every provisioned worktree's .claude/skills/ and logged "skill seeding: N of M" as if that were success — but .claude/skills/ is a Claude Code CLI convention. opencode has no such discovery, so a kind: opencode member never actually read a seeded skill even though the log said N of M succeeded. Two changes, both required: 1. Deliver it. OpenCodeLauncher.skillInstructionFiles scans <cwd>/.claude/skills/*/SKILL.md at spawn time (the one point the launcher knows both the kind and the cwd) and appends each to the generated opencode.json's instructions[] array, the same channel already used for the member charter and IDE rules. A skill folder with no SKILL.md is named and skipped rather than silently dropped. 2. Stop claiming it where the claim can't be verified. GitWorktrees.seedSkills' log now says explicitly that consumption depends on the member's kind and points at the launcher's own log; OpenCodeLauncher logs its own kind-aware "skill delivery: M of N ..." line once the kind is actually known, naming any folder it could not turn into an instructions[] entry. fleetd.example.yaml's memberSkills: doc previously claimed "Claude Code members only; an opencode member reads a different path (.opencode/agent) this key does not touch" — false as of this fix, corrected to name both kinds and how each consumes it. Tests: OpenCodeLauncherTest gains two cases driving the real GitWorktrees#add seeding path (not a hand-built fixture) into an opencode-kind spawn — one asserting a seeded skill's SKILL.md lands in instructions[] plus the honest log line, one covering a skill folder without SKILL.md (delivered skills still flow, the malformed one is named in the log and excluded from instructions[]). ClaudeCodeLauncher is untouched — its native .claude/skills/ discovery already worked and is out of scope. mvn -B clean test: Tests run: 1603, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS |
||
|
|
5a467e1f8b |
fleetd #466: escalate BackendQuarantine's cooldown on repeated exhaustion
A flat 30-minute quarantine retries a weekly subscription limit about 336 times before the window resets. BackendQuarantine now doubles the cooldown on each consecutive exhaustion of the same credential (no more than one base cooldown after the previous quarantine's deadline), capped at 12x the base cooldown (~6h at the 1800s default), and resets back to the base cooldown once a base-cooldown's worth of quiet has passed with no further exhaustion. The flat two-argument constructor is unchanged (equivalent to multiplier 1.0 / ceiling == base), so all ~20 existing call sites keep their current shape and behaviour. Production wiring (Fleetd.main) switches to the new BackendQuarantine.withEscalation factory. This only touches the exhaustion path (BackendQuarantine's one production caller is Fleetd.exhaustionSink, fired on BACKEND_EXHAUSTED alone) and never the separate, unescalated cooling-off mechanism (BackendOutagePolicy, fixed 60s) that guards against a transient backend-error storm. |
||
|
|
1fb6176783 |
Merge #457: exhaustedPattern goes hot, and the warning names the fix (fleetd #446)
Three rounds. Round 1 made exhaustedPattern a hot config key, made the quarantine warning name the fix instead of only the fact, and added model/reason to fleet_profiles' quarantined rows. Round 2 extracted the warning text into usageLimitFixWarning/usageLimitFixWarningNoModel and pinned both. Round 3 extracted the sink itself into a static exhaustionSink(...) factory and pinned what it actually logs, using a ListAppender on this class's own logger. Where the mutation numbers below come from, stated exactly. The battery ran on merge commit 3d2d521, tree |
||
|
|
49df79203c |
Merge #464: a test that charter text names only registered tools (fleetd #464)
CharterToolSurfaceTest extracts every fleet_* / bridge_* token from configured
launch charters and every tool("fleet_...") FleetMcp registers, then asserts the
first set is a subset of the second.
Verified on the merge commit. Its three acceptance criteria are met:
- Catches the ticket's own example. Fixture charter naming bridge_send, the tool
CB-634 renamed away: KILLED.
- Fails loudly with no charter text. Fixture stripped of every tool name: KILLED
by its named.isEmpty() guard, not a silent pass.
- Fails loudly with no registered tools. The tool("...") scrape broken so it
matches nothing: KILLED by its registered.isEmpty() guard.
- And one cell of my own: the server stops registering fleet_reply, which the
fixture names. KILLED. This is what proves the 'registered' half reads real
production source and is not a second fixture.
The scrape finds 11 registered tools: ack, ask, list, poll, profiles, reply,
send, spawn, status, stop, whoami. An independent count of every "fleet_x"
literal in FleetMcp.java is also 11.
WHAT THIS DOES NOT CLOSE, and it is the ticket's actual gap. The charter half is
a @TempDir fixture the test writes itself, so no charter text anyone writes can
make this test fail. Measured: the test mentions fleetd.yaml 0 times, and the
commit changes 0 production files -- FleetConfig.validateCharters() still never
reads charter text (0 lines of its body mention a tool name). So this pins the
comparison logic and acts as a rename tripwire for the two tools the fixture
names. It does not check the live config. That needs a production-side check and
is filed as a follow-up.
That residue is my ticket's fault, not the worker's: the three criteria I wrote
are exactly the three it met.
A note on my own battery, because it nearly published four false kills. The
first run showed rc=1 on all four mutation cells and I would have read that as
four kills. It was zsh: unquoted parameters are NOT word-split, so
'mvn -B $scope test' passed '-Dtest=X -DfailIfNoTests=false' as ONE argument and
surefire ran zero tests. The tell was a missing 'Tests run:' line. The rerun
proves the harness first -- selector alone must report 'Tests run: 1' -- and
every cell now prints its surefire summary count so a void cell cannot pass for
a kill.
|
||
|
|
235644c0f0 |
Merge #467: listFleet's callerIsPrimary default fails closed (fleetd #463)
A wrapper overload that is called with no callerIsPrimary argument used to
default it to true, so a forgotten argument silently handed out the lead's
coordination state. It now defaults to false: a missing identity fails closed.
Verified on the merge commit, not the branch:
- The funnel is real. FleetMcp has 7 listFleet declarations and 7 real calls
(an 8th 'listFleet(' match is a javadoc {@link}). Exactly one call writes a
literal for the new boolean, and it writes false; exactly one writes the real
predicate, coordinatorVisibleTo(principal(exchange)) in the MCP handler. No
call writes true.
- Control battery on the merge: 1584 tests green unmutated.
- M1, the fix reverted at the one line that writes the default (false -> true):
KILLED by listCompatOverloadWithNoCallerIsPrimaryArgumentOmitsTheCoordinatorKey.
- M2, the half this round weakened. The worker rewrote
listIsByteForByteUnchangedForThePrimaryCaller and dropped its byte-for-byte
equality assertion, which was correct because that comparison ran against the
implicit-default overload -- now the path #463 closes. So: does anything still
notice if the primary's coordinator row silently loses a field? Dropped
heldCount: KILLED by two tests, that same test and
listReportsAnHonestHeldCountAndDurabilityNotJustPendingZero.
- M3, a regression check on #439's own gate, 'if (callerIsPrimary)' -> 'if (true)':
KILLED by three tests.
What is no longer pinned, stated plainly: the primary's output is now checked
field by field, not as a whole string. A field that no test names could
disappear without failing anything. Every field the tests do name is pinned,
proven by M2. The whole-output answer belongs to fleetd #460.
One control in my own battery was wrong and is worth recording: I labelled
', true);' in FleetMcp.java 'must be 0' and it is 2 -- row.put("configured",
true) and m.put("self", true), neither a listFleet delegation. The pattern was
too wide. The count above comes from a walk over each declaration and call
instead.
|
||
|
|
7772b41993 |
fleetd #446 round 3: extract exhaustionSink and pin its caller (Cell A/B)
Round 2 pinned usageLimitFixWarning/usageLimitFixWarningNoModel's TEXT via FleetdUsageLimitFixWarningTest, but a mutation battery against the merged PR proved two gaps in the caller that builds main()'s real quarantine ExhaustionSink: nothing proved the sink's log.warn actually invokes either method (Cell A), and nothing proved it picks the right one for a profile with vs without a configured model: (Cell B). Extract the inline lambda into a new static Fleetd.exhaustionSink(...) factory (same refactor-for-testability class the lead approved in round 2 for the two static warning methods), behaviourally unchanged from the lambda it replaces. FleetdExhaustionSinkWarningTest drives this factory's return value directly and asserts on the real text a ListAppender attached to Fleetd's own logger captures, covering both cells from one mechanism: - Cell A (ternary result replaced by a literal string): confirmed red, 1594 run / 1 failure, restored, confirmed green (1594/0). - Cell B (ternary's two branches swapped): confirmed red, 1594 run / 2 failures (both test methods independently caught it), restored, confirmed green (1594/0). |
||
|
|
eccd0548ce |
Merge #465: state canonical invariant 5 as a purpose, not a banned tool (fleetd #458)
Verified by me on a local merge of |
||
|
|
c4e23eebad | fleetd #464: guard charter tool names | ||
|
|
7df503dfc2 |
fleetd #463: default listFleet's callerIsPrimary to false, fail closed
The compat overload at FleetMcp.java:1373 defaulted callerIsPrimary to a literal true, so a caller that forgot the argument silently got the coordinator row (this daemon's coord-id, mailbox state, held-mail previews, peer reachability) -- lead-to-lead state fleetd #439 just gated. Flip the default to false: a forgotten argument now yields a missing row instead of a leaked one. Six FleetMcpTest methods relied on the implicit true to see the coordinator row at all; they now pass true explicitly through the canonical overload. listIsByteForByteUnchangedForThePrimaryCaller's own premise (comparing the implicit-default path against an explicit-true path) was the shape of the bug, so it now only exercises the explicit-true path. Added listCompatOverloadWithNoCallerIsPrimaryArgumentOmitsTheCoordinatorKey to pin the new default: a compat overload called with no callerIsPrimary argument, against a fully-configured lead channel, must produce a result with the coordinator key absent -- not empty, not redacted, absent. |
||
|
|
f5e02fedd6 |
plans: commit the fleet01 move plan, with today's state measured on the host
This plan was written 2026-09-05 and has been sitting untracked in the
working tree since, so nobody but this machine could read it and nothing
recorded that it existed. plans/ is a tracked directory here.
Committed with a status section measured today over a read-only ssh survey,
because a five-day-old plan committed as-is would read as current:
- Phases 1 to 3 are done. Both systemd user units exist, are active and
enabled, linger is on, one java process (so the old restart.sh
double-daemon problem is gone), the live fleetd.yaml is on the host, and
the jar was rebuilt 2026-09-10 02:10 UTC.
- The checkout has drifted again: main at
|
||
|
|
29e7a06c49 |
fleetd #458: restate invariant 5 by purpose, not mechanism
Invariant 5 banned 'driving the terminal multiplexer directly', naming herdr CLI and socket as the banned tool. That bans a mechanism. What it protects is the control plane: nobody may move a fleet session, pane or peer by a route that skips the bridge's policy checks. In this repo herdr is itself the subject under test, so four contract tests must open its socket on purpose (see the addendum's 'Herdr socket tests' note). Under the old wording, a worker assigned to that code reads invariant 5 and finds its only path to finish the task banned. Restate the invariant by purpose: never move fleet state except through the bridge. herdr's CLI and socket stay as the named example of the banned route, not the definition of it. |
||
|
|
92a96fcbd8 |
Merge #462: gate the coordinator row on a named predicate the handler must consult (fleetd #439)
Verified by me on a local merge of |
||
|
|
13482872bb |
contract test: keep the loaded run's passes, discard only its failure
The javadoc threw away the whole loaded run as "not clean evidence". The
fixed cell order makes that too strong in one direction. A cell that runs
last on a climbing load has a free explanation for FAILING. It has no free
explanation for PASSING: surviving a worse condition than a fair order
would have given it is evidence in the safe direction. So the old version's
0 of 3 is still discarded, and the three passes are kept with the load each
one ran at.
Also names the cell that tests the swallow explanation head-on, which the
old text left as "nobody has managed that yet". SHELL_READY_TIMEOUT_MS at 0
types input at once — the worst case for "typed before the prompt" — and it
passed 3 of 3 at load 18.42 to 23.65. The wider read window cannot explain
that away, because a swallowed keystroke is lost, not late: the command
never runs, so no amount of polling makes its output appear.
And a warning not to carry the raw load average to another host. Load
average counts differently per core and per operating system, so only load
per core compares. I broke that rule myself when comparing this run with
another host's numbers.
Both points came from the fleet01 lead reviewing
|
||
|
|
c1ca6273fc |
fleetd #439: pin the caller at the fleet_list call site, not just the gate
Review of PR #462 found M2: the coordinatorVisibleTo gate (then an inline principal(exchange).isPrimary() check) could survive a mutation that replaced the argument with a literal true at the one production call site, because every existing test drove listFleet directly and supplied the boolean itself -- nothing exercised the handler's own call. - Name the decision: FleetMcp.coordinatorVisibleTo(Principal), a small package-private predicate next to denyFor/recordPrimarySingleton. The fleet_list handler now calls coordinatorVisibleTo(principal(exchange)) instead of inlining .isPrimary(). - Pin the predicate's role table in FleetMcpAuthzTest (onlyThePrimaryMaySeeTheCoordinatorRow), covering primary/worker/ architect and, newly, anonymous. - Add a source-reading detector at the boundary (theFleetListHandlerActuallyConsultsCoordinatorVisibleTo), same idiom as toolsTheServerRegisters/everyRegisteredToolHasItsHandlerActionPinned: it reads FleetMcp.java, isolates the listHandler block, asserts (as a control) that the block actually contains a listFleet( call, then asserts the call's trailing boolean argument is exactly coordinatorVisibleTo(principal(exchange)) -- not a literal true/false. Both mutations from the review were reproduced and killed by these tests, then reverted; see the PR body for the full break-and-restore transcript. |
||
|
|
5f1b260c81 |
addendum: the canonical sync check is the lead's, not a member's
I put "the sync script must print in sync: True" in a worker's acceptance criteria for #455. The worker could not run it and said so, honestly, instead of inventing a pass. My brief was the defect. A member's provisioned worktree has wiki/ uninitialized, so the script dies with FileNotFoundError. Measured in three worker worktrees: git submodule status printed a leading '-' and wiki/ held 0 entries. The primary's own clone printed a leading '+' and the file was there. The note is dated, gives the re-measure command, says what each outcome means, and says to delete it once it stops reproducing - as this file requires of any measurement in an addendum. Canonical block untouched: the sync script itself reports in sync: True. |
||
|
|
30d6872779 |
fleetd #446 follow-up: pin the WARNING text and the fleet_profiles model/reason fields
A mutation battery run against merged PR #457 (d02dd1b) proved criteria 2 and 3 shipped without a test that could catch them breaking: deleting either row.put("model", model) or row.put("reason", reason) in FleetMcp.profilesView left all 1586 tests green (rc=0), and renaming the "usage-limit fix:" log tag to something meaningless did too. Criterion 1's own mutation (LiveExhaustedPatterns snapshotting instead of reading live) was correctly killed by the existing FleetdExhaustionDetectionArmedWiringTest/LiveExhaustedPatternsTest — only 2 and 3 were unguarded. - Extracted the exhaustionSink WARNING text out of two inline SLF4J {}-placeholder log.warn calls into two static methods, Fleetd.usageLimitFixWarning(profile, model) and Fleetd.usageLimitFixWarningNoModel(profile, quarantineCooldownSeconds), the same extracted-static-method + dedicated-test idiom as exhaustedPatternCoverageLine/errorPatternCoverageLine. log.warn is now called with each method's return value as a single already-formatted argument, so the string a test asserts on is byte-identical to what fleetd.out receives. No behaviour change: same text, same two branches, same call site. - New FleetdUsageLimitFixWarningTest pins the leading "usage-limit fix:" grep tag and three specific facts (profile name, model name, "enabled: false" under models.allow) rather than the whole sentence, plus the no-model fallback's profile name and cooldown-seconds substitution and its explicit absence of "enabled: false". - New FleetProfilesQuarantineModelReasonFieldsTest exercises FleetMcp.QuarantineSource with a modelFor/reasonFor that actually return values (every existing test used QuarantineSource.none() or a 3-arg form defaulting both to null), asserting the quarantined row's model/reason are present when supplied and absent (not null, not blank) when modelFor/reasonFor return null or a blank string. - Each of the three: implemented, broken by hand (row.put deleted / tag renamed), confirmed the new test goes red, restored, confirmed green again. See PR body and this ticket's fleet_reply for the verbatim failure output of all three. |
||
|
|
9d1306d442 |
Merge #461: project addendum for the herdr socket carve-out (fleetd #455)
Doc-only, one file, 13 added lines, inside §Project addendum. Verified by me:
- Canonical block byte-identical: 17468 chars on main and on the branch.
- The sync script in CLAUDE.md prints "in sync: True" both before and after.
- The note's own re-measure command works. Run verbatim, it returns exactly the 4 files
the note names, and no others:
fleetd/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java
fleetd/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java
fleetd/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java
fleetd/src/test/java/dev/ltms/fleet/herdr/HerdrContractTest.java
Control that the search reaches the tree: 120 test java files, 63 of them mention herdr.
That control matters here — a zero-match re-measure command would tell a future session to
delete a live restriction.
- All four required elements are present: the carve-out, what stays banned (the control plane),
who it applies to, and the perishable half (dated 2026-09-10, the command, what each outcome
means, and delete-when-stale).
- Cross-references #458 for the canonical restatement, which is deliberately not in this change.
Pre-send check applied to the note itself: a worker assigned to AgentControlContractTest can now
name one legal action that finishes its task — let the test open the herdr socket in a throwaway
workspace it tears down.
|
||
|
|
e54e3d87ea |
fleetd #439: omit fleet_list's coordinator key for non-primary callers
The coordinator row is lead-to-lead coordination state (coord-ids, mailbox facts, held-message previews). fleet_list returned it to every caller, including a worker or an architect, because coordinatorView() had no way to know who was asking. Gate at the call site inside listFleet: a new overload takes callerIsPrimary and only assembles/attaches the coordinator row when it is true, so the key is absent (not empty) for a worker or an architect. The MCP handler now passes principal(exchange).isPrimary(); every other listFleet overload keeps passing true, so callers with no caller identity (existing unit tests, the no-op wrappers) are unaffected -- confirmed by a byte-for-byte comparison test against the pre-fix overload. Authz's READ case is untouched: it stays shared by fleet_status, fleet_profiles and fleet_whoami, and the gate here is purely inside fleet_list's own result assembly. |
||
|
|
bf027f10b9 | #455: document herdr test socket exception | ||
|
|
3c5873dfe2 |
fleetd #453: point the new javadoc at the right javadoc
#456's first paragraph said "HerdrPeerLauncher's own {@link
#spawn(SpawnRequest, PlacementDecision)} javadoc". That link resolves to this
interface's own abstract declaration, not to HerdrPeerLauncher's override, so a
reader who follows it lands on the wrong text. The second paragraph already
used the plain {@code HerdrPeerLauncher.spawn(...)} form; both now match.
Also says who is actually forced to read the paragraph, because #456's
reasoning rests on it and the two cases differ. A class that implements this
interface directly must write a body for spawn(SpawnRequest,
PlacementDecision) - it is abstract here - so it reads this javadoc. A
subclass of HerdrPeerLauncher does not: HerdrPeerLauncher already implements
that method (member/HerdrPeerLauncher.java:611) and the subclass inherits the
body. For a subclass the paragraph is advice, not a gate.
javadoc -Ddoclint=reference: 5 "reference not found", the same 5 in the same 5
untouched files as origin/main at
|
||
|
|
e29227d5f4 |
Merge #456: document the override obligation on PeerLauncher.defaultProfileFor/place (fleetd #453)
Doc-only, one file, 17 added lines. Verified by me on a local merge of |