# Building Kanban from scratch: a board that lets hundreds of coding agents work in parallel

> **What this tutorial is**: a from-scratch build plan whose blueprint is **kanban** (`github.com/cline/kanban`, Apache-2.0, v0.1.70). You will act as a developer who can also do product work: start from a PRD, then explode kanban into an executable development plan—**each functional module listed in build order, with purpose, implementation shape, stack, definition trade-offs, strengths and costs, and how the pieces couple**. No technical detail is too small.
> **Core narrative**: follow the action “the user clicks the play button on a card” from zero to the end—how it creates an independent worktree, how it starts an agent, how every agent message is reverse-injected onto the card via hooks, how you review the diff, how you commit / open a PR, how the dependency chain auto-starts the next card. That chain is the skeleton of the whole project.
> **How to read**: read chapter 1 (PRD) and chapter 2 (overview and milestones) first, then proceed in order. Chapter 24 (the full workflow) is the climax; read it closely after modules 1–9.

---

## Contents

### Part I · Charter and foundation
1. [PRD: what we will and will not build](#ch1)
2. [Overview: three architectural ideas, milestones, team](#ch2)
3. [Module 0: Project scaffold and engineering discipline](#ch3)

### Part II · Board core (build in order)
4. [Module 1: Data model and API contract (zod as the whole contract)](#ch4)
5. [Module 2: Board state machine and optimistic-concurrency persistence](#ch5)
6. [Module 3: git layer — worktree, symlink, checkpoint, git UI data](#ch6)
7. [Module 4: HTTP server and static assets](#ch7)
8. [Module 5: tRPC API layer (4 routers + dependency injection)](#ch8)
9. [Module 6: Real-time state stream (WS snapshot + incremental fanout)](#ch9)

### Part III · Terminal and multi-agent execution
10. [Module 7: PTY terminal and dual WebSocket](#ch10)
11. [Module 8: Session manager and state machine](#ch11)
12. [Module 9: Multi-agent adapter layer (7 CLI agents)](#ch12)
13. [Module 10: Hooks reverse-injection](#ch13)

### Part IV · Deep Cline integration
14. [Module 11: Cline SDK integration (`ClineCore` multi-session host)](#ch14)
15. [Module 12: Event translation and message repository](#ch15)
16. [Module 13: Provider and MCP services](#ch16)

### Part V · Frontend and experience
17. [Module 14: Frontend board UI (React + optimistic-concurrency trio)](#ch17)
18. [Module 15: Frontend persistent terminal (xterm dual WS)](#ch18)
19. [Module 16: git UI, comments, and commit/PR](#ch19)
20. [Module 17: Home agent and board-injection instructions](#ch20)

### Part VI · Security, close, and full workflow
21. [Module 18: Four security layers (passcode / Host / CORS / token)](#ch21)
22. [Module 19: CLI and subcommands (`task` / `hooks` / `update`)](#ch22)
23. [Module 20: Desktop Electron](#ch23)
24. [“After the user clicks play”: the full workflow](#ch24)
25. [Testing strategy and engineering (dogfood / CI/CD)](#ch25)

---

<h2 id="ch1">Chapter 1 PRD: what we will and will not build</h2>

### 1.1 One-sentence product definition

> **kanban is a local multi-agent board: each card = one independent git worktree + one independent terminal session. It organizes multiple coding agents (Claude Code / Codex / Cline / Droid / Kiro) to work in parallel, uses four board columns for process, card links as a dependency chain, and hooks to reverse-inject live state. It supports diff review, comment round-trip, auto-commit / auto-open PR.** (README’s own words: *A replacement for your IDE better suited for running many agents in parallel and reviewing diffs*)

### 1.2 Users and scenarios

- **Core user**: developers/teams who heavily use coding agents—when 10–100 tasks run in parallel, the IDE can no longer hold them.
- **Scenario A (parallel)**: one large requirement splits into 20 cards, all running at once; each card has its own worktree; zero merge conflicts.
- **Scenario B (pipeline)**: card A finishes → auto-commit → card B (a linked waiter) auto-starts → commit again → card C… fully autonomous chain execution.
- **Scenario C (review)**: the agent finishes and the card enters the review column; a human reads the diff, leaves inline comments, sends them back; when satisfied, one-click Commit / Open PR.
- **Scenario D (monitor)**: scan hundreds of cards at a glance; each card live-shows what the agent is currently saying and which tool it is using.

### 1.3 Functional requirements (FR) and priority

| ID | Feature | Priority |
|---|---|---|
| FR-1 | Local server + browser board (four columns: `backlog`/`in_progress`/`review`/`trash`) | P0 |
| FR-2 | Per-card independent git worktree + gitignored-path symlink sharing | P0 |
| FR-3 | Per-card independent terminal (`node-pty` + browser xterm) | P0 |
| FR-4 | Multi-agent detection and adaptation (`claude`/`codex`/`cline`/`droid`/`kiro`) | P0 |
| FR-5 | Hooks reverse-inject live state (tool calls / messages shown on the card) | P0 |
| FR-6 | Card links → dependency-chain auto-start | P1 |
| FR-7 | Diff review + inline-comment round-trip | P1 |
| FR-8 | Commit / Open PR (inject a prompt so the agent does git itself) | P1 |
| FR-9 | `auto-commit` / `auto-PR` fully autonomous mode | P1 |
| FR-10 | Cline native SDK deep integration (Provider/OAuth/MCP) | P1 |
| FR-11 | Desktop Electron | P2 |
| FR-12 | Multi-project management + git-history visualization + shortcut script buttons | P2 |

### 1.4 Non-functional requirements (NFR)

- **Local-first**: everything runs in a local process; the browser is only the control plane; data does not leave the machine (telemetry can be turned off).
- **Zero-config start**: `npx kanban` is immediately usable; auto-detects installed agents; no account required.
- **Concurrency-safe**: multiple instances, multiple tabs, multiple viewers share one PTY without mixing.
- **Crash recovery**: after the server restarts, card state, sessions, and worktrees all recover.
- **Security**: bind `127.0.0.1` by default; remote mode uses a passcode; defend against DNS rebinding and cross-origin reads.
- **Performance**: live-state push for hundreds of cards must not stall (150ms batch merge, back-pressure control).

### 1.5 Explicit non-goals

- ❌ **Do not implement the agent main loop yourself**—the agent is the executor; kanban is the orchestration layer.
- ❌ **Do not manage the agent’s provider keys** (on the PTY path)—that is each agent’s own job; only the Cline SDK path needs it.
- ❌ **Do not auto-merge code**—git conflicts are handed to the agent (it understands context).
- ❌ **Do not do multi-user collab / cloud sync**—a local single-user tool.

### 1.6 PRD acceptance bar

- `npx kanban` starts at the root of any git repo; the browser opens the board automatically.
- Create a card → play → the card enters `in_progress`; a worktree appears under `~/.cline/worktrees/`.
- The agent’s messages / tool calls appear on the card in real time (hooks injection).
- Card finishes → review column; click Commit → the worktree becomes a branch commit; trash → worktree cleaned up, the dependency chain auto-starts the next card.
- After the server crashes and restarts, all card state and sessions recover.

---

<h2 id="ch2">Chapter 2 Overview: three architectural ideas, milestones, team</h2>

### 2.1 Target architecture (aligned with kanban v0.1.70)

```
Browser (control plane, React SPA)
   │ tRPC httpBatchLink / WS state stream / WS terminal io+control
Local runtime (the only source of truth)
   ├─ server layer: runtime-server / middleware / assets / runtime-state-hub
   ├─ API layer: tRPC 4 routers (runtime/workspace/projects/hooks)
   ├─ core layer: api-contract(zod) / workspace-state / task-board-mutations
   ├─ execution layer: terminal (PTY+dual WS) / cline-sdk (ClineCore) / workspace (git)
   └─ persistence: ~/.cline/kanban/ (JSON) + ~/.cline/worktrees/ (git)
```

Three core ideas (from `docs/architecture.md`):
1. **The browser is only the control plane**: render, send commands, receive live updates; it does not store state.
2. **The local runtime is the only source of truth**: projects/worktrees/sessions/git/streaming state all live in the local process.
3. **Two agent execution paths**: most agents = PTY child processes; Cline = native SDK session (in-process).

### 2.2 Milestones (~6 months, 3-person team)

| Phase | Content | Aligned version |
|---|---|---|
| M1 (1 month) | Scaffold + data model + worktree + basic board (card CRUD / four columns) | ~0.1.4 |
| M2 (1 month) | PTY terminal + session manager + Claude Code adapter + xterm | ~0.1.5 |
| M3 (1 month) | Hooks reverse-injection + Codex/Droid/Kiro adapters + dependency chain | ~0.1.9 |
| M4 (1 month) | Diff review + comment round-trip + Commit/PR + auto-review | ~0.1.12 |
| M5 (1 month) | Cline SDK integration + Provider/MCP + context compaction | ~0.1.30 |
| M6 (1 month) | Desktop Electron + multi-project + git-history visualization + polish/release | ~0.1.70 |

### 2.3 Team split (3 people)

- **Backend engineer A**: server, git, terminal, multi-agent adapters, hooks.
- **Frontend engineer B**: board UI, terminal UI, git UI, state sync.
- **Full-stack engineer C**: Cline SDK integration, tRPC API, security, desktop, release.

### 2.4 Engineering discipline (three red lines, from `AGENTS.md`)

1. **No `any`, ban inline import** (Biome-enforced).
2. **Prefer direct PATH detection over launching an agent from an interactive shell**—`which` spawns a shell; conda/nvm environments will freeze the runtime.
3. **GritQL static checks**: ban `console.*`, ban `process.exit` (except CLI entry), ban destructuring `process.env`.

---

<h2 id="ch3">Chapter 3 Module 0: Project scaffold and engineering discipline</h2>

### Purpose
One npm package produces a CLI (`bin: kanban`), a library entry (`main`), web-ui static assets, and a desktop app. The toolchain is unified.

### Implementation shape (aligned with kanban’s `package.json`)

- Single package (not a monorepo) + subdirectory `web-ui/` (independent npm package) + `packages/desktop/` (independent private package). Root package `type: module`.
- Scripts: `build` (clean → web:build → esbuild → copy web-ui artifacts → sentry sourcemap); `dev` (tsx watch); `typecheck` (`tsc --noEmit`); `test` (vitest); `check` (biome + typecheck + test).
- Dependencies: commander (CLI), `node-pty` (PTY), `ws` (WebSocket), tRPC (server/client), zod (contract), `proper-lockfile` (file lock), `tree-kill` (process tree), `@xterm/headless` (server-side terminal mirror), `@clinebot/core` (Cline engine), `@sentry/node` (telemetry).

### Stack detail
TypeScript 5.9 + esbuild (bundle) + tsx (dev) + Biome (lint/format) + vitest (tests) + husky (pre-commit: biome → typecheck → test:fast) + GritQL (structural checks).

### Definition trade-offs
- Bundle with esbuild rather than tsc: fast, and only one `node-pty` needs to be external.
- `npm run dogfood` is a development weapon: first build, then run yourself with the production CLI—force “eat your own cooking.”

### Strengths and costs
- ✅ Single package is simple; publish pushes one npm package; ❌ web-ui and desktop each need a manual `npm install` (`install:all` script).
- ✅ tsx watch is second-scale hot reload; ❌ esbuild does no typecheck (typecheck runs separately).

### How the pieces couple
The root package’s `dist/` is the delivery form of CLI and web-ui; desktop’s `stage-cli.mjs` copies `dist/` into the Electron shell; `dogfood.mjs` depends on a complete build.

### Acceptance
- `npm run build && npx kanban` starts the service; `npm run check` is all green; `npm run dogfood` can run.

---

<h2 id="ch4">Chapter 4 Module 1: Data model and API contract (zod as the whole contract)</h2>

### Purpose
Frontend, backend, and CLI share one type contract—**one `z` type defines the whole API**, so the frontend cannot say east while the backend says west.

### Implementation shape (aligned with `src/core/api-contract.ts`, 1277 lines)

- All request/response schemas are named in pairs `runtimeXxxRequestSchema / ResponseSchema` + exported inferred types.
- Core types: `RuntimeBoardCard` (`id`/`title`/`prompt`/`startInPlanMode`/`autoReviewMode`/`images`/`agentId`/`clineSettings`/`baseRef`/`createdAt`/`updatedAt`), `RuntimeTaskSessionSummary` (`state`/`mode`/`reviewReason`/`latestHookActivity`), board 4 columns, `dependencies` (`fromTaskId`→`toTaskId`).
- Session state machine: `idle`/`running`/`awaiting_review`/`failed`/`interrupted`, `reviewReason: attention|exit|error|interrupted|hook`.
- WS push messages are a `discriminatedUnion` (11 kinds: `snapshot`/`workspace_state_updated`/`task_sessions_updated`/`task_ready_for_review`/`task_chat_message`…).
- `src/core/api-validation.ts` (605 lines) second-pass validation: trim, required, dedupe, path sandbox.

### Stack detail
zod v4 + TypeScript type inference. web-ui imports the same contract via alias `@runtime-trpc`.

### Definition trade-offs
- Card `title` is derived from the prompt automatically (truncated to 80 characters) rather than typed by hand—less user burden; `task-id` uses a 5-char short id for spoken communication.
- `"done"` is compatibility-mapped to `trash`—early versions called it done; later unified as trash (recycle-bin semantics: recoverable).

### Strengths and costs
- ✅ The contract is the documentation; a type change fails the whole chain at compile time; ❌ the contract file will bloat (1277 lines) and needs disciplined maintenance.
- ✅ `discriminatedUnion` makes WS message types safe; ❌ adding a message kind must touch contract + backend + frontend.

### How the pieces couple
Module 2 (state persistence) serializes these types; module 5 (tRPC) procedure inputs/outputs directly reference these schemas; module 6 (state stream) push messages are this `discriminatedUnion`.

### Acceptance
- Frontend import of contract types passes typecheck; change one field, all three ends (server/CLI/web) fail to compile together.

---

<h2 id="ch5">Chapter 5 Module 2: Board state machine and optimistic-concurrency persistence</h2>

### Purpose
Board state (cards/columns/dependencies/session summaries) persists safely; concurrent edits from multiple tabs do not lose data.

### Implementation shape (aligned with `src/state/workspace-state.ts` 745 lines + `src/core/task-board-mutations.ts` 657 lines + `src/fs/locked-file-system.ts` 165 lines)

- Storage layout: `~/.cline/kanban/workspaces/<id>/{board.json, sessions.json, meta.json}`.
- **Pure-function mutations**: `task-board-mutations.ts` `add`/`move`/`update`/`trash`/`delete`/`link` each return a new board, do not mutate the original—isomorphic with React immutable updates.
- **Revision optimistic concurrency**: `saveWorkspaceState({board, sessions, expectedRevision})`; `meta.json` increments `revision`; mismatch throws `WorkspaceStateConflictError` carrying `currentRevision`.
- **Atomic write + file lock**: `locked-file-system.ts` uses `proper-lockfile`; `withLocks` locks in lockfile-path sort order (prevent deadlock); `writeTextFileAtomic` tmp+rename.

### Stack detail
TypeScript + `proper-lockfile` + `node:fs`. No database—JSON files are enough, and human-readable/repairable.

### Definition trade-offs
- No SQLite/leveldb: board state is low-frequency small data; JSON + atomic write is enough; session message volume is large and uses the SDK’s own storage (see module 12).
- Lock granularity: one lock for the whole workspace state, simple and reliable; the cost is that high-frequency ops queue.

### Strengths and costs
- ✅ The file is the truth; crash does not lose (tmp+rename); ✅ optimistic concurrency fits frontend interaction better than a pessimistic lock (does not block drag); ❌ file locks are unreliable on NFS / some network disks; ❌ no transactions (multi-file inconsistency is backstopped by recovery logic).

### How the pieces couple
Module 1 defines types → module 2 persists; module 5’s `workspace.saveState` procedure is exposed to the frontend (with `expectedRevision`); the frontend calls with 120ms debounce (module 14).

### Acceptance
- Two tabs drag a card at the same time; the later save receives CONFLICT; the frontend auto-syncs and retries; `kill -9` the server and restart, board state is intact.

---

<h2 id="ch6">Chapter 6 Module 3: git layer — worktree, symlink, checkpoint, git UI data</h2>

### Purpose
**Use git’s mechanisms for resource isolation**: one worktree per card, agents in parallel with zero conflicts; symlink shares dependencies to save install cost; checkpoint records each turn’s changes.

### Implementation shape (aligned with all of `src/workspace/`)

1. **Worktree lifecycle** (`task-worktree.ts`, 695 lines):
   - `git worktree add --detach <baseCommit>`, path `~/.cline/worktrees/<taskId>/<workspaceLabel>`.
   - **Detached HEAD, no branch**—the branch is created dynamically only at Commit/Open PR.
   - An existing worktree is authoritative; it is not rebuilt because `base` advanced; creation runs under `.git/kanban-task-worktree-setup.lock`.
   - On trash, `captureTaskPatch` stores the diff under `~/.cline/kanban/trashed-task-patches/`, then `git worktree remove --force`; on resume, `git apply` restores.
2. **Symlink shared dependencies** (`syncIgnoredPathsIntoWorktree`, L358): `git ls-files --others --ignored --exclude-per-directory=.gitignore --directory` lists gitignored paths → in-place `symlink(main repo→worktree)` → write a kanban-managed block into `.git/info/exclude` → fail silently skip → `getUniquePaths` keeps only the shallowest roots.
3. **Checkpoint** (`turn-checkpoints.ts`, 92 lines): after each message turn, create a commit with a **temporary `GIT_INDEX_FILE`** (`/tmp`) (author `kanban-checkpoint`), `update-ref refs/kanban/checkpoints/<base64(taskId)>/turn/<N>`—does not pollute the branch; pure refs.
4. **git command wrapping** (`git-utils.ts`): `execFile("git", ["-c","core.quotepath=false", ...])`, 10MB `maxBuffer`; **non-zero exit does not throw**, returns `{ok,stdout,stderr,exitCode}`.
5. **git UI data** (`git-history.ts`, 478 lines): `git log --topo-order --date-order` + `\x1f`/`\x1e` separators + `for-each-ref` + `rev-list --not` to compute relation.
6. **Sync actions** (`git-sync.ts`, 385 lines): fetch / pull `--ff-only` (refuse if local changes) / push / checkout / discard.
7. **Turbopack exception** (`task-worktree-turbopack.ts`): Turbopack following a symlink breaks the cache → if detected, `node_modules` is not symlinked.
8. **Path sandbox** (`path-sandbox.ts`): `isPathWithinRoot` prevents escape.

### Stack detail
git CLI (do not pull a node-git library—`execFile` is more controllable) + `node:fs` symlink + `proper-lockfile`.

### Definition trade-offs
- **Detached worktree rather than one branch per card**: branch semantics (based on which base, when to push) wait until commit/PR, avoiding a pile of premature branches.
- **kanban does not auto-commit/merge**: auto-commit injects a prompt so the agent does git itself (module 17)—git conflict handling is given to the agent that understands context.
- **Checkpoint uses a temporary index + commit-tree rather than a direct commit**: does not pollute the workspace, does not fire hooks, does not touch the branch.

### Strengths and costs
- ✅ worktree is a git-native mechanism; isolation is thorough and auditable; ✅ symlink saves a copy of `node_modules` per card (several GB); ❌ if the agent edits gitignored files they will leak across (the README is explicit: do not do that); ❌ Turbopack is a special case; detection logic must be maintained.

### How the pieces couple
Module 2 persists board state (the worktree is the card’s “shadow”); module 7’s PTY process `cwd` points at the worktree; module 10’s workspace-trust auto-trusts only kanban-created worktrees; module 14’s diff panel calls `getWorkspaceChanges`.

### Acceptance
- Open 3 cards in parallel; 3 worktrees each have independent `git log`; `node_modules` is a symlink and is not committed; after trashing a card, `~/.cline/kanban/trashed-task-patches/` has a patch; after resume, files restore.

---

<h2 id="ch7">Chapter 7 Module 4: HTTP server and static assets</h2>

### Purpose
One local HTTP(S) server: serve React static assets + mount tRPC + mount WS + mount MCP OAuth callback.

### Implementation shape (aligned with `src/server/runtime-server.ts` 519 lines + `middleware.ts` 167 lines + `assets.ts`)

- **Native `node:http`/`node:https`**, no express (fewer deps, finer control).
- Request chain: middleware (Host whitelist + CORS gate) → passcode gate (remote mode only) → MCP OAuth callback → tRPC handler (`createHTTPHandler`, `basePath /api/trpc`) → static assets (multi-path probe of web-ui build artifacts, path-traversal defense, SPA fallback `index.html`).
- WS on `server.on("upgrade")` splits by pathname: `/api/runtime/ws` (state stream), `/api/terminal/io`, `/api/terminal/control`.
- Supports `--https --cert --key` (self-signed certs for remote access).

### Stack detail
`node:http` + `ws` + `@trpc/server/adapters/standalone`.

### Definition trade-offs
- Use the standalone adapter rather than express: zero framework deps; HTTP handler and WS upgrade share the same server instance.
- SPA fallback: any non-API path returns `index.html` (React has no router; URL query holds state).

### Strengths and costs
- ✅ Native http is fast, no framework baggage; ❌ all middleware is handwritten (Host/CORS/passcode); you must guarantee correctness yourself.
- ✅ Static assets and API share one port, one URL; ❌ in development the Vite dev server is another port (`dev-full.mjs` coordinates).

### How the pieces couple
Module 5 (tRPC) mounts here; module 6 (state stream) and module 7 (terminal WS) split on upgrade; module 18 (security) passcode is the gate here.

### Acceptance
- `curl http://127.0.0.1:<port>/` returns `index.html`; `/api/trpc/...` goes tRPC; `/api/runtime/ws` upgrade succeeds.

---

<h2 id="ch8">Chapter 8 Module 5: tRPC API layer (4 routers + dependency injection)</h2>

### Purpose
A type-safe RPC layer for frontend and backend, serving three clients: web-ui, CLI, and hooks child processes.

### Implementation shape (aligned with `src/trpc/app-router.ts` 731 lines + 4 API files)

- `runtimeAppRouter` exposes 4 routers: **runtime (~40 procedures: session/Cline/MCP/shell/update), workspace (14: git/changes/worktree/state/file search), projects (5: multi-project management), hooks (1: event ingest)**.
- **Thin routing + dependency injection**: each procedure is only schema pairing then a delegate to a service on the context.
- `workspaceProcedure` middleware forces workspace scope (parses `workspaceId`/`workspacePath` from the `x-kanban-workspace-id` header).
- `errorFormatter` surfaces CONFLICT’s `currentRevision`.
- Transport is pure HTTP (`httpBatchLink`); three clients: web-ui (client cached by `workspaceId`), CLI `task.ts`/`hooks.ts` (custom fetch + internal Bearer token).

### Stack detail
`@trpc/server` v11 + `@trpc/client` + zod. No tRPC ws adapter—WS is only for the state stream and the terminal.

### Definition trade-offs
- A runtime procedure is a “coordinator,” not an “executor”: `startTaskSession` internally dispatches by `agentId` onto the SDK/PTY two paths.
- Board CRUD does not enter the tRPC workspace router—the CLI does `mutateWorkspaceState` directly on JSON files (optimistic concurrency), then calls `workspace.notifyStateUpdated` to broadcast. **One write path** (the file); reads go tRPC.

### Strengths and costs
- ✅ Type safety through three ends; ✅ procedures are thin, logic in the service layer is unit-testable; ❌ a 40+ procedure file needs careful organization (split by domain).
- ✅ CLI and web share the same API semantics; ❌ hooks child processes must manage token auth by hand.

### How the pieces couple
Module 1 contract defines inputs/outputs; module 2 state layer is called by `workspace.api`; module 10’s hooks child process is a client of `hooks.api`; module 14 frontend `trpc-client` connects directly.

### Acceptance
- web-ui calling `runtime.startTaskSession` can start a session; CLI `kanban task start` and a web action on the same card have the same effect.

---

<h2 id="ch9">Chapter 9 Module 6: Real-time state stream (WS snapshot + incremental fanout)</h2>

### Purpose
Live state for hundreds of cards (agent messages / tool calls / board changes) pushed to the browser in milliseconds.

### Implementation shape (aligned with `src/server/runtime-state-hub.ts` 604 lines)

- Single WS endpoint `/api/runtime/ws?workspaceId=`.
- First connection sends a full `snapshot`; afterwards incremental push, 11 message kinds (`discriminatedUnion`):
  - `workspace_state_updated` / `task_sessions_updated` (**150ms batch merge**—high-frequency events collapsed into one)
  - `task_chat_message` / `task_chat_cleared`
  - `task_ready_for_review` / `workspace_metadata_updated` / `mcp_auth_updated` / `cline_session_context_updated`
- Fanout: broadcast to all subscribers; isolated by `workspaceId`.

### Stack detail
`ws` + in-memory hub (`Map<workspaceId, Set<client>>`).

### Definition trade-offs
- WS rather than SSE: bidirectional (the browser must send ack/control messages), and the same stack as terminal WS.
- 150ms batch merge: collapse “multiple events of one tool” into one push, reducing client render pressure.

### Strengths and costs
- ✅ Incremental is better than polling (saves CPU, live); ✅ snapshot lets a new tab open instantly; ❌ state is all in memory; after server restart, recover from module 2’s files + frontend refetch calibration.

### How the pieces couple
Module 5’s services call the hub to broadcast on state change; module 14’s `use-runtime-state-stream` is its frontend mirror; module 10’s `hooks.ingest` also triggers a broadcast after migrating state.

### Acceptance
- Open two tabs; drag a card in one, the other tab syncs live; when the agent sends a message, the card activity area updates in milliseconds.

---

<h2 id="ch10">Chapter 10 Module 7: PTY terminal and dual WebSocket</h2>

### Purpose
One real terminal per card (the agent’s TUI runs inside it); the browser xterm stays in real-time bidirectional sync with it.

### Implementation shape (aligned with `src/terminal/pty-session.ts` 161 lines + `ws-server.ts` 591 lines + `terminal-protocol-filter.ts` + `terminal-state-mirror.ts`)

- **`node-pty`**: `PtySession.spawn()` a real PTY; `stop()` kills the process group (`process.kill(-pid, SIGTERM)`).
- **Dual WS** (`server.on("upgrade")` splits by pathname):
  - `/api/terminal/io`: **raw byte stream**—server `ws.send(chunk)` feeds xterm directly; browser messages are keyboard bytes passed through `writeInput`.
  - `/api/terminal/control`: JSON—`{type:"state",summary}` / `{type:"exit",code}` / `{type:"restore",snapshot,cols,rows}`; the browser sends `resize/stop/output_ack/restore_complete`.
- **Multi-tab share**: multiple viewers of the same task share one PTY; `TerminalStateMirror` (`@xterm/headless` + SerializeAddon) gives a new viewer an offline snapshot.
- **Back-pressure** (modeled on VS Code): track each viewer’s `bufferedAmount` + unacked bytes (high-water 100KB); if any lags → `pauseOutput` pauses the shared PTY; when all catch up → `resumeOutput`.
- **Protocol filter**: parse ESC/OSC/CSI byte by byte; intercept `OSC 10;?`/`11;?` and answer colors; drop Droid DA queries (`CSI c`); half-cut sequences stored in `pendingChunk`.
- **workspace-trust auto-confirm**: detect a “trust this folder” prompt and auto-enter (only for kanban-created worktrees).

### Stack detail
`node-pty` (native module, needs a patch in desktop) + `ws` + `@xterm/headless` + `@xterm/addon-serialize`.

### Definition trade-offs
- **Server-side mirror xterm**: headless xterm runs on the server as a scroll buffer; the browser only renders—this is the foundation of a “persistent terminal” (switching cards does not lose content).
- Protocol filtering is done on the server, not the browser: the browser may not be connected yet; a TUI query would hang.

### Strengths and costs
- ✅ Byte-level pass-through = any TUI can run (not only an agent; vim too); ✅ back-pressure protects the agent process from a slow browser; ❌ `node-pty` is a native module; cross-platform build / Electron packaging both need handling (`patch-node-pty.mjs` fixes the asar double-suffix bug).

### How the pieces couple
Module 8 (session manager) holds `PtySession`; module 9 (agent adapters) decides what command to spawn; module 15 (frontend persistent terminal) is the browser side of io/control; module 6’s hub and terminal WS are independent.

### Acceptance
- Browser xterm can run `vim`; two tabs share the same PTY output consistently; after one tab disconnects and reconnects, restore produces the full scroll buffer.

---

<h2 id="ch11">Chapter 11 Module 8: Session manager and state machine</h2>

### Purpose
Concurrently manage all task sessions: start/stop/input/crash recovery/restart persistence.

### Implementation shape (aligned with `src/terminal/session-manager.ts` 1040 lines + `session-state-machine.ts` 78 lines + `agent-registry.ts` 128 lines)

- `TerminalSessionManager`: `Map<taskId, SessionEntry>`; each entry holds summary + active `PtySession` + listeners + `restartRequest`.
- **Crash auto-restart**: `shouldAutoRestart` (at most 3 times in a 5-second window) → `scheduleAutoRestart` re-runs with the saved `restartRequest`.
- **State machine** (pure function `reduceSessionTransition`): `idle`/`running`/`awaiting_review`/`failed`/`interrupted`; event-driven transitions (`hook.to_review`, `process.exit`, etc.).
- `hydrateFromRecord`: after restart, restore summary from disk.
- `markInterruptedAndStopAll`: graceful stop of all (pairs with shutdown-coordinator integration tests).
- `agent-registry.ts`: `resolveAgentCommand` (compose command from catalog), `detectInstalledCommands`, `buildRuntimeConfigResponse` (for the settings UI).

### Stack detail
TypeScript + `node-pty` + `tree-kill` (`server/process-termination.ts` double-insurance process-tree cleanup).

### Definition trade-offs
- Session and card are separated: the card is a process entity, the session is an execution entity; one card lifecycle can start/restart many times.
- Auto-restart is rate-limited (5s/3 times) to prevent a crash loop burning CPU.

### Strengths and costs
- ✅ Crash self-heal is a hard need for long-running agents; ✅ the state machine is a pure function, easy to test; ❌ complex state transitions (`reviewReason` subdivisions) are a mental load; migration rules are scattered.

### How the pieces couple
Module 7 supplies the `PtySession` primitive; module 10’s hook events are state-machine inputs; module 5’s `runtime.startTaskSession` is the entry; module 6 broadcasts summary changes.

### Acceptance
- Manually kill the agent process; within 5 seconds it auto-restarts and progress is kept; after 3 consecutive crashes, stop restarting and mark `failed`.

---

<h2 id="ch12">Chapter 12 Module 9: Multi-agent adapter layer (7 CLI agents)</h2>

### Purpose
Detect which CLI agents the user has installed; for each, generate the correct launch args and hook config—**engine-neutral**.

### Implementation shape (aligned with `src/terminal/agent-session-adapters.ts` 1451 lines + `command-discovery.ts` 78 lines + `agent-catalog.ts` 95 lines)

- **Command discovery**: do not use `which` (spawning a shell has conda/nvm side effects); scan PATH with `accessSync(X_OK)` directly; Windows uses `PATHEXT`.
- **`agent-catalog`**: autonomous launch args for 7 agents: claude `--permission-mode auto`, codex `--auto-approve-all`, droid `autonomyMode`, etc. **The README depends on these experimental features**.
- **7 adapters** (`prepareAgentLaunch` dispatches by `agentId`):
  - claude: `--settings` injects `settings.json` (7 hook kinds all point at `kanban hooks notify`)
  - codex: `-c features.hooks=true` + 5 config overrides + sha256 trust hash pre-trust + `CODEX_TUI_RECORD_SESSION=1` polling rollout jsonl
  - gemini: `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
  - opencode: dynamically generate a JS plugin, payload base64 then `$` shell-call the kanban CLI
  - droid: `settings.json` (`autonomyMode: spec/auto-high/normal`)
  - kiro: `~/.kiro/agents/kanban.json` (`tools:["*"]` + hooks)
  - cline: generate bash/PowerShell scripts under `--hooks-dir`

### Stack detail
TypeScript + each agent’s official CLI args / hook mechanisms.

### Definition trade-offs
- Default cline (always `installed:true`, SDK deep integration); other agents are selectable only when detected.
- opencode/gemini are commented out at v0.1.70 (5 kinds enabled)—**adapters evolve incrementally: ship the main ones first, then fill in**.

### Strengths and costs
- ✅ Engine neutrality is the differentiation (Claude Code/Codex/Cline all run on one board); ❌ each agent’s hook mechanism is different; adapters are ongoing maintenance (Codex even needs a wrapper polling logs); ❌ depends on permission-bypass experimental features; the README self-labels Research Preview.

### How the pieces couple
Module 10 (hooks) is the adapters’ “exit”; module 7 (PTY) runs the command the adapter generated; module 2’s card `agentId` field decides which adapter; the settings UI calls `buildRuntimeConfigResponse`.

### Acceptance
- A machine with only claude installed auto-selects claude; claude/codex/cline machines can all start a task and have state appear on the card in real time.

---

<h2 id="ch13">Chapter 13 Module 10: Hooks reverse-injection</h2>

### Purpose
Every sentence the agent says, every tool call, is reflected on the card in real time—**without polling terminal output** (each TUI’s format is unreliable).

### Implementation shape (aligned with `src/commands/hooks.ts` 820 lines + `hook-events/*`)

The core sentence: **while the agent works, it actively calls kanban’s own CLI to report state**.

```
Agent event → hook command (kanban hooks notify --event ...)
  → child process reads JSON payload from stdin (or --metadata-base64)
  → parse toolName/activityText/finalMessage
  → tRPC hooks.ingest.mutate back to the runtime server
  → state-machine transition + broadcast workspace_state_updated / task_ready_for_review
```

- `ingest` is synchronously blocking (wait for ack); `notify` is a background detached process (best-effort, does not block the agent).
- `hook-events/` parses each agent’s payload: codex (TUI session log + rollout jsonl 200ms poll + fingerprint dedupe), kiro/droid (normalize functions).
- `hook-runtime-context.ts`: `KANBAN_HOOK_TASK_ID/WS_ID` env vars carry task ownership.

### Stack detail
TypeScript + child-process stdin + tRPC mutate. Hook config is each agent’s official mechanism.

### Definition trade-offs
- State ownership uses env vars rather than args: the hook child inherits env from the agent, so it naturally carries `taskId`.
- `notify` is a detached process: the agent must not be blocked by the hook (claude’s hook is synchronous; slowness would freeze the agent).

### Strengths and costs
- ✅ Semantics are clean (“this tool finished” is a fact, not a guess from garbled output); ✅ adapts 7 agents; ❌ Codex hooks are unreliable, so it is forced to poll rollout jsonl (every 200ms, high cost); ❌ each agent has a payload format; parsers are the maintenance bulk.

### How the pieces couple
Module 9 generates hook config → module 10 receives and parses → `hooks.api` (module 5) ingest → state machine (module 8) transitions → hub (module 6) broadcasts → card UI (module 14) renders.

### Acceptance
- When the agent starts a tool, the card live-shows “Using `read_files`” / “Completed `run_commands`”; when the agent requests permission, the card becomes `awaiting_review`.

---

<h2 id="ch14">Chapter 14 Module 11: Cline SDK integration (`ClineCore` multi-session host)</h2>

### Purpose
Deep-embed the Cline engine (`@clinebot/core`): same process, session-level integration, full Provider/OAuth/MCP capability.

### Implementation shape (aligned with `src/cline-sdk/sdk-runtime-boundary.ts` 126 lines + `cline-session-runtime.ts` 580 lines)

- **The only import boundary**: import `@clinebot/core` only here; re-export `ClineCore.create({backendMode:"auto"})`.
- **Key fact**: the npm-published `@clinebot/core@0.0.38` exposes a **stateful multi-session host**—`sessions` Map (`start`/`send`/`stop`/`abort`/`get`/`list`/`readMessages`/`subscribe`), `send()` incremental continue-chat, `SessionHistoryRecord` on disk. This differs from Cline `main`’s stateless `AgentRuntime` (the published API is still moving fast; the code keeps a 0.0.36 compatibility branch).
- **Task ↔ session mapping**: `ClineSessionRuntime` (an interface defined here) + `InMemoryClineSessionRuntime` implementation; `sessionIdByTaskId`/`taskIdBySessionId` bidirectional Map; `createSessionId(taskId)` is prefixed; after restart, recover from `host.list()`.
- `send()` takes `delivery: "queue"|"steer"` (queue vs interrupt the current turn).
- `cline-runtime-setup.ts`: assemble `userInstructionService` (skills/rules/workflows watch), `resolvePrompt` (slash commands), `requestToolApproval` (**default all `approved:true`**).

### Stack detail
`@clinebot/core` ^0.0.38 + TypeScript. The runtime boundary is deliberately narrowed to 126 lines—**changing the engine version only touches this one file**.

### Definition trade-offs
- Use `ClineCore` rather than implementing `SessionRuntime` yourself: engine-neutral but Cline-first—Cline is in-house; deep integration is the default path.
- `requestToolApproval` defaults to all-pass: board mode (autonomous) is about letting the agent work; human gating is at the review column (process level), not the tool level.

### Strengths and costs
- ✅ Get Cline’s full capability for free (Provider catalog/OAuth/MCP/account); ✅ the boundary file isolates version differences; ❌ SDK version-evolution risk (0.0.36/0.0.38 compatibility branches say the API is unstable); ❌ inconsistent with `main`’s API; easy to get confused when reading official source.

### How the pieces couple
Module 5’s runtime procedures call `ClineTaskSessionService`; module 12 (event translation) consumes `subscribe` events; module 13 (Provider/MCP) is the SDK’s config layer; module 2’s card `clineSettings` field is passed through.

### Acceptance
- Cline session start / continue-chat / interrupt all work; switching `@clinebot/core` versions only changes the boundary file.

---

<h2 id="ch15">Chapter 15 Module 12: Event translation and message repository</h2>

### Purpose
Translate the event stream the SDK pushes into a card-displayable summary + messages; message read-back does not duplicate storage.

### Implementation shape (aligned with `src/cline-sdk/cline-event-adapter.ts` 816 lines + `cline-message-repository.ts` 364 lines + `cline-session-state.ts` 440 lines + `cline-context-overflow-compaction.ts` 125 lines)

- **Event translation** (`applyClineSessionEvent`): 5 event classes (`agent_event`/`chunk`/`hook`/`status`/`ended`) → summary patch + message mutation:

| SDK event | Card effect |
|---|---|
| `assistant-text-delta` / `content_start` / `content_end` | assistant message |
| `tool-started` / `tool-finished` | tool message (`Tool: xxx` / `Input: ...`) |
| `run-finished` / `done` | `awaiting_review` (`hook`/`error`/`interrupted`) |
| `error` / `run-failed` | `awaiting_review(error)` |
| `ask_followup_question` / `plan_mode_respond` | `awaiting_review(attention)` |

- **Message repository**: SDK-side messages live in `~/.cline` session storage (`host.readMessages`); kanban only stores an in-memory view; after page refresh, `hydrateTaskMessages` hydrates.
- **Context-overflow compaction**: send/start errors → 32 regexes judge over-long → stop the old session → take the latter half of messages (cut to a user message) → first user-message preview embedded in a Cline compaction note → restart and re-run. (Temporary; wait for the SDK to provide pluggable compaction.)
- **Tool-call display** (`cline-tool-call-display.ts`): `read_files(a.ts:1-20, b.ts)` summary; `kanban task create` turned into human “Creating task”.

### Stack detail
TypeScript + SDK event types. No extra deps.

### Definition trade-offs
- kanban does not re-persist Cline sessions—trust the engine’s storage; the message view is pure memory + hydrate.
- Compaction uses “truncate + note” rather than an LLM summary: fast, zero cost; the side effect is dropped context (acceptable for a board; the user can see it).

### Strengths and costs
- ✅ Event-translation rule table is clear and testable (`cline-event-adapter.test.ts`); ✅ messages are not stored twice; ❌ compaction is “drop memory”; long tasks that overflow repeatedly will degrade; ❌ many event kinds; a missed branch = silently dropped messages.

### How the pieces couple
Module 11’s `subscribe` feeds the adapter; module 6 broadcasts chat messages; module 14’s chat panel renders `ClineTaskMessage`; module 5’s `getTaskChatMessages` reads the repository.

### Acceptance
- Agent streaming output appears character by character (chunk merge); tool calls show a summary; over-long context auto-compacts, restarts, and card state stays continuous.

---

<h2 id="ch16">Chapter 16 Module 13: Provider and MCP services</h2>

### Purpose
Model configuration and external-tool injection on the Cline SDK path—**only serves Cline** (PTY-path agents use their own config).

### Implementation shape (aligned with `src/cline-sdk/cline-provider-service.ts` 1294 lines + `cline-mcp-settings-service.ts` 216 lines + `cline-mcp-runtime-service.ts` 879 lines)

- **Provider**: `Llms.getAllProviders()` catalog + model list (local/remote catalog + LiteLLM probe); managed OAuth three classes (`cline`/`oca`/`openai-codex`, tokens prefixed `workos:`) + device auth + manual `apiKey` + env vars; `ClineAccountService` looks up profile/balance/org; `resolveLaunchConfig()` outputs `{providerId, modelId, apiKey, baseUrl, reasoningEffort}`.
- **MCP config**: read/write the **Cline standard file** `~/.cline/data/settings/cline_mcp_settings.json`—the same config as the Cline extension.
- **MCP tool injection**: on every `startTaskSession`, `createToolBundle()`: settings → `InMemoryMcpManager` → `createMcpTools` → inject into `localRuntime.extraTools` + `disableMcpSettingsTools:true` to dedupe.
- **MCP OAuth**: self-implemented `OAuthClientProvider` (persisted to `cline_mcp_oauth_settings.json`); callback goes through the kanban endpoint `/kanban-mcp/mcp-oauth-callback`.

### Stack detail
`@clinebot/core` (`Llms`/`ProviderSettingsManager`) + `@modelcontextprotocol/sdk` + zod.

### Definition trade-offs
- MCP config shares a file with the Cline extension: MCP configured in kanban also works in the Cline extension (ecosystem interop).
- OAuth callback goes through kanban’s own endpoint: MCP remote-server authorization does not depend on a browser.

### Strengths and costs
- ✅ Reuse Cline’s 179 Provider IDs; ✅ tool injection is session-level (each session has an independent tool bundle); ❌ 1294-line provider façade is high complexity; ❌ when an MCP tool shares a name with a built-in agent tool, dedupe needs care.

### How the pieces couple
Module 5’s runtime procedures (provider CRUD/OAuth/MCP settings) → module 11’s start args; module 12’s tool-call display recognizes MCP tool names; desktop (module 20) OAuth relay forwards deep-link callbacks.

### Acceptance
- OAuth-login Anthropic in the browser; account balance displays; add an MCP server; the card agent can use its tools.

---

<h2 id="ch17">Chapter 17 Module 14: Frontend board UI (React + optimistic-concurrency trio)</h2>

### Purpose
Board UI: drag cards, live state, detail panel (chat + diff + terminal), git view, project navigation.

### Implementation shape (aligned with `web-ui/src/App.tsx` 1199 lines + `state/board-state.ts` + `runtime/` trio + `components/` 89 files)

- **Layout**: `ProjectNavigationPanel` (left, draggable/collapsible) + TopBar + main. Main has three forms: Home (`KanbanBoard` or `GitHistoryView` + bottom home terminal), `CardDetailView` (`absolute inset-0` overlay, URL query records task id), Fallback.
- **Board state machine** (`board-state.ts` ~21k lines): `normalizeBoardData` (schema validate/clean), `applyDragResult`, `trashTaskAndGetReadyLinkedTaskIds` (cascade dependencies); `drag-rules.ts` (review column cannot be entered by hand; trash can only be entered, not left).
- **Data-sync trio**:
  1. `use-runtime-state-stream.ts`: WS connection, `useReducer` store, exponential-backoff reconnect (500ms→5s), snapshot full + incremental merge.
  2. `use-workspace-sync.ts`: stream → `normalizeBoardData` → `setBoard`, **revision dedupe**, refetch calibration when the page is visible.
  3. `use-workspace-persistence.ts`: 120ms debounce save + `expectedRevision` optimistic lock; on CONFLICT, update local with `currentRevision` + refetch overlay.
- **Card live state**: `getCardSessionActivity()` parses the summary’s `latestHookActivity` → “Thinking…” / tool tags / “Waiting for review” / final message; status-dot color refreshes live.
- **Component library**: `@radix-ui` primitives + Tailwind v4 + lucide + `@hello-pangea/dnd` + `react-virtuoso` + sonner + motion (not shadcn).

### Stack detail
React 19 + Vite + Tailwind v4 + Radix + tRPC client + `ws`.

### Definition trade-offs
- **No React Router**: state-driven views; URL query only records task id—SPA single-page complexity stays low.
- Optimistic update: drag takes effect immediately, failure rolls back—interaction snappiness first; consistency is backstopped by revision.

### Strengths and costs
- ✅ The optimistic-concurrency trio is textbook local-first data sync; ✅ component layering is clear (`ui`/`detail-panels`/`git-history`/`shared`/`dependencies`); ❌ `App.tsx` 1199 lines is a large single component; ❌ state is all memory + WS; no offline cache.

### How the pieces couple
Module 6’s WS is its input; module 5’s tRPC is its write channel; module 2’s revision is its concurrency anchor; module 15 (terminal) is embedded at the bottom of the detail panel.

### Acceptance
- Drag a card; it moves immediately and two tabs stay consistent; agent messages appear on the card live; disconnect auto-reconnects and does not lose state.

---

<h2 id="ch18">Chapter 18 Module 15: Frontend persistent terminal (xterm dual WS)</h2>

### Purpose
A terminal in the browser that “does not lose content because you switched cards.”

### Implementation shape (aligned with `web-ui/src/terminal/persistent-terminal-manager.ts` ~21k lines + `use-persistent-terminal-session.ts`)

- xterm@6 wrapper + **dual WS**: `/api/terminal/io` (binary stdout/stderr + uplink stdin), `/api/terminal/control` (JSON: `restore`/`state`/`exit`/`error`/`resize`/`output_ack`/`stop`).
- `clientId` (`randomUUID`) identifies the tab: backend shares PTY by `taskId`, isolates socket state by `clientId`.
- **“Persistent” implementation**: DOM nodes “park” in a hidden `#kb-persistent-terminal-parking-root`—switching cards does not destroy; on restore, write the server scroll snapshot back.
- Addons: Fit, WebGL (degrade on context loss), Clipboard, WebLinks, Unicode11; ResizeObserver 50ms debounce report.
- `terminal-prompt-heuristics.ts`: heuristically judge a shell prompt from output text (for `waitForLikelyPrompt`).

### Stack detail
`@xterm/xterm` + `@xterm/addon-fit` etc. + `ws`.

### Definition trade-offs
- Snapshot restore goes through control WS rather than re-pulling full io: control plane and data plane are separated; restore completes in one shot.
- Terminal-controller registry (`terminal-controller-registry`): programmatic input (git actions, shortcuts) prefers local WS `input`/`paste`; only falls back to tRPC when not connected.

### Strengths and costs
- ✅ Switching cards loses zero content; experience approaches an in-IDE terminal; ✅ dual WS separation keeps the protocol clear; ❌ the parking root is hidden DOM; memory/focus management needs care.

### How the pieces couple
Module 7 (server-side mirror + dual WS) is its backend; module 14’s `CardDetailView` embeds `AgentTerminalPanel`; module 19’s commit/PR uses paste mode.

### Acceptance
- Switch away from a card and back; terminal content is intact; vim renders correctly; after WebGL context loss, auto-degrade to canvas.

---

<h2 id="ch19">Chapter 19 Module 16: git UI, comments, and commit/PR</h2>

### Purpose
Without leaving the browser: look at the commit graph, switch branches, review diffs, inline comments, Commit / Open PR.

### Implementation shape (aligned with `web-ui/src/git-actions/build-task-git-action-prompt.ts` + `components/git-history/` 6 components + `use-git-actions.ts`)

- **git history**: `GitHistoryView` + `use-git-history-data` (page size 150, working-copy/commit dual views, per-file lazy-load diffs) + `git-refs-panel` (branch selection).
- **Commit/PR**: `handleCommitTask` → compose prompt (template four-layer fallback: user template → user default → server default → fallback sentence; the only variable `{{base_ref}}`) → `sendTaskSessionInput` (**paste mode into the terminal**) → **the agent itself executes git**. Template semantics: in the worktree, cherry-pick onto `base_ref` and commit / open a PR.
- fetch/pull/push/checkout/discard go through tRPC `workspace.runGitSyncAction`.

### Stack detail
React + tRPC + xterm paste.

### Definition trade-offs
- **Let the agent execute git rather than kanban running it automatically**: merge-conflict handling needs contextual understanding; the agent already can; kanban only sends the instruction.
- Template four-layer fallback: default-template availability first; users can customize (`commitPromptTemplate`/`openPrPromptTemplate` in config).

### Strengths and costs
- ✅ Conflict handling is “outsourced” to the agent; code size is small and robust; ❌ agent-executed git is uncertain (may fail and retry); the review step must backstop; ❌ inline-comment round-trip (click a line, leave a comment, send back to the agent) is high cost (the comment must be injected into the session).

### How the pieces couple
Module 6 (git data) feeds git-history components; module 5 (`workspace.runGitSyncAction`) supplies sync actions; module 15 (terminal paste) executes the prompt; module 2’s `autoReviewMode` decides how automatic.

### Acceptance
- In card detail, look at the diff, click a line to comment back to the agent; click Commit; the agent completes the commit in the worktree and replies with the result.

---

<h2 id="ch20">Chapter 20 Module 17: Home agent and board-injection instructions</h2>

### Purpose
The sidebar-chat agent that “helps me split work into tasks”—it does not write code; it only operates the board.

### Implementation shape (aligned with `src/prompts/append-system-prompt.ts` 316 lines)

Core injected system instructions:
- **Role**: “Kanban board management helper,” **explicitly forbidden from coding/editing files**—implementation requests are always turned into creating tasks.
- **Command-prefix resolution**: generate `npx -y kanban` / `pnpm dlx kanban` / `yarn dlx kanban` / `bun x kanban` or a direct node/tsx call, depending on install method.
- **Full CLI reference**: `task list/create/update/done/delete/link/unlink/start` args and semantics.
- **Link and auto-review pipeline**: review complete → auto-start linked backlog tasks; `auto-commit`/`auto-open-pr` combined implement fully autonomous chain execution.
- **Workspace identity**: when cwd is under `.cline/worktrees/`, force `--project-path` (point back from the worktree to the main workspace).

### Stack detail
Pure prompt engineering + CLI design. No new deps.

### Definition trade-offs
- The home agent is an ordinary agent session + injected instructions; no special runtime—**it reuses modules 8/11 session facilities**.
- “No coding” is written into the prompt, not enforced: a soft constraint the model is expected to obey (low cost; occasional failure is acceptable).

### Strengths and costs
- ✅ One instruction gives the agent board-operator capability; product value is large (“split this requirement into 10 parallel tasks”); ✅ the CLI is a stable protocol between agent and board; ❌ the soft constraint may fail (the agent itches to edit code); ❌ instructions must be maintained as the CLI evolves.

### How the pieces couple
Module 22 (CLI `task` subcommands) is what it calls; module 2 (dependency chain) is how it links cards; the home session shows in the sidebar (module 14).

### Acceptance
- Tell the home agent “split this requirement into tasks and link them”; it correctly creates cards, links cards, and starts the first one.

---

<h2 id="ch21">Chapter 21 Module 18: Four security layers (passcode / Host / CORS / token)</h2>

### Purpose
A local service must not be exposed to strangers—**threat model**: in remote-bind mode, the local runtime has arbitrary code-execution capability (git/shell/filesystem).

### Implementation shape (aligned with `src/security/passcode-manager.ts` 247 lines + `middleware.ts` 167 lines + `runtime-server.ts`)

Four layers of defense in depth:
1. **Bind `127.0.0.1` by default**—other machines cannot reach it.
2. **Host whitelist**—defend against DNS rebinding (when a malicious domain resolves to `127.0.0.1`, a wrong Host header is refused immediately).
3. **CORS precise Origin whitelist**—stop other local web pages from cross-origin reads (a malicious page cannot steal the API with a browser).
4. **Remote-mode passcode**: enabled when `--host` binds non-loopback; 8-digit random code (`randomBytes` rejection sampling, pure memory), `timingSafeEqual` compare, 5 failures per IP lock 30s, after verification issue a 32-byte token (HttpOnly + `SameSite=Strict` Cookie, 24h).

Also `KANBAN_INTERNAL_AUTH_TOKEN` bearer token: used by CLI child processes (`hooks ingest` / `task`) for auth, not through the browser flow. Static assets are allowed through (so React’s `PasscodeGate` can render first); API is hard-intercepted.

### Stack detail
`node:crypto` + cookie header + ws upgrade checks.

### Definition trade-offs
- The passcode only stops “unauthorized browser visitors”; trust the local user (loopback has no passcode).
- Host/CORS dual gates are the foundation of “most attacks are blocked even without a password.”

### Strengths and costs
- ✅ Defense in depth is layered clearly; each layer is independently testable; ✅ 8-digit random code + rate limit is enough against brute force; ❌ remote-mode UX is poor (must type a code every 24h); ❌ static-asset pass-through means React source is reachable (no secrets; acceptable).

### How the pieces couple
Module 4 (server) hangs the gates; module 5 (tRPC) sits behind them; module 7 (terminal WS upgrade) checks the same way; module 22 (CLI) carries the internal token.

### Acceptance
- Default loopback works with no code; after `--host 0.0.0.0`, no-code access is refused; after entering the code, 24h skip-login; 5 wrong tries lock 30s.

---

<h2 id="ch22">Chapter 22 Module 19: CLI and subcommands (`task` / `hooks` / `update`)</h2>

### Purpose
The CLI is the protocol surface between agent and board: a human can use it; the home agent can use it too.

### Implementation shape (aligned with `src/cli.ts` 755 lines + `src/commands/task.ts` 1347 lines + `src/commands/hooks.ts` 820 lines + `src/update/update.ts` 787 lines)

- **commander**: main command `kanban` (start server + open browser, port-reuse probe of an existing instance); subcommands `task` (`list`/`create`/`update`/`trash`/`delete`/`link`/`unlink`/`start`, all JSON output), `hooks` (`ingest`/`notify`/`gemini-hook`/`codex-hook`/`codex-wrapper`), `update`.
- **`task` core pattern**: board changes **do not go tRPC**; this process does `mutateWorkspaceState` (optimistic concurrency) directly on JSON files, then calls `workspace.notifyStateUpdated` to broadcast; only worktree/session/query go through runtime. trash/delete stop the session, delete the worktree, auto-start ready linked tasks.
- **`update`**: look up the latest version on the npm registry; recognize npm/pnpm/yarn/bun/npx; auto-update at startup or shutdown time.
- Lazy load: subcommands take a light path and do not pull up the server stack.

### Stack detail
commander + zod + custom fetch (with internal token).

### Definition trade-offs
- **`task` writes the file directly rather than going through the API**: CLI and server are two entries of the same process; sharing the state layer directly is simpler; the server only broadcasts.
- All-JSON output: the agent can parse (headless-friendly).

### Strengths and costs
- ✅ One write path (file + optimistic concurrency); CLI/web behavior is consistent; ✅ JSON output is naturally scriptable; ❌ CLI and tRPC dual entries must keep semantics consistent (man page in sync); ❌ auto-update may interrupt the user (done at shutdown time).

### How the pieces couple
Module 10 (hooks child process) calls the `hooks` subcommand; module 17 (home-agent instructions) calls the `task` subcommand; module 20 (desktop) spawns the main command.

### Acceptance
- `kanban task create "fix bug"` creates a card and puts it on the board; `kanban task link A B` builds a dependency; `kanban task start A` starts a session.

---

<h2 id="ch23">Chapter 23 Module 20: Desktop Electron</h2>

### Purpose
A “install and use” desktop app: resident process, native menus, bundled Node runtime, crash self-heal, OAuth deep links.

### Implementation shape (aligned with `packages/desktop/src` 2,327 lines of TS)

**A pure process wrapper; it does not import any root-package code**:
- `spawn("kanban", ["--no-open","--port",3484,...])` → HTTP health probe (`<title>Kanban</title>`) → `BrowserWindow` `loadURL`.
- `runtime-orchestrator.ts` (604 lines): `connect` (probe an existing runtime; if none, `startOwnRuntime`) / `restart`/`shutdown`/`dispose`; dual probes (attached 500ms crash detect, recovery 2s auto-reconnect); `powerSaveBlocker` against App Nap; generation counter + terminated lock against races.
- `runtime-child.ts`: spawn + 30s ready poll + `treeKill` graceful shutdown + 4GB V8 heap cap.
- `oauth-relay.ts` (46 lines): `kanban://oauth/callback` deep link → main forwards to `http://127.0.0.1:3484/kanban-mcp/mcp-oauth-callback`, retry 3 times.
- `runtime-child-env.ts`: GUI-process PATH completion for Homebrew/nvm—double-click launch has no shell environment.
- Shim uses `ELECTRON_RUN_AS_NODE=1` to reuse the Electron binary as Node (the user need not install Node).
- Pack: root build → `stage-cli.mjs` copies dist → electron-builder `extraResources` + `asarUnpack` (only unpack `cli`/`node-pty`/`*.node`) → `patch-node-pty.mjs` fixes the asar double-suffix bug.

### Stack detail
Electron + electron-builder + `node-pty`.

### Definition trade-offs
- **Zero root-package import, zero custom IPC, runtime dep only `node-pty`** (a hard constraint of the 5-way split): the desktop is only a shell; all logic is in the CLI.
- Health probe uses page title rather than the port: light, and it verifies that web-ui actually rendered.

### Strengths and costs
- ✅ The shell is extremely thin; CLI evolution is automatically followed by the desktop; ✅ OAuth deep links are a hard capability pure web cannot do; ❌ dual runtime (Electron + Node) is large; ❌ `node-pty` native module needs a patch in Electron (a build pit).

### How the pieces couple
Module 4 (server) is what is spawned; module 13 (MCP OAuth) deep-link callbacks are forwarded by the relay; module 19 (CLI) is the child-process body.

### Acceptance
- Double-click the desktop app; the board starts; `kanban://` deep-link OAuth login succeeds; kill the runtime process; it comes back within 2s.

---

<h2 id="ch24">Chapter 24 “After the user clicks play”: the full workflow</h2>

This is the climax of the book. Follow one card from creation onto the dependency chain:

```
┌─ 1. Create card ─────────────────────────────────────────┐
│ User clicks "New task" on the board → fill prompt → pick   │
│ agent/column → frontend optimistic-updates the card →     │
│ 120ms debounce saveWorkspaceState (expectedRevision lock) │
│ → write board.json                                         │
│ → if autoReview is on, also create a turn checkpoint ref  │
└──────────────────────────┬───────────────────────────────┘
                           ▼
┌─ 2. Play ─────────────────────────────────────────────────┐
│ Click ▶ → tRPC runtime.startTaskSession                    │
│  ├─ a) Ensure worktree (if missing):                        │
│  │    git worktree add --detach <baseCommit>              │
│  │    → symlink gitignored paths (node_modules etc.)     │
│  │    → .git/info/exclude mark kanban-managed block        │
│  │    → whole path under .git/kanban-task-worktree-setup.lock │
│  ├─ b) Choose execution path: agentId == cline?           │
│  │    ├─ Cline SDK: ClineCore.start (in-process session)  │
│  │    │    → Provider/OAuth resolve → MCP tool injection     │
│  │    └─ Other agent: PTY spawn (node-pty child)         │
│  │         → generate hook config (--settings / -c hooks) │
│  │         → workspace-trust auto-confirm (if prompted)  │
│  └─ Card → in_progress, session → running                 │
└──────────────────────────┬───────────────────────────────┘
                           ▼
┌─ 3. Execute and live state (loop) ────────────────────────┐
│ Agent works → every event fires:                         │
│  ├─ hook path: hook command → kanban hooks notify         │
│  │    → stdin JSON → tRPC hooks.ingest                    │
│  │    → state-machine transition + capture turn checkpoint │
│  │    → runtime-state-hub broadcast task_sessions_updated  │
│  └─ SDK path: subscribe events → cline-event-adapter     │
│       → summary patch + message mutation → same broadcast    │
│ Browser: WS received → useReducer → card activity updates  │
│  ("Using read_files" / "Thinking..." / final message)       │
│  Terminal: PTY bytes → protocol filter → server xterm      │
│        mirror → io WS → browser xterm (back-pressure)    │
│  If agent asks permission/a question → awaiting_review(attention) │
│  If context over-long → truncate compact → restart session│
└──────────────────────────┬───────────────────────────────┘
                           ▼
┌─ 4. Done and review ──────────────────────────────────────┐
│ Agent done → run-finished/hook → awaiting_review        │
│  → card enters review (latestHookActivity=finalMessage)    │
│  → notification (readyForReviewNotificationsEnabled)      │
│ User clicks card → detail: chat + diff (vs baseRef) + term│
│  → click a line to comment → send back to agent → continue │
│ Satisfied → click Commit / Open PR:                       │
│  → compose prompt ({{base_ref}} template) → paste terminal │
│  → agent itself cherry-pick/commit/open PR → reply result │
│ auto-commit on → agent auto-commits when done → trash     │
└──────────────────────────┬───────────────────────────────┘
                           ▼
┌─ 5. Close and dependency chain ───────────────────────────────┐
│ Drag card to trash (or auto-complete) →                  │
│  ├─ stop session → capture diff patch → git worktree remove │
│  ├─ trashTaskAndGetReadyLinkedTaskIds:                   │
│  │    find backlog deps waiting on this card → auto start  │
│  └─ Fully autonomous: card A commit → trash → card B auto- │
│      start → commit → trash → card C … the whole chain     │
│      unattended                                            │
└───────────────────────────────────────────────────────────┘
```

### Key FAQ

**Q1: Why does the worktree use detached HEAD instead of creating a branch?**
Branch semantics (based on which base, whether to push, where the PR comes from) are only decided at “commit / open PR.” Creating a branch early is deciding for the user. A detached worktree is only an isolated execution space; at commit time the template cherry-picks onto `base_ref` and then creates a branch.

**Q2: What is the difference between hooks reverse-injection and polling terminal output?**
Polling is “guess state from garbled output” (each TUI’s format differs and will change); hooks are the agent’s official event exit—“this tool finished” is a fact. But Codex hooks are unreliable, so it instead uses a wrapper + polling rollout jsonl (every 200ms). There is no silver bullet; each agent must be waited on individually.

**Q3: Why does Cline go through the SDK while other agents go through PTY?**
The SDK (`@clinebot/core`) gives the deepest integration: Provider catalog, OAuth, MCP tool injection, account balance—but it only serves Cline. Other agents can only run as black-box processes, fishing events via hooks. The cost of two paths is that `sendTaskSessionInput` must try Cline first, then the terminal.

**Q4: Is one card = one session? How does the session recover after restart?**
Yes. Cline path: `sessionId` is prefixed with `taskId`; after restart, recover by prefix from `host.list()`; messages hydrate from SDK persistent storage. PTY path: session-manager `hydrateFromRecord` restores summary from disk; the PTY process itself is not recoverable (restart means re-run, using `restartRequest`).

**Q5: Won’t multiple tabs / viewers sharing one terminal get mixed up?**
No. The backend shares PTY by `taskId`, isolates socket state by `clientId`; each viewer takes its own snapshot via control WS `restore`; back-pressure ensures a slow viewer does not drag down the shared PTY.

**Q6: What does revision optimistic concurrency actually prevent?**
It prevents “two tabs editing the board overwrite each other.” Each save carries `expectedRevision`; the server compares; mismatch throws CONFLICT + `currentRevision`; the frontend updates local then refetch overlays—the last writer’s data is not lost; intermediate state takes the server as truth.

---

<h2 id="ch25">Chapter 25 Testing strategy and engineering (dogfood / CI/CD)</h2>

### Test matrix (aligned with kanban’s `test/` directory)

| Layer | Content | Strategy |
|---|---|---|
| unit (`web-ui/tests` + tests inside `src`) | state machine, contract, drag rules, prompt templates | pure functions first, zero mocks |
| runtime (`test/runtime/` 23) | api-validation, hooks parse, task-worktree, cline-sdk, terminal | temp HOME + git sandbox |
| integration (`test/integration/` 6) | task-worktree, workspace-state, shutdown-coordinator, runtime-state-stream, cli-compat | `describe.sequential` + real git ops |

Key integration tests:
- **task-worktree**: verify git worktree create / symlink mirror / cleanup.
- **workspace-state**: verify persistence and CONFLICT.
- **shutdown-coordinator**: after SIGTERM, `in_progress`/`review` cards are not lost.

### dogfood (aligned with `scripts/dogfood.mjs` 470 lines)

- First build, then run yourself with the production CLI—**eat your own cooking**.
- `--skip-shutdown-cleanup`: with multiple instances, the lockfile-elected “cleanup owner” does cleanup, preventing duplicates.
- Strip the repo’s `node_modules/.bin` (avoid shadowing global agent CLIs); 10s SIGKILL backstop.
- The repo ships `.claude`/`.cline`/`.codex`/`.factory` config: **the same repo is used as a real scene by 4 agents at once**.

### CI/CD (aligned with `.github/`)

- `ci.yml`: push/PR → `main` trigger.
- `test.yml`: 3-platform matrix (ubuntu20/22 + macos22) build + lint + typecheck + tests.
- `publish.yml`: manual `workflow_dispatch` with a specified tag → verify `vX.Y.Z` matches `package.json` → `prepublishOnly` → **OIDC trusted publishing npm publish** → auto-extract changelog, create GitHub Release → Slack.
- Release SOP (`RELEASE_WORKFLOW.md` + `.clinerules/workflows/release.md`): maintainer edits CHANGELOG/version/tag → `gh workflow run publish.yml`.

### Engineering-discipline checklist

1. Every commit passes biome + typecheck + test:fast (husky).
2. GritQL bans `console.*` / `process.exit` (except CLI entry) / `process.env` destructure.
3. No `any`, ban inline import.
4. Prefer PATH detection over launching an agent from an interactive shell.
5. New agent support = adapter + hooks parse + integration tests, the trio.

### Acceptance

- All-platform `npm run check` green; `npm run dogfood` can run itself; the release flow produces an npm package + GitHub Release in one click.
