From 457458437fe8c15f5aedc8408f6ccfeda1f69c3f Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 5 Sep 2026 12:42:20 +0700 Subject: [PATCH] #362: make the plugin visible, and fix the drift that made it unusable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /.claude/agents/.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 --- .claude-plugin/marketplace.json | 8 +- CLAUDE.md | 11 + plans/fleet-plugin/plan.md | 337 ++++++++++++++++++++++++++++++ plugin/.claude-plugin/plugin.json | 6 +- plugin/.mcp.json | 4 +- plugin/README.md | 40 +++- plugin/skills/setup/SKILL.md | 61 ++++-- 7 files changed, 433 insertions(+), 34 deletions(-) create mode 100644 plans/fleet-plugin/plan.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 355f81e..e1fc0b4 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,15 +1,15 @@ { - "name": "claude-bridge", + "name": "fleetd", "description": "Tooling for orchestrating a fleet of delegated coding agents through the fleetd MCP gateway.", "owner": { "name": "LTMS" }, "plugins": [ { - "name": "claude-bridge", + "name": "fleet", "source": "./plugin", - "description": "Make a project bridge-ready: mount the fleetd MCP gateway and apply standard Claude Code settings so a session can orchestrate delegated workers. Ships no credentials.", - "version": "0.1.0", + "description": "Mount the fleetd MCP gateway and apply standard Claude Code settings so a session can orchestrate delegated workers. Ships no credentials.", + "version": "0.2.0", "author": { "name": "LTMS" } diff --git a/CLAUDE.md b/CLAUDE.md index 4ac699a..6361f3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,6 +210,17 @@ must obey belongs in the charter, not here. - **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 `/.claude/agents/.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 diff --git a/plans/fleet-plugin/plan.md b/plans/fleet-plugin/plan.md new file mode 100644 index 0000000..485b7c7 --- /dev/null +++ b/plans/fleet-plugin/plan.md @@ -0,0 +1,337 @@ +# Fleet as a Claude Code plugin — plan + +Status: draft for architect review. Not implemented. +Author: primary (lead `opus`, Mac fleet). Date: 2026-09-05. + +## 0. Correction — this already exists, and that changes the plan + +I wrote sections below as if the plugin were new work. It is not. **This repo is already a Claude +Code marketplace and already ships a plugin**, added in `ef1e014` (CB-527) and last touched in +`2e138a1` (CB-634): + +``` +.claude-plugin/marketplace.json -> name "claude-bridge", plugins: [ ./plugin ] +plugin/.claude-plugin/plugin.json -> name "claude-bridge", version 0.1.0 +plugin/.mcp.json -> mounts "fleetd" at http://127.0.0.1:8765/mcp +plugin/skills/setup/SKILL.md -> a full onboarding skill +plugin/README.md +``` + +The `setup` skill is good and covers most of what section 5 proposes: preflight, merge-not-clobber +into `.mcp.json`, read-only permissions only, credentials by env-var name, and a verify step that +insists on a **real spawn** because a green `/healthz` proves nothing. + +So the operator's question — "can we pack things into plugins?" — is already answered *yes, and it +was built*. The real question is why it did nothing for the kb session. The answer is drift plus +invisibility. + +### The drift, measured + +| # | Finding | Evidence | +|---|---|---| +| 1 | **Mount name mismatch.** The plugin mounts the server as `fleetd`; the daemon's own constant is `fleet` | `plugin/.mcp.json` vs `PeerLauncher.java:34` `String MCP_MOUNT_NAME = "fleet"` | +| 2 | **URL hardcoded**, no env indirection, so one plugin cannot serve two hosts or ports | `plugin/.mcp.json` | +| 3 | **Ships no worker skills and no agents** | `plugin/` has 1 skill (`setup`); `.claude/skills/` has 5 and `.claude/agents/` has 3, none of them in `plugin/` | +| 4 | **Stale identity advice.** `setup` §5 tells the operator to pin `primary.terminal:` | CB-579 replaced that with `fleet.leaders.*.tab`. `record Primary` still exists (`FleetConfig.java:954`), so the advice is not dead — but it is no longer the mechanism | +| 5 | **Stale install path.** README says `/plugin marketplace add ltms/claude-bridge` | the repo is `fleet/fleetd` since CB-623 | +| 6 | **Stale names.** Plugin and marketplace are both `claude-bridge` | the project renamed to `fleetd` in CB-634 | +| 7 | **Nothing references it.** `grep -rn "plugin/" CLAUDE.md docs/*.md` returns nothing | so no session is ever told the plugin exists — which is exactly why I planned it from scratch | + +Finding 7 is the root cause of the other six. A shipped capability that no instruction file +mentions gets no maintenance, and the next person rebuilds it. That is the same failure the +`CLAUDE.md` "Features" rule was written to stop. + +Finding 3 is the one that matters most for the operator's actual problem. A worker spawned into a +**kb** worktree has no `implementer` skill, because only `claude-bridge` carries one in +`.claude/skills/`. Every brief that says "Load the implementer skill" is a no-op outside this repo. +The plugin is the right home for those skills and does not carry them yet. + +## 1. The problem, restated + +A project is "fleet-enabled" today by hand-edits nobody wrote down in one place: + +- a `fleet.leaders.` entry in a host's gitignored `fleetd.yaml`; +- the repo must carry `.claude/skills/*` for a worker to load `implementer` or `reviewer`; +- the repo must carry the canonical bridge block in its `CLAUDE.md`; +- the MCP mount arrives only because `LeadLauncher` and `ClaudeCodeLauncher` add `--mcp-config` + to the argv they build. + +Shown live on 2026-09-05: an operator opened `claude` by hand in `/home/ltms/LTMS/kb` on fleet01 +and the session had **no `fleet_*` tools at all**, because a hand-started agent never gets the +launcher's `--mcp-config`. The plugin would have fixed that — if it had been installed, and if it +had been mentioned anywhere. + +## 2. What was verified, and how + +| Claim | Evidence | +|---|---| +| A plugin can install globally | `~/.claude/plugins/installed_plugins.json` — scopes in use are `project` (5), `local` (4), **`user` (1)** | +| A plugin can mount an MCP server | `~/.claude/plugins/marketplaces/kb-alms/.mcp.json` mounts `memory` at `"url": "${KB_MEMORY_URL}"` | +| A plugin can carry skills, agents, commands, hooks | `kb-alms` ships `skills/` + `hooks/hooks.json`; `umputun-cc-thingz/plugins/planning` ships `agents/` + `skills/` | +| Env vars interpolate in a plugin's `.mcp.json` | same `kb-alms` file: `${KB_MEMORY_URL}`, `${MEMORY_MCP_TOKEN}` | +| A marketplace can be a plain git repo | `known_marketplaces.json` — `mgnl-code-review` has `"source": "git", "url": "https://..."` | +| **This repo is already such a marketplace** | `.claude-plugin/marketplace.json`, committed in `ef1e014` | +| A lead already binds to a project directory | `FleetConfig.java:1017` `record Leader(..., String workspace, String cwd)`; used at `LeadLauncher.java:193-199` | +| The lead's mount comes from argv, not config | `LeadLauncher.java:253` adds `--mcp-config` | + +## 2b. Architect review + one measurement changed the design + +The architect verified the plan against the code and returned **build it with these changes**. Two +of its findings are load-bearing. I checked both myself. + +### A plugin cannot carry the agent definitions — confirmed + +`ClaudeCodeLauncher.java:371` calls +`agentDefinitionFile(spec.cwd(), spec.role(), ".claude", "agents")`, and +`HerdrPeerLauncher.java:359-365` returns a path only when +`/.claude/agents/.md` `Files.isRegularFile`. `ClaudeCodeLauncher.java:391-393` adds +`--agent` **only** when that returns non-null. `OpenCodeLauncher` does the same for +`.opencode/agent`. + +So the file must exist **in the member's worktree**. Moving `.claude/agents/*.md` into the plugin +would silently stop every member getting `--agent`. **The agents stay in the repo.** My plan had +this wrong. + +### The plugin does not reach members at all — confirmed, and worse than the architect could see + +`ClaudeCodeLauncher.java:285` does `putIfPresent(workerEnv, "CLAUDE_CONFIG_DIR", cfg.configDir())`. +A member with `configDir` set reads that directory, not the operator's `~/.claude`. + +The architect could not check how far that goes, because `fleetd.yaml` is gitignored. I measured it: + +- **All four Claude profiles on the live fleet set `configDir`** — `local`, `local-direct`, `opus`, + `sonnet` (`grep -c configDir fleetd/fleetd.yaml` = 4). +- Each `ccs` instance has its **own** `plugins/` directory: `gx10` (8 entries), `ltms` (11), + `ollama` (9), `work` (9). +- Those directories are **four separate real directories with four separate inodes**, and + `installed_plugins.json` in each is a **separate inode with an identical md5** + (`51c6e1c853e32e656b817e123fbbfcc5`). They are *copies made once*, not links. + +So a plugin installed at user scope lands in exactly one instance's store. It would have to be +installed once per `CLAUDE_CONFIG_DIR`, and each copy would then drift. **The plugin is not a +delivery mechanism for member-facing assets on this host.** + +A side effect worth recording: my own session's `CLAUDE_CONFIG_DIR` is set, so the +`~/.claude/plugins/*` evidence in section 2 is not even this session's store. The claims about what +a plugin *can* do still hold — they were read from real manifests — but the directory I read them +from is the wrong one for any conclusion about *this* session. + +### The design that follows + +Split by audience, not by mechanism: + +| Audience | Delivered by | Carries | +|---|---|---| +| operator / lead (a human opening any project) | **the plugin**, per config dir | the MCP mount, `setup`, the bridge charter | +| member (a worker in a provisioned worktree) | **worktree provisioning** | `.claude/skills/*`, `.claude/agents/*` | + +The second row is not a new idea — it is what the code already does for agents, and it is why +`agentDefinitionFile` looks in the worktree. Extending worktree provisioning to seed +`.claude/skills/` from a fleetd-owned source is the consistent move, and it is what actually fixes +"a worker in kb has no `implementer` skill". The plugin never could. + +### Other review findings I accepted + +- **Mount-name collision is real.** `PeerLauncher.java:34` is `fleet`; the plugin mounts `fleetd`. + A lead with both gets two mounts of one daemon and duplicate `fleet_*` tools. Rename the + plugin's server to `fleet`. +- **Keep `--mcp-config` in `LeadLauncher`.** It is config→argv from `profile.mcpUrl()`, not drift, + and it is the only path that works for a member with its own `configDir`. +- **I overstated #359.** `LeadCoordLoop.java:174-197` returns null and logs a warning that names the + fix; `tick()` leaves the message unacked, so the broker holds it and delivers once a lead is + named. It **stalls loudly and recovers** — it is not silent, and it is not data loss. My wording + in issue #361 needs the same correction. +- **Stage 1 ends with tools that mostly cannot be used until stage 3**, because authority still + comes from the tab. Section 3 already said this; the stage table did not. + +## 3. What a plugin can and cannot do + +This is the part that decides the design, so it is stated before the design. + +**A plugin gives tools. It does not give authority.** + +`fleet_whoami` resolves a caller's role from the connection, not from what is mounted. A +hand-started session in kb that mounts `fleet_*` through a plugin will be resolved as a **worker** +and refused on every orchestration call, because its pane is not in a tab matching +`fleet.leaders.*.tab`. + +So the plugin alone does not make a project fleet-enabled. It makes it *tool*-enabled. Registering +the lead stays fleetd's job. Any plan that forgets this ships a plugin that looks installed and +does nothing. + +```mermaid +flowchart TB + P["fleet plugin
(user scope, every session)"] --> T["fleet_* tools mounted"] + D["fleetd.yaml
leaders.kb {tab, cwd}"] --> A["role = primary"] + T --> W["can call fleet_*"] + A --> W2["calls are authorized"] + W --> OK["working lead"] + W2 --> OK + T --> NO["tools mounted, every call refused"] + classDef good fill:#2f855a,stroke:#22543d,color:#ffffff; + classDef bad fill:#9b2c2c,stroke:#63171b,color:#ffffff; + class OK good + class NO bad +``` + +*Both halves are needed. The plugin is the left half only.* + +## 4. Proposed architecture — three layers + +### Layer 1: the plugin — lead-side only, fix the one that exists + +Keep it at `plugin/`, keep the marketplace at `.claude-plugin/marketplace.json`. Do not create a +second one, and do not put member-facing assets in it (see 2b). + +``` +.claude-plugin/marketplace.json -> rename to "fleetd"; keep source ./plugin +plugin/ + .claude-plugin/plugin.json -> rename to "fleet"; bump version + .mcp.json -> mount name "fleet" (match PeerLauncher.MCP_MOUNT_NAME), + url "${FLEETD_MCP_URL}", 8765 default documented + skills/setup/SKILL.md -> EXISTS. fix the stale primary.terminal advice (§5) + skills/bridge-charter/SKILL.md -> NEW: the canonical CLAUDE.md block + README.md -> fix the install path (fleet/fleetd, not ltms/claude-bridge) +``` + +**Not in the plugin:** `agents/*.md` (the launcher requires them in the member's worktree — +`ClaudeCodeLauncher.java:371,391`) and the three worker skills (a member with `configDir` set never +reads the operator's plugin store — measured in 2b). Those belong to layer 1b. + +**A rename is a breaking change for anyone who installed 0.1.0.** The mount name goes `fleetd` -> +`fleet`, so a project whose `.claude/settings.json` pre-allows `mcp__fleetd__fleet_whoami` stops +matching. Only this fleet has it installed today, so the cost is small now and grows. Decide once. + +**This solves propagation of the charter.** `CLAUDE.md` says the bridge block "must stay +byte-identical with the template in the wiki" and that "other projects carrying the block need the +same edit" — a hand-copy the file itself admits is fragile, with a python snippet to check it. A +plugin skill turns that into a version bump. + +### Layer 1b: worker skills reach members through the worktree, not the plugin + +This is the change that actually fixes "a worker in kb cannot load `implementer`". + +Worktree provisioning already writes into the member's tree — the parity overlay, the neutralised +`.mcp.json`, the IDE overlay. Add one more: seed `/.claude/skills/` from a fleetd-owned +source directory, so every member gets `implementer`, `reviewer` and `hunter` whatever repo it is +working in. `.claude/agents/` is already required there by the launcher, so this follows the +grain of the design rather than cutting across it. + +Open question for implementation: copy or symlink, and where the source lives (a config key such +as `memberSkills:`, or the plugin's own directory read by the daemon). A symlink is one source of +truth but breaks if the member's tree is archived; a copy drifts but is self-contained. + +### Layer 2: host-global fleet settings + +`~/.fleet/fleetd.yaml` — the things that are true for the **machine**, not the project: + +- `broker:` and `coordinator:` (URIs come from env, no secrets in the file) +- `profiles:` — backends, models, credentials, weights +- `memberCredentials:` policy +- `worktreeRoot`, `worktreeGroup` + +### Layer 3: per-project settings, committable + +`/.fleet/project.yaml` — the things that are true for the **repo**: + +```yaml +lead: + tab: "lead: kb" + profile: opus +ide: + projectDir: "" # kb is a Python repo, everything at the root +worktree: true +``` + +**This split fixes a contradiction that exists today.** `ideProjectDir` is a property of a *repo* +(fleetd's Maven module is a subdirectory; kb's code is at the root) but the config key is +per-*profile*. One profile therefore cannot serve both repos — measured on fleet01 on 2026-09-05, +where the key had to be commented out to make kb work. Moving it to a project file removes the +contradiction rather than working around it. + +It is also committable, because it holds no secrets. A project that has been fleet-enabled once +stays fleet-enabled for everyone who clones it. + +## 5. The `fleet-setup` skill + +What the operator actually asked for: one command that makes any project fleet-compatible. + +```mermaid +sequenceDiagram + participant Op as Operator + participant Sk as fleet-setup skill + participant Fs as project files + participant Fd as fleetd + Op->>Sk: /fleet-setup (in any project) + Sk->>Fs: write .fleet/project.yaml + Sk->>Fs: add the bridge block to CLAUDE.md (if absent) + Sk->>Fd: register the lead (tab + cwd) + Fd-->>Sk: tab created, lead launched + Sk-->>Op: report what changed, and what is still manual +``` + +*The skill writes the project half and asks the daemon for the host half.* + +The registration step needs something that does not exist yet: an MCP tool such as +`fleet_workspace_add{path, tab, profile}`, or a `fleetd` config include so a project file is picked +up without hand-editing the host file. **This is the one genuinely new piece of daemon work.** + +## 6. Does this reduce fleetd's complexity? + +Honestly: **partly**. Claiming more than this would be wrong. + +**Yes, in three places.** + +1. Skill and agent delivery leaves the daemon and the repos entirely. +2. Worktree config neutralisation (fleetd #134) gets safer. It blanks `.mcp.json` so the primary's + IDE and forge servers do not leak into a worker. Today the fleet mount survives only because + the launcher re-adds it by argv. With a user-scope plugin the fleet mount is outside the file + being neutralised, so the two concerns stop fighting. +3. The `ideProjectDir` per-profile/per-repo contradiction disappears. + +**No, in the places that matter most.** fleetd still owns spawn, authorization, worktrees, herdr, +the broker, tickets, and identity. A plugin cannot do any of those. The plugin is a **distribution** +mechanism, not a replacement for the daemon. + +**And it adds one new risk.** The mount URL becomes a second source of truth. `fleetd.yaml` has the +port; the plugin has the URL. Mitigation: the plugin reads `${FLEETD_MCP_URL}` only, and the host +env is the single place it is set. + +## 7. Rollout stages + +| Stage | Content | Ends with | +|---|---|---| +| 0 | **Make it visible.** One `CLAUDE.md` line and one Features entry saying the plugin exists and where | nobody re-plans it a third time | +| 1 | Fix the plugin's drift: mount name `fleet`, `${FLEETD_MCP_URL}`, names, README, stale `primary.terminal` advice | a lead in any project can install one plugin and get the mount | +| 1b | Seed `.claude/skills/` into provisioned worktrees | **a worker in *kb* can load `implementer`** | +| 2 | `.fleet/project.yaml` schema + `FleetConfig` reads it; `ideProjectDir` moves there | kb and fleetd both work off one profile | +| 3 | Fix #359, then config-include for lead registration, wired into the existing `setup` skill | `/fleet:setup` in a fresh project produces a working lead | +| 4 | Roll out to fleet01; retire the hand-copied CLAUDE.md block in favour of the skill | one `git pull` propagates the charter | + +Stage 0 is minutes of work and is the one that stops this happening again, so it goes first. +**Stage 1b carries most of the value** and is independent of the plugin — it could ship first if the +plugin rename needs more thought. Stage 1 alone ends with tools a hand-started session mostly +cannot use, because authority still comes from the tab; that is fixed in stage 3, not stage 1. +#359 moves ahead of stage 3 on the architect's advice, because stage 3 is what creates the second +lead. + +## 8. Questions for the architect + +1. **Is the layer-2 / layer-3 split right?** Specifically: should `profiles:` stay host-global, or + should a project be able to pin which profiles it uses? Cost of getting this wrong is a config + that has to be re-split later. +2. **Config include, or a new MCP tool, for registering a project's lead?** An include is passive + and survives a restart; a tool is live but writes to a gitignored file the daemon owns. +3. **What happens when the plugin is absent?** Should `LeadLauncher` keep its `--mcp-config` + belt-and-braces, or is that the drift risk we should remove? Note opencode members cannot use + Claude plugins at all, so `OpenCodeLauncher` keeps its ephemeral config either way. +4. **Does a user-scope plugin mount leak into members in a way we do not want?** Members already + inherit user-scope MCP servers (`--mcp-config` adds, it does not replace). A worker getting + `fleet_*` is correct and already happens. Confirm nothing else in the plugin should be + worker-invisible. +5. **Two leads on one host both hold a subscription seat.** Is per-project leads the right unit, or + should one lead serve several projects by changing cwd? +6. **Blocking defect to fix first or alongside:** `LeadCoordLoop.resolveLocalLead()` (lines + 174-190) routes a peer message to "the sole lead" when no lead is *named* after + `coordinator.selfId`. The moment a host has two leads — exactly what this plan encourages — + cross-host coordination silently stops. Tracked as #359. diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index fb7e874..82e14f1 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { - "name": "claude-bridge", - "description": "Make a project bridge-ready: mount the fleetd MCP gateway and set up standard Claude Code settings so this session can orchestrate a fleet of delegated workers. Ships no credentials.", - "version": "0.1.0", + "name": "fleet", + "description": "Make a project fleet-ready: mount the fleetd MCP gateway and set up standard Claude Code settings so this session can orchestrate a fleet of delegated workers. Lead-side only — member skills and agents travel in the worktree. Ships no credentials.", + "version": "0.2.0", "author": { "name": "LTMS" }, diff --git a/plugin/.mcp.json b/plugin/.mcp.json index e5b6f0f..2a9939e 100644 --- a/plugin/.mcp.json +++ b/plugin/.mcp.json @@ -1,8 +1,8 @@ { "mcpServers": { - "fleetd": { + "fleet": { "type": "http", - "url": "http://127.0.0.1:8765/mcp" + "url": "${FLEETD_MCP_URL}" } } } diff --git a/plugin/README.md b/plugin/README.md index a1fe065..cc33d33 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,11 +1,24 @@ -# claude-bridge (Claude Code plugin) +# fleet (Claude Code plugin) -Makes a project **bridge-ready**: mounts the `fleetd` MCP gateway and applies standard Claude Code +Makes a project **fleet-ready**: mounts the `fleetd` MCP gateway and applies standard Claude Code settings, so the session can orchestrate a fleet of delegated workers. **This plugin ships no credentials.** Every secret is referenced by environment-variable *name*; the values stay with the user. Nothing the plugin writes is unsafe to commit. +## Scope — lead-side only + +This plugin configures **the session you are sitting in**: a lead, or any human-started Claude Code +session that wants to talk to the daemon. It deliberately does **not** carry the worker playbook +skills or the role agent definitions, and it cannot: + +- the launcher adds `--agent` only when `/.claude/agents/.md` exists in the + member's own tree (`ClaudeCodeLauncher.java:371,391`), so agent files must live in the repo; +- a member's `CLAUDE_CONFIG_DIR` points at its profile's config directory + (`ClaudeCodeLauncher.java:285`), so it never reads the operator's plugin store. + +Member-facing assets travel in the worktree, not in this plugin. See fleetd #362. + ## What it is not The plugin is the **client-side setup**, not the bridge. `fleetd` is a separate daemon and `herdr` @@ -16,22 +29,35 @@ not try to install system services on your behalf. ## Install ```shell -/plugin marketplace add ltms/claude-bridge -/plugin install claude-bridge@claude-bridge +/plugin marketplace add https://git.ltms.dev/fleet/fleetd +/plugin install fleet@fleetd +``` + +Export the gateway URL — the plugin mounts `${FLEETD_MCP_URL}`, not a hardcoded address, so one +plugin serves hosts that run the daemon on different ports: + +```shell +export FLEETD_MCP_URL=http://127.0.0.1:8765/mcp ``` Then, in the project you want to onboard: ```shell -/claude-bridge:setup +/fleet:setup ``` ## What you get | Component | Effect | |---|---| -| `.mcp.json` | mounts `fleetd` at `http://127.0.0.1:8765/mcp` for any session with the plugin enabled | -| `skills/setup` | `/claude-bridge:setup` — preflight, project settings, credential guidance, and verification | +| `.mcp.json` | mounts `fleet` at `${FLEETD_MCP_URL}` for any session with the plugin enabled | +| `skills/setup` | `/fleet:setup` — preflight, project settings, credential guidance, and verification | + +The server is named **`fleet`** on purpose: that is `PeerLauncher.MCP_MOUNT_NAME` in the daemon and +the name a spawned member's own mount carries. Version 0.1.0 named it `fleetd`, which produced two +mounts of one daemon for anyone who also had a project-level `.mcp.json`. Upgrading from 0.1.0 is a +**breaking change** — a project that pre-allowed `mcp__fleetd__fleet_whoami` in +`.claude/settings.json` must be updated to `mcp__fleet__*`. Because the plugin carries its own `.mcp.json`, an installed plugin needs no project-level MCP file at all. The setup skill writes one only when you want the mount to work *without* the plugin — diff --git a/plugin/skills/setup/SKILL.md b/plugin/skills/setup/SKILL.md index 64180d5..22df494 100644 --- a/plugin/skills/setup/SKILL.md +++ b/plugin/skills/setup/SKILL.md @@ -36,9 +36,17 @@ a time. command -v herdr && herdr --version 2>&1 | head -1 || echo "MISSING: herdr" command -v ccs && ccs version 2>&1 | head -1 || echo "MISSING: ccs (needed for worker profiles)" command -v codex && codex --version 2>&1 | head -1 || echo "absent: codex (optional)" -curl -s -m 5 http://127.0.0.1:8765/healthz || echo "MISSING: fleetd daemon is not reachable" +curl -s -m 5 "${FLEETD_MCP_URL%/mcp}/healthz" 2>/dev/null \ + || curl -s -m 5 http://127.0.0.1:8765/healthz \ + || echo "MISSING: fleetd daemon is not reachable" +[ -n "$FLEETD_MCP_URL" ] && echo "FLEETD_MCP_URL is set" || echo "MISSING: FLEETD_MCP_URL" ``` +**`FLEETD_MCP_URL` is required.** The plugin's own `.mcp.json` mounts `${FLEETD_MCP_URL}` rather +than a hardcoded address, so one plugin can serve hosts that run the daemon on different ports. If +it is unset the mount does not resolve. The usual value is `http://127.0.0.1:8765/mcp`; tell the +user to export it, do not write it into a file for them. + A healthy daemon answers with its status **and the herdr protocol it negotiated**: ```json @@ -70,7 +78,7 @@ The entry to add, exactly: ```json { "mcpServers": { - "fleetd": { + "fleet": { "type": "http", "url": "http://127.0.0.1:8765/mcp" } @@ -78,8 +86,13 @@ The entry to add, exactly: } ``` -If `.mcp.json` already exists, add only the `fleetd` key and leave every other server untouched. -If a `fleetd` entry is already there with a different URL, **ask** rather than assuming yours is +**The server must be named `fleet`.** That is `PeerLauncher.MCP_MOUNT_NAME` in the daemon, the name +a spawned member's mount carries, and the name the `mcp__fleet__*` role heuristic in `CLAUDE.md` +keys on. An earlier version of this plugin named it `fleetd`, which gave a lead with both a project +file and the plugin **two mounts of the same daemon** and a duplicated `fleet_*` tool set. + +If `.mcp.json` already exists, add only the `fleet` key and leave every other server untouched. +If a `fleet` entry is already there with a different URL, **ask** rather than assuming yours is right — a non-default port usually means a deliberate second daemon. > **If this plugin is installed, you can skip this step entirely.** The plugin ships its own @@ -111,11 +124,11 @@ project already set. "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ - "mcp__fleetd__fleet_whoami", - "mcp__fleetd__fleet_list", - "mcp__fleetd__fleet_status", - "mcp__fleetd__fleet_profiles", - "mcp__fleetd__fleet_poll" + "mcp__fleet__fleet_whoami", + "mcp__fleet__fleet_list", + "mcp__fleet__fleet_status", + "mcp__fleet__fleet_profiles", + "mcp__fleet__fleet_poll" ] } } @@ -161,19 +174,31 @@ fleet_whoami ``` - `{"role":"primary"}` — correct, you are done with this step. -- `{"role":"worker", …}` — **this is the trap.** If the primary runs inside a herdr pane, the - daemon resolves it to a terminal and classifies it as a worker, refusing `spawn`/`send`/`stop`: - every verb an orchestrator exists to call. It is **self-locking**, because the daemon can only - *learn* the primary's terminal from those same refused calls. The only way out is an - operator-set pin in the daemon's config: +- `{"role":"worker", …}` — **this is the trap.** If the lead runs inside a herdr pane whose tab the + daemon does not recognise, it is classified as a worker and refused on `spawn`/`send`/`stop`: + every verb an orchestrator exists to call. It is **self-locking**, because those are the same + calls that would tell the daemon who you are. + + Identity is the **tab label**, matched exactly and case-insensitively: ```yaml - primary: - terminal: term_xxxxxxxxxxxx # the terminalId fleet_whoami just reported + fleet: + leaders: + kb: # name it after coordinator.selfId if this host uses lead-to-lead + profile: opus + tab: "lead: kb" # the exact label of the tab this lead sits in + cwd: /path/to/the/project ``` - The daemon reads this **at boot**, so it needs a restart. Re-pin whenever the primary moves - panes — a stale pin fails exactly as silently as no pin. + A tab label is stable across restarts of the agent inside it, which is why CB-579 replaced the + older `primary.terminal:` pin — a herdr `terminal_id` changed on every restart and cost a config + edit each time. `primary.terminal:` still parses, but it is no longer the mechanism; do not + reach for it. + + The daemon reads `leaders:` **at boot**, so a new entry needs a restart. Two things to check + afterwards: that `fleet_whoami` now answers `primary`, and that no *stale* tab carries the same + label — duplicate lead tabs are their own failure (#359), and they stall lead-to-lead delivery + until one lead is named after `coordinator.selfId`. Then prove the fleet actually works, with a real spawn: -- 2.52.0