main
1056 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
45aca9eb3e |
fleetd #446: make exhaustedPattern hot, name the fix in the warning, report it in fleet_profiles
The model gate can be turned off at runtime (models.allow[].enabled: false, hot since fleetd #422), but the usage-limit detector it's meant to react to was compiled once at Fleetd.main startup into a frozen Map<String,Pattern> — arming or disarming exhaustedPattern needed a daemon restart. Backwards for a feature meant to react live. - New LiveExhaustedPatterns: reads exhaustedPattern off the live config supplier per lookup (matching CompositePeerLauncher#models0's live-supplier pattern), caching compiled Pattern objects by PROFILE NAME (not pattern text — pattern text would grow unboundedly as an operator tunes a regex across reloads; profile names are bounded by the small, restart-gated set of configured profiles). patternFor() backs CompletionResolver's classification; armed() backs fleet_profiles' exhaustionDetectionArmed — both read the same object, the fleetd #404 single-accessor rule CompositePeerLauncher.modelGateState() established for the model gate. - Fleetd.java: on BACKEND_EXHAUSTED, log a WARNING naming the profile's model and the exact fix (enabled: false under models.allow, hot, no restart; remove it again once the window resets) — or, when the profile has no model: configured, say quarantine is the only thing keeping spawns off it. - FleetMcp.QuarantineSource gains modelFor/reasonFor; fleet_profiles' quarantined rows gain model/reason fields so a lead can see why without reading the daemon log. capacityView (fleet_list) intentionally untouched — scoped to fleet_profiles only. - ConfigRef/FleetConfig docs + fleetd.example.yaml updated: exhaustedPattern moves from Deferred to Hot. errorPattern stays deferred on purpose (out of scope for this ticket). - Tests: LiveExhaustedPatternsTest (new, unit-level hotness/caching proof), FleetdExhaustionDetectionArmedWiringTest (rewritten — same QuarantineSource object, read before and after a reload, asserts the answer flips with no restart), ConfigRefTest/ConfigRefProfileCoverageTest updated for the new Hot/Deferred classification. |
||
|
|
2af13ab1ff |
fleetd #449: name the decisive cell and the load the claim was measured at
The previous comment named a cause with no cell behind it. fleet01's review made the point: a confident wrong mechanism gets copied, and a confident under-determined one gets copied the same way. So the comment now names the cell that settles it. Hold the old 1000ms write sleep and change only the read - 800ms fixed sleep becomes a 5s poll - and the test goes 0 of 3 to 3 of 3. The read deadline was the whole story. It also says where: a 12-core macOS host near idle (load 2.6 to 5.9). The loaded run agreed, but its load climbed from 7 to 50 while the cells ran and the old version ran last, so it is not clean evidence and the comment says so. Comment only. No test or production code changed. |
||
|
|
0788d84be8 |
fleetd #453: document the override obligation on PeerLauncher.defaultProfileFor/place
Decision: leave both as default methods (option 1), not abstract. Neither default is a live defect today — HerdrPeerLauncher is the sole single-profile implementer and the degenerate answer (ignore role, always defaultProfile()) is correct for it. Making them abstract would force ~10 boilerplate one-line overrides across 5 unrelated PeerLauncher test doubles (NeverSpawnsLauncher, RaceLauncher, NoResumeLauncher, ClearContextSpyLauncher, LazyIdLauncher) that never call either method, for a risk that is speculative (no multi-profile HerdrPeerLauncher subclass exists or is planned). Strengthens both javadocs with an explicit MUST-override warning and cross- references HerdrPeerLauncher.spawn(SpawnRequest, PlacementDecision)'s existing #450 javadoc, which already names both methods as "unoverridden here" and ties that to being a single-profile adapter -- the concrete place a future multi-profile launcher author would read, since #450 made that method abstract and any subclass must write its body. |
||
|
|
20c1094cbf |
fleetd #449: say what the timing fix actually proved, not what it assumed
The polling fix that landed in #452 is right, but its javadoc named a mechanism nobody measured: that input typed before the shell's prompt was swallowed by the shell's own startup. I mutated the settle poll away — SHELL_READY_TIMEOUT_MS = 0, so input is typed at once with no wait — and the test passed 3 of 3. So waitForText is the load-bearing half, and the proven cause is the old 800ms READ deadline, not the 1000ms write delay. The direction is the point: typing at 0ms works where typing at 1000ms failed. If early input were swallowed, 0ms would be worse than 1000ms. It is better, so the swallow explanation is unsupported. waitUntilSettled stays as cheap insurance, now labelled as insurance rather than as the fix. Comment-only; AgentControlContractTest still green. |
||
|
|
9011c59b9f |
Merge #452: run the contract tag in CI, fix the stale protocol 14 assertion (fleetd #449)
Verified on a locally built merge onto |
||
|
|
c11ad71ed0 |
Merge #448: fleet_ack errors instead of claiming success on a miss (fleetd #437)
Verified by the lead on head |
||
|
|
bdcf285265 |
Merge #451: make PeerLauncher.spawn(req, decision) abstract (fleetd #450)
Verified by the lead on head |
||
|
|
d4f93a7b13 |
fleetd #449: fix stale herdr protocol 14 javadocs/assertion, diagnose and fix the timing-raced AgentControlContractTest, select contract tests by tag in CI
- HerdrClient.java, HerdrCodec.java, HerdrContractTest.java: the herdr port to
protocol 19 (CB-521) left the client javadoc and the contract test's own
assertion still saying protocol 14 / herdr 0.7.0. Updated to 19 / 0.8.0 and
renamed pingReturnsProtocol14 -> pingReturnsProtocol19. Verified the
assertion is real by temporarily changing the expected value to 20 (fails),
then restoring 19 (passes).
- AgentControlContractTest.java: tabCreateInjectsEnvIntoTheSeedShell was
failing, not skipping, on a host with a live herdr socket. Diagnosed with a
temporary instrumented run (not committed) that polled the pane every
200ms before and after sending input: the seed shell reliably takes ~2.5s
to reach its prompt (measured 3x), while the test's fixed 1000ms sleep
raced that startup. Input typed too early was swallowed by the shell's own
startup, leaving the typed line followed by the "Restored session" banner
and no command output — indistinguishable at a glance from the env map
never reaching the shell. Once the shell was actually ready, the injected
env value showed up in ~200ms, ruling out an env-seam defect. Replaced both
fixed sleeps with bounded polling on the actual conditions (pane text
settling, then the expected output appearing). Ran the fixed test 3x
standalone, all green.
- .gitea/workflows/ci.yml: the "Contract tests" step ran exactly one class by
name (-Dtest=AmqpReplyInboxContractTest), silently excluding every other
@Tag("contract") test from CI including the herdr ones above -- which is
how the stale protocol 14 assertion went unnoticed. Changed to
-Dgroups=contract, which selects the whole tagged group and picks up
future contract tests automatically.
|
||
|
|
cfebc575ea |
fleetd #450: make PeerLauncher.spawn(SpawnRequest, PlacementDecision) abstract
The default re-entered the single-argument spawn(SpawnRequest), which re-runs
checks that can refuse the profile place() just chose (#444's window). Only
CompositePeerLauncher overrode it; a future placement-doing launcher could
have inherited the wrong body silently.
Give every current implementer an explicit override, chosen by what it does:
- HerdrPeerLauncher (base of ClaudeCodeLauncher/OpenCodeLauncher, neither of
which overrides spawn(req) or place()) does no placement filtering of its
own, so it gets the re-entering form.
- CompositePeerLauncher's routing-form override is untouched.
- 5 test-fake PeerLauncher implementers (SessionManagerTest, FleetdBackendErrorSinkTest)
get overrides matching their existing spawn(SpawnRequest) shape: delegating
wrappers delegate, unreachable stubs throw, the single-profile fake re-enters.
ConfigRef does not implement PeerLauncher at all (confirmed in this tree at
|
||
|
|
822327eed5 |
Merge #447: pin the place()-to-spawn() window PlacementDecision closes (fleetd #444)
Verified by the lead on the exact tree that lands (head |
||
|
|
5289eb509f |
fleetd #437: pin the ack hit/miss contract in the AMQP contract test
AmqpReplyInboxContractTest is the one contract-group class CI actually runs, and it never asserted on ack()'s return value at all — so the exact defect this ticket fixes (reporting success for an ack that removed nothing) was unpinned in the adapter fleetd runs live. Add ackReportsHitVsMissAgainstARealBroker: a msgId never held for an owned target returns false without throwing, a real held reply returns true and is removed, and acking the same msgId again returns false. Ran against both broker modes the class supports: Testcontainers (AMQP_URI unset) and an external broker via AMQP_URI (the CI shape, using a disposable container — not the shared local LavinMQ instance). |
||
|
|
e4c703a51a |
fleetd #444: separate the adapter's fallback default from the decided profile
Review found the fixture's StubLauncher fell back to 'sol' too — the
same profile place() decides — so an UNSTAMPED request could land on
spawnCount('sol') by coincidence, and the assertion's claim that the
request 'actually carried sol' was unproven. Dropping the stamping
(SpawnRequest routedReq = req) while keeping the routing survived the
test unchanged.
Fix: give the adapter 'b' as its own fallback default instead, so an
unstamped request counts against 'b', not 'sol'. Verified both
mutations against the single test in isolation:
- drop-stamping (routedReq = req): RED, expected <sol> but was <b>
- re-entering (return spawn(req.withProfile(decision.profile())))
i.e. M1 from the first round: still RED, PlacementException
naming the now-quarantined 'sol'
Restored both; full suite green at 1575 tests.
No changes to src/main — PeerLauncher's javadoc from the first round
is unchanged.
|
||
|
|
703a05db41 |
fleetd #437: fleet_ack errors instead of claiming success on a miss
ReplyInbox.ack now returns boolean (true = removed, false = nothing to
remove) instead of void, so FleetMcp.ack can finally tell a hit from a
miss. FleetMcp.ack returns an error when the boolean is false, naming
fleet_poll{coordId} for held peer mail, which has no route through this
call. MessageService.ackReply propagates the boolean; drainReplies keeps
ignoring it (its own javadoc already documents that loss window as
deliberate). Updated the tool schema's target description to match.
Rewrote FleetMcpTest's ack tests to publish a real message before
asserting success, and added tests for a never-queued id and a coord-id
target, both now erroring. Added boolean assertions to
InMemoryReplyInboxTest's existing ack cases.
|
||
|
|
3f036b2a62 |
fleetd #444: pin the place()-to-spawn() window PlacementDecision closes
Add a test that resolves place(role) while nothing is quarantined, then quarantines the resolved profile's credential BEFORE spawning against the held PlacementDecision. CompositePeerLauncher.spawn(req, decision) must still honor the decision and land on the quarantined profile, since it never re-runs the explicit-profile enforce* checks. Verified the test kills the regression: with the override's body replaced by the re-entering spawn(req.withProfile(...)) form, this exact test goes RED with a PlacementException naming the now- quarantined profile; restored, the full suite is green (1575 tests). Also documents on PeerLauncher's default spawn(req, decision) that a launcher routing across more than one profile MUST override it, naming the four enforce* checks the default's re-entry re-applies. |