Skip to content

v0.17.0

Choose a tag to compare

@Kewton Kewton released this 31 Jul 13:28
· 445 commits to main since this release
37748bf

[0.17.0] - 2026-07-31

Highlight: 「完了したが壊れていた」の検出を、人間の目視から exit code へ移したリリース。 実行契約(.commandmate/tasks/*.yaml)・検証ゲート(commandmate verify / wait --verify の exit 0/20/21)・TaskStatus 状態機械・Eval メトリクスという検証基盤一式を導入し(Epic #1539)、それを /orchestrate と /work-plan の委任経路へ配線して既定で検証が走る状態にした(#1580 / #1581 / #1582 / #1583 / #1593)。契約ファイル自身が「作業証跡」に数えられて未着手検出(exit 21)を殺す問題は、work-evidence の commits / uncommitted と scope の committed / uncommitted という対になる 4 経路すべてで塞いでいる — 隔離環境の実機 UAT で、契約・verify.yaml・実作業の 3 パスがある worktree に対し changed=2(契約のみ除外され verify.yaml は数えられる)となることを実測した。あわせてチュートリアル・LP・プロダクト紹介の動画/GIF 素材(#1553〜#1577)と Skill 配布まわりを揃えた。

Added

  • ワーカー委任の標準経路を契約付き send + wait --verify に変更し、監視の完了判定をタスク状態に切り替え (#1581): 検証基盤(#1539 で実機認定済み)は完成していたのに、委任レシピ(.claude/commands/orchestrate.md と orchestrate-monitor skill)は素の send + capture 解析のままで、既定では検証機構が一度も使われなかった。orchestrate.md の Phase 3 を「契約起案(Phase 2-4 で新設)→ send --contract → wait --on-prompt human --verify → exit code 分岐」へ書き換え、exit 0/20/21/10/124 の対応表と、20 の再指示上限 2 回・21 の切り分け手順(composer 未確定/権限プロンプト/未起動)を明文化した。Phase 5 の「ワーカーに lint/tsc/test を実行させて報告文を読む」も commandmatedev verify --json に置き換えている(報告文の解析こそ exit code が置き換えた当のもの)。verify-completion.sh は 完了判定の一次ソースをタスク状態に変更し、capture 由来のヒューリスティクスをフォールバックへ降格した(#1539 設計原則 4)。終局ステータスを 3 分岐にしたのが要点で、succeeded→COMPLETE / failed・cancelled→新トークン VERIFY_FAILED / not_started→NOT_STARTED。ゲート不合格のワーカーは commit も idle streak も「完了」に見えるため、capture 由来の信号だけでは合格と区別できず、COMPLETE 一本ではマージ判断に使えなかった。評価順は「生存ペイン → タスク状態 → capture ヒューリスティクス」で、GENERATING/PROMPT/RATE_LIMIT をタスク状態より先に置いたのは task list --limit 1 が最新タスクを返す以上、過去の契約のあとに素の send をした worktree では古い裁定を読みうるため(生成中のワーカーを COMPLETE と誤報する経路)。未終局ステータス(pending/running/waiting_input/verifying)と空・未知は必ずフォールバックへ落とす — 特に running は重要で、send は送信直後にタスクを running にするので composer に Enter が入らなかったケースが永久に running のまま残り、これで判定すると STARTED ガードの回帰(#1512)をそのまま戻すことになる。データ供給は既存の hooks 機構に合わせて hooks-task.sh を新設し、--hooks を繰り返し指定可能にした(作業量は hooks-git.sh、タスク状態は hooks-task.sh と、契約付き運用では両方要るため)。フックを渡さなければ既定出力は 1 バイトも変わらない。Issue 本文との差分 3 点を実測側に倒している: (1) 本文の「scripts/tests/ の fixture テストを更新」は誤りで、.claude/skills/orchestrate-monitor/scripts/tests/ は存在せず実際の置き場は tests/unit/skills/orchestrate-monitor/(npm run test:unit は vitest run tests/unit なので .claude/skills/** に置いたテストは CI で一度も走らない)、(2) 本文は一次ソースの参照手段として API しか挙げていないが CLI の task list / task show が実在し、シェルからは CLI が素直(base URL と認証トークンの解決を CLI が持っている)。task list <worktree-id> は worktree だけで引けるので、send --contract が stdout に出す task id を監視ループへ渡す必要が無い — #1539 認定 worktree に対する実測で succeeded / failed / not_started の 3 行が新しい順に返り、タスクが無い worktree では stdout が空・通知は stderr であることも確認した(この「空 = 無回答」がフォールバックの入口になる)、(3) 本文は素の send 4 箇所(170/210/216/277 行)をすべて契約付きへ置き換えよとするが、Phase 2.5(根本原因分析)は契約に載せられない — wait --verify は gateIds 未指定で selectGates() が runWorkEvidence: true を返すため work-evidence が必ず走り、コードを変更しない分析依頼は成功するほど exit 21 になる。Phase 2.5 は素の send のまま残し、その理由を本文に 1 段落書いた。本文の行番号(170/210/216/277/181/236)は再検証してすべて一致していた。hooks-task.sh は cut -f2 の結果を製品の TASK_STATUSES で allow-list してから返す(cut はタブが無い行では行全体を、列が入れ替われば別の列を返すので、旧 CLI のエラー文が「ステータス」として到着しうる)。日英ドキュメント(docs/user-guide/workflow-examples.md と docs/en/user-guide/workflow-examples.md)に契約付き並列開発フローを 1 節ずつ追加し、.agents/skills/orchestrate-monitor は存在しないためミラーは作っていない。副次的に monitor.sh の既定 --session-prefix cm が実セッション名と一致しないことを実測で見つけた(tmux ls は mcbd-claude-<worktree-id>、生成元は src/lib/session/claude-session.ts の getSessionName())。tmux send-keys の失敗は 2>/dev/null || true で握り潰されるため、既定のまま回すと承認 Enter も rate limit の a も再送も1 回も届かないまま監視だけが続く。既定値の変更は #1581 のスコープ外(monitor-resend.test.ts が cm-w1 を固定している)なので振る舞いは変えず、SKILL.md・orchestrate.md・日英ワークフロー例のすべての実行例に --session-prefix mcbd-claude を入れ、SKILL.md には実測して渡す理由を書いた。exit 21 の切り分け手順では commandmatedev respond "$WT" "" を書きかけたが、respond.ts が空文字を exit 2 で弾くことを実測して tmux send-keys -t "mcbd-claude-$WT" Enter に直している。全 8 本の .sh が bash 3.2.57 で bash -n 通過(連想配列・mapfile 不使用、空配列は ${#arr[@]} ガード経由でのみ展開)。タスク状態分岐の無効化・failed→COMPLETE 化・生存ペイン veto の順序戻し・allow-list 除去・--hooks の last-wins 化・poll 行からの task= 削除という 6 種の変異注入で、該当テストだけが赤くなることを実測済み(それぞれ 6 / 3 / 3 / 4 / 1 / 10 件)。

  • 検証履歴の read API(GET /api/verification/runs・/runs/:runId)と commandmate verify history / verify show (#1593): verify.yaml の継続改善(#1594)が前提とする一次データの供給口を作った。Issue 本文の現状認識 1 点を実測側に倒している — 本文は「現状の API は検証の起動と集計のみで、ラン単位・ゲート単位の履歴を外部から参照できない」とするが、GET /api/worktrees/:id/verify/runs と GET /api/worktrees/:id/verify/runs/:runId は #1543 で既に実在する。実際に欠けていたのは (1) worktree 横断の一覧、(2) 期間フィルタ、(3) 一覧レスポンスへのゲート verdict 同梱(既存の worktree 別一覧は gate results を一切返さない)、(4) CLI 経路の 4 点で、既存 2 本は据え置き新設 2 本を追加した(棲み分けは「この worktree で何が起きたか」対「このリポジトリで何が起きたか」で、verify.yaml のチューニングが実際に問うのは後者)。DB 層は listVerificationRunsForPeriod(db,{worktreeId?,days?,limit?}) を追加し、順序契約は既存 listVerificationRuns と同じ started_at DESC, id DESC を踏襲した(本文記載の getVerificationRun 286 行 / listVerificationRuns 357 行は実測と一致)。一覧は logTail を型からも SELECT 列からも外している — null を返すのではなくフィールドごと存在しないようにしたので、500 run 分のログ末尾(MB 級)が一覧に混ざることは SQL の段階で起こり得ない。この「含まれないこと」はレスポンス型の目視ではなく、not.toHaveProperty('logTail')・キー集合の完全一致・シリアライズ後の全文検索の 3 通りで DB 層と API 層の両方に固定した。days / limit は DB 層ではクランプ(読み取り専用フィードで、10000 件要求は「取れるだけ欲しい」の意)、API 層では範囲外を 400(ユーザのタイポは黙って丸めるより弾く方が親切)と非対称にしてある。worktreeId はパスではなくフィルタなので該当なしは 404 ではなく空配列。CLI 側は本文のフィールド指定(日時 / worktree / trigger / status / 失敗ゲート名)に先頭の #<run-id> を足した — これが無いと一覧から verify show へ辿る手段が無く、一覧が読めても使えない。history / show は 20 / 21 を返さない(この 2 つは「今のツリーが検証に落ちた」の意で、過去の run への問い合わせは現在のツリーへの判定ではない)ため、引数不正 2 / 404 は 99 / 該当 0 件は exit 0(人間向けは stderr に一行、--json では常に [])に割り当てた。実装上の落とし穴を 1 つ踏んでいる: commander は同名フラグを先に宣言した親に束縛するため、verify が run モード用に持つ --json / --token が verify history --json の指定を吸い、サブコマンド側は既定値のまま human 出力を出す(root program の option scoping を変える enablePositionalOptions() は全コマンドの解析に波及するので採らず、command.parent.opts() へのフォールバックで局所解決した)。ログ本文の一覧への混入・親フラグ継承の切断・期間フィルタの無効化・skipped の失敗ゲート扱い・404 特別扱いの除去・limit 範囲検証の除去という 6 種の変異注入で、追加したテスト 90 件のうち該当分のみが赤くなる(4 件 / 7 件 / 8 件 / 1 件 / 4 件)ことを実測済み。npm run lint / npx tsc --noEmit / npm run test:unit(727 files・13003 tests)/ npm run build:cli / npm run build すべて exit 0。

  • /work-plan の成果物に実行契約(.commandmate/tasks/issue-<N>.yaml)の起案を組み込み (#1582): 実行契約(正準仕様 docs/design/task-contract.md)は「達成条件・変更可能範囲・検証ゲートを着手前に宣言する」レビュー対象の成果物だが、/work-plan(.claude/commands/work-plan.md)は人間向けの計画テキストしか出さず、契約はどの工程でも作られていなかった。「8. 実行契約の起案」を新設し(キー別の書き方の表・契約エラーになる書き方・契約の置き場所と運用)、成果物チェックリスト/出力先に契約を追加、出力フォーマットに v1 の雛形 yaml を 1 本置いた。ja/en の docs/user-guide/commands-guide.md も /work-plan の出力内容・出力先を対で更新している。Issue 本文の列挙から必須キー title が漏れていたため雛形と表に追加した — contract-parser.ts の validateText は title を非空文字列として必須にしており、本文のフィールド列挙(goal / scope.allow / verify.gates / autoYes / success)だけを写した契約は title: required で契約エラーになる。本文の「#1580 以降は未コミット可」も採用していない — #1580 は未マージで、develop の work-evidence ゲート(gate-runner.ts の git status --porcelain 行数カウント)は未コミットの契約ファイル自体を作業証跡として数える。そのまま配布すると「エージェントが何もしていない」(commandmate verify の exit 21)を検出できなくなるため、運用は「送信前にコミットする」と書き、除外は #1580 で扱う旨だけ残した。あわせて scope ゲートは .commandmate/ を常時許可する(scope-gate.ts の ALWAYS_ALLOWED_PREFIX)ので契約ファイル自身を scope.allow に書く必要はないことも明記した。雛形は「読んで大丈夫そう」ではなく実際に parseTaskContract へ通して検証しており(gates は .commandmate/verify.yaml に実在する lint / typecheck / unit に解決)、tests/unit/tasks/work-plan-contract-template.test.ts が同じ検査を CI で恒久化する。このガードは 5 種の変異注入(未知キー追加・verify.gates: []・実在しないゲート id・仕様書参照の削除・scope.allow: [])で当該テストが赤くなることを実測済み。.agents/commands/ は存在しないためミラーは作っていない。

  • report metrics をリリース前チェックと進捗報告の定点に組み込み (#1583): #1551 の Eval メトリクスは実装済みだが見る工程がどこにも定義されていなかったため、.claude/skills/release/SKILL.md(Phase 1 に「1-4. Vibe メトリクス定点観測」)・docs/release-guide.md(事前準備 5)・.claude/commands/progress-report.md(レポート内容 4「検証メトリクス」)に取得手順を追加した。ドキュメント/レシピのみの変更で src/ の製品コードは触っていない。Issue 本文の 2 点を実測側に倒している: (1) 本文は --days が「1..90 にクランプされる」とするが、クランプは起きない — computeVibeMetrics() は内部で clamp するものの CLI も API もそこへ到達せず、CLI は範囲外・非整数を Error: --days must be an integer between 1 and 90. で exit 2(ExitCode.CONFIG_ERROR)、route は 400 を返す(--days 0 / 91 / abc を実行して exit 2 を実測)。したがって手順側に「呼ぶ前に 1..90 へ丸める」shell 計算を明記した。(2) 本文は「サーバ未稼働・データ 0 件(successRate: null)」を同一の degrade として扱うが、この 2 つは別事象である — サーバ未稼働は exit 1(ExitCode.DEPENDENCY_ERROR、Server is not running.)で JSON が一切出ず、> file 2>&1 するとファイルにエラー文が入るためパースは失敗する。データ 0 件は exit 0 かつ tasks.total === 0 の帰結。よって判定は exit code で行い、JSON をパースしてから判断してはいけないことを 3 文書すべてに書いた。degrade は「メトリクスなし」と明記してスキップし、リリースも進捗レポート生成もブロックしない。取得コマンドは exit code を失わない > ファイル 2>&1; echo $? の形で書き、| grep に繋がないことをテストで固定した(パイプすると exit code が grep のものに化ける)。Issue が名指ししていた .claude/commands/progress-report.md は目次にすぎず、実際にレポートを生成するのは .claude/prompts/progress-report-core.md(スラッシュコマンド/サブエージェント両モードが読む共通プロンプト)なので、そちらの Phase 3 と出力テンプレートにも節を追加し、.claude/agents/progress-report-agent.md の Report Structure も合わせた(片方だけ直すとサブエージェント経路=pm-auto-dev が使う経路で無効になる)。省略条件は日次レポートの buildMetricsSection() と同じく tasks.total / verification.runs / intervention.humanResponds / intervention.autoAnswered が全ゼロのときで、null を 0% と書かないことも明記した。.agents/skills/release は存在せず、ミラーも作っていない(このリポジトリのミラー方針は skill 単位で、demo-video / video-to-gif / cmate-verify の 3 つのみが両ルートに置かれる)。新設した tests/unit/docs/release-metrics-checkpoints.test.ts は文書の数値を散文で繰り返さず MAX_METRICS_DAYS / GATE_FAIL_BREAKDOWN_LIMIT / ExitCode を import して突き合わせるので、定数を動かすと文書側が落ちる。MAX_METRICS_DAYS の変更・| grep への差し替え・; echo $? の除去・サーバ未稼働 exit code の改竄・「ブロックしない」の反転・GATE_FAIL_BREAKDOWN_LIMIT の変更・.agents/skills/release の新設という 7 種の変異注入で、追加したテスト 37 件のうち該当分が実際に赤くなり、復元で緑に戻ることを確認済み。

  • work-evidence / scope ゲートの変更集合から契約ファイル(.commandmate/tasks/**)を除外 (#1580): オーケストレータが worktree に契約を置いてすぐ send する軽量フローが成立するようにした。従来は契約ファイル自身が作業証跡として数えられ、何もしなかったエージェントと作業したエージェントが区別できず wait --verify の exit 21(作業証跡ゼロ)が検出できなかったため、「契約を base ブランチへ先にマージする」「セットアップコミットを作って options.baseRef をタグへ固定する」運用が必須だった。除外は CONTRACT_DIR_PREFIX(src/lib/verification/scope-gate.ts)1 箇所に集約し、対になる 4 経路すべてに入れている: work-evidence の commits(rev-list --count <base>..HEAD -- ':(top)' ':(exclude,top).commandmate/tasks/'。:(top) は「除外だけの pathspec」を避けつつ cwd ではなくリポジトリルートに固定する)と uncommitted、scope の committed 側(git diff)と uncommitted 側(git status)。work-evidence の git status を -z --untracked-files=all に変えた — 従来は素の porcelain を行数で数えており、新規の .commandmate/tasks/ が ?? .commandmate/tasks/ の 1 ディレクトリエントリに畳まれるため、パス単位の除外が「パスでないもの」を判定することになる(空白入りパスの C クォート、rename の old -> new 連結も同じ理由で外せない)。porcelain のパーサは scope-gate と共有し、エントリ単位の string[][] を返すよう変えた上で「どのパスも契約ファイルであるエントリ」だけを数えない — rename は 1 エントリが 2 パスを持つため(R <new>NUL<old>)、契約を作業ディレクトリへ持ち出す変更を落とさないためである。.commandmate/verify.yaml は除外していない: 契約本体は送信時に tasks.contract_json へスナップショットされる(src/lib/db/tasks-db.ts)ので後編集が判定に影響せず tasks/ の除外は改竄安全だが、verify.yaml のゲート定義は毎ラン読み直されるため、エージェントが自分のゲートを弱めた場合に scope の deny で検出できる状態を残す。この非対称の理由はコードコメントにも 1 行残した。Issue 本文との差分 2 点を実測側に倒している: (1) 本文は「契約の scope.allow に .commandmate/tasks/** を書かない限りスコープ違反で不合格になる」とするが、ALWAYS_ALLOWED_PREFIX = '.commandmate/' が既に allow の要求から除外しておりこの記述は成立しない — 実際に残っていた穴は changed=N に契約が混じることと、契約が deny を宣言した場合に契約ファイル自身が違反に数えられることの 2 点だった、(2) 本文の受入条件「既存テストが無修正で green」も成立せず、does not count the contract directory against the scope が changed=2 を期待していたため changed=1 への更新が要る(除外の結果として正しい値)。本文のファイルパス・行番号(gate-runner.ts 385-395 / scope-gate.ts 281 / tasks-db.ts 6-8・79-80)は再検証して一致を確認した。rev-list の pathspec 除去・uncommitted フィルタ除去・some→every(rename 分岐)・committed 側のみ除外・uncommitted 側のみ除外・プレフィックスの .commandmate/ への拡大・-z -uall の差し戻しという 7 種の変異注入で、追加した 11 件のテストが該当するものだけ赤くなることを実測済み。

  • チュートリアルに操作 GIF 10 本を埋め込み、Step 3 に「承認待ち」の節を新設 (#1576): docs/user-guide/tutorial.md と docs/en/user-guide/tutorial.md は画像が 1 枚も無く、「リポジトリを追加 をクリック」等の UI 指示がすべて文字だけだったため、#1575 の GIF 10 本(5 シナリオ × ja/en)を Step と 1:1 で埋め込み、各所に「映っているもの」の一文と冒頭の録画方法の注記(隔離環境・private リポジトリ名やソースを含まない・実 LLM ではなくキャプチャ済み端末出力の再生)を添えた。キャプションは docs/images/tutorial/storyboards/*.yaml が宣言するシーンとテロップ、および record-scenes.ts の SCENES が実際に撮る操作だけを根拠に書いており、映像に無いことは主張していない。Step 3 の「承認待ち」は埋め込みのついでではなく本文側の実在する穴で、現在の Step 3 は指示文から結果へ飛び、エージェントが承認を求めて停止しうることに一切触れていなかった(cm-t3-approve-prompt の置き場所が本文に無いこと自体が証拠)。新節では停止時の状態表示・PC とスマホ幅それぞれの応答場所・Auto Yes の存在(有効化はしない)を書き、「このチュートリアルで体験すること」の表にも 1 行足した。Issue 本文との差分 4 点を実測側に倒している: (1) 停止中の一覧表示は本文の「待機中」ではなく common.status.waiting の「応答待ち」(SIDEBAR_STATUS_CONFIG.waiting.labelKey)で、「待機中」は home.waiting すなわち 概要 画面の集計側の語なので両方を書き分けた、(2) cm-t1-add-repository の映像が到達するのは本文の「セッションとして一覧に現れる」ではなく リポジトリ一覧の行(シーンは /api/repositories に path が載ったあと repository-row-* を待って終わる)、(3) UI のラベルは docs で使われてきた「Auto-Yes」ではなく autoYes.label の「Auto Yes」(ハイフン無し)なので UI 名で書いた、(4) GIF 10 本の実測合計は本文の見積り約 4.5MB ではなく 3,184,627 バイト(3.04MB)(to-gif.sh --report 実測。480x300 / 10〜12 秒 / 80〜96 フレーム)。cm-t1-add-repository は本文の手順と一致しないことをキャプションに明記した — 本文 Step 1-2 は クローン URL タブから取り込むが、録画環境がネットワークに出られないためデモは ローカルパス タブでローカルのリポジトリを追加しており(UrlNormalizer.getUrlType が返すのは https:// / git@ / ssh:// の 3 形式だけで、ローカルパスは null になりクローン URL として通らない)、隠すと本文と映像の食い違いに読者が先に気づく。cm-t5-review-changes は「再起動して初めて画面が変わる」の直前に置くにあたり、見出しの無い裸の画像を節の途中に落とさず独立した ### 節にした(既存見出しは 1 つも改名していないので被リンクは切れない)。Step 番号の振り直し(Step 1.5 の解消)はスコープ外として手を付けていない。10 本すべてが各ページからの相対パスで実在すること(ja は ../images/、en は 1 階層深い ../../images/)、ja ページに .en.gif 参照が 0 件・en ページに .ja.gif 参照が 0 件であること、全 10 本が複数フレーム(80〜96)を持ち先頭フレームで静止しないことを実測で確認済み。

  • チュートリアル操作シナリオのデモ 5 本(ja/en の GIF 10 本)と、絵コンテを「実装の部分集合」に緩める変更 (#1575): docs/images/tutorial/ に Step と 1:1 対応する GIF 10 本と絵コンテ 5 本を追加し、record-scenes.ts に add-repository / sync-worktrees / review-diff の 3 シーンを実装した。着手の前提として絵コンテ検証を変えている: storyboard.ts は record id と SCENES の両方向一致を要求していたため、1 つの絵コンテがすべての実装済みシーンを含まねばならず、シーンを 3 本足すと既存 12 個の絵コンテが即座に検証エラーになった(既存 10 本がどれも同じ 4 カットなのはこの制約の帰結)。unused 側を errors から外し、代わりに demo-video.sh が絵コンテの record id から --scene を組み立てて渡す形にした — 「撮った映像を黙って捨てる」ことは構造的に起きなくなるので、制約を緩めたのではなく制約が守ろうとしていたものを設計で保証する形に置き換えている(missing 側は録画時に実際に落ちるので維持)。既存 12 個の絵コンテが無変更で通り続けることは回帰テストで固定した。Issue 本文との差分を 3 点、実測側に倒している: (1) パス指定の登録先は本文の POST /api/repositories ではなく POST /api/repositories/scan(repositories/route.ts は GET と DELETE しか export していない。validate-path は入力中のヒント表示専用で登録経路ではない)、(2) review-diff の同期点は本文の git/diff では成立しない — あれはコミット指定専用で commit が 7〜40 桁 hash でなければ 400 を返し作業ツリーの変更を一切扱えないため、Git ペイン自身が読む git/staged の unstaged に変えた(git/status の isDirty も不可: untracked だけでも true になり、シーンがクリックする git-unstaged-list が空のままテイクが始まる)、(3) 絵コンテ 3 は本文では respond-from-mobile 単独だが、カセットの行が @input で送信を待つため承認フレームが描画されず prepare がタイムアウトするので send-and-generate を手前に置いた(シーンが部分集合として選べるようになったことで露出した依存で、この順序はテストで固定した)。リポジトリ追加・同期の UI には data-testid が 1 つも無く、ローカライズされた文言か CSS クラス連鎖でしか掴めなかったため add-repository-button / sync-all-button / repository-path-input / repository-scan-submit を付与した。サーバレンダリング済みボタンは React が onClick を貼る前に Playwright の actionability を満たすので、最初のクリックが黙って捨てられ 30 秒後に無関係な要素のタイムアウトとして現れる(実際に踏んだ)— clickUntilEffective が観測可能な結果が出るまでクリックし直す。隔離の不変条件は維持(専用ポート 3399・CM_DB_PATH は $HOME 配下・WORKTREE_REPOS は使い捨て seed のみ・停止は記録 PID 経由・clone URL 不使用でネットワークに出ない)。unused の復活・$SCENE_ARGS の削除・awk の全行選択・plan 生成順の入替・staged/unstaged の取り違え・seed ブランチ名の drift・clone タブの使用・viewport の不一致・未実装 id の配置という 9 種の変異注入で、追加したテストが実際に赤くなることを確認済み。

  • LP に特徴デモ 4 本を掲載し、#1272 のメディアガードを出所基準へ作り替えた (#1577): website/ は .github/workflows/pages.yml がビルド無しで素の HTML として配信するため markdown サニタイザを通らず、docs で GIF が必須だった制約は当てはまらない。#1574 の英語版デモから 4 本(parallel-worktrees / status-at-a-glance / approve-from-phone / tmux-in-browser)を website/assets/media/ へコピーし(Pages は website/ しかデプロイしないので docs/ 参照は本番で 404 する)、poster webp つきで gallery 直前の新セクションに置いた。ヒーローには置いていない — screenshot-desktop.webp が LCP 要素かつ og:image で、100KB 予算と eager 読み込みのテストが付いている。website/ の増加は wc -c 実測で 2,567,022 バイト(mp4 4 本 2,422,299 + poster 4 枚 138,728 + テキスト差分 5,995)。同じ 20 秒素材の GIF は 1 本 1.3MB なので mp4 の方が軽い。ガードは <video> 全面禁止・動画拡張子全面禁止の 2 件を、場所と許可リストによる規則へ書き替えた — #1272 が守ろうとしたのは「個人環境で撮った映像が公開ページに載らないこと」であってコンテナ形式ではないのに形式で縛っていたため、同一フッテージの GIF が全チェックを素通りした(docs/images/demo-mobile.gif は現に存在する)。HEAD 時点のガードに 1.4MB の GIF を 1 枚置いて旧テスト 30 件が全部緑のままであることを実測して穴を確認し、新ガードでは同じ GIF が落ちることを確認している。新しい規則は「動くものは website/assets/media/ 配下だけ」「そこに置けるのは ALLOWED_MEDIA に名前があるファイルだけ」(増やす=テストを編集する=隔離環境で撮ったかを人間が判断する地点)「許可リストに載っているのに実在しないファイルがあれば落ちる」(リストが名ばかりに腐るのを防ぐ)で、旧素材名の参照禁止・5MB 上限・og:image 固定は維持した。preload="none" の黒板を避ける poster の実在、iOS Safari が autoplay に要求する muted / playsinline、レイアウトシフトを防ぐ width / height も固定している。prefers-reduced-motion: reduce では main.js が autoplay を外して controls を出す(CSS では autoplay を止められない)。Issue 本文との差分 2 点を実測側に倒した: (1) 本文は「5MB 上限は現在動画拡張子にしか掛かっていない」とするが、当該テストは walk(website/) の全ファイルをサイズで見ており拡張子非依存なので GIF にも元から掛かっていた(穴は拡張子リストを持つ「動画ファイル禁止」テストの側だけ)、(2) 掲載位置は本文指定の gallery セクション内ではなくその直前の独立セクションにした("On desktop and on mobile" という見出しの下に特徴デモ 4 本は乗らない。ヒーローに置かないという制約は満たしている)。Chromium 実機で 4 本とも autoplay・ループすること、reducedMotion: 'reduce' コンテキストでは 4 本とも paused かつ controls で currentTime が 0 のままであることを Playwright で確認済み。許可リスト外メディアの設置・6MB GIF・assets/media/ 外への動画設置・muted 除去・playsinline 除去・poster の docs 参照化・デモ 3 本化・ヒーローへの <video> 混入・reduced-motion 処理の差し戻し・poster ファイル削除の計 11 種の変異注入で、追加・改訂したテスト 38 件が実際に赤くなることを確認済み。

  • video-to-gif スキル: 録画済みの動画を、GitHub の markdown ビューアが実際に再生できる GIF に変換する .claude/skills/video-to-gif/scripts/to-gif.sh を追加した(.agents/skills/ にも byte-identical で配置)。既存の demo-video の compose.sh --gif は合成パイプラインの途中でしか発火せず 720px/12fps/sierra2_4a 固定だったため、完成済み mp4 から docs 用の GIF を作る経路が無く、その都度使い捨てスクリプトを書いていた。既定の dither を切った — 同一素材(20 秒 / 1280x800 の UI キャプチャ)を無劣化リファレンスに対する SSIM で実測したところ、colors=256 dither=none が 1,073,725 バイト / SSIM 0.99653、従来の colors=128 dither=sierra2_4a が 1,271,671 バイト / SSIM 0.99160 で、ディザは 2 軸とも悪化させる(空間ノイズが GIF の LZW run を壊し、画面録画の大半を占める平坦な UI パネルには均すべきグラデーションが無い)。colors=64 + sierra2_4a が 1,472,301 バイトと colors=128 の同ディザより大きくなる逆転も併せて確認しており、パレットが粗いほど誤差拡散が遠くまで広がるためである。--max-bytes(既定 1.5M)を満たすまで幅・fps・パレットを 3 軸ラウンドロビンで一段ずつ落として再試行し(fps を底まで落としてから解像度に手を付ける順序にはしない — 4fps 等倍も 360px 30fps も中間の妥協より読めない)、底まで行っても収まらなければ exit 1 でファイルを書かない(--allow-oversize で書けるが exit 1 は維持)。サイズは一貫して wc -c で測る — APFS では du -h が 1,536,216 バイトのファイルを 2.3M と報告し、実際にこの差で総量を 38MB と 25.8MB に読み違えかけた。--ladder は ffmpeg を一切呼ばずに再試行の段取りだけを出すので、CI ランナー(ffmpeg 無し)でも降下の算術と引数ゲートを実行できる。トリムは -ss / -t をいずれも入力オプションとして -i の前に置く — 引数列の末尾に足すとパレット入力の入力オプションとして解釈され、--duration 6 が 16 秒の GIF を吐いた。幅の縮小率・fps 下限の無視・ラダーの打ち切り・パレット半減の無効化・du での計測・引数検証順序の入替を含む変異注入で、追加したテスト 43 件が実際に赤くなることを確認済み。

  • Eval メトリクス(タスク成功率・検証合格率・人間介入回数)の集計と API / CLI / 日次レポート統合 (#1551): 「ハーネスが Vibe Engineering を成立させている度合い」を、Phase 1〜3 が残した実記録(tasks / verification_runs / verification_gate_results / task_events)から集計する src/lib/metrics/vibe-metrics.ts、GET /api/metrics/vibe?days=7、commandmate report metrics [--days N] [--json]、日次レポートプロンプトの <verification_metrics> セクションを追加した。読み取り専用で migration は追加していない。分母ゼロの比率は null であって 0 ではない — 「12件中0件成功」と「そもそも0件」を 0.0% という同じ文字列で報告するのは嘘になるため、CLI もプロンプトも n/a と描く。集計元テーブルは毎回 sqlite_master で存在確認してからクエリするので、v49〜v51 未適用の旧 DB ではセクション単位でゼロに degrade する(日次レポート生成が、誰も要求していない機能のせいで旧 DB で落ちることを避けるため)。gateFailBreakdown は failed / timeout のみ計上し skipped / error は除外する(意図的スキップも実行前エラーも作業への判定ではなく、数えると「失敗していないゲートが失敗として並ぶ」)。再指示ループ数は message_sent(from_status が failed / not_started)を分子、その期間に不合格へ入った distinct task 数を分母とし、両方を task_events 単独から取る(tasks と join すると v51 以前の履歴が分母にだけ効いて平均が歪む)。介入回数は状態機械に拒否されたイベントも数える — 検証中に届いたプロンプトへ人間が答えれば機械は遷移を拒否するが、人間は現に手を止めて答えているのであって、それがここで測っている費用そのもの。suppressedByPolicy は Phase 2-3 の抑止ログが DB 化されていないため v1 は null 固定(数値を入れると発明になる)。日次レポートの window は Date.now() ではなく対象日に固定した(過去日のレポートが「直近24時間」を語ってしまうため)。プロンプト側は活動ゼロの日にセクションごと省略し、gate id は verify.yaml 由来のため sanitizeMessage でタグをエスケープする(</verification_metrics> を含む gate id でセクションを閉じられないこと、および長さフォールバック時に落ちるのが metrics ではなく user_data 側であることをテストで固定)。テストは seed から手計算した literal を期待値にしており(実装と同じ式で導出すると同じ誤りに両方が同意する)、日次レポートのテストは prompt builder も集計器もモックせず実 DB の行が実数としてプロンプト文字列に入ることを assert する(キー名だけの一致は空セクションでも通るため)。境界(since は含み until は含まない)・分母ゼロの null・skipped の除外・初回 message_sent を再指示に数えない・テーブル不在時 degrade・日次 window の固定など計 28 種の変異注入で、追加したテストが実際に赤くなることを確認済み。

  • 構造化イベント受け口 POST /api/hooks/agent-event と Claude / Codex 配線を追加 (#1549): エージェントの完了検知を tmux 画面の文字列解析だけに頼らず、エージェント CLI 自身が発するイベント(Claude Code の Stop hook、Codex の notify)を第一級ソースにする。文字列解析は廃止せずフォールバックとして残す(hook は設定した人の環境にしか無いので、未設定機で黙って何もしない完了検知は、どこでも不正確な完了検知より悪い)。worktree の解決キーは cwd で、.. を含むパスは正規化せず 400 で拒否する(resolve() を通すと検証時と使用時で値の意味が変わり、それが traversal 検査の破られ方そのもの)。percent-decode 後も再検査し、実在する worktree に解決される traversal 表記も拒否することをテストで固定した。解決成功も失敗も 202 {accepted:true} で同一ボディを返す(外部から叩ける経路なので、応答差が「登録済みディレクトリの探索器」になる)。event: stop は active task に agent_idle(payload {source:'hook'})を記録し、契約に success.autoVerifyOnStop: true(契約 v1 に追加。既定 false — 他 2 つの success フラグが「判定基準」なのに対しこれだけサーバに動作を起こさせるフラグで、本フィールド以前の契約が Stop hook 設定だけで検証ランを走らせ始めてはならない)があれば startVerification(trigger='task') を起動する。claude-done は message 記録の従来動作をそのまま保ち、末尾で同じ stop 処理へ委譲する。lastStopEventAt を CurrentOutputPayload に露出するのみとし、wait / ポーラー / Auto-Yes の完了判定は一切変更していない(二重ソースを実測前に切り替えるのは「既知の不正確さ」を「未知の失敗モード」と交換することになるため、判定への組込みは後続 Issue)。同梱スクリプト scripts/hooks/cmate-agent-event.sh は bash 3.2 互換で、POST 失敗でも exit 0 で終わる(サーバ停止でエージェントのセッションを壊さない)。設定手順は docs/user-guide/agent-event-hooks.md。Issue 本文との差分 1 点を実測側に倒した: 本文は終端タスクへの stop も「拒否として記録される」ことを含意するが、getActiveTaskForInstance が active status(running / waiting_input / verifying)しか解決しないため succeeded などの終端タスクでは state machine に到達せず行も残らない(拒否行が残るのは verifying 中の stop)。両者を別テストとして固定した。traversal 検査除去・source payload 除去・autoVerifyOnStop 既定反転・auth 除外リストへの追加・202 ボディの情報漏れ・stop 効果の無効化・委譲の削除・露出の削除・bash4 構文混入の計 9 種の変異注入で、追加したテスト 59 件が実際に赤くなることを確認済み。

  • TaskStatus 状態機械と task_events 追記専用イベントログを追加 (#1548): Phase 2-1 では各所が updateTaskStatus に次の status を自分で決めて渡していたため、遷移規則が 6 箇所に分散し互いに一致しているのは偶然でしかなかった。純関数 src/lib/tasks/task-state-machine.ts に規則を集約し、src/lib/tasks/task-transition-service.ts の applyTaskEvent を tasks.status の唯一の writer として、updateTaskStatus / insertTaskEvent は @/lib/db バレルから re-export しない形に閉じた。migration v51 の task_events は status の変更ではなく変更しようとした試みを記録する追記専用ログで、Phase 4 Eval の「介入回数」「再指示ループ回数」はこの表から数える(最新値しか持たない status 列からはどちらも答えられない)。不正遷移は throw せず to_status = NULL の行として記録する — 検証中のプロンプト検出は競合であってバグではなく throw するとポーラが落ちるが、黙って捨てると配線バグと競合が区別できなくなる。UPDATE と INSERT は同一トランザクション(status だけ動いてイベントが無い/その逆は、どちらもログが表を説明できなくなる)。発火点は send --contract(message_sent)・response-checker のプロンプト保存(prompt_detected)・auto-yes-poller の応答成功(prompt_answered_auto)・/respond と /prompt-response の両 route(prompt_answered_human)・gate-runner の run 開始/終了(verify_*)・新規 POST /api/tasks/:taskId/cancel(cancel)で、いずれも active task が無ければ何もしない(契約なし運用への影響ゼロ。回帰テストで固定)。Issue 本文との差分 3 点を実測側に倒した: (1) migration 番号は本文の v50 ではなく v51(v50 は #1545 が使用済み)、(2) 本文のイベント表に無い send_failed を追加 — send --contract の配送失敗報告は Phase 2-1 で既に動いており、直接 UPDATE を閉じる際に落とすとタスクが永久に pending に残る、(3) respond route は候補が 2 つ実在しどちらも人間の応答経路(/respond は chat の prompt ボタン専用、/prompt-response は端末プロンプト UI と commandmate respond CLI)なので両方に配線した。gate-runner の終端 status 事前チェックは廃止し可否を状態機械に委ねた — succeeded/cancelled は機械が拒否する一方、再実行で正当に開き直る failed/not_started を status チェックでは区別できなかった。遷移表は 8 status × 11 event の全 88 セルを、実装から導出せず literal な期待表として有効・無効の両方で固定し、satisfies never で列挙追加時にビルドが落ちるようにした。トランザクション除去・UPDATE/INSERT 順序入替・発火点削除の変異注入で、追加したテストが実際に赤くなることを確認済み。

  • スコープ検証(git diff × scope glob)を組み込みゲート scope として追加 (#1546): 実行契約の scope.allow / scope.deny に対して実際の変更ファイル集合を突き合わせ、ワーカーが担当範囲外(共有設定・無関係モジュール)を触る事故を機械検出する。src/lib/verification/scope-gate.ts を work-evidence の直後・コマンド系ゲートの前に実行し、success.requireScopeClean が true の契約では verify.gates に書かなくても自動で走る(書いてあるのに走らないと「スコープを守る」と宣言して誰も確認しない契約が成立する)。判定対象は git diff --name-only -z --no-renames <merge-base> HEAD と git status --porcelain -z --untracked-files=all の和集合で、未追跡ファイルと rename の両パスを含む。git のオプションはいずれも偽判定を避けるために必要である — 人間向け書式は空白入りパスを C クォートし rename を old -> new に繋ぐので分割すると存在しないファイルが生まれ、-uall が無いと新規ディレクトリが ?? dir/ に畳まれてゲートがファイルでなくディレクトリ名を判定し、--no-renames が無いと移動元ディレクトリが空になった事実が消える。glob は自前解釈の小さな部分集合とし、[ ] をリテラルにした — Next.js の src/app/proxy/[...path]/ を文字クラスと解釈すると当該ディレクトリを指すパターンが何にもマッチせず無関係なパスにマッチする。またディレクトリを指すパターンはその配下すべてにマッチする(src/lib ≡ src/lib/**)。この規則が無いと、契約作者がディレクトリを書く最も自然な表記が中の全ファイルを違反にしてしまう。.commandmate/ 配下と契約ファイル自身は allow の要求から除外するが、明示的な deny は効く。scope の skipped は gateIds で名指しされたときだけ run 集計に数える — 既定ゲート集合に常に含まれるので、skipped を一律 error に倒す既存規則をそのまま適用すると契約を使っていないリポジトリの検証がすべて error になる(名指し=「断った」/契約なし=「判定すべき宣言が無い」)。glob ライブラリは直接依存に追加しなかった — picomatch / minimatch はいずれも推移依存のみで型定義も無く、この worktree の node_modules は兄弟 worktree と 14 本の hardlink を共有しているため、並列ワーカー稼働中の再展開は他の作業を壊しうる(判断理由は docs/design/task-contract.md §2.2.5)。glob 解釈・git 呼び出し・allow/deny 優先順位・ゲート集計・契約解決の計 32 種の変異注入で、追加したテストが実際に赤くなることを確認済み。

  • 実行契約(Task Contract)を導入 (#1545): .commandmate/tasks/<name>.yaml v1(正準仕様 docs/design/task-contract.md)・migration v50 の tasks テーブル・契約パーサ src/lib/tasks/contract-parser.ts・commandmate send --contract / commandmate task list|show・GET|POST /api/worktrees/:id/tasks / GET|PATCH /api/tasks/:taskId を追加。送信メッセージは契約前文+goal で、完了条件は verify.yaml の gates[].command を解決した実コマンドで書かれる。startVerification は taskId 未指定でも active task を解決して契約の verify.gates を既定ゲートにし、run 結果で task を succeeded / failed / not_started へ遷移させる(wait --verify は CLI 変更なしで契約検証になる)。契約は本フェーズでは宣言であって強制ではない(scope ゲートは #1546、autoYes enforcement は #1547)。未知キーは Issue 本文の「無視」ではなく verify-config.ts の実測作法どおり拒否する(allowPromtTypes の綴り間違いが黙って通ると「縛っているように見えて縛っていない契約」が生まれる)。denyPatterns は正規表現としてコンパイル可能であることを検証し 200 文字上限(ReDoS 対策)。succeeded はクライアントから報告できない(API が 400 で拒否)— 「エージェントが完了と言った」を合格として受け入れないため。契約ゲート既定の削除・run 結果マッピングの常時 succeeded 化の 2 種の変異注入でテストが実際に赤くなることを確認済み。

  • .commandmate/tasks/*.yaml(実行契約)を Git 追跡対象にし、追跡ポリシーを可視化 (#1545 の前提整備): .commandmate/ はランタイムデータ置き場のため全体を除外しているが、その中の設定(verify.yaml・実行契約)はチームで共有すべきものなので追跡する必要がある。Phase 2-1 が契約を .commandmate/tasks/<name>.yaml に置く設計のため、着手前に規則を整えた。負パターンを 1 行足すだけでは効かない — git は除外されたディレクトリの中を走査しないので、ディレクトリを除外解除 → 中身を再除外 → 拡張子で許可、の 2 段構えが要る(#1540 で verify.yaml が踏んだ罠と同型)。利用者が状態を知る手段として scripts/check-commandmate-tracking.sh(npm run check:tracking)を追加し、追跡されるべきファイルと隣に置かれうる無視されるべきファイルの両方を一覧で報告する。規則を壊すと正しい書き方のヒントつきで exit 1 で落ちる。CI ガード tests/unit/config/commandmate-tracking.test.ts と設計文書 docs/design/commandmate-directory-tracking.md も追加した。判定には git check-ignore --no-index を使う — 既定では index が参照され、すでに追跡済みのファイルはルールに関係なく「無視されない」と報告されるため、verify.yaml の例外行を削除する変異を注入してもテストが緑のままだった(空振り緑)。--no-index 追加後は当該変異を含む 3 種の変異注入すべてでテストが実際に赤くなることを確認済み。

  • 検証ゲート Skill cmate-verify と .commandmate/verify.yaml v1 仕様を追加 (#1540): 検証ゲートの設定形式を製品実装(Phase 1)より先に実地検証するための先行 Skill。docs/design/verification-config.md を正準仕様とし、.claude/skills/cmate-verify/ と .agents/skills/cmate-verify/ へ byte-identical に配置(Claude は前者、Codex / Antigravity は後者しか読まない)。ランナーは bash 3.2 互換で、ゲートを定義順に逐次実行し失敗しても残りを実行して全結果を報告、判定は必ず実 exit code で行う(出力の grep は $? を隠す)。macOS に timeout(1) が無い前提で、job control によるプロセスグループ単位の timeout kill を実装(直接の子だけを kill すると孫が生き残る/signal 送出前に pgid が pid と一致することを確認して無関係なプロセスグループを巻き込まない)。組み込みゲート work-evidence が「作業の痕跡ゼロ」を not_started として弾き、skipInPrimaryCheckout がメイン checkout でのコマンド実行を止める(全 skip は passed ではなく skipped)。fixture ベースの自己完結テスト 123 アサーション(vitest 非依存)+ vitest ラッパで CI からも実行。

  • .commandmate/verify.yaml の型安全なローダ/バリデータを追加 (#1541): 正準仕様 docs/design/verification-config.md を製品コード側で実装した src/lib/verification/verify-config.ts(yaml パッケージを dependencies に追加)。loadVerifyConfig(repoPath) はファイルが無ければ null、パース/検証に失敗すれば 全違反を issues に集約した単一の VerifyConfigError を throw する(1件目で打ち切るとエージェントが直すたびに次の1件が出て往復が増えるため)。既定値は仕様どおり timeoutSec 600 / maxLogTailBytes 8192 / skipInPrimaryCheckout true、baseRef は null(デフォルトブランチ解決は利用側の責務)。未知キーはトップレベル / gate / options とも設定エラーとして拒否する — Issue 本文は「前方互換のため無視」としているが、仕様 §2.1「未知のトップレベルキーは設定エラー」・§8「v1 は閉じた集合」と Phase 0 参照実装(verify-run.sh の unknown top-level key / unknown gate key / unknown options key)の双方がこれを拒否しており、黙って読み飛ばすと「設定したつもりのゲートが走っていないのに passed」になるため、仕様と実測を正とした。bash 参照実装は awk パース前に外側クォートを剥がすため timeoutSec: "900" を 900 と解釈する。同じ verify.yaml が Skill と製品の両方で読めることが仕様 §1 の目的なので、整数・真偽値の正準リテラルに限りクォート形も受理する。既定 maxLogTailBytes を 32768(リポジトリ自身の明示上書き値)に変える/未知キーを無視する/違反1件目で打ち切る/予約 ID を空にする/ファイル無しで throw する等 7 種の変異注入で、追加したテスト 55 件が実際に赤くなることを確認済み。リポジトリ自身の .commandmate/verify.yaml が実際に読めることも受入テストで固定した。

  • 検証ゲートの実行結果を永続化する verification_runs / verification_gate_results テーブルと DB アクセサを追加 (#1542): exit code を持つ実行記録は execution_logs(スケジュール実行)と assistant_executions(非対話アシスタント)にしか無く、インタラクティブなエージェント作業は chat_messages しか残らないため「終わった」と「要求を満たした」を区別できなかった。migration v49 で 1 検証試行 = verification_runs 1行 / その中の 1 コマンド = verification_gate_results 1行として記録する。gate results は run に ON DELETE CASCADE(run 抜きの gate 行は記録ではなくノイズであるため)。status / trigger の語彙は writer 側の検証だけでなく DB の CHECK 制約で固定した — この表の存在意義は語彙が区別する内容そのものであり、typo が新しい status 値として着地すると誰も query しないバケツが黙って生まれる。run の not_started(作業証跡ゼロでそもそも検証対象が無い)は failed(判定した上で不合格)と別値、error はゲート実行以前の内部エラー、gate の skipped は意図的スキップで理由を log_tail に残す(スキップが PASS と読まれないため)。task_id は Phase 2(#1545)の tasks 到着まで FK を張らない自由カラムとした(存在しない表への REFERENCES は foreign_keys ON 下で全 INSERT を失敗させる)。アクセサ src/lib/db/verification-db.ts は create* が running で開き finish* が閉じる二段構成とし(途中クラッシュを recoverable な running 行として残すため)、finish* は対象行が無ければ throw する(黙って no-op にすると、記録していない verdict を記録したと呼び出し側が信じる — まさにこの表が防ぐべき失敗)。listVerificationRuns は started_at DESC, id DESC で、同一 ms の tie でも順序が全順序になるよう id を tiebreak に使う。id tiebreak 削除・?? null を || 化(exit code 0 が NULL に化ける)・finish* の存在チェック削除・CHECK 制約削除・CASCADE 削除・index 削除・feed の ORDER BY 差し替え 3種・gate の run スコープ喪失、計 11 種の変異注入でテストが実際に赤くなることを確認済み。

  • 検証ゲート実行エンジン gate-runner と検証 API を追加 (#1543): .commandmate/verify.yaml のゲートを worktree の cwd で逐次実行し、判定を実 exit code で下して verification_runs / verification_gate_results に永続化する。POST /api/worktrees/:id/verify は 202 で runId だけ返し(ゲートはテストスイートなので接続を数分保持しない)、GET /verify/runs / GET /verify/runs/:runId で証跡を読む。組み込み work-evidence が「コミットも未コミット変更も無い」を not_started として弾き、以降のゲートを skipped にする。skipInPrimaryCheckout(既定 true)は worktreePath の realpath がサーバプロセスの process.cwd() と一致したらコマンド系ゲートを実行しない — 稼働サーバの足元で build を回して配信中の chunk を壊した事故が実際に 2 回あったため。skipped が 1 つでもあれば run は passed にならず error とし、「チェックしなかった」が「チェックして問題なかった」に化けないようにした。失敗したゲートで run を打ち切らず全ゲートを実行する(1 往復ごとに問題が 1 件ずつ出るのを避けるため)。timeout はプロセスグループへ SIGTERM→5 秒→SIGKILL(shell: true の子だけを kill すると孫が worktree を掴んだまま残る)。log_tail はチャンク境界に依存しないバイト厳密な末尾 maxLogTailBytes — 当初のチャンク単位破棄は「4KB 書込+改行 1 バイト」で改行だけを tail として残す欠陥があり、変異注入テストで発見して修正した。同一 worktree の多重実行は 409、全体同時実行は 2 本のセマフォで待たせる。再起動で孤児化した running 行は起動時 reconcile が error に倒す(放置するとその worktree が永久に 409 で塞がる)。gate-runner 14 種・API/reconciler 15 種の変異注入で、追加したテストが実際に赤くなることを確認済み。

  • デモ動画収録スキル demo-video の基盤を追加 (#1553): 隔離デモ環境(使い捨て seed repo・専用ポート・$HOME 配下 DB・自前プロセスグループ)、キャプチャ済み ANSI カセットを tmux pane へ再生してサーバの status-detector/response poller/UI を実物のまま駆動する偽エージェント(LLM のみ差し替え・モックゼロ)、@playwright/test をライブラリとして使う scene 単位 webm 録画の 3 点。.claude/skills と .agents/skills へ byte-identical 配置。

  • demo-video に絵コンテ・日英テロップ・ffmpeg 合成・30 秒尺検証ゲートを追加 (#1554): demo-video.sh 一発で demo-30s.ja.mp4 / demo-30s.en.mp4 が出る。文言の編集点は storyboard/default.yaml だけで、シーンの尺もテロップの in/out も合成の切り貼りもそこから機械算出する(手書きタイムコードは無い)。ロケールはフル録画方式で、UI ごと切り替えて 2 回撮り、遷移ごとに <html lang> を実測して不一致ならテイクを失敗させる(「UI 言語がテロップ言語と一致」を目視でなく機械で担保)。テロップは HTML→透過 PNG→ffmpeg overlay で焼き込み(drawtext の日本語 fontfile/エスケープ問題を回避し意匠を CSS 1 箇所に集約、文字列は textContent 注入なので絵コンテがマークアップを注入できない)。尺は ffprobe 実測が 30.0±0.5s を外れたら exit 1。承認シーン用にカセットへ実キャプチャ由来の承認プロンプトを追加し、{{TASK}} 置換(承認後も元の指示を echo し続ける)と Scene.prepare(API 待ちを録画開始前に行う)を導入した。生成物はリポジトリ外に出しコミットしない(配布は Release アセット)。

  • 実行契約の autoYes ポリシーを Auto-Yes リゾルバへ enforcement (#1547): resolveAutoAnswer() は「デフォルト選択肢に常に応答する」素朴実装で、契約に何を書いても自動応答は制約されなかった。resolveAutoAnswerWithPolicy() が off / safe / allow-listed / denyPatterns を評価し、auto-yes-poller.ts の detectAndRespondToPrompt が応答直前に active task の契約を引き当てて渡す(Auto-Yes は status-detector を経由せず detectPrompt を直接呼ぶため、ここが唯一の enforcement 点)。ポリシーは抑止しかしない — 従来ルールが答えを出さないプロンプトを応答に変えることはなく、契約なし・policy=null・mode:null は従来動作と完全一致(回帰テストで固定)。denyPatterns は mode:null でも効かせる(列挙したのに何も守らないのは契約の最悪の失敗モード)。マッチ対象は質問文+instructionText+全選択肢ラベル(Claude は承認対象のコマンドを質問文の上に置くため質問文だけでは効かない)。パターンは validateStopPattern(長さ・safe-regex2・構文)で審査し評価不能なパターンは抑止側に倒す、マッチ対象テキストは 20,000 文字までの決定的な境界で切る(壁時計 timeout に判定を委ねない)。getActiveTaskForInstance() を追加 — primary インスタンスは instance_id が NULL と tool id の両方で記録され = ? ではどちらも確実に引けないため、codex-2 の契約が codex を縛る事故を防ぐ。ポリシーは 5 秒 TTL でキャッシュする(抑止されたプロンプトはポーラーの重複ガードに載らず毎 poll 再評価されるため)が、DB 失敗時は従来動作へ倒す(契約を持たない既存ユーザーを巻き込まないため)。抑止時は poller:auto-yes-suppressed-by-policy を理由付きで記録し、人間への通知は既存の prompt→WS broadcast→Web Push 経路が担う(この依存関係も統合テストで固定)。ポリシー無視・評価不能パターンの fail-open 化・マッチ対象の縮小・テキスト境界の撤去・インスタンス絞り込みの喪失、計 7 種の変異注入でテストが実際に赤くなることを確認済み。

  • commandmate verify コマンドと wait --verify / --require-work を追加 (#1544): 検証ゲートを CLI から起動し、wait の成功条件を「エージェントが止まった」から「検証に合格した」へ引き上げる。exit code は 合格 0 / ゲート不合格 20 / 作業証跡ゼロ 21 / タイムアウト 124 で、判定不能(error・cancelled)は 20 ではなく 99 に倒す(「判定できなかった」を「判定してダメだった」と読ませないため)。検証は完了検知できた worktree だけに走り、プロンプト検出(10)やタイムアウト(124)はそのまま返す。複数 worktree では完了検知は並行・検証は直列(サーバ側が同時実行数 2 に制限しているため並行させても queue するだけ)、集約の優先順位は 10 > 20 > 21 > 124。直列実行の並行化・not_started→0 への写像・優先順位の入れ替え・プロンプト時のガード除去・timeout 判定を終端 status 判定より前に置く、の 5 種の変異注入でテストが実際に赤くなることを確認済み。

Changed

  • セッション状態語彙の変換を src/lib/session/status-mapping.ts へ一元化 (#1550): SessionStatus(検出)・boolean 三つ組(isRunning / isWaitingForResponse / isProcessing)・BranchStatus(表示)の相互変換が 3 ファイルに散在していたため、変換だけを 1 モジュールへ移して対応表をゴールデンテストで固定した(挙動変更ゼロ)。deriveCliStatus は @/types/sidebar から移設し同名で re-export(既存 import と既存テストは無修正)、worktree-status-helper.ts の status === 'waiting' / === 'running' インライン式は sessionStatusToActivityFlags()(satisfies never で exhaustive)に置換、src/app/api/worktrees/route.ts に private 定義されていた逆方向の deriveSessionStatus(三つ組 → SessionStatus)も同モジュールへ移した。UIPhase は waiting → receiving → complete が履歴依存のシーケンスであり SessionStatus の写像ではないので、唯一の表引き(プロンプト検出 → prompt)を持つ worktreeUIReducer に残し、本モジュールには写像関数を作っていない。既存の worktree-status-helper.test.ts は検出器を常に status: 'ready' でモックしており移設対象の running / waiting 分岐を一度も通らないため、SessionStatus 全値を helper 経由で駆動する回帰テストを別ファイルで追加した。マッピング 4 種+ helper 配線 1 種の変異注入で、追加したテストが実際に赤くなることを確認済み。