← Blog

Symphony自身がどのようにSymphonyを使って開発しているか

Hirotaka MiyagiSoftware Engineer
5 min read

OpenAI の Symphony は、Linear のような Issue Tracker をコーディングエージェントのコントロールプレーンにする仕組みです。一定間隔で Issue Tracker をポーリングし、Todo の Issue を見つけたら Issue ごとに隔離されたワークスペースを作って Codex セッションを起動します。完了条件を満たさずに止まっていれば継続を促し、レビュー待ちのステータスに到達したら手を止めて人間に渡します。

Symphony の使い方は2通りあり、1つは SPEC.md をコーディングエージェントに渡して、好きな言語や Issue Tracker のアダプタを実装してもらう方法。SPEC.md はこちらです。

2つ目はリポジトリに同梱されている Elixir の実験的なリファレンス実装をビルドして実行する方法です。私は後者をカスタマイズして使っています。

Symphony を使い始める上で、リポジトリごとの設定や指示プロンプトは WORKFLOW.md と呼ばれるファイルに書きます。ただ実際に書こうとするとなかなか難しいです。Symphony で起動するセッションは人間が介入しない前提なので、無人で品質を落とさずに開発し切ってもらうためのプロンプトやワークフローを定義する必要があります。

幸い Symphony 自身が Symphony を使って開発されており、その WORKFLOW.md がリポジトリに含まれています。このファイルは、無人セッションでの開発用ハーネスをどう設計すればよいかの参考になりました。

今回は Symphony リポジトリに同梱されている WORKFLOW.md を上から引用し読み解きながら、Symphony が Symphony をどのように使っているかを紹介します。

以下引用は筆者による和訳で、2026/08/30 時点の内容を基に記載しています。原文は上記ファイルを参照してください。

WORKFLOW.md は設定とプロンプトを定義する

まず Symphony のWORKFLOW.md は YAML の frontmatter と Markdown の本文でできていて、役割が分かれています。

  • frontmatter: ランタイム設定。どの Tracker のどのステータスを拾い、どれだけ並列で回し、どのエージェントをどんな権限で起動するか
  • 本文: Issue ごとにレンダリングされるプロンプトテンプレート。エージェントが何をどの順でやるかのポリシー

前半がオーケストレーターである Symphony への指示、後半がエージェントである Codex への指示、と読むと分かりやすいです。

frontmatter: いつ動き出し、いつ止まるか

まず Tracker の設定です。

tracker:
  kind: linear
  provider:
    project_slug: "symphony-0c79b11b75ea"
  required_labels: []
  active_states:
    - Todo
    - In Progress
    - Merging
    - Rework
  terminal_states:
    - Closed
    - Cancelled
    - Canceled
    - Duplicate
    - Done

まず、Symphony 自身の開発では Issue Tracker として、Linear が使われていることがわかります。
Symphony は Linear のほか GitHub Issues、GitLab、Jira、Asana のアダプタをサポートしています。

次に active_states と terminal_states があり、Symphony の挙動のほとんどはこの2つで決まります。オーケストレーターは定期的に active_states にある Issue を候補として取得して Codex セッションをディスパッチし、実行中の Issue についても Tracker のステータスを取り直します。ステータスが terminal_states に入ればワーカーを止めてワークスペースを掃除し、どちらにも属さないステータスに移った場合もワーカーを終了します。

ステータスを見てみると、ほとんどが Linear のデフォルト(Backlog / Todo / In Progress / Done / Canceled / Duplicate)であり、タスク管理で一般的なステータスであることがわかります。独自に足されているのは Rework、Merging、Closed です。またこのリストにはないですが Human Review というステータスも活用されています。terminal_states に Cancelled と Canceled が両方並んでいるのはなぜでしょうね。Rework、Merging、Human Review については後半に説明します。

続いてポーリングとワークスペースの設定です。

polling:
  interval_ms: 5000
workspace:
  root: ~/code/symphony-workspaces
