本文へスキップ

カンバンボードにラベルを付けると iOS の実装が始まる環境を作る

📋 目次

背景

Xcode 27 で、Xcode MCP にヘッドレスモードが入りました。 それまでは Xcode を起動して許可ダイアログを人が押す必要があったので、無人で回すことができませんでした。

sudo xcrun mcp-server enable --unsafe-always-allow-all-agents
sudo xcrun mcp-server allow-folder ~/work --always
xcrun mcp-server start

CLI で許可できるようになったということは、人が画面の前にいなくても Xcode を扱えるということです。 Device Interaction のツールを使えばシミュレータの操作まで届きます。

そうなると、どこまで任せられるのかが気になります。 カンバンボードのチケットにラベルを付けるだけで、実装からシミュレータでの確認まで進み、人はレビューだけすればよい。 そういう形が作れるのか試しました。

構成

組み合わせたのは次の 4 つです。

  • Linear:カンバンボード。チケットの列が、そのまま進行の状態になる。
  • Symphony:ボードを定期的に読み、条件に合うチケットごとに Codex を起動するデーモン。
  • Codex:実装するエージェント。
  • Tart:macOS の VM ランタイム。Xcode ごとこの中に閉じ込める。

Linear のチケットを Symphony が拾い、VM の中で Codex が実装する、という並びです。

カンバンボードでラベルを付けてから人のレビューに渡るまでの流れ

VM に Xcode 27 と Symphony を入れ、その中で Codex を回します。 ホストには何も置きません。

Symphony の設定は WORKFLOW.md 一枚です。 YAML の前書きで挙動を、本文でプロンプトを指定します。

Linear はワークフロー状態がそのままボードの列なので、拾う条件に列を書けます。

tracker:
  kind: linear
  required_labels: [agent]
  active_states:
    - Todo
    - In Progress
    - In Review
  terminal_states:
    - Done
    - Canceled

agent ラベルが付いていて、かつ active_states の列にあるチケットだけを拾います。 判定は AND なので、ラベルの無いチケットは同じボードにあっても触られません。 自分が手で書くチケットとエージェントのチケットが、同じボードの上で混ざりません。

Todo に agent ラベルの付いたチケットが並んでいる

Xcode MCP の登録は Codex 側の設定に書きます。 DEVELOPER_DIR を渡さないと xcrun がどの Xcode を指すかが環境依存になるので、VM の中で xcode-select -p を解決して埋めています。

[mcp_servers.xcode]
command = "xcrun"
args = ["mcpbridge"]
env = { DEVELOPER_DIR = "@@DEVELOPER_DIR@@" }

Codex は既定で自分のサンドボックスを持ち、書き込みを制限したり、ツールの実行に承認を求めたりします。 VM の中でさらに縛ると .git に書けず、コミットが失敗します。 隔離は VM で成立しているので、Codex 側は開けておきます。

codex:
  command: codex --config shell_environment_policy.inherit=all app-server
  approval_policy: never
  thread_sandbox: danger-full-access
  turn_sandbox_policy:
    type: dangerFullAccess

shell_environment_policy.inherit=all も要ります。 Codex は既定で *TOKEN* を含む環境変数を子シェルから外すので、これが無いと gh が認証できません。

通信先を VM の外から絞る

VM の中には、リポジトリのコードや GitHub のトークンが入っています。 ビルドやシミュレータでの実行を通じて、それらがどこかのサーバへ送られてしまうのは避けたいところです。

Tart に付いている softnet というパケットフィルタを使って、事前に指定したドメイン以外へは通信できないようにしました。 このフィルタは VM の外側で動くので、中でどんな権限を取っても迂回できません。

VM の外側で通信先を絞る仕組み

既定では全部止めておき、通したい宛先だけを書きます。

@host                    名前解決に要る
@github                  GitHub
api.linear.app           カンバンボード

宛先の IP が変わっても、書いた内容を読み直して反映するので、VM を起動し直さずに済みます。

起動の手順にも組み込んであります。 本当に絞れているかを実際に試し、通ってはいけない宛先に届いたら Symphony を起動しません。

==> フィルタが効いているか確かめます
    example.com は塞がり、api.github.com は通ります

どの列で誰が動くか

Symphony が拾うのは active_states に書いた列だけです。 チケットがその外へ出ると、走っているエージェントは止まります。 完了を伝える API は無いので、列を移すことが唯一の合図になります。

列動くのは
Todoエージェント拾って実装を始める
In Progressエージェント実装している
In ReviewエージェントPR を出したあと、自分のコードを見直している
Human Review人エージェントは止まっている
Done誰もマージ済み。作業場も片付ける

In Review を active_states に含めているので、エージェントは PR を出したあとも動き続けます。 差分と受け入れ条件を読み直し、直すところがあれば同じブランチに積みます。

見直しが終わったら Human Review に移して止まります。 この列は active_states にも terminal_states にも入れていません。 拾われないので止まりますが、終了でもないので作業場は残ります。

だから差し戻しが軽くなります。 直してほしいことがあれば In Review に戻すだけで、同じ作業場から再開します。

