Repository navigation
v0.17.0
[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 分岐」へ書き換え、exit0/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を踏襲した(本文記載のgetVerificationRun286 行 /listVerificationRuns357 行は実測と一致)。一覧は 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 検査除去・sourcepayload 除去・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 respondCLI)なので両方に配線した。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>.yamlv1(正準仕様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.yamlv1 仕様を追加 (#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件が出て往復が増えるため)。既定値は仕様どおりtimeoutSec600 /maxLogTailBytes8192 /skipInPrimaryCheckouttrue、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_runs1行 / その中の 1 コマンド =verification_gate_results1行として記録する。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_keysON 下で全 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 種の変異注入で、追加したテストが実際に赤くなることを確認済み。