こんにちは、エンジニアのオオバです。

お悩みさん
お悩みさん
  • AIのターンが終わったら作業枠は空いたことにしていい?
  • AIが質問して止まっているのに気づけない
  • PRが未マージの枠に別のAIを割り当ててしまう
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    僕は作業枠(Dev1・Dev2…という作業用のフォルダ)ごとに Claude Code や Codex を並べて動かしています。

    どの枠でAIが動いているかは、自作の Dev Slots というツールでメニューバーに出しています。

    ところが、このツールは「作業中」しか見ていませんでした。

    AIのターンが終わった瞬間に、枠の表示が空きに戻ってしまう。

    「終わったなら空いたんでしょ?」って思いますよね。

    結論、空いていません。

    AIが「どちらで進めますか?」と返事を待っていたり、PRがまだマージされていなかったりするからです。

    その枠に別のAIを割り当てると、二重アサインの事故が起きます。

    ということで本記事では、Dev Slots に「回答待ち」と「確認待ち」の2つの状態を足した話をお届けします。

    AIの最後の返答に質問や依頼があれば「回答待ち」(青)、未コミットか未マージのコミットがあれば「確認待ち」(紫)。マージ時刻か「完了にする」を押した時刻が最後の返答より後なら空きに戻す

    先に結論をまとめます。

    作業枠の回答待ち・確認待ちの判定

    ①回答待ち(青):最後に触ったAIの返答に「?」の行や返事が要る依頼がある

    ②確認待ち(紫):git statusに変更があるか、origin/developへ未マージのコミットが残っている

    ③完了時刻(マージ時刻と手動の完了の新しい方)がAIの最後の返答より後なら空きに戻す

    ざっくり言うと、前半が2つの状態の判定方法、後半が実際に使ってみて出た誤判定とその直し方です。

    では本編に入っていきます。

    →11万文字で徹底解説した「DOTweenの教科書」Unityアニメーションの超効率化ツールはこちら

    やりたかったこと:空いていない枠を空きに見せない

    表示の優先順位は次のようにしました。

    1. 衝突(赤)
    2. 作業中(オレンジ)
    3. 回答待ち(青)
    4. 確認待ち(紫)
    5. 空き(白)

    回答待ちは、記号を C(Claude Code)/ X(Codex)のまま色だけ青にしています。

    確認待ちは紫で、担当のAIが分からない枠は「•」で出します。

    マージ済みで変更も無い枠は、空きに戻ります。

    テスト環境のメニューバー表示。左から回答待ち(青のC)、担当不明の確認待ち(紫の•)、確認待ち(紫のC)

    テスト環境のメニューバー表示。左から回答待ち(青のC)、担当不明の確認待ち(紫の•)、確認待ち(紫のC)

    これまでの表示はこんな感じです。

    実際のメニューバー。両方のMacのDev2でCodexが作業中(オレンジのX)

    実際のメニューバー。両方のMacのDev2でCodexが作業中(オレンジのX)

    作業中のオレンジしか色が無く、AIが手を止めた枠は全部ただの点になっていました。

    回答待ちの判定:最後の返答が質問で終わっているか

    最初の実装は、最後に触ったツールの返答が質問で終わっているかを見る形です。

    Claude Code

    トランスクリプトの末尾の返答が「?」で終わっていれば回答待ちにしました。

    末尾の ** や「」」は無視しています。

    AskUserQuestion や ExitPlanMode の返事を待っている状態も、回答待ちとして数えます。

    Codex

    Codex は、rollout の task_complete イベントに last_agent_message(最後の返答)が入っています。

    その末尾を見るだけで済みました。

    後から別のツールが触ったら待っていない

    同じ枠を後から別のツールが触った場合、その質問はもう待っていないとみなします。

    実ログで確かめると、質問で終わっていたのは Codex が直近145ターン中0件、Claude が直近80セッション中2件。

    誤判定はほぼ出ない数字です。

    ハマった点:質問ボックスで終えたターンが「作業中」に見えていた

    Claude デスクトップアプリは、ターンの最後に質問ボックス(mcp__ccd_turn__wrap_up の ask)を出します。

    このとき、トランスクリプトの最後のレコードは assistant の end_turn になりません。

    ツール結果を返す user レコードが最後に来ます。

    従来の判定は「最後が user ならプロンプト送信直後なので作業中」でした。

    そのため、ユーザーの返事待ちなのに、staleMinutes の30分のあいだ作業中と表示されていたわけです。

    ツール結果のレコードのときは、tool_use_id で直前の assistant レコードの tool_use まで遡るように直しました。

    そこでツール名と input の ask を見て、質問ボックスなら回答待ちにしています。

    確認待ちの判定:gitだけで見る

    確認待ちは、git のコマンドだけで判定しています。

    未コミットと未マージ

    未コミットの変更は、未追跡ファイルを含めて見ます。

    git status --porcelain  
    

    マージ済みかどうかは、HEAD が origin/develop の祖先になっているかで確かめます。

    git merge-base --is-ancestor HEAD origin/develop  
    

    squashマージ対策はmerge-tree

    squash マージだと、HEAD のコミットは origin/develop の祖先になりません。

    そこで git merge-tree を使いました。

    git merge-tree --write-tree origin/develop HEAD  
    

    結果のツリーが origin/develop のツリーと同じなら、「マージしても何も変わらない」のでマージ済みとみなします。

    --write-tree は git 2.38 以降の機能で、macOS 標準の Apple Git 2.39.5 で使えます。

    負担は5秒ごとのスキャンでも問題なし

    1枠あたりの時間は、git status が0.09秒、merge-tree が0.02秒でした。

    5秒ごとにスキャンしても負担になりません。

    origin/developを裏でfetchする

    GitHub で PR をマージしても、手元の origin/develop は fetch するまで古いままです。

    そこで base の1ブランチだけを、定期的に裏で git fetch しています。

    スキャンを待たせないよう、subprocess.Popen(start_new_session=True) で投げっぱなしにしました。

    fetch の結果は次のスキャンで使います。

    ちなみに、gh で PR の状態を見る案は採りませんでした。

    なぜなら SSH で見に行く別の Mac にまで、gh の認証を求めたくなかったから。

    確認:テスト38件と、テスト環境での見た目の確認

    テストは38件書きました。

    見た目は、一時フォルダを HOME にしたテスト環境でアプリをもう1つ起動して確かめています。

    ところがメニューバーが混んでいて、ステータス項目がノッチの裏に隠れてしまいました(笑)。

    そこで CGWindowListCopyWindowInfo でステータス項目のウィンドウIDを取り、screencapture -l で直接撮っています。

    冒頭の青と紫の画像は、この方法で撮ったものです。

    誤判定1:質問を並べたあとに補足を書いて終える返答が「空き」になった

    使い始めてすぐ、取りこぼしが見つかりました。

    MacBook Air の Dev3 で、Claude が「確認したいこと」として質問を7つ並べていました。

    そのあとにマップの叩き台を書いて、「→ ADVへ」で終えています。

    判定は「最後の1文が?で終わるか」だったので、空き表示のまま。

    僕からも「ボタンを押させる確認じゃなくて、ただ自分に確認して回答が欲しいときも待ちにしてほしい」と頼みました。

    返答全体から質問と依頼を探す

    判定を、返答全体から次のどちらかを探す形に変えています。

    実ログ約470返答で確かめました。

    途中にだけ質問がある返答は4件。

    3件は本当に返事待ちで、残りの1件は引用(>)したブログ下書きの中の「?」でした。

    引用・コード・URL の中は見ないようにしています。

    「違ったら教えてください」は待っていない

    依頼の言い回しは56件ありました。

    多くは「内容を確認して、修正があれば教えてください」のような確認の依頼で、これは待ちとして数えます。

    一方で「違ったら教えてください」「何か落ちていたら教えてください」は、返事を待っていません。

    正規表現の否定の後読みで外しました。

    (?<![らば])教えてください

    1つの返答が別レコードに分かれている

    Claude Code のトランスクリプトは、1つの返答が thinking と text の別レコードに分かれます。

    同じ message.id を遡って全文をつなげてから判定するようにしました。

    表示の調整:MacごとにP / Aの座布団バッジ

    僕は MacBook Pro と MacBook Air の2台で枠を動かしています。

    どちらの Mac で見ても MBP → Air の順に固定し、先頭に座布団つきの白抜き文字 P / A を置きました。

    区切り線はやめています。

    調整後のメニューバー。P(MBP)とA(Air)の座布団バッジ、Air Dev3は青のC(回答待ち)

    調整後のメニューバー。P(MBP)とA(Air)の座布団バッジ、Air Dev3は青のC(回答待ち)

    座布団はメニューバーの文字色で塗る

    座布団は、角丸の板をメニューバーの文字色(labelColor)で塗っています。

    文字は destinationOut のブレンドで抜き、その画像を NSTextAttachment で差し込みました。

    ただ、そのまま抜くとメニューバーの背景まで抜けてしまいます。

    そこで CGContext の beginTransparencyLayer の中で描いています。

    描画のたびに塗るので、壁紙が変わって文字色が白と黒で入れ替わっても追従します。

    ちなみに一度10ptに小さくしたら小さすぎたので、13ptに戻しました。

    誤判定2:「〜してください」は依頼とは限らない

    依頼の言い回しを足したら、今度は完了済みの MacBook Air Dev1 が回答待ち(青)になりました。

    原因は完了報告の「気をつけてほしいこと」にあった、次の一文です。

    「家や木など複数マスの物は…物全体を囲んで選んでください」

    僕への依頼に見えて、中身はツールの使い方の説明だったわけです。

    形まで見て数える

    次のように直しました。

    実ログ約470返答で見直すと、依頼だけで回答待ちになるのは約50件。

    どれも本当に返事待ちでした。

    正規表現で「依頼」を拾うときは、動詞だけ見ていると使い方の説明まで拾ってしまいます。

    「AかBを〜」「条件つきの〜たら」のような 文の形 まで見る必要があるということ。

    誤判定3:マージしたのに空きに戻らない

    次に来たのが「マージしたのに点(空き)に戻らない」という問題です。

    2枠を調べると、原因はそれぞれ別でした。

    MBP Dev2:fetchの間隔が長かった

    PR は10:37:53にマージ済みでした。

    ところが裏の git fetch が10:36に走ったばかりで、次の fetch は10:41。

    それまで手元の origin/develop が古く、紫(未マージ1コミット)のままでした。

    fetch の既定の間隔を5分から1分に縮めています。

    Air Dev2:git管理外のロックファイルが残っていた

    こちらは PR がマージ済みで、未マージのコミットもありません。

    それでも、社内ツールが作るロックファイル master-admin-sync.lock(git 管理外)が残っていて、「未コミット1件」で紫のままでした。

    ファイルの状態だけを見ていると、こういう副産物で誤判定します。

    マージした時刻で「完了」を判定する

    そこで「マージで完了した枠」を時刻で判定するようにしました。

    HEAD を base に取り込んだ最初のマージコミットの時刻を、次のコマンドで取ります。

    git log --ancestry-path --merges --reverse --format=%ct HEAD..origin/develop  
    

    その時刻が「最後に触ったAIの最後の返答」より後なら、マージで作業が完了したとみなして空きにします。

    Air Dev2 は、Codex の最後の返答が10:19、マージが10:20で条件を満たしました。

    マージ後にAIへ続きを頼めば、最後の返答がマージより後になるので、通常どおり判定されます。

    「マージしたけどまだタスクが残っている」ケースもこれで拾えます。

    しかも「マージしてよいですか?」で回答待ちになっていた枠も、マージ自体が返事なので空きに戻ります。

    最後の手段:メニューの「完了にする」で手で戻す

    それでも自動判定で戻らない枠は残ります。

    マージが不要だった作業や、判定しきれない返答などです。

    そこで、回答待ち・確認待ちの枠の下に「✓ 完了にする(空きに戻す)」を足しました。

    手動の完了もマージと同じ式に乗せる

    押した時刻を記録し、マージ時刻と同じ扱いにしています。

    完了時刻はマージ時刻と押した時刻の新しい方で、それがAIの最後の返答より後なら空き。

    自動の判定と手動の判定を同じ式に乗せたので、コードはほぼ増えていません。

    AIが続きを始めれば最後の返答が完了より後になるので、自然に通常の表示へ戻ります。

    取り消しの操作は作りませんでした。

    記録は枠があるMacに置く

    完了の記録は、枠がある Mac に置いています。

    MBP から Air の枠を完了にすると、判定スクリプト自体を SSH の標準入力で送り、--done を付けて実行して Air 側に書きます。

    どちらの Mac のメニューにも反映されます。

    学び:AIの状態は「止まったか」だけでは足りない

    振り返ると、最初の「作業中かどうか」だけの表示は、信号機に青と赤しか無いようなものでした。

    止まっている車が、信号待ちなのか駐車しているのかが分からない。

    AIのターンが終わっても、僕の返事待ちやレビュー待ちで枠はふさがっています。

    もう1つの学びは、自動判定には必ず取りこぼしが出るということです。

    質問の判定もマージの判定も、実ログで見直すたびに例外が出てきました。

    最後は人が「完了」と言える出口を作っておくと、判定の精度を上げ続けなくて済みます。

    まとめ

    まとめ

    ①AIのターンが終わっても、回答待ちと確認待ちの枠は空いていない

    ②回答待ちは返答全体の「?」の行と依頼の言い回しで判定する。依頼は「〜か選んで」「〜たら教えて」など文の形まで見る

    ③確認待ちはgit status・merge-base・merge-treeで判定し、origin/developは裏で1分ごとにfetchする

    ④マージ時刻か手動の完了時刻がAIの最後の返答より後なら空きに戻す

    AIを何本も並べて動かしていると、どの枠が本当に空いているのか分からなくなりがちですよね。

    色が2つ増えただけで、二重アサインの心配がだいぶ減りました。

    同じように複数のAIを並べて使っている人の参考になれば嬉しいです。

    オススメ記事
    検証環境