symphony Documentation

Symphony turns project work into isolated, autonomous implementation runs, allowing teams to manage work instead of supervising coding agents.

26,543 stars Elixir Elixir 1 file ยท ~7,910 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy symphony on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name symphony -p 1:1 --restart always openai/symphony:latest
Configured Services & Ports
โš™๏ธ worker1
image: build locally
๐Ÿ”Œ Ports: ${SYMPHONY_LIVE_DOCKER_WORKER_1_PORT}:22
โš™๏ธ worker2
image: build locally
๐Ÿ”Œ Ports: ${SYMPHONY_LIVE_DOCKER_WORKER_2_PORT}:22
docker-compose.yml
yaml (docker-compose.yml)
services:
  worker1:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${SYMPHONY_LIVE_DOCKER_WORKER_1_PORT}:22"
    volumes:
      - ${SYMPHONY_LIVE_DOCKER_AUTHORIZED_KEY}:/run/symphony/ssh/authorized_key.pub:ro
      - ${SYMPHONY_LIVE_DOCKER_AUTH_JSON}:/root/.codex/auth.json:ro

  worker2:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${SYMPHONY_LIVE_DOCKER_WORKER_2_PORT}:22"
    volumes:
      - ${SYMPHONY_LIVE_DOCKER_AUTHORIZED_KEY}:/run/symphony/ssh/authorized_key.pub:ro
      - ${SYMPHONY_LIVE_DOCKER_AUTH_JSON}:/root/.codex/auth.json:ro

File: README.md

Symphony

Symphony turns project work into isolated, autonomous implementation runs, allowing teams to manage
work instead of supervising coding agents.

In this demo video, Symphony monitors a Linear board for work and spawns agents to handle the tasks. The agents complete the tasks and provide proof of work: CI status, PR review feedback, complexity analysis, and walkthrough videos. When accepted, the agents land the PR safely. Engineers do not need to supervise Codex; they can manage the work at a higher level.

[!WARNING]
Symphony is a low-key engineering preview for testing in trusted environments.

Running Symphony

Requirements

Symphony works best in codebases that have adopted
harness engineering. Symphony is the next step --
moving from managing coding agents to managing work that needs to get done.

Option 1. Make your own

Tell your favorite coding agent to build Symphony in a programming language of your choice:

Implement Symphony according to the following spec:
https://github.com/openai/symphony/blob/main/SPEC.md

Option 2. Use our experimental reference implementation

Check out elixir/README.md for instructions on how to set up your environment
and run the Elixir-based Symphony implementation. You can also ask your favorite coding agent to
help with the setup:

Set up Symphony for my repository based on
https://github.com/openai/symphony/blob/main/elixir/README.md


License

This project is licensed under the Apache License 2.0.


File: elixir/docs/logging.md

Logging Best Practices

This guide defines logging conventions for Symphony so Codex can diagnose failures quickly.

Goals

  • Make logs searchable by issue and session.
  • Capture enough execution context to identify root cause without reruns.
  • Keep messages stable so dashboards/alerts are reliable.

Required Context Fields

When logging issue-related work, include both identifiers:

  • issue_id: Linear internal UUID (stable foreign key).
  • issue_identifier: human ticket key (for example MT-620).

When logging Codex execution lifecycle events, include:

  • session_id: combined Codex thread/turn identifier.

Message Design

  • Use explicit key=value pairs in message text for high-signal fields.
  • Prefer deterministic wording for recurring lifecycle events.
  • Include the action outcome (completed, failed, retrying) and the reason/error when available.
  • Avoid logging large payloads unless required for debugging.

Scope Guidance

  • AgentRunner: log start/completion/failure with issue context, plus session_id when known.
  • Orchestrator: log dispatch, retry, terminal/non-active transitions, and worker exits with issue context. Include session_id whenever running-entry data has it.
  • Codex.AppServer: log session start/completion/error with issue context and session_id.

Checklist For New Logs

  • Is this event tied to a Linear issue? Include issue_id and issue_identifier.
  • Is this event tied to a Codex session? Include session_id.
  • Is the failure reason present and concise?
  • Is the message format consistent with existing lifecycle logs?

File: elixir/docs/token_accounting.md

Codex Token Accounting

This document explains how Codex reports token usage through the app-server protocol and how Symphony should account for it.

