### Adr/0001 Host Authoritative Workspaces # Host-authoritative workspaces: the inventory-and-command model > **Partially superseded by ADR 0005 and ADR 0006.** The host-authoritative stance, the > rejection of spec/status convergence, and the registry consequences below all stand. The > transport clause — "mutating host state happens only through one-shot durable commands in an > outbox" — no longer holds at all: mutations are plain fail-fast RPCs (0005 for workspace > verbs, 0006 generally), and deletion intent survives unreachability as a durable client > tombstone swept by a deletion-only reconcile loop (0006). The durable index also now lives > host-side (the workspace registry), with the desktop keeping a mirror + annotations. Workspaces (repos, worktrees, directories) live on hosts that emdash shares with humans and other tools. We decided the host is authoritative for what exists: the desktop keeps a *registry* — a tracking index of discovered reality plus desktop-only annotations — and sync converges the *record* toward the *world* (adopt untracked worktrees, mark tracked-but-gone rows missing). Mutating host state happens only through one-shot durable commands in an outbox (idempotent by deterministic id, executed when the host is reachable, cancelled when the host is forgotten). Desktop-owned records (tasks, automations, projects) delete immediately and never block on a host. ## Considered options The Kubernetes-style spec/status model (record is desired state; controllers converge the world toward it) was explicitly rejected, for three reasons: 1. **Emdash is a guest on the host, not the owner.** A convergence controller would fight the user — recreating worktrees they deleted, removing worktrees they made by hand. What the user does to the host is fact, not drift. 2. **Convergence assumes an always-on controller colocated with the truth.** The desktop is intermittently connected and sometimes permanently gone. The only standing loop we keep (orphaned-session GC) runs host-side for exactly this reason. 3. **Multi-writer desired state is incoherent.** Both emdash and users create worktrees; a spec model would need desired state backfilled for artifacts emdash never asked for. In our model creation provenance is optional (`config` is NULL for adopted workspaces). Prior art and detailed comparison (K8s API conventions, Terraform state, git's own worktree admin records): `agents/research/reconciliation-models.md`. Git's worktree tracking is the closest precedent — filesystem-authoritative records, auto-prune of stale entries, lock-with-reason protecting annotated ones. ## Consequences - **Positive-assertion invariant**: a snapshot is a positive assertion about what exists; a failed, partial, or errored scan writes nothing to the registry. "Host reachable but the scan errored" must be indistinguishable from "host unreachable" — never from "the repo has no worktrees". This is the guard against the mass-drop hazard Terraform documents for misconfigured refresh. - **Immediate pure-mirror untrack**: never-annotated adopted rows are untracked on the first confirming snapshot (no git-style grace period) — they are reconstructible by the next scan, and the positive-assertion invariant already blocks the failure mode a grace period would hedge. - Registered rows with desktop annotations are never auto-deleted; they surface as missing until the user untracks them. - Out-of-band deletions are recorded, never repaired; out-of-band creations are adopted, never reverted. --- ### Adr/0002 Registry Single Writer # The Registry is the sole writer of the workspaces table The `workspaces` table is the Registry from ADR 0001, and its invariants (untrack-never-delete, tombstone/revert symmetry, observation columns owned by sync, annotation columns owned by features) were re-implemented by ten independent writers across six slices. We decided a single Registry module owns every write through a verb vocabulary — `register` (emdash-created, with Provenance), `adopt` (host-discovered, without), `refresh` (observations only), `untrack`/`revertUntrack`, `resurrect`, `annotate`, and `purge` (hard delete, valid only on already-untracked rows) — with verbs accepting an optional transaction handle so callers keep atomicity across tables. Raw drizzle access to the table outside the module is banned by lint. ## Considered options - **Full read/write monopoly** — rejected: read needs are join-shaped and consumer-specific (search wants branch names per task, sync wants live roots per host), so the interface would inflate into one-caller getters. Instead the Registry exports only `getLive`/`findLiveByKey` and a named `liveWorkspaces()` predicate that external joins must build on. - **Writes only, reads raw** — rejected: the liveness rule ("tracked" means `untrackedAt IS NULL`) appeared ~26 times; when its meaning changes (e.g. excluding removal-pending rows), that must be a one-site edit. ## Consequences - The positive-assertion invariant is structural, not remembered: the snapshot type consumed by convergence is only constructible from a successful scan, and snapshot application returns a report. - `purge` legitimizes retention deletion of dead rows; it asserts rows are already untracked and throws otherwise. Untracking remains the only way a tracked row leaves the Registry. - The `isAnnotated` predicate (task link or Provenance present) is a Registry rule, not a sync detail. --- ### Adr/0003 Teardown Scripts In Remove Worktree # Teardown scripts run inside the removeWorktree verb > **Superseded by ADR 0005.** Workspace removals are no longer queued offline — they are plain > RPCs that fail fast when the host is unreachable — so the queued-offline case this exception > existed for is gone. Teardown now runs inside `deactivateWorkspace` (kill-sessions + time-boxed > non-fatal teardown), which the delete verbs compose server-side. The narrowing constraints > below (non-fatal, time-boxed, only user script inside a verb) carry over to that placement. Host verbs are pure inventory mutations — user scripts were deliberately kept out of them: `scripts.prepare` and `scripts.setup` run in the session plane (stamped workspace initialization), so a durable host operation never blocks on or fails from user code. We made one exception: `scripts.teardown` executes as a stage of the `removeWorktree` expansion, between kill-sessions and the worktree removal itself. ## Why the exception Teardown's job is to undo what setup/run started — dev servers, containers, tunnels — and those live on the host, surviving desktop disconnects. Removals can be queued offline: the outbox submits `removeWorktree` when the host reconnects, possibly hours after the desktop enqueued it. At execution time the only party present is the host, mid-verb. Every alternative placement fails exactly the queued-offline case the outbox exists for: - Desktop-triggered teardown at deactivation runs only when the host is reachable at enqueue time — silently skipped otherwise. - Running it both at deactivation and in the verb executes user scripts twice in the common case and leans on an idempotency promise we chose not to rely on for prepare. ## Constraints that keep the exception narrow - **Non-fatal**: a failed or timed-out teardown records a warning stage and the removal proceeds. A broken teardown script must never make a workspace undeletable. - **Time-boxed**: the stage has a hard timeout; the verb never hangs on user code. - Teardown remains the *only* user script inside any verb. Creation stays pure git — prepare and setup run in the session plane (see the legacy-path-retirement map, ticket 02). --- ### Adr/0004 Cancellation Is Best Effort Across Planes # Cancellation is best-effort across planes > **Retired by ADR 0006.** The outbox and its pending desktop records no longer exist, so > there is no desktop-local `cancelled` state to define. What remains of this ADR's reasoning: > forget-host and "Untrack anyway" purge deletion intent without host confirmation, an > in-flight create cancels via the RPC signal, and orphaned host effects still surface through > the observation plane (adoption) — never silently lost. A desktop cancellation settles the desktop record immediately, without waiting for the host to confirm. `cancelled` on the desktop means "I stopped asking" — not "it didn't happen." If the intent already reached an unreachable host, the host may run the operation to completion anyway; that **orphaned host execution is permitted by design**, and its outcome re-enters the desktop through the ordinary observation path (scan and registry reconciliation) on reconnection, never silently lost. ## Why The naive faithfulness claim — the desktop record's terminal outcome eventually equals the host record's — is falsified by cancel-while-disconnected: the desktop settles `cancelled`, the unreachable host runs to `succeeded`, and the two records disagree forever. Something has to give, and the alternatives are worse: - **Holding cancellation pending until host confirmation** lets an unreachable host pin the desktop record open indefinitely — holding its claims and blocking every successor operation on those resources. That is the wrong trade for an intent ledger, whose job is to never block the desktop on host availability. - **Guaranteeing the host doesn't execute** is physically impossible once the intent has been forwarded and the wire is down. ## What faithfulness still means The single equality claim splits into three checkable properties (assurance inventory, `bridge.P2`–`P4`): - **Fabrication-freedom** (safety): when a desktop record settles with an outcome attributed to host execution, that outcome is one the host record actually reached. The desktop never invents success or failure — `cancelled` is desktop-local by definition, not attributed. - **Convergence** (liveness): every *non-cancelled* desktop record eventually settles with the host record's terminal outcome, under eventual reconnection. - **Orphan observation** (liveness): a host outcome orphaned by a desktop-local cancellation is eventually observed on reconnection — e.g. a worktree created by a cancelled `createWorktree` shows up via snapshot and is adopted, not leaked. ## Constraints - Cancellation toward the host stays best-effort in mechanism too: if the host is reachable, the bridge forwards the cancel and the host stops the work at the next stage boundary; the desktop does not wait on that round-trip to settle. - UI copy and downstream consumers must not read `cancelled` as "no effects happened." Effects of orphaned execution surface later as observed reality. - Orphan observation leans on the adoption path, which currently strips provenance; recovering provenance for orphaned creations is tracked separately (assurance map, orphan provenance recovery). --- ### Adr/0005 Host Workspace Registry Plain Rpc Verbs # Host-side workspace registry with plain-RPC lifecycle verbs Each host runs a workspace registry: a sole-writer runtime over its own durable store, holding registered paths plus host-computed observations (branch, dirty, ahead/behind, changed lines including untracked files), published to desktops through a single `records` live model that merges the durable rows with an in-memory runtime overlay (creation progress, activation state, notices). The filesystem remains the source of truth — the registry observes it and auto-adopts worktrees of registered repositories; it never converges the world toward a record (ADR 0001's host-authoritative stance is unchanged). The desktop keeps its mirror + annotations and applies each delivery as a full snapshot through the registry sole writer (ADR 0002's verbs stay). Workspace lifecycle is six task-independent verbs — `createWorkspace`, `createWorktree`, `activateWorkspace`, `deactivateWorkspace`, `deleteWorkspace`, `deleteWorktree` — modeled as **plain wire RPCs**. This supersedes ADR 0001's transport clause ("mutating host state happens only through one-shot durable commands in an outbox") for workspace mutations: no inbox/outbox, no durable operation log, no liveJob endpoints. Handlers serialize exclusively per-repository for worktree create/delete and per-workspace for activate/deactivate/delete (waits, not errors). *(ADR 0006 fixed the implementation: a `KeyedMutex` with fixed repo-before-workspace acquisition order — the kernel claim machinery retires entirely.)* ## Why plain RPCs The outbox existed to survive two gaps: desktop-to-host disconnection at enqueue time, and daemon interruption mid-execution. Both are now handled without durable commands: - **Unreachable host → fail fast.** Every verb returns a typed `host-unreachable` error and this design queues nothing. A workspace-specific pending-intents queue ("mini-outbox") was considered and rejected: it reintroduces the exact machinery this design removes. "Removal pending" leaves the workspace vocabulary. The system-wide offline-delete answer — durable client-side tombstones swept by an entity-generic reconcile loop (operation-log-retirement effort) that calls these delete verbs when the host is reachable — composes with this contract rather than changing it; idempotent deletes are what make that sweep safe to repeat. - **Interruption → durable fact records, client-driven retry.** `createWorktree` registers its record at the start (`lastCreateOutcome: 'started'`) and records `succeeded` or `failed(stage, error)` durably. An interrupted creation is visible in the registry after restart; recovery is a client replay (same UUID + identical immutable spec re-executes; a succeeded one no-ops; a divergent spec errors). The host never re-converges on its own — outcomes are facts, not desired state. Progress needs no job objects: callers watch the `records` live model, whose runtime overlay carries creation stage and activation script states. After a daemon restart the overlay is simply absent — activation is ephemeral by design (`lastActivatedAt` is an observation, never a durable "active" flag). ## Consequences - `deactivateWorkspace` is the sole owner of kill-sessions + time-boxed non-fatal teardown; the delete verbs compose it server-side. This retires ADR 0003 (teardown inside the removeWorktree verb): 0003's rationale was the queued-offline removal, which no longer exists. - Deletes are idempotent (absent id succeeds) and never refuse for dirty/unpushed state; informed confirmation is the client's job, powered by mirror observations — no preflight RPC. - `deleteWorkspace` never touches disk; `deleteWorktree` is the only artifact-destroying verb. - Desktop-side scan tiers, the workspace snapshot pull sync, and the workspace outbox definitions retire; freshness (fs-events primary, activity-gated escalation, polling floor, explicit refresh verb) is a host concern. - Retiring the outbox for anything else (conversations delete) is a separate effort; until then the two transports coexist, scoped by domain. --- ### Adr/0006 Tombstone And Reconcile Deletion # Tombstone-and-reconcile deletion; the operation log retires Deletion of host-resident entities is the one host mutation that must survive host unreachability. It no longer rides a durable command queue: deleting an entity **tombstones its mirror row** — a durable client-side mark carrying the frozen deletion options — and an **entity-generic reconcile sweep** runs whenever the host is reachable, calling the owning surface's idempotent delete verb for each tombstoned entity and purging the tombstone when the mirror confirms the host record gone. The tombstoned row is the durable intent and the queue; there is no ledger, no minted idempotency keys, and no drain ordering. With that, the durable-operation machinery retires on both planes: the kernel (operation records, claims, admission, conflict policies, dispatch, its SQLite stores), the desktop outbox/ledger engine, the host command inbox, the desktop↔host operation bridge, and the operation projections. Host verb handlers serialize with a `KeyedMutex` (per-repo exclusive for worktree create/delete, per-workspace for activate/deactivate/delete, fixed repo-before-workspace acquisition order). This supersedes ADR 0001's command clause generally — ADR 0005 already superseded it for workspace mutations; this ADR extends the plain-RPC + tombstone model to every host-resident entity kind (conversations included) and removes the machinery itself. ADR 0001's snapshot half — host authority, the positive-assertion invariant, adopt/missing convergence — stands unchanged. ## The model - **Creation and lifecycle verbs are plain fail-fast RPCs** (ADR 0005). Nothing is queued for them; interrupted creates recover via the host's own durable record plus idempotent client replay. - **Deletion intent is a tombstone**: written atomically at delete time on the mirror row, freezing the deletion options and the target record's UUID. Identity-keyed removal — the verb no-ops when the record at the path carries a different id — closes the delete-racing-create hole; a tripped guard counts as converged. Creation admission refuses a path carrying a pending tombstone (a data check, replacing claim conflicts). - **The sweep is entity-generic**: one thin primitive (sweep + retry + purge), per-kind idempotent removal functions contributed by each registry. Triggers: boot, reconnect, tombstoned-while-reachable, plus a 10-minute backstop that doubles as the retry vehicle (per-item backoff). Failure classes are host-written on the record (transient retries silently; terminal stops and surfaces Retry / Untrack-anyway). No cross-kind ordering — verbs are self-sufficient; refusals resolve by later retry. - **Outcomes live on host records, not operation records**: verbs annotate the record (stage, class, message, timestamp) before returning; the annotation syncs to the mirror, which is the only thing the UI reads. RPC returns are loop control, never UI truth. - **No tombstone expiry**: the tombstoned row is visible with affordances; boundedness comes from visibility, the terminal-failure stop, and Untrack-anyway — not from a timer. - **Forget-host purges mirror rows, tombstones included.** ## Consequences - **ADR 0004 is retired.** With no outbox there is no pending desktop record to cancel; "cancelled" as a desktop-local terminal state disappears. The cancellations that remain are forget-host and Untrack-anyway (both purge intent) and aborting an in-flight create via the RPC signal. Orphaned host effects still surface through the observation plane — that half of 0004's reasoning lives on in the adoption path. - The desktop keeps no admission guard, no claims, and no cross-client coordination; the only client-side concurrency mechanism is an in-memory per-tombstone single-flight marker. The never-implemented "Host claim" concept is dropped. - Automation runs await the plain `createWorktree` RPC; failure attribution stays on the run record, fed by the RPC error. - History becomes per-step durable last-outcomes on host records (`lastCreateOutcome`, `lastRemovalAttempt`, per-script outcomes) — no event list; nothing rendered one. - Tombstones are the future multi-client sync unit for deletions: the reconcile tombstone and the planned remove-wins CRDT tombstone are one record, and any client may execute one (safe by host serialization + idempotency + mirror-confirmed purge). - Both kernel SQLite databases drop in a hard cutover (nothing server-side is shipped). Design detail and the guarantee-by-guarantee gap walk: `.scratch/operation-log-retirement/spec.md`. --- ### Adr/0007 Workspace Host Runtime Retired # The workspaceHost runtime retires; the registry is the workspace plane The v8 wire cleanup removes the `workspaceHost` contract and its runtime worker entirely (13 host runtime contracts → 12). The registry established by ADR 0005 was already the sole owner of workspace lifecycle; workspaceHost had shrunk to a residue of one observation and three dead or single-caller verbs. This ADR records the collapse — it fulfills rather than revises ADR 0005/0006, whose models are unchanged. Disposition of the surface: - `measureUsage` — the git-aware disk observation (total bytes plus reclaimable git-ignored artifact bytes) moves to `workspaceRegistry.measureUsage({ workspaceId })`, id-keyed like the registry's other per-workspace observations. - `initializeWorkspace` — cut. Its one consumer, the automations runtime, rewires to `workspaceRegistry.createWorkspace` (idempotent, designed for registering existing paths, worktrees auto-adopting their parent repository) followed by `activateWorkspace`. Side benefit: automation workspaces become registry-visible at run start instead of waiting for adoption scans. - `runWorkspaceScript` and `notices` — cut as dead. Production scripts flow through registry activation and `terminals.runWorkflow`; the UI reads notices from the registry records overlay. The workspace-host worker leaves both worker graphs (the desktop gateway entries and the workspace-server daemon's), and the `workspace-host-actions` mirror service is deleted. Its session-cleanup code, which registry deactivation uses, survives as imported library code under the registry runtime — an implementation detail with no wire surface. The aggregate `workspaceWireContract` loses the `workspaceHost` key. ## Consequences - One fewer required supervised child process per host; the automations worker gains an explicit dependency on the workspace-registry runtime in both graphs. - Workspace observations, lifecycle, and now usage measurement live on a single contract; there is no second "host" plane to keep consistent with the registry. - Re-adding any cut verb later is an additive minor bump under the v8 protocol. --- ### CONTRIBUTING # Contributing to Emdash Thanks for your interest in contributing. We favor small, focused PRs with clear intent. This guide covers the local development setup, the commands that matter, and the conventions contributors should follow before opening a PR. ## Quick Start ### Prerequisites - Git - Any reasonably recent `pnpm` (install via Homebrew, `npm install pnpm`, or `curl -fsSL https://get.pnpm.io/install.sh | sh -`) - Optional, but useful for integration work: - GitHub CLI (`gh`) - At least one supported coding agent CLI - Docker, when working on remote development infrastructure That is the whole toolchain requirement. `package.json` pins both the package manager (`packageManager: pnpm@10.28.2`) and the Node runtime (`devEngines.runtime` with `onFail: "download"`), so any pnpm on PATH swaps itself to the pinned version and provisions the pinned Node — checksummed in `pnpm-lock.yaml` — when it runs in this repo. You do not need nvm, corepack, or a preinstalled Node of the right version. If you use [mise](https://mise.jdx.dev) for toolchain auto-switching, the committed `mise.toml` pins node and pnpm for you; it is optional and required by nothing. `.nvmrc` remains as a compatibility hint for other version managers. ### Get The Source Fork the repository on GitHub, then clone your fork: ```bash git clone https://github.com//emdash.git cd emdash ``` ### Install From the repo root: ```bash pnpm install ``` This single command provisions the pinned pnpm and Node if needed, installs dependencies, and prepares the native modules — after it succeeds the machine is ready for every dev flow. This repository is a pnpm workspace. The Electron app is in `apps/emdash-desktop/`, and shared workspace packages live in `packages/`. ### Start Development For normal app development, run the full workspace dev command from the repo root: ```bash pnpm run dev ``` The root `dev` command now does two things: 1. Builds all packages under `packages/`. 2. Starts package watch builds and the Electron desktop app in parallel. Use this command when you are changing code in `packages/` or when you want the same startup path a fresh contributor will use. If you are only working inside `apps/emdash-desktop/`, you can run the Electron dev server directly: ```bash cd apps/emdash-desktop pnpm run dev ``` Important distinction: - `pnpm run dev` from the repo root starts the workspace package watchers and the app together. - `pnpm run dev` from `apps/emdash-desktop/` starts only `electron-vite dev` for the desktop app. - If app code imports changed package output, prefer the root command so package `dist/` files stay current. Renderer changes usually hot reload. Main-process changes under `apps/emdash-desktop/src/main/` may require restarting the Electron dev app. ## Repository Layout This is a pnpm workspace monorepo. - `apps/emdash-desktop/` - Electron desktop app package - `apps/emdash-desktop/src/main/` - Electron main process, RPC controllers, services, database, PTY, SSH, Git, GitHub, updates, and integrations - `apps/emdash-desktop/src/preload/` - typed Electron preload bridge - `apps/emdash-desktop/src/renderer/` - React composition shell and shared browser infrastructure - `apps/emdash-desktop/src/core/` - vertical slices with APIs, Node implementations, browser UI, contributions, and manifests - `apps/emdash-desktop/drizzle/` - generated Drizzle migrations and metadata - `apps/emdash-desktop/scripts/` - release, verification, and build scripts - `packages/core/` - transport-agnostic core runtime primitives - `packages/shared/` - shared workspace primitives - `packages/ui/` - shared UI components and theme system - `packages/plugins/` - plugin interfaces and helpers - `agents/` - architecture, workflow, convention, integration, and risk docs Root scripts are aggregate workspace scripts. Most app-specific commands live in `apps/emdash-desktop/package.json`. ## Common Commands Run these from the repo root unless noted. Root scripts run through Nx, which builds projects in dependency order and caches results locally. A second run of any cached target (`build`, `test`, `typecheck`, `lint`, `format:check`) replays instantly if inputs have not changed. ```bash pnpm run dev # build packages, watch packages, and start the Electron app pnpm run build # build every workspace package pnpm run format # format with oxfmt pnpm run format:check # check formatting without writing pnpm run lint # lint with oxlint pnpm run typecheck # run TypeScript checks pnpm run test # run workspace tests pnpm run affected # lint, typecheck, and test only projects changed vs. main pnpm run graph # open the Nx project graph in the browser ``` Individual project targets are addressable from the root without `cd`: ```bash nx package:mac @emdash/emdash-desktop nx db:reset @emdash/emdash-desktop nx build:theme @emdash/ui ``` See `agents/workflows/nx.md` for a full explanation of the Nx setup. Useful app-local commands from `apps/emdash-desktop/`: ```bash pnpm run dev # start electron-vite dev for the desktop app only pnpm run dev:debug # start with debug logging pnpm run dev:main # watch the Electron main process pnpm run dev:renderer # watch the renderer pnpm run build # build the Electron app pnpm run build:main # build main process only pnpm run build:renderer # build renderer only pnpm run package # build and package desktop artifacts pnpm run rebuild # rebuild native Electron dependencies pnpm run reset # clean app dependencies and reinstall ``` Useful package-local commands from a package under `packages/`: ```bash pnpm run dev # watch-build that package with tsdown pnpm run build # build that package with tsdown pnpm run test pnpm run typecheck ``` ## Local Validation Before opening or merging a PR, run the local merge gate: ```bash pnpm run format pnpm run lint pnpm run typecheck pnpm run test ``` There are no pre-commit hooks. CI enforces format:check, typecheck, lint, and test via `nx affected` — only projects touched by the PR are checked. The Playwright-backed `browser` Vitest projects are skipped in CI, so the full local suite is still expected before merging. ## Development Workflow 1. Create a feature branch: ```bash git checkout -b feat/ ``` 2. Keep PRs small and focused. Update docs when behavior changes. Include screenshots or short recordings for UI changes where they help reviewers understand the result. 3. Run validation locally. Use the full merge gate above for broad changes. For narrow work, it is fine to run focused tests while iterating, then run the full gate before opening or merging the PR. 4. Commit using Conventional Commits: ```text fix(opencode): change initialPromptFlag from -p to --prompt for TUI feat(docs): add changelog tab with GitHub releases integration ``` 5. Open a pull request. Describe the change, the reason for it, and the validation you ran. Link related issues when relevant. ## Code Style - Use TypeScript strict mode. - Use top-level `import` statements, not `require()`. - Do not introduce npm or yarn lockfiles. - Use `pnpm`. - Format with `oxfmt`. - Lint with `oxlint`. - Keep lines near the configured `printWidth` of 100 characters. - Use 2 spaces, semicolons, single quotes in TypeScript, double quotes in JSX, LF endings, and trailing commas where valid in ES5. - Avoid `any`. If a boundary requires it, keep the escape local and document why. - Do not re-export as a shortcut. Import from the original source. ## App Architecture Conventions The app follows this high-level flow: ```text Renderer -> typed RPC client -> preload bridge -> Electron main -> controllers -> services ``` Main process: - RPC handlers live in `src/main/core/*/controller.ts`. - Controllers should delegate to imported operation or service functions. - Expected failures should use the `Result` pattern from `src/main/lib/result.ts`. - Prefer `execFile` over `exec`. - Treat shell escaping, PTY spawning, SSH commands, and worktree paths as security-sensitive. - Preserve secret redaction in logging and telemetry code. Renderer: - Feature UI lives under `src/core/features//browser/`. - Shared renderer primitives, stores, hooks, commands, PTY, Monaco, modal infrastructure, and UI live under `src/renderer/lib/`. - Renderer RPC calls go through `rpc` from `src/renderer/lib/ipc.ts`. - New modals must be registered in `src/renderer/app/modal-registry.ts`. - New views must be registered in `src/renderer/app/view-registry.ts`. - New commands should use `src/renderer/lib/commands/registry.ts` and view-level `commandProvider` hooks when possible. - Components use `PascalCase`; hooks use `useX` camelCase or an existing local pattern. State and stores: - Access task managers through `getTaskManagerStore(projectId)`, not `project.taskManager`. - Access mounted projects through `asMounted(getProjectStore(id))`. - Never use `asProvisioned(...)!` or `asMounted(...)!`; use explicit null checks. - State guards should check `kind !== 'ready'` rather than enumerate non-ready states. - Task selectors live in `src/core/features/tasks/browser/stores/task-selectors.ts`. - Project selectors live in `src/core/features/projects/browser/stores/project-selectors.ts`. ## Database And Migrations Development database paths use Electron `app.getPath('userData')`. - macOS: `~/Library/Application Support/emdash-dev/emdash4.db` - Linux: `~/.config/emdash-dev/emdash4.db` - Windows: `%APPDATA%\emdash-dev\emdash4.db` Use an isolated scratch database when working on schema or migration changes. From the repo root: ```bash EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev ``` For app-only development, change into `apps/emdash-desktop/` first so this starts only `electron-vite dev`: ```bash cd apps/emdash-desktop EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run dev ``` Reset dev databases from `apps/emdash-desktop/`: ```bash pnpm run db:reset ``` Database rules: - Do not hand-edit numbered Drizzle migrations or `drizzle/meta/`. - Use `pnpm run db:generate` for new migrations. - Update fixtures and migration tests when schema behavior changes. - Run focused database validation from `apps/emdash-desktop/` when relevant: ```bash pnpm run db:fixtures pnpm run test:migrations ``` Read `agents/risky-areas/database.md` before changing database internals. ## Worktrees, PTY, SSH, And Providers Emdash orchestrates coding agents in Git worktrees and PTY sessions. These areas are high impact. - Do not delete worktree folders manually unless you know the matching Git state. Prefer in-app cleanup or `git worktree prune` from the main repository. - Do not weaken shell quoting, spawn behavior, environment allowlists, or secret redaction. - PTY environment passthrough must use the allowlist in `src/main/core/pty/pty-env.ts`. - Provider changes may need updates to shared provider metadata, dependency detection, PTY behavior, hooks/plugins, renderer assumptions, and tests. Read the relevant risk or integration doc before touching these areas: - `agents/risky-areas/pty.md` - `agents/risky-areas/ssh.md` - `agents/integrations/providers.md` - `agents/integrations/mcp.md` ## Testing Notes - Unit tests use Vitest. - Main database integration tests run in the `main-db` Vitest project. - Migration tests run in the `migrations` project. - Fixture generation runs in the `fixtures` project. - Renderer browser tests use Playwright-backed `@vitest/browser-playwright`. - Main-process tests are colocated under `src/main/core/**/*.test.ts`. - Renderer unit tests live under `src/renderer/tests/`. - Renderer browser tests live under `src/renderer/tests/browser/`. - Integration-style tests create temporary repos and worktrees in `os.tmpdir()`. From `apps/emdash-desktop/`, the app test command is: ```bash pnpm run test ``` It runs the app Vitest projects: ```text node, main-db, migrations, browser, scripts ``` ## Native Dependencies After native dependency changes, rebuild Electron native modules from `apps/emdash-desktop/`: ```bash pnpm run rebuild ``` This is especially relevant for `better-sqlite3` and `node-pty`. ## Remote Development Stack The workspace-server stack (`apps/workspace-server/docker-compose.yaml`) is the only Docker-backed remote-dev stack. When working on SSH/remote development infrastructure, start it from `apps/workspace-server/`: ```bash pnpm run run:docker-remote ``` Read `agents/workflows/remote-development.md` and `agents/risky-areas/ssh.md` before making SSH behavior changes. ## Issue Reports And Feature Requests Use GitHub Issues. Include: - Operating system - Emdash version or commit SHA - Node and pnpm versions, if development-related - Steps to reproduce - Expected behavior - Actual behavior - Relevant logs, terminal output, or screenshots Do not include secrets, tokens, private keys, local app databases, or private repository content in public issues. ## Release Process For Maintainers Do not dispatch release workflows, publish packages, or upload artifacts unless you are explicitly doing release work. The app version lives in `apps/emdash-desktop/package.json`. For release version bumps, run these from `apps/emdash-desktop/`: ```bash pnpm version patch pnpm version minor pnpm version major ``` This updates `package.json` and `pnpm-lock.yaml`, creates a version commit, and creates a tag. Production releases are dispatched through GitHub Actions: ```bash gh workflow run release-prod.yml --ref main -f arch=both ``` Canary releases are dispatched through: ```bash gh workflow run release-canary.yml --ref main -f arch=both ``` Production releases publish artifacts to GitHub Releases as the primary update feed and Cloudflare R2 as fallback. Canary releases currently publish to R2 only. ## Further Reading - `agents/README.md` - `agents/quickstart.md` - `agents/architecture/overview.md` - `agents/architecture/main-process.md` - `agents/architecture/renderer.md` - `agents/conventions/ipc.md` - `agents/conventions/main-patterns.md` - `agents/conventions/renderer-patterns.md` - `agents/conventions/typescript.md` - `agents/workflows/nx.md` - `agents/workflows/testing.md` - `agents/workflows/worktrees.md` --- ### README Emdash
[Download](https://emdash.sh/download) · [Docs](https://emdash.sh/docs) · [Releases](https://github.com/generalaction/emdash/releases/latest) · [Discord](https://discord.gg/f2fv7YxuR2) · [Contributing](CONTRIBUTING.md)
[](./LICENSE.md) [](https://github.com/generalaction/emdash/releases) [](https://github.com/generalaction/emdash) [](https://github.com/generalaction/emdash/commits/main) [](https://github.com/generalaction/emdash/graphs/commit-activity) [](https://discord.gg/f2fv7YxuR2) Y Combinator W26 [](https://twitter.com/intent/follow?screen_name=emdashsh)
Emdash is a desktop app for running AI coding agents in parallel. Each task runs in its own Git worktree, so you can explore multiple fixes or features at once, review the diffs, and merge what works. It works with local projects and remote machines over SSH. Bring the CLI agents you already use: Claude Code, Codex, OpenCode, Amp, and more. Emdash product screenshot ## What You Can Do - Run multiple coding agents at once without juggling terminals. - Keep every agent isolated in its own Git worktree and branch. - Send issues and tickets from Linear, GitHub, Jira, GitLab, Asana, Featurebase, Monday.com, Forgejo, or Plain into an agent. - Review diffs, create pull requests, inspect CI checks, and merge from one place. - Work locally or on your own remote machines over SSH/SFTP. ## Installation | Platform | Install | | --- | --- | | macOS | `brew install --cask emdash` · [Apple Silicon](https://github.com/generalaction/emdash/releases/latest/download/emdash-arm64.dmg) · [Intel](https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.dmg) | | Windows | [Installer](https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.msi) · [Portable](https://github.com/generalaction/emdash/releases/latest/download/emdash-x64.exe) | | Linux | [AppImage](https://github.com/generalaction/emdash/releases/latest/download/emdash-x86_64.AppImage) · [Debian package](https://github.com/generalaction/emdash/releases/latest/download/emdash-amd64.deb) | See the [latest release](https://github.com/generalaction/emdash/releases/latest) for all desktop builds. ## Agents Emdash detects installed provider CLIs automatically. It supports agents like Claude Code, Codex, Cursor, OpenCode, Amp, Devin, Qwen Code, Droid, and GitHub Copilot. For agents with lifecycle-hook support, Emdash installs marker-tagged entries in the agent's user-level config. These hooks let Emdash track status, notifications, and resumable sessions, and silently do nothing when the agent runs outside an Emdash session. See [Providers](https://emdash.sh/docs/providers) for the full list, setup commands, and provider-specific behavior. ## Remote Projects Connect to remote machines with SSH/SFTP and run the same parallel workflow on remote codebases. Emdash supports SSH agent, key, and password authentication, with credentials stored in your OS keychain. See [Remote Projects](https://emdash.sh/docs/remote-projects) for setup details. ## Privacy Emdash is local-first. App state is stored in a local SQLite database, and Emdash does not send your code or chats to Emdash servers. Agent CLIs may send code, prompts, and context to their own providers. Their data handling depends on the provider you choose. Telemetry is optional and can be disabled in Settings or by launching with: ```bash TELEMETRY_ENABLED=false ``` See [Telemetry](https://emdash.sh/docs/telemetry) for details. ## Contributing Contributions are welcome. Read the [Contributing Guide](CONTRIBUTING.md), open an issue, or join the [Discord](https://discord.gg/f2fv7YxuR2). ## License Licensed under the [Apache-2.0 license](LICENSE.md). ---