Consolidated technical reference for the ADE (Agentic Development Environment) system. This document is the entry point for engineers and AI agents who need to understand the shape of the system before reading feature-specific docs. Deeper subsystem docs live under docs/features/.
ADE is a local-first development control plane that orchestrates AI-assisted software engineering across parallel worktrees. The center of the system is the ADE brain: the always-on, machine-owned ADE process for one channel. The brain hosts every project on that machine through a project registry, exposes a multi-project JSON-RPC surface on the channel's local endpoint, serves the sync websocket for ADE Mobile, and carries executor authority. Desktop, the terminal ade code client, the iOS app, and SSH-attached desktop windows are all clients that attach to a local brain or remote runtime transport and invoke runtime-owned actions through that one surface.
The brain owns everything that needs to survive a client closing: worktree-per-lane git isolation, multi-provider agent chat, work-session orchestration, a Linear-integrated CTO agent acting as a team lead, worker delegation, a pipeline builder for visual automations, stacked pull requests with conflict simulation, computer-use proofs, the sync service that replicates projects to other devices, and the per-machine credential store and agent registry. Nothing leaves the user's machine by default: AI work runs through user-authenticated CLIs (Claude Code, Codex), local API-key routes (OpenCode server), or local model endpoints (Ollama, LM Studio, vLLM).
ADE ships as one computer install, ADE Mobile, and the marketing site:
flowchart TB
subgraph LocalMachine["One ADE computer install, one channel"]
Desktop["Electron desktop app<br/>apps/desktop"]
Code["ADE Code TUI<br/>ade code"]
Shell["ade CLI<br/>typed commands"]
Brain["ADE brain<br/>always-on runtime process<br/>$ADE_HOME/sock/ade.sock"]
Bridge["Desktop bridge<br/>~/.ade/sock/desktop-bridge.sock"]
end
Desktop -->|"local RPC attach"| Brain
Code -->|"local RPC attach"| Brain
Shell -->|"local RPC attach"| Brain
Brain -->|"Electron-only actions"| Bridge
Bridge -->|"WebContentsView, screenshots, browser state"| Desktop
subgraph ProjectState["Project .ade state"]
Database[".ade/ade.db<br/>SQLite + cr-sqlite"]
Lanes[".ade/worktrees/*<br/>lane worktrees"]
Artifacts[".ade/artifacts + cache<br/>proof, transcripts, packs"]
end
Brain --> Database
Brain --> Lanes
Brain --> Artifacts
IOS["ADE Mobile<br/>controller client"] <-->|"machine pairing + sync WebSocket<br/>catalog, changesets, commands"| Brain
DesktopRemote["Desktop window<br/>SSH-bound client"] <-->|"ade rpc --stdio"| RemoteRuntime["Remote runtime transport<br/>uploaded ade-* binary"]
RemoteRuntime --> RemoteProject["Remote project .ade state"]
┌───────────────────────────────┐
│ apps/web (marketing + DL page)│
└───────────────────────────────┘
┌───────────────────────────────────────────────┐
│ apps/ade-cli (BRAIN + RUNTIME) │
│ ─────────────────────────────────────────────│
│ ADE brain process │
│ - always-on runtime for one channel │
│ - listens on $ADE_HOME/sock/ade.sock │
│ - login service (launchd / systemd / Win) │
│ - multi-project RPC + project registry │
│ - sync service (cr-sqlite over WebSocket) │
│ - credential store, agent registry │
│ - dispatches CLI runtimes: │
│ claude · codex · opencode · cursor │
│ - SQLite + cr-sqlite per project (.ade/ade.db)│
│ ─────────────────────────────────────────────│
│ Also exposes: │
│ - `ade rpc --stdio` single-session over SSH │
│ - `ade <command>` typed CLI surface │
│ - `ade code` terminal Work client (Ink+React)│
└───────────────────────────────────────────────┘
▲ ▲ ▲ ▲
│ local │ local │ WebSocket │ stdio over
│ local RPC │ local RPC │ │ SSH
│ │ │ │
┌──────────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────────┐
│ apps/desktop │ │ ade code TUI │ │ apps/ios │ │ apps/desktop │
│ (Electron, multi-│ │ (apps/ade-cli│ │ SwiftUI │ │ window bound to a│
│ window — one │ │ /tuiClient) │ │ controller│ │ remote brain │
│ window/project) │ │ │ │ (never │ │ (RemoteConnection│
│ LocalRuntime- │ │ defaults to │ │ runs │ │ Pool, bootstrap- │
│ ConnectionPool │ │ machine brain│ │ agents) │ │ uploads bundled │
│ │ │ │ │ │ │ runtime binary) │
└──────────────────┘ └──────────────┘ └──────────┘ └──────────────────┘
All clients share the brain's view of
projects, lanes, agent chats, work sessions,
processes, sync.
│
▼
┌─────────────────────────┐
│ User code: git worktrees│
│ under .ade/worktrees/ │
└─────────────────────────┘
Live runtime state is replicated between paired devices through cr-sqlite changesets carried over WebSocket; the sync service runs inside the ADE brain, not in the desktop app. ADE Mobile pairs with a machine — typically the user's primary desktop-class machine — and receives that machine's project catalog from the brain. The sync WebSocket is one brain-level listener on a stable port (default 8787, with preferred-port retry before any scan); when the hosted project switches, the new project's host service adopts the connected phones instead of dropping them. A second desktop on the same network is also a client of that brain, not a peer host. A desktop window can be re-pointed at a runtime on a remote machine over SSH; the binding is per-window, so the same Electron process can drive a local project in one window and an SSH-bound project in another. The remote path starts ade rpc --stdio on the remote and routes runtime actions through the same multi-project JSON-RPC surface. See features/remote-runtime/README.md.
Source code crosses machines through plain git. ADE does not own a git server.
Product positioning and workflows live in docs/PRD.md. This document is strictly technical.
apps/ade-cli/ contains the brain process, manual runtime entry points, the ade CLI surface, and the ade code terminal client. It ships as one Node binary that runs in several modes.
Run modes:
- Brain — the normal mode. Boots the multi-project JSON-RPC server, hosts the per-project services on demand, serves sync, and listens on the channel's local endpoint (POSIX:
$ADE_HOME/sock/ade.sock; Windows: a named pipe under\\.\pipe\ade-<hash>, with the hash derived inapps/desktop/src/shared/adeRuntimeIpc.ts). On POSIX the headless RPC socket directory is created0700and the socket itself chmodded0600so only the owning user can connect (named pipes skip the chmod). Installable / removable as a login service withade brain start/ade brain stop(per-platform installers inapps/ade-cli/src/serviceManager/). - Manual runtime (
ade runtime run) — starts a foreground runtime process on an explicit endpoint. Sync is always off so it cannot claim brain authority; use a separateADE_HOMEwhen you also want full machine-state isolation. - Single-session CLI —
ade <command>connects to the local brain over the machine endpoint, dispatches one project-scoped action, and exits. With--headless, the CLI bootstraps a project's services directly from the repository instead of going through the machine brain — used in CI and for one-off scripts. - SSH stdio bridge (
ade rpc --stdio) — runs a single-session JSON-RPC runtime over stdin/stdout. This is what desktop'sRemoteConnectionPoolexecs over SSH afterbootstrapRemoteRuntimehas uploaded a matchingade-<platform-arch>binary. Exits when the SSH channel closes. - Terminal client (
ade code) — launches the Ink + React Work chat (apps/ade-cli/src/tuiClient/). Defaults to attaching to the machine brain and will start it if the endpoint is missing.ade --socket /path coderequires a specific endpoint;ade code --embeddedkeeps the in-process runtime fallback explicit.
Multi-project RPC. The runtime exposes runtime-scoped methods (projects.list/add/remove/touch, sync.*, runtime/info, machineInfo.get, runtimeEvents.subscribe/unsubscribe) directly. Project-scoped operations dispatch through ade/actions/call with a projectId. Per-project services are spun up lazily by ProjectScopeRegistry (apps/ade-cli/src/services/projects/projectScope.ts) which calls createAdeRuntime({ projectRoot, ... }) the first time a project is touched. The project registry (projectRegistry.ts) is the durable list of known projects; machineLayout.ts resolves machine-wide paths under ~/.ade/. Wire formats live in apps/ade-cli/src/multiProjectRpcServer.ts.
Runtime-side services (under apps/ade-cli/src/services/):
| Directory | Role |
|---|---|
projects/ |
Project registry, project scope (per-project runtime), machine layout. |
sync/ |
Sync service, peer client, device registry, pairing store, PIN store, sync protocol, remote command service, Tailscale CLI resolver. The sync service now lives here; desktop's old in-process sync host is disabled by default (env-gated ADE_ENABLE_DESKTOP_SYNC_HOST=1 for diagnostics only). |
credentials/ |
Per-machine credential store. |
agentRegistry.ts |
Per-machine agent registry. |
Service managers. apps/ade-cli/src/serviceManager/installLaunchd.ts (macOS), installSystemd.ts (Linux), installWindows.ts (Windows) register the brain as a login-time service. index.ts is the platform router; common.ts carries shared types (ServiceManagerResult, ServiceManagerStatusResult).
Session identity. The runtime resolves caller role from ADE context env vars and command flags. Role vocabulary: cto, orchestrator, agent, external, evaluator.
Action surface. First-class command families cover lanes (including ade lanes link-linear-issue / detach-linear-issue for post-creation Linear issue linking, and ade lanes create-from-linear / batch-create-from-linear to spin up one or many issue lanes — optionally launching an agent chat with --start-chat), git, diffs, files, PRs, runs, shells, chats (including ade chat create --from-linear-issue <id>, ade chat attach-linear-issue / detach-linear-issue / linear-issues for session-scoped issue attachment), agents, CTO, Linear (the write bridge an attached CLI agent uses: ade linear attach / detach / issues / issue / comment / set-state / assign / label, with --this-session resolving the issue id from $ADE_LINEAR_ISSUE_IDS so a launched agent needs no Linear token — see features/linear-integration/README.md), tests, proof, settings, the iOS Simulator (ade ios-sim / ade ios / ade simulator — see features/ios-simulator/README.md), the Cursor Cloud bridge (ade cursor cloud agents | runs | artifacts | repos | models | me — talks directly to @cursor/sdk without going through the ADE runtime endpoint), the App Control bridge for Electron apps (ade app-control / ade app / ade electron — launch, connect, stop, status, screenshot, snapshot, inspect, select, click, type, scroll, key, targets, attach, logs, terminal write, terminal signal — see features/computer-use/app-control.md), the chat-scoped terminal (ade terminal list / read / write / signal / active), and a generic ade actions run <domain.action> escape hatch for every registered ADE service action. The action allow-list adds three domains for these surfaces: app_control (every public method on AppControlService), terminal (list, read, write, signal, activeForChat against ptyService), and named iOS Simulator actions for launch, live view, inspection, input, and Preview Lab workflows.
Proof subcommands — ade proof capture (alias of screenshot), ade proof attach <path>, ade proof record, ade proof launch, ade proof interact, ade proof list/status/environment/ingest. attach infers the artifact kind from the file extension and routes through ingest_computer_use_artifacts with backendStyle: "manual". Capture-style commands set preferHeadless: true on the plan so the connection layer drops to headless mode unless --socket is explicitly requested. All proof subcommands accept --owner-kind / --owner-id (with chat and pr aliases) to layer an explicit owner on top of the inferred session identity.
Bundled runtime artifacts. Per-platform ade-<platform-arch> binaries plus their native dep tarballs live under apps/desktop/resources/runtime/, with packaged ADE CLI resources providing the ptyHostWorker.cjs used by remote terminals. release-core.yml builds the cross-platform set; bootstrapRemoteRuntime uploads missing or hash-mismatched artifacts on first SSH connect from the desktop client.
Headless install. A standalone runtime can be installed on a headless machine without going through the desktop installer — but note that releases currently publish macOS desktop assets only, so the runtime binaries + install.sh are not on the release page (their publish block in release-core.yml is commented out). Remote machines reached over SSH don't need this path: bootstrapRemoteRuntime uploads the desktop app's bundled runtime artifacts. When the publish block is re-enabled:
curl -fsSL https://github.com/arul28/ADE/releases/latest/download/install.sh | shUse ADE_VERSION=vX.Y.Z for a pinned release or ADE_INSTALL_DIR to choose the destination directory.
Install + PATH wiring (when the desktop ships ade). On macOS / Linux the desktop installer drops the launcher at $HOME/.local/bin/ade; on Windows it lands at %LOCALAPPDATA%\ADE\bin\ade.cmd. After a successful install on Windows, the packaged .cmd installer adds the target directory to HKCU Environment\Path when needed and broadcasts an environment-change notification. After a successful install on POSIX, ensureUserBinOnShellPath appends a marked export PATH="$HOME/.local/bin:$PATH" block to the user's shell rc (.zshrc for zsh, .bashrc for bash, .profile otherwise) iff (a) the install dir isn't already on the inherited PATH and (b) the file doesn't already contain the marker / line / target dir. The install IPC reply tells the renderer which profile was edited so the Settings/Onboarding UI can prompt the user to open a new terminal or source it.
Windows packaging. The installer lays down ade-cli-windows-wrapper.cmd plus an ade-cli-install-path.cmd helper alongside the bundled Electron Node runtime. The helper installs %LOCALAPPDATA%\ADE\bin\ade.cmd, updates the user PATH when needed, and then ade works from a new normal Windows shell without a global Node install. See §14.4 for the packaging flow.
Desktop bridge endpoint. The ADE runtime runs apps/ade-cli/dist/cli.cjs under ELECTRON_RUN_AS_NODE=1, so it has no access to renderer-side Electron APIs (WebContentsView, nativeImage, session, …). A small set of services own real desktop UI and therefore cannot live in the runtime — most notably BuiltInBrowserService, which drives the Browser pane's WebContentsView. The desktop main process hosts those services and exposes them to the runtime over a side-channel JSON-RPC Unix-domain socket / named pipe.
The endpoint path is resolved by apps/ade-cli/src/services/projects/machineLayout.ts: <adeHome>/sock/desktop-bridge.sock on macOS / Linux (e.g. ~/.ade/sock/desktop-bridge.sock stable, ~/.ade-beta/sock/desktop-bridge.sock beta), and \\.\pipe\ade-desktop-bridge[-<channel-suffix>] on Windows. ADE_DESKTOP_BRIDGE_SOCKET_PATH overrides it for dev launches against a non-default ADE home. The server lives in apps/desktop/src/main/services/builtInBrowser/desktopBridgeServer.ts, wired up from main.ts right after builtInBrowserService is constructed and torn down with it on app shutdown. The runtime-side proxy is apps/ade-cli/src/services/builtInBrowser/desktopBridgeClient.ts; createAdeRuntime in bootstrap.ts assigns it to runtime.builtInBrowserService so the existing action registry slot resolves transparently (skipped when runtimeProfile === "chat"). Both sides share the same method allowlist: getStatus, showPanel, setBounds, navigate, createTab, switchTab, closeTab, reload, goBack, goForward, stop, startSession, listSessions, endSession, observe, getTrace, click, typeText, dispatchKey, scroll, fill, clear, wait, startInspect, stopInspect, captureScreenshot, selectPoint, selectCurrent, clearSelection, claim.
Today only the built_in_browser domain rides this bridge; the pattern is generic and other Electron-only domains can be added the same way. The client lazy-connects on first call and reconnects on the next call after any failure. When no desktop is running, each call surfaces a clear Desktop browser bridge not running at <path>. Open ADE Desktop with a project to enable \ade browser` commands.` error and every other runtime domain stays functional. This is distinct from the retired renderer-hosted RPC mode used before the multi-project runtime: the ADE runtime still owns the full action surface, and the bridge is narrowly scoped to services that physically require an Electron renderer host.
The desktop app is a client of the runtime. It owns a trusted main process, a narrow typed preload bridge, the React renderer, and the shared TypeScript contracts that the whole monorepo (including the ADE CLI runtime) consumes — but the data plane it operates on lives in the ADE runtime.
| Directory | Role |
|---|---|
apps/desktop/src/main/ |
Node process with full OS access. Hosts windows, registers IPC handlers, routes runtime-backed APIs through local/remote runtime pools, spawns the local ADE runtime when needed, and owns Electron-only services that cannot run inside the runtime. Entry: main.ts. |
apps/desktop/src/preload/ |
Typed bridge. Entry: preload.ts. Uses contextBridge.exposeInMainWorld("ade", { ... }). Runtime-backed APIs route through LocalRuntimeConnectionPool (local) or RemoteConnectionPool (SSH-bound window); file APIs are strict once a local/remote runtime is bound, while usage/budget reads only route to runtime for remote-bound windows. During project switches, mutating runtime/sync calls that target the ambiguous active binding are blocked, read-only calls avoid refreshing stale bindings, active remote opens can be awaited before retrying reads, and remote lane preview URLs are localized through desktop-owned TCP forwards. Explicitly targeted work can pass an OpenProjectBinding pin through callPinnedRuntimeAction to route to the captured project during a switch, used by detached draft launches and rollback. |
apps/desktop/src/renderer/ |
React 18 SPA. No Node access, no filesystem access, no direct process/network. Everything goes through window.ade. Entry: main.tsx. |
apps/desktop/src/shared/ |
Types, IPC channel constants (ipc.ts), model registry (modelRegistry.ts), keybindings, and other DTOs. Imported by both desktop and apps/ade-cli. New runtime-facing types live in shared/types/remoteRuntime.ts and shared/types/core.ts. |
apps/desktop/src/generated/ |
Build-time generated code (e.g., bootstrap SQL snapshots). |
apps/desktop/src/test/ |
Shared vitest setup and fixtures. |
apps/desktop/src/types/ |
Ambient type declarations. |
Multi-window shell. main.ts hosts multiple BrowserWindow instances; opening another project opens it in a dedicated window. Each window has its own runtime binding (local pool or a specific remote target). External controllers — for example a ade code TUI — can drive desktop window navigation via the app/navigate JSON-RPC method against the runtime; the desktop's IPC tracing carries window ID so logs distinguish which renderer surface invoked a channel.
Runtime binding pools.
apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts— desktop-side client for the local brain endpoint. Spawns or attaches to the machine endpoint, registers local projects withprojects.add, dispatches local runtime actions, applies short per-call timeouts for project registration / file actions / event polling, emits alocal_runtime.action_slowwarning log when an action call exceeds 500 ms or throws (the log breaks the total intoensureProjectMs/connectMs/daemonCallMsso a stalled renderer call is debuggable before the IPC timeout fires), and best-effort installs the background service in packaged builds. Local project windows use this binding consistently outside unit tests.apps/desktop/src/main/services/remoteRuntime/— SSH-bound runtime pool.remoteTargetRegistry.tsstores saved machines under~/.ade/secrets/remote-machines.json(manual host plus an optionalroutes[]of Tailscale / Bonjour / manual addresses with per-routelastSucceededAtand manual-disconnect state);sshTransport.tshandles ssh-agent / key based transport with bounded connect/exec timeouts, normalized handshake errors, disabled SSH keepalives, and alternate routes ranked by most-recent success;remoteBootstrap.tsdoes first-connect runtime upload + version/hash negotiation against the bundledade-<platform-arch>binary, native deps, and PTY host worker, falls back across alternate ADE channel homes (.ade,.ade-alpha,.ade-beta) when the preferred home has no compatible runtime, treats version / channel / capability skew ascompatibilityWarningsinstead of fatal errors, and records which route succeeded;remoteConnectionPool.tskeeps the per-window remote runtime binding alive, gatesprojects.*runtime calls against the connection'scapabilities.machineProjectsflags (missing capabilities reject the call with a self-describing error), reconnects safely on read-only actions (get*/list*/read*/search*/diagnosticsGet*and a small allowlist), owns local TCP forwards for remote preview ports, memoizes optional-action fallbacks, and emits eviction notifications when SSH or the JSON-RPC client closes;remoteConnectionService.tslistens for those evictions, marks targets aserror, preserves explicit manual disconnects, pauses automatic reconnect after repeated implicit failures, surfaces the capabilities + compatibility warnings on everyRemoteRuntimeConnectionStatus, and re-probes saved connections onpowerMonitor.resume/unlock-screen;runtimeRpcClient.tsis the JSON-RPC client (per-call timeouts tear the connection down so the pool reconnects rather than dangling the request, and remote errors are now formatted with the original method name plus the JSON-RPCcode/message/datafor clearer diagnostics);runtimeDiscovery.tsruns Bonjour + Tailscale in parallel and returns{ machines, diagnostics }so a missing or stucktailscaleCLI does not silently swallow LAN discovery.
Build outputs (configured in apps/desktop/tsup.config.ts):
| Entry | Source | Purpose |
|---|---|---|
main/main.cjs |
src/main/main.ts |
Electron main process |
main/packagedRuntimeSmoke.cjs |
src/main/packagedRuntimeSmoke.ts |
Post-package smoke test for PTY spawn, Claude SDK init, Codex availability, and ADE CLI readiness. |
preload/preload.cjs |
src/preload/preload.ts |
Renderer bridge. |
Terminal-native Work chat client (Ink 7 + React 19) for agents and power users who live in a shell, built into apps/ade-cli/src/tuiClient/. Its UI dependencies live under apps/ade-cli and are intentionally independent of the desktop renderer's React stack. It is a peer of the desktop client, not a wrapper around it: it speaks the same multi-project JSON-RPC surface and binds to an ADE runtime the same way.
- Attached mode (default): connects to
$ADE_HOME/sock/ade.sock, or to an explicit endpoint passed on the parentadeinvocation. Starts the brain if the endpoint is missing. - Embedded mode:
--embedded/--headlessruns the sharedapps/ade-cliservices in-process without going through a machine brain. Used when no brain endpoint or manual runtime endpoint is reachable.
Shared chat DTOs are imported from apps/desktop/src/shared/types/* (never the renderer barrel) so npm run typecheck in apps/ade-cli covers both typed commands and the TUI. Entry: apps/ade-cli/src/tuiClient/cli.tsx → apps/ade-cli/dist/tuiClient/cli.mjs, loaded by ade code. The built TUI bundle is intended to run in isolation: tsup bundles its Ink/xterm/highlight dependencies and injects ESM shims for __dirname / __filename; both apps/ade-cli/scripts/verify-built-cli.mjs and the desktop artifact validators smoke-import it and run runAdeCodeCli(["--help"]). The TUI can hand off to a desktop window via the app/navigate JSON-RPC method when a desktop client is attached to the same runtime.
Native SwiftUI app acting as a controller. It pairs with an ADE machine over WebSocket and reads live state from a local cr-sqlite-backed SQLite database that mirrors the project's ade.db. The phone never runs agents.
- Stack: native SwiftUI +
SQLite3C API + iOS system SQLite. - CRDT: pure-SQL CRR emulation layer (trigger-based change tracking) since iOS blocks
sqlite3_load_extension()/sqlite3_auto_extension(). Changesets are wire-compatible with desktop cr-sqlite. - Core services:
Database.swift,SyncService.swift,KeychainService.swift. - Shipped tabs: Lanes, Files, Work, PRs, CTO, Settings.
- Shipped: one Lock Screen widget for prioritized agent, PR, sync, offline, and idle status.
- Planned: Automations, Graph, History tabs; iPad layout; Spotlight.
- Target: iOS 26+, iPhone + iPad.
A Vite/React SPA that serves the public marketing site, download page, and the deeplink landing page. Five pages: HomePage, DownloadPage, OpenPage, PrivacyPage, TermsPage. Independent package (ade-web), deployed via Vercel (apps/web/vercel.json). Not a runtime dependency of the desktop app. Shared-origin with the Mintlify docs site (docs.json at repo root).
The /open route is the HTTPS half of the ADE deeplink scheme (https://ade-app.dev/open?type=...&...). apps/web/api/open.ts is a Vercel serverless function that self-fetches index.html, rewrites OpenGraph + Twitter meta tags from the query params so chat-app unfurlers (Slack, Discord, iMessage, Gmail, Linear) show a rich card without executing JavaScript, then hands the SPA over to OpenPage which attempts the ade:// upgrade in the browser and falls back to an install/marketing card if no handler is registered. Supported targets include lanes, Work sessions, repo branches, PRs, and Linear issues. See features/deeplinks/README.md.
ADE uses Node's native node:sqlite driver (no better-sqlite3 dependency) with a vendored cr-sqlite loadable extension:
- Engine source:
apps/desktop/src/main/services/state/kvDb.ts(schema bootstrap, CRR enablement, sync API) andcrsqliteExtension.ts(extension loader). Both the desktop main process and the ADE CLI runtime import the same engine module from here; they do not maintain parallel schemas. The database is owned by whichever process opened it first for a given project; in normal desktop operation that owner is the ADE runtime, while desktop in-process users are limited to pre-binding flows, diagnostics, tests, and Electron-only work. - Database file:
<project_root>/.ade/ade.db. - WAL mode handles durability;
flushNow()is a no-op. - CRRs: eligible tables are marked via
SELECT crsql_as_crr('table_name')at startup. Virtual/internal tables (sqlite_%,crsql_%) are excluded. Marking is dynamic — new tables are picked up automatically unless excluded. - Sync API (
AdeDb.sync):getSiteId(),getDbVersion(),exportChangesSince(version),applyChanges(changes). Used by the sync transport. - Merge semantics: last-writer-wins per column with Lamport timestamps; each device has a site ID at
.ade/secrets/sync-site-id. - Engineering rule under CRR retrofit: app-level
ON CONFLICT(...)upserts must target PK only; secondary UNIQUE constraints do not survive CRR marking.
Schema bootstrap in kvDb.ts creates ~103 tables. Anchor tables for agents reading this doc:
| Table | Purpose |
|---|---|
projects |
One row per opened repo. Keyed by root_path. |
lanes |
Worktree-backed units of work. Types: primary, worktree, attached. Supports parent/child stacks, run binding, color/icon/tags. |
local_worktree_residual_cleanups |
Machine-local lane-delete cleanup debt for residual managed worktree directories. Stores absolute paths and is excluded from CRR replication because only the runtime on that machine can safely retry removal. |
terminal_sessions |
Tracked PTY sessions per lane with transcript path and head SHAs. The chat_session_id column (indexed) marks terminals owned by a chat (chat terminal drawer, App Control launch terminal); ptyService exposes them through the ade.terminal.* IPC and the terminal ADE action domain. The owner_pid column (indexed) identifies the ADE OS process that owns the live runtime for the row — cross-process reconcile/dispose paths check it before sweeping so concurrent surfaces don't mark each other's live sessions dead. See §3.5. |
runtime_processes |
Machine-local process-liveness registry. Every ADE process (desktop main, brain process, TUI runtime) inserts a row on boot keyed by the process incarnation (pid, started_at) and refreshes last_seen on a 5 s heartbeat. The table is excluded from CRR replication because PIDs are only meaningful on the current OS; reconcile / dispose paths cross-reference terminal_sessions.owner_pid and owner_process_started_at against locally known and live rows to tell "row whose local owner crashed" from "row a sibling process is actively managing" without detaching sessions owned by another synced machine. See §3.5. |
session_deltas |
Post-session diff stats + touched files + failure lines. Input to pack generation. |
operations |
Audit log of every significant mutation (git, pack updates). Pre/post HEAD SHAs enable undo. |
process_definitions / process_runtime / process_runs |
Managed-process lifecycle (derived from ade.yaml). |
test_suites / test_runs |
Declared test suites and their execution history. |
pull_requests / pr_review_threads / pr_checks |
GitHub PR projections with queue and stack metadata. |
integration_proposals |
PR merge-plan simulations. Stores source lanes, pairwise results, sequential resolution state, optional adopted merge target (preferred_integration_lane_id), and merge-target drift snapshot (merge_into_head_sha). |
computer_use_artifacts + computer_use_artifact_links |
Canonical proof-artifact records and cross-domain ownership. |
devices + sync_cluster_state |
Device registry and singleton host-authority row (host is brain_device_id internally; legacy naming). |
local_crr_change_suppressions |
Local-only (excluded from CRR replication) high-water marks per (table_name, site_id). AdeDb.sync.exportChangesSince filters local-site rows for any listed table at or below through_db_version so a viewer-join wipe of devices / sync_cluster_state cannot leak DELETE rows back to the host. See §13.1. |
kv |
Generic key-value store for UI layout, config trust hashes, misc settings, and short-lived recovery records such as agent-chat-parallel-launch:<projectRoot>:<laneId>. |
Types for these tables are split into domain modules under apps/desktop/src/shared/types/. The barrel index.ts re-exports core, models, git, lanes, conflicts, prs, files, sessions, chat, config, automations, packs, budget, usage, and more. Feature docs under docs/features/ call out the table subsets that are load-bearing for each surface.
<project-root>/
├── .ade/
│ ├── .gitignore # Tracked; ignores machine-local ADE state
│ ├── ade.yaml # Shared (tracked): processes, stacks, tests, templates
│ ├── local.yaml # Personal overrides (ignored)
│ ├── local.secret.yaml # Secret integration config (ignored)
│ ├── ade.db # SQLite + cr-sqlite (runtime, ignored)
│ ├── worktrees/<slug>-<uuid>/ # Lane worktrees (ignored)
│ ├── transcripts/ # PTY transcripts (ignored)
│ ├── cache/ # Runtime scratch (ignored)
│ ├── artifacts/ # Pack exports, history artifacts (ignored)
│ ├── cto/
│ │ ├── identity.yaml # Local CTO identity (ignored)
│ │ ├── CURRENT.md # Running status markdown (ignored)
│ │ └── daily/<YYYY-MM-DD>.md
│ ├── agents/<slug>/ # Per-worker identity and daily logs (runtime, ignored)
│ ├── templates/ # Lane and automation templates (tracked when human-authored)
│ ├── skills/ # Exported skill markdown (tracked when human-authored)
│ ├── workflows/linear/ # Linear workflow config (tracked when present)
│ ├── project-icons/ # Imported project icon overrides (tracked when ade.yaml.iconPath points at one)
│ ├── ade.sock # Unix socket for ADE RPC (runtime)
│ └── secrets/ # Machine-local secret material (ignored)
│ ├── github/*.bin # safeStorage-encrypted tokens
│ ├── sync-site-id
│ ├── sync-device-id
│ └── sync-bootstrap-token
└── ~/.ade/ # Global state (user profile directory)
├── global-state.json # Recent projects list
└── logs/ # Main-process structured logs
Portability buckets (intentionally distinct):
- Git-tracked shared scaffold —
.ade/.gitignore,ade.yaml, human-authoredtemplates/**,skills/**,workflows/linear/**,project-icons/**. This is the only.ade/subset that flows through normal clone/pull. The shared.ade/.gitignoreis now*with explicit allowlist entries for those scaffold files (so the next time someone touches.ade/from a fresh tool the runtime state stays out of git automatically). - ADE sync state — the replicated
ade.dbtables that flow through cr-sqlite over WebSocket when devices join the same host. - Machine-local runtime — worktrees, caches, transcripts, artifacts, secrets, sockets, and generated context markdown. Never leaves the device.
Project scaffold modes. initializeOrRepairAdeProject(projectRoot, { mode }) controls whether a project gets the full shared scaffold or stays local-only:
mode: "shared"always materializes the canonical files (.ade/.gitignore,ade.yaml, the tracked placeholder.gitkeeps, plus local-only CTO identity state) and scrubs any leftover.ade/ignore lines from.gitignore/.git/info/exclude. Triggered automatically fromcreateLocalProject, every shared-config save, and any helper that callsensureSharedAdeProjectScaffold(projectRoot)(e.g.setProjectIconOverrideFromSelection,linearWorkflowFileService.save).mode: "auto"(the default foropenProject) keeps the project local-only when no shared scaffold files exist yet — it ensures.git/info/excludehas a.ade/entry so a brand-new clone or a personal-only setup never accidentally promotes runtime state into git, and only flips to the shared layout when shared scaffold files are already present (or after a save call promotes them).mode: "local"is reserved for force-local repair flows.
ADE is a multi-process system on a single machine: the desktop main process, the brain process, and any number of manual/TUI runtimes can all be live against the same project DB simultaneously. To prevent one process from disposing or reconciling another's live PTYs and SDK sessions, every long-lived row gets an owner_pid / owner_process_started_at identity and every process maintains a heartbeat in the machine-local runtime_processes table.
apps/desktop/src/main/services/runtime/processRegistryService.ts is the per-process registrar.
- On
start()it inserts/refreshes its own process-incarnation row inruntime_processes(pid,role, optionalprojectRoot,startedAt,lastSeen) and runs an idempotentpruneStale()over rows older than 10× the liveness window. - A 5 s heartbeat (
heartbeatIntervalMs, configurable) writeslast_seenso siblings can see this process is alive. The intervalunref()s so it never blocks shutdown. - Liveness checks (
isPidLive(pid),listLivePids(),listLiveProcessIdentities()) consider a row live whenlast_seenis withinlivenessWindowMs(default 15 s = 3× heartbeat) so a single missed heartbeat doesn't false-positive a sibling as dead.listKnownPids()andlistKnownProcessIdentities()expose all locally recorded owners regardless of heartbeat age. The registrar's own pid is always reported as live and known. stop()clears its row outright on graceful shutdown so siblings don't have to wait the liveness window to free up ownership.
ptyService.create() records processRegistry.pid and processRegistry.startedAt on the new terminal_sessions row's owner columns. sessionService.reconcileStaleRunningSessions() accepts both live owners and known local owners: rows with live local owners are left alone, rows with known but no-longer-live local owners can be swept to detached, and rows with unknown owner identity are preserved because they may have been synced from another machine. Dispose paths run the same ownership check before tearing down runtimes a sibling still manages.
Roles are open-ended strings; today's vocabulary is desktop-main, ade-serve-daemon for the brain process role, and tui-runtime. The desktop main process constructs the registry in main.ts and threads it into ptyService, sessionService, and reconcile callers via the per-project context. The ade-serve-daemon literal is retained in live runtime_processes rows until the internal role vocabulary is migrated.
- Schema is defined idempotently —
CREATE TABLE IF NOT EXISTS+CREATE INDEX IF NOT EXISTS. - One-time schema-compat migration at startup: retrofits
NOT NULLon PKs and strips UNIQUE/FK constraints incompatible with cr-sqlite CRRs. A pre-cr-sqlite backup (<db>.pre-crsqlite-w1.bak) is written on first CRR enablement. - Feature migrations add columns via
ALTER TABLE ADD COLUMN, wrapped bycrsql_begin_alter/crsql_commit_alterto stay CRR-safe. - Targeted per-domain migrations live in
kvDb.migrations.test.ts, which covers the consolidated upgrade path for orchestration/worker tables plus later CRR-safe schema cleanups. - The canonical iOS bootstrap schema is exported from desktop
kvDb.tstoapps/ios/ADE/Resources/DatabaseBootstrap.sqlso iOS stays schema-compatible.
Service entry points live under apps/desktop/src/main/services/ai/. The subsystem has three parts: provider-routed execution, permission profiles, and ADE CLI-backed tool surfaces.
- Router —
aiIntegrationService.tsresolves a task → model → provider class and dispatches. - Model registry —
apps/desktop/src/shared/modelRegistry.tsis the single source of truth. EachModelDescriptorcarries identity (id,shortId,providerRoute,providerModelId), capabilities, pricing, context sizing, auth type (cli-subscription,api-key,openrouter,local), and optionalharnessProfile/discoverySourcefor safety metadata. - Classes:
- CLI-wrapped (Claude via
@anthropic-ai/claude-agent-sdk, Codex CLI via@openai/codex-sdk) — spawned as subprocesses; Claude uses the SDKquery()stream with ADE's async input pump and bundled Claude Code binary, while Codex uses its app-server JSON-RPC bridge. Authentication inherits from the user's own CLI login. ADE context is exposed through environment variables, and agents can call back into ADE with theadeCLI. - API-key / OpenRouter (Anthropic, OpenAI, Google, Mistral, DeepSeek, xAI, Groq, Together AI, OpenRouter) — routed through the OpenCode server (
opencodebinary, user-installed or bundled). Discovery viaopenCodeInventory.ts; replaces dynamic portion of the registry. - Local (Ollama, LM Studio, vLLM) — OpenAI-compatible local endpoints through OpenCode. Discovery via
localModelDiscovery.ts.
- CLI-wrapped (Claude via
- Detection pipeline:
authDetector.ts— detects subscriptions, API keys, OpenRouter, local endpoints.providerCredentialSources.ts— reads Claude OAuth credentials, Codex tokens, macOS Keychain.providerConnectionStatus.ts— builds theAiProviderConnectionssnapshot surfaced to the renderer.providerRuntimeHealth.ts— per-provider health (ready,auth-failed,runtime-failed).claudeRuntimeProbe.ts— lightweight SDK probe on force-refresh to distinguish bundled Claude binary readiness from authentication readiness.modelsDevService.ts— non-blocking 6-hour refresh that enriches pricing and context-window metadata in the registry frommodels.dev.
- ADE action status surface:
ai.getStatus,ai.listApiKeys, andai.getOpenCodeRuntimeDiagnosticsexpose the same provider readiness, stored-key, and OpenCode runtime health data to renderer settings andade codemodel setup through the shared ADE action registry. - Fallback: if no usable provider is present, ADE runs in guest mode — deterministic features (packs, diffs, conflicts) continue; AI surfaces are disabled with explanatory UI.
Permission configuration is class-based, not provider-bucketed:
permissionConfig.cli— for CLI-wrapped models. Claude usesclaudePermissionMode(default,auto,acceptEdits,bypassPermissions,plan); Codex usesapprovalMode(untrusted,on-request,on-failure,never) +sandboxPermissions(read-only,workspace-write,danger-full-access).permissionConfig.inProcess— for API/local models. ADE-defined planning/coding tool profiles constitute the full tool surface.- ADE-owned tools (repo mutation, context export, proof registration) always enforce ADE's own permission and policy layers regardless of provider mode — preserving the audit boundary.
- Sandbox budgets:
maxBudgetUsdper-session cap for Claude; per-task daily budgets for narratives, PR descriptions, and terminal summaries.
Agent tools are split by domain:
| File | Domain |
|---|---|
ai/tools/universalTools.ts |
Mutating tools (bash, writeFile, editFile), read/search tools, web tools, todos, and ask-user. |
ai/tools/workflowTools.ts |
Workflow interaction tools. |
ai/tools/ctoOperatorTools.ts |
CTO-only operator tools. |
ai/tools/linearTools.ts |
Linear integration tool surface. |
ai/tools/webFetch.ts / webSearch.ts |
Outbound web access. |
ai/tools/readFileRange.ts / globSearch.ts / grepSearch.ts |
Read-only file tools shared across all roles. |
ai/tools/editFile.ts |
Edit-path tool wired to ADE-controlled write flow. |
ai/tools/systemPrompt.ts |
Base system prompt; adapts wording based on exposed tool names. |
ADE CLI is the cross-process action surface. Workers spawned as CLI children inherit ADE context env vars and can call the ade command to invoke ADE-owned actions layered on top of their native provider tools.
apps/desktop/src/shared/modelRegistry.ts + apps/desktop/src/shared/modelProfiles.ts:
MODEL_REGISTRY— static CLI-wrapped entries + dynamically populated API-key/local entries. The Claude block starts with the new Opus 4.8 1M entry (anthropic/claude-opus-4-8, display nameClaude Opus 4.8 1M, provider modelclaude-opus-4-8, 1,000,000 context / 128,000 max output), while preserving the existing Claude Opus 4.7 and Opus 4.7 1M entries (anthropic/claude-opus-4-7-1m, aliasesopus[1m]/claude-opus-4-7[1m]). Opus entries advertise the fulllow|medium|high|xhigh|maxreasoning tier set andcostTier: "very_high".ModelDescriptor.serviceTiers?: string[]advertises optional service tiers (today:"fast", set on Codex GPT 5.4 / 5.5 entries and dynamic Cursor SDK/CLI rows) that the UI's Fast Mode toggle keys off. Codex maps it to the JSON-RPCserviceTierargument; Cursor SDK maps it through discovered model parameters, and Cursor CLI launches use the matching fast model alias when present.ModelProviderGroup="claude" | "codex" | "opencode" | "cursor" | "droid". Cursor and Droid each have their own top-level provider group used by the model picker, identity routing, and tracked CLI provider catalog.- Helpers:
getModelById,getModelPricing,updateModelPricingInRegistry,replaceDynamicOpenCodeModelDescriptors,resolveProviderGroupForModel,resolveModelDescriptorForProvider,getRuntimeModelRefForDescriptor,modelSupportsServiceTier(descriptor, tier)/modelSupportsFastMode(descriptor). - Reasoning tier passthrough (
providerOptions.ts) maps tier strings directly to each provider's native config (thinking.type,reasoningEffort,thinkingConfig.thinkingLevel, etc.) — no arbitrary token budgets. Claude Opus registry rows advertiselow | medium | high | xhigh | max, while the global Claude quick-pick ladder remainslow | medium | high | max. Interactive chat (Terminals, Work), CTO delegation, and automation-launched agent sessions flow through the unified executor with the same permission plumbing.
Related feature docs: Chat, Agents, CTO, and Automations.
apps/desktop/src/preload/preload.ts (~8,545 lines) exposes ~550 methods on window.ade:
contextBridge.exposeInMainWorld("ade", { ... })— the only cross-isolated-world surface.- Methods are typed via TypeScript imports from
apps/desktop/src/shared/types/. - Two categories: invoke methods (
ipcRenderer.invoke(channel, args)returningPromise<T>) and event subscriptions (ipcRenderer.on(channel, handler)). contextIsolation: true,nodeIntegration: false,sandbox: false(required for preload functionality).- Global window type:
apps/desktop/src/preload/global.d.ts. window.ade.project.getDroppedPath(file)wraps Electron'swebUtils.getPathForFile()so renderer drag-drop handlers can resolve the absolute path of aFilepayload without the renderer needing Node APIs. Used by the Command Palette project browser to accept dropped folders.
apps/desktop/src/shared/ipc.ts defines the single IPC const with ~550 named channel strings in a ade.<domain>.<action> namespace:
ade.app.* # app lifecycle, clipboard text and image (writeClipboardText, writeClipboardImage, saveClipboardImageAttachment), paths, image data-URL preview (getImageDataUrl), the deeplink navigation push channel ade.app.navigate (AppNavigationRequest payloads from the ade:// protocol handler, the ade code app/navigate JSON-RPC, and the iOS deeplinks.open sync command — see features/deeplinks/README.md), and the one-way zoom push channel ade.app.zoomCommand (AppZoomCommand "in"/"out"/"reset" sent from the native View menu to the renderer's window.ade.zoom.onCommand so menu/keyboard zoom shares the in-app zoom path — display %, persistence, and the macOS traffic-light inset)
ade.project.* # project open/close/switch/state, unified local+remote recents (listRecent, key-based forget/reorder, setRecentPinned), in-app directory browser (browseDirectories, getDetail), favicon resolver/override (resolveIcon, chooseIcon, removeIcon) with local-only filesystem allowlists
ade.onboarding.*
ade.lanes.* # lane list/create/delete/stack/template/env/port/proxy/rebase
# delete pipeline: ade.lanes.delete + ade.lanes.delete.cancel
# + ade.lanes.delete.risk preflight + ade.lanes.delete.event push
# Linear linkage: ade.lanes.linkLinearIssues / unlinkLinearIssues
# (lane-scoped) + attachLinearIssueToSession /
# detachLinearIssueFromSession / listLinearIssuesForSession /
# listLinearIssuesForLaneSessions (session-scoped, backed by
# session_linear_issues; see features/linear-integration/README.md)
ade.files.* # file tree, read, write, search, watch
ade.diff.* # lane-scoped change list + per-file diff / patch (diffService)
ade.pty.* # PTY spawn/write/kill, data/exit events
ade.git.* # stage/commit/push/sync/revert/cherry-pick/stash
ade.github.* # PR list, review, merge, checks. Also exposes
# repo-scoped helpers used by the Linear setup flow:
# listRepoAutolinks / createRepoAutolink (autolink
# references like ADE-* -> Linear), listRepoLabels,
# listRepoCollaborators, listRepoIssues.
ade.prs.* # stacked PR queue, integration, rebase/issue
# resolver sessions, and merge readiness
ade.conflicts.* # risk matrix, simulation, proposals
ade.cto.* # identity, agent roster, Linear
ade.sessions.* # terminal session CRUD
ade.files.* # runtime-routed file workspace/tree/read/write/watch/search actions,
# including paginated children, Git decorations, range reads,
# blame, and local-only explicit external opens; fallback IPC handlers
# run the same fileService code.
ade.agentChat.* # agent chat sessions, model inventory, parallel launch state.
# Includes ade.agentChat.modelCatalog (provider-grouped catalog
# used by desktop + TUI + iOS ModelPickers; accepts
# `{ mode: "cached"|"refresh-stale"|"force", refreshProvider?: "opencode"|"cursor"|"droid"|"lmstudio"|"ollama" }`)
# and ade.agentChat.codex.* goal controls backed by
# Codex app-server thread/goal RPCs.
modelPicker.* # cross-surface model favorites/recents backed by
# per-project CRR tables (`model_picker_favorites`,
# `model_picker_recents`) and shared by desktop,
# TUI, and iOS sync commands.
ade.ai.* # AI integration status + provider auth (storeApiKey/deleteApiKey/getStatus/...).
# ade.ai.isOpenCodeInstalled is a cheap probe (no runtime spin-up)
# used to gate the ModelPicker OpenCode rail + Settings install CTA.
ade.ai.cursorCloud.* # Cursor background-agents bridge: listRepositories, listAgents, listRuns, getAgent, createRun, followUp, streamRun, cancelRun, archiveAgent / unarchiveAgent / deleteAgent, listArtifacts / downloadArtifact, openChat (mirror an existing cloud agent into an ADE chat session)
ade.automations.*
ade.orchestration.* # work-tab orchestration: runCreate, bundleRead, manifestReadSection,
# manifestPatch, planAppend, planWrite, spawnAgent, agentInject,
# assetRegister, claimTask, subscribe (push). Lead-only planning
# and validation transitions are service methods exposed through
# orchestration runtime tools, not raw renderer IPC patches.
# Preload bridge in
# preload/orchestrationBridge.ts; renderer consumes via
# components/orchestration/orchestrationDataSource.ts.
ade.processes.* / ade.tests.* # processes also expose group bulk ops:
# ade.processes.startGroup / stopGroup / restartGroup
ade.config.* # project config get/save/trust
ade.keybindings.*
ade.sync.* # device registry, PIN pairing (getPin/setPin/clearPin), QR payload, lane presence announce (setActiveLanePresence), host transfer
ade.usage.* # token/cost accounting
ade.layout.* / ade.graph.*
ade.computerUse.*
ade.iosSimulator.* # macOS-only iOS Simulator drawer + Preview Lab: getStatus/launch/shutdown/screenshot/getScreenSnapshot/getInspectorSnapshot/inspectPoint/getPreviewCapability/listPreviewTargets/resolvePreviewMatch/ensurePreviewWorkspace/renderCurrentPreview/renderPreview/openPreviewWorkspace/startStream/stopStream/getStreamStatus/getWindowState/listWindowSources/tap/typeText/drag/swipe/selectPoint, plus the ade.iosSimulator.event push channel
ade.appControl.* # Electron app control bridge over Chrome DevTools Protocol: getStatus/launch/launchInTerminal/connect/stop/screenshot/getSnapshot/inspectPoint/selectPoint/click/typeText/scroll/dispatchKey/listTargets/attachToTarget, plus the ade.appControl.event push channel (session-started/updated/stopped, selection, screencast frame)
ade.builtInBrowser.* # in-app web browser owned by `builtInBrowserService`: getStatus/showPanel/setBounds/attachWebview/navigate/createTab/switchTab/closeTab/reload/goBack/goForward/stop/startSession/listSessions/endSession/observe/getTrace/click/typeText/dispatchKey/scroll/fill/clear/wait/startInspect/stopInspect/captureScreenshot/selectPoint/selectCurrent/clearSelection/claim, plus the ade.builtInBrowser.event push channel (status / open-request / selection / selection-cleared / error). Backs the Work sidebar's Browser tab and the renderer-wide `openUrlInAdeBrowser()` link router.
ade.terminal.* # chat-owned terminal control: list/read/write/signal/activeForChat. Resolves a chat's active terminal via chatSessionId so in-chat agents and the App Control panel can drive the visible launch terminal.
ade.updates.*
apps/desktop/src/main/services/ipc/registerIpc.ts (~6,400 lines) is the single registration point:
ipcMain.handle(IPC.channelName, async (event, args) => { ... })for invoke channels.- Every handler is wrapped with a timeout — 30 seconds by default, with explicit longer budgets for known long operations such as direct lane delete, iOS Simulator launch/control, App Control, and built-in browser actions. Runtime-dispatched actions use the runtime-call channel budget; the timeout wrapper no longer inspects the action payload to give
lane.deletea special runtime-dispatch override. - Every handler emits structured tracing:
ipc.invoke.begin,ipc.invoke.done,ipc.invoke.failedwith call ID, channel, window ID, duration, and summarized args/results. AppContextindirection: handlers close over a context pointer that swaps atomically on project switch, so IPC channels remain registered across project transitions.- Multi-window shell — the app can host multiple
BrowserWindowinstances (for example when opening another project in a dedicated window). Handler tracing already carries window ID so logs and diagnostics distinguish which renderer surface invoked a channel;main.tsties each window to its set of open project roots before routing into services. Two maps inmain.tsdrive this:windowProjectRootstracks the active foreground project per window, andwindowProjectTabRootstracks every project root that window currently has open as a tab. Project-scoped event broadcasts (emitToProjectWindows) deliver to any window whose active or open-tab set contains the project, so background tabs keep receiving live updates.ade.app.getWindowSessionreturns{ project, binding, openProjectTabs }for the requesting window; the renderer mirrors its open-tab list back to main withade.app.setWindowProjectTabs({ rootPaths })so the main process can keep those project contexts warm and clean up on window close. Renderer tab switches use cached project/lane snapshots for warm activation, retain caches for every open tab root even if a project is absent from recents, keep Work and Lanes mounted after first visit, and cover cold switches with a project-transition veil. - Project context retention.
MAX_WARM_IDLE_PROJECT_CONTEXTS = 100is a soft cap for project contexts with no user work.hasActiveProjectWorkloads(ctx)protects any context that has live chat sessions (viaagentChatService.hasRetainableSessions()— any session the user hasn't explicitly closed or deleted, not just mid-turn ones), live PTYs (ptyService.hasLiveSessions()), active managed processes, or queued tests. Eviction is best-effort and never tears down a context with work; the cap exists only as a safety valve against opening hundreds of empty projects in a long session.
High-frequency events flow from main → renderer via webContents.send(channel, payload). Partial list:
| Event | Producer | Consumer |
|---|---|---|
ade.pty.data / ade.pty.exit |
ptyService | TerminalView, Work tab |
ade.files.change |
fileWatcherService | Files tree, diff views |
ade.processes.event |
processService | Run tab, stack buttons |
ade.tests.event |
testService | Test panel |
ade.conflicts.event |
conflictService | Conflicts page, Graph overlay |
ade.prs.event |
prPollingService | PRs page, stacked queue |
ade.agents.event |
CTO/worker services | CTO tab feed |
ade.lanes.rebaseSuggestions.event / ade.lanes.autoRebase.event / ade.lanes.rebase.event |
rebase services | Lanes + Graph |
ade.project.missing |
projectService | Shell banner |
ade.project.state.event |
projectState | Startup flow |
ade.sync.* events |
syncService | Settings → Sync |
Renderer telemetry events flow back to main: renderer.route_change, renderer.tab_change, renderer.window_error, renderer.unhandled_rejection, renderer.event_loop_stall.
Most services described here live under apps/desktop/src/main/services/<domain>/ in the desktop client's main process. Some are runtime delegations: they front a runtime-owned subsystem (project registry, sync service, agent registry, credential store, multi-project RPC) through a thin local or remote pool. The runtime-side equivalents live under apps/ade-cli/src/services/. Summary:
| Domain | Key files | Role |
|---|---|---|
ai/ |
aiIntegrationService.ts, authDetector.ts, providerConnectionStatus.ts, claudeRuntimeProbe.ts, modelsDevService.ts, compactionEngine.ts, tools/* |
Provider routing, detection, tool definitions, compaction. |
agentTools/ |
agentToolsService.ts |
Agent tool registry metadata surfaced to the renderer. |
appControl/ |
appControlService.ts, appControlLaunchCommand.ts |
Chrome DevTools Protocol bridge for developer-owned Electron apps. Launches a chat-owned PTY running the user's dev command (or connects to an existing --remote-debugging-port), polls /json for ready CDP targets, attaches a long-lived CdpClient WebSocket, and exposes screenshot / DOM snapshot / hit-test / click / type / scroll / key dispatch / screencast frames. appControlLaunchCommand.ts owns the shell-command detection and debug-flag injection helpers for direct Electron and package-script launches. inspectPoint and selectPoint produce AppControlContextItems for the chat composer (DOM packet + screenshot + source-file candidates resolved by findSourceMatches over an indexed tree of project source files). See features/computer-use/app-control.md. |
builtInBrowser/ |
builtInBrowserService.ts, builtInBrowserNavigation.ts, builtInBrowserPermissions.ts |
In-app web browser owned by the main process. Allocates WebContentsView tabs against persistent project-profile partitions derived from the active project root (fallback persist:ade-browser, cap 10 tabs per window/project service), positions them over a renderer-supplied bounds rect, drives navigation / tabs / reload / back / forward, attaches the Chrome DevTools Protocol debugger for inspect-mode hit tests and background input dispatch, captures screenshots, handles Electron download requests with sanitized unique filenames in the user's Downloads folder, and emits BuiltInBrowserEventPayloads to subscribers. Tabs carry per-tab owner metadata (ownerLaneId, ownerChatSessionId, ownerClaimedAt, ownerLeaseExpiresAt), and observe/click/type/key/scroll/fill/clear/wait/screenshot/select/reload/back/forward/stop can target a hidden or non-active tab by tabId or by a lightweight browser session id. Browser sessions bind one agent workflow to one tab, remember owner plus last observation/trace ids, support ade browser session <action> <id> aliases, and end explicitly or when the tab closes. Browser agent observations are scratch PNG/JSON files under .ade/cache/browser-observations/ with a bounded DOM element list for selector/text/index targeting, optional numbered element-map screenshots, console/network diagnostics, pending request count, and short-lived element handles that preserve same-origin iframe/open-shadow-root context. Click/fill/clear/press/wait can resolve selector, text, testId, elementIndex, or handle, scroll the target into view, focus as needed, and reject disabled targets before dispatching CDP input. Waits wake from browser/network/page activity events with a timeout fallback; network-idle requires complete ready state, zero pending browser requests, and a configurable idle window. Per-tab action traces record action, target metadata, before/after URL, duration, observation id, session id, and errors; typed/fill values are represented by length rather than raw text. Scratch observations prune to the latest 3 observations per tab by default and sweep stale cache files; ade browser proof promotes a fresh observation through the computer-use artifact broker. Desktop bridge calls from ade browser carry the runtime project root and are routed only to an ADE window/project tab for that root, so a command from one project cannot mutate another project's browser. Navigation normalization and protocol allowlisting live in builtInBrowserNavigation.ts; Google-auth permission policy lives in builtInBrowserPermissions.ts; WebAuthn account selection is registered per Electron storage session in builtInBrowserWebAuthn.ts. Consumed by ChatBuiltInBrowserPanel (Work sidebar Browser tab), which hides the native view while ADE overlays intersect the browser rectangle, and by openUrlInAdeBrowser() in the renderer so renderer-side link clicks open inside ADE rather than the system browser. |
automations/ |
automationService.ts, automationPlannerService.ts, automationIngressService.ts, automationSecretService.ts |
Rule lifecycle, NL → rule planner, inbound triggers, per-rule secrets. |
chat/ |
agentChatService.ts, runtimeEvents.ts, buildClaudeV2Message.ts, markdownSlashCommandDiscovery.ts, claudeSlashCommandDiscovery.ts, codexSlashCommandDiscovery.ts, cursorSlashCommandDiscovery.ts, projectSlashCommandDiscovery.ts, slashCommandPromptExpansion.ts, cursorSdk* (cursorSdkPool.ts, cursorSdkWorker.ts, cursorSdkProtocol.ts, cursorSdkPolicy.ts, cursorSdkSystemPrompt.ts, cursorSdkEventMapper.ts), sessionRecovery.ts |
Agent chat sessions (lane-scoped + orchestration worker/coordinator). Builds Claude messages, hosts the Cursor SDK in a Node worker pool, formalizes the cross-runtime event vocabulary, discovers and resolves provider-specific slash commands through a shared markdown engine, recovers sessions on restart, derives prompt-based lane names for parallel model launches, and manages Codex app-server goals with persisted, unlimited-budget session state. |
computerUse/ |
computerUseArtifactBrokerService.ts, controlPlane.ts, localComputerUse.ts, agentBrowserArtifactAdapter.ts, syntheticToolResult.ts |
Proof-artifact broker (ingests, owner links, review state, routing), control-plane snapshot helpers, macOS capture capability descriptor, agent-browser payload parser, and the synthetic-tool-result helper used by the Claude compaction path. proofObserver.ts was removed in the rebuild — there is no passive auto-ingest. |
config/ |
projectConfigService.ts, laneOverlayMatcher.ts |
Load/save .ade/ade.yaml + local.yaml; trust enforcement; lane overlays. |
conflicts/ |
conflictService.ts |
Pairwise dry-merge simulation, risk matrix, proposal generation. |
cto/ |
ctoStateService.ts, workerAgentService.ts, workerBudgetService.ts, workerHeartbeatService.ts, linearSyncService.ts, linearIngressService.ts, linearOAuthService.ts, linearRoutingService.ts, linearDispatcherService.ts, linearCloseoutService.ts, flowPolicyService.ts, linearLaneCardService.ts |
CTO identity, worker agents, session logs, and Linear sync/ingress/OAuth/routing/dispatcher/closeout. linearLaneCardService posts the Linear attachment card and builds the cross-machine ADE deeplink that backs the card's URL. |
deeplinks/ |
protocolHandler.ts |
Registers the ade:// OS protocol handler for the packaged Stable desktop build, owns the single-instance lock, buffers cold-start URLs until app.whenReady(), and dispatches parsed URLs through IPC.appNavigate to the focused window. Beta, Alpha, and source builds can receive explicitly delivered links but do not claim the OS-default handler. Re-used by the iOS Send-to-Mac sync command (syncRemoteCommandService.deeplinks.open). Shared parser + builder live in apps/desktop/src/shared/deeplinks.ts; the PR "Open in ADE" footer is in apps/desktop/src/shared/adeDeeplinkFooter.ts. See features/deeplinks/README.md. |
devTools/ |
devToolsService.ts |
Probe for git + gh CLI availability. |
diffs/ |
diffService.ts |
Diff computation for file panes. |
feedback/ |
feedbackReporterService.ts |
In-app feedback reporting. Two-stage: prepareDraft generates a structured issue title + labels (AI-assisted when a model is selected, deterministic fallback otherwise) so the user can review before posting; submitPreparedDraft files the GitHub issue. Each submission records generationMode and a generationWarning so the UI can flag deterministic drafts. |
files/ |
fileService.ts, fileWatcherService.ts, fileSearchIndexService.ts |
Workspace file tree, read/write, watch, index. |
git/ |
git.ts, gitOperationsService.ts, gitConflictState.ts |
Low-level git runner, high-level lane-scoped ops, conflict state queries. |
github/ |
githubService.ts |
GitHub REST/GraphQL access; PR CRUD; checks; reviewers. |
history/ |
operationService.ts |
Operation audit records (one row per mutation). |
ios/ |
iosSimulatorService.ts |
macOS-only iOS Simulator backend: tool readiness probes, simctl device + app discovery, build/install/launch with progress events (hardened with simctl bootstatus and simctl install timeouts), screenshot + ADEInspector + accessibility hit-test, Simulator.app window live-view status, idb-backed input, and single-owner chat session locking. The macOS Simulator window placement / capture state probe (getSimulatorWindowState, prepareSimulatorWindowForCapture) lives next to the IPC handlers in ipc/registerIpc.ts because it depends on the active BrowserWindow. See features/ios-simulator/README.md. |
ipc/ |
registerIpc.ts, runtimeBridge.ts, ipcTimeouts.ts |
Single registration point for all IPC handlers. runtimeBridge.ts owns the runtime-facing channels (remote target registry, remote-runtime connect / project list / project-open / action dispatch / sync dispatch / event stream, per-target listActionRegistry lookup against the remote daemon, local-work checks, LAN + Tailscale discovery with diagnostics) and routes runtime calls through LocalRuntimeConnectionPool or RemoteConnectionPool based on the active window binding. Remote project opens are generation-guarded per window/webContents before main persists the binding. It also subscribes powerMonitor resume and unlock-screen to remoteConnectionService.probeSavedConnections() so a laptop waking up cycles dead SSH sessions before the renderer pokes them. ipcTimeouts.ts carries the default 30-second handler timeout plus named channel-level overrides for long direct IPC operations; it does not inspect runtime action payloads. |
jobs/ |
jobEngine.ts |
Event-driven background scheduler for lane refresh + conflict prediction. Coalesced, debounced. |
keybindings/ |
keybindingsService.ts |
User keybindings read/write. |
lanes/ |
laneService.ts, laneEnvironmentService.ts, laneTemplateService.ts, laneProxyService.ts, portAllocationService.ts, autoRebaseService.ts, rebaseSuggestionService.ts, laneLaunchContext.ts, oauthRedirectService.ts, runtimeDiagnosticsService.ts |
Worktree lifecycle, env bootstrap, templates, reverse proxy, port leases, auto-rebase, suggestions, OAuth redirect, diagnostics. |
logging/ |
logger.ts |
File-backed structured logger. |
localRuntime/ |
localRuntimeConnectionPool.ts |
Desktop-side client for the local brain endpoint. Spawns or attaches to the machine endpoint, registers local projects with projects.add, dispatches local runtime actions with per-call timeouts where needed, emits local_runtime.action_slow warn logs (with ensureProjectMs / connectMs / daemonCallMs breakdown) whenever a call exceeds 500 ms or throws, polls runtime events, and installs the background service best-effort in packaged builds. |
onboarding/ |
onboardingService.ts, onboardingSuggestedConfig.ts |
First-run flow, defaults detection, existing lane discovery. onboardingSuggestedConfig.ts contains pure workflow parsing and suggested .ade/ade.yaml generation. |
opencode/ |
openCodeRuntime.ts, openCodeServerManager.ts, openCodeBinaryManager.ts, openCodeInventory.ts, openCodeModelCatalog.ts |
OpenCode server spawn, binary resolution, model discovery. |
orchestration/ |
orchestrationService.ts, applyPatches.ts, patchPolicy.ts, manifestNormalization.ts, runtimeProfile.ts |
Work-tab orchestration for multi-phase plans. orchestrationService manages run lifecycle, manifest persistence, the leadState.planning state machine, plan.md, validation strategy/findings, asset bundles, and the lineage delegation ledger (lead→worker/validator spawn + result edges). patchPolicy keeps privileged fields (leadState.planning, planSpec, and the /lineage ledger) behind service methods so the lead cannot forge intake, planning rounds, model routing, approval readiness, or delegation edges with a raw patch. runtimeProfile resolves the active orchestration profile per session and gates model selection / plan approval on planning readiness. The renderer surfaces live in renderer/components/orchestration/ (see §7.3). The former orchestrator/ and missions/ directories were consolidated into this service. |
processes/ |
processService.ts |
Managed-process lifecycle per lane, readiness probes, restart policies. |
projects/ |
adeProjectService.ts, configReloadService.ts, projectService.ts, logIntegrityService.ts, recentProjectSummary.ts, projectBrowserService.ts, projectDetailService.ts |
Project detection + .ade repair/bootstrap, reload on config change, recent-project metadata. recentProjectSummary.ts emits local and remote recent summaries without disk-inspecting remote paths. projectBrowserService is the in-app directory autocomplete used by the Command Palette project browser (typed-path completion, .git detection, home expansion, system-picker fallback); projectDetailService returns repo metadata (branch, dirty count plus staged/unstaged/untracked breakdown, ahead/behind, last commit, README excerpt inputs, language mix, lane count, last-opened) for the palette's preview pane. |
prs/ |
prService.ts, prPollingService.ts, prSummaryService.ts, queueLandingService.ts, prIssueResolver.ts, prRebaseResolver.ts, integrationPlanning.ts, integrationValidation.ts |
PR CRUD, polling (with per-PR last_polled_at cursor), AI summary cache keyed by (prId, head_sha), stacked-queue landing, AI-assisted issue resolution, rebase resolution, integration planning, and merge-into-existing-lane proposal adoption. |
pty/ |
ptyService.ts |
node-pty spawn, PTY I/O bridging, transcript writing. |
remoteRuntime/ |
remoteTargetRegistry.ts, sshTransport.ts, remoteBootstrap.ts, remoteConnectionPool.ts, remoteConnectionService.ts, runtimeRpcClient.ts, runtimeDiscovery.ts |
Saved SSH machines (manual host + alternate routes[] with lastSucceededAt and manual-disconnect state), ssh-agent/key transport with bounded connect/exec timeouts and multi-route fallback, first-connect runtime upload/version/SHA verification with channel-home fallback (.ade / .ade-alpha / .ade-beta) and capability/version skew demoted from fatal errors to RemoteRuntimeConnectResult.compatibilityWarnings, remote project catalog, action dispatch (with a projects.* capability gate against RemoteRuntimeCapabilities.machineProjects), local TCP forwards for remote preview ports, reconnect/eviction with pool eviction listeners and implicit reconnect backoff, powerMonitor resume probe, and LAN + Tailscale discovery that returns diagnostics alongside machines. The JSON-RPC client formats remote errors with the original method name plus the JSON-RPC code / message / data for clearer diagnostics. |
runtime/ |
tempCleanupService.ts, processRegistryService.ts, machineStateMigration.ts |
Runtime temp cleanup. processRegistryService is the per-process heartbeat registrar against machine-local runtime_processes (see §3.4); reconcile/dispose paths in sessionService and ptyService consult live and known owner sets before sweeping terminal_sessions rows so sibling processes and synced remote-machine owners are preserved. machineStateMigration carries one-shot migrations of the per-machine state files under ~/.ade/. |
sessions/ |
sessionService.ts, sessionDeltaService.ts |
Terminal session CRUD, post-session delta computation. |
shared/ |
utils.ts, imageDimensions.ts, queueRebase.ts, packLegacyUtils.ts, transcriptInsights.ts |
Cross-domain utilities, including shared record guards and PNG/JPEG dimension parsing used by App Control and iOS Simulator capture paths. |
state/ |
kvDb.ts, crsqliteExtension.ts, globalState.ts, projectState.ts, onConflictAudit.ts |
SQLite schema + open, CRR extension loader, global state file, per-project state init. globalState.upsertRecentProject accepts preserveRecentOrder so reactivating an already-known project (by app focus, deep link, etc.) refreshes its lastOpenedAt in place instead of jumping it to the front of the recents list. Recent projects use stable keys: local rows are keyed by absolute root path, remote rows by remote:<targetId>:<projectId>, so a remote path string never collides with a local project. Pinned rows are retained above normal recency ordering and survive beyond the cap. model_picker_favorites and model_picker_recents are per-project CRR tables shared by desktop, TUI, and iOS; they are primary-key-only so CRR can convert them, with the recents cap enforced in modelPickerStore.ts. AdeDb.sync.discardUnpublishedChangesForTables(tableNames) lets a service clear local CRR state for specific tables without leaking those clears to sync peers — it records the cleared tables and through_db_version in the local-only local_crr_change_suppressions table, and exportChangesSince filters local-site rows for those tables at or below that version on the way out. The local-only excluded set (still kept out of replication) includes that suppression table itself, the snapshot caches, local_worktree_residual_cleanups, pr_auto_link_ignores, pull_request_ai_summaries, and runtime_processes. crsql_changes DELETE statements run through a helper that swallows the read-only-table error the cr-sqlite extension raises when a CRR-managed table is wiped, with a db.crr_changes_cleanup_skipped warn log instead of failing the migration. |
sync/ |
syncService.ts, syncHostService.ts, syncPeerService.ts, syncRemoteCommandService.ts, syncProtocol.ts, deviceRegistryService.ts, syncPairingStore.ts |
Thin delegation to the ADE runtime's sync service. The authoritative sync service now lives in apps/ade-cli/src/services/sync/; the desktop main-process instances default to a non-host viewer role for legacy state and tests. The old in-process host is disabled unless ADE_ENABLE_DESKTOP_SYNC_HOST=1 (diagnostics only). Wire formats — WebSocket envelope, remote command routing, device registry, pairing secrets — are the same across both implementations. Viewer joins clear the local devices + sync_cluster_state rows and then call db.sync.discardUnpublishedChangesForTables(["devices", "sync_cluster_state"]) so the resulting DELETE rows do not leak back to other peers; the peer client follows up with syncPeerService.acknowledgeLocalDbVersion() to advance the outbound cursor past the suppressed range. |
tests/ |
testService.ts |
Test-suite execution + run history. |
updates/ |
autoUpdateService.ts |
Electron auto-update wrapper around electron-updater. Owns the renderer-visible AutoUpdateSnapshot (idle | checking | downloading | ready | installing | error), uses compareUpdateVersions (SemVer-aware) to dedupe / supersede staged installers and to reconcile pendingInstallUpdate against the running version on next boot. Packaged builds schedule startup/periodic checks; source/dev launches construct the service without auto-check timers so missing app-update.yml never surfaces as a renderer error. quitAndInstall() is async: it re-runs checkForUpdates({ allowReady: true }) to confirm the staged build is still latest, and only then flips to installing and calls updater.quitAndInstall(false, true). |
usage/ |
usageTrackingService.ts, budgetCapService.ts, ledgers/localUsageLedgers.ts |
Token/cost accounting, budget enforcement. usageTrackingService.ts owns polling, aggregation, pacing, GitHub stats, and cache orchestration; local provider ledger scanners live under usage/ledgers/. Budget caps can match a rule scope while usd-per-run evaluates usage records keyed to the active run id. Threshold state is shared at module level across all createUsageTrackingService instances so multiple project contexts don't fire duplicate threshold events; main.ts adds a final IPC-level dedup gate with a 10-minute TTL per provider:threshold:resetCycle key. |
perf/ |
perfLog.ts, perfIpc.ts, metricsSampler.ts, aggregator.ts |
Opt-in local performance harness. ADE_PERF_RUN_ID opens a JSONL event log, samples Electron process metrics, records IPC durations, accepts renderer perf marks/web-vitals, and aggregates each run into summary.json. |
Startup sequencing: every background service goes through scheduleBackgroundProjectTask() in main.ts, which provides explicit labels, ADE_ENABLE_* env gates, project.startup_task_begin/_done/_enabled/_skipped telemetry, and per-task delays. Integrations stay dormant-until-configured.
Project-init step timing goes through measureProjectInitStep(step, task) — a wrapper that logs project.init_step { projectRoot, step, durationMs } around each hot-path operation (db_open, lane.ensure_primary, ade_rpc.socket_server_start, sync.initialize, etc.) so cold-start latency shows up in the logs by phase. Sync-service initialization is scheduled through scheduleBackgroundProjectTask rather than awaited inline, gated by ADE_ENABLE_SYNC_INIT.
Shutdown pipeline: main.ts owns a single requestAppShutdown({ reason, exitCode, fastKillFirst?, forceAfterMs? }) path driving a central state machine (shutdownRequested → shutdownPromise → shutdownFinalized). Hooks into before-quit, window close, SIGINT, SIGTERM, process.exit, will-quit, and uncaughtException all funnel through it. runImmediateProcessCleanup() disposes automations, tests, processes, PTYs, agent chat runtimes, DB flush, and then calls shutdownOpenCodeServers(). A forceAfterMs timer (default 8 s, 5 s for signals/uncaught) hard-exits if cleanup hangs. User-initiated quit (main window close or before-quit) routes through confirmQuitWarning() — a modal dialog that explains that quitting will end agents and background processes owned by the desktop session, including OpenCode servers, terminal sessions, and test runs.
On startup the main process also invokes recoverManagedOpenCodeOrphans({ force: true }) (see services/opencode/openCodeServerManager.ts) to reap previous-run OpenCode processes left behind after a crash. Orphan detection matches processes by the managed marker env (ADE_OPENCODE_MANAGED=1) and/or the shared XDG config root, and confirms orphaning either by dead owner PID (ADE_OPENCODE_OWNER_PID) or reparent-to-init. Each acquire of a shared OpenCode server also invokes pruneIdleSharedEntries() which compacts idle entries from older configs (pool_compaction reason).
| Layer | Tech |
|---|---|
| Framework | React 18 |
| Language | TypeScript |
| Router | React Router |
| State | Zustand (global + per-domain) |
| Styling | Tailwind CSS 4 + CSS custom properties |
| Primitives | Radix UI |
| Icons | Lucide React |
| Terminal | xterm.js |
| Editor/Diff | Monaco Editor |
| Graph canvas | React Flow |
| Pane layouts | react-resizable-panels, in-house PaneTilingLayout |
| Virtualization | @tanstack/react-virtual |
Electron renderer runtime does not wrap the app in React.StrictMode. Browser-mock development (outside Electron) still uses Strict Mode. The app uses BrowserRouter on normal http(s) origins and HashRouter inside Electron/file-like contexts; App.tsx also bridges legacy #/route fragments into BrowserRouter paths so old ADE deep links keep working in the browser-hosted dev shell.
apps/desktop/src/renderer/state/appStore.ts — Zustand store holding project, lanes, selected lane, theme, provider mode, keybindings, per-project work-view state. Built as a createStore<AppState>()(createAppState) factory so multiple stores can be instantiated; the module exposes a default rootAppStore plus a per-project factory and React context:
createProjectAppStore(project, projectBinding?)returns a fresh per-surface store pre-hydrated with the local or remote project binding + a copy of root-store user preferences. Setters for theme/terminal/chat preferences point at the root store so user preferences mutate in one place and are then mirrored into every project store viahydrateProjectAppStorewheneverrootPrefschange inProjectTabHost. This is what lets two open project tabs share a theme even though they have independent lane/chat state.AppStoreProvider+AppStoreContextscope the active store to aProjectSurfacesubtree. TheuseAppStorehook reads fromuseContext(AppStoreContext) ?? rootAppStore, anduseAppStoreApi()returns the bareStoreApifor components that want imperativegetState()access without subscribing.useAppStore.getState / setState / subscribestill point at the root store so code that needs cross-window globals (recent projects, user preferences, the root binding) can continue to call it directly.- Narrow selectors on components to minimize re-renders.
refreshLanesaccepts independent lane-status and lane-snapshot flags. Callers can refresh cheap runtime snapshot decorations without recomputing git status, or update git status without rebuilding conflict/rebase/auto-rebase overlays; statusless refreshes preserve the previousLaneStatus/parentStatusin store so the UI does not flicker to unknown git state.- Per-project work-view state keyed by the active project root (
WorkProjectViewState; remote bindings use the remote root). Includes the right-edge Work sidebar fieldsworkSidebarOpen,workSidebarTab("git" | "files" | "ios" | "app-control" | "browser"), andworkSidebarWidthPct(clamped 26–55) — persisted alongside the rest of the work-view state underade.workViewState.v1. The sidebar consolidates lane-scoped tools that were previously split across separate floating panes; per-chat iOS / App Control drawers still exist onAgentChatPanebut are suppressed when the chat is mounted as a Work tile so the sidebar owns those surfaces at lane scope. Remote-bound Work sidebars expose only the runtime-backed Git and Files tabs; local-only iOS Simulator, App Control, and Browser panes stay hidden. Thebrowsertab is not lane-scoped on local bindings: each ADE window/project profile gets its own tab set and inspect state, while browser storage is shared through the persistent project-profile partition for the active project root. - Project tab bookkeeping.
openProjectTabRoots: string[]is the LRU-ordered list of project roots open in the window (mirrored to the main process viaade.app.setWindowProjectTabsso background services keep those projects warm);projectInfoByRoot: Record<string, ProjectInfo>caches theProjectInfopayload for tab favicons and offline tab rendering.setProjectis the only path that mutates either map. - Stale-while-revalidate switch caches.
laneSelectionByProjectremembers the{ laneId, sessionId }selection per project root so switching tabs lands on the lane/chat the user last had open instead of "first lane".laneCacheByProjectmirrors the last good{ lanes, laneSnapshots }per root;switchProjectToPathapplies the cached entry immediately on switch (no spinner, no chat-pane unmount) and refreshes silently in the background.sessionsCacheByProjectdoes the same foruseWorkSessionsso the chat tabs / terminal grid don't blank during a tab swap. All three caches are pruned to active + recent-projects when a project actually changes. projectRevisionis a monotonically incrementing counter bumped insidesetProjectwhenever the active project root actually changes. Long-lived renderer-side caches (most notably the module-level xterm runtime cache inTerminalView.tsx) subscribe to it and tear down any entries whoseprojectRoot/projectRevisionno longer match, so PTYs never bleed between projects. All project-transition paths (refreshProject,openRepo,switchProjectToPath,closeProject) go throughsetProjectto keep the counter honest.
Domain stores co-located with their pages follow the same factory + context pattern when they need per-page isolation:
chatDraftStore.ts— draft messages per chat session.
Feature-grouped under apps/desktop/src/renderer/components/:
app/ # shell, App.tsx, TopBar, TabNav, LinearIssueBrowser (multi-select + batch actions), LinearIssueResolveModals (single + batch), LinearQuickViewButton, startup, splash
project/ # Play tab, run/test/process controls
lanes/ # list/detail/inspector, stacks, laneDesignTokens.ts
files/ # tree, editor, diffs
terminals/ # TerminalView, WorkViewArea (PaneTilingLayout-backed grid), WorkSidebar, workSessionTiling, LaneCombobox
conflicts/ # risk matrix, simulation, resolution
graph/ # WorkspaceGraphPage (decomposed into nodes/edges/dialogs)
prs/ # PR list/detail, stacked queue, shared/
history/ # operation timeline
automations/ # rule list, pipeline builder
cto/ # CTO page, identity editor, team panel, pipeline, shared/designTokens.ts
orchestration/ # OrchestrationPanel, TaskCard, PlanMarkdown, PhaseAccordion, PlanningTimeline, ValidationFindings
onboarding/ # first-run flows
settings/ # keybindings, agents, data, context, sync
chat/ # AgentChatPane + composer + subpanels
shared/ # MentionInput, shared interactive bits
ui/ # pure presentation primitives
Design tokens have been intentionally trimmed. The CTO design tokens at apps/desktop/src/renderer/components/cto/shared/designTokens.ts are the example style: a small set of Tailwind class constants (cardCls, surfaceCardCls, shellBodyCls, inputCls, labelCls, etc.) and a constrained accent palette (ACCENT.purple/blue/green/pink/amber). Lane design tokens live at lanes/laneDesignTokens.ts and are imported across lanes/PRs/settings.
PaneTilingLayout— recursive pane trees for high-density workspaces, backed by pure ops inpaneTreeOps.ts(reconcilePaneTree,splitPaneAtEdge,swapPanes,detectDropEdge). Trees persist perlayoutIdviawindow.ade.tilingTree; panel sizes persist separately viaDockLayoutStateand are reset whenever the tree mutates.SplitPane/ resizable panels — structured 2/3-pane views.- Work view's grid mode is
PaneTilingLayoutseeded bybuildWorkSessionTilingTree(sessionIds)(inrenderer/components/terminals/workSessionTiling.ts); every session becomes aFloatingPaneleaf withgrid-tilechrome. - Project tab hosting:
App.tsx'sProjectTabHostmounts one persistentProjectSurfaceper open project tab inside a single window, keyed by the runtime binding (local:<root>or the remote binding key) so a local and remote view of the same path cannot share renderer state accidentally. EachProjectSurfaceowns its own zustand store instance (createProjectAppStore(project, projectBinding)), pre-hydrated with the project binding plus a copy of root-store user preferences (theme, terminal preferences, chat font, sound, density, etc.). User-preference setters point at the root store, so changes flow to one place and are then mirrored into every project store on the nextrootPrefschange. A LRU sorts mounted surfaces and caps the warm-mounted set atWARM_PROJECT_SURFACE_LIMIT = 8; surfaces beyond that limit are dropped from the React tree (their store entry is GC'd) but the persisted lane/chat caches in the root store keep their data live so a re-mount is cheap. - Per-surface routing: each surface remembers its own route (
/work,/lanes,/files,/prs,/cto,/automations,/settings, …) underade:project-route:<bindingKey>inlocalStorage.ProjectTabHostswaps which surface isactivebased on the foreground project tab, stashing the outgoing route and replaying the incoming surface's last route vianavigate(..., { replace: true }). Inactive surfaces stay in the tree (aria-hidden,inert, absolutely positioned atz-index: -1, opacity 0, pointer-events none) so chats / terminals / live polling don't tear down on tab swap. - Work-surface reveal:
ProjectRouteContentkeeps the/workroute mounted lazily inside each project surface. When the surface itself becomes active and the route is a work route, it dispatches theWORK_SURFACE_REVEALED_EVENTwindow event so terminal tiles can clear their texture atlas, force-fit, and refocus. - Page-level active gating: lazy feature pages (
LanesPage,FilesTab,WorkspaceGraphPage,PRsPage,ReviewPage,HistoryPage,AutomationsPage,AutomationsTemplatesPage,CtoPage,SettingsPage) accept anactive?: booleanprop and gate everyuseEffectthat fires IPC polling, event subscriptions, or initial data fetches behind it. Inactive surfaces in background project tabs render their last state but don't poll — the project's runtime is still alive, so the freshness is restored on the next refresh when the user returns. - The desktop TopBar project tab strip resolves a per-project favicon via
window.ade.project.resolveIcon(rootPath)and caches the result in a module-localMap. Tabs without an icon (or a missing project root) fall back to theFolderPhosphor glyph; the same component drives the loading-pulse animation when a tab is being switched into or closed. - Layout state persists to SQLite (
layout,tilingTree,graphStatedomains via thekvtable).
Enforced rules (from the stability overhaul):
- All background services go through
scheduleBackgroundProjectTask()— no rawsetTimeoutfor service startup. - New integrations are dormant-until-configured.
- Feature pages stage data: cheapest (list/summary/topology) first, heavy (dashboard/settings/model metadata/overlays) on delay.
- Never mount expensive trees eagerly — settings dialogs, advanced launcher sections unmount when closed.
- Renderer polling is route-scoped; terminal attention only polls on terminal routes; lane panels only poll while live sessions exist. The plain PR list does not fire a GitHub refresh on mount, renders active-repository PR snapshots only, skips conflict analysis, and defers rebase-needs / auto-rebase polling until the user opens a workflow tab or selects a PR. Selected PR detail reads apply progressively so slow comments or action-run hydration do not block status/checks/files from painting. Workflow PR views batch merge contexts and conflict analysis against metadata-only lane rows instead of running per-PR git/status work. The Lanes page reuses the
LaneSummary.autoRebaseStatussnapshot already in the lane list instead of probing per-lane onLaneGitActionsPanemount; a fallback probe runs only when the snapshot is missing and after a visibility-gated 3.5 s delay. Run'sLaneRuntimeBarkeeps health/process refreshes separate from preview routing / port / OAuth refreshes so process events do not reread routing state. The Work top-bar sync chip refreshes on focus and onsync-statusevents instead of a 5 s interval. The chat composer's Cursor model inventory is fetched lazily —ProviderModelSelectorcallsonOpenon first open of the model catalog, andAgentChatPane.refreshCursorModelInventoryis the only entry point that hitscursorwithactivateRuntime: true. - Shared caches for high-frequency calls (
sessionListCache, GitHub fingerprint-based snapshots). - Memoize expensive renderer computations (
useMemo,React.memo); isolate frequently-refreshing subtrees (e.g., budget footers). Promise.allSettledoverPromise.allfor parallel startup — one failing service must not block others.- Settings sections that surface a snapshot read the cached snapshot on mount (
ade.usage.getSnapshot) instead of forcing a refresh; an explicit Refresh button drives recompute. - Persistence callbacks dedupe against the last-saved value: the workspace-graph view-mode persister tracks the last-loaded preference root and skips the immediate write that the load handler's
setViewModewould otherwise fire.
CLI-launcher and shell-quoting helpers (cliLaunch.ts, shell.ts) live under apps/desktop/src/shared/ so the desktop renderer, chat launch helpers, ADE CLI action surface, and sync remote-command service share one provider launch contract. Renderer imports go through thin re-export shims under apps/desktop/src/renderer/. The mobile launcher path (work.startCliSession) uses the same shared helpers on the host side, so iOS CLI launches do not depend on renderer modules.
Themes: six shipped themes (e-paper, bloomberg, github, rainbow, sky, pats), persisted in localStorage.ade.theme, applied via data-theme on root. Token-based palettes in apps/desktop/src/renderer/index.css.
renderer/lib/dialogBus.ts— tiny pub/sub that lets the onboarding tour engine (and anyone else) open/close dialogs by a stable id (lanes.create,settings.ai, etc.) without prop-drilling. Dialogs subscribe by id; asubscribeAllchannel exists for devtools. Default singleton exportdialogBus.renderer/onboarding/waitForTarget.ts— polls for a DOM target (ref ordata-onboarding-target) with a visibility check so tour steps anchor reliably to async-mounted UI.renderer/onboarding/TourController.ts— imperative driver for the onboarding tour state machine.renderer/onboarding/docsLinks.ts— typed registry of internal/public doc URLs (docs.lanes,docs.cto, …) that tour steps and theHelpMenulink to.renderer/components/onboarding/fx/*— shared motion-FX primitives (ActIntro,AnimatedField,Confetti,GhostCursor,MorphingTree,Spotlight,StaggeredText,TourIllustration) with auseReducedMotionhook. Used by the 13-act first-session tutorial and per-tab tours.
Related UI docs: Terminals UI surfaces, Files and editor, and Onboarding and settings.
| Secret | Location | Protection |
|---|---|---|
| GitHub PAT | .ade/secrets/github/*.bin |
safeStorage.encryptString (OS-backed) |
| API provider keys | .ade/secrets/api-keys.json |
Plaintext 0600 |
| Claude OAuth creds | Claude's own store | Inherited |
| Codex auth tokens | Codex's own store | Inherited |
| macOS Keychain entries | OS Keychain | OS-backed |
| Sync site ID | .ade/secrets/sync-site-id |
Plaintext, never syncs |
| Sync device ID | .ade/secrets/sync-device-id |
Plaintext, never syncs |
| Sync bootstrap token | .ade/secrets/sync-bootstrap-token |
Plaintext, never syncs |
| External-ADE CLI secrets | .ade/local.secret.yaml |
Plaintext, never syncs |
┌──────────────── Main process (trusted) ──────────────┐
│ Full Node access: git, fs, PTY, sqlite, process │
│ ┌────────────────────────────────────────────────┐ │
│ │ Preload bridge (contextBridge) │ │
│ │ window.ade = { /* ~550 typed methods */ } │ │
│ └────────────────────────────────────────────────┘ │
├──────────────── Renderer (untrusted) ────────────────┤
│ React app · no require() · no node · no net │
│ Only path: window.ade.* + CSP │
└──────────────────────────────────────────────────────┘
BrowserWindow hardening:
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: false, // required for preload functionality
preload: "preload.cjs",
}CSP (rendererCsp.ts): default-src 'self'; script-src 'self' (no eval, no inline scripts); style-src 'self' 'unsafe-inline' (required for Tailwind); connect-src 'self'; img-src 'self' data: plus a host-scoped allowlist (no blanket https:) for the image origins PR/README surfaces actually load — the GitHub avatar/asset hosts (*.githubusercontent.com, github.githubassets.com, …) and www.gravatar.com / secure.gravatar.com (commit-author identicon fallback).
Every IPC handler validates its arguments; invalid args return structured errors, never crash. Every handler has a 30s timeout by default; ipcTimeouts.ts carries per-channel overrides for long-running operations and inspects the payload of localRuntime.callAction / remoteRuntime.callAction so action-specific timeouts (e.g. lane.create / lane.delete → 4 min; ios_simulator.launch → 10 min) apply even when the channel itself is generic. Every handler emits structured tracing.
- ADE CLI session identity is resolved from env vars and the
initializehandshake. - Role validation: only
cto,orchestrator,agent,external,evaluatoraccepted. - API keys for provider-routed (non-CLI) models are stored via
apiKeyStore.ts.
- Redaction (
shared/utils.tsredactSecrets()) scrubs Bearer tokens, OpenAI/Anthropic API keys (sk-), GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_/github_pat_), Slack tokens (xox*), AWS access keys (AKIA/ASIA), and JSON-embedded sensitive key-value pairs before any log write or AI-context serialization. - Sanitization (
sanitizeStructuredData()) enforces depth limits, redacts sensitive keys, and truncates oversized arrays/strings. - Bounded AI payloads — narrative/proposal/PR description calls use
LaneExportStandardorLaneExportLite+ConflictExportStandard(token-budgeted), not raw pack dumps or transcript slabs. - Path validation (
resolvePathWithinRoot()) resolves symlinks viarealpathSyncbefore containment checks. Applied to lane env init, coordinator tools, process working dirs, sync artifact paths, ADE CLI context file resolution, computer-use artifact ingestion. - Config trust: process/test commands from
ade.yamlrequire SHA-256 hash approval before execution. Commands inlocal.yamlare always trusted. Trust stored inkvwith the config hash as key.
Related trust-boundary docs: Computer-use artifact broker, Computer-use backends, and Configuration schema.
- ADE shells out to the system
gitbinary (not isomorphic-git). Rationale: full feature parity, hook compatibility, native credential handling, performance. - All commands go through
runGit/runGitOrThrowinapps/desktop/src/main/services/git/git.ts(timeout support, structured output parsing). - High-level ops in
gitOperationsService.ts— wrap every mutation inrunLaneOperation(): resolve lane, capture pre-HEAD, record operation, execute, capture post-HEAD, finalize record, fireonHeadChangedif needed.
Each non-primary lane maps to a dedicated worktree:
git worktree add -b ade/<slug>-<uuid8> .ade/worktrees/<slug>-<uuid8> <base_ref>Lane types (per lanes.lane_type):
| Type | Worktree location | Notes |
|---|---|---|
primary |
Project root | The main repo checkout (e.g., main). |
worktree |
.ade/worktrees/<slug>-<uuid8> |
Standard ADE lane. |
attached |
User-specified path | Pre-existing worktree linked to ADE (attached_root_path column). |
Worktree lifecycle: create (60s timeout), archive (DB status only, worktree remains on disk), delete (git worktree remove + optional git branch -D), cascade-delete dependent rows (deltas, sessions, operations, pack index).
- Lanes have
parent_lane_id(self-FK onlanes). Stacks are parent/child chains. - Stack operations: rebase propagation, base-ref resolution (
shared/laneBaseResolution.ts). autoRebaseService.ts+rebaseSuggestionService.ts— automatic rebase proposals when parent moves; user can accept/defer/dismiss.computeLaneStatus()returns{ dirty, ahead, behind }on demand, no caching. Status derivation usesgit status --porcelain=v1andgit rev-list --left-right --count.
- Queue landing (
queueLandingService.ts) — ordered PR landing with rebase propagation. - Conflict prediction —
conflictService.tsusesrunGitMergeTree():git merge-tree --write-tree --messages --merge-base <base> <branchA> <branchB>
- Pairwise dry-merge simulation across all active lanes; output parsed into structured
ConflictOverlapentries. - Triggered on debounced lane/head changes via the job engine; periodic prediction is off by default in dev stability mode.
- Result: risk matrix surfaced on Graph + Conflicts pages, confidence-scored proposals (
high/medium/low) with apply/discard UI.
ensureRelativeRepoPath()rejects empty, null-byte, absolute, and traversal paths.- Force push uses
--force-with-lease, never--force. - Branch-protection support on primary lane.
- Destructive ops (discard, hard reset) require UI confirmation.
Related Git docs: Lanes, Lane runtime isolation, and Pull requests.
ADE carries continuity through the records owned by each runtime surface: chat transcripts, CTO and worker session logs, daily logs, and explicit context documents. These are read directly by the services that need them; there is no separate retrieval layer in between.
ADE does not generate PRD or architecture bootstrap documents. Agent prompts tell models to inspect the repository directly when they need product or architecture context, starting with AGENTS.md, README.md, docs/, package manifests, and relevant source files.
| Call type | Payload |
|---|---|
| Narrative generation | LaneExportStandard (lane, bounded) |
| Conflict proposal | LaneExportLite (lane) + LaneExportLite (peer, optional) + ConflictExportStandard |
| PR description | LaneExportStandard with commit history |
| Initial context (repo scan) | Targeted file/commit digests |
Proof is intentional. Agents run computer use through whatever tool they already have — Claude's computer_use, Codex shell, a scripted browser, a headless Playwright run, a local screenshot. ADE stays out of that loop. When the agent reaches a checkpoint worth showing, it files an artifact through the broker (directly or via ade proof capture / attach), optionally with a caption. That record is what the drawer UI renders and what reviewers see.
The previous control-plane model — ComputerUsePolicy (off/auto/enabled, allowLocalFallback, retainProof, preferredBackend), passive proofObserver ingestion from chat tool_result events, and the Settings > Computer Use panel — was removed. There is one path now: intentional ingest via the broker.
apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.ts is the ingest boundary. It accepts ComputerUseArtifactInput[] (path, remote URI, inline text, inline JSON), materializes on-disk sources into the project artifacts dir via secureCopyFromDescriptor (uses O_NOFOLLOW + atomic rename to resist symlink tricks), writes the canonical computer_use_artifacts row, and links to one or more owners (lane, chat_session, automation_run, github_pr, linear_issue).
Allowed import roots (trust boundary): .ade/artifacts, .ade/tmp, os.tmpdir(), ~/.agent-browser. Other paths are rejected.
Supporting files in the same directory:
controlPlane.ts— buildsComputerUseOwnerSnapshot(recent artifacts + activity) andComputerUseSettingsSnapshot(backend readiness, capabilities) over the broker.localComputerUse.ts— exportsgetLocalProofCaptureCapabilities(), a macOS-only descriptor reporting whetherscreencapture, app launch, and GUI-interaction commands are available.agentBrowserArtifactAdapter.ts— parses agent-browser payloads intoComputerUseArtifactInput[].syntheticToolResult.ts— produces tool-result stubs during Claude compaction so a previously-executed tool response can be re-surfaced without re-running the tool.
Canonical proof kinds: screenshot, video_recording, browser_trace, browser_verification, console_logs.
Canonical tables:
computer_use_artifacts— proof kind, backend name/style, source tool metadata, title/description, URI, storage kind, MIME type, review/workflow state, timestamps.computer_use_artifact_links— cross-domain ownership, so the same artifact can graduate from exploratory chat evidence to a PR comment without losing provenance.
Channels (under ade.proof.*, renamed from ade.computerUse.*):
ade.proof.listArtifacts,ade.proof.getOwnerSnapshot,ade.proof.routeArtifact,ade.proof.updateArtifactReview,ade.proof.readArtifactPreview, plus aade.proof.eventpush channel.ade proof capture/attach/listin the ADE CLI are the cross-process surface; they call into the broker.
Renderer surfaces:
ChatComputerUsePanel(drawer under the chat composer) and PR/automation proof surfaces.- Review actions (
accepted/needs_more/dismissed/published) remain as first-class per-artifact actions. - Computer-use readiness moved into
IntegrationsSettingsSection— the standaloneComputerUseSection.tsxis gone.
The sync subsystem is owned by the ADE runtime (apps/ade-cli/src/services/sync/). When a project is opened, its scope creates a sync service inside the runtime; that runtime is the sync authority. The desktop client and iOS client both connect to the same service. Desktop's old in-process host code path is disabled by default and only re-enabled with ADE_ENABLE_DESKTOP_SYNC_HOST=1 for diagnostics.
- Runtime / desktop: native cr-sqlite loadable extension (
.dylib/.dll) loaded viaopenKvDb(...)inkvDb.ts. - iOS: pure-SQL CRR emulation in
apps/ios/ADE/Services/Database.swift—crsql_master,crsql_site_id,crsql_changes, per-table<table>__crsql_clocktables replicated as plain SQLite, with INSERT/UPDATE/DELETE triggers writing Lamport-versioned rows tocrsql_changes. Custom SQLite functions (ade_next_db_version(),ade_local_site_id(),ade_capture_local_changes()) provide trigger context. Changesets are wire-compatible with the runtime's cr-sqlite. - Merge: last-writer-wins per column. Each device has a unique site ID; Lamport timestamps per column.
- Sync API (
AdeDb.sync):getSiteId,getDbVersion,exportChangesSince(version, { maxRows?, throughDbVersion? }),applyChanges(changes),discardUnpublishedChangesForTables(tableNames). - Bounded, snapshot-isolated exports:
exportChangesSincescans boundeddb_versionwindows (the sync pump walks 250k-version windows per poll) inside a read transaction that pins the WAL snapshot — thecrsql_changesvtab aborts on concurrent commits and a bareLIMITcannot bound a vtab scan. Startup self-heals orphaned__crsql_clock/__crsql_pksshadow tables (base table dropped, shadows left behind), which otherwise abort everycrsql_changesscan. - Suppression:
discardUnpublishedChangesForTableswrites a per-table, per-site high-water mark into the local-onlylocal_crr_change_suppressionstable. SubsequentexportChangesSincecalls drop local-site rows for those tables at or below that mark, so a local wipe (e.g. clearingdevicesandsync_cluster_statewhen joining another host as a viewer) cannot leak back as DELETE rows. The viewer-join path follows the wipe withsyncPeerService.acknowledgeLocalDbVersion()to advance the outbound cursor past the suppressed range. - Transport: one brain-level WebSocket listener on port 8787 by default (preferred-port retry for ~3 s before falling back to a port scan, so restarts do not drift the port phones saved); JSON-framed changesets + zlib compression for large batches; encoded envelopes >720 KB are sliced into
envelope_chunkframes for peers declaring thechunkedEnvelopescapability; 30s ping/pong. The same envelope channel carries project catalog, project-switch, and runtime-scoped project-action messages (browse/open/create/clone/list GitHub repos/default parent directory); on a hosted-project switch the new host service adopts the open sockets, so connected phones survive the swap. A machine-wide fallback handler serves catalog/project actions when no project host owns the listener, while handoff-time reconnects still park for adoption by the next host. Phones keep per-host-DB sync cursors keyed by theserverDbSiteIdfromhello_ok, and the host filters high-churn tables the phone never reads (transcripts, operations, usage logs, automation runs) from phone changesets.
- Sync authority: a runtime on one reachable machine owns live execution side effects (agents, PTYs, processes) for a given project. Stored in the synced
sync_cluster_statesingleton row (brain_device_idis the legacy internal column name). Transfer requires a clean preflight (no running turns, live PTYs, running processes). CTO history and idle chats are durable and survive handoff. - Controllers: other connected devices (phones always; a second desktop optionally). Controllers read synced state and send commands to the authority runtime.
- Independent desktops: a second Mac can run its own ADE runtime and work independently through git without joining an ADE sync session. The tracked
.ade/scaffold/config layer makes a clone look like an ADE project immediately.
- App launch reads pairing secret from iOS Keychain.
- Opens WebSocket to host after racing all saved address candidates with concurrent TCP probes (happy eyeballs) — a dead LAN IP no longer delays the live Tailscale route. Sends local
db_versionplus the per-host-DB cursor map (remoteDbVersionBySite); host replies with itsserverDbSiteIdand sends catch-up changesets. hello_okcan include the host's mobile project catalog and project-action feature flag. The iOS app shows a native project home until an active project is selected, can browse/open/create/clone projects on the paired machine when project actions are available, then drivesproject_switch_request/project_switch_result; the port stays stable across switches.- Bidirectional sync continues; inbound processing (envelope parse, gunzip, chunk reassembly, changeset decode + apply) runs off the main actor. On disconnect: a fast exponential-backoff burst, then an indefinite ~30 s slow-heartbeat retry — the phone never permanently gives up.
reconnectIfPossibleis guarded against overlapping runs. - Chat streaming resumes by sequence: each
chat_eventcarries a host-assigned per-sessionseqbacked by a replay buffer;chat_subscribepassessinceSeqso reconnects replay only the missed events. The subscribe ack also carriesturnActive(live turn state from the agent chat service) so a phone subscribing mid-turn renders streaming/stop affordances immediately even when the byte-capped snapshot tail dropped the turn's start event.chat.getTranscriptpages older history via an opaque cursor. - All reads are local and scoped to the active project id — the iOS tab is instant and offline-capable after the selected project's row has hydrated.
- Writes from user actions: write locally, replicate to host. Execution commands (create PR, run command) are routed to the host via the
command/command_ack/command_resultmessage flow. - Sub-protocols: changeset sync, project catalog/switch, file access,
subscribed terminal stream/control, chat stream (live
chat_eventpush from host), command routing, and lane presence announce/release. Command routing includes the Work CLI launcher (work.startCliSession), whose provider command construction is shared with the desktop Work tab throughapps/desktop/src/shared/cliLaunch.ts. - Pairing is a user-set 6-digit PIN stored at
.ade/secrets/sync-pin.jsonon the host. The phone sends the PIN once; the host returns a durable per-device secret. QR payload is v2 (host identity + port + address candidates, no pairing code). - Widgets:
ADELockScreenWidgetreads from a sharedWorkspaceSnapshotin the App Group container. Home Screen, Control Center, and ActivityKit surfaces are not registered. - Tabs: Lanes, Files, Work, PRs, CTO, Settings.
- LWW per column via Lamport timestamps is the default merge.
ON CONFLICT(...)upserts must target PK only (non-PK UNIQUE does not survive CRR retrofit).- Non-PK merge cases use explicit select-then-update.
.ade/local.secret.yaml(API keys, ADE CLI configs), sync site ID, sync device ID, sync bootstrap token: never sync.- Each device stores its own pairing secret in OS Keychain.
- Linear creds, GitHub tokens, provider API keys stay on the host.
- Commands from non-host devices validated and executed by the host only.
Related sync docs: Sync and multi-device, iOS companion, and Remote commands.
ADE/
├── apps/
│ ├── ade-cli/ # ADE brain, manual runtime entry points, `ade` CLI, `ade code`
│ ├── desktop/ # Electron client (multi-window; local + SSH-bound runtime bindings)
│ ├── ios/ # Native SwiftUI controller (WebSocket to ADE machine)
│ └── web/ # Marketing + download landing (Vite + React)
├── docs/
│ ├── PRD.md
│ ├── features/
│ ├── perf/
│ ├── plans/
│ └── playbooks/
├── scripts/ # Release, validate, notarize, after-pack (per-platform)
│ # Platform-specific: validate-mac-artifacts.mjs,
│ # validate-win-artifacts.mjs, ade-cli-windows-wrapper.cmd, etc.
├── apps/desktop/vendor/crsqlite/
│ ├── darwin-arm64/
│ ├── darwin-x64/
│ └── win32-x64/ # Prebuilt cr-sqlite native binaries per platform
├── .github/workflows/
│ ├── ci.yml
│ ├── prepare-release.yml
│ ├── release.yml
│ └── release-core.yml
├── docs.json # Mintlify public docs config (separate site)
├── package.json # Root test aggregator
└── .ade/ # Self-hosted ADE project state (ignored subset)
Root package.json is a thin aggregator: npm test and npm run test:ci run the desktop suite in CI-style shards plus the ade-cli suite. npm run test:coverage runs desktop coverage plus the ade-cli suite.
Per-app scripts:
| App | Key scripts |
|---|---|
apps/desktop |
dev, build (tsup + vite), typecheck, test (vitest), lint (ESLint), dist:mac, dist:mac:universal:signed:zip, notarize:mac:dmg, validate:mac:artifacts, rebuild:native, version:ci, version:release, ade:dev, ade:build, ade:test. |
apps/ade-cli |
dev, build, typecheck, test (typed CLI commands, headless runtime, and Ink Work chat TUI). |
apps/web |
dev, build, preview, typecheck. |
apps/ios |
Xcode project; tests via xcodebuild test / Xcode. |
Stages:
- Install (
installjob) — checkout, setup Node 22, parallelnpm ciacross desktop, ade-cli, and web with a shared cache keyed on those lockfiles. - Parallel checks:
secret-scan— gitleaks on full history.typecheck-desktop—cd apps/desktop && npm run typecheck.typecheck-ade-cli—cd apps/ade-cli && npm run typecheck.typecheck-web—cd apps/web && npm run typecheck.lint-desktop— ESLint onsrc/**/*.{ts,tsx}.test-desktop— 8-way shard matrix:npx vitest run --shard=${{ matrix.shard }}/8across shards 1–8.test-ade-cli— full ade-cli vitest.build— desktop, ade-cli, and web built sequentially after install.validate-docs—node scripts/validate-docs.mjs.
- Gate (
ci-pass) — all required jobs must pass (if: always()with failure/cancelled detection).
Sharding is required because the desktop suite is large enough to be slow in a single process.
- Tooling: Vitest with
nodeenvironment,pool: "forks",maxForks: 4, 20s test/hook timeouts. - Config:
apps/desktop/vitest.workspace.tsdefines theunit-main,unit-renderer, andunit-sharedprojects. The pinned Vitest version does not support CLI--project, sotest:unitis plainvitest run,test:integrationfilters*.integration.test.*, andtest:componentfilterssrc/renderer/**/*.test.*. Root desktop sharding usesscripts/run-desktop-test-shards.mjs, which runsvitest run --shard=N/8for shards 1-8. - Test locations: colocated with source (
*.test.ts/*.test.tsx) undersrc/**. - Setup:
apps/desktop/src/test/setup.ts(browser/DOM mocks viabrowserMock.ts). - Philosophy: keep tests that carry real value; aggressively remove brittle UI/render tests; keep mutation + integration coverage solid.
- Smoke tests:
packagedRuntimeSmoke.test.tsfor packaged runtime.
macOS:
npm run dist:mac— notarized .dmg for local distribution.npm run dist:mac:universal:signed— universal x64+arm64 signed builds.npm run dist:mac:universal:signed:zip— zip archive variant.
Windows:
npm run dist:win— x64 installer viaelectron-builder --win --x64, wrapped withvalidate:win:artifacts(preflight) andvalidate:win:release(post-build) checks inapps/desktop/scripts/validate-win-artifacts.mjs.- Windows-only wrappers for the bundled
adeCLI ship inapps/desktop/scripts/:ade-cli-windows-wrapper.cmd(launcher) andade-cli-install-path.cmd(idempotent PATH install helper). The platform-agnostic.shwrapper covers macOS/Linux. - The Windows installer bundles the prebuilt
cr-sqlitenative binary fromapps/desktop/vendor/crsqlite/win32-x64/and a Windows node-pty ConPTY worker.validate-win-artifacts.mjsasserts each one is unpacked. - GitHub Actions
release-core.ymlbuilds and validates Windows artifacts. The release job picks upWINDOWS_CSC_LINK/WINDOWS_CSC_KEY_PASSWORD(or legacyWIN_CSC_*) from secrets and forwards them as electron-builder'sCSC_LINK/CSC_KEY_PASSWORDto sign the installer andapp.exe; the desktop config sets SHA-256 hashing and the DigiCert RFC3161 timestamp server. When the secrets are absent, the workflow still produces unsigned Windows artifacts. - Ongoing Windows integration lane (rebase with
main, smoke tests, backlog):docs/development/windows-port-lane.md.
Post-packaging hardening (apps/desktop/scripts/):
runtimeBinaryPermissions.cjs— restores exec bits onnode-ptyspawn helpers, Codex vendor binaries, Claude SDK ripgrep helpers; patchesnode-ptyunixTerminal.jsfor ASAR-unpacked paths.after-pack-runtime-fixes.cjs— electron-builder after-pack hook. Covers both platforms: runs the permissions pass on macOS and stages CLI wrappers + runtime shims on Windows.validate-mac-artifacts.mjs/validate-win-artifacts.mjs— per-platform artifact validators; confirm expected binaries, release signing state, bundled ADE CLI help, isolated ADE Code TUI help, and every required bundled ADE Agent SkillSKILL.md. They also fail if the bundled TUI references__dirname/__filenamewithout ESM shims. Windows signing verification is opt-in with--require-signedorADE_REQUIRE_WIN_SIGNING=1.notarize-mac-dmg.mjs— Apple notarization.
- Internal docs (this directory +
docs/) — for engineers and agents. Not published. - Public docs site — Mintlify, configured in
docs.jsonat repo root. Content lives alongside the repo (introduction.mdx,quickstart.mdx,welcome.mdx,key-concepts.mdx, plus subdirsgetting-started/,guides/,lanes/,chat/,cto/,pull-requests/,configuration/,tools/,computer-use/,automations/,ai-tools/). Thememaple, brand primary#7C3AED. - Doc validation:
scripts/validate-docs.mjsruns in CI to catch broken links / structure drift.
- Main-process logger —
apps/desktop/src/main/services/logging/logger.ts(createFileLogger). Writes structured JSONL to~/.ade/logs/<project>/ade-main.log. Categories:ipc.*,project.startup_task_*,renderer.*, per-service telemetry. - Redaction — all log writes pass through
redactSecrets()/sanitizeStructuredData(). - Retention — local, indefinite until user clears.
- IPC tracing — every handler emits
ipc.invoke.begin/ipc.invoke.done/ipc.invoke.failedwith call ID, channel, window ID, duration, summarized args. Mandatory for new handlers. - Renderer lifecycle —
renderer.route_change,renderer.tab_change,renderer.window_error,renderer.unhandled_rejection,renderer.event_loop_stall. Mandatory for new surfaces that introduce novel lifecycle transitions. - Startup tasks —
project.startup_task_enabled,project.startup_task_skipped,project.startup_task_begin,project.startup_task_donewith durations. - Usage tracking —
usageTrackingService.ts+usage/ledgers/*+budgetCapService.tsaccount for tokens and cost per provider/model/call-type; surfaced in the top-bar Usage popup (HeaderUsageControl→UsageQuotaPanel+ collapsibleBudgetCapEditor).main.tskeeps a dormant usage tracker alive while no project context is open so the main menu can show provider usage from machine-level Claude/Codex auth, then pauses it while project-scoped contexts own polling. - Local perf runs —
scripts/perf-launch.mjs/scripts/run-perf-scenario.mjslaunch ADE with a run id, feed renderer scenarios, and collect JSONL events plussummary.jsonunder~/.ade/perf-runs/<runId>/. This is local-only diagnostics, not external telemetry. - No external telemetry — ADE does not ship analytics to any cloud service. All telemetry is local.
- Every cleanup step is
try/catchisolated — one failing service must not block shutdown. - IPC handlers return structured errors, never crash the renderer.
- CTO and AI UI components use try/catch around async loads with
isLoading/errorstate and retry actions. - Graceful degradation: when no provider is configured, AI surfaces show explanatory disabled state rather than spinning.
- Explicit fallbacks: Linear sync skips when no credentials/workflows; Linear ingress stays dormant without config; trivial session summaries skip AI entirely.
- Dev tools probe —
devToolsService.tschecks forgitandghCLI availability at startup, surfacing warnings in UI. - Port allocation —
portAllocationService.tsmanages per-lane port leases with orphan recovery. - Runtime diagnostics —
runtimeDiagnosticsService.tssurfaces lane launch context and runtime state. - Sync telemetry —
sync_cluster_state+ device registry surfaced in Settings → Sync. - Operation timeline —
operationService.ts+ History page provide full audit trail for debugging and undo. - Shutdown sequence:
- Stop head watcher + background timers.
- Dispose pollers and ingress services.
- Stop file watchers, tests, managed processes.
- Dispose PTYs and agent chat sessions.
- Dispose sync service (stop host, disconnect peer).
- Flush SQLite before service disposal begins (durable writes first).
- Per-service
try/catch-isolated dispose. - Final SQLite flush + close.
- Product spec · PRD.md
- Runtime and remote bindings · Remote runtime
- Terminal client · ADE Code
- Project dashboard / Run tab · Project Home
- Lanes and Git isolation · Lanes
- Agent chat · Chat
- Pull requests and queues · Pull Requests
- Multi-device sync and iOS · Sync and Multi-device
- Terminal sessions and Work · Terminals and Sessions
- Computer-use proof · Computer Use
- Deeplinks · Deeplinks
- Settings and onboarding · Onboarding and Settings
- Feature index · features/