A cross-platform Electron desktop app for driving many coding-agent terminal
sessions across your projects — built around a git-worktree-per-task model and
the Biznex wf multi-session workflow (architect → implementer → reviewer,
coordinated through one checkpoint file).
Project ─► Session (= a git worktree on its own branch) ─► Role tabs + shells
(Architect · Implementer · Reviewer · Log · shells)
Each session gets an isolated worktree so several tasks in one repo never collide. Agents (Claude, Codex, Kimi, Gemini, Copilot, OpenCode) run as real terminal processes; the app just orchestrates them, watches for the workflow checkpoint, and gives you files/diff/compose tooling around each one.
pnpm install # installs deps; postinstall rebuilds native modules for Electron
pnpm dev # rebuilds native modules + starts electron-vite with hot reloadpnpm dev opens the app. Add a project (a git repo), expand it, and create a
session — see How it works.
Requirements: Node 20+ and pnpm. Agents you want to launch (claude,
codex, kimi, …) must be installed and on your PATH.
Every agent is a separate CLI you install and authenticate yourself — the app
only launches it, wires the per-stage flags, and (where supported) resumes its
session. None is privileged: choose a different agent per role (Architect /
Implementer / Reviewer) in a project's stage config. New stages default to
claude / opus.
| Agent | Executable | Default model | Effort | "Dangerous" maps to | Session resume |
|---|---|---|---|---|---|
| Claude | claude |
opus |
low…max → --effort |
--dangerously-skip-permissions |
✅ app-minted id (--session-id / --resume) |
| Codex | codex |
gpt-5.5 |
none…xhigh → -c model_reasoning_effort |
--ask-for-approval never --sandbox danger-full-access |
fresh (warns) |
| Kimi | kimi |
kimi-code/kimi-for-coding |
low…max → KIMI_MODEL_THINKING_EFFORT (env) |
--yolo |
✅ CLI-minted id, captured → --session |
| Gemini | gemini |
gemini-2.5-pro |
— | --yolo |
fresh (warns) |
| Copilot | copilot |
— (no model flag) | — | --allow-all |
fresh (warns) |
| OpenCode | opencode |
anthropic/claude-opus-4-8 |
— | — | fresh (warns) |
Model fields are editable — most CLIs accept custom aliases, and clearing the
field lets the CLI fall back to its own default. Effort is only sent to agents
that document the flag and is ignored (with a warning) for the rest; it is always
applied per launch — Kimi through a scoped KIMI_MODEL_THINKING_EFFORT, so your
global config is untouched. An agy (Antigravity) runtime is also wired but
unverified — adjust its launcher once checked against the real binary.
Session resume. Only Claude and Kimi reopen the exact prior conversation.
Claude accepts an app-minted id; Kimi mints its own session_<uuid> on a fresh
launch, which the app captures and replays with kimi --session <id> (never
--continue — all roles share one worktree, so "most recent for this directory"
could resume the wrong agent). Every other agent relaunches fresh and warns.
Install note (Kimi): its TypeScript
MoonshotAI/kimi-code CLI (executable
kimi) is not the legacy Python kimi-cli package — install kimi-code
(brew install kimi-code, npm i -g @moonshot-ai/kimi-code, or the official
installer) and run kimi once to /login.
Same app, same UI, two topologies. Pick by where you want the repos and the agents to live.
| Local | Remote | |
|---|---|---|
| Runs | the Electron app on your machine | a headless Node runner on one machine + a client anywhere |
| Repos, worktrees, agent logins | on your machine | on the runner |
| Client | the app window | any browser, or the desktop app pointed at the runner |
| Survives closing the client | n/a | yes — terminals, agents and Auto Pilot keep running |
| Use it for | ordinary solo work | a big always-on box, or driving sessions from a laptop/iPad |
pnpm dev # development, hot reload
pnpm package:mac # or :win / :linux for an installable buildEverything stays on your machine. This is the default and needs no extra setup.
The runner owns the repos, worktrees, PTYs and agent OAuth; the client only draws the UI and sends your keystrokes. Reloading, closing a tab, or switching devices never restarts setup, agents, tabs or Auto Pilot.
On the runner machine:
ELECTRON_SKIP_BINARY_DOWNLOAD=1 pnpm install --ignore-scripts
pnpm remote:build # builds native modules for Node, not Electron
set -a; source ~/.config/agent-coordinator/runner.env; set +a
pnpm remote:runner # listens on 127.0.0.1:4765It binds loopback only — no router port is opened. To reach it from another device, put one of these in front:
- Tailscale Serve — a private HTTPS URL on your tailnet. Nothing is exposed to the Internet. Simplest, and the right default.
- Cloudflare Tunnel — serves it on your own domain
(
https://coordinator.yourdomain.com). This publishes to the public Internet, so put Cloudflare Access in front of it or treat the Coordinator token as the only thing guarding shells on that machine.
Then open the URL and enter the token, or point the desktop app at it:
AGENT_COORDINATOR_REMOTE_URL="wss://coordinator.yourdomain.com/rpc" \
AGENT_COORDINATOR_REMOTE_TOKEN="your-token" \
"/Applications/Agent Coordinator.app/Contents/MacOS/Agent Coordinator"Browser and desktop can be connected to the same runner at once.
deploy/README.md has the full walkthrough: generating the
secrets, both proxies step by step, and keeping the runner alive across reboots
with launchd (macOS) or systemd (Linux).
electron-builder produces installers per OS. Because the app links native
modules (node-pty, better-sqlite3), you must build on each target OS
(or in CI) — you can't cross-compile them locally.
pnpm build # type-safe production bundle (main + preload + renderer)
pnpm package:mac # → dist-packages/ (unpackaged .app; change the mac target to dmg/zip if you want an installer)
pnpm package:win # NSIS installer
pnpm package:linux # AppImage
pnpm package # package for the host OSThe app icon is build/icon.png (macOS-squircle template; build/icon-source.png
is the original square art). electron-builder generates the platform icons from it.
Nothing you edit under src/ is live until out/ is rebuilt — the desktop app
and the runner both execute the compiled bundle, never the sources.
Remote runner (the systemd/launchd service):
cd /path/to/agent-coordinator
git pull # only if the change came from elsewhere
pnpm install --ignore-scripts # only if dependencies changed
pnpm remote:build # native addons for Node + bundle to out/
# Linux (systemd user service)
systemctl --user restart agent-coordinator-runner
# macOS (launchd)
launchctl kickstart -k "gui/$(id -u)/com.agent-coordinator.runner"Confirm it came back before trusting it:
systemctl --user status agent-coordinator-runner --no-pager # Linux
journalctl --user -u agent-coordinator-runner -n 40 --no-pagerUse pnpm remote:build, never pnpm build, for the runner: pnpm build
compiles the native addons for Electron, and the runner is plain Node — it
will not load them. pnpm test and pnpm install rebuild them for your
default Node, which is wrong for both; run pnpm remote:build again
afterwards. See Native-module gotcha.
Restarting the runner kills every PTY. Terminals, agents and Auto Pilot come
back from persisted intent (deploy/README.md → Actualizaciones), but
whatever a command was half-way through does not — so restart when no build,
migration or agent turn is in flight.
Local desktop app:
pnpm build # or pnpm dev for hot reloadThen relaunch the app window; the running one keeps the old bundle.
node-pty and better-sqlite3 are compiled against a specific ABI:
pnpm dev/pnpm buildrunelectron-rebuildfirst → built for Electron. ✅pnpm testrunspretest: pnpm rebuild better-sqlite3→ built for Node (so Vitest can load it). ❌ for Electron.
So after running pnpm test, rebuild before launching the app:
npx electron-rebuild -f -w node-pty,better-sqlite3
# or just run `pnpm dev` / `pnpm build`, which do it for youThe runner (pnpm remote:build) is pinned to the Node version in .nvmrc, and
scripts/build-remote-native.mjs re-executes itself under that Node no matter
which one you invoke it with.
The pin exists because node::ObjectWrap is a header-only class baked into each
addon. Node 24.19.0 (nodejs/node#63642)
made its destructor call RemoveEnvironmentCleanupHook(). better-sqlite3's
Statement derives from it, so when V8 collects a dead statement during a GC
driven by a platform task — no entered v8::Context — Node aborts the whole
process with Assertion failed: (env) != nullptr (src/api/hooks.cc:142).
So the service starts via scripts/run-remote-runner.sh, which resolves the
pinned Node itself and refuses to boot on a mismatch — the systemd/launchd unit
never names a version and cannot drift from the one that did the build.
One thing to watch: pnpm test and pnpm install rebuild these addons with
your default Node. Run pnpm remote:build again afterwards before restarting
the runner; the build fails loudly if it ever compiles against affected headers.
| Script | What it does |
|---|---|
pnpm dev |
Rebuild native (Electron) + electron-vite dev with hot reload |
pnpm build |
Rebuild native (Electron) + production bundle to out/ |
pnpm remote:build |
Rebuild native (Node, pinned by .nvmrc) + production bundle to out/ — use this before restarting the runner |
pnpm remote:runner |
Run the headless runner in the foreground (the service uses scripts/run-remote-runner.sh) |
pnpm typecheck |
tsc --noEmit (strict) |
pnpm test |
Vitest (rebuilds better-sqlite3 for Node first) |
pnpm package[:mac|:win|:linux] |
Build + electron-builder |
The left rail lists your projects (git repos). Expand one to see:
- ⌂ Repo root (gold) — a workspace rooted at the repo itself: shells + files + diff, no agent/checkpoint tabs. Always available, even with no session.
- Its work sessions, each with its own worktree.
Creating a session mints a git worktree at <repo>/.worktrees/<slug> on a new
branch (feature/… or fix/…), optionally copying .env* files into it
(recursively, for monorepos). A session can also reuse ignored dist/generated
output from the repo root and skip the configured setup command. That fast path
only runs when the root is clean and both checkouts are at the same commit; it
uses filesystem copy-on-write clones when supported. The user should select it
only when the root output is current. Removing a session removes the worktree
(after confirmation). Everything the session's agents do happens inside that
worktree.
A feature/fix session can start somewhere other than a fresh branch — pick Start from in the new-session dialog:
- Continue — check out an existing branch writable, to pick up a session a teammate started (or your own from another machine). The branch is fetched and fast-forwarded first; if it has diverged from its remote, or another session already holds it, creation stops and says so rather than starting on the wrong code.
- Fork — cut a new
feature/<slug>branch from a base an architect published the specs and plan on (typicallydevelop), starting fromorigin/<base>rather than from local commits.
Either way you can adopt a checkpoint committed on that branch: the picker reads it straight out
of the ref without checking it out, and the session is born past the gate — Implementer and Reviewer
unlocked, wf implement <checkpoint> pre-typed. The agent conversation is not resumed (those ids
live on the machine that created them); the checkpoint is the handoff.
A session also records the branch it started from, so Diff compares against that base instead of
always against main.
If the configured setup command fails, its terminal stays open as a repair shell while agent tabs remain gated. After fixing the worktree there, use Continue with agents to persist the setup as ready and resume the session's normal agent/kickoff flow.
The app is built for the Biznex multi-session workflow. A session's tabs are the three roles plus a Log:
- Architect (labelled Diagnose for a fix) — you brainstorm/plan (feature)
or
wf fix <bug>(fix). The architect writes the spec + plan + checkpoint and hands off — it does not implement. - Implementer / Reviewer — disabled until a checkpoint exists. The app
watches the worktree for a checkpoint matching the project's glob (default
docs/workflow/checkpoints/*-checkpoint.md); the moment one appears, these tabs light up. - Log — renders the checkpoint's
▶ NEXTblock (with a copy button), the latest Plan de corrección as a dedicated card, and reconciled open/closed finding counts across review and fix rounds.
Opening a role tab spawns the agent (claude --resume …, kimi --session …,
codex …, etc.) and
pre-types the matching wf command (e.g. wf implement <checkpoint>) without
submitting — you press Enter. Each tab shows a green "how to start" hint for its
state. When an agent quits, the tab drops to a usable shell instead of dying.
The New session dialog offers four, and the kind decides which role tabs exist and where the session starts:
| Kind | Tabs | Starts in | Worktree |
|---|---|---|---|
| New feature / Bug fix | Architect · Implementer · Reviewer | Architect (or Implementer if it adopts a checkpoint) | new branch, or an existing one — see Starting from work that already exists |
| PR review | Reviewer | Reviewer, auto-submitted | detached at the branch under review — read-only by construction |
| PR fix | Implementer · Reviewer (+ Architect when Diagnose first) | Implementer, or Architect when diagnosing first | writable checkout of the PR's source branch |
PR sessions can be created from a branch pair, or from a PR link once the
project has a VCS host and API token (Bitbucket and GitHub). From a link
the app resolves source/target, pulls the whole PR conversation into a gitignored
.agent-pr-context.md, and refuses to create a stale worktree if the checkout
does not land on the PR's head commit. A PR fix commits but never pushes — push
is a separate gated button.
Per session, a conductor can drive the workflow without you pressing Enter: it
watches the checkpoint, waits for it to settle (default 4s), and launches the
next role's wf command itself. It stops on its own at a configurable re-loop
limit (default 3 reviewer→implementer rounds) so a disagreeing pair cannot spin
forever, and pauses with a reason instead of guessing whenever the checkpoint
does not say clearly what comes next. Both values are per-project settings.
+ opens a plain shell in the worktree; the ⌂ button opens one in the repo
root (gold ROOT badge). Shells persist a bounded scrollback across restarts,
have clickable file paths, and accept drag-and-drop — drop a file/image onto a
terminal to insert its absolute path (so the agent can read it).
- Files (right sidebar, toggle in the topbar): browse/open/edit any file. Markdown opens as a rendered preview + editor; a Worktree ⇄ Repo root toggle switches roots. A Refresh button re-scans for new files.
- Diff: the session's changes vs. the branch point (tracked + untracked), per-file, with 8 lines of context.
- Composer (per file / per diff): select lines → Add stages a GitHub-style
path#Lx-Lyreference (not the code — the agent reads the file). Accumulate references + notes, then Send (paste into a chosen agent tab) or Send & Run (paste + Enter) — pick the target tab per send.
Per-user state lives in Electron's userData: the project registry
(better-sqlite3), sessions.json, workspace-layout.json (which tabs were open),
per-shell scrollback, and per-agent session ids (for exact resume flags). Reopening the app
restores your open sessions and tabs.
electron-vite with the standard three-process split:
src/
├── main/ # Electron main: PTYs (node-pty), git worktrees, project registry
│ │ # (better-sqlite3), checkpoint watchers (chokidar), IPC handlers
│ ├── ipc/ # registerIpcHandlers / registerTerminalIpcHandlers
│ ├── projects/ # project registry, worktrees, start points, diff, checkpoint watch, layout
│ ├── terminals/ # pty manager, scrollback, agent session/uuid stores
│ ├── vcs/ # Bitbucket + GitHub providers, encrypted token store
│ ├── remote/ # headless runner: WebSocket server + CLI entry point
│ ├── runtime/ # the process-agnostic coordinator wiring both entry points share
│ └── platform/ # Electron-specific system integration behind an interface
├── preload/ # contextBridge: exposes the typed AgentCoordinatorApi to the renderer
├── renderer/ # React 18 UI (SessionView, SessionTerminal, FileTree, GitDiffView, Composer, …)
└── shared/ # IPC contract + workflow types shared by main/preload/renderer
The desktop app and the headless runner are the same coordinator with
different hosts: runtime/coordinator-runtime.ts owns sessions, terminals and
watchers, while Electron and the WebSocket server only supply transport and
system integration. That is why a browser client can attach to a session the
desktop app started, and why reloading a client restarts nothing.
- Terminals:
node-ptyprocesses rendered with@xterm/xterm. The renderer writes to a PTY via IPC; a bracketed-paste helper inserts multi-line text without submitting. - Contract:
src/shared/ipc/contract.tsis the single source of truth — channel names, theAgentCoordinatorApiinterface, and payload types. - TypeScript strict throughout; Vitest covers the main-process stores, registries, and workflow helpers.
Reference material from ADE may live under reference-not-commit/ (git-ignored).