An autonomous engineering team for your Linear project. Symphony is a desktop app that watches your Linear board and dispatches coding agents β Codex or Claude Code β 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) Β·
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 yourafter_create
hook (typicallygit clone
- dependency install).Dispatchβ it renders your prompt template with the issue's identifier, title, state, and description, then drives a Codex or Claude Code 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. The only network calls are to Linear's API and whatever your agents and hooks do.
macOS(primary target; Tauri builds for other platforms are untested)- A
Linearworkspace and apersonal API key - At least one agent CLI installed and authenticated:
codex
β OpenAI Codex CLIclaude
β Claude Code CLI
git
, plus whatever your repository's install step needs
** 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 inSettings β 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 filters (workspace slug, project ID, identifier prefix like ENG
) narrow which issues Symphony picks up. Use Validate in Settings to check your configuration and confirm the agent CLIs are discoverable before starting.
Symphony's behavior is configured entirely in Settings β no config file to edit:
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). With several repos configured, every issue routes to exactly one, first match wins: arepo:<name>
label on the issue in Linear, then the repo claiming the issue's Linear project, then the repo claiming its team key (e.g.ENG
), then the repo markeddefault. 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/project/team filters, and the workflow states that drive dispatch: issues in anactive state(e.g.Todo
,In Progress
,Rework
,Merging
) get an agent; issues in aterminal state(e.g.Done
,Canceled
) are left alone.Agentβ which CLI runs issues (codex
orclaude
), an optional launch command (wrappers with arguments likemycode --agent claude
are fine; Symphony appends its own flags), the per-turn timeout, and the backend's sandbox/permission options: approval policy, thread sandbox, and network access for Codex; permission mode and allowed/disallowed tool rules for Claude Code.Workerβ polling interval, max concurrent agents, retry backoff cap, and the lifecycle hooks (underHooks (advanced)):after_create
,before_run
,after_run
,before_remove
. 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_HOOK
in their environment; the repo variables reflect the repo the issue routed to.
The prompt template at the bottom of Settings is the instruction document sent to the agent for each issue. Placeholders in {{...}}
form are rendered from the Linear issue when a run starts; the reference panel next to the 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.
- Your Linear API key lives in the
OS keychain, not in a file. - Runs, issues, and agent events are stored in a local
SQLite database under the app data directory (
~/Library/Application Support/xyz.anantjain.symphony
on macOS), alongside daily-rotated logs and per-run workspaces. - Agents run with the sandbox/permission settings you give them under
Settings β Agent. The defaults (approval_policy: never
,permission_mode: auto
, network access on for Codex) are tuned for unattended runs in disposable workspaces β review them before pointing Symphony at anything sensitive.
src-tauri/
β Tauri desktop shell, commands, keychain-backed settings, event forwardingsrc/
β React dashboard (Overview, Runs, Issues, 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 and Claude process driverscrates/symphony-worker
β recovery, polling loop, retries, hooks, workspace lifecycle
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).
pnpm release:mac
This builds, signs, notarizes, and staples the distributable DMG, then verifies the result with spctl
and stapler validate
. 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
The Developer ID Application certificate named by APPLE_SIGNING_IDENTITY
must be installed in the login keychain; the script validates it before building.
The finished DMG lands in target/release/bundle/dmg/
.
pnpm release:publish
This runs the signed build above, then tags v<version>
(read from src-tauri/tauri.conf.json
) and creates a GitHub release with the DMG attached under both its versioned name and the stable name Symphony.dmg
β the file behind the download link at the top of this README, which always serves the newest release. Bump the version in src-tauri/tauri.conf.json
(and keep package.json
in sync) before publishing.
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 CONTRIBUTING.md for the full contributor guide, including TypeScript bindings regeneration.