Files
clawmates/handoff/AGENT_HANDOFF.md
T
Omar SobhandClaude Opus 4.8 39bacbd1d2 World page: live WebGL Gource-style visualization (Phases A+B)
Replaces the static React-Flow world forest with a real-time WebGL view of the
swarm working, plus the live event contract that feeds it.

Phase A — the live contract + feed:
- frontend/src/lib/live/taxonomy.ts — the 13-event Clawmates Event Taxonomy as
  typed TS (the shared World/Observe contract).
- frontend/src/lib/live/useClawmatesLive.ts — native shared-singleton live client
  (EventSource to /api/world/live, typed fan-out, replay-of-last-state to late
  subscribers, refcounted, synthetic fallback so views are always alive).
- crates/cm-api/src/routes/world.rs — GET /api/world/live, authed + workspace-
  scoped SSE emitting the taxonomy (real agent.status from live-container state,
  a topology.update of the workspace's agents, telemetry with real doorsPending),
  mirroring run_events_sse. normalize() seam documented for run_events->world.touch.

Phase B — the WebGL engine:
- frontend/src/components/world/engine.ts — Gource-inspired force-directed model:
  org/company/team tree (sibling repulsion + parent spring + friction), agent
  pawns that converge on the touched node and beam it, world nodes that glow with
  heat and fade when idle. Framework-agnostic (renderer-independent) state+math.
- frontend/src/components/world/WorldCanvas.tsx — three.js scene (ortho cam,
  UnrealBloom), render loop syncing engine state, camera auto-fit, HTML labels,
  raycast click->select, Hierarchy/Flat/Live formation switch.
- Dashboard.tsx: swap <WorldFlow/> -> <WorldCanvas/> at the world-tier seam
  (shared claw-tier pieces untouched; WorldFlow.tsx kept for now).
- Adds three (+ @types/three).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:23:34 -07:00

290 lines
14 KiB
Markdown

# Clawmates Visualization Layer — Agent Setup & Real-Time Wiring Handoff
> **Audience:** an autonomous build agent (or a human operator) provisioning a Linux node.
> **Goal:** install everything required to *serve* the Clawmates visualizations and *feed them live*
> from the durable runner so the World / Observe / Dashboard views update in real time as the
> claws actually work.
>
> Read this file top-to-bottom **once** before running anything. Every command is idempotent and
> safe to re-run. Where a step touches the existing Clawmates platform (the Rust workspace +
> Next.js app), it is clearly marked **[INTEGRATES WITH cm-api]**; you can also run the whole
> layer in **demo mode** with synthetic events and zero backend.
---
## 0. What you are installing
Three browser-native visualizations plus a thin real-time bridge:
| Asset | What it is | Lives in |
|---|---|---|
| **Clawmates World** | The Large World map with three formations — **Hierarchy** (org▸company▸team▸claw tree), **Flat** (peer mesh), and **Live** (Gource-style convergence on a `<canvas>`). | `visualizations/Clawmates World.dc.html` |
| **Clawmates Observe** | Two linked observation modes — **Agent** close-up (live reasoning stream + the agent's live computer screen) and **System** mission-control (both claws, comms, routines, telemetry). | `visualizations/Clawmates Observe.dc.html` |
| **Clawmates Dashboard** | The tiered admin dashboard (org→company→team→claw) with topology morph + agent anatomy. | `visualizations/Clawmates Dashboard.dc.html` |
| **Live bridge** | A Node service that subscribes to the runner's event stream, normalizes it to the **Clawmates Event Taxonomy** (§4), and re-emits one SSE feed the browsers consume. | `realtime/server.mjs` |
| **Client adapter** | `window.ClawmatesLive` — connects to the bridge, dispatches typed events, and exposes them to each visualization. | `realtime/clawmates-live.js` |
The visualizations are **`.dc.html` files**: self-contained HTML that renders via the bundled
`support.js` runtime. They open directly in a browser — no build step, no npm install to *view*
them. The only thing that needs installing is (a) a static web server to serve them and (b) Node
for the live bridge.
---
## 1. System requirements
- **OS:** Linux x86_64 (Debian/Ubuntu 22.04+ or any systemd distro). ARM64 works too.
- **CPU/RAM:** 2 vCPU / 2 GB is plenty; the canvas animation runs client-side in the browser.
- **Node.js:** v20 LTS or newer (for the live bridge only).
- **A static file server:** any of `caddy`, `nginx`, or `npx serve`. Caddy is recommended (auto-HTTPS, one binary).
- **Outbound network:** only needed to *download* packages during install. The visualizations and
bridge run fully offline afterward — consistent with the §15 "sandboxes have no network" model.
- **A modern browser** to display: Chromium/Chrome 120+ or Firefox 121+ (Canvas2D + ES2020).
---
## 2. One-shot install
From the unzipped package root:
```bash
chmod +x install.sh
./install.sh
```
`install.sh` will:
1. Verify Node ≥ 20 (and print install instructions if missing — it will **not** silently install a runtime).
2. Install the bridge's dependencies (`npm ci` in `realtime/`, single dependency: none beyond Node built-ins — it's intentionally dependency-free).
3. Install **Caddy** if no static server is found (via the official apt repo; falls back to `npx serve`).
4. Write a `.env` from `.env.example` if one does not exist.
5. Print the exact next commands for **demo mode** and **live mode**.
If you prefer to do it by hand, the rest of this document is the manual path.
---
## 3. Run it
### 3a. Demo mode (no backend — synthetic events)
Best first run: proves the whole pipe lights up.
```bash
# Terminal 1 — the live bridge in demo mode (emits synthetic agent activity)
cd realtime
CLAWMATES_MODE=demo node server.mjs
# → Live bridge on http://localhost:8420/live (SSE) · mode=demo
# Terminal 2 — serve the visualizations
caddy file-server --root ../visualizations --listen :8080
# (or: npx serve ../visualizations -l 8080)
```
Open **http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live**
The `?live=` query param tells the client adapter where the SSE feed is. Switch to the **Live**
formation and you should see the agent particles converge on nodes **driven by real events from the
bridge**, not the built-in synthetic loop. (Without `?live=`, every visualization falls back to its
self-contained demo animation, so it always looks alive even with nothing wired.)
### 3b. Live mode [INTEGRATES WITH cm-api]
Point the bridge at the durable runner's SSE gateway and give it a session token. The bridge swaps
the session for a bearer token exactly like the Next.js `/api` proxy does, then streams.
```bash
cd realtime
cp ../.env.example ../.env # then edit ../.env
CLAWMATES_MODE=live node server.mjs
```
Required `.env` values for live mode:
```ini
CLAWMATES_MODE=live
# Where the runner publishes its raw run/turn SSE stream (cm-api streaming gateway):
CM_API_SSE_URL=http://127.0.0.1:8080/v1/runs/stream
# Bearer token (or a session cookie the bridge will exchange):
CM_API_TOKEN=sk-... # service token scoped to read run events
# The port this bridge listens on for browsers:
LIVE_PORT=8420
# Comma-separated allowed origins for the browser SSE (CORS):
LIVE_ALLOWED_ORIGINS=http://localhost:8080
```
> **Security note (carry this through):** the bridge is *read-only* on the runner stream. It must
> **never** hold or forward secret-broker credentials, and it must not expose any door-*approval*
> endpoint to the public browser. Approvals stay on the authenticated cm-api path. The bridge only
> *renders* that a door is pending (`door.request`) so the UI can badge it; the actual approve/deny
> call goes through your existing authenticated API, not through this bridge.
---
## 4. The Clawmates Event Taxonomy (the contract)
This is the heart of the real-time layer. The runner produces many internal events; the bridge
**normalizes** them into this small, stable set. The browser only ever sees these. Full JSON Schema
is in `realtime/events.schema.json`; this is the human summary.
Every event is a line on the SSE feed: `event: <type>\ndata: <json>\n\n`.
### Agent lifecycle & work
| Type | Payload | Drives |
|---|---|---|
| `agent.status` | `{ agentId, status: "online"\|"working"\|"idle", role }` | Status dots in every view; the list rail. |
| `agent.task.update` | `{ agentId, taskId, title, elapsedMs, steps:[{label,state:"done"\|"active"\|"pending"}] }` | Observe → "WORKING ON NOW" card. |
| `agent.reasoning.delta` | `{ agentId, text }` (token chunk) | Observe → REASONING STREAM console (append). |
| `agent.computer.frame` | `{ agentId, app:"browser"\|"terminal"\|"slack", url?, lines?:[{text,kind}] }` | Observe → the live computer screen tile. |
### Tools, doors & safety (§15)
| Type | Payload | Drives |
|---|---|---|
| `agent.tool.call` | `{ agentId, tool, target, doorRequired:bool }` | Reasoning stream "tool" lines. |
| `door.request` | `{ doorId, agentId, action, target, summary }` | The door-approval toast + "doors awaiting approval" badge. |
| `door.resolve` | `{ doorId, decision:"approve"\|"deny", by }` | Dismisses the toast; logs to history. |
### Collaboration & world
| Type | Payload | Drives |
|---|---|---|
| `agent.message` | `{ fromAgentId, toAgentId, text, ts }` | Observe System → INTER-AGENT COMMS stream; World → message-in-flight on the connector. |
| `world.touch` | `{ agentId, nodeId, kind:"service"\|"event" }` | **World → Live (Gource): the agent particle retargets to `nodeId` and emits a beam.** |
| `node.activity` | `{ nodeId, label, kind, heat:0..1 }` | World → node glow/pulse intensity. |
| `topology.update` | `{ formation:"hierarchy"\|"flat"\|..., nodes:[...], edges:[...] }` | World → re-layout (add/remove org units, agents, projects). |
### System telemetry & routines
| Type | Payload | Drives |
|---|---|---|
| `telemetry` | `{ tokensPerMin, costPerHr, loops, doorsPending }` | Top-bar pills + Observe System telemetry strip. |
| `routine.update` | `{ routineId, name, kind:"cron"\|"loop", owner, schedule?, progress?, state }` | Observe System → ROUTINES & LOOPS; the scheduler view. |
**Mapping rule of thumb:** the runner already emits a checkpointed event per step on the durable
runner. In the bridge's `normalize()` you translate each runner event into one or more taxonomy
events above. Start by mapping just `world.touch`, `agent.status`, and `telemetry` — that alone makes
the Large World view come alive — then add the rest incrementally.
---
## 5. How each visualization consumes the feed
The client adapter `clawmates-live.js` is included by each `.dc.html` (or injected — see §6). It:
1. Reads `?live=<url>` (or `window.CLAWMATES_LIVE_URL`); if absent, **does nothing** and the view keeps
its built-in synthetic animation.
2. Opens an `EventSource` to that URL.
3. Dispatches each event as a `CustomEvent` on `window` **and** calls any registered handlers:
```js
window.ClawmatesLive.on('world.touch', e => worldEngine.touch(e.agentId, e.nodeId));
window.ClawmatesLive.on('agent.reasoning.delta', e => observe.appendReasoning(e.agentId, e.text));
```
4. Buffers events fired before a view registers, and replays the last value per `nodeId`/`agentId`
so a late-loading view paints current state immediately (mirrors the runner's "resume at
checkpoint" behavior).
### Integration points inside the `.dc.html` files (where synthetic → live)
- **World — Live canvas:** in the logic class, the `_startLive()` engine has `targets[]` (the
project/service/event nodes) and an `agents[]` array whose `.target` index is currently chosen at
random on a timer. **Replace that random retarget with `world.touch`:** when a `world.touch`
arrives, set the matching agent's `.target` to the index of `nodeId` and force a beam. Use
`topology.update` to rebuild `targets[]`. Everything else (beams, sparks, glow) already keys off
the node being "hot", so it just works.
- **Observe — Agent:** the WORKING-ON-NOW card, the REASONING STREAM, and the computer screen are
static markup today. Bind them to `agent.task.update`, `agent.reasoning.delta`, and
`agent.computer.frame` respectively (append for deltas; replace for task/frame).
- **Observe — System & Dashboard:** bind the telemetry pills to `telemetry`, the comms list to
`agent.message`, routines to `routine.update`, and the door toast to `door.request`/`door.resolve`.
Each of these is a small `ClawmatesLive.on(...)` handler that calls `this.setState(...)`. No re-layout
of the markup is needed — you are feeding values into components that already exist.
---
## 6. Embedding into the existing Next.js app [INTEGRATES WITH cm-api]
You have two deployment shapes; pick one.
**A) Standalone (fastest):** serve the `visualizations/` folder with Caddy as in §3 and link to it
from the app, or drop it behind the same domain at `/viz/*`. The bridge runs as its own systemd
service. Good for an ops/observability surface that lives beside the product.
**B) In-app (tightest):** mount each `.dc.html` inside the Next.js app via an `<iframe src="/viz/...">`,
and have the app pass the live URL through: `/viz/world?live=/api/live`. Proxy `/api/live` in Next
to the bridge so it inherits the same session→bearer swap and CORS as the rest of `/api`. This keeps
auth and origin identical to the product.
Either way, the bridge is the **only** new long-running process. Run it under systemd:
```ini
# /etc/systemd/system/clawmates-live.service
[Unit]
Description=Clawmates Live Visualization Bridge
After=network.target
[Service]
WorkingDirectory=/opt/clawmates/handoff/realtime
EnvironmentFile=/opt/clawmates/handoff/.env
ExecStart=/usr/bin/node server.mjs
Restart=always
RestartSec=2
# Hardening — read-only, no privileged access (consistent with §15 posture):
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
```
```bash
sudo cp -r . /opt/clawmates/handoff
sudo systemctl enable --now clawmates-live
sudo systemctl status clawmates-live
```
---
## 7. Verification checklist
Run these in order; each should pass before moving on.
1. `node realtime/server.mjs` (demo) prints `Live bridge on http://localhost:8420/live · mode=demo`.
2. `curl -N http://localhost:8420/live` streams `event: ...` lines that don't stop. (Ctrl-C to exit.)
3. The World view at `?live=...` → **Live** formation shows particles **retargeting in sync** with the
`world.touch` lines you see in the curl output (not the smooth random demo motion).
4. Kill the bridge mid-stream → the browser shows the reconnect state and **resumes** when it returns
(EventSource auto-reconnects; the adapter replays last-known state).
5. **[live]** Point at cm-api, trigger a real run, and confirm `telemetry` numbers in the top bar move.
---
## 8. Troubleshooting
- **View looks alive but ignores the feed** → you didn't pass `?live=` (or `window.CLAWMATES_LIVE_URL`
isn't set); it's running the built-in synthetic animation. That's the intended fallback.
- **CORS error in console** → add the browser's origin to `LIVE_ALLOWED_ORIGINS` in `.env`.
- **SSE connects then drops every ~30s** → a proxy is buffering/timing out SSE. Disable buffering
(`proxy_buffering off;` in nginx; Caddy is fine by default) and set read timeout high.
- **Canvas blank on the Live tab** → the tab was never the active formation at first paint; switch to
it once. (The engine starts on mount/visibility.)
- **High CPU** → only the Live canvas animates; cap devicePixelRatio (already capped at 2) or pause
rendering when the tab is hidden (`document.hidden`).
---
## 9. What to build next (ordered)
1. Wire `world.touch` + `topology.update` → the World Live engine. (Biggest visual payoff, smallest change.)
2. Make canvas nodes clickable → drive the right-hand PANEL and drill into an agent (switch to Observe/Agent).
3. Bind Observe Agent to `agent.task.update` / `agent.reasoning.delta` / `agent.computer.frame`.
4. Add a **time-scrubber** that replays the durable runner's checkpoint log — Gource-style playback of
the world's history.
5. Promote the bridge from read-only mirror to a typed gateway with backpressure + per-tenant scoping.
---
*Generated as a handoff for the Clawmates visualization layer. The `.dc.html` files are the source of
truth for the UI; this document plus `realtime/` is everything needed to serve them and make them
breathe with live agent activity.*