457458437f
CB-527 shipped a Claude Code plugin and a marketplace in this repo. Nothing in
CLAUDE.md or docs/ ever named it, so a later session planned the same feature
from scratch. The wiki Features entry existed and was correct, but wiki/ is a
submodule whose pointer is never advanced, so no session reads it.
Visibility:
- CLAUDE.md addendum now names plugin/ and both structural limits, so every
session sees it. This is the change that stops the rebuild happening again.
- wiki/11-Features.md records the rename and why the entry alone was not enough.
Drift (each measured against the code, not assumed):
- mount name fleetd -> fleet, matching PeerLauncher.MCP_MOUNT_NAME. The old name
gave a lead with both a project .mcp.json and the plugin two mounts of one
daemon and a duplicated fleet_* tool set.
- url is now ${FLEETD_MCP_URL} instead of a hardcoded address, so one plugin can
serve hosts running the daemon on different ports. Plain ${VAR}, the form
kb-alms proves works here; ${VAR:-default} is untested and not used.
- plugin claude-bridge -> fleet, marketplace claude-bridge -> fleetd, version
0.2.0. Breaking for a 0.1.0 install: mcp__fleetd__* becomes mcp__fleet__*.
- README install path ltms/claude-bridge -> the fleet/fleetd remote.
- the setup skill's §5 told operators to pin primary.terminal:. CB-579 replaced
that with fleet.leaders.*.tab. Replaced, with the duplicate-tab warning (#359).
Scope: the plugin is lead-side only, and cannot be otherwise. The launcher adds
--agent only when <worktree>/.claude/agents/<role>.md exists in the member's own
tree (ClaudeCodeLauncher.java:371,391), and a member's CLAUDE_CONFIG_DIR points
at its profile's config dir (ClaudeCodeLauncher.java:285), so a member never
reads the operator's plugin store. On this Mac all four Claude profiles set
configDir, and the four ccs instances hold four separate copies of the plugin
store -- same md5, different inodes. Seeding member skills through the worktree
is #362 scope item 3, implemented separately.
Note for anyone verifying a plugin: `claude plugin validate` does NOT read
.mcp.json. Replacing it with `{ this is not json at all` still passes, exit 0.
Refs #362, #359
400 lines
28 KiB
Markdown
400 lines
28 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.
|
||
|
||
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 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.
|
||
|
||
### 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 and cost, not in
|
||
tier, so the default is rarely what you want.
|
||
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.
|
||
6. **Verify yourself.** Re-run the build and the checks. A worker cannot run your IDE tooling, any
|
||
forge tools it appears to have hold a blocked credential and fail, and a piped command
|
||
(`… | tail`) hides failures behind a zero exit — never promote a worker's "clean" to a fact.
|
||
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}`.
|
||
|
||
**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.
|
||
|
||
| 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) · 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_reply{content}` — the one case a lead replies |
|
||
| Collect a held reply | `fleet_poll{target}` · then `fleet_ack{target, msgId}` |
|
||
| Tear down a member | `fleet_stop{paneId}` |
|
||
|
||
### 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.
|
||
|
||
Being messaged by a peer does not make you its worker: answer with `fleet_reply`, and push back on
|
||
the substance if it is wrong. 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. **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.
|
||
5. **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 server you may
|
||
find there holds a deliberately blocked credential and fails every call, by design.
|
||
6. **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`).
|
||
- **`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.
|
||
- **Primary-side skills** (not delegation playbooks — a worker cannot use them):
|
||
`port-to-opencode` (make an OpenCode session a participant in this workspace) and
|
||
`fleets-status` (report every fleet that shares one LavinMQ instance).
|
||
- **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. Saying "shipped"
|
||
about code the live daemon has never loaded is a false report. 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.
|
||
|
||
**Use the script — do not hand-roll the steps.**
|
||
|
||
```bash
|
||
scripts/redeploy-fleetd.sh --check # report state, change nothing
|
||
scripts/redeploy-fleetd.sh # build, confirm drain, restart, verify
|
||
scripts/redeploy-fleetd.sh --yes # skip the drain prompt (fleet already checked)
|
||
```
|
||
|
||
It builds before it stops anything, so a failed build never leaves the fleet down; it waits for the
|
||
old process to exit rather than assuming; it polls `/healthz`; and it anchors its log checks to a
|
||
line marker taken before the restart, so old errors cannot be misread as new ones. Run `--check`
|
||
first — it is read-only and reports whether the forge token resolves, which nothing else tells you.
|
||
|
||
The script encodes the five things below, each of which has gone wrong here before. Read them anyway:
|
||
if the script is unavailable or a step fails, this is what it was protecting you from.
|
||
|
||
1. **Login shell, or workers silently lose their forge token.** The daemon inherits
|
||
`WORKER_GITEA_TOKEN` from the shell that starts it, and that comes from
|
||
`${SHARED_ENV}/tools/secrets.sh`. Start it from a non-login shell and the variable is empty, the
|
||
daemon starts fine, and the failure appears much later as workers that cannot open a PR. Nothing
|
||
logs this at startup — the script's `--check` is the only thing that reports it, and it checks
|
||
whether the name resolves without ever printing the value.
|
||
2. **Drain live members first.** `fleet_list`, then `fleet_stop` each member, and collect anything
|
||
you still want with `fleet_poll` before you kill anything. A restart drops in-flight tickets and
|
||
rendezvous, and a member's report is not recoverable once its ticket is gone.
|
||
3. **A restart is the only way deferred config keys take effect.** That is usually the reason to do
|
||
it. The startup log names which keys it accepted and which it deferred — read those lines rather
|
||
than assuming.
|
||
4. **Re-check identity afterwards.** Call `fleet_whoami` and confirm it still answers `primary`. The
|
||
lead is found by its tab label (`fleet.leaders.*.tab`), and a lead whose tab no longer matches is
|
||
demoted to worker, which refuses every orchestration call.
|
||
5. **Prove the new jar is the one running.** Confirm a *fresh* `fleetd listening` line at the end of
|
||
`fleetd/fleetd.out`, dated after the restart. An old daemon that never died looks identical from
|
||
the outside.
|
||
|
||
**Permission.** A `CLAUDE.md` rule grants intent, not tool permission — the command classifier
|
||
refuses a bare `kill` on the daemon whatever this file says. The script is the seam that fixes that:
|
||
it is one auditable command, so the operator allow-lists it once instead of approving a stop and a
|
||
start every time. The rule lives in the operator's Claude Code settings:
|
||
|
||
```json
|
||
{ "permissions": { "allow": ["Bash(scripts/redeploy-fleetd.sh:*)"] } }
|
||
```
|
||
|
||
Granted by the operator on 2026-08-15. If a call is still refused, do **not** route around it by
|
||
running the stop and start as separate commands — that is exactly the approval the script replaced.
|
||
Say what you were going to run and why, and let the operator decide.
|
||
|
||
### 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
|
||
```
|
||
|
||
## 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`.
|