It is based on the current Codex source in codex-rs, especially:

  • app-server/README.md
  • protocol/src/protocol.rs
  • app-server/src/bespoke_event_handling.rs
  • app-server-protocol/src/protocol/v2.rs
  • exec/src/event_processor_with_jsonl_output.rs
  • state/src/extract.rs

Short Version

  • last_token_usage means "the latest increment".
  • total_token_usage means "the cumulative total so far".
  • thread/tokenUsage/updated is the live streaming notification for token usage.
  • turn/completed carries final turn state, and turn-level usage is exposed separately from the live thread token stream.
  • Generic usage fields are event-specific. Do not assume every usage payload is a cumulative thread total.

Primary Source Semantics

Codex defines TokenUsageInfo like this:

rust
pub struct TokenUsageInfo {
    pub total_token_usage: TokenUsage,
    pub last_token_usage: TokenUsage,
    pub model_context_window: Option,
}

The important behavior is in append_last_usage:

rust
pub fn append_last_usage(&mut self, last: &TokenUsage) {
    self.total_token_usage.add_assign(last);
    self.last_token_usage = last.clone();
}

That gives the core semantics:

  • last_token_usage: the newest chunk of usage that was just added
  • total_token_usage: the accumulated total after adding that chunk

This is the most important accounting rule in the Codex source.

Event Types

`codex/event/token_count`

Codex core emits token count events containing TokenUsageInfo.

These events can carry:

  • info.total_token_usage
  • info.last_token_usage
  • info.model_context_window

Symphony sees these events wrapped inside the app-server message stream.

Meaning:

  • total_token_usage is an absolute cumulative snapshot
  • last_token_usage is the delta that produced that snapshot

`thread/tokenUsage/updated`

The app-server converts token count events into a dedicated thread-scoped notification:

rust
let notification = ThreadTokenUsageUpdatedNotification {
    thread_id: conversation_id.to_string(),
    turn_id,
    token_usage,
};

ThreadTokenUsage is defined as:

rust
pub struct ThreadTokenUsage {
    pub total: TokenUsageBreakdown,
    pub last: TokenUsageBreakdown,
    pub model_context_window: Option,
}

And it is populated directly from TokenUsageInfo:

rust
impl From<CoreTokenUsageInfo> for ThreadTokenUsage {
    fn from(value: CoreTokenUsageInfo) -> Self {
        Self {
            total: value.total_token_usage.into(),
            last: value.last_token_usage.into(),
            model_context_window: value.model_context_window,
        }
    }
}

Meaning:

  • thread/tokenUsage/updated is the canonical live notification for token usage
  • tokenUsage.total is an absolute thread total
  • tokenUsage.last is the latest increment that produced that total

The app-server README is explicit: token usage streams separately via thread/tokenUsage/updated.

`turn/completed`

The app-server README says turn/completed carries final turn state and token usage.

There are two important details:

  1. The app-server protocol turn/completed notification contains a final turn object.
  2. The exec event processor also emits a turn-completed event that includes a usage struct.

In the exec event processor, the turn-completed usage is built from the most recent captured total_token_usage:

rust
if let Some(info) = &ev.info {
    self.last_total_token_usage = Some(info.total_token_usage.clone());
}

Then on turn completion:

rust
let usage = if let Some(u) = &self.last_total_token_usage {
    Usage {
        input_tokens: u.input_tokens,
        cached_input_tokens: u.cached_input_tokens,
        output_tokens: u.output_tokens,
    }
}

Important consequence:

  • a turn-completed usage payload is not the same schema as ThreadTokenUsage
  • it should be interpreted in the context of the specific event that emitted it
  • it must not be blindly mixed with thread/tokenUsage/updated accounting

Generic `usage`

Codex uses the word usage in multiple places.

That does not mean all usage maps have the same semantics.

Examples:

  • thread/tokenUsage/updated.tokenUsage.total: absolute cumulative thread total
  • thread/tokenUsage/updated.tokenUsage.last: latest delta
  • turn-completed usage: event-specific completion usage payload

Rule:

  • never classify a usage map by name alone
  • classify it by event type and payload path

What The Metrics Mean

Absolute totals

These are safe high-water-mark style counters:

  • info.total_token_usage
  • tokenUsage.total on thread/tokenUsage/updated