hooks:
  after_create: |
    git clone --depth 1 https://github.com/openai/symphony .
    if command -v mise >/dev/null 2>&1; then
      cd elixir && mise trust && mise exec -- mix deps.get
    fi
  before_remove: |
    cd elixir && mise exec -- mix workspace.before_remove

ポーリング間隔は5秒です。SPEC のデフォルトは30秒なので、かなり詰めた設定になっています。
ワークスペースは Issue ごとに ~/code/symphony-workspaces/<識別子> として作られ、after_create でリポジトリの clone と依存解決まで済ませます。 エージェントが起動した時点で、すでにビルドできるリポジトリが目の前にある状態です。
ここは git worktree を使って分離する手もあるかと思いますが、Symphony では clone を選択しているようですね。

最後にエージェントの設定です。

agent:
  max_concurrent_agents: 10
  max_turns: 20
codex:
  command: codex --config shell_environment_policy.inherit=all --config 'model="gpt-5.5"' --config model_reasoning_effort=xhigh app-server
  approval_policy: never
  thread_sandbox: workspace-write
  turn_sandbox_policy:
    type: workspaceWrite
    networkAccess: true

同時に10エージェントに制限しています。これは動作する端末のスペックや作業するリポジトリの大きさなどで調整することになるでしょう。

また1セッションあたり最大20ターンとしています。ターンとは Codex が作業を開始して終了するまでを指していて、ステータスが active_states にあるにも関わらず作業を終了してしまった場合、Symphony は同じセッションのまま作業の継続を指示します。このとき元のタスクプロンプトは再送されず、継続用の短い指示だけが送られます。20ターンを使い切ってもまだ active なままなら、ワークスペースはそのままに新しいセッションを作り直します。

なお、turn_timeout_ms(1ターンの無音上限、デフォルト1時間)や stall_timeout_ms(エージェントからのイベントが途絶えてからワーカーを止めるまでの上限、デフォルト5分)といったタイムアウトも同じ frontmatter で設定できますが、この WORKFLOW.md では省略されています。 デフォルト値を使っているようです。

続いては Codex の設定で、approval_policy: never なのでユーザーへの承認は求めず、書き込みはワークスペース内に限定した上でネットワークは許可、というのが Symphony 自身の運用設定です。
README に「信頼できる環境でのテスト用のエンジニアリングプレビュー」と警告があるとおり、強めの設定になっています。

プロンプト: 無人セッションであることを最初に宣言する

ここから Markdown 本文、つまりエージェントに渡されるプロンプトです。冒頭はこうなっています。

あなたは Linear のチケット {{ issue.identifier }} に取り組んでいます {% if attempt %} 継続時のコンテキスト:

  • これは継続試行 #{{ attempt }} である。通常の継続の場合もあれば、失敗後のリトライの場合もある。
  • ゼロからやり直すのではなく、現在のワークスペースの状態から再開すること。
  • 新しいコード変更に必要でない限り、完了済みの調査や検証を繰り返さないこと。

{% if attempt %} のブロックが継続セッション用の分岐です。前述のとおりセッションは何度も作り直されるので、2回目以降は「ゼロからやり直すな、ワークスペースの現状から再開しろ」と明示的に伝えています。

そして無人実行であることの宣言。

  1. これは無人のオーケストレーションセッションである。人間に後続作業を依頼しないこと。
  2. 早期に停止してよいのは、真に外部的なブロッカー(必要なツール・認証・権限・シークレットの欠如)のときだけである。
  3. 最終メッセージでは、完了した作業とブロッカーのみを報告すること。「ユーザーへの次のステップ」を含めないこと。

対話セッションのつもりで書くと、エージェントは「次はこうしてください」と人間に投げて終わろうとします。それを最後の1行で禁止しています。誰も読まない最終メッセージに提案を書かせず、やったことと詰まったことだけを報告させる。無人ループ用のプロンプトで最初に効いてくる差分だと思います。

