# Kanban source analysis: a board that lets hundreds of coding agents work in parallel

> **Subject**: kanban (`github.com/cline/kanban`), Apache-2.0, this analysis is based on **v0.1.70** (checked out 2026-07-12; the npm package name is also `kanban`).
> **Code size**: 561 files / 9.1MB. `src/` 92 TS files, 30,402 lines (local runtime); `web-ui/src` 240 ts/tsx files, 58,039 lines (React frontend; including config and styles, the whole `web-ui/` tree is 269 files); `packages/desktop/` Electron shell, 11 TS files, 2,327 lines; `test/` 59 test files (143 in the whole repo). Runtime dependency `@clinebot/core ^0.0.38` (the npm-published Cline engine).
> **One-line position**: Cline’s own “IDE replacement”—**one board, each card = an independent git worktree + an independent terminal session, so many coding agents work in parallel without colliding**. It does not write the agent main loop. It is an “agent orchestration layer”: it loads 7 CLI agents (Claude Code / Codex / Cline / Droid / Kiro, plus the Cline native SDK) into cards, uses four board columns (backlog→in_progress→review→trash) to run the process, uses card links as a dependency chain, and uses hooks to inject live status back into the board.
> **Audience**: readers who have already read at least one analysis in this series (especially the Cline source analysis). This lecture uses the same frame and cites `file:line` so you can check the source.

---

## Contents

