diff --git a/2-Message-Server.md b/2-Message-Server.md index a9b4e65..2b60bd7 100644 --- a/2-Message-Server.md +++ b/2-Message-Server.md @@ -272,12 +272,12 @@ envelope" resolves to **a worker-side hook that runs our code at turn end**: and best-effort parses the last assistant block (reuse AgentAPI's `msgfmt`). This is lossy and is the reason the envelope path is preferred. -> **Open design question.** The envelope contract (fields, how the worker signals "blocked, -> need input" vs "done", how tool/diff artifacts are attached) is **not yet pinned down**. -> A `Stop`-hook only sees the transcript, so rich structure (e.g. a machine-readable result -> object) requires the worker to *emit* it deliberately — via a skill/slash-command it calls -> before finishing, or a convention the hook parses. Treat the envelope schema as a design -> deliverable for M2, not a solved detail. +> **Envelope contract — now specified.** A first-cut envelope (fields `from`/`to`/`session`/ +> `turn`/`corr`/`kind`/`body`, and the `kind` verb vocabulary that tells a recipient what to do +> next) is defined in [Use Cases → The ID contract](7-Use-Cases#mechanism-4--the-id-contract-envelope). +> Landing it is **Stage 2** ([Roadmap](8-Roadmap), ticket `CB-201`). The MCP-mounted worker +> emits its reply via `bridge_reply` (structured); the `Stop`-hook envelope remains the fallback +> for a non-MCP worker. Still open: exactly how tool/diff artifacts attach to `review.reply`. ## Delivery gating & races (the injector is a single writer) diff --git a/7-Use-Cases.md b/7-Use-Cases.md new file mode 100644 index 0000000..7ab8366 --- /dev/null +++ b/7-Use-Cases.md @@ -0,0 +1,190 @@ +# 7. Use Cases + +Concrete scenarios on the finalized architecture ([sole gateway](1-Architecture) + +[MCP-unified](2-Message-Server) + ccs-spawned workers). The **flagship** is a code-review +*conversation* between the primary Opus and a worker running the **GX10 vLLM** model — it +exercises every mechanism the system needs (trigger, discovery, ccs spawn, the ID contract, +and worker lifecycle), so we design it in full, then catalogue the rest. + +Everything below holds the invariants: the primary stays env-CLEAN on Pro/Max, the worker's +model/account come from a **ccs profile**, and both talk only to `bridged` over MCP. + +## Flagship — a review conversation (Opus ↔ gx00 reviewer) + +You're working in Opus on code in a workspace and want a second pair of eyes — an actual +back-and-forth **review conversation**, not a one-shot lint — with a cheap worker on the GX10 +vLLM model. Opus never leaves its subscription; it just calls MCP tools. + +```mermaid +sequenceDiagram + participant O as "Opus (primary, env CLEAN)" + participant B as "bridged (gateway)" + participant C as "ccs + herdr" + participant W as "worker · gx00-vllm" + + O->>B: "bridge_sessions() — what workers can I use?" + B-->>O: "profiles:[gx00-vllm=DeepSeek, …] · sessions:[]" + O->>B: "bridge_send(to: reviewer@gx00-vllm, {kind: review.request, diff, focus})" + Note over B: "guard: ccs env gx00-vllm → base_url host on allowlist ✓" + B->>C: "spawn: ccs gx00-vllm claude (new herdr pane)" + C->>W: "worker Ready → inject review.request" + activate W + W->>B: "bridge_reply({kind: review.reply, findings:[…]})" + deactivate W + B-->>O: "tool result = findings" + Note over O: "reads findings, wants to dig into one" + O->>B: "bridge_send(session s_7f3a, {kind: question — why finding 3 high-sev})" + B->>W: "inject into the SAME reviewer pane (context still warm)" + activate W + W->>B: "bridge_reply({kind: answer, …})" + deactivate W + B-->>O: "tool result = answer" + Note over O,W: "same reviewer session reused across turns → the diff stays in its context" +``` + +*Figure: discovery → trigger → ccs-spawn (guarded) → structured reply → follow-up on the same +warm session. Every arrow from Opus is an MCP tool call to `bridged`; the worker's provider +routing lives entirely in its ccs profile.* + +The rest of this page is the five mechanisms this scenario needs. + +## Mechanism 1 — triggering a subtask (`bridge_send`) + +Opus delegates with **one** tool call: + +```jsonc +bridge_send({ + "to": "reviewer@gx00-vllm", // role@profile, or a live session id + "kind": "review.request", + "body": { "workspace": "/repo", "base": "main", "head": "HEAD", + "focus": ["correctness","security"], "instructions": "…" }, + "mode": "block" // block (default) → reply as tool result; async → ticket +}) +``` + +`bridged` resolves the target (spawn-or-reuse, below), injects the turn into the worker's +herdr pane gated on `agent_status`, and — for `mode:"block"` — holds the call open until the +reply lands (worker `bridge_reply` or `agent_status=done`), returning it as the tool result. +Review turns are short, so they block; a long/detached job would use `mode:"async"` and come +back via idle-pane injection ([Mode 2](1-Architecture#traffic-two-modes-across-the-gateway)). + +## Mechanism 2 — worker discovery (knowing your choices) + +Opus shouldn't hard-code pane ids or guess what's available. `bridge_sessions()` returns both +what's **runnable** and what's **live**: + +```jsonc +bridge_sessions() → { + "profiles": [ // spawnable = the ccs worker roster (Mechanism 3) + { "name": "gx00-vllm", "model": "DeepSeek-V3", "host": "gx00.ltms.dev", "status": "available" }, + { "name": "ollama-local","model": "llama3.1", "host": "ollama.ltms.dev","status": "available" } + ], + "sessions": [ // live worker sessions right now + { "id": "s_7f3a", "role": "reviewer", "profile": "gx00-vllm", "agent_status": "idle" } + ] +} +``` + +This is the "let Opus know its worker choices" surface: the catalogue is `bridged`'s configured +roster of ccs worker profiles, and the live list is what it's already running. + +## Mechanism 3 — spawning via ccs profiles (the flexibility) + +**A worker's identity *is* a ccs profile.** `ccs [claude-args…]` launches `claude` +with that profile's account and provider routing, so `bridged` never hand-assembles env — it +just picks a profile: + +- **Config.** `bridged.yaml` lists worker profiles by name; each maps to a ccs profile + (`ccs api` profile pointing at GX10 vLLM / Ollama), an expected model, and an allowlisted + `base_url` host. +- **Spawn.** `bridged` tells herdr to open a pane and `send_text`: `ccs gx00-vllm claude` + (plus flags — workspace dir, an injected reviewer system prompt). No `ANTHROPIC_BASE_URL=…` + prefix; the profile carries it. +- **Guard (subscription boundary, in ccs terms).** Before spawning, `bridged` runs + `ccs env ` and validates the **resolved** `ANTHROPIC_BASE_URL` host against the + off-subscription allowlist. A profile that resolves to `api.anthropic.com` (a subscription + profile) is **refused as a worker** — that would burn your quota. The primary Opus is *your* + session on *your* subscription profile; `bridged` never spawns it. +- **Swap = repoint.** Changing the reviewer's model is choosing a different ccs profile — no + `bridged` code change. Add a profile → it appears in `bridge_sessions().profiles`. + +```mermaid +flowchart LR + CFG["bridged.yaml
worker profiles"] --> PICK["pick profile
gx00-vllm"] + PICK --> GUARD{"ccs env host
on allowlist?"} + GUARD -->|"no (api.anthropic.com)"| REJ["refuse — would burn subscription"] + GUARD -->|"yes (gx00.ltms.dev)"| SPAWN["herdr: ccs gx00-vllm claude"] + SPAWN --> PANE["worker pane
Ready"] + classDef ok fill:#2f855a,stroke:#22543d,color:#ffffff; + classDef bad fill:#b7791f,stroke:#7b341e,color:#ffffff; + class SPAWN,PANE ok + class REJ bad +``` + +*Figure: ccs profile selection is the spawn contract; `ccs env` is how the subscription guard +sees the resolved endpoint before committing.* + +## Mechanism 4 — the ID contract (envelope) + +Every message across the gateway is one envelope. It answers two questions each side needs: +**whose message is this** (`from`/`to`) and **what do I do next** (`kind`). + +| Field | Meaning | +|---|---| +| `v` | envelope version (`1`) | +| `from` | sender identity — `primary:opus`, or `role@profile` / session id for a worker | +| `to` | recipient — `role@profile` or a live session id | +| `session` | `bridged` session id (the worker session), e.g. `s_7f3a` | +| `turn` | monotonic counter within the session | +| `corr` | correlation id (`session#turn`) — `bridged`'s rendezvous matches a reply to its request | +| `kind` | **verb.noun** telling the recipient what to do (vocabulary below) | +| `body` | kind-specific payload | + +**`kind` vocabulary (the "what to do next"):** + +| kind | direction | body | +|---|---|---| +| `review.request` | primary → worker | `{workspace, base, head\|diff, files?, focus[], instructions}` | +| `review.reply` | worker → primary | `{summary, findings:[{file,line,severity,issue,suggestion}], verdict}` | +| `question` / `answer` | either way | free-form follow-up tied to the same `session` | +| `ask` | worker → primary | worker-initiated blocker (needs a decision) — via `bridge_ask` | +| `ack` / `status` | control | delivery/liveness, no new turn | + +The worker learns this contract from a **reviewer skill / `CLAUDE.md` snippet** injected at +spawn ("you are a reviewer; requests arrive as `review.request`; reply with `bridge_reply` +`kind: review.reply`"). So both ends know the sender and the required next action without a +held-open conversation — one request in, one structured reply out. + +## Mechanism 5 — worker lifecycle + +For a review conversation the reviewer is **persistent within a work session** so the diff +stays warm across follow-ups, and recyclable so it never outgrows its context window: + +- **Spawn-on-demand** — first `review.request` for a workspace spawns the profile's worker. +- **Reuse** — subsequent turns (`question`, next-file review) target the same `session`; its + context carries the code under review. +- **Recycle (Ralph loop)** — on a context/idle cap `bridged` checkpoints (commit + `STATE.md`) + and respawns fresh — **not** `claude --resume`. See + [Architecture → Worker lifecycle](1-Architecture#worker-lifecycle--the-ralph-loop). +- **Drain** — an `idle_ttl` (e.g. 20 min idle) or workspace close tears the pane down. + +Policy knobs in `bridged.yaml`: `idle_ttl`, `context_cap`, `max_workers_per_profile`. + +## More use cases (catalogue) + +Same machinery, different `kind`/lifecycle: + +| Use case | Shape | +|---|---| +| **Delegated refactor / codegen** | `bridge_send(kind: task.request)`, blocking; worker edits + commits on a branch; reply = diff summary. Opus reviews/merges. | +| **Test writing / log triage** | Bulk, cheap, parallelizable — a local worker profile; fire several concurrently and reduce. | +| **Parallel multi-file review** | A **fleet** of workers, one per area, fan-out/gather — see [Team](6-Team). | +| **Perpetual worker** | Long-running across many Ralph recycles, state on disk; woken by async injection ([Mode 2](1-Architecture)). | +| **Event-bus automation** | A webhook hits `bridged`'s REST ingress → injects an idle worker → result back to Opus or a chat bridge. No human in the loop. | + +## Related + +- **[Architecture](1-Architecture)** — invariants, two modes, lifecycle state machine. +- **[Message Server](2-Message-Server)** — the `bridged` design these mechanisms live in. +- **[Roadmap](8-Roadmap)** — the staged plan + tickets that build this review scenario first. +- **[Team](6-Team)** — the fleet/orchestration layer above a single review. diff --git a/8-Roadmap.md b/8-Roadmap.md new file mode 100644 index 0000000..515a6e4 --- /dev/null +++ b/8-Roadmap.md @@ -0,0 +1,162 @@ +# 8. Roadmap & Delivery + +A **walking-skeleton-first** plan: Stage 1 delivers the flagship +[review scenario](7-Use-Cases#flagship--a-review-conversation-opus--gx00-reviewer) end-to-end +(thin but real — Opus gets a review back from a gx00 worker over MCP), then later stages add +reply fidelity, the subscription guard, lifecycle, async, and hardening. This re-scopes the +[Message Server](2-Message-Server#build-plan-milestones) M0–M4 milestones around the scenario, +so there's something usable after every stage. + +## Stages + +```mermaid +gantt + title Indicative delivery sequence (relative durations, not committed dates) + dateFormat YYYY-MM-DD + axisFormat %b %d + section Skeleton + Stage 1 — review walking skeleton :s1, 2026-07-14, 10d + section Fidelity + Stage 2 — envelope + guard + reply :s2, after s1, 8d + section Scale + Stage 3 — lifecycle + discovery + fleet :s3, after s2, 10d + section Async + Stage 4 — async + durability + split :s4, after s3, 10d + section Harden + Stage 5 — auth · metrics · CI · systemd :s5, after s4, 8d +``` + +| Stage | Goal | Delivers (usable outcome) | +|---|---|---| +| **1 — Walking skeleton** | One review, happy path | Opus mounts `bridged` (MCP), calls `bridge_send` with a diff, gets a review back from a real `ccs gx00-vllm claude` worker. Single hardcoded profile, same host, no guard/lifecycle. | +| **2 — Contract + guard** | Trust the reply, trust the boundary | Structured [envelope](7-Use-Cases#mechanism-4--the-id-contract-envelope) + worker `bridge_reply`; reply rendezvous; subscription guard via `ccs env`; reviewer skill. | +| **3 — Lifecycle + discovery** | Reuse, recycle, choose | Session manager (spawn/reuse/recycle Ralph loop, `idle_ttl`); `bridge_sessions` roster+live; multiple profiles; `bridge_ask`. | +| **4 — Async + split-host** | Detached + cross-host | `mode:"async"` + idle-pane injection; internal queue (durability); split-host `Stop`-hook adapter that polls `bridged`. | +| **5 — Harden** | Production shape | Auth/TLS, `/metrics` + `/healthz`, mock-socket CI, systemd unit, per-session authz + audit. | + +## Tech stack + +Consolidated from [Message Server](2-Message-Server#proposed-tech-stack); the ccs pieces are new. + +| Concern | Choice | Note | +|---|---|---| +| **Language** | **Go** | Single static binary → `scp`/systemd; goroutines fit socket + MCP + queue fan-in; reuse `agentapi` `msgfmt`. | +| **MCP server** | `mcp-go` / official Go SDK, streamable-HTTP | The SERVER-face contract both sides mount. | +| **herdr client** | Unix-socket NDJSON, id-correlated + event stream | Native herdr contract. | +| **Worker spawn** | **`ccs claude`** into a herdr pane | Profile = worker identity; no env-prefix. | +| **Subscription guard** | **`ccs env `** → resolved `base_url` host allowlist | Boundary check in ccs terms. | +| **Config** | YAML (`koanf`) + env | `bridged.yaml`: worker profiles, allowlist, bind addr, lifecycle knobs. | +| **Internal queue** *(Stage 4)* | Redis Streams (ack + visibility) | Below the gateway; embedded queue OK single-host. | +| **Observability** | `slog` + Prometheus `/metrics` + `/healthz` | Stage 5. | +| **Supervision** | systemd unit, ordered after herdr | Stage 5. | +| **Testing** | Mock herdr socket + golden transcripts; fake `ccs`/`claude` stubs | Deterministic CI, no TTY. | + +## Tickets by stage (2–5) + +Compact scope; expand into detailed tickets when a stage starts (as Stage 1 is below). + +| Stage | Tickets | +|---|---| +| **2** | `CB-201` envelope schema + codec · `CB-202` worker `bridge_reply` tool + reviewer skill · `CB-203` reply rendezvous (corr match; resolve on reply *or* `done`) · `CB-204` subscription guard via `ccs env` + allowlist · `CB-205` blocked-worker path (`bridge_ask`) | +| **3** | `CB-301` session manager (spawn/reuse/recycle) · `CB-302` Ralph checkpoint (`STATE.md` + commit) · `CB-303` `idle_ttl`/`context_cap`/drain · `CB-304` `bridge_sessions` roster+live · `CB-305` multi-profile routing (`role@profile`) | +| **4** | `CB-401` `mode:"async"` + ticket · `CB-402` idle-pane injection delivery · `CB-403` internal queue (Redis Streams) · `CB-404` split-host `Stop`-hook adapter (polls `bridged`) · `CB-405` REST ingress for event-bus | +| **5** | `CB-501` bearer auth + TLS · `CB-502` `/metrics` + `/healthz` · `CB-503` mock-socket CI · `CB-504` systemd unit + ordered start · `CB-505` per-session authz + audit log | + +## Stage 1 — detailed tickets + +**Goal:** Opus, from its own subscription session, mounts `bridged` and gets a code review +back from a real `ccs gx00-vllm claude` worker — same host, one hardcoded profile, reply via a +pane scrape (envelope comes in Stage 2). This is the thinnest end-to-end vertical slice. + +**Definition of done for the stage:** `CB-107` demo passes. + +--- + +### CB-101 — herdr socket client +**Scope.** Connect to `~/.config/herdr/herdr.sock` (`HERDR_SOCKET_PATH`); implement +`Call(method, params) → result` (NDJSON, `id`-correlated) and `Subscribe(subs) → <-chan Event` +for `pane.agent_status_changed`. Probe `session.snapshot` on connect; fail fast on schema +mismatch. +**Acceptance.** Unit test against a **mock socket** replays a `workspace.create` round-trip and +one event; `session.snapshot` shape asserted. +**Deps.** none. + +### CB-102 — spawn a worker via ccs profile +**Scope.** Given a profile name, open a herdr pane and `send_text` `ccs claude` +(+ workspace `cwd`); detect **Ready** via `output_matched` / `pane.read {source:"detection"}` +(prompt-ready), not an absent status. +**Acceptance.** Against a fake `ccs`+`claude` stub in a real herdr pane, the worker reaches +`Ready`; the resolved command is exactly `ccs gx00-vllm claude` (asserted from a spawn log). +**Deps.** CB-101. + +### CB-103 — status-gated injector +**Scope.** Per-pane FIFO; `Deliver(pane, text)` sends `send_text` + `send_keys "enter"` **only** +when `agent_status ∈ {idle, blocked}`, else queues until the next idle event. Single writer per +pane; status-check + send serialized on the event goroutine (close the TOCTOU window). +**Acceptance.** A delivery issued while the pane is `working` lands only after the `idle` event; +two rapid deliveries never interleave (golden transcript). +**Deps.** CB-101. + +### CB-104 — blocking `bridge_send` + reply capture +**Scope.** `bridge_send(to, body, {mode:"block"})` → resolve `to` to the (single, hardcoded) +worker pane → inject the request text → wait for `agent_status=done` → return +`pane.read {source:"recent-unwrapped"}` of the last assistant block as the tool result. (No +envelope yet; scrape is acceptable for Stage 1.) +**Acceptance.** A canned request produces a non-empty reply string derived from the pane; a +`working→done` transition unblocks the call; a timeout returns a typed "still working" error. +**Deps.** CB-102, CB-103. + +### CB-105 — MCP server (SERVER face) +**Scope.** Streamable-HTTP MCP server exposing `bridge_send` and `bridge_status`; bind +`127.0.0.1:8080`. Provide the one-line mount: `claude mcp add --transport http bridge +http://127.0.0.1:8080/mcp`. +**Acceptance.** `claude mcp list` shows `bridge` connected from the primary; calling +`bridge_status` returns the worker's live `agent_status`. +**Deps.** CB-104. + +### CB-106 — config + wiring +**Scope.** `bridged.yaml` load (`koanf`): one worker profile (`gx00-vllm` → ccs profile, model, +`base_url` host), bind address, workspace root. `slog` startup line logging the resolved worker +command (secrets redacted). Run as a foreground process (systemd deferred to Stage 5). +**Acceptance.** Bad/missing profile fails fast with a clear message; a valid config boots and +logs the resolved (redacted) spawn command. +**Deps.** none (parallel with CB-101). + +### CB-107 — end-to-end review demo (stage gate) +**Scope.** Scripted demo: start herdr → start `bridged` → mount MCP on a primary `claude` → +from the primary, `bridge_send` a real diff with `kind:"review.request"` (body carried as text +for now) → assert a review comes back as the tool result. Document the exact steps in +[Setup](4-Setup). +**Acceptance.** The demo runs green on one host end-to-end; the worker is verifiably the +`ccs gx00-vllm` process (not the subscription); the primary's env has no `ANTHROPIC_BASE_URL`. +**Deps.** CB-105, CB-106. + +```mermaid +flowchart LR + CB101["CB-101
herdr client"] --> CB102["CB-102
ccs spawn"] + CB102 --> CB103["CB-103
injector"] + CB103 --> CB104["CB-104
bridge_send"] + CB104 --> CB105["CB-105
MCP server"] + CB106["CB-106
config"] --> CB107 + CB105 --> CB107["CB-107
e2e demo (gate)"] + classDef gate fill:#2f855a,stroke:#22543d,color:#ffffff; + class CB107 gate +``` + +*Figure: Stage 1 dependency order. CB-106 runs in parallel; everything converges on the CB-107 +end-to-end gate.* + +## Open decisions (surface before/while building) + +- **ccs worker profiles** — which existing ccs profiles (or new `ccs api` profiles) back the + `gx00-vllm` / local workers, and their exact `base_url` hosts for the allowlist. +- **Reviewer system prompt** — ship the reviewer skill as a `CLAUDE.md` snippet vs a + slash-command/skill the worker loads at spawn. +- **Envelope-as-text (Stage 1) → structured (Stage 2)** — confirm the Stage-1 shortcut (request + body inlined as prompt text, reply scraped) is acceptable before the envelope lands. + +## Related + +- **[Use Cases](7-Use-Cases)** — the review scenario and the five mechanisms these stages build. +- **[Message Server](2-Message-Server)** — component design the tickets implement. +- **[Setup](4-Setup)** · **[Operations](5-Operations)** — bring-up and day-2, fleshed out as stages land. diff --git a/Home.md b/Home.md index b693f95..7baa296 100644 --- a/Home.md +++ b/Home.md @@ -75,6 +75,8 @@ Read in order (the sidebar mirrors this): 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) ## Status diff --git a/_Sidebar.md b/_Sidebar.md index c905fe9..ccbc20f 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -10,6 +10,8 @@ 4. [Setup](4-Setup) — bring-up 5. [Operations](5-Operations) — day-2 runbook 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 --- 🟢 herdr-centric `bridged` · AgentAPI = fallback