Skip to content

v0.32.0

Choose a tag to compare

@Kewton Kewton released this 07 Sep 10:15
· 24 commits to main since this release
ee5d2a4

[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_id NULL の 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 のループガード側)