v0.34.0
[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を tagrust-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の先頭に混ざっていた。新設の leaftools/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ではなく新しい reasonunsupported_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-yesPOST など)は 400instance_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に共有 helperevaluateDialogPresence(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_detectedtask 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 フォールバック(警告して手動応答へ)は陰性判定と同一視せずそのまま残した。入力は新規 fixturetests/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 の経路に食わせていたのを実測 fixturecopilot-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-gatemod が注入され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)を追記した。