Devplane

Architecture

A map for contributors: one binary and one store, the hook writes, the host folds, the pure core reaches nothing outside. Processes, the store, the API routes, recovery and the window.

Processes

  Claude Code / Codex / Copilot session
        │ command hooks
        ▼
  devplane hook ── decides in its own process, answers on stdout
        │ appends events, decisions, held asks
        ▼
  ~/.devplane/devplane.db  (SQLite) ◀── read commands, with no host
        ▲ tailed every 500 ms
        │
  devplane up ── the host: the one long-lived process and the only socket
        ▲ HTTP + SSE on 127.0.0.1
  browser tab · desktop window · devplane CLI
  • The hook writes. devplane hook decides in-process, appends to SQLite, and exits. If the store will not open, the decision goes to ~/.devplane/pending-decisions.jsonl for the next host to file. There is no hook receiver route.
  • The host folds. devplane up tails the hooks’ rows, drives agents over ACP, receives telemetry, polls the roster and GitHub, runs gates, and serves the workbench. Nothing starts it automatically.
  • Read commands need no host. Without one they replay the store in memory, write nothing, and say what they cannot see.

One host per home

~/.devplane/host.json names the host’s port, version, start time, binary and pid; alive means /healthz answers, not that the pid exists. A second host on the same home is refused. devplane down posts /api/quit and waits for the port to close. A second instance needs its own DEVPLANE_HOME and port.

Source map

PathWhat
src/core/the pure domain: events, reducers, changes, policy, attention, review, certificate, verify. No async, runtime, database, HTTP, clock or file system; tests/purity.rs enforces it
src/hook.rs, src/observe/the hook entry point; roster, OTLP, status line, OpenCode feed, agent connect
src/acp/, src/driven.rsthe Agent Client Protocol client for every driven agent
src/change.rs, src/gates.rs, src/base_checks.rs, src/git.rsworktrees, gates, base checks, offers; git through its CLI
src/github/GitHub’s REST and GraphQL APIs, device-flow sign-in, reviews
src/store.rs, src/schema.sqlthe SQLite store
src/api.rs, src/host.rs, src/poller.rsthe HTTP API, host start-up and shutdown, the tail and pollers
src/cli/every command
src/app/the desktop window (feature app)
ui/the workbench: Svelte 5 and Vite, built into ui/dist and embedded in the binary

Events are EventEnvelope { id, at, run_id, project_id?, source, event }, append-only. Reducers are pure (Run, Event) -> Run, tested by replay. The inbox is derived from state, never stored.

The store

SQLite with WAL and FTS5, one file, schema version 18. A schema change starts a new store and moves the old one aside (after an upgrade).

TablesWhatKept
events, events_fts, runs, projectsobservations, the runs folded from them, their search indexevents 30 days, finished runs 7 days
messageswhat driven agents said30 days
changeseach change: project, branch, worktree, agentalways
decisionsthe decision log; appended, never updatedalways
askswhat an agent put to a person, and what became of italways
seen, review_commentshunks a person marked seen (by content, no time); review commentsalways
projection, channel_health, attention_log, agent_capabilities, looks, announcedthe tail’s mark, channel latency, inbox outcomes, what each agent advertised, when you last looked, which notifications were shownbookkeeping

Pruning runs once at host start.

The API

HTTP + JSON on 127.0.0.1, with Server-Sent Events on /api/stream (event and message frames; ?run= narrows it). Everything but /healthz and the static files needs a token (Security). Routes marked window need the desktop window’s proof as well.

