#168: revise the front matter, Approaches, and the OpenCode port

- Home and _Sidebar: renamed the product to fleet / fleetd. Dropped the claim
  that AgentAPI is a "swappable fallback injector" — it was never built, and no
  AgentAPI code exists under fleetd/src/main/java.
- Home also lost two claims that chapters 1 and 2 removed today: there is no SSE
  route, and the page no longer names a gateway host as if it were fixed. The
  allowed hosts are a config value and differ per deployment.
- Home no longer carries a release number, a ticket count or a test total. Those
  go stale in days and then read as current facts; 8-Roadmap holds the record.
- 3-Approaches keeps AgentAPI as discarded research, which is that page's job,
  but never in the present tense. Evidence: a search for agentapi under
  fleetd/src/main/java finds nothing, and both Profile.kind values run through
  HerdrPeerLauncher.
- 12-Claude-to-OpenCode: the sample mount name was `fleetd`, which teaches a
  second product name. Every launcher writes the same constant,
  PeerLauncher.MCP_MOUNT_NAME = "fleet". A hand-written config using another key
  mounts under a name the role-detection ladder never checks.
Dai Ha
2026-08-31 10:35:11 +07:00
parent 06cceeee7c
commit e9c4f96b6f
4 changed files with 80 additions and 55 deletions
+22 -12
@@ -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
+32 -21
@@ -20,7 +20,7 @@ flowchart TD
Q -->|"raw keystrokes into the tmux pane"| TMUX["tmux send-keys<br/>/ PTY paste"]
HD --> V0["✅ our leading choice<br/>(status events + multiplex;<br/>symmetric single-host)"]
AA --> V1["◐ fallback injector<br/>(swappable behind fleetd)"]
AA --> V1["✕ never built<br/>(discarded research)"]
SDK --> V2["✅ if driver is our own code"]
BUS --> V3["⚠ async bus events only<br/>(lands at turn boundary)"]
TMUX --> V4["⚠ fragile — the raw primitive<br/>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)
</content>
- [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)
+23 -19
@@ -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<br/>(Claude Code, env CLEAN)<br/>MCP client"]
subgraph BD["fleetd — standalone daemon (not a claude process)"]
SRV["SERVER face<br/>MCP · REST/SSE · policy"]
SRV["SERVER face<br/>MCP · REST · policy"]
CLI["CLIENT face<br/>status-gated injector · herdr socket"]
SRV --> CLI
end
HERDR["herdr<br/>panes · agent-status"]
W["member pane<br/>ANTHROPIC_BASE_URL set<br/>MCP client"]
M["llm.ltms.dev<br/>(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.
+3 -3
@@ -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