Use these when you want:

  • live dashboard totals
  • stable per-thread accumulation
  • recovery after missed intermediate events

Deltas

These are incremental additions:

  • info.last_token_usage
  • tokenUsage.last on thread/tokenUsage/updated

Use these only when:

  • no absolute total is available
  • you are explicitly handling additive updates

Context window

model_context_window is not spend. It is the model's context limit.

Codex also has logic that can "fill to context window", which sets:

  • total_token_usage.total_tokens = context_window
  • last_token_usage.total_tokens = delta

So total_tokens can reflect context-window normalization behavior, not just a raw upstream token report.

For Symphony, model_context_window should be displayed or logged separately from spend.

Track usage per active Codex thread.

For each thread, keep:

  • absolute_total: latest accepted absolute total snapshot
  • accumulated_total: the total you expose in UI/API
  • last_seen_turn_id

Preferred source order

When a token-related event arrives, use this precedence:

  1. thread/tokenUsage/updated.tokenUsage.total
  2. TokenCountEvent.info.total_token_usage

Ignore these for accounting:

  • thread/tokenUsage/updated.tokenUsage.last
  • TokenCountEvent.info.last_token_usage
  • generic usage maps
  • turn-completed usage

Do not treat generic params.usage as equivalent to a cumulative thread total unless the event type makes that meaning explicit.

Algorithm

If an absolute total is present

  • Treat it as a thread-level snapshot.
  • If it is greater than or equal to the stored absolute_total, replace the stored absolute total.
  • Set exposed totals from that absolute snapshot.
  • Do not add the corresponding delta again.

If no absolute total is present

  • Ignore the event for accounting.
  • Keep the last accepted absolute high-water mark unchanged.

Why this matters

If you misclassify a per-turn usage payload as an absolute thread total, later turns can appear to stall because a smaller per-turn number is compared against a larger cumulative baseline.

What Symphony Should And Should Not Do

Do

  • Prefer thread/tokenUsage/updated for live reporting.
  • Treat tokenUsage.total as authoritative for thread totals.
  • Key accounting by thread_id, not just issue id.
  • Expect one thread to span multiple turns when Symphony reuses a live Codex thread.

Do not

  • Do not treat every usage map as absolute.
  • Do not count tokenUsage.last or last_token_usage into dashboard totals.
  • Do not add turn-completed usage on top of already-counted live thread totals unless you can prove it represents missing spend.
  • Do not reset accounting just because a new turn starts on the same thread.

Practical Interpretation For Symphony Logs

When reading raw app-server events:

  • codex/event/token_count
    • useful if you are inspecting nested info.total_token_usage
  • thread/tokenUsage/updated
    • best source for live dashboard and API totals
  • turn/completed
    • best used as end-of-turn state, not as an unconditional additive token event

Why `total_token_usage` Is The Durable Choice

Codex itself consistently prefers cumulative totals when it needs durable state:

  • the state extractor stores info.total_token_usage.total_tokens
  • the exec event processor caches the last total_token_usage and uses that on turn completion

That is a strong signal for Symphony:

  • use absolute totals as the main accounting surface
  • ignore last/delta values for totals

If Symphony documents token reporting externally, the contract should be:

  • Live token totals come from Codex thread-scoped cumulative usage.
  • Incremental usage may also be emitted, but Symphony does not use it for totals.
  • Turn-completed usage is event-specific and should not be assumed to be a fresh additive increment.
  • Reporting is thread-based, and multiple turns can occur on one thread.

Implementation Checklist

  • Prefer thread/tokenUsage/updated.tokenUsage.total
  • Fallback to info.total_token_usage
  • Ignore last for totals
  • Key totals by thread_id
  • Do not classify generic usage by field name alone
  • Do not double-count turn-completed usage after live updates

File: elixir/README.md

Symphony Elixir

This directory contains the current Elixir/OTP implementation of Symphony, based on
SPEC.md at the repository root.

[!WARNING]
Symphony Elixir is prototype software intended for evaluation only and is presented as-is.
We recommend implementing your own hardened version based on SPEC.md.

Screenshot

How it works

  1. Polls the configured tracker for candidate work (included adapters: Linear, GitHub Issues, Jira
    Cloud, Asana, and GitLab)
  2. Creates a workspace per issue
  3. Launches Codex in App Server mode inside the
    workspace
  4. Sends a workflow prompt to Codex
  5. Keeps Codex working on the issue until the work is done

