技術 約10分で読めます

BacklogをAIエージェントの制御盤にして、課題処理を半自動化する

いけさん目次

別案件でBacklogの課題対応を回しているうちに、Backlogのステータスをそのままエージェントの状態に対応づけると運用を組みやすいことに気づいた。
一次読解からローカル確認、処理中 課題のオーケストレーション、並列実装、patch統合までをスクリプトとエージェントに回していて、今のところ人間が手を入れているのは「課題を 処理中 へ移す」「統合済みの差分にローカルでOKを出す」「本番反映する」の3点だけだ。
この記事は、どこをスクリプトとエージェントに任せていて、どこを人間のゲートに残しているか、という線引きの運用メモになる。

最初から完成形を設計したわけではない。
Codex CLIに課題本文を渡して確認コメントを書かせる、という小さい自動化から始めて、誤読を見つけるたびに禁止事項と出力フォーマットを足していった結果が今の形だ。

手元ではCodex CLIで組んでいるが、これはCodex固有の話ではない。
やっているのは、課題本文、コメント、添付、ローカル確認メモ、候補ファイルあたりをまとめて読ませて判断させることなので、コンテキストの大きいエージェントなら置き換えがきく。Claude Codeでも、入れ替えれば同じ役を担えるはずだ。
以降の文中の「Codex」も、その意味で読んでほしい。特定のCLIに縛られた設計ではない。

Backlogをチケット置き場ではなくエージェントの状態機械として使う

Backlogのステータス(未対応、仕様・ログ確認中、処理中)を、そのままパイプラインの状態として扱う。
状態の遷移は、AIが起こすものと人間が起こすものに分かれる。
ほとんどの遷移はスクリプトとエージェントに任せていて、人間が起こすのは 処理中 への着手承認、統合済み差分へのローカルOK、本番反映の3つだけだ。

flowchart TD
  A[未対応] -->|AI一次読解| B[仕様・ログ確認中]
  B -->|AIローカル確認メモ| C{人間が処理中へ移す}
  C --> D[処理中]
  D -->|オーケストレーター稼働| E[detached worktreeで並列実装]
  E -->|patch提出| F[メインCodexが直列統合・検証]
  F --> G{人間がローカルでOK}
  G --> H{人間が本番反映}

人間が触るのは図のひし形3つだけ。処理中 へ移す着手承認、統合済み差分へのローカルOK、本番反映で、それ以外の遷移はスクリプトとエージェントが進める。
ただ、そもそもなぜこれをGitHubのIssueではなくBacklogでやっているのか、という前提から先に書いておく。

なぜGitHub IssueではなくBacklogか

このリポジトリには少し変わった事情がある。
クローンした先を別サイト用のシステムとして動かしているのではなく、1つのリポジトリの中でブランチを切って、各サイトごとのUIを放り込んでいる。ブランチがそのままサイトの実体になっている構成だ。
そのため、作業用に気軽にブランチを切れない。ブランチを切る操作が、別サイトのUIを足すことと同義になってしまうからだ。

GitHubのIssueは、Issueからブランチを切ってPRを出す流れとセットで使うのが自然だ。
ところがこのリポジトリではブランチがサイトの実体なので、その流れがそのまま乗らない。Issueとブランチを素直に対応づけられず、かえって混乱する。

公開範囲の問題もある。
プライベートリポジトリなので、Issueを見たり投げたりできるのはアクセス権を持つ人だけだ。誰でも課題を足せるわけではない。

その点Backlogは、プロジェクト単位で関係者が入っている。
誰でも課題を投げられるし、何が投げられているかも関係者なら見られる。受付の窓口としては、こちらのほうが広く開いている。
そういう事情でBacklog運用を試している。都合が悪くなったら別の入れ物に変えればいいだけなので、今のところはこの形だ。

以下、各段階で何を自動化し、3つのゲートに何を残しているかを順に書く。

未対応を一次読解して仕様・ログ確認中へ送る

ここは実装済みの部分。
Backlog APIで未対応課題を取得し、課題本文、既存コメント、添付情報、画像添付をCodex CLIへ渡して、Backlogへ投稿する確認コメントのJSONを生成させる。

