Notes
TILにノートタイプを導入してテンプレートを分ける
TILを全て同じフォーマットで書くと、内容の性質に合わない構成になりがち。「バグ修正の話」と「ツール紹介」では読み手が期待する情報構造が違う。
ノートタイプ(insight, tried, automation, harness, architecture, bugfix)を定義し、タイプごとにテンプレートとスタイルガイドを分けた。
templates/
insight.md # 知見: コード例必須、Before/After推奨
tried.md # 試した: 良かった点・気になった点・向いている場面
automation.md # 自動化: Before(手動)→After(自動)の対比
harness.md # AI制御: 設計判断+プロンプト例
architecture.md # 設計: 構造図+トレードオフ
bugfix.md # バグ: 症状→原因→解決
- AIエージェントが抽出時にタイプを自動判定し、対応するテンプレートで生成する
- タイプごとのスタイルガイド(
writing-style.md)で「insightはコード例なしだと却下」のようなルールを管理する - フロントマターに
type: insightを記録して、後から集計や検索に使える
タイプを増やしすぎると分類に迷うので、6種類くらいが運用しやすい。新タイプの追加はフィードバックから「既存タイプに当てはまらないTILが繰り返し出る」場合のみにする。
AIエージェントが作るPRはdraftをデフォルトにする
AIエージェントにPR作成を任せるスキルで、gh pr create にデフォルトで --draft を付けるようにした。
エージェントが作ったPRがready状態で出てしまうと、レビュワーに通知が飛んで「まだ見なくていいのに」となる。人間が確認してからreadyにする方が安全。
# Before: readyで作成される
gh pr create --title "feat: add feature" --body "..."
# After: draftで作成される
gh pr create --draft --title "feat: add feature" --body "..."
ポイント:
- エージェントの出力は「人間のレビュー待ち」をデフォルトにしておくと安全
- readyにするのは人間の判断。エージェント側で勝手にreadyにしない
- スキルのテンプレートレベルで
--draftを埋め込んでおけば、毎回指示する必要がない
Issue起点のAIエージェント自動実装パイプラインを組む
Claude CodeやCodexにはworktree機能が組み込まれているが、「issueを読む→worktreeを作る→実装する→テストする→PRを出す」までを一気通貫で自動化するには、エージェントの上にオーケストレーション層が要る。
仕組み:
- issueのステータス(Ready/In Progress/In Review)を状態マシンとして管理する
- 各issueに対して独立したworktreeを作成し、そのworktree内でAIエージェントのセッションを起動する
- エージェントにissueのPlanとDoDを読ませ、実装→verify→自動修正のループを回す
- verifyが通ったらPRを作成し、issueをIn Reviewに遷移する
# 1 issue = 1 worktree = 1 PR を厳守
# issueごとにロックファイルで重複実行を防止
story-dev --story 41 # issue #41のworktreeで実装開始
story-dev --story 79 # 別ターミナルでissue #79を並行実装
やってみてどうだったか:
- エージェント組み込みのworktreeは「ブランチを切って作業する」単位。パイプライン全体の管理(issue選定、ステータス遷移、verify条件の判定)は自前で組む必要がある
- issueにPlan/DoD/verifyコマンドを書いておくと、エージェントが「何をもって完了か」を自律判断できる
- 並行実行時はgit common dir配下にissue単位のロックファイルを置いて衝突を防ぐ
- 人間のレビューがボトルネックになるので、2〜3本の並行が実用的な上限
ローカル認証バイパスでAIエージェントの動作確認を可能にする
クラウド認証(Cloudflare Access等)に依存するアプリでは、AIコーディングエージェントがローカル開発サーバーで認証画面をテストできない。認証トンネルの設定が必要で、エージェントには操作できない。
仕組み:
- 開発サーバー起動時にフラグ(環境変数やクエリパラメータ)を渡す
- 開発モードの場合のみ、固定のテストユーザーで認証をバイパスするミドルウェアを挿入する
- 本番コードには一切影響しない条件分岐にする
// 開発専用の認証バイパスミドルウェア
function devAuthMiddleware(req, res, next) {
if (process.env.NODE_ENV !== "development") return next();
// テスト用ユーザーをセッションに注入
req.user = { id: "dev-user", email: "dev@localhost" };
next();
}
やってみてどうだったか:
- エージェントが認証付きページのスクリーンショットテストを全ページ実行できるようになった
- テスト用アカウントのデータが存在しない場合もあるので、シードデータも合わせて用意する必要がある
- 本番環境への誤デプロイ防止として、
NODE_ENVチェックだけでなくビルド時にデッドコード除去される仕組みが望ましい
大規模スキーマ移行を6フェーズに分けてADRで管理する
複数モジュールが共有しているリソースの所有モデルを変更するとき、一気にやるとリスクが高い。ADR(Architecture Decision Record)で設計根拠を残しつつ、6フェーズに分けて段階的に移行するパターンがうまくいった。
- スキーマ追加 — 新しいカラムを追加するだけ。既存コードに影響なし
- 新規書き込みの切り替え — 新規レコードから新カラムに書き込む
- 既存データのバックフィル — 既存レコードに遡及的に値を付与
- 読み取りの切り替え — 新カラムベースのクエリに段階的に移行
- 旧カラムの参照停止 — 旧キーへの依存を完全に除去
- クリーンアップ — 旧カラム削除、不要コードの除去
各フェーズが独立してデプロイ・ロールバックできるのがポイント。途中で問題が起きても前のフェーズに戻せる。
ADRとissue管理を連携させると効果的:
- ADRからissueへの参照を張る — 「なぜこの順番か」「完了条件は何か」をトレースできる
- フェーズごとにissueを切る — 進捗が可視化され、レビュー単位も小さくなる
- 設計の根拠がADRに残る — 半年後に「なぜこうしたか」を追える
「書き込みを先に切り替え → バックフィル → 読み取りを切り替え」の順序が重要。逆にすると、バックフィル前の古いデータが新しいクエリで漏れる。
lefthookでシークレット漏洩と破壊的操作をpre-commitで防ぐ
Git hooksツールの lefthook を使って、コミット前にシークレット漏洩チェックと破壊的操作の検出を自動化した。
Before: シークレットの混入や rm -rf、DROP TABLE のような危険なコマンドがそのままコミットされうる状態。
After: lefthook の pre-commit フックで自動検出し、コミットをブロックする。
# lefthook.yml
pre-commit:
commands:
config-guard:
run: bash scripts/config-guard.sh
glob: "*.{env,yml,yaml,toml,json,sh}"
config-guard スクリプトでは以下をチェックする:
- シークレット検出 — ステージされたファイル内の
token=,secret=,password=などのパターンをgrepで検知 - 破壊的操作の検出 —
rm -rf /,DROP TABLE,git push --forceなど危険なコマンドパターン - 設定ファイルの妥当性 — 環境変数ファイルに本番値が混入していないか
lefthook を選んだ理由は、YAML一枚で設定が完結し、glob でファイルタイプごとにフックを分けられること。husky + lint-staged でも似たことはできるが、lefthook の方がポリグロット(多言語)プロジェクトでの設定がシンプル。
テストも用意する。fixture ファイル(意図的に違反を含むファイル)を置いて、スクリプトが正しく検出するかを検証する。CIでもこのテストを回すことで、ガード自体の品質を担保できる。
grepベースのカスタムlintでAIエージェントのアンチパターンを検出する
汎用lint(ESLint, oxlint等)だけではカバーしきれないプロジェクト固有のルールを、シェルスクリプト+grepで実装してCIに組み込む方法。AIエージェントが繰り返しやすいアンチパターン(レイヤー境界違反、秘密値のログ出力など)を自動検出できる。
やり方はシンプルで、grep -rn でパターンマッチし、違反があれば非ゼロで終了するスクリプトを書くだけ。
#!/bin/bash
# 例: domain層から外側レイヤーへのimportを禁止
violations=$(grep -rn "from ['\"].*/(application|presentation|infrastructure)/" src/domain/)
if [ -n "$violations" ]; then
echo "ERROR: domain層から外側レイヤーへのimport違反"
echo "$violations"
exit 1
fi
このアプローチの良いところ:
- 導入コストが低い — ESLintプラグインを書くより圧倒的に速い。grepとシェルスクリプトだけで完結する
- fixtureテストが簡単 — 違反するコードと正常なコードのサンプルファイルを置いて、スクリプトの検出結果を確認するだけ
- AIエージェントとの相性が良い — エラーメッセージにルールIDと修正指示を含めておくと、エージェントが自動修正しやすい
運用上気をつけるポイント:
- 誤検知率を計測する — 1スプリント(2週間)で誤検知率20%を超えたらパターンを見直す
- ルール対応表をドキュメント化する — ルールID・カテゴリ・実装状態を一覧にして、チームで共有する
- 既存CIに相乗りさせる —
package.jsonのlintスクリプトにカスタムlintを追加すれば、特別なCI設定は不要
招待承認のような複合操作はD1 batchでアトミックにする
プロジェクトへの招待→承認→メンバー追加のフローは、invitation更新とmember挿入の2クエリが必要になる。片方だけ成功すると「承認済みだがメンバーでない」という不整合が起きる。
Cloudflare D1の db.batch() を使えば、複数のprepared statementをトランザクション相当でまとめて実行できる。
const results = await db.batch([
db.prepare(
"UPDATE invitations SET status = 'accepted', accepted_by = ?1, accepted_at = ?2 WHERE id = ?3"
).bind(userId, now, invitationId),
db.prepare(
"INSERT INTO members (project_id, user_id, role) VALUES (?1, ?2, ?3)"
).bind(projectId, userId, role),
]);
ポイント:
- D1にはBEGIN/COMMITはないが、
batch()が同等の保証を提供する - 招待の重複チェック(既にpendingがあるか)はbatchの前にSELECTで行う
- 招待→承認→メンバー追加のような状態遷移を伴う複合操作は、batch対象の筆頭候補
issue作成スキルに種別自動判定を入れて適切なテンプレートを選ぶ
AIエージェントのissue作成スキルで、ユーザーの依頼内容からissueの種別(story / refactor / chore)を自動判定し、種別ごとに異なるヒアリングフローとテンプレートを適用するようにした。
仕組み:
- キーワードベースの自動判定 +
--typeフラグによる明示指定の2段構え - storyなら「ユーザーストーリー形式(As a … I want … So that …)」でヒアリング
- refactorなら「現状 → 目標 → 制約」の構造でヒアリング
- choreなら「何を・なぜ・影響範囲」のシンプルな構造
# 判定の優先順位
1. --type フラグが指定されていればそれを使う
2. キーワードマッチ:
- "リファクタ" / "refactor" / "整理" → refactor
- "依存更新" / "upgrade" / "CI" / "設定" → chore
- それ以外 → story(デフォルト)
やってみてどうだったか:
- refactorやchoreにユーザーストーリー形式を強制していたときより、自然なissueが作れるようになった
- キーワード判定はそこまで精度が要らない。間違えてもエージェントがヒアリング中に気づいて修正できるので、ざっくりでよい
エージェントのタスク実行をPlan→Approve→Execute→Verifyに分離する
AIエージェントにissueの実装を任せるとき、計画と実行を一気にやらせると意図と違う方向に進みがち。Plan→Approve→Execute→Verifyの4フェーズに分離した。
仕組み:
- Plan — issueを読み、実装計画(# Plan)と完了定義(# DoD)をissueに書き出す。コードは書かない
- Approve — 人間が計画をレビューして承認する。修正指示があればPlanに戻る
- Execute — 承認済みの計画に従ってコードを書く。実行開始時にPlanとDoDの存在をゲートでチェック
- Verify — DoDに基づいてセルフチェックし、PRを作成する
# 実行ゲートの擬似コード
plan=$(gh issue view $ISSUE --json body | jq -r '.body' | grep '# Plan')
dod=$(gh issue view $ISSUE --json body | jq -r '.body' | grep '# DoD')
if [ -z "$plan" ] || [ -z "$dod" ]; then
echo "Error: Plan and DoD must exist before execution"
exit 1
fi
やってみてどうだったか:
- 計画段階で「この方針でいいか」を確認できるので、大きな手戻りが減った
- issueに計画が残るので、後から「なぜこの実装にしたか」を追えるようになった
- ゲートはシンプルなテキスト存在チェックで十分。厳密なフォーマット検証は運用が重くなる
refs: https://nyosegawa.github.io/posts/harness-engineering-best-practices-2026
git hookスクリプトでstdinがブロックする問題をTTY判定で回避する
lefthookからシェルスクリプトを呼び出したとき、cat や read でstdinを読もうとするとスクリプトがハングする問題があった。Codex(OpenAI)に修正を任せたらサクッと直してくれた。
原因:
lefthookはスクリプトのstdinをパイプで接続する。ターミナルから直接実行した場合はstdinにユーザー入力が来るが、lefthook経由だとパイプの先に何もないので cat がEOFを待ち続けてブロックする。
解決:
# stdinがTTY(ターミナル)かパイプかを判定
if [ -t 0 ]; then
# ターミナルから直接実行 → stdinを読める
FILES=$(cat)
else
# パイプ経由(lefthookなど) → stdinを読まずにfallback
FILES=$(git diff --cached --name-only)
fi
調査の勘所:
[ -t 0 ]はファイルディスクリプタ0(stdin)がターミナルに接続されているかを判定するPOSIXの方法- git hookスクリプトはターミナル直接実行とhookマネージャ経由の両方で動くことを想定して、stdin以外のfallback(
git diff --cached等)を用意しておくとよい - この手のシェルスクリプトの環境依存バグは、AIエージェントに任せると原因特定から修正まで速い
AIエージェントの設定ファイル編集をhookでブロックする
AIエージェントにコード修正を任せると、linter設定やCI設定を勝手に緩めることがある。git hookでステージされたファイルをチェックし、保護対象ファイルへの変更をブロックする仕組みを入れた。
仕組み:
- pre-commitフックで、ステージされたファイル名を保護リストと照合する
- 完全一致(
biome.json)とサフィックス一致(lefthook.yml)の2パターンでマッチング - マッチしたら固定フォーマットのエラーメッセージを出して中断
# 保護対象の定義例
PROTECTED_EXACT=("biome.json" ".oxlintrc.json" "lefthook.yml")
PROTECTED_SUFFIX=("settings.json" "package.json")
for file in "$@"; do
for pattern in "${PROTECTED_EXACT[@]}"; do
[[ "$(basename "$file")" == "$pattern" ]] && block "$file"
done
for pattern in "${PROTECTED_SUFFIX[@]}"; do
[[ "$file" == *"$pattern" ]] && block "$file"
done
done
やってみてどうだったか:
- エージェントが「lintエラーを直すために設定を変更しよう」とする行動を確実にブロックできた
- 環境変数
CONFIG_GUARD_ALLOW=1でオーバーライドできるようにしておくと、意図的な設定変更時に困らない - lefthookから呼ばれるとstdinがパイプになるので、TTY判定を入れてstdinの読み取りをスキップする必要があった(次のTIL参照)
