An open API service indexing awesome lists of open source software.

https://github.com/metyatech/symphony-metyatech

TypeScript implementation of the OpenAI Symphony service specification: a long-lived CLI service that polls Linear, creates one workspace per issue, and launches a Codex app-server worker.
https://github.com/metyatech/symphony-metyatech

agents cli codex linear nodejs symphony typescript

Last synced: 17 days ago
JSON representation

TypeScript implementation of the OpenAI Symphony service specification: a long-lived CLI service that polls Linear, creates one workspace per issue, and launches a Codex app-server worker.

Awesome Lists containing this project

README

          

# Symphony

Symphony is a TypeScript implementation of the draft OpenAI Symphony service specification. It runs as a long-lived CLI service that polls Linear, creates one workspace per issue, and launches a Codex app-server worker in that issue workspace.

## Supported Environment

- Node.js 22 or newer
- npm 11 or newer
- A Linear API key
- A `codex app-server` executable compatible with the targeted Codex app-server protocol

## Install and Build

```sh
npm install
npm run build
```

## Usage

Create a repository-owned `WORKFLOW.md`:

```md
---
tracker:
kind: linear
api_key: $LINEAR_API_KEY
project_slug: DEMO
trigger_label: symphony-ready
workspace:
root: ./.symphony-workspaces
codex:
command: codex app-server
---

Work on {{ issue.identifier }}: {{ issue.title }}.
```

Validate configuration without starting the daemon:

```sh
LINEAR_API_KEY=lin_api_xxx symphony --workflow ./WORKFLOW.md --check
```

Start the service:

```sh
LINEAR_API_KEY=lin_api_xxx symphony --workflow ./WORKFLOW.md
```

By default, every structured JSON log line emitted to stderr is also saved to a
**bounded rotating log file** at `/.symphony/logs/symphony.log`.
Use `--quiet` to suppress informational log lines on stderr and in the log file;
warnings and errors are still emitted and persisted.

Machine-readable validation output:

```sh
LINEAR_API_KEY=lin_api_xxx symphony --workflow ./WORKFLOW.md --check --json
```

## Configuration

`WORKFLOW.md` supports optional YAML front matter and a strict Liquid-compatible prompt body. Unknown template variables and filters fail the affected run attempt. Relative `workspace.root` values resolve relative to the workflow file directory, and `$VAR_NAME` indirection is resolved only where the spec allows it. If `workspace.root` is absent, top-level `workspaces_root` is accepted as a workflow-directory-relative compatibility alias; `workspace.root` always takes precedence when both are present.

Required dispatch fields are `tracker.kind`, `tracker.api_key`, `tracker.team`, and `codex.command`. `tracker.project_slug` and `tracker.trigger_label` are optional scoping fields. Defaults follow the upstream Symphony specification for polling interval, active and terminal states, hook timeout, concurrency, retry backoff, and Codex timeouts.

### File logging

File logging is **enabled by default** for long-running service starts. Configure
it under `logging.file`:

```yaml
logging:
file:
enabled: true
path: .symphony/logs/symphony.log
max_bytes: 10485760
max_files: 5
```

- `enabled`: set to `false` to opt out and keep stderr-only logging.
- `path`: relative values resolve **under `workspace.root`**. The default is
`.symphony/logs/symphony.log` inside that root.
- `max_bytes`: active file limit. Symphony rotates before appending a line that
would exceed this value.
- `max_files`: total retained files, including the active file. For example,
`max_files: 5` keeps `symphony.log` plus `.1` through `.4`.

`symphony --check` and `symphony --check --json` validate logging configuration
without creating log directories or files. **Restart Symphony after changing
`logging.file` settings**; running services do not reload them dynamically.

### Dashboard API

Symphony starts the dashboard API by default on an OS-assigned dynamic port. On
successful startup, the structured log event `dashboard_api_started` includes
both the actual port and a ready-to-open URL:

```json
{ "event": "dashboard_api_started", "port": 51234, "url": "http://localhost:51234" }
```

Use `server.port: auto` to make the dynamic behavior explicit, or set a fixed
port from `1` through `65535` when your environment requires a stable endpoint:

```yaml
server:
port: auto # default; binds with port 0 and reports the actual port
```

```yaml
server:
port: 4242 # fixed override
```

For backward compatibility, `server.port: 0` disables the dashboard API. A YAML
`null` value also disables it; other strings, negative numbers, non-integers,
and values above `65535` are rejected during configuration loading.

After the API binds, Symphony writes machine-readable discovery metadata to
`/.symphony/dashboard-api.json`:

```json
{
"pid": 12345,
"port": 51234,
"url": "http://localhost:51234",
"started_at": "2026-01-02T03:04:05.000Z"
}
```

The discovery file is removed when the dashboard API is disabled and during
normal Symphony shutdown. `--quiet` suppresses informational log output,
including `dashboard_api_started`, but the discovery file is still written when
the API starts.