During app-server sessions, the selected tracker adapter may advertise provider-native tools. The
Linear serves linear_graphql, GitHub Issues serves github_api, Jira Cloud serves
jira_rest, Asana serves asana_api, and GitLab serves gitlab_api. Symphony executes those
tools with configured host-side auth and removes declared tracker-token environment variables from
the Codex child, so the agent does not need a second tracker login.

If a claimed issue moves to a terminal state (Done, Closed, Cancelled, or Duplicate),
Symphony stops the active agent for that issue and cleans up matching workspaces.

If Codex reports that operator input, approval, or MCP elicitation is required, Symphony keeps the
issue claimed and exposes it as blocked in the runtime state, JSON API, and dashboard. Blocked
entries are in memory only; restarting the orchestrator clears that blocked map, so any still-active
tracker issue can become a dispatch candidate again after restart.

How to use it

  1. Make sure your codebase is set up to work well with agents: see
    Harness engineering.
  2. Get a new personal token in Linear via Settings โ†’ Security & access โ†’ Personal API keys, and
    set it as the LINEAR_API_KEY environment variable.
  3. Copy this directory's WORKFLOW.md to your repo.
  4. Optionally copy the commit, push, pull, land, and linear skills to your repo.
    • The linear skill expects Symphony's linear_graphql app-server tool for raw Linear GraphQL
      operations such as comment editing or upload flows.
  5. Customize the copied WORKFLOW.md file for your project.
    • To get your project's slug, right-click the project and copy its URL. The slug is part of the
      URL.
    • When creating a workflow based on this repo, note that it depends on non-standard Linear
      issue statuses: "Rework", "Human Review", and "Merging". You can customize them in
      Team Settings โ†’ Workflow in Linear.
  6. Follow the instructions below to install the required runtime dependencies and start the service.

Prerequisites

We recommend using mise to manage Elixir/Erlang versions.

bash
mise install
mise exec -- elixir --version

Run

bash
git clone https://github.com/openai/symphony
cd symphony/elixir
mise trust
mise install
mise exec -- mix setup
mise exec -- mix build
mise exec -- ./bin/symphony ./WORKFLOW.md

Burrito releases

Symphony ships self-contained executables built with
Burrito. They embed Erlang/OTP, Elixir, and Symphony,
but still expect codex, git, and the selected tracker credentials on the target machine.

Supported release targets:

  • macos_arm64
  • macos_x86_64
  • linux_arm64
  • linux_x86_64

v* tags publish all four targets with checksums. A manual workflow run builds the same
artifacts without creating a release.

After downloading the executable for your platform from a release:

bash
chmod +x ./symphony-v0.0.1-macos_arm64
./symphony-v0.0.1-macos_arm64 ./WORKFLOW.md

Configuration

Pass a custom workflow file path to ./bin/symphony when starting the service:

bash
./bin/symphony /path/to/custom/WORKFLOW.md

If no path is passed, Symphony defaults to ./WORKFLOW.md.

Optional flags:

  • --logs-root tells Symphony to write logs under a different directory (default: ./log)
  • --port also starts the Phoenix observability service (default: disabled)

The WORKFLOW.md file uses YAML front matter for configuration, plus a Markdown body used as the
Codex session prompt.

Minimal example:

md
---
tracker:
  kind: linear
  provider:
    project_slug: "..."
workspace:
  root: ~/code/workspaces
hooks:
  after_create: |
    git clone [email protected]:your-org/your-repo.git .
agent:
  max_concurrent_agents: 10
  max_turns: 20
codex:
  command: codex app-server
---

You are working on an issue from the configured tracker {{ issue.identifier }}.

Title: {{ issue.title }} Body: {{ issue.description }}