生成したコメントは、人間のコメントと区別できるように先頭マーカーを固定している。

※AIによる自動判定です。認識違いがあればご指摘ください。

このコメントを書き込んだら、課題を 仕様・ログ確認中 へ移動する。
受付直後の課題に対して、AIが先に内容を読んで「こう理解した」を残し、ステータスを一段進める、という一次対応の自動化だ。

すでにAIの一次読解コメントが付いている課題は対象から外し、同じ判定を二重に書き込まないようにしている。

定期実行はmacOSのLaunchAgentに任せている。
07:00から21:00まで15分ごとに動かす設定で、深夜に課題が動かない時間帯はそもそも起こさない。

仕様・ログ確認中で止めてローカル読み取りだけ行う

これも実装済みの部分。
仕様・ログ確認中 の課題には、ローカルリポジトリの読み取り確認だけを行う段階を追加した。
ここでの目的は実装の準備であって、実装そのものではない。だから禁止事項を明示している。

  • ファイル編集をしない
  • テストを走らせない
  • ビルドしない
  • デプロイしない
  • 本番環境にアクセスしない
  • DB操作をしない
  • ネットワークアクセスをしない

読み取り対象からも一部を外している。
ログ系のディレクトリと、過去の作業経緯を貯めたアーカイブは読まない。
このあたりは量が多く、現在の課題判断に古い文脈が混ざると読解がぶれるためだ。

ローカル確認のコメントにも専用のマーカーを付ける。

※AIによるローカル確認メモです。実装は開始していません。

一度ローカル確認を書いた課題でも、人間のコメントがその後に入ることがある。
最新のAIローカル確認コメントより後に人間コメントが付いた場合だけ、内容を踏まえて再確認コメントを入れる。
これにも別マーカーを付け、初回確認と再確認を取り違えないようにしている。

※AIによるローカル再確認メモです。実装は開始していません。

AIのコメントだけが続いているうちは、再確認も追記しない。新しい人間の指摘がないのに同じ読解を重ねても意味がないからだ。
そしてこの段階ではステータスを動かさない。ローカル確認はあくまで読解の精度を上げるための準備で、着手の合図にはしない。
現時点では、人間がBacklog上で 処理中 へ移した時点を実装開始のゲートとして残している。

ローカル確認コメントを課題の抄訳として使う

ローカル確認の出力は自由記述ではなく構造化している。
トップに commentconfidencelocal_check を持ち、local_check の中をさらに分けている。

comment      … Backlogへ書く本文
confidence   … 読解の確信度
local_check
  candidate_files        … 触りそうなファイル
  implementation_scope   … 実装範囲の見積もり
  estimate               … 規模感(small / medium / large)
  blockers               … 未確定点・着手前に潰す点
  can_start_from_local   … ローカル情報だけで着手できるか

こうすると、Backlogに残るコメントが課題の抄訳になる。
「この課題はこのファイル群を触る話で、規模はこのくらい、未確定なのはここ」とAIが先に整理した状態で人間が読む。
人間側は、曖昧な指示を一から言語化する代わりに、AIが立てた読解の間違いだけを直せばよくなる。
候補ファイルと未確定点が先に並ぶので、指示出しのコストが下がる。

自動で処理中へ進めてよい条件

ここは「余地はあるが、まだ有効化していない」部分。
構造化出力がそろえば、機械的に 処理中 へ進める判定は書ける。条件はこうなる。

  • can_start_from_localtrue
  • blockers が空
  • estimate が small か medium
  • DB変更を伴わない
  • 本番データ操作を伴わない
  • 既存の作業開始ロックと候補ファイルが被らない
  • 課題本文やコメントに仕様判断待ちが残っていない

これらが全部そろえば、自動で 処理中 へ動かす余地がある。
ただし現時点では、この自動遷移はオフにしている。
処理中 へ移した瞬間に後述のオーケストレーターが実装を回し始めるので、その引き金だけは人間が引く、という運用にしている。処理中 への遷移を「着手してよい」の合図として残しているわけだ。

処理中以降のオーケストレーション

