Invariant 5 ("never drive the multiplexer directly, no socket") is unsatisfiable for a worker assigned to herdr code #455
Closed
opened 2026-09-10 12:38:30 +02:00 by ltms
·
4 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#455
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 from a worker's own disclosure on #449, not from a review. The worker did the right thing and it exposed a defect in the rule, not in the worker.
What happened
The #449 worker was assigned to fix
HerdrContractTestandAgentControlContractTest. Both talk to a real herdr over its unix socket — that is the entire point of a contract test. While diagnosing, the worker made one read-only ad-hoc query to the herdr socket outside the test client, then reported it against itself as a rule violation.It was read-only, it changed nothing, and the worker disclosed it unprompted. I am not treating it as a fault.
The defect in the rule
CLAUDE.md, canonical block, invariant 5:For almost every worker this is clear and right. For a worker assigned to herdr code it is unsatisfiable as written, because the code under test is itself a socket client:
UnixSocketHerdrClient.connect()opens the socket. RunningHerdrContractTestopens the socket. So a worker told both "fix this contract test" and "never touch the socket" has been handed two instructions with no legal move — which is a shape I have already filed against my own briefs (#330, and the note in fleetd #446's history): a constraint pair whose only compliant action is to do nothing.The worker resolved it the sensible way — use the socket through the test client, treat anything else as off-limits — and then correctly flagged that its one ad-hoc query fell outside even that reading. The rule gave it no way to know whether that reading was right.
Why this needs care rather than a quick edit
Invariant 5 is in the canonical block, which
CLAUDE.mdrequires to stay byte-identical with the wiki template (Use Cases → The portableCLAUDE.mdblock) and to be propagated to every other project carrying it. So this is not a one-file change, and I did not want to make it inline while closing a ticket.It also must not become a loophole. The rule exists because going around the bridge bypasses identity, authorization and status gating. The carve-out has to be narrow enough that "I was debugging" never justifies driving panes.
Proposed shape (not a mechanism — argue it)
The distinction that seems to hold is orchestration versus code under test:
AgentControlContractTestis a good model of the safe version already: it creates its own throwaway space, probes only the shell in its own seed pane, never touchesclaude, and always tears the space down.Open question I do not have a confident answer to, and would like a second opinion on: does the carve-out belong in the canonical block at all? It is specific to this repo, where herdr is a dependency under test. Every other project mounting the block has no herdr code, so for them the flat ban is exactly right and an exception is pure risk. That argues for leaving the canonical invariant alone and putting the carve-out in the project addendum, which is where repo-specific rules are supposed to live.
If that is right, the fix is small and local, and the ticket is mostly about writing it precisely.
Acceptance
CLAUDE.mdpasses.Priority
Medium. Nothing is broken, but this will recur every time herdr code is delegated, and the cost is a worker either refusing work it should do or doing it while believing it is breaking a rule. Both are bad, and the second one only surfaces if the worker is honest enough to say so — which is not something to rely on.
The layering question is answered, and the answer comes with a better fix and a reusable test
I asked a peer lead to read this ticket, because
CLAUDE.md's own rule says the author is the worst reader of their own qualifier placement. Their answer agreed with my lean and then improved on it. Recording it here as the decision, with attribution.Agreed: the carve-out does not belong in the canonical block
Reason, in their words: every other project carrying the block has no herdr code, so for them the flat ban is correct, and an exception is pure risk bought for a benefit they can never collect.
The better fix: the invariant is stated as a mechanism when its purpose is a control plane
This is the part I had missed. Invariant 5 says:
That prohibits a syscall. What it actually means is "do not manipulate panes outside fleetd's control." In a repo where herdr is the system under test, opening the socket is not bypassing the control plane — it is exercising the dependency. Two different acts that happen to share one syscall, and only a mechanism-shaped rule confuses them.
The general form, which is worth more than this ticket: a rule stated as a mechanism becomes unsatisfiable in every context where the mechanism is the subject, and has to be patched per-context forever. A rule stated as a purpose is correct in both contexts and needs no exception at all.
The sequencing is not optional — do NOT do both at once
Their strong recommendation, which I am adopting:
Why not the canonical rewording first: it propagates to every project carrying the block, including ones neither of us can see, and the failure mode of a too-loose purpose statement is that it is interpretable. A worker can argue its way past a purpose in a way it cannot argue past "no socket". That is a real cost and it lands everywhere at once.
Bundling them means the risky change rides in on the safe one's justification. Two changes, two reviews, different blast radii.
A placement test to reuse, since this will recur
Applied here: the purpose (do not bypass fleetd) is universal; the mechanism test (is touching the socket evidence of bypassing?) is project-specific. That gives the answer without a judgment call about this particular rule.
And a pre-send check for the shape that caused this ticket
The constraint-pair defect — a brief whose only compliant action is to do nothing — is cheaply detectable at authoring time:
One sentence to check, against a worker burning a turn discovering there is no legal move. I file these against myself after the fact; this makes it a pre-send step instead of a post-mortem. Adopted.
Revised scope for this ticket
In scope now: the project addendum only. Add the orchestration-versus-code-under-test distinction to
CLAUDE.md's## Project addendum — claude-bridgesection, below the canonical block. Do not touch invariant 5. Do not touch the wiki template. The sync check must still pass, which it will, because the canonical block is unchanged.Filed separately, not here: restating invariant 5 by purpose rather than by mechanism. That is a canonical change with a wide blast radius and it needs its own review.
Acceptance is unchanged except for the layer, which is now decided rather than open: the orchestration ban must not weaken — driving panes, sending input to another session, or reading another session's terminal stays banned for every role with no debugging exception.
The canonical restatement is now its own ticket: #458.
So the order is fixed:
another. Ships first.
wiki/7-Use-Cases.mdand every other project that carries the block.#458 is marked blocked on this one, and its scope says not to start it until this has shipped.
Done. PR #461 merged as
9d1306d.The note lives in §Project addendum, 13 lines, and the canonical block was not touched.
What I verified myself, because the worker could not:
mainand on the branch.CLAUDE.mdprintsin sync: Trueboth before and after.names and no others:
control matters here more than usual — a re-measure command that matches nothing would tell a
future session to delete a live restriction.
One defect, and it was mine. I wrote "the sync script must print
in sync: True" into theworker's acceptance criteria. The worker could not run it:
It reported that plainly and did not claim the check passed. That is the right behaviour and I
want it on the record as such.
The cause, measured in three worker worktrees: a provisioned worktree has
wiki/uninitialized.git submodule statusprints a leading-and the directory holds 0 entries. The primary's ownclone prints a leading
+and the file is there. So the criterion was unsatisfiable for a memberfrom the moment I wrote it — the same shape as every other entry in my own list of briefs that
were the defect source.
Fixed in
5f1b260: the addendum now says the sync check is the lead's, that it must never be amember's acceptance criterion, and that a member asked for it must report that it could not run
it rather than invent a pass. Dated, with the re-measure command and a delete-when-stale line.
Next, and deliberately separate: #458 restates canonical invariant 5 by purpose instead of by
mechanism. This addendum gets deleted only if that restatement actually subsumes it — not
automatically.
Criterion 4 decision: the addendum note stays
#458 shipped as
eccd054. Canonical invariant 5 now reads:Criterion 4 of this ticket asked whether the restated invariant makes this ticket's addendum note redundant. It does not. The note stays, and the reason is worth writing down because it is not obvious.
The restated invariant removes the unsatisfiability. Under the old wording ("no
herdrCLI, no socket", full stop) a worker assigned to herdr contract code found that the only action which could finish its task was banned outright. Under the new wording, opening the socket against a throwaway workspace the test creates and tears down is legal on its own terms — it moves no real fleet session, pane or peer. That was the sharp problem and it is gone.But the note still does two things the invariant cannot do on its own:
A rule that is satisfiable is not the same as a rule a worker can apply without re-deriving it. That is the distinction I got wrong when I first wrote criterion 4, which assumed the two would stand or fall together.
The note also stays perishable-by-design, per the canonical block's own requirement: it carries its measurement date (2026-09-10), the command that re-measures it, and the instruction to delete it once it stops reproducing.