From 50db17b8b7f0aa761d521f8169d05e7539d08e32 Mon Sep 17 00:00:00 2001 From: dimakis Date: Thu, 2 Jul 2026 00:33:40 +0100 Subject: [PATCH] docs: comprehensive documentation rewrite Rewrite README with accurate module table, full API summary, documentation index, and updated architecture diagram. Create seven new dedicated docs: - architecture.md: module structure, data flow, session lifecycle, design decisions - api-reference.md: complete REST API reference with request/response schemas - v2-protocol.md: WebSocket streaming protocol guide with message lifecycle - skills.md: skills system user guide with authoring instructions - task-board.md: task orchestration guide with DFS execution, spec mode, templates - session-isolation.md: worktree isolation guide with enforcement, cleanup, hooks - packages.md: npm workspace package reference for protocol, harness, client Co-Authored-By: Claude Opus 4.6 --- README.md | 301 ++++++++----- docs/api-reference.md | 883 ++++++++++++++++++++++++++++++++++++++ docs/architecture.md | 422 ++++++++++++++++++ docs/packages.md | 564 ++++++++++++++++++++++++ docs/session-isolation.md | 187 ++++++++ docs/skills.md | 184 ++++++++ docs/task-board.md | 273 ++++++++++++ docs/v2-protocol.md | 584 +++++++++++++++++++++++++ 8 files changed, 3285 insertions(+), 113 deletions(-) create mode 100644 docs/api-reference.md create mode 100644 docs/architecture.md create mode 100644 docs/packages.md create mode 100644 docs/session-isolation.md create mode 100644 docs/skills.md create mode 100644 docs/task-board.md create mode 100644 docs/v2-protocol.md diff --git a/README.md b/README.md index 82d9ae54..3d0a7c1d 100644 --- a/README.md +++ b/README.md @@ -2,88 +2,108 @@ Claude Code on your phone. A self-hosted web UI built on the [Agent SDK](https://docs.anthropic.com/en/docs/claude-code/sdk), designed for mobile over [Tailscale](https://tailscale.com). - - - ## Features - **Streaming chat** with thinking blocks, tool pills, and markdown -- **Three modes** — Ask (read-only), Agent (file edits allowed), Auto (shell too). Switch mid-chat. -- **Slash-command skills** — `/simplify`, `/risk-scan`, `/pr-review`, `/person`, `/review-response`, `/land-pr`, `/pr-shepherd`. Type `/` to browse. -- **Voice** — push-to-talk input (STT) and auto-speak output (TTS) via [Yapper](https://github.com/dimakis/yapper). Graceful degradation when offline. -- **MCP tools** — reads `~/.cursor/mcp.json`, passes servers to every session -- **File browser** — view and edit repo files, switch between worktree roots -- **Task board** — recursive multi-session task orchestration with spec mode, completion summaries, and verification hooks -- **Worktree sandbox** — opt-in git worktree isolation per session, multi-repo support via `.mitzo.json` -- **Session resilience** — phone sleeps, WS drops, session survives. Reattach on reconnect. Message snapshot recovery for iOS silent drops. -- **iOS app** — native wrapper via Capacitor with push notifications and home-screen install -- **Auto-rename sessions** — sessions get meaningful names via LLM summarization after every few prompts -- **Quick actions** — one-tap commands via `.mitzo.json` -- **Push notifications** — ntfy + Pushover (Apple Watch) when Claude needs approval -- **Image attachments** — send photos/screenshots from your camera -- **Session history** — resume past conversations, swipe to dismiss - -## Quick start +- **Three modes** -- Ask (read-only), Agent (file edits allowed), Auto (shell too). Switch mid-chat. +- **Slash-command skills** -- `/simplify`, `/risk-scan`, `/pr-review`, `/person`, `/review-response`, `/land-pr`, `/pr-shepherd`. Type `/` to browse. +- **Voice** -- push-to-talk input (STT) and auto-speak output (TTS) via [Yapper](https://github.com/dimakis/yapper). Graceful degradation when offline. +- **MCP tools** -- reads `~/.cursor/mcp.json`, passes servers to every session +- **File browser** -- view and edit repo files, switch between worktree roots +- **Task board** -- recursive multi-session task orchestration with spec mode, completion summaries, and verification hooks +- **Worktree sandbox** -- deterministic git worktree isolation per session, multi-repo support via `.mitzo.json` +- **Session resilience** -- phone sleeps, WS drops, session survives. Reattach on reconnect. Message snapshot recovery for iOS silent drops. +- **iOS app** -- native wrapper via Capacitor with push notifications and home-screen install +- **Desktop mode** -- side-by-side chat + file viewer on wide screens +- **Auto-rename sessions** -- sessions get meaningful names via LLM summarization after every few prompts +- **Quick actions** -- one-tap commands via `.mitzo.json` +- **Push notifications** -- ntfy + Pushover (Apple Watch) + APNs when Claude needs approval +- **Image attachments** -- send photos/screenshots from your camera +- **Session history** -- resume past conversations, search, swipe to dismiss +- **Multi-model reasoning** -- deliberation and fusion orchestrators for collaborative multi-model reasoning +- **Observability** -- OpenTelemetry tracing (Jaeger), structured logging (Pino/Loki/Grafana), experiment tracking (MLflow) + +## Quick Start ```bash git clone https://github.com/dimakis/mitzo.git && cd mitzo npm install cp .env.example .env # set AUTH_PASSPHRASE, AUTH_SECRET, REPO_PATH npm run build && npm start -# http://localhost:3100 +# https://localhost:3100 ``` -Access from your phone: install [Tailscale](https://tailscale.com/download) on server and phone, then open `http://:3100`. No HTTPS needed — Tailscale encrypts via WireGuard. +Access from your phone: install [Tailscale](https://tailscale.com/download) on server and phone, then open `https://:3100`. Tailscale encrypts via WireGuard -- no public DNS, no port forwarding needed. + +See [docs/onboarding.md](docs/onboarding.md) for the full setup walkthrough including HTTPS certificates, iOS app, voice, push notifications, and observability. ## Architecture ``` -Phone (Tailscale) ──┬── HTTP: REST API - └── WebSocket: v2 streaming protocol - │ - Server (Node + TypeScript) - │ - ├── query-loop: SDK events → v2 protocol - ├── session-registry: detach/reattach/snapshot - ├── MCP servers from Cursor config - ├── git worktrees (opt-in) - └── passphrase + JWT auth +Phone / Laptop (Tailscale) + | + +-- HTTPS: REST API (Express) + +-- WSS: v2 streaming protocol + | + Your Mac (Node.js + TypeScript) + | + +-- Anthropic Agent SDK + | +-- query-loop: SDK events -> v2 block protocol + +-- Session registry (detach/reattach/snapshot recovery) + +-- Connection registry (single multiplexed WS per client) + +-- Worktree manager (multi-repo git isolation) + +-- Task orchestrator (goal decomposition + DFS execution) + +-- Skill registry (bundled + user + repo scoped) + +-- MCP servers (from Cursor config) + +-- Hook bridge (project hooks -> SDK) + +-- Event store (SQLite, session replay + search) + +-- Push notifications (ntfy + Pushover + APNs) + +-- Passphrase + JWT auth + +-- Reasoning harness (deliberation + fusion orchestrators) + + Observability (optional, podman) + +-- Jaeger (OTLP traces) + +-- Loki (log aggregation) + +-- Grafana (dashboards) + +-- MLflow (experiment tracking) ``` -The server translates raw SDK stream events into a v2 block lifecycle protocol (`block_start` → `block_delta` → `block_end`). Explicit turn boundaries (`message_start`/`message_end`), deferred finalization, and message snapshots for reconnect recovery. See [docs/design/message-protocol-v2.md](docs/design/message-protocol-v2.md). +The server translates raw SDK stream events into a **v2 block lifecycle protocol** (`block_start` > `block_delta` > `block_end`). Sessions survive WebSocket disconnects -- when your phone reconnects, it reattaches and replays from a snapshot. See [docs/v2-protocol.md](docs/v2-protocol.md). -### Packages (`packages/`) — npm workspace +### Packages (`packages/`) -- npm workspace Mitzo uses an npm workspace with three internal packages shared between server and frontend: -| Package | Purpose | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `@mitzo/protocol` | Core protocol types, Zod schemas (v2 WS messages, API schemas), tool summarization, event store definitions | -| `@mitzo/harness` | Session registry, connection registry, permission handler, worktree guard, tool tiers, skill policy, auto-rename, notifications, logger | -| `@mitzo/client` | Frontend state management: `MitzoConnection` (single multiplexed WS), Zustand store (`createMitzoStore`), v2 protocol parser, session switching, message reducer | +| Package | Purpose | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@mitzo/protocol` | Core types, Zod schemas (v2 WS messages, API schemas), tool summarization, event store definitions, agent definition types, constants | +| `@mitzo/harness` | Session registry, connection registry, permission handler, worktree guard, tool tiers, skill policy, auto-rename, notifications, reasoning orchestrators, model providers, logger | +| `@mitzo/client` | Frontend state management: `MitzoConnection` (single multiplexed WS), Zustand store with 12 state slices, v2 protocol parser, API client, SSE fallback transport, React hooks | + +See [docs/packages.md](docs/packages.md) for the full package reference. ### Backend (`server/`) -**Core** — Event streaming, session lifecycle, SDK integration +**Core** -- Event streaming, session lifecycle, SDK integration -| File | Purpose | -| ----------------------- | ----------------------------------------------------------------------------------- | -| `query-loop.ts` | SDK → v2 event translator. Deferred `message_end`, snapshot state, block lifecycle. | -| `chat.ts` | Agent SDK `query()`, prompt assembly, streaming-input queue, session restore API | -| `session-registry.ts` | Session state: detach, reattach, rekey, TTL abort, snapshot storage | -| `permission-handler.ts` | `canUseTool` callback — auto-allow by tier, prompt via WS + push notifications | -| `async-queue.ts` | `AsyncIterable` queue for follow-up messages and interrupt | +| File | Purpose | +| ----------------------- | ------------------------------------------------------------------------------------ | +| `query-loop.ts` | SDK -> v2 event translator. Deferred `message_end`, snapshot state, block lifecycle. | +| `chat.ts` | Agent SDK `query()`, prompt assembly, streaming-input queue, session restore API | +| `session-registry.ts` | Session state: detach, reattach, rekey, TTL abort, snapshot storage | +| `permission-handler.ts` | `canUseTool` callback -- auto-allow by tier, prompt via WS + push notifications | +| `async-queue.ts` | `AsyncIterable` queue for follow-up messages and interrupt | -**Skills** — Slash-command system +**Skills** -- Slash-command system -| File | Purpose | -| -------------------- | --------------------------------------------------------- | -| `skills.ts` | Skill registry — scoped discovery, precedence, collisions | -| `slash-commands.ts` | Slash-command parsing and prompt expansion | -| `skill-policy.ts` | Per-turn tool restriction from skill frontmatter | -| `native-commands.ts` | Built-in native commands (`/skills`) | +| File | Purpose | +| -------------------- | ---------------------------------------------------------- | +| `skills.ts` | Skill registry -- scoped discovery, precedence, collisions | +| `slash-commands.ts` | Slash-command parsing and prompt expansion | +| `skill-policy.ts` | Per-turn tool restriction from skill frontmatter | +| `native-commands.ts` | Built-in native commands (`/skills`) | -**Task Board** — Multi-session orchestration +**Task Board** -- Multi-session orchestration | File | Purpose | | ---------------------- | ------------------------------------------------------------------------------------------- | @@ -120,65 +140,61 @@ Mitzo uses an npm workspace with three internal packages shared between server a **WebSocket & Transport** -| File | Purpose | -| ------------------- | ------------------------------------------------------------------ | -| `ws-handler-v2.ts` | v2 WebSocket message dispatcher: hello handshake → session routing | -| `ws-transport.ts` | `SessionTransport` adapter wrapping WebSocket connections | -| `null-transport.ts` | Null transport for testing | -| `ws-schemas.ts` | Zod schemas for WebSocket message validation | +| File | Purpose | +| ------------------- | ------------------------------------------------------------------- | +| `ws-handler-v2.ts` | v2 WebSocket message dispatcher: hello handshake -> session routing | +| `ws-transport.ts` | `SessionTransport` adapter wrapping WebSocket connections | +| `null-transport.ts` | Null transport for testing | +| `ws-schemas.ts` | Zod schemas for WebSocket message validation | + +**Chat REST Handler** + +| File | Purpose | +| ---------------------- | --------------------------------------------------------------------------------------------- | +| `chat-rest-handler.ts` | HTTP alternative to WebSocket: SSE stream + POST endpoints for send/stop/interrupt/permission | **Supporting** -| File | Purpose | -| ----------------------- | ------------------------------------------------- | -| `tool-tiers.ts` | Risk classification + mode/tier auto-allow matrix | -| `tool-summary.ts` | Summarizes tool inputs for pill display | -| `permissions.ts` | Request/response registry | -| `content-blocks.ts` | SDK content block parsing | -| `event-store.ts` | Persistent event store for session replay | -| `auto-rename.ts` | LLM-based session auto-renaming | -| `hook-bridge.ts` | Project hooks → Agent SDK bridge | -| `api-schemas.ts` | Zod validation schemas for HTTP | -| `mcp-config.ts` | Loads Cursor MCP config | -| `repo-config.ts` | `.mitzo.json` reader | -| `app.ts` | Express app factory (testability via supertest) | -| `inbox.ts` | Inbox integration endpoint | -| `internal-token.ts` | Internal token generation for inter-process auth | -| `auth.ts` | Passphrase + JWT | -| `git-version.ts` | Local/remote commit comparison | -| `port-check.ts` | Prevents duplicate server instances | -| `constants.ts` | Server-wide constants | -| `index.ts` | Express app, HTTP server + WebSocket | -| `goal-client.ts` | ContexGin Goal Registry client | -| `progress-tracker.ts` | Progress tracking utilities | -| `prompt-compare.ts` | Prompt comparison utilities | -| `workflow-templates.ts` | Workflow templates | -| `workload-store.ts` | Workload persistence | -| `session-overview.ts` | Session overview API | -| `signal-processor.ts` | Signal processing utilities | - -### Frontend (`frontend/`) — React 19 + Vite - -React 19 + Vite. Ten pages (`Login`, `SessionList`, `ChatView`, `DesktopChatView`, `FileViewer`, `InboxView`, `CalendarView`, `TodoView`, `TodoDetailView`, `TaskBoard`), a `useReducer`-based message state machine (`useChatMessages`), module-level WebSocket pool with 500-message buffer, and components for thinking blocks, tool pills, tool groups, permission banners, and a slash-command picker. Capacitor wraps the frontend for iOS deployment via TestFlight. +| File | Purpose | +| ------------------- | ------------------------------------------------- | +| `tool-tiers.ts` | Risk classification + mode/tier auto-allow matrix | +| `tool-summary.ts` | Summarizes tool inputs for pill display | +| `permissions.ts` | Request/response registry | +| `content-blocks.ts` | SDK content block parsing | +| `event-store.ts` | Persistent event store for session replay | +| `auto-rename.ts` | LLM-based session auto-renaming | +| `hook-bridge.ts` | Project hooks -> Agent SDK bridge | +| `api-schemas.ts` | Zod validation schemas for HTTP | +| `mcp-config.ts` | Loads Cursor MCP config | +| `repo-config.ts` | `.mitzo.json` reader | +| `app.ts` | Express app factory (testability via supertest) | +| `auth.ts` | Passphrase + JWT | +| `internal-token.ts` | Internal token generation for inter-process auth | +| `goal-client.ts` | ContexGin Goal Registry client | +| `index.ts` | Express app, HTTP server + WebSocket | + +### Frontend (`frontend/`) -- React 19 + Vite + +React 19 + Vite. Ten pages (`Login`, `SessionList`, `ChatView`, `DesktopChatView`, `FileViewer`, `InboxView`, `CalendarView`, `TodoView`, `TodoDetailView`, `TaskBoard`), a `useReducer`-based message state machine (`useChatMessages`), module-level WebSocket with sequence tracking and reconnect recovery, and components for thinking blocks, tool pills, tool groups, permission banners, and a slash-command picker. Capacitor wraps the frontend for iOS deployment via TestFlight. **Key Hooks:** -- `useChatMessages` — v2 protocol message reducer (MESSAGE_START/BLOCK_START/BLOCK_DELTA/BLOCK_END/TOOL_RESULT/MESSAGE_END/SESSION_END/MESSAGE_SNAPSHOT/RESTORE) -- `useTaskBoard` — task CRUD + loop control + WS subscriptions -- `useVoice` — STT (push-to-talk) + TTS (auto-speak toggle, voice selection, sequential chunk playback) -- `useFileNavigation` / `useFileEditor` — file browser and editing -- `useSessionOverview` — session metadata and statistics -- `useAutoSpeak` — auto-speak TTS preferences -- `useServiceHealth` — health status for Yapper, ContexGin +- `useChatMessages` -- v2 protocol message reducer (MESSAGE_START/BLOCK_START/BLOCK_DELTA/BLOCK_END/TOOL_RESULT/MESSAGE_END/SESSION_END/MESSAGE_SNAPSHOT/RESTORE) +- `useTaskBoard` -- task CRUD + loop control + WS subscriptions +- `useVoice` -- STT (push-to-talk) + TTS (auto-speak toggle, voice selection, sequential chunk playback) +- `useFileNavigation` / `useFileEditor` -- file browser and editing +- `useSessionOverview` -- session metadata and statistics +- `useAutoSpeak` -- auto-speak TTS preferences +- `useServiceHealth` -- health status for Yapper, ContexGin **Key Components:** - `MessageBubble` (UserBubble/TextBubble), `ThinkingBlock`, `ToolPill`, `ToolGroup`, `PermissionBanner`, `ChatInput`, `SlashPicker` -- `TaskNode`, `TaskCreateForm`, `LoopControls`, `TaskSidebar` — task board UI -- `VoiceSettings` — speaker toggle with pulse indicator, voice picker dropdown grouped by language -- `SessionOverview` — session metadata card -- `ContextPanel` — boot context viewer -- `FileBrowserPanel` — file tree navigation +- `TaskNode`, `TaskCreateForm`, `LoopControls`, `TaskSidebar` -- task board UI +- `VoiceSettings` -- speaker toggle with pulse indicator, voice picker dropdown grouped by language +- `SessionOverview` -- session metadata card +- `ContextPanel` -- boot context viewer +- `FileBrowserPanel` -- file tree navigation ## Environment @@ -232,36 +248,95 @@ Drop this in your repo root to customize the home screen, enable multi-repo sess "extraTools": "Bash" } ], - "repos": [{ "name": "sibling-repo", "path": "../sibling-repo" }], + "repos": { "sibling-repo": "../sibling-repo" }, "contextBlocks": { "Architecture": "/path/to/architecture.md" }, - "venvPaths": [".venv/bin"] + "venvPaths": [".venv/bin"], + "toolTierOverrides": { + "mcp__jira__jira_search": "safe" + } } ``` -- **quickActions** — one-tap buttons on the home screen -- **repos** — sibling repos for multi-repo worktree sessions (each gets its own isolated worktree) -- **contextBlocks** — markdown files injected into every session as domain knowledge -- **roots** — switchable repo roots in the file browser -- **venvPaths** — Python venv paths added to `PATH` +- **quickActions** -- one-tap buttons on the home screen +- **repos** -- sibling repos for multi-repo worktree sessions (each gets its own isolated worktree) +- **roots** -- switchable repo roots in the file browser +- **contextBlocks** -- markdown files injected into every session as domain knowledge +- **allowedPaths** -- additional directories Claude can access beyond `REPO_PATH` +- **venvPaths** -- Python venv paths added to `PATH` +- **toolTierOverrides** -- override default risk tier for any tool (`safe`, `standard`, `elevated`, `unknown`) See [docs/onboarding.md](docs/onboarding.md) for a full configuration walkthrough. +## API + +Mitzo exposes a REST API and a WebSocket protocol for chat interaction: + +| Category | Endpoints | Description | +| ----------- | ------------------------------------------------------------- | ----------------------------------------------------------------- | +| Auth | `POST /api/auth/login`, `logout`, `check` | Passphrase login, JWT cookies | +| Sessions | `POST /api/sessions`, `GET`, `DELETE`, `PUT rename`, `search` | Session lifecycle and management | +| Chat (WS) | `ws://host/ws/chat` | v2 streaming protocol -- send, interrupt, stop, permissions, mode | +| Chat (REST) | `GET /api/chat/events` (SSE) + POST endpoints | HTTP alternative to WebSocket | +| Tasks | `GET/POST/PATCH/DELETE /api/tasks`, loop control | Task board CRUD and orchestration | +| Files | `GET /api/files/list`, `read`, `download`, `PUT write` | File browser operations | +| Skills | `GET /api/skills` | Available skills registry | +| Config | `GET /api/config`, `models`, `version` | Server configuration and metadata | +| Calendar | `GET /api/calendar` | Calendar events and sprints | +| Todos | `GET/POST /api/todos` | Todo items (Telos integration) | +| Inbox | `GET/POST /api/inbox` | Agent inbox items | +| Workload | `GET/PATCH/DELETE /api/workload/items` | Workload signal tracking | +| Events | `GET /api/events` (SSE) | Server-sent events for live updates | +| Push | `POST /api/push/register` | Device token registration (APNs) | + +See [docs/api-reference.md](docs/api-reference.md) for the complete reference with request/response schemas. + +## Documentation + +| Document | Description | +| ---------------------------------------------- | -------------------------------------------------------------------------- | +| [Onboarding](docs/onboarding.md) | Full setup walkthrough -- server, mobile, iOS app, voice, observability | +| [Architecture](docs/architecture.md) | Deep dive into module structure, data flow, and design decisions | +| [API Reference](docs/api-reference.md) | Complete REST API and WebSocket protocol reference | +| [v2 Protocol](docs/v2-protocol.md) | WebSocket streaming protocol -- message lifecycle, reconnection, subagents | +| [Skills](docs/skills.md) | Skills system -- discovery, precedence, authoring custom skills | +| [Task Board](docs/task-board.md) | Task orchestration -- goal decomposition, DFS execution, spec mode | +| [Session Isolation](docs/session-isolation.md) | Worktree isolation -- multi-repo, enforcement, cleanup, external hooks | +| [Packages](docs/packages.md) | npm workspace package reference -- protocol, harness, client | + +### Design Documents + +Internal design documents live in `docs/design/`. These capture implementation decisions and are not user-facing: + +- `message-protocol-v2.md` -- v2 streaming protocol design +- `global-task-board.md` -- task board architecture +- `skills-system-v1-plan.md` -- skills system design +- `session-isolation-overhaul.md` -- session isolation redesign +- `session-state-machine.md` -- session state machine +- `voice-integration.md` -- voice architecture +- `tts-playback.md` -- TTS playback design +- `streaming-input-session-control.md` -- streaming input +- `otel-deep-instrumentation.md` -- observability roadmap +- `context-blocks.md` -- context block injection +- `token-visibility.md` -- token usage display + ## Development ```bash npm run dev # backend + frontend concurrently -npm test # vitest — full suite +npm test # vitest -- full suite npm run lint # eslint npm run format:check # prettier ``` -Pre-commit: husky + lint-staged + commitlint (conventional commits). The hook also runs [gitleaks](https://github.com/gitleaks/gitleaks) if installed, scanning staged changes for secrets. gitleaks is **optional** — the hook skips it gracefully when not found. Install via `brew install gitleaks` (macOS) or see the [gitleaks docs](https://github.com/gitleaks/gitleaks#installing). +Pre-commit: husky + lint-staged + commitlint (conventional commits). The hook also runs [gitleaks](https://github.com/gitleaks/gitleaks) if installed, scanning staged changes for secrets. gitleaks is optional -- the hook skips it gracefully when not found. + +**All work goes through branches and PRs.** A pre-commit hook rejects commits on `main`. ## Tech -Node.js, Express, React 19, Vite, TypeScript, Claude Agent SDK, Vitest, ESLint, Prettier. +Node.js, Express, React 19, Vite, TypeScript, Claude Agent SDK, Vitest, ESLint, Prettier, Capacitor (iOS), Zustand, Zod, Pino, OpenTelemetry, SQLite (better-sqlite3). ## Attribution diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 00000000..1141e1eb --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,883 @@ +# API Reference + +Complete REST API and WebSocket protocol reference for the Mitzo server. Default endpoint: `https://localhost:3100`. + +All endpoints require authentication via session cookie unless otherwise noted. Login via `POST /api/auth/login` to obtain a cookie. + +## Authentication + +### POST /api/auth/login + +Login with passphrase. + +**Request:** + +```json +{ "passphrase": "your-passphrase" } +``` + +**Response:** + +```json +{ "ok": true, "token": "jwt-token-string" } +``` + +Sets `HttpOnly` cookie for subsequent requests. + +### POST /api/auth/logout + +Clear authentication cookie. + +**Response:** `{ "ok": true }` + +### GET /api/auth/check + +Verify current authentication status. + +**Response:** `{ "ok": true }` or `401 Unauthorized` + +## Sessions + +### POST /api/sessions + +Create a new session with optional worktree isolation. Used by external hooks (Claude Code, Cursor) to get worktree paths. + +**Auth:** Internal token (`X-Internal-Token` header) + +**Request:** + +```json +{ + "source": "claude-code", + "initialPrompt": "Fix the login bug", + "summary": "Session summary", + "mode": "agent", + "model": "claude-opus-4" +} +``` + +| Field | Type | Required | Description | +| --------------- | -------- | -------- | ---------------------------------------------------------- | +| `source` | `string` | Yes | Source identifier (e.g., `claude-code`, `cursor`, `mitzo`) | +| `initialPrompt` | `string` | No | Initial prompt for the session | +| `summary` | `string` | No | Session summary/title | +| `mode` | `string` | No | Permission mode: `ask`, `agent`, `auto` | +| `model` | `string` | No | Model override | + +**Response:** + +```json +{ + "sessionId": "2026-07-01-abc123", + "worktrees": { + "primary": "/path/to/repo/.claude/worktrees/2026-07-01-abc123", + "sibling": "/path/to/sibling/.claude/worktrees/2026-07-01-abc123" + }, + "isolation": true +} +``` + +### GET /api/sessions + +List sessions with pagination. + +**Query parameters:** + +| Parameter | Type | Default | Description | +| --------- | --------- | ------- | ----------------------- | +| `offset` | `number` | `0` | Pagination offset | +| `limit` | `number` | `20` | Page size | +| `full` | `boolean` | `false` | Include filesystem scan | + +**Response:** + +```json +{ + "sessions": [ + { + "id": "abc123", + "summary": "Fix login bug", + "lastModified": 1720000000, + "isActive": true, + "totalTokens": 15000, + "numTurns": 5 + } + ], + "hasMore": true +} +``` + +### GET /api/sessions/:id/messages + +Get all messages for a session. + +**Response:** Array of `FinishedMessage` objects. + +### GET /api/sessions/:id/meta + +Get session metadata including token usage, branch, working directory, and mode. + +**Response:** + +```json +{ + "sessionId": "abc123", + "summary": "Fix login bug", + "branch": "session/abc123", + "cwd": "/path/to/worktree", + "mode": "agent", + "state": "ACTIVE", + "inputTokens": 10000, + "outputTokens": 5000, + "cacheReadTokens": 3000, + "cacheCreationTokens": 1000, + "costUsd": 0.45, + "promptCount": 5, + "createdAt": 1720000000, + "updatedAt": 1720001000 +} +``` + +### GET /api/sessions/:id/events + +Get events since a sequence number (for reconnection replay). + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | -------- | ------------------------------ | +| `after` | `number` | Sequence number to resume from | + +**Response:** Array of stored events with sequence numbers. + +### GET /api/sessions/active + +Get currently active (attached) sessions. + +### GET /api/sessions/search + +Search sessions by query string. + +**Query parameters:** + +| Parameter | Type | Default | Description | +| --------- | -------- | -------- | ------------ | +| `q` | `string` | required | Search query | +| `limit` | `number` | `50` | Max results | + +### DELETE /api/sessions/:id + +Hide/delete a session from the list. + +### DELETE /api/sessions + +Hide all sessions. + +### PUT /api/sessions/:id/rename + +Rename a session. + +**Request:** `{ "title": "New name" }` + +### POST /api/sessions/suspend + +Suspend sessions (used by iOS background handler via `sendBeacon()`). + +**Request:** + +```json +{ + "connectionId": "conn-123", + "sessions": [{ "sessionId": "abc123", "lastSeq": 42 }] +} +``` + +**Response:** `204 No Content` + +## Chat (REST Alternative) + +HTTP alternative to the WebSocket protocol. All endpoints require the `X-Connection-ID` header obtained from the SSE welcome event. + +### GET /api/chat/events + +Open a Server-Sent Events stream. Returns a `welcome` event containing the `connectionId`. + +**SSE Events:** + +``` +event: welcome +data: {"connectionId": "conn-abc123"} + +event: message +data: {"v": 2, "type": "block_start", ...} +``` + +### POST /api/chat/send + +Send a chat message. Same payload as the WebSocket `send` message. + +### POST /api/chat/interrupt + +Interrupt the current query with a follow-up message. + +### POST /api/chat/stop + +Stop the current query. + +### POST /api/chat/permission + +Respond to a permission request. + +### POST /api/chat/mode + +Set chat mode (`ask`, `agent`, `auto`). + +### POST /api/chat/watch + +Subscribe to session events. + +### POST /api/chat/unwatch + +Unsubscribe from session events. + +### POST /api/chat/switch + +Switch active session. + +### POST /api/chat/suspend + +Suspend session. + +### POST /api/chat/close + +Close session. + +### POST /api/chat/reconnect + +Reconnect to sessions with event replay. + +## Configuration + +### GET /api/config + +Get server configuration including repo path, MCP servers, quick actions, and context blocks. + +**Response:** + +```json +{ + "repoPath": "/path/to/repo", + "mcpServers": ["jira", "gitlab"], + "quickActions": [ + { + "label": "Run Tests", + "desc": "Full suite", + "prompt": "Run tests and report." + } + ], + "contextBlocks": { + "Architecture": { "title": "Architecture", "path": "/path/to/arch.md" } + }, + "fileViewerRoots": [{ "label": "Main", "path": "/path/to/repo" }] +} +``` + +### GET /api/models + +Get available LLM models. + +### GET /api/version + +Get server build info. + +**Response:** + +```json +{ + "hash": "abc1234", + "commit": "feat: add dark mode", + "updateAvailable": false +} +``` + +### POST /api/version/check + +Check for available updates by comparing local and remote git commits. + +### GET /api/service-health + +Get health status of dependent services (Yapper, ContexGin). + +**Response:** + +```json +{ + "services": [ + { "name": "yapper", "ok": true, "detail": "healthy" }, + { "name": "contexgin", "ok": false, "detail": "connection refused" } + ], + "checkedAt": 1720000000 +} +``` + +## Skills + +### GET /api/skills + +Get available skills (merged from all scopes with collision info). + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `cwd` | `string` | Working directory for repo-scoped skill discovery | + +**Response:** Array of skill definitions with name, description, source scope, and collision metadata. + +## Files + +### GET /api/files/roots + +Get configured file browser roots. + +**Response:** + +```json +[ + { "label": "Main", "path": "/path/to/repo" }, + { "label": "Tooling", "path": "/path/to/tools" } +] +``` + +### GET /api/files/list + +List directory contents. + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | -------- | -------------------------- | +| `root` | `string` | Root path | +| `dir` | `string` | Directory relative to root | + +**Response:** + +```json +{ + "currentDir": "/path/to/repo/src", + "entries": [ + { "name": "index.ts", "isDir": false }, + { "name": "components", "isDir": true } + ] +} +``` + +### GET /api/files/read + +Read file contents. + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | -------- | ------------------ | +| `path` | `string` | Absolute file path | + +**Response:** + +```json +{ + "path": "/path/to/file.ts", + "content": "const x = 1;\n...", + "ext": "ts" +} +``` + +### GET /api/files/download + +Download file as binary attachment. + +### PUT /api/files/write + +Write file contents. + +**Request:** + +```json +{ + "path": "/path/to/file.ts", + "content": "const x = 2;\n...", + "createIfMissing": true +} +``` + +**Response:** `{ "ok": true }` + +### GET /api/images/:imageId + +Get a tool result image by ID. Returns binary image data with appropriate Content-Type header. + +## Git + +### GET /api/git/info + +Get current branch and worktree information. + +**Response:** + +```json +{ + "branch": "main", + "worktrees": [ + { + "id": "session-abc123", + "branch": "session/abc123", + "repo": "primary", + "path": "/path/to/worktree" + } + ] +} +``` + +### GET /api/worktrees + +List all git worktrees. + +## Task Board + +### GET /api/tasks + +Get all tasks as a tree. + +**Response:** `{ "tasks": [...] }` + +### POST /api/tasks + +Create a task. + +**Request:** + +```json +{ + "title": "Add dark mode support", + "parentId": "goal-123", + "description": "Implement theme switching", + "priority": 1, + "sessionPolicy": "reuse", + "stageType": "agent", + "maxRetries": 3 +} +``` + +| Field | Type | Required | Description | +| --------------- | -------- | -------- | ---------------------------------------- | +| `title` | `string` | Yes | Task title | +| `parentId` | `string` | No | Parent task ID (for subtasks) | +| `description` | `string` | No | Task description | +| `priority` | `number` | No | Priority (lower = higher priority) | +| `sessionPolicy` | `string` | No | `reuse` (Phase 2 only) | +| `stageType` | `string` | No | Stage type: `agent`, `wait_for_signal` | +| `gateConfig` | `object` | No | Gate configuration for signal stages | +| `maxRetries` | `number` | No | Max retry attempts on failure | +| `templateId` | `string` | No | Template this task was instantiated from | + +### GET /api/tasks/:id + +Get a specific task. + +### PATCH /api/tasks/:id + +Update a task. + +### DELETE /api/tasks/:id + +Delete a task. + +### POST /api/tasks/:id/approve + +Approve a task in `pending_review` status. + +### POST /api/tasks/:id/reject + +Reject a task. Optional `feedback` in body. + +### POST /api/tasks/:id/signal + +Send a signal to a `wait_for_signal` stage task. + +**Request:** + +```json +{ + "status": "success", + "artifacts": { "pr_url": "https://github.com/..." } +} +``` + +## Loop Orchestration + +### GET /api/loop/status + +Get orchestration loop status. + +**Response:** + +```json +{ + "state": "running", + "goalId": "goal-123", + "activeTaskId": "task-456", + "progress": { "done": 3, "total": 5 }, + "specMode": false, + "awaitingApproval": false +} +``` + +### POST /api/loop/start + +Start the orchestration loop. + +**Request:** + +```json +{ + "goalId": "goal-123", + "specMode": true +} +``` + +### POST /api/loop/pause + +Pause execution. + +### POST /api/loop/resume + +Resume execution. + +### POST /api/loop/stop + +Stop the loop. + +### POST /api/loop/spec/approve + +Approve spec-mode decomposition. + +### POST /api/loop/spec/reject + +Reject spec-mode decomposition (re-plan). + +## Workflows and Templates + +### GET /api/templates + +List available workflow templates. + +### GET /api/templates/:id + +Get a specific template. + +### POST /api/templates + +Create a workflow template. + +**Request:** + +```json +{ + "name": "PR Review Pipeline", + "description": "Standard PR review workflow", + "stages": [ + { "title": "Run tests", "stageType": "agent" }, + { "title": "Wait for CI", "stageType": "wait_for_signal", "gateConfig": { "type": "gh_ci" } }, + { "title": "Review", "stageType": "agent" } + ] +} +``` + +### DELETE /api/templates/:id + +Delete a template. + +### POST /api/workflows/instantiate + +Instantiate a template as a goal. + +**Request:** + +```json +{ + "templateId": "tmpl-123", + "title": "Review PR #42", + "variables": { "pr_number": 42 } +} +``` + +**Response:** `{ "task": {...} }` + +## Signals + +### POST /api/signals/resolve + +Resolve a signal by gate metadata (for external agents like Centaur). + +**Auth:** Internal token + +**Request:** + +```json +{ + "type": "gh_ci", + "repo": "dimakis/mitzo", + "pr": 42, + "status": "success", + "artifacts": { "run_url": "https://..." } +} +``` + +**Response:** `{ "ok": true, "matched": ["task-456"] }` + +Signal types: `gh_ci`, `gh_review`, `centaur_review`, `human_approval`. + +## Inbox + +### GET /api/inbox + +List inbox items. + +### POST /api/inbox + +Create an inbox item. + +**Request:** + +```json +{ + "source": "troubadour", + "title": "Cross-spoke connection found", + "body": "Found a connection between...", + "tags": ["proposal"] +} +``` + +### GET /api/inbox/:filename + +Get inbox item content. + +### POST /api/inbox/:filename/approve + +Approve an inbox item. + +### DELETE /api/inbox/:filename + +Delete an inbox item. + +## Calendar + +### GET /api/calendar + +Get calendar events and sprint information. + +**Query parameters:** + +| Parameter | Type | Default | Description | +| --------- | -------- | ------- | ----------------------- | +| `date` | `string` | today | Start date (YYYY-MM-DD) | +| `days` | `number` | `7` | Number of days (1-31) | + +**Response:** + +```json +{ + "startDate": "2026-07-01", + "endDate": "2026-07-07", + "events": [...], + "sprints": [...] +} +``` + +## Todos + +### GET /api/todos + +Get todo items (Telos integration). + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | --------- | ------------------------- | +| `profile` | `string` | Filter by profile | +| `refresh` | `boolean` | Force refresh from source | + +### POST /api/todos + +Create a todo item. + +**Request:** + +```json +{ + "summary": "Implement dark mode", + "profile": "work", + "parentId": "parent-id" +} +``` + +### POST /api/todos/:id/action + +Perform an action on a todo. + +**Request:** + +```json +{ + "action": "done", + "days": 7 +} +``` + +Actions: `done`, `snooze`, `archive`, `delete`. + +## Workload + +### GET /api/workload/items + +List workload items. + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | --------- | -------------------- | +| `profile` | `string` | Filter by profile | +| `status` | `string` | Filter by status | +| `starred` | `boolean` | Filter starred items | + +### GET /api/workload/items/:id + +Get a specific workload item. + +### PATCH /api/workload/items/:id + +Update a workload item. + +### DELETE /api/workload/items/:id + +Delete a workload item. + +### POST /api/workload/items/:id/promote + +Promote a workload item to a task board task. + +### POST /api/workload/signals + +Ingest a workload signal. + +### POST /api/workload/signals/batch + +Batch ingest workload signals. + +## Push Notifications + +### POST /api/push/register + +Register a device token for APNs push notifications. + +**Request:** `{ "token": "device-token-string" }` + +### DELETE /api/push/register + +Unregister a device token. + +### POST /api/push/notification-action + +Handle iOS notification actions. + +**Request:** + +```json +{ + "sessionId": "abc123", + "actionId": "VIEW_ACTION", + "userText": "optional reply text" +} +``` + +Action IDs: `VIEW_ACTION`, `LATER_ACTION`, `REPLY_ACTION`. + +## Events (SSE) + +### GET /api/events + +Server-Sent Events stream for live updates. Used by the frontend for real-time session activity, health status, and task state changes. + +**Event types:** + +| Event | Description | +| ------------------------ | ------------------------------- | +| `connected` | Connection established | +| `session_activity` | Session overview snapshot | +| `health` | Service health update | +| `sessions_changed` | Session list changed | +| `task_state` | Full task tree update | +| `task_updated` | Single task update | +| `task_deleted` | Task deletion | +| `workload_item_created` | Workload item created | +| `workload_item_updated` | Workload item updated | +| `workload_batch_updated` | Multiple workload items updated | + +## Permission (No-Auth Fallback) + +### POST /api/permission/:permId/respond + +Respond to a permission request via direct URL (used by ntfy notification deep links). + +**Query parameters:** + +| Parameter | Type | Description | +| ---------- | -------- | ------------------------------ | +| `token` | `string` | ntfy auth token | +| `decision` | `string` | Decision (can also be in body) | + +Decisions: `once`, `always`, `deny`. + +## Internal Endpoints + +These endpoints require the internal token (`X-Internal-Token` header) and are used by the task board MCP server and external hooks. + +### POST /api/internal/task-tools/set + +Set task children (agent decomposition). + +### POST /api/internal/task-tools/complete + +Mark current task complete. + +### GET /api/internal/task-tools/status + +Get current task status. + +### POST /api/internal/task-tools/block + +Block current task with a reason. + +### POST /api/internal/task-tools/artifact + +Add an artifact to the current task. + +### GET /api/repos + +List configured repos. + +### POST /api/repos/open + +Open a repo session with worktree allocation. + +## Common Error Shape + +All error responses follow the same shape: + +```json +{ + "error": "Human-readable error message" +} +``` + +HTTP status codes follow standard conventions: `400` for bad requests, `401` for unauthorized, `404` for not found, `500` for server errors. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..d8c06f51 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,422 @@ +# Architecture + +Deep dive into Mitzo's module structure, data flow, and design decisions. + +## System Overview + +Mitzo is a Node.js + TypeScript server and React 19 frontend that provides a mobile-first web UI for Claude Code sessions via the Anthropic Agent SDK. It runs as a long-lived HTTPS server, translating raw SDK stream events into a v2 block lifecycle protocol delivered over WebSocket (or SSE fallback). + +``` ++-------------------------------------------------------------------+ +| Mitzo Server | +| | +| +----------+ +----------+ +----------+ +--------------+ | +| | Query | | Session | | Connect | | Worktree | | +| | Loop | | Registry | | Registry | | Manager | | +| | | | | | | | | | +| | SDK -> | | detach/ | | single | | create/ | | +| | v2 proto | | reattach | | mux WS | | cleanup/ | | +| | snapshot | | snapshot | | watch/ | | guard | | +| +----------+ +----------+ | sync | +--------------+ | +| | | +----------+ | | +| v v | v | +| +---------------------------------------------------------+ | +| | Express + WebSocket | | +| | REST API SSE stream WS v2 handler | | +| +---------------------------------------------------------+ | +| | | | | | +| +---------+ +----------+ +----------+ +---------------+ | +| | Event | | Task | | Skill | | Permission | | +| | Store | | Board | | Registry | | Handler | | +| | (SQLite)| | (SQLite) | | | | | | +| +---------+ +----------+ +----------+ +---------------+ | ++-------------------------------------------------------------------+ +``` + +## Module Dependency Graph + +``` + +----------+ + | index | + | (server) | + +----+-----+ + | + +----------+----------+ + v v v + +--------+ +--------+ +--------+ + | app | | WS | | Chat | + |(routes)| |handler | | REST | + +---+----+ +---+----+ +---+----+ + | | | + +------+------+ v v + v v v +--------+ +--------+ ++------+ +------+ +--| query | | chat | +|files | |tasks | | | loop | | | +|inbox | |loop | | +---+----+ +---+----+ +|auth | | | | | | ++------+ +------+ | v v + | +--------+ +--------+ + | |session | |perm. | + | |registry| |handler | + | +---+----+ +---+----+ + | | | + | v v + | +--------+ +--------+ + +->|event | |tool | + |store | |tiers | + +--------+ +--------+ +``` + +Arrows indicate "depends on" relationships. Most modules are independently testable via dependency injection and the `app.ts` factory. + +## Request Flow: Chat Message + +A single user message flows through the system as follows: + +``` +1. Client sends 2. WS handler 3. Chat module 4. Query loop + v2 "send" msg routes msg starts SDK query translates events ++-------------+ +-------------+ +-------------+ +-------------+ +| prompt: | | validate | | assemble | | SDK event: | +| "fix bug" |------>| schema |------->| system |------->| text delta | +| sessionId: | | find/create | | prompt + | | tool_use | +| null (new) | | session | | query() | | tool_result | ++-------------+ +-------------+ +-------------+ +-------------+ + | +5. v2 protocol 6. Transport 7. Connection | + events layer registry | ++-------------+ +-------------+ +-------------+ | +| block_start |<------| session |<-------| broadcast |<------------+ +| block_delta | | transport | | to watchers | +| block_end | | adapter | | | +| message_end | +-------------+ +-------------+ ++-------------+ + | + v +8. Client store ++-------------+ +| Zustand | +| reducer | +| dispatches | +| React re- | +| renders | ++-------------+ +``` + +### Step by Step + +1. **Client sends `send` message** via WebSocket. `sessionId: null` starts a new session; a non-null ID continues an existing one. + +2. **WS handler** (`ws-handler-v2.ts`) validates the message against Zod schemas. For new sessions, it calls `startChat()` which creates a `SessionRegistry` entry, allocates worktrees, and builds the system prompt. + +3. **Chat module** (`chat.ts`) assembles the full system prompt (base + worktree paths + task context + context blocks), creates an `AsyncQueue` for streaming input, and calls the Agent SDK's `query()` with the user's prompt. + +4. **Query loop** (`query-loop.ts`) receives raw SDK events and translates them into v2 protocol messages. It maintains an `openBlockCount` to defer `message_end` until all blocks are closed. It tracks snapshot state for reconnection recovery. + +5. **v2 protocol events** (`block_start`, `block_delta`, `block_end`, `message_end`) are emitted as JSON strings. + +6. **Session transport** (`ws-transport.ts`) wraps the WebSocket connection and handles send operations. The transport is swappable for testing via `NullTransport`. + +7. **Connection registry** broadcasts events to all connections watching the session. Multiple connections can watch a session simultaneously (e.g., phone + laptop). + +8. **Client store** (`@mitzo/client`) parses the v2 protocol messages via the protocol parser, dispatches actions to the Zustand store, and React components re-render. + +## Session Lifecycle + +Sessions have a well-defined state machine: + +``` + +--------+ + |CREATED | + +---+----+ + | + v + +--------+ + +------>|STARTING| + | +---+----+ + | | + | v + | +--------+ + | +--->|ACTIVE |<----+ + | | +---+----+ | + | | | | + | | +----+----+ | + | | | | | + | | v v | + | | +------+ +------+ + | | |DETACH| |SUSP. | + | | +--+---+ +--+---+ + | | | | + | +----+ +----+ + | | + | v + | +--------+ + | |CLOSING | + | +---+----+ + | | + | v + | +--------+ + +-------|ENDED | + +--------+ +``` + +| State | Description | +| ----------- | --------------------------------------------------------------- | +| `CREATED` | Session allocated, worktrees not yet created | +| `STARTING` | SDK `query()` call in progress, worktrees being set up | +| `ACTIVE` | Session is running, connected to a transport | +| `DETACHED` | WebSocket disconnected but session still alive (30s TTL) | +| `SUSPENDED` | Client explicitly suspended (iOS background, sendBeacon) | +| `CLOSING` | Graceful closeout in progress (agent asked to commit/summarize) | +| `ENDED` | Session terminated, resources cleaned up | + +### Detach and Reattach + +When a WebSocket connection drops (phone locks, network change), the session enters `DETACHED` state with a 30-second TTL. If the client reconnects within that window: + +1. Client sends `reconnect` message with `sessions[]` and `lastSeq` per session +2. Server replays missed events from the event store (starting from `lastSeq + 1`) +3. Session transitions back to `ACTIVE` +4. If the SDK query was still running, streaming continues seamlessly + +If TTL expires, the session enters a two-phase closeout: the agent is asked to commit work and summarize, then the session is aborted. + +### Suspend (iOS Background) + +iOS kills WebSocket connections when the app enters background. Mitzo handles this with a proactive suspend signal: + +1. Client calls `sendSuspend()` which uses `sendBeacon()` (survives page unload) to `POST /api/sessions/suspend` +2. Server marks sessions as `SUSPENDED` (distinct from `DETACHED`) +3. On foreground return, client reconnects and resumes from last sequence number + +## Event Store + +The event store (`event-store.ts`) is a SQLite database (`.mitzo/events.db`) that provides crash-safe persistence for session events. Every v2 protocol message is appended with a monotonic sequence number per session. + +``` +events table ++-----+------------+--------+---------+------------+ +| seq | session_id | type | payload | created_at | ++-----+------------+--------+---------+------------+ +| 1 | abc123 | send | {...} | 1720000001 | +| 2 | abc123 | m_start| {...} | 1720000002 | +| 3 | abc123 | b_start| {...} | 1720000003 | +| ... | ++-----+------------+--------+---------+------------+ + +sessions table ++------------+---------+--------+-----+------+------+-------+ +| session_id | summary | branch | cwd | mode | ... | state | ++------------+---------+--------+-----+------+------+-------+ +``` + +Key capabilities: + +- **Replay**: `getEventsAfter(sessionId, afterSeq)` replays missed events for reconnection +- **Search**: Full-text search across session summaries and user messages +- **Session metadata**: Tracks tokens, cost, mode, branch, worktree, state, timestamps +- **Attention tracking**: Identifies sessions that need user attention (permissions pending, errors) +- **Prompt counting**: Tracks prompts per session for auto-rename scheduling + +## Permission System + +The permission handler implements a multi-layer check for every tool invocation: + +``` +Tool invocation + | + v +1. Skill policy check + (allowed-tools ceiling) + | + v +2. Worktree guard + (write path enforcement) + | + v +3. Auto-allow check + (mode x tier matrix) + | + v +4. Session allow-list + (permanent approvals) + | + v +5. User prompt + (WS + push notification) + | + v +Decision: allow / deny +``` + +### Tool Tiers + +Every tool is classified into a risk tier: + +| Tier | Examples | Ask | Agent | Auto | +| ---------- | ---------------- | ------ | ------ | ------ | +| `safe` | Read, Glob, Grep | allow | allow | allow | +| `standard` | Edit, Write | prompt | allow | allow | +| `elevated` | Bash | prompt | allow | allow | +| `unknown` | MCP tools | prompt | prompt | prompt | + +Tiers can be overridden per-tool in `.mitzo.json` via `toolTierOverrides`. + +### Worktree Guard + +When worktree isolation is enabled, `checkWorktreePolicy()` inspects Write, Edit, and Bash tool inputs. If the target path falls outside the session's worktree directories, the tool call is denied with a redirect message (the agent self-corrects). Read operations are unrestricted. + +## Skill System + +Skills are reusable prompt packages invoked via `/slash-command` in chat. + +``` +Discovery pipeline: + 1. Native commands (TypeScript) -> highest precedence + 2. Repo-local (.mitzo/skills/) -> per-project + 3. User (~/.mitzo/skills/) -> global + 4. Bundled (./skills/) -> fallback +``` + +Each skill is a markdown file with YAML frontmatter. The frontmatter can declare `allowed-tools` which acts as a ceiling on tool permissions during the skill's execution (never expands permissions, only restricts). + +See [docs/skills.md](skills.md) for the full guide. + +## Task Board + +The task board provides multi-session goal decomposition and autonomous execution. + +``` +Goal (root task) + | + +-- Subtask 1 (pending) + +-- Subtask 2 (active -> assigned to session) + | | + | +-- Sub-subtask 2a (done) + | +-- Sub-subtask 2b (pending) + +-- Subtask 3 (pending) +``` + +**Orchestration flow:** + +1. User creates a goal via the UI +2. `startLoop()` assigns the goal to a session +3. The agent decomposes the goal into subtasks via `TaskSet` +4. In spec mode, decomposition pauses for human approval +5. The orchestrator picks tasks in DFS order, one at a time +6. Each task is assigned to a session, executed, and marked complete +7. Status cascades up the tree (failed child -> failed parent) + +The task store uses SQLite (`.mitzo/tasks.db`) with WAL mode. The orchestrator is a stateless tick-based state machine -- it re-reads from SQLite on every tick, making it resilient to crashes. + +See [docs/task-board.md](task-board.md) for the full guide. + +## Observability + +### Logging + +Pino structured JSON logger with three transport targets: + +| Target | Purpose | Configuration | +| ----------- | ----------------------------------- | ----------------------- | +| `pino-roll` | Daily-rotated JSON files in `logs/` | Always active | +| stdout | JSON (or `pino-pretty` in dev) | Always active | +| `pino-loki` | Pushes to Grafana Loki | When `LOKI_HOST` is set | + +Every log line includes `module`, `msg`, `level`, `time`. When an OTel span is active, `trace_id` and `span_id` are injected via the Pino mixin for log-to-trace correlation. + +### Tracing + +OpenTelemetry via `BatchSpanProcessor` with OTLP HTTP exporter to Jaeger. Opt-in when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. + +Instrumented operations: `ws.switch_session`, `ws.send`, `ws.reconnect`. + +### Infrastructure + +Four containers via `docker-compose.yml` (podman): + +| Service | Port | Purpose | +| ------- | ----------------------- | ------------------------------------------- | +| Jaeger | 16686 (UI), 4318 (OTLP) | Distributed trace viewer | +| Grafana | 3002 | Log viewer + dashboards (no login required) | +| Loki | 3200 | Log aggregation backend | +| MLflow | 5050 | Experiment tracking | + +## Reasoning Harness + +Mitzo includes two multi-model reasoning orchestrators for complex decision-making: + +### Deliberation + +Multiple agents with defined roles engage in structured debate rounds. Each role has a model assignment. A judge evaluates arguments and declares a winner. + +``` +Role A (model-1) --+ + | +Role B (model-2) --+--> Judge --> Winner + Transcript + | +Role C (model-3) --+ +``` + +### Fusion + +A panel of models independently responds to a prompt. A judge model synthesizes the responses into a single output, analyzing agreement and disagreement. + +``` +Panel Member 1 --+ +Panel Member 2 --+--> Judge --> Synthesized Output +Panel Member 3 --+ +``` + +Both orchestrators are configurable via agent definitions and can be loaded from YAML configs. + +## Transport Layer + +Mitzo supports two transport mechanisms for the v2 protocol: + +### WebSocket (Primary) + +Single multiplexed WebSocket per client. All sessions share one connection. Messages carry explicit `sessionId` for demuxing. The `ConnectionRegistry` manages watch subscriptions and broadcasts. + +### SSE + REST (Fallback) + +For environments where WebSocket is unavailable: + +- `GET /api/chat/events` opens a Server-Sent Events stream +- Client receives a `welcome` event with a `connectionId` +- Subsequent interactions use POST endpoints (`/api/chat/send`, `/api/chat/stop`, etc.) with `X-Connection-ID` header +- Same v2 protocol semantics, different transport + +## Design Decisions + +### SDK as the Engine + +Mitzo uses the Anthropic Agent SDK's `query()` function directly rather than implementing its own conversation loop. This means Claude sessions in Mitzo have identical capabilities to the Claude Code CLI -- same tools, same MCP support, same hook system. The SDK is the single source of truth for tool definitions and execution. + +### Single Multiplexed WebSocket + +v2 of the protocol moved from per-session WebSocket connections to a single multiplexed connection per client. Every message carries a `sessionId`. This eliminates connection storms when switching sessions, simplifies reconnection (one reconnect, all sessions resume), and matches how mobile browsers actually work (one WS is more reliable than many). + +### Event Store for Replay, Not Cache + +The event store is the canonical record of session events, not a cache. Reconnection replays from the store, not from in-memory buffers. This makes crash recovery straightforward -- restart the server, sessions pick up where they left off. + +### SQLite for Persistence + +Both the event store and task store use SQLite with WAL mode. This provides crash-safe persistence, concurrent read access, and zero-config deployment. WAL mode allows reads during writes without blocking. + +### Worktree Isolation by Default + +Every session gets its own git worktree on a dedicated branch. This prevents cross-session contamination -- two concurrent sessions can't step on each other's changes. The guard is enforced at the tool level (not advisory), so the agent can't accidentally write outside its sandbox. + +### Mobile-First, Desktop-Capable + +The UI is designed for phone-sized screens first. Desktop gets a side-by-side layout with chat + file viewer, but the phone experience is the primary design target. Touch interactions, swipe gestures, and push notifications are first-class features. + +### Stateless Orchestrator + +The task orchestrator (`TaskOrchestrator`) is deliberately stateless. Every `tick()` re-reads the full task tree from SQLite. This makes it resilient to crashes (no in-memory state to lose) and simplifies reasoning about behavior (the current state is always what's in the database). + +### Provider-Agnostic Core + +The `@mitzo/harness` package includes a `ModelProvider` abstraction that supports Anthropic (via Vertex) and Google (Gemini) models. The reasoning orchestrators use this abstraction, allowing deliberation and fusion across different model providers. diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 00000000..14cad740 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,564 @@ +# Package Reference + +Mitzo uses an npm workspace with three internal packages shared between server and frontend. All packages live in `packages/` and are referenced via workspace dependencies. + +## @mitzo/protocol + +Core protocol types, validation schemas, and shared constants. Zero runtime dependencies beyond `zod` for schema validation. + +### Types + +#### Message Types + +The protocol defines three representations of messages at different lifecycle stages: + +| Type | Stage | Blocks | Use Case | +| ------------------ | --------- | ----------------------------- | ---------------------------------- | +| `StreamingMessage` | In-flight | `Map` | Active streaming in frontend | +| `FinishedMessage` | Persisted | `FinishedBlock[]` | Completed messages for display | +| `MessageSnapshot` | Recovery | `SnapshotBlock[]` | Server-side state for reconnection | + +```typescript +interface FinishedMessage { + messageId: string; + role: 'user' | 'assistant'; + blocks: FinishedBlock[]; + images?: ImageAttachment[]; + contextBlocks?: string[]; + timestamp: number; +} + +interface FinishedBlock { + blockId: string; + blockType: BlockType; + content: string; // text content or tool summary + toolName?: string; // for tool_use blocks + toolInput?: RawToolInput; + result?: string; // tool result + images?: ToolResultImage[]; + subagent?: FinishedSubagentState; +} +``` + +#### Block Types + +```typescript +type BlockType = 'text' | 'thinking' | 'redacted_thinking' | 'tool_use'; +``` + +#### Modes and Tiers + +```typescript +type MitzoMode = 'ask' | 'agent' | 'auto'; +type ToolTier = 'safe' | 'standard' | 'elevated' | 'unknown'; +``` + +#### Session Types + +```typescript +type SessionState = + | 'CREATED' + | 'STARTING' + | 'ACTIVE' + | 'DETACHED' + | 'SUSPENDED' + | 'CLOSING' + | 'ENDED'; +type SessionClosedBy = 'user' | 'auto' | 'abandoned'; + +interface Session { + id: string; + summary: string; + lastModified: number; + branch?: string; + isActive: boolean; + isAttached: boolean; + totalTokens: number; + numTurns: number; + telosTaskId?: string; + closedBy?: SessionClosedBy; +} +``` + +#### Permission Types + +```typescript +interface PermissionRequest { + permId: string; + toolName: string; + toolInput: RawToolInput; + tier: ToolTier; + title?: string; + description?: string; +} +``` + +#### Agent Definition Types + +```typescript +interface AgentDefinition { + name: string; + identity: AgentIdentity; + provider: AgentProvider; + context: AgentContextConfig; + governance?: AgentGovernance; + memory?: AgentMemoryConfig; + output?: AgentOutput; +} + +interface AgentProvider { + default: string; // default model + tiering?: { + fast?: string; + standard?: string; + capable?: string; + }; +} +``` + +#### Subagent Types + +```typescript +interface FinishedSubagentState { + messageId: string; + blocks: FinishedBlock[]; + summary?: string; + usage?: SubagentUsage; +} + +interface SubagentUsage { + inputTokens: number; + outputTokens: number; + cacheReadTokens?: number; + cacheCreationTokens?: number; +} +``` + +### Schemas (Zod) + +The protocol package exports Zod schemas for all WebSocket messages: + +```typescript +// Client -> Server +HelloMessage; // { type: 'hello', protocolVersion: number } +ReconnectMessage; // { type: 'reconnect', sessions: [...] } +V2SendMessage; // { type: 'send', prompt, sessionId, ... } +V2InterruptMessage; // { type: 'interrupt', prompt, sessionId, ... } +V2StopMessage; // { type: 'stop', sessionId } +V2PermissionResponseMessage; +V2SetModeMessage; +WatchMessage; +UnwatchMessage; +SwitchSessionMessage; +SessionSuspendMessage; +SessionCloseMessage; + +// Discriminated union +IncomingWsMessageV2; // Union of all client->server messages +``` + +### Functions + +```typescript +// Tool summarization +getRawInput(toolName: string, toolInput: unknown): RawToolInput; +summarizeToolInput(toolName: string, toolInput: unknown): string; + +// Language detection +languageFromPath(filePath: string): string; + +// Content block parsing +extractToolResultText(result: unknown): string; +extractToolResultImages(result: unknown): RawToolResultImage[]; +parseContentBlocks(blocks: unknown[]): FinishedBlock[]; +``` + +### Constants + +```typescript +TOOL_RESULT_MAX_CHARS: 50_000; +TOOL_SUMMARY_MAX_CHARS: 200; +RAW_INPUT_MAX_CHARS: 50_000; +NOTIFY_SNIPPET_MAX_CHARS: 150; +SESSION_PAGE_SIZE: 20; +SESSION_MESSAGES_LIMIT: 100; +MAX_OBSERVERS_PER_SESSION: 10; +CONTEXT_CEILING_TOKENS: 200_000; +``` + +### Event Store (Subexport) + +Available via `@mitzo/protocol/event-store`: + +```typescript +class EventStore { + constructor(dbPath: string, logger?: EventStoreLogger); + + // Events + append(sessionId: string, type: string, payload: unknown): number; + getEventsAfter(sessionId: string, afterSeq: number, limit?: number): StoredEvent[]; + getSessionEvents(sessionId: string): StoredEvent[]; + + // Sessions + upsertSession(meta: Partial): void; + getSession(sessionId: string): SessionMeta | null; + listSessions(limit?: number): SessionMeta[]; + searchSessions(query: string, limit: number): SessionSearchResult[]; + + // State management + setSessionState(sessionId: string, newState: SessionState, opts?): void; + recordUsage(sessionId: string, usage: object): void; + getAttentionSessions(): SessionMeta[]; + incrementPromptCount(sessionId: string): number; +} +``` + +Peer dependency: `better-sqlite3`. + +--- + +## @mitzo/harness + +Server-side session management, permissions, and orchestration. This is the core server package that sits between the Express routes and the Agent SDK. + +### Session Registry + +Manages the lifecycle of SDK sessions: + +```typescript +class SessionRegistry { + register(clientId: string, transport: SessionTransport): ManagedSession; + get(clientId: string): ManagedSession | undefined; + list(attached?: boolean): ManagedSession[]; + + // Snapshot management + snapshot(clientId: string, sessionId: string): MessageSnapshot | null; + updateSnapshot(clientId: string, snapshot: MessageSnapshot): void; + + // Observers + addObserver(clientId: string, observer: SessionTransport): void; + removeObserver(clientId: string, observer: SessionTransport): void; + broadcast(sessionId: string, data: unknown, excludeClientId?: string): void; + + // Lifecycle + detach(clientId: string): void; + reattach(clientId: string, sessions: Array<...>, resumeSeq?: number): void; + suspend(clientId: string): void; + resume(clientId: string): void; + scheduleCloseout(clientId: string): void; +} +``` + +Key constants: + +```typescript +DETACHED_TTL_MS: 30_000; // 30s before detached session aborts +CLOSEOUT_LEAD_MS: 30_000; // Time given for graceful closeout +CLOSEOUT_TIMEOUT_MS: 5_000; // Hard timeout after closeout +PERMISSION_TIMEOUT_MS: 60_000; // 1 minute for permission responses +``` + +### Connection Registry + +Manages the v2 single-multiplexed-WS model: + +```typescript +class ConnectionRegistry { + register(connectionId: string, transport: SessionTransport): void; + get(connectionId: string): Connection | undefined; + remove(connectionId: string): void; + + watch(connectionId: string, sessionId: string): void; + unwatch(connectionId: string, sessionId: string): void; + setActive(connectionId: string, sessionId: string | null): void; + + broadcast(sessionId: string, data: unknown): void; + sync(connectionId: string, sessionId: string, fromSeq: number): void; +} +``` + +### Permission Handler + +Builds the `canUseTool` callback for the Agent SDK: + +```typescript +function buildPermissionHandler( + clientId: string, + registry: SessionRegistry, + opts?: { notify?: boolean }, +): (toolName: string, toolInput: unknown, context: ToolContext) => Promise; +``` + +The handler checks in order: skill policy -> worktree guard -> auto-allow -> allow-list -> user prompt. + +### Tool Tiers + +```typescript +function getToolTier(toolName: string): ToolTier; +function shouldAutoAllow(toolName: string, mode: MitzoMode): boolean; +function applyTierOverrides(overrides: Record): void; +function getAllowedToolsForMode(mode: MitzoMode): Set; +``` + +### Worktree Guard + +```typescript +function checkWorktreePolicy( + session: ManagedSession, + toolName: string, + toolInput: unknown, + opts?: { logger?: Logger }, +): Promise; // returns violation message or null +``` + +### Model Providers + +Multi-model abstraction supporting Anthropic (via Vertex) and Google (Gemini): + +```typescript +interface ModelProvider { + model: string; + call(messages: ProviderMessage[], opts?: CallOptions): Promise; +} + +function createProvider(model: string): ModelProvider; +function createProviders(models: string[]): Map; +function calculateCost(model: string, usage: object): number; +``` + +### Reasoning Orchestrators + +```typescript +// Deliberation: structured multi-agent debate +class DeliberationOrchestrator { + run(context: string, callbacks?: object): Promise; +} + +// Fusion: parallel panel + judge synthesis +class FusionOrchestrator { + run(context: string, callbacks?: object): Promise; +} + +// Config builders +function buildDeliberationConfig(options: object): DeliberationConfig; +function buildFusionConfig(options: object): FusionConfig; +``` + +### Notifications + +```typescript +// ntfy +function sendPermissionNotification(opts: NotifyOpts): Promise; +function isConfigured(): boolean; + +// Pushover +function sendPermissionNotification(opts: PushoverOpts): Promise; +function isConfigured(): boolean; +``` + +### Auto-Rename + +```typescript +function shouldAutoRename(sessionMeta: SessionMeta): boolean; +function extractRecentPrompts(events: StoredEvent[]): string[]; +function generateSessionName(prompts: string[]): Promise; +``` + +### SSE Registry + +```typescript +class SseRegistry { + add(id: string, res: Response): void; + remove(id: string): void; + broadcast(event: string, data: unknown): void; +} +``` + +--- + +## @mitzo/client + +Framework-agnostic frontend state management and transport. The core is a Zustand vanilla store; React hooks are available as an optional subexport. + +### MitzoConnection + +Single multiplexed WebSocket connection with automatic reconnection: + +```typescript +class MitzoConnection { + constructor(config: MitzoConnectionConfig); + + connect(): void; + disconnect(): void; + send(msg: object): boolean; + onMessage(listener: (data: string) => void): void; + isConnected(): boolean; + getConnectionId(): string | null; + + // Sequence tracking for reconnection + trackSeq(sessionId: string, seq: number): void; + getLastSeq(sessionId: string): number; + clearSession(sessionId: string): void; + getTrackedSessions(): string[]; + + // iOS background + sendSuspend(): void; +} +``` + +Configuration: + +```typescript +interface MitzoConnectionConfig { + buildUrl(): string; // WebSocket URL builder + createWebSocket(url: string): WebSocketLike; + reconnectDelayMs?: number; // Default: 1000ms + suspendUrl?: string; // POST endpoint for sendBeacon fallback +} +``` + +Features: + +- Automatic hello/welcome handshake +- Reconnection with session replay via `reconnect` message +- Pending message queue (up to 100 messages during reconnect) +- Browser lifecycle listeners (online/offline, visibilitychange) +- Heartbeat for connection health +- `sendSuspend()` uses `sendBeacon()` for iOS background (falls back to WS) + +### Zustand Store + +The store has 12 state slices: + +| Slice | State | Key Fields | +| ------------- | ----------------- | -------------------------------------------------------------------- | +| `sessions` | Session list | `list`, `current`, `meta`, `loading` | +| `messages` | Chat messages | `messages`, `current` (streaming), `running`, `permission`, `branch` | +| `connection` | Transport state | `status`, `connectionId`, `error` | +| `permissions` | Pending approvals | `pending` (Record by permId) | +| `tasks` | Task board | `items`, `loopStatus` | +| `workload` | Workload items | `items` | +| `inbox` | Inbox items | `items` | +| `calendar` | Calendar events | `events`, `sprints` | +| `todos` | Todo items | `items` | +| `config` | Server config | `contextBlocks`, `skills`, `mode`, `model` | +| `tokens` | Token tracking | `sessions` (per-session), `totals`, `ceiling` | +| `progress` | Progress blocks | `blocks` (by progressId) | + +#### Actions + +```typescript +// Chat +sendMessage(text: string, opts?: SendMessageOptions): void; +interruptMessage(text: string, opts?: SendMessageOptions): void; +stopGeneration(): void; +respondToPermission(permId: string, decision: string): void; +setMode(mode: MitzoMode): void; + +// Sessions +switchSession(id: string): Promise; +newSession(): void; +closeSession(): void; +loadSessions(): Promise; + +// Tasks +loadTasks(): Promise; +createTask(input: object): Promise; +startLoop(goalId: string, specMode?: boolean): Promise; +pauseLoop(): Promise; +resumeLoop(): Promise; +stopLoop(): Promise; +approveTask(id: string): Promise; +rejectTask(id: string, feedback?: string): Promise; +``` + +### Protocol Parser + +Converts raw server JSON into typed store actions: + +```typescript +function parseServerMessage(msg: string, callbacks: ProtocolCallbacks): ParseResult; +``` + +Callbacks cover every server message type: + +```typescript +interface ProtocolCallbacks { + onMessageStart?(data: object): void; + onBlockStart?(data: object): void; + onBlockDelta?(data: object): void; + onBlockEnd?(data: object): void; + onToolResult?(data: object): void; + onPermissionRequest?(data: object): void; + onSessionActive?(data: object): void; + onBootContext?(data: object): void; + // ... and more +} +``` + +### API Client + +REST client for non-realtime operations: + +```typescript +class MitzoApiClient { + constructor(baseUrl: string, transport: TransportAdapter, options?: object); + + auth(): Promise; + getAppConfig(): Promise; + getVersion(): Promise; + getGitInfo(): Promise; + listDir(path: string): Promise; + getCalendarData(): Promise; + listSessions(limit?: number): Promise; + getSessionMeta(sessionId: string): Promise; + searchSessions(query: string): Promise; +} +``` + +### SSE Connection (Fallback) + +For environments where WebSocket is unavailable: + +```typescript +class SseConnection { + constructor(config: SseConnectionConfig); + + connect(): void; + disconnect(): void; + send(msg: object): void; + isConnected(): boolean; +} +``` + +Uses `EventSource` for server->client and HTTP POST for client->server. + +### React Hooks (Subexport) + +Available via `@mitzo/client/hooks`: + +```typescript +function useStore(): MitzoStoreState; +function useConnection(): ConnectionSlice; +function useMessages(): MessagesSlice; +function usePermission(): PermissionsSlice; +function useSessions(): SessionsSlice; +function useTokens(): TokensSlice; +``` + +Tree-shakeable -- only import what you use. + +### Event Bus + +Broadcast events across the application: + +```typescript +class EventBus { + listen(channel: string, listener: (data: unknown) => void): () => void; + broadcast(channel: string, data: unknown): void; + connect(): void; + disconnect(): void; +} +``` + +Backed by `EventSource` for server-pushed events (SSE stream at `/api/events`). diff --git a/docs/session-isolation.md b/docs/session-isolation.md new file mode 100644 index 00000000..72a0fda8 --- /dev/null +++ b/docs/session-isolation.md @@ -0,0 +1,187 @@ +# Session Isolation + +Every Mitzo session gets deterministic isolation via git worktrees. Each session operates on its own branch in its own directory, preventing cross-session contamination. + +## How It Works + +When a new session starts with isolation enabled: + +1. **Worktree creation** -- `createSessionWorktrees()` creates a git worktree for the primary repo and every repo listed in `.mitzo.json` +2. **Branch creation** -- each worktree gets a dedicated branch: `session/` +3. **Path setup** -- worktree paths are injected into the system prompt so the agent knows where to work +4. **Env vars** -- `MITZO_SESSION_ID` and `MITZO_REPO_` are set for every repo + +### Paths and Branches + +| Item | Pattern | +| ----------------- | ------------------------------------------------------- | +| Worktree path | `/.claude/worktrees//` | +| Branch name | `session/` | +| Primary env var | `MITZO_REPO_PRIMARY` | +| Secondary env var | `MITZO_REPO_` (uppercase, hyphens to underscores) | + +### Multi-Repo + +When `.mitzo.json` declares secondary repos, all of them get worktrees at session start: + +```json +{ + "repos": { + "mitzo": "/Users/you/tools/mitzo", + "team-home": "/Users/you/redhat/team_home", + "centaur": "/Users/you/projects/centaur" + } +} +``` + +This creates four worktrees per session: one for the primary repo (`REPO_PATH`) and one for each secondary repo. The agent can work across all of them in a single session. + +## Write Enforcement + +The worktree guard (`checkWorktreePolicy()` in `@mitzo/harness`) inspects every tool call that could modify files: + +| Tool | Checked | What's Inspected | +| ------- | ------- | ------------------------------------------- | +| `Write` | Yes | `file_path` parameter | +| `Edit` | Yes | `file_path` parameter | +| `Bash` | Yes | Command string (path extraction heuristics) | +| `Read` | No | Read operations are unrestricted | +| `Glob` | No | Read operations are unrestricted | +| `Grep` | No | Read operations are unrestricted | + +If a write target falls outside the session's worktree directories, the tool call is **denied** with a redirect message telling the agent the correct worktree path. The agent self-corrects. No user prompt, no approval flow. + +### Enforcement by Client + +| Client | Enforcement | Mechanism | +| ----------- | -------------------- | -------------------------------------------- | +| Mitzo | Programmatic deny | `checkWorktreePolicy()` in `canUseTool` | +| Claude Code | cwd + system prompt | `SessionStart` hook sets working directory | +| Cursor | Advisory + git guard | `alwaysApply` rule + pre-commit rejects main | + +### System Prompt Injection + +`buildWorktreeSystemPrompt()` generates a lookup table of all repo paths for the agent: + +``` +## Session Worktrees +Session ID: 2026-07-01-abc123 + +- **primary (cwd)**: /path/to/repo/.claude/worktrees/2026-07-01-abc123 +- **mitzo**: /path/to/mitzo/.claude/worktrees/2026-07-01-abc123 +- **team-home**: /path/to/team_home/.claude/worktrees/2026-07-01-abc123 +``` + +This ensures the agent knows the exact paths without guessing. + +## External Hooks + +Mitzo creates worktrees for Claude Code and Cursor sessions too, not just its own. + +### How It Works + +1. On startup, Mitzo generates an internal token and persists it to `~/.mitzo/internal-token` +2. A `SessionStart` hook in your repo's `.claude/hooks/` or `.cursor/hooks/` reads this token +3. The hook calls `POST /api/sessions` with the internal token +4. The server creates worktrees for all configured repos and returns the paths +5. Claude Code/Cursor sessions get the same isolation as Mitzo sessions + +### Claude Code Hook + +```bash +#!/bin/bash +# .claude/hooks/session-isolate.sh +TOKEN=$(cat ~/.mitzo/internal-token 2>/dev/null) +if [ -z "$TOKEN" ]; then exit 0; fi + +RESPONSE=$(curl -s -X POST http://localhost:3100/api/sessions \ + -H "Content-Type: application/json" \ + -H "X-Internal-Token: $TOKEN" \ + -d '{"source": "claude-code"}') + +if [ $? -eq 0 ]; then + CWD=$(echo "$RESPONSE" | jq -r '.worktrees.primary // empty') + if [ -n "$CWD" ]; then + echo "{\"cwd\": \"$CWD\"}" + fi +fi +``` + +### Cursor Hook + +Similar pattern, but outputs `agent_message` with all worktree paths for the system prompt. + +## Cleanup + +### Automatic Cleanup + +Stale worktrees (older than 96 hours) are cleaned up automatically on server startup. The cleanup scans both `.claude/worktrees/` and `.cursor/worktrees/` directories in all configured repos. + +### Dirty Worktree Handling + +Worktrees with uncommitted changes are **not** deleted during cleanup. Instead: + +1. The cleanup process detects uncommitted work +2. Creates a commit with the uncommitted changes +3. Creates a draft PR from the session branch +4. Flags the worktree in the mgmt inbox for human review +5. Then removes the worktree directory + +This prevents losing work from sessions that were interrupted before the agent could commit. + +### Manual Cleanup + +```bash +# Via the mgmt CLI (if using mgmt workspace) +./mgmt session cleanup [session-id] + +# Or manually +git worktree list # see all worktrees +git worktree remove # remove a specific worktree +git branch -d session/ # delete the session branch +``` + +## Session Index + +Mitzo maintains a YAML session index at `/.claude/sessions/index.yaml`. This tracks: + +- Active and closed sessions +- Worktree paths per repo +- Session metadata (title, creation time, last activity) + +The index is useful for finding prior session work without grepping through worktree directories. + +## Configuration + +### Enabling/Disabling + +Set `WORKTREE_ENABLED=false` in `.env` to disable isolation entirely. Sessions work directly on the main repo. This is the kill switch. + +### Skipping Worktree Creation + +Sessions with explicit `cwd` or `resume` parameters skip worktree creation. This allows: + +- Resuming an existing session in its original worktree +- Starting a session in a specific directory (e.g., a quick action with a `cwd` override) + +### Data Files + +Worktrees are lightweight git checkouts. They don't include: + +- `.venv/` (Python virtual environments) +- Parquet data files +- `node_modules/` (uses the main repo's copy via npm workspace resolution) +- Any gitignored files + +This is expected. The agent works with code, not data artifacts. + +## Troubleshooting + +| Problem | Fix | +| -------------------------- | ---------------------------------------------------------------------------------------- | +| Worktree creation fails | Run `git worktree list` in the failing repo to check for conflicts | +| Stale worktrees piling up | Run `git worktree prune` then restart Mitzo (auto-cleanup runs on start) | +| Agent writes to wrong path | Check that the system prompt includes worktree paths (should be automatic) | +| Branch already exists | A prior session with the same ID left a branch. Delete with `git branch -d session/` | +| npm/node_modules missing | Expected in worktrees. Symlink or use the main repo's packages | +| Data files missing | Expected. Worktrees only contain tracked files | diff --git a/docs/skills.md b/docs/skills.md new file mode 100644 index 00000000..20333615 --- /dev/null +++ b/docs/skills.md @@ -0,0 +1,184 @@ +# Skills System + +Skills are reusable prompt packages invoked via `/slash-command` in the chat input. They provide a way to package common workflows, enforce tool restrictions, and share prompt templates across repos. + +## Using Skills + +Type `/` in the chat input to see all available skills. The `SlashPicker` component shows skill names, descriptions, source badges, and collision notes. + +``` +/simplify -- reduce complexity and duplication +/risk-scan -- failure modes, missing tests, unsafe assumptions +/pr-review -- review a pull request +/person -- people profile lookup and update +/review-response -- triage and fix PR review comments +/land-pr -- shepherd a PR from open to merged +/pr-shepherd -- persistent PR lifecycle monitoring +``` + +Skills can accept arguments: + +``` +/pr-review 42 -- review PR #42 +/person akram -- look up Akram's profile +/pr-shepherd mitzo#350 -- monitor PR #350 in the mitzo repo +``` + +## Bundled Skills + +Mitzo ships with these skills in the `skills/` directory: + +| Skill | Description | Arguments | +| ------------------ | ---------------------------------------------------------------------------------- | ---------------- | +| `/simplify` | Code review focused on reducing complexity, duplication, and cleanup opportunities | None | +| `/risk-scan` | Security-oriented audit -- failure modes, missing tests, unsafe assumptions | None | +| `/pr-review` | Review a pull request -- diff analysis, code quality, architecture alignment | PR number or URL | +| `/person` | People profile lookup and update | Person name | +| `/review-response` | Triage and fix PR review comments | PR number or URL | +| `/land-pr` | Land a PR -- rebase, squash, merge | PR number or URL | +| `/pr-shepherd` | Persistent PR lifecycle monitoring -- conflicts, CI, reviews, merge-readiness | repo#number | + +## Custom Skills + +Create your own skills by adding markdown files with YAML frontmatter. + +### Skill file format + +```markdown +--- +name: deploy +description: Deploy to staging or production +allowed-tools: [Bash, Read] +arguments: + - name: environment + description: Target environment (staging or production) + required: true + - name: branch + description: Branch to deploy + required: false +--- + +Deploy the application to the {{environment}} environment. + +{{#if branch}} +Deploy from the {{branch}} branch. +{{/if}} + +Run the deployment script and report the result. +Include the deployment URL in your response. +``` + +### Frontmatter fields + +| Field | Type | Required | Description | +| --------------- | ---------- | -------- | ---------------------------------------- | +| `name` | `string` | Yes | Skill name (used as `/name` command) | +| `description` | `string` | Yes | One-line description shown in the picker | +| `allowed-tools` | `string[]` | No | Tool restriction ceiling (see below) | +| `arguments` | `array` | No | Named arguments with descriptions | + +### Arguments + +Each argument has: + +| Field | Type | Required | Description | +| ------------- | --------- | -------- | --------------------------------------------------- | +| `name` | `string` | Yes | Argument name | +| `description` | `string` | Yes | Description shown in help | +| `required` | `boolean` | No | Whether the argument is required (default: `false`) | + +Arguments are injected into the skill body via `{{argument_name}}` template syntax. Positional arguments map to the declared order. + +## Discovery Scopes + +Skills are discovered from three locations, in precedence order: + +``` +1. Repo-local: /.mitzo/skills/*.md +2. User: ~/.mitzo/skills/*.md +3. Bundled: /skills/*.md +``` + +### Precedence Rules + +When skills from different scopes share the same name, the highest-precedence scope wins: + +1. **Native commands** (TypeScript) -- highest precedence. Currently: `/skills`. +2. **Repo-local** -- skills in your project's `.mitzo/skills/` directory. +3. **User** -- skills in `~/.mitzo/skills/` (available in all repos). +4. **Bundled** -- skills shipped with Mitzo. + +The `/` picker shows collision notes when a skill shadows another. For example, if you have a repo-local `/simplify` that shadows the bundled one, the picker will note "overrides bundled". + +## Tool Restrictions + +The `allowed-tools` frontmatter field enforces a ceiling on what tools Claude can use during the skill's execution: + +```yaml +allowed-tools: [Read, Glob, Grep] +``` + +This means Claude can **only** use Read, Glob, and Grep during this skill -- even if the user is in Auto mode. The restriction never expands permissions beyond the current mode. It is enforced by `skill-policy.ts` via the `canUseTool` callback. + +If `allowed-tools` is not specified, the skill uses the mode's default permissions. + +## API + +### GET /api/skills + +Returns the merged skill registry with collision metadata. + +**Query parameters:** + +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------- | +| `cwd` | `string` | Working directory for repo-scoped discovery | + +**Response:** Array of skill objects with `name`, `description`, `source` (scope), and collision information. + +## Adding Skills to Your Repo + +1. Create `.mitzo/skills/` in your repo root +2. Add markdown files with the frontmatter format above +3. Skills appear immediately in the `/` picker -- no restart needed + +Example: a deployment skill for your project: + +```markdown +--- +name: deploy +description: Deploy to production +allowed-tools: [Bash, Read] +--- + +Deploy the application: + +1. Run `npm run build` +2. Run `npm run deploy` +3. Verify the deployment succeeded +4. Report the deployment URL +``` + +Example: a code review skill with restricted tools: + +```markdown +--- +name: audit +description: Security audit of recent changes +allowed-tools: [Read, Glob, Grep] +--- + +Audit the most recent commit for security issues: + +1. Read the diff with `git diff HEAD~1` +2. Check for hardcoded secrets, SQL injection, XSS +3. Report findings with severity ratings +``` + +## Native Commands + +Native commands are TypeScript-implemented commands that bypass the prompt system entirely. Currently: + +- `/skills` -- lists all available skills with their sources and collision info + +Native commands always take precedence over prompt-based skills. diff --git a/docs/task-board.md b/docs/task-board.md new file mode 100644 index 00000000..b24a268d --- /dev/null +++ b/docs/task-board.md @@ -0,0 +1,273 @@ +# Task Board + +The task board provides multi-session goal decomposition and autonomous execution. Drop a high-level goal, Claude decomposes it into subtasks, and the orchestrator executes them sequentially using DFS ordering. + +For the internal design rationale, see `docs/design/global-task-board.md`. + +## Concepts + +### Task Tree + +Tasks form a tree hierarchy. A goal is a root task with subtasks as children: + +``` +Goal: "Add dark mode support" + +-- Task: "Add theme context and provider" (done) + +-- Task: "Update component styles" (active) + | +-- Subtask: "Update header" (done) + | +-- Subtask: "Update sidebar" (pending) + | +-- Subtask: "Update main content" (pending) + +-- Task: "Add toggle in settings" (pending) + +-- Task: "Run tests and fix failures" (pending) +``` + +### Task Status + +| Status | Description | +| ---------------- | ------------------------------------------ | +| `pending` | Not yet started | +| `active` | Currently assigned to a session | +| `done` | Completed successfully | +| `failed` | Failed (blocks siblings from proceeding) | +| `blocked` | Blocked by a dependency or external signal | +| `skipped` | Skipped by the orchestrator | +| `pending_review` | Awaiting human approval (spec mode) | + +### Status Cascade + +Status propagates up the tree automatically: + +- If any child is `failed`, the parent becomes `failed` +- If any child is `blocked`, the parent becomes `blocked` +- If any child is `active`, the parent becomes `active` +- If any child is `pending_review`, the parent becomes `pending_review` +- If all children are `done` or `skipped`, the parent becomes `done` +- Otherwise, the parent stays `pending` + +### DFS Ordering + +The orchestrator picks tasks in depth-first order. Within siblings, lower priority numbers go first. This means the tree is executed top-down, left-to-right, finishing each subtree before moving to the next sibling. + +## Using the Task Board + +### Creating a Goal + +From the Task Board page, create a root goal: + +1. Tap "New Goal" +2. Enter a title (e.g., "Add dark mode support") +3. Optionally add a description with more context +4. Create the goal + +### Starting the Loop + +Once you have a goal, start the orchestration loop: + +1. Tap "Start" on the goal +2. Choose whether to use **spec mode** (recommended for complex goals) +3. The orchestrator assigns the goal to a session + +### Spec Mode + +In spec mode, the loop pauses after the agent decomposes the goal into subtasks. You review the proposed plan before execution begins: + +1. Start loop with spec mode enabled +2. Claude decomposes the goal into subtasks +3. Loop pauses -- you see the proposed task tree +4. **Approve** to proceed with execution +5. **Reject** to have Claude re-plan with optional feedback + +This prevents the agent from charging ahead with a bad decomposition. + +### Loop Controls + +| Control | Description | +| ------- | -------------------------------------- | +| Start | Begin executing from the goal | +| Pause | Pause after the current task completes | +| Resume | Resume from where it paused | +| Stop | Stop the loop entirely | + +### Task Actions + +| Action | Description | +| ------- | ----------------------------------------------------------- | +| Approve | Approve a `pending_review` task | +| Reject | Reject with optional feedback (task goes back to `pending`) | + +## Orchestrator State Machine + +The `TaskOrchestrator` is a singleton with three states: + +``` + start() pause() +idle -------> running -------> paused + ^ | | + | stop() | resume() | + +-------------+<---------------+ +``` + +### Tick-Based Execution + +The orchestrator is **stateless** -- every `tick()` re-reads the full task tree from SQLite and determines the next action. No polling; ticks are triggered by: + +- Tool completions (agent finishes a task) +- REST mutations (user creates/updates tasks) +- Loop state changes (start/pause/resume/stop) + +This makes the orchestrator resilient to crashes. Restart the server and it picks up where it left off. + +### Orphan Detection + +During each tick, the orchestrator checks for active tasks whose `session_id` doesn't match any alive session. Orphaned tasks are reclaimed to `pending` status so they can be re-assigned. + +## Agent Tools + +The agent interacts with the task board via MCP tools delivered through a child-process MCP server: + +| Tool | Description | +| ------------------------------- | -------------------------------------------- | +| `mcp__task-board__TaskSet` | Decompose a task into subtasks | +| `mcp__task-board__TaskComplete` | Mark the current task as done with a summary | +| `mcp__task-board__TaskStatus` | Get the current task and sibling status | +| `mcp__task-board__TaskBlock` | Block the current task with a reason | + +These tools are classified as `safe` tier (auto-allowed in all modes). + +### Task Context Injection + +When a session is assigned a task, the task context is injected into the system prompt as XML blocks: + +```xml + + + Update header component + Add dark mode class switching + + + + Add theme context + Created ThemeContext with light/dark... + + + Update sidebar + + + + Update component styles + + +``` + +Summaries from completed siblings are included (capped at 2000 chars) so the agent has context about what was already done. + +## Persistence + +The task store uses SQLite (`.mitzo/tasks.db`) with WAL mode and foreign keys: + +- Tasks table with tree structure (parentId foreign key) +- Status cascade computed on every write +- DFS ordering via recursive CTE queries +- Orphan detection queries + +## Workflow Templates + +Templates allow you to define reusable task structures: + +```json +{ + "name": "PR Review Pipeline", + "description": "Standard PR review with CI gate", + "stages": [ + { "title": "Run tests", "stageType": "agent" }, + { "title": "Wait for CI", "stageType": "wait_for_signal", "gateConfig": { "type": "gh_ci" } }, + { "title": "Code review", "stageType": "agent" } + ] +} +``` + +### Stage Types + +| Type | Description | +| ----------------- | ------------------------------------------- | +| `agent` | Executed by a Claude session | +| `wait_for_signal` | Pauses until an external signal is received | + +### Gate Config + +Signal stages wait for external events: + +| Gate Type | Description | +| ---------------- | -------------------------- | +| `gh_ci` | GitHub CI check completion | +| `gh_review` | GitHub PR review | +| `centaur_review` | Centaur automated review | +| `human_approval` | Manual human approval | + +Signals are resolved via `POST /api/signals/resolve` or `POST /api/tasks/:id/signal`. + +### Instantiation + +Create a task tree from a template: + +```bash +curl -X POST http://localhost:3100/api/workflows/instantiate \ + -H 'Content-Type: application/json' \ + -d '{ + "templateId": "tmpl-123", + "title": "Review PR #42", + "variables": { "pr_number": 42 } + }' +``` + +## REST API + +### Tasks + +| Method | Endpoint | Description | +| -------- | ------------------------ | ----------------------------------- | +| `GET` | `/api/tasks` | Get all tasks as tree | +| `POST` | `/api/tasks` | Create a task | +| `GET` | `/api/tasks/:id` | Get specific task | +| `PATCH` | `/api/tasks/:id` | Update task | +| `DELETE` | `/api/tasks/:id` | Delete task | +| `POST` | `/api/tasks/:id/approve` | Approve pending_review task | +| `POST` | `/api/tasks/:id/reject` | Reject with optional feedback | +| `POST` | `/api/tasks/:id/signal` | Send signal to wait_for_signal task | + +### Loop + +| Method | Endpoint | Description | +| ------ | ------------------------ | ------------------------------------------ | +| `GET` | `/api/loop/status` | Get loop state | +| `POST` | `/api/loop/start` | Start loop (body: `{ goalId, specMode? }`) | +| `POST` | `/api/loop/pause` | Pause loop | +| `POST` | `/api/loop/resume` | Resume loop | +| `POST` | `/api/loop/stop` | Stop loop | +| `POST` | `/api/loop/spec/approve` | Approve spec decomposition | +| `POST` | `/api/loop/spec/reject` | Reject spec decomposition | + +### Templates + +| Method | Endpoint | Description | +| -------- | ---------------------------- | ---------------------------- | +| `GET` | `/api/templates` | List templates | +| `GET` | `/api/templates/:id` | Get template | +| `POST` | `/api/templates` | Create template | +| `DELETE` | `/api/templates/:id` | Delete template | +| `POST` | `/api/workflows/instantiate` | Instantiate template as goal | + +## WebSocket Events + +The task board broadcasts state changes to all connected clients: + +| Event | Description | +| -------------- | ---------------------------------------- | +| `loop_status` | Loop state changed (idle/running/paused) | +| `task_state` | Full task tree update | +| `task_updated` | Single task update | +| `task_deleted` | Task deletion | + +## Session Policy + +Currently Phase 2, which supports `reuse` session policy only -- tasks are assigned to the existing session. Phase 3 will add `spawn` (new session per task) and `auto` (orchestrator decides). diff --git a/docs/v2-protocol.md b/docs/v2-protocol.md new file mode 100644 index 00000000..76bec757 --- /dev/null +++ b/docs/v2-protocol.md @@ -0,0 +1,584 @@ +# v2 Streaming Protocol + +Mitzo uses a custom WebSocket protocol (v2) to stream Claude Code session events between server and client. This document covers the message lifecycle, reconnection semantics, subagent nesting, and transport alternatives. + +For the internal design rationale, see `docs/design/message-protocol-v2.md`. + +## Connection Lifecycle + +### Handshake + +``` +Client Server + | | + |--- hello {protocolVersion: 2} --> + | | + |<-- welcome {connectionId} ---- | + | | +``` + +The client sends a `hello` message immediately after the WebSocket opens. The server responds with a `welcome` containing a `connectionId` that identifies this connection for the session's lifetime. + +```json +// Client -> Server +{ "type": "hello", "protocolVersion": 2 } + +// Server -> Client +{ "type": "welcome", "protocolVersion": 2, "connectionId": "conn-abc123" } +``` + +### Reconnection + +When a WebSocket drops and reconnects, the client sends a `reconnect` message with the sessions it was watching and the last sequence number received for each: + +```json +// Client -> Server +{ + "type": "reconnect", + "sessions": [ + { "sessionId": "sess-1", "lastSeq": 42 }, + { "sessionId": "sess-2", "lastSeq": 17 } + ] +} + +// Server -> Client +{ + "type": "reconnected", + "sessions": [ + { "sessionId": "sess-1", "replayed": 5, "running": true }, + { "sessionId": "sess-2", "replayed": 0, "running": false } + ] +} +``` + +The server replays all events after each session's `lastSeq` from the event store. The client processes replayed events through the same reducer as live events, so the UI catches up seamlessly. + +## Session Management + +### Watching Sessions + +A connection can watch multiple sessions simultaneously. Watched sessions receive event broadcasts. + +```json +// Subscribe +{ "type": "watch", "sessionId": "sess-1" } +// Confirm +{ "type": "watched", "sessionId": "sess-1" } + +// Unsubscribe +{ "type": "unwatch", "sessionId": "sess-1" } +// Confirm +{ "type": "unwatched", "sessionId": "sess-1" } +``` + +### Switching Active Session + +The active session is the one the client is currently interacting with. Only one session can be active per connection. + +```json +// Switch to session +{ "type": "switch_session", "sessionId": "sess-1" } +// Server confirms with session metadata +{ + "type": "session_switched", + "sessionId": "sess-1", + "mode": "agent", + "cwd": "/path/to/worktree", + "branch": "session/sess-1", + "wtId": "wt-123", + "running": true, + "tokens": { + "input": 10000, + "output": 5000, + "cacheRead": 3000, + "cacheCreation": 1000, + "costUsd": 0.45 + } +} + +// Clear active session +{ "type": "switch_session", "sessionId": null } +// Confirm +{ "type": "session_cleared" } +``` + +### Suspend (iOS Background) + +```json +// Client -> Server (via WS or sendBeacon fallback) +{ + "type": "session_suspend", + "sessions": [{ "sessionId": "sess-1", "lastSeq": 42 }] +} +``` + +### Close Session + +```json +{ "type": "session_close", "sessionId": "sess-1" } +// Confirm +{ "type": "session_close_ack", "sessionId": "sess-1" } +``` + +### Session Takeover + +If another connection takes over an active session, the original connection receives: + +```json +{ "type": "session_takeover", "sessionId": "sess-1" } +``` + +## Chat Messages + +### Sending a Message + +```json +{ + "type": "send", + "sessionId": null, + "prompt": "Fix the login bug", + "clientMsgId": "msg-uuid-1", + "mode": "agent", + "images": [{ "data": "base64...", "mediaType": "image/png" }], + "contextBlocks": ["Architecture"], + "extraTools": "Bash", + "isolation": true, + "telosTaskId": "telos-123", + "agentName": "workspace-assistant" +} +``` + +| Field | Type | Required | Description | +| --------------- | ---------------- | -------- | ---------------------------------------------------------- | +| `sessionId` | `string \| null` | Yes | `null` to start a new session, or existing session ID | +| `prompt` | `string` | Yes | User message (min 1 char) | +| `clientMsgId` | `string` | Yes | Client-generated message ID for dedup | +| `model` | `string` | No | Model override | +| `mode` | `string` | No | Permission mode: `ask`, `agent`, `auto` | +| `cwd` | `string` | No | Working directory override | +| `extraTools` | `string` | No | Additional tools to allow | +| `isolation` | `boolean` | No | Enable worktree isolation | +| `images` | `array` | No | Image attachments (base64 + mediaType) | +| `contextBlocks` | `string[]` | No | Context block IDs to inject | +| `telosTaskId` | `string` | No | Link session to a Telos task | +| `agentName` | `string` | No | Agent definition name (alphanumeric, hyphens, underscores) | + +When `sessionId` is `null`, the server creates a new session and responds with: + +```json +{ "type": "session_id", "sessionId": "sess-new-123" } +``` + +### Interrupting + +Send a follow-up message while Claude is still responding: + +```json +{ + "type": "interrupt", + "sessionId": "sess-1", + "prompt": "Actually, also fix the logout", + "clientMsgId": "msg-uuid-2" +} +``` + +### Stopping + +Abort the current query: + +```json +{ "type": "stop", "sessionId": "sess-1" } +``` + +### Permission Response + +Respond to a tool permission prompt: + +```json +{ + "type": "permission_response", + "sessionId": "sess-1", + "permId": "perm-123", + "decision": "once" +} +``` + +Decisions: `once` (allow this invocation), `always` (add to session allow-list), `deny`. + +### Setting Mode + +```json +{ "type": "set_mode", "sessionId": "sess-1", "mode": "auto" } +// Confirm +{ "type": "mode_changed", "sessionId": "sess-1", "mode": "auto" } +``` + +## Message Lifecycle + +The core of the v2 protocol is the block lifecycle. Every assistant response follows this pattern: + +``` +message_start + +-- block_start (text) + | +-- block_delta (streaming text chunks) + | +-- block_delta + | +-- block_end + +-- block_start (tool_use) + | +-- block_delta (tool input JSON) + | +-- block_end + | +-- tool_result + +-- block_start (text) + | +-- block_delta + | +-- block_end +message_end +``` + +### message_start + +Marks the beginning of an assistant turn. + +```json +{ + "v": 2, + "type": "message_start", + "ts": 1720000001000, + "messageId": "msg-server-1", + "sessionId": "sess-1" +} +``` + +### block_start + +A content block begins. Block types: `text`, `tool_use`. + +```json +// Text block +{ + "v": 2, + "type": "block_start", + "ts": 1720000002000, + "blockId": "block-1", + "blockIndex": 0, + "blockType": "text", + "sessionId": "sess-1" +} + +// Tool use block +{ + "v": 2, + "type": "block_start", + "ts": 1720000003000, + "blockId": "block-2", + "blockIndex": 1, + "blockType": "tool_use", + "toolName": "Edit", + "toolUseId": "tool-use-123", + "sessionId": "sess-1" +} +``` + +### block_delta + +Streaming content for an open block. For text blocks, `delta` is a string. For tool use blocks, `delta` may be a partial JSON object. + +```json +{ + "v": 2, + "type": "block_delta", + "ts": 1720000003500, + "blockId": "block-1", + "blockIndex": 0, + "delta": "Here's the fix for the", + "sessionId": "sess-1" +} +``` + +### block_end + +A content block is complete. + +```json +{ + "v": 2, + "type": "block_end", + "ts": 1720000004000, + "blockId": "block-1", + "blockIndex": 0, + "sessionId": "sess-1" +} +``` + +### tool_result + +The result of a tool execution, delivered after the tool's `block_end`. + +```json +{ + "v": 2, + "type": "tool_result", + "ts": 1720000005000, + "blockId": "block-2", + "blockIndex": 1, + "sessionId": "sess-1", + "result": "File updated successfully" +} +``` + +### message_end + +Marks the end of an assistant turn with usage statistics. + +```json +{ + "v": 2, + "type": "message_end", + "ts": 1720000006000, + "messageId": "msg-server-1", + "sessionId": "sess-1", + "stopReason": "end_turn", + "usage": { + "inputTokens": 5000, + "outputTokens": 2000, + "cacheReadTokens": 1500, + "cacheCreationTokens": 500 + } +} +``` + +### session_end + +Marks the end of a query (SDK query complete). + +```json +{ + "v": 2, + "type": "session_end", + "ts": 1720000007000, + "sessionId": "sess-1", + "usage": { + "inputTokens": 15000, + "outputTokens": 8000 + } +} +``` + +## Deferred message_end + +The server uses an `openBlockCount` to defer `message_end` until all blocks are closed. This handles the case where the SDK emits `message_end` before the last tool result is delivered: + +``` +SDK order: v2 order (deferred): + text text + tool_use tool_use + message_end <-- early tool_result <-- delivered first + tool_result message_end <-- deferred until here +``` + +The `forceFlushPendingMessage()` function force-closes any open blocks at turn boundaries and session end, preventing orphaned streaming state. + +## Subagent Messages + +When Claude spawns a subagent (via the Agent tool), Mitzo tracks it as a nested message stream within the parent tool use block. + +``` +message_start + +-- block_start (tool_use: "Agent") + | +-- block_delta (tool input) + | +-- block_end + | +-- subagent_start + | | +-- subagent_block_start (text) + | | | +-- subagent_block_delta + | | | +-- subagent_block_end + | | +-- subagent_block_start (tool_use) + | | | +-- subagent_block_delta + | | | +-- subagent_block_end + | | | +-- subagent_tool_result + | | +-- subagent_end + | +-- tool_result +message_end +``` + +### subagent_start + +```json +{ + "v": 2, + "type": "subagent_start", + "ts": 1720000010000, + "parentToolId": "tool-use-123", + "parentBlockId": "block-2", + "parentToolName": "Agent", + "sessionId": "sess-1" +} +``` + +### subagent_block_start / subagent_block_delta / subagent_block_end + +Same structure as top-level blocks, but prefixed with `subagent_` and carrying `parentToolId`: + +```json +{ + "v": 2, + "type": "subagent_block_start", + "ts": 1720000011000, + "parentToolId": "tool-use-123", + "blockId": "sub-block-1", + "blockIndex": 0, + "blockType": "text", + "sessionId": "sess-1" +} +``` + +### subagent_tool_result + +```json +{ + "v": 2, + "type": "subagent_tool_result", + "ts": 1720000012000, + "parentToolId": "tool-use-123", + "blockId": "sub-block-2", + "blockIndex": 1, + "result": "File created", + "sessionId": "sess-1" +} +``` + +### subagent_end + +```json +{ + "v": 2, + "type": "subagent_end", + "ts": 1720000013000, + "parentToolId": "tool-use-123", + "sessionId": "sess-1", + "usage": { + "inputTokens": 3000, + "outputTokens": 1500 + } +} +``` + +### subagent_cancelled + +If a subagent is interrupted or fails: + +```json +{ + "v": 2, + "type": "subagent_cancelled", + "ts": 1720000014000, + "parentToolId": "tool-use-123", + "sessionId": "sess-1" +} +``` + +## Other Server Messages + +### user_message + +Echoed back after the server receives a `send` message: + +```json +{ + "type": "user_message", + "sessionId": "sess-1", + "id": "msg-uuid", + "prompt": "Fix the bug", + "clientMsgId": "msg-uuid-1" +} +``` + +### boot_context + +Session context metadata sent on connect or session switch: + +```json +{ + "type": "boot_context", + "sessionId": "sess-1", + "source": "contexgin", + "tokenCount": 8000, + "tokenBudget": 12000, + "sourceCount": 6 +} +``` + +### session_resumed + +Sent when a suspended session is resumed: + +```json +{ + "type": "session_resumed", + "sessionId": "sess-1", + "replayed": 3 +} +``` + +### skill_invoked + +Notification that a skill was resolved and invoked: + +```json +{ + "v": 2, + "type": "skill_invoked", + "name": "pr-review", + "source": "bundled", + "arguments": { "pr": "42" } +} +``` + +### native_command_result + +Result of a native command (like `/skills`): + +```json +{ + "v": 2, + "type": "native_command_result", + "command": "skills", + "content": "Available skills:\n- /simplify ..." +} +``` + +### error + +```json +{ + "type": "error", + "error": "Session not found: sess-999" +} +``` + +## Message Versioning + +All v2 protocol messages include a `v: 2` field. This allows the client to distinguish v2 messages from any legacy formats during migration periods. + +## Sequence Numbers + +Every event stored in the event store gets a monotonic sequence number per session. Clients track the last received `seq` per session and use it for reconnection replay. The `MitzoConnection` class handles this automatically via `trackSeq()` and `getLastSeq()`. + +## SSE Alternative + +For environments where WebSocket is unavailable, the same protocol semantics are available over Server-Sent Events: + +1. `GET /api/chat/events` opens an SSE stream +2. Server sends `welcome` event with `connectionId` +3. Client uses POST endpoints (`/api/chat/send`, `/api/chat/stop`, etc.) with `X-Connection-ID` header +4. Server pushes v2 events over the SSE stream + +The message format is identical; only the transport differs. + +## Client Implementation + +The `@mitzo/client` package provides a complete client implementation: + +- **`MitzoConnection`** -- manages the WebSocket lifecycle, hello/welcome handshake, reconnection with seq replay, and pending message queue (up to 100 messages during reconnect) +- **Protocol parser** -- `parseServerMessage()` converts raw JSON into typed actions +- **Zustand store** -- dispatches parsed messages through a reducer that maintains streaming state, finished messages, permissions, and session metadata +- **React hooks** -- `useChatMessages`, `useConnection`, `useSessions`, etc. + +See [docs/packages.md](packages.md) for the full client API.