主要な技術的決定
なぜ flock ではなく mkdir ベースのロックを使用するのか?
macOS には flock が標準搭載されていません。mkdir ベースのロックは mkdir をアトミック操作(POSIX 保証)として使用し、5秒のタイムアウトと古いロックの強制破棄を備えています。トラップにより、予期しない終了時のクリーンアップが保証されます。
なぜフェイルオープンフックなのか?
プラグインは jq が存在しない環境にインストールされる可能性があり、非パイプライン作業中は state.json が存在しない場合があります。フェイルクローズは正当な操作をブロックしてしまいます。各フックは command -v jq を確認し、存在しない場合は exit 0 します。
なぜエージェントはハードコードされた sonnet ではなくユーザーが設定したモデルを継承するのか?
柔軟性とユーザーコントロールのためです。以前はすべてのエージェントがフロントマターに model: sonnet を指定しており、ユーザーの設定に関わらずすべてのパイプライン実行を Sonnet に強制していました。エージェントフロントマターから model: フィールドを削除することで、Claude Code はユーザーのアクティブなモデル(/model で選択したモデルまたは Claude Code のデフォルト)を使用します。特定のモデルを固定したいユーザーは、個別のエージェントフロントマターファイルに model: <name> を追加できます(BACKLOG #21 のエージェントごとの選択メカニズム)。コスト管理には、ユーザー自身のモデル設定が適切な手段です。
なぜオーケストレーターはコードを読まないのか?
トークン節約のためです。オーケストレーターが実装ファイルを読み取ると、各フェーズでコンテキストが増大し、推論品質が低下します。すべてのフェーズを通じて小さなアーティファクトファイル(合計約 500 行)のみを読み取ることで、オーケストレーターは高速かつ集中した状態を保ちます。
このルールは diff 出力にも拡張されます:レビューエージェント(Phase 6 の impl-reviewer、Phase 7 の comprehensive-reviewer)は、オーケストレーターが事前に計算して diff を挿入するのではなく、自身のエージェントコンテキスト内で git diff main...HEAD を自己実行します。diff はエージェントのコンテキストで消費され、オーケストレーターのコンテキストでは消費されません — トークン節約ルールが満たされます。
なぜインラインプロンプトではなく別個のエージェントファイルを使用するのか?
- 各エージェントが永続的でバージョン管理可能なシステムプロンプトを持つ
- エージェントは他のスキルから再利用できる
- モデルはフロントマターでエージェントごとに設定できる
- オーケストレーターの SKILL.md が小さいまま保たれる(インラインプロンプトありだと約 900 行になるところを約 500 行)
ガード移行パターン(シェル → Go MCP ハンドラー)
pre-tool-hook.sh に新しいガードが追加された場合(例:ルール 3a–3j)、同じ不変条件を mcp-server/tools/ の対応する Go MCP ツールハンドラー内でも強制する必要があります。
パターン:
シェルフック(
pre-tool-hook.sh): bash/edit ツール呼び出しで発火するガード。ディスクのstate.jsonを使用し、jqで読み取ります。bash コマンドを exit 2 でブロックします。このレイヤーは常にアクティブです — MCP サーバーがインストールされていない場合でも。Go ハンドラー(
tools/guards.go): MCP ツールが呼び出されたときに発火するガード。state.ReadState()からすでに読み込まれたstate.Stateを読み取ります。ブロッキングガードはIsError=trueを返し、非ブロッキング警告は"warning"JSON キーの下に含まれます。
2つのレイヤーは独立していて補完的です。MCP サーバーが使用されている場合、Go ハンドラーが最初に発火します。シェルフックはすべての Bash ツール呼び出しでも独立して発火します。
新しいガードを追加する際の移行チェックリスト:
- [ ]
pre-tool-hook.shに名前付きチェック関数を追加する(ディスパッチブロックにインラインで記述しない) - [ ]
mcp-server/tools/guards.goに対応する関数を追加する(ブロッキング:errorを返す;警告:stringを返す) - [ ]
mcp-server/tools/handlers.goの関連ハンドラーから呼び出す - [ ] シェルガード(
test-hooks.sh)と Go ガード(tools/guards_test.go)の両方にテストを追加する - [ ] ガードカタログに新しいルールを文書化する
なぜ analysis.md と investigation.md が別個のファイルなのか?
Phase 1–2 の2つの出力ファイルは異なる役割を持ち、それらの間には厳格なデータ依存関係があります:
- analysis.md(Phase 1 — situation-analyst): コードベースの現状をマッピングします — 関連ファイル、インターフェース、型、データフロー、既存のテスト。何を変更すべきかについての意見のない読み取り専用の調査です。
- investigation.md(Phase 2 — investigator): analysis.md の上に構築します — investigator エージェントは明示的な入力として analysis.md を読み取ってから、根本原因分析、エッジケース、リスク、外部依存関係、先行技術、曖昧点、削除/リネームの影響を追加します。
これらを1つのファイルにマージすると、このシーケンシャルな依存関係が壊れます:investigator は同じファイルを読み取りと書き込みの両方に使用するか、Phase 1 のコンテンツをプロンプトに挿入する必要があり(ファイルが API の原則に違反する)、ディスクに存在しなくなります。
分割が重要な追加の理由が4つあります:
- 再開のセマンティクス — 各ファイルは独立したフェーズチェックポイントです。Phase 2 が失敗した場合、Phase 1 の analysis.md はすでにディスクにあり、investigator は situation analyst を再実行せずにリトライできます。
- コンシューマーの粒度 —
task-decomposer(Phase 4)はinvestigation.mdのみを読み取り;architectとdesign-reviewerは両方を読み取ります。別個のファイルにより、各コンシューマーは正確に必要なものだけを読み込めます。 - アーティファクトガード —
pipeline_report_resultは Phase 1 完了時にanalysis.mdを、Phase 2 完了時にinvestigation.mdを独立して検証します。単一のマージされたファイルでは、1つのガードが2つの異なるセクションを検証する必要があり、ガードのロジックがコンテンツ構造に結合されます。 - 調査フロー — パイプラインが調査として実行される(実装フェーズなし)場合、Phase 2 の後で終了し、両方のファイルを最終的な成果物としてユーザーに提示します。別個のファイルにすることで、出力が2つの名前付きドキュメントとしてナビゲートしやすくなります。
2ファイルへの分割は工数レベルに関係なく維持されます。両方のファイルが同じパイプライン実行で生成されますが、それらは異なる役割を果たし、異なるダウンストリームエージェントによって消費されます。
なぜ SKILL.md のクロスリファレンスにインラインコメントアンカーを使用するのか?
SKILL.md はレンダラーではなく、生のマークダウンを読む LLM によって消費されます。HTML アンカー(<a id="...">)はレンダリングビューでは非表示ですが、生テキストでは LLM に見えてしまいます。採用された規約は、ターゲットの見出しに <!-- anchor: <token> --> を追加し、すべての散文参照でそのトークン(見出しテキストではなく)を使用します。トークンは短く、小文字で、ハイフン区切りであり、grep anchor: で検索できます。これが SKILL.md の安定ラベル規約です。新しいクロスリファレンスセクションを追加する際は、このパターンに従ってください。