RouteMethodPurpose
/healthzGETok <version>; liveness
/devplane/otel/v1/logs, /devplane/otel/v1/tracesPOSTOpenTelemetry logs (Claude Code) and traces (Copilot, Codex)
/api/boardGETthe working set (?all=true: everything)
/api/inboxGETwhat needs a person
/api/streamGETServer-Sent Events
Runs
/api/runs/{id}GETone run
/api/runs/{id}/messagesGETwhat a driven agent said
/api/runs/{id}/promptPOSTmessage a driven run
/api/runs/{id}/snoozePOSTquieten a run’s inbox items
/api/runs/{id}/stopGET, POSTwhat stopping would leave; stop a driven run
/api/runs/{id}/claimPOSTthe agent’s stop claim (its spec and tests conflict)
/api/agentsGETdrivable agents and their capabilities
Asks
/api/asksGETevery ask and its outcome
/api/asks/{id}/answerPOSTanswer a permission or a question; recorded window only with the window’s proof
/api/asks/{id}/undoPOSTtake back a held answer
Changes
/api/changesGET, POSTevery change with state and standing; start one
/api/changes/preflightPOSTwhat starting would do; writes nothing
/api/changes/adoptPOSTmake a hand-made branch a change
/api/changes/draftPOSTstart a change that drafts a spec
/api/changes/{id}GETone change, its runs, tasks and drifts
/api/changes/{id}/verify, /base-checksPOSTrun the gates; run the base checks
/api/changes/{id}/retry, /resume, /loginPOSTone more feedback round; reconnect to the session; sign the agent in
/api/changes/{id}/reviewGETthe review
/api/changes/{id}/review/seenPOSTmark a hunk, or every hunk at a path, seen
/api/changes/{id}/commentsGET, POSTreview comments; add one
/api/changes/{id}/comments/{cid}DELETEdelete an unsent comment
/api/changes/{id}/comments/sendPOSTsend every unsent comment as one prompt
/api/changes/{id}/drift/accept, /drift/tellPOSTdecide spec drift
/api/changes/{id}/draft/accept, /draft/rejectPOSTdecide a draft
/api/changes/{id}/clarifyPOSTanswer an open-question line
/api/changes/{id}/claim/readPOSTmark the stop claim read
/api/changes/{id}/certificateGETthe done certificate
/api/changes/{id}/offer, /finish, /archivePOSToffer, finish, archive
/api/changes/{id}/snoozePOSTquieten its inbox items
/api/changes/{id}/openPOSTopen the worktree in an editor or terminal (window), else its path
Projects and setup
/api/projectsGETregistered projects
/api/projects/trustPOSTtrust a repository (window)
/api/projects/reloadPOSTre-read projects and ~/.devplane/trusted.toml
/api/projects/scanGETwhat trusting a path would allow, before trust
/api/projects/{id}/gates/candidatesGETcommands the repository already has, for Declare a check
/api/projects/{id}/gatesPOSTwrite [gates] check after confirmation
/api/projects/{id}/config/open-questionsPOSTwrite the offered open_questions
/api/projects/{id}/snoozePOSTquieten its GitHub items
/api/first-runGETthe first-run checklist
/api/connect/{agent}, /api/disconnect/{agent}POSTconnect or disconnect an agent’s hooks
/api/grantsGET, POSTyour grants; write an offered one (window)
/api/grants/revokePOSTremove a grant (window)
/api/specsGETspecifications and the requirement-to-task trace
/api/setupGETthis machine and each devplane.toml, read back
GitHub
/api/githubGETeach host’s sign-in state; never the token
/api/github/login, /logout, /refreshPOSTdevice-flow sign-in; delete a token; reload and re-read
/api/forgeGETthe last GitHub poll, per project
/api/issuesPOSTa repository’s open issues, live
Record and diagnostics
/api/decisionsGETthe decision log
/api/decisions/unattributedGETcalls a vendor reported deciding that no authority can be named for
/api/explainGETwhat the gate would decide about one call
/api/searchGETfull-text search
/api/diagnosticsGETwhat devplane doctor reports
/api/modesGETwhich sessions decide without you
The app
/api/app/update, /api/app/updatesGET, POSTupdate-check state; your answer to may it check
/api/app/update/install, /dismissPOSTinstall and restart; dismiss an offer
/api/quitting, /api/quitGET, POSTwhat quitting would end; quit the host
/, /{file}GETthe embedded workbench

Recovery and shutdown

On host start: file the spool, load projects, changes and runs, reconcile with the roster and live pids, mark lost where a process is gone, re-arm stall timers, open the API, and let the tail catch up. An unreadable roster never marks a run lost. A driven run caught mid-flight becomes interrupted, branch and worktree untouched, with resume where the agent supports it (Driving agents).

devplane down (or Quit) says what it ends, stops answering and polling, stops each driven agent’s process group, and waits, bounded, for their endings to be written. A killed host cannot: the next host raises each of its agents still running as a critical inbox item with the command to end it.

The window

devplane up --window runs the host in the app’s process and opens a WebView on the same URL a browser gets: same page, same API, no data over the window’s bridge. It adds notifications, a tray count, a global shortcut, devplane:// links, opening a worktree in an editor or terminal, and the proof the window routes need. Tauri 2, behind the Cargo feature app, off by default, so the command line links no WebKit.

DEVPLANE_UI=/path/to/dist devplane up --no-browser serves a built interface from disk.

Technology

AreaChoice
CoreRust 1.94+, Tokio, serde, tracing
Agentsthe Agent Client Protocol Rust SDK
HTTPaxum
StoreSQLite via sqlx, WAL, FTS5
Telemetryan OTLP/HTTP JSON reader
GitHubREST and GraphQL over reqwest with rustls; the token in the OS credential store via keyring
WorkbenchSvelte 5 and Vite; fetches nothing from any other origin
WindowTauri 2

Performance

Asserted by a test or fixed in code:

  • Deciding a prohibition costs under 60 ms over starting the binary at all.
  • The host folds hook-written rows within 500 ms.
  • The roster is polled every 2 s while anything is busy, 10 s when idle; GitHub every five minutes.
  • The embedded workbench stays under 250 KB gzipped.

Something wrong or out of date? Edit this page.