summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.dispatch/package-agent.md81
-rw-r--r--.dispatch/rules/frontend-inject-transport.md7
-rw-r--r--.dispatch/rules/frontend-interpreter-generic.md8
-rw-r--r--.dispatch/rules/frontend-no-ambient-state.md7
-rw-r--r--.dispatch/rules/frontend-pure-core.md7
-rw-r--r--.dispatch/rules/frontend-styling.md12
-rw-r--r--AGENTS.md163
-rw-r--r--ORCHESTRATOR.md209
-rw-r--r--README.md2
-rw-r--r--backend-handoff.md2
10 files changed, 122 insertions, 376 deletions
diff --git a/.dispatch/package-agent.md b/.dispatch/package-agent.md
deleted file mode 100644
index 5c1a54d..0000000
--- a/.dispatch/package-agent.md
+++ /dev/null
@@ -1,81 +0,0 @@
-<!-- ORCHESTRATOR-ONLY meta (see ORCHESTRATOR.md): every FE summon is assembled as
- package-agent.md + the inlined .dispatch/rules/* + the per-summon TASK block.
- This is the base for ALL FE owner-agents; nothing here is restated per summon. -->
-
-# Frontend Owner-Agent — Brief
-
-You are the **sole owner-agent for exactly ONE unit** — a single feature module /
-directory under `src/`. Your unit + job are in the **TASK** at the end. You build
-it, test it, and write a report — nothing else. If no single unit is named, stop.
-
-## Hard guardrails (NON-NEGOTIABLE)
-- **Single-writer, directory-scoped.** Read/create/edit any file inside your unit's
- directory. Never create or edit anything OUTSIDE it — not another feature, not
- `src/app/` (the composition root), not root config (`package.json`,
- `tsconfig.json`, `vite.config.ts`, `biome.json`), not the harness, not the
- backend repo (`../arch-rewrite`).
-- **Need a change outside your unit?** Do NOT make it — write a CHANGE-REQUEST in
- your report (a sibling's public export, a backend contract, root config,
- composition wiring). The orchestrator dispatches it.
-- **No workspace/dep wiring.** Don't `bun install` or edit root config; list a new
- dep / wiring need as a CR.
-- **No git** (no commits, branches, pushes, resets).
-
-## What you may read (visibility)
-- **Your own unit:** every file, freely.
-- **The contracts you consume:** reproduced IN-REPO under `.dispatch/*.reference.md` — read THOSE:
- - `.dispatch/ui-contract.reference.md` — `@dispatch/ui-contract` (surfaces + surface WS protocol).
- - `.dispatch/wire.reference.md` — `@dispatch/wire` (`Chunk`/`StoredChunk`+`seq`/`ChatMessage`/
- `AgentEvent`/`TurnSealedEvent`/`Usage` + metrics: `StepMetrics`/`TurnMetrics`, `usage.stepId`,
- `step-complete`, `done.durationMs`/`done.usage`, `tool-result.durationMs` — the chat wire types).
- - `.dispatch/transport-contract.reference.md` — `@dispatch/transport-contract` (HTTP endpoints +
- `ChatRequest`/`ModelsResponse`/`ConversationHistoryResponse` + WS chat ops + the unified
- `WsClientMessage`/`WsServerMessage` unions).
-
- Your code imports `@dispatch/ui-contract` / `@dispatch/wire` / `@dispatch/transport-contract`
- normally, but **do NOT read `node_modules/@dispatch/*`** — they symlink to the backend repo
- (OUTSIDE this repo) and a headless permission prompt will HANG the run (see "Headless read
- boundary").
-- **Sibling units — PUBLIC SURFACE only:** their `index.ts` exports. Don't read
- their internals (needing them ⇒ the contract is incomplete → report a CR).
-
-## Headless read boundary (you run non-interactively)
-You run HEADLESS: a Read of any file OUTSIDE this repo (`dispatch-web/`) triggers a
-permission prompt that CANNOT be answered → the run HANGS until aborted. Use Read/Edit ONLY
-within `dispatch-web/`. If you believe you need a file outside your scope, do NOT attempt
-the read — STOP and write the need in your report, then end.
-
-## Engineering standard (the inlined `.dispatch/rules/*` govern; in brief)
-- **Pure core / injected shell.** Decision logic is `input → output`: zero DOM,
- zero `fetch`/WS, zero Svelte. Put it in a `.ts` module (e.g. `logic/`) that tests
- with NO mounting and NO mocks. Effects are INJECTED (props or an `adapter/`).
-- **Svelte-thin.** `.svelte` files wire props/events to pure logic + render; no
- business logic. (biome lints `.ts`/`.js` only; `svelte-check` owns `.svelte`.)
-- **No ambient state.** Own state explicitly; runes wrap the pure reducer;
- subscriptions are disposed on unmount.
-- **Tests, asymmetric.** Pure logic → vitest with ZERO internal mocks (never
- `vi.mock` of our own modules). Components → a few `@testing-library/svelte`
- tests; don't chase coverage there, don't mock siblings. Faking the OUTERMOST
- edge (a fake socket/fetch/clock) is the only allowed mock.
-- **Isolation over DRY.** Self-contained over a shared helper wired between
- features. The only shared surfaces are the imported contracts.
-- **Strict TS:** respect `exactOptionalPropertyTypes` + `noUncheckedIndexedAccess`.
-
-## Verify before finishing — YOUR UNIT IN ISOLATION
-Sibling units may be built IN PARALLEL, so do NOT judge yourself by the whole project.
-`svelte-check` is whole-project: if it reports errors in files OUTSIDE your unit's directory,
-that is concurrent work-in-progress — IGNORE it and ensure only YOUR files (plus any existing
-consumer you must not break) are error-free. The SCOPED checks below are your authoritative
-signals. Run them, and paste the output into your report:
-- `bunx svelte-check --tsconfig ./tsconfig.json` → 0 errors in YOUR files
-- `bunx vitest run src/<your-dir>` → all pass (count goes up)
-- `bunx biome check src/<your-dir>` → clean
-The orchestrator runs the authoritative full `typecheck`/`test`/`check`/`build`.
-
-## Report (REQUIRED) → `reports/<your-unit>.md`
-1. Files created/changed. 2. Public surface you expose (exported types/functions/
-components). 3. New test names + the isolated-verify output. 4. Change-requests
-(sibling export, backend contract, root config, composition wiring) — explicit and
-actionable.
-
-Your specific **TASK** follows at the end of this prompt.
diff --git a/.dispatch/rules/frontend-inject-transport.md b/.dispatch/rules/frontend-inject-transport.md
deleted file mode 100644
index fbe83d7..0000000
--- a/.dispatch/rules/frontend-inject-transport.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Rule: inject the transport; parsers are pure
-
-The WS/NDJSON framing + parsing is a PURE function (bytes/messages → typed events);
-the socket/fetch is INJECTED. Test the parser with crafted chunk inputs (and
-trace-replay-style fixtures), never a live connection. The op-protocol core is a
-pure state machine: `reduce(intent, incoming) → { viewModel, outgoingCommands }`;
-the carrier (WebSocket) is the injected shell.
diff --git a/.dispatch/rules/frontend-interpreter-generic.md b/.dispatch/rules/frontend-interpreter-generic.md
deleted file mode 100644
index 557f2d4..0000000
--- a/.dispatch/rules/frontend-interpreter-generic.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Rule: the surface interpreter is generic
-
-The surface interpreter switches on field KINDS (toggle/progress/selector/stat/
-button/custom), NEVER on a surface id. An `if (surface.id === "...")` imports a
-feature's identity into the platform and breaks isolation (guardrail 1). An
-unknown field `kind` or a `custom` `rendererId` with no registered renderer →
-GRACEFUL SKIP, never a crash. Render from the spec; the backend owns what a
-surface contains.
diff --git a/.dispatch/rules/frontend-no-ambient-state.md b/.dispatch/rules/frontend-no-ambient-state.md
deleted file mode 100644
index 663bf1a..0000000
--- a/.dispatch/rules/frontend-no-ambient-state.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Rule: no ambient state (frontend)
-
-State is owned per-unit and passed explicitly. NO module-global mutable store
-reached from everywhere — that is the old FE's "tools leak across tabs" /
-"model resets on tab switch" bug class. Svelte runes (`$state`) are a THIN
-reactive wrapper over a pure reducer, never the home of logic. Subscriptions are
-owned and disposed on unmount (no orphaned or duplicate subscriptions).
diff --git a/.dispatch/rules/frontend-pure-core.md b/.dispatch/rules/frontend-pure-core.md
deleted file mode 100644
index fa7bc2e..0000000
--- a/.dispatch/rules/frontend-pure-core.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Rule: pure core / injected shell (frontend)
-
-Decision logic — reducers, view-models, formatters, parsers — is pure
-(input → output): NO DOM, NO `fetch`/WebSocket, NO Svelte import. Put it in a
-`.ts` module that tests with zero mounting and zero mocks. Effects (socket, fetch,
-IndexedDB, clock) are INJECTED at the edges (props or an adapter). This is for
-testability, not purity dogma — stop where it would only add ceremony.
diff --git a/.dispatch/rules/frontend-styling.md b/.dispatch/rules/frontend-styling.md
deleted file mode 100644
index e123619..0000000
--- a/.dispatch/rules/frontend-styling.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Rule: styling is DaisyUI v5 + the dracula theme (UI units only)
-
-The global stylesheet already enables **DaisyUI v5** with the **dracula** theme
-(`data-theme` on `<html>`). Do NOT add or change theme config, re-import Tailwind/DaisyUI,
-or hand-roll a design system — just apply DaisyUI / Tailwind utility classes in your
-`.svelte` files (e.g. `select`, `textarea`, `btn btn-primary`, `tabs`, `chat chat-start` /
-`chat-end` + `chat-bubble`).
-
-Keep components THIN: a `.svelte` file wires props/events to pure logic and applies classes
-— it holds NO business logic (that stays in `logic/` / the reducer). Render plain semantic
-HTML decorated with classes. biome lints `.ts`/`.js` only; `.svelte` correctness is
-`svelte-check`'s job.
diff --git a/AGENTS.md b/AGENTS.md
index 9f077ba..3f7d428 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,18 +1,25 @@
-# Dispatch Web — Constitution (root AGENTS.md)
+# Dispatch Web — Agent Guide (root AGENTS.md)
-> Loaded every session. Non-obvious, project-specific rules only. If a fresh
-> frontier model could infer it from the code, it is NOT here (P6).
+> Loaded every session — the single source of truth for working in this repo: the
+> build constitution (code rules) + the workflow. Non-obvious, project-specific rules
+> only — if a fresh frontier model could infer it from the code, it is NOT here (P6).
> Full design + rationale: `../arch-rewrite/notes/frontend-design.md` (and the
> backend's `notes/restructure-plan.md` §1 for P1–P8).
+>
+> **You are the single agent for this repo.** You plan, author the cross-unit
+> contracts/types, write the feature code, verify, keep the build green, and keep the
+> living backend handoff current. There is no separate orchestrator and no summoned
+> sub-agents — you do all of it yourself.
## What this is
The **web frontend** for Dispatch — a SEPARATE repo from the backend
(`../arch-rewrite`). It is a **thin shell + pure feature libraries + a surface
host**, NOT a default-SvelteKit ball of mud. It consumes the backend's typed
-contracts (`@dispatch/ui-contract` + the wire types) over HTTP + a WebSocket.
-There is **no mandatory "chat is the root"**: the app is a COMPOSITION of feature
-modules + surfaces, assembled at the composition root; legitimate frontends may
-compose without chat at all.
+contracts (`@dispatch/ui-contract` + the wire types) over HTTP + a WebSocket. The app
+is a COMPOSITION of feature modules + surfaces, assembled at the composition root
+(`src/app/`); there is **no mandatory "chat is the root"** — legitimate frontends may
+compose without chat at all. Built with the backend's methodology: pure core / inject
+effects / no ambient state / typed contracts / asymmetric testing.
## Stack
Bun + Vite + Svelte 5 (runes) + TypeScript (strict). Biome for lint/format
@@ -21,54 +28,124 @@ Bun + Vite + Svelte 5 (runes) + TypeScript (strict). Biome for lint/format
semantics — it flags template-used vars as unused). Vitest + `@testing-library/
svelte` for tests.
+## Repo geography
+```
+src/app/ composition root (imports + wires feature modules + surface host)
+src/core/ PURE: transcript · cache · surfaces (interpreter) · protocol · wire
+src/features/<unit>/ logic/ (pure) · ui/ (svelte) · adapter/ (effects)
+src/adapters/ injected browser effects: WS client, fetch, IndexedDB, history
+.dispatch/ mirrored backend contracts (*.reference.md) + rules/
+reports/ (gitignored)
+```
+Backend (SEPARATE repo, contracts only): `../arch-rewrite` — consume
+`@dispatch/ui-contract` (`file:` dep) + the wire types. Do NOT edit it.
+
## The non-negotiable rules
-- **Pure core / injected shell.** Decision logic (reducers, view-models,
- formatters, parsers) is pure `input → output`: zero DOM, zero `fetch`/WebSocket,
- zero Svelte import. Effects (WebSocket, fetch, IndexedDB, history, clipboard)
- are INJECTED at the edges. State is a pure reducer; Svelte runes are a THIN
- reactive wrapper over it, never the home of logic.
-- **No ambient state.** State is owned per-unit and passed explicitly. No
- module-global mutable store reached from everywhere — that is the old FE's
- "tools leak across tabs" / "model resets on tab switch" bug class.
-- **Components are thin.** A `.svelte` file wires props/events to pure logic and
- renders; it holds no business logic.
-- **One owner per unit.** Each feature module / file has exactly ONE editing
- agent. To change another unit you report the need up — you do not edit it.
-- **Contracts are the only cross-unit surface.** You see other units' public
- exports (`index.ts`) and the imported `@dispatch/ui-contract` / wire types —
- never their internals. Needing internals ⇒ the contract is incomplete; report it.
-- **The surface interpreter is GENERIC.** It switches on field KINDS
- (toggle/progress/...), NEVER on a surface id. No `if (surface.id === "...")` —
- that imports a feature's identity and breaks isolation.
-- **Typed coupling.** Cross-feature links are typed imports/callbacks; no
- stringly-typed event bus. Discovery-by-id (surface catalog, subscribe) is
- sanctioned DATA flow, not a code reference.
+- **Pure core / injected shell.** Decision logic (reducers, view-models, formatters,
+ parsers) is pure `input → output`: zero DOM, zero `fetch`/WebSocket, zero Svelte
+ import. Effects (WebSocket, fetch, IndexedDB, history, clipboard) are INJECTED at the
+ edges. State is a pure reducer; Svelte runes are a THIN reactive wrapper over it,
+ never the home of logic.
+- **No ambient state.** State is owned per-unit and passed explicitly. No module-global
+ mutable store reached from everywhere — that is the old FE's "tools leak across tabs" /
+ "model resets on tab switch" bug class. Subscriptions are owned and disposed on unmount.
+- **Components are thin.** A `.svelte` file wires props/events to pure logic and renders;
+ it holds no business logic.
+- **Contracts are the cross-unit surface.** Cross-unit dependencies go through a unit's
+ public exports (`index.ts`) + the imported `@dispatch/ui-contract` / wire types — not
+ another unit's internals. If you find yourself needing a sibling's internals, the
+ contract is incomplete: fix the boundary, don't reach in.
+- **The surface interpreter is GENERIC.** It switches on field KINDS (toggle/progress/…),
+ NEVER on a surface id. No `if (surface.id === "…")` — that imports a feature's identity
+ and breaks isolation. An unknown field kind / unregistered `custom` renderer → graceful
+ skip, never a crash.
+- **Typed coupling.** Cross-feature links are typed imports/callbacks; no stringly-typed
+ event bus. Discovery-by-id (surface catalog, subscribe) is sanctioned DATA flow, not a
+ code reference.
+- biome covers `.ts`/`.js`; `.svelte` correctness is `svelte-check`'s job.
+
+## Workflow
+1. Plan the change; split a big change into dependency-ordered steps — build the shared
+ contract/types first, then the producers, then the consumer / composition root.
+2. Overlap/vocab check vs `GLOSSARY.md` before naming anything new — ask the user before
+ coining a term (see Vocabulary).
+3. Author/extend the cross-unit seam (the shared type/port/`index.ts` export) before
+ writing the code that crosses it.
+4. Write it: pure logic in `logic/` (zero DOM/fetch/Svelte), thin `.svelte` in `ui/`,
+ effects injected at the edges (`adapter/` or props). Tests alongside.
+5. Verify yourself (below).
+6. Commit the milestone; update progress + the living handoff (see Backend seam).
+
+## Verification (run these yourself — trust the green, not intent)
+```
+bun run typecheck # svelte-check — 0 errors
+bun run test # vitest — note the pass count
+bun run check # biome (.ts/.js) — clean
+bun run build # vite build — succeeds
+git status --short # confirm you only touched what you meant to
+```
+Asymmetric testing: pure logic → vitest with NO internal `vi.mock` of our own modules;
+components → a few `@testing-library/svelte` tests (fake only the OUTERMOST edge —
+socket/fetch/clock, never a sibling module). Re-run the suite TWICE when a change touches
+effects backed by a shared global (`fake-indexeddb`, `localStorage`) to catch cross-test
+pollution. After a slice that touches the wire or browser effects, run a LIVE probe (below).
## Backend seam (cross-repo)
-The backend is `../arch-rewrite` (separate repo; `lsp references` does NOT span
-the boundary). You consume `@dispatch/ui-contract` (surfaces) + the wire types as
-a pinned dependency. Need a backend contract change? REPORT IT UP — the
-orchestrator carries it across (couriered via the user). Never reach into the
-backend repo.
+The backend is `../arch-rewrite` (separate repo; `lsp references` does NOT span the
+boundary). You consume `@dispatch/ui-contract` + the wire/transport types as pinned `file:`
+deps. **Read the in-repo mirrors `.dispatch/*.reference.md`, never `node_modules/@dispatch/*`**
+(they symlink out of the repo); regenerate the relevant mirror whenever a contract changes.
+- **FE contract change** (a shared FE type / service handle): edit it, run `lsp references`,
+ and update every consumer yourself.
+- **Backend contract change:** `lsp` does not span repos — REPORT IT UP via the living
+ handoff `backend-handoff.md` (repo root, tracked); the user couriers it to the backend and
+ brings the reply back. On the new version: re-pin the `file:` dep → re-mirror the relevant
+ `.dispatch/*.reference.md` → update FE consumers. NEVER edit the backend repo.
+
+Keep `backend-handoff.md` current at every milestone: FE slice status, pinned contract
+versions + mirrors, open asks / roadblocks for the backend, findings, and likely next asks.
## Surfaces (the modular UI mechanism)
-A **surface** is backend-declared, frontend-agnostic data (fields + values +
-actions), rendered generically. See `frontend-design.md` §4. You render surfaces;
-you never special-case a specific one.
+A **surface** is backend-declared, frontend-agnostic data (fields + values + actions),
+rendered generically. See `frontend-design.md` §4. You render surfaces; you never
+special-case a specific one.
+
+## Live integration probe (effectful seams hide bugs unit tests can't)
+Pure/unit green is necessary, NOT sufficient: a transport/storage SHELL only fails for real
+against a running backend (the slice-1 WS-upgrade bug, and the secure-context
+`crypto.randomUUID` blank page, only surfaced live). After a slice that touches the wire or
+browser effects, run a LIVE probe:
+- `scripts/live-probe.ts` (`bun scripts/live-probe.ts`) drives the REAL network-facing modules
+ (`adapters/ws` + `core/chunks` + cache + HTTP sync) against the running backend. Keep it OUT
+ of `bun run test` so a down backend never reds CI. It runs in Bun (global `WebSocket`/`fetch`);
+ use `fake-indexeddb/auto` for IndexedDB.
+- The backend is the USER's process — never boot it yourself (headless boot+probe HANGS + leaks
+ servers, EADDRINUSE). Confirm reachability first (`curl :24203/health`), then CONNECT. Probe
+ with the REAL env (never an overridden empty key).
+- Browser-only bugs (secure-context APIs, theme, layout) still need a human at the page — list
+ the exact things to click and ask the user to confirm.
## Commands
- `bun run typecheck` — svelte-check
- `bun run test` — vitest
- `bun run check` — biome (`.ts`/`.js`)
- `bun run build` — vite build
+- `bun run dev` — Vite dev server (port 24204). Full stack: backend `bin/up`
+ (HTTP :24203, surface WS :24205) + FE Vite :24204.
+
+## Status
+Slices 1–3 DONE + committed (surface system + WS; conversation transcript cache + delta
+streaming; tabs + model selector + DaisyUI/dracula), plus per-conversation cwd + LSP view,
+context size, cache-warming (+ retention/timer), markdown, smart auto-scroll, and
+multi-client live view (subscribe/reconnect + the user prompt on the event stream). Plan in
+`../arch-rewrite/notes/frontend-design.md` §10.
## Reports
-Finish a task → write `reports/<your-unit>.md` (gitignored): what you built, the
-public surface, test/typecheck/build output, and any contract gaps / change-
-requests (incl. backend-contract CRs for the orchestrator to courier).
+Optionally record a finished milestone in `reports/<name>.md` (gitignored): what you built,
+the public surface, verification output, and any contract gaps / backend-contract CRs.
## Vocabulary
-Use `GLOSSARY.md`. Shared backend terms are canonical (conversation, turn, step,
-chunk, AgentEvent, model name, …). FE terms: surface, region, field kind,
-action / action ref, surface catalog. **"view" is RESERVED** (old-Dispatch sidebar
-UX, future). Never invent a synonym.
+Use `GLOSSARY.md`. Shared backend terms are canonical (conversation, turn, step, chunk,
+AgentEvent, model name, …). FE terms: surface, region, field kind, action / action ref,
+surface catalog. **"view" is RESERVED** (old-Dispatch sidebar UX, future). Never invent a
+synonym.
diff --git a/ORCHESTRATOR.md b/ORCHESTRATOR.md
deleted file mode 100644
index eb5e381..0000000
--- a/ORCHESTRATOR.md
+++ /dev/null
@@ -1,209 +0,0 @@
-# ORCHESTRATOR.md — driving dispatch-web (the frontend)
-
-> **You are the orchestrator for the frontend repo.** You do NOT write feature
-> code. You plan, author contracts/config/harness, summon owner-agents (one per
-> unit), verify, resolve, keep the build green. Read fully before acting. Also
-> read: `AGENTS.md` (the FE constitution you enforce), `GLOSSARY.md`,
-> `.dispatch/rules/`, and the design home `../arch-rewrite/notes/frontend-design.md`
-> (+ `../arch-rewrite/notes/restructure-plan.md` §1 for P1–P8). This MIRRORS the
-> backend's `../arch-rewrite/ORCHESTRATOR.md` — read that for the deep rationale.
-
-## 0. Mental model
-The frontend is a **composition of feature modules + a surface host**, built with
-the backend's methodology (pure core / inject effects / no ambient state / typed
-contracts / one owner per unit / asymmetric testing). The team structure is
-isomorphic to the module structure: agents communicate only through contracts. The
-surface system (backend-declared, frontend-agnostic UI) is the modular UI
-mechanism — see `frontend-design.md` §4. No feature (not even chat) is the
-mandatory structural root.
-
-## 1. The golden workflow
-1. Plan the unit(s); split into dependency-topological **waves** of disjoint units, and
- WIDEN each wave where you can (§2a); one agent owns one unit.
-2. Overlap/vocab check vs `GLOSSARY.md` before naming anything new (§5.6 — ask the
- user before coining a term).
-3. Pre-author the cross-unit seam (§3) + write the per-summon TASK to
- `prompts/<unit>.md` (gitignored). RE-READ `.dispatch/rules/` + the §2 scoping map
- before each wave — summon from the files, NOT from memory.
-4. Summon a wave via `opencode run` (§2); disjoint units run in PARALLEL (§2a).
-5. Verify the reports + independently re-run typecheck/test/check/build (§4).
-6. Resolve contract gaps / CRs / agent failures (§5, §5a).
-7. Commit the milestone; update progress + the living handoff (§10).
-
-## 2. Summoning agents (`opencode run`)
-**Working dir:** the repo root `/home/tradam/projects/dispatch/dispatch-web` (so the
-agent's `lsp` tool uses THIS repo's TS server).
-**Model:** `opencode-go/mimo-v2.5-pro` for building.
-**Invocation:** concatenate the brief + the scoped rules + the TASK; redirect output
-to a log file; never use `-f`.
-```bash
-cd /home/tradam/projects/dispatch/dispatch-web && \
-opencode run --dir /home/tradam/projects/dispatch/dispatch-web \
- -m opencode-go/mimo-v2.5-pro \
- "$(cat .dispatch/package-agent.md)
-$(cat .dispatch/rules/frontend-pure-core.md .dispatch/rules/frontend-no-ambient-state.md)
-
-## TASK
-$(cat prompts/<unit>.md)" \
- > reports/<unit>.run.log 2>&1
-```
-**MANDATORY — capture output to a file, never display it** (the stream is huge and
-will crash the harness). Read the agent's `reports/<unit>.md`; `grep`/`tail` the log
-only for a specific error.
-**Run discipline:** do NOT background (no shell `&`); large timeout (e.g. 1800000 ms).
-Each summon is its own FOREGROUND `opencode run`. **Parallelism = emit several summon
-calls in ONE assistant message (concurrent tool calls), one per unit — NOT shell
-backgrounding and NOT `&`.** Parallel summons ONLY for disjoint file sets (single-writer);
-see §2a. `AGENTS.md` is auto-loaded by opencode — never `cat` it.
-
-**GOTCHA — headless cross-repo read = HANG.** An agent's Read of any file OUTSIDE `--dir`
-(here `dispatch-web/`) triggers a permission prompt that CANNOT be answered headlessly → the
-run wedges until aborted. The `@dispatch/ui-contract` `file:` dep symlinks OUT of this repo, so
-reading `node_modules/@dispatch/*` hangs. Mitigation (in place): the contract is mirrored in-repo
-under `.dispatch/*.reference.md` — one per consumed backend contract (`ui-contract`, `wire`,
-`transport-contract`) — agents read THOSE; the brief forbids `node_modules/@dispatch/*` reads.
-**Regenerate the relevant snapshot whenever that contract changes** (re-pin → re-mirror → fan out
-consumers), and point `package-agent.md` at every mirror. Agents are told: if you'd need a file
-outside your scope, report it and STOP — never attempt the read.
-
-### `.dispatch/rules/` scoping map (inline ONLY the matching rows)
-- **Every FE agent:** `frontend-pure-core.md`, `frontend-no-ambient-state.md`.
-- **Surface interpreter / renderer / field-component unit:** + `frontend-interpreter-generic.md`.
-- **Transport / protocol / WS-client unit:** + `frontend-inject-transport.md`.
-- **Any unit that builds `.svelte` UI (app shell, chat view/composer, surface field components):** + `frontend-styling.md`.
-
-## 2a. Parallel execution — WAVES
-Throughput comes from running disjoint units at once. Organise it as waves:
-- **A wave = a set of units that (a) touch DISJOINT files and (b) have no compile-time
- dependency on each other** (each imports only already-built units + the pinned contracts).
- Launch a wave by emitting one summon `Bash` call per unit IN A SINGLE MESSAGE (§2). Later
- waves depend on earlier ones; the composition root (`src/app/`) is almost always the LAST wave.
-- **Widen waves deliberately.** Before summoning, look for dependency edges you can remove so a
- unit moves into an earlier (wider) wave — e.g. make an adapter GENERIC so it doesn't import a
- feature's port (a `LocalStore<T>` instead of a tabs-specific store), or have the consumer define
- the port it needs so the producer/consumer split disappears. Fewer edges ⇒ wider waves ⇒ faster.
-- **One writer per file, always** — even across waves. If two units would edit the same file, they
- are NOT separable; merge them into one unit or sequence them.
-- **After a wave:** read every report, run the §4 checks ONCE for the whole wave (not per unit),
- commit the milestone, then start the next wave. Don't interleave a new wave before the prior one
- is green.
-
-## 3. The per-summon `prompts/<unit>.md` is JUST the TASK
-The invariant guardrails live in `package-agent.md` + the inlined rules. The TASK
-states only the non-inferable, project-specific job: your unit's directory; the job
-+ algorithm naming the contract types involved; the contract file(s) to read
-(`.dispatch/*.reference.md`, a sibling's `index.ts`); the required named test cases.
-
-**Pre-author the cross-unit seam.** When two units in the SAME wave must interoperate (a producer
-+ a consumer that never see each other's internals), the orchestrator pins the shared interface in
-BOTH prompts before summoning: name the exact port/type, say who DEFINES it and who IMPORTS it
-(consumer-defines-port is the default — the adapter implements it), and which `index.ts` to import
-from. That precise seam is what lets disjoint, blind units compose on the first try.
-
-**Tell each agent it has company.** Add the concurrency note to every wave prompt: sibling units are
-being built in OTHER dirs right now; `svelte-check`/biome are whole-project, so if a check reports
-errors OUTSIDE your unit's dir, that's concurrent WIP — ignore it and ensure YOUR files are clean.
-The orchestrator's post-wave run (§4) is the source of truth.
-
-**Make agents IMPLEMENT, not deliberate.** A summoned owner must edit files + run its checks + write
-its report in the one run. Prompts should say so explicitly when needed (see §5a).
-
-## 4. Verification (re-run yourself — trust nothing)
-```bash
-cd /home/tradam/projects/dispatch/dispatch-web
-bun run typecheck # svelte-check — 0 errors
-bun run test # vitest — note the pass count
-bun run check # biome (.ts/.js) — clean
-bun run build # vite build — succeeds
-git status --short # confirm the agent stayed in its lane
-```
-Trust = contracts + public surfaces + green checks + the report — NOT reading impl.
-For pure units, confirm tests use NO internal `vi.mock` of our modules.
-
-**Concurrency caveat:** because `svelte-check`/biome/`vitest` are whole-project, an agent's OWN
-verification (mid-wave) can transiently see a sibling's half-written file. Don't act on an agent
-report's out-of-dir errors; YOUR post-wave run is authoritative. **Run the suite TWICE** when a wave
-touched effects backed by a shared global (e.g. `fake-indexeddb`, `localStorage`) — to catch
-cross-test pollution / flakiness before committing.
-
-## 5. Errors, CRs, cross-repo
-- **FE contract change** (a shared FE type / service handle): the owner edits it,
- runs `lsp references`, reports the consumer list; the orchestrator dispatches the
- fan-out.
-- **CR for build/config** (root tsconfig/vite/biome/package.json): the orchestrator
- edits directly. **CR for impl** (a sibling, composition wiring in `src/app/`): the
- orchestrator SUMMONS the owning agent.
-- **BACKEND contract change (cross-repo):** `lsp references` does NOT span the two
- repos. The FE pins `@dispatch/ui-contract` + wire types as a dependency. A needed
- backend change is reported UP and **couriered by the user** to the backend
- orchestrator (via the living handoff, §10); on the new version, re-pin + re-mirror +
- fan out FE consumers. NEVER edit the backend repo.
-
-## 5a. Agent-failure recovery patterns
-- **Plan-only / "shall I proceed?" agent.** A summon sometimes returns a PLAN and stops without
- editing (no diff, no report). Detected via `git status` + a missing `reports/<unit>.md`. Re-summon
- the SAME TASK prefixed with: "IMPLEMENT THIS NOW — make all edits, run the checks, write the
- report; do not stop to plan or ask." Don't hand-fix its work.
-- **Behaviour change reds a SIBLING's tests (test fan-out).** When a unit's new behaviour invalidates
- another unit's test assertions, the failing tests belong to that OTHER owner — summon it with a
- focused "fix these N failing tests to match the new behaviour" TASK (state the behaviour). The
- orchestrator does not edit feature tests itself.
-- **Agent strayed out of its lane.** `git status` after every wave; if an agent touched a file
- outside its dir, decide per §5/§6: keep it if it's legitimately the orchestrator's lane (build/
- config/harness) and note it; otherwise revert + re-summon with a tighter scope. (Seen: an `app`
- agent edited root `vitest-setup.ts` instead of filing a CR — adopted because it's config, but it
- should have been a CR.)
-- **Flaky green.** If a wave's suite passes once but the units use shared-global effects, re-run
- before trusting (see §4).
-
-## 6. Restrictions (NEVER violate)
-- Single-writer: never two agents on one file.
-- The orchestrator never reads/edits feature impl (`.ts`/`.svelte`). It MAY edit:
- (a) locally-mirrored/consumed contract pins, (b) build/config (tsconfig, vite,
- biome, package.json, .gitignore), (c) harness/docs (this file, AGENTS.md,
- GLOSSARY.md, `.dispatch/`, prompts/, reports/). The composition root (`src/app/`)
- changes ONLY via a summoned owner. Roadblock → ask the user.
-- The surface interpreter is GENERIC (no surface-id special-casing).
-- biome covers `.ts`/`.js`; `.svelte` correctness is `svelte-check`'s.
-
-## 7. Repo geography
-```
-/home/tradam/projects/dispatch/dispatch-web (THIS repo)
- AGENTS.md ORCHESTRATOR.md GLOSSARY.md
- .dispatch/{package-agent.md, rules/frontend-*.md}
- src/app/ composition root (imports + wires feature modules + surface host)
- src/core/ PURE: transcript · cache · surfaces (interpreter) · protocol · wire
- src/features/<unit>/ logic/ (pure) · ui/ (svelte) · adapter/ (effects)
- src/adapters/ injected browser effects: WS client, fetch, IndexedDB, history
- prompts/ (gitignored) reports/ (gitignored)
-```
-Backend (SEPARATE repo, contracts only): `/home/tradam/projects/dispatch/arch-rewrite`
-— consume `@dispatch/ui-contract` (`file:` dep) + the wire types. Do NOT edit it.
-
-## 8. Status
-Slices 1–3 DONE + committed (surface system + WS; conversation transcript cache + delta streaming,
-live-verified 9/9; tabs + model selector + DaisyUI/dracula). Plan in
-`../arch-rewrite/notes/frontend-design.md` §10. Dev server: `bun run dev` (port 24204); full stack:
-backend `bin/up` (HTTP :24203, surface WS :24205) + FE Vite :24204.
-
-## 9. Live integration probe (effectful seams hide bugs unit tests can't)
-Pure/unit green is necessary, NOT sufficient: a transport/storage SHELL only fails for real against a
-running backend (the slice-1 WS-upgrade bug, and the secure-context `crypto.randomUUID` blank page,
-only surfaced live). After a slice that touches the wire or browser effects, run a LIVE probe:
-- A gated `scripts/live-probe.ts` (run with `bun scripts/live-probe.ts`) that drives the FE's REAL
- network-facing modules (`adapters/ws` + `core/chunks` + cache + HTTP sync) against `bin/up`. Keep it
- OUT of `bun run test` so a down backend never reds CI. It runs in Bun (global `WebSocket`/`fetch`);
- use `fake-indexeddb/auto` for IndexedDB.
-- The backend is the USER's process (never boot it yourself — headless boot+probe HANGS + leaks
- servers, EADDRINUSE). Just CONNECT to the open ports; confirm reachability first (`curl :24203/
- health`). Probe with the REAL env (never an overridden empty key).
-- Browser-only bugs (secure-context APIs, theme, layout) still need a human at the page — list the
- exact things to click and ask the user to confirm.
-
-## 10. Living cross-repo handoff — `backend-handoff.md`
-`lsp` can't span repos, so the FE↔backend seam flows through ONE rolling doc, `backend-handoff.md`
-(repo root, tracked), kept current so the user can courier the whole seam at any time. It records: FE
-slice status; the pinned contract versions + mirrors; OPEN asks / roadblocks for the backend;
-findings (e.g. "model is per-turn, not persisted per conversation"); and likely NEXT asks. Update it
-at every milestone and whenever a backend need arises; the user carries it across and brings the
-reply back (e.g. `backend-handoff-reply.md`).
diff --git a/README.md b/README.md
index 1d6e60c..9586b33 100644
--- a/README.md
+++ b/README.md
@@ -100,4 +100,4 @@ bun run check # biome (.ts/.js; .svelte correctness is svelte-check's job
## Documentation
- **Design + plan:** `../arch-rewrite/notes/frontend-design.md`
-- **Build rules:** `AGENTS.md` · **Orchestration:** `ORCHESTRATOR.md` · **Vocabulary:** `GLOSSARY.md`
+- **Build rules + workflow:** `AGENTS.md` · **Vocabulary:** `GLOSSARY.md`
diff --git a/backend-handoff.md b/backend-handoff.md
index b784eef..b5cf1eb 100644
--- a/backend-handoff.md
+++ b/backend-handoff.md
@@ -3,7 +3,7 @@
> **Purpose:** the single rolling document the FE orchestrator keeps current so the user can hand off
> the whole FE↔backend seam at any time — on completion OR at a roadblock. Updated continuously.
> **From:** dispatch-web orchestrator · **To:** arch-rewrite orchestrator · **Courier:** the user.
-> `lsp` does NOT span the repos (ORCHESTRATOR §5) — every cross-repo ask flows through here.
+> `lsp` does NOT span the repos (AGENTS.md § Backend seam) — every cross-repo ask flows through here.
_Last updated: 2026-06-12. **FE is current on `[email protected]` / `[email protected]`.** All handoffs
to date are consumed: surfaces + WS, conversation transcript/metrics, tabs + model selector,