Linear issue scope is controlled by `tracker.team`, `tracker.active_states`, optional `tracker.project_slug`, and optional `tracker.trigger_label`. When `project_slug` or `trigger_label` is configured, Symphony applies both Linear query filters and local eligibility checks so issues outside the configured project or missing the trigger label are not dispatched or continued. Slugs and labels are normalized case-insensitively.

## Multi-Repository Workspaces

A single workflow can drive work that spans many repositories. Configure a
`repositories:` block in `WORKFLOW.md` and Symphony clones the right repos
into each issue workspace before the agent starts.

```yaml
repositories:
owner: metyatech # default GitHub owner for repo: labels
base_url: https://github.com # default
protocol: https # https or ssh
label_prefix: "repo:" # how repos are picked from issue labels
default: [] # repos to always clone, e.g. ["shared-config"]
required: false # set true to fail when no repos are selected
local:
prefer_existing: false # set true to use matching local checkouts first
roots: [] # directories whose immediate children are repo checkouts
isolation: none # none or mwt; default preserves direct local checkout behavior
init_if_missing: false # mwt mode only: initialize .mwt/config.toml when absent
init_no_verify: false # mwt mode only: pass noVerify only when no verify script exists
branch_template: "symphony/{{ issue.identifier }}"
path_template: "{{ workspace }}/{{ repo }}"
overrides: {} # e.g. XroidVerse.default_branch or Verseday/XroidVerse.default_branch
```

Repositories are selected from issue labels matching `label_prefix` and from
`repositories.default`. A label of `repo:frontend` resolves to
`metyatech/frontend`; a label of `repo:other-org/lib` overrides the owner.
Selected repos are cloned into `//` on first run and reused
unchanged on subsequent runs so the agent's branches and uncommitted edits
survive across continuation turns.

For a GHWS-style workspace where repositories already exist side by side, set
`repositories.local.prefer_existing: true` and list the workspace roots to
search. Symphony then checks each configured root for an immediate child whose
directory name matches the selected repository name. If that child is a real Git
checkout root, Symphony passes that existing checkout to Codex instead of
cloning another copy into the issue workspace. When no matching local checkout
exists, clone/reuse under the issue workspace still applies.

```yaml
repositories:
owner: metyatech
required: true
local:
prefer_existing: true
roots:
- . # relative to WORKFLOW.md
```

Local checkouts are not copied, linked, reset, or deleted by Symphony. Hook
metadata reports them with `SYMPHONY_REPO__CREATED_NOW=false`, and
workspace cleanup removes only the issue workspace directory.

For mwt-managed local isolation, set `repositories.local.isolation: mwt`.
Symphony still selects a local seed checkout from `repositories.local.roots`,
but Codex receives an issue-scoped managed worktree path instead of the seed
checkout path. Symphony initializes missing `.mwt/config.toml` only when
`init_if_missing` is `true`. Auto-initialization and worktree creation opt into
mwt's dirty-seed bypasses so existing tracked edits in the seed checkout do not
block dispatch; Symphony does not delete, reset, stash, or otherwise manage
those edits. If `init_no_verify` is also `true`, Symphony passes
`noVerify: true` only for seeds where mwt would not discover `scripts.verify` or
a supported `scripts/verify.*` wrapper, so repositories with a real verify
command keep mwt's verification path.

```yaml
repositories:
owner: Verseday
required: true
local:
prefer_existing: true
roots:
- .. # GHWS root relative to WORKFLOW.md
isolation: mwt
init_if_missing: true
init_no_verify: true
branch_template: "symphony/{{ issue.identifier }}"
path_template: "{{ workspace }}/{{ repo }}"
overrides:
XroidVerse:
default_branch: develop
Verseday/XroidVerse:
default_branch: develop
```

A `repo:Verseday/XroidVerse` issue label selects the local `XroidVerse` seed
checkout from the configured roots, creates or reuses branch
`symphony/`, and places the managed worktree at
`/XroidVerse`. Existing managed worktrees are reused only when
the branch and rendered path both match; a same-branch worktree at another path
is treated as a conflict rather than being reset or recreated. Terminal cleanup
does not delete mwt worktrees. Symphony logs warnings with the retained worktree
paths and leaves the issue workspace in place for explicit operator cleanup.

When `repositories.default` selects exactly one checkout, or labels select
exactly one checkout, Codex starts with that checkout as its current working
directory. When multiple repositories are selected, Codex keeps the issue
workspace as its current working directory so every checkout remains visible as
a sibling directory. If `repositories.required` is `true` and no labels or
defaults select a repository, the run fails during workspace preparation before
the Codex runner starts.

The Liquid prompt template receives a `repos` array alongside `issue` and
`attempt`, so a single template can address every selected repo:

```md
You are working on {{ issue.identifier }}.

Available repositories:
{% for repo in repos %}- {{ repo.name }} at {{ repo.path }}
{% endfor %}
```