Notes:

  • If a value is missing, defaults are used.
  • tracker.kind selects an adapter. Adapter-owned endpoint, scope, and auth settings belong under
    tracker.provider; the current Linear adapter still accepts the older flat endpoint,
    api_key, project_slug, and assignee aliases for compatibility.
  • tracker.required_labels is optional. When set, an issue must have every
    configured label to dispatch or continue running. Label matching ignores
    case and surrounding whitespace. A blank configured label matches no issue.
  • Safer Codex defaults are used when policy fields are omitted:
    • codex.approval_policy defaults to {"reject":{"sandbox_approval":true,"rules":true,"mcp_elicitations":true}}
    • codex.thread_sandbox defaults to workspace-write
    • codex.turn_sandbox_policy defaults to a workspaceWrite policy rooted at the current issue workspace
  • codex.turn_timeout_ms is the maximum silence interval while a turn is streaming. Each
    app-server update resets it; it is not a total turn runtime cap.
  • Supported codex.approval_policy values depend on the targeted Codex app-server version. In the current local Codex schema, string values include untrusted, on-failure, on-request, and never, and object-form reject is also supported.
  • Supported codex.thread_sandbox values: read-only, workspace-write, danger-full-access.
  • When codex.turn_sandbox_policy is set explicitly, Symphony passes the map through to Codex
    unchanged. Compatibility then depends on the targeted Codex app-server version rather than local
    Symphony validation.
  • Workflows that run package managers or other commands that resolve external hosts should set
    networkAccess: true in codex.turn_sandbox_policy; otherwise DNS/network access may be denied
    by the Codex turn sandbox.
  • agent.max_turns caps how many back-to-back Codex turns Symphony will run in a single agent
    invocation when a turn completes normally but the issue is still in an active state. Default: 20.
  • If the Markdown body is blank, Symphony uses a default prompt template that includes the issue
    identifier, title, and body.
  • Use hooks.after_create to bootstrap a fresh workspace. For a Git-backed repo, you can run
    git clone ... . there, along with any other setup commands you need.
  • If a hook needs mise exec inside a freshly cloned workspace, trust the repo config and fetch
    the project dependencies in hooks.after_create before invoking mise later from other hooks.
  • For the Linear adapter, tracker.provider.api_key reads from LINEAR_API_KEY when unset or
    when value is $LINEAR_API_KEY. The legacy flat tracker.api_key alias behaves the same way.
  • Do not put a literal tracker token in a repo-owned WORKFLOW.md if Codex can read that
    workspace. Use $VAR/host-side secret references so Symphony can keep the token out of the
    child environment.
  • For path values, ~ is expanded to the home directory.
  • For env-backed path values, use $VAR. workspace.root resolves $VAR before path handling,
    while codex.command stays a shell command string and any $VAR expansion there happens in the
    launched shell.
yaml
tracker:
  provider:
    api_key: $LINEAR_API_KEY
workspace:
  root: $SYMPHONY_WORKSPACE_ROOT
hooks:
  after_create: |
    git clone --depth 1 "$SOURCE_REPO_URL" .
codex:
  command: "$CODEX_BIN --config 'model=\"gpt-5.5\"' app-server"
  • If WORKFLOW.md is missing or has invalid YAML at startup, Symphony does not boot.
  • If a later reload fails, Symphony keeps running with the last known good workflow and logs the
    reload error until the file is fixed.
  • server.port or CLI --port enables the optional Phoenix LiveView dashboard and JSON API at
    /, /api/v1/state, /api/v1/, and /api/v1/refresh.

