Fleetd.main's injected wirings are unpinned: 16 call sites go inert with a fully green suite #612
Open
opened 2026-09-20 12:26:00 +02:00 by ltms
·
11 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#612
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Fleetd.mainbuilds the daemon by handing injected "source" objects and lookups toFleetMcp,to the loops, and to
CompletionResolver. Most are a one-line call at themaincall site.Nothing calls
Fleetd.mainfar enough to observe which object it actually passed, so a callsite can be swapped for its inert variant —
X.none(),_ -> null,() -> Map.of()— and thewhole suite stays green.
This is not a theory. #602/#606 shipped exactly that defect:
mainpassedFleetMcp.LeadConfigDirSource.none(), every test passed, and the live daemon reported"state":"unknown"for every lead. The fix extracted a factory and pinned the factory. Thecall site is still open, and this sweep re-measured it.
Method
Each site: mutate the
maincall site to its inert equivalent →mvn -o compile→ fullmvn -o test→ record red/green → revert → confirmgit diff --statempty before the next one.Baseline on
9a992d0: 1841 tests, 0 failures.17 mutation cycles were run this way — 16 suspected sites plus 2 control mutations on sites
expected to be covered. Both controls went red, which is what makes the 16 greens evidence
rather than an absence:
LeadSeatSource→none()failedFleetdLeadSeatWiringTest.fleetMcpConstructionStillWiresLeadSeatLookupworktreeBranchLookup→_ -> nullfailedFleetdCompletionResolverWiringTest.worktreeBranchLookupIsStillPassedAtTheCallSiteI reproduced the top-ranked gap myself, independently, on a clean
origin/mainworktree:Ranked by what a live daemon does, not by fix effort
Line numbers as of
9a992d0.exhaustedPatternLookup/liveExhaustedPatterns(418-419)publishExhaustionSink(472),forwardingExhaustionSink(227)replyInboxOpener(520)quarantineSource(658),outageSource(660-663)fleet_profiles/fleet_listconfidently report "not quarantined"/"not cooling off" for a profile that is. An operator debugging failed spawns is actively misledleadConfigDirSource(678)releaseCleanup(627)healthFailTarget(603)leadMailboxOpener(528)capacitySource(668)fleet_listreports zero configured profiles. Spawning unaffected. Loud, likely caught fastloopHealthSource(665)healthCoverageSource(669)turnRegistrar(511)Rank 1 deserves a note. The comment directly above those two lines already names this exact
outcome:
A comment naming the worst outcome in the file, sitting on top of two unpinned call sites.
Two shapes, and they need different fixes
Shape A — a false report, enforcement intact (4, 9, 10, 11, and 5). The real gate is built
separately from the real object, so behaviour is right and only the report lies. Bad because an
operator trusts the report while debugging.
Shape B — behaviour silently degrades (1, 2, 3, 6, 7, 8). Nothing else enforces it. The
daemon does the wrong thing and says nothing.
Fix Shape B first. A wrong answer an operator can see beats a wrong action nobody can.
What a fix must not be
Three existing wiring tests pass by asserting the exact source text of the call site
(
FleetdCompletionResolverWiringTest.backendErrorArgumentsAreStillNamedAtTheCallSite,FleetdBackendQuarantineWiringTest,FleetdLeadRolloverWiringTest). That catches a deletion andnothing else: it goes green on a call site that names the right symbols and still passes the
wrong thing, and it goes red on a harmless reformat. It is a fourth copy of the source, not a
test of behaviour.
The durable fix is structural: make
main's composition callable by a test — extract theassembly into one method a test can drive and inspect — rather than adding a seventeenth
per-site assertion. Whoever picks this up should propose that shape before writing tests.
Provenance
The 16 sites were measured by a hunter sweep (17 mutate/build/full-test/revert cycles, tree clean
after each,
git status --porcelainempty at the end). Rank 1 I re-ran myself; the output isabove. Ranks 2-12 I have not personally reproduced — that is the hunter's measurement, and
its method was validated by the two controls.
Three further sites are listed in that sweep as "covered by source-text match only": the hunter
confirmed the asserted substring is present in the live file but did not mutate them. Treat those
three as unverified in both directions.
A 17th site landed today, and I measured it before merging it
Merged #616 (fleetd #613) adds
reportRoleFallbackGaps(cfg)toFleetd.main, right aftercfg.validateAll(). It is a new instance of exactly this issue's shape. Recording it here so the structural fix has to cover it and it does not get lost.I measured it with a mutation pair, on the PR branch before merging:
mainreplaced with a commentTests run: 1870, Failures: 0— BUILD SUCCESSif (true) return;)Tests run: 1870, Failures: 2, Errors: 1— BUILD FAILURETree confirmed clean between runs (
git diff --statempty, mutant marker count back to 0).The pair is what makes this evidence. The method is pinned — four new tests drive it directly and go red when it stops working. The call site is not:
maincan stop calling it entirely and the whole suite stays green. Without the second mutation the first green would just be an absence of evidence.This one is Shape B, and close to the worst case
The feature is a boot log line. There is no separate enforcement path underneath that stays correct — if the call site goes inert, the capability is 100% absent and nothing anywhere says so. That puts it with ranks 1, 2, 3, 6, 7 and 8 rather than with the reporting-only sites.
It is also mildly self-referential: a call site that silently does not run, added to warn operators about config that silently does not do what they expect.
Why I merged it anyway
There is no cheap correct fix available today. The tests already use the right instrument — a real
ListAppenderon the logger, assertinggetFormattedMessage(), not the call site's source text. Pinning the call site properly needs exactly what this issue asks for:main's composition made drivable by a test. Adding a per-site source-text assertion instead would produce the anti-pattern this issue explicitly rules out, and would be a fourth copy of the source.So the gap is pre-existing in kind, not new in kind, and the change itself closes a real observability hole. Blocking it on a structural fix that is still being designed would trade a measured small gap for a known larger one.
What this asks of the fix
Whoever designs the shape should treat this as a 17th site and check it is covered — it is a plain
static voidcall taking onlycfg, so it is one of the easiest possible cases. A shape that cannot pin this one will not pin the harder resource-bound sites either (ranks 2, 3, 8). It is a good smoke test for a candidate design.An architect is working on the shape now; I will add its proposal to this issue when it reports.
Architect proposal for the fix shape, with a delegation split
An architect worked this and settled on a shape. Recording it in full so implementation can start from it. I have not yet verified its code claims myself — it says plainly which parts it checked and which it reasoned about, and I have kept that separation below.
The decision
Use this issue's extraction idea, but do not return a test-only snapshot. Extract the real boot composition into a package-private assembly that owns the objects production uses:
Fleetd.mainkeeps config loading, the startup reports and validation, then calls this withResourcePorts.system().FleetdRuntimeowns shutdown plus the builtCompletionResolver,Injector, broker resources,FleetMcp,FleetApp, the loops, and the wiring bundles.FleetdRuntimeis the observation seam: its package-private accessors expose the same bundles it owns and closes. Tests must never receive a second copy — that is what keeps this from becoming a snapshot that can lie.Preserve the existing construction and start order. Do not build everything then start everything; that changes boot timing. Move the statements as they are.
The load-bearing rules:
CompletionResolvertakes one required dependency bundle, removing the choice between its full constructor and the shorter defaulting overloads at the call site.RuntimeHooks.install(...)both creates and registers, collapsing "the factory is correct" and "main used the factory" into one claim.FleetMcp.ReportingSourcesis one required constructor argument with no production default, removing the per-source swap points.LoopHealthSource.none(),LeadConfigDirSource.none()or similar. Tests that want inert values construct an explicit test bundle.Resource-bound sites — the part I most wanted an answer on
Two linked contracts, no live broker needed:
FakeHerdr,@TempDirpaths and fake broker openers returning sentinelReplyInbox/LeadMailboxobjects, proving the assembly calls the openers and passes their returned objects into the live graph.That covers rank 2 through the real OpenCode launcher over
FakeHerdr, and ranks 3 and 8 through fake-opened sentinels plus the real-opener contract.Claimed site coverage — all 16
liveExhaustedPatterns,exhaustedPatternLookup,forwardingExhaustionSink,publishExhaustionSink,replyInboxOpener,releaseCleanup,healthFailTarget,leadMailboxOpenerquarantineSource,outageSource,leadConfigDirSource,capacitySource,loopHealthSource,healthCoverageSource, coordinator peersturnRegistrarThe residual risk, which is the same one I measured today
This matters and it is honest. It is exactly the gap I measured on the 17th site earlier today: the method was pinned, the call site was not. The mitigation — never give the assembly an inert variant that compiles — is a real answer and cheaper than a subprocess test. Worth adopting as a standing rule, not just here.
The four units
Unit A — extract the real boot assembly. Create
FleetdAssembly/FleetdRuntime; move post-validation composition out ofmain; addAssemblyInputsandResourcePortsfor env reads, herdr clients, broker openers, clocks, schedulers, shutdown-hook registration and HTTP start. Acceptance:mainstill loads/reports/validates before any socket or broker work; a fakeResourcePortsrecords and asserts the current start and close order; the daemon assembles withFakeHerdr, a temp filesystem, fake openers and no real HTTP bind; every opened resource is closed byFleetdRuntime.close(). Report: the recorded start/close order and any statement that could not move without reordering startup.Unit B — pin the silent behaviour loss first (Shape B). Add
ExhaustionWiring,BrokerResources,RuntimeHooks. Created once by the assembly; consumers take the bundle; no rebuilding at a second call site. Acceptance: behaviour, not source text and not reflection — exhaustion text classifiedBACKEND_EXHAUSTED; the published sink quarantines through the same bridge given to the OpenCode launcher; configured openers are called and their sentinels reach the real consumers; releasing a session abandons its waiter, releases inbox ownership and forgets its lead binding; a health failure fails a pending ticket; the injector uses the resolver's registrar on the callback-throws path. Each listed inert mutation must turn a targeted test red.Unit C — pin the reporting graph (Shape A). Replace the long optional
FleetMcpreporting argument list with one required immutableReportingSources; share the sameQuarantineSource/OutageSource/LoopHealthSourceinstances betweenFleetMcpandFleetApp. Acceptance: drive the assembledFleetMcpthrough its existing handler seam with no HTTP; real state appears infleet_list/fleet_profiles; prove both consumers share the same instances.Unit D — delete the source-text guards and run the final mutation gate. Remove
FleetdCompletionResolverWiringTest,FleetdBackendQuarantineWiringTest,FleetdLeadRolloverWiringTest, moving each claim to a behavioural assembly test. Acceptance: no replacement test readsFleetd.javatext; the full inert-mutation matrix runs against every site and each turns the suite red; every mutation reverted with a clean tree at handoff.Risks it named, and what settles each
FleetdRuntime, never copies.FleetdRuntime.close()owns every scheduler, inbox, mailbox, loop, MCP server and herdr router; assert a resource ledger.What the architect actually checked, in its own words
Checked in the code: the named factories are called in
mainwhile their tests mostly call each factory directly;FleetMcphas an overload supplyingLoopHealthSource.none()andLeadConfigDirSource.none();CompletionResolverhas several shorter constructors supplying inert or legacy collaborators; the real broker opener tests prove a network attempt but not thatmainuses those openers; the three named tests do readFleetd.javatext. Tree left clean.Not done: it did not run Maven, did not run a mutation, and did not reproduce this issue's green results — those come from the original sweep. The assembly owner, the bundles, the ports split, the unit split and the decision to preserve interleaved startup order are its own design conclusions.
It formed this position alone; no second architect was available to compare against.
My read
I am adopting this shape. Unit A is the gate — until
main's composition is drivable, B, C and D have nothing to attach to. I will delegate A first and verify the recorded start/close order myself against the currentmainbefore B begins, since "preserve the existing order" is the one acceptance criterion that cannot be checked after the fact.Unit A is built and it turns the build red. The A→B→C→D order cannot work as designed.
Unit A landed as PR #620. I ran the gate the worker could not run, and it fails. This is not a worker defect — the worker disclosed that its own sandbox classifier refused
mvn clean installtwice, stopped rather than substituting another command, and said plainly that it could not claim the suite passed. That was the correct behaviour and it is why the problem is visible now instead of after a merge.The measurement
Built the merge result (PR #620 merged with current
origin/main8915e40) in a scratch worktree,mvn clean install -o:1878 = 1877 + 1, the one newFleetdAssemblyLifecycleTest, so the arithmetic is clean — nothing was silently dropped. The 9 failures are all in source-text tests:FleetdCompletionResolverWiringTestFleetdBackendQuarantineWiringTestFleetdLeadRolloverWiringTestFleetdLeadSeatWiringTestFleetdConnectionIdentityConstructionTestFleetdFleetAppConstructionTestEvery one reads
Files.readString(Path.of("src/main/java/dev/ltms/fleet/Fleetd.java"))and assertssource.contains(...). Each class javadoc says so itself: "This test checks source text, not runtime behaviour." Unit A moved that text intoFleetdAssembly.java, so they fail by construction. This is unavoidable for any correct Unit A — moving the composition out ofFleetd.javais the whole ticket.The count of source-text tests in this issue is too low
This issue says three source-text tests must be handled, and Unit D names exactly those three. I measured:
8 files, not 3. Six fail under Unit A. Two survive —
FleetdConfigRefWiringTestandFleetdHerdrControlConstructionTest— because the text they assert (config load, herdr connect) stays inmainby design.So Unit D as written would leave three broken files unhandled and two more still reading a file whose composition has moved.
Why this cannot be fixed by deleting them early
These are not stale tests. Each is the only guard against a specific, previously measured regression, and each says so in its own failure message:
LeadSeatSourcedropped or swapped fornone()— fleetd #176, "the same shape as fleetd #248's measured mutations"worktreeBranchLookupreplaced by_ -> null, andbackendErrorPatterns/backendErrorSinkreplaced bylegacy()/none()— fleetd Fleetd's composition root is untested: a feature can be silently unwired and every test stays green (#248)BackendQuarantine.withEscalation(...)reverted to the flat two-argument constructor — fleetd #466, whose message spells out the live consequence: "about 336 times across the week"LeadRolloverassignment, including theleadsargument added by the #480 follow-upEach message ends with the same sentence: "this source check is what must go red instead." They are a fourth copy of the source, and this issue is right that they are a poor instrument — but they are currently the only instrument. Deleting them before a behavioural replacement exists drops real coverage for four measured incidents. A replacement that is not red today is not a replacement.
The sequencing defect
The plan is A → B → C → D, with D deleting the source-text guards last. That order cannot run:
main.The dependency is the reverse of the plan: the guards must be dealt with at or before A, not after C.
One more thing about the original sweep's controls
This issue's sweep was validated by two controls, and both are source-text tests —
FleetdLeadSeatWiringTestandFleetdCompletionResolverWiringTest. So the controls proved "a source-text test notices when the source text changes", which is trivially true. They did not demonstrate that any behavioural instrument existed to catch an inert substitution.This does not overturn the sweep's conclusion. The 16 greens still show the gap, and the lead reproduced rank 1 independently. But the controls were weaker than they read, and that is worth knowing before the same method is used to validate Unit D's final mutation matrix.
What I am asking architects to settle
Not whether to do the work — whether the unit order should change, and how the guards are carried across. I am not merging #620 until this is settled.
An authority question that is NOT the architects' to settle
The outgoing lead's handover lists the source-text test files as not to be opened without the operator, carving out only the three Unit D names. Unit A forces three more open. I am raising that with the operator separately; architects should design as if the answer may be no, and say what changes if it is.
Operator decision: test files are the fleet's call. The handover's gate on them is lifted.
I asked the operator whether Unit A may open the three source-text test files beyond the three Unit D names. The answer, in their words:
Two things follow, and the second is the one that matters.
1. The permission question is closed, and it should not have been asked
Test files are inside the fleet's authority. The outgoing lead's handover listed "the 18 test files asserting on source text" as not to be opened without the operator. That gate is lifted. Any of the eight
Fleetd.javasource-text tests may be changed, replaced or removed as part of #612, and no future session needs to ask again.This was my misread, not the handover's fault in substance — but the entry is now wrong and a future lead would obey it. I am recording the correction here, and it goes into the next handover.
The operator's own framing is the useful rule: going to the operator is for actions the fleet has no authority to take — money, access, promises to third parties. Changing our own tests is not one of them. I spent an operator round-trip on something the charter already let me decide.
2. The real acceptance criterion, from the operator
This is the binding constraint on every remaining unit, and it is stricter than "make the build green". Deleting nine failing assertions makes the build green. It does not make anything valid.
So the bar for #612 is:
FleetdConfigRefWiringTest,FleetdHerdrControlConstructionTest) pass because their asserted text stayed inmain. That is luck, not design. They need checking against the new structure too, not just leaving alone because they are green.Green is not the target. A test that passes because the thing it was watching moved out from under it has not been satisfied — it has been blinded.
For the two architects working this now
I briefed you both to answer in two forms — "if the operator says yes" and "if the operator says no". The answer is yes. Give me the "yes" branch as your primary recommendation. Keep any reasoning about the constrained branch only where it changes what you would do.
The operator's "still valid for latest code" line is now an acceptance criterion, not a preference. Design to it explicitly: for every guard your sequence removes, name the replacement and say how you would make it go red on purpose.
Lead decision on the re-sequencing — two architects, they disagreed, I decided
I asked two architects the same question, blind to each other. They agree on most of it and disagree on one thing. Here is the disagreement and my ruling.
They agree on all of this
main.FleetdHerdrControlConstructionTestis now blind. It asserts only thatFleetd.javadoes not containnew AgentControl(/new WorkspaceControl(. Unit A moves that construction toFleetdAssembly.java, so the test passes because its subject left the file. The previous lead planted the guarded text intoFleetdAssembly.javaand the test stayed green (tests="1" failures="0"), then reverted.FleetdConfigRefWiringTestis fine and stays. PR #620 keeps theConfigRefconstruction inFleetd.main, so it still watches its own call site, and it has a positive anchor.Where they disagreed
FleetdAssembly.java(re-point them), overriding this ticket's ban, on two conditions — each re-pointed file gets an unrelated positive anchor, and a javadoc line names the unit that later deletes it. Its argument: the ban is on adding a seventeenth assertion as the fix; moving an existing one so it still points at its own subject is carriage, and the assertion count never rises.Ruling: architect 2. Do not re-point the source assertions. The ticket's ban stands.
I did not decide this on preference. Architect 1's plan buys one thing:
mainstays guarded through the transition. Architect 2's plan delivers that same thing, because under itmainsimply keeps the guards it already has until the atomic landing. So the benefit is not unique to re-pointing, while two costs are:FleetdAssembly.java's bundles. A guard re-pointed in A′ has to be re-pointed or rewritten again in B and C. That is churn that buys no safety.I keep one thing from architect 1, because it is right and it is what separates the two survivors above: any source-text test that stays must have an unrelated positive anchor, and a javadoc line naming the unit that deletes it. That is exactly why
FleetdConfigRefWiringTestis sound andFleetdHerdrControlConstructionTestis not.The rule every unit under this ticket now follows
A guard may be deleted only in the same commit that adds its replacement, and that replacement must be red today — shown by reverting the behaviour it pins and watching the named test fail. Deleting a failing assertion makes the build green and validates nothing.
Order of work
FleetdAssemblyLifecycleTest.java:51-53says is not exercised, andreportRoleFallbackGaps(cfg)atFleetd.java:185, which sits outside the assembly boundary and so cannot be caught if it is deleted.FleetdHerdrControlConstructionTestfails to cover. Each is mutation-proven before its guard is removed.These are not all delegated at once. Step 2's units all drive the assembled graph that step 1 changes, so fanning them out now would collide in the same files. Step 1 lands first.
One check still owed before C is briefed
FleetMcpAuthzTestis a source-text test onFleetMcp.java, and Unit C rewrites that constructor intoReportingSources. Architect 1 flagged it and did not chase it. One grep before C is briefed.A correction to the record
Architect 2 framed its answer as "if the operator says yes / if the operator says no" on whether test files may be changed. That premise is out of date and it was not told in time. The operator settled this on 2026-09-22: test files are the fleet's call and the gate is lifted. So the "yes" branch is the live one. The "no" branch can be ignored.
The owed check on
FleetMcpAuthzTest— done. It will not go silently blind.Architect 1 flagged this and did not chase it, and my decision comment listed it as "one grep before C is briefed". I ran it.
FleetMcpAuthzTestis a source-text test. It readsFleetMcp.javaatFleetMcpAuthzTest.java:52,257:But it is not the same trap as
FleetdHerdrControlConstructionTest, and the difference is the thing worth writing down.theFleetListHandlerActuallyConsultsCoordinatorVisibleTocarries three explicit control assertions before the real one:So if Unit C moves the text this test is anchored on, the test goes red and says why. It cannot pass because its subject left the file. That is exactly the failure mode
FleetdHerdrControlConstructionTesthas, and this test is built against it.Consequence for Unit C: no pre-emptive work is owed here. Brief C normally. If C's rewrite moves
listHandler =orstopHandler =out ofFleetMcp.java, the worker will see a loud, self-describing failure naming the anchor to fix. That is the test working, not a regression to route around — and the worker must fix the anchor, not delete the assertion.The general rule this confirms, and the one I put in the decision comment: a source-text test is sound when a control assertion fails loudly the moment its scrape stops matching. It is blind when it only asserts a negative —
assertFalse(source.contains(...))— because a negative is satisfied by an empty file, a moved subject, or a typo in the needle.FleetdConfigRefWiringTestandFleetMcpAuthzTestare the sound kind.FleetdHerdrControlConstructionTestis the blind kind and still needs its behavioural replacement.Step 1 of the order of work is done — the A-gaps are closed
PR #624 is merged into
worker/fleetd-612-unita-87807e-1, not intomain. PR #620 stays unmerged and now carries both gap fixes. It lands onmainonly after step 2, as ruled above.What landed
LeadChannelHandle(LeadChannel+AutoCloseable) widens the injection seam so a test can supply a fake closeable channel instead of a real broker connection. This costs nothing:LeadMailboxalready implemented both interfaces. New test:FleetdAssemblyCoordinatorLifecycleTest.reportRoleFallbackGapsis inside the boundary. The assembly boundary now starts immediately aftercfg.validateAll(), which pulls in both post-validation calls that Unit A had left behind inmain. New test:FleetdAssemblyRoleFallbackBoundaryTest.The pre-I/O order is unchanged. It was
validateAll()→reportRoleFallbackGaps→assertChartersNameOnlyRegisteredTools→ assembly. It is nowvalidateAll()→ assembly, with those same two calls as the first statements insideassembleAndStart, in the same relative order, before the herdr socket or anything else that touches the outside world. The boundary moved; the sequence did not.Verification I ran myself
Tests run: 1880, Failures: 9. That is 1878 + 2 new tests.FleetdCompletionResolverWiringTestfour, plusFleetdBackendQuarantineWiringTest,FleetdLeadRolloverWiringTest,FleetdLeadSeatWiringTest,FleetdConnectionIdentityConstructionTestandFleetdFleetAppConstructionTest. No new failure. These are step 2's job.reportRoleFallbackGaps. I deleted the other moved call,assertChartersNameOnlyRegisteredTools, to test the claim thatFleetdStartupValidationTeststill pins it by drivingmainend to end. It compiled, and that test went red (1 of 7). So both moved calls are genuinely pinned — the new boundary test covers one, a pre-existing end-to-end test covers the other. Restored, tree clean, 7/7 green.A new same-shape finding, reported and deliberately not fixed
The worker was briefed to look for the shape behind gap 2 — a call that runs before the boundary a test can reach, so deleting it is invisible — and report without fixing. It found one:
It also did the useful negative work of ruling out the neighbours: the five report calls before it (
reportRequiredSecrets,reportGitHostShape,reportMemberTrustModel,reportMemberCredentialsGap,reportExhaustedPatternGap) are not in this category, becauseFleetdStartupReportTestalready drivesmainend to end and asserts each one's log line.This matters more than an ordinary follow-up:
assertPrimaryCleanis the subscription guard. An unpinned call site there means deleting the guard would compile clean and leave the suite green. Not in scope for #612 — filing separately rather than widening this ticket.Next
Step 2: the behavioural replacements, one per guard — #176, #248, #466, #480, CB-185 identity, CB-185 app, and the router-owned controls that
FleetdHerdrControlConstructionTestfails to cover. Each mutation-proven before its guard is removed. These can now be delegated against the assembly, since the boundary they drive is settled.Step 2 is dispatched — three units, against the Unit A branch
Step 1 landed as #620 (Unit A) plus #624 (A-gaps), both on
worker/fleetd-612-unita-87807e-1. That branch is not onmainyet, and it cannot be until step 2 finishes. Here is why.Measured state of the Unit A branch
Commit
608e449,mvn -o testinfleetd/, 2026-09-22:The 9 failures are the whole of step 2's work. They are six test files that pass by scraping the source text of
Fleetd.javafor call sites that Unit A moved intoFleetdAssembly.java:FleetdAssembly.javaFleetdCompletionResolverWiringTest(4 tests)FleetdConnectionIdentityConstructionTestFleetdFleetAppConstructionTestFleetdLeadSeatWiringTestFleetdBackendQuarantineWiringTestFleetdLeadRolloverWiringTestWhy they are not simply re-pointed at the new file
That was the other option on the table, and it is rejected on the ticket's own reasoning. A source-text assertion goes green on a call site that names the right symbols and still passes the wrong thing, and red on a harmless reformat. Re-pointing it buys a passing suite and no coverage. It is a second copy of the source.
Two of the nine are worth calling out:
FleetdCompletionResolverWiringTest.worktreeBranchLookupIsStillPassedAtTheCallSiteandFleetdLeadSeatWiringTest.fleetMcpConstructionStillWiresLeadSeatLookupwere the hunter sweep's two control mutations, and both genuinely went red. That is real coverage being replaced, not dead weight, so each replacement has to be at least as strong.The split
PaneLocatorover both herdr daemons,FleetAppwith both clientsEach unit adds its behavioural replacement and deletes its own guard in the same commit. Splitting those two into different PRs makes a deleted test look identical whether it was superseded or quietly dropped. The commit and PR body have to name, per deleted test, what it pinned and which new test pins it now.
Every unit drives the real seam —
FleetdAssembly.assembleAndStart(...)returning aFleetdRuntimewhose accessors hand back the production objects, not copies — and reaches each behaviour throughruntime.mcp(),runtime.app()orruntime.completion(). No unit may add a field toFleetdRuntime: three workers in one constructor is a guaranteed conflict, and inspecting a field is only one step better than scraping the source anyway. It still does not prove the object is the one the live path consults.Acceptance, same for all three
A replacement that is not red today is not a replacement. Each worker mutates the real call site in
FleetdAssembly.javato its inert variant, runs only its new test, records the real output, reverts, and re-runs — and pastes both outputs with the exact edit and line number. I verify every claim with a mutation the worker did not run.What is left after this
main.Ranks 1, 2, 3, 6, 7 and 8 in the table at the top of this issue are Shape B (behaviour silently degrades, nothing else enforces it) and are still untouched. They are step 4's subject, not step 2's.
Correction for unit B2 (PR #626) — one more test needed before merge
This is the authoritative channel for this correction. If it disagrees with the original brief, this is newer and it wins.
What I verified, and what held
PR #626 is good work. I re-ran the worker's claims independently and two of them hold:
LsofPeerPidLookupreally does exclude its own pid —private final long selfPid = ProcessHandle.current().pid();andcurrent != selfPid. So an in-process test client and daemon share a pid, the lookup returns nothing, andPaneLocatoris never reached. The two production accessors (ConnectionIdentity#panes(),FleetMcp#identity()) are therefore a genuine necessity, not a shortcut. Both are additive getters over collaborators the objects already hold. Accepted.new PaneLocator(herdr, memberHerdr)→new PaneLocator(herdr), dropping the member daemon instead of the lead one. It was killed byconnectionIdentityAlsoSearchesTheMemberDaemon. The identity pair covers both directions plus a negative control, which is better shaped than the guard it replaces.The gap
My second independent mutation survived. On
FleetdAssembly.java:518:Both new
FleetdAssemblyFleetAppTestcases stay green while the lead herdr client is dropped.This is not an equivalent mutant. It is the symmetric form of the CB-185 defect:
GET /healthzwould report green while the lead daemon is down, which is the same class of invisible failure the ticket exists to stop.And the guard being deleted would have caught it. Its positive assertion was:
That text does not survive the mutation. So as it stands, PR #626 is a net loss of coverage in one direction — which fails the bar set for step 2: a replacement must be at least as strong as the guard it removes.
The old guard caught it only incidentally, by pinning the spelling (it would go red on a harmless variable rename too). That does not change the conclusion. The behaviour is real and worth a test on its own merits.
What is needed
One more case in
FleetdAssemblyFleetAppTest, symmetric to the one that already exists:Prove it red with the
new FleetApp(memberHerdr, memberHerdr, ...)mutation above, revert,touchthe file, and re-run. Nothing else in the PR needs to change.Explicitly not owed
The dropped
GET /sessionsmerging assertion stays dropped. The worker tried it against the real assembly, hit a real401becauseAuthz.Action.READneedsCaller.resolved()withpid > 0, and documented that in the new test's javadoc rather than inventing a pass. That is the right call, andFleetAppTwoDaemonTeststill covers the merge itself. Do not reopen it.Correction for unit B3 (PR #628) — one gap, and two things that are NOT gaps
This is the authoritative channel for this correction. If it disagrees with the original brief, this is newer and it wins.
The branch is green
I trial-merged B3 onto the current Unit A tip (which already carries B1 and B2) and built it:
FleetMcp.javawas changed by both B2 and B3 and git merged the two additive hunks with no conflict — and the merged tree compiles and passes, which a clean auto-merge does not by itself prove. The threepublicaccessors are accepted for the same reason B2's were: the assembly tests must live indev.ltms.fleetto buildResourcePorts,FleetMcplives indev.ltms.fleet.mcp, so package-private is unreachable. The worker disclosed this up front rather than burying it.What held
I ran mutations the worker did not, in the "keep every symbol, pass the wrong thing" shape rather than replacing a call with an inert variant:
Fleetd.leadSeatLookup(() -> config.get().profiles(), leaders, leads)→..., java.util.Map.of(), leads). The call site still names the factory and is still called; only its leaders are starved. Killed byassembledLeadSeatSourceReportsALiveLeadsSeat(expected: <1> but was: <0>). Stronger than the guard it replaces.Not a gap — recorded so it is not re-raised
The quarantine clock is unpinned, and that is not this PR's doing.
BackendQuarantine.withEscalation(ports.nanoClock(), ...)→withEscalation(System::nanoTime, ...)survives. But the deleted guard asserted:That is the pre-Unit-A text. The old guard would have gone green on my mutant — it required exactly what I mutated to. So the replacement is strictly stronger here, and the unpinned
ports.nanoClock()argument is a gap Unit A introduced, not a regression from B3. It belongs with #629, which is about the sameResourcePortsseam. Do not ask B3 to fix it.The gap
FleetdAssembly.java:408:Both rollover tests stay green while the roll is wired to the member daemon's agent control instead of the lead's.
It survives only because the test configures a single
FakeHerdrand nomemberHerdrSocket.FleetdAssembly.java:140-142then falls back tomemberHerdr = herdr, sorouter.leadAgents()androuter.memberAgents()wrap the same client and nothing could tell them apart. Under the live config on this host, which does set a member socket, they are different daemons — so the mutant sends/clearand the bootstrap text to the wrong daemon and the lead never rolls. That is a real fleetd #480 defect, not an equivalent mutant.And the deleted guard would have caught it. It asserted the exact string
LeadRollover leadRollover = leadRollover(cfg, router.leadAgents(), config, leads);, and its own failure message names this precise mode:So PR #628 currently trades away coverage in the one direction the guard's author explicitly warned about.
What is needed
Configure a distinct member herdr socket in
FleetdLeadRolloverAssemblyTestsoleadAgents()andmemberAgents()are different clients, then assert the roll drives the lead daemon. Prove it red with therouter.memberAgents()mutation above, revert,touch, re-run.This is the same lesson B2 hit: a two-daemon wiring cannot be pinned by a one-daemon fixture. B2's
FleetdAssemblyConnectionIdentityTestalready builds two distinct fakes and is the pattern to copy.Nothing else in the PR changes.
Steps 1–3 are done and live on
mainmainis26f1986. Full suite, stale surefire reports cleared first:Daemon redeployed: pid 63248, jar
85e64b1afb5c,fleetd listeningat 12:47:49,fleet_whoamistillprimary, and a real sonnet spawn reachedidlebefore being stopped. A green/healthzonly proves herdr answers, so the spawn is the check that matters.What landed
All six source-text guard files are gone. Every replacement drives the real
FleetdAssembly.assembleAndStart(...)and inspects the production objects throughFleetdRuntime. None reads the source text of a.javafile.Verification
Every unit was checked with a mutation the worker did not run, in the "keep every symbol, pass the wrong thing" shape rather than swapping a call for an inert variant — because that is the shape a source-text guard is blind to by construction.
Killed: starving
worktreeBranchLookupwith an empty roster; starvingbackendErrorPatternLookupwith an empty map; starvingleadSeatLookupwith empty leaders; dropping the member daemon fromPaneLocator.Two survived on first pass and were sent back rather than waved through:
new FleetApp(memberHerdr, memberHerdr, ...)left both tests green. The deleted guard assertedsource.contains("new FleetApp(herdr, memberHerdr, workers,")and would have caught it, so the PR was a net loss of coverage in that direction. Fixed by the symmetric lead-down case.Fleetd.leadRollover(cfg, router.memberAgents(), ...)left both tests green, because the fixture used oneFakeHerdrand nomemberHerdrSocket, soFleetdAssembly.java:140-142collapsed both agent controls onto one client. Fixed by giving the test two distinct sockets. The deleted guard's own failure message named this exact mode.Both are the same lesson: a two-daemon wiring cannot be pinned by a one-daemon fixture.
One survivor was ruled not a gap: the quarantine clock. The deleted guard required the pre-Unit-A text
withEscalation(System::nanoTime,and would have gone green on that mutant too, so the replacement is strictly stronger. That gap belongs to #629.The merge was not mechanical
mainmoved 8 commits while Unit A was in flight. #622 (fleetd #621) addedrequireOperatorConfirminside the block Unit A had already moved. Taking Unit A's side of theFleetd.javaconflict — the resolution a merge tool suggests — would have dropped it and reverted the operator's #621 fix, with a clean build and no failing test, because the 13-argumentLeadHeartbeatLoopoverload still delegates withtrue. Carried across by hand in72f46d7.That is this ticket's own defect shape, found in this ticket's own merge.
Follow-ups filed, not delegated
assertPrimaryCleanfromFleetd.mainleaves the suite green.ResourcePorts, so any test with an unhealthy lead herdr pays 30 real seconds.requireOperatorConfirmwiring is unpinned (the one above).All three are call sites the assembly owns and no test observes — the same family this ticket exists to close.
Still open here
Step 4: ranks 1, 2, 3, 6, 7 and 8 in the table at the top. Those are Shape B — behaviour silently degrades, nothing else enforces it — and they are the ones that actually hurt a running fleet. Rank 1 hands a genuine usage-limit refusal back to a waiting caller as real completed work.