Repository navigation
v0.38.0
[0.38.0] - 2026-09-16
Highlight: CommandMate の画面から shift+tab を送り、エージェントの権限モードを切り替えられるようにした(claude / Command Code / codex / copilot / antigravity、#2592)。現在のモードは PC・チャット面・スマホの操作列にチップで表示する。許可ダイアログの上では shift+tab が「このセッションの全編集を許可」になるので、そのあいだボタンは押せない。マージ前の実機 UAT で、Issue 本文から作った fixture では見えなかった不具合を 4 件見つけ、直してから入れた。あわせて、許可ダイアログに自由文を送ると、拒否のつもりの文でもハイライト中の
1. Yesが確定して操作が実行されていた経路を 3 つ塞いだ(#2573 / #2583 / #2584。どれも実機でmkdirが実行され、API はsuccess: trueを返していた)。Command Code については、数字で回答した直後の余分な Enter が未送信の下書きを送ってしまう事象(#2574)と、スケジュール実行でツールが拒否されても警告なしにcompletedと記録される事象(#2577)も直した。
Added
- feat(worktree): shift+tab でエージェントの権限モードを切り替えられるようにした (#2592): claude / Command Code / codex / copilot / antigravity の 5 ツールに
ICLITool.agentModeSpec()を追加し、composer の操作列に「モード」ボタンと現在モードのチップを PC footer・チャット面・スマホの 3 面へ設置。モードはフレームから毎回読み直し(detectAgentMode())、poll と push の双方で同じ値になる。読めないときはunknownを返してチップを描かない(codex / copilot / agy は既定モードで何も描画しないため「行が無い=default」とは決め打たない)。許可ダイアログ表示中はshift+tabが「このセッションの全編集を許可」を意味するため、sessionStatus === 'ready'かつプロンプト・選択リスト・dismiss パネル・未分類フレームのいずれも出ていない時だけ押せる。GET /api/worktrees/[id]/current-outputはagentModeを返す。マージ前の実機 UAT を受けて、agy 1.2.4 のフッタ(? for shortcutsと同じ行にモード語が並ぶ形)と copilot 1.0.85 のautopilot (limited)を読めるようにし、agy が accept-edits / plan で休止中のとき入力欄のモードバナーを空の入力欄として扱うようにした(それまではrunning扱いでwaitが終わらず、送信もできず、UI からは元のモードに戻せなかった)。codex 0.154 のステータスバーにスレッド名や Plan バッジが付いてもモデルと推論の強さを読み取れるようにし(Plan に入っても UI がxhighのままだった)、codex のモデル連動の注意書きはタッチ端末でも見えるよう常時表示にした。opencode(BTabは agent 切替)・vibe-local(バインド無し)・gemini(footer 未計測)は対象外。 - feat(schedule): CMATE.md を直接編集した command-code スケジュールでも書き込み系ツール拒否の設定を警告する (#2576): 有効な行の Permission が
--permission-mode系 5 値のとき、Logs タブ上部に警告バナーを出し、サーバログにもparse:command-code-direct-write-tools-deniedを記録する。判定はisCommandCodeDirectWriteToolsDenied()1 つを編集ダイアログ・バナー・パーサで共有し、実行時のbuildCliArgsと一致させた(空セルを編集で開いたときの誤警告も解消)。警告はエラーと別経路のcollectScheduleWarnings()で返すため、登録も実行も止めない。文言は「読み取り専用になる」ではなく「エージェントが直接呼ぶ書き込み系ツールが拒否される」とした - feat(cli):
commandmate lsの表にAUTO_YES列を足し、確認待ちを「Auto-Yes が残り N 分ある」と「誰も答えない」に分けて読めるようにした (#2575): Auto-Yes が失効したあとに出た確認待ちに気づけなかった事例(2026-09-14、:60301 の worktree で 33 分放置)を調べ直した結果、既存機能の不在ではなかった —— 失効は #959 で UI に出て、失効後の push は #1999 のゲートがauto-yes-inactiveで通し、ブラウザを開いていれば #1788 のアプリ内トースト(既定 ON)と #1789 のチャイム、サイドバー / Sessions タイル(#2512)が効き、CLI ではcommandmate waitが Auto-Yes 無効なら即 exit 10 を返す。届かなかった直接の原因は購読 0 件のさらに手前で、当該サーバに VAPID 鍵が設定されておらず(GET /api/push/vapid→{"configured":false}、~/.commandmate/.envにCM_VAPID_*無し)購読 UI 自体が出ない構成だった(設定漏れ。commandmate initが鍵を生成するようになった #2123 より前の.env)。残っていた隙間は CLI の表だけで、lsはwaitingを出しても「その待ちに Auto-Yes が武装しているか」を出せず、失効前に残り時間を一覧で見る手段も無かった。そこでGET /api/worktreesが #2512 以降すでに全行に載せているautoYesByInstanceと、未集約のsessionStatusByInstanceだけから(API・DB・Web UI・push ゲート・commandmate statusは 1 バイトも変えずに)6 列目AUTO_YESを導出する: その行の STATUS を出している instance のうち、最初に Auto-Yes を失う instance の残り時間(waiting→待っている instance /running→処理中 /ready→稼働中 /idle→exited。素のidle行だけ武装している全 instance にフォールバック)をMM:SS/H:MM:SS(Web UI のカウントダウンと同じ書式)で出し、対象に 1 つでも非武装があればoff、期限の無い武装はon、判定できない行(#2512 より古いサーバ /idle以外なのに STATUS を出す instance が応答に無い)は-。最小値にしたのが要点で、claude(待機・非武装)とcodex(待機・残り 10 分)が同居する行に10:00を出すと「誰も答えない確認待ち」が他人の残り時間の陰に隠れるためoffを優先する(代わりにready行で非武装が 1 つ同居すると武装側の残り時間は表から消える。内訳はls --jsonのsessionStatusByInstance×autoYesByInstanceかcapture --json --instance)。代表 instance が既定エージェントのプライマリでなければ(codex-2)と instance ID を添える(そのまま--instanceに渡せる。alias を持つツールの REASON は集約で落ちる #1926 ので、この括弧が行内で唯一の instance 指定になることがある)。失効の畳み込みはサーバ側(getEnabledAutoYesByWorktree)に加えてクライアント側でもexpiresAt <= nowを非武装として扱い、武装していると誤って見せる方向には倒れない。列は末尾に追加(先頭 5 列ID NAME STATUS REASON DEFAULTの順と開始位置・--json・--quietは不変。--jsonには導出値を足さない —— 機械の正規経路は保留まで知っているwaitの exit 10 とcapture --jsonのautoYes)、最終列だけpadEndしない(instance ID は最大 64 文字で、1 行の長い ID が全行に空白を足して狭い端末で折り返すため)。docs が scope 外でこの版に載らないため、列の読み方と限界(offは失効・未設定・手動 OFF を区別しない/残り時間は「自動応答される」保証ではなくポリシー保留や自由入力には答えない/-の 2 つの意味/内訳の確認先)はls --helpのaddHelpTextに置いた(wait --helpが #1926 で同じことをした先例)。型はtsconfig.cli.jsonのpaths: {}があるためls.ts内のローカル拡張(LsWorktreeItem/AutoYesInstanceWire)で読み、サーバ型AutoYesInstanceSummaryとの双方向代入可能性とCliToolSessionStatus→ per-instance エントリの 4 キーの代入可能性をtsc --noEmitで落ちる型アサーションに固定した。formatTimeRemainingは@/config/tmux-pane-configを推移的に import するため CLI から読めず(lint /tsc/ vitest は@/を解決するのでnpm run build:cliだけが落ちる)、同書式のformatAutoYesRemainingを複製して境界 5 点(999ms→00:00、59:59.999→59:59、1h→1:00:00、8h→8:00:00、10 分→10:00)で同一性を pin。explainsStatusは REASON 列のpickStatusEntryから切り出して共有し、STATUS の語彙(4 語)と REASON の選び方は不変。テストは新規tests/unit/cli/commands/ls-auto-yes-2575.test.ts30 件(push の設定・購読に依存しない入力だけでoffと10:00を判定できることの pin、失効境界、旧サーバの-、instance 対象化、最小値と同値時の既定優先、idle 限定フォールバック、非 idle 空集合の-、trade-off 2 件、書式同一性、--json/--quiet不変、outputHelp()の凡例、型アサーション)に加え、表を 5 列ちょうどで固定していたtmux-session-name-2317.test.tsを「先頭 5 列不変+6 列目AUTO_YES+mcbd-を含まない」に更新し、ls-sidebar-convergence-2060.test.tsの「ルートの実応答にlsが読むフィールドが在る」検査にautoYesByInstanceを足した。検算として 8 種の構造保存変異(expiresAt > now反転 / 最小を最大に / 既定優先の tie-break 反転 / instance 対象化の除去 / 非武装を無視 / 常にoff/ フォールバックの idle 限定を外す / help 凡例の削除)を 1 件ずつ注入して 8 件すべてが赤になること、復元が md5 でバイト同一であることを確認した。src/cli/commands/ls.tsのカバレッジは stmts 97.52% / branch 96.25% / funcs 100%。
Changed
- docs(cli):
commandmate lsの 6 列(REASON/AUTO_YESを含む)をガイド・チュートリアル・CLI 埋め込みドキュメントに反映した (#2586):lsの表は #1926 でREASON、#2575 でAUTO_YESが入って 6 列になっていたが、追従したのはls --helpだけで、出荷済みビルドで列の意味を説明しているのは--helpのみという状態だった(#2575 の申し送り)。src/cli/docs/agent-operations.tsのTable format (ID, NAME, STATUS, DEFAULT)を 6 列に直し、REASON values/AUTO_YES valuesの節(対象 instance の選び方、MM:SS/H:MM:SS/on/off/-/(<instanceId>)の 6 値、残り時間は自動応答の保証ではないこと、内訳は--jsonのsessionStatusByInstance/autoYesByInstance)を追加。docs/user-guide/cli-operations-guide.mdに「AUTO_YES列の意味」節を、docs/en/user-guide/cli-operations-guide.mdには**#1926 から欠けたままだった REASON 節ごと** ja と揃えて追加し(ja-en-heading-parity.test.tsは##しか数えないため###の欠落は赤にならず 2 Issue 分ドリフトしていた)、両チュートリアルのID NAME STATUS DEFAULTを 6 列に更新して-/offが何を意味するかの注を付けた。ガイドの出力例は手で整形せずcreateLsCommand()の実出力を貼った —— 旧例は ID 列の幅が formatter と一致しておらず、実際には出ない表だった。新設のtests/unit/docs/ls-columns-2586.test.tsが/api/worktreesをモックして表をレンダリングし、4 つのドキュメントがその文字列をそのまま含むことと、埋め込みドキュメントが 6 列を順番に名指すことを固定する(7 列目が増えれば赤になる)。例はwaiting+off(=誰も答えない確認待ち。#2575 の主目的)と03:12 (codex-2)(REASON が-のまま AUTO_YES だけが instance を名指す行)を含み、awk -F' +' '$3 == "waiting" && $6 ~ /^off/'で抽出できることまで示した($NFだとoff (codex-2)の行が漏れる)。src/cli/types/api-responses.tsの「sessionStatusByInstanceは CLI が読まない」というコメントも #2575 以降は誤りなので、ls.ts内のローカル型LsWorktreeItemで読んでいること・共有型を広げない理由に書き換えた。実装(src/cli/commands/ls.ts)は無変更 - chore(skills): 公式 Catalog の
cmate-uat新設に合わせ、.commandmate/uat.yamlを追跡対象にし、CommandMate 自身の UAT 環境宣言を置いた (#2590):cmate-uat(commandmate-skills 0.1.0)がリポジトリごとに読む環境宣言.commandmate/uat.yamlを、verify.yamlと同じ「設定」として.gitignoreの追跡例外に加えた。例外はファイル 1 本だけを名指しにしている。!/.commandmate/uat*のように prefix で書くと、run の成果物(既定の.commandmate/uat/)まで追跡されてしまうためで、この区別はscripts/check-commandmate-tracking.shと unit test の両方で、追跡される側・されない側の両方向から固定した。CommandMate 自身のuat.yamlでは、UAT サーバをenv -iで起動し、CM_DB_PATH/CM_ROOT_DIRを run ディレクトリの下に明示し、tmux を私設ソケットに向けることを宣言している。これまで.claude/commands/uat.mdとdev-reports/issue/<n>/uat/で手作業で行っていた隔離を、宣言として書き起こしたもの(本番 DB を掴んだ過去の事故への対策)。.claude/skills/sync-map.jsonのnotMappedにskills/cmate-uatを記録した(名前の似たuat.mdは移植元ではなく、別に書き起こした文書)。公式 Catalog の Skill 一覧は、docs/design/public-messaging.mdと LP(website/index.html)の両方でcmate-uatを足して 15 件にした
Fixed
- fix(prompt): テキスト欄を 1 つも持たない許可ダイアログへ
respond/ API で自由文を送ると、拒否のつもりの文がハイライト中の1. Yesを確定して操作が実行される事象を止めた (#2583): #2573 の自由文ゲートはrequiresTextInputの選択肢が 1 つも無い画面を判定せず fail-open だったため、claude 2.1.273 の Bash 許可(1. Yes / 2. Yes, and always allow access to … / 3. Yes, and switch to auto mode / 4. No)と agy 1.2.3 のRun this command?(1. Yes, run command / … / 4. No, cancel)へ自由文を送るとsuccess: trueでmkdirが実行された(2026-09-16 実測、develop9f4b4e29)。塞がるか否かが承認対象のパス文字列にcustomを含むか(TEXT_INPUT_PATTERNSの/custom/i)に依存していた。sendPromptAnswerのassertFreeTextHasATextFieldに、既存のisTypedTextFieldOption判定の外側(text-bearing 選択肢が 0 件の分岐、#2584 の担当分岐とは別)でFreeTextAtChoiceOnlyPromptError(reason: 'unresolvable_answer'、optionCount保持、回答文は引用しない SEC-003)を投げる早期拒否を追加し、キーを 1 つも送らずに拒否する。#1726 の pin は「エージェントのAskUserQuestionペイロードが無い=画面が唯一の権威」という権威の話であり、ここで拒否の根拠にしているのは再検証済みの実キャプチャから読んだ画面の選択肢そのものなので、範囲外の数値(answer_out_of_rangeは非数値のみを読むこのゲートの対象外)とpromptDataの無い呼び出しは従来どおり通す形で pin を 1 ケースだけ狭めた(判断の根拠はassertFreeTextHasATextFieldの doc comment と 1726 のテストに記載)。commandmate respondは拒否時にcommandmate respond <id> <number>とcommandmate capture <id>を示すヒント行を追加で表示する。Command Code のType something...実測テキスト欄(#2522)とrespond <id> noのラベル解決(#1681)は不変。 - fix(cli):
commandmate statusが呼び出し元 shell のCM_VAPID_*をサーバの設定と取り違えて Push 未設定警告を沈黙させる問題を修正 (#2585):printVapidStatus()が読んでいたloadEffectiveEnv()は{...process.env, ...parsed}(daemon.start() の env 合成の再現)で、.envにキーが無いときは呼び出し元 shell の値がそのまま残るため、鍵を持たないサーバを「設定済み」と読んで #2123 の警告を出さなかった(実測 2026-09-16::60301は/api/push/vapidに{"configured":false}を返すがstatusは無言。#2575 の調査はこの沈黙のために根本原因へ辿り着けなかった)。判定を (1) 稼働サーバへのGET /api/push/vapid(1 秒デッドライン・CM_AUTH_TOKEN送出・redirect: 'manual')、(2) 応答が得られないときはサーバの.envのみを読む新しいloadEnvFileValues()の順に変更し、process.envはどちらの下敷きにもしない。.envにある鍵をサーバが持っていない場合は再起動を促す 1 行を、shell だけが export している場合はその旨の 1 行を既存の文面に追記する。subject 検査(#2124)はエンドポイントが報告しないため従来どおり.envから判定する - fix(prompt): AskUserQuestion のカーソルが自由文行に無いときの自由文送信を拒否 (#2584): Command Code の
AskUserQuestionに自由文を送ると、Type something...行にカーソルが無い場合は文字が無視され後続 Enter がハイライト中の別の選択肢を確定していた(API はsuccess: trueを返すため誤認する)。sendPromptAnswerにassertFreeTextCursorIsOnTheFieldを追加し、実測済みテキスト欄を持つ multiple_choice でカーソル(isDefault)がその行に無いときはFreeTextAnswerRejectedError(kind: 'cursor_elsewhere'、reason: 'unresolvable_answer')でキーを 1 つも送らず拒否する。拒否メッセージはテキスト欄の番号と Enter が確定したはずの行番号を示し、回答文は引用しない(SEC-003)。カーソルが自由文行にある画面(#2522)の挙動は不変。 - fix(prompt): 許可ダイアログで「No, tell … what to do differently」を選んで理由を書くと、拒否ではなくハイライト中の既定(
1. Yes)が確定して操作が実行される事象を止めた (#2573):PromptPanel/MobilePromptSheetはrequiresTextInputの選択肢で番号ではなく入力テキストを送り、sendPromptAnswerのテキスト経路がそれをメニューに打って Enter を押していた(Command Code 1.53.1 で「この操作はしないでください」を送るとmkdirが実行され API はsuccess: true)。requiresTextInputは語の読みであって行の実体ではなく、同じフラグが本物のテキスト欄(Command CodeAskUserQuestionのType something...、#2522)とメニュー行(No, tell Command Code / and tell Codex / and tell Copilot what to do differently、承認行が引用したコマンド中のcustom等)の両方に立つため、実測済みのテキスト欄ラベルだけを許可リストで判定するisTypedTextFieldOption(prompt-detect-multiple-choice.ts)を追加。sendPromptAnswerは非数値の回答が来たとき、multiple_choice の text-bearing な選択肢がすべてメニュー行ならキーを 1 つも送らずFreeTextAnswerRejectedError(reason: 'unresolvable_answer'、回答文は引用しない)で拒否し、/prompt-responseは 200{ success: false, reason: 'unresolvable_answer' }を返す。パネル/シートはメニュー行ではテキスト欄を出さず番号を送る(ツールは composer に戻るので指示は通常メッセージで送る)。Type something...への自由文(#2522)と、text-bearing な選択肢を持たないプロンプトへの自由文(#1726)は従来どおり。番号送信→理由の別送(対応内容 2)は本件では未実装(#2574 の Enter 抑止はマージ済み。残るのは番号確定後の入力先をツールごとに実測すること) - fix(schedule): command-code のスケジュール実行でツール呼び出しが pre-tool hook に拒否されたとき、exit 0 /
completedのまま警告なしで記録されるのを止め、実行ログ一覧の行に警告を出すようにした (#2577):claude-executorが NDJSON を result 行だけに絞る前に{"type":"event","event":{"type":"tool_hook_blocked",…}}を読み、toolNameとhookOutputをWarning: command-code blocked N tool call(s) (tool_hook_blocked): write_file (1); M tool call(s) ranの要約行+理由行として実行ログの出力の先頭に書く(ExecutionResult.warningにも同じ要約行)。status は CLI の判定のまま変えないので、拒否のあと別経路で作業を終えた実行はfailedにならず、非ゼロ終了・エラー subtype・タイムアウトはfailed/timeoutのまま警告だけが併記される。判定はfinalTextに依存しない。GET /api/worktrees/:id/execution-logsは result の先頭だけを読んで各行にwarningを返し(本文は従来どおり返さない)、Logs タブは警告のある行に警告文を表示して completed を成功色で塗らない。成果物(ファイル生成・メール送信)ができたかの検証はしない。拒否イベントの実バイトは command-code 1.53.1 で採取した fixture(tests/unit/session/fixtures/command-code-tool-hook-blocked-2577/) - fix(detection): Command Code の許可ダイアログに数字で回答したとき、確定後の余分な Enter が composer に着弾して未送信の下書きを送信してしまう事象を止めた (#2574): 許可ダイアログは数字がホットキーで即確定する(1.53.1 実機で
1単独送信によりダイアログが閉じコマンドが実行されることを確認)が、汎用パーサが作る promptData にsubmitModeが無くsendPromptAnswerはanswer_then_enterで数字の後にC-mを送っていた。考え中に composer へ打った下書きがその Enter で新しいユーザー発話として送られることを実機で再現した。command-code にdetectDialog(tools/command-code/permission.ts、フッタ↑/↓ navigate · enter select · … to bypass all permissionsが最終行でその直上に❯付き番号リストがあるときだけkind: 'permission'/answerMode: 'numbered'/submitMode: 'answer_only')を追加し(hasDialogRules: true)、DialogVerdictに任意項目submitModeを足して、sendPromptAnswerが #2033 のガードで読んだ送信直前フレームのダイアログ判定をpromptData.submitModeより優先して Enter を決めるようにした。Auto-Yes ポーラ・respond・チャット画面の全経路に効く。Epic #2249 決定 3(hooks 駆動の承認は不可)は変えておらず、AUTO_YES_DIALOG_GATE_DEFAULT_MODE['command-code']はlegacyのまま(許可ダイアログしか認識しないルールで enforce にするとAskUserQuestionが/prompt-responseでunsupported_dialog_layoutになり、Auto-Yes の box 除去済みフレームでは質問リーダが読めず全質問が抑止されるため。根拠をコメントとテストに固定)。修正後の実機では tmux への送信がsend-keys 1のみになり、下書きは composer に未送信のまま残ることを Auto-Yes 形(フレーム無し)と/prompt-response形(フレーム渡し)の両方で確認した