English lecture · source-traced

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 c

EN
lecture
source
file:line
no
embellishment
中文
sibling

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.


Part 1

Contents

Part I · Charter and foundation

  1. PRD: what we will and will not build
  2. Overview: three architectural ideas, milestones, team
  3. Module 0: Project scaffold and engineering discipline

Part II · Board core (build in order)

  1. Module 1: Data model and API contract (zod as the whole contract)
  2. Module 2: Board state machine and optimistic-concurrency persistence
  3. Module 3: git layer — worktree, symlink, checkpoint, git UI data
  4. Module 4: HTTP server and static assets
  5. Module 5: tRPC API layer (4 routers + dependency injection)
  6. Module 6: Real-time state stream (WS snapshot + incremental fanout)

Part III · Terminal and multi-agent execution

  1. Module 7: PTY terminal and dual WebSocket
  2. Module 8: Session manager and state machine
  3. Module 9: Multi-agent adapter layer (7 CLI agents)
  4. Module 10: Hooks reverse-injection

Part IV · Deep Cline integration

  1. Module 11: Cline SDK integration (ClineCore multi-session host)
  2. Module 12: Event translation and message repository
  3. Module 13: Provider and MCP services

Part V · Frontend and experience

  1. Module 14: Frontend board UI (React + optimistic-concurrency trio)
  2. Module 15: Frontend persistent terminal (xterm dual WS)
  3. Module 16: git UI, comments, and commit/PR
  4. Module 17: Home agent and board-injection instructions

Part VI · Security, close, and full workflow

  1. Module 18: Four security layers (passcode / Host / CORS / token)
  2. Module 19: CLI and subcommands (task / hooks / update)
  3. Module 20: Desktop Electron
  4. “After the user clicks play”: the full workflow
  5. Testing strategy and engineering (dogfood / CI/CD)

Part 2

Chapter 1 PRD: what we will and will not build

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.

Part 3

Chapter 2 Overview: three architectural ideas, milestones, team

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.

Part 4

Chapter 3 Module 0: Project scaffold and engineering discipline

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.

Part 5

Chapter 4 Module 1: Data model and API contract (zod as the whole contract)

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.

Part 6

Chapter 5 Module 2: Board state machine and optimistic-concurrency persistence

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.

Part 7

Chapter 6 Module 3: git layer — worktree, symlink, checkpoint, git UI data

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):
  2. git worktree add --detach <baseCommit>, path ~/.cline/worktrees/<taskId>/<workspaceLabel>.
  3. Detached HEAD, no branch—the branch is created dynamically only at Commit/Open PR.
  4. An existing worktree is authoritative; it is not rebuilt because base advanced; creation runs under .git/kanban-task-worktree-setup.lock.
  5. On trash, captureTaskPatch stores the diff under ~/.cline/kanban/trashed-task-patches/, then git worktree remove --force; on resume, git apply restores.
  6. 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.
  7. 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.
  8. 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}.
  9. 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.
  10. Sync actions (git-sync.ts, 385 lines): fetch / pull --ff-only (refuse if local changes) / push / checkout / discard.
  11. Turbopack exception (task-worktree-turbopack.ts): Turbopack following a symlink breaks the cache → if detected, node_modules is not symlinked.
  12. 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.

Part 8

Chapter 7 Module 4: HTTP server and static assets

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.

Part 9

Chapter 8 Module 5: tRPC API layer (4 routers + dependency injection)

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.

Part 10

Chapter 9 Module 6: Real-time state stream (WS snapshot + incremental fanout)

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.

Part 11

Chapter 10 Module 7: PTY terminal and dual WebSocket

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.

Part 12

Chapter 11 Module 8: Session manager and state machine

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.

Part 13

Chapter 12 Module 9: Multi-agent adapter layer (7 CLI agents)

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.

Part 14

Chapter 13 Module 10: Hooks reverse-injection

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.

Part 15

Chapter 14 Module 11: Cline SDK integration (`ClineCore` multi-session host)

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.

Part 16

Chapter 15 Module 12: Event translation and message repository

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.

Part 17

Chapter 16 Module 13: Provider and MCP services

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.

Part 18

Chapter 17 Module 14: Frontend board UI (React + optimistic-concurrency trio)

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:
  • use-runtime-state-stream.ts: WS connection, useReducer store, exponential-backoff reconnect (500ms→5s), snapshot full + incremental merge.
  • use-workspace-sync.ts: stream → normalizeBoardData → setBoard, revision dedupe, refetch calibration when the page is visible.
  • 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.

Part 19

Chapter 18 Module 15: Frontend persistent terminal (xterm dual WS)

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.

Part 20

Chapter 19 Module 16: git UI, comments, and commit/PR

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.

Part 21

Chapter 20 Module 17: Home agent and board-injection instructions

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.

Part 22

Chapter 21 Module 18: Four security layers (passcode / Host / CORS / token)

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.

Part 23

Chapter 22 Module 19: CLI and subcommands (`task` / `hooks` / `update`)

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.

Part 24

Chapter 23 Module 20: Desktop Electron

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.

Part 25

Chapter 24 “After the user clicks play”: the full workflow

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.


Part 26

Chapter 25 Testing strategy and engineering (dogfood / CI/CD)

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.
↑