Repository navigation
v0.32.1
[0.32.1] - 2026-09-08
Highlight: 委任と転写の 2 つの経路に残っていた「静かに壊れる」欠陥をまとめて塞ぎました。同一マシンで 2 つのサーバが 1 つの tmux サーバを共有すると、エージェント内の
commandmateが起動元とは別のサーバへ接続します(hooks は正しく、CLI だけが割れる)。askは exit 0 を返すので呼び出し側は気づけず、実測では委任が無関係な worktree へ流れました。起動行に起動サーバ自身のCM_PORTを pin して真因を塞ぎ(#2403)、万一同じ不一致が起きてもwhoami/instances/peersが接続先 URL つきで申告するようにしています(#2404)。あわせて転写の保存経路を 4 箇所直しました — Stop 受け口の自己待ち(実測 105/105 失敗、#2398)、飽和した codex ペインがフッタ 1 行に化ける件(#2400)、dedup の早期 return で転写リーダーが二度と呼ばれない件(#2399)、4 MiB 窓に頭が入らないターンが頭切れのまま「完成した返答」として残る件(#2402)です。
Added
- feat(demo-video): README 冒頭の 30 秒を UX 先行の
readme-hero絵コンテで撮る (#2381):record-scenes.tsにrepo-tab-switch/agent-tabs/delegate-ask/reply-file-link/mobile-approve/mobile-file-linkの 6 シーンを追加(sidebar 折りたたみ・チャット面・Agent ペインは localStorage seed、撮影前に warm-up、afterで撮影外検証)。絵コンテにclaude-cassette:(cut が claude ペインのカセットを宣言し--storyboard readme-hero1 つで回る)、gif: { width, fps, maxBytes }(600px / 10fps / 1.84MB のゲート。超過は palette 256→128→64 色で再試行し、それでも超えれば GIF を消して exit 1)、record シーンのhead: N(take の先頭 N 秒 + 末尾をジャンプカット)、telop.position: top(帯をヘッダー下へ。PNG に焼くので compose.sh は座標を持たない)を追加。fake-agent.shに@pass行(同一カセット内で次の指示を開始。{{TASK}}/{{EXEC_OUTPUT}}を空にする)を追加し、@input行はフレームを描いてから settle を読む(起動バナーが返答として保存される競合を閉じる)。claude-hero.cast(委任パス +@pass+ 承認パス)とtranscripts/claude-tests.jsonlを追加。env-up.shのSessionStartにmodel(スマホのセッション行に claude のモデルが出る)とCM_LAUNCHED_BY=commandmate-cli(画面の CLI 表記がcommandmateになる)を追加。public-messaging.md§5 / §6 に hero の行を追記しpublic-messaging.test.tsが 16 行を照合。 - feat(demo-video): 偽エージェントを 5 ツールに広げ、
@exec/@transcript/@hook行と隔離 HOME の偽転写でチャット面に Markdown の返答を出す (#2380):fake-agent.shに--tool <id>(mcbd-<tool>-<worktreeId>の導出と opencode 80×200 のジオメトリ)・--idle-only(起動画面の保持)・@exec(先頭語commandmateかつシェル演算子なしに限定、語分割の後に{{…}}を差し込む)・@transcript(再生時刻で転写ターンを追記)・@hook(UserPromptSubmit/Stopを実 CLI と同じ形で送り、wait/askの 60 秒保留を回避)を追加。env-up.shは$HOMEが実ログイン HOME なら何も書かずに exit 2 し、隔離 HOME に claude / codex の転写骨格を置いてSessionStartを告げ、seed に.commandmate/agents.yaml(5 体)とsrc/components/layout/Header.tsxを足す。env-down.shはツール名にハイフンを含むペイン(mcbd-command-code-…)も拾い、植えた転写だけを消す。demo-video.shは 5 バイナリを依存に加え 5 ペインを起こす。カセット 5 本(実キャプチャ由来、個人パスなし)と転写テンプレート 2 本を同梱し、隔離サーバでの往復(send→commandmate ask→ History にclaude-turn:/codex-turn:の Markdown 4 行)を実測で確認 - test(mobile): スマホの Agent ペインの「委任方法をコンポーザーに挿入」を実機経路で固定し、この面に delegate 実装が無いのは継承だと明記 (#2382): Issue は
MobileAgentInstancesPane.tsxにdelegatが 0 件であることから「スマホ Agent ペインには無い」と判断していたが、このペインは共有 roster エディタAgentInstancesPaneをそのまま埋め込んでおり、#2376 の行メニュー項目agent-instance-delegate-<id>・fetchDelegationBrief・insertIntoVisibleComposer・DELEGATE_TEXTはスマホでも最初から同じ 1 実装で動いていた(隔離 dev サーバ・390×844・Playwright で実測: Tools → Agent の Codex 行 → 挿入で下端に常駐するコンポーザーに PC と同じ定型文が入り、--instance codexは/resolve-targetの答えと一致、Terminal / Chat タブへ切り替えても保持される)。二重実装は Issue 自身が禁じているので項目は増やさず、tests/unit/components/worktree/MobileAgentInstancesPane.test.tsxにこのラッパー経由の 5 件(項目の存在と文言の一致/挿入と--instanceがサーバの答えであること/画面上のセッションの行の拒否と読み取り 0 件/別行は拒否されないこと/コンポーザー無しは例外でなくnoComposerトースト)を追加し、期待値 3 箇所の変異でそれぞれ赤になることを確認した。あわせて module コメントに、行アクションは共有ペイン由来でgrep delegatが 0 件なのは想定どおりであること、スマホではコンポーザーが全タブ下端に常駐するので「コンポーザー無し」は worktree 画面では起きないこと、「自分には委任しない」判定は chat surface のdata-instance-idを読むが Agent ペイン(Tools タブ)と chat surface は同時に表示されないためスマホでは不発(実機で Claude タブ表示中に Claude 行から挿入できることを確認、PC の terminal モードと同じ設計上の帰結)であることを書いた。未達 2 点(scope 外、#2395 で対応): (1) 受入条件「表示中の自分自身の行にはメニュー項目が出ない」は、メニューが共有ペインのもので、かつ下端コンポーザーの送り先activeInstanceIdがこのペインに届いていないため満たせない —NotesAndLogsPane経由でactiveInstanceIdを通し共有ペインに self 行を隠す prop を足すか、モバイルシェルがコンポーザーの送り先をdata-instance-id相当で公開する必要がある。(2)/delegateはスマホの worktree 画面でも同じ定型文を入れる(⌘K で開いて実測)が、/worktrees/*ではGlobalMobileNavが隠れHeaderのトリガは PC 限定なのでスマホにはパレットを開く画面上の入口が無く、入口のある Home からはnoComposerトーストになる。
Changed
- docs(readme): README 冒頭を UX の 30 秒に差し替え、契約 → 検証の GIF を lede 直下の新節へ下ろした (#2383): 冒頭の
demo-desktop.gif(cm-11-contract-verify.enの 0〜18 秒。最初の GUI フレームまで 18 秒かかり、ja README にも en テロップが出ていた)を #2381 のreadme-hero絵コンテの出力へ置き換え、ロケールごとに別ファイルを参照するようにした:README.md→docs/images/demo-hero.en.gif(1,406,883 バイト)、docs/ja/README.md→docs/images/demo-hero.ja.gif(1,552,144 バイト)。どちらも 600x375 / GIF89a / 300 フレーム / 30 秒で、絵コンテが宣言する 1.84MB 予算の内側(ffprobeとstatで実測)。動画は撮り直していない —~/Desktop/commandmate-demo/readme-hero/の承認済み出力をcmpで byte-identical にコピーしただけで、再エンコードしていない。altは映像の内容(タブ帯 → 5 エージェント → チャット面で委任 → 返答のリンクでファイル → スマホで同じこと)で書いた。契約 → 検証のカットは捨てず、docs/images/demo-desktop.gifをgit mvでdocs/images/demo-contract-verify.gifに改名して(内容は無変更、909,922 バイト)lede 直後に起こした新節へ移した。見出しと本文はdocs/design/public-messaging.md§3 のカード 2 から逐語コピー(en:## Verified, not vibe-checked+ "Gates you declared decide whether the work is done, and the exit code is the verdict."、ja:## 「たぶん動く」ではなく検証済み+ 「完了を決めるのはあなたが宣言したゲートで、判定は実 exit code である。」)で、言い換えていない。Issue 本文の「本文は §2 定義文から」は採らなかった — §2 の定義文は README:20 に既にあり、6 行下で同じ文を繰り返すことになるうえ、検証の節の本文として内容が合わない(契約で許された §2 / §3 のうち §3 を採った)。2 本目のdocs/images/demo-mobile.gifは hero にスマホが入ったのでgit rmし、README.md/docs/からの参照を 0 件にした(「Works on desktop and mobile…」の 1 文は残す)。git ls-files docs/images | grep demo-はdemo-contract-verify.gif/demo-hero.en.gif/demo-hero.ja.gifの 3 本。出荷する 2 本の hero から全 600 フレーム(en 300 / ja 300)を復号して tesseract で OCRし、個人パス(/Users/)・プライベート IP・旧製品名MyCodeBranchDesk・ユーザ名にヒット 0 件であることを実測したうえで、6 フレームずつの montage を目視して隔離 seed(cmdemo-app/cmdemo-docs/feature/demo-dark-mode/fix/demo-login-error)だけが映っていること、ja 版のテロップと UI が日本語であることを確認した。docs/en/features/product-highlights.mdとdocs/features/product-highlights.mdの出自段落は両方とも実ファイルに合わせて書き直し(hero はreadme-hero絵コンテ・ロケール別 2 本、契約節はcm-11の 0〜18 秒)、消えたスマホ GIF の切り抜き幅の知見(枠 370px ではなく帯の文字幅で決める。帯幅 594px で切るとスマホが 187px になり字が読めない)はファイル名を外して残した。public-messaging.test.tsの README ピン(hero / 定義文 / 禁止語 / ja・en の##同数)は弱めておらず、新節は en / ja に同時に入れたので##は 10 → 11 で揃っている。tests/unit/website/landing-page.test.tsはwebsite/しか歩かないので影響なし
Fixed
- fix(hooks): codex の Stop 受け口が転写の閉鎖を待たなくなり、落ちていた返答の Markdown 行が書かれるようになった (#2398): codex は Stop hook のコマンド(relay の同期
curl)が終了するまで rollout にtask_completeを書かないため、#2264 の「3 回 × 500ms 待って読み直す」は自分が塞いでいる追記を待つ自己待ちになっていた(実測 2026-09-07、3 日分のログで codex 105 回中 0 回成功。受け口の応答とtask_completeの差は 5 ターンで 3 / 16 / 59 / 61 / 63 ms、すべて応答の後)。captureTranscriptTurnOnStopは同期の読みを 1 回だけにし、false かつ転写が在るときは応答を返してから detach した遅延読み(STOP_TRANSCRIPT_DEFERRED_DELAYS_MS= 150 / 500 / 2000 / 5000 ms の有限回)へ切り替える。遅延読みもstructured-history-gateのcaptureStructuredHistoryTurnを通るので per-instance 直列化・冪等な書き込み・broadcastMessage・onRelayTurnCompletedはそのまま効く。あわせてAgentStopOutcomeに 3 値のstructuredHistoryOutcome(captured/deferred/unavailable)を足し、遅延読みの成否はstop-history-capture-deferred-capturedなどのログに出す(受け口は既に応答済みなので戻り値には出せない)。claude / antigravity / command-code は 1 回目で成功する(実測 506/508・9/9・1/1)ため分岐に入らず #2264 の挙動のまま。副次的に、codex のターン終了が毎回 1 秒遅れていた分がなくなる - fix(detection): 飽和した codex ペインの返答がフッタ1行に化けるのを直す (#2400): claude / copilot / opencode / command-code と同型の bottom-pinned chrome リーダー
findCodexChromeStartを codex にも用意し、composer 行(› Ask Codex to do anything)とその下のステータスバー(model · cwd)をcontentEndの外へ出した。捕捉窓が飽和(#1670)するとターンのアンカーが「最新のユーザープロンプトのエコー」に切り替わるが、下端から探して最初に当たる›が composer だったため、抽出開始がその次の行=ステータスバーになり、飽和したペインではどのターンでも assistant 行がgpt-6-astra xhigh · ~/share/work/…の1行だけになっていた。内容が毎ターン同一なのでisDuplicateResponseがそこで永久ロックし、以降そのペインは返答を1件も記録できなかった。composer の判別は #2310 の SGR 規則(composer は bold グリフ、確定したエコーは dim グリフ、ダイアログの選択行は着色グリフか bold ラベル)で行い、旧実装の placeholder 文字列リスト(Implement/Find and fix/Type/Summarize。codex 0.1x の文言で 0.15x では1つも一致しなかった)は撤去した。送信直後のエコーは bold グリフで composer と区別できないことを 0.153.4 実機で計測したため、findCodexUserEchoIndexは属性に加えて位置(composer はフレーム最下段の›)で判定する。 - fix(relay): relay がワーカーの返答として scraper の junk 行を配ってしまう穴を塞ぐ (#2401): #2377 の返答選択
findWorkerReplyは「最新の非 prompt な assistant 行」を出自を見ずに採っていたため、転写リーダーを持つツール(claude / codex / antigravity / command-code / opencode)でも、スクレイパが書いたrequest_idNULL の行 — codex のフッタ 1 行、部分 scrape、送信直前にフラッシュされる生 ANSI ダンプ(#2398 / #2400 で実測) — が本物のcodex-turn:行より新しければ、それが「相手の返答」として依頼元エージェントのコンポーザーに配送され得た(ログ上の配達は 0 件で未観測、コードの読みによる latent bug)。#2386 が同型の穴をaskで塞いだ判定と同じもの(/-turn:/の shape マッチ。CLI 層への逆依存を作らないため正規表現は lib 側に複製し、5 ツールの集合はCLI_TOOL_IDS網羅のRecordにして 9 個目のツールがtscで落ちるようにした)をfindWorkerReplyに入れ、転写を持つツールでは turn 行だけを候補にするようにした。転写を持たないツール(copilot / gemini / vibe-local)はスクレイプ行が唯一の記録なので従来どおり採り、既存の静穏窓(RELAY_SCRAPE_QUIET_MS× 最大 3 回)もそちら専用になった。転写を持つツールの scrape 完了エッジ(settled=false)は、junk を配る代わりに転写行が着地するまで 15 秒/1 秒間隔で待ち(#2386 が codex 0.153.4 で実測した 5.2 秒のずれの 3 倍。askと同じ窓)、猶予が尽きたら何も配らずに戻る — relay は常設の指示なので転写リーダー自身のsettled通知か次の完了で配送でき、それも来なければ依頼元には期限切れ通知が届く(フッタ 1 行を返答として渡すより安全な側)。あわせてfindWorkerReplyの furniture 除外をrelay-sys:だけからモデル変更行(model-changed:、#2357)にも広げ、askと同じ 2 名前空間を飛ばすようにした。テストはtests/unit/lib/relay/relay-delivery.test.tsを 38 件に増やし(junk が本物より新しい fixture で本物が配られること・本物が無ければ何も配られず relay が open のまま残ること・猶予中に着地した転写行は配られること・待ち続けずに必ず resolve すること・oc-turn:も候補になること)、既存の prompt 行 / relay システム行の除外テストは転写を持つツールでは新フィルタに吸収されて空虚になるため scrape 専用ツール(gemini)側へ移して load-bearing に保った。変異注入 3 種で非空虚性を確認済み(provenance フィルタを外すと 6 件が赤/猶予の分岐を外すと 1 件が赤/model-changed:の除外を外すと 1 件が赤) - fix(polling): dedup で止まった tick でも転写リーダーを再照会する (#2399): scrape の内容が前回と同一だと
checkForResponseがduplicate-response-skippedで return し、その下にある転写リーダー(claude / codex / antigravity / command-code)が 30 分のMAX_POLLING_DURATIONのあいだ二度と呼ばれなかった。「画面が静止した」は転写にとって「まだターンが閉じていない」なので、リーダーへの唯一の ask が構造的に早すぎ、Stop hook が失敗したターンの Markdown 行が永久に落ちていた(実測 2026-09-07 codex:task_completeは ask の 1.8 秒後)。skip の内側からリーダーを再照会するようにし(最初の重複 tick で 1 回、以後 3 tick に 1 回。STRUCTURED_HISTORY_RECHECK_TICK_INTERVAL)、書けたらstructured-history-recheck-capturedを出して終える。先に保存された scrape 行は残す(skip 地点からその行を同定できず、archivedは #168 の「操作者が History を消した」印であるため)。 - fix(cli):
whoami/instances/peersに「起動したサーバとは別のサーバに繋いでいる」ことを申告させる (#2404): tmux セッション名から導いた worktree が接続先サーバの一覧に無いとき、whoami --jsonがserverMismatch: trueと接続先serverUrlを返し、人間向け出力には stderr で警告を出す(exit code は 0 のまま。サーバ一覧が読めないときは不一致と判定しない)。instances/peersの 404 はResource not found. Check the worktree ID. (server: <url>)と接続先を必ず名乗り、自セッションの worktree が一覧に無いときは「id ではなくサーバが違う」1 行を添える。peers --jsonにもserverUrlを追加(selfは不変)。従来はこの矛盾に誰も名前を付けられず、askが exit 0 のまま無関係な worktree へ委任していた(#2403) - fix(hooks): codex の転写リーダーが窓外に始まったターンを無言で頭切れのまま書くのを直す (#2402): 4 MiB の転写窓より長いターンでは
task_startedが窓の外に落ちるが、buildCodexTurnsは開始レコードの有無を見ておらず、頭が欠けた本文が完成した返答として履歴に残っていた(30 MB / 7,408 行の rollout で実測。7 時間・387 items・compacted4 回のターンのtask_startedと最初の 2 プロンプトが窓外)。CodexTurnAccumulator.startedをclosedと対になる開始側の印として追加し、印の無いターンは本文の先頭に_(head truncated)_を付けて書き、窓が実測できた事実だけをcodex-transcript-turn-headlessに載せる。turn_idが全item_completedに載る codex は窓外でも正しい key が読めるため、claude(collectHeadlessClaudeTurnは key を捏造できず書かない)とは逆に書く方を採った。窓外の steer プロンプトは本文照合ができず誤 adopt が永久に残るため adopt しない(理由はdocs/design/codex-transcript-reader.md§4.5)。転写窓 4 MiB は据え置き。 - fix(hooks): エージェント起動行に起動サーバ自身の
CM_PORTを pin する (#2403): 同一マシンの 2 サーバが tmux サーバを共有すると、tmux のグローバル環境には先に起動したサーバのCM_PORTが載り、pane の-zshがそれを継ぐ。loadClientEnv()の precedence はprocess.env>~/.commandmate/.env(#1743 の仕様)なので、60301 で起動したサーバのエージェント内で打ったcommandmate ls/instancesが 3000 のサーバに接続していた(hooks は 60301 で正しく、CLI だけが割れる。askは exit 0 で返るため呼び出し側は気づけない)。renderAgentLaunchCommand()がgetServerPort()(hook URL と同一の出どころ)を全プランの末尾に付けるようになり、7 ソースすべての起動行がCM_PORT='<起動したサーバのポート>'を載せる。CM_PORTはAGENT_LAUNCH_CONFIG_ENV_VARS側(CODEX_HOMEと同じ、子プロセスから strip しない)に置いたので、エージェントが起こした孫プロセスのcommandmateも同じサーバへ向く。