Skip to content

v0.32.1

Choose a tag to compare

@Kewton Kewton released this 07 Sep 23:02
· 27 commits to main since this release
b7bc498

[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-hero 1 つで回る)、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_id NULL の行 — 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・compacted 4 回のターンの 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 も同じサーバへ向く。