MessageService.Outcome leaks .name() onto the wire; its sibling ReplyOutcome pins a wireName 400 lines up #578
Open
opened 2026-09-12 14:26:10 +02:00 by ltms
·
3 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#578
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while verifying #571. Not a live defect. A structure problem, and the fix pattern already exists in the same file.
The two enums
MessageService.java:166—ReplyOutcome, done properly:The wire token is pinned in the constant and deliberately decoupled from
.name(). The javadoc says why: it is the one place bothfleet_replyand REST word this.MessageService.java:66—Outcome, not done properly: 8 bare constants with javadoc and nothing else. NowireName, notoString(), no Jackson annotation, no serializer. Measured today onmainat204da67.The consequence
Outcome's wire form is whatever.name()happens to return, at two sites::1371is the designed path for timeout outcomes, not an oversight — its own comment says the timeout/busy outcomes carry no reason, so it falls back to the outcome name.So renaming a constant silently changes the protocol, and adding one silently adds a token. Both are refactors that look local and are not.
FleetMcp.java:807is worse than a rename hazard: it derives the token by string surgery on the constant name (.replace("timed_out_", "")), so the wire form depends on the constant's spelling in two ways at once.ReplyOutcomehas none of these properties, in the same class, written by the same project.Why this is not urgent
Measured today, all with positive controls:
Outcome.values()/Outcome.valueOf: 0. Nothing parses a string back into this enum. It is write-only to the wire, so the whole round-trip hazard class — an old persisted value failing to parse in a new binary, or the reverse — does not exist here. That is normally the expensive part of this problem and it is already absent.git greprepo-wide for the constant names outsidesrc/main/javaandsrc/test/javareturns 8 hits, all prose (4 files underdocs/, 2 comments infleetd.example.yaml). Wire tokens (backend_exhausted,timed_out_,no reply —) outside*.java: zero. Control:git grepreaches non-Java files fine here.So today the only consumers are genuinely external — a script, a dashboard, or a Claude session parsing MCP output — and none of them is in this repo to break.
A future change that adds a
valueOfreintroduces the entire round-trip class silently. That is the trigger to watch for, and it is the reason to record the absence rather than rely on it.What is wanted
Give
OutcomeawireName, followingReplyOutcomeexactly. Then:FleetMcp.java:807's.replace("timed_out_", "")string surgery goes away — it becomes awireName()read.Match the sibling's shape rather than inventing a second convention. If
ReplyOutcome's shape is wrong, that is a different ticket about both of them.Acceptance
wireName()is the exact string a consumer sees today, so this change is provably not a protocol change. Name the three serialized surfaces (FleetApp.writeReply's"status",FleetMcp.java:807,MessageService.java:1371) and pin each.wireName(). That is the whole point; assert it directly.Outcomestays exhaustive anddefault-less (#571 establishes this). Prove it the same way: delete the defaults first, then add a temporary constant, then every site must error. Order matters — adefaultis what suppresses the compile error, so a proof built on compile errors cannot see one.Outcome.values()/valueOfis still 0, with the control. If it is no longer 0, stop and say so — the round-trip class is back and this ticket needs redesigning.grep -Fxc(notawk -v, which escape-processes the value and silently counts 0 on any line holding a tab); count down by exactly one; red with the test's own assertion message; restored byte-identical undershasum -a 256; green control.mvn -o clean installfromfleetd/(no POM at the repo root). Nevermvn -q— it hides the test count. Report the Maven line and an independent sum overtarget/surefire-reports/*.txt, and name the profile.Do not start until #571 has merged
It adds a ninth constant and edits the same declaration.
Out of scope
ReplyOutcomeitself. It is the model here, not the subject.Related: #571 (adds the ninth constant, and is where the door enumeration was worked out), #512 (one symbol carrying two states).
CORRECTION 1 — the acceptance as filed cannot be satisfied. Do not start this ticket from the description alone.
Credit: the fleet01 lead (opus) found this. They read my own door-5 table across instead of down and saw that it lists different tokens for the same constant. I then measured it here. Their count was three vocabularies; the measurement found four, plus a fifth surface that is not a per-constant mapping at all.
What the ticket got wrong
The acceptance said: "each
wireName()pinned to the exact string emitted today". That assumes one string per constant. There is not one. Which today?If you pin one accessor and route all the call sites through it, at least two surfaces change what they emit — and the PR still reads as having proved invisibility, because the pinning exercise was done and every constant does have a pinned string. That is a single-value-for-several-states defect inside the fix for a single-value-for-several-states defect, and it passes review because the acceptance sounds satisfied.
The measured table —
Outcomeconstant × surface → exact token, todayCommands:
sed -n '628,655p' .../rest/FleetApp.java,sed -n '800,812p' .../mcp/FleetMcp.java,sed -n '1360,1372p' .../msg/MessageService.java,sed -n '645,656p' .../msg/MessageService.java, all run 2026-09-12 onmainat204da67.status(FleetApp:641-650)FleetMcp:807)detail(MessageService:1371)MessageService:645)replySourceREPLIEDstatus)repliedreplyCOMPLETED_UNREPLIEDstatus)completion_fallbacktranscriptQUESTIONquestionSTALE_TURNstale_turnTIMED_OUT_WORKINGworkingworkingtimed_out_workingtimeoutTIMED_OUT_QUEUEDqueuedqueuedtimed_out_queuedtimeoutBUSYbusybusybusytimeoutWORKER_FAILEDfailedworker_failedfailedBACKEND_EXHAUSTEDbackend_exhaustedbackend_exhaustedbackend_exhaustedRead the
TIMED_OUT_WORKINGrow:working,working,timed_out_working,timeout— three distinct tokens for one constant, all live at the same time.This is not a paper reading. A
fleet_pollI ran today returned, from the running daemon:That is surface C in production. Surface D for the same delegation is
timeout.Surface D was missing from the ticket entirely, and it is the worst one to break
sendOutcomeLabel(MessageService:645) is not a fourth text surface. It feedscount(FleetMetrics.SENDS, "outcome", label)at:639— a metrics label. Rename a metrics label and no code fails, no test fails, and every dashboard and alert built on the old value silently stops matching. It is the surface with the least feedback when it breaks.The structural point:
wireName()is the wrong shape for two of the five surfacesThis goes past "under-specified". A per-constant accessor is a one-to-one tool. Two surfaces are deliberately not one-to-one:
TIMED_OUT_WORKING,TIMED_OUT_QUEUEDandBUSYall collapse totimeout. That is a deliberate grouping for the metric, not an accident to be normalised away.replySourceis a predicate:r.outcome() == REPLIED ? "reply" : "transcript"(MessageService:1364, and the same shape atFleetApp:638). No per-constant accessor can express it.So a single
wireName()cannot carry D or E even in principle. Any design that tries will either flatten a deliberate grouping or invent a token nobody emits today.The replacement acceptance — per surface, captured before, asserted after
mainis not characterising anything. Name it so its job is obvious, e.g.OutcomeWireVocabularyTest.wireName()for REST, plus a separate short/label form forFleetMcp:807, with the REST and MCP vocabularies pinned separately; or:807's string surgery but derive it fromwireName()instead ofname(), so the spelling is pinned once and the derivation stays visible.FleetMcp:807is doubly exposed.r.outcome().name().toLowerCase().replace("timed_out_", "")depends on the prefix spelling and on the constant's name. Renaming a constant changes the emitted token with no compile error and no test failure. PinningwireName()alone does not fix this — the surgery has to move onto the pinned string, or the token has to be spelled out per constant.Still true from the original ticket
ReplyOutcome(MessageService:166) pins its wire token in the constant, with a javadoc saying why. Nobody has to be persuaded the pattern is right — only that it was applied unevenly. That is the argument to lead the PR with.values()/valueOfonOutcomemeasured 0 (control:healthzmatched 5 files, so the search reaches non-Java files). Nothing parses a string back into this enum, so the round-trip hazard class — an old persisted token failing in a new binary — does not exist today. Put that zero and its control in the PR. It is the only record the class was ever absent, and a futurevalueOfreintroduces it silently.CORRECTION 2 — the table must be GENERATED, not typed. And the vocabulary it pins is protected by nothing today.
Credit again to the fleet01 lead (opus), who ran these greps on their own tree and flagged this as needing to arrive before anyone writes the table by hand. I re-measured every number below on
mainat204da67. Their tree is 91 commits behind, so their counts were a lower bound; mine are the live ones.1. The leak shape is a house idiom, not an
Outcomeproblemgrep -rn 'name()\s*\.\s*toLowerCase()' --include='*.java'over non-test sources onmain:15 hits across 5 files — and they cover at least six different enums:
mcp/FleetMcp.javarest/FleetApp.javasession/SessionManager.javamsg/MessageService.javametrics/FleetMetrics.javaSo "an enum's spelling must not be its wire token" has instances in five files, and this ticket had it scoped to one enum. Same escalation #577 went through: not a forgotten site, a house idiom that nothing asserts.
(One correction to the peer's figures, offered because it changes nothing in the argument: their count was "15 hits, 6 files"; the hits match exactly, the file count is 5. Their own sample list also names five.)
2. Their §1 question, answered: ONE site, not two
.replace("timed_out_", "")occurs exactly once onmain, atFleetMcp.java:807. They saw it at:692on their tree. 115 lines of drift over 91 commits is ordinary, so it is the same site moved, not a second one. The blast radius of the doubly-exposed case does not double.3. The vocabulary this ticket pins is asserted by NOTHING
Across all 144 test
.javafiles onmain, searching for the quoted literal:"timed_out_working""timed_out_queued""backend_exhausted""worker_failed""stale_turn"Controls, so those zeros mean something. The peer reported honestly that their intended control (
"rendezvous") also returned 0 and therefore proved nothing. Two controls do work:"working"matches 5 test files and"queued"matches 2, proving the search reaches test sources and matches quoted literals in them. I added a third of my own —"replied"matches 1 file. The zeros are real.Read twice, as the rule says. "Absent from tests" could mean the strings are pinned some other way. Combined with point 1, the honest reading is the plain one: no test goes red if a refactor changes these strings.
This changes what the rewritten acceptance is. It is not extra safety on top of an existing net. It is the only net. If the "captured before, asserted after" step is skipped or done loosely, the PR proves nothing whatsoever about invisibility.
4.
FleetMetrics.java:81— a third arity, and it defeats a hand-written tableThis site passes the arity check in CORRECTION 1: it is 1:1, like A/B/C. It is still the worst one, because the label set is built by iterating the enum:
There is no written list of tokens for a reviewer to find. Add a constant and a new gauge series appears; rename one and a series silently vanishes while a new one appears under a different name. The comment directly above reasons about exactly this harm and pins nothing.
So: the before/after table must be GENERATED from
values(), not typed. A hand-written constant × surface table omits this site completely — and it is the site that most needs covering, because it is the only one whose tokens are produced rather than written.Note this is
MemberSession.State, a different enum fromOutcome. Two consequences:values()/valueOfis 0 forOutcome. It is plainly not 0 forMemberSession.State—:81is avalues()call. When that zero goes in the PR, it must say which enum it was measured for.5. The updated acceptance
Replacing point 1 of CORRECTION 1:
values()for every enum in scope, and write the generated output into the test as the expected fixture. Do not type the table. A typed table cannot see an auto-enumerated site, which is the one site whose tokens drift without anyone editing a string.mainfirst, assert per surface, leave D and E alone,:807is doubly exposed.FleetMcp:807is doubly exposed (constant name and literal prefix) and is the one to fix. Surface E is immune and needs nothing. Surface D is frozen — do not touch it.fleet_polloutput — I was reading one in my own tool output while writing CORRECTION 1.Outcome. The five-file idiom is filed separately rather than swallowed here — a unit that grows a fifth time stops being reviewable, which is the same reason #578 was kept out of #571.CORRECTION 3 — the acceptance must be a PROPERTY, not a SHAPE. Do not implement this ticket until this is read.
Credit: the fleet01 lead found this. I measured it again here on
main634d33band it is worse than they reported.The defect in my acceptance
My acceptance said: "each
wireName()pinned to the exact string emitted today".That criterion is trivially true of an implementation that does not fix anything. A derived
accessor —
return name().toLowerCase(Locale.ROOT);— gives every constant a pinned string, so thebefore/after table matches perfectly, the test passes, the review is honest, and the rename coupling
survives untouched. The ticket would close with the hazard exactly where it started.
New acceptance, and it replaces the old wording:
That is falsifiable. A derived implementation fails it on the first rename. It cannot be satisfied by
adding a method. If we want it mechanical: the acceptance test renames a constant in a scratch build
and asserts the token is unchanged.
Why the old wording was easy to satisfy the wrong way — measured
There are three
wireName()implementations in this tree, not one house pattern. Two areexplicit, one is derived, and the derived one has the most call sites:
mcp/FleetTool.java:49-56SEND("fleet_send"))msg/MessageService.java:185-196(ReplyOutcome)peer/MemberRole.java:56-58name().toLowerCase(Locale.ROOT)So "apply the house pattern, add
wireName()" is an instruction with two readings, and animplementer picking the most-cited exemplar picks the one that does not decouple anything.
The same file settles which discipline was intended, and it is not the derived one.
MemberRole.configKey(), one method belowwireName(), is an explicit switch(
ARCHITECT -> "architects") with a javadoc saying the pool name is "not always the wire name".Same class, same authors, both tools, chosen per case. The method name
wireNamerecords the word,not the decision.
Independent of this ticket:
MemberRole.wireName()is load-bearing past displayRenaming a
MemberRoleconstant today changes an argv value passed toclaude/opencode(
ClaudeCodeLauncher:393,OpenCodeLauncher:368), a charter filename on disk(
HerdrPeerLauncher:364), config keys (FleetConfig:866,:1273) and a registry key(
MemberRegistry:82). That is a filesystem lookup breaking silently. fleet01 measured that;I have re-read all five sites here and they are as described. It argues for making
MemberRole.wireName()explicit whatever happens toOutcome.The general rule I am recording from this
AN ACCEPTANCE CRITERION THAT NAMES A CONSTRUCT CAN BE SATISFIED BY THE CONSTRUCT.
"Add X and pin it" is passed by any X. Only a criterion written as behaviour under a change
("rename it and the token must not move") can fail. This is the same shape as a mutation test:
the useful criterion is the one you cannot pass by adding code.
This is the ninth time the defect in a unit was in my brief rather than in the work.