1. [Project overview: an IDE replacement—a multi-agent parallel board](#ch1)
2. [Panorama: browser control plane + local runtime as source of truth + two agent paths](#ch2)
3. [Startup: the CLI brings up a local server; the browser is the only UI](#ch3)
4. [Board data model: cards, four columns, dependency chains, session state machine](#ch4)
5. [git worktree isolation: one worktree per card + symlink-shared deps + checkpoint](#ch5)
6. [Terminal subsystem: node-pty + dual WebSocket + backpressure and protocol filtering](#ch6)
7. [Multi-agent adapter layer: 7 CLI agents + reverse-injection hooks](#ch7)
8. [Cline SDK integration: ClineCore multi-session host + event translation + context compaction](#ch8)
9. [Provider and MCP: model config and tool injection](#ch9)
10. [tRPC API layer and auth: four layers of defense in depth](#ch10)
11. [Frontend board UI: React state-driven + persistent terminals + optimistic concurrency](#ch11)
12. [Desktop Electron: a pure process wrapper + OAuth relay](#ch12)
13. [Engineering and release: dogfood, CI/CD, GritQL static checks](#ch13)
14. [Cross-cut: vs the Cline engine / Claude Code / task-management products](#ch14)
15. [Summary: three most distinctive designs and their trade-offs](#ch15)

---

<h2 id="ch1">Chapter 1 · Project overview: an IDE replacement—a multi-agent parallel board</h2>

**What is kanban?** The first line of the README: *A replacement for your IDE better suited for running many agents in parallel and reviewing diffs*. It solves a pain the Cline team actually had: **once you have tens or hundreds of coding agents running, the IDE cannot hold them**—every agent is editing the same tree, stepping on each other’s diffs, and you cannot watch all of their output at once.

kanban’s answer: **turn “an agent session” into a draggable card**.

- Each card has its own **independent git worktree** (`git worktree add --detach`). The agent works in an isolated tree; conflicts are naturally zero.
- Each card has its own **independent terminal** (a node-pty process). The agent’s TUI renders directly in the browser.
- **Four board columns** run the process: backlog (todo) → in_progress (running) → review (awaiting review) → trash (done / discarded).
- **Card links** are the dependency chain: `⌘`+click links two cards. When the upstream card finishes and goes to trash, the downstream card starts automatically. Combined with auto-commit, that is a fully autonomous pipeline.
- **Hooks reverse-inject status**: every utterance, every tool call, callbacks through a hook into kanban’s own CLI and pushes state onto the card in real time—you can scan hundreds of cards without opening any terminal.

It is part of the Cline ecology (`github.com/cline/` org). The core dependency is `@clinebot/core` (the npm-published Cline engine)—but it **does not lock you to Cline**: by default it detects which CLI agents are installed locally (claude / codex / cline / droid / kiro) and takes all of them.

> [!WARNING]
> The README itself says this is a **Research Preview**, depending on experimental CLI-agent features (bypass permissions, runtime hooks)—the autonomous launch flags in `agent-catalog.ts` are blunt: `--permission-mode auto`, `--dangerously-bypass-approvals-and-sandbox`, `--auto-approve-all`.

### Digitized project shape

| Dimension | Number | Note |
|---|---|---|
| Version | v0.1.70 (2026-07) | From v0.1.4 to v0.1.70 took about 5 months (CHANGELOG 565 lines) |
| Backend | src/ 92 files / 30,402 lines | Local runtime: server, PTY, git, tRPC, cline-sdk |
| Frontend | web-ui/src 240 ts/tsx files / 58,039 lines (web-ui/ whole tree 269 files) | React + Vite + Tailwind v4, single page, no router |
| Desktop | packages/desktop/src 2,327 lines TS | Electron shell, a pure process wrapper |
| Dependency | @clinebot/core ^0.0.38 | Cline engine npm release (stateful session model) |
| Supported agents | 7 kinds | claude/codex/cline/opencode/droid/kiro/gemini (5 enabled) |
| Storage | JSON files + git refs | `~/.cline/kanban/` (board state) + `~/.cline/worktrees/` (worktrees) + git refs (checkpoint) |
| License | Apache-2.0 | Commercially friendly |

### How it relates to Cline / Claude Code

- **vs Cline**: Cline (the previous analysis) is an “engine + UI” monorepo whose core is a single-agent `AgentRuntime`. kanban directly imports `@clinebot/core` and runs the Cline engine inside a card. **Cline is the single-agent engine; kanban is the scheduling board for N agents**—engine vs orchestration layer.
- **vs Claude Code**: Claude Code is a single-session TUI (Ink). kanban runs Claude Code as a **subprocess** (`--settings` injects hooks), one per task. The README is blunt: the IDE (where Claude Code lives) no longer holds parallel agents.

---

<h2 id="ch2">Chapter 2 · Panorama: browser control plane + local runtime as source of truth + two agent paths</h2>

`docs/architecture.md` gives three core ideas. They are the key to the whole architecture:

1. **The browser is only the control plane**: render, send commands, receive live updates. It stores no state and runs no business logic.
2. **The local runtime is the only source of truth**: projects / worktrees / sessions / git / streaming state all live in the local Node process.
3. **Two agent execution paths**: most agents are **PTY processes** (the CLI binary runs directly inside node-pty); Cline takes a **native SDK session** (`ClineCore` in-process host, session-oriented).

### Panorama layers

```
┌─────────────────────────────────────────────────────────┐
│  Browser (control plane)  React SPA: board / xterm / git  │
│  tRPC httpBatchLink ── WS state stream ── WS terminal io ── WS control │
└──────────────┬──────────────────────────────────────────┘
               │ localhost HTTP + WebSocket
┌──────────────▼──────────────────────────────────────────┐
│  Local runtime (only source of truth)  src/ ~30k lines    │
│  ┌──────────┬─────────────┬──────────────┬─────────────┐ │
│  │ tRPC     │ state-stream│ terminal mgr │ git layer    │ │
│  │ 4 router │ runtime-    │ session-     │ task-        │ │
│  │ 60+ proc │ state-hub   │ manager      │ worktree     │ │
│  └──────────┴──────┬──────┴──────┬───────┴──────┬──────┘ │
│            ┌───────▼──────┐ ┌────▼─────┐ ┌───────▼─────┐  │
│            │ Cline SDK    │ │ PTY proc │ │ 7 Agent     │  │
│            │ ClineCore    │ │ node-pty │ │ CLI adapters│  │
│            │ (in-process) │ │          │ │ (subprocess)│  │
│            └──────────────┘ └──────────┘ └─────────────┘  │
└──────────────────────────────────────────────────────────┘
```

### The two execution paths in detail

**Path A: PTY subprocess** (claude / codex / droid / kiro / opencode / gemini)
`session-manager.ts` spawns a node-pty process per card; the agent TUI runs inside it. Agent hook config (claude’s `--settings`, codex’s `-c hooks.*`, kiro’s `~/.kiro/agents/kanban.json`, etc.) callbacks every event as `kanban hooks notify`—this is “reverse injection”: **while the agent is working, it actively calls kanban’s CLI**.

**Path B: Cline native SDK** (built-in cline)
The `cline-sdk/` layer imports `@clinebot/core`’s `ClineCore` class **in the same process**, `ClineCore.create({backendMode:"auto"})` starts a multi-session host, and kanban wraps it with `ClineSessionRuntime` to map “task ↔ session”. Events are subscribed directly; no hooks needed.

### Why two paths?

The Cline SDK gives the deepest integration (provider catalog, OAuth, MCP tool injection, account balance) but **can only run Cline**. The PTY path can run anything, but can only scrape status via hooks. kanban’s strategy: **Cline by default (deep integration); every other agent automatically degrades to PTY + hooks**.

---

<h2 id="ch3">Chapter 3 · Startup: the CLI brings up a local server; the browser is the only UI</h2>

### Entry (`src/cli.ts`, 755 lines)

`src/index.ts` is only a re-export. The real entry `cli.ts` uses **commander**:

- Main command `kanban`: start the local server and auto-open a browser. `shouldAutoOpenBrowserTabForInvocation` decides which invocation styles should open a browser (direct npx opens; being called by another process does not).
- **Port reuse**: `tryOpenExistingServer` probes whether the port is already occupied by kanban—if so, reuse the existing instance, only open the browser, do not start a second server.
- Options: `--host/--port/--no-open/--skip-shutdown-cleanup/--https/--cert/--key/--update/--no-passcode`.
- Subcommands (`src/commands/`): `task` (list/create/update/trash/delete/link/unlink/start), `hooks` (ingest/notify/gemini-hook/codex-hook/codex-wrapper), `update`; `mcp` is deprecated.
- **Lazy load**: the server stack (tRPC, terminal, git) is all dynamically imported. Running a `task` subcommand takes the light path and starts in a second.

### HTTP server (`src/server/runtime-server.ts`, 519 lines)

No express—**native `node:http`/`node:https`**. Request path:

```
middleware (Host allowlist + CORS gate)
  → passcode gate (remote mode only)
  → MCP OAuth callback (/kanban-mcp/mcp-oauth-callback)
  → tRPC handler (createHTTPHandler, basePath /api/trpc)
  → static assets (assets.ts: multi-path probe of web-ui artifacts, path-traversal guard, SPA fallback)
```

**Live push uses WebSocket (the `ws` library), not SSE**: `/api/runtime/ws` is fanout by `runtime-state-hub.ts` (604 lines)—first connection gets a full `snapshot`, then incremental `workspace_state_updated` / `task_sessions_updated` (150ms batch merge) / `task_chat_message` / `task_ready_for_review` and 11 message kinds in all. The terminal uses two independent WS channels, io / control (see Chapter 6).

The browser is opened with the `open` package (`browser.ts`).

---

<h2 id="ch4">Chapter 4 · Board data model: cards, four columns, dependency chains, session state machine</h2>

### API contract (`src/core/api-contract.ts`, 1277 lines)

The entire API contract is defined in **zod**, shared by frontend, backend, and CLI (web-ui imports the same copy via alias `@runtime-trpc`). This is the most stable foundation in the project: **one z type defines the whole API**. Server validation, CLI construction, and frontend rendering all read the same schema.

### Card model (`RuntimeBoardCard`)

| Field | Note |
|---|---|
| `id` | 5-character short id (`task-id.ts`, readable and memorable) |
| `title` | Derived from the prompt (`task-title.ts` truncates to 80 characters) |
| `prompt` | Task description sent to the agent |
| `startInPlanMode` | Whether to start in plan mode |
| `autoReviewEnabled` / `autoReviewMode` | Auto review (commit / pr) |
| `images` | Attached screenshots |
| `agentId` | Which agent (claude/codex/cline/...) |
| `clineSettings` | Cline-session-specific settings |
| `baseRef` | Which git ref the worktree is based on |
| `createdAt` / `updatedAt` | Timestamps |

### Four columns + dependency chain

The board is fixed at **4 columns**: `backlog → in_progress → review → trash` (“done” is compat-mapped to trash).

`dependencies` (fromTaskId→toTaskId) is the automation core:
- **Only backlog tasks can be linked** (as a dependency or a waiter).
- **When a review card is trashed, its backlog dependents are unlocked and auto-started**—that is the “dependency-chain auto-pipeline”: task A finishes and goes to trash → task B starts automatically.

### Session state machine (independent of column moves)

`session-state-machine.ts` (78 lines) is a pure function `reduceSessionTransition`:

```
idle → running → awaiting_review (reviewReason: attention|exit|error|interrupted|hook)
                → interrupted → running
```

- `hook.to_review`: running → awaiting_review (the agent requests human intervention)
- `hook.to_in_progress` / `agent.prompt-ready`: back to running
- `process.exit`: → interrupted or awaiting_review

The status-dot colors on the card UI and the `Waiting for review` label all come from this state machine.

### Persistence and concurrency (`src/state/workspace-state.ts`, 745 lines)

- Storage: `~/.cline/kanban/workspaces/<id>/{board.json, sessions.json, meta.json}`.
- **revision optimistic concurrency**: `saveWorkspaceState` takes `expectedRevision`; meta.json increments; conflict throws `WorkspaceStateConflictError` and surfaces `currentRevision`—the frontend syncs and retries.
- All mutations (`task-board-mutations.ts`, 657 lines) are **purely functional**: add/move/update/trash/delete/link each return a new board; the caller persists. Board-state change is the same idea as React’s immutable updates.

---

<h2 id="ch5">Chapter 5 · git worktree isolation: one worktree per card + symlink-shared deps + checkpoint</h2>

This is kanban’s most load-bearing engineering design—**using git’s own mechanism as resource isolation**.

### One worktree per card (`src/workspace/task-worktree.ts`, 695 lines)

```
git worktree add --detach <baseCommit>
path: ~/.cline/worktrees/<taskId>/<workspaceLabel>
```

Key decisions:
- **Detached HEAD, no branch**. Branches are created dynamically only at Commit / Open PR (the commit template cherry-picks back into the base worktree).
- **Once a worktree exists, it is authoritative**; it is not rebuilt because the base advanced (to avoid accidentally deleting task progress).
- **On trash, capture a diff patch** into `trashed-task-patches/<taskId>.<commit>.patch`, then `git worktree remove --force`; on resume, `git apply` restores—cards can “die and come back”.
- The whole create path is serialized under `.git/kanban-task-worktree-setup.lock`.

### Symlink-shared dependencies (`syncIgnoredPathsIntoWorktree`, L358)

The biggest cost of many worktrees is that each wants `npm install`. kanban’s answer:

1. `git ls-files --others --ignored --exclude-per-directory=.gitignore --directory` lists every gitignored path in the main repo (`node_modules`, `dist`, etc.).
2. For each path, **symlink in place (main repo → worktree)**—when the agent reads `node_modules` in the worktree, it actually reads the main repo’s copy.
3. Simultaneously write a `# kanban-managed-symlinked-ignored-paths` block into `.git/info/exclude` so the symlink itself is never committed.
4. Failures are silently skipped (`mirrorIgnoredPath` catches every error and returns `"skipped"`); they do not block worktree creation.
5. `getUniquePaths` depth-sorts and keeps only the shallowest root (if you have `node_modules`, do not also create `node_modules/foo/bar`).

**Turbopack exception** (`task-worktree-turbopack.ts`, 167 lines): Turbopack **follows the symlink to the real path**. If `node_modules` is a symlink, incremental cache breaks and many worktrees contend for the same `node_modules/.cache`—so when a Turbopack project is detected, those `node_modules` are **not symlinked** (added to the skip set).

### checkpoint (`src/workspace/turn-checkpoints.ts`, 92 lines)

Unrelated to the Cline engine’s checkpoint; this is kanban’s own: **after each message round ends**, in the worktree, using a **temporary index** (`GIT_INDEX_FILE` points at a /tmp tempfile → `read-tree HEAD` → `add -A` → `write-tree` → `commit-tree`) create a real commit (author `kanban-checkpoint`), then `update-ref refs/kanban/checkpoints/<base64(taskId)>/turn/<N>`.

Key: **does not pollute the working tree, does not touch branches, pure refs**. Rollback = switch to the corresponding ref. Call site is a best-effort capture at the end of each session round in `runtime-api.ts`.

### git UI data (`git-history.ts`, 478 lines)

The commit graph in the browser: `git log --topo-order --date-order`, fields delimited by `\x1f`/`\x1e` (hash/author/date/subject/parentHashes), with `rev-list --count` for paging; branches/upstream use `for-each-ref` + `rev-list --not` to compute each commit’s relation (selected/upstream/shared) for commit-graph coloring.

### Merge strategy (`git-sync.ts`, 385 lines)

**kanban does not auto-commit/merge**—`runGitSyncAction` only supports fetch / pull --ff-only / push / checkout / discard. auto-commit / auto-open-pr is implemented by **injecting a prompt so the agent itself git add/commit/push/opens a PR** (see Chapter 10, append-system-prompt). This is a clever trade-off: git-conflict handling is left to the agent (it understands the context); kanban only owns the process.

### File locks (`src/fs/locked-file-system.ts`, 165 lines)

Based on proper-lockfile: `withLocks` **sorts lockfile paths then takes them one by one** (deadlock prevention); `writeTextFileAtomic` writes a tempfile + `rename` for atomic replace. Worktree creation, patch writes, and info/exclude updates all go through it.

---

<h2 id="ch6">Chapter 6 · Terminal subsystem: node-pty + dual WebSocket + backpressure and protocol filtering</h2>

### PTY (`src/terminal/pty-session.ts`, 161 lines)

Uses **node-pty**: `PtySession.spawn()` creates a real PTY process; the agent CLI runs directly in it. `onData` hands output byte chunks to the manager; `write()` writes browser input back; on `stop()`, `kill()` then `process.kill(-pid, SIGTERM)` kills **the whole process group** (POSIX side).

Data path: `PTY → onData → protocol filter → TerminalStateMirror (headless xterm mirror) → broadcast to all WS listeners → browser xterm render`.

### Dual WebSocket (`src/terminal/ws-server.ts`, 591 lines)

Two `WebSocketServer({noServer:true})`, demuxed on `server.on("upgrade")` by pathname; `clientId` query distinguishes tabs:

- **io stream**: the server `ws.send(chunk)`s raw Buffer bytes (the browser xterm is fed directly); a browser `message` is keyboard bytes passthrough `writeInput`.
- **control stream**: JSON messages `{type:"state", summary}` / `{type:"exit", code}` / `{type:"restore", snapshot, cols, rows}`; the browser sends `resize` / `stop` / `output_ack` / `restore_complete`.

**Multi-tab sharing**: many viewers of the same task share one PTY; `TerminalStateMirror` (xterm headless + SerializeAddon) gives a new viewer **offline scroll-buffer snapshot restore**.

**Backpressure (VS Code-style)**: track each viewer’s `bufferedAmount` + unacked bytes (high-water 100KB). If any viewer falls behind, `pauseOutput` pauses the shared PTY; only when all catch up does `resumeOutput`—so a slow browser cannot stall the agent process.

### Protocol filter (`terminal-protocol-filter.ts`)

Byte-by-byte parse of ESC/OSC/CSI sequences: **intercept `OSC 10;?`/`11;?`** (when the browser is not yet connected, the server answers fg/bg color), **drop Droid’s DA (Device Attributes) query** (`CSI c`)—so the TUI does not wait forever for a browser that is not yet connected; half-cut sequences across chunks are held in `pendingChunk`. ANSI strip uses a homemade `stripAnsi()` (`output-utils.ts`, no third-party library).

### workspace-trust auto-confirm (`claude-workspace-trust.ts` / `codex-workspace-trust.ts`)

Detect trust-prompt text in PTY output (claude regex "trust this folder"; codex ordered tokens "do you trust the contents of this directory"), rolling buffer 16KB, delay 100ms, **auto-send Enter to confirm**—but **only for worktrees kanban itself created**. During startup it also intercepts the TUI’s OSC 10/11 color queries and synthesizes replies.

### Process management (`session-manager.ts`, 1040 lines)

`TerminalSessionManager` uses `Map<taskId, SessionEntry>` for concurrent sessions; crash recovery `shouldAutoRestart` (at most 3 times in a 5-second window) → `scheduleAutoRestart` reruns from the saved `restartRequest`; `markInterruptedAndStopAll` stops everything; `hydrateFromRecord` restores the summary from disk after restart. Process-tree cleanup is dual insurance: PTY kills the process group + `server/process-termination.ts` tree-kill (Windows SIGTERM tree kill).

---

<h2 id="ch7">Chapter 7 · Multi-agent adapter layer: 7 CLI agents + reverse-injection hooks</h2>

### Command discovery (`command-discovery.ts`, 78 lines)

**Does not `which`** (avoids the side effects of spawning a shell/conda/nvm). Directly `accessSync(X_OK)` scans `process.env.PATH` (Windows uses PATHEXT). Detects 7 binaries: `claude/codex/cline/opencode/droid/kiro-cli/gemini`; `RUNTIME_LAUNCH_SUPPORTED_AGENT_IDS` only enables **cline/claude/codex/droid/kiro** (opencode/gemini are commented out). **Default cline** (always installed:true).

### 7 adapters (`agent-session-adapters.ts`, 1451 lines)

`prepareAgentLaunch()` dispatches by agentId:

| Agent | Injection | Mechanism |
|---|---|---|
| **claude** | `--settings <hooks dir>/settings.json` | Stop/SubagentStop/PreToolUse/PermissionRequest/PostToolUse/Notification/UserPromptSubmit—7 hook commands all point at `kanban hooks notify --event ...`; autonomous adds `--permission-mode auto` |
| **codex** | `-c features.hooks=true` + `-c hooks.Stop=...` and 5 config overrides | Compute a hook-config **sha256 trust hash** into `hooks.state` so Codex does not pop a trust confirm; output parsing relies on the TUI session log |
| **gemini** | `GEMINI_CLI_SYSTEM_SETTINGS_PATH` | Points at a generated settings.json (BeforeTool/AfterTool/BeforeAgent/AfterAgent/Notification) |
| **opencode** | dynamically generate a **JS plugin** | Listen `message.updated/session.status/tool.execute.before/permission.ask`, **base64-encode** the payload then `$` shell-call `kanban hooks notify --metadata-base64`; parse preferred model from config/auth/model state files to prevent provider drift |
| **droid** | settings.json (autonomyMode: spec/auto-high/normal) | PreToolUse matcher distinguishes active tools |
| **kiro** | write `~/.kiro/agents/kanban.json` (tools:["*"] + hooks) | start with `--agent kanban` |
| **cline** | generate bash/PowerShell scripts under `--hooks-dir` | read JSON from stdin, grep to decide user_attention, then turn `to_review` |

### Hooks reverse injection (`src/commands/hooks.ts`, 820 lines)

The core mechanism in one sentence: **while the agent is working, it actively calls kanban’s own CLI to report status**.

```
Agent event → hook command (kanban hooks notify --event ...)
  → subprocess 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** (waits for ack); `notify` is a `spawnBackgroundKanban()` detached background process (best-effort, does not block the agent).
- **Codex special case**: `hooks codex-wrapper` wraps the real codex, sets `CODEX_TUI_RECORD_SESSION=1` so it writes a session log + rollout jsonl, then `startCodexSessionWatcher()` polls file increments every 200ms; `parseCodexEventLine` maps `task_started/agent_message/task_complete/exec_command_begin` into activity events (dedup by fingerprint).
- **Status attribution**: `KANBAN_HOOK_TASK_ID/WS_ID` env vars (`hook-runtime-context.ts`) pass task ownership between adapter ↔ hook subprocess.

**Why not poll terminal output?** Because each agent’s TUI output format is completely unreliable; a hook is the agent’s officially supported “event exit,” with clean semantics (“this tool finished,” “this message is for the user”).

---

<h2 id="ch8">Chapter 8 · Cline SDK integration: ClineCore multi-session host + event translation + context compaction</h2>

`src/cline-sdk/` 17 files / 7,094 lines—kanban’s deep-integration layer with the Cline engine. This is the chapter with the most contrast value against the previous Cline analysis.

### Not import SessionRuntime, but ClineCore (`sdk-runtime-boundary.ts`, 126 lines)

The only import boundary; re-exports `ClineCore.create({backendMode:"auto"})`. **The load-bearing fact**: npm-published `@clinebot/core@0.0.38` exposes a **stateful multi-session host**—`ClineCore` has a `sessions` Map (start/send/stop/abort/delete/get/list/update/readMessages/subscribe). Sessions are first-class; `send()` incrementally continues the chat; `subscribe()` pushes streaming events; `SessionHistoryRecord` lands on disk.

This is completely different from the **stateless AgentRuntime** on Cline’s main branch (`new AgentRuntime()` + `run("")` re-entry)—**the npm-published `@clinebot/core` and cline’s current main-branch API are evolving in different directions** (comments on the 0.0.36 compatibility branch also confirm the API layer is still moving fast). kanban depends on a “stateful session” model because it naturally needs “one task = one continuable session.”

### Task ↔ session mapping (`cline-session-runtime.ts`, 580 lines)

`ClineSessionRuntime` interface + `InMemoryClineSessionRuntime` implementation:
- Bidirectional Map bind: `sessionIdByTaskId / taskIdBySessionId`.
- `createSessionId(taskId)` = `"<sanitized taskId>-<ts>-<rand>"`; after restart, recover by prefix from `host.list()`.
- `send()` takes `delivery: "queue"|"steer"`—queue queues; steer interrupts the current turn.

### Event translation (`cline-event-adapter.ts`, 816 lines)

`applyClineSessionEvent(input)` parses events pushed by `subscribe()` into 5 classes (agent_event / chunk / hook / status / ended). Each `AgentEvent` maps to a 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) |
| `assistant-reasoning-delta` | reasoning message |
| `ask_followup_question / plan_mode_respond` | needs human attention → awaiting_review(attention) |

### Message repository (`cline-message-repository.ts`, 364 lines)

SDK-side messages live in session storage under `~/.cline` (`host.readMessages(sessionId)` reads them back); kanban only keeps an in-memory `ClineTaskMessage` view. After a page refresh, `hydrateTaskMessages` hydrates from the SDK’s persisted session. **kanban does not re-persist Cline sessions**—it trusts the engine’s own storage.

### Context-overflow compaction (`cline-context-overflow-compaction.ts`, 125 lines)

After send/start errors, use **32 regexes** to decide “prompt/context too long,” then stop the old session; `compactPersistedMessagesForContextOverflow` takes the latter half of messages (cut at a user-message boundary), embeds a preview of the first user message into a Cline-official-style compaction note (`[Previous conversation history was removed...]`), `restartTaskSession(initialMessages=compacted)` reruns. Comments state this is a **temporary scheme**, waiting for a pluggable compaction API from the SDK.

### Tool-call display (`cline-tool-call-display.ts`, 352 lines)

`getClineToolCallDisplay(toolName, input)` compresses a tool call into a one-line summary (`read_files(a.ts:1-20, b.ts)`), specialized for read_files/run_commands/search_codebase/editor/fetch_web_content/skills/ask_question; `resolveKanbanCommandDisplay` recognizes `kanban task <create|link|...>` and turns it into human speech (“Creating task”)—so the card can show “the agent is creating a task.”

---

<h2 id="ch9">Chapter 9 · Provider and MCP: model config and tool injection</h2>

### Provider (`cline-provider-service.ts`, 1294 lines)

UI-facing provider facade `createClineProviderService()`:
- **Provider catalog**: `Llms.getAllProviders()` (the full Cline ecology).
- **Model list**: local + remote catalog + LiteLLM `/models` probe.
- **Login methods**: three managed OAuth kinds (cline / oca=Oracle Code Assist / openai-codex, tokens prefixed `workos:`), device auth, manual apiKey, env vars `CLINE_API_KEY`/`OCA_API_KEY`.
- **Account**: `ClineAccountService` looks up profile/balance/org; OAuth expiry auto-refresh.
- `resolveLaunchConfig()` assembles `{providerId, modelId, apiKey, baseUrl, reasoningEffort}` and injects it into start.

Default `cline` provider, default model `anthropic/claude-sonnet-4.6`.

### MCP (`cline-mcp-settings-service.ts` 216 lines + `cline-mcp-runtime-service.ts` 879 lines)

- settings-service reads/writes the **Cline standard config file** `~/.cline/data/settings/cline_mcp_settings.json`—**the same MCP config as the Cline extension** (zod validation, compatible with stdio/sse/streamableHttp).
- runtime-service, on every `startTaskSession`, `createToolBundle()`: read servers from settings → register with `InMemoryMcpManager` → `createMcpTools` generates AgentTool → inject into ClineCore via `localRuntime.extraTools`, and set `disableMcpSettingsTools:true` to dedup.
- Ships a **full MCP OAuth flow**: `@modelcontextprotocol/sdk` Stdio/SSE/StreamableHTTP clients + a homemade `OAuthClientProvider` (persisted to `cline_mcp_oauth_settings.json`); callback goes through kanban’s own HTTP endpoint `/kanban-mcp/mcp-oauth-callback`.

### Provider on the CLI path

PTY-path agents use **their own config** (claude’s `~/.claude/settings.json`, codex’s `~/.codex/`, etc.)—kanban does not touch it, because that is the agent’s own business. **Only the Cline SDK path needs kanban to manage providers**, which is why the provider service lives entirely in the cline-sdk layer.

---

<h2 id="ch10">Chapter 10 · tRPC API layer and auth: four layers of defense in depth</h2>

### tRPC boundary (`src/trpc/app-router.ts`, 731 lines)

`runtimeAppRouter` exposes 4 routers: **runtime (~40 procedures), workspace (14), projects (5), hooks (1)**. The pattern is “thin routing + dependency injection”—each procedure only pairs input/output schema then delegates to a service injected on the context.

- `workspaceProcedure` middleware forces workspace scope: parse `{workspaceId, workspacePath}` from the `x-kanban-workspace-id` header; missing throws BAD_REQUEST.
- `errorFormatter` surfaces CONFLICT’s `currentRevision` to the frontend for optimistic-concurrency retry.
- Transport is **pure HTTP fetch** (`httpBatchLink`). There are three clients: web-ui’s `getRuntimeTrpcClient` (cached by workspaceId), CLI `task.ts`/`hooks.ts` (custom fetch that auto-attaches an internal Bearer token). **No tRPC ws adapter**—WS is only for the state stream and the terminal.

### Core runtime API procedures

- `startTaskSession`: agentId priority = last session > card override > config default; `cline` takes the SDK path, otherwise the PTY path; on trash-restore, probe a persisted Cline session; best-effort capture of a turn checkpoint.
- `stopTaskSession` / `sendTaskSessionInput` (try Cline first, then the terminal) / `getTaskChatMessages` / `sendTaskChatMessage` (recognizes `/clear`) / `reloadTaskChatSession` / `abortTaskChatTurn` / `cancelTaskChatTurn`.
- `startShellSession` / `runCommand` (shortcut buttons run `npm run dev`).
- Cline config domain: provider CRUD, account profile/balance/org/switch, OAuth login/device code, MCP settings/OAuth.
- `resetAllState` (delete `~/.cline/data|kanban|worktrees` one-click reset).

### Board instructions injected into the home agent (`src/prompts/append-system-prompt.ts`, 316 lines)

The sidebar-chat agent that “helps me split work into tasks” is injected a system instruction:
- **Role**: “Kanban board management helper,” **explicitly forbidden from coding / editing files**—implementation requests are always turned into creating tasks.
- **Full CLI reference**: parameters and semantics of `task list/create/update/done/delete/link/unlink/start`.
- **Linking and auto-review pipeline**: review complete → auto-start linked backlog tasks; auto-commit/auto-open-pr combined yield fully autonomous chained execution.
- **Workspace identification**: if cwd is under `.cline/worktrees/`, force `--project-path` (from the worktree back to the main workspace).

### Auth: four layers of defense in depth

1. **Default bind 127.0.0.1 only**—other machines cannot reach it (`runtime-server.ts`).
2. **Host allowlist** (`middleware.ts`)—anti DNS rebinding: non-allowlisted Host is refused outright.
3. **CORS exact Origin allowlist**—stops other pages on the same machine from cross-origin reading (a malicious page cannot steal your API through the browser).
4. **Remote mode adds a passcode** (`src/security/passcode-manager.ts`, 247 lines): enabled when `--host` binds non-loopback. 8-digit random code (`randomBytes` rejection sampling, memory only), `timingSafeEqual` compare, 5 failures per IP lock 30 seconds, after verify issue a 32-byte random token (HttpOnly + SameSite=Strict Cookie, 24h TTL). Also `KANBAN_INTERNAL_AUTH_TOKEN` bearer token for CLI subprocesses (hooks ingest / task).

**Threat model**: when bound remotely, the local runtime exposes arbitrary code-execution capability (git, shell, filesystem). The passcode stops **unauthorized browser visitors** (and brute force)—static assets are allowed through (so React’s PasscodeGate can render first); the API is hard-gated.

---

<h2 id="ch11">Chapter 11 · Frontend board UI: React state-driven + persistent terminals + optimistic concurrency</h2>

### Entry and layout (`web-ui/src/App.tsx`, 1199 lines)

A single root component, **no React Router, purely state-driven view switching**. Provider stack: `PasscodeGateProvider → TelemetryProvider → AppErrorBoundary → TooltipProvider`.

Layout = left `ProjectNavigationPanel` (draggable/collapsible) + right TopBar + main area. Main area has three shapes:
- **Home**: `KanbanBoard` or `GitHistoryView` (`isGitHistoryOpen` toggles) + bottom `ResizableBottomPane` (home terminal—a shell you can run without creating a card).
- **CardDetailView**: after selecting a card, `absolute inset-0` covers Home; inside is Chat panel + diff/file tree + bottom `AgentTerminalPanel` (URL query records the task id).
- **Fallback**: disconnected → `RuntimeDisconnectedFallback`; no permission → `KanbanAccessBlockedFallback`.

### Board state machine (`state/board-state.ts`, ~21k lines)

Purely functional board state machine: `normalizeBoardData` (schema validate/clean WS raw data), `addTaskToColumn`, `applyDragResult`, `moveTaskToColumn`, `trashTaskAndGetReadyLinkedTaskIds` (cascading deps). `drag-rules.ts` defines drag legality (review column cannot be dropped into by hand; trash can only enter, not leave).

### Data-sync trio (the consistency core)

1. **use-runtime-state-stream.ts** (~15k lines): connect `ws://host/api/runtime/ws`, `useReducer` maintains the store, exponential-backoff reconnect (500ms→5s). On `snapshot`, fill in full; incremental messages merge by `updatedAt`; chat upserts by id.
2. **use-workspace-sync.ts**: stream’s workspaceState → `normalizeBoardData` → setBoard, **dedup by `revision`** (old revision discarded); when the page becomes visible again, refetch full to calibrate.
3. **use-workspace-persistence.ts**: 120ms debounce `saveWorkspaceState({board, sessions, expectedRevision})`; on CONFLICT, update local with returned `currentRevision`, toast "Synced latest state", and refetch to overwrite.

Strategy in one sentence: **local optimistic update (drag moves instantly, fail rolls back) → debounce save + expectedRevision optimistic lock → WS as authority + revision monotonicity so old writes cannot overwrite new writes**.

### Persistent terminal (`terminal/persistent-terminal-manager.ts`, ~21k lines)

xterm@6 wrapper + **dual WebSocket** (io binary + control JSON). `clientId` (randomUUID) identifies the browser tab; the backend shares one PTY per taskId and isolates socket state per clientId.

**What “persistent” means**: DOM nodes can be “parked” into a hidden `#kb-persistent-terminal-parking-root`—switching cards does not destroy the terminal; on `restore` the scroll-buffer snapshot is written back (`restore/snapshot` in the control protocol). Addons: Fit, WebGL (auto-degrade on context loss), Clipboard, WebLinks, Unicode11; ResizeObserver 50ms debounce reports cols/rows.

### git UI and commit/PR (`git-actions/build-task-git-action-prompt.ts`)

commit/PR prompt = template interpolation, four-layer fallback: user template → user default template → server default → fallback sentence; the only variable is `{{base_ref}}`. `use-git-actions.ts`’s `handleCommitTask` → assemble prompt → `sendTaskSessionInput` (**paste mode writes into the terminal**) so the agent itself runs git—again “give the work to the agent.”

### Component library and experience

- **Not shadcn**: `@radix-ui/react-*` primitives + **Tailwind CSS v4** + `lucide-react` icons + `@hello-pangea/dnd` (board drag) + `react-virtuoso` (virtual lists) + `sonner` (toast) + `motion`. `components/ui/` is a handwritten thin wrap.
- **PWA**: `sw.js` only intercepts navigation requests; on failure returns a branded "Waiting for Cline" fallback page and HEADs `/` every 2s to auto-refresh (server crash auto-recovery).
- **onboarding**: first-run guide (pick Agent / configure Cline provider); `isSelectedAgentAuthenticated` is the gate.
- **telemetry**: sentry (release `kanban@version`) + posthog (**only 4 manual events**: task_created / task_dependency_created / tasks_auto_started_from_dependency / task_resumed_from_trash—only the load-bearing board behaviors).
- **open-targets**: generate open commands by platform (`open -a "VS Code"` / `code <path>`), supporting vscode/cursor/windsurf/zed/xcode/ghostty etc.—click a card and open the worktree in an IDE.

---

<h2 id="ch12">Chapter 12 · Desktop Electron: a pure process wrapper + OAuth relay</h2>

### Position (`packages/desktop/`, src 11 TS files 2,327 lines + `disconnected.html` 152 lines + 13 tests)

**The desktop app is a pure “process wrapper”; it imports no code from the root package.** The CLI is spawned as a subprocess (`spawn("kanban", ["--no-open","--port",3484,...])`); after HTTP health probe (checking `<title>Kanban</title>`) confirms ready, BrowserWindow loadURLs `http://127.0.0.1:3484`.

Module split:

| Module | Lines | Duty |
|---|---|---|
| `main.ts` | 311 | single-instance lock, `kanban://` protocol deep link, window-state persist, before-quit force-sync shutdown |
| `runtime-orchestrator.ts` | 604 | lifecycle core: connect (first probe an existing runtime, else startOwnRuntime) / restart / shutdown / dispose; dual probes (attached 500ms detect external-runtime crash, recovery 2s auto-reconnect); powerSaveBlocker against App Nap |
| `runtime-child.ts` | 334 | spawn + 30s ready poll + treeKill graceful close, 4GB V8 heap cap |
| `window-registry.ts` / `window-state.ts` | 298 / 178 | multi-window management, atomic write of state file, offscreen-window clamp |
| `window-factory.ts` | 221 | renderer crash recovery (did-fail-load + render-process-gone → health probe → disconnected screen / retry) |
| `oauth-relay.ts` | 46 | see below |

### OAuth relay

The agent’s OAuth callback arrives at the desktop as a `kanban://oauth/callback?...` deep link; the main process parses it and forwards params to `http://127.0.0.1:3484/kanban-mcp/mcp-oauth-callback`, retry 3 times then popup on failure—**pure web cannot intercept a system-level custom protocol**, which is one of the hard reasons the desktop app exists.

### Why a desktop app?

1. **Resident process**: native menus, multi-window, system tray.
2. **Ships a Node runtime**: the shim uses `ELECTRON_RUN_AS_NODE=1` to reuse the Electron binary; the user does not install Node.
3. **GUI-launch PATH enhancement** (`runtime-child-env.ts`): fill in Homebrew/nvm etc. paths—pure CLI never has this problem; double-click-to-open does.
4. **Crash auto-bring-up**: recovery probe 2s auto-reconnect.

Packaging path: root `build.mjs` esbuild produces `dist/cli.js`+`dist/web-ui/` → `stage-cli.mjs` copies into `packages/desktop/cli/` → electron-builder `extraResources` + `asarUnpack` (only unpack cli/node-pty/*.node) → `patch-node-pty.mjs` postinstall fixes the `app.asar→unpacked` dual-suffix bug.

---

<h2 id="ch13">Chapter 13 · Engineering and release: dogfood, CI/CD, GritQL static checks</h2>

### dogfood: using kanban to develop kanban

- `scripts/dogfood.mjs` (470 lines): first `npm run build`, then spawn `dist/cli.js` in an independent process group running the **production build**; `--skip-shutdown-cleanup` is handed to the lockfile-elected “cleanup owner” (prevents multi-instance double cleanup); strip the repo’s `node_modules/.bin` (avoid shadowing global Agent CLIs); 10s SIGKILL fallback.
- The repo ships `.claude/`, `.cline/kanban/config.json` (shortcuts run dogfood/dev-full), `.clinerules/workflows/release.md`, `.codex/environments/environment.toml` (setup script auto-symlinks node_modules to the main worktree), `.factory/settings.json`—**the same repo is a real usage scene for four agents at once: Claude Code / Cline / Codex / Factory Droid**; the worktree+symlink trick is reused directly by the .codex config.

### Dev norms (AGENTS.md, 8KB of “tribal knowledge”)

- No `any`, no inline import, upgrade dependencies rather than downgrade.
- web-ui uses Tailwind v4 + Radix + Lucide.
- **Prefer direct PATH detection over launching the agent through an interactive shell** (conda/nvm will freeze the runtime).
- For Cline behavior, look at `@clinebot/core` + `src/cline-sdk/`.

### Static checks and CI

- **GritQL** (`grit/`, 4 .grit files): ban console.*, ban `__home_agent__:` literals, ban process.env destructuring, ban process.exit (except CLI entry)—structural constraints stronger than lint.
- `.husky/pre-commit`: biome check staged → typecheck → `test:fast`.
- `.github/`: `ci.yml` (push/PR) + `test.yml` (3-platform matrix: ubuntu20/22 + macos22) + **`publish.yml`**: manual workflow_dispatch with a tag → version-consistency check → `npm run prepublishOnly` → **OIDC trusted publishing `npm publish`** → auto-extract changelog, create GitHub Release → Slack.

### Version evolution (CHANGELOG, v0.1.4 → v0.1.70)

| Version | Milestone |
|---|---|
| 0.1.4 | Start: git worktree parallel tasks, diff review, linked tasks, MCP integration |
| 0.1.5 | Droid agent, dogfood, xterm + node-pty terminal |
| 0.1.11 | **Remove MCP, switch to skill** (evidence of an architecture turn) |
| 0.1.12 | UI redesign |
| 0.1.15 | Cross-platform Windows/Linux |
| 0.1.65+ | runtime child manager, new-version notification, Electron desktop-app scaffold |

5 months from 0.1.4 to 0.1.70—**this is exactly the speed a “board + agent pipeline” produces when it runs itself**.

---

<h2 id="ch14">Chapter 14 · Cross-cut: vs the Cline engine / Claude Code / task-management products</h2>

### 14.1 vs the Cline engine (previous analysis)

| Dimension | Cline (engine) | kanban (orchestration layer) |
|---|---|---|
| Position | single-agent engine (main loop + tools + Provider) | scheduling board for N agents |
| Core abstraction | AgentRuntime stateless loop | card = worktree + session |
| Session model | stateless `run("")` re-entry | stateful `ClineCore` session (npm 0.0.38) |
| Status display | TUI / WebView, one at a time | board batch + hook live |
| Resource isolation | single workspace | git worktree, one per card |
| Relationship | kanban depends on it (@clinebot/core) | engine-neutral (7 agents all supported) |

**Essential difference**: Cline answers “how does one agent work smartly”; kanban answers “how do a pile of agents get organized to work.” kanban’s orchestration (dependency chains, auto-start, batch monitoring) is what the Cline engine does not have; Cline’s depth (Provider/OAuth/MCP) kanban takes for free via the SDK.

### 14.2 vs Claude Code

| Dimension | Claude Code | kanban |
|---|---|---|
| Interaction | single-session Ink TUI | board + xterm (multi-session parallel) |
| Multi-task | `--resume`/`--fork-session` by hand | cards + dependency chain automatic |
| Parallelism | single process (can spawn subagent) | independent worktree + process per card |
| Review | none built-in | review column + inline comments fed back |
| Relationship | kanban runs it as a subprocess (injects hooks) | Claude Code is one of kanban’s 7 agents |

### 14.3 vs task-decomposition multi-agent (OpenWorker / Manus lineage)

Those are “multi-agent **inside** an Agent” (coordinator splits work → delegates to specialists, sharing one session context); kanban is “**dev-process-level** multi-agent” (each agent independent session + independent worktree + a human review gate). The former is **reasoning division of labor**; the latter is **engineering parallelism**—one at the thinking layer, one at the repo layer.

### 14.4 vs project-management tools (Linear / Jira / GitHub Projects)

kanban’s cards are **alive**: they have a terminal, a worktree, they can start an agent, they can be linked into a pipeline. Traditional board cards are “a requirement description”; kanban’s cards are “a process that is working”—**the object of task management moved from a document to an execution unit**.

### 14.5 One-sentence division of labor

- **Cline engine**: one agent’s thinking and execution.
- **Claude Code**: one agent’s interactive terminal.
- **kanban**: multi-agent organization, isolation, monitoring, review, and automated pipeline.
- **Claude Code / Codex / Cline / Droid**: kanban’s executors (pluggable).

---

<h2 id="ch15">Chapter 15 · Summary: three most distinctive designs and their trade-offs</h2>

### Three things (if you remember only three designs)

**1. git worktree is resource isolation; symlink is cost optimization**
No container, no sandbox. `git worktree add --detach` in one command gives each agent an isolated working tree, zero-conflict parallel; gitignored directories (`node_modules`) symlink back to the main repo, saving one `npm install` per card. Combined with `.git/info/exclude` managing the symlink itself. This is a textbook case of “use git’s mechanism to solve an engineering problem”—**the cost**: when the agent modifies gitignored files such as node_modules, the share will leak across (the README is explicit: "don't use Kanban"); Turbopack cache will trip over the symlink, so they added a Turbopack-detection exception.

**2. Hooks reverse injection: the agent actively calls kanban’s CLI to report status**
Do not poll terminal output (format unreliable). Instead inject `kanban hooks notify` into each agent’s official hook mechanism—every utterance, every tool call, actively callbacks kanban. That makes live status scanning of hundreds of cards possible, and naturally fits 7 agents (each generates a different hook config). **The cost**: every agent needs an adapter; Codex even needs a wrapper + polling rollout jsonl (its hooks are not even reliable); it depends on experimental features (bypass permissions, runtime hooks); the README itself marks Research Preview.

**3. Dual path: engine-neutral and deep integration coexist**
Default Cline takes the native SDK (same process, Provider/OAuth/MCP all deeply integrated); other agents take the PTY subprocess (hooks only). One path gets the deepest integration; one path gets the widest compatibility. **The cost**: two execution paths, two status sources (SDK events vs hook events); `sendTaskSessionInput` must try Cline first then the terminal; `@clinebot/core` 0.0.38’s API is still moving fast (the 0.0.36 compatibility branch is still kept).

### Trade-offs and “who it is for”

- **For**: teams doing multi-agent parallel development, individuals who want to build autonomous pipelines for agents, developers studying agent orchestration.
- **Not for**: single-agent users (Claude Code itself is enough), enterprise scenes that need strong audit/permission control (default bypass permissions), scenes that modify gitignored files (symlink share will leak).
- **Relation to the ecology**: it is the Cline ecology’s “command stand”—engine (Cline), executors (each CLI agent), orchestration layer (kanban), three layers distinct.

### 🔍 Source pointers (go check the source)

| Want to know | Look here |
|---|---|
| Card model / four columns / deps | `src/core/api-contract.ts` (1277 lines) + `task-board-mutations.ts` |
| Session state machine | `src/terminal/session-state-machine.ts` (78 lines) |
| worktree + symlink | `src/workspace/task-worktree.ts` (695 lines, L358 symlink) |
| checkpoint | `src/workspace/turn-checkpoints.ts` (92 lines) |
| Terminal + dual WS + backpressure | `src/terminal/pty-session.ts` + `ws-server.ts` (591 lines) |
| 7 agent adapters | `src/terminal/agent-session-adapters.ts` (1451 lines) |
| Hooks reverse injection | `src/commands/hooks.ts` (820 lines) |
| Cline integration boundary | `src/cline-sdk/sdk-runtime-boundary.ts` (126 lines) |
| Event translation | `src/cline-sdk/cline-event-adapter.ts` (816 lines) |
| tRPC + auth | `src/trpc/app-router.ts` + `src/server/middleware.ts` + `passcode-manager.ts` |
| Frontend sync trio | `web-ui/src/runtime/use-runtime-state-stream.ts` + `use-workspace-persistence.ts` |
| Persistent terminal | `web-ui/src/terminal/persistent-terminal-manager.ts` |
| Desktop | `packages/desktop/src/runtime-orchestrator.ts` (604 lines) |
| Board-injected instructions | `src/prompts/append-system-prompt.ts` (316 lines) |
