v0.26.0
並列オーケストレーションの判定を壊す 3 つの穴(ポート衝突・flake・上流障害)を製品側で塞いだ回です。いずれも BorderFreeKidsMap の実運転で定量的に代償が出たもので、commandmate-skills 側の版と対になっています。
| package | 版 |
|---|---|
cmate-verify |
0.5.0 |
cmate-verify-advisor |
0.3.0 |
cmate-orchestrate |
0.32.0 |
cmate-issue-authoring |
0.9.0 |
mutex / retryOnFail / flakyIsPass / requireEnvClean は 4 実装すべてが同じ集合を受理します(docs/design/verification-config.md §9.5)。
Added
- chore(skills): vendored な cmate-verify を上流と揃え、
mutex/retryOnFail/flakyIsPass/requireEnvCleanを受理させる (#1861): v0.26.0 の verify.yaml parity は 3 リポジトリ位置・4 実装にまたがるが、#1771 / #1772 は.claude/skills/sync-map.jsonの sha256 pin を壊さないため vendored copy を意図的に触っておらず、同じ verify.yaml が製品側(commandmate verify)では exit 0、vendored ランナー(.claude/skills/cmate-verify/scripts/verify-run.sh)では exit 2 に割れていた(実測 2026-08-20:unknown gate key: retryOnFail/flakyIsPass/mutex/unknown options key: requireEnvCleanの 4 件で設定エラー)。上流 Kewton/commandmate-skills PR #225(6faa33f、skills #223 / #224)のverify-run.sh(554 → 963 行)をcmpでバイト一致する逐語コピーとして取り込み、.agents/skills/cmate-verify/へもミラーした(同 verify.yaml の再実測で vendored も exit 0 /GATE e2e PASS exit=0 duration=0s waited=0s/GATE env-clean SKIP reason=no-baseline/RESULT passed)。SKILL.mdは policy がport-requiredなので逐語コピーせず、キー表・ロック規約(~/.commandmate/locks/<name>.lock/mkdir/ownerの pid・host・token /CM_VERIFY_LOCK_ROOT)・waitedをdurationに足さない契約・CM_WORKTREE_INDEXを standalone 側は設定しない理由を、CommandMate 側の Issue 番号(#1771 / #1772 / #1740)と相対パスに読み替えて書き直した。fixture は counterpart(tests/fixtures/cmate-verify/fixtures/)から 18 本を追加し、suite の assertion は 200 → 317(MIN_ASSERTIONSは bash 側・vitest ラッパ側とも 300 に引き上げ、新セクションが黙って落ちても緑にならないようにした)。counterpart は fixture と run-tests.sh を package の外に置くためskills-sync-map.mjs check --counterpartがscripts/tests/**を MISSING と報告するが、これは構造上の既知差(vendored は Node の無い導入先でも suite を回せるよう同梱する)であり、その旨を sync-map のrationale に明記した - feat(cli):
waitが「ターンが成立したか」を見るようにする (#1839): 上流 API 障害でエージェントが何も実行せず composer に戻ると、waitは exit 0 を返し--verifyは work-evidence ゼロ(exit 21)を返すため、呼び出し側が「ターンは成立したが成果物が無い」と誤読していた(実測 #1834: 529 × 13 に対し exit 21 が 12 回)。隔離環境の実測(2026-08-20 / stub 529 + 実 claude 2.1.236 / 実 API 不使用)で、崩れたターンではStophook が1 度も届かない一方Notification(idle_prompt)は +62 秒で届くことを確認し、Stopのみをターン終端とする判定を追加した(hooks が来ていないインスタンスの挙動は不変)。あわせて上流障害の署名をsrc/lib/detection/upstream-faults.tsに集約してcapture --jsonのupstreamFaultとして公開し(canary の重複定義を解消)、opt-in のwait --fail-on-upstream-faultが exit 11 を返すようにした。waitの完了行には判定根拠(basis=hook_stop/session_gone/scraper_ready)が付く - feat(verification): FLAKY を一級の outcome にする(gate 単位 opt-in の同一 tree 再実行) (#1772): ランナーの結果は PASS / FAIL しか無く、「この 1 件だけ赤ならまず再実行」は人間の部族知識だった(実測 2026-08-10 / Kewton/BorderFreeKidsMap: unit ゲートの禁止語検査
not.toContain("fac-")が乱数 UUID の9fac-に一致して failし、同一 tree で再実行したら pass = 1 fail 52 pass → 53 pass)。オーケストレーション配下ではワーカーもオペレータも赤の原因を自分の変更に求めて時間を焼く。gate 単位の opt-inretryOnFail: 1を追加し、fail したゲートを同一 tree でもう 1 回だけ再実行して、fail→pass をFLAKYとして両ランの exit code と duration ごと記録するようにした。値域は 0 か 1 のみ(2 以上は設定エラー = 十分な回数を回せばどんな赤も緑になるので上限そのものが機能の中身)、2 回とも fail は FAIL のまま、再実行するのは非ゼロ終了だけ(TIMEOUT は予算を使い切っており 2 回目が実時間を倍にする/mutex 待ちの SKIP と起動失敗はコマンドが 1 度も走っていない)、2 回目が裁定に到達しなければ1 回目の FAIL が立つ(2 回目を採ると work を裁定したゲートが exit 99 に化けて判定が弱くなる)。裁定上の扱いはflakyIsPassで選び、既定は「FLAKY は fail 扱い」= ゲートは 1 bit も弱くならない(retryOnFail: 1が買うのは「何が起きたか」に名前が付くことだけ)。flakyIsPassは gate 単位とした(skills #224 はこちらを正とすること) —retryOnFailが gate 単位である以上ゲート宣言 1 つで裁定まで読めるべきであり、unitの乱数 fail とe2eの実レースを1 つの答えに強制せずに済み、options 単位だと決して発火しない宣言(retryOnFail無しのゲートに対する宣言)が正当な設定として通ってしまうため。flakyIsPass: trueをretryOnFail: 1無しで書くのは設定エラー。verification_gate_resultsに列が無く DB マイグレーションは scope 外なので、両ランの数値は #1771 のwaitedと同じ**log_tailの行頭アンカー**[flaky] runs=2 outcome=flaky|fail exit=1,0 duration=45.0s,44.0s verdict=pass|failで運ぶ(outcome=failでもアンカーを書く = 2 回落ちは flakiness への反証であり advisor の分母。両ランのログ全文も残す = 「2 回で何が違ったか」がこの機能の唯一の問い)。列に入るのはその裁定を出したランの status/exit なのでstatus=failedの隣にexit=0が並ぶ行は作らず、duration_msは両ランの和で #1625 のfinished_at - started_at === duration_msも不変。GATE 行の確定綴りはdocs/design/verification-config.md§9.3 の表(standaloneGATE unit FLAKY exit=1,0 duration=45s,44s/ CommandMateGATE unit FLAKY (exit=1,0, 45.0s,44.0s)。flakyIsPassの値で綴りは変わらない — pass と数えた FLAKY をPASSと綴ると本機能が可視化する唯一の事実が消える)で、仕様は同 §10。verify showとverify --json/verify show --json(gates[].flaky)から履歴として読み戻せる(verify historyの一覧行はゲート要約にlog_tailを含まない設計のため出ない)。retryOnFailを宣言しないゲートの出力は 1 バイトも変わらない - feat(verification): ゲートに worktree ごとの env 注入と gate 単位の
mutexを宣言できるようにする (#1771): verify.yaml のゲートはコマンドと timeout しか宣言できず、「このゲートはマシン上で同時に 1 つしか走れない」(固定ポート・ローカル DB・エミュレータ)を表現できなかった。並列 worktree で重なると後発が資源衝突で即 fail し、記録はGATE e2e FAIL exit=1だけ = 変更の欠陥と環境の衝突が区別できない(実測: BorderFreeKidsMap で planner が 9 wave を計算したのに e2e ゲートが 60303 番ポートを専有するため 16 回の直列 dispatch に開き直した)。対処は 2 段構えで、並列度を保てるのは 1 つ目だけ。(1) コマンド系ゲートの実行 env にCM_WORKTREE_ID/CM_WORKTREE_INDEXを注入し、E2E_PORT=$((60400+CM_WORKTREE_INDEX))で衝突自体を無くせるようにした。CommandMate は worktree を採番していない(Issue 本文の前提は実測と異なる — id は basename 由来の TEXT 主キーで順序列も作成時刻も無く、唯一の並びはupdated_at DESC)ため番号は新設のレジストリ~/.commandmate/worktree-index/<n>がO_EXCLで払い出す(同じ worktree は毎回同じ番号/同時に走る 2 worktree は必ず別番号。ハッシュ案は 30 worktree で約 35% 衝突するため fallback に留めた)。(2)gates[].mutex: <name>を追加し、同名を宣言したゲートを~/.commandmate/locks/<name>.lock(mkdir方式。macOS にflock(1)が無い)でマシン全体で 1 つに直列化する。待ち時間はdurationに足さずwaited=として別に記録する(混ぜると timeout 調整と advisor の入力が歪む)。ロックがtimeoutSecの間空かなければ TIMEOUT ではなくSKIP reason=mutex-waitで run はerror(exit 99 = 判定不能)= 20(不合格)ではない。lock path 規約・GATE 行の綴り・両ランナーが受理すべきキー集合はdocs/design/verification-config.md§9 に確定した形で記載(skills 側 #223 がこれを正として実装する)。mutexを宣言しないゲートの出力は 1 バイトも変わらない - feat(polling): プロンプト dedup のスキップを
capture --jsonに露出する (#1695): 重複抑止で落としたプロンプトの累積回数と最終スキップ時刻をpromptDedup(skippedCount/lastSkippedAt)として公開し、「プロンプトが出たはずなのに保存されていない」ときに dedup が原因か検出漏れ(#1676)かを CLI から判別できるようにした。あわせて response 側 dedup(isDuplicateResponse、#1268)にduplicate-response-skippedログを追加(従来ログすら無かった) - feat(verify): scope ゲートに「何がどの allow パターンで許可されたか」の証跡を残す (#1841): scope ゲートは違反 path しか報告せず、
allowが完全一致 path だった頃は「パターン=ファイル」で足りていたが、#1546 でsrc/**のような glob が正式化された後はその run で実際に何が許可されたのかが契約からも log からも読めなくなっていた。log_tailにadmitted:節を足し、合否を問わず「許可された変更 path ← それを許可した allow パターン」を残す。記録するのは宣言順で最初に一致したパターン(allow: ["src/**", "src/lib/**"]ならsrc/lib/a.tsはsrc/**。最後に一致を名指すと「消しても判定が変わらないルール」を読者に提示することになる)、allowに無いのに許可された path は(exempt: .commandmate/)/(exempt: contract path)と括弧付きで名乗り(契約を grep しても見つからないのが事実だから)、denyで落ちた path はadmitted:に入らずout of scope:側に拒否した deny パターンが付く(「revert する」と「allow を広げる」の切り分け)。両節とも 100 件(MAX_REPORTED_VIOLATIONS)で切り、切ったことを... (+N more)と名乗る — 切り詰めは表示規則であり、判定は全ファイルに対して行われる(切った分の違反も exit に反映される)。admitted:をout of scope:より前に置いたのは、CLI が不合格ゲートの log を末尾 40 行しか表示しないため(後ろに置くと違反一覧とガイダンスが画面外へ流れる)。あわせてverify --json/verify show --jsonの scope ゲート結果に機械可読のscope(admitted: [{path, pattern}]/violations: [path]/totals: {changed, admitted, violations})を足した(既存フィールドは 1 つも変えていない)。totalsを別に持つのは 2 配列がレポートと同じ 100 件で切れるためで、「scope 外が在るか」はviolations.lengthではなくtotals.violationsで見る。pass / fail の裁定は 1 バイトも変えていない:ScopeMatcher.isViolation()は新しいclassify()の否定に委譲するので、判定と証跡が食い違う経路そのものが無く、既存の scope-gate テスト 47 件は無改変で緑 - feat(cli): stop-pattern が何にマッチしたかを
capture --jsonに露出する (#1694):--stop-patternの発火はautoYes.stopReasonで分かるが、何にマッチしたかはどの層にも出ておらず、ビルドログがパターン文字列を含んだだけの誤爆(#1678 A-5)と正当な停止を運用者が切り分けられなかった。マッチ行+前後 1 行の抜粋をautoYes.stopMatchedTextとして露出する。抜粋は文字数ではなく UTF-8 バイトで 400 バイトに切り詰め(日本語フレームは 1 文字 3 バイトのため)、切り詰めたときだけ末尾に…[truncated]を付ける。抜粋は発火時のみ記録し、expired や手動 disable では持ち越さない(起きていない発火として読まれるため) - test(hooks): Auto-Yes v2(
PermissionRequest裁定)の実 TUI 検証を canary シナリオとして固定する (#1847): #1724 の手動検証 3 項目のうち未記録だった 2 項目を、実claudeを回す検出カナリア(#1727)にpermission-hook-allow/permission-hook-no-decisionとして固定した。本番のbuildClaudeLaunchCommandが書いた--settingsをカナリア内の受け口(127.0.0.1:0の ephemeral ポート)へ向け、裁定は本体のresolvePermissionRequestをそのまま呼ぶ(DB を要する契約読み出しと allow 監査の 2 箇所だけPermissionDecisionDepsで差し替え)。allowでダイアログが出ずツールが走ること・denyPatterns一致の no-decision でダイアログが出てautoYes.lastSuppressionに理由が載ることを、pane と本番同一 getter で組んだstructuredEventsの両方で確認する。非空振りは新フラグ--mutate-verdict(受け口が逆の裁定を返す)で証明する。あわせて Claude Code 2.1.236 で既定の permission mode が auto mode になったため承認ダイアログ自体が描画されなくなっていた問題に対処し、全カナリアセッションを--permission-mode manualで起動するようにした(2 本目以降のセッションだけが起動タイムアウトで落ちる形で表面化していた)
Changed
- refactor(hooks):
AgentEventSourceの I/F 申し送り 5 件を裁定する (#1846): #1759 の抽象は 6 ツールを I/F 変更ゼロで受け止めたが、その過程で 5 件の申し送りが報告だけされて未裁定のまま残っていた。7 本目のツールが同じ回避策を書く前に、全件に採用/不採用を付けてdocs/design/agent-event-source-interface.md§3.3 に残した(不採用の理由も残す — 同じ申し送りが 3 回目に来ないようにするため)。線は 1 本: 2 実装以上が独立に同じ回避策へ到達したものだけ I/F に入れる。採用 2 件: ①prepareLaunchの引数をAgentLaunchContext{target, executablePath, worktreePath}へ(worktreePathは必須)。gemini のinjectGeminiHookSettings()別 export(#1762 の回避策。cli-tools/gemini.tsが 2 回呼んでいた)が消え、6 ソースとも設定書き出しがprepareLaunch1 箇所に揃う。AgentInstanceRefはキーなので 3 フィールドのまま ②AgentLaunchPlan.env(必須)とrenderAgentLaunchCommand。codex / copilot / gemini / antigravity の4 実装が独立にNAME=valueをcommandへ前置しており(宣言されていない前提=「起動側はシェルである」に 4 箇所が乗っていた)、これを剥がして適用を 1 箇所に集約した。ペインに送られるバイト列は 1 バイトも変えていない。不採用 3 件: ③NoDecisionBehaviorへのdenies追加 — 前提が失効している。「agy はblocksで近似」は #1762 時点の話で、#1779 が agy 1.1.12 を実測してproceedsに直しており(src/lib/hooks/sources/antigravity/source.ts:181)、{}を送るのは CommandMate ではない ④supportedEventsのemittable/delivered分割 — 「届く語」であると型 doc に明記するに留めた。分割すると消費層がどちらを見るか選ばされ、emittableを選ぶと copilot のpre_tool_useと gemini のBeforeToolでちょうど永久に待つ ⑤definePullEventSourceへの turn-gate 内蔵 — 状態機械は汎用でもフレーム語彙が完全に opencode 固有なので、pull 型を足すときの必須手順(§4 手順 6′ / §5)にした。裁定は不採用分も含めtests/unit/hooks/sources/launch-contract-1846.test.tsが固定する
Documentation
- docs(cli):
capture --jsonの各フィールドの意味論を明文化する (#1840):
docs/user-guide/cli-operations-guide.md(およびdocs/en/の対応節)の capture 節に、
content/realtimeSnippet/lineCount/isRunning/
sessionStatus・sessionStatusReason/structuredEvents・lastStopEventAtの 6 行表を追加した。
監視スクリプトが実際に踏んでいた 2 つの誤読を名指しで潰している:
contentはlastCapturedLine以降の差分なのでポーラーが先に保存していれば正常時でも空
(src/lib/session/current-output-builder.ts:535-556)、
isRunningは tmux セッションが存在して healthy という意味だけでターン進行中ではない
(src/lib/session/claude-session.ts:543-556)。画面が空かどうかは
realtimeSnippet.trim() === ''とlineCountで見る。あわせて wait 節に、完了判定が
sessionStatus === 'ready'(未分類フレームでない)またはセッション消滅であって
ターンの成立は見ていないことを明記した(src/cli/commands/wait.ts:356)
Fixed
- chore(skills): vendored な cmate-verify が run の途中で自分の
$WORKDIRを消していた欠陥を上流から取り込む (#1864):.claude/skills/cmate-verify/scripts/verify-run.shは.claude/skills/sync-map.jsonの sha256 で counterpart に pin されているため、#1861 でバイト一致させた直後に上流 Kewton/commandmate-skills #228(PR #230 =9604e8f)が欠陥を修正した結果、vendored copy だけが欠陥を抱えたままになっていた。欠陥は「速いゲートほど確実に踏む」もので、ゲートが watchdog の fork より先に終わるとwaitが即座に返り、その直後のkill -TERM "$rga_wpid"(timeout watchdog の後始末)が fork 直後の subshell に届く —— bash は fork した子で signal handler は reset するが EXIT trap の文字列は残すので、子が自分の signal disposition を戻す前に catch 可能な signal を受けるとtermsig_handler() → run_exit_trap()が親から継いだrm -rf "$WORKDIR"を実行し、まだ走っている run の作業 directory が消える。以降のゲートはverify-run.sh: line 734: /tmp/cmate-verify.XXXXXX/gate-<id>.log: No such file or directoryを出したうえでFAIL exit=1 duration=0s(出力なし)= 何も裁定していない不合格を返していた。修正は上流の逐語コピー(963 → 993 行、counterpart とcmpでバイト一致)で、引き金(kill -s KILL= SIGKILL は catch できないので死ぬプロセスの中で shell の code が 1 命令も走らない)と結果(EXIT trap をcleanup_workdir()にし、BASH_SUBSHELLが 0 = top-level shell でなければ何もしない)の両方を塞ぐ。回帰は counterpart から取り込んだ fixtureworkdir-lifetime.yaml(48 ゲートすべてが即座に fail するので runner は全ゲートの log tail を出さねばならない)を 12 回回す run-tests.sh の case 23(4 assertion。48×12 は上流の変異実測で「修正を全戻しすると 20 回中 20 回赤」になる最小単位で、24×6 では 20 回中 15 回しか赤にならない = 4 回に 1 回見逃す回帰テストになる)。あわせて #225 から在った別の flake ——duration=3sの literal 一致に依存した mutex assertion(duration はdate +%sの秒単位計測なので 3 秒の hold が秒境界をまたぐと 4 と読める)を「waitedはちょうど 0」と「duration は 3 秒以上」の 2 つに分割 —— も同じ移植で取り込んだ。suite の assertion は 317 → 322。.agents/skills/cmate-verify/へもミラーし(diff -r無差分)、sync-map の pin を 59 files へ張り直した(counterpart は fixture と run-tests.sh を package の外に置くためcheck --counterpartがscripts/tests/**を MISSING と報告するのは従来どおり構造上の既知差)。実測(macOS 26.6 / bash 3.2 / arm64):bash .claude/skills/cmate-verify/scripts/tests/run-tests.shを 20 回連続で回して 20/20 緑(毎回 322 passed, 0 failed)。No such file or directoryとno output capturedの署名はどの run にも 0 件、not ok行も 0 件。これを受けて #1863 がtest-unitだけubuntu-latestに固定していたci-pr.ymlの TEMPORARY EXCEPTION を外し、他 9 ジョブと同じ fork フォールバック付きself-hosted条件式へ戻した(ARM64 の self-hosted runner は本欠陥を毎 run 再現させていた唯一の環境で、固定はその再現環境を失わせていた) - ci(e2e): Playwright インストールのハングを実測に基づき apt 側で塞ぎ、ブラウザは解決後バージョンでキャッシュする (#1844):
test-e2eのnpx playwright install --with-deps chromiumが繰り返しハングしていた(2026-08-19 だけで 88 分 / 3h55m / 3h48m、その後 #1830 のステップ上限 20 分に当たって run32248871561が赤)。Issue は~/.cache/ms-playwrightのキャッシュを最優先の主因対策として挙げていたが、当日の E2E ジョブ 6 本のステップログをDownloading Chrome for Testing行で二分して実測すると、ブラウザのダウンロードは全サンプルで 0.09〜0.11 分と一定で、バラつきもタイムアウトも 100% apt 側だった(apt フェーズ 0.20m / 3.14m / 7.63m / 9.22m / 10.44m / タイムアウト)。ubuntu-24.04 ランナーでは Chromium の実ライブラリは全て導入済みで、--with-depsが実際に取得するのはフォント 9 パッケージ 21.1 MB だけ(azure.archive.ubuntu.com が 14〜21 kB/s に落ちるのが原因)。tests/e2eに画素比較(toHaveScreenshot/toMatchSnapshot)は 1 件も無く、スクリーンショットは失敗時の調査用のみなので、このフォントが合否を変えることはない。よって 1 ステップを分割し、(1) システム依存playwright install-depsは 6 分予算のcontinue-on-error(超過時は::warningを出して続行、本当に必要なライブラリが欠ければRun E2E testsが Playwright 自身のエラーで落ちる)、(2) ブラウザはactions/cacheで~/.cache/ms-playwrightをキャッシュし、キーはpackage.jsonの^1.56.1ではなく解決後の実バージョン(playwright-core/package.jsonのversion)に紐付け、古いブラウザでのヒットを避けるためrestore-keysは付けない。キャッシュヒット時はダウンロードステップ自体をスキップする - fix(cli):
waitの抑止通知が全ての reason を「契約由来」と名乗る (#1843):formatSuppressionNoticeの前置きがby contract policy固定だったため、#1829 が追加したagent-launch-dialog(codex の起動ダイアログはツール自身の起動シーケンスに任せるという製品側の判断。契約は一切関与しない)まで契約の仕業として表示され、契約を使っていない worktree の運用者が存在しないdenyPatternsを探す羽目になっていた。reason ごとに前置きを出し分け(契約由来の 4 種=mode-off/deny-pattern/deny-pattern-unusable/type-not-allowedは従来文言のまま、agent-launch-dialogは「起動ダイアログ表示中」と述べる)、未知の reason は契約由来を騙らずそのまま名指しする。出し分けはRecord<AutoYesSuppressionReason, string>なので reason を足して文言を決め忘れるとtscが落ち、CLI 側のミラー型がサーバ側の union から離れた場合もtests/unit/cli/config/cross-validation.test.tsの型アサーションでtscが落ちる。wait --jsonのautoYesSuppression.reasonは不変