Linear adapter profile

  • Config: use tracker.kind: linear with tracker.provider.endpoint (default
    https://api.linear.app/graphql), api_key (defaults to LINEAR_API_KEY and accepts
    $VAR), required project_slug, and optional assignee (a Linear user ID or me,
    defaulting to LINEAR_ASSIGNEE).
    The legacy flat tracker.endpoint, api_key, project_slug, and assignee aliases remain
    supported. required_labels, active_states, and terminal_states stay under tracker.
  • Scope and paging: candidate reads filter the configured project slug and requested state names,
    following Linear pages of 50. ID refreshes are also project-scoped and batch up to 50 IDs. Empty
    state/ID lists return {:ok, []} without a Linear request.
  • Identity and normalization: issue.id is the Linear issue ID and issue.native_ref is currently
    nil. Records missing a nonblank ID, identifier, title, or state are dropped from candidate
    pages and fail ID refreshes. State keeps Linear's spelling; integer priorities are preserved and
    other priority values become nil; RFC 3339 timestamps are parsed and unusable timestamps become
    nil. Labels are trimmed, lowercased, deduplicated, and blanks are dropped; blockers come from
    inverse blocks relations.
  • Dispatchability: the adapter marks an issue dispatchable only when optional assignee routing
    matches and a Todo issue has no non-terminal blocker. The generic scheduler then applies
    active/terminal states, required labels, claims, retries, and concurrency.
  • Tool: the Linear adapter advertises linear_graphql, accepting either a raw query string or an
    object with nonblank query and optional object variables. Symphony executes it host-side
    with the session-bound endpoint/token and strips declared token environment variables from the
    Codex child. project_slug scopes scheduler reads, not raw tool calls; the tool can access
    whatever the configured Linear token can access.
  • Responsibility and errors: linear_graphql adds no idempotency key, retry, scope guard, or
    rate-limit policy, so workflows own idempotent mutations and handling provider errors. Read/config
    failures use {:error, :missing_linear_api_token}, {:error, :missing_linear_project_slug},
    {:error, :invalid_linear_endpoint}, {:error, :invalid_linear_assignee},
    {:error, :missing_linear_viewer_identity}, {:error, {:linear_api_status, status}},
    {:error, {:linear_api_request, reason}}, {:error, {:linear_graphql_errors, errors}},
    {:error, :linear_unknown_payload}, or {:error, :linear_missing_end_cursor}. Tool results
    are maps with "success", JSON-string "output", and text "contentItems"; invalid
    arguments, missing auth, and transport failures return "success" => false with
    {"error": {"message": ...}}, while top-level GraphQL errors preserve the response body with
    "success" => false.
    For portable reporting, map missing/invalid token, project, endpoint, assignee, or viewer errors
    to tracker_config or tracker_auth, request failures to tracker_transport, non-200 responses to
    tracker_response (429 is tracker_rate_limited), GraphQL/unknown payload failures to
    tracker_payload, and missing cursors to tracker_pagination; logs and tool responses carry the
    human-readable provider detail.