Default posture: ステータスから始め、workpad に書く

全体の姿勢を定めるセクションです。長いので抜粋します。

  • まずチケットの現在のステータスを確認し、そのステータスに対応するフローに従う。
  • すべての作業は、追跡用の workpad コメントを開き、新しい実装作業に入る前に最新化することから始める。
  • 実装の前に、計画と検証設計に多めの労力を割く。
  • まず再現する: 修正対象を明確にするため、コードを変更する前に必ず現在の挙動・問題のシグナルを確認する。
  • 進捗については、永続的な Linear コメント1つを唯一の情報源として扱う。
  • 対応する品質バーを満たしたときにのみ、ステータスを動かす。

「まずステータスを見て、対応するフローに入る」が全体の骨格です。Issue のステータスがステートマシンの状態そのもので、プロンプトはその遷移表として書かれています。

もう一つの軸が workpad です。Issue に貼る ## Codex Workpad という見出しのコメント1つを進捗の唯一の情報源とし、そこを更新し続けます。セッションが20ターンで使い捨てられても、次のセッションはこのコメントを読めば計画・完了項目・受け入れ条件を復元できます。コンテキストを揮発させる代わりに、Tracker のコメントを永続メモリとして使う構成です。ここが max_turns と対になっています。

スコープ管理についても明確です。

実行中にスコープ外の意味ある改善が見つかった場合は、スコープを広げるのではなく別の Linear issue を起票する。後続 issue には明確なタイトル・説明・受け入れ条件を含め、Backlog に置き、現在の issue と同じプロジェクトに割り当て、現在の issue を related としてリンクし、後続が現在の issue に依存する場合は blockedBy を使うこと。

「ついでに直す」を禁止し、代わりに Backlog へ Issue を切らせます。そして切り方(タイトル・受け入れ条件・プロジェクト・リンク種別)も指定されています。無人で回すループでは、スコープが膨張するとそのままレビュー不能な PR に直結するので、ここは厳しく縛る価値があります。切られた Issue は Backlog に入るので、人間が Todo に動かせば次のループに乗ります。

Status map: ステータスの意味を1箇所に書く

  • Backlog -> このワークフローの対象外。変更しない。
  • Todo -> キュー投入済み。実作業の前に直ちに In Progress へ遷移する。
  • In Progress -> 実装が進行中。
  • Human Review -> PR が添付され検証済み。人間の承認待ち。
  • Merging -> 人間が承認済み。land スキルのフローを実行する(gh pr merge を直接呼ばない)。
  • Rework -> レビュアーが変更を要求。計画と実装のやり直しが必要。
  • Done -> 終端ステータス。以降の対応は不要。

frontmatter の active_states は「どれを拾うか」しか決めていないので、各ステータスの意味はここで言葉で定義します。Backlog は意図的に Symphony に拾わせないためのステータスです。自分の経験でも、AI に作成させた Issue はどこか本質的でなく、そのまま進行させるのは微妙に感じることが多いです。人間が調整してから Todo に上げるのが良いと思います。

Step 0 はこの表を実行手順に落としたものです。

Todo のチケットでは、開始時の手順を次の順番どおりに行う:

  • update_issue(..., state: "In Progress")
  • ## Codex Workpad のブートストラップコメントを探す、なければ作成する
  • そのあとで初めて分析・計画・実装の作業を始める

In Progress への遷移を最初に、しかも分析より前にやらせています。5秒ごとにポーリングされ10並列で実行される環境では、着手した Issue を即座に可視化しないと状況が読めなくなります。

Step 1: workpad を作り、再現してから書き始める

issue に対して、永続的なスクラッチパッドコメントを1つ探すか作成する:

  • 既存のコメントからマーカー見出し ## Codex Workpad を検索する。
  • 見つかった場合はそのコメントを再利用する。新しい workpad コメントを作らない。
  • workpad のコメント ID を保持し、進捗の更新はその ID にのみ書き込む。

