Skip to content

Latest commit

 

History

History
1032 lines (838 loc) · 80.3 KB

File metadata and controls

1032 lines (838 loc) · 80.3 KB

開発プロセスの判断基準

「どう作るか」ではなく「どう判断するか」の知見 — rewrite/refactor、外部コントリビューション、 ADR の粒度、ドキュメントの書き方。各項目は「事象 → なぜ → 適用方法」。 規範的なプロセス(ADR 先行、小さい typed commit 等)は CONVENTIONS.md を参照。


設計・実装の判断

ナレッジは設計判断の瞬間に、機構名で引く

事象: 1 セッションで、既に記録済みの回避策を持つ GUI 欠陥を 2 件出した (2026-09、メニューバー GUI)。(1) パネルのユーザーリサイズ要望に対し、 MenuBarExtra 上のグリップ式を実装 → 挙動の違和感で廃棄 → その後に 「リサイズ可能メニューバー NSPanel」の実装済み先例と罠 2 件のエントリを発見。 (2) click-away: .nonactivatingPanel では resign イベントが来ない経路がある ため「表示中だけイベントモニタで自前クローズが定石」というエントリが存在し、 同じ移行作業の最中にそれを読んでいたのに適用せず、外側クリックで閉じない まま一度リリースした。

なぜ: 参照のトリガーが「プロジェクト着手時」だったから。着手時に索引を 読んでも、その後の個別の設計判断(リサイズ方式の選定・クローズ経路の設計) では再検索が起きない。どちらの欠陥も知識の欠如ではなく、判断時点での検索の 欠如で生まれた。「読んだのに適用しない」は「知らなかった」より高くつく — 読了の記憶が「確認済み」という誤った安心を生むため。

適用方法:

  • トリガーは設計判断・機構の新規採用のたび(プロジェクト単位ではなく)。 採用しようとしている機構の名前(NSPanel・notarize・reload・cask …)で ナレッジベースとワークスペース memory を検索し、全ヒットを読んでから コードを書く。
  • 参照プロジェクトから移植するときは、コードより先にその CLAUDE.md / AGENTS.md を全読する — 罠はコードには写っておらず、そこに記録されている。
  • 読んで終わりにしない: 「読んだエントリのうち、いまの設計を制約するのは どれか」を明示してから実装に入る。
  • 規範本体は .github/CONVENTIONS.md §Consult and feed the knowledge base。

事象: 636 行の単一 main.go のツールは、リファクタするより仕様から書き直した方が 速かった。一方、既にファイル分割されカバレッジ中程度のツールはリファクタで十分だった。

再実装を選ぶ基準(3 条件が揃ったら検討):

  • 単一巨大ファイル(600 行+)でグローバル状態が関数内から直接参照されている
  • テストカバレッジが極端に低く(<10%)ブラックボックステストのみ
  • エラーパスがテスト不能(log.Fatal が非 main 関数に散在、I/O がハードコード)

再実装の手順: 仕様抽出(README + 既存コード + テスト期待値)→ アーキテクチャ文書 → 現行コードを legacy/ に退避 → testdata と期待値ファイルは維持(リグレッション担保)→ 新規実装 → 回帰テスト全パス → legacy 削除。

回避策を作る前に upstream が直していないか確認する

事象: pin 留めした vendored 依存(git submodule)の制約に対し、独自の変換器を 設計・実装したが、upstream は pin の 5 日後のコミットで全く同じ修正を入れていた。 submodule を 3 コミット bump するだけで自作実装は丸ごと不要になった(しかも自作版には 見落としがあり不完全だった)。

適用方法: 依存の制約にぶつかったら (1) まず upstream の最新 / issue / PR を調べる (git log HEAD..origin/master を grep)、(2) pin が新しくても直近コミットに fix が あり得る、(3) bump の副作用は API 差分(ヘッダの diff)で確認、(4) submodule は upstream を指したまま bump する(fork 不要)。

構造的 drift は最小修正でなく前提から再構成する

事象: 同じ情報が 4 箇所に手作業で並列維持されている問題に「並列リストを 1 箇所に 集約」の最小修正を提案したが、「完全に再構成する前提で検討すべき」と差し戻された。 結果として情報の発生源そのものを消す全面再構成になり、drift の根本原因が消えた。

適用方法: 「drift が観測される」「同じ症状のバグが複数バージョンで再発」のサインが 出たら、最小修正の代わりに「最終形はどうあるべきか」を 2–3 文で提示して判断を仰ぐ。 再構成のコストは、後続のメンテコスト削減と波及防止で回収される。ただし全面再構成は 承認を得てから — 勝手にやらない。

少数派の不便報告で default を変えない — opt-in flag で救済する

事象: 「default の暗黙挙動で困った」という報告に対し、strict な修正を検討したが、 (a) 既存ユーザーは困っていない、(b) 完全な fix は原理的に穴が残る、(c) 報告者は習熟層で 問題に即気付ける — の 3 拍子が揃っていた。

適用方法(トリアージ手順):

  1. 報告者以外に「現状 default で困っている利用者」がいるか確認。ゼロなら opt-in が最有力。
  2. その挙動が destructive(データ破壊/クラッシュ)か recoverable(空の結果/即気付ける)か 判定。recoverable なら opt-in 寄り。
  3. 完全な fix が可能か検討。穴が残るなら opt-in にして「smart にしました」と約束しない。

設計形: CLI flag + config の両対応(CLI > config > default)、default は現状互換維持、 選択肢は最初から enum で 3 状態(smart / 現状互換 / 一切自動処理しない)にする — opt-out flag を 1 個だけ足すと入力経路ごとに flag が連鎖して肥大化する。README に 「なぜ default が X か」を 1 段落残す。

破壊的変更は「利用者の実在確認 → 選択肢提示 → 承認」の順で

事象: 互換性の論点を先に出さずに config 再構成を進めて割り込まれた。逆に、非推奨化 コードを実装した直後に「利用者がいないから削除でいい」となり、書いた端から捨てる 二度手間も起きた。

適用方法:

  1. 利用者の実在を先に確認する。 リリース済みでも利用者ゼロなら互換性コードは不要。
  2. 利用者がいるなら user-facing interface を変える前に手を止め、選択肢 (clean break / migration shim / example のみ変更)を提示する。
  3. 承認を得てからコードを書く。互換コードを先に書かない。

移行・統合はリグレッションテスト先行で

事象: ライブラリ統合の移行で、先に現行動作のリグレッションテストを書いたことで、 移行後の動作差異が「改善」か「退行」かを正確に判定できた。

適用方法: 移行前に現行動作のテストを書く → 移行 → 失敗箇所を改善/退行で仕分け → 改善ならテスト期待値を更新、退行なら対処。

有限資源の外部 API は「予算」を設計に組み込む

事象: 1日あたりのコール数に上限がある API を定期ポーリングする設計。上限超過は 明示的なエラーではなく "Unauthorized" (401) で返ってきた。つまりトークンが誤って いる場合と区別がつかない

なぜ: 多くの従量 API は超過を認証失敗として表現する。クライアント側が消費量を 知らないと、障害時に「設定が壊れた」と誤診する。

適用方法:

  • 消費数を永続化して数える。メモリ保持では再起動で忘れ、再起動ループが上限を すり抜けて突破する。
  • 上限より低い予算を既定にし、アドホックな手動実行や同一資格情報を使う他クライアント の余地を残す。
  • 設定時点で拒否する: 「N台 × 間隔」から1日の消費量を算出し、予算超過になる設定は 診断コマンドやインストール時点で拒否し、収まる値を提示する。運用開始後に静かに壊れるより はるかによい。
  • エラー型に「超過の可能性がある」ことを表現し(401 も rate-limited 扱いにする等)、 指数バックオフの対象にする。
  • 診断・状態表示に本日の消費量と現在設定での見込みを出す。数字が「謎の障害」になる前に 見えるようにする。

同じ仕事をする実体が複数あるとき、調整プロトコルではなく条件付き no-op で防ぐ

事象: 常駐デーモンと GUI アプリの両方が同じデータを収集できる構成。素朴には 「デーモンが動いているか」を GUI が判定して分岐することになるが、判定・同期・競合の 面倒がすべて残る。

適用方法:前回の結果が古いときだけ実行する」という条件付き実行を1つ用意し、 両方がそれを呼ぶ。片方が動いていれば結果は新しく、もう片方の実行はコスト 0 で終わる。 何も動いていなければ、呼んだ方が実行役になる。相手の存在を知る必要がなく、同期させる 設定もない。

  • 破壊的な重複(二重書き込み・二重課金)が問題になる場合は、これに加えて排他ロックを 併用する。ロックは PID ファイルではなく flock 等のプロセス終了で自動解放される 仕組みを選ぶ(クラッシュで残留しない)。
  • 「動いているか」の表示は、どちらが動いているかではなくデータの新しさで判定する。 片方だけを信じる表示は、もう片方が働いているときに「停止中」と嘘をつく。

境界をまたぐ出力は、同じバイト列ではなく同じ内容を保証する

症状: 保存するファイルと配信するメッセージを 1 つの描画器で作っていた。 「実装が 1 つなら食い違わない」という理屈だったが、食い違った。配信先の方言に インライン Markdown リンクが無く、ファイルには効くリンクが、配信物には リンクがゼロ入っていた。さらに方言をもう 1 つ足した瞬間に分割器も壊れた。 記事境界を ## ### のマッチで探しており、新しい方言はそれを使わない ため、記事の途中で切り始めた。

適用方法:

  • 描画器を 1 つに保つ方針は正しいが、不変条件は同じ内容として述べる。 フレーバー引数を持たせ、フレーバーごとに「効く情報が失われていないこと」 (全 URL の存在、全項目の非分断)を検査する。
  • 見栄えのする形より、どの変換器も壊せない形を選ぶ。独立行の裸の URL は あらゆる方言を生き延びる。インラインリンクは真っ先に落ちる。
  • 描画器が選んだマークアップから文書構造を逆算しない。描画器に境界 (不可分ブロックの列)を出させ、消費側はそれを詰める。それ以外は次の 出力形式で必ず壊れる。

生成レポートの但し書きは、読み手にとって何が変わるかを書く

症状: ダイジェストの異常報告の節が「このソースの summary が記事の要約に なっていない」で終わっていた。正確で、内部的で、読み手には何もできない。 この節は 2 つの読者を混ぜていた——設定を保守する人への覚書と、出力を読む人への 警告である。

適用方法:

  • 「これは読み手が導く結論を変えるか」で分ける。何が欠けているか、何が薄い 根拠で判断されたか、どこを別途確かめるべきかは但し書き。ソースの挙動は 保守であり、運用者への実行報告に属する。
  • スキーマに両方(何が起きたか / 何が変わるか)を持たせ、後者が無いエントリを 検証器が拒否する。読み手が何もできないエントリは、できるエントリを埋める。
  • 自動生成分にも同じ規律が要る。「X の記事が欠けており再実行しても戻らない」は 使えるが、「X で gap を検知」は使えない。

候補の比較は、精度ではなく「出力契約を満たすか」で足切りしてから

事象: 既存コンポーネントの置き換え候補 6 件を精度で比較し、最も精度の良いものが 現行の半分の誤り率という結果を得た。ところがその候補はタイムスタンプを出せず、 こちらの出力形式(時刻付きセグメント)を満たせなかった。2 番目に良い候補も同様だった。 精度表を先に作っていたら、使えない候補を推薦していた。実際に使える候補は、 精度で見れば中位のものだった。

なぜ: 比較表を作るとき、最も目立つ指標(精度、速度、サイズ)から埋めたくなる。 だが必須要件は連続量ではなく可否であり、可否で落ちるものは連続量をどれだけ 稼いでも復活しない。順序を逆にすると、落ちる候補の測定に時間を使ったうえ、 数値の見栄えに引きずられて要件を「なんとかできないか」と考え始める。

適用方法:

  • 比較表の精度列の左に必須要件の列を置く。出力形式、入力の長さ上限、 ライセンス、追加される依存、対応プラットフォーム。
  • 要件を満たさない行は数値を測る前に落とす。ただし表には残し、落とした理由を 書く — 「なぜあの高精度なやつを選ばなかったのか」は必ず後で訊かれる。
  • 能力を問い合わせる API がある場合、「数」ではなく「所属」で判定する。 ある実装は対応言語を 1 件だけ名乗りながら、あらゆる言語指定を 「非対応」で拒否した。count > 0 を条件にすると必ず踏む。
  • 要件を満たす候補が 1 つしか残らなかったなら、それ自体が結論である。 精度比較は「その 1 つの中で version をどれにするか」に縮む。

fork は参照プロジェクトの「未適用の教訓」も継承する — 独立検証は fork 元にも当てる

事象: 既存のメニューバー GUI を fork して姉妹ツールを作った際、独立検証パスが 「通知の許可が拒否されても画面に何も出ず、トグルは ON のまま」を指摘した (使用量集計 GUI の対、2026-09)。この欠陥は fork 元に元からあり、ナレッジには 「拒否されている場合は UI がそう述べ、設定ペインへの導線を出す」が記録済み だった。fork 元は当該エントリより前に書かれていたため適用されておらず、fork は そのまま引き継いだ。同じパスで、fork 側で新たに作った JSON フィールド (チェックサム不一致数)を decode しながらどの画面にも出していない箇所も見つかった。

なぜ: 「実績のあるコードを写す」は、写す時点の教訓で監査されたコードを写す ことを意味しない。参照プロジェクトの CLAUDE.md / AGENTS.md を全読しても、そこに 書かれていない未適用の教訓は見えない。独立検証者は fork 元への忠誠を持たない ので、「元からそうだった」を理由に見逃さない。

適用方法:

  • fork で作るときは、独立検証の対象に「fork 元が書かれた後に記録された教訓」を 明示的に含める(ナレッジ・memory の日付を見て、fork 元より新しいエントリを 列挙させる)。
  • 検証で見つかった fork 元由来の欠陥は、fork 側で直すだけでなく fork 元にも 起票する(同じ症状が本家で続く)。
  • 「契約に載せたが描画していない」フィールドは、契約追加のコミットで grep で 消費者を確認する — 追加した本人が一番見落とす。

一括変更・機械的変更

vendored テンプレートの一括更新は「生成物同一」を機械検証してから配る

事象: テンプレート変更を「機械作業だから」と 69 リポジトリへ一括コピーしようとして、 「各プロジェクトで正常に動作するかチェックしない転換作業はおかしい」と指摘された。 検証スクリプトを書いたら 67 リポジトリで新旧生成物のバイト単位一致を示せた — 安全の根拠が初めて手に入った。

適用方法:

  1. 各リポジトリの実際の生成コマンドを新旧テンプレートで実行して diff を取る (ビルド不要になるようダミー入力を工夫する)。
  2. 全件一致を確認してから sweep(dry-run → apply)。sweep は detached HEAD と ローカル変更のあるリポジトリを弾く。
  3. sweep 後に再検証 + 組織ヘルスチェック。
  • 落とし穴: アーカイブ済みリポジトリは push できず永久に drift する — チェックは ホスティング側の archived 状態を正として除外する。同一リポジトリの二重チェック アウトがあると sweep が二重コミットする。

「指摘ゼロ」は成功ではない — 検査器が走った証拠と対にする

症状: 同じ失敗が一日に 3 つの形で出た。数週間 panic していた linter が 「6 リポに lint 設定が存在しない」事実を隠していた —「文句が無い」が「クリーン」に 読めていた。errcheck の自動修正器が「指摘 0 件」を報告したが、実際は「ビルドが壊れて linter が何も見つけられなかった」だった。そして 15 リポのリリースが tap 更新工程を 黙って飛ばしたまま「完了」と報告された — 何も失敗しなかったのは、何も実行されて いなかったからである。

なぜ: ゲートの沈黙には 2 種類ある。「検査して問題なし」と「そもそも検査して いない」。出力だけでは区別できず、指摘件数しか読まない機械ループは破壊を進捗と 取り違える。

どう適用するか:

  • すべての「0 件」に、検査器が実行された証拠を対にする: 修正ループ内での build + テスト(コードを編集する fixer は毎パス go build でゲートする)、 検査器自身の exit status、そして「50 → 0」を意味あるものにする事前カウント。
  • 機械的 sweep の後は、sweep が最適化した指標ではなく結果面の全体 (build・テスト・lint)を監査する。sweep 自身の成功数値は、sweep 自身が 壊し得る唯一の数値である。
  • 複数工程の手順の完了は「エラーが無いこと」ではなく正典チェックリストとの 突き合わせで判定する。飛ばした工程は構造的に沈黙するからである。

境界不変条件を変えたら、旧境界の消費者を全数処分する

症状: プロジェクトディレクトリの隣に第 2 の書き込みルート(セッション作業 ディレクトリ)を追加した。sandbox プロファイル・ファイルツール・システム プロンプト・ステータス表示は更新した — が、auto-approve のリスク階梯は依然として 単一ルートで「プロジェクト外」を判定しており、作業ディレクトリへの全書き込みが モデル評価か人間プロンプトを要した(最初の実地トランスクリプトで 3 件。うち 1 件は 1,871 トークンを費やして誤答に到達)。階梯を直して教訓を再適用したら、もう 1 つの 消費者(@ 参照リゾルバ)が出てきた。

なぜ: 「書き込み可能な世界はプロジェクトディレクトリ」という不変条件は 1 箇所に保存されていない — 「このパスは中か?」と問うたことのある全消費者が それぞれ再実装している。新境界の生産者(sandbox・ツール)を更新すると完了した 気になるのは、それが作っている当のコンポーネントだからで、古い消費者は旧境界の まま黙って動き続け、後からそれぞれ別の不可解なバグとして現れる。

どう適用するか:

  • 不変条件は識別子の消費先に宿る: 境界を符号化している識別子を grep -rn projectDir の要領で洗い、出荷に全ヒットを列挙する。
  • 各消費者を明示的に処分する — 変更するか、変えない理由を記録する (プロジェクト限定のまま残す @ 補完は 1 行のコメントに値する判断。 未処分の消費者は潜在バグである)。
  • 処分リストを ADR に書く。この実地バグの類型は「誤ったコード」ではなく 「誰も再確認しなかったコンポーネント」である。
  • 境界を名指しする理由文・監査行も境界に追随させる: 作業ディレクトリへの 書き込みを「プロジェクト内」と記録する — あるいは存在しないルートを主張する 拒否文を出す — のは監査証跡を虚偽にする。

core.worktree を要するリポジトリではリンク worktree が壊れる

症状: submodule の中に新しい git worktree を作ってタスクを投入したところ、 その worktree では追跡ファイルが全て「削除済み」に見え、git ディレクトリ自身の 中身(HEAD / config / hooks/ / index / info/ / logs/)が untracked と して現れた。メインのチェックアウトは無傷で健全だった。

なぜ: submodule の git ディレクトリはチェックアウトの外にあるため、 core.worktree(git ディレクトリからの相対パス)を必須とする。 extensions.worktreeConfig を有効化しながら core.worktree をメイン worktree 専用の config.worktree へ退避させないと、共有 config に残ったそれをリンク worktree が 継承する。同じ相対パスが、解決の起点によって別の場所に着地する:

解決の起点 着地先
メイン gitdir …/modules/<name> チェックアウト ✅
リンク gitdir …/modules/<name>/worktrees/<wt> git ディレクトリ自身 ❌

作業ツリーのルートが git ディレクトリになる。症状はこれで説明が付く。

どう適用するか:

  • 条件は「submodule だから」ではないgit config --get core.worktree が 何かを返すかどうかである。同じ拡張が有効な standalone リポジトリは正常に動く — 継承すべき core.worktree を持たないからだ。隔離 worktree へ作業を投入する前に これを確認する。
  • 新規 submodule では再現しない。git worktree add 単体は無害である。壊れるには 「拡張が有効かつ退避がされていない」状態が要り、それを作るのは隔離側の仕組みである。
  • 復旧: git worktree remove --force <フルパス> → 残ったブランチを削除 → git config --unset extensions.worktreeConfig → worktree を 1 つ作って git rev-parse --show-toplevel が正しいことを実証してから削除する。
  • 残骸の全数確認: grep -l worktreeConfig */.git/modules/*/config と、孤児化した worktree ディレクトリの find。登録とディレクトリはどちらの順でも生き残り得る。

一括リネームは長い識別子から処理する

事象: register-objectregister_object を先に走らせた結果、 sandbox-register-objectsandbox-register_object に化け、後からの置換が空振りして リネーム漏れになった。

適用方法: 対象を長さ降順にソートして処理する(ある識別子が別の識別子の部分文字列に なる関係があるなら、必ず長い方が先)。最後に旧パターンの grep で残骸ゼロを確認する。 区切り文字置換(kebab → snake)は特に衝突しやすい。

外部コントリビューション

拒否する PR からも正当なシグナルを採掘する

事象: AI エージェントが自動生成・投稿した PR は、複数の変更の bundle + エージェントの 内部ファイル混入で close 相当だったが、中に正当な防御改善 3 件と実在のバグ報告が 埋もれていた。これらを clean に再実装して出荷した。

適用方法:

  1. 拒否理由(scope 混在 / AI artifact 混入等)を具体的に書いて close。bundle は分割を要求。
  2. セキュリティ/安定性修正・バグ報告は拒否とは独立に評価し、採用するものは 自分で 1 から書き直す(PR のコードはコピーしない)。テスト・根拠を自前で付ける。
  3. 採用した報告は commit message と issue でクレジットし、close 後も「vX.Y.Z で出荷した」 とループを閉じる。
  4. 報告者は誠実なことが多い — 敵対的に扱わない。

挙動記述のドキュメント PR は「実装と突き合わせて」審査する

事象: 実地ノート PR に「1 個でも失敗すると呼び出し全体が失敗する。部分成功はしない」 という実測に基づく正確な記述があった。そのままマージすれば不具合が仕様として固定される ところだった。実装を確認すると、他の超過系は skip して continue しているのに読めない オブジェクトだけ全体を中断しており、非対称 = 意図しない設計だった。コードを先に直し、 記述を書き換えてからマージした。

適用方法:

  • 審査の問いを 2 段にする: ① 記述は実際の挙動と一致するか、② その挙動は意図された ものか。② を飛ばすと不具合が仕様に昇格する。
  • 同種の事象が別の扱いを受けていたら疑う(「A は skip、B は fatal」の非対称は書き忘れの ことが多い)。
  • 直す順はコード → 記述 → マージ。先にマージすると誤った記述が公開状態で残る。
  • 挙動記述はドキュメント全域に散っている — grep で全面を拾ってから直す。

ADR・リリースの粒度

ADR の置き場所は「何を拘束するか」で決め、書く瞬間に問いを強制する

事象: 組織 ADR ログの最初の 16 件のうち 9 件がプロジェクト固有 (1 アプリの内部設計、1 スキルの設計)で、後日まとめて移設する羽目になった。 消費済み番号ごとにリダイレクトが残る。

なぜ: 執筆時点で成文化された置き場が組織ログしかなかった — 規約は 「組織全体の決定はここ」と言うだけで、プロジェクト決定の置き場を書いて いなかったため、唯一文書化されたログがデフォルトの吸い込み口になった。 ADR の書式は既存レコードの模倣で学習されるため、最初の誤配置が残り 8 件の 参照形状になった。後から足した散文ルールもハイブリッドレコード (ツール廃止+後継の設計)を誤判定した — 「廃止は組織側」の条項が、実体は 後継の設計であるレコードを組織側に留めるように読めてしまう。

適用方法:

  • 両方のログを先に成文化する。書かれていない選択肢は、文書化された デフォルトに絶対勝てない。
  • 「何を拘束するか」で決める: 他プロジェクトを拘束 → 組織ログ / 1 プロジェクトのみ → そのプロジェクトのログ / 廃止+設計のハイブリッド → 設計本体は後継側、廃止の事実は組織索引行とリダイレクトに残す。
  • ADR ヘッダーテンプレートに必須の Binds フィールド (organization / プロジェクト名)を置く。模倣が書式と一緒に「どちらの ログか」の問いも複製するようになり、ログと矛盾する値はレビューで一目で 分かる。
  • 配置ルールは実際に起きた誤配置を反例としてルールの隣に書く。抽象基準は 二度外れた — 読まれるのは実例のほう。
  • レコードを移設するとき: 番号は消費済みのまま、リダイレクトを残す (公開済みリリースノートと CHANGELOG がその URL を指す)。更新可能な 参照は新しい場所へ直接貼り替える。

移植したコードは移植元の ADR 番号を素で持ち込む — 移植元名で修飾し、存在検査で閉じる

事象: 既存ランタイムからパッケージ単位で移植して別プロダクトラインを 起こしたリポジトリ(2026-09)で、コメント内の ADR-NNNN 約 850 箇所が 移植元の番号のままだった。自リポの ADR は 0001〜0005 しか無く、ADR-0004 は 自リポでは「MCP ツールの遅延広告」、移植元では「自動承認の階梯」を指す。 自動承認のコメントから自リポの ADR-0004 を開いた保守者は別の設計を読む。 出荷テンプレートまで、存在しない ADR-0008 / ADR-0077 を利用者に案内していた。

なぜ: 移植はコメントごと写す。移植元の設計参照は保つ価値がある (「なぜそうなっているか」は移植元の記録にしか無い)が、番号だけでは どのリポジトリの記録か判別できない。自リポの ADR が増えるたびに衝突範囲が 広がるので、放置すると悪化する一方になる。

適用方法:

  • 移植元の記録は gem-agent ADR-NNNN のように移植元名で修飾して写す。素の ADR-NNNN は自リポの記録だけに使う。
  • 自リポの番号と衝突する範囲(今回は 0001〜0005)は機械置換できない — 一件ずつ読んで分類する。それ以外は正規表現で一括してよい。
  • クラスは AST テストで閉じる: コメントと文字列リテラルの素の ADR-NNNNdocs/en/adr/NNNN-*.md が実在しなければ失敗。改行で gem-agentADR-0065 が別行に割れた 1 件をこのテストが検出した。
  • 出荷物(設定テンプレート・利用者向け文書)には ADR 番号を書かず、設定キー 名で説明する。

小粒で独立な UX 改善は 1 個の wide-scope ADR にまとめる

事象: 実機フィードバック由来の小さな改善 5 件(各 <100 行、strictly additive)に 個別 ADR を立てると、Context の繰り返しと薄い Alternatives で ADR が機能しなくなる。

適用方法:

  • bundle 可の基準: すべて strictly additive / 既存パスを変えない / 決定理由が共通の 根拠で説明できる。
  • 単独 ADR の基準: 設計トレードオフが独立 / Alternatives が固有 / 半年後にその 1 件で 検索される可能性がある。1 つでも該当なら個別に。
  • bundle ADR は Decision を番号付きで割り、Alternatives は項目ごとに書く(省略しない)。 README / CHANGELOG では bundle を解いて項目別に書く。bundle ADR ≒ 1 マイナーリリース。
  • 反証: 破壊的変更が混ざるなら bundle 不可(polish ADR と breaking ADR に分ける)。

相互強化する設計変更は同時に出荷する

事象: 同じ症状の別々の増幅要因を治療する 2 つの ADR を発見。片方の機能がもう片方の 出力シグナルに依存しており、別リリースに分けるとどちらも単独では体感が変わらない 状態だった。

適用方法: 設計議論の早期に「両者がどの input/output を共有しているか」を確認する。 共有があれば: ADR は別文書のまま個別承認、実装は 1 つの version bump にまとめ、 リリースノートで「together」を明示する。

en/ja ミラーは「ペアの存在」だけ検証しても中身は守れない — 識別子で照合する

事象: 二言語ドキュメントのミラー検査スクリプトが「docs/en/X.md に対応する docs/ja/X.ja.md が存在するか」だけを検証していた。ある機能追加のコミットが README.ja.md だけを更新し README.md を更新しなかったため、英語版から機能 1 つの記述が 6 リリースにわたって欠落したが、ペアは存在するので検査は毎回 green。 しかもルートの README はそもそも検査対象外だった。両言語の INDEX には「完全対応。 スクリプトで機械検証」と書かれており、実際より強い保証を主張していた

なぜ: 散文は翻訳をまたいで比較できない、という正しい前提から「中身は検査 できない」という誤った結論に飛んでいた。実際には、翻訳が変えてはいけない要素 がある — ツール名・設定キー・CLI フラグ・スラッシュコマンドといった識別子であり、 これらこそが陳腐化する当のものである。

適用方法:

  1. バッククォート内かつコードフェンス外の識別子だけを両言語から抽出し、集合が 一致することを検査する。抽出対象は --flag / /command / snake_case / [section].key のような、言語不変な形に限定する。
  2. プレースホルダ(<escaped-path><エスケープ済パス>)・ファイル名・散文は 除外する。導入前に全ペアで空振り率を測ること: 素朴な「全コードスパン一致」 は 55 ペア中 24 ペアで誤検出したが、識別子に絞ると 0 件になり、同時に本物の 片側漏れを 3 件検出した。
  3. ルートの README もペアとして含める。 ドキュメントディレクトリだけ歩く検査は、 実際に事故が起きる場所を見ていない。
  4. 検査が散文に反応したら、規則を緩めるのではなくそのバッククォートを外す
  5. 検査が主張する保証と、INDEX 等に書く文言を一致させる。「完全対応を機械検証」と 書くなら、構造だけ見ているのか中身も見ているのかを正確に書く。

ドキュメント

アーキテクチャ文書は「なぜ」を書く

事象: 「こうなっている」の構造説明だけでは、将来の変更判断に必要なコンテキストが 伝わらないという明示的なフィードバックを受けた。

適用方法:

  • 各セクションを「なぜ X なのか」形式で構成し、却下した代替案とその理由も書く。
  • 実運用で得られた知見(実測データ、インシデント)を根拠として含める。
  • コードを読めば分かる「what」は書かない。
  • 技術革新が大きい場合はサブシステム別に分巻し、概要文書から各巻へリンクする。

プロンプトを使うツールの文書はプロンプト原文を載せる

事象: 日本語版ドキュメントでプロンプト内容を翻訳のみで記載すると、実際に LLM に 送信される指示との乖離が生じ誤解を招く。

適用方法: 原文(英語プロンプト)を blockquote でそのまま記載し、その下に翻訳を 付ける。評価基準(severity 等)は判定基準と判定例をテーブルで明記。共通ボイラー プレートは初出時に全文、以降は省略記号で。

序列を記した文書は、対象群が増えるたびに端点を検証し直す

事象: 自社 MCP サーバ群を「その照会を誰が観測できるか」で序列化した戦術文書が、 最上段を「対象は urlscan.io からの訪問を見る」としていた。その後に増えた 2 本のうち 1 本は自分の Chrome を操作するサーバで、自社 IP からの直接接触——記載された天井 より確実に露出が大きい。既存の行はどれも間違っていない。間違っていたのは梯子の 終端で、エージェントはそこを「これより悪い手はない」と読む。

なぜ: カタログのドリフト(行の欠落)は監査で見つかり、影響も漸進的。序列の ドリフトは違う。実際の最大値より低い位置に天井を置いた尺度は、誤った上限を能動的に 教えるうえ、抜け落ちた段こそが熟慮なしに踏まれる段になる。同じ点検で対になる失敗も 出た——「すべてのサーバが get_usage を持つ」という断定に対し、実際には 3 本が 持たない。散文中の全称量化子は、次の追加が黙って壊せる約束になる。

適用方法:

  • 何かが序列付けされている群にコンポーネントを足すときは、メンバーシップだけでなく 序列の端点を導出し直す。行を足す前に「最上段の上、最下段の下に何か生まれたか」を 問う。
  • そうした文書のスコープは用途ではなく能力で切る。ブラウザ自動操作サーバが OSINT 戦術書に入るのは、目的が何であれ対象に接触できるから。カテゴリで除外すると、 最も露出の大きいツールが未記述のまま残る。
  • 「すべての X は Y」ではなく「大半の X は Y、例外は A・B・C」と書く。例外の列挙は 1 文で済み、将来の暗黙の破綻を将来の 1 行修正に変換できる。
  • 元の ADR は書き換えず参照で改訂する。旧序列とその理由が読める形で残る。

inspired-by でも帰属を書く

事象: 直接の移植ではなく「設計を参考にした」ケースでも、帰属を書くコストはゼロで、 省略のリスクは非ゼロ。

適用方法: 他プロジェクトの設計やヒューリスティクスを参考にしたら、「derivative work か」の判定に関わらず (1) ソースファイルのコメントでプロジェクト・作者・ライセンスを 参照、(2) LICENSE に Third-Party Notices 節を追加する。

多くの保持者がポインタで指すセッション資源は中身を差し替えて再起動し、その消費者は一覧にして歩く

事象: CLI エージェントが /clear を新セッション(新 id・トランスクリプト・ 作業ディレクトリ)にし、新しい事実を子に export した。消費者 3 つが古い方を 保っていた: 起動時に組んだ sandbox プロファイル(新作業ディレクトリへの シェル書込が全部拒否された)、ロガーを値で捕まえていた side-call ツール (usage 記録が閉じたファイルへ行った)、旧 id を引数に spawn 済みの MCP サーバー(claim は旧セッションに帰属し、フックは新セッションを報告した)。 テレメトリは id を resource に焼き込んでおり「既知の制約」と宣言されていた。 どれもローテーション自体より 1 回遅いレビューで見つかった。

なぜ重要か: 以前の live-reload バグから得た「変数を追え」は、消費者が コピー・派生物(コンパイル済みプロファイル)・子プロセスを持つ場合には 足りない。そしてポインタが多くの場所(エージェント、defer 時点で レシーバを束縛した closer、子の sub-sink)に保持される資源は、変数の 再代入では置き換えられない。

適用方法:

  1. セッション資源が回るときは消費者一覧を書く — 起動時にそれを読んだ 全ての場所、派生物と spawn 済みプロセスを含む — そしてローテーションを 一覧を歩く関数にする。各消費者を旧値・新値の両方に対して試すテストで 固定する(sandbox のテストは両ディレクトリに書く)。
  2. 多所でポインタ保持される資源はその場で再起動する: ポインタは保ち、 mutex 下で中身を差し替え(Restart(id))、派生ハンドル(Sub)には フィールドをコピーさせず root を読ませる。deferred な呼び出しと長寿命の 保持者は何もせず追随する。
  3. 事実を引数や環境に持つプロセスは、教えるのではなく spawn し直す — 操作者の手動 reload と同じ経路で。
  4. 「既知」と書いた制約は、まだ回していない消費者である。文書にそう書き、 操作者がそれを求めてくると想定する。

修正は根本原因の根絶が基本 — レビュワーは観測し、コントリビュータが決める

症状: CLI エージェント(2026-09)で、リリース後の外部レビューが 9 パス続いた。 所見 103 件を毎回「全件修正」したが、次のパスで同型の別事例が出た。9 パス後に 所見を根本原因で分類すると、48 件(再発 16 件中 13 件)が 3 つの原因 — コマンド 文字列からシェルの意味論を再導出していた、封じ込め API が字面パスを返して 呼び手が開き直していた、上限が「打ち切りの事実」を返していなかった — に帰着し、 それぞれは 1 回の構造変更で閉じた。利用者からは「局所ケースにフォーカスしすぎ。 根本原因を直せ」と指摘された。

なぜ: CONVENTIONS の「smallest change that addresses the root cause」は、実務では 「最小の変更」の側だけが読まれ、「報告を黙らせる最小の変更」に退化する。レビュワーは 症状の観測者で設計を持たないから、その指摘は必然的に局所的で、それ自体は正しい 振る舞い。問題は受け手が指摘を「修正案」として併合することで、そうすると レビュワーの次の綴り探しに設計が付き合うことになる。一方で、局所的な観測から 本物の穴(端末への注入、.git 丸ごと差し替え)も出る — 併合しないことと無視する ことは別。

適用:

  1. レビュー指摘は直す前に分類する: 全件を根本原因クラスに振り、件数と再発数を 数える。同型が 3 件、または修正後の再発が 1 件あれば、インスタンス修正を止めて クラスの原因を書き出す。
  2. 原因は「領域」で言う: 無限領域(文字列・綴り・個別プログラム名)で判定していないか、 名前とハンドル(パスと fd)を混ぜていないか、上限が打ち切りの事実を返しているか、 同じ判定が複数箇所に実装されていないか。
  3. 構造で閉じ、クラスを名指しするテスト(アーキテクチャテスト + 振る舞いテスト)で 固定する。個別インスタンスの回帰テストは補助。
  4. 一時パッチを入れるなら、根本原因と閉じる予定を ADR か issue に書く。書けないなら それはパッチではなく設計の問題。
  5. レビュワーの指摘は事実として採り、修正案としては採らない。 どう閉じるかは コントリビュータが設計で決める。採らない指摘は理由を書いて記録する(ADR の 「記録のみ」節、レビュー回答)— 無言で捨てるのは併合と同じく判断の放棄。
  6. 「全件修正した」は成果ではない。クラスが閉じたかが成果。

CONVENTIONS.md §Build small, fix small の「Root cause before patch」「Reviewers observe; contributors decide」がこの規則の正。

索引の検査を識別子で行うと改名を素通りする — 番号はリンクではない

事象: ADR ファイルと索引エントリを 4 桁番号で突き合わせる検査があった。ADR を改名した ところ、両言語の索引が存在しないパスを指したまま、「索引は完全で順序も正しい」と緑を出し 続けた。

なぜ: 番号は改名を生き延びる。検査は「エントリが存在すること」を確かめていて、「リンクが 辿れること」は一度も確かめていなかった。

適用方法: 索引内の相対リンクをファイルシステムに対して解決する検査を足す。識別子の一致と リンクの解決は別の性質であり、前者は後者を含意しない。

GitHub 運用

リポジトリの Watch は自動では付かない(auto-watch は 2025-05 廃止)

事象: org 配下 124 リポジトリの Watch 状態がばらついていた(アクティブの約半数が Default のままで、外部からの issue / PR の通知が届かない状態)。原因は GitHub が 2025-05-23 に「Automatically watch repositories」を廃止したこと — 廃止前に作られた リポジトリは自動 Watch されており、それ以降のものは Default のまま、という時期による 断層だった。設定画面から該当トグルが消えているのはこのため。

なぜ重要か: Default(Participating & @mentions)はオーナーであっても新規 issue / PR を 通知しない。単独運営の org では、Watch していないリポジトリへの外部コントリビュー ションは静かに埋もれる。

適用方法:

  • リポジトリ作成直後に明示的に Watch する: gh api -X PUT /repos/<org>/<name>/subscription -F subscribed=true (scaffold チェックリストに含める)。
  • アーカイブ時は Ignore に切り替える(アーカイブ済みは issue/PR を受け付けないので Watch は純粋なノイズ): -F ignored=true
  • 現状の棚卸しは REST API で機械的にできる: GET /repos/<org>/<name>/subscription(404 = Default、subscribed = Watching、 ignored = Ignoring)。

(#N) は参照のみ — 自動クローズには Closes キーワード

事象: fix(...): ... (#15) の括弧参照でコミットしても issue は自動クローズされず、 open のまま残って指摘された。PR を経由せず main 直 push でリリースする運用では、 コミット本文に明示しない限りクローズされない。

適用方法: issue を解決するコミットは括弧参照に加えて本文末尾に Closes #N を書く。 複数 issue は Closes #15, closes #16各番号にキーワードを付ける(1 つの Closes に 複数番号は GitHub が拾わない)。リリースコミットではなく fix コミット側に書く。

リンク済みランタイムの未使用機能は「ただ」に見える — 順番はライセンス→実測→実装

事象: ある前処理機能を検討したところ、すでに静的リンクしているランタイムが その C API を持っていた。第三の依存は不要、ブリッジ 1 本、実行コストも RTF 0.04。 「ほぼ無料」に見えたが、結論は不採用だった。

なぜ: 見えていなかったコストが 2 つあった。

  1. モデルのライセンス。 候補 2 種とも重みのライセンスが未宣言で、既定配布に載せ られない。これは技術で解決できないので、最初に見るべきだった。
  2. 効く対象が想定と違った。 主たる動機(A の改善)には届かず(18 → 14、目標は数人)、 想定していなかった B(別の失敗モード)に効いた。「効いた/効かない」ではなく 「何に効いたか」を分けて測らないと、採否の判断材料にならない。

適用方法:

  • ライセンスを技術検討より前に見る。 逆順にすると、動くと分かったものを捨てる ことになり、判断が感情的に難しくなる
  • ブリッジを書く前に、上流の CLI 例を submodule からそのまま建てて測る。 リンク済みランタイムなら追加の configure だけで建つことが多く、自前バインディングが 公開していないパラメータもこの方法で評価できる
  • 効果は期待した対象ごとに分けて測る。1 つの総合評価にまとめると、 「期待した所には効かなかった」が「効いた」に埋もれる
  • 不採用も ADR に残す。次に同じ機能を見つけた人が同じ調査を繰り返さないために、 実測値と再検討の条件(何が変われば話が変わるか)を書く
  • ただし不採用は CHANGELOG に書かない。CHANGELOG は利用者から見た変化の記録で、 入れなかった機能は利用者から見て何も変わっていない。"Changed" の下に「入れないことに した」と並べると、何が変わったのか読めなくなる。判断は ADR、寄与者向けの注意は AGENTS.md、CHANGELOG は挙動とドキュメントが実際に変わったときだけ

ステータス出力はドキュメントではない — 毎回表示される正しい行も「で?」と読まれる

症状: CLI エージェントのフィールドテストで、オペレータが UI 文字列の 一族を指摘した: /usage の毎回のキャプション(「cache はコスト/レイテンシ の節約でウィンドウではない」)、自分が手で書いたファイルへの「(手書き)」 ラベル、/mcp 一覧のたびに付く 2 行の機能説明、/help 内の設計理由 (「あなた自身がタイプしたものだから」)、そして手動改行されたソースが 広い端末で画面幅の半分で折り返るヘルプ。どの行も正確だった。オペレータの 反応はどれも「…で?」だった。

なぜ重要か: これらは設計文書の散文が UI に漏れたものである — 設計者の関心(正直性の但し書き・分類学・理由)に答えており、その瞬間の オペレータの問いに答えていない。毎回表示される「正しいが行動に繋がらない」 キャプションは読み手に読み飛ばしを訓練し、読み飛ばしは行がついに意味を 持つ場所(警告・開示)でこそ致命的になる。コストは毎回のレンダで、 永遠に支払われる。

適用方法: オペレータ向けテキストを「どの問いに答えるか」で仕分ける:

  1. セッションの事実(数値・状態・変化する出所・いま起きたこと)→ コマンド出力に属する。
  2. 機能の説明(但し書き・理由・分類・使い方)→ リファレンス docs に 一度だけ。移すのが修正であり、事実の削除ではない。
  3. 教えるのは空状態で — オペレータが実際に「で、どうすれば?」と 問う唯一の場所: 書くべきファイルと打つべきコマンドを名指しする空の 一覧は良い UX であり、同じ文が埋まった一覧に付けばノイズである。
  4. イベント単位の開示は残す(クリップされた行・非表示行・返却された 予約メッセージ): 現れた瞬間に行動可能だからである。
  5. ヘルプはマニュアルではなく地図: 1 項目 1 行・桁揃え・セクション間に 空行・理由なし — そしてソース内で文を手動改行しない。書かれた改行は 広い端末で「半分幅の折り返し」になる。
  6. 終了には説教ではなく領収書: スクロールバックの最後の行は 「どう戻るか」(再開コマンド)と「いくらかかったか」に答える — 2 行だけ、何かが起きたときだけ。
  7. 設計参照は画面に出さない。 バナー注記の (ADR-0074) や、規則が ある理由を説明する節は実装者の独り言で、操作者には何の得もない。 ADR を見ながら機能を書くたびにこの癖は戻るので、構造で閉じる: 操作者向け・モデル向けパッケージの文字列リテラルに ADR 番号があれば 落ちる AST テストを置く。
  8. リリース前にラベルを集めて通読する。 40 ファイルに散った文言は、 操作者が読むように — 1 画面ずつ、他に何も持たずに — は読めない。1 文書 (ja/en カタログを verb にサンプル値を入れて並置、注記/エラー/フラグ help、 --help 頁)を生成し、変更を書いていない読み手が基準で通読する: 事実と 次のコマンドだけ、設計参照なし、理由なし、ja と en は同じことを言う、 サンプルで自然に読める。diff レビューではできない — 見えるのは変更行で あって画面ではない。

「docs は同じコミットで更新」は文書名を指定しない — 変更種別で経路を引き、全体像文書は機械検査する

事象: 「挙動変更は同じコミットで docs を更新」という規約の下で、すべての コミットは確かに docs を更新していた — それでもアーキテクチャリファレンスは 3 ADR 分の内部パッケージ 5 つを欠き、設定リファレンスはサブコマンド 1 つを欠き、 仕様書のセキュリティ設計節は 2 ADR 前のモデルを説明していた。更新されたのは 機能名から辿れる文書(承認の挙動なら承認リファレンス、ツールならツール リファレンス)で、全体を語る文書には届いていなかった。構造変更の多くはレビュー ラウンドの fix: コミットで入り、ADR とエージェント向け説明ファイルだけを更新した。

なぜ: 規約はいつを言い、どれをを言っていない。全体像文書(アーキテクチャ・ 仕様)には新機能の名前が無いので grep では見つからず、コードと文書を比較する検査も 無かった — 既存の検査は全部が対称性検査(en/ja ミラー・ADR 索引)だった。経路も 検出器も無い規則は、いちばん近いものを更新すれば満たされてしまう。

適用方法:

  • エージェント向け説明ファイル(AGENTS.md)に変更種別 → 文書の表を置く: 新パッケージ → アーキテクチャの一覧、新コールバック → UI 契約の段落、新サブコマンド/ フラグ/キー → 設定の表・例示 config・README の usage、承認/信頼/sandbox の挙動 → 承認リファレンス・アーキテクチャの承認節・仕様のゲート表とセキュリティ設計、 新レコード/状態ファイル → セッションリファレンスと配置 ADR への Amended by 注記、 過去 ADR の記述と変わる変更 → その ADR への Amended by 行。
  • 構造を変える fix:feat: と同じ行を取る。
  • 経路化できる行は docs 検査で機械化する: internal/ の全ディレクトリが アーキテクチャリファレンスに載っている、エージェント options 構造体の exported func フィールドが全て載っている、cobra の Use: が全て設定の表にある。初回実行で 古い ADR 由来の欠落がさらに 2 件見つかった。
  • 対称性検査(en↔ja・ADR↔索引)は必要だがこのクラスは見えない。その緑を 「docs は最新」と読まない。

アーカイブ状態は GitHub にしか無い — ローカル走査は死んだリポを掴む

事象: ローカルの Makefile を根拠に「Linux 版を配布している 63 リポ」を数え、そこから 作業対象 21 リポを選んだ。**3 つが GitHub 上でアーカイブ済み(read-only)**だった。うち 1 つでは実在の欠陥(終了時に飛行中の送信 goroutine を待たず、監査テレメトリが静かに落ちる) を特定したが、push できないので直せなかった。同じ調査で、org 内で唯一 Apache-2.0 の 2 件も アーカイブ済みリポだった。

なぜ: リポジトリが生きているかはリモート側の属性で、作業ツリーには痕跡が無い。判定を gh の呼び出しに寄せると、オフラインでは効かず、呼び忘れれば静かに死んだリポが混じる。 「カタログに (archived) と書く」方式は文字列規則なのでドリフトする。

適用方法:

  • アーカイブ済みは専用の umbrella に隔離し、元シリーズをディレクトリ階層として残すarchive-series/<series>/<project>)。「生きているか」と「何だったか」の両方を ls が 答えるようになり、ネットワーク呼び出しもマーカーの維持も要らなくなる。
  • 稼働 umbrella にアーカイブ済みが混じったら落ちる検査を置く。特例で走査を飛ばすのでは なく誤配置として名指すと、構造が自分で維持される。
  • 一括作業の対象リストを作ったら、着手前にアーカイブ状態を確認する。ローカルのファイルは この問いに答えられない。
  • アーカイブ済みリポで見つけた欠陥はタスクではなくナレッジ。直せないので、一般化できる なら還元し、できないなら寝かせる。
  • カタログ行を移すときは書き直さず原文のまま移す。後継の名前とアーカイブ理由は、 アーカイブ後にこそ読まれる情報で、要約すると真っ先に落ちる。

submodule に埋め込まれた .git は、外すときだけ牙を剥く

事象: umbrella からアーカイブ済み submodule を外そうとしたところ、 fatal: could not migrate git directory ...: Directory not empty で中断した。調べると 9 件の submodule が .git をファイルではなくディレクトリとして持っていた — submodule のパスに独立したクローンがそのまま座っている状態。うち 2 件は umbrella 側の .git/modules/<name> に 5 か月前の残骸が残っており、移行先が塞がっていた。

なぜ: 正常な submodule の .gitgitdir: ../.git/modules/<name> という 1 行の ファイルで、リポジトリの実体は umbrella 側にある。submodule のパスへ直接 clone すると、 git は gitlink を記録して受け入れるので、日常の commit / push / status は全て正常に動く。 壊れているのに何も言わない。 表に出るのは deinit / rm のときだけで、git はまず埋め込み .git を umbrella へ「吸収」しようとし、移行先が空でなければ中断する。つまり気づくのは 一括移設の途中という最悪の場面になる。

適用方法:

  • 検出は「<submodule>/.git がディレクトリか」の一点。一括作業の前に全 submodule を 走査する。
  • 是正は git submodule absorbgitdirs。追跡ファイルを変えないのでコミットは発生せず、 HEAD もブランチも変わらない。
  • 移行先に残骸があるものは先に退避する。残骸の HEAD と更新時刻を実体と比べれば、 どちらが生きているかは即座に判る。残骸は再帰削除せず退避に留め、確認後に人が消す。
  • 事前に全対象がクリーン・push 済みであることを確認する。
  • 事後は 4 点まで見る: .git がファイルであること、HEAD が不変であること、 git submodule status+/- を出さないこと、実際に fetch できること。

umbrella で git add -u は submodule のポインタを黙って巻き込む

事象: 全シリーズの CLAUDE.md と README から古いビルド指示を消す一括修正で、各 umbrella を git add -u でステージした。直前に配下ツールを push していたため、移動していた submodule のポインタが全て同じコミットに入った。結果、5 つの umbrella で「ドキュメント 修正」というメッセージのコミットが、無関係なツールの bump を 1〜4 件抱えることになった。 push 済みかつ履歴に埋もれた位置だったため、分割は見送った。

なぜ: submodule のポインタは gitlink(mode 160000)という追跡ファイルで、-u は 追跡ファイルの変更を区別なく拾う。作業ツリー上はディレクトリに見えるのに、 git status --short には M <tool> と 1 行出るだけで、ドキュメントの変更と見分けが つかない。umbrella では配下ツールを push した瞬間に必ずこの差分が生まれるので、一括作業 では高確率で踏む。

適用方法:

  • umbrella では git add -u / git add -A を使わず、パスを明示して add する
  • コミット前に git diff --cached --stat を見る。gitlink は「1 行だけ変わったファイル」の 顔をするので、意図しないツール名が並んでいないかを目で確認する。
  • ポインタの bump は別コミットにする。chore: bump <tool> to vX.Y.Z という形が守られて いるのは、後から「いつ何を取り込んだか」を追うため。混ぜるとその索引が消える。
  • 分割が現実的なのは push 前だけ。 埋もれてから直すには rebase と force-push が要り、 commit hygiene という理由には見合わない。踏んだら記録して次に活かす。

報告は制御ではない — 危険への手当てが「起動時に表示する」なら免罪符を疑う

事象: CLI エージェント(2026-09)で、MCP ツール除外機構の初稿が「サーバーごとに 毎起動 1 行の drift 報告」を出そうとした。利用者の評価は「出しとけばなんでも許される という免罪符。で? なに? という状態」。既存のバナーを数えると 28 行分の候補があり、 それは誰かが設計した結果ではなく、起動中に stderr へ書かれたものが自動的にバナー行に なる配線(stderr を起動ノートにティー)で積み上がっていた。同じリポジトリの AGENTS.md には「4 つのリリースが説明的バナーを出荷した — 誰も全体を 1 箇所で見ていなかったから」 と記録済みだった。決着は ADR で規則を 1 文にした: 「他の何も言わない事実だけがバナーに 行を持つ」。28 → 3 行。drift 報告は機構ごと落とし、既存の承認ゲートが覆うと結論した。

なぜ: 毎回出る状態表示はどの回も読まれない(本書「ステータス出力はドキュメントでは ない」の読み飛ばし訓練と同根)。それに加えて、危険への手当てとして報告を出すのは、 制御を設計しなかった事実を報告で覆う行為になる。制御なら止まる・効く・測れる。報告は どれもしない。そして誰も壁を建てると決めないまま壁が建つ配線があると、次の機能も同じ 経路で行を足す — 初稿がまさにそうしていた。

適用方法:

  1. 危険への手当てとして「表示する」を提案したら、それは制御か免罪符かを自分に問う。 制御でなければ設計をやり直す — 既存のゲートが覆っているなら報告は不要、覆っていない なら報告では足りない。
  2. 起動時に印字してよいのは ①状態ではなく変化 ②次にやることが名指しされている ③滅多に出ないの 3 条件を満たす行だけ。状態は問われた場所(設定パネル・一覧 コマンド等のオンデマンド表示)で答える。先に「オンデマンド表示が既にあるか」を 確認する。
  3. バナーをパッケージにして事実の型で閉じる: バナーが知ってよい事実を struct の欄に 限定し、行を足したい機能は欄を足して「なぜ他の何も言わないか」を答える。stderr の ティーのような「書けば出る」配線は切る。組み立て済みのバナーを操作者文言の通読 文書の先頭に描く — 断片の一覧では最初の 1 画面が読めない。
  4. 逆側の過剰補正に注意 — 利用者が待っている処理の沈黙はハングに見える。禁じて いるのは無条件の状態表示であって、進行中の作業の通知ではない。

「再読み込み」経路の再利用は、変更の粒度と適用の粒度が合うときだけ

事象: CLI エージェント(2026-09)の設定パネルで MCP サーバーやそのファンクションを 1 行オン/オフするたびに、既存の「全体再接続」経路(/reload 相当)を呼んでいた。 全サーバーのプロセスを kill → 再 spawn → tools/list し、それを TUI のイベントループ内で 同期実行するので、25 本のサーバーを持つ機では矢印キー 1 回が数秒かかり、その間に打った キーが詰まって後から一気に効いた。リリース後の利用者報告で判明。修正はサーバー 1 本だけ を再接続する経路(動作中なら再 list のみ、オフは停止、オンは起動、他は不触)。その修正の リリース前独立検証で、さらに 1 件: サーバーのツール削除を 名前 prefix で行っていたため、 foo のトグルが隣の foo__bar の live tool を黙って消し、64 字上限で切り詰められた名前は 削除漏れして再登録が全件失敗した。prefix 照合は元々 transcript の帰属用に「エッジは許容」 と文書化されていたが、削除の駆動に転用した時点で許容できないエッジになっていた。

なぜ: 既存の再読み込み経路は「全部やり直す」ことで正しさを買っている。全体を対象に する操作(明示的な reload)ならその代償は妥当だが、1 単位を変える操作に再利用すると、 代償が単位数に比例して膨らみ、UI のイベントループを塞ぐ。そして、ある用途で「有界だから 許容」と記録したエッジは、同じプリミティブの呼び手が強い用途(削除・拒否)に変わった 瞬間に再審査が要る — 文書化された許容は用途つきであって、プリミティブの性質ではない。

適用方法:

  • 対話操作から既存の reload 経路を呼ぶ前に、変更の粒度(1 行)と経路の粒度(全体)を 突き合わせる。合わないなら 1 単位の経路を作る。全体経路は明示コマンド専用に残す。
  • 1 単位の経路は「何を触らないか」をテストで固定する: 隣の単位が close も再 list もされず、 ツールが同じ名前で残ることを数える(spawn 回数・list 回数・close 回数のカウンタ)。
  • 「有界エッジとして許容」と書いた prefix / 近似照合は、呼び手が変わるコミットで再審査 する。1 単位の削除は、その単位が登録した名前の集合(有限・既知)を記録して exact で行う。
  • 同期実行の残りは測ってから決める: 1 単位の再 spawn(1〜3 秒)が残るなら、非同期化 (適用中の表示つき)は次の判断であって、粒度修正と同じコミットに混ぜない。

呼び出し箇所を 1 つに集約する変更は、grep ではなく「振る舞いの分岐」で数え上げる

事象: CLI の TUI で、送信した行がスクロールバックに残らないという利用者報告。 ターン・シェルエスケープ・スキル展開などは "\n" + "> " + 入力 を出していたのに、 スラッシュコマンドの分岐だけが応答をそのまま出しており、入力欄は送信時にクリアされる ため「どのコマンドの結果か分からず、直前の出力との境目も無い」状態になっていた。修正は ヘルパー 1 つに集約し、既存 5 箇所も含めて全経路をそこに通すこと。ところがリリース前の 独立検証で 7 番目の分岐が見つかった: スキル展開の成功パスのすぐ上にある姉妹の early return(errMsg != "" { return ... })が同じ欠陥のまま残っていた。

なぜ: 集約対象を洗うのに使った道具が **grep(置換対象の文字列リテラル)**だった。 grep は「既にそのコードを持っている箇所」しか返さない。直すべきなのは、それを 持っていない箇所である。 掃き漏らしは構造的に、同じ if ブロック内で成功パスの 隣にある error return に出る — 目は成功パスを追い、early return は「別のこと」に 見える。さらに、集約後に書いた文書が「全ての送信行はエコーされる」と全称で主張して しまい、実行されない行(キュー拒否・空入力)や応答がパネルである場合に偽になった。 集約は網羅の証明ではないのに、集約したという事実が全称主張を誘う。

適用方法:

  • 集約・一括置換の対象は、振る舞いで定義して数え上げる(「応答を出力する submit の 分岐」)。置換対象リテラルの grep は補助に過ぎない。定義した集合を関数の分岐と突き 合わせ、件数を先に言う(「submit には応答を出す分岐が 7 つある」)。
  • early return と姉妹の成功パスを対で見る。 同じ条件ブロック内の if err != nil / if errMsg != "" は、成功パスと同じ扱いを要する同格の出口である。
  • 集約後の全称主張は、反例を 1 分探してから書く。書けないなら、真になる水準まで 述語を下げる(「送信した全ての行」→「実行される行」+例外を名指し)。
  • 検証は分岐を並べたテーブルテストで閉じる。1 経路の回帰テストは、掃き漏らした分岐を 永久に見ない。

関連: 「境界不変条件を変えたら、旧境界の消費者を全数処分する」(あちらは識別子の grep が 効く形、こちらは効かない形)。

「仕様に無い」は一次資料の引用が無ければ検証されていない — 実装の注釈は仕様の証拠にならない

事象: 「MCP プロトコルにはキャンセル通知が無い」という断定が ADR とナレッジに 4 か月残り、別プロジェクトの独立検証で初めて誤りと判明した。仕様は最初の公開版から その通知を定義していた。

なぜ: ADR は仕様書の URL・schema・章名を一切引かず、自社クライアントのコード 注釈(「ブロック中の読み取りを解除する唯一の手段は kill」)だけを根拠にしていた。 実装の制約を仕様の不在と取り違えたもので、「調べた」という記憶だけが残り、 何を調べたかは残らなかった。否定的な断定は肯定より検証されにくい — 「無い」ことを 確かめるには一次資料を読むしかないのに、読んだ痕跡が無ければ誰も再検証しない。

適用方法:

  • 「X には Y が無い」と ADR・ナレッジに書くときは、一次資料の所在(仕様の版・ schema のパス・章名)を必ず併記する。引けないなら「未確認」と書く。
  • 自社実装のコメント・挙動は「自社実装がどうか」の証拠であって、上流仕様・上流 製品の証拠にはならない。両者を別の文に分ける。
  • 断定に基づく設計判断が正しくても、根拠の誤りは訂正する。結論が同じでも 「なぜ正しいか」が変わると、次の判断で効く条件が変わる。

兄弟プロダクトが共有する機構は、片方だけ直しても直っていない

事象: 同じ設計から派生した 2 つのランタイム(一方が他方の移植元)で、資格情報読取の境界をカーネルへ移す設計変更を両方に入れた。その後の独立検証で見つかった欠陥 2 件は、両方に同型で存在した。どちらの修正も 2 回書くことになった。さらに、移植先のツール説明文は移植元で撤回済みの挙動を 1 リリース遅れで宣言し続けていた。ADR には「移植元であって上流ではない」「再現しない機能はこれ」と書いてあったが、両者が実際に共有しているコードをどう扱うかは、どちらの文書にも書かれていなかった。

なぜ: 移植関係の文書は「何を持ち込むか/持ち込まないか」を決めるために書かれる。すでに両方にあるコードは、その問いの外側に落ちる。結果、片方のリポジトリで作業する人には兄弟の存在を示す信号が何も無く(移植元側の AGENTS.md は移植先に一度も言及していなかった)、共有機構の修正が片側で止まる。止まったことは、次に誰かが同じ欠陥を踏むまで分からない。

適用方法:

  • 両者が持つ機構の欠陥・設計変更は、同じ作業の中で両方直すを既定の規則として、両方のエージェント向け文書(AGENTS.md)に書く。片方だけに書くと、もう片方で作業する人には届かない。
  • 共有している機構を名前で列挙する。「共通部分」と書くと読み手ごとに範囲が変わる。
  • 規則が何でないかも書く: これは機能を移植する規則ではない。移植関係の ADR が決める範囲は変えない。
  • 片方だけ直すと決めたら、コミットメッセージと ADR に理由を書く。書かれていない分岐は次の人に見落としとして読まれ、間違った方向に「修正」される。

upstream に修正を提案する前に、設定ではなく参加の履歴を読む

何が起きたか: 局所パッチが vendored 依存の実在の欠陥を直し、その欠陥は依存の main の先端、 つまりプロジェクトが固定しているまさにそのコミットに生きていた。次の一手として upstream に 提供するのが自然に見えた。リポジトリは公開、MIT、未アーカイブ、issue は開いており pull request は 誰でも作れる設定で、どれも「貢献歓迎」と読める。しかし履歴は別のことを語っていた。CONTRIBUTING も issue/PR テンプレートも discussions も無く、fork は 0、star は 1、維持者の関連 2 リポジトリの pull request はすべて本人がエージェント生成のブランチから出したもので、うち 3 件は 3 週間以上 オープンのまま。唯一の issue も本人のものだった。判断は、局所パッチを維持し、次の依存更新時に 再検討することだった(spice-client、2026-09-18)。

適用方法: リポジトリの設定は「機構として何が可能か」を語るのであって、「何が望まれているか」では ない。提案が読まれるかを測る安価な信号は参加の履歴のほうである。fork の有無、所有者以外のアカウントに よる pull request、外部からの issue、そして維持者自身の pull request がマージされるまでの滞留時間。 それらがすべてゼロなら、提案は返事を期待しない贈り物として予算を取るか、見送る。単独かつエージェント 主導の開発は、同じ修正に自力で到達する可能性が最も高い形でもあり、提案の価値をさらに下げる。 いずれにせよ、判断とその根拠をパッチの隣に書き残し、次の人が調査をやり直さないようにする。そして 更新経路で「upstream 側で穴が塞がれたか」を確認させる。パッチが素直に当たらなくなることがその信号である。

vendored パッチは上流に対して再生して検証する — ハッシュは「変わっていない」を示すだけで「説明できている」を示さない

事象: 固定した上流の依存に、ADR ごとのローカルパッチを当てて vendored していた。 全ファイルのハッシュとパッチファイルのハッシュを記録し、検査スクリプトで照合していた。 新しい修正を入れる段になって、記録されたパッチ 2 本が、実際の vendored ツリーと上流の 差分を説明しなくなっていることに気付いた。検査は最初から最後まで緑のままだった。

なぜ: ハッシュが固定しているのは「誰も記録せずに編集していない」ことだけである。 パッチファイル自体もハッシュで固定されているが、それはパッチの中身が変わっていないと いう意味であって、そのパッチを上流に当てたら vendored ツリーになるという意味ではない。 この 2 つは独立した性質で、前者だけを検査していると、パッチは説明文へと静かに劣化する。 劣化に気付くのは、上流の固定を進める時、つまり最も判断材料が要る時である。

適用方法: 記録したパッチを固定した上流に対して実際に当て直し、結果が vendored ツリーと一致することを検査する。

  • パッチが触るファイルはパッチ自身のヘッダから読み取る。新しいパッチを足しても検査側を 直さずに済む。
  • パッチは UPSTREAM 記録が列挙する順序で当てる。順序も記録の一部である。
  • 上流の取得にネットワークが要るので、通常のテストとは別のターゲットにしてよい。ただし 依存の固定を動かす前には必ず走らせる。
  • ローカルパッチが 1 本を超えたら、1 パッチ = 1 判断(1 ADR)に分ける。どの決定が どの行を必要としているかが、後で上流の変更と突き合わせる唯一の手掛かりになる。

ローカルパッチは被試験系の一部 — 依存の不具合を追う前に自分の差分を疑う

事象: 依存ライブラリを通したファイル送信が途中で止まった。依存側の実装を読み込み、 再入可能性の欠陥を見つけて直した。それでも止まった。3 つ目に見つかった真因は別にあった。 1 つ目の原因は、2 つの ADR 前に自分たちが当てたローカルパッチの中にあった。権限が 拒否された経路で早期 return しており、同じメソッド末尾にある別の処理(転送と表示構成の 駆動)を丸ごと飛ばしていた。

なぜ: ローカルパッチは、リポジトリの中で最もレビューの薄いコードである。上流の テストは当然それを通さない。自分たちのテストは、パッチが対象とした機能(この例では クリップボード)だけを見る。パッチが同じ関数の他の責務に与える影響は、どちらの 網にも掛からない。しかも本人の記憶の中では「クリップボードの小さな変更」として 格納されているので、無関係な機能を追う時に候補から外れる。

適用方法:

  • 依存の不具合を疑い始めたら、その依存への自分の差分を最初に読む。上流の原本と 並べて読むこと。記憶ではなく差分を読む。
  • パッチを当てた関数が、パッチの目的以外に何をしているかを確認する。早期 return と guard は、末尾にある無関係な処理を巻き込む代表格である。
  • 1 つの症状の裏に複数の原因が並ぶことを想定する。先に見つかった原因を直しても症状が 変わらないのは「直し方が悪い」ではなく「まだ他にある」であることが多い。
  • パッチを広げたら、その判断を元の ADR ではなく今の ADRに記録する。元のパッチが いつ何のために広がったかが、次に上流の固定を動かす人への引き継ぎになる。