diff --git a/changelog.d/3438.md b/changelog.d/3438.md new file mode 100644 index 000000000..fa07a7752 --- /dev/null +++ b/changelog.d/3438.md @@ -0,0 +1,2 @@ + +- **fix(agent-health): 日次確認の報告にツールごとの起動モデルを残す** (#3438): pass の check は evidence を残さないため、どのモデルで確かめたかが報告から読めなかった。レポートの各ツールに `launchedModel`(画面・hook から読めたモデル。読めなければ「不明」。opencode / opencode-v2 は複製した `model.json` の `recent[0]` も並べる)を足し、要約の最後に「起動したモデル:」を出す。この項目の無い古いレポートは「不明」として読む。 diff --git a/docs/module-reference.md b/docs/module-reference.md index 2932167ab..cadea7f8a 100644 --- a/docs/module-reference.md +++ b/docs/module-reference.md @@ -308,8 +308,8 @@ const myState = getOrInitGlobal('__myState', () => new Map()); | `src/lib/hooks/permission-adjudication.ts` + `permission-decision-state.ts` + `pending-decision-recheck.ts` + `structured-decision-response.ts` | **CommandMate 自身が握っている承認の裁定**(Issue #1898)。hook 系 5 ツールは「エージェントが待っている要求の body」で答えるので裁定は 1 箇所で終わるが、opencode は SSE で受けて別 POST で返すため、裁定する契機が**フレーム到着 / 再接続 / ポリシー変更**の 3 つに分かれる。`permission-adjudication.ts`=その 1 実装(`resolvePermissionRequest` は共有のまま。**裁定 → 送達 → 記録**の順で、`settled` は `delivered && capabilities.permissionReplyReleasesPrompt`)。`pending-decision-recheck.ts`=ポリシー変更時の再裁定(`capabilities.resync !== 'none'` のソースだけ。契機は `POST /api/worktrees/:id/auto-yes` の `enabled:true`。採用上限 `MAX_RECHECKED_DECISIONS`=50 で超過は件数を露出)。`structured-decision-response.ts`=`respond` の構造化経路(`capabilities.eventIdentity` が id を宣言するソースのみ。**decisionId をクライアントから受け取らない**=解決済み target の保留分から番号で選ぶので DR4-003 / S6 を構造で満たす。`yes` は**最も狭い allow の `once`** へ、`--default` は拒否)。`permission-decision-state.ts`=`structuredEvents.permissionDecision` の露出(`tool-input-normalization-state.ts` と同型・**読み戻さない**)。送達したかどうかは `pending-decisions.ts` の `DecisionSlot.delivery` / `recordDecisionDelivery` / `answerPendingDecisionWithReceipt` で受け取る — 従来 `decide()` の中でログに出して捨てていた値で、**送達できなかった allow は人間を塞いだまま**なので release の可否を分ける **Issue #2039: `structured-decision-response.ts` に question の写像を追加**。`questionDecisionOptions(spec)`=第 1 問の選択肢を `StructuredDecisionOption`(`{number,label,reply}`、`reply` はラベルそのもの=wire 値)で返す — 承認と question を 1 つの形で読めるようにして #2040 の番号写像を可能にするための意図的な型共有。`resolveStructuredQuestionAnswer(spec, answer)`=番号(`1` / multiSelect なら `1,3`、重複は畳む)→ ラベル(大小無視)→ 自由入力(`MAX_QUESTION_FREE_TEXT_LENGTH`=1000 を超えたら**切り詰めず拒否**)の順で解決し、範囲外の数字は自由入力に落とさず `answer_out_of_range`(数え間違いを「そのまま文字列で送る」に化けさせないため)。多問 spec は `multi_question_unsupported` で拒否(`answers` の他要素に測定された意味が無い)。`questionAnswerVerdict()`=`{kind:'answer', answers}`。**`answerStructuredDecision()` 自体は従来どおり permission しか選ばない** — pending から question を拾って番号を当てる判断は #2040 の担当(「ちょうど 1 件のときだけ、0 件 / 2 件以上は 404 / 409」)で、本 Issue はその写像を export しておくところまで | | `src/lib/session/structured-prompt.ts` | 構造化検出だけが見えているダイアログの縮退プロンプト(Issue #1725)。`StructuredPromptSource`(`notification` / `permission-request`)・`buildStructuredPromptData()`(ライブ payload、`status:'pending'`)・`buildStructuredPromptHistoryRecord()`(履歴行、`status:'unclassified'`)・`buildStructuredPromptQuestion()`。`type` は既存の `UNCLASSIFIED_PROMPT_TYPE` を再利用する — `wait`/`capture --prompts`/PromptPanel が既にその語で「読めなかった枠」を扱っており、第 2 の語を足すと全員が両方を覚える必要が出る。**`PromptType` には足さない**(`types/models.ts` の注記どおり、応答経路が受け取ってはいけない値を網羅 map に生やさないため)。`Notification` の payload には選択肢が無い(#1721 §5.5)ので options は常に空、`message` は**表示専用**(機械判断は `notification_type`=D3)。question は**番号で答えるよう促す** — `respond yes` は番号つきダイアログでは意味解決されず Enter=既定選択に化ける(#1681)。型のみ import なので client component(PromptPanel)から読んでも安全。**PromptPanel** は `type==='unclassified'` を見て操作 UI を一切出さず(payload に選択肢が無い以上、ボタンは「どのキーを送るか」の当て推量になる)、`prompt.unclassifiedTitle` / `unclassifiedInstruction` / `unclassifiedRespondCommand`(`locales/{en,ja}/prompt.json`)で案内する。サーバ生成の `question` は locale を持たない英文なので画面では使わず、`message` だけ原文表示する。**Issue #1726**: `StructuredAskUserQuestionSummary`(第 1 問の question+ラベル列+問い数)を任意で持たせ、「ダイアログが開いている」だけでなく**何を訊かれたか**を出す。**番号は載せない** — 1 回の tool call は 1 問 1 画面で進み最後に `1. Submit answers / 2. Cancel` になるのに遷移のイベントが無く、画面が見えない層はどの画面かを知りようがない(番号を出すと別の問いへの回答になる)。PromptPanel は question とラベルを箇条書きで描くだけで操作 UI は出さない。**Issue #1898: `StructuredDecisionOption` / `STRUCTURED_DECISION_OPTIONS`(1=Allow once / 2=Allow always / 3=Reject)と `decisionOptions` を追加** — `eventIdentity` が id を宣言するソース(現状 opencode のみ)のダイアログに限り、答えられる verdict を載せる。**`options` は空のまま**で別フィールドに置くのが要点で、ここの番号は**画面の行ではなく REST の verdict** だから公開できる(番号を `options` に入れると、画面へ番号を打つ全経路がこの payload を answerable と見なす)。question 文も「番号は信用するな」から「`respond 1` を送れ」に切り替わる。gate は `current-output-builder` が capability を読んで決める **Issue #2031: `decisionId` / `patterns` を `StructuredPromptFacts` と `StructuredPromptWaitingData` に追加し、`buildStructuredPromptData` が copy する**。#1932 は受け側 `readPromptDecisionId` だけを実装しており、送り側が無いので PromptPanel の 3 択は到達不能コードだった。`decisionId` は**条件付き spread ではなく常に出す**(`null` 可)— 他のフィールドは散文なので不在=空だが、これは「探した結果、宛先の承認は無い」という答であり、不在(=このビルドはこの項目を出さない)と区別できなければならない。`isAddressableDecision()` は受け側とまったく同じ判定(非空文字列)を 1 か所に置いたもので、`current-output-builder` が `decisionOptions` と `decisionId` を**同一の式**から導くために使う。`patterns` は `Allow always` が保存する規則で、保持時に `boundDecisionPatterns` が件数・長さの両軸で丸める(Issue #2031) **Issue #2100: `buildStructuredPromptQuestion` の askUserQuestion 段落を分岐**。`decisionId` があり question が 1 件で `decisionOptions` が無いとき(=`readPromptQuestionChoices` が picker を描く条件)は `1 = alpha, 2 = beta` と番号を名指す。それ以外は従来どおり「画面から番号を読め」の警告(Claude の picker は画面側が権威なので不変)。`promptData.question` は PromptPanel が選択肢ボタンの**上**に描く文字列なので、番号が実際に届く payload で警告を出すのは自己矛盾だった Issue #2951: `structuredDecisionOptionsFor(cliToolId)`(ツール固有の承認ラベル。v2 は `Always allow`、他は共有リストそのもの)。`StructuredAskUserQuestionSummary.custom?: true`(自由記述可の質問) **Issue #2965**: `hasApiAnswerableDecision(eventIdentity, pendingDecisions)` — `permission-id` の source で id 付き・配達期限内の保留 decision があるか。注意文 2 種の記録ゲート。 | | `scripts/canary/` | 検出カナリア(Issue #1727、`npm run canary`)。実 `claude` を使い捨て tmux セッションで起動し 11 シナリオ(idle / 許可ダイアログ / AskUserQuestion+タスクパネル併存 / `/model` オーバーレイ / 生成中 / Auto-Yes v2 の allow・no-decision 裁定)の capture を**本番と同じ 2 経路**(`detectSessionStatus(生capture)` と Auto-Yes 経路の `detectPrompt(stripBoxDrawing(stripAnsi(...)))`)に食わせて assert する。capture は本番同値の `capture-pane -p -e -S -1000` + 200×1000 ペイン(#1708 のレイアウトはこの geometry でしか再現しない)。**隔離**: 全 tmux 呼び出しが `buildTmuxArgs()` で `-L cmate-canary-*` を強制(`kill-server` は `PrivateTmuxServer.killServer()` 経由のみ・`bind-key`/`-g` は拒否・target は常に `=name:`)/使い捨て HOME は realpath 解決して seed し `show-environment HOME` で**転送を assert**/実 `~/.claude/settings.json` の sha256 と `mcbd-*` 一覧を各シナリオ前後で再検証(違反 exit 3)。`TMUX_TMPDIR` は `$TMUX` 下で無視されるため隔離に使わない。**認証**は `CLAUDE_CODE_OAUTH_TOKEN`/`ANTHROPIC_API_KEY` → macOS keychain 複製の順(隔離 HOME では keychain へ自動フォールバックしない=実測)。期限 15 分未満の credential は refresh token ローテーションで本人をログアウトさせ得るため拒否。exit code は 0=緑 / 1=検出回帰 / 2=前提エラー / 3=ガード違反 / 4=上流障害で判定不能(`529 Overloaded` 等は `blocked` として failed と区別し、最大 180 秒は時計を止めて待つ)。`--mutate` は各シナリオの `mutantExpectation`(もっともらしい誤答)で走らせ**全部赤にならなければ自己テスト失敗**=非空振り証明。**Auto-Yes v2 の 2 シナリオ**(#1847)は本番の `buildClaudeLaunchCommand` が書いた `--settings` をカナリア内の受け口(`hook-receiver.ts`、`127.0.0.1:0` の ephemeral ポート)へ向け、裁定は本体の `resolvePermissionRequest` をそのまま呼ぶ(`PermissionDecisionDeps` で契約読み出しと allow 監査の**DB 依存 2 箇所だけ**を差し替え)。期待値は pane に加えて本番と同じ getter で組んだ `structuredEvents` / `autoYes.lastSuppression` と probe ファイルの実在を見る。これらの非空振りは `--mutate-verdict`(受け口が逆の裁定を返す。hook シナリオ以外は SKIP)で証明する。全セッションは `--permission-mode manual` で起動する(2.1.236 で既定が auto mode になり、auto mode では承認ダイアログが描画されず ready フッタも変わるため)。実フレームは `tests/fixtures/canary/.ts`(ANSI 除去・email 秘匿)と `.raw.txt` に毎回保存され、赤いときはそのまま修正用 fixture になる。純関数(期待値・argv 構築・ガード・シリアライザ)は `tests/unit/canary/` が commit 済み fixture で固定するので CI では tmux も課金も不要。手順・費用・CI 組み込み案 A/B は docs/qa/detection-canary.md **Issue #2050: 2 つ目のツール(opencode)**。`--tool ` で切り替え、1 回の実行が駆動するのは 1 ツールだけ(使い捨て HOME・geometry・起動完了行・起動フラグがツールごとに違うため)。ツール差は `tool-profiles.ts` の 1 エントリに集約し、`session.ts` はそこから形を取る。**起動完了行は検出器が読む行であってはならない** — 同じ行だと idle シナリオがゲートの保証をなぞるだけの空振りになるので、claude は `? for shortcuts`、opencode は `tab agents ctrl+p commands`(branch E の `┃ Ask anything...` ではない)を使う。opencode は本番と同じ **80×`OPENCODE_PANE_HEIGHT`(200)** ペイン・capture 200 行で、陽性分岐 A0/A/C/D/E に 1 本ずつの 5 シナリオ(`opencode-idle` / `opencode-generating` / `opencode-permission` / `opencode-picker` / `opencode-turn-complete`)。期待値(`opencode-expectations.ts`)は**本番 verdict(`evidence` 含む)**と**検出器が読む行とは別の行**による構造的事実(ダイアログ見出し `△ Permission required` / picker の `Connect provider ctrl+a` ヒント行 / busy フッタの spinner セル)を独立に assert する — 同じ定数を使い回すと定数を壊したとき両方の主張が同時に動き「その状態に到達したのか」が言えなくなるため。使い捨て HOME は opencode の `auth.json` を **mode 600 で複製**し `opencode.jsonc` に model と `permission: { bash: "ask", … }` を書く(1.18.22 の既定では `ls -la` がそのまま実行されダイアログが出ない=実測。claude の `--permission-mode manual` と同じ性質の固定)。`sanitizeEnv` は `XDG_{CONFIG,DATA,STATE,CACHE}_HOME` / `OPENCODE_CONFIG*` も落とす(HOME を差し替えても実ディレクトリへ戻る経路が残るため)。guard は claude の `settings.json` に加えて `~/.config/opencode/opencode.json{,c}` と `auth.json` を sha256、`~/.local/share/opencode/opencode.db` を **size+mtime** で各シナリオ前後に照合する(58MB あり毎回ハッシュすると実行より重い)。**版ずれ検出**: preflight の ` --version` を `parseCliVersion` に通し `DETECTOR_VERIFIED_AGAINST` と突き合わせて `fresh` / `stale` / `rules-ahead` / `unmeasured` / `unreadable` を報告(pane geometry も併記)。既定は警告のみで、`--strict-version` のときだけ **exit 5**(版番号だけで赤くなるカナリアは無視されて死ぬため)。赤が出たときはサマリ直後に drift を再掲する。mutant は**そのシナリオが決して描かない行**を要求するものを選ぶ(`opencode-generating` に `opencode-turn-complete` を当てると実測 12.8 秒のターンが `--mutate` の 30 秒時計に収まって mutant が真になり「空振り」と誤報される)。CI 側の非空振り証明は `tests/unit/canary/canary-opencode-2050.test.ts` が commit 済みフレームの 1 行を**構造を保ったまま**壊して行う。実測は docs/design/opencode-server-live-verification.md §18 **Issue #2047: `tool-profiles.ts` の opencode `paneWidth` をリテラル `80` から `OPENCODE_PANE_WIDTH` へ**。カナリアの主張は『本番と同じ geometry で撃っている』なので、既定が動いてもリテラルが残ると主張だけが嘘になり何も赤くならない。`resolveOpencodePaneWidth()` **ではなく既定定数**を取る——運用者の `CM_OPENCODE_PANE_WIDTH` は自分のペインの話で、それがカナリアに届くと `tests/fixtures/canary/opencode-*.raw.txt`(80 桁で採取)と違う幅で撃つことになる。`tests/unit/canary-opencode-geometry-2047.test.ts` が既定の変更を赤で通知する(=fixture 5 本の採り直しと `OPENCODE_VERIFIED_AGAINST.paneGeometry` の押し直しが要るというチェックリスト)(Issue #2047) **Issue #2486**: claude のシナリオが 11 本に(AskUserQuestion の `askuserquestion-tabs` / `-preview` / `-tabs-preview` / `-respond-walk`。`askuserquestion-scenarios.ts` / `askuserquestion-expectations.ts` / `respond-walk.ts`)。`respond-walk` は `/prompt-response` の送信前検証と同じ呼び出し列(`detectPrompt` → `evaluateDialogPresence` → `judgePromptResponse` → `resolvePromptAnswer`)と本番の `sendPromptAnswer` で 1 問目・2 問目・Submit を答える。本番 tmux コードは `CanarySession.withProductionTmux` 経由(`$TMUX` を私設ソケットへ向け、本番 `listSessions()` と私設サーバの一覧一致を assert してから実行)。シナリオは `workspaceFiles` で作業ディレクトリに入力ファイルを置け、`drive()` 内の拒否は `ScenarioStepError` でその画面のフレームを残して赤になる | -| `scripts/agent-health/` | エージェント CLI 日次確認の実行部(Issue #2878)。`daily.sh`(Schedule の入口。develop へ早送り同期→lock 変化時のみ npm install→`run.ts --synced-from`。同期失敗は最小レポート+exit 2、#2924。同期の前に共通ロックを取り run.ts の後まで持つ。取れなければ `run-locked` の最小レポート+exit 2、#3359)、`run.ts`(入口)→ `main.ts`(共通ロック `run-lock.ts`(#3359)・listener・ツール順の実行・後始末・レポート/state 書き出し)、`probe-tool.ts`(起動→起動時ダイアログ→画面とターン→判定)、`tool-table.ts`(ツールごとの起動フラグ・依頼文・承認ダイアログの出し方と断るキー)、`hook-listener.ts`、`tmux-driver.ts`、`production-log.ts`(本番 `logs/server.log` への漏れを数える)。手順は `docs/user-guide/agent-health.md` `opencode-v2-server.ts`(opencode-v2 の probe 用: 本番の `reserveOpencodeV2Server` でポート・パスワードを確保、`/api/event` の全 frame の `type` を記録する `ServerEventRecorder`、終了後の serve プロセス・ポート残留の検査。#2937)。`tool-table.ts` の `launchEnv`(起動行の前に置く `K=V`。opencode-v2 は `XDG_STATE_HOME` を一時 dir へ)と `server: 'opencode-v2'`。`probe-tool.ts` の `eventCheckMode`(hooks / server-sse / skip) **Issue #3020**: codex は `-c check_for_update_on_startup=false` で起動する(起動時の更新ダイアログを出さず、利用者の `version.json` も取り直さない)。それでも出た更新ダイアログは起動時のダイアログ `update` として `2. Skip` を選ぶ(`tool-table.ts` の `CODEX_UPDATE_DIALOG_OPEN` / `CODEX_UPDATE_SKIP_OPTION`)。 **Issue #3021 / #3022**: `tool-table.ts` の `seedFiles`(起動前に利用者の状態から読み取り専用で複製するファイル。v1 opencode と opencode-v2(#3428。opencode2 2.0.18 も同じ `$XDG_STATE_HOME/opencode/model.json`)は、`opencodeModelSeed()` で `opencode/model.json` だけを隔離した `XDG_STATE_HOME` へ複製してモデル選択を引き継ぐ。入力履歴・`service.json`・ロックは引き継がない)と `userStateHome()`。`probe-tool.ts` の `seedStateFiles()`、`waitForStartup` は空のペインを起動画面として扱わない。 **Issue #3044**: メトリクス計測。`metrics.sh`(入口。`daily.sh --sync-only` で同期→`metrics.ts`。同期失敗は `metrics: []`・`scriptErrors` の最小 JSON+exit 2)、`metrics.ts`(lock `metrics.lock`・3 並列で 10 分以内・JSON/state 書き出し・exit 0/1/2)、`metrics-runners.ts`(npm audit・semgrep・gitleaks・ESLint complexity(報告専用の一時設定)・jscpd/knip(npx)・npm outdated・vitest coverage(月曜 JST)の薄いラッパー。欠如・失敗・時間切れは skip)。依頼文 `docs/agent-health/metrics-prompt.md`(Schedule `agent-health-metrics` 06:30、新規起票 1 日 2 件まで) **Issue #3046**: `release-report.ts`(入口)→ `release-report-main.ts`(リリース判断 HTML。gh・git・`npm audit --omit=dev` は注入可能な `exec` 越しの薄いラッパー `Facts`、依頼の記録・計測・agent-health レポート・orchestrate の `runs//` を読むだけ。既定の出力は `
/workspace/agent-health//release-readiness.html`。exit 0 書けた / 2 引数・異常)。 **Issue #3045**: `dispatch.ts`(入口)→ `dispatch-main.ts`(#3045。gh・commandmate を注入可能な exec で呼ぶ: 選定→ラベル確認→busy 判定→`/clear`→入力待ち確認→依頼送信(exit 99 は 2 分後に 1 回再送)→`auto-dispatched` ラベル・コメント→`dispatch/.json`。exit 0/1/2、`--dry-run`)。`release-report-main.ts` は記録の `runSuffix` で run のファイルを絞る **Issue #3053**: `screen-picker`(`screen-idle` の直後に選択画面を開いて判定し Esc だけで閉じる)。`tool-table.ts` の `picker`(`PickerSpec`: 開くコマンド・開いたことの確認文字列 `CLAUDE_MODEL_PICKER_OPEN` / `CLAUDE_EFFORT_PICKER_OPEN` / `CODEX_MODEL_PICKER_OPEN`・画面ごとの `expectPrompt`(claude は false #1495、codex の `/model` は true #2868)・`closeKey: 'Escape'`・監視する設定ファイル)。`probe-tool.ts` の `pickerTurn`(Enter はコマンドに 1 回だけ)。`main.ts` は前後で設定の値を比べ、変わっていたら書き戻さず exit 2。**Issue #3054**: `metrics-runners.ts` に性能の計測(`production-log.ts` の `locateServerLog` で本番 `logs/server.log` と `server.log.1`〜`.3` をストリームで 1 回だけ読み 3 指標で共有、`server.pid` の子の `ps -o rss=,%cpu=` を 5 秒×6 回、`GET /api/worktrees` 3 回)。 **Issue #3158**: `catalog-check.ts`(入口)→ `catalog-check-main.ts`(スラッシュコマンドカタログのずれの日次チェック。`npm run catalog:refresh -- --check` を注入可能な `runCheck`、gh を `exec` 越しに呼ぶ: 判定→当日の agent-health レポートと attestation の版の比較→作成者 kewton の open な `catalog-drift` Issue を作成・更新(件数が動いたらコメント)・close→`catalog/.json`→`AGENT_HEALTH_CATALOG` 行。読むだけでリポジトリ内に書かない。exit 0/1/2、`--dry-run`)。依頼文 `docs/agent-health/catalog-prompt.md`(Schedule `agent-health-catalog` 07:30) `metrics-runners.ts` の `bug-flow`(Issue #3185。`gh issue list --label bug --state all` で直近 7 日を読む。gh の欠如・失敗は skip)。 **Issue #3310**: `metrics-runners.ts` に `ci-flaky`(`gh run list --created`→失敗・やり直しの run の attempt ごとに `gh run view --json jobs`・`--log-failed`。`gh` の失敗・時間切れは skip、読む attempt は 30 まで)。 **Issue #3311**: `hook-observation` は performance の 3 指標と同じ本番ログの 1 回の読み込みから計測。 **Issue #3313**: `main.ts` は 6 ツールすべてを選んだ実行で gemini・vibe-local・copilot の version だけを読み(`probeLimitedTool`)、`coverage`(9 ツール × 7 check)と `summary` をレポートに入れ要約を標準出力へ。`probe-tool.ts` の skip はすべて `skipCheck` で種類(`skipKind`)つき。`tool-table.ts` に `LIMITED_TOOL_SPECS`(gemini=signed-out・vibe-local/copilot=unsupported と起動しない理由。vibe-local の版は `vibe-coder.py --version`)。 **Issue #3312**: `product/`(第 2 段)— `supervisor.sh`(排他 `$CM_PRODUCT_RUN_DIR/supervisor.lock`→回収→安全確認→実行 `CM_PRODUCT_STAGE_CMD`→後始末→確定、期限 07:25/07:45/07:50)・`deadline-guard.sh`(期限の番人)・`lib.sh`・`ledger.py`(`ledger/.json`、planned→acquired、資源ごとの本人確認、合わなければ unknown)・`finalize.ts`。`release-report-main.ts` は `/product/.json` を `product/` がある場合だけ読む。 **Issue #3420**: `probe-tool.ts` はターンごとに依頼前の画面を持ち、`screen-running`・`screen-quoted-dialog` を `judgeTurnScreen`、opencode-v2 の `hook-correlation` を `judgeServerEvents` で判定する。 | -| `src/lib/agent-health/` | エージェント CLI 日次確認(`scripts/agent-health/run.ts`)の純粋関数とレポート型(Issue #2878)。`types.ts`(`AgentHealthReport` — #2879 の AI が読むインターフェース)、`report.ts`(`versionChanged`・state の引き継ぎ・exit code 0/1/2)、`hook-correlation.ts`(listener に届いた hook を `worktreeId`/`instanceId` で突き合わせる。#2874/#2891 の型を fail にする)、`tmux-command.ts`(tmux argv は必ず `-L cm-agent-health`、子の環境から `TMUX`・`CM_*` を除く)、`config-guard.ts`(global-singleton の hook 設定を保存しバイト単位で書き戻し sha256 で確認)、`screen-checks.ts`・`dialog-select.ts`・`cli-args.ts` `server-events.ts`(opencode-v2 の `hook-correlation` 枠の判定: `evaluateServerEvents`(running ターン中の `session.execution.started`/`succeeded`、受信 type 一覧)・`evaluateOpencodeV2LaunchLine`(launch.sh 経由か / `--standalone` 落ち)・`evaluateServerLeftovers`。#2937)。`AGENT_HEALTH_TOOLS` は 6 ツール(末尾 `opencode-v2`) **Issue #3031**: `config-guard.ts` の `restoreTrustState(snapshot, marker, same = sameJsonWithoutMarker)` は比較方式(`TrustStateComparator`)を選べる。codex の `config.toml` は `sameTomlWithoutMarker`(marker を含むヘッダの表と空行を行単位で除いて比較、`tomlLinesWithoutMarker`)で、確認の一時パスの `[projects.""]` 表だけが差分のときに限り書き戻す(`tool-table.ts` の `TrustStateFile`)。 **Issue #3044**: `metrics-types.ts`(`MetricsReport` — #3045/#3046 が読む計測 JSON の型、`METRIC_IDS`、閾値の定数)、`metrics-parse.ts`(各ツールの出力→計測値。npm audit は advisory 単位で数えパッケージ単位で候補化、registry 不達は `check-npm-audit.mjs` と同じ判定で skip)、`metrics-rules.ts`(前回比: 新たに閾値超え・悪化だけを `candidates`、続く security は `outstanding`、`buildQueue`・`nextMetricsState`(skip は前回値を保持)・`decideMetricsExitCode`・`isCoverageDay`)、`metrics-args.ts` **Issue #3046**: `dispatch-record.ts`(`~/.commandmate/agent-health/dispatch/.json` の型 `DispatchRecord` と寛容な `parseDispatchRecord`。#3045 が import)、`release-readiness.ts`(`decideReadiness` — NO-GO/要判断/GO と理由・次の一手、`summarizeWorkflowRuns`・`summarizeCheckRollup`・`findPullRequestForIssue`・`parseVerifyExit`・`pickWaitLog`・`reproducesFailAfter`・`compareMetrics`・`countHighAdvisories`・`parseReleaseReportArgs`)、`release-readiness-html.ts`(外部資源なし・ライト/ダーク対応の 1 ファイル HTML、Markdown の小さな部分集合を escape して描画)。 **Issue #3045**: `dispatch.ts`(#3045。Claude 3 への自動依頼の純粋関数: 対象選定=作成者 kewton・`agent-health`/`metrics`・未 `auto-dispatched`、バグ全件→改善 security 優先 2 件・合計 5 件、`judgeAgentState`(`ls --json` の claude-3)、`buildRequest`・`sendArgv`(送り先は定数固定)・記録・`AGENT_HEALTH_DISPATCH` 行)。`dispatch-record.ts` に省略可能な `runSuffix`・`reason` を追加、`release-readiness.ts` に `selectRunFiles`(runSuffix で run のファイルを絞る) **Issue #3053**: `AGENT_HEALTH_CHECK_IDS` に `screen-picker`。`screen-checks.ts` の `evaluatePickerScreens`(画面ごとの判定を 1 つの check に。期待は `waiting`・`hasActivePrompt=false`・`SELECTION_LIST_REASONS`・画面ごとの `isPrompt`)。`picker-settings.ts`(`readPickerSettings` / `describePickerSettingsChanges`: settings.json・config.toml の model / effort の比較、純粋関数)。**Issue #3054**: `metrics-perf.ts`(ログ行の解析・直近 24 時間の ` ` ごとの集計・p50/p95・`ps` の解析。名前以外の JSON の値は出力に写さない)、`metrics-types.ts` に分類 `performance`・4 metricId・閾値、`metrics-rules.ts` に性能の判定(前から超えるものも `outstanding`)と `buildQueue` の並び(security 新規→保守性→performance 新規→security 続き→performance 続き)。 **Issue #3103**: `buildRequest(issues, termsPath)` は改行の無い 1 行(`/orchestrate <番号…> <条件ファイル> の条件に従うこと`)。条件は `buildTerms(date, issues)` で作り、`dispatch-main.ts` が送る前に `dispatchTermsPath()`(`workspace/agent-health/<日付>/dispatch-terms-.md`)へ書く(書けなければ送らない)。**Issue #3104**: `issueKind` はラベル `perf`(`PERF_LABEL`)の Issue を対象外にする(`REQUIRED_LABELS` には入れない)。 **Issue #3142**: `release-readiness.ts` の `analyzeCheckRollup(rollup, {ignoreCancelled})`。本日マージされた PR のチェック(`release-report-main.ts` の `prChecks()`)では、マージ後に自動キャンセルされた `cancelled` を判定から外して件数(`cancelledChecks`)だけを表に出す。develop の push run(`summarizeWorkflowRuns`)は不変 **Issue #3158**: `catalog-check.ts`(カタログ日次チェックの純粋関数: `judgeCatalogCheck`(`CATALOG_CHECK_IGNORED_WARNING_PREFIXES`=`opencode provider skipped` を渡し opencode 1.x の skip を検査不能に数えない)、`compareVersions`・`versionToken`・`formatVersionGaps`(版の差。opencode 1.x は除外、drift にはしない)、`parseTrackingIssues`(作成者 kewton のみ)・`planIssueSync`(drift→create/update、clean→close/none、inconclusive→none)、`catalogIssueBody`・`CatalogCheckRecord`・`formatCatalogLine`) **Issue #3159**: `dispatch.ts` に種別 `catalog`(ラベル `CATALOG_LABEL`=`catalog-drift`。`REQUIRED_LABELS` に追加)。`selectDispatchTargets` はバグ全件→catalog(`MAX_CATALOG_ISSUES`=1、古い順)→改善 2 件・合計 5 件。`buildTerms` は catalog を含む run に「`/catalog-reconcile` の無人実行節に従う」(`CATALOG_SKILL_PATH`)を足す。`dispatch-record.ts` の `DISPATCH_ISSUE_KINDS` に `catalog` `bug-flow.ts`(Issue #3185。bug Issue 本文の「分類」節の語彙定数と解析・集計。分母 0 は null、`candidates` は常に空)。分類 `process`(fail にしない)。 **Issue #3183**: `frame-archive.ts` — `screen-*` 不合格時に画面全体を `/../frames//-.txt`(+`.json`)へ保存。`CM_AGENT_HEALTH_SAVE_FRAMES=all` で合格時も。 **Issue #3310**: `ci-flaky.ts`(develop の push の CI の不安定さ。`gh run list`・`gh run view --attempt --json jobs`・`--log-failed` を読む純粋関数 `parseCiRunList`・`runsNeedingAttempts`・`parseCiJobs`・`parseFailedTests`・`classifyCiJob`(`test`/`infra`/`other`)・`measureCiFlaky`(やり直しで通った・別 SHA で 2 回以上=不安定、毎回落ちる=落ち続け))。`metrics-types.ts` に分類 `ci`・`CI_FLAKY_*`・`MetricResult.records`、`metrics-rules.ts` は `ci` を performance と同じく新規=候補・続き=`outstanding`。 **Issue #3311**: `hook-observation.ts`(本番ログから写しでない hook の破棄数と食い違いの長さの分布を数える観測指標。metricId `hook-observation`、category `process`。候補にしない)。 **Issue #3313**: `types.ts` に `AGENT_HEALTH_LIMITED_TOOLS`・`AGENT_HEALTH_REPORT_TOOLS`(9 ツール)・`AgentHealthSkipKind`・`AgentHealthCheck.skipKind`・`AgentHealthReport.coverage` / `summary`。`coverage.ts`(`skipCheck`・`buildCoverage`(9 ツール × 7 check、結果の無いマスは not-selected / not-recorded)・`coverageHeadline`・`summarizeCoverage`・`checksLimitedTools`)。 **Issue #3314**: `measureKnip` は未使用ファイルの相対パスを items/findings に入れ、前回に無いパスを候補にする(未使用 export は件数のみ)。 **Issue #3359**: `run-lock.ts`(`acquireRunLock`・`runLockDir`・`parseRunLockOwner`。scripts/uat/run-lock.sh と同じ共通ロック。inherited は release しない)。 **Issue #3389**: `countTypeSafety(text, fileName)` は TypeScript のパーサで数える(型の位置の `any`・コメント先頭の指示だけ。`declare global` の `var` の `no-var` 抑止は除外、`TYPE_SAFETY_COUNT_VERSION`=2)。`countVersion` が前回値と違う指標は `evaluateAll` が基準を置き直し、summary に 1 回だけ出す。 **Issue #3312**: `product-result.ts`(製品の経路の確認(第 2 段)の結果の型 `ProductRunResult`・`buildProductRunResult(state, ledger)`・`publishProductRunResult`(一時ファイル→rename、0644、dir/target の symlink は拒否、既定 `/Users/Shared/commandmate-check`))、`product-judgement.ts`(`judgeProductRun`=fail>unknown>skip>pass、無い・古い・run id/SHA 不一致は not-run/`countProductSkipStreak`=3 日で要対応)。`release-readiness.ts` の `ReadinessFacts.product?`(fail=NO-GO、unknown・not-run・skip 3 日=要判断、未設定は判定不変)。 **Issue #3420**: `model-auth.ts`(モデル(provider)が `Error: Unauthorized` で拒んだターンの判定。`judgeTurnScreen`(`screen-running`/`screen-quoted-dialog` の fail を `signed-out` の skip に)・`judgeServerEvents`(SSE: 未着が `succeeded` だけで `session.execution.failed` が届いたとき)。pass はそのまま、他のモデルエラーは fail)。`server-events.ts` に `OPENCODE_V2_SUCCEEDED_EVENT_TYPE`・`OPENCODE_V2_EXECUTION_FAILED_EVENT_TYPE`。 | +| `scripts/agent-health/` | エージェント CLI 日次確認の実行部(Issue #2878)。`daily.sh`(Schedule の入口。develop へ早送り同期→lock 変化時のみ npm install→`run.ts --synced-from`。同期失敗は最小レポート+exit 2、#2924。同期の前に共通ロックを取り run.ts の後まで持つ。取れなければ `run-locked` の最小レポート+exit 2、#3359)、`run.ts`(入口)→ `main.ts`(共通ロック `run-lock.ts`(#3359)・listener・ツール順の実行・後始末・レポート/state 書き出し)、`probe-tool.ts`(起動→起動時ダイアログ→画面とターン→判定)、`tool-table.ts`(ツールごとの起動フラグ・依頼文・承認ダイアログの出し方と断るキー)、`hook-listener.ts`、`tmux-driver.ts`、`production-log.ts`(本番 `logs/server.log` への漏れを数える)。手順は `docs/user-guide/agent-health.md` `opencode-v2-server.ts`(opencode-v2 の probe 用: 本番の `reserveOpencodeV2Server` でポート・パスワードを確保、`/api/event` の全 frame の `type` を記録する `ServerEventRecorder`、終了後の serve プロセス・ポート残留の検査。#2937)。`tool-table.ts` の `launchEnv`(起動行の前に置く `K=V`。opencode-v2 は `XDG_STATE_HOME` を一時 dir へ)と `server: 'opencode-v2'`。`probe-tool.ts` の `eventCheckMode`(hooks / server-sse / skip) **Issue #3020**: codex は `-c check_for_update_on_startup=false` で起動する(起動時の更新ダイアログを出さず、利用者の `version.json` も取り直さない)。それでも出た更新ダイアログは起動時のダイアログ `update` として `2. Skip` を選ぶ(`tool-table.ts` の `CODEX_UPDATE_DIALOG_OPEN` / `CODEX_UPDATE_SKIP_OPTION`)。 **Issue #3021 / #3022**: `tool-table.ts` の `seedFiles`(起動前に利用者の状態から読み取り専用で複製するファイル。v1 opencode と opencode-v2(#3428。opencode2 2.0.18 も同じ `$XDG_STATE_HOME/opencode/model.json`)は、`opencodeModelSeed()` で `opencode/model.json` だけを隔離した `XDG_STATE_HOME` へ複製してモデル選択を引き継ぐ。入力履歴・`service.json`・ロックは引き継がない)と `userStateHome()`。`probe-tool.ts` の `seedStateFiles()`、`waitForStartup` は空のペインを起動画面として扱わない。 **Issue #3044**: メトリクス計測。`metrics.sh`(入口。`daily.sh --sync-only` で同期→`metrics.ts`。同期失敗は `metrics: []`・`scriptErrors` の最小 JSON+exit 2)、`metrics.ts`(lock `metrics.lock`・3 並列で 10 分以内・JSON/state 書き出し・exit 0/1/2)、`metrics-runners.ts`(npm audit・semgrep・gitleaks・ESLint complexity(報告専用の一時設定)・jscpd/knip(npx)・npm outdated・vitest coverage(月曜 JST)の薄いラッパー。欠如・失敗・時間切れは skip)。依頼文 `docs/agent-health/metrics-prompt.md`(Schedule `agent-health-metrics` 06:30、新規起票 1 日 2 件まで) **Issue #3046**: `release-report.ts`(入口)→ `release-report-main.ts`(リリース判断 HTML。gh・git・`npm audit --omit=dev` は注入可能な `exec` 越しの薄いラッパー `Facts`、依頼の記録・計測・agent-health レポート・orchestrate の `runs//` を読むだけ。既定の出力は `
/workspace/agent-health//release-readiness.html`。exit 0 書けた / 2 引数・異常)。 **Issue #3045**: `dispatch.ts`(入口)→ `dispatch-main.ts`(#3045。gh・commandmate を注入可能な exec で呼ぶ: 選定→ラベル確認→busy 判定→`/clear`→入力待ち確認→依頼送信(exit 99 は 2 分後に 1 回再送)→`auto-dispatched` ラベル・コメント→`dispatch/.json`。exit 0/1/2、`--dry-run`)。`release-report-main.ts` は記録の `runSuffix` で run のファイルを絞る **Issue #3053**: `screen-picker`(`screen-idle` の直後に選択画面を開いて判定し Esc だけで閉じる)。`tool-table.ts` の `picker`(`PickerSpec`: 開くコマンド・開いたことの確認文字列 `CLAUDE_MODEL_PICKER_OPEN` / `CLAUDE_EFFORT_PICKER_OPEN` / `CODEX_MODEL_PICKER_OPEN`・画面ごとの `expectPrompt`(claude は false #1495、codex の `/model` は true #2868)・`closeKey: 'Escape'`・監視する設定ファイル)。`probe-tool.ts` の `pickerTurn`(Enter はコマンドに 1 回だけ)。`main.ts` は前後で設定の値を比べ、変わっていたら書き戻さず exit 2。**Issue #3054**: `metrics-runners.ts` に性能の計測(`production-log.ts` の `locateServerLog` で本番 `logs/server.log` と `server.log.1`〜`.3` をストリームで 1 回だけ読み 3 指標で共有、`server.pid` の子の `ps -o rss=,%cpu=` を 5 秒×6 回、`GET /api/worktrees` 3 回)。 **Issue #3158**: `catalog-check.ts`(入口)→ `catalog-check-main.ts`(スラッシュコマンドカタログのずれの日次チェック。`npm run catalog:refresh -- --check` を注入可能な `runCheck`、gh を `exec` 越しに呼ぶ: 判定→当日の agent-health レポートと attestation の版の比較→作成者 kewton の open な `catalog-drift` Issue を作成・更新(件数が動いたらコメント)・close→`catalog/.json`→`AGENT_HEALTH_CATALOG` 行。読むだけでリポジトリ内に書かない。exit 0/1/2、`--dry-run`)。依頼文 `docs/agent-health/catalog-prompt.md`(Schedule `agent-health-catalog` 07:30) `metrics-runners.ts` の `bug-flow`(Issue #3185。`gh issue list --label bug --state all` で直近 7 日を読む。gh の欠如・失敗は skip)。 **Issue #3310**: `metrics-runners.ts` に `ci-flaky`(`gh run list --created`→失敗・やり直しの run の attempt ごとに `gh run view --json jobs`・`--log-failed`。`gh` の失敗・時間切れは skip、読む attempt は 30 まで)。 **Issue #3311**: `hook-observation` は performance の 3 指標と同じ本番ログの 1 回の読み込みから計測。 **Issue #3313**: `main.ts` は 6 ツールすべてを選んだ実行で gemini・vibe-local・copilot の version だけを読み(`probeLimitedTool`)、`coverage`(9 ツール × 7 check)と `summary` をレポートに入れ要約を標準出力へ。`probe-tool.ts` の skip はすべて `skipCheck` で種類(`skipKind`)つき。`tool-table.ts` に `LIMITED_TOOL_SPECS`(gemini=signed-out・vibe-local/copilot=unsupported と起動しない理由。vibe-local の版は `vibe-coder.py --version`)。 **Issue #3312**: `product/`(第 2 段)— `supervisor.sh`(排他 `$CM_PRODUCT_RUN_DIR/supervisor.lock`→回収→安全確認→実行 `CM_PRODUCT_STAGE_CMD`→後始末→確定、期限 07:25/07:45/07:50)・`deadline-guard.sh`(期限の番人)・`lib.sh`・`ledger.py`(`ledger/.json`、planned→acquired、資源ごとの本人確認、合わなければ unknown)・`finalize.ts`。`release-report-main.ts` は `/product/.json` を `product/` がある場合だけ読む。 **Issue #3420**: `probe-tool.ts` はターンごとに依頼前の画面を持ち、`screen-running`・`screen-quoted-dialog` を `judgeTurnScreen`、opencode-v2 の `hook-correlation` を `judgeServerEvents` で判定する。 **Issue #3438**: `probe-tool.ts` は `look()` ごとに画面のモデルを読み、終了後に hook・複製した `model.json` と合わせて `ProbeOutcome.launchedModel` を返す(ログ `: model