Notes
JWT認証ミドルウェアを関心事で分割する
JWT認証を1ファイルで実装すると、JWKS取得・署名検証・ルーティングが混在して見通しが悪くなる。関心事ごとに3つのモジュールに分離すると、テスタビリティと再利用性が上がる。
分離の構成:
- JWKS取得・キャッシュ — 公開鍵の取得とTTLベースのキャッシュ(例: 5分)。ネットワーク障害時のフォールバックもここで吸収する
- JWT署名検証 — RS256署名の検証、base64urlデコード、audience・有効期限のチェック。純粋な関数として実装できる
- 認証エンドポイント定義 — ログイン・ログアウト・認証情報取得のパスを共有モジュールにまとめる
auth/
jwks.ts # JWKS取得 + TTLキャッシュ
jwt-verify.ts # RS256検証 + claims検証
endpoints.ts # 認証関連パスの定数
middleware.ts # ↑を組み合わせたミドルウェア本体
ポイント:
- JWKS取得を分離すると、テスト時に固定の公開鍵を注入できる。テストが外部依存なしで動く
- JWT検証を純粋関数にすると、改ざん検知やexpiry検証のユニットテストが書きやすい
- エンドポイントパスを共有モジュールにすると、フロントエンドからも参照できて不整合が起きにくい
AIコードレビューの知見をファイルに自動蓄積する
AIエージェントにコードレビューさせる際、レビュー結果を使い捨てにせず、アーキテクチャやドメインの知見をファイルに永続化する仕組みを追加した。
レビューテンプレートの末尾に「知見永続化フェーズ」を設ける:
## Knowledge Persistence
レビュー中に発見した以下の知見をファイルに追記してください:
- **アーキテクチャ知見** → `skills/clean-architecture-reviewer/references/knowledge.md`
例: 「このプロジェクトではrepository層でneverthrowを使う」
- **ドメイン知見** → `skills/product-owner-reviewer/references/knowledge.md`
例: 「storyのpriorityは0が最高優先度」
次回のレビューではこの知見ファイルが自動的にコンテキストとして読み込まれるので、同じ指摘を繰り返さず、プロジェクト固有の判断基準が蓄積されていく。
実質的に「レビュアーが学習する」仕組みをファイルベースで実現している。RAGのようなベクトルDBは不要で、Markdownファイルの追記だけで機能する。
GitHub issueコメントからAIコード生成を安全にトリガーする
GitHub Actionsの issue_comment イベントを使って、issueやPRのコメントからAIコーディングエージェントを起動するワークフローを構築した。/codex のようなプレフィックスをトリガーにし、全コメントで発火しないようにする。
安全に運用するためのポイント:
- コマンドインジェクション対策 — コメント本文を
${{ github.event.comment.body }}で直接シェルに展開すると任意コマンド実行の脆弱性になる。環境変数経由で渡す:
env:
COMMENT_BODY: ${{ github.event.comment.body }}
steps:
- run: |
# $COMMENT_BODY はシェル変数として安全に展開される
INSTRUCTION="${COMMENT_BODY#/codex }"
- 同時実行の制御 —
concurrencyグループをissue/PR番号単位で設定し、同じissueへの連続コメントで重複実行を防ぐ:
concurrency:
group: codex-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: true
実行状況の可視化 — 開始時にリアクション(👀)を付け、完了時に結果をコメントとして投稿する。失敗時もエラー内容を投稿して、ワークフローログを見に行かなくても状況が分かるようにする
入力バリデーション — 指示文の最大長を制限し、あまりに短い指示には確認を返す
neverthrowでリポジトリ層のエラーをResult型に統一する
TypeScriptプロジェクトでリポジトリ層(DB操作)のエラーハンドリングをtry-catchからneverthrowのResult型に切り替えた。
Before:
// try-catchだと呼び出し側でエラー型が不明
async function createProject(data: NewProject): Promise<Project> {
try {
const result = await db.insert(projects).values(data).returning();
return result[0];
} catch (e) {
throw new Error("Failed to create project");
}
}
After:
import { ok, err, ResultAsync } from "neverthrow";
async function createProject(
data: NewProject
): ResultAsync<Project, RepositoryError> {
return ResultAsync.fromPromise(
db.insert(projects).values(data).returning(),
(e) => new RepositoryError("CREATE_FAILED", e)
).map((rows) => rows[0]);
}
usecase層では .andThen() でチェインし、presentation層で .match() でHTTPレスポンスに変換する。エラーが型で追跡できるので、「このusecaseがどんなエラーを返しうるか」がシグネチャだけで分かる。
テストではリポジトリのモックを集約ファイルに切り出し、ok()/err() で返り値を差し替えると、成功・失敗パスの両方を簡潔にテストできる。
AIエージェントの実装→レビュー→PRをシェルスクリプトで自動化する
Claude CLIのようなAIコーディングエージェントに、issue単位の開発ライフサイクル全体をシェルスクリプトで自動化させる仕組みを作った。6フェーズ構成:
- worktree作成 —
git worktree addでmainから隔離ブランチを切る。エージェントの作業がメインの作業ディレクトリに影響しない - 実装 —
claude -p(CLIの非対話モード)にプロンプトテンプレートとストーリーファイルを渡して実装させる - 3観点レビュー — 実装完了後、同じくCLI経由でレビュープロンプトを実行。受け入れ条件チェック・アーキテクチャレビュー・PO視点レビューの3つを1回で回し、結果に
<signal>REVIEW_PASSED</signal>orREVIEW_FAILEDを出力させてシェルスクリプトで判定する - PR作成 —
gh pr createでPRを自動作成 - マージ待ち — 30秒間隔で
gh pr view --json stateをポーリングし、人間のマージを待つ - クリーンアップ —
git worktree removeでworktreeを削除し、ストーリーファイルをdoneディレクトリに移動
レビューのゲート設計がポイント。レビュー結果をstructured outputではなく、本文末尾の <signal> タグで表現すると、シェルスクリプトの grep だけで合否判定できる:
REVIEW_OUTPUT=$(claude -p "$REVIEW_PROMPT" --cwd "$WORKTREE_DIR")
if echo "$REVIEW_OUTPUT" | grep -q "REVIEW_FAILED"; then
echo "Review failed. Aborting PR creation."
exit 1
fi
進捗ログはストーリーファイル自体の ## Activities セクションに追記する方式にした。別ファイルに書くと散逸するが、ストーリーファイルに書けば作業経緯がissue定義と一体化して残る。
VSCode Workspaceでマルチリポジトリ構成のAIアシスタントが認識するコンテキスト境界
複数リポジトリを1つのVSCode Workspaceで開いている場合、AI拡張機能が認識するコンテキスト(cwd)はアクティブファイルの属するワークスペースフォルダに依存する。.code-workspaceファイルでフォルダ順を制御できるが、AIのコンテキスト選択に直接影響しないことがある。
対策パターン:
- リポジトリごとにウィンドウを分ける — 最も確実だが切り替えコストがある
- ワークスペース設定でデフォルトcwdを指定する — 拡張機能がサポートしていれば有効
- プロジェクトルートにコンテキストファイルを配置する —
CLAUDE.mdやAGENTS.mdを各リポジトリルートに置き、AIが自動で読み込むようにする
パターン3はAI側の規約に沿ったファイルを置くだけで済むため低コストで効果が高い。
AIエージェントループをCLIツールチェインに統合する段階的パターン
自律的にタスクを消化するAIエージェントループ(PRDを入力にコードを生成し続けるツール等)を既存の開発ツールチェインに組み込む際、統合度と実装コストのバランスで4段階に分けて考えると導入判断がしやすい。
- マーケットプレイス/プラグインをそのまま利用 — 最小コスト。まず動作確認に使う
- セットアップスクリプトとして同梱 — プロジェクトテンプレートに組み込み、毎回同じ構成で再現可能にする
- MCPツールとしてラップ — 自然言語で「初期化して」「5イテレーション回して」と呼べる。統合度は最高だが実装コストも高い
- Skillファイルで手順を誘導 — 低コストで手順をドキュメント化。実行自体はBashに委ねる
ポイントは「エージェントループ自体がCLIツールを実行エンジンとして使う」構造を理解すること。統合の本質は、エージェントの実行コマンドを開発者体験にどう自然に載せるかにある。段階2+3の組み合わせ(セットアップ自動化+MCP統合)が、導入と運用の両方を揃える筋の良い選択肢になることが多い。
refs: https://github.com/snarktank/ralph
Android ビルド失敗: PNG ファイル形式の不一致
問題
Expo で Android preview ビルド(eas build)が以下のエラーで失敗:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:mergeReleaseResources'.
> Android resource compilation failed
ERROR: .../drawable-mdpi/assets_images_icon.png: AAPT: error: file failed to compile.
原因
ファイル拡張子と実際のフォーマットの不一致
該当ファイルは .png 拡張子だが、実際は JPEG 形式だった:
$ file assets/images/icon.png
assets/images/icon.png: JPEG image data, JFIF standard 1.01, ...
Android の aapt2(Android Asset Packaging Tool)は、拡張子と実際のフォーマットが一致しない画像をコンパイルできない。
影響を受けたファイル
assets/images/icon.pngassets/images/onboarding_1.pngassets/images/onboarding_2.pngassets/images/onboarding_3.pngassets/images/onboarding_4.png
これらのファイルはAntigravityを使ってnano bananaで生成したものだった
解決方法
macOS の sips コマンドで正しい PNG 形式に変換:
for file in assets/images/icon.png assets/images/onboarding_{1,2,3,4}.png; do
sips -s format png "$file" --out "$file"
done
変換後の確認
$ file assets/images/icon.png
assets/images/icon.png: PNG image data, 1024 x 1024, 8-bit/color RGB, non-interlaced
予防策
画像ファイルを追加する際は
fileコマンドで実際の形式を確認する画像編集ソフトで「名前を付けて保存」する際、拡張子だけでなく形式も正しく選択する
CI/CD で画像形式チェックを追加することも検討
参考
AAPT2 は Android Studio / Gradle で使用されるリソースコンパイラ
iOS は拡張子と形式の不一致を許容するが、Android は厳格
