Skip to content

v0.34.0

Choose a tag to compare

@Kewton Kewton released this 12 Sep 01:27
· 20 commits to main since this release
7d20d46

[0.34.0] - 2026-09-12

Highlight: 委任まわりの検出と CLI の信頼性を立て直したリリース。Claude の AskUserQuestion は、選択肢に preview が付くと commandmate respond が prompt_no_longer_active で拒否され、複数質問のタブを1 問も進められなかった(#2486)。食い違う --instance <tool> と --agent <別tool> は送信前に止まらず、ad-hoc セッションへ本文が届いて ask が待ち先を見失っていた(#2487)。どちらも隔離環境の実機 UAT で修正前後を対照して確認している(25 件中 24 PASS・不合格 0、dev-reports/uat/2486-2487/)。

Added

  • feat(chat): 保存済み assistant 行が連続するとき、ターンの境目を時刻の区切りで示すようにした (#2458): recordClaudeUserTurn は task-notification などで開いたターンの user 行を書かないため、後続ターンの返答が見出し無しで前の返答に連結され、「いつ何が終わったのか」が読めなくなっていた(4 つの返答が 1 つの Assistant 18:33 の下に並ぶ)。buildChatTranscriptRows が行の request_id に載っているターンキー(AGENT_MARKDOWN_REQUEST_ID_PREFIXES の 5 prefix、prefix 込みの文字列全体で比較)を読み、キーが変わった assistant 連続行にだけ時刻のみの薄い区切りを出す。role ラベルは繰り返さない(showHeader は従来どおり role 見出し専用の boolean のままで、新しい ChatRowHeader(variant: 'none' | 'role' | 'time'/startedAtMs)を別 prop として ChatMessageBubble に渡す)。時間差ではなくキーで切るのは、長いターンが数分かけて複数行を書くため gap 規則では 1 つの返答が割れるから。キーを持たない行(pane scrape・req_…・承認ダイアログ)は区切りを作らないし run を終わらせない — 直近の既知キーを保持するので [A, 未知, A] は 1 ターン、[A, 未知, B] は 2 ターンになる。開始側の時刻は「対応する user 行が保存されている」ことが示せるときだけ併記する(18:18 → 18:33、示せなければ終了時刻のみ 18:59): claude / antigravity / command-code は turn 行と prompt 行が同一 id なので correlatedPromptRequestId() で引けるが、codex は turn_id と UserMessage の item id が別物、opencode は oc-prompt: 行自体が無いため区切りだけ出して開始時刻を推定しない。同一 scope(cliToolId + instanceId)・同一表示 segment に一意な非 archived / 非 optimistic の user 行があり、その時刻が返答より後でない場合のみ採用し、直前の user 行や groupMessagesIntoPairs での穴埋めはしない(#2445 の current/previous 境界は別 buildChatTranscriptRows 呼び出しなので、ターン記憶も時刻対応もまたがらない)。書式は新設の formatChatTurnTime(endedAt, startedAt, now)(src/lib/date-utils.ts、formatSessionNoteTimestamp と同じくロケール非依存の 24 時制。両端が参照日なら HH:mm、日を跨ぐ/参照日でないなら両端とも M/d HH:mm、同一分に丸まって両側同じ表示になるときは 1 回だけ、無効な Date は空文字で Invalid Date を出さない)。見出しはスクロール内容の内側の 1 行なので固定 footer の高さは増えず、仮想化の row key は message ID のまま(区切りは行ではなくメッセージ行の内側に描かれるので messageRowIndexById の index はずれない)。DB の timestamp・並び順・origin.kind 保存・History の ConversationPairCard は変更していない。
  • feat(chat): チャット面の AskUserQuestion を「質問」として承認と区別する (#2460): 保存済み prompt 行を承認 / 質問 / 送信確認に分類し、質問ラベルから picker のタブ行(← ☐ … ✔ Submit →)を除去。同じ質問セットの送信確認は 0〜5,000ms(両端含む)・同一ペイン・同一 prompt run・review の質問文一致という条件でその質問へ畳み込み、件数を増やさず確認者を回答者と別情報として表示する(Auto-Yes が答えた質問が「この画面で応答」と表示されなくなる)。サマリと開閉 aria-label は承認数・質問数・独立確認数を個別に数える。

Changed

  • chore(catalog): codex 0.154.0 が追加した /worktree をスラッシュコマンドカタログに反映 (#2497): codex-rs/tui/src/slash_command.rs を tag rust-v0.154.0 で読み直した。enum SlashCommand は 60 variant で、内部用の debug-m-drop / debug-m-update(description が "DO NOT USE")を除く 58 個が active。rust-v0.153.4 からの差分は Worktree の追加 1 件だけで、カタログに codex の /worktree 行を足し、src/config/slash-commands-attestations.json の codex を version 0.154.0 / observedAt 2026-09-12 / 58 コマンドへ更新した(カタログと attestation は必ず同じコミットで動かす。片方だけ進むと tests/unit/lib/standard-commands.test.ts の pin が「catalog にあるのに attested でない」で赤になる — これは審査ゲートであって欠陥ではない)。説明文は codex(新しい worktree で会話を開始・再開する)と command-code(git worktree の作成・一覧・切り替え)で別物なので、slashCommands.descriptions.worktree を memory / logout と同じツール別キーに分割した。

Fixed

  • fix(detection): Claude Code の AskUserQuestion で選択肢に preview が付くと commandmate respond が prompt_no_longer_active で拒否される問題を修正 (#2486): Claude Code 2.1.268 は選択肢に preview が付くと選択肢の右に枠を開く(1 番の選択肢の行末に ┌─…─┐、その下に │ … │ の行と └─…─┘、さらに枠の字下げで Notes: press n to add notes)。この約 20 行が選択肢と footer の間に入るため、detectClaudeDialog の選択肢ブロック探索(footer から非空 8 行まで遡る)が選択肢に届かずダイアログを保証できず、/prompt-response は開いている picker を prompt_no_longer_active で拒否していた(同じ画面で wait --on-prompt agent は detectPrompt を根拠に exit 10 を返すので両者が食い違う)。実機(2.1.268・200x1000・私設 tmux)で Issue の入力を 1 要素ずつ変えて採取した結果(tests/fixtures/claude-live-2486/)、拒否の原因は preview 枠で、タブ行(← ☐ 権限モード ☐ Goal/範囲 ✔ Submit →)単独では拒否は起きず、タブ行とその上の直前ツールの行が question / approvalTarget の先頭に混ざっていた。新設の leaf tools/claude/picker-chrome.ts が枠を構造(1 番の選択肢の行末の上辺・同じ幅の下辺・その下の picker footer・各行の最初の │)で見つけ、findClaudeChrome() がタスクパネル・HUD と同じ 1 つの chrome として detectPrompt の Pass 2・findClaudeTranscriptTail・detectClaudeDialog に共有させる(枠と行を共有する選択肢行は枠の手前で切り、折り返した (Recommended) はラベルに戻す。枠の中の 1. / 2. を選択肢と読んで Merge / Pray を公開していた誤読も解消)。タブ行は上方走査の上端として question / approvalTarget / ダイアログ種別の文脈を切り、表示層 chat-tool-approvals.ts の ASK_USER_QUESTION_TAB_ROW(#2460)は同じ leaf から import する 1 定義になった。picker の footer が出ているのに枠の形を検証できないフレーム(描画途中など)は prompt_no_longer_active ではなく新しい reason unsupported_dialog_layout(メッセージ付き・送信前に拒否)で返し、respond は Answer was not sent と表示する(exit 99 は据え置き)。Auto-Yes の方針は不変(単一質問と同じく強調中の選択肢に答える)。npm run canary に claude の 4 シナリオ(askuserquestion-tabs / askuserquestion-preview / askuserquestion-tabs-preview / askuserquestion-respond-walk)を追加し、最後の 1 本は $TMUX を私設ソケットへ向けて転送を assert したうえで /prompt-response と同じ検証列と本番の sendPromptAnswer を使い、1 問目 → 2 問目 → Submit まで答え切ることを実機で確認した。
  • fix(session): 登録行の無い --instance <tool> に別ツールの --agent を重ねたとき、食い違いとして止めずに ad-hoc セッションへ送っていた問題を修正 (#2487): resolveSessionTarget() は roster に登録行の無いインスタンスでは明示指定(--agent / ?cliTool / body.cliToolId)を無条件に採用していたため、#868 の primary anchor(インスタンス id が CLI tool id そのものなら、それはそのツールの主インスタンス)が明示指定の付いた瞬間に無効化されていた。登録行の無い worktree で ask <wt> "…" --instance antigravity --agent command-code を実行すると、exit 2 で止まらず ad-hoc の mcbd-command-code-<wt>-antigravity が立ってそこへ本文が届き、待機は agy(instanceId=antigravity の主インスタンス)を見続けて「the agent has not started this turn yet」で exit 124 になっていた(UAT 2026-09-11 / TC-79-4B 実測)。登録行が無くてもインスタンス id が CLI tool id で要求ツールがそれと違うときは、主インスタンス(resolvedBy: 'primary')に解決し、roster の食い違いと同じ形の conflict(rosterCliTool にはインスタンス id 自身、判別用に primaryAnchor: true)を載せる。strict 呼び出し元は既存の経路のまま拒否に倒れる — resolveSessionTargetStrict() を使う副作用系 route(kill-session / terminal / auto-yes POST など)は 400 instance_tool_conflict、CLI の ask / send / respond は resolve-target の応答を受けて送信前に exit 2。読み取り系(current-output / capture)は主インスタンスを読み、conflict を payload・警告に出す。食い違いの文言はサーバ側・CLI 側とも、primary anchor のときは「registered as」「roster entry を更新」「re-register」を言わず、is the primary instance of <tool> と「--agent を外す/--agent <tool> を渡す/別の インスタンスを指定(例 --instance <requested>)」を示す(roster の食い違いの文言と payload は不変)。CLI の client-fallback(resolve-target を持たない旧サーバ向けの手元解決)も同じ食い違いを同じ conflict(primaryAnchor: true)で検出して exit 2 にする — 検出だけで primary anchor 段そのものは足さない(--agent 無しの未登録ツール名 id は従来どおり未解決のまま旧サーバに任せる、DR2-008)。tool id ではない ad-hoc id は従来どおり — codex-3 などに --agent を付けた場合は明示指定が唯一の宣言として採用され(サーバでは resolvedBy: 'explicit'、client-fallback でも conflict なし)、send --agent … --instance <ad-hoc id> --register の経路は変わらない。--agent <tool> 単独の ask(#2479、selector と要求ツールが一致)も従来どおり explicit。ツール名の id を別ツールで登録しようとする send --instance codex --agent claude --register は、mcbd-claude-<wt>-codex(#1629 の形)を作る前に同じく exit 2 で止まる。
  • fix(ui): worktree 詳細画面のヘッダーが幅に収まらないとき、右端の表示サイズ切替・Info ボタンが画面外に押し出されて切れないようにした (#2481): サイドバーを開いた 1470px(MacBook Air 13" 相当)でヘッダーの中身が 178px はみ出し、親の overflow-hidden に黙って切られて表示サイズ切替と Info ボタンに到達できなかった。右側の操作群(End・状態セレクト・表示サイズ・Info)は縮めず、左側(名前・リポジトリ・ブランチ・検証チップ)が省略表示で幅を譲る。それでも足りない分は、ラベル付きエージェントピルを幅に応じて 1 本ずつ既存の「+N」メニューへ畳んで作る(上限は従来どおり 4 本。ResizeObserver でサイドバー開閉・ウィンドウ幅の変化に追従し、広がれば戻る。アクティブなインスタンスを優先して残す順位と awaitingInstruction バッジは不変)。畳むピルが尽きた極端に狭い幅では左側を横方向にクリップし、右側の操作には重ねない。
  • fix(antigravity): agy 1.2.1 でツールを使ったターンの後も、同じセッションへ send / ask できるようにする (#2478): 送信前の準備判定が ? for shortcuts を必須にしていたため、agy 1.2.1 がツールを使ったターンの後にこの語を描かなくなると、そのセッションへの以後の send / ask がすべて 15 秒後に exit 99(Antigravity prompt not ready)で落ちていた。判定を状態検出(detect.ts)と同じ根拠 — 選択リスト・trust ダイアログが出ていないこと、罫線に挟まれた > の入力欄が画面の末尾にあること、生成中の表示(スピナー・Generating・esc to cancel)が無いこと — で読むようにした。? for shortcuts は十分条件として残る
  • fix(ui): マウスを接続した macOS でリポジトリタブ帯に横スクロールバーが常時表示され、タブが帯の上半分に押し込まれる問題を修正 (#2480): サイドバーを閉じると最上段に出るリポジトリタブ帯(#2374)の nav は overflow-x-auto だけでスクロールバーの表示を OS 任せにしていたため、スクロールバーを常時表示する macOS(マウス類の接続中、または「常に表示」設定)では 15px のバーが 32px の帯を占め、タブが 16px に潰れていた。scrollbar-hide でバーを消し(タブは帯いっぱいの 31px に戻る)、バーを失うマウス利用者のために縦ホイールを横スクロールへ変換する onWheel を追加した(横成分が勝る入力・Shift+ホイール・Ctrl+ホイール=ピンチズームはブラウザに任せるので、トラックパッドの横スワイプは従来どおり。行単位で報告するブラウザの delta はピクセルへ換算)。スクロール位置を示すものが無くなるため、表示中の worktree のリポジトリのタブを初回表示・画面遷移の後・「…」ボタンの出現で帯が狭まったときに scrollIntoView({ block: 'nearest', inline: 'nearest' }) で帯の中へ出すようにした(従来は「…」メニューから選んだときだけ)。
  • fix(cli): ask --agent <tool> を --instance なしで使うと、送信は <tool> 宛てなのに完了待ちと返答の読み出しが worktree 既定のインスタンスを見ていた問題を修正 (#2479): --agent だけの呼び出しでもツール id を selector として一度だけ解決し(ロスターに同じ id の行があればその行、無ければその tool の主インスタンス)、送信・待機・返答の読み出しを同じインスタンスに揃えた。既定の claude が未起動の worktree では、本文が command-code に届いた3秒後に Not started: ... no running claude session (resolvedBy=worktree-default) の exit 21 で終わっていた(UAT 実測。--instance command-code 併記なら exit 0 で返答が返っていた)。既定の tool が起動している worktree では、依頼した tool ではなく既定の tool の状態で完了を判定していた。--instance を指定したとき・--instance も --agent も無いときの挙動は変わらない。
  • fix(cli): send/ask の長い本文が Claude Code に末尾だけ届き、それでも Message sent. を返していた問題を修正 (#2464): 512 バイトを超える本文は tmux send-keys -l ではなく load-buffer + paste-buffer -p -r -d(1 回の bracketed paste)で流し、Enter は composer が本文全体を示すまで押さないようにした。Claude Code 2.1.268 は bracketed でない入力を pty の読み取り単位(macOS で 1,022 バイト)ごとに組み立てて最後の 1 回分しか残さないため、1 KB の brief は末尾 2 バイト、4 KB は 8 バイトだけが届いていた(clear を切っても Enter を 3 秒遅らせても同じ)。本文が composer に揃わなかったときは送信せずに取り除き、send / ask は exit 99(Message body did not arrive intact)で失敗する。あわせて送信後の読み戻しが 1000 行ペインの末尾の空行だけを見ており、上から描画する codex / Command Code / Antigravity の composer を一度も読めていなかった点を修正。48 KiB / 240 行までを claude / codex / Command Code / Antigravity の転写とのバイト一致で確認し、send --help と commandmate docs --section delegation に明記した。
  • fix(scripts): stop.sh / stop-server.sh がポートに接続しているだけのプロセス(CommandMate を開いているブラウザの network service など)まで kill する問題を修正 (#2473): lsof -ti:<port> はローカル側・リモート側どちらかのポートが一致するソケットをすべて返すため、:3000 で待ち受けるサーバに加えて、そこへ接続しているだけのクライアント(Chrome の network service=CommandMate 以外のタブの通信ごと、他セッションの commandmate wait、hook 中継の curl)まで SIGTERM→2 秒後 SIGKILL の対象になり、ログには PID しか残っていなかった。ポート→PID 検索を共有ヘルパー scripts/lib/port-pids.sh の find_listen_pids_by_port(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t。lsof が無い環境の ss+fuser 代替経路は据え置き)に集約し、stop.sh / stop-server.sh / start.sh / build-and-start.sh / status.sh / health-check.sh をこれに寄せた。停止前には各対象を Stopping <pid> (<コマンドライン>) の形で記録する(print_port_targets)。/rebuild スキル・/uat コマンド・DEPLOYMENT / cli-setup-guide / TESTING_GUIDE(ja/en)の lsof -ti:3000 | xargs kill -9 系も LISTEN 限定の形に置き換えた。静的ガード tests/unit/config/port-kill-listen-only-2473.test.ts(LISTEN を絞らない lsof ポート検索を kill につなぐ行を scripts / docs / .claude から締め出す)と、実プロセス(待ち受けサーバ+接続し続けるクライアント)で helper と両 stop スクリプトを検証する tests/unit/scripts/port-pids-listen-only-2473.test.ts(-sTCP:LISTEN を外す変異で赤になる)を追加
  • fix(cli): 相手セッションの Auto-Yes が答えようとしている prompt で wait / ask が即 exit 10 を返し、委任が止まる問題を修正 (#2463): --on-prompt agent(既定)の prompt 分岐が autoYes.enabled を読まず、相手側で自動承認される権限ダイアログ(2026-09-09 実測: Command Code 1.51.3 → Antigravity の git log && npm test)でも即 exit 10 を返していたため、「exit 10 は答えずに人へ返す」規律の委任元がそのたびに止まっていた。push 通知ゲート decidePromptPush() の auto-yes-answering にあたる場合(Auto-Yes 有効かつ policy suppression が今回の prompt のものでない)だけ、猶予窓(既定 30 秒、--auto-yes-grace <seconds> で変更、0 で従来どおり即 exit 10)の間ポーリングを続け、prompt が消えれば通常の完了判定へ戻る(次の prompt は新しい窓)。窓を過ぎても prompt が残る場合と、窓の中で policy が回答を withheld した場合は従来どおり exit 10(後者は autoYesSuppression 付き)。猶予中は stderr に 1 行出すだけで stdout には何も出さない。--timeout / --stall-timeout が窓の途中で切れる場合は 124 ではなく exit 10 で返す。ask は pollWorktree を共有するので同じ挙動(フラグ無し=30 秒)、--on-prompt human は不変。wait --help と commandmate docs(wait 節・delegation 節)に説明を追加。
  • fix(verify): 契約付き委任で、委任そのものが起動したエージェントセッションを env-clean が「自分のセッションを残した」と誤判定し、作業に問題の無いワーカーが毎回 exit 20 になる問題を修正 (#2472): send --contract は task(= env-clean のベースライン)を作ってからメッセージを送り、セッションが無い worktree ではその送信がセッションを作るため、ベースラインに無い mcbd-<cli>-<wt> が必ず 1 本増えていた(#2470、/orchestrate 2456〜2460 の 5 本すべて。1 回目の送信が失敗してセッションが先に在った #2468 だけが PASS)。ベースライン保存時に task 行の (worktreeId, cliToolId, instanceId) から resolveSessionName() でそのタスクのセッション名を求めて snapshot に taskSession として記録し、tmux-sessions の追加のうちその名前と完全一致する 1 本だけを免除する。同じ worktree の別セッション(mcbd-codex-<wt>、別インスタンスの mcbd-claude-<wt>-2 等)の追加と、タスク自身のセッションを含むすべての消失は従来どおり違反。免除したセッションは + <name> [task session, excused] として出力に残し(件数・status からは除く)、ヘッダに task-session= を出す。キーの無い #2472 以前のベースラインでは何も免除しない(判定は従来どおり、ヘッダは task-session=unrecorded)。
  • fix(history): 4 MiB を超える Claude の長いターンの応答が保存されず、チャット面・History に出ない問題を修正 (#2470): 転写リーダーは末尾 CLAUDE_TRANSCRIPT_TAIL_BYTES(4 MiB)だけを読み、ターンは prompt 記録でしか開かないため、prompt が窓の外に出たターン(直近 14 日・1,663 ターン中 19 件=1.1%、最大 23.72 MiB。すべて人間起点の長い orchestrate / UAT)は claude-transcript-no-turn で false になり、Stop フック経路にはフォールバックが無く、poller も再起動や 30 分上限で居なくなるため、応答が DB に一行も書かれなかった。窓に prompt 記録が無く orphan の assistant 記録だけがあるときに限り、窓を 8 → 16 → 32 → 64 MiB と倍々に広げ(前回読んだ範囲の手前だけを追加で読む)、prompt 記録が入った時点で通常どおり user 行・assistant 行を書く(ログ claude-transcript-window-extended に fromBytes / toBytes / promptOffsetFromEnd)。64 MiB でも prompt 記録に届かないターンは、読める範囲の末尾を、先頭に「先頭が欠けている」旨の 1 行を置いた assistant 行として保存する(user 行は書かない。マーカーは英語のプレーンな Markdown — 本文は保存時点で固定され、書き込む Stop フックにはロケールが無いため、既存の _(truncated)_ と同じ方式)。キーは claude-turn:partial:<sessionId>:<ターンを閉じた最後の assistant 記録の uuid> で、ライブ表示用のセッション単位キー claude-turn:partial:<sessionId> とは別物(同じセッションで 2 回起きても 2 行になる)。窓の先頭の記録をキーにしないのは、終了後の追記(turn_duration / stop_hook_summary 等)で窓の先頭が動くため(2026-09-11 実測: 298 転写・閉じたターン 1,973 件で、閉じた後に次の prompt より前へ assistant 記録が来た例は 0)。未終了のターンは広げた後も従来どおり書かず not_yet_closed を返す(#2264 / #2436)。ファイル全体を読んでも prompt 記録が無いときは従来どおり scrape に任せる。窓内に prompt 記録があるケースは読み方も結果もバイト単位で不変。claude-transcript-no-turn は warn に上げ、orphan があるときは読んだバイト数と size を併記する。実行中のライブ表示(readClaudeTurnProgress)は毎秒走るため窓を広げず、従来どおり partial で表示する。
  • fix(detection): Claude Code 2.1.267 の files-edited HUD 行を AskUserQuestion 確認画面の footer と誤読し、開いているダイアログに答えられなくなる問題を修正 (#2468): 2.1.267 は画面最下段に No changes this session / +N files edited before this session (show) を右寄せで常駐描画する。findClaudeTranscriptTail がこの行を転写末尾として返し、detectClaudeDialog がそれを選択肢の下の footer と読んで footer 判定(Esc to cancel / Enter to select 等以外は不成立)で null を返したため、footer を描かない確認画面(Ready to submit your answers? → ❯ 1. Submit answers / 2. Cancel)だけが evaluateDialogPresence で「ダイアログ無し」と判定され、UI の送信・Auto-Yes・commandmate respond がすべて prompt_no_longer_active で拒否されていた(画面にはダイアログが出たまま)。実機 fixture(tests/fixtures/claude-live-2468/)で測った 2 形を allowlist として findClaudeTaskPanelLines に加え(右寄せなので trim した行で照合、単数形 +1 file も許容)、転写末尾の walk・detectClaudeDialog の footer 判定・detectPrompt の Pass 2 がこの 1 つの行集合を共有する。footer 判定はパネル/HUD 行だけの footer を「footer 無し」として扱い、完了マーカーや本文が続く番号リストは従来どおり拒否する(#2457 の陰性対照は緑のまま)。+1 file / +2 files edited … が選択肢 1 / 2 と読まれて detectPrompt 自体が確認画面を見失う経路も同じ修正で閉じた。アイドル画面でも同じ HUD 行(No changes this session と +N file(s) edited … の 2 行になることがある)が完了マーカー ✻ <Verb> for <N>s の下に来て findClaudeTranscriptTail の末尾になっていたため、完了マーカーによる idle 証拠が取れていなかったのも同じ修正で直る(修正前後を実フレームで確認)。あわせて PC split ペイン(TerminalSplitPaneContent)と詳細画面(useWorktreeDetailController)の handlePromptRespond は応答 API の 200 { success: false } を拒否として扱い、カードを消さずに「ダイアログの状態が変わったため送信できませんでした。画面を確認してください。」をトースト表示する(従来は response.ok だけを見てカードを消し、次のポーリングでカードが復活していた)。prompt-response/route.ts は変更していない。
  • fix(schedule): CMATE.md の Cron 式を書き換えても稼働中の cron job に反映されない問題を修正 (#2456): 同期ループは既存 scheduleId に対して existingState.entry = entry を行うだけだったため、createScheduleState() が new Cron(entry.cronExpression) で croner オブジェクトへ焼き込んだ Cron 式は差し替わらず、Message / CLI Tool / Permission / model の変更は反映されるのに Cron 式の変更だけがサーバー再起動まで恒久的に無視される状態だった(DB も API も新式を答えるのに、実際に発火するのは旧式という三者不一致)。replaceCronExpression() を追加し、有効な同一 scheduleId の entry について parser が trim 済みの cronExpression を旧 entry と文字列比較して、異なるときだけ timer を交換する。交換は「新 Cron を paused: true で先に組む → 旧 timer を stop() → 新 timer を resume() → cronJob と entry を同時に publish」の順で、パターンを拒否し得る唯一の手順が旧 timer だけが生きている間に終わるため、不正な式は既存 timer/entry を一切変えずに落ちる。ScheduleState は同一オブジェクトのまま cronJob だけを差し替えるので、isExecuting の排他が timer 交換をまたいで共有され、実行中の CLI プロセスは Cron 変更で停止も再起動もされず、新式の tick が実行中に来ても既存の concurrent skip と同じ扱いで起動しない(croner の protect: true は callback が Promise を返さない以上あてにできないため、守るのは state 側)。cron callback の閉包も entry から state へ移したので、次回実行は常に最新の metadata を受け取る。準備・交換のどちらで失敗しても候補 timer は破棄され 2 個の timer が同時に生きることはなく、成功ログは出さずに scheduleId / worktreeId / 旧式 / 新式 / phase を schedule:update-failed に記録し、当該 worktree の mtime キャッシュを落として CMATE.md の mtime が変わらないままでも次の sync で再試行できるようにする。正常な変更時のみ schedule:updated(scheduleId / worktreeId / 旧式 / 新式。prompt 本文は含めない)を 1 回記録し、変更なし・metadata のみ・失敗では出さない。停止済み timer の復旧経路も同じ state を保持して最新式で登録するようになった。あわせて MAX_CONCURRENT_SCHEDULES の判定を有効 timer の新規追加時のみに移動 — 従来は上限に達すると return で同期全体を打ち切っており、既存スケジュールの Cron 変更・無効化はおろか stale cleanup と disableStaleSchedules() まで巻き添えで飛んでいた。切替後は新式の「現在より後の次回」から実行し、既に通過した新予定の即時実行・取り戻しはせず、timezone / DST / 秒フィールド等の croner 設定と Cron parser の仕様は変更していない。テストは tests/unit/lib/schedule-manager.test.ts に 12 件(fake timer 上で旧 tick が発火しないこと、metadata のみでは cronJob の identity と次回予定が不変であること、実 executeSchedule と未解決 Promise を返す CLI executor stub による「実行中の Cron 変更 → 新 tick の skip → 旧実行の完了 → 後続 tick の実行」、A→B→C の連続変更、停止済み復旧、無効化・再有効化・削除、別 worktree/別 schedule 不変、例外注入時の二重 timer 不在、上限到達時の既存変更・無効化・stale cleanup)と tests/integration/schedule-cron-change-2456.test.ts に 3 件(実ファイル・実 parser・一時 DB・実 route での DB → manager → active API の整合)を追加し、切替を無効化した変異では unit 8 件・integration 1 件が赤になること、交換失敗時に候補 timer を stop せず resume する変異では二重 timer のテストだけが赤になることを確認済み。
  • fix(detection): Claude の回答本文にある番号リストが対話 prompt として保存され、チャット面に偽の「ツール承認」チップが出る問題を修正 (#2457): #1928 がツール自身の detectDialog を Auto-Yes の前に置いたとき、履歴保存経路(response-checker)と手動再確認経路(/prompt-response)は汎用 parser の番号リスト推定を単独で信じたまま残っていた。claude は buildDetectPromptOptions が requireDefaultIndicator: false を返すので、❯ がどこにも無い Markdown の 1. / 2. / 3. でも multiple_choice 候補になる — ペイン全体を読む限り prompt-detect-multiple-choice.ts の composer barrier がそれを弾くが、checkForResponse の最終判定は chrome を切り落とした result.response を読み直すため、位置に基づくガードが 1 つも残っておらず、素の回答が messageType: 'prompt' + promptData として chat_messages に入っていた。auto-yes-dialog-gate.ts に共有 helper evaluateDialogPresence(cliToolId, promptType, frame) を切り出し(既存 evaluateAutoYesDialogGate も同じ内部関数を通るようにした)、extractResponse の早期/末尾の prompt 確定経路・checkForResponse の保存直前・/prompt-response の tmux 再確認の 4 箇所に配線した。陽性は dialog !== null で、evaluateAutoYesDialogGate().allowed ではない — allowed は「数字を打ってよいか」なので opencode の keys 駆動ストリップ(#1893)で false になり、そのまま保存可否に繋ぐと誰も自動応答できない prompt だけが History から消える。判定に渡すのは候補を得たのと同じ capture のフレーム全体(result.response でも質問だけでも tail でもない)で、しかも stripBoxDrawing 前の綴り — opencode は入力ボックスの gutter を permission strip の anchor にしているため box drawing を落とすと生きたダイアログが null に化ける(dialogs.test.ts が既に文書化している唯一の例外)。normalizeFrame が自前で stripAnsi し、各ツールの規則が必要なら自分で stripBoxDrawing するので、非冪等な掃除が二重に掛かることも無い。CM_AUTOYES_DIALOG_GATE は Auto-Yes 専用の override のままで共有 gate は読まない(unattended pipeline が prompt に答えなくなった操作者は「何を History に残すか」については何も言っていない)。Auto-Yes の有効/無効も条件ではない — 偽 prompt を書いていたのは response poller で、これは自動応答の有無に関わらず全セッションで回る。棄却された候補は prompt 行・dedup・prompt_detected task event・待機 episode/push・prompt 分岐固有の poll 停止をどれも起こさず、通常の完了判定を満たす回答は既存の転写/scrape 経路でそのまま保存される(生成途中は isComplete: false を維持し、#2436/#2437 の二重保存防止も不変)。/prompt-response は棄却時に既存 JSON 契約どおり HTTP 200 + success:false, reason:'prompt_no_longer_active', answer: answer ?? '' を返してキーを 1 つも送らず、capture 自体が失敗したときの #287 フォールバック(警告して手動応答へ)は陰性判定と同一視せずそのまま残した。入力は新規 fixture tests/fixtures/claude-idle-numbered-list-2457/(合成 8 フレーム。README に合成理由・200x1000 の chrome の写し方・空行 filler を落とした根拠・フレームごとの pre-gate 判定表を記載)と実測 tests/unit/lib/detection/fixtures/claude-live-1708/(permission / AskUserQuestion 確認画面 / idle)で、gate を stub で外した対照ファイルが「偽 prompt が保存され、書きかけの番号リストでターンが終わる」ことを再現して非空虚性を担保している。あわせて response-checker-dedup-visibility-1695.test.ts の copilot ケースが claude のペインを copilot の経路に食わせていたのを実測 fixture copilot-live-1885/permission-dialog.txt に差し替えた(copilot は自分のフッタでしか vouch しないので、旧フレームでは copilot の dedup 挙動を copilot が描かない画面で検証していた)。
  • fix(chat): 太字の裸 URL の直後に全角句読点が続くと autolink が伸びて太字が外れリンク先が壊れる問題を修正 (#2459): **https://example.com/issue/2454**(注記) のように書くと remark-gfm の autolink literal が **(注記) まで href に取り込み、強調も閉じず宛先も壊れていた。共有 remark プラグイン src/lib/markdown(SHARED_REMARK_PLUGINS = remarkGfm + remarkJapaneseUrlBoundary)を新設し、node.position の offset で元ソースを切り出して裸 URL だけを判別したうえで(url === 子 text では [url](url) や <url> と区別できない)、()、。「」【】 の最初の出現で URL を終端する。巻き込まれた ** / __ は href から必ず外し、直前 sibling がソース上で未エスケープ・隣接の同じ開始 delimiter だと確認できたときだけ strong に復元する(\**URL**(注記) は捏造しない)。[text](url) / 参照リンク / <url> / 画像 / code、www.・メールの自動リンク、percent-encoded や /日本語パス は一切変更せず、position 欠落やソース不一致は例外を投げずに no-op、二重適用は冪等。Chat(本文・reasoning・tool activity の 3 描画)、History の ConversationPairCard、MarkdownPreview の 3 コンポーネントが同じ配列を共有する。
  • fix(schedule): CMATE.md の command-code スケジュールが既定で読み取り専用になり「成功ログ・成果物ゼロ」で終わる問題を修正 (#2454): Permission 列に CommandMate 独自値 yolo を追加し(COMMAND_CODE_SCHEDULE_PERMISSIONS = yolo + 既存5値)、DEFAULT_PERMISSIONS['command-code'] を 'default' から 'yolo' へ変更。commandcode -p は --yolo で起動したときにしか書き込めない — 1.53.0 バンドル実測で、--yolo 以外だと print-permission-gate mod が注入され edit_file / write_file / shell_command / monitor_command / kill_shell の 5 ツールを block:true で弾く。--permission-mode はこのゲートに一切効かないので、.choices() の 5 値はどれも読み取り専用にしかならない。しかもブロックはランを落とさず exit 0 / subtype:"success" で終わるため、実行ログは成功のまま成果物だけがゼロになっていた。src/lib/session/claude-executor.ts の「明示 permission が無ければ --yolo」という else 分岐は元から在ったが、src/lib/cmate-parser.ts が空セルを DEFAULT_PERMISSIONS['command-code'](当時 'default')で埋めるためスケジュール経路からは到達不能で、buildCliArgs を直呼びする既存 unit は全緑のままこの欠陥を通していた(今回は parseSchedulesSection() の出力を buildCliArgs に渡す end-to-end テストで固定し、既定値を 'default' に戻すと赤になることを変異注入で確認済み)。yolo は copilot 列の yolo と同じくフラグ名を書く形であって --permission-mode の 6 番目の値ではないため、COMMAND_CODE_PERMISSIONS(5 値)は CLI の受理集合として据え置き、列の語彙だけを 6 値に広げている(--yolo と --permission-mode を同時に出さない排他は従来どおり)。あわせて headless の argv に --no-auto-update を常時付与 — 無いと resolveCliStartupPlan が実行のたびに更新チェックと背景自己更新を走らせ、cron スケジュールが更新ループになる(対話側の COMMAND_CODE_LAUNCH_FLAGS は import せずリテラルで書き、綴りのずれは両方を import する unit が押さえる。--trust / --skip-onboarding は print モードが TUI の trust / onboarding より先に dispatch されるため写していない)。ScheduleEditDialog は command-code 選択時に 6 値を yolo 先頭で表示し初期選択も yolo、5 値のいずれかを選ぶと「print モードでは書き込み系 5 ツールがブロックされ読み取り専用になる」旨の注記を出す。ja/en ドキュメントには permissions.disableBypass が真だと --yolo 自体が無効化されて同じ読み取り専用状態に戻ること、permissions.deny は --yolo でも効くこと、headless で withheld されるツール(ask_user_question / enter_plan_mode / exit_plan_mode / plan_review / todo_write / cron_create / cron_list / cron_delete / taste)を追記した。