GitHub Issues adapter

  • Config: use tracker.kind: github with required tracker.provider.repo in owner/repo form,
    optional token (defaults to GITHUB_TOKEN and accepts $VAR), and optional api_url
    (default https://api.github.com, HTTPS only). Set explicit active_states and
    terminal_states; active entries may be open and terminal entries may be closed.
  • Reads and identity: polling is scoped to the configured repository; issue.id is the
    repository issue number, issue.identifier is GH-<number>, hidden or deleted 404 issues are
    omitted on refresh, and pull requests returned by the Issues API are not dispatchable.
  • Tool and auth: github_api accepts a relative REST path plus optional params and JSON
    body; Symphony executes it host-side with the session-bound token, removes configured tracker
    credentials and provider authentication aliases from the Codex child, and leaves raw tool access
    limited by that token's GitHub permissions.

Jira Cloud adapter

  • Config: use tracker.kind: jira with provider base_url, email, api_token, and required
    project_key; the first three default to JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN
    and accept $VAR. Set explicit Jira-native active_states and terminal_states.
  • Issues and reads: candidate reads and ID refreshes stay scoped to the configured project and
    requested statuses; issue.id is Jira's immutable ID and issue.identifier is the issue key.
  • Blockers: inward Blocks links populate blocked_by; issues in Jira's new status category
    wait until blockers reach configured terminal states, while in-progress categories keep running.
  • Tool: jira_rest sends relative /rest/api/3/ requests host-side with configured Basic auth,
    strips token environment variables from Codex, and can reach whatever the Jira credential can.

Asana adapter

  • Config: use tracker.kind: asana with required tracker.provider.project_gid, optional
    endpoint (default https://app.asana.com/api/1.0), and api_key (defaults to ASANA_PAT and
    accepts $VAR); active_states and terminal_states are project section names.
  • Scope: Symphony polls tasks in the configured project, treats their section as state, and omits
    deleted or out-of-project tasks during ID refreshes.
  • Tool: asana_api sends relative Asana REST requests host-side with the configured auth; Symphony
    strips ASANA_PAT and configured token variables from the Codex child, while raw tool calls are
    not limited to the configured project.

GitLab adapter

  • Configure tracker.kind: gitlab with tracker.provider.project_path, optional api_url, and
    api_key (default GITLAB_PAT); use opened and closed tracker states.
  • Symphony reads project issues by IID and exposes route-safe GL- identifiers.
  • gitlab_api forwards raw GitLab REST requests with host-side auth and keeps configured tracker
    credentials and provider authentication aliases out of the Codex child.

Web dashboard

The observability UI now runs on a minimal Phoenix stack:

  • LiveView for the dashboard at /
  • JSON API for operational debugging under /api/v1/*
  • Bandit as the HTTP server
  • Phoenix dependency static assets for the LiveView client bootstrap
  • Tracker issue identifiers link to the tracker-provided URL when it uses http or https

Project Layout

  • lib/: application code and Mix tasks
  • test/: ExUnit coverage for runtime behavior
  • WORKFLOW.md: in-repo workflow contract used by local runs
  • ../.codex/: repository-local Codex skills and setup helpers

Testing

bash
make all

Run the real external end-to-end test only when you want Symphony to create disposable Linear
resources and launch a real codex app-server session:

bash
cd elixir
export LINEAR_API_KEY=...
make e2e

Optional environment variables:

  • SYMPHONY_LIVE_LINEAR_TEAM_KEY defaults to SYME2E
  • SYMPHONY_LIVE_SSH_WORKER_HOSTS uses those SSH hosts when set, as a comma-separated list

make e2e runs two live scenarios:

  • one with a local worker
  • one with SSH workers

If SYMPHONY_LIVE_SSH_WORKER_HOSTS is unset, the SSH scenario uses docker compose to start two
disposable SSH workers on localhost:. The live test generates a temporary SSH keypair,
mounts the host ~/.codex/auth.json into each worker, verifies that Symphony can talk to them
over real SSH, then runs the same orchestration flow against those worker addresses. This keeps
the transport representative without depending on long-lived external machines.

Set SYMPHONY_LIVE_SSH_WORKER_HOSTS if you want make e2e to target real SSH hosts instead.

The live test creates a temporary Linear project and issue, writes a temporary WORKFLOW.md, runs
a real agent turn, verifies the workspace side effect, requires Codex to comment on and close the
Linear issue, then marks the project completed so the run remains visible in Linear.

Run the opt-in GitHub Issues live test with a disposable/scratch repository:

bash
cd elixir
export SYMPHONY_LIVE_GITHUB_REPO=owner/scratch-repo
export GITHUB_TOKEN=...
SYMPHONY_RUN_GITHUB_LIVE_E2E=1 mix test test/symphony_elixir/github_live_e2e_test.exs

Run the opt-in Jira Cloud live test against a disposable project whose credential can browse,
create, comment on, transition, and delete issues:

bash
cd elixir
export JIRA_BASE_URL=https://your-site.atlassian.net
export JIRA_EMAIL=...
export JIRA_API_TOKEN=...
export SYMPHONY_LIVE_JIRA_PROJECT_KEY=TEST
SYMPHONY_RUN_JIRA_LIVE_E2E=1 mix test test/symphony_elixir/jira_live_e2e_test.exs

Run the opt-in Asana live E2E against disposable Asana resources:

bash
cd elixir
export ASANA_PAT=...
export SYMPHONY_LIVE_ASANA_WORKSPACE_GID=...
# Required only when the workspace is an organization:
# export SYMPHONY_LIVE_ASANA_TEAM_GID=...
SYMPHONY_RUN_ASANA_LIVE_E2E=1 mix test test/symphony_elixir/asana_live_e2e_test.exs

Run the opt-in GitLab live E2E against a disposable project:

bash
cd elixir
export GITLAB_PAT=...
export SYMPHONY_LIVE_GITLAB_PROJECT_ID=...
SYMPHONY_RUN_GITLAB_LIVE_E2E=1 mix test test/symphony_elixir/gitlab_live_e2e_test.exs

FAQ

Why Elixir?

Elixir is built on Erlang/BEAM/OTP, which is great for supervising long-running processes. It has an
active ecosystem of tools and libraries. It also supports hot code reloading without stopping
actively running subagents, which is very useful during development.

What's the easiest way to set this up for my own codebase?

Launch codex in your repo, give it the URL to the Symphony repo, and ask it to set things up for
you.

License

This project is licensed under the Apache License 2.0.


File: docs/symphony-smoke-board-review.md

Symphony Board Review Smoke Test

This brief note confirms this is a Symphony smoke test for the board review workflow.


File: docs/symphony-smoke-test-one.md

Symphony Smoke Test

Symphony picked up Jira issue SD-4 and handled it by adding this isolated smoke-test doc.