What this tutorial is: a from-scratch build plan whose blueprint is kanban (
github.com/cline/kanban, Apache-2.0, v0.1.70). You will act as a developer who can also do product work: start from a PRD, then explode kanban into an executable development plan—each functional module listed in build order, with purpose, implementation shape, stack, definition trade-offs, strengths and costs, and how the pieces couple. No technical detail is too small. Core narrative: follow the action “the user clicks the play button on a card” from zero to the end—how it creates an independent worktree, how it starts an agent, how every agent message is reverse-injected onto the card via hooks, how you review the diff, how you commit / open a PR, how the dependency chain auto-starts the next card. That chain is the skeleton of the whole project. How to read: read chapter 1 (PRD) and chapter 2 (overview and milestones) first, then proceed in order. Chapter 24 (the full workflow) is the climax; read it closely after modules 1–9.
Contents
Part I · Charter and foundation
Part II · Board core (build in order)
- Module 1: Data model and API contract (zod as the whole contract)
- Module 2: Board state machine and optimistic-concurrency persistence
- Module 3: git layer — worktree, symlink, checkpoint, git UI data
- Module 4: HTTP server and static assets
- Module 5: tRPC API layer (4 routers + dependency injection)
- Module 6: Real-time state stream (WS snapshot + incremental fanout)
Part III · Terminal and multi-agent execution
Part IV · Deep Cline integration
Part V · Frontend and experience
Part VI · Security, close, and full workflow
- Module 18: Four security layers (passcode / Host / CORS / token)
- Module 19: CLI and subcommands (
task/hooks/update) - Module 20: Desktop Electron
- “After the user clicks play”: the full workflow
- Testing strategy and engineering (dogfood / CI/CD)
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 kanbanis 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.1by 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 kanbanstarts 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.
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)
- No
any, ban inline import (Biome-enforced). - Prefer direct PATH detection over launching an agent from an interactive shell—
whichspawns a shell; conda/nvm environments will freeze the runtime. - GritQL static checks: ban
console.*, banprocess.exit(except CLI entry), ban destructuringprocess.env.
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 packagetype: 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-ptyneeds to be external. npm run dogfoodis 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:allscript). - ✅ 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 kanbanstarts the service;npm run checkis all green;npm run dogfoodcan run.
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
titleis derived from the prompt automatically (truncated to 80 characters) rather than typed by hand—less user burden;task-iduses a 5-char short id for spoken communication. "done"is compatibility-mapped totrash—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.
- ✅
discriminatedUnionmakes 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.
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.tsadd/move/update/trash/delete/linkeach return a new board, do not mutate the original—isomorphic with React immutable updates. - Revision optimistic concurrency:
saveWorkspaceState({board, sessions, expectedRevision});meta.jsonincrementsrevision; mismatch throwsWorkspaceStateConflictErrorcarryingcurrentRevision. - Atomic write + file lock:
locked-file-system.tsusesproper-lockfile;withLockslocks in lockfile-path sort order (prevent deadlock);writeTextFileAtomictmp+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 -9the server and restart, board state is intact.
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/)
- Worktree lifecycle (
task-worktree.ts, 695 lines): git worktree add --detach <baseCommit>, path~/.cline/worktrees/<taskId>/<workspaceLabel>.- Detached HEAD, no branch—the branch is created dynamically only at Commit/Open PR.
- An existing worktree is authoritative; it is not rebuilt because
baseadvanced; creation runs under.git/kanban-task-worktree-setup.lock. - On trash,
captureTaskPatchstores the diff under~/.cline/kanban/trashed-task-patches/, thengit worktree remove --force; on resume,git applyrestores. - Symlink shared dependencies (
syncIgnoredPathsIntoWorktree, L358):git ls-files --others --ignored --exclude-per-directory=.gitignore --directorylists gitignored paths → in-placesymlink(main repo→worktree)→ write a kanban-managed block into.git/info/exclude→ fail silently skip →getUniquePathskeeps only the shallowest roots. - Checkpoint (
turn-checkpoints.ts, 92 lines): after each message turn, create a commit with a temporaryGIT_INDEX_FILE(/tmp) (authorkanban-checkpoint),update-ref refs/kanban/checkpoints/<base64(taskId)>/turn/<N>—does not pollute the branch; pure refs. - git command wrapping (
git-utils.ts):execFile("git", ["-c","core.quotepath=false", ...]), 10MBmaxBuffer; non-zero exit does not throw, returns{ok,stdout,stderr,exitCode}. - git UI data (
git-history.ts, 478 lines):git log --topo-order --date-order+\x1f/\x1eseparators +for-each-ref+rev-list --notto compute relation. - Sync actions (
git-sync.ts, 385 lines): fetch / pull--ff-only(refuse if local changes) / push / checkout / discard. - Turbopack exception (
task-worktree-turbopack.ts): Turbopack following a symlink breaks the cache → if detected,node_modulesis not symlinked. - Path sandbox (
path-sandbox.ts):isPathWithinRootprevents 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_modulesper 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_modulesis a symlink and is not committed; after trashing a card,~/.cline/kanban/trashed-task-patches/has a patch; after resume, files restore.
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 fallbackindex.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.mjscoordinates).
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>/returnsindex.html;/api/trpc/...goes tRPC;/api/runtime/wsupgrade succeeds.
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)
runtimeAppRouterexposes 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.
workspaceProceduremiddleware forces workspace scope (parsesworkspaceId/workspacePathfrom thex-kanban-workspace-idheader).errorFormattersurfaces CONFLICT’scurrentRevision.- Transport is pure HTTP (
httpBatchLink); three clients: web-ui (client cached byworkspaceId), CLItask.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”:
startTaskSessioninternally dispatches byagentIdonto the SDK/PTY two paths. - Board CRUD does not enter the tRPC workspace router—the CLI does
mutateWorkspaceStatedirectly on JSON files (optimistic concurrency), then callsworkspace.notifyStateUpdatedto 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.startTaskSessioncan start a session; CLIkanban task startand a web action on the same card have the same effect.
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_clearedtask_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.
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—serverws.send(chunk)feeds xterm directly; browser messages are keyboard bytes passed throughwriteInput./api/terminal/control: JSON—{type:"state",summary}/{type:"exit",code}/{type:"restore",snapshot,cols,rows}; the browser sendsresize/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 →pauseOutputpauses 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 inpendingChunk. - 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-ptyis a native module; cross-platform build / Electron packaging both need handling (patch-node-pty.mjsfixes 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.
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 + activePtySession+ listeners +restartRequest.- Crash auto-restart:
shouldAutoRestart(at most 3 times in a 5-second window) →scheduleAutoRestartre-runs with the savedrestartRequest. - 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 (
reviewReasonsubdivisions) 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.
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 withaccessSync(X_OK)directly; Windows usesPATHEXT. agent-catalog: autonomous launch args for 7 agents: claude--permission-mode auto, codex--auto-approve-all, droidautonomyMode, etc. The README depends on these experimental features.- 7 adapters (
prepareAgentLaunchdispatches byagentId): - claude:
--settingsinjectssettings.json(7 hook kinds all point atkanban hooks notify) - codex:
-c features.hooks=true+ 5 config overrides + sha256 trust hash pre-trust +CODEX_TUI_RECORD_SESSION=1polling 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.
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
ingestis synchronously blocking (wait for ack);notifyis 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_IDenv 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. notifyis 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” / “Completedrun_commands”; when the agent requests permission, the card becomesawaiting_review.
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/coreonly here; re-exportClineCore.create({backendMode:"auto"}). - Key fact: the npm-published
@clinebot/core@0.0.38exposes a stateful multi-session host—sessionsMap (start/send/stop/abort/get/list/readMessages/subscribe),send()incremental continue-chat,SessionHistoryRecordon disk. This differs from Clinemain’s statelessAgentRuntime(the published API is still moving fast; the code keeps a 0.0.36 compatibility branch). - Task ↔ session mapping:
ClineSessionRuntime(an interface defined here) +InMemoryClineSessionRuntimeimplementation;sessionIdByTaskId/taskIdBySessionIdbidirectional Map;createSessionId(taskId)is prefixed; after restart, recover fromhost.list(). send()takesdelivery: "queue"|"steer"(queue vs interrupt the current turn).cline-runtime-setup.ts: assembleuserInstructionService(skills/rules/workflows watch),resolvePrompt(slash commands),requestToolApproval(default allapproved: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
ClineCorerather than implementingSessionRuntimeyourself: engine-neutral but Cline-first—Cline is in-house; deep integration is the default path. requestToolApprovaldefaults 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/coreversions only changes the boundary file.
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
~/.clinesession storage (host.readMessages); kanban only stores an in-memory view; after page refresh,hydrateTaskMessageshydrates. - 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 createturned 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.
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 prefixedworkos:) + device auth + manualapiKey+ env vars;ClineAccountServicelooks 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 intolocalRuntime.extraTools+disableMcpSettingsTools:trueto dedupe. - MCP OAuth: self-implemented
OAuthClientProvider(persisted tocline_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.
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 (KanbanBoardorGitHistoryView+ bottom home terminal),CardDetailView(absolute inset-0overlay, 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,useReducerstore, 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 +expectedRevisionoptimistic lock; on CONFLICT, update local withcurrentRevision+ refetch overlay.- Card live state:
getCardSessionActivity()parses the summary’slatestHookActivity→ “Thinking…” / tool tags / “Waiting for review” / final message; status-dot color refreshes live. - Component library:
@radix-uiprimitives + 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.tsx1199 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.
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 bytaskId, isolates socket state byclientId.- “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 (forwaitForLikelyPrompt).
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 WSinput/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.
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 ontobase_refand 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/openPrPromptTemplatein 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.
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.
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.
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); subcommandstask(list/create/update/trash/delete/link/unlink/start, all JSON output),hooks(ingest/notify/gemini-hook/codex-hook/codex-wrapper),update. taskcore pattern: board changes do not go tRPC; this process doesmutateWorkspaceState(optimistic concurrency) directly on JSON files, then callsworkspace.notifyStateUpdatedto 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
taskwrites 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 Bbuilds a dependency;kanban task start Astarts a session.
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-ptynative 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.
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.
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/.factoryconfig: the same repo is used as a real scene by 4 agents at once.
CI/CD (aligned with .github/)
ci.yml: push/PR →maintrigger.test.yml: 3-platform matrix (ubuntu20/22 + macos22) build + lint + typecheck + tests.publish.yml: manualworkflow_dispatchwith a specified tag → verifyvX.Y.Zmatchespackage.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
- Every commit passes biome + typecheck + test:fast (husky).
- GritQL bans
console.*/process.exit(except CLI entry) /process.envdestructure. - No
any, ban inline import. - Prefer PATH detection over launching an agent from an interactive shell.
- New agent support = adapter + hooks parse + integration tests, the trio.
Acceptance
- All-platform
npm run checkgreen;npm run dogfoodcan run itself; the release flow produces an npm package + GitHub Release in one click.