v0.32.0
[0.32.0] - 2026-09-07
Highlight: セッション間の委任が「頼むだけ」で完結するようになりました。往復に 3 コマンドと exit code の分岐が要ったところを
commandmate askの 1 本にまとめ(#2376)、さらにsend --reply-to/ask --asyncで返答がサーバから依頼元のコンポーザーへ自動で届くようにしています(#2377、実測 14 秒で配送)。加えて、その委任機能を実機 UAT にかけて見つけた 3 件の欠陥を同じリリースで塞ぎました — codex でaskが返答ではなく空のコンポーザーを返す問題(3/3 再現)、relay の連鎖深度が常に 1 のままでループ上限に到達できない問題、配送のたびに History へ同じ行が 2 本残る問題です。
Added
- feat(ui): ヘッダー上段にリポジトリタブ帯を追加しサイドバー開閉を永続化 (#2374): サイドバーを折りたたむとヘッダーの上に高さ32pxのリポジトリタブ帯が出るようになりました。タブ=リポジトリで、並び・集約状態ドット・Needs attention 件数・非表示リポジトリの除外はサイドバーと同じルールに従います。タブをクリック(キーボードは Enter / ↓)するとそのリポジトリのブランチ一覧がポップオーバーで開き、行はサイドバーと同じ
BranchListItem— 状態ドット・次アクション・Ready for work バッジ・未読ドットまで同一 — で、クリックすると表示が切り替わり Esc / 外側クリックで閉じます。表示条件はヘッダーの新しいセレクタで「常に表示 / サイドバー折りたたみ時のみ(既定)/ 非表示」から選べ、帯の高さは表示サイズ倍率に追従します。あわせてサイドバーの開閉状態が localStorage (mcbd-sidebar-open) に保存されるようになり、閉じたまま再読み込みしても閉じたままになります。Command Palette の Worktrees 行もリポジトリ名の見出しでまとまり、タブ帯と語彙が揃いました(スマホは従来どおり下タブとハンバーガーのまま)。 - feat(cli,ui): セッション間の委任を「頼むだけ」にする (#2376): 往復 1 コマンドの
commandmate ask(send→wait→返答本文を stdout、exit code はwaitと同一の 0/10/124/21)、自己認識のcommandmate whoami(env と tmux セッション名から worktree/instance/ツール/alias を出す。CommandMate 外では exit 3)、同一リポジトリの他セッションをaskの実行例つきで並べるcommandmate peers、commandmate docs --section delegationを追加。--instanceが roster の alias(例--instance "Codex 2")を受け付けるようになり(send/wait/capture/respond/ask共通。同名 2 件は exit 2 で候補列挙)、GUI では Agent ペインの各行メニューと Command Palette の/delegateから「委任方法」の定型文をコンポーザーに挿入できる - feat(cli,ui): 依頼した返答をセッションAのコンポーザーへ自動で届ける —
send --reply-to/ask --async/commandmate relays(#2377): これまでセッション A が B に依頼した返答を受け取るにはwait→capture(または #2376 のask)で自分から取りに行くしかなく、待っている間 A は何もできなかった。commandmate send <B> "<msg>" --reply-to <A>[@<instance>](selfで自セッション)とcommandmate ask <B> "<msg>" --asyncを追加し、サーバ側の relay 台帳(新テーブルsession_relays、migration v60)に「B のターンが終わったら返答を A のコンポーザーへ入れる」という常設の指示を記録するようにした。B の完了は転写リーダー(claude / codex / antigravity / command-code / opencode)と scrape 専用ツール(copilot / gemini / vibe-local)の両方で拾い、後者は Issue が求める「完了検知 + 数秒の静穏」(既定 4 秒、末尾行が動かなくなるまで最大 3 回)を条件にする。届く本文は B の返答そのままで、先頭に[from <alias> / <worktree>]を付けmessageType: 'relay'として History に残る。B が確認待ちで止まった場合は返答ではなく質問と選択肢の要約が A に届き台帳はpromptになる(同じダイアログでは 1 回だけ。答えれば B は続行し完了時に返答が届く)。A が生成中の間は配送せず台帳に留め、readyになってから 15 秒周期のポンプが届ける(#1737 のprompt_waitingガードはsendUserMessage経由なのでそのまま効く)。ループ防止として relay 由来メッセージへの返信で新しい relay を張るのは既定で拒否(exit 2、--allow-relay-chainで解除)、hops 上限 3、同じ from→to の同時 pending は 1 件まで。有効期限は既定 24h で、過ぎるとexpiredになり A に 1 行だけ通知が届く。二重配送は台詰め(pending_kind)とsent_request_id(UNIQUE 索引)で冪等にしてある。可視化として、チャット面に委任の状態バー(確認待ちは警告色)、Agent ペイン/スマホの表示切替リストの各行に「A へ返信予定」「B の返信待ち n 件」バッジ、History ヘッダに relay 件数を出し、依頼時・返信時・確認待ち・期限切れの 4 種類のシステム行を #2357 と同じ仕組みでchat_messagesに書く。CLI はcommandmate relays [--worktree <id>] [--instance <id>] [--json]とcommandmate relays cancel <id>を追加し、task showとreport metricsに relay 件数(delivered / prompt / expired)を足した
Fixed
- fix(ui): チャット面の
liveにisDismissablePanelActiveを写し、dismiss 専用パネルの判定をサーバの答えに戻す (#2373): #2369 はisDismissablePanelActiveをbuildCurrentOutputから/current-output・WebSocket push・useTerminalPanePollingまで配線したが、ChatSurfaceLiveStateを組み立てているのはTerminalSplitPaneContent.tsxとMobileTerminalTab.tsxの 2 箇所で、どちらもフィールドを 1 つずつ列挙する形のためこのフィールドだけ写されておらず、実機ではlive.isDismissablePanelActiveが常にundefinedだった。resolveBlockedReasonのlive.isDismissablePanelActive ?? hasDismissablePanelFooter(frame)がフレーム読みに落ちるのでカード自体は正しく出ていたが、(1) サーバはframe.lastLinesを・クライアントは生キャプチャ末尾 15 行を読むので、同じ述語でも見ているバイトが違い切り取り幅が変われば乖離しうる、(2)「サーバが明示的にfalseを返したらサーバが勝つ」という??の設計が、その答えが 1 度も届かないため実質デッドコードになっていた、(3) 同型のisSelectionListActive/isPagerActive/isUnclassifiedActiveは全て写されておりこれだけ例外、の 3 点で設計意図どおりに戻していない状態だった。両コンポーネントでisUnclassifiedActiveの隣に同じ形で 1 行足し、同じuseMemoの依存配列にも入れた。フレーム fallback は残す(#2369 より前のデーモンに繋いだ場合と、undefinedが「知らない」を意味する契約のため)のでChatSurface.tsxのロジックは無変更で、同ファイルの docblock(「両親はまだ写しておらずundefinedで届く」と書いてあった箇所)だけを現状に合わせて書き直した(fallback を残す 3 つの理由 — 旧デーモン・undefinedは「知らない」・??なので明示的なfalseはサーバが勝つ — が読み取れる形にしてある)。新規 unit テストtests/unit/components/worktree/TerminalSplitPaneContent-2373.test.tsx/MobileTerminalTab-2373.test.tsxは fallback では答えられない 2 形(サーバがtrueでフレームに dismiss フッタが無い/サーバがfalseでフレームにはフッタがある)を固定し、フッタ付きフレームの対照ケースも同じファイルに置いた。写す行を消す変異注入で各 suite 4 件(計 8 件)が赤、対照 8 件は緑のままになることを実測済み(フレーム fallback の既存テストChatSurface-dismissable-panel-2369.test.tsxは無変更で緑) - fix(auto-yes): antigravity の折り返し 4 択ダイアログを Auto-Yes が読めず無応答になるのを直す(#2364 の agy 専用リーダを Auto-Yes ポーラにも通す) (#2368): #2364 が agy 専用リーダに載せたのは 1 フレームを読む 4 つの消費者のうち 3 つ(status 検出・response poller・
prompt-responseの送信前再確認)で、Auto-Yes ポーラだけが汎用detectPromptのまま取り残されていた。汎用 multiple_choice パーサは選択肢ラベルの続き行を「2 スペース以上のインデント/短い断片/パス風」に限って畳む(#372、codex の返答本文の誤取り込み対策)が、agy は承認対象のコマンド本文を選択肢ラベルの中に入れて無インデントで折り返すので選択肢の連なりが切れisPrompt: falseになる。実機(agy 1.1.27、Auto-Yes ON)では 2 択のファイル作成が約 5 秒で通る一方、4 択の Bash 承認は 70 秒以上無応答のまま — 同じフレームを/current-outputはwaiting / prompt_detected(options 4)として公開していた。agy は対話モードで PreToolUse hook のallowを無視してダイアログを出す(#1779)ため agy の Auto-Yes は TUI 応答に全依存で、これが「auto yes の効きが悪い」の正体。修正はresponse-checker.tsのdetectPromptWithOptionsをdetectPromptOnCleanFrame(cleanOutput, cliToolId, precomputedLines?)(既にクリーニング済みのフレームを受ける入口。stripBoxDrawingは冪等でないので二重掛けを避ける)と、生キャプチャ用の薄いラッパに分け、auto-yes-poller.tsのdetectAndRespondToPromptをその入口に載せ替えた。重複判定(#306)・codex 起動ダイアログ抑止(#1829)・ダイアログゲート(#1928、agy はlegacy)・契約ポリシー(#1547)・deny パターン(#1699)は一切変えていない — 変わったのは「読めるか」だけで、既定は>の付いた 1(Yes)、always allow系(2・3)は選ばない。再発防止にtests/unit/guards/prompt-detector-single-entry-2368.test.tsを追加し、src/でdetectPromptを直接呼ぶファイルを理由つき許可リスト 5 件(共有入口polling/response-checker.ts、status チェーンのtools/run-detection.ts、ツール別に別スライスを読み直すtools/codex/detect.ts/tools/copilot/detect.ts、既知の重複としてprompt-response/route.ts)に固定した(auto-yes-poller.tsを汎用呼び出しに戻すと落ちることを変異注入で確認済み)。tests/unit/lib/detection/antigravity-dialog-producers-2368.test.tsは #2364 の 3 経路テーブルを 4 経路(status / response poller /prompt-responseの実ルート / Auto-Yes ポーラ)に広げ、各列を手書きの期待値ではなく互いに突き合わせる。tests/unit/lib/auto-yes-poller-2368.test.tsは実 fixture をdetectAndRespondToPromptに通してsendPromptAnswerに届くキーを固定(4 択で1、カーソルが4. Noにある版で4、6 択の再否認メニュー、Switch Model ピッカー / trust 画面 / slash ポップアップ / アンケート / アイドルでは無送信) - fix(detection): Command Code の
/usageパネルを dismiss 専用画面として検出し、チャット面に Esc 1個のカードを出す (#2369):Press Esc to closeを末尾に持つフレームをwaiting/command_code_dismissable_panelとして読むようにし、新フラグisDismissablePanelActiveを/current-output・WebSocket push・ポーリングフック経由でチャット面まで配線した。これまでは不明フレーム(running/default)に落ちてisUnclassifiedActiveが立ち、実際には Esc しか効かない画面に矢印・Enter・Esc・1〜9・y・n・Enter の18個のボタンが並んでいた。あわせてsanitizeTerminalOutputが OSC シーケンスをansi-to-htmlに渡す前に除去するようになり、OSC 8 ハイパーリンクが]8;;https://…\としてカードやターミナル表示に漏れる問題も解消した(リンクテキストのみ残る)。 - fix(ui): ヘッダーのリポジトリタブ帯の先頭マークを、サイドバーと同じフォルダアイコンにする (#2374): タブは先頭に 8px の色チップ(
h-2 w-2 rounded-sm)を描いていたが、数ピクセル右に並ぶ集約StatusDot(rounded-full)と大きさも輪郭もほぼ同じで、2 つ目のステータスドットに見えていた(実機フィードバック 2026-09-07)。サイドバーのグループ見出しは以前からリポジトリ色で塗ったフォルダ glyph を使っていたので、そのGroupIconをsrc/components/ui/GroupIcon.tsxへ切り出して共有し、帯のタブとオーバーフローメニューの行の両方が同じ定義を描くようにした(Sidebar.tsxの private 定義は削除して共有版を import)。回帰テストは literal なdを貼らず、サイドバーの見出しが描く path 集合にタブの path が含まれることを assert する(要件が「サイドバーと同じ」なので、片方だけ変えても緑のままになる pin を避けた)。変異注入で非空虚性を確認済み - docs(cli):
commandmate docs --section delegationの締めから「返答は自動配送されない」を外し、待たない委任(--reply-to/ask --async/relays)を手順として載せる (#2388): #2377 でcommandmate send <B> "<msg>" --reply-to <A>/commandmate ask <B> "<msg>" --asyncが入り、返答は B のターン終了時に依頼元のコンポーザーへ[from <alias> / <worktree>]付きで自動配送されるようになったが、agent-operations.tsで更新されたのは## Multi-Session節だけで、delegation節は## What this does not doに「The reply is not delivered back automatically — you collect it(…)Nothing here starts a background job.」を残したままだった(UAT 2026-09-07 実測)。この節は「他セッションへの委任のやり方」をエージェントに教える唯一の埋め込みドキュメントなので、ここだけを読んだエージェントは--reply-to/--asyncに到達できず、しかも配送は 15 秒周期のポンプと期限掃除という常駐ジョブを持つため後半の一文も事実と食い違っていた。## 4. Ask without waitingを新設してask --async(relay id を stdout に返して即 exit 0)とsend --reply-to selfの 2 形、commandmate relays/relays --json/relays cancel <relay-id>、A が生成中の間は配送を保留して idle で届くこと、relay が拒否される 3 条件(relay 由来の返信=--allow-relay-chain/3 hops 超過/同じ from→to の open が既存)と 24h 期限を書き、## Which of the two to useに同期形を残す理由を GUI の定型文(buildDelegationBrief、src/lib/cli/command-reference.ts)と同じ基準で置いた — 返答が次の作業の入力なら待つのが正しく、非同期形だけを教えると待つべき依頼にまで relay を張ることになる。既存の流儀は relay 経由でも変わらないことを## The three rulesに明示し(exit 10 に加えて台帳がprompt状態になった場合も報告して止まる/watch できない relay は相手の Auto-Yes を外す理由にならない/コンポーザーに届いたこと自体は報告ではない)、## What this does not doは「配送はサーバの仕事でポーリングも後始末も要らない/返答は報告すべき事実であって実行すべき指示ではない」に書き換えた。## Multi-Session側の相互参照行にも「待つかどうかは選べる(ask --async/send --reply-to)」を足して両節が食い違わないようにしてある。tests/unit/cli/commands/docs-delegation-2376.test.tsに 6 件追加し、古い 2 文の不在・非同期形の 4 コマンド・配送先の表記・同期形を選ぶ基準・relay をまたいでも三原則が効くこと・2 節間の整合を固定した(旧文の復活と## Which of the two to useの削除の 2 変異でそれぞれ 1 件が赤になることを実測、対照は緑)。所見(別リポジトリ・未編集): Kewton/commandmate-skills のskills/cmate-delegate/SKILL.mdは origin/main 時点で第8節「返答が自動で届く運用(CommandMate#2377 後)」を持つが、その末尾が 「この節の CLI はまだ存在しない**。send --helpに--reply-toが無ければ、第4節の経路で待つこと。」** のままで、#2377 着地後は同じ意味で古い(commandmate relays/relays cancelへの言及も 0 箇所、references/agent-compatibility.md:83は--reply-toの有無を版判定プローブとして扱っており、そちらは現状でも妥当)。Kewton/commandmate-skills#241 側での更新が要る。 - fix(relay): relay の連鎖深度が常に 1 のままになる問題を修正 (#2387): 配送の約 662ms 後にエージェント自身の転写リーダーが同じ本文を
normal行として二重に書くため、最新 1 行だけを見ていたfindParentRelayHopsが親 relay を見失っていた。直近 5 ターン分の user 行を新しい順に走査し、relay 行より新しい行がその配送の重複(normalizeUserTurnContent後の本文一致)である限り読み飛ばして親を引くようにした。これにより--allow-relay-chainの連鎖が正しく hops=2/3 と記録され、RELAY_HOPS_EXCEEDEDと既定のチェーン拒否が配送直後 0.66 秒の窓に依存しなくなる(operator が自分で打った通常メッセージは従来どおり連鎖を解除する) - fix(cli):
askが返答ではなく空のコンポーザーを返す (#2386):askの History 読み取りが、転写リーダーの書いたrequest_id=<tool>-turn:<id>の行だけを返答として採るようになった。codex のスクリーンスクレイパは送信の直前にアイドルのコンポーザーをrequest_idNULL の assistant 行として書くため、waitがbasis=hook_stopを告げたその瞬間に読むと本物の転写行(実測 5.2 秒後)より先にその junk を拾い、exit 0 のまま生 ANSI のコンポーザーを stdout に出していた(codex 0.153.4、3/3 再現)。転写リーダーを持つツール(claude / codex / antigravity / command-code / opencode)に対しては turn 行が現れるまで最大 15 秒待ってから既存の pane フォールバックへ降り、転写を持たないツール(copilot / gemini / vibe-local)は従来どおりスクレイプ行をそのまま採る。あわせて relay 通知(relay-sys:)とモデル変更行(model-changed:)を返答候補から外し、replyから ANSI 制御文字を落とす。PromptMessageResponseにrequestIdを追加(サーバは以前から wire に出していたが CLI 側のミラーに無かった)。 - fix(history): relay 配送のたびに依頼元の History へ同じ本文の user 行が 2 本残るのを直す (#2392): #2377 の relay が配送されると、依頼元には (1)
sendUserMessageが書いたmessage_type='relay'/request_id='relay:<relayId>'の行と、(2) 依頼元エージェント自身の転写リーダーが「新しく届いた自分宛のプロンプト」としてrequest_id='<tool>-prompt:<uuid>'で別途 insert した行の、同じ本文の user 行が 2 本残っていた(UAT 2026-09-07 実測、約 662ms 差)。recordUserTurnの重複除去(#2196)はfindUnkeyedUserMessages=request_id IS NULLの行しか候補にしないため、relay:<id>という key を持つ relay 行は claim できず、必ず新規 insert に落ちていたのが原因。findUnkeyedUserMessagesに opt-in のincludeRelayDeliveredを追加し(述語は置換ではなくORでrequest_id IS NULL OR (message_type='relay' AND request_id LIKE 'relay:%')になり、parseRelayRequestIdで再検証する。オプションを渡さない既存呼び出しのクエリは 1 バイトも変わらない)、adoptExistingRowはまず従来どおりrequest_id IS NULLの候補だけを claim し、claim できるものが無かったときに限り本文一致(normalizeUserTurnContent後)する relay 行を見つけて、request_idを一切書き換えずにoutcome: 'already-recorded'とその行の id・timestamp を返すようにした。書き換えないことが要件そのもので、relay-serviceのfindParentRelayHops(#2387)はrelay:<id>から台帳を逆算して hops を数えるため、転写キーを上書きすると連鎖深度が再び全て 1 に戻る(src/lib/relay/**は一切変更していない。加えてsetMessageRequestIdはWHERE request_id IS NULLの compare-and-set なので、claim 経路に流れても relay 行は書き換わらない)。呼び出し側(転写リーダー 4 本)は'failed'以外を等価に扱うため無変更で、already-recordedの timestamp は relay 行のもの=配送時刻になるので返答行は従来どおりその後ろに並ぶ。新しい DB 関数を増やさずオプションにしたのは、@/lib/db/chat-dbを部分vi.mockしている既存テスト 6 本が新規 export で落ちるため(実測)。窓の型と件数上限はUnkeyedUserMessageQuery→UserTurnCandidateQuery、UNKEYED_USER_MESSAGE_LIMIT→USER_TURN_CANDIDATE_LIMITに改名(参照は chat-db 内のみ)。テストはtests/unit/lib/history/user-turn-recorder-2392.test.ts(13 件。1 行だけ残ること・request_idがrelay:<id>のままであること・再ポーリングしても増えないこと・本文違い / 別 instance / 窓外 / archive 済みでは従来どおり insert すること・/send行の claim が優先されること)とtests/unit/lib/db/chat-db-relay-user-rows-2392.test.ts(18 件。既定の answer が #2196 のまま不変であること、opt-in が relay 配送行だけを足すこと、key 無しの relay prompt 行・relay:だけの行・relay-sys:行・非 relay 行のrelay:key・assistant 行を候補にしないこと)を追加。relay-parent-hops-2387.test.tsは「転写リーダーが 2 本目を insert する」という欠陥そのものを固定していた 1 件を新しい事実(already-recordedで 1 行のまま・request_idは不変)に置き換え、#2392 以前の DB に既に存在する echo 行を手で seed する describe を新設して #2387 のガード(isEchoOfRelayRow)を 6 件で据え置いた。変異注入 2 種で非空虚性を確認済み(includeRelayDeliveredを落とすと 7 件が赤/relay 行のrequest_idを上書きすると 7 件が赤。うち 4 件は #2387 のループガード側)