diff --git a/13-User-Guide.md b/13-User-Guide.md new file mode 100644 index 0000000..c5c5417 --- /dev/null +++ b/13-User-Guide.md @@ -0,0 +1,444 @@ +# 13 โ€” User Guide + +> **Status: ๐ŸŸข Written against the running system, 2026-08-17 (release 1.1).** +> Every command here was run on this host. Where a claim is not measured, it says so. +> +> This is the operator page. Chapters 1โ€“3 say *why the system is built this way*, 9โ€“12 say *how the +> code is put together*, 11 lists *what it can do*. This page is the one you read when you have to +> bring it up, run it, or fix it. It assumes you have done this before and forgotten the details. + +--- + +## 1. What this is, and what it is not + +`bridged` is a **message bus between AI agent sessions**. One session orchestrates (the **lead**), +and it delegates work to **members** running in other terminal panes, often on other models and +other vendors. All traffic goes through `bridged`'s MCP tools. No session talks to another session, +to a broker, or to the network directly. + +```mermaid +flowchart LR + LEAD["lead session
(Claude Code, subscription)"] + subgraph BD["bridged โ€” a plain Java daemon"] + SRV["MCP + REST
policy, authz, lifecycle"] + INJ["injector
status-gated"] + SRV --> INJ + end + HERDR["herdr
owns panes and PTYs"] + M1["member pane
claude-code"] + M2["member pane
opencode"] + GW["llm.ltms.dev
the one gateway"] + + LEAD -->|"bridge_send"| SRV + M1 -.->|"bridge_reply"| SRV + M2 -.->|"bridge_reply"| SRV + INJ -->|"unix socket"| HERDR + HERDR --> M1 + HERDR --> M2 + M1 --> GW + M2 --> GW + + classDef ext fill:#2b6cb0,stroke:#1a365d,color:#ffffff; + classDef core fill:#2f855a,stroke:#22543d,color:#ffffff; + class LEAD,GW ext + class SRV,INJ,HERDR core +``` + +*Figure 1 โ€” who talks to whom. The lead never touches herdr; the members never touch each other.* + +**What it is not**, and each "not" is a decision, not a gap: + +| Not | Why | +|---|---| +| Not a build system | It does not compile, lint, or test your code. Members run whatever the repo gives them. The lead verifies. | +| Not an environment manager | It does not install toolchains or set up a member's language runtime. It only sets a small, named set of environment variables at spawn. | +| Not a Claude-only tool | `kind: claude-code` and `kind: opencode` are both first-class. A member can be GPT, DeepSeek, or Claude. | +| Not a scheduler with a budget | There is **no cost metering and no refusal on spend**. `maxLoad` per profile is the only throttle you have. See ยง3. | +| Not a security boundary against your own members | A member runs as you, on your machine. `memberCredentials` limits which secrets it sees. It does not sandbox anything else. | + +**The two invariants.** Break either and nothing else matters. + +1. The lead **never** sets `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN`. It stays on the + subscription. Only the daemon moves a member off it, at spawn. +2. The bridge is the **only** channel. Text printed in a pane reaches nobody. An answer that is not + in a `bridge_*` call is discarded silently. + +--- + +## 2. Install + +Four things must be true before anything works. In order, because each one depends on the last. + +### 2.1 herdr + +`bridged` does not own terminals. `herdr` does. `bridged` drives it over a Unix socket. + +```bash +herdr --version # 0.8.0 on this host +curl -s http://127.0.0.1:8765/healthz +# {"status":"ok","herdr":{"version":"0.8.0","protocol":19}} +``` + +**The protocol number is the thing to check, not the version.** `bridged`'s adapter speaks one +herdr wire protocol. If herdr is upgraded and the protocol moves, `/healthz` still says `ok` โ€” +because the socket connects โ€” and **every spawn fails**. Green health with a broken fleet is the +normal way this breaks. See trap 2 in ยง6. + +### 2.2 The daemon + +Build and run from this repo. There is no POM at the repo root: + +```bash +mvn -f bridged/pom.xml clean install # never pipe this โ€” a pipe hides BUILD FAILURE +java -jar bridged/target/bridged.jar bridged/bridged.yaml +``` + +In practice you never run those two by hand. Use the script (ยง4). + +### 2.3 Secrets, and the login shell + +All credentials live in one file: `${SHARED_ENV}/tools/secrets.sh`. It is sourced only by a **login +shell**. This one fact causes more lost hours than anything else in the system. + +- The daemon inherits `AI_GATEWAY_TOKEN` and `WORKER_GITEA_TOKEN` **from the shell that started it**. +- Start it from a non-login shell and both are empty. The daemon boots, `/healthz` is green, and + nothing complains. +- The failure surfaces hours later: a gateway profile gets a 401, or a member cannot open a pull + request. + +`launchd` does not run a login shell either. That is the only reason +`scripts/bridged-launchd-wrapper.sh` exists โ€” it `exec`s `zsh -lc` so the store gets sourced, while +keeping one process so launchd's PID tracking still works. Read its header; it explains the trap +better than this paragraph. + +> On this host the daemon is **not** under launchd. It runs as a plain `java -jar` started by the +> redeploy script from a login shell. `launchctl list | grep bridg` returns nothing. + +The daemon logs which required secret names resolved at startup +(`Bridged.reportRequiredSecrets`). Read those lines. But note the gap: it **skips profiles marked +`subscription: true`**, on purpose, because they need no token. So a green secret report says +nothing about your subscription profiles. + +### 2.4 The lead's tab + +The daemon finds the lead by its **herdr tab label**, and by nothing else: + +```yaml +fleet: + leaders: + opus: + tab: "lead: opus" # must match the real tab label +``` + +Matching is case-insensitive and both ends are stripped. Internal spacing is **not** forgiving: +`"lead: opus"` and `"lead:opus"` are different. A session whose tab does not match is resolved as a +**worker**, and every orchestration call it makes is refused. + +The old `terminal:` key is gone. A daemon at CB-579 or later **refuses to start** if `terminal:` is +still in the config, and also if `tab:` is missing. That refusal is deliberate โ€” a silently demoted +lead was worse than a daemon that will not boot. + +--- + +## 3. Configure + +`bridged/bridged.yaml` is the live config. It is **gitignored**. `bridged.example.yaml` is the +tracked, documented copy. Two consequences you will meet: + +- Members cannot see the live config. They work in worktrees of the tracked repo. So a change to + what a config **key means** can pass review, merge green, and break the running fleet โ€” because + nobody who reviewed it could see the file it breaks. Check and fix the live config yourself at + merge time. +- The example file is the documentation. A key that is read by code but missing from the example is + a real defect (that is how `subscription:` stayed undocumented for months โ€” CB-610). + +### The knobs that cost money + +This is the section to reread before you change anything. + +| Knob | What it does | The cost | +|---|---|---| +| `subscription: true` | The member runs on **your own Claude plan**. No `ANTHROPIC_BASE_URL`, no token โ€” it inherits your Claude Code auth. | Every spawn bills your plan and eats your usage limit. Off-subscription is the whole point of the daemon, so treat `true` as a deliberate exception. Mutually exclusive with `baseUrl`; setting both is refused at load. | +| `maxLoad` | Members allowed at once on this profile. | **The only throttle that exists.** There is no metering, no budget, no refusal on spend. When `maxLoad` is reached, a spawn is refused with `no capacity โ€ฆ no fallback` โ€” it does not fall back to a cheaper profile. | +| `weight` | Share of automatic placement. | `weighted` placement is **not cheapest-first**. It spreads by ratio across every profile that has a free slot, so paid spawns happen while the free local profile is idle. The workaround here is `local.weight: 100`; the real fix is open as CB-589. | +| `fleet.reviewers` | Which profiles an unqualified reviewer spawn may land on. | Review is the fan-out step โ€” several members per pull request. Leaving the free profile out of this pool was the single largest avoidable cost in the fleet. | + +Live profiles on this host: + +| Profile | Kind | Model | maxLoad | Pays | +|---|---|---|---|---| +| `local` | claude-code | deepseek-v4-flash via `llm.ltms.dev/anthropic` | 2 | gateway | +| `local-direct` | claude-code | deepseek-v4-flash direct to `gx00.gw:8000` | 2 | nothing, LAN only, `weight: 0` | +| `gx` | opencode | `gx/deepseek-v4-flash` via `llm.ltms.dev/v1` | 2 | gateway | +| `opus` | claude-code | `claude-opus-5` | 1 | **your Claude plan** | +| `sonnet` | claude-code | `claude-sonnet-5` | 3 | **your Claude plan** | +| `sol` | opencode | `openai/gpt-5.6-sol` | 1 | OpenAI | +| `terra` | opencode | `openai/gpt-5.6-terra` | 2 | OpenAI | + +`sol` and `terra` share **one** OpenAI credential. When it hits its limit, both die at once, and an +opencode member dying looks like a short clipped reply rather than an error. + +### The gateway + +`llm.ltms.dev` is the one front door, and `AI_GATEWAY_TOKEN` is the single key. + +- `kind: claude-code` โ†’ `https://llm.ltms.dev/anthropic`. **Never `/v1`** โ€” on `/v1` the model's + thinking output silently disappears. +- `kind: opencode` โ†’ `https://llm.ltms.dev/v1`. The path is taken as-is. + +Both known gateway ceilings were fixed on 2026-08-15: the body limit went 32 KiB โ†’ 32 MiB, and the +route timeout 60 s โ†’ 86400 s. The remaining risk is an Envoy HTTP/1.1 bug that can truncate a +stream with a `200` and no terminator. Our members cannot detect that; it looks like a short answer. + +### Credentials a member can see + +`memberCredentials` is deny-by-default. You name what is allowed; everything else known is replaced +with a sentinel string. Current state: **34 known, 7 allowed, 29 blocked**, measured inside a live +member pane on 2026-08-17. + +It has **two halves**, and only both together work: + +1. The launcher writes the member's environment at spawn. +2. A `BRIDGED_MEMBER`-guarded block at the **end** of `${SHARED_ENV}/tools/secrets.sh` re-applies it. + +Half 2 is not optional. The member's pane runs a **login shell**, which re-sources the whole secret +store and overwrites whatever the launcher set. The guarded block must be the last thing in the +file, or the store overwrites it in turn. + +The policy is re-read **on every spawn**, so an allow-list edit needs no restart. That is measured, +not assumed. + +> Rule in force: only the lead and architects may use `GITEA_ACCESS_TOKEN`. Members get +> `WORKER_GITEA_TOKEN`, which is a minimal `write:repository` token, never the admin one. + +--- + +## 4. Run, and prove it runs + +### Start and restart + +Use the script. Do not hand-roll the steps. + +```bash +scripts/redeploy-bridged.sh --check # read-only: reports state, changes nothing +scripts/redeploy-bridged.sh # build, confirm drain, restart, verify +scripts/redeploy-bridged.sh --yes # skip the drain prompt (fleet already checked) +scripts/redeploy-bridged.sh --no-build # restart the jar you already have +``` + +Run `--check` first, always. It is the only thing that reports whether the forge token resolves, +and it reports that **without printing the value**. + +The script builds **before** it stops anything, so a failed build never leaves the fleet down. It +waits for the old process to exit instead of assuming. It polls `/healthz`. And it anchors its log +checks to a marker taken before the restart, so old errors cannot be misread as new ones. + +**A merge is not a deployment.** The running daemon holds the jar it started with. Code merged to +`main` does nothing until you rebuild and restart. Saying "shipped" about code the live daemon has +never loaded is a false report. + +**Drain first.** `bridge_list`, collect anything you still want with `bridge_poll`, then +`bridge_stop` each member. A restart drops in-flight tickets and rendezvous. A member's report is +not recoverable once its ticket is gone. + +### Verify โ€” `/healthz` is not enough + +`/healthz` proves the socket connects. It does not prove a spawn works, that identity resolves, or +that the new jar is the one running. Four checks, in order: + +1. **A fresh boot line.** Confirm a new `bridged listening` line at the end of `bridged/bridged.out`, + dated after the restart. An old daemon that never died looks identical from outside. +2. **Deferred keys.** The startup log names which config keys it accepted and which it deferred. A + deferred key needing a restart is usually the whole reason you restarted. Read those lines rather + than assuming. +3. **Identity.** `bridge_whoami` must still answer `primary`. If the tab label changed, the lead is + now a worker and every orchestration call is refused. +4. **A real spawn.** Spawn one cheap member and stop it. This is the only check that catches a herdr + protocol mismatch. + +> Restarting the daemon **cuts your own MCP mount**, and it does not reconnect. So you cannot run +> `bridge_whoami` from the session that restarted it. Ask the operator to run `/mcp` to reconnect. +> This is why the restart is done from the lead but verified after a reconnect. + +--- + +## 5. Delegate + +Eleven tools. This is the whole surface. + +| Intent | Tool | +|---|---| +| Confirm your own role | `bridge_whoami` | +| See backends available | `bridge_profiles` | +| Start a member | `bridge_spawn{role?, profile?, cwd?, worktree?, ticket?, sessionName?, resumeSessionId?}` | +| See the fleet | `bridge_list` โ†’ `leads` + `members` | +| One member's state | `bridge_status{sessionId}` | +| Delegate, blocking | `bridge_send{sessionId, content}` | +| Delegate, long task | `bridge_send{sessionId, content, wait:false}` โ†’ ticket | +| Answer a member's `bridge_ask` | `bridge_send{turnId, content}` โ€” **not** `sessionId` | +| Collect a reply | `bridge_poll{ticket}` or `bridge_poll{target}` | +| Clear a reply from the inbox | `bridge_ack{target, msgId}` โ€” **`target`, not `ticket`** | +| Member ends its turn | `bridge_reply{content}` | +| Member asks the lead | `bridge_ask{question}` | +| Tear down | `bridge_stop{paneId}` | + +### The loop + +```mermaid +sequenceDiagram + participant L as lead + participant B as bridged + participant M as member + L->>B: bridge_spawn โ€” all units first + L->>B: bridge_send with wait false โ€” then all sends + B-->>L: ticket + B->>M: injected when the member is idle + M->>B: bridge_reply + B-->>L: nudge into the lead's own pane + L->>B: bridge_poll by ticket + L->>B: bridge_ack by target and msgId + L->>B: bridge_stop by paneId +``` + +*Figure 2 โ€” the delegation loop. Spawn is separate from send on purpose.* + +**Spawn every unit first, then send them all.** Spawning and sending in one loop is how parallel +work silently becomes serial. It is the most common way this whole layer is wasted. + +**Prefer `wait:false`.** A blocking `bridge_send` is capped by **your own MCP client timeout**, +about 60 seconds โ€” far below any real task's runtime. The cap is in your client, not in the daemon, +so no server setting fixes it. + +**Line 1 of every brief names a skill.** `Load the implementer skill.` or `Load the reviewer skill.` +Those skills are opt-in, and that line is what makes them reliable. The brief must be +self-contained: the member sees your message and the repo, and nothing of your context, your plan, +or your screen. + +**Since CB-588, a `wait:false` ticket that finishes nudges your pane by itself.** You do not have to +poll on a timer. It needs an injectable pane and is capped at 5 pending nudges, so still watch +anything you cannot afford to lose. + +### Who may call what + +| Call | Allowed | +|---|---| +| spawn / stop / drain | lead only | +| send | lead or architect | +| reply / ask | any member, **only as itself** | +| read / status | any authenticated caller | + +Identity comes from the connection, never from an argument. You cannot act as another session. A +call outside your role is refused, not queued. + +### The lead's own job + +Delegation does not delegate responsibility. These stay with you and are not delegable: the +conversation with the operator, decomposition, the final judgment, **verification**, and the +**merge**. Merging on a reviewer's word is delegating the merge by proxy. + +A member cannot run your IDE tooling. Any forge tools it appears to have hold a deliberately blocked +credential and fail every call. And a piped command hides failures behind a zero exit code. So never +promote a member's "clean" to a fact โ€” re-run the build yourself. + +--- + +## 6. When it breaks + +Twelve traps, all hit for real this year. Grouped by where they bite. + +### Bring-up + +**1. The daemon started from a non-login shell.** +Symptom: everything green for hours, then a member cannot open a pull request, or a gateway profile +gets a 401. Nothing logs it at the time. Fix: `scripts/redeploy-bridged.sh --check` is the only +thing that reports it. Restart from a login shell, or via the launchd wrapper. + +**2. `/healthz` is green and every spawn fails.** +Cause: herdr was upgraded and its wire protocol moved past what the adapter speaks. The socket still +connects, so health is `ok`. Fix: check `protocol` in the `/healthz` body, and verify any restart +with one real spawn โ€” not with health. + +**3. The lead is resolved as a worker.** +Cause: the herdr tab label no longer matches `fleet.leaders.*.tab`. Every orchestration call is +refused. Fix: match the tab exactly โ€” internal spacing counts. Note the old `terminal:` key is +removed; a current daemon refuses to start if it is still present. + +**4. A merge is not a deployment.** +The running daemon holds its original jar. Rebuild and restart, then prove it with a **fresh** +`bridged listening` line. Also: the restart cuts your own MCP mount and it never reconnects, so you +cannot verify identity from that session โ€” ask for `/mcp`. + +### Losing a member's work + +**5. The ticket expired.** +Ticket time-to-live is about 10 minutes. After that `bridge_poll{ticket}` returns +`timed_out_working`. The member is usually fine and its real answer arrives later โ€” in the member's +**inbox**. Drain it with `bridge_poll{target}`, then `bridge_ack{target, msgId}`. + +**6. Reading the reply inbox is destructive.** +`GET /sessions/{id}/replies` drains on first read. If you `curl` it through `head` or a parser that +dies, the reply is gone. Always redirect it to a **file** first. + +**7. The idle reaper ate the report.** +A member that is `done` holds its report only until `lifecycle.idleTtlSeconds` (1800 s here). Then +the ticket, the pane, and the report are all gone, with no scrape fallback. Poll before the reaper. + +**8. A second send to a busy member never lands.** +`timed_out_queued` means **never delivered** โ€” the member is fine. Worse, the stale brief stays +queued and restarts the member when it next goes idle. Back up the member's commit first: +`git fetch :refs/backup/x`. + +**9. `liveStatus: working` is not progress.** +A member can report busy for hours doing nothing. Check file modification times in its worktree and +snapshot `git diff` before and after. Do not read its terminal โ€” use `bridge_status`. + +**10. Never brief a member to "ask me".** +`bridge_ask` blocks for about 55 seconds and no nudge extends it. It is also invisible to +`bridge_poll`. If you are running async, you will not see the question in time. Decide before you +delegate, or give the member an explicit default. + +### Merging a member's work + +**11. The reported branch is not the branch it committed to.** +`bridge_list` reports the branch **spawn provisioned**, not the one the member actually used. Check +`git -C branch --show-current`, or you push an empty ref and the merge says +"Already up to date". + +**12. Members see a months-old wiki, and cannot see the live config.** +`wiki/` is a submodule whose pointer is never advanced, so a member's checkout is a stale snapshot. +Never brief "read `wiki/โ€ฆ`" โ€” paste the text, and commit the member's wiki entry yourself. Same +shape for `bridged.yaml`: it is gitignored, so a member cannot see the file its change may break. + +### The general shape behind several of these + +A checker that is **narrower than what everyone believes it checks**, with nothing saying so. The +credential probe hardcoded 31 names against a policy of 29 and exited `0`. A config test checked +only column-0 keys, so a nested key that bills your Claude plan stayed undocumented. A scheduled +sweep never ran once because its gate used a sentinel in a numeric field. + +Two habits fix most of it: **make a checker print its own denominator** ("checked 26 of 29", not +"26 blocked"), and when a green check is the evidence for a claim, **ask what it does not cover +before believing it**. + +--- + +## 7. Where to look next + +| You want | Go to | +|---|---| +| Why the design is herdr-centric at all | [1 Architecture](1-Architecture), [3 Approaches](3-Approaches) | +| The daemon's own design | [2 Message Server](2-Message-Server) | +| The as-built code map, classes, state machines | [9 Implementation](9-Implementation) | +| What a capability does, the knob, why it exists, the gotcha | [11 Features](11-Features) | +| Orchestrating a mixed-vendor fleet | [6 Team](6-Team), [7 Use Cases](7-Use-Cases) | +| Adding a second host or a non-Claude peer | [10 Cross-Host Messaging](10-Cross-Host-Messaging), [12 Claude โ†’ OpenCode](12-Claude-to-OpenCode) | +| What is left to build | [8 Roadmap](8-Roadmap) | +| The rules every session must follow | `CLAUDE.md` in the repo โ€” it loads into every session | +| Rendezvous, `bridge_ask`, detached delivery, turn-done fallback | `docs/MCP-Contract.md` **ยง6 only** โ€” the rest of that page is a pre-build design doc and its tool names never caught up with the code | +| Every config key, documented | `bridged/bridged.example.yaml` | + +**The one thing that is easy to forget.** This repo *is* the bridge, so the charter block in +`CLAUDE.md` is not documentation about someone else's system โ€” it is the instruction surface this +codebase ships. A code change that quietly makes it untrue is an incomplete change. The block in +`CLAUDE.md` and the template in [7 Use Cases](7-Use-Cases) must stay byte-identical; there is a +check script in `CLAUDE.md` that proves it. diff --git a/4-Setup.md b/4-Setup.md index 680bd4f..e8e78f8 100644 --- a/4-Setup.md +++ b/4-Setup.md @@ -1,40 +1,25 @@ # 4. Setup -> **Status:** ๐ŸŸ  Stub โ€” scope defined, procedure not yet written. `bridged` is at the design -> stage ([Message Server](2-Message-Server)); concrete install steps land with **M0โ€“M1** of the build plan. +> **Status: โšซ Superseded by [13 User Guide](13-User-Guide) ยง2 (2026-08-17).** +> This page was written in the design era as a scope note and the procedure was never filled in. +> Its content had gone wrong in every detail โ€” a decommissioned model host, port 8080, herdr +> protocol 14, a systemd unit that does not exist on this host, Redis Streams / NATS that were never +> built. Rather than leave those claims in place, the page now points at the written procedure. -This page will cover standing up the bridge on an off-subscription worker host. +**Go to [13 User Guide](13-User-Guide) โ†’ ยง2 Install.** It covers, against the running system: -## Scope (what this page will contain) +- herdr, and why you check the **protocol number** rather than the version; +- building and starting the daemon; +- the **login shell** rule for `${SHARED_ENV}/tools/secrets.sh` โ€” the single most expensive trap in + bring-up, and why `scripts/bridged-launchd-wrapper.sh` exists; +- the lead's **tab label**, which is how the daemon resolves who the lead is. -1. **Prerequisites** โ€” [herdr](https://herdr.dev) installed and its server running; a worker - `claude` that inherits your `CLAUDE.md`/hooks/skills/MCP; a reachable worker model - (`ollama.ltms.dev` or GX10 vLLM) with a bearer token. -2. **herdr** โ€” start the headless server; confirm the socket at - `~/.config/herdr/herdr.sock` (or `HERDR_SOCKET_PATH`); verify with `ping` (expect - `protocol: 14` on herdr 0.7.0) and `workspace.list`. -3. **`bridged`** โ€” deploy the binary, `bridged.yaml` (worker model, `base_url` allowlist, - auth token, bind address), and a **systemd** unit ordered *after* herdr. -4. **Worker session** โ€” create the first worker pane with the env-prefixed launch line - (`ANTHROPIC_BASE_URL=https://ollama.ltms.dev ANTHROPIC_AUTH_TOKEN=โ€ฆ claude`); confirm the - subscription guard accepts it and `pane.process_info` shows the expected egress host. -5. **MCP mount (unified, both sides)** โ€” register `bridged` as an MCP server on the primary - **and** each worker with one line โ€” - `claude mcp add --transport http bridge http://127.0.0.1:8080/mcp` (or a shared - `.mcp.json` / `CLAUDE.md` entry every session inherits). The primary then delegates via the - `bridge_send` tool (single blocking call per delegation) and workers reply via - `bridge_reply`. Same-host needs nothing more โ€” `bridged` delivers async by injecting an idle - pane. Only for a **split-host** primary (not a herdr pane) add a `Stop`-hook that long-polls - **`bridged`** (never a broker) for detached wake-ups. -6. **Topology choice** โ€” single-host vs split-host (see [Message Server](2-Message-Server) โ†’ *Deployment - model*). If durability or cross-host async is needed, configure `bridged`'s **internal** queue - (Redis Streams / NATS); it stays behind the gateway โ€” no Claude session connects to it. +## The one rule that was already right on this page -## Non-negotiable during setup - -The **primary** host/process must **never** be given `ANTHROPIC_BASE_URL`. Only worker panes -carry it. See [Architecture](1-Architecture) โ†’ *Subscription boundary*. +The **lead** host and process must **never** be given `ANTHROPIC_BASE_URL` or +`ANTHROPIC_AUTH_TOKEN`. Only member panes carry them, and only the daemon sets them, at spawn. See +[Architecture](1-Architecture) โ†’ *Subscription boundary*. ## Related -- [Message Server](2-Message-Server) ยท [Architecture](1-Architecture) ยท [Operations](5-Operations) ยท [Approaches](3-Approaches) +- [13 User Guide](13-User-Guide) ยท [2 Message Server](2-Message-Server) ยท [1 Architecture](1-Architecture) ยท [11 Features](11-Features) diff --git a/5-Operations.md b/5-Operations.md index a7d0bd8..405899c 100644 --- a/5-Operations.md +++ b/5-Operations.md @@ -1,39 +1,35 @@ # 5. Operations -> **Status:** ๐ŸŸ  Stub โ€” scope defined, runbook not yet written. Fills in as `bridged` reaches -> **M4 โ€” Harden** (auth/TLS, metrics, systemd) in the [Message Server](2-Message-Server) build plan. +> **Status: โšซ Superseded by [13 User Guide](13-User-Guide) ยง4 and ยง6 (2026-08-17).** +> This page was a design-era scope note; the runbook was never written. Several of its claims were +> overtaken by the build โ€” `recycle()` events do not exist, per-session authorization shipped +> (`auth/Authz`), and the internal broker is optional rather than required. -Day-2 runbook for a running bridge. +**Go to [13 User Guide](13-User-Guide):** -## Scope (what this page will contain) +- **ยง4 Run, and prove it runs** โ€” `scripts/redeploy-bridged.sh`, why a merge is not a deployment, + draining before a restart, and the four checks that go beyond `/healthz`. +- **ยง6 When it breaks** โ€” twelve traps hit for real this year, grouped by bring-up, losing a + member's work, and merging a member's work. -- **Health** โ€” `GET /healthz` liveness, `GET /metrics` (Prometheus), reading live - `agent_status` per session via `GET /sessions`, and confirming the **MCP endpoint** is - reachable from both the primary and the workers (`claude mcp list` shows `bridge` connected). -- **Restart & recovery** โ€” ordered restart (herdr before `bridged`); how `bridged` - re-attaches to existing panes via `workspace.list`/`pane.list`; internal broker/queue replay of unacked items. See - [Architecture](1-Architecture) โ†’ *Failure modes & single points of failure* for what each outage costs. -- **Model swaps** โ€” repoint a worker to a different `base_url`/model by recycling its pane - (Ralph loop); the subscription guard re-validates the new host against the allowlist. -- **Lifecycle / context ceilings** โ€” observing recycle events; confirming workers externalize - state (git + `STATE.md`) before a recycle so continuity survives (see [Message Server](2-Message-Server) โ†’ - *Worker session lifecycle*). -- **Troubleshooting** โ€” stuck `working` (model endpoint down), `blocked` awaiting input, - injection collisions on a hand-driven pane, envelope-vs-scrape reply mismatches. -- **Security ops** โ€” token rotation, keeping the port off public interfaces; note that one - `bridged` is currently **one trust domain** (no per-session authz yet โ€” [Message Server](2-Message-Server) - โ†’ *Security*). +## What actually shipped, against what this page predicted -## Guardrails to watch (from [Architecture](1-Architecture)) +| This page said | What is true now | +|---|---| +| "no per-session authz yet" | Per-session authorization shipped. `auth/Authz` holds the role table; identity comes from the connection, never from an argument. | +| "observing recycle events" | There is no `recycle()`. Lifecycle is an idle reaper plus a context cap โ€” `lifecycle.idleTtlSeconds`, `lifecycle.contextCap`, `lifecycle.drainTimeoutSeconds`. | +| "internal broker/queue replay" | The broker is optional. With `broker:` commented out the daemon uses an in-memory inbox. When it is on, it is **LavinMQ**, never RabbitMQ. | +| "`GET /healthz` liveness" | Still true, and still not sufficient. Health can be green while every spawn fails, because the socket connects but the herdr wire protocol has moved. Check `protocol` in the body and verify with a real spawn. | +| "recycling a pane to swap models" | Spawn a member on a different profile instead. Profiles are the unit of model choice. | -- The **primary must never perpetual-poll** โ€” quota burn. Async wake-ups are `bridged` - inject-on-idle by default; the `Stop`-hook is only the split-host-primary exception, and it - polls `bridged` (never a broker). -- Cross-agent ping-pong needs a round/turn budget โ€” enforced centrally in `bridged` (sole - gateway), not per-session sentinels. -- The **internal** broker/queue must run with **ack + visibility timeout + consumer groups** - so a mid-turn crash re-delivers instead of dropping. +## Guardrails that still hold + +- The lead must **never** busy-poll a member's pane โ€” it burns your subscription for nothing. Async + wake-ups are injected into the lead's own pane when a ticket goes terminal (CB-588). +- Keep the port off public interfaces. The daemon binds loopback and trusts it; the exposure check + only fires on a non-loopback bind. +- Rotate tokens. Members get a minimal `write:repository` forge token, never the admin one. ## Related -- [Message Server](2-Message-Server) ยท [Architecture](1-Architecture) ยท [Setup](4-Setup) ยท [Approaches](3-Approaches) +- [13 User Guide](13-User-Guide) ยท [2 Message Server](2-Message-Server) ยท [1 Architecture](1-Architecture) ยท [11 Features](11-Features) diff --git a/Home.md b/Home.md index 7baa296..632658a 100644 --- a/Home.md +++ b/Home.md @@ -1,12 +1,16 @@ # claude-bridge -A **subscription-safe bridge** that lets a primary **Claude Code (Opus 4.8, on Pro/Max)** -session drive a **secondary Claude agent running a different model** via its own -`ANTHROPIC_BASE_URL` โ€” without ever putting a proxy on the primary. +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. + +> **New here, or here to operate it? Start at [13 User Guide](13-User-Guide).** That page is written +> against the running system: install, configure, run, delegate, and the traps. This page explains +> 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 the worker a *real Claude Code process* so it -> inherits `CLAUDE.md`, hooks, skills, and MCP โ€” just pointed at a cheaper/local model. +> on GX10 DeepSeek). `claude-bridge` 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. ## Leading approach โ€” herdr-centric message server (`bridged`) @@ -17,9 +21,9 @@ workers mount** โ€” one unified Claude setup and the **sole communication gatewa stays for non-Claude clients; any broker is `bridged`-internal, below the gateway). herdr owns the PTYs, multiplexing, persistence, and **agent-status events**; `bridged` owns policy (subscription boundary, session lifecycle, status-gated -delivery) and the client contract. The worker `claude` launches with -`ANTHROPIC_BASE_URL=https://ollama.ltms.dev` + a bearer token; the primary Opus stays -env-clean and calls `bridged`'s MCP tools. +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 `bridged`'s MCP tools. ```mermaid flowchart LR @@ -30,8 +34,8 @@ flowchart LR SRV --> CLI end HERDR["herdr
panes ยท agent-status"] - W["worker claude pane
ANTHROPIC_BASE_URL set
MCP client"] - M["ollama.ltms.dev
(worker model)"] + W["member pane
ANTHROPIC_BASE_URL set
MCP client"] + M["llm.ltms.dev
(the one gateway)"] OPUS -->|"MCP bridge_send (blocks)"| SRV W -.->|"MCP bridge_reply"| SRV @@ -72,14 +76,24 @@ Read in order (the sidebar mirrors this): 1. **[Architecture](1-Architecture)** โ€” process model, the two invariants, two traffic modes 2. **[Message Server](2-Message-Server)** โ€” ๐ŸŸข **`bridged`**, the herdr-centric message server (primary approach) 3. **[Approaches](3-Approaches)** โ€” herdr-centric vs AgentAPI vs Agent SDK vs bus/tmux (research matrix) -4. **[Setup](4-Setup)** โ€” running herdr + `bridged` + a worker pointed at `ollama.ltms.dev` -5. **[Operations](5-Operations)** โ€” health, restart, model swaps, troubleshooting -6. **[Team](6-Team)** โ€” team-lead orchestrating a mixed Claude + local-LLM worker fleet -7. **[Use Cases](7-Use-Cases)** โ€” flagship code-review conversation (Opus โ†” gx00 worker) + the five mechanisms -8. **[Roadmap](8-Roadmap)** โ€” walking-skeleton-first stages, tech stack, and tickets (Stage 1 detailed) +4. **[Setup](4-Setup)** โ€” ๐ŸŸ  stub, never written. Use **[13 User Guide](13-User-Guide) ยง2** instead. +5. **[Operations](5-Operations)** โ€” ๐ŸŸ  stub, never written. Use **[13 User Guide](13-User-Guide) ยง4 and ยง6** instead. +6. **[Team](6-Team)** โ€” a lead orchestrating a mixed-vendor fleet +7. **[Use Cases](7-Use-Cases)** โ€” the code-review scenario, the mechanisms, and the portable `CLAUDE.md` block +8. **[Roadmap](8-Roadmap)** โ€” stages, tech stack, and tickets +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 it exists, the gotcha +12. **[Claude โ†’ OpenCode](12-Claude-to-OpenCode)** โ€” porting a workspace to a second host +13. **[User Guide](13-User-Guide)** โ€” ๐ŸŸข **the operator page.** Install, configure, run, delegate, and the traps. ## Status -๐ŸŸข Design โ€” **herdr-centric `bridged` message server** selected as the primary approach -(2026-07-11), superseding the AgentAPI plan (2026-07-08). AgentAPI is retained as a fallback -injector. See **[Message Server](2-Message-Server)**. +๐ŸŸข **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. + +The **herdr-centric `bridged` 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 +**[Message Server](2-Message-Server)** for the design and **[13 User Guide](13-User-Guide)** for how +to run it. diff --git a/_Sidebar.md b/_Sidebar.md index 9ec486f..860c35a 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -7,8 +7,8 @@ 1. [Architecture](1-Architecture) โ€” system ยท 2 invariants ยท 2 modes 2. [Message Server](2-Message-Server) โ€” the `bridged` design 3. [Approaches](3-Approaches) โ€” transports compared, why herdr -4. [Setup](4-Setup) โ€” bring-up -5. [Operations](5-Operations) โ€” day-2 runbook +4. [Setup](4-Setup) โ€” โšซ superseded by 13 +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 @@ -16,6 +16,7 @@ 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 12. [Claude โ†’ OpenCode](12-Claude-to-OpenCode) โ€” porting a workspace to a second host +13. **[User Guide](13-User-Guide)** โ€” ๐ŸŸข install ยท configure ยท run ยท delegate ยท the traps --- ๐ŸŸข herdr-centric `bridged` ยท AgentAPI = fallback