コメントを増やさないことが繰り返し強調されています。ループが回るたびにコメントが増えると、Issue が人間にとって読めなくなるためです。

環境スタンプの指定も面白いところです。

workpad の先頭に、コードフェンス1行でコンパクトな環境スタンプを含めること:

  • 形式: <host>:<abs-workdir>@<short-sha>
  • 例: devbox-01:/home/dev-user/code/symphony-workspaces/MT-32@7bdde33bc
  • Linear の issue フィールドから推測できるメタデータ(issue ID、status、branch、PR link)は含めない。

どのホストのどのワークスペースのどのコミットで作業していたかを1行で残す。並列で動くセッションを人間が追跡するための最小限の情報です。Issue のフィールドから分かることは書くな、という指定も付いています。

実装前の手順も具体的です。

  • 実装の前に、具体的な再現シグナル(コマンドと出力、スクリーンショット、または決定論的な UI の挙動)を取得し、workpad の Notes セクションに記録する。
  • コードを編集する前に pull スキルを実行して最新の origin/main と同期し、その結果を workpad の Notes に記録する。

再現の確認と origin/main の同期を、コードを触る前の必須手順として置いています。どちらも「やったつもり」で飛ばされやすい工程なので、workpad への記録とセットで義務化されているのが効きそうです。

PR feedback sweep: レビューコメントを取りこぼさない

すべてのチャネルからフィードバックを収集する:

  • PR のトップレベルコメント (gh pr view --comments)
  • インラインのレビューコメント (gh api repos/<owner>/<repo>/pulls/<pr>/comments)
  • レビューのサマリ/ステート (gh pr view --json reviews)

対応の余地があるレビュアーのコメントは、インラインのレビューコメントを含め、人間・ボットを問わずすべてブロッキングとして扱う。次のいずれかが成立するまでブロッキングのままとする:

  • コード/テスト/ドキュメントを更新して対応した、または
  • 明確で根拠のある反論を、そのスレッドに返信として投稿した

GitHub の PR フィードバックは3系統に分かれていて、gh pr view --comments だけではインラインのレビューコメントが取れません。自分の経験でもエージェントに任せると高確率で取りこぼす部分で、コマンドまで指定するのは妥当に思いました。

そしてすべてのコメントをブロッキング扱いにした上で、抜ける道を「対応する」か「根拠を付けて反論を返す」の2つに限定しています。反論を明示的に許しているのもポイントで、すべて対応させるとエージェントは的外れな指摘にも従ってコードを歪め始めます。

Step 2〜3: Completion bar とマージまで

前半で触れた Human Review は、frontmatter の active_states にも terminal_states にも含まれていません。エージェントがこのステータスに移した瞬間、その Issue はディスパッチの対象から外れ、実行中のワーカーも次のポーリングで終了します。明示的な停止命令ではなく、ステータス設計そのものが停止条件になっているわけです。

その Human Review に移す前の条件は独立したセクションで列挙されています。

  • Step 1/2 のチェックリストが完全に完了し、単一の workpad コメントに正確に反映されている。
  • 受け入れ条件と、チケットが指定する必須の検証項目が完了している。
  • 最新のコミットで検証/テストがグリーンである。
  • PR フィードバックスイープが完了し、対応すべきコメントが残っていない。
  • PR のチェックがグリーンで、ブランチが push され、PR が issue にリンクされている。
  • 必要な PR メタデータ(symphony ラベル)が付いている。

ステータス遷移を「完了条件を満たしたことの宣言」として扱う設計です。エージェントの自己申告に頼る部分ではありますが、条件が列挙されていればチェックリストとして検証させられます。

人間に渡したあとの扱いも明示されています。

  • issue が Human Review のときは、コードを書いたりチケットの内容を変更したりしない。
  • issue が Merging のときは .codex/skills/land/SKILL.md を開いて従い、PR がマージされるまで land スキルをループで実行する。gh pr merge を直接呼ばない。
  • マージが完了したら issue を Done に移す。

Human Review では何もするな、を先に指示しています。前述のとおり Human Review はディスパッチ対象外なのでそもそもセッションは起動しませんが、エージェント自身がこのステータスに移した直後、次のポーリングでワーカーが止まるまでのわずかな時間が残ります。プロンプト側でも縛っているのはそのためでしょう。

個人的には Merging をステータスとして用意しているのはなるほどと思った点でした。Symphony のワークフローでは人間が PR のレビューをするため、そのまま GitHub の UI 上でマージすればいいんじゃないかと初見では思いましたが、このステータスが便利な点はコンフリクトがある場合です。「内容はいいがコンフリクトだけ解消してからマージして」という指示が、Merging ステータスに移動するだけで表現できます。

Merging から呼ばれる land スキルの目的を見ると、その射程がわかります。

  • PR が main とコンフリクトしていない状態にする。
  • CI をグリーンに保ち、失敗したら直す。
  • チェックが通ったら squash merge する。
  • PR がマージされるまでユーザーに制御を返さない。ブロックされない限りウォッチャーループを回し続ける。

もちろんトークンがもったいないので、コンフリクトも CI の失敗もない場合はそのまま UI 上でマージするとよいでしょう。ただしこのワークフローで issue を Done に動かすのは land を終えたエージェントだけなので、UI でマージした場合は issue が Human Review のまま残ります。Linear 側の自動遷移を設定する前提です。

Step 4: Rework は差分修正ではなくやり直し

  1. Rework は差分的なパッチ当てではなく、アプローチの完全なリセットとして扱う。
  2. issue の本文と人間のコメントをすべて読み直し、今回の試行で何を変えるのかを明示的に特定する。
  3. issue に紐づく既存の PR をクローズする。
  4. issue から既存の ## Codex Workpad コメントを削除する。
  5. origin/main から新しいブランチを作る。

もう一つの独自ステータス、Rework です。レビューで方針から否定されたときに、既存の PR とブランチと workpad を全部捨てて作り直させます。
無人セッションでは途中介入ができないため、しばしば誤った方向性のまま突き進むことはあります。そういう時にリセットしてやり直す道を用意しているのは合理的です。

まとめ

ここまで説明してきた Symphony 自身のワークフローを図にすると、次のようになります。

Symphony 自身のワークフロー図。issue 作成から Backlog、Todo、In Progress、Human Review、Merging、Done へと進み、Human Review だけが active_states に含まれないため Symphony が停止する

また WORKFLOW.md に何を書くべきか、この例から取り出せる要点はこのあたりでした。

  • ステータスをステートマシンとして設計する。どこで拾い(active_states)、どこで人間に渡し(リストから外す)、どこで再開するかを frontmatter とプロンプトの両方で一致させる
  • セッションは使い捨て、記憶は Issue Tracker に置く。max_turns でセッションが作り直される前提に立ち、workpad コメント1つを永続メモリにする
  • 無人であることを明示する。人間への申し送りを禁止し、早期終了の条件を外部ブロッカーだけに限定する
  • スコープの膨張を別 Issue に逃がす。切り方まで指定して、ループの外に出す
  • 取りこぼしやすい操作はコマンドごと書く。PR フィードバックの3チャネルのように、抽象的な指示では抜ける工程を具体化する

Symphony 自体はまだエンジニアリングプレビューですが、WORKFLOW.md は Symphony を使わなくても読む価値があります。コーディングエージェントを無人で回すハーネスとして何を用意する必要があるのか、その項目リストとして参考になりました。

今回は Symphony 自身が Symphony をどのように使っているかを紹介しました。しかし、無人セッションではIssue起票時にある程度方向性を決め切る必要があること、ゴールを満たしていることを確認するためのQA環境のセットアップなど、考えるべきことが他にもあります。
私自身も普段の業務で Symphony を使って複数タスクを並列で進行しているため、次回の記事ではその使い方も紹介します。