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
Owner

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 HerdrContractTest and AgentControlContractTest. 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:

Never drive the terminal multiplexer directly (no herdr CLI, no socket). The bridge owns policy; the multiplexer owns PTYs. Going around the bridge bypasses every rule above.

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:

$ grep -rl 'herdr' fleetd/src/main '--include=*.java' | wc -l
56

UnixSocketHerdrClient.connect() opens the socket. Running HerdrContractTest opens 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.md requires to stay byte-identical with the wiki template (Use Cases → The portable CLAUDE.md block) 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:

  • Driving the multiplexer to make something happen in the fleet — create a pane, send input to a peer, read someone's terminal, start or stop an agent — stays banned outright, for every role, with no debugging exception.
  • Exercising the herdr client as the subject of a test or a fix, inside a throwaway space the test creates and tears down, is the assignment and cannot be banned.

AgentControlContractTest is a good model of the safe version already: it creates its own throwaway space, probes only the shell in its own seed pane, never touches claude, 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

  • A worker assigned to herdr code can read one document and know what it may and may not do with the socket, without inferring it.
  • The orchestration ban is not weakened: driving panes, sending input to another session, or reading another session's terminal is still banned for every role, with no debugging exception.
  • If the change lands in the canonical block rather than the addendum, the wiki template is updated in the same change and the sync check in CLAUDE.md passes.
  • State plainly which layer you put it in and why, since that is the actual decision here.

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.

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 `HerdrContractTest` and `AgentControlContractTest`. 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: > **Never drive the terminal multiplexer directly** (no `herdr` CLI, no socket). The bridge owns policy; the multiplexer owns PTYs. Going around the bridge bypasses every rule above. 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: ``` $ grep -rl 'herdr' fleetd/src/main '--include=*.java' | wc -l 56 ``` `UnixSocketHerdrClient.connect()` opens the socket. Running `HerdrContractTest` opens 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.md` requires to stay byte-identical with the wiki template ([Use Cases](https://git.ltms.dev/fleet/fleetd/wiki/7-Use-Cases) → *The portable `CLAUDE.md` block*) 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**: - Driving the multiplexer to **make something happen in the fleet** — create a pane, send input to a peer, read someone's terminal, start or stop an agent — stays banned outright, for every role, with no debugging exception. - Exercising the herdr client **as the subject of a test or a fix**, inside a throwaway space the test creates and tears down, is the assignment and cannot be banned. `AgentControlContractTest` is a good model of the safe version already: it creates its own throwaway space, probes only the shell in its own seed pane, never touches `claude`, 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 - A worker assigned to herdr code can read one document and know what it may and may not do with the socket, without inferring it. - The orchestration ban is not weakened: driving panes, sending input to another session, or reading another session's terminal is still banned for every role, with no debugging exception. - If the change lands in the canonical block rather than the addendum, the wiki template is updated in the same change and the sync check in `CLAUDE.md` passes. - State plainly which layer you put it in and why, since that is the actual decision here. ## 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.
Author
Owner

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:

Never drive the terminal multiplexer directly (no herdr CLI, no socket).

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:

  1. Addendum now. Local, reversible, and it unblocks a herdr-assigned worker today.
  2. Then the purpose-restatement as its own canonical change, reviewed on its own merits, with the addendum deleted only if the restatement actually subsumes it.

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

Does the rule's violation set differ by project? If the same concrete action is legal in project A and illegal in project B, the rule is project-scoped and belongs in the addendum. If the purpose is universal but only the mechanism test varies by project, the purpose goes canonical and the mechanism test goes local.

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:

Before sending a brief, name one legal action that accomplishes the assignment. If you cannot produce one, the brief is unsatisfiable.

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-bridge section, 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 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: > Never drive the terminal multiplexer directly (no `herdr` CLI, no socket). 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: 1. **Addendum now.** Local, reversible, and it unblocks a herdr-assigned worker today. 2. **Then the purpose-restatement as its own canonical change**, reviewed on its own merits, with the addendum deleted only if the restatement actually subsumes it. 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 > **Does the rule's violation set differ by project?** If the same concrete action is legal in project A and illegal in project B, the rule is project-scoped and belongs in the addendum. If the purpose is universal but only the mechanism test varies by project, the purpose goes canonical and the mechanism test goes local. 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**: > Before sending a brief, name **one** legal action that accomplishes the assignment. If you cannot produce one, the brief is unsatisfiable. 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-bridge` section, 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.
Author
Owner

The canonical restatement is now its own ticket: #458.

So the order is fixed:

  1. This ticket (#455) — the project addendum only. It changes one repo and cannot break
    another. Ships first.
  2. #458 — restate canonical invariant 5 by purpose instead of by mechanism, then propagate to
    wiki/7-Use-Cases.md and 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.

The canonical restatement is now its own ticket: #458. So the order is fixed: 1. **This ticket (#455)** — the project addendum only. It changes one repo and cannot break another. Ships first. 2. **#458** — restate canonical invariant 5 by purpose instead of by mechanism, then propagate to `wiki/7-Use-Cases.md` and 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.
ltms closed this issue 2026-09-10 13:18:18 +02:00
Author
Owner

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:

  • Canonical block byte-identical: 17468 chars on main and on the branch.
  • The sync script in CLAUDE.md prints in sync: True both before and after.
  • The note's own re-measure command works. Run verbatim it returns exactly the 4 files the note
    names and no others:
    $ grep -rl 'UnixSocketHerdrClient.connect()' fleetd/src/test/java --include='*.java'
    fleetd/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java
    fleetd/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java
    fleetd/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java
    fleetd/src/test/java/dev/ltms/fleet/herdr/HerdrContractTest.java
    
    Control that the search reaches the tree: 120 test java files, 63 of them mention herdr. That
    control matters here 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 the
worker's acceptance criteria. The worker could not run it:

FileNotFoundError: [Errno 2] No such file or directory: 'wiki/7-Use-Cases.md'

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 status prints a leading - and the directory holds 0 entries. The primary's own
clone prints a leading + and the file is there. So the criterion was unsatisfiable for a member
from 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 a
member'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.

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: - Canonical block byte-identical: 17468 chars on `main` and on the branch. - The sync script in `CLAUDE.md` prints `in sync: True` both before and after. - The note's own re-measure command works. Run verbatim it returns exactly the 4 files the note names and no others: ``` $ grep -rl 'UnixSocketHerdrClient.connect()' fleetd/src/test/java --include='*.java' fleetd/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java fleetd/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java fleetd/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java fleetd/src/test/java/dev/ltms/fleet/herdr/HerdrContractTest.java ``` Control that the search reaches the tree: 120 test java files, 63 of them mention herdr. That control matters here 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 the worker's acceptance criteria. The worker could not run it: ``` FileNotFoundError: [Errno 2] No such file or directory: 'wiki/7-Use-Cases.md' ``` 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 status` prints a leading `-` and the directory holds 0 entries. The primary's own clone prints a leading `+` and the file is there. So the criterion was unsatisfiable for a member from 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 a member'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.
Author
Owner

Criterion 4 decision: the addendum note stays

#458 shipped as eccd054. Canonical invariant 5 now reads:

  1. Never move a fleet session, pane or peer except through the bridge. The bridge owns policy; the multiplexer owns PTYs. Any route that changes fleet state without the bridge's checks bypasses every rule above — the herdr CLI and its socket are the usual example.

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 herdr CLI, 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:

  1. It names the four files the carve-out covers — so a worker does not have to re-derive whether its own test "really" moves fleet state. Under the new invariant that judgement is possible. It is still a judgement, made by the least-context reader in the system, at the moment they are trying to finish something else.
  2. It records why — fleetd #449, where a fake and the real herdr disagreed for weeks under a fully green suite. Without that line the carve-out looks like an exception someone wanted, and gets re-litigated the next time a reviewer reads it.

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.

## Criterion 4 decision: the addendum note stays #458 shipped as `eccd054`. Canonical invariant 5 now reads: > 5. **Never move a fleet session, pane or peer except through the bridge.** The bridge owns policy; the multiplexer owns PTYs. Any route that changes fleet state without the bridge's checks bypasses every rule above — the `herdr` CLI and its socket are the usual example. 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 `herdr` CLI, 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: 1. **It names the four files the carve-out covers** — so a worker does not have to re-derive whether its own test "really" moves fleet state. Under the new invariant that judgement is *possible*. It is still a judgement, made by the least-context reader in the system, at the moment they are trying to finish something else. 2. **It records why** — fleetd #449, where a fake and the real herdr disagreed for weeks under a fully green suite. Without that line the carve-out looks like an exception someone wanted, and gets re-litigated the next time a reviewer reads it. **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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#455