An autonomous engineering team for your Linear project. Symphony is a desktop app that watches your Linear board and dispatches coding agents — Codex, Claude Code, Cursor, or opencode — to work on issues, each in its own freshly cloned workspace. You triage and review; Symphony orchestrates. Based on the spec from OpenAI.
⬇ Download for macOS (Apple Silicon) ·
Dependency graph
How it works
- Poll — a local worker polls Linear for issues in the states you mark as active (e.g.
Todo,In Progress,Rework). - Prepare — for each issue, Symphony creates an isolated workspace and runs your
after_createhook (typicallygit clone+ dependency install). - Dispatch — it resolves the routed repository's workflow (or the saved default), renders it with the issue's identifier, title, state, and description, then drives a Codex, Claude Code, Cursor, or opencode agent session natively over their structured event streams.
- Track — agent events, token counts, retries, failures, and provider rate-limit signals are recorded in a local SQLite database and streamed live to the dashboard.
- Retry — failed runs are retried with exponential backoff, and the retry prompt includes the previous run's error context.
Everything runs on your machine. Network calls go to Linear's API, the tools your agents and hooks use, and — in packaged builds — GitHub Releases for a lightweight update check at launch and every 15 minutes. Choose Symphony → Check for Updates… to check immediately. Update bundles are downloaded only after you click Update.
Retros
The Retro view turns repeated run failures and workpad confusion into reviewable workflow improvements. Each generated retro covers terminal runs since the previous completed retro and groups its findings by repository.
New retros prepare an exact diff for every suggested prompt or skill change. Review each proposal with Accept or Reject; accepted changes remain local until the review is complete. Symphony then offers the applicable actions:
- Apply default workflow updates the validated workflow stored in Settings and hot-reconfigures the worker when possible.
- Create implementation PRs groups accepted repository-workflow and skill changes into one pull request per repository. Each PR uses a deterministic
symphony/retro-*branch and contains only the reviewed target files.
Proposals record the workflow hash or repository revision they were generated from. If the prompt, default branch, or target file changes before execution, Symphony marks the batch stale instead of applying an unseen merge. PR progress and successful links are retained per repository, so a failure in one repository does not hide successful PRs for another. Retros created before reviewable diffs were introduced remain available as read-only historical reports.
Requirements
- macOS (primary target; Tauri builds for other platforms are untested)
- A Linear workspace and a personal API key
- At least one agent CLI installed and authenticated:
codex— OpenAI Codex CLIclaude— Claude Code CLIagent— Cursor Agent CLI (cursor-agentalso works)opencode— opencode CLI
git, plus whatever your repository's install step needs
Getting started
Download Symphony.dmg — the latest signed and notarized build for macOS (Apple Silicon). Open it, drag Symphony to Applications, and launch.
Or build and run from source:
git clone https://github.com/anantjain-xyz/symphony-rust.git cd symphony-rust pnpm install pnpm tauri dev # or: pnpm tauri build
See Building for production bundles and signed releases.
On first launch the Overview shows a setup checklist:
- Connect Linear — paste your API key in Settings → Linear. It is stored in the macOS keychain, never on disk.
- Add your repositories — one or more Git URLs; each run clones the repo its issue routes to.
- Start the worker — the ▶ button in the top bar. Symphony begins polling and dispatching.
Optional Linear team and project filter lists narrow which issues Symphony picks up. Values within a list are ORed; when both lists are set, an issue must match at least one team and at least one project. Use Validate in Settings to check your configuration and confirm the agent CLIs are discoverable before starting.
Settings and workflows
Symphony's operational behavior is configured in Settings:
- Repositories — the Git repos runs clone, each with its own install command, plus where per-run workspaces are created (one folder per repo, then per issue). Every issue routes to exactly one repo: a
repo:<name>label or bare<name>label on the issue in Linear wins, then Symphony uses the repo marked default. The default is optional; without a matching label or default, the issue is skipped. An issue whoserepo:label matches no configured repo is skipped — an explicit label is never silently rerouted. Every run records the repo it was dispatched to; with several repos configured the dashboard tags runs with it and the Runs view can filter by repo. - Linear — API key (keychain), optional workspace plus comma-separated project/team filter lists, and the workflow states that drive dispatch. Each list matches any configured value; when both are non-empty, both dimensions must match. Issues in an active state (e.g.
Todo,In Progress,Rework,Merging) get an agent; issues in a terminal state (e.g.Done,Canceled) are left alone. - Agent — which CLI runs issues (
codex,claude,cursor, oropencode), an optional launch command (wrappers with arguments likemycode --agent claudeare fine; Symphony appends its own flags), the per-turn timeout, custom session environment variables (e.g.CURSOR_API_KEYfor Cursor), and the backend's options: permission mode, thread sandbox, and network access for Codex; permission mode and allowed/disallowed tool rules for Claude Code; mode, force/trust, sandbox, and optional model for Cursor; optional model and agent plus a skip-permissions toggle for opencode (on by default — opencode auto-rejects every tool call in non-interactive mode without it). - Worker — polling interval, max concurrent agents, retry backoff cap, and the lifecycle hooks (under Hooks (advanced)):
after_create,before_run, andafter_run. Hooks are shell scripts that run in the workspace with$REPO_URL,$REPO_NAME,$ISSUE_ID,$ISSUE_IDENTIFIER,$ISSUE_TITLE,$ISSUE_STATE,$ISSUE_BRANCH,$RUN_NUMBER,$SYMPHONY_INSTALL_CMD, and$SYMPHONY_HOOKin their environment; the repo variables reflect the repo the issue routed to. Hook process groups are cleaned after the hook returns, so a hook must not leave background services for a later lifecycle phase.
The default workflow at the bottom of Settings is the instruction document sent to the agent for each issue. A repository can override those instructions by checking a regular UTF-8 file named SYMPHONY-WORKFLOW.md or symphony-workflow.md into its root. The uppercase name wins if both exist. Empty files, unsupported placeholders, symlinks, and other invalid files fall back to the saved default workflow.
For every dispatch, Symphony fetches the repository's latest default branch without checking it out over the issue branch. A valid repository workflow from that branch is used; if the refresh fails, Symphony tries its cached default-branch copy before falling back to the saved default. Repository cards in Settings show the detected source. When the default is active, GitHub.com and supported GitHub Enterprise repositories can create a symphony/install-workflow PR that copies the saved default into canonical SYMPHONY-WORKFLOW.md.
Repository workflow files replace only the agent instruction document. Tracker settings, lifecycle hooks, agent backend and permissions, workspace location, polling, and concurrency remain controlled by Settings.
Placeholders in {{...}} form are rendered from the Linear issue when a run starts; the reference panel next to the default-workflow editor lists them and inserts one at the cursor on click:
| Placeholder | Renders as |
|---|---|
{{issue.id}} |
Internal Linear ID |
{{issue.identifier}} |
Issue key, e.g. SYM-42 |
{{issue.title}} |
Issue title |
{{issue.description}} |
Full issue body (empty if none) |
{{issue.state}} |
Current Linear state |
{{issue.branch}} |
Git branch from Linear (may be empty) |
{{issue.labels}} |
Labels, comma-separated |
{{issue.blockers}} |
Blocking issue identifiers, one - <id> bullet per line |
{{repo.name}} |
Name of the repo the issue routed to |
{{repo.url}} |
Git URL of the routed repo |
Retried runs automatically get a ## Retry context section appended with the prior run's error and recent events.
Data and security
- Your Linear API key lives in the OS keychain, not in a file.
- Custom session environment variables are saved in
settings.jsonand injected into agent sessions alongside Symphony's runtime variables like$LINEAR_API_KEY,$REPO_URL, and$REPO_NAME. - Runs, issues, and agent events are stored in a local SQLite database under the app data directory (
~/Library/Application Support/xyz.anantjain.symphonyon macOS), alongside daily-rotated logs and per-run workspaces. - Agents run with the sandbox/permission settings you give them under Settings → Agent. Codex defaults to Approve for me, which keeps workspace sandboxing and routes boundary crossings through Auto-review; network access remains on for unattended GitHub and Linear workflows. Claude defaults to
permission_mode: auto, while Cursor defaults toforce+trust. Review these settings before pointing Symphony at anything sensitive, and reserve Codex Full Access for externally sandboxed environments. - Packaged builds check the public GitHub Releases feed for signed updates. When one is available, a compact button beside the Symphony logo expands to Update on hover or keyboard focus. Installation is always user-initiated and warns before interrupting active work or discarding unsaved Settings changes.
Architecture
src-tauri/— Tauri desktop shell, commands, keychain-backed settings, event forwardingsrc/— React dashboard (Overview, Runs, Issues, Retro, Settings)crates/symphony-core— domain types, workflow config, prompt renderingcrates/symphony-storage— SQLite schema, repository, broadcast event buscrates/symphony-tracker— Linear GraphQL client and issue normalizationcrates/symphony-agents— native Codex, Claude, Cursor, and opencode process driverscrates/symphony-worker— recovery, polling loop, retries, hooks, workspace lifecycle
Building
Prerequisites: Rust (stable), Node.js ≥ 20 with pnpm, and on macOS the Xcode Command Line Tools (xcode-select --install).
pnpm install pnpm tauri dev # run the app with hot reload pnpm tauri build # production bundle: .app + .dmg pnpm typecheck && pnpm test && cargo test --workspace # the checks CI runs
pnpm tauri build writes artifacts to target/release/bundle/ (macos/Symphony.app, dmg/*.dmg); pass --debug for a faster unoptimized bundle. On macOS the pnpm tauri wrapper sets CI=true during builds so DMG creation uses Tauri's deterministic path instead of Finder AppleScript window decoration, which can time out in non-interactive shells (set TAURI_BUNDLER_DMG_IGNORE_CI=true to opt out).
Signed macOS release
This builds, signs, notarizes, and staples the distributable DMG, creates the signed Tauri updater archive, then verifies both outputs. Apple signing and notarization credentials live in ~/.symphony-release.env (override the location with SYMPHONY_RELEASE_ENV):
APPLE_SIGNING_IDENTITY=... # e.g. "Developer ID Application: Jane Doe (TEAMID1234)" APPLE_API_ISSUER=... # App Store Connect issuer ID (UUID) APPLE_API_KEY=... # API key ID APPLE_API_KEY_PATH=... # absolute path to the AuthKey_<id>.p8 file # Optional overrides; the updater key defaults to ~/.tauri/symphony.key TAURI_SIGNING_PRIVATE_KEY_PATH=... TAURI_SIGNING_PRIVATE_KEY_PASSWORD=...
The Developer ID Application certificate named by APPLE_SIGNING_IDENTITY must be installed in the login keychain; the script validates it before building.
Generate the updater key once with pnpm tauri signer generate -w ~/.tauri/symphony.key. The public key is embedded in the app; keep the private key outside the repository and back it up securely. Losing it prevents future in-app updates for versions that trust it.
The finished DMG lands in target/release/bundle/dmg/. The updater bundle and signature land beside the app in target/release/bundle/macos/.
Publishing a release
This runs the signed build above, then tags v<version> (read from src-tauri/tauri.conf.json) and creates a draft GitHub release. It uploads and verifies the versioned DMG, stable Symphony.dmg, signed Symphony.app.tar.gz, its signature, and latest.json before publishing the release. The stable DMG and updater feed therefore always resolve to one complete release. Bump the version in src-tauri/tauri.conf.json (and keep the other version manifests in sync) before publishing.
The first release containing the updater must still be installed manually by users on an older build. Every later stable release can be discovered and installed from inside Symphony.
The script refuses to run unless you're on a clean main checkout matching origin/main, and it needs an authenticated GitHub CLI (gh) with push access.
See the documentation index for the repository map and development guide, and CONTRIBUTING.md for the pull-request and packaging guide.
