diff --git a/12-Claude-to-OpenCode.md b/12-Claude-to-OpenCode.md index 6696474..9b26dfc 100644 --- a/12-Claude-to-OpenCode.md +++ b/12-Claude-to-OpenCode.md @@ -2,7 +2,7 @@ **What this page is for.** Taking a project a Claude Code session already works in, and making an OpenCode session a first-class participant in the same workspace — same instructions, same MCP -servers, same bridge mount. +servers, same `fleet` mount. **The short version: there is almost nothing to port.** OpenCode reads `CLAUDE.md` natively. The only artifact you create is one `opencode.json` mapping MCP servers. Everything below was verified @@ -64,11 +64,18 @@ belong in the global file and shared ones in the project file. "$schema": "https://opencode.ai/config.json", "instructions": ["CLAUDE.md"], "mcp": { - "fleetd": { "type": "remote", "url": "http://127.0.0.1:8765/mcp", "enabled": true } + "fleet": { "type": "remote", "url": "http://127.0.0.1:8765/mcp", "enabled": true } } } ``` +The mount name here must be `fleet`, not `fleetd`. Every launcher writes this same constant name +(`PeerLauncher.MCP_MOUNT_NAME = "fleet"`, `PeerLauncher.java:34`) into the config it builds for a +spawned worker — `ClaudeCodeLauncher` into an inline `--mcp-config` (`ClaudeCodeLauncher.java:348`), +`OpenCodeLauncher` into the `opencode.json` it writes (`OpenCodeLauncher.java:345`). A hand-written +config using a different key mounts a server under a name the role-detection ladder in `CLAUDE.md` +does not check, so an opencode peer set up by hand must match this name exactly. + Set `instructions` explicitly even though `CLAUDE.md` is found anyway — the fallback applies only while no `AGENTS.md` exists. @@ -91,8 +98,9 @@ files opencode never reads. Verified here: a clean login shell exist only inside processes Claude Code spawned. Run `opencode mcp list` from such a process and everything is green; run it from a real terminal and context7 is `⚠ needs authentication`. -Worse, the two launch paths disagree on names. A bridge-spawned worker gets `GITEA_TOKEN` + -`GITEA_HOST` from `applyGitToken` (see [Features](11-Features) → the git-forge token grant), not +Worse, the two launch paths disagree on names. A fleetd-spawned worker gets `GITEA_TOKEN` + +`GITEA_HOST` from `applyGitToken` (see [Features](11-Features) → the git-forge token grant; the +method lives at `HerdrPeerLauncher.java:893-900` and both launchers call it), not `GITEA_ACCESS_TOKEN` — so an `opencode.json` written against the terminal's environment fails inside a worker, and vice versa. @@ -101,7 +109,7 @@ So the supply route follows from who launches opencode: | Launcher | Route | Here | |---|---|---| | a human, from a terminal | `{file:.secrets/…}` | gitignored `.secrets/`, workspace-scoped | -| a bridge-spawned worker | `{env:…}` | `applyGitToken` + the profile's `env:` block | +| a fleetd-spawned worker | `{env:…}` | `applyGitToken` + the profile's `env:` block | For the terminal case this repo uses `{file:}` with **workspace-relative** paths — verified: opencode resolves them against the project root, so no shell export and no direnv is needed, and the secret @@ -163,21 +171,23 @@ authenticated tool per server, and confirm nothing you withheld appears. - **A leftover `AGENTS.md` silently shadows `CLAUDE.md`.** If one exists from an earlier port, delete it, or opencode reads the stale copy instead of the live rules. Check this first. -- **Skills do not port.** OpenCode uses its own agent markdown under `.opencode/agents/`; - `.claude/skills/**` is not read. A delegation brief that says "load the *implementer* skill" has - no effect on an opencode peer — spell the procedure out in the brief instead. +- **Skills do not port.** OpenCode uses its own agent markdown under `.opencode/agents/` + (`HerdrPeerLauncher.java:322-326` builds that path for both launchers); `.claude/skills/**` is not + read. A delegation brief that says "load the *implementer* skill" has no effect on an opencode + peer — spell the procedure out in the brief instead. - **Identity comes from the terminal, not the runtime.** A peer must be spawned into its own pane; running `opencode` in the primary's pane resolves as the primary. -- **The subscription guard doesn't apply.** It is `ANTHROPIC_BASE_URL`-shaped; opencode - authenticates through its own credentials, so `maxLoad` is the only spend control that reaches it. +- **The subscription guard doesn't apply.** It is `ANTHROPIC_BASE_URL`-shaped and lives only in + `ClaudeCodeLauncher`; `OpenCodeLauncher.buildLaunch` never calls it. opencode authenticates + through its own credentials, so `maxLoad` is the only spend control that reaches it. --- ## Appendix — the Codex port (superseded) Chapter 12 previously documented porting to OpenAI Codex. That path was abandoned in favour of -opencode, which the bridge already supports through `OpenCodeLauncher`. Four findings are kept -because they were verified by hand and explain *why* the comparison went this way: +opencode, which fleetd already supports through `OpenCodeLauncher`. Four findings are kept because +they were verified by hand and explain *why* the comparison went this way: - **Codex has no `CLAUDE.md` fallback.** Instructions had to be translated into `AGENTS.md` by a third-party tool, whose terminology map rewrote file paths into sentences that were wrong rather diff --git a/3-Approaches.md b/3-Approaches.md index 37b6449..424fab1 100644 --- a/3-Approaches.md +++ b/3-Approaches.md @@ -20,7 +20,7 @@ flowchart TD Q -->|"raw keystrokes into the tmux pane"| TMUX["tmux send-keys
/ PTY paste"] HD --> V0["✅ our leading choice
(status events + multiplex;
symmetric single-host)"] - AA --> V1["◐ fallback injector
(swappable behind fleetd)"] + AA --> V1["✕ never built
(discarded research)"] SDK --> V2["✅ if driver is our own code"] BUS --> V3["⚠ async bus events only
(lands at turn boundary)"] TMUX --> V4["⚠ fragile — the raw primitive
herdr/AgentAPI productize"] @@ -71,26 +71,35 @@ Unix-socket JSON API. `fleetd` (see [Message Server](2-Message-Server)) drives i - **Subscription-safe:** only the worker pane launches with `ANTHROPIC_BASE_URL`; `fleetd` is a plain daemon (no quota) that enforces the boundary in code. - **Trade-off:** herdr's socket is **local-only** (`fleetd`'s MCP/HTTP spans hosts, not - herdr), and it is a young, single-dev project — so `fleetd` keeps the injector **pluggable** - and its durability in an **internal** queue behind the gateway. Replies are best carried as a + herdr), and it is a young, single-dev project — so `fleetd` keeps delivery durability in an + **internal** queue behind the gateway. The shipped injector is the herdr one: a + status-gated, single writer per worker (`Injector.java:48`), and it is a `final` class — + not a pluggable interface a second injector could slot into. Replies are best carried as a structured envelope, not scraped. -## 2. AgentAPI — HTTP over terminal emulation *(fallback injector)* +## 2. AgentAPI — HTTP over terminal emulation *(never built — research only)* [`coder/agentapi`](https://github.com/coder/agentapi) wraps the Claude Code **CLI** as an HTTP server and drives the CLI's terminal underneath (essentially a hardened, stateful `tmux send-keys` with parsing): `POST /message`, `GET /events` (SSE), `GET /status`. It was -the original leading choice; herdr now supersedes it, but it remains a **swappable fallback -injector** behind `fleetd`'s interface. +the original leading choice, until herdr superseded it. **It was never built**: a +case-insensitive search for `agentapi` under `fleetd/src/main/java` returns nothing, and +both `Profile.kind` values (`claude-code`, the default, and `opencode`, +`FleetConfig.java:265-270`) run through `HerdrPeerLauncher` +(`ClaudeCodeLauncher.java:43`, `OpenCodeLauncher.java:54`) — herdr is the only injection +path that ships. The mechanism below describes the external project, not this codebase. -- **Injects into a live session:** yes — but only the *worker* (it wraps one CLI); the - primary direction still needs `fleetd`'s async path (idle-injection, or a split-host - `Stop`-hook polling `fleetd`). +- **Injects into a live session:** in its own design, yes — but only the *worker* (it wraps + one CLI); the primary direction still needs `fleetd`'s async path (idle-injection, or a + split-host `Stop`-hook polling `fleetd`). - **Completion signal:** a **screen-stability heuristic**, not structured events. - **Cross-host:** native HTTP — its one edge over herdr, but `fleetd` already provides the HTTP layer on top of herdr, so that edge is neutralized. -- **When to reach for it:** if herdr can't run, or as the second injector implementation to - de-risk herdr's immaturity. Its `msgfmt` reply parser is worth reusing regardless. +- **Why it was dropped:** herdr's structured status events beat a screen-stability + heuristic, and `fleetd` already covers the cross-host part, so that one edge buys + nothing here. It stays as discarded research: a second injector implementation to + de-risk herdr, and the possible reuse of its `msgfmt` reply parser, which is not in this + codebase. ## 3. Agent SDK — streaming input *(good if the driver is our own code)* @@ -106,7 +115,7 @@ events and permission callbacks instead of scraping a terminal. ## 4. Message-queue + Stop-hook long-poll *(raw async primitive — behind the gateway in our design)* The **only pure-hooks** way to pull an external message into the **same** session. In -`claude-bridge` this is **not** how a Claude session normally receives async work — under the +`fleet` this is **not** how a Claude session normally receives async work — under the sole-gateway rule `fleetd` delivers async by **injecting an idle pane**, and no Claude session polls a queue. The Stop-hook survives in exactly one place: a **split-host primary** that isn't a herdr pane, where the hook long-polls **`fleetd`** (not the queue) for wake-ups. The raw @@ -161,14 +170,15 @@ live session, works today, but it is terminal automation — timing-sensitive, n **herdr's `send_text`/`send_keys` is this, productized** — with a stable session, structured status events, and multiplexing — which is why we build on herdr rather than hand-rolled -`send-keys` (and why AgentAPI, the other productized form, is only the fallback). +`send-keys` (AgentAPI is the other productized form of the same idea; it was never built +and is research only). ## Research matrix | Approach | Transport | Inject into running session? | Completion signal | Symmetric (both panes)? | Cross-host | Subscription-safe | Fragility | |---|---|---|---|---|---|---|---| | **herdr via `fleetd`** ✅ | socket → terminal + events | ✅ (idle-gated) | ✅ status events² | ◐ single-host¹ | via `fleetd` MCP/HTTP (sole gateway) | ✅ (guard in code) | Low–Med (herdr young) | -| **AgentAPI** ◐ (fallback) | HTTP → terminal emulation | ✅ (worker only) | ⚠ screen-stability | ❌ | ✅ native HTTP | ✅ (worker-only env) | Low | +| ~~AgentAPI~~ (never built — research) | HTTP → terminal emulation | ✅ (worker only) | ⚠ screen-stability | ❌ | ✅ native HTTP | ✅ (worker-only env) | Low | | **Agent SDK streaming** | in-process generator | ✅ | ✅ typed events | n/a | ✅ | ✅ | Low (driver = code) | | **Queue + Stop-hook** | hook long-poll | ✅ (turn boundary) | via turn end | ✅ (symmetric) | ✅ | ✅ | Medium | | **tmux `send-keys`** | keystrokes / PTY | ✅ | ❌ scrape `capture-pane` | ✅ | ⚠ ssh | ✅ | High | @@ -187,18 +197,20 @@ structured `fleet_reply` (preferred), with a `Stop`-hook envelope as fallback** ## Recommendation -- **Primary Opus → worker (the bridge's main path):** **herdr via `fleetd`** — +- **Primary Opus → worker (the main path of this system):** **herdr via `fleetd`** — status-gated injection, structured completion/blocked events, symmetric (single-host), multiplexed, persistent, with the subscription boundary enforced in code. Selected. See [Message Server](2-Message-Server) / [Architecture](1-Architecture). -- **Keep AgentAPI as a swappable fallback injector** behind `fleetd`'s interface, so - herdr's immaturity is a de-riskable risk rather than a load-bearing one. +- **AgentAPI — never built:** considered and dropped. This page keeps it as research + (section 2), not as a fallback an operator can select; herdr's immaturity stands as a + known risk, and `fleetd` de-risks it with the internal queue and status-gated injection + instead of a second injector. - **External event bus → worker (async wake-ups):** the bus hits **`fleetd`'s REST ingress**; `fleetd` enqueues internally if needed and **injects the idle worker** — the worker runs no queue-polling hook. Complementary to the sync path, not a replacement — different trigger shape, same single gateway. -- **Avoid hand-rolled `tmux send-keys`** unless neither herdr nor AgentAPI can run; it's the - same idea with all the fragility left in. +- **Avoid hand-rolled `tmux send-keys`** unless herdr can't run on the host; it's the same + idea with all the fragility left in. ## Sources @@ -209,5 +221,4 @@ structured `fleet_reply` (preferred), with a `Stop`-hook envelope as fallback** - [Stop-hook task-enforcement pattern](https://claudefa.st/blog/tools/hooks/stop-hook-task-enforcement) - [claude-mem hooks architecture](https://docs.claude-mem.ai/hooks-architecture) - [Issue #27441 — inter-agent message injection](https://github.com/anthropics/claude-code/issues/27441) · [Issue #24947 — `claude inject`](https://github.com/anthropics/claude-code/issues/24947) -- [Claude-Code-Remote](https://github.com/JessyTsui/Claude-Code-Remote) · [OpenACP guide](https://dev.to/tigergethigher/how-to-control-claude-code-from-telegram-discord-or-slack-self-hosted-open-source-1jk8) - +- [Claude-Code-Remote](https://github.com/JessyTsui/Claude-Code-Remote) · [OpenACP guide](https://dev.to/tigergethigher/how-to-control-claude-code-from-telegram-discord-or-slack-self-hosted-open-source-1jk8) \ No newline at end of file diff --git a/Home.md b/Home.md index 8a7ce0e..733c3db 100644 --- a/Home.md +++ b/Home.md @@ -1,4 +1,4 @@ -# claude-bridge +# fleet A **subscription-safe bridge** that lets a lead **Claude Code** session on Pro/Max delegate work to **members running on other models and other vendors** — without ever putting a proxy on the lead. @@ -8,7 +8,7 @@ A **subscription-safe bridge** that lets a lead **Claude Code** session on Pro/M > the shape of the design and why it was chosen. > Sibling of [`crush-bridge`](https://git.ltms.dev/systems/vms) (Tier 2 → headless Crush -> on GX10 DeepSeek). `claude-bridge` keeps a Claude member a *real Claude Code process* so it +> on GX10 DeepSeek). `fleet` keeps a Claude member a *real Claude Code process* so it > inherits `CLAUDE.md`, hooks, skills, and MCP — just pointed at a cheaper model. It also runs > non-Claude members (`kind: opencode`) as first-class peers. @@ -17,25 +17,26 @@ A **subscription-safe bridge** that lets a lead **Claude Code** session on Pro/M A small always-on message server, **`fleetd`**, controls [herdr](https://herdr.dev) (an agent multiplexer, "tmux for agents") over its Unix-socket API and exposes a clean 2-way messaging API as an **MCP server that both the primary and the -workers mount** — one unified Claude setup and the **sole communication gateway** (REST/SSE -stays for non-Claude clients; any broker is `fleetd`-internal, below the gateway). herdr owns +workers mount** — one unified Claude setup and the **sole communication gateway**. A plain +REST surface covers the same features for non-Claude clients; there is no event-stream route. +Any broker is `fleetd`-internal, below the gateway. herdr owns the PTYs, multiplexing, persistence, and **agent-status events**; `fleetd` owns policy (subscription boundary, session lifecycle, status-gated -delivery) and the client contract. A Claude member launches with `ANTHROPIC_BASE_URL` pointed at -the gateway, `https://llm.ltms.dev/anthropic`, plus a bearer token; the lead stays env-clean and -calls `fleetd`'s MCP tools. +delivery) and the client contract. A Claude member launches with `ANTHROPIC_BASE_URL` pointed at whichever gateway its profile +names, plus a bearer token. The allowed hosts are a config value, so they differ per +deployment. The lead stays env-clean and calls `fleetd`'s MCP tools. ```mermaid flowchart LR OPUS["Opus — primary
(Claude Code, env CLEAN)
MCP client"] subgraph BD["fleetd — standalone daemon (not a claude process)"] - SRV["SERVER face
MCP · REST/SSE · policy"] + SRV["SERVER face
MCP · REST · policy"] CLI["CLIENT face
status-gated injector · herdr socket"] SRV --> CLI end HERDR["herdr
panes · agent-status"] W["member pane
ANTHROPIC_BASE_URL set
MCP client"] - M["llm.ltms.dev
(the one gateway)"] + M["the configured gateway"] OPUS -->|"MCP fleet_send (blocks)"| SRV W -.->|"MCP fleet_reply"| SRV @@ -70,11 +71,11 @@ flowchart LR **injects the lead's own idle pane** to wake it. - **Different model** per member process sidesteps Claude Code's lack of per-subagent provider routing — the worker isn't a subagent, it's its own configured process. -- **AgentAPI** ([`coder/agentapi`](https://github.com/coder/agentapi)) was kept on paper as a - swappable *fallback injector*. It was **never built** — `grep -ri agentapi fleetd/src/main` - returns nothing, and the only injection path in the shipped code is the herdr one. Treat it as a - discarded option, not a fallback you can switch to. See [Approaches](3-Approaches) for why herdr - won and [Message Server](2-Message-Server) for the full design. +- **AgentAPI** ([`coder/agentapi`](https://github.com/coder/agentapi)) was considered and dropped: + it was **never built**. No AgentAPI code exists under `fleetd/src/main/java`, and the shipped + injection path is the herdr one, which every profile selects through `Profile.kind` + (`claude-code` or `opencode`, `FleetConfig.java:265-270`). See [Approaches](3-Approaches) for + why herdr won and [Message Server](2-Message-Server) for the full design. ## Pages @@ -96,11 +97,14 @@ Read in order (the sidebar mirrors this): ## Status -🟢 **Running.** Release 1.1 is code-complete (2026-08-16): 20 of 20 tickets closed, 870 tests green, -the daemon live on this host. Chapters 4 and 5 were never written past their scope note; chapter 13 -replaced them. +🟢 **Running.** The daemon is live on this host. Chapters 4 and 5 were never written past their +scope note, and chapter 13 replaced them. + +This page does not carry release numbers, ticket counts or test totals. Those go stale within days +and then read as current facts. See [8 Roadmap](8-Roadmap) for the delivery record. The **herdr-centric `fleetd` message server** was selected on 2026-07-11, superseding the AgentAPI -plan of 2026-07-08. AgentAPI is retained as a fallback injector and has not been needed. See +plan of 2026-07-08. AgentAPI was considered and dropped: it was never built, so there is no +fallback injector to switch to. See **[Message Server](2-Message-Server)** for the design and **[13 User Guide](13-User-Guide)** for how -to run it. +to run it. \ No newline at end of file diff --git a/_Sidebar.md b/_Sidebar.md index 6f955f6..10dd9d2 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -1,4 +1,4 @@ -### 📖 claude-bridge +### 📖 fleet [Home](Home) — overview & the decision @@ -11,7 +11,7 @@ 5. [Operations](5-Operations) — ⚫ superseded by 13 6. [Team](6-Team) — orchestrating a mixed fleet 7. [Use Cases](7-Use-Cases) — the review scenario + mechanisms -8. [Roadmap](8-Roadmap) — stages, tech stack, tickets +8. [Roadmap](8-Roadmap) — delivery record: what is live, what is off, what was dropped 9. [Implementation](9-Implementation) — as-built code map · classes · flows · state machines 10. [Cross-Host Messaging](10-Cross-Host-Messaging) — broker topology · exchanges · queues per entity 11. [Features](11-Features) — what it can do · the knob that turns it on · why · the gotcha @@ -19,4 +19,4 @@ 13. **[User Guide](13-User-Guide)** — 🟢 install · configure · run · delegate · the traps --- -🟢 herdr-centric `fleetd` · AgentAPI = fallback +🟢 herdr-centric `fleetd` · AgentAPI = research, never built