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-boxednon-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 aggregateworkspaceWireContract 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: [email protected]) 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 inpnpm-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 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:
git clone https://github.com/<you>/emdash.git
cd emdashInstall
From the repo root:
pnpm installThis 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 inapps/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:
pnpm run devThe 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:
cd apps/emdash-desktop
pnpm run devImportant 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 underapps/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 inapps/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.
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 browserIndividual project targets are addressable from the root without cd:
nx package:mac @emdash/emdash-desktop
nx db:reset @emdash/emdash-desktop
nx build:theme @emdash/uiSee agents/workflows/nx.md for a full explanation of the Nx setup.
Useful app-local commands from apps/emdash-desktop/:
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 reinstallUseful package-local commands from a package under packages/:
pnpm run dev # watch-build that package with tsdown
pnpm run build # build that package with tsdown
pnpm run test
pnpm run typecheckLocal Validation
Before opening or merging a PR, run the local merge gate:
pnpm run format
pnpm run lint
pnpm run typecheck
pnpm run testThere 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:
git checkout -b feat/<short-slug>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:
fix(opencode): change initialPromptFlag from -p to --prompt for TUI
feat(docs): add changelog tab with GitHub releases integration5. 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:
Renderer -> typed RPC client -> preload bridge -> Electron main -> controllers -> servicesMain 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<T, E> 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/<feature>/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:
EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run devFor app-only development, change into apps/emdash-desktop/ first so this starts
only electron-vite dev:
cd apps/emdash-desktop
EMDASH_DB_FILE=/tmp/emdash-scratch.db pnpm run devReset dev databases from apps/emdash-desktop/:
pnpm run db:resetDatabase 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:
pnpm run db:fixtures
pnpm run test:migrationsRead 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:
pnpm run testIt runs the app Vitest projects:
node, main-db, migrations, browser, scriptsNative Dependencies
After native dependency changes, rebuild Electron native modules fromapps/emdash-desktop/:
pnpm run rebuildThis 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/:
pnpm run run:docker-remoteRead 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/:
pnpm version patch
pnpm version minor
pnpm version majorThis updates package.json and pnpm-lock.yaml, creates a version commit, and
creates a tag.
Production releases are dispatched through GitHub Actions:
gh workflow run release-prod.yml --ref main -f arch=bothCanary releases are dispatched through:
gh workflow run release-canary.yml --ref main -f arch=bothProduction 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
<img alt="Emdash" src="https://github.com/user-attachments/assets/a2ecaf3c-9d84-40ca-9a8e-d4f612cc1c6f" />
<div align="center">
Download ยท Docs ยท Releases ยท Discord ยท Contributing
<br />
[](./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)
<a href="https://www.ycombinator.com"><img src="https://img.shields.io/badge/Y%20Combinator-W26-orange" alt="Y Combinator W26"></a>
[](https://twitter.com/intent/follow?screen_name=emdashsh)
</div>
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.
<img alt="Emdash product screenshot" src="https://emdash.sh/media/blog/public-v1-beta/v1beta.jpg" />
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 ยท Intel |
| Windows | Installer ยท Portable |
| Linux | AppImage ยท Debian package |
See the latest release 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 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 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:
TELEMETRY_ENABLED=falseSee Telemetry for details.
Contributing
Contributions are welcome. Read the Contributing Guide, open an
issue, or join the Discord.
License
Licensed under the Apache-2.0 license.
---