Hooks receive the same information through `SYMPHONY_REPOS` (a comma-separated
list of selected repo names) and per-repo env slots
`SYMPHONY_REPO__PATH`, `SYMPHONY_REPO__URL`,
`SYMPHONY_REPO__NAME`, `SYMPHONY_REPO__CREATED_NOW`. The slot is
the repo name uppercased with non-alphanumeric characters replaced by `_`
(e.g. `shared-lib` becomes `SYMPHONY_REPO_SHARED_LIB_PATH`).

When no `repositories:` block is present, Symphony stays in single-repo
(legacy) mode: nothing is cloned automatically, `repos` is an empty array,
and hook scripts are responsible for any workspace population.

## Workspace Hooks

Symphony creates and reuses per-issue directories under `workspace.root` but does not clone or reset repositories by itself. Each hook (`after_create`, `before_run`, `after_run`, `before_remove`) is a shell snippet executed with the issue workspace as the current working directory. The same script is invoked on Windows via `powershell.exe -NoProfile -Command` and on POSIX via `sh -lc`.

The following environment variables are exported to every hook so that a single workflow can target many repositories without parsing the workspace directory name:

| Variable | Description |
| -------------------------------- | ---------------------------------------------------------------------------- |
| `SYMPHONY_HOOK_NAME` | One of `after_create`, `before_run`, `after_run`, `before_remove`. |
| `SYMPHONY_WORKFLOW_DIR` | Absolute directory containing `WORKFLOW.md`. |
| `SYMPHONY_WORKSPACE_ROOT` | Absolute path of the configured `workspace.root`. |
| `SYMPHONY_WORKSPACE_PATH` | Absolute path of the issue workspace (also the hook `cwd`). |
| `SYMPHONY_WORKSPACE_KEY` | Sanitized directory name derived from the issue identifier. |
| `SYMPHONY_WORKSPACE_CREATED_NOW` | `true` when this hook run accompanies workspace creation, `false` otherwise. |
| `SYMPHONY_ISSUE_ID` | Tracker-internal issue id. |
| `SYMPHONY_ISSUE_IDENTIFIER` | Human identifier such as `FE-7`. |
| `SYMPHONY_ISSUE_TITLE` | Issue title. |
| `SYMPHONY_ISSUE_STATE` | Current tracker state name. |
| `SYMPHONY_ISSUE_PRIORITY` | Numeric priority or empty string. |
| `SYMPHONY_ISSUE_BRANCH_NAME` | Tracker-suggested branch name or empty string. |
| `SYMPHONY_ISSUE_URL` | Tracker URL or empty string. |
| `SYMPHONY_ISSUE_LABELS` | Comma-separated label names. |
| `SYMPHONY_ISSUE_DESCRIPTION` | Issue description (may contain newlines). |

For most multi-repo workflows the `repositories:` block above is the
recommended path: Symphony clones the selected repos for you and the hook
only needs to perform repo-specific setup (install dependencies, fetch
branches, etc.). The example below uses `SYMPHONY_REPOS` to install
dependencies for every cloned repo:

```yaml
hooks:
after_create: |
set -e
IFS=',' read -ra REPOS <<< "$SYMPHONY_REPOS"
for repo in "${REPOS[@]}"; do
slot=$(printf '%s' "$repo" | tr '[:lower:]-' '[:upper:]_' | tr -dc 'A-Z0-9_')
eval path=\$SYMPHONY_REPO_${slot}_PATH
(cd "$path" && [ -f package.json ] && npm install) || true
done
```

Hook failures in `after_create` and `before_run` fail the affected run; `after_run` and `before_remove` failures are logged at `warn` level and do not block teardown.

## Implementation-Defined Policy

This implementation uses structured JSON logs on stderr as the status surface and
persists the **same emitted lines** to the configured rotating log file when file
logging is enabled. Rotation happens before appending a line that would exceed
`logging.file.max_bytes`; Symphony keeps at most `logging.file.max_files` files,
including the active file.

Log persistence failures are **non-fatal**: Symphony keeps running and emits one
safe warning to stderr without recursively attempting to save that warning to the
failed log file. The warning omits file paths and original error messages to
avoid leaking local or secret-bearing details.

Symphony does not provide a web UI. It treats Codex approval, thread sandbox, and
turn sandbox values as pass-through fields from workflow configuration.
User-input-required events are not satisfied by Symphony; runs rely on the
configured Codex app-server behavior and fail on process errors, turn timeouts,
or cancellation.

Child processes (hooks and Codex) receive a scrubbed environment that removes parent-process variables whose names look secret-like (`api_key`, `token`, `secret`, `password`, `credential`, `authorization`). Issue fields exported via `SYMPHONY_*` are not redacted; do not place secrets in tracker fields. Codex stderr and runtime messages are redacted before they are written to structured logs.

## Development Commands

```sh
npm run format
npm run lint
npm run build
npm test
npm run verify
```

## Release

This package is not yet published. Before publishing, run `npm run verify`, bump `package.json`, tag the same version, and publish from a clean checkout.