シミュレータでの確認まで任せる

Xcode MCP には、ビルドとテストのほかにシミュレータを操作するツールがあります。 セッションを開いてインストールと起動を済ませ、座標を指定してタップする、という流れです。

DeviceInteractionStartWorkspaceSession
DeviceInteractionInstallAndRun
DeviceInteractionSynthesize   interactionCommand: "t 200 400"
DeviceInteractionEndSession

呼ぶたびにスクリーンショットと UI 階層が返ります。 画面の見た目が変わる変更なら、これを PR に添えるようプロンプトに書いてあります。

gh pr create --attach '<path>#<alt text>'

シミュレータごと VM の中に入っているので、ホストの Xcode には触りません。

人がレビューしやすい状態まで作る

PR が上がったあと、レビューする人にビルド手順を追わせたくありません。 Xcode Cloud と Tophat の組み合わせを通します。

Xcode Cloud がビルドし、その成果物を指す Tophat のリンクを PR コメントに貼ります。 レビューする人はリンクを押すだけで、シミュレータにアプリが入って起動します。

この構成では、Xcode Cloud が持っている成果物を Cloudflare R2 へ置き直していました。 App Store Connect が画面に出すダウンロードリンクが認証済みのブラウザセッションに依存していて、Tophat の http プロバイダに渡すと 401 になるためです。

置き直しを無くせないかと思い、Tophat に Xcode Cloud 用の拡張を出しています。 App Store Connect API は同じ成果物に対して署名済みの downloadUrl を返すので、そちらを使えば認証なしで取得できます。 リンクではなくビルド実行の識別子を受け取る形にしたので、成果物ができる前に URL を組み立てられます。

動くものを触ってからコードを読める状態にしておくと、レビューの精度が変わります。 Tophat は Android にも同じ形で使えます。

ボードを見ていれば実行状況が分かる

Symphony のプロンプトには、進捗を Codex Workpad という一本のコメントに書き続けるよう指示してあります。 別々の完了報告を投げず、同じコメントを編集し続ける形です。

Codex Workpad に進捗が書かれ、PR がボードに紐づいている

書かれる内容はこうなります。

## Codex Workpad

- Implemented root .gitattributes with `* text=auto` and `*.pbxproj merge=union`.
- Verified with `git diff --check` and `git check-attr`.
- Committed as 2b4216b on KUS-9-add-gitattributes, pushed, and opened PR #22.
- Simulator/build verification was not run because this is an attributes-only change.

何をして、何を確かめて、何を確かめていないかが読めます。 設定を詰めている最中に踏んだ問題も、エージェント自身がここに書いてくれていたので原因を特定できました。 VM にログインしてログを掘る前に、ボードを見れば書いてあります。

4 件が In Review に並び、それぞれに PR 番号が付いている

ブランチ名にチケット ID を入れるようプロンプトに書いてあるので、Linear が PR をチケットに紐づけます。 ボードの上で、チケットと PR と進捗が一つに見えます。

上がってくる PR はこうなります。

エージェントが出した PR。ブランチ名にチケット ID が入り、Linear のチケットへのリンクが貼られている

差し戻しの往復もこの画面に出ます。 Human Review から In Review に戻すと、エージェントは同じブランチに直したぶんを積みます。 作り直さないので、レビューの指摘とコミットが 1 対 1 で並びます。

まとめ

ヘッドレスモードが入ったことで、iOS アプリ開発の自動化に足りなかった部分が埋まった印象です。 VM を用意する過程に人が関わる必要がなくなり、ビルドとテストとシミュレータ操作が回るので、動いているあいだ人が見ている必要もありません。

VM に閉じ込めたのは、Xcode ごと中に入れられるからです。 Xcode を閉じ込められるコンテナが無いため、コンテナで組むとビルドだけホストへ橋渡しすることになります。 境界の外でビルドが走るので、閉じ込めた意味が薄れます。 逆に VM を境界にすると決めたなら、その中でさらに縛らないほうが素直に動きます。

人が触るのは、ラベルを付けるところと Human Review でレビューするところだけです。 そのあいだの実装、ビルド、シミュレータでの確認、セルフレビューはエージェントが進めます。 Xcode Cloud と Tophat を通しておけば、レビューする人は動くものを触ってから読めます。

この環境は手元の Mac にある必要がありません。 EC2 の Mac インスタンスのようなクラウドへ移せば、手元に Mac が無くても同じ形が回るはずです。

そうなると、関われる人の範囲が変わります。 開発環境を持っていなくても、カンバンボードを操作できれば実装を始められます。 ディレクターが自分でチケットを書き、上がってきた PR を Tophat のリンクから触って確かめる、という関わり方もできるかもしれません。 コードを読まなくても、動くものを触って良し悪しを伝えられます。

Symphony は Linear のほかに Jira と Asana と GitLab のアダプタも同梱しています。 列を状態として持つトラッカーを使っているなら、同じ形が組めるはずです。 よければ試してみてください。

参考リンク

この記事をシェア