{"id":50412353,"url":"https://github.com/cskwork/oh-my-symphony","last_synced_at":"2026-05-31T04:04:57.472Z","repository":{"id":356656218,"uuid":"1233519809","full_name":"cskwork/oh-my-symphony","owner":"cskwork","description":"Multi-agent fork of OpenAI Symphony — drives Codex, Claude Code, Gemini, and Pi CLIs from one orchestrator with a Jira-style CLI Kanban TUI","archived":false,"fork":false,"pushed_at":"2026-05-18T12:53:48.000Z","size":1969,"stargazers_count":4,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-18T13:23:22.131Z","etag":null,"topics":["ai-agents","asyncio","claude-code","codex","gemini","kanban","linear","orchestration","python","rich","symphony","tui"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cskwork.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-09T03:42:49.000Z","updated_at":"2026-05-18T12:53:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cskwork/oh-my-symphony","commit_stats":null,"previous_names":["cskwork/symphony-multi-agent","cskwork/oh-my-symphony"],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/cskwork/oh-my-symphony","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cskwork%2Foh-my-symphony","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cskwork%2Foh-my-symphony/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cskwork%2Foh-my-symphony/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cskwork%2Foh-my-symphony/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cskwork","download_url":"https://codeload.github.com/cskwork/oh-my-symphony/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cskwork%2Foh-my-symphony/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33718498,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","asyncio","claude-code","codex","gemini","kanban","linear","orchestration","python","rich","symphony","tui"],"created_at":"2026-05-31T04:04:56.874Z","updated_at":"2026-05-31T04:04:57.459Z","avatar_url":"https://github.com/cskwork.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# oh-my-symphony\n\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)\n[![Python: 3.12+](https://img.shields.io/badge/Python-3.12%2B-3776AB.svg)](https://www.python.org/)\n[![Tests](https://github.com/cskwork/oh-my-symphony/actions/workflows/tests.yml/badge.svg)](https://github.com/cskwork/oh-my-symphony/actions/workflows/tests.yml)\n[![GitHub stars](https://img.shields.io/github/stars/cskwork/oh-my-symphony?style=social)](https://github.com/cskwork/oh-my-symphony/stargazers)\n\n\u003e One terminal. One Kanban board. Four AI coding agents\n\u003e (**Codex**, **Claude Code**, **Gemini**, **Pi**) — pick per ticket, run in\n\u003e parallel, watch live.\n\n![symphony tui screenshot](docs/tui-screenshot.svg)\n\n\u003csub\u003e`symphony tui ./WORKFLOW.md` — columns are your tracker's states; cards show the active agent, turn count, last event, and accumulated tokens. Live indicators: ● running, ↻ retry queued, ✓ done.\u003c/sub\u003e\n\n**Stop juggling AI coding CLIs.** Symphony hands each Kanban ticket to the\nagent you want, runs them concurrently in isolated `git worktree` workspaces,\nand shows live progress — turn counts, token usage, rate-limit headroom — in\na Jira-style TUI you never have to leave your terminal for.\n\n[**Try it in 60 seconds, no AI CLI required →**](#try-it-in-60-seconds-no-agent-cli-required)\n\n## Why Symphony?\n\n- **No vendor lock-in.** Swap Codex ↔ Claude Code ↔ Gemini ↔ Pi with one\n  YAML line, or mix backends per ticket. New agents (Ollama, local models,\n  anything with a CLI) drop in behind a thin `AgentBackend` Protocol — four\n  steps, no orchestrator changes.\n- **See what your agents are actually doing.** Live Kanban shows turn count,\n  last event, accumulated tokens, and rate-limit headroom for every running\n  card. No more \"is it stuck or just thinking?\" — and no SaaS dashboard to\n  log into.\n- **Run dozens of tickets in parallel, unattended.** Concurrency is built in:\n  every ticket gets its own `git worktree` workspace, so agents can't step on\n  each other. Headless mode mirrors progress to a Markdown file you can\n  `tail -F` in any editor; macOS keep-awake stops the lock screen from\n  killing overnight pipelines.\n- **No SaaS, no API key, no signup to try.** File-based Markdown Kanban\n  means tickets live in `git` next to your code. Linear is supported as a\n  drop-in tracker; you don't need it.\n- **Battle-tested core.** Forked from\n  [OpenAI's official Symphony reference implementation](https://github.com/openai/symphony).\n  The orchestrator, scheduler, retry policy, workspace lifecycle, and prompt\n  renderer are all upstream — this fork is a thin layer that adds the four\n  backends and the TUI.\n- **Operator-grade tooling out of the box.** `symphony doctor` catches the\n  five most common first-run failures (port collisions, missing CLIs,\n  placeholder URLs, unwritable workspaces) in one pass. `symphony service\n  start/stop/restart/logs` runs the orchestrator as a managed background\n  service. A web viewer adds **Pause / Resume** for running cards and a real\n  branch-picker for feature / merge branches.\n\n## Who is this for?\n\n- **Solo devs** running unattended overnight refactors across dozens of\n  tickets while they sleep.\n- **Teams** parallelizing bug fixes, doc updates, or migration tickets across\n  multiple coding agents simultaneously.\n- **Researchers and reviewers** comparing how Codex, Claude Code, Gemini, and\n  Pi tackle the same task side by side, with identical prompts and\n  workspaces.\n- **Anyone** who hit the \"one chat window per agent\" ceiling and wants a\n  real orchestrator with a Kanban they can read at a glance.\n\n## How it works\n\n\u003cdetails\u003e\n\u003csummary\u003ePlain-text version of the TUI (for terminals viewing raw README)\u003c/summary\u003e\n\n```text\n  agent=codex  tracker=linear  workflow=WORKFLOW.md  lang=en   running=2  retrying=1   │  tokens in=84,200 out=27,640 total=111,840\n                                                                                       │  rate-limits=requests_remaining=4823, tokens_remaining=1.2M\n\n╭── Todo (3) ──────╮ ╭── In Progress (2) ──╮ ╭── Review (1) ──╮ ╭── Done (2) ──╮ ╭── Archive (1) ──╮ ╭── detail ───────────────────────╮\n│  DEMO-120  P1    │ │  DEMO-104  ●  P1    │ │  DEMO-122  P3  │ │  DEMO-088    │ │  DEMO-074       │ │  DEMO-104                       │\n│  Migrate auth …  │ │  Fix race condi…    │ │  Doc: contri…  │ │  Drop dead-… │ │  Old experim…   │ │  Fix race condition in pagina…  │\n│  #backend …      │ │  turn 4  20,180t    │ │  #docs         │ │  DEMO-091    │ │                 │ │                                 │\n│                  │ │  Patched cursor…    │ ╰────────────────╯ │  Bump deps…  │ ╰─────────────────╯ │  state=In Progress              │\n│  DEMO-111  ↻ P2  │ │                     │                    ╰──────────────╯                     │  runtime=running                │\n│  Refactor cach…  │ │  DEMO-098  ●  P2    │                                                         │  turn=4                         │\n│  retry #2  tur…  │ │  Add /api/sear…     │                                                         │  in=14,200  out=5,980           │\n│                  │ │  turn 2  11,310t    │                                                         │  total=20,180                   │\n│  DEMO-121  P2    │ │  Added token-bu…    │                                                         │  Patched cursor advance;        │\n│  Wire feature …  │ ╰─────────────────────╯                                                         │  running test suite...          │\n│  blocked by D…   │                                                                                 ╰─────────────────────────────────╯\n╰──────────────────╯\n\nq quit · r refresh · enter details · 1-9 zoom lane · t/T page lanes · d density · p detail-pane · L language · a archive · / filter · ?\n```\n\n\u003c/details\u003e\n\nA multi-agent fork of [OpenAI's Symphony reference implementation](https://github.com/openai/symphony).\nUpstream polls a tracker (Linear or a local Markdown Kanban) and runs a Codex\nsession inside a per-issue workspace. This fork keeps that orchestrator and\nadds:\n\n1. A pluggable **AgentBackend** layer with four concrete adapters:\n   - **Codex** — `codex app-server` (JSON-RPC stdio, multi-turn) — original\n   - **Claude Code** — `claude -p --output-format stream-json --verbose`\n     (NDJSON events, per-turn subprocess with `--resume`)\n   - **Gemini** — `gemini -p \"\"` (one-shot per turn, stdin prompt → stdout result)\n   - **Pi** — `pi --mode json -p \"\"` (JSONL events, per-turn subprocess with\n     `--session` resume; supports Anthropic / OpenAI / Gemini / Bedrock backends\n     under one CLI — see [pi.dev](https://pi.dev))\n2. A **Jira-style CLI Kanban TUI** built on [Textual](https://textual.textualize.io)\n   that replaces the upstream server-rendered HTML dashboard. Columns are\n   tracker states; cards show the active agent, turn count, last event, and\n   accumulated tokens. Cards are focusable, the mouse wheel scrolls each lane,\n   and pressing `enter` on a card opens a full-detail modal.\n\nThe orchestrator, scheduler, retry policy, workspace manager, tracker layer,\nand prompt renderer are unchanged from upstream — this fork is a thin layer\non top of a battle-tested orchestrator core.\n\n## Pick an agent\n\nSet `agent.kind` in your `WORKFLOW.md`:\n\n```yaml\nagent:\n  kind: claude          # codex | claude | gemini | pi\n\nclaude:\n  command: claude -p --output-format stream-json --verbose\n  resume_across_turns: true\n  turn_timeout_ms: 3600000\n\npi:\n  command: pi --mode json -p \"\"\n  resume_across_turns: true\n  turn_timeout_ms: 3600000\n```\n\nEach backend reads its own block (`codex`, `claude`, `gemini`, `pi`); only the\none matching `agent.kind` is used at runtime. The Codex `linear_graphql`\nclient tool is only advertised when `agent.kind=codex`.\n\n`agent.kind` is the global default. A file-board ticket can opt into a\ndifferent backend by adding ticket frontmatter:\n\n```yaml\nagent:\n  kind: codex\n```\n\nThe flat alias `agent_kind: codex` is also accepted for hand-edited cards.\nAll backend command and timeout settings still come from the matching global\n`codex:`, `claude:`, `gemini:`, or `pi:` block in `WORKFLOW.md`.\nWhen creating file-board tickets from the CLI, use\n`symphony board new TASK-2 \"title\" --agent-kind codex`.\n\nFor file-board workflows, `agent.auto_triage_actionable_todo` defaults to\n`true`: a Todo ticket with a body and an `Acceptance Criteria` section moves to\nExplore with a one-line `## Triage` note without spending a model turn. Bug\ntickets, blocked tickets, ambiguous tickets, and Linear trackers still use the\nTodo prompt.\n\n## Install\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev]\"\n```\n\nMake the relevant CLI available on `$PATH`:\n\n| `agent.kind` | required CLI on `$PATH` |\n|--------------|------------------------|\n| `codex`      | `codex` (with `app-server` subcommand) |\n| `claude`     | `claude` (Claude Code) |\n| `gemini`     | `gemini` (Gemini CLI)  |\n| `pi`         | `pi` (Pi coding-agent — `npm i -g @earendil-works/pi-coding-agent` or `curl -fsSL https://pi.dev/install.sh \\| sh`; sign in once via `pi` → `/login` (OAuth, credentials cached at `~/.pi/agent/auth.json`) — no env var needed) |\n\n## Try it in 60 seconds (no agent CLI required)\n\nWant to see the TUI move cards around before installing `codex`, `claude`,\nor `gemini`? Use the bundled **mock backend** — it speaks the same JSON-RPC\nprotocol as Codex but does no real work, just simulates turns and emits\ntoken-usage ticks.\n\n```bash\ngit clone https://github.com/cskwork/oh-my-symphony.git\ncd oh-my-symphony\npython3 -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -e \".[dev]\"\n\n# WORKFLOW.md pointed at the mock backend\ncat \u003e WORKFLOW.md \u003c\u003c'YAML'\n---\ntracker: { kind: file, board_root: ./kanban,\n           active_states: [Todo, \"In Progress\"],\n           terminal_states: [Done, Cancelled, Blocked] }\npolling: { interval_ms: 5000 }\nworkspace: { root: ~/symphony_workspaces }\nhooks:\n  after_create: \": noop\"\n  before_run:   \": noop\"\n  after_run:    \"echo done\"\nagent:  { kind: codex, max_concurrent_agents: 1, max_turns: 3, max_total_turns: 60 }\ncodex:  { command: python -m symphony.mock_codex }\nserver: { port: 9999 }\n---\nYou are picking up ticket {{ issue.identifier }}: {{ issue.title }}.\nYAML\n\nsymphony board init ./kanban\nsymphony board new TASK-1 \"smoke test\"\nsymphony tui ./WORKFLOW.md\n```\n\nWithin ~5 seconds TASK-1 grows a green ● indicator in the **Todo** column,\nwith a turn counter and token totals climbing. Quit with `Ctrl-C` when\nyou've seen enough; then proceed to the real walkthrough below.\n\n\u003e Cards stay in their original column under the mock — only a real agent\n\u003e would rewrite `kanban/TASK-1.md` to move the card to **Done**. The mock\n\u003e exists to prove the orchestrator → backend → workspace → hooks pipeline\n\u003e end-to-end without an LLM call.\n\n\u003e Tunables for the mock: `SYMPHONY_MOCK_TURN_SECONDS=12`,\n\u003e `SYMPHONY_MOCK_FAIL_EVERY_N_TURNS=3`, etc. — see `src/symphony/mock_codex.py`.\n\n---\n\n## Preflight — `symphony doctor`\n\nBefore launching, sanity-check your setup:\n\n```bash\nsymphony doctor ./WORKFLOW.md\n```\n\nOutput (one line per check):\n\n```\nPASS  server.port=9999              127.0.0.1:9999 is free\nPASS  agent.kind=claude             claude → /usr/local/bin/claude\nFAIL  hooks.after_create            contains placeholder 'my-org/my-repo' — every dispatch will fail with rc=128. Switch to the worktree default or replace with a real clone / `: noop`.\nPASS  workspace.root=~/symphony_workspaces  exists and is writable\nPASS  tracker.board_root            ./kanban (3 tickets)\n```\n\nExit code is `0` when all checks pass, `1` if any FAIL, `2` if `WORKFLOW.md`\nitself can't be loaded. The doctor catches the most common first-run\nfailures in one pass: port collision, missing CLI on `$PATH`, the shipped\nplaceholder clone URL, unwritable workspace, missing board directory.\n\n---\n\n## Quickstart — your first task end-to-end\n\nThis walks from a clean clone to a running ticket, using the file-based\ntracker and Claude Code as the agent.\n\n### 1. Initialize the board\n\n```bash\nsymphony board init ./kanban\n# → initialized board at ./kanban, sample ticket DEMO-001.md\n```\n\nEach ticket is one Markdown file with YAML frontmatter at `kanban/\u003cID\u003e.md`.\nThe orchestrator only **reads** ticket files; the agent **writes** them when\nit transitions state.\n\n### 2. Author `WORKFLOW.md`\n\nUse the **file-tracker** example (the other one, `WORKFLOW.example.md`,\npoints at Linear and needs an API key):\n\n```bash\ncp WORKFLOW.file.example.md WORKFLOW.md\n```\n\nFour blocks matter for first-run sanity:\n\n```yaml\ntracker:\n  kind: file\n  board_root: ./kanban\n  active_states: [Todo, \"In Progress\"]\n  terminal_states: [Done, Cancelled, Blocked]\n\nworkspace:\n  root: ~/symphony_workspaces\n\nhooks:\n  # Each ticket gets its own workspace at workspace.root/\u003cID\u003e.\n  # The shipped default attaches it as a `git worktree` of the host repo\n  # on a `symphony/\u003cID\u003e` branch — host working tree stays untouched.\n  # Use `: noop` instead while you experiment without a host repo.\n  after_create: |\n    : noop                       # ← swap for the worktree default in WORKFLOW.file.example.md\n  before_run: |\n    : noop                       # runs before every agent turn\n  after_run: |\n    echo \"run finished at $(date)\"\n\nprompts:\n  # Symphony sends base plus only the file for the ticket's current state.\n  base: ./docs/symphony-prompts/file/base.md\n  stages:\n    Todo: ./docs/symphony-prompts/file/stages/todo.md\n    \"In Progress\": ./docs/symphony-prompts/file/stages/in-progress.md\n```\n\n\u003e ⚠ The shipped `WORKFLOW.example.md` / `WORKFLOW.file.example.md` default to\n\u003e attaching the per-ticket workspace as a **git worktree** of the host repo\n\u003e (the directory containing `WORKFLOW.md`) on a `symphony/\u003cID\u003e` branch. The\n\u003e host working tree is never disturbed; merge results back with\n\u003e `git -C \u003chost\u003e merge symphony/\u003cID\u003e` (or open a PR from that branch) when\n\u003e you're satisfied — explicit operator action, never automatic.\n\u003e\n\u003e If your code lives in a *different* remote than the WORKFLOW.md repo,\n\u003e swap the hook for `git clone \u003cremote\u003e .` instead. While experimenting\n\u003e without any repo, use `: noop`.\n\n### 3. Add a ticket\n\n```bash\nsymphony board new TASK-1 \"Fix flaky pagination test\" \\\n  --priority 2 \\\n  --labels backend,test \\\n  --description \"tests/test_pagination.py::test_cursor_advance is flaky on CI.\"\n# → created kanban/TASK-1.md\n```\n\nInspect:\n\n```bash\nsymphony board ls                    # all tickets\nsymphony board ls --state Todo       # filter by state\nsymphony board show TASK-1           # full body\n```\n\n### 4. Launch the TUI\n\n```bash\nsymphony tui ./WORKFLOW.md\n```\n\nWithin one poll tick (`polling.interval_ms`, default 30s) the orchestrator\ndispatches a worker, the card grows a green ● indicator (with turn counter\nand token totals), and the agent runs. On success the agent rewrites\n`kanban/TASK-1.md` to set `state: Done` and append a `## Resolution`\nsection — that file edit is what moves the card from the **Todo** column\ninto **Done**. Quit with `Ctrl-C`.\n\n\u003e Cards are placed in columns based on the ticket file's `state` field\n\u003e (`tui.py` reads it on each tick). The green ● indicator is overlaid on\n\u003e top of the card and does **not** change which column it sits in. So a\n\u003e running ticket stays in **Todo** until the agent itself rewrites the\n\u003e file — that's by design (the orchestrator only reads ticket files; the\n\u003e agent owns writes).\n\n\u003e The TUI needs a real terminal (TTY). If you launch it from a script /\n\u003e background process / non-interactive shell, the process exits silently —\n\u003e always run it in a foreground terminal.\n\n### 4b. Headless mode + `WORKFLOW-PROGRESS.md`\n\nDrop `tui` to run the orchestrator without opening the Kanban UI:\n\n```bash\nsymphony ./WORKFLOW.md                  # headless; progress mirror auto-on\nsymphony ./WORKFLOW.md --no-progress-md # headless; no progress file\n```\n\nA live `WORKFLOW-PROGRESS.md` is rewritten next to your workflow file on\nevery tick (default ~30s) and on every state change in between. Open it\nin your editor to follow along without a TTY:\n\n```markdown\n# Symphony Progress\n_Updated: 2026-05-16 14:22:31 UTC_\n\n## Kanban\n| State        | Tickets |\n|--------------|---------|\n| Todo         | OLV-005, OLV-006 |\n| In Progress  | OLV-002 (8m12s · 12k tok) |\n| Review       | OLV-001 |\n| Done         | OLV-003, OLV-004 |\n\n## Recent transitions\n- `2026-05-16 14:22:31Z`  **OLV-002**  Todo → In Progress\n- `2026-05-16 14:18:04Z`  **OLV-001**  In Progress → Review\n```\n\nOverride location or limits via `WORKFLOW.md` frontmatter (or `--progress-md-path`):\n\n```yaml\nprogress:\n  enabled: true                     # default true; CLI --no-progress-md wins\n  path: docs/STATUS.md              # default: WORKFLOW-PROGRESS.md beside WORKFLOW.md\n  max_transitions: 20               # how many recent transitions to keep\n```\n\nThe mirror is read-only output — Symphony rewrites the file atomically;\ndo not edit it by hand.\n\n#### macOS keep-awake\n\nWhile a run is active, Symphony holds a wake-lock on macOS so the screen\nsaver / lock screen cannot interrupt a long unattended pipeline (the\nprocess itself is fine either way, but a locked display blocks operator\nattention and many auto-suspend policies). Disable per run with\n`--no-keep-awake`, or persist in `WORKFLOW.md`:\n\n```yaml\nsystem:\n  keep_awake: false   # default true; CLI --no-keep-awake also wins\n```\n\nNon-macOS hosts log `keep_awake_skipped` and continue without a wake-lock.\n\n#### Slack notifications (optional)\n\nOpt in by setting a Slack incoming-webhook URL. With the block below in\n`WORKFLOW.md`, Symphony posts one message per tracker state transition.\nOmit the block and nothing is sent — the feature is fully off by default.\n\n```yaml\nnotifications:\n  slack:\n    webhook_url: $SLACK_WEBHOOK_URL    # required; $VAR resolved at load time\n    enabled: true                       # default true when webhook is set\n    notify_on_states: []                # empty = every transition; or e.g. [Done, Blocked]\n    templates:                          # optional per-state overrides\n      Done: \"✅ ${identifier} ${title} (${workflow})\"\n      Blocked: \"🚧 ${identifier} blocked — ${title}\"\n    username: Symphony\n    icon_emoji: \":robot_face:\"\n    timeout_ms: 5000\n```\n\nTemplate placeholders: `${identifier}` `${title}` `${prev_state}`\n`${next_state}` `${workflow}` `${reason}`. Bad templates render the unknown\nkey literally — they never raise. Network errors are caught and logged\n(`slack_notify_network_error`) so a Slack outage cannot block the\norchestrator's transition path.\n\n### 5. Inspect the result\n\n```bash\nsymphony board show TASK-1               # the agent's ## Resolution lives in the body\nls ~/symphony_workspaces/TASK-1          # workspace it operated in\n```\n\nSymphony writes structured logs to **stderr only**. To keep them around,\nredirect at launch:\n\n```bash\nmkdir -p log\nsymphony tui ./WORKFLOW.md 2\u003e\u003e log/symphony.log\n# or, while running headless:\nsymphony ./WORKFLOW.md --port 9999 2\u003e\u00261 | tee -a log/symphony.log\n```\n\nThen `tail -F log/symphony.log` works.\n\n### 6. Move tickets manually (rare)\n\n```bash\nsymphony board mv TASK-1 Blocked         # forces a state transition\n```\n\nThe orchestrator re-evaluates on the next poll tick. Manual transitions are\nfor unsticking — normally the agent transitions tickets itself per the\nstage-specific prompt files configured by `WORKFLOW.md`.\n\n### How dispatch works in one diagram\n\n```\n┌────────────┐    poll      ┌──────────────┐    matches active_states\n│  kanban/   │  ─────────▶  │ Orchestrator │  ─────────────────────────┐\n│  *.md      │   30s tick   │ (scheduler)  │                            │\n└────────────┘              └──────────────┘                            ▼\n      ▲                            │                          ┌──────────────────┐\n      │                            │ creates workspace        │  Workspace       │\n      │ agent writes               ▼                          │  ~/sym…/TASK-1   │\n      │ ## Resolution     ┌──────────────────┐                │  + after_create  │\n      │ + state: Done     │  AgentBackend    │  ◀────────────│    hook ran      │\n      └───────────────────│  (codex/claude/  │                └──────────────────┘\n                          │   gemini)        │                          │\n                          │  per-turn loop   │  before_run hook ──▶ turn(s)\n                          └──────────────────┘                          │\n                                                                        ▼\n                                                                  after_run hook\n```\n\n## Per-ticket artefacts\n\nEvery artefact a ticket produces lives under `docs/\u003cTICKET-ID\u003e/\u003cstage\u003e/`. See [`docs/PIPELINE.md`](docs/PIPELINE.md#per-ticket-artefact-root) for the layout, what to commit, and the `${LLM_WIKI_PATH:-./docs/llm-wiki}/` carve-out.\n\n## Custom prompts\n\n`WORKFLOW.md` can point at editable prompt files under `docs/`:\n\n```yaml\nprompts:\n  base: ./docs/symphony-prompts/file/base.md\n  stages:\n    Todo: ./docs/symphony-prompts/file/stages/todo.md\n    Explore: ./docs/symphony-prompts/file/stages/explore.md\n    Plan: ./docs/symphony-prompts/file/stages/plan.md\n    \"In Progress\": ./docs/symphony-prompts/file/stages/in-progress.md\n```\n\nSymphony sends `base` plus only the prompt file for the ticket's current\nstate, keeping each turn smaller than the old all-stage prompt. If the\n`prompts` block is absent, the inline body of `WORKFLOW.md` still works as\nthe legacy fallback.\n\n---\n\n## Run\n\n### Background service + JSON API\n\n```bash\nsymphony ./WORKFLOW.md --port 9999\n```\n\nJSON API endpoints (unchanged from upstream):\n\n| Method | Path                       | Purpose                                      |\n|--------|----------------------------|----------------------------------------------|\n| GET    | `/api/v1/state`            | Snapshot — running, retrying, totals, limits |\n| GET    | `/api/v1/\u003cidentifier\u003e`     | Issue detail (404 with structured error)     |\n| POST   | `/api/v1/refresh`          | Coalesced trigger of poll + reconcile        |\n\nThe HTML dashboard at `/` from upstream has been removed in this fork; the\nprimary UI is the CLI Kanban below.\n\n### CLI Kanban TUI (primary UI)\n\n```bash\nsymphony tui ./WORKFLOW.md\n# equivalent\nsymphony ./WORKFLOW.md --tui\n```\n\n#### Recommended default: TUI + JSON API together\n\nThe TUI is the primary operator view and the JSON API is the\nprogrammatic / curl-friendly view. Run both in one process by pinning\n`server.port` in `WORKFLOW.md` and launching with `--tui`\n(`tools/board-viewer/` remains available as an optional in-browser\nkanban, see below):\n\n```yaml\n# WORKFLOW.md\nserver: { port: 8765 }\n```\n\n```bash\nsymphony --tui ./WORKFLOW.md\n# kanban renders in the terminal, JSON API listens on 127.0.0.1:8765\ncurl -s http://127.0.0.1:8765/api/v1/state | jq\n```\n\nUse `--port N` on the CLI to override the workflow value, or drop the\n`server` block to disable the HTTP API entirely.\n\nColumns are tracker states (`active_states` first, then `terminal_states`).\nCards display issue identifier + title, priority, labels (or blockers), and a\nruntime indicator:\n\n- **● green** — currently running, shows `turn N`, last event, accumulated tokens\n- **↻ yellow** — in retry queue, shows `retry #N` and the last error\n- **✓ green** — completed in this session\n\nKey bindings (also auto-listed in the footer):\n\n| Key                | Action                                       |\n|--------------------|----------------------------------------------|\n| `q`                | Quit (drains active workers cleanly)         |\n| `r`                | Force a refresh + re-poll the tracker        |\n| `?`                | Show all key bindings as a notification      |\n| `tab` / `shift+tab`| Move focus to next / previous card or lane   |\n| `j` / `↓`          | Scroll focused lane down one row             |\n| `k` / `↑`          | Scroll focused lane up one row               |\n| `space` / `pgdn`   | Page down                                    |\n| `b` / `pgup`       | Page up                                      |\n| `g` / `home`       | Jump to top                                  |\n| `G` / `end`        | Jump to bottom                               |\n| `enter`            | Open the focused card's full-detail modal    |\n| `esc` / `q`        | Close the modal (when one is open)           |\n\nMouse: clicking a card focuses it, the wheel scrolls its lane.\n\n#### Managed background service\n\nFor day-to-day operation, prefer the built-in service command over ad-hoc\nshell jobs. It records the workflow it started under\n`.symphony/run/\u003cworkflow-hash\u003e.json`, so the same `WORKFLOW.md` cannot be\nstarted again on a second port by accident:\n\n```bash\nsymphony service start ./WORKFLOW.md --port 9999 --viewer-port 8765\nsymphony service status ./WORKFLOW.md\nsymphony service restart ./WORKFLOW.md\nsymphony service stop ./WORKFLOW.md\nsymphony service logs ./WORKFLOW.md\n```\n\n`service start` runs `symphony doctor` before spawning, starts the\norchestrator with Python's module runner, and starts `tools/board-viewer/`\nwhen that folder exists. Commands are launched without a shell, so the same\npath works on macOS, Linux, and Windows.\n\nSince v0.4.7, the board viewer (default `--viewer-port 8765`) is no longer\nread-only: running cards surface **Pause / Resume** buttons and the header\nrefresh button triggers an orchestrator `poll + reconcile`. The header also\nshows real local git branch dropdowns for `agent.feature_base_branch` and\n`agent.auto_merge_target_branch`, so operators can choose where new feature\nbranches start and where Learn merges land without editing YAML by hand.\n\n#### One-shot launchers\n\nFor developers who don't want to remember the full `symphony tui` invocation,\nthe repo ships two launcher scripts that prefer `.venv/bin/symphony` over\n`PATH`, run `symphony doctor` first, then open the TUI in a new terminal\nwindow:\n\n```bash\n./tui-open.sh                     # macOS / Linux — uses iTerm or Terminal.app\n./tui-open.sh path/to/WORKFLOW.md # explicit workflow path\ntui-open.bat                      # Windows — uses cmd /k\n```\n\nBoth scripts abort the launch if `doctor` reports a FAIL so you do not paint\nthe alt-screen on top of unreadable preflight output.\n\n### File-based Kanban tracker\n\nIf you don't have Linear, use the local Markdown-file tracker (unchanged from\nupstream):\n\n```yaml\ntracker:\n  kind: file\n  board_root: ./kanban\n```\n\n```bash\nsymphony board init ./kanban\nsymphony board new DEV-1 \"Title\" --priority 2\nsymphony tui ./WORKFLOW.md\n```\n\n## Layout\n\n```\nsrc/symphony/\n  backends/\n    __init__.py        AgentBackend Protocol + factory + normalized events\n    codex.py           Codex JSON-RPC stdio backend (was upstream agent.py)\n    claude_code.py     Claude Code stream-json backend\n    gemini.py          Gemini one-shot backend\n    pi.py              Pi --mode json backend (per-turn subprocess, --session resume)\n  trackers/\n    __init__.py        TrackerClient Protocol + factory\n    file.py            FileBoardTracker (Markdown ticket files)\n    linear.py          LinearClient (Linear GraphQL)\n  cli/\n    __init__.py        re-exports `main` for the `symphony` console_script\n    __main__.py        keeps `python -m symphony.cli ...` working for service.py\n    main.py            root dispatch + `symphony [WORKFLOW]`\n    board.py           `symphony board ...` file-tracker helper\n    doctor.py          `symphony doctor` WORKFLOW.md preflight checks\n  utils/\n    archive.py         auto-archive selector\n    auto_merge.py      symphony/\u003cID\u003e branch → host repo merge\n    keep_awake.py      macOS caffeinate wrapper (no-op on other platforms)\n    wiki_sweep.py      Learn-prompt wiki integrity sweep\n  agent.py             back-compat shim re-exporting backends.* symbols\n  workflow.py          typed config — adds AgentConfig.kind + Claude/Gemini/Pi configs\n  orchestrator.py      scheduler; uses build_backend() + build_tracker_client() factories\n  tui.py               Textual Kanban TUI (replaces server.py dashboard)\n  server.py            JSON API only (HTML root removed)\n  service.py           `symphony service` background lifecycle\n  mock_codex.py        runnable via `python -m symphony.mock_codex` for demos/tests\ntui-open.sh            cross-platform launcher (macOS / Linux): doctor preflight + open TUI in a new terminal window\ntui-open.bat           Windows equivalent\n```\n\n## Tests\n\n```bash\npytest -q\n```\n\nThe test suite covers the upstream conformance suite, backend unit tests for\nthe factory, event normalization, Claude / Pi usage accumulation, Gemini\nsession synthesis, and Pi failure-reason detection, plus Textual\n`Pilot`-driven smoke tests for the TUI app. Subprocess-driven integration\ntests against real CLIs are intentionally not in CI — run them locally.\n\n## Design notes\n\n### Why four different lifecycles behind one Protocol?\n\n- **Codex** opens one `app-server` subprocess per issue and speaks the\n  current `codex app-server` JSON-RPC protocol (`initialize` + `thread/start`\n  + `turn/start` + streamed `turn/completed` and `item/completed`\n  notifications). Multi-turn within one process. Older `v2/initialize`-style\n  releases are not supported — pin to `codex-cli ≥ 0.39` (current upstream).\n- **Claude Code** has no persistent server; sessions are tracked by ID. Each\n  `run_turn` spawns a fresh `claude -p` and uses `--resume \u003csession-id\u003e` from\n  turn 2 onward.\n- **Gemini CLI** is one-shot per invocation with no native session model.\n  Each turn is independent; we synthesize a `gemini-\u003cuuid\u003e` session id so the\n  orchestrator's bookkeeping stays consistent.\n- **Pi** has no persistent server but auto-saves sessions to\n  `~/.pi/agent/sessions/`. Each `run_turn` spawns a fresh `pi --mode json` and\n  passes `--session \u003cid\u003e` from turn 2 onward. The session id is read from the\n  first `{\"type\":\"session\"}` JSONL line; per-message `usage` is accumulated\n  off `message_end` events, and `agent_end` is treated as the terminal event.\n  Auth is delegated to Pi: the OAuth/API-key store at `~/.pi/agent/auth.json`\n  populated by `/login` is inherited by the subprocess, so Symphony itself\n  never handles credentials.\n\nThe `AgentBackend` Protocol hides these differences. The orchestrator only\nsees normalized events (`session_started`, `turn_completed`, `turn_failed`,\n…) and the latest usage / rate-limit snapshots.\n\n### What the TUI does and does not do\n\nThe board is observer-only: cards move when the agent rewrites the underlying\nticket file (file tracker) or transitions the issue (Linear), never as a\ndirect UI action. That matches the upstream design philosophy — the\norchestrator is the source of truth and the UI is a thin reflection.\n\nWhat you *can* do interactively:\n\n- Focus any card with `tab` / `shift+tab` or by clicking it.\n- Scroll a lane with the mouse wheel, `j` / `k`, or page keys.\n- Open a focused card's full description in a modal with `enter`.\n\nWhat is intentionally out of scope:\n\n- **No card drag-drop.** Move tickets via `symphony board mv ID State`\n  (file tracker) or in your tracker UI directly.\n- **No agent-output log pane.** Agent stdout/stderr goes to the structured\n  log; tail it with `tail -F log/symphony.log` in a side terminal.\n- **No write actions to the tracker** beyond what the agent does itself.\n\n## What is *not* implemented\n\nInherited from upstream:\n\n- SSH worker extension — single-host only.\n- Persistent retry queue across process restarts.\n- Tracker adapters beyond Linear and the file-based Kanban.\n- First-class tracker write APIs in the orchestrator. Ticket writes still\n  happen through the agent (`linear_graphql` for Codex, direct file edits for\n  the file-based Kanban).\n\nFork-specific gaps:\n\n- Claude Code's mid-turn streaming usage events are read but not surfaced;\n  the terminal `result` event is the source of truth for token totals.\n- Gemini token usage is not reported by the CLI in stable form, so totals\n  stay at zero for that backend.\n- Multi-turn continuity for Gemini is not supported (no session protocol\n  exists in the CLI). Each `run_turn` is independent.\n\n## Contributing\n\nPRs welcome. External contributions should target `dev` by default; see\n[CONTRIBUTING.md](CONTRIBUTING.md) and the PR template for the full review\nchecklist. Before opening one:\n\n```bash\npip install -e \".[dev]\"\npytest -q          # must stay green\n```\n\nBackend adapters live under `src/symphony/backends/`. Adding a new agent\n(e.g. an Ollama-driven local model) means:\n\n1. implementing the `AgentBackend` Protocol in a new module,\n2. registering it in `build_backend()` (`src/symphony/backends/__init__.py`),\n3. adding a `\u003ckind\u003eConfig` dataclass to `workflow.py` and threading it\n   through `build_service_config` + `validate_for_dispatch`,\n4. extending `SUPPORTED_AGENT_KINDS`.\n\nThe bar for upstreaming a backend is: passes the existing factory + event\nnormalization tests, doesn't bleed protocol-specific types into the\norchestrator, and ships a default `\u003ckind\u003e` block in `WORKFLOW.example.md`.\n\n## Acknowledgements\n\nThis project is built on top of OpenAI's\n[Symphony](https://github.com/openai/symphony) reference implementation. The\nupstream Apache-2.0 licensed work provides the orchestrator, the scheduler,\nand the workspace lifecycle that make this fork possible. See `NOTICE` for\nattribution details.\n\nThe TUI is built on Will McGugan's [Textual](https://textual.textualize.io)\nframework, with [rich](https://github.com/Textualize/rich) used directly for\ntext styling inside cards.\n\nPipeline stage rules adapt the evidence-first ideas of [cskwork/backend-dev-skills](https://github.com/cskwork/backend-dev-skills) (MIT).\n\n## License\n\n[Apache 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcskwork%2Foh-my-symphony","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcskwork%2Foh-my-symphony","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcskwork%2Foh-my-symphony/lists"}