ここも動いている部分。
処理中 に移った課題だけを実装オーケストレーターの対象にする。
オーケストレーターは1時間に1回程度の粒度で回していて、Backlogを読み、AIローカル確認メモ、候補ファイル、未確定点、既存の作業開始ロックを確認してから担当を割り当てる。
そのうえで、並列に進めてよいか競合判定し、競合しない課題だけをエージェントへ渡す。

作業開始ロックと完了の合図

複数のエージェントが同じ課題を二重に拾わないように、担当エージェントは着手前にBacklogへ作業開始ロックのコメントを入れる。

※AI作業開始メモです。
issue: PROJ-123
agent: codex-1
base: 現在ブランチ名 @ HEAD短縮hash
workdir: detached作業場または別cloneのパス
触る予定: path/to/fileA, path/to/fileB
統合担当: main-codex

完了したら、成果と次のアクションを残す。

※AI作業完了メモです。
issue: PROJ-123
成果: patch提出済み
残り: main-codexによる適用・検証・コミット待ち

Backlogのコメント欄を、エージェント間のロックと受け渡しの掲示板として使う形だ。

並列化してよい条件と競合扱い

並列化するなら、複数エージェントに同じ作業ツリーを直接触らせないのが前提になる。
そのうえで、どの課題なら並列に回してよいかを競合判定で絞る。

競合扱いにするのは、ファイル名が同じケースだけではない。
次のいずれかに当たれば競合とみなす。

  • 同じ画面、同じ管理画面ビュー、同じレイアウト
  • 同じAPIのコントローラー / サービス / リポジトリ / モデル
  • DBのマイグレーション、スキーマ、シード、設定テーブル
  • 全体で共有する設定ファイルや依存定義、lockfile
  • 共通コンポーネント、共通CSS、共通ユーティリティ
  • 認証、登録、ポイント、決済、通知などの横断ドメイン

並列化してよいのは、別画面、別API、DB変更なし、共有ファイルなしが明確なときだけにする。
少しでも横断ドメインに触れるなら、並列化せず直列で回す。

ブランチを切らずdetached worktreeで分ける

前述のとおり、このリポジトリではブランチがサイトの実体なので、作業用にブランチを切れない。
並列で実装するとなると、ブランチ以外の方法で作業場を分ける必要がある。

代わりに、現在ブランチの固定HEADからdetached worktree、または別cloneを作る。
各エージェントはそこで作業し、commitではなくpatch / diffを返す。
作業の成果物をブランチに乗せず、差分の塊として受け渡す形にする。

patchをメインCodexが直列に統合する

集まったpatchは、メインのCodexだけが現在ブランチへ適用する。
git apply --check 相当で当たるかどうかを確認しながら直列に適用し、ローカルで検証するところまでをメインCodexが担当する。
並列で実装した差分を、一本の整合したツリーに落とす役を一箇所に集めている。

ここで人間のゲートが1つ入る。統合・検証済みのローカル差分を人間が見て、OKを出して初めてcommit / pushへ進む。
雑に「これやって」と投げた課題でも、ここまで来れば差分とローカル確認メモがそろっているので、見るべきものははっきりしている。

本番反映とDBは別ゲートに残す

自動化を進めても、ここは別ゲートにする。
次の操作は、ローカルの読解や実装準備とは扱いを分けて、コード修正の自動化とは別のゲートに置く。

  • 本番反映
  • DB変更
  • 既存データを増やす・消す・更新する操作
  • 破壊的なgit操作
  • 本番ディレクトリの一括上書き
  • rsyncや同期削除
  • 公開資産やストレージなど、特定ディレクトリへの一括操作

裏を返すと、コード修正から、人間のローカルOKを挟んだcommit / pushまでは自動側に置けている。
そこから先の本番反映を行うのはメインCodexだけにして、しかも明示的な許可、本番反映ルール、非破壊の事前確認をそろえてからにする。
ローカルで差分が当たって検証が通ることと、本番にそれを反映してよいことは別の判断だ。
「ローカルまでは自動で進めるが、本番反映と既存データ操作は人間が握る」という線を、最後まで残す前提で組んでいる。