f336bcef39
The fleet01 lead found this. My edit in f5c6a0e left one prose line at 128
columns inside a paragraph wrapped at about 96. It is invisible to any check
that normalises by paragraph, and visible in any raw digest of the block.
No wording changed. Block stays 20937 bytes. The wiki template gets the same
rewrap in its own commit, so the two stay byte-identical.
448 lines
34 KiB
Markdown
448 lines
34 KiB
Markdown
# claude-bridge — project instructions
|
||
|
||
## Bridge communication (enforced — read this first)
|
||
|
||
> **Canonical block.** Everything down to §Layering is the portable bridge charter, copied verbatim
|
||
> into every project that mounts the bridge MCP. Keep it byte-identical with the template in the
|
||
> wiki ([Use Cases](https://git.ltms.dev/fleet/fleetd/wiki/7-Use-Cases) → *The portable
|
||
> CLAUDE.md block*); improvements go to the template first, then out to each project. Anything
|
||
> specific to *this* repo lives under §Project addendum below, never inline above it.
|
||
>
|
||
> **Anything you measure in an addendum is perishable.** Date it, give the command that
|
||
> re-measures it and what each outcome means, and tell the reader to delete the section once
|
||
> it stops reproducing. The four parts work together: deciding what would falsify a claim is
|
||
> the expensive step, and a reader in the middle of another task will not pay it, so a bare
|
||
> "verify before relying on this" costs the same space and does nothing. The case this is for
|
||
> is a note that goes stale as a live restriction — it will tell a future session it cannot do
|
||
> the thing at the moment doing it becomes the job.
|
||
|
||
If no `fleet_*` MCP tools are mounted in this session, this section does not apply — skip it.
|
||
|
||
`fleetd` is the **sole communication gateway** between agents here. The orchestrating session (the
|
||
**primary**) and every delegated peer (a **member**) mount the *same* MCP server and talk only
|
||
through its `fleet_*` tools. No session addresses a peer, a broker, or the network directly.
|
||
|
||
### Which role am I? — settle this before acting
|
||
|
||
**Every role reads this file.** A member runs in a git worktree of this same repo, so it inherits
|
||
this `CLAUDE.md` verbatim, and every rule below is role-conditional.
|
||
|
||
**Call `fleet_whoami`.** It returns `primary`, `worker`, or `architect`, resolved by the daemon from
|
||
your connection — unforgeable, and the same resolution its authorization gate uses. A worker also
|
||
carries its `sessionId`, `profile`, `worktree` and `branch`; an architect carries the slot name it
|
||
was bound to. Don't infer what you can ask.
|
||
|
||
Only if that call is unavailable, fall back to these — each is one-way, so keep reading until one
|
||
fires: the reply charter in your system prompt (*"You are a spawned member in the
|
||
claude-bridge fleet"*) ⇒ **spawned member**; fleet tools prefixed `mcp__fleet__*` ⇒ **spawned
|
||
member** (the launcher fixes that mount name; a primary's mount is named by whoever wrote its
|
||
`.mcp.json`, so it varies — and a member spawned before CB-632 still says `mcp__bridge__*`); `ANTHROPIC_BASE_URL` set ⇒ **spawned member** (Claude-model members run
|
||
on a clean env, so its *absence* proves nothing). None of these separate a worker from an architect —
|
||
only `fleet_whoami` does. **Still unsure ⇒ act as a worker**, the most restricted member role. The
|
||
two mistakes are not symmetric: a primary acting as a worker is refused by the authorization gate —
|
||
loud and self-correcting — while a member acting as the primary ends its turn with no `fleet_reply`,
|
||
and the sender silently receives nothing. Fail toward the recoverable error.
|
||
|
||
### Invariants — both roles, no exceptions
|
||
|
||
1. **Never set, export, or forward `ANTHROPIC_BASE_URL`** (or `ANTHROPIC_AUTH_TOKEN`). The primary
|
||
stays on subscription; only the bridge puts a member off it, at spawn. Mounting the bridge must
|
||
never move a session across that boundary.
|
||
2. **The bridge is the only channel.** Text you print in your terminal reaches nobody — the other
|
||
side cannot see your screen. An answer that isn't in a `fleet_*` call is silently discarded.
|
||
3. **Identity comes from the connection, never an argument.** Workers never pass a target; you
|
||
cannot act as another session. Spawn/stop/drain are lead-only; **send is lead or architect**;
|
||
reply/ask are only-as-itself — any peer may answer for its own pane, and for no other. A call
|
||
outside your role is refused, not queued.
|
||
4. **Delivery is status-gated: one message per turn.** Don't busy-poll a peer's terminal and don't
|
||
re-send because a call looks slow — the bridge delivers when the peer is `idle`, `blocked` or
|
||
`done`. A spawned member must **also** have mounted the bridge MCP: until it has, it is not
|
||
deliverable, and a send waits on that gate for ~60s and then fails without ever reaching its pane.
|
||
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.
|
||
|
||
### Primary (lead) — run this on every task, in order
|
||
|
||
**Delegate by default — that is the job.** With the bridge mounted you are an orchestrator on a
|
||
metered subscription, and workers are cheap, parallel, and disposable. The default answer to "who
|
||
does this?" is **a worker**, not you. Reach for `fleet_send` before you reach for `Edit`. The steps
|
||
below are the procedure — run them in order, every task, not only the big ones.
|
||
|
||
0. **Know your role** — `fleet_whoami`, once per session, before anything else.
|
||
1. **Split.** Write the unit list. Every unit carries: scope · the files or PR in question ·
|
||
acceptance criteria · exactly what to report back. A unit with no acceptance criteria is not
|
||
ready to delegate — refine it or keep it.
|
||
2. **Gate each unit** on one question: **"can I write a brief good enough for a worker to
|
||
succeed?"** — *not* "could I do this faster myself?" (usually you could; doing it yourself costs
|
||
your context and your subscription, while a wasted worker turn costs a worker turn). Yes ⇒
|
||
delegate. The keep-list is closed: the conversation with the user, decomposition and planning,
|
||
the final judgment call, verification, merges, and anything that depends on context only you
|
||
hold. Nothing else is yours by default.
|
||
3. **Spawn every delegated unit first** — `fleet_spawn{profile, worktree:true, ticket}`, one per
|
||
unit, *before* sending any. Pass `profile` explicitly: profiles differ in model, cost and
|
||
LIVENESS, not in tier, so the default is rarely what you want. The default is whatever the
|
||
daemon reports, and on a host where it sits on an exhausted or withdrawn credential every
|
||
unqualified spawn fails — sometimes loudly, sometimes as a member that spawns fine and then
|
||
produces nothing. `fleet_profiles` reports the default; check it once per session.
|
||
4. **Then send them all** — `fleet_send{sessionId, content, wait:false}`. Line 1 of every brief is
|
||
`Load the <name> skill.` naming the worker's playbook; those skills are opt-in and that line is
|
||
what makes them reliable. Where the project ships no such skill, spell the procedure out in the
|
||
brief instead. The brief is self-contained — the worker sees your message and the repo, nothing
|
||
of your context, your plan, or your screen.
|
||
5. **Collect** — `fleet_poll{ticket}` → `fleet_ack{target, msgId}`. Answer a worker's `fleet_ask`
|
||
with `fleet_send{turnId, content}` — **not** `sessionId`. A worker gone quiet is diagnosed with
|
||
`fleet_status`, never by reading its terminal; it also reports an open question and the `turnId`
|
||
that answers it. **A worker's ask waits ~55 seconds, and no nudge makes that longer** — so never
|
||
brief a worker to "ask me". Decide before you delegate, or give it an explicit default.
|
||
**A correction cannot reach a busy member.** A `fleet_send` to a working member is *accepted* and
|
||
returns a ticket, and is then never delivered — measured here three times in one session, and the
|
||
member was released still executing a brief that had been retracted twice. The receipt is true and
|
||
it is a fact about the *mailbox*; what you needed was a fact about the *pane*. **A push delivery
|
||
needs the recipient free at send time; a pull channel needs only that they look before acting.** So
|
||
put every correction on the **ticket**, which they can read whenever they look, and send as the
|
||
notification. That obliges you, not them: **all corrections go to the ticket, and the brief is
|
||
write-once.** The member cannot check which source is newer — it just always prefers the ticket —
|
||
so the day you revise a brief in place instead of commenting, it obeys your rule and does the wrong
|
||
thing. A *first* brief for a unit not yet running is not a correction, and may be the whole spec.
|
||
6. **Verify yourself.** Re-run the build and the checks. A worker cannot run your IDE tooling, any
|
||
forge MCP server it appears to have holds a blocked credential and fails every call, and a piped
|
||
command (`… | tail`) hides failures behind a zero exit — never promote a worker's "clean" to a
|
||
fact. Its injected repo-scoped `GITEA_TOKEN` is a different credential and does work, so a worker
|
||
reporting that it opened its own PR is reporting something it really can do.
|
||
7. **Review — fan out.** Spawn reviewers against the diff, one per dimension or per file, with
|
||
`wait:false`. Never the implementer of the scope it reviews, and brief them from the diff — not
|
||
from the implementer's rationale, which carries its own blind spot. Dispatch each PR's reviewers
|
||
as it lands; don't wait for the last implementer. Under ~50 changed lines, skip the fan-out and
|
||
read it yourself.
|
||
8. **Adjudicate, merge, tear down — yours alone.** Read the diff yourself: fully if it is small,
|
||
targeted at the reported findings and the risky paths if it is large. Reviewer findings direct
|
||
your attention; they never substitute for it. Then merge, then `fleet_stop{paneId}`.
|
||
**If the forge refuses you the merge** — a protected branch, a token without the grant — the
|
||
adjudication is still yours. Read the diff, decide, and hand the operator a merge-ready queue
|
||
with the refusal quoted. Never report a PR as merged, and never call one "ready to merge"
|
||
without having read the diff yourself. A refusal is exactly when that shortcut is tempting,
|
||
because no action is left that forces you to look, and taking it turns this step into
|
||
forwarding a reviewer's verdict — which is delegating the merge by proxy, two lines above.
|
||
**Test a refusal; do not read it off a permissions field.** A protected branch holds its merge
|
||
rights separately from the repository permissions, so that field can say yes while the merge is
|
||
refused, and still say no after a grant makes it work. Probe instead, with a request that cannot
|
||
succeed on its merits, so a rejection can only mean the refusal. Treat a transport failure as a
|
||
third answer that proves nothing: a timeout, a DNS error or a bad URL is not a refusal, and
|
||
counting it as one makes you sure of something you never measured.
|
||
|
||
**Steps 3 and 4 are separate on purpose** — spawning and sending in one loop is how parallel work
|
||
silently becomes serial, and it is the most common way this layer is wasted. For the same reason,
|
||
prefer `wait:false` + `fleet_poll` for anything non-trivial: a blocking `fleet_send` is capped by
|
||
*your own* MCP client call timeout (~60s), well below the task's real runtime.
|
||
|
||
**Delegating does not delegate responsibility.** Workers open PRs; you are the gate. Never delegate
|
||
the merge — and merging on a reviewer's word is delegating it by proxy.
|
||
|
||
**When a decision blocks you, consult architects — not the operator.** Spawn one or more architect
|
||
members, give them the question and the evidence you have, and act on what they agree. They are
|
||
authorized to settle it, not only to advise. Architects first form independent positions, then
|
||
compare them. If they still disagree after that comparison, they return both positions and their
|
||
checked evidence; the lead decides. Go to the operator only for an action the fleet has no
|
||
authority to take, such as spending money, granting access, or making a promise to someone else.
|
||
**Then write the decision on the ticket.** Taking the operator out of the loop also removes the
|
||
signal they used to get, because that signal was the block itself — work stopped, so they found
|
||
out. A ticket comment replaces it, and it reaches them whether or not they are at a terminal when
|
||
you decide.
|
||
|
||
| Intent | Tool |
|
||
|---|---|
|
||
| Confirm your own role | `fleet_whoami` |
|
||
| See backends available | `fleet_profiles` |
|
||
| Start a member | `fleet_spawn{role?, profile?, cwd?, worktree?, ticket?, sessionName?, resumeSessionId?}` → `sessionId` + `paneId` |
|
||
| See the fleet | `fleet_list` → `leads` (your peers) + `members` (each carries `agentSessionId` when its backend knows one) + `loopHealth` (`RUNNING`, `STALLED`, or `STOPPED` for `statusPoller` and `sessionReaper`) · one peer's state: `fleet_status{sessionId}` |
|
||
| Delegate (blocking) | `fleet_send{sessionId, content}` |
|
||
| Delegate (long task) | `fleet_send{sessionId, content, wait:false}` → ticket → `fleet_poll{ticket}` |
|
||
| Answer a member's `fleet_ask` | `fleet_send{turnId, content}` — **not** `sessionId` |
|
||
| Message a **peer lead** on this host | `fleet_send{sessionId: <their terminal>, content}` — `fleet_list` → `leads` reports it. Coordination only, **never** a task |
|
||
| Message a **peer lead** on another daemon or host | `fleet_send{coordId: <their coord-id>, content}` — needs a `coordinator:` block; your own coord-id is in `fleet_list`. Coordination only, **never** a task |
|
||
| Answer a peer lead that messaged you | `fleet_send{coordId}` — or `{sessionId}` if they are on this host. **Not** `fleet_reply`: it has no peer route and the publish is refused |
|
||
| Read your own held lead-to-lead mail (no ack) | `fleet_poll{coordId: <your own coord-id, from fleet_list's coordinator.selfId>}` — primary-only; never acks, so `fleet_list`'s `held[]` still shows it after. `fleet_list`'s `held[]` gives only a truncated preview — this is the only way to read the full body |
|
||
| Collect a held reply | `fleet_poll{target}` · then `fleet_ack{target, msgId}` |
|
||
| Tear down a member | `fleet_stop{paneId}` |
|
||
| Replace your OWN lead session when its context is full | `fleet_handover{action:"open", reason?}` → write the handover file it names → `fleet_handover{action:"confirm", token, operatorConfirmed}`. Primary-only. **In that order**: the file must be modified *after* `open`, or `confirm` refuses it as stale. There is no terminal parameter — the pane is always your own, so you can never roll another lead. `{action:"cancel", token}` drops a pending request |
|
||
|
||
### Lead ↔ lead — coordinate, never delegate
|
||
|
||
`fleet_list` returns `leads` alongside `members`; your own row carries `self: true`. Every other row
|
||
is a peer — an orchestrator with its own context, its own members, and its own judgment. An empty
|
||
`members` array means no members are spawned; it says nothing about peers.
|
||
|
||
**A lead never assigns a task to another lead.** Work goes to members — only ever downward, never
|
||
sideways. Sending a peer a brief with acceptance criteria is a category error: a brief is a member's
|
||
artefact, and a peer is not yours to task. If a unit needs doing and it falls in your area, spawn a
|
||
member and delegate it yourself; if it falls in the peer's area, say so and let the peer assign it.
|
||
The traffic between leads is coordination and nothing else:
|
||
|
||
1. **Divide the map, not the work.** Agree who owns which area, then each of you assigns inside your
|
||
own. Split by **context ownership** — whoever already holds the context owns that area — and say
|
||
who takes what, in one message, before either of you starts. Two leads silently working the same
|
||
unit is the failure mode here, and neither notices until the merge.
|
||
2. **Share findings, hazards, and corrections.** What you have already discovered, what broke, what
|
||
the next person will trip on. This is the traffic that actually pays for the channel: it costs one
|
||
message and saves a peer a rediscovery.
|
||
3. **Verify a peer exactly as you verify yourself.** Peer status buys nothing: check the claim
|
||
against the code, and re-run the build. A peer's correction gets the same treatment — right or
|
||
wrong on the evidence, not on who said it. Neither of you merges the other's work unreviewed.
|
||
**N observations are N data points only if they differ in the axis you are trusting.** This cuts
|
||
both ways. N *failures* blamed on one cause are one data point when the cases share what you are
|
||
not varying. N *agreeing measurements* are also one data point when they share an instrument —
|
||
two hosts, two operators and the same formula is one formula, not two confirmations.
|
||
4. **Ask a peer to read your project addendum.** Your addendum is instruction surface: every future
|
||
session on your host obeys it, and a wrong one is obeyed just as faithfully as a right one. The
|
||
author is the worst reader of their own qualifier placement — measured here, one addendum carried
|
||
two defects and a non-author found both. If you have no peer, at least re-read it asking "which
|
||
sentence goes false first, and would a reader reach the caveat before acting?"
|
||
|
||
Being messaged by a peer does not make you its worker: answer the way you would open —
|
||
`fleet_send{coordId}` for another daemon, `fleet_send{sessionId}` on this host — and push back on
|
||
the substance if it is wrong. `fleet_reply` resolves a member's blocked `fleet_send`; a peer's
|
||
coord-id message is durable and non-blocking, so there is nothing for it to resolve. A peer that
|
||
simply complies has thrown away the reason there are two of you.
|
||
|
||
### Member (worker or architect) — the turn contract
|
||
|
||
1. **Load the playbook skill the lead named** before doing anything else.
|
||
2. **Do the assigned scope only.** Note anything you spot outside it in one line; don't go hunt it.
|
||
3. **`fleet_ask{question}`** when a decision is genuinely the lead's (ambiguous requirement, two
|
||
defensible fixes, "bug or intended?"). It blocks and you resume the *same* turn with the answer.
|
||
Don't ask what you could decide yourself.
|
||
4. **Re-read the ticket before you act on anything you were told earlier**, and again before you
|
||
commit. A message reaches you only while you are free to receive it; the ticket is there whenever
|
||
you look. **If a ticket comment contradicts your brief, the ticket comment is newer and it wins.**
|
||
5. **End the turn with exactly one `fleet_reply{content}`**, carrying your complete answer. This is
|
||
the whole handoff. No `fleet_reply` ⇒ the sender gets nothing and the exchange stalls.
|
||
Do **not** lean on the completion fallback to carry your answer for you: when you end a turn
|
||
without replying, the bridge scrapes your pane, and it can return only the last 4000 characters.
|
||
A clipped scrape is marked as partial, but the missing text is gone — your report reaches the
|
||
lead with its end cut off.
|
||
6. **Report honestly.** State only what you actually ran and its real output, including failures,
|
||
and never claim the result of a check you had no way to run. **Measure your own tools; do not
|
||
assume them.** What you mount depends on your backend: an opencode member gets the bridge and
|
||
nothing else, while a Claude Code member also inherits the operator's user-scope MCP servers,
|
||
which the bridge never chose for you. Two rules follow. The primary's IDE tooling is still not
|
||
yours, whatever you see. And **a mounted tool is not a working tool** — the forge MCP server you
|
||
may find there holds a deliberately blocked credential and fails every call, by design. That is
|
||
not your only forge route, and the two must not be confused: the repo-scoped `GITEA_TOKEN` the
|
||
daemon injects into your environment does work, and using it to open your own PR is part of the
|
||
job. A blocked MCP tool is never a reason to skip that step.
|
||
7. **Never merge.** Stage files explicitly — never `git add -A` — and leave alone anything the
|
||
project marks as not-yours-to-commit.
|
||
|
||
### Where each rule lives (don't duplicate — extend the right layer)
|
||
|
||
| Layer | Scope | Reaches |
|
||
|---|---|---|
|
||
| the launcher's reply charter | the one rule that must survive with no repo: *end every turn with `fleet_reply`* | every spawned member, at launch, every peer kind — never a lead |
|
||
| **this section** | protocol + orchestration policy | primary **and** every member that reads the repo — tracked in git, so worktrees inherit it |
|
||
| role agent definition files | role contract and per-job procedure | a member whose launcher binds its role to the matching file in its worktree |
|
||
| role playbook skills | per-job procedure (commit/PR recipe, finding format) | a member told to load one |
|
||
| the bridge's own docs | design detail, flows, error model | on demand |
|
||
|
||
A rule belongs in **exactly one** layer — the outermost one that must obey it. A member without a
|
||
repo checkout still gets the launcher's reply charter, which is why that one rule stays there.
|
||
Peers that don't read `CLAUDE.md` (non-Claude adapters) get the charter only, so any rule *they*
|
||
must obey belongs in the charter, not here.
|
||
|
||
## Project addendum — claude-bridge (not part of the canonical block)
|
||
|
||
- **This repo is the bridge.** The daemon is `fleetd`, its MCP mount is `http://127.0.0.1:8765/mcp`,
|
||
and the code behind the rules above is `mcp/FleetMcp` (tools), `auth/Authz` (the role table),
|
||
`mcp/ConnectionIdentity` (connection→role), and `worker/*Launcher` (`REPLY_CHARTER`).
|
||
- **Herdr socket tests (measured 2026-09-10).** In this repo, herdr is a subject under test. A
|
||
worker assigned to herdr code, and the lead, may let a test open the herdr socket directly in a
|
||
throwaway workspace that the test tears down. This only covers
|
||
`fleetd/src/test/java/dev/ltms/fleet/herdr/AgentControlContractTest.java`,
|
||
`fleetd/src/test/java/dev/ltms/fleet/herdr/HerdrContractTest.java`,
|
||
`fleetd/src/test/java/dev/ltms/fleet/herdr/PaneLocatorContractTest.java`, and
|
||
`fleetd/src/test/java/dev/ltms/fleet/herdr/WorkspacePlacementContractTest.java`. It is not a
|
||
general licence. Using the herdr CLI or socket to move a real fleet session, pane, or peer stays
|
||
banned. That is the control plane that invariant 5 protects. Re-measure with
|
||
`grep -rl 'UnixSocketHerdrClient.connect()' fleetd/src/test/java --include='*.java'`. A non-empty
|
||
result means tests still open the socket and this note still applies. An empty result means nobody
|
||
does this any more; delete this section. Canonical invariant 5 restatement is tracked in #458 and
|
||
is not part of this change.
|
||
- **`fleet_profiles`/`fleet_list` report two separate outage states, and they are not the same
|
||
thing.** *Quarantined* (CB-578) means the backend told us it is out of capacity — a long,
|
||
1800s-default cooldown. *Cooling off* (fleetd #201/#227) means a profile's credential threw two
|
||
distinct backend errors (a non-exhaustion failure such as an HTTP 5xx) within 60 seconds — a
|
||
short, fixed 60s cooldown, not configurable per profile. Each check runs independently, so a
|
||
profile can show both at once. In the JSON: a cooling profile carries `credentialId` and
|
||
`coolingOffForSeconds`; a quarantined profile carries `quarantinedForSeconds`; a profile hit by
|
||
both carries all three fields, and either state alone already sets that profile's `free` to `0`.
|
||
A `fleet_spawn` naming a cooling-off profile is refused before it ever reaches the backend
|
||
adapter, with a message naming the credential and the remaining seconds ("cooling off after
|
||
repeated backend errors") — distinct wording from a quarantine refusal, so don't conflate the
|
||
two when reading a spawn failure.
|
||
- **Skills available to delegate:** `implementer` (worktree → commit → push → own PR),
|
||
`reviewer` (one diff → one structured finding) and `hunter` (sweep a package → several ranked
|
||
findings, change nothing). Name exactly one in every delegation. **`reviewer` and `hunter` are
|
||
not interchangeable** — `reviewer` caps the answer at one finding in about 90 words, so naming
|
||
it for a multi-finding sweep hands the worker two contradictory output contracts. That has
|
||
already cost three workers' turns: each wrote a good report to its terminal and ended the turn
|
||
with no `fleet_reply`, and the scrape returned the tail of the brief instead.
|
||
Spawn `implementer` with role `dev`, `reviewer` with role `reviewer`, and `hunter` with role
|
||
`hunter`.
|
||
- **Primary-side skills** (not delegation playbooks — a worker cannot use them):
|
||
`port-to-opencode` (make an OpenCode session a participant in this workspace),
|
||
`fleets-status` (report every fleet that shares one LavinMQ instance),
|
||
`redeploy-fleetd` (rebuild and restart the live daemon after a merge) and
|
||
`handover` (write the file a fresh lead session inherits when the outgoing one hands off,
|
||
fleetd #480).
|
||
- **This repo is also a Claude Code marketplace, and ships a plugin.** `.claude-plugin/marketplace.json`
|
||
points at `plugin/`, which carries the MCP mount and the `setup` skill
|
||
(`/claude-bridge:setup` — make any project bridge-ready). It was added in CB-527 and then went
|
||
unmentioned by every instruction file, so it drifted and a later session planned it from scratch
|
||
(#362). **Read `plugin/` before designing anything about onboarding a project.** Two limits are
|
||
structural, not bugs: a plugin cannot carry the role agent files, because
|
||
`ClaudeCodeLauncher.java:371` requires `<cwd>/.claude/agents/<role>.md` in the member's own
|
||
worktree; and a plugin cannot deliver anything to members at all, because
|
||
`ClaudeCodeLauncher.java:285` exports `CLAUDE_CONFIG_DIR` and every Claude profile here sets it,
|
||
so a member never reads the operator's plugin store. **The plugin is the lead-side surface;
|
||
member-facing assets travel in the worktree.**
|
||
- **Never commit** `.mcp.json` (the primary's local copy, flagged `--skip-worktree`) or `wiki/`
|
||
(a submodule with its own remote).
|
||
- **A provisioned worktree neutralizes `.mcp.json`, `opencode.json` and `.autoenv`** — the repo's
|
||
committed copies would otherwise mount the primary's IDE and forge servers (fleetd #134). The
|
||
worktree's copy of each is a stub, **not** the repo's real file, so a worker that reads one and
|
||
reports what it found is reporting on the stub. The daemon logs a per-spawn summary, but the
|
||
worker cannot see that log. From inside its own worktree a worker — or a lead debugging one —
|
||
reads the list with `git config --worktree --get-all fleet.neutralizedConfig`, and the
|
||
consequence with `git config --worktree --get fleet.neutralizedConfigNote`. Never brief a worker
|
||
to edit one of these files: the edit cannot be committed, and it will not tell you so.
|
||
- **Flows and the error model** — rendezvous, `fleet_ask`, detached delivery, the turn-done
|
||
fallback and status gating — are diagrammed in `docs/MCP-Contract.md`. That page is now flows
|
||
only: its pre-build tool catalogue, parameter tables and REST paths were deleted rather than
|
||
corrected, because a hand-maintained second copy of the tool surface is what drifted for a month
|
||
while this line pointed every session at it (CB-609 / #114). **The live MCP schema is the tool
|
||
reference**, with the intent→tool table above as the short form. `McpContractDocTest` fails if
|
||
that page names a `fleet_*` tool the server does not register. The flows are kept out of this
|
||
file because this file loads into every session's context.
|
||
|
||
### Redeploying the daemon — the lead may do this (primary only)
|
||
|
||
**A merge is not a deployment.** The running `fleetd` holds the jar it was started with, so a
|
||
feature merged to `main` does nothing until the daemon is rebuilt and restarted. The lead **may and
|
||
should** redeploy rather than hand the job back to the operator. **Workers must never do this** — a
|
||
worker has no business restarting the daemon it is talking through, and stopping it kills the
|
||
worker's own channel mid-turn.
|
||
|
||
**Load the `redeploy-fleetd` skill before you redeploy.** It holds `scripts/redeploy-fleetd.sh`
|
||
and its flags, the drain step, the operator's permission grant, and the five checks that have each
|
||
gone wrong here before. Do not hand-roll the steps from memory.
|
||
|
||
### The prompt is part of the product — update it with the code (mandatory)
|
||
|
||
This repo *is* the bridge, so the canonical block above is not documentation about someone else's
|
||
system: it is the instruction surface this codebase ships. **Every change here must end by asking
|
||
whether the block still tells the truth.** A code change that silently invalidates it is an
|
||
incomplete change — the agents reading it have no other source.
|
||
|
||
Before you call any work done, check the row that matches what you touched:
|
||
|
||
| You changed… | Re-read and update… |
|
||
|---|---|
|
||
| a `fleet_*` tool — added, removed, renamed, or its params/semantics | the primary's intent→tool table; any rule that names that tool |
|
||
| `Authz` / the role table | invariant 3, and the primary-only vs worker-only claims |
|
||
| `ConnectionIdentity` / how a caller is resolved | the `fleet_whoami` paragraph and the fallback ladder |
|
||
| `REPLY_CHARTER`, or a launcher's mount/flags | the fallback ladder (`mcp__fleet__*`), and the layering table's top row |
|
||
| the injector / status gating | invariant 4 |
|
||
| worktree provisioning or the parity overlay | the "both roles read this file" premise — it rests on the worker's worktree being a checkout of this repo |
|
||
| `.claude/skills/**` | the addendum's skill list, and the "name the playbook" rule |
|
||
| a new peer kind (non-Claude adapter) | what that peer can read — anything it must obey belongs in its charter, not in the block |
|
||
| **anything an operator can use, configure, or observe** — an MCP tool, a `fleetd.yaml` knob, an endpoint, a visible behaviour | **[Features](wiki/11-Features.md)** — one entry: what it does · the knob that turns it on · **why it exists** · the gotcha |
|
||
|
||
That last row is not bookkeeping. Chapters 1–10 answer *how is this built* and *why this way*;
|
||
none of them has a home for *what can it do and how do I turn it on*, so for twenty tickets a
|
||
shipped capability landed nowhere and the Roadmap went on claiming the stage was finished. The
|
||
*why* line is the one that matters — without it a decision gets re-litigated from scratch a month
|
||
later. Internal contract changes go to `wiki/9-Implementation.md` instead; test and coverage work
|
||
is a Roadmap line. A change that touches none of the three earns no entry, and that is a normal
|
||
outcome rather than an omission.
|
||
|
||
Then **propagate**: the block in this file and the template in the wiki
|
||
([Use Cases](https://git.ltms.dev/fleet/fleetd/wiki/7-Use-Cases) → *The portable `CLAUDE.md`
|
||
block*) must stay byte-identical, and other projects carrying the block need the same edit. Verify
|
||
rather than trust:
|
||
|
||
```bash
|
||
python3 - <<'PY'
|
||
import pathlib
|
||
c = pathlib.Path("CLAUDE.md").read_text()
|
||
w = pathlib.Path("wiki/7-Use-Cases.md").read_text()
|
||
S, E = "## Bridge communication (enforced", "## Project addendum — claude-bridge"
|
||
block = c[c.index(S):c.index(E)].rstrip() + "\n"
|
||
i = w.index("```markdown\n") + len("```markdown\n")
|
||
print("in sync:", w[i:w.index("\n```\n", i) + 1] == block)
|
||
PY
|
||
```
|
||
|
||
**Only the lead can run that check (measured 2026-09-10).** A member's provisioned worktree has
|
||
`wiki/` uninitialized, so the script dies with `FileNotFoundError: wiki/7-Use-Cases.md`. Measured
|
||
in three worker worktrees: `git submodule status` printed a leading `-` and `wiki/` held 0
|
||
entries; the primary's own clone printed a leading `+` and the file was there. So never make this
|
||
check a member's acceptance criterion — it is unsatisfiable for them, and a brief that asks for it
|
||
is asking a worker to invent a pass. A member told to check it must say it could not run it, and
|
||
must never report it as passed. The lead runs it in the main clone before merging. Re-measure with
|
||
`git submodule status` in a member's worktree: a leading `-` means this still applies; once it
|
||
prints a commit with no `-`, delete this paragraph.
|
||
|
||
## IDE MCP tools & validation workflow (enforced)
|
||
|
||
> **Primary only.** Workers have no IDE MCP mount — if you are a worker, skip this section and
|
||
> report the build/test output you actually ran (see §Bridge communication → Worker).
|
||
|
||
Two IDE MCP servers are connected: **intellij-index** (semantic code intelligence) and
|
||
**jetbrains** (file problems, reformat, debugger). IntelliJ has multiple projects open; our
|
||
module is **`fleetd`**. Always pass these to IDE MCP tools:
|
||
|
||
- `project_path` = `/Users/dai.ha/LTMS/claude-bridge/fleetd`
|
||
- IDE paths are relative to `fleetd/` (e.g. `src/main/java/dev/ltms/fleet/...`)
|
||
|
||
### After editing any file — mandatory
|
||
|
||
1. **`ide_sync_files{paths}`** — the built-in Edit/Write tools write to disk; the IDE index is
|
||
stale until synced, or IDE nav/refactor/diagnostics give wrong results.
|
||
2. **`ide_diagnostics{file}`** (or `jetbrains get_file_problems`) — clear **all** errors *and*
|
||
warnings. IDE inspections catch what a build won't (unused params/fields, redundant
|
||
modifiers, resource leaks, "always same arg", …). These diagnostics are **per-file**.
|
||
3. **`mvn clean install`** (Bash) — required for **overall project health** (clean build + full
|
||
test run). A per-file-clean file can still break the build or another module. This is the
|
||
whole-project gate before declaring work done or committing.
|
||
|
||
**Whenever dependencies change (or a `pom.xml` edit), validate CVEs with
|
||
`jetbrains get_file_problems{filePath: "fleetd/pom.xml"}`** — its Mend.io check reflects the
|
||
dependencies on disk. (Note: `ide_diagnostics` / intellij-index does NOT re-resolve dependencies
|
||
after a pom edit without a full Maven reimport, so it reports stale CVE results — don't trust it
|
||
for this.) Treat a CVE warning like any other: bump to a patched version and confirm
|
||
`mvn clean install` still passes. If the latest available version is still flagged (EOL line,
|
||
"insufficient information", or config-file-only advisories), document it as accepted in the pom
|
||
rather than chasing a fix that doesn't exist.
|
||
|
||
### Use IDE MCP tools for navigation, refactoring, and diagnostics only
|
||
|
||
- **Navigate (prefer over Grep/Read for symbols):** `ide_find_definition`, `ide_find_class`,
|
||
`ide_find_file`, `ide_find_references`, `ide_find_implementations`, `ide_find_super_methods`,
|
||
`ide_call_hierarchy`, `ide_type_hierarchy`, `ide_search_text` (regex: `jetbrains
|
||
search_in_files_by_regex`).
|
||
- **Refactor (prefer over multi-file Edit / rm / mv):** `ide_refactor_rename` (position-based,
|
||
updates all refs/overrides/tests), `ide_refactor_safe_delete`, `ide_move_file`,
|
||
`jetbrains reformat_file`.
|
||
- **Diagnose:** `ide_diagnostics` / `jetbrains get_file_problems`.
|
||
|
||
### Do NOT route build/test/one-offs through the IDE
|
||
|
||
Keep `mvn` (compile/test/package/`clean install`) and **all** one-off shell commands on **Bash**.
|
||
Do not use `jetbrains build_project`, `execute_run_configuration`, or `execute_terminal_command`
|
||
to replace them.
|
||
|
||
### Java 25 notes
|
||
|
||
Prefer the unnamed lambda parameter `_` for required-but-unused params; a non-public
|
||
`static void main(String[])` is valid (JEP 512) and boots via `java -jar`.
|