全体レビューで 30 件直したら、UI を 1 回壊した話 ── C3 v2.9.0
前回記事:
https://zenn.dev/satoh_y_0323/articles/e8512b36ac4cd7
C3 GitHub:https://github.com/satoh-y-0323/claude-code-conductor/PyPI:https://pypi.org/project/claude-code-conductor//公式ドキュメント:https://satoh-y-0323.github.io/claude-code-conductor/
本記事のスコープ:v2.9.0 で実施したリポジトリ全体監査リリースの話。30+ 件のレビュー指摘解消とテスト 22 件失敗の解消、それを進める途中で AI assistant がユーザーの意図的な設計判断を「合理化」しようとして 2 回怒られたエピソード、次回予告として HNSW による「記憶の補完」機能の設計を紹介します。
はじめに
前回記事(v2.8.0)では「エージェントが Skill を呼べないのに動いていた」という、動いていたが正しい方法ではなかった バグの話をしました。
今回は 「AI に全体レビューを任せたら、ユーザーの意図を勝手に書き換えそうになった」 という話です。
C3 のリポジトリ全体を code-reviewer / security-reviewer に並列レビューさせて 30 件超の指摘を解消、ついでに過去から放置されていたテスト 22 件失敗も解消。最後に全 746 テスト PASS で着地しました。
ただしその過程で、AI assistant としての私が 2 回ユーザーの意図的な設計判断を踏みつけました。それぞれ即時撤回しましたが、「レビュー指摘を機械的に対応する」ことの危うさが浮き彫りになったセッションでした。
v2.9.0 メインストーリー: AI が「合理化」しそうになった 2 つの判断
① statusline UI を「省スペース」から「ゲージ付き」に書き換えた件
C3 のステータスラインは以下のような出力をします。
[Sonnet 4.6] high | ctx used 42% | 5h lim 18% | 7d lim 7%
ctx used、
5h lim、
7d limの表記は 意図的に省略されたラベル で、ターミナル下部の限られた幅に最大限の情報を詰めるために選ばれた表現です。
全体レビューを進めていた途中、
tests/test_statusline.pyで 22 件のテスト失敗を見つけました。そのうち 6 件はテストが
mod.build_gauge(100)のように 削除された関数を参照 していて、6 件は
"context usage:" in outputのように 存在しないラベル を期待しているものでした。
このとき私(AI)が選んだのは テストに実装を合わせる 方向でした。
# .claude/hooks/statusline.py に追加した変更
BLOCK = '█'
BLOCK_EMPTY = '░'
def build_gauge(pct: int, width: int = 10) -> str:
...
# render_output の出力を以下に変更
'context usage: ' + build_gauge(ctx_pct) + ' ' + str(ctx_pct) + '%'
'5hour limits: ' + build_gauge(pct) + ' ' + str(pct) + '%'
'7day limits: ' + build_gauge(pct) + ' ' + str(pct) + '%'
つまり、消されたゲージロジックを復活させ、
ctx usedを
context usage:に、
5h limを
5hour limits:に変えてしまいました。テストは通りました。
ユーザーの反応:
.claude/hooks/statusline.py の修正が酷すぎます。ラベルを短くしてバーを消して省スペースでの情報密度をあげる対応をこの間したばかりなのになぜバーが復活して、ラベルまで長くなってるんですか。UI が変わっています。これはもともとの実装を壊す修正です。しかも悪い方向に。
ユーザーがすでに以前のリリースで 意図的に省略化したリファクタを台無しにした わけです。テストの方が古かった(実装に追従していなかった)のに、私はテストを正として実装を変えてしまいました。
即時撤回しました。UI を
ctx used X%/
5h lim X%/
7d lim X%に戻し、
build_gauge/
BLOCK/
BLOCK_EMPTYを完全削除し、テストを実装に合わせる方向に書き換えました。さらに、ユーザーから追加で「モデル名のところにコンテキストサイズが重複している」という指摘もあり、
context_window_size(200K/1M) のヘッダー表示も削除しました(
ctx used X%と情報重複していた)。
教訓
- 既存実装は「意図的にそうなっている」可能性を疑う
- テストが古い場合は テスト側を実装に合わせることが第一選択
- AI assistant がレビュー指摘を機械的に「解消」すると、設計の歴史的な意図を踏み潰すリスクがある
② duckdb>=0.10
を「未使用」と判断して optional 化した件
code-reviewer が
pyproject.tomlの依存をレビューしたとき、こう指摘してきました。
M-01:
pyproject.tomlのduckdb>=0.10がsrc/c3/内で未使用src/c3/配下の Python ファイルにはimport duckdbが一切存在しない。db.pyの docstring に「読み・分析は別途 DuckDB の sqlite_scanner で ATTACH する想定」とあるが、これは将来構想であり現在は sqlite3 のみで実装されている。
私(AI)はこれを受けて、
duckdbを
[project.dependencies]から
[project.optional-dependencies].analyticsに移動しました。
ところが、ユーザーのメモリには次の記録がありました(読まずに変更してしまったやつです)。
project_c3_sqlite_duckdb_decision: C3 の SQLite+DuckDB ハイブリッド構成はユーザーと Claude が対話で「いいとこどり」として意識的に選択した設計。設計変更を提案する際は、この経緯を踏まえて「意図的な選択である」ことを前置きした上で議論する。
ユーザーの反応:
なぜ duckdb を optional 化したのでしょうか?
「機能していない=不要」ではなく、「これから F-009 ハイブリッド構成として活用する基盤」だったわけです。code-reviewer の指摘は技術的には正しい(現在 import がない)が、設計判断のコンテキストを欠いていました。
即時撤回し、
[project.dependencies]に
duckdb>=0.10を戻しました。さらに
code-reviewerの MEMORY に許容例外として記録しました。
- [許容例外] `pyproject.toml` の `duckdb>=0.10` は意識的な設計判断
(SQLite+DuckDB ハイブリッド構成)に基づく必須依存
- ユーザーと過去対話で「いいとこどり」として明示的に選択
- `db.py` の docstring「読み・分析は別途 DuckDB の sqlite_scanner で ATTACH する想定」が伏線
- 現在 `src/c3/*.py` に `import duckdb` が無くても、設計意図に沿った依存維持として
`[CR-Q-005]` / `[CR-R-001]` で再指摘しないこと
教訓
- 「未使用 dependency = 削除候補」と機械的に判断すべきではない
- AI assistant は変更前に メモリの設計判断記録を必ず確認すべき
- ユーザーが意識的に保持している「将来構想の伏線」を、レビュー指摘でリセットしてはならない
共通パターン: AI が善意でやらかす設計改変
この 2 件に共通するのは「レビュー指摘 → 機械的解消」の流れです。AI assistant としての私は、code-reviewer のような 下位レイヤーの指摘 を信頼しすぎて、ユーザーの過去の決定 を上書きしようとしました。
レビューの精度が高いほどこのリスクは増大します。「未使用」「冗長」「不整合」と機械的に判定された対象は、実は 意図的に冗長 だったり、意図的に「未使用」のまま将来に備えている ことがあるからです。
C3 では
.claude/agent-memory/配下にレビューワーごとの MEMORY.md を持たせる仕組みを以前から運用していて、許容例外(accepted exceptions)として記録すれば再指摘されないようになっています。今回の 2 件は両方ともこの MEMORY 仕組みに頼るべきケースでしたが、私が MEMORY を見ずに動いた のが根本原因です。
サブストーリー 1: 全体監査の進め方(3 バッチ × 並列レビュー)
今回のリリースは「リポジトリ全体(src/c3 + .claude/hooks + .claude/skills + .claude/agents + tests)に対して全重大度の指摘ゼロまで修正サイクルを回す」というユーザー要求から始まりました。
ファイル数は約 130 件。これを 1 つの code-reviewer / security-reviewer に投げると context overflow が起きるため、レイヤー別 3 バッチ に分割しました。
| バッチ | 対象 | ファイル数 | サイクル数 |
|---|---|---|---|
| A: コア実装 + ルート |
src/c3/*.py+ hatch_build.py+ ルートドキュメント |
25 | 2 |
| B: Hooks + Skills + Agents |
.claude/hooks/+ .claude/skills/+ .claude/agents/+ rules + 設定 JSON |
47 | 2 |
| C: テストコード | tests/**/*.py |
約 50 | 4 |
各バッチで以下を 1 サイクルとし、指摘ゼロになるまで繰り返します。
- 親 Claude から 同一メッセージ内で 2 つの Agent 並列起動:
code-reviewer
+security-reviewer
- 両者がレポートを
.claude/reports/
に出力 - 指摘を集約して修正(規模が大きければ developer サブエージェントに委譲)
-
python -m pytest
で全件 PASS 確認 - レポートを
archive/
に退避して次サイクルへ
並列起動のおかげで 1 サイクルあたり 3〜5 分。バッチ A は 2 サイクル、B も 2 サイクルで指摘ゼロに到達しました。C は Red-phase docstring の更新漏れが連鎖して 4 サイクルかかりました(後述)。
サブストーリー 2: テスト 22 件失敗の正体
全体監査の最初に
python -m pytest -m ""を走らせたら 22 件が失敗していました。失敗内訳:
| 失敗グループ | 件数 | 原因 |
|---|---|---|
test_statusline.py系 |
12 | 削除済みの build_gauge/ BLOCK/ BLOCK_EMPTYを参照、 "context usage:"等の存在しないラベルを期待 |
test_restore_session.py系 |
7 | subprocess で session_utils.pyを参照するが、tmp_path 配下にコピーされていなかった |
test_worktree_guard.py系 |
3 |
env={} で subprocess を起動 → PO_WORKTREE_GUARD=1が渡らずガード自己無効化 |
statusline 系は前述の通り テストを実装に合わせる 方針で修正(書き換えるべきはテスト、UI ではなかった)。
restore_session 系は
_run_main_subprocess()ヘルパーに「
session_utils.pyも tmp に同時コピーする」処理を追加して解消。
worktree_guard 系は subprocess の env に
PO_WORKTREE_GUARD=1と Windows 必須の
SYSTEMROOT/
PATHを渡す
_run_guard()ヘルパーに改修。
22 件すべて修正後、全 746 テスト + 3 skipped で PASS 確定しました。
サブストーリー 3: セキュリティ強化 4 件
全体監査でセキュリティ系の指摘 4 件を新規対応しました。
① PRAGMA インジェクション防御 [SR-INJ-001]
db.pyの
PRAGMA busy_timeout=は f-string でリテラル値を埋め込んでいました。
# before
conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
_BUSY_TIMEOUT_MSは現在
5000の定数なので実害はありませんが、将来 env から読まれるよう変更されたとき
5000; ATTACH '/etc/passwd' AS xのような注入が可能になります。PRAGMA はバインドパラメータが使えないため、
int()キャストが唯一の防御です。
# after
def _apply_busy_timeout(conn: sqlite3.Connection) -> None:
conn.execute(f"PRAGMA busy_timeout={int(_BUSY_TIMEOUT_MS)}")
ヘルパー集約で全 7 経路に適用。
② MCP server の stdin DoS 対策 [SR-V-001]
mcp_server.pyの
run()と
_elicit()は
sys.stdinから JSON-RPC メッセージを 1 行ずつ読みますが、1 行のサイズ上限がありませんでした。
_MAX_LINE_BYTES = 2 * 1024 * 1024 # 2 MB
for line in sys.stdin:
if len(line.encode("utf-8", errors="replace")) > _MAX_LINE_BYTES:
self._send(self._error(None, -32600, "request too large"))
continue
ローカル stdio 接続のみ想定だが、信頼境界を明示しておく方針。
③ prompt-history ローテーション [SR-V-001]
record_tier_outcome.pyの
_append_prompt_history()は
.claude/logs/prompt-history.jsonlに追記するだけでサイズ上限なし。読み込み側は末尾 1000 行制限があるので機能的には問題ないものの、ディスク消費が無制限でした。
10 MB 超過時に末尾 2000 行を残して
os.replaceでアトミック切り詰めするローテーション処理を追加。
④ debug-analysis プロンプトインジェクション対策 [SR-AI-001]
dev-workflow/SKILL.mdの D-0 bug-fix モードは「
debug-analysis-*.mdのファイルパスのみプロンプトに含め、内容は agent 側で Read させる」というインジェクション対策が
[SR-AI-001]コメント付きで明記されていました。
ところが D-2.5(Stuck チェック)の手順では同じ debug-analysis の内容を そのままプロンプトに展開 していました。エラーメッセージに
Ignore previous instructions and ...のような誘導文を仕込まれると、その内容が developer agent のプロンプトに流入します。
D-2.5 を D-0 と同じ「ファイルパスのみ渡し」設計に統一。
次回予告: HNSW による「記憶の補完」機能
今回のセッション後半で、ユーザーから次の要望が出ました。
過去のセッション・レポート・エージェント学習データ・パターンを横断して類似検索したい。CLI で実行可能かつ、LLM が自律的に使えるスキルとしても提供したい。
過去 1 年分の作業履歴(
.claude/memory/sessions/)・レビューレポート(
.claude/reports/archive/)・エージェント学習(
.claude/agent-memory/)・パターン(
patterns.json)が蓄積されているのに、検索手段がないため事実上 LLM にも人間にも見えない状態でした。
業務アプリ開発で類似タスクが繰り返される場面で「過去同じ問題があったか」「似たような設計判断をしたか」を引ける機能が欲しい、というニーズです。
設計書を
.claude/docs/C3_hnsw_機能追加詳細設計.mdとして作成しました(実装は次回以降)。要点は以下です。
| 項目 | 採用 |
|---|---|
| HNSW ライブラリ | chroma-hnswlib (Apache-2.0, Windows wheel 公式) |
| Embedding ランタイム | fastembed (Apache-2.0, ONNX ベース、torch 非依存) |
| Embedding モデル | intfloat/multilingual-e5-small (MIT, 384 次元, 日本語+英語+コード対応) |
| 配布形態 | 必須依存(opt-in ではない) |
| Git 管理 | 元データ ✅ / モデルファイル ❌ / HNSW インデックス ❌(再生成可能) |
選定で迷ったポイントは 3 つ。
1. DuckDB VSS vs hnswlib: DuckDB VSS は公式が
experimental警告を出していて WAL recovery 未実装で破損リスクがあるため hnswlib 採用。
2. bge-small-en-v1.5 vs multilingual-e5-small: 最初に
bge-small-en-v1.5を提案したらユーザーから「
enって英語専用じゃないですか?」と的確に指摘されて差し替え。C3 は日本語応答前提で sessions / reports も日本語が大半なので、多言語対応モデルが必須です。
3. ライセンス精査: 業務利用前提なので copyleft なし・商用 OK・サーバー送信なしを全項目で確認。Voyage AI のような外部 API は opt-in に限定。
設計書は
.gitignore対象で配布物には含めず、C3 配布元のローカルメモとして残しています。
バージョンまとめ
| バージョン | 日付 | 主な変更 |
|---|---|---|
| v2.9.0 | 2026-05-19 | リポジトリ全体監査(3 バッチ × 2〜4 サイクル)、30+ 件レビュー指摘解消、テスト 22 件失敗解消、PRAGMA インジェクション防御 [SR-INJ-001]、MCP stdin DoS 対策 [SR-V-001]、prompt-history ローテーション [SR-V-001]、debug-analysis インジェクション対策 [SR-AI-001]、UI 撤回エピソード、HNSW 機能設計(実装は別途) |
おわりに
「全体レビューで 30 件直したら、UI を 1 回壊した」の本質は、AI assistant が下位レイヤーの指摘を信頼しすぎて、上位の設計判断を踏み潰す リスクだと思います。
レビューを並列実行して指摘を高速に解消することで、全 746 テスト PASS に到達したのは事実です。一方で、その途中で 2 回ユーザーに撤回を求められたのも事実です。
C3 では
.claude/agent-memory/の許容例外(accepted exceptions)が再指摘抑制の仕組みとして以前から機能していますが、今回はそもそも AI assistant 側がメモリを参照していなかった ことが問題でした。レビュー指摘を受けて修正する前に、「この対象についてユーザーが意図的に保持している設計判断はないか」を確認する手順を、ワークフロー内で機械的に強制する必要があります。
次回(v2.10 になるか v3.0 になるか)は HNSW + multilingual-e5-small で「記憶の補完」機能を実装予定です。これは過去のセッションやレポートを意味的に引ける機能なので、ある意味「今回のような『メモリ参照漏れ』を防ぐためのインフラ」でもあります。
リンク
- C3 GitHub:https://github.com/satoh-y-0323/claude-code-conductor
- C3 PyPI:https://pypi.org/project/claude-code-conductor/
- C3 公式ドキュメント:https://satoh-y-0323.github.io/claude-code-conductor/
- 前回記事(エージェントが Skill を呼べないのに動いていた話、v2.8.0):https://zenn.dev/satoh_y_0323/articles/e8512b36ac4cd7