Notes
MCPツールは1機能1PRで積み上げると安定して進む
MCPサーバーにCRUD系のツールを追加するとき、一度に全部作ろうとせず1ツール1PRで積み上げるアプローチが効果的だった。1日で get / create / update / comment の4ツール+関連情報取得を安定してリリースできた。
構造:
mcp-server/
tools/
get-resource.ts # PR #1
create-resource.ts # PR #2 (get-resourceの存在を前提に)
update-resource.ts # PR #3
create-comment.ts # PR #4
get-workspace.ts # PR #5
なぜこうしたか:
- 依存の方向が自然に決まる — getを先に作り、create/updateはgetの動作を前提にできる。テストもgetの結果を使って検証できる
- CIが毎回グリーンになる — 差分が小さいのでレビューもCI修正も局所的で済む
- ロールバックが容易 — 問題が出ても1ツール単位で切り戻せる
- 途中で方針変更しやすい — 3つ目を作る頃にはパターンが固まり、最初の2つをリファクタする判断もしやすい
代替案として「全ツールを1つの大きなPRで出す」方法もあるが、コンフリクトやCI修正が複雑になりがちで、途中で手が止まるリスクが高い。
GitHub ActionsでPR本文に必須セクションを強制する
PR本文にADR(Architecture Decision Records)セクションを含めるルールを作ったが、AIエージェントがルールを守らないことがあった。CLAUDE.mdに書いてもスキップされるケースがあるため、CIで機械的にブロックする方が確実。
Before(手動):
レビュー時に「ADRセクションがない」と指摘して差し戻し。見落としも発生する。
After(自動):
# .github/workflows/pr-body-check.yml
name: PR Body Check
on:
pull_request:
types: [opened, edited]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check required sections
uses: actions/github-script@v7
with:
script: |
const body = context.payload.pull_request.body || '';
const required = ['## Related ADRs'];
const missing = required.filter(s => !body.includes(s));
if (missing.length > 0) {
core.setFailed(`PR本文に必須セクションがありません: ${missing.join(', ')}`);
}
ポイント:
types: [opened, edited]で作成時と編集時の両方をチェックする- AIエージェントに「ルールを守れ」と書くより、CIで落とすほうが確実に機能する
- 必須セクションが増えたら
required配列に追加するだけで拡張できる
macOS標準のbash 3は連想配列が使えない
macOSに標準搭載されている bash は 3.x 系で、declare -A (連想配列)が使えない。bash 4以降の機能。
# bash 4+: 動く
declare -A counts
counts["key"]="value"
# bash 3 (macOS default): syntax error
Homebrew で bash 5 を入れていれば使えるが、CI環境やチームメンバーの環境では入っていない前提で書くほうが安全。
回避策:
- 連想配列の代わりに
grep+ 一時ファイルやawkでキーバリューを管理する /bin/bashではなく/usr/bin/env bashを使い、PATH上の新しい bash を使う手もあるが、全環境に bash 4+ がある保証はない- 複雑なデータ構造が必要ならシェルスクリプトをやめて Python や jq に切り替える
ポイント:
#!/bin/bashで書くスクリプトはmacOSのbash 3互換を意識するdeclare -A以外にも、${var,,}(小文字変換)、|&(パイプのstderrリダイレクト)なども bash 4+ 限定
画面統合は「機能の棚卸し→移植→廃止」の順で進める
同じドメインの表示バリエーションが複数画面に分散しているとき、一気にリライトするのではなく段階的に統合する。
手順:
- 棚卸し — 各画面の固有機能を洗い出す。一覧・フィルタ・並べ替え・一括操作・削除など、画面ごとに何ができるかをテーブルにまとめる
- ベース画面の選定 — 最も機能が多い画面、または目標UIに最も近い画面をベースにする
- 機能の移植 — 他の画面にしかない機能をベース画面に移植する。このとき新しい抽象化は作らず、まず動く状態にする
- 参照の付け替え — ルーティング・ナビゲーション・テストの参照先をベース画面に切り替える
- 旧画面の削除 — 参照がなくなった画面ファイルとそのテストを削除。不要になった型定義やユーティリティも一緒に消す
実際に5画面→1画面の統合をやったとき、棚卸しで「バックログ画面のD&D並べ替え」が他の画面にないことが分かり、これをベース画面に移植する必要があった。棚卸しを飛ばすと、統合後に「あの機能どこ行った?」が起きる。
削除フェーズでは grep で旧画面名・旧ルートヘルパーへの参照が残っていないか確認する。テストファイルのimportに残りがちなので注意。
週次GCレポートの「次サイクルアクション」をwork item化する
コード品質の週次監査(harness GC)で見つかった逸脱を、レポートに書いて終わりにせず、追跡可能なwork itemまで落とし込むパイプラインを回した。
Before(手動):
週次レポートを書いて「次やること」リストを作るが、翌週には忘れて同じ逸脱が再発する。レポートが読まれるだけのドキュメントになりがち。
After(自動):
- 週次GCレポートを自動生成する(KPI集計、逸脱の列挙、次サイクルアクションの記載)
- レポートのPRをマージしたら、次サイクルアクションをwork itemとしてBacklogに登録する
- 通常のスプリント計画で優先順位づけされ、実装に回る
## レポートの「次サイクルアクション」の例
1. PRテンプレートに Verification セクションを必須化する
2. session-complete で verify 省略理由を自動追記する
3. Review Finding Rate の N/A 表示ルールを明文化する
↓ それぞれ独立したwork itemとしてBacklogに登録
ポイント:
- レポートとwork itemの変換を明示的にやることで「書いて終わり」を防ぐ
- 1つのアクションは1つのwork item。粒度を揃えると優先順位づけしやすい
- work itemの「関連情報」にレポートPRへのリンクを残しておくと、なぜその作業が必要かの文脈が辿れる
osascriptに変数を渡すときはon run argvを使う
macOSの osascript でシェル変数を文字列展開すると、ダブルクォートを含む値でAppleScriptの構文が壊れる。
# NG: 変数にダブルクォートが含まれると構文エラー
TITLE='He said "hello"'
osascript -e "display notification \"$TITLE\""
on run argv を使えば引数として安全に渡せる。
# OK: 引数として渡すので展開の問題がない
osascript - "$TITLE" "$MESSAGE" <<'EOF'
on run argv
display notification (item 2 of argv) with title (item 1 of argv)
end run
EOF
ポイント:
- ヒアドキュメントは
<<'EOF'(シングルクォート)にして、シェルの変数展開を防ぐ on run argvでAppleScript側の引数として受け取る- 通知以外(
do shell script等)でも同じパターンが使える
放置worktreeの棚卸し — work-item状態を先に戻してから削除する
AIエージェントがworktreeを作成してタスクに着手すると、対応するwork-itemが in-progress に遷移する。PRを作らず中断したworktreeが溜まると、work-itemが実態と合わない状態で残り続ける。
仕組み:
- worktreeの一覧を取得し、各ブランチの状態(PRの有無、未マージコミット、mainとの差分)を確認する
- PRなし+mainと同一 → 安全に削除可能
- PRなし+未マージコミットあり → 内容を確認して判断(大抵は中途半端な実装で再実装可能)
in-progress/in-reviewのwork-itemをreadyに戻す- その後worktreeとブランチを削除する
棚卸しの手順:
- 全worktreeを列挙し、各ブランチのPR有無・未マージコミット・mainとの差分を確認する
- タスク管理の状態を実態に合わせて復元する(in-progress → ready 等)
- その後にworktreeとブランチを削除する
順序が重要で、2→3の順でないとタスク管理側に不整合が残る。
やってみてどうだったか:
- 9つのworktreeが溜まっており、5件のwork-itemの状態修正が必要だった。エージェントが自律的にworktreeを作るプロジェクトでは、定期的な棚卸しが要る
- 「worktree削除」と「work-item状態の復元」を別々にやると不整合が残る。状態を戻すのが先、削除は後の順序が重要
- 次はworktree作成時にタイムアウトや自動クリーンアップの仕組みを入れたい
GitHub Actionsのpermissionsは未指定だとreadがデフォルト
GitHub Actionsでpermissionsを明示的に書くと、書かなかったスコープはすべてnoneになる。一方、permissionsキー自体を省略すると、リポジトリ設定に依存する(多くの場合read)。
# これだと issues: write 以外は全部 none になる
permissions:
issues: write
# issueコメントだけしたい場合でも contents: read が必要なケースがある
permissions:
contents: read
issues: write
id-tokenだけはデフォルトがnone(他のスコープと違う)。OIDC認証が必要な場合は明示的にid-token: writeを追加するghコマンドはGitHub-hosted runnerにプリインストールされているので、別途インストール不要。GITHUB_TOKENも自動で設定される- CIでAIエージェントにgitやghコマンドを使わせる場合、permissionsの設定が実質的なサンドボックスになる。必要最小限のスコープだけ与えることで、エージェントの行動範囲を制限できる
refs: https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
ドキュメントの鮮度を自動チェックして陳腐化を防ぐ
プロジェクトのドキュメント(ADR、README、設計ドキュメント等)がコードの変更に追従できず、古い情報のまま放置される問題があった。手動で定期的に見直すのは続かない。
Before(手動):
気づいたときにドキュメントを見直す → 気づかないまま数ヶ月放置されるドキュメントが発生。新メンバーが古い設計ドキュメントを信じて実装してしまうリスク。
After(自動):
ドキュメントファイルの最終更新日と関連コードの変更日を突き合わせ、乖離が大きいものをリストアップする「doc freshness guard」を導入。
# 例: 90日以上更新されていないドキュメントを検出
find docs/ -name "*.md" -mtime +90 -print
# 関連コードが更新されているのにドキュメントが古いケースを検出
# ドキュメント内で参照しているファイルパスやシンボル名をgrepし、
# そのファイルの最終コミット日とドキュメントの最終コミット日を比較
ポイント:
- 「最終更新が古い」だけでなく「関連コードが変わったのにドキュメントが更新されていない」を検出するのが重要。安定しているドキュメントまでノイズにならないように
- スケジュールタスクやCI上で定期実行し、結果をissueやSlackに通知すると放置されにくい
- ドキュメントのフロントマターに
related_paths: [src/auth/]のようなメタデータを持たせると、関連コードとの紐付けが正確になる
自律開発エージェントにPR数ゲートを設けてスループットを制御する
スケジュールタスクで自律的にissueを拾って実装・PR作成するエージェントを動かしていたが、レビューが追いつかないまま次々とPRが作られて並行ブランチが増えすぎる問題があった。
仕組み:
- エージェント起動時にオープンPR数をチェックし、閾値(例: 3件)以上なら何もせず終了する
- レビュー・マージが進んでPR数が減ると、次回のスケジュール実行で新しいissueを拾い始める
# scheduled-task prompt
事前にGitHubのPull Requestを確認し3つ以上が存在している場合は、
以降の処理を行わず。ここで処理をやめる
(並行作業が発生しすぎないように)
エージェント側の実装:
open_prs=$(gh pr list --state open --json number --jq 'length')
if [ "$open_prs" -ge 3 ]; then
echo "Open PRs: $open_prs (threshold: 3). Skipping."
exit 0
fi
やってみてどうだったか:
- レビュー待ちPRが溜まらなくなり、1つずつ確実にマージできるペースになった
- 閾値は「1人でレビューできるPR数」に合わせるのがよさそう。個人プロジェクトなら2〜3件
- PR数だけでなく「最後のPRが何時間前に作られたか」も見ると、バースト的な生成をさらに抑制できそう
Cloudflare D1のテストでは readD1Migrations + applyD1Migrations でマイグレーションを適用する
@cloudflare/vitest-pool-workers でD1を使うテストを書くとき、テストヘルパーにCREATE TABLE文をハードコードしていた。マイグレーションファイルを追加するたびにテストヘルパーも手動で同期する必要があり、乖離してテストが壊れることがあった。
@cloudflare/vitest-pool-workers が提供する readD1Migrations と applyD1Migrations を使えば、本番と同じマイグレーションファイルをテストDBにも適用できる。
// vitest.config.ts(Node.jsコンテキスト)
import {
defineWorkersConfig,
readD1Migrations,
} from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig(async () => {
const migrationsPath = path.join(__dirname, "migrations");
const migrations = await readD1Migrations(migrationsPath);
return {
test: {
poolOptions: {
workers: {
miniflare: {
d1Databases: { DB: { migrationsPath } },
bindings: { TEST_MIGRATIONS: migrations },
},
},
},
},
};
});
// test/helpers.ts(Workerコンテキスト)
import { env } from "cloudflare:test";
import { applyD1Migrations } from "cloudflare:test";
export async function resetDatabase() {
await applyD1Migrations(env.DB, env.TEST_MIGRATIONS);
// DELETE文でデータだけクリア(テーブル作成は不要)
await env.DB.exec("DELETE FROM orders; DELETE FROM users;");
}
ハマりどころ:
readD1MigrationsはNode.jsコンテキスト(vitest.config.ts)で呼ぶ。Worker内ではfsが使えないapplyD1Migrationsはcloudflare:testからimportする。Workerコンテキスト専用wrangler.tomlのmigrations_dirを設定するだけでは自動適用されない。明示的にAPIを呼ぶ必要がある
中間データにメタデータを含めておくと後工程の外部ツール呼び出しが減る
RAW現像パイプラインで、recipe.json(現像パラメータ)→ XMP変換 → darktable-cli実行、という流れを組んでいた。XMP生成時にクロップ値を決めるためにEXIF情報が必要だったが、recipe.jsonにEXIF情報がなく、毎回exiftoolを実行し直していた。
recipe.json生成時にEXIF情報を一緒に埋め込むようにしたところ:
- XMP生成が自己完結する — recipe.jsonだけ読めばXMPを作れる。
exiftoolの再実行が不要 - バッチ処理が速くなる — 75枚の一括現像で、1枚ごとの
exiftool呼び出しがなくなった - デバッグしやすい — recipe.jsonを見るだけで「このRAWのセンサーサイズはいくつで、WBは何Kだったか」がわかる
{
"file": "IMG_0001.ARW",
"recipe": { "exposure": 0.5, "temperature": 6300 },
"exif": {
"image_width": 6048,
"image_height": 4024,
"white_balance": "Auto",
"iso": 800
}
}
パイプラインの中間データは「次の工程が必要とする情報」を全部持たせておくと、工程間の結合度が下がる。後から「あのメタデータも必要だった」と気づいて追加するより、最初から入れておく方が手戻りが少ない。
