Day1 はプチ演習1〜3 とハンズオン1 です。左のメニュー(狭い画面では上の[目次])から、いま進めている演習と Step に直接飛べます。課題の正式な文面と提出先は GitLab の Issue にあり、このページはその進め方を1操作ずつ説明します。
プチ演習1 規模別レビュアーの2定義
レビュアーの切り替え境界
変更の大きさでレビュアーを当て分けられるようになります。3行の修正のために最重量のモデルを待つ状態をなくします。
| 触るもの | .claude/agents/review-light.md、.claude/agents/review-full.md、.claude/settings.json。差分を作るために app/libraries/util.php と app/controllers/stock_ctl.php を1行ずつ触ります |
|---|---|
| 作るもの | 境界の1文を自分の言葉に書き換えた定義2本、settings.json の3行(model / effortLevel / maxEffortLevel)、記録表4行 |
| 所要 | [20min] |
review-full に回します。上から下へ戻る細い矢印は、軽いほうで読み始めてから境界を超えていたと分かった場合の道です。図に書いてある 50行 は配布した定義ファイルの数字なので、自社の値に書き換える前提で見てください。次の4つがそろってから手順に入ります。
- Claude Desktop で
dl-training-app(事前セットアップで取り込んだフォルダ)をプロジェクトとして開いている - VSCode でも同じフォルダを開き、左のエクスプローラーに
app/db/tests/.claudeが見えている .claude/agents/にreview-light.mdとreview-full.mdの2本がある- VSCode の左下のブランチ名が
ex/p1-で始まっている
ブランチの切り替えと、表示が食い違うときの確認
ブランチは VSCode の左下のブランチ名をクリックして切り替えます。一覧から main を選んだあと、ソース管理ペインの … メニューで プル を実行し、もう一度左下をクリックして 新しいブランチの作成 を選び、ex/p1-yamada と入力します。yamada はご自身の GitLab のユーザー名に置き換えてください。
画面の表示が他の受講者と食い違うときは、版と環境変数を見ます。Claude Desktop のバージョンは設定画面で確認できます。中の Claude Code が 2.1.280 より前だと、model: opus の行き先が Opus 5.5 ではなく Opus 5 になります。maxEffortLevel は 2.1.267 で入った設定で、これより古いと settings.json に書いても効きません。あわせて環境変数 CLAUDE_CODE_EFFORT_LEVEL に値が入っていないことを確認してください。値が入っていると定義側の effort より強く、この演習の表示がそちらに固定されます。
手を動かす前に、次の2点をメモに書き出してください。配布した定義には 50行 という数字が入っていますが、その数字が自社に合っているかは別の話です。
- 1軽い側と重い側を分ける境界を、自分の言葉で1行にします。行数で切るのか、触った層(DB・権限・外部連携)で切るのか、両方を使うのか。決めた理由も1行添えます。
- 2自社のレビューで「毎回出るが毎回見送っている指摘」を1つ思い出し、軽い側の「書かないもの」に入れるかどうかを決めます。
- 1[2min] VSCode で
.claude/agents/review-light.mdを開き、frontmatter(先頭の---で囲まれた部分)のmodel/effort/toolsを見ます(sonnet/medium)。そのまま最終行にある境界の1文の前半を、自分で決めた境界に書き換えて保存します。「review-full の対象です」の文言は変えません。手順5の判定に使います。 - 2[1min]
review-full.mdを開き、frontmatter がopus/xhighになっていることを見てから、「観点2 絶対制約」の箇条書きに、自社で絶対に守らせたい制約を1行足して保存します。 - 3[3min]
.claude/settings.jsonを開き、{の直後、"permissions"の前に3行を足して保存します。{ "model": "sonnet", "effortLevel": "medium", "maxEffortLevel": "xhigh", "worktree": { "baseRef": "head" }, "permissions": { "deny": [], "ask": [], "allow": [] }, "hooks": {} }worktreeの3行は配布時点で入っているので、消さずに残します。保存したら、Claude Desktop のサイドバーで 新しいセッション(Ctrl+N)を開き、プロジェクトフォルダに同じdl-training-appを選びます。settings.jsonはセッション開始時に読まれるので、書き換えたあとは必ずこの操作が要ります。新しいセッションの入力欄の右下に出るモデル名と effort の表示が、Sonnet と 中 になっていることを確認してください。 - 4[3min] 小さい差分を1つ作ります。VSCode で
app/libraries/util.phpを開き、warehouse_name()の中で倉庫コードと名前を対応させている箇所に、既存の行と同じ書き方で倉庫コード4を九州とする1行を足して保存します。続けて Claude Desktop のターミナルで差分をファイルに落とします。ターミナルは右上の>_アイコン、またはCtrl+バッククォートで開きます。セッションの作業ディレクトリで開くので、フォルダを移動する必要はありません。サブエージェントはReadとGrepとGlobしか持たないので、差分は自分でファイルにしてから渡します。手順1〜3で書き換えた.claude配下の3ファイルも未コミットのまま残っているので、差分はパスでutil.phpだけに絞ります。git diff --output=small.diff -- app/libraries/util.php git diff --stat -- app/libraries/util.php
--statの行にutil.php | 1 +と出て、エクスプローラーにsmall.diffが現れれば取れています。 - 5[3min] Claude Desktop の入力欄に下の2行を貼って送ります。先頭の
@"review-light (agent)"は、入力欄で@を打つと出る一覧からreview-light (agent)を選んでも同じです。@"review-light (agent)" small.diff をレビューしてください。 指示は「warehouse_name に倉庫コード 4 = 九州 を足す」です。動いている間に右上の ⋮ メニューの バックグラウンドタスク を開き、review-lightの行のモデル名とトークン数を控えます。サブエージェントの effort はこの画面に出ないので、定義に書いた値を表に書きます。
入力欄で @に続けてrevまで打つと出る候補です。(agent)が付いている行がサブエージェントで、付いていないREVIEW.mdはファイルの候補です。上下キーでreview-light (agent)を選んで Enter を押すと、入力欄に@"review-light (agent)"が入ります。候補の並びは講師環境のもので、review-otherとspec-reviewerは午後のハンズオンで使います。 - 6[2min] 同じ
small.diffを@"review-full (agent)"にも渡し、バックグラウンドタスクのモデル名とトークン数を同じように控えます。 - 7[2min] 次は重い側の担当になるものを渡します。差分は作らず、在庫の引き当てを扱う
app/controllers/stock_ctl.phpのassign()をそのまま軽い側に渡します。境界は行数だけでなく「DB・権限・外部連携に触る変更」も見ているので、これで差し戻されます。差し戻されたら同じ依頼を重い側に出し直し、両方のモデル名とトークン数を控えます。@"review-light (agent)" app/controllers/stock_ctl.php の assign() をレビューしてください。 在庫の引き当てをしている箇所です。 - 8[1min] 記録表の4行を埋め、後片付けをします。
small.diffはコミットしないので、VSCode のエクスプローラーで削除します。残す変更は2つのコミットに分けます。ソース管理ペインでapp/libraries/util.phpだけをステージして「倉庫コード 4 を足す」でコミットし、続けて.claude配下の3ファイルをステージして「レビュアー2本と settings の3行」でコミットします。プチ演習2 はこの2つのコミットの上から始めます。
| 見る場所 | バックグラウンドタスクのペイン(エージェント名、モデル名、トークン数)、返答末尾の 判定: の1行 |
|---|---|
| できた形 | 小さい差分では観点2つ分の出力と 判定: の1行だけが返り、DB に触る依頼では review-full の対象です の1行が返る |
small.diffに軽い側を当てると、判定:の1行で終わるstock_ctl.phpのassign()を軽い側に渡すとreview-full の対象ですが返る- バックグラウンドタスクの行に、定義した
modelのモデル名が出ている - 記録表の4行が埋まり、トークンの列に数字が入っている
util.phpと.claude配下の3ファイルが2つのコミットに分かれ、ソース管理ペインの変更一覧が空になっている(stock_ctl.phpは触っていない)
記録表です。トークンの列は、バックグラウンドタスクのペインでその行に出た数字を書きます。
| 差分 | レビュアー | モデル(ペイン) | effort(定義) | 指摘件数 | うち重大 | トークン(ペイン) | |---|---|---|---|---|---|---| | small.diff | review-light | | | | | | | small.diff | review-full | | | | | | | assign() | review-light | | | | | | | assign() | review-full | | | | | |
Sonnet 5 が出ていれば、定義した frontmatter がそのまま効いています。サブエージェントの effort はこの画面に出ません。Claude Desktop の入力欄では /tasks は使えず、「isn't available in this environment」と返りますレビュアーを2本に分ける理由
3行の修正にも4観点のレビューが最重量のモデルで走ると、確認の待ち時間だけが増えます。観点を絞った軽い側を前段に置き、境界を超えたものだけを重い側へ送ると、待ち時間とトークンの両方が下がります。境界の1文を配布物の数字のまま使わないでほしいのは、何行で切るのが適切かが業務ごとに違うからです。行数ではなく触った層で切ったほうが合う現場もあります。
軽い側の返答で指摘が0件なら、表を省いて 判定: 合格 の1行だけが返ります。命名やインデントの話が混ざっていたら、定義の「書かないもの」が効いていません。大きい差分で4観点のレビューが返ってきた場合は、review-light.md の最終行にある境界の1文が消えていないかを確認してください。assign() は tr_stock と tr_order を続けて書き換えるので、軽い側は差分の中身を読まずに手を引くのが正しい動きです。
モデルとエフォートの対応
どのモデルにどのエフォートを当てるかは、配布物のプチ演習1のフォルダにある対応表にまとめてあります。段の名前と数はツールによって違うので、自社ハーネスへ写すときはこの表を横に置いてください。
maxEffortLevel は全スコープのうち最も低い値が上限になります。ここを high のままにすると、review-full に書いた xhigh は high に丸められ、重い側が重くなりません。
トークンの数え方
サブエージェント1本ごとのトークン数は、バックグラウンドタスクのペインの行に出ます。手順6から手順8までの各回の数字を、その行から記録表に写してください。セッション全体の使用量は入力欄に /usage と送ると見られますが、こちらは累計なので1回分は引き算でしか出ません。本番の運用では軽い側と重い側をセッションの境界で切り替えますが、この演習では差を測るため、あえて1セッションで行き来させます。
つまずいたときは
| 症状 | 直し方 |
|---|---|
| ペインに当てたつもりと違うモデル名が出る | 定義の model が読まれていません。ファイル名と name が一致しているか、frontmatter が --- で閉じているかを見ます |
| 実行が数秒で終わり、ペインを開く前に完了になる | 完了した分も同じペインの「完了」の欄に残ります。そこでモデル名を読み、effort は frontmatter の値を記録表に書きます |
settings.json を保存しても効かない | VSCode に波線が出ていないかを見ます。波線が出たまま保存すると settings.json はファイルごと無視されます |
| 差分が文字化けして届く | git diff > small.diff の形で書き出していないかを見ます。PowerShell 5.1 の > は UTF-16 でファイルを作ります。--output= は git 自身が書くので UTF-8 になります |
定義の model より環境変数が勝つ | CLAUDE_CODE_SUBAGENT_MODEL_FORCE が 1 になっていないかを見ます。1 のときだけ環境変数 CLAUDE_CODE_SUBAGENT_MODEL が定義より先に効きます |
追加と考察、発展課題
軽い側と重い側で、同じ差分に対する指摘の中身がどう違ったかを1行で書いてください。件数の差よりも、重い側だけが出した指摘が「見送ってよいもの」だったかどうかを見ます。見送ってよいものばかりなら、その観点は重い側からも外せます。自社の既存のレビュー用エージェントが5行の修正に何分かけているかを思い出し、この2本に置き換えたときに減る時間を見積もってください。
発展として、3本目にテストの欠落だけを見る定義を tools: Read, Grep, Glob で書いてみてください。観点を1つに絞ると、モデルを haiku まで落としても指摘の質が落ちないことがあります。落ちる場合は、観点の書き方が曖昧だということです。Day2 のハンズオン2で test-gap-reviewer を使うので、そこで自分の版と見比べられます。
maxEffortLevelは全スコープの上限になるので、既存エージェントの effort が丸められることがあります- 定義の
modelは環境変数CLAUDE_CODE_SUBAGENT_MODELより優先されます。CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1のときだけ逆になります - 自社では、既存のレビュー用エージェントの前段に軽い側を置き、数行の修正で最重量のモデルを待たない形にします
| 持ち帰るもの | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
review-light.md | .claude/agents/ 直下。既存のレビュー用エージェントは残し、軽い変更の入口だけをこちらに向けます | .claude/agents/ 直下。中身の差はありません |
review-full.md | 同上。既存の4観点はこちらへ寄せます | 同上 |
settings.json の3行 | 既存の settings.json に追記します | 同じ3行を追記します |
| トークンと所要の記録 | 持ち帰り台帳.md の1行 | 同じ |
プチ演習2 巻き戻し制約の機械強制
戻せない操作の停止
AI が一度に広げた変更を、自分の手直しを巻き込まずに戻せるようになります。戻す手間そのものを減らすために、触らせる範囲を先に絞る側も設定で作ります。
| 触るもの | .claude/settings.json の permissions(ask と deny)、CLAUDE.md の戻し方の節。実験用に index.php、app/libraries/util.php、app/controllers/ 配下を動かします |
|---|---|
| 作るもの | 確認に回す ask 3本、戻せない操作を止める deny 10本(Bash と PowerShell の5組)、AI が触った範囲だけを戻す手順1本 |
| 所要 | [20min] |
この演習で扱うのは、AI に書かせたときにだけ起きる戻し方です。1回の依頼で複数のファイルが同時に書き換わり、そこへ自分の手直しが混ざるところが、人が1人で書いていたときとの違いになります。
index.php だけは1ファイルの中に AI の行と自分の行が同居しています。この1ファイルをファイル単位で破棄すると自分の行も一緒に消えるので、選択した範囲を元に戻す側の操作を使います。
ask と deny を先に作ってから、戻す手順のほうを試します。- プチ演習1 の変更が、ソース管理ペインで2つのコミットに分かれて確定している(
util.phpの1行と、.claude配下の3ファイル) - ソース管理ペインの変更一覧が空になっている。未確定の変更が残っていると、戻った分と残った分が混ざります
- VSCode の左下のブランチ名が
ex/p2-で始まっている。ex/p1-から続けて切ります。mainには戻りません - Claude Desktop で同じフォルダを開いている
- パーミッションモードが 手動 になっている。自動 のままだと確認のダイアログが出ず、手順1と手順8が成立しません。新しいセッションを開くたびに、送る前にモードを見てください
- 1ファイル編集ツールで変えたファイルと、シェルのコマンドで名前を変えたファイル。セッションを巻き戻したあと、それぞれどうなっているかを予想します。
- 2自社のリポジトリで「AI に書き換えさせたくないディレクトリ」を1つ挙げます。手順3でそこを設定に書きます。
- 1[2min] Claude Desktop の入力欄に次を貼って送り、2つの変更を続けて実行させます。1つ目はファイル編集ツール、2つ目はシェルのコマンドです。確認はどちらも、その1回を許可する側の選択肢を選びます。
index.php の先頭に「// rewind test」という行を1行足してください。 そのあとシェルのコマンドで app/libraries/util.php を util.bak にリネームしてください。実行後、VSCode でindex.phpの1行目に// rewind testが入っていること、app/librariesの中がutil.bakになっていることを見ます。 - 2[2min] 入力欄に
/rewindと打って送ります。メッセージの一覧が開いたら、手順1の依頼文を選びます。確認画面の「復元」を コードのみ にして 巻き戻し を押します。index.phpは戻り、util.bakは残ります。セッション単位の巻き戻しが届くのは、AI がファイル編集ツールで書いた範囲だけです。
/rewindを送った直後の一覧です。これまでのメッセージが並び、戻したい地点の文を選びます。写っている依頼文は別のセッションのもので、演習では手順1の依頼文の行を選びます
地点を選んだ後の確認画面です。「復元」の欄で コードのみ を選ぶと、会話は残してファイルだけ戻ります。変更されるファイル数と、シェルコマンドによる変更は戻らない旨がここに出ます - 3[1min]
util.bakを元の名前に戻します。VSCode のエクスプローラーでapp/libraries/util.bakを右クリックし、名前の変更 でutil.phpに戻してください。ソース管理ペインの変更一覧が空になれば片付いています。ここを飛ばすと、午後のハンズオンでテストが全部赤になります。 - 4[2min] 人の確認に回す範囲を先に決めます。
.claude/settings.jsonの"ask": []を次の内容に置き換えて保存します。db配下の編集とgit pushが、どのモードでも確認に回ります。パスの規則はEdit(...)で書きます。Write(...)で書いたパスの規則は受け付けられても参照されません。"ask": [ "Edit(./db/**)", "Bash(git push *)", "PowerShell(git push *)" ]VSCode でsettings.jsonのaskに3本が並び、赤い波線が出ていないことを確認してから、Ctrl+Nで新しいセッションを開いて同じフォルダを選び直します。この手順のあいだだけ、パーミッションモードを 編集を受け入れる に切り替えてください。app/libraries/util.phpの末尾にコメントを1行足すよう依頼すると、確認なしで書き換わります。続けてdb配下のファイルの末尾にコメントを1行足すよう依頼すると、こちらだけ確認が出ます。確認では許可しない側を選び、2つとも見たらモードを 手動 に戻し、util.phpに足された1行はソース管理ペインで 変更を破棄 します。 - 5[4min] AI が触った範囲だけを戻します。次を送り、複数のファイルを一度に書き換えさせます。
app/libraries と app/controllers の中から3ファイルを選び、 それぞれの先頭に「// reviewed」というコメント行を1行ずつ足してください。AI が動いている間に、ご自身でindex.phpの末尾に// 手直しの1行を足して保存します。終わったらソース管理ペインを開き、AI が書き換えた3ファイルの行だけを選んで 変更を破棄 を実行します。index.phpの手直しは一覧に残ります。 - 6[3min] 1つのファイルの中で混ざった場合を分離します。
index.phpの先頭にコメント行を足すよう AI に依頼し、実行後にソース管理ペインでindex.phpをクリックして差分エディタを開きます。AI が足した行だけを選択し、右クリックの 選択した範囲を元に戻す を実行してください。末尾の// 手直しは残ります。ファイル単位で破棄すると自分の1行も一緒に消えるので、ここは行単位で切ります。 - 7[2min] 戻せない操作そのものを止めます。
.claude/settings.jsonの"deny": []を次の内容に置き換えて保存します。denyはaskとallowより先に評価され、allowではdenyに例外を作れません。"deny": [ "Bash(git reset --hard *)", "PowerShell(git reset --hard *)", "Bash(git push --force *)", "PowerShell(git push --force *)", "Bash(git push -f *)", "PowerShell(git push -f *)", "Bash(git checkout -- *)", "PowerShell(git checkout -- *)", "Bash(git clean *)", "PowerShell(git clean *)" ]VSCode でsettings.jsonのdenyに10本が並び、赤い波線が出ていないことを確認してから、Ctrl+Nで新しいセッションを開いて同じフォルダを選び直します。Claude Desktop の入力欄では/permissionsは使えないので、登録の確認はいつもこのファイルで行います。同じ禁止を
Bash(...)とPowerShell(...)の2行で書くのは、Windows で Git for Windows が入っていると、Claude がコマンドを打つときの主なシェルが PowerShell になるためです。このとき Claude は Bash ツールではなく PowerShell ツールを呼び、Bash(...)の規則はその呼び出しに当たりません。Bash(...)だけを書いた設定は、講師の Mac では止まって見えても、皆さんの Windows では素通りします。PowerShell(...)の規則はrmとRemove-Itemのような別名も同じコマンドとして照合し、大文字と小文字を区別しません。 - 8[2min] 止まることを確かめます。次の1行をそのまま送ります。
git reset --hard HEAD~1 を実行してください。返答に、permissions.denyの規則で拒否されて実行できなかった旨が出て、ソース管理ペインのコミットの先頭が動いていなければ合格です。確認のダイアログが出た場合は、規則が読まれていません。許可せずに手順7の確認からやり直してください。あわせてCLAUDE.mdの見出し「戻し方」の箇条書きに、自社の運用に合う1行を足します。書いた場所は文脈であって設定ではありません。実際に止めているのは手順7のdenyです。最後に後片付けをします。ソース管理ペインでindex.phpの 変更を破棄 を押して// 手直しの1行を消し、.claude/settings.jsonとCLAUDE.mdの2つをステージして「巻き戻し制約: ask と deny、戻し方の1行」でコミットします。
| 見る場所 | VSCode のエクスプローラーとソース管理ペイン、settings.json の ask と deny、手順8の返答、ソース管理ペインの コミット グラフの先頭 |
|---|---|
| できた形 | AI が書いた範囲だけが戻り、自分の手直しが1行も消えていない。git reset --hard を名指しで頼んでも規則で拒否され、履歴が書き換わらない |
- コードのみ で巻き戻したあと、
index.phpは戻りutil.bakが残ることを自分の目で見た - 編集を受け入れる のあいだ、
app配下は確認なしで書き換わり、db配下だけ確認が出た - AI が書き換えた3ファイルだけが一覧から消え、
index.phpの// 手直しが残っている index.phpの中で、AI が足した行だけが消えて自分の1行が残っている- 手順8の返答に
permissions.denyによる拒否の旨が出て、コミットの先頭が動いていない
deny が効いたときの返答です。講師がターミナル版の Claude Code で撮ったもので、Claude Desktop では見た目が違います。見るのは、permissions.deny がブロックした旨が返答に入っていることです。返答の言い回しは毎回変わるので、判定は拒否の旨とコミットの先頭が動いていないことの2つで行ってくださいセッションの巻き戻しで戻らない4種
チェックポイントが追いかけているのは、AI がファイル編集ツールで書いた変更だけです。次の4つは戻りません。
| 戻らないもの | 戻す手段 |
|---|---|
シェルのコマンド(Bash ツールと PowerShell ツール)で動かしたファイル(rm / mv / cp / git) | ソース管理ペインの 変更を破棄、または名前を戻す |
| サブエージェントが書いた変更 | 同上 |
| 自分で手編集した分、別セッションの編集 | 同上。ここが残るからこそ、部分ロールバックが要ります |
| シンボリックリンクのファイル | 復元の対象から外れます。リンク先のファイルをソース管理ペインで戻します |
手順2の確認画面には、戻るファイルの数と、シェルコマンドによる変更は戻らない旨が出ます。戻るファイルが0なら、選んだ地点が違います。
混ざる前に避ける手と、混ざった後の手
自分の手直しを先にコミットしてから AI に依頼すれば、そもそも混ざりません。これが一番安い手です。ただし、AI が動いている間に別の箇所を直したくなる場面は実際に起きるので、混ざった後の手も持っておきます。
混ざり方は2段階です。ファイルが分かれているだけなら、ソース管理ペインで行を選んで 変更を破棄 を当てれば済みます(手順5)。同じファイルの中で混ざったら、差分エディタで行を選ぶ 選択した範囲を元に戻す に落とします(手順6)。どちらも未確定の変更にだけ効きます。コミットしてしまった後は、そのコミットを revert して逆差分を足す形になります。
ファイル数での制限
ask と deny で書けるのは、ツールの種類とパスの形までです。「1回の依頼で5ファイル以上を書き換えたら止める」という数での制限は、設定には書けません。数える処理が要るので、ここは PreToolUse フックの仕事になります。プチ演習3 で作るフックが、そのまま書き換えの入口になります。
範囲を絞る側と戻す側は対になっています。絞れていれば戻す手間は小さく、絞れていなければ毎回どこまでが AI の変更かを読み直すことになります。先に絞るほうが安く付きます。
コミット済みの変更を戻すとき
確定した後の戻し方は、AI が絡んでも普通の手順と同じです。公開済みのコミットは revert で逆差分を足します。GitLab の Web 画面でも、マージ済みのマージリクエストの画面から Revert が押せます。未公開の分だけを消したいときに reset --hard を使いますが、これは手順7で AI には禁じた側です。ご自身の手で打つ場合も、消したコミットを探すには git reflog が要ります。これはコマンドでしか見られないので、Claude Desktop の統合ターミナル(右上の >_ アイコン、または Ctrl+バッククォート)から打ちます。
git reflog
CLI で同じことをする場合
Claude Desktop と CLI は同じ設定ファイルを読みます。ここで書いた ask と deny、CLAUDE.md の1行は、どちらから使っても同じように効きます。書き分けは要りません。研修では Claude Desktop で進めます。
つまずいたときは
| 症状 | 直し方 |
|---|---|
| Esc を2回押しても一覧が開かない | Claude Desktop では Esc 2回の開き方は効きません。入力欄に /rewind と打って送ります。ターミナル版の claude では入力欄を空にして Esc を2回押しても開きます |
settings.json に赤い波線がある | JSON が壊れていて、ファイルごと読まれていません。波線の箇所のカンマと括弧を直して保存し、Ctrl+N で開き直します |
| 設定を足したのに効かない | Ctrl+N で新しいセッションを開き直したかを見ます。settings.json はセッション開始時に読まれます |
手順4で db 配下にも確認が出ずに書き換わる | パーミッションモードが 自動 になっているか、ask を足したあと開き直していません。編集を受け入れる に変え、Ctrl+N で開き直してから手順4をやり直します |
| 手順8が拒否されずに確認のダイアログが出る | 許可しない側を選びます。deny が読まれていないので、settings.json の波線と、開き直したかを見ます |
| 変更を破棄 が見当たらない | ソース管理ペインのファイル名の行にマウスを乗せると、右端に矢印のアイコンが出ます |
| 復元したつもりで残っている | シェルのコマンドで動かしたファイルか、シンボリックリンクです。どちらも巻き戻しの対象外なので、ソース管理ペインで戻します |
追加と考察、発展課題
手順8では依頼を名指しにしました。「さっきの変更を、最後の push のところまで全部戻してください」のように曖昧に頼むと何が起きるかも試してください。AI が最初から revert を選べば何も止まらず、reset --hard を選べば規則で止まります。どちらになったかと、止まったあとに何が提案されたかを1行書いておきます。
deny はコマンドの文字列を照合しているだけです。git -C . reset --hard と書いた場合にどうなるかを1回試してください。通る場合、文章でも設定でも止まらない領域が残っていることになります。ここを塞ぐのがプチ演習3 のフックです。
発展として、手順4で書いた ask の範囲を deny に変えてみてください。確認すら出さずに止める形になります。確認を挟む範囲と、そもそも書かせない範囲の線をどこに引くかは、自社のディレクトリ構成で決まります。deny と ask と allow の3つを同じリポジトリで併用すると、どの操作がどの層で止まるかが1枚で見えるようになります。持ち帰るときは、この割り振り表を添えてください。
askとdenyが届くのは Claude Code のツール呼び出しだけです(Claude Desktop と CLI は同じ設定を読むので、どちらでも効きます)。ご自身がターミナルに打つgit checkout -- .は止まりません。人の手元まで含めて止めたいなら、置き場所はリポジトリのブランチ保護と CI になりますaskを広げると確認が増えて手が止まり、狭めると範囲外まで書き換えられます。まずは戻すのが高くつくディレクトリだけに当てます- 部分ロールバックが効くのは未確定の変更だけです。コミットした後は
revertに切り替わります - 既存の
allowと重なってもdenyが勝ちます
| 持ち帰るもの | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
ask 3本 | permissions.ask。戻すのが高くつくディレクトリから書きます | 同じ形。パスの区切りは同じです |
deny 10本 | 既存の permissions.deny に追記します。PowerShell(...) の側を落とさないでください | 同じ10本。bash を正にしていても、Windows の端末で動かす人がいれば対のまま置きます |
CLAUDE.md の戻し方の1行 | CLAUDE.md の該当節 | 規約本体の該当節(当日確定) |
| 部分ロールバックの手順 | 持ち帰り台帳.md の1行 | 同じ |
プチ演習3 秘密情報の遮断とフックの単体テスト
シェル経由の秘密情報の遮断
設定では塞げない読み取りの経路をフックで止め、そのフックが動くかどうかを AI に聞かずに検査できるようになります。
| 触るもの | ダミーの .env、.claude/settings.json、.claude/hooks/block-destructive.ps1、.claude/hooks/tests/cases.json |
|---|---|
| 作るもの | 読み取りの deny 3本、hooks.PreToolUse の登録1本(matcher は Bash|PowerShell)、自分で考えたテストケース1件 |
| 所要 | [15min] |
permissions の deny はツールを選ぶ段で効くので、そこで塞ぎきれない書き方を拾うには、後段にある PreToolUse を使います。分岐の右側、終了コード 2 を返したときだけ実行が止まり、止めた理由の文がそのままモデルへ返ります。- プチ演習2 の変更が確定し、ソース管理ペインの変更一覧が空になっている
- VSCode の左下のブランチ名が
ex/p3-で始まっている。mainには戻りません - リポジトリ直下にダミーの
.envがある。本物の接続情報は置きません - ソース管理ペインの一覧に
.envが出ていない。.gitignoreに.envの行は配布時点で入っています
ダミーの .env の作り方
Claude Desktop の統合ターミナルで1行打ちます。ターミナルは右上の >_ アイコン、または Ctrl+バッククォートで開きます。Mac は echo 'DB_PASS=dummy-secret-123' > .env です。
Set-Content .env 'DB_PASS=dummy-secret-123'
.gitignore と .worktreeinclude には .env の行が配布時点で入っているので、書き足す必要はありません。ソース管理ペインの一覧に .env が出なければ合っています。.env が出たままコミットすると、あとで削除しても履歴に残ります。.env.tpl は op:// の参照文字列しか持たないので、こちらは追跡したままにします。
- 1自社で「AI に読ませたくないファイル」を3つ挙げ、そのファイル名の形を書きます。固定名か、
credentialsを含む名前か、拡張子で決まるかで、書く正規表現が変わります。 - 2そのファイルに触る「止めたい形」と「止めてはいけない形」を1つずつ挙げます。手順5でテストケースに変えます。
- 1[2min]
.claude/settings.jsonのdenyの配列の最後"PowerShell(git clean *)"の末尾にカンマを付け、その下に読み取りの3本を足します。最後の1本にカンマは付けません。VSCode でdenyに13本が並び、赤い波線が出ていないことを確認してから、保存してCtrl+Nで新しいセッションを開きます。"Read(./.env)", "Read(./.env.*)", "Read(./**/credentials*)" - 2[1min]
.envを読ませてみます。Read ツールが権限で拒否され、中身は返りません。.env の中身を教えてください。 - 3[2min] 同じセッションで、今度はシェルのコマンドで読ませます。Windows では Claude が PowerShell ツールで
Get-Content .envかcat .envを打ちます。確認で許可する側を選ぶとDB_PASS=dummy-secret-123がそのまま返ります。Read(./.env)のdenyは PowerShell ツールの経路を塞ぎません。cat .env の結果を貼ってください。演習リポジトリのCLAUDE.mdには「.envと認証情報を読みません」の1行があるので、コマンドを打つ前に断られることもあります。その場合は「文脈で断られた」と記録して手順4へ進みます。止めたのはCLAUDE.mdの文脈で、設定ではありません。頼み方を変えれば通ることがある状態なので、手順4からのフックで止める理由は変わりません。 - 4[2min] フックを登録します。本体の
.claude/hooks/block-destructive.ps1(Mac はblock-destructive.sh)は同梱済みです。VSCode で開き、規則が「patternsが全部一致し、exceptが1つも一致しないとき止める」の組になっていることだけ見てから、settings.jsonの"hooks": {}を次の塊に置き換えて保存します。"hooks"のキーを2つにすると JSON が壊れ、ファイルごと無視されます。"hooks": { "PreToolUse": [ { "matcher": "Bash|PowerShell", "hooks": [ { "type": "command", "command": "powershell.exe", "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.ps1"] } ] } ] }matcherは「どのツールの直前に走らせるか」で、Bash|PowerShellは Bash ツールと PowerShell ツールの両方です。Bashだけにすると、Windows で Claude が PowerShell ツールから打ったコマンドではフックが呼ばれません。commandに実行するプログラム、argsに引数を分けて書く形なので、フォルダのパスに空白や日本語が入っても引用符の問題が起きません。Mac の方は"command": "bash"と"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh"]の2行に差し替えます。完成形は.claude/settings.example.jsoncにあります。 - 5[2min] AI を起動せずに1件流します。Claude Desktop の統合ターミナルで次の2行を打ちます。1行目がフックに JSON を渡す部分、2行目が終了コードを表示する部分です。
2が返れば止まっています。echo '{"tool_name":"PowerShell","tool_input":{"command":"Get-Content .env"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File .claude\hooks\block-destructive.ps1 $LASTEXITCODEMac はecho '{"tool_name":"Bash","tool_input":{"command":"cat .env"}}' | bash .claude/hooks/block-destructive.shとecho $?です。 - 6[2min]
.claude/hooks/tests/cases.jsonを開き、配列の最後の要素の閉じ}の後ろにカンマを付けて、自分で考えたケースを1件足します。idは他と重ならない名前にします。保存したらターミナルでランナーを回します。Mac はbash .claude/hooks/tests/run_cases.shです。{ "id": "ps-ng-my-secret", "hook": "block-destructive", "input": { "tool_name": "PowerShell", "tool_input": { "command": "type .env" } }, "expect_exit": 2, "note": "PowerShell の type でも同じ経路" }powershell -NoProfile -ExecutionPolicy Bypass -File .claude\hooks\tests\run_cases.ps1
足す位置の図です。配列の最後にある要素の閉じ }の直後にカンマを付け、その下に1件を続けます。新しく足した要素が最後になるので、その閉じ}にはカンマを付けません。 - 7[1min]
Ctrl+Nで新しいセッションを開き、手順3と同じ依頼をもう一度します。今度は止まります。登録の確認はsettings.jsonを VSCode で開いて見ます。Claude Desktop の入力欄では/hooksは使えません。
フックが止めたときの Claude Desktop の返答です。講師の Mac で bash 版のフックが git reset --hardを止めた例なので、皆さんの画面ではフック名がblock-destructive.ps1、理由の行がenv-fileになります。止めたのが規則かフックかは、この返答の文言で分かります。規則が止めたときはpermissions.denyによる拒否と返ります - 8[1min] ソース管理ペインで
.claude/settings.jsonと.claude/hooks/tests/cases.jsonの2つをステージし、「秘密情報の遮断: deny 3本、PreToolUse、テスト1件」でコミットします。.envが一覧に出ていないことをもう一度見てから確定してください。午後のハンズオン1 のワークツリーは、このコミットから切られます。
| 見る場所 | $LASTEXITCODE、ランナーの PASS / FAIL 行と末尾の件数、settings.json の登録内容 |
|---|---|
| できた形 | 手で流した JSON に理由の2行と 2 が返る。ランナーが 23 cases: 23 passed, 0 failed。Claude Desktop から .env を読むコマンドが止まる |
denyだけの状態で、シェルのコマンドから.envが読めてしまうこと、またはCLAUDE.mdの文脈だけで断られたことを1度見た- 自分で足したケースを含めてランナーが
23 cases: 23 passed, 0 failedで終わる - フックを登録したあと、同じ依頼が止まる
git reset --hard を流した例です。command: git reset --hard の行に続いて echo "exit=$?" が 2 を返しています。PowerShell 版で Get-Content .env を流すと、理由の行と command: の行のあとに $LASTEXITCODE が 2 になります。AI を起動せずに済むので、書き換えたその場で試せます手動検査とランナーの出力
手順5で出る内容です。1行目が理由、2行目が止めたコマンド、最後が終了コードです。
[block-destructive] env-file: touches a .env file. Keep secrets out of the session: wrap the single command with run-with-secrets.ps1 (op run) or read them from CI/CD variables. command: Get-Content .env 2
0 が返っていたら、フックがペイロードを読めていません。settings.json が壊れた JSON だとファイルごと無視されるので、VSCode で開いて赤い波線が無いかを確認してください。
手順6のランナーの出力です。1行目に対象の版と件数が出て、そのあと1件1行で並びます。配布時点の22件に自分で足した1件を含めて、末尾の合計が23件になります。
target=PHP 7.4 / cases=23 / materials=v2 2026-09-17 PASS bash-ok-run-tests (exit 0) PASS bash-ok-op-run (exit 0) PASS ps-ok-git-status (exit 0) PASS bash-ng-reset-hard (exit 2) PASS ps-ng-get-content-env (exit 2) PASS ps-ng-my-secret (exit 2) 23 cases: 23 passed, 0 failed
1件でも FAIL が出たらランナーは exit 1 を返し、そのケースの出力を続けて表示します。自分で足したケースだけが落ちる場合は、正規表現が type .env の形を拾えていません。
止める幅の決め方
.env の規則はこの形です。patterns が全部一致し、except が1つも一致しないときに止めます。
patterns = @('(^|[\s"/\\=])\.env([^A-Za-z0-9_]|$)')
except = @('\.env\.tpl([^A-Za-z0-9_]|$)')
.env.tpl だけを例外にしているのは、op:// の参照文字列しか持たないファイルで、ここを止めると起動用のスクリプトが動かなくなるためです。例外はこの1本だけにしてください。広く止めれば業務が止まり、狭く止めればすり抜けます。cases.json があると、この幅を1件ずつ足しながら詰められます。
フックの例外とは別に、deny の Read(./.env.*) はファイル名の照合なので .env.tpl にも当たります。AI の Read ツールでは .env.tpl も読めません。中身を確かめるときは、人が VSCode で開きます。
PowerShell で引っかかるところ
| 症状 | 直し方 |
|---|---|
.ps1 cannot be loaded because running scripts is disabled on this system | 呼び出しに -ExecutionPolicy Bypass が付いているかを見ます。手で叩くときも同じ指定が要ります。付いているのに出る場合は、グループポリシーで実行ポリシーが固定されています。この端末では ps1 は動かないので、講師に知らせてください |
| フックのメッセージが化ける | .ps1 は ASCII だけで書きます。日本語版 Windows の PowerShell 5.1 は、指定しなければ CP932 で読み書きします。日本語は cases.json の note に置けます。こちらは UTF-8 で読まれます |
| bash 版が素通りする | jq が無いと標準エラーに1行出して通します。ガードを止めるよりセッション開始時に大きく報告する設計で、sessionstart_verify.sh が jq の不在を知らせます |
CLI で同じことをする場合
Claude Desktop と CLI は同じ settings.json を読むので、ここで登録したフックはどちらから使っても走ります。ランナーは AI を起動しないので、結果も同じです。研修では Claude Desktop で進めます。
追加と考察、発展課題
except に入れる必要があるファイルを、ご自身の職場から1つ挙げてください。自社の hooks にテストが揃っている側と揃っていない側があるなら、揃っていない側から1本選んで、今日と同じ形のケースを足すところまでを持ち帰りの宿題にします。
発展として、matcher を Bash だけに戻して開き直し、手順7と同じ依頼をしてみてください。PowerShell ツールから打たれたコマンドにはフックが呼ばれず、読めてしまうはずです。確かめたら Bash|PowerShell に戻します。無人実行を設計する Day2 では、この性質が止め金の土台になります。あわせて、ブロックの理由の文面を自分のチームの言葉に書き換えてください。この文面はそのままモデルに返るので、次の手として何をすればよいかが書いてあるかどうかで、止めたあとの動きが変わります。
PreToolUseはdenyと同じく、どのパーミッションモードでも走ります。exceptを広げると業務が止まり、狭めるとすり抜けますmatcherがBashだけだと、Windows の PowerShell ツールには効きません- フックは手元で動く仕組みなので、設定を外した人には効きません。消されたら困るものは、リポジトリのブランチ保護と受け入れ時の検査で持ちます
同じフックを別の環境に写すと、黙って効かなくなることがあります。次の4つは、どれも「止まらずに通る」方向に倒れます。フックの単体テストを書く理由がここにあります。
| 効かなくなる原因 | そのときの見え方 |
|---|---|
| 改行が CRLF になっている | フックが呼ばれず、画面に何も出ません。.gitattributes に *.sh text eol=lf を書きます |
| 実行ビットが落ちている | 同じく何も出ません。WSL の /mnt/c 配下では chmod +x が効かないので、フックを WSL 側に置きます |
| パスを絶対パスで書いている | 他の人の手元で保護したいパスに当たらず、通ります。リポジトリ相対に直します |
| 判定に使うコマンドが無い | jq などが無いとエラーを握りつぶして通します。コマンドの存在確認を入れます |
環境は「AI ツールが動く場所」「コードが置いてある場所」「ランタイムの版」の3軸で見ます。WSL と Windows をまたぐ構成は、フックが1回動くたびにパスの変換が入るので遅くなります。判定と実行を分け、止める対象を設定ファイルに出しておくと、環境が増えても入口を1本足すだけで済みます。組み合わせ別の設計は、配布物の 50_環境別のフック設計/環境別のフック設計.md にまとめてあります。
| 持ち帰るもの | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
読み取りの deny 3本 | 既存の permissions.deny に追記します。Read(...) は PowerShell の Get-Content には効かないので、フックと組にします | 同じ3本 |
block-destructive.ps1 | hooks/ 直下。既存のフックと同じ PowerShell 5.1 と ASCII の規則に合わせます | 持ち込みません。bash 版を使います |
block-destructive.sh | 持ち込みません | hooks/ 直下。既存の検査スクリプトと同じ並びに置きます |
| 追加したテストケース | hooks/tests/cases.json に1件追記します | テストの無い検査スクリプトが残っているので、同じ形の受入テストを1本足す入口にします |
ハンズオン1 改修1本の AI 駆動一周と実装・レビューの分離
Issue 1本を調査から MR まで一周させ、同じ差分を3通りで読ませたときに指摘がどう変わるかを、自分の数字で言えるようになります。
実装とレビューの分離
題材は演習リポジトリの Issue #1 です。見積一覧の顧客名検索で、入力値を SQL 文字列にそのまま連結している箇所を直します。
本体は Step 4 です。実装したセッションに「レビューして」と頼むのと、履歴を切った読み手に読ませるのとでは、出てくる指摘が同じになりません。この差を手元で測ってから、分離の置き方を自社ハーネスへ1本書き戻して終わります。
終わったかどうかの判定
上から順に、自分で確認できる形にしてあります。講師に聞かずに判定してください。
- MR が1本あり、
lint-phpdev/lint-phpver/test/test-phpunitの4つが緑になっている。途中でlint-phpverが赤になった方は、混入した構文と行が MR の説明欄に記録してある(Issue #1 の受入条件3) git grep -n "customer_name" -- app/controllers/estimate_ctl.phpの結果に、$_GET['customer_name']を$whereへ連結している行が無いindex()のdb_select呼び出しが第2引数のパラメータ配列を取っているtests/phpunit/EstimateSearchTest.phpに、顧客名に'を含む入力のテストが1本以上増えている- そのテストが変更前のコードでは落ちたことを、CI のログで確認した
- MR の「AI レビュー結果」表の3行が件数と所要時間まで埋まり、4行目の
ai_reviewが件数か「未実施」で埋まっている - MR の「人が判断した採否」表に全指摘が入り、直さなかったものに理由が書いてある
ai_gateの結果を見て、今日はそれが止める力を持っていないことを説明できる- 自社ハーネスに分離の置き方が1本入っている
始める前にそろっている状態
| 開いておく画面 | Claude Desktop、VSCode、ブラウザで GitLab の演習プロジェクト |
|---|---|
| いるファイル | 手元にクローン済みの dl-training-app。読むだけのものは Issue #1、REVIEW.md、.claude/agents/review-other.md |
| 直前の演習の成果物 | プチ演習1〜3で .claude/settings.json に足した model、effort、ask、deny、PreToolUse フックの設定。ex/p3- のブランチにコミット済みであること。ワークツリーはこのブランチの HEAD から切られるので、コミットしていない変更は持ち込まれません |
| 紙かメモ帳 | Step 1 で3つの欄を書きます |
| パーミッションモード | Claude Desktop を 手動 にしておきます。自動 のままだと確認が出ないまま編集が進み、Step 3 で差分を読む前に変更が増えます。Step 2 でワークツリーに移ったら一度見直してください |
https://gitlab-09291006aidev.give-app.net/dlive-training/dl-training-app/-/issues にあります。使うのはラベル 演習::ハンズオン1 が付いた1本だけです。8つの Step と時間の配分
| Step | やること | 目安 |
|---|---|---|
| 導入 | 全体図の確認と題材の共有 | [5min] |
| Step 1 | Issue #1 を読み、影響範囲の仮説と改修方針と検証方法を紙に書く | [15min] |
| Step 2 | worktree で作業場所を隔離し、grep-scout の調査結果と仮説を突き合わせる | [20min] |
| Step 3 | 機能テストを先に足し、実装し、1ステップ1コミットで進める | [35min] |
| Step 4 | 同じ差分を3通りでレビューし、件数と所要時間を記録する | [25min] |
| Step 5 | 指摘を委譲判定シートに仕分け、限定修正する | [15min] |
| Step 6 | MR を開き、CI の6ジョブの結果を受ける | [20min] |
| Step 7 | 作業指示書を生成し、自分で赤入れする | [5min] |
| Step 8 | 分離の置き方を自社ハーネスへ書き戻す | [20min] |
| 合計 | [160min] |
Step 3 が一番長く、Step 4 がその次です。時間が押したときに削るのは Step 7 で、Step 4 と Step 5 は削りません。
この演習で触るもの
| 触るファイル | app/controllers/estimate_ctl.php の index()、tests/phpunit/EstimateSearchTest.php、MR の説明欄、docs/shijisho/issue-1.md |
|---|---|
| 操作 | 紙に3欄を書き、Claude Desktop のワークツリーで作業場所を分け、grep-scout に調査を委譲する。テストを先に push して赤を見てから実装し、同じ差分を3通りでレビューして4分類に仕分け、MR を仕上げて指示書を生成する |
| 見る場所 | バックグラウンドタスクのモデル名、CI の test-phpunit が赤から緑に変わる履歴、lint-phpver の結果、MR の Pipelines タブに並ぶ2本のパイプライン |
| 達成の状態 | 「終わったかどうかの判定」の9項目。MR 1本、lint-phpver 以外の3ジョブが緑(lint-phpver は緑か、赤なら記録して残す)、テストが変更前に落ちた履歴、3通りの計測表、採否表、自社ハーネスに1本 |
| 自社での使いどころ | Issue から MR までの標準の流れと、実装した本人に採点させない仕組み |
| 影響が及ぶ範囲 | main は保護されていて直接 push できない。worktree には .env と vendor/ が入らない。date_from / date_to と sql_lib.php は範囲外 |
この演習で使う語と、作るものの置き場所
先に押さえる語です。パイプラインは、push か MR の作成をきっかけに GitLab が .gitlab-ci.yml の内容を実行する1回分のことで、その中の1つ1つの処理(lint-phpdev、test-phpunit など)がジョブです。ジョブを実際に動かすサーバーをランナーと呼び、演習環境では受講者全員で共有しています。サブエージェントは、本体の Claude Code とは別の履歴で動き、決められたツールだけを持つ下請けの Claude です。grep-scout と review-other がこれにあたり、入力欄で @"名前 (agent)" と書いて呼びます。REVIEW.md はリポジトリ直下にあるレビュー基準のファイルで、CI の ai_review とサブエージェントの両方がこれを読みます。
終わったときに手元に残っているものです。名前と場所をここで決めておくと、Step 8 で自社ハーネスへ移すときに探さずに済みます。
| 生成物 | 名前と場所 |
|---|---|
| 仮説と方針のメモ | 紙、または docs/handson1/plan.md |
| 影響範囲の調査結果 | grep-scout の出力。MR 説明の「影響範囲」欄に貼る |
| 実装 | app/controllers/estimate_ctl.php |
| 機能テスト | tests/phpunit/EstimateSearchTest.php に追記 |
| 3通りレビューの計測 | MR 説明の「AI レビュー結果」欄 |
| 委譲判定シート | MR 説明の「人が判断した採否」欄 |
| 作業指示書 | docs/shijisho/issue-1.md |
| ブランチと MR | Step 2 で控えた worktree- で始まるブランチから main への MR 1本 |
ブランチ名は Step 2 でワークツリーを作ったときに worktree- で始まる名前が自動で付きます。この資料の worktree-issue-1 は、その控えた名前に読み替えてください。ブランチ名が ex/ で始まらないのは11本の演習のうちここだけです。Step 2 の折りたたみの手順でワークツリーを使わずに進めた方は、ex/h1-<ユーザー名> になります。.claude/skills/implement-issue/SKILL.md の手順3にある ai/issue-1 は、夜間の無人実行が自分で作るときの名前です。今回は自分で起動するので使いません。
Issue の読み解きと、紙に書く3つの欄 [15min]
AI を起動する前に、自分で Issue を読んで結論を出します。
- 1ブラウザで GitLab の演習プロジェクトを開き、左のメニューの Plan から Issues を選びます。一覧の
#1が今回の Issue です。 - 2
背景、現状、期待する振る舞い、受入条件、対象ファイル、範囲外の6つの見出しを読みます。このタブは Step 6 と Step 7 でもう一度使うので閉じないでください。 - 3
範囲外の2つを声に出して確認します。date_fromとdate_toの連結、app/libraries/sql_lib.phpの共通処理です。 - 4下の3つの欄を紙に書きます。書けたら伏せておき、Step 2 の終わりに開いて突き合わせます。
| 欄 | 書くこと |
|---|---|
| 影響範囲の仮説 | 触るファイル名。そのファイルを呼んでいる場所。関係するテーブル名。今あるテストのうち、この変更で壊れる可能性があるもの |
| 改修方針 | どう直すか。選ばなかった案を1つと、選ばなかった理由 |
| 検証方法 | 誰が何を実行して、何が見えたら正しいか。Issue の受入条件6項目のうち、自分で確認できるのはどれか |
- 3つの欄がすべて埋まっている。空欄のまま Step 2 に進まない
- 範囲外の2つを言える
- 受入条件6項目のうち、自分で確認できるものに印が付いている
先に自分で結論を出す理由と、受入条件の確かめ方
ここを飛ばすと、Step 2 で返ってくる調査結果が正しいのかどうかを判定できなくなります。突き合わせる相手が無いからです。改修方針の欄で案が1つしか出ていないときは、まだ方針が決まっていないことが多いです。
範囲外の2つは、Step 4 のレビューで指摘として上がってきます。そのときに「範囲外だから直さない」と判断できる状態にしておいてください。
受入条件の1番は次の1行で判定できます。Windows の PowerShell には grep が無いので、Windows と Mac のどちらでも同じ結果が出るこの形を使ってください。
git grep -n "customer_name" -- app/controllers/estimate_ctl.php
自分で確認できる条件と、CI に任せる条件を分けて書いておくと、Step 6 で結果を見る順番が決まります。
ハンズオン1 影響範囲の調査と実装
worktree での作業場所の隔離と、調査の委譲 [20min]
作業用のチェックアウトを別に作ってから始めます。元のフォルダには手が入らなくなるので、途中で方針を変えたときにフォルダごと捨てられます。
- 1VSCode でこれまでの演習フォルダを開き、左下のブランチ名が
ex/p3-で始まっていることを確かめてから、左サイドバーの枝分かれアイコン(ソース管理)を押します。変更が残っていたら、メッセージ欄に「午前の設定を持ち込む」と書いて コミット を押してください。ワークツリーはこのブランチの最後のコミットから切られるので、未コミットの変更は入りません。 - 2Claude Desktop で新しいセッションを開き、入力欄の上の行のプロジェクトフォルダに演習フォルダ(
index.phpがある場所)を選びます。 - 3同じ行のブランチ名の右にある ワークツリー にチェックを入れ、入力欄に「作業を始めます」と1行送ってセッションを開始します。名前を入れる欄はありません。
- 4入力欄の上に出たブランチ名を、紙に控えます。
worktree-で始まる名前が自動で付いています。以降、この資料のworktree-issue-1は控えた名前に読み替えてください。 - 5VSCode でも同じ作業場所を開きます。ファイル から フォルダーを開く を選び、演習フォルダの
.claude/worktrees/の下にできた1つのフォルダを指定してください。左下の青い帯のブランチ名が、手順4で控えた名前であることを確認します。 - 6午前の設定が入っているかを見ます。VSCode のファイル一覧で
.claude>settings.jsonを開き、denyの13行(プチ演習2 の10本とプチ演習3 の3本)、askの3行、model、PreToolUseが入っているかを確認してください。denyが空だった場合は、下の折りたたみ「grep-scout が返す見出しと、worktree の注意点」の最後の2段落の手順に切り替えます。
.claude/worktrees/ の下にできたフォルダの中だけです。 元のフォルダ側のファイルを開いて直すと、変更がワークツリーのブランチに入らず、push しても CI に届きません。開いているファイルのタブにマウスを乗せ、パスに .claude\worktrees\ が含まれていることを確認してから編集してください。下の折りたたみの手順でワークツリーを使わずに進める方は、元のフォルダで ex/h1- のブランチに乗っていることを確かめます。調査を grep-scout に委譲します。影響範囲を調べるだけのサブエージェントで、Read と Grep と Glob しか持っていません。書き換えはできない作りです。
- 7手順3で開始した同じセッションの入力欄に、次の1行をそのまま貼って送信します。Issue の本文は丸ごと貼らないでください。
@"grep-scout (agent)" 見積一覧の顧客名検索の条件組み立てを直します。影響範囲を調べてください
- 8実行中に右上の ⋮ メニューの バックグラウンドタスク を開いて、モデル名が Haiku になっていることを確認します。
.claude/agents/grep-scout.mdの frontmatter に書いたmodel: haikuが効いている場所です。調査には1分から3分かかります。 - 9返ってきた一覧と、Step 1 で紙に書いた仮説を突き合わせ、下の表を埋めます。
| 突き合わせ | 書くこと |
|---|---|
| 自分だけが挙げたもの | grep-scout が見落としたのか、自分の思い込みか。どちらかを判定する |
| grep-scout だけが挙げたもの | 自分が見落とした理由。ファイルを開いて事実かどうかを確かめる |
| 両方が挙げたもの | そのまま MR の「影響範囲」欄へ |
## 同じ知識が重複している箇所 の節は必ず読んでください。今回の Issue では触らない箇所も挙がりますが、片側だけ直す事故はこの節が予防します。重複の節に出る例
自分の仮説と突き合わせてから開いてください。見積の状態コード(作成中・提出済など)の対応表が、app/libraries/util.php、app/controllers/estimate_ctl.php、app/views/estimate_list.php、app/views/estimate_detail.php の4か所にあります。Day2 のハンズオン3 では、この4か所を1つにまとめる Issue #4 を無人で流します。
- ワークツリーのブランチ名を紙に控えてあり、VSCode の左下のブランチ表示がその名前になっている
- ワークツリーの
.claude/settings.jsonに午前の設定が入っている - 突き合わせの表が3行とも埋まっている
- MR の「影響範囲」欄に貼る内容が決まっている
grep-scout が返す見出しと、worktree の注意点
grep-scout の定義は .claude/agents/grep-scout.md にあり、調べる順番と返す形がそこに書いてあります。返ってくる見出しはこの7つで、行番号のない記述は書かない決まりにしてあります。
## 対象 ## 触るファイル ## 呼び出し元と呼び出し先 ## データベース ## テストの有無 ## 同じ知識が重複している箇所 ## 確認できなかったもの
worktree は新しいチェックアウトなので、.env や vendor/ は入りません。今回の演習では使わないので、このまま進めて構いません。持ち込む必要が出た場合は、プロジェクトルートに .worktreeinclude を置いて、入れたいファイルを書きます。フックを書くときの $CLAUDE_PROJECT_DIR は元のプロジェクトルートを指したままで、worktree のパスは標準入力の JSON の cwd に入ります。
分岐元は settings.json の worktree.baseRef で決まります。既定の fresh は origin/main から切るので、手元のブランチで足した設定が入りません。配布した settings.json には head が入っていて、開始したときに乗っていたブランチの HEAD から切ります。
手順6で deny が空だった場合は、ワークツリーを始めたときに ex/p3- 以外のブランチに乗っていたか、設定をコミットしていなかった可能性があります。いったんセッションをアーカイブし、元のフォルダで ex/p3- に切り替えて設定をコミットしてから、手順2からやり直してください。
それでも deny が入っていない場合は、baseRef が効かずに main から切られています。ワークツリーを使わずに進めます。セッションをアーカイブし、VSCode で元のフォルダを開いて左下のブランチ名が ex/p3-<ユーザー名> であることを確かめ、新しいブランチの作成 で ex/h1-<ユーザー名> を切ります。Claude Desktop は ワークツリー のチェックを入れずに、元のフォルダで新しいセッションを開きます。以降の worktree-issue-1 は ex/h1-<ユーザー名> に、.claude/worktrees/ の下のフォルダは元のフォルダに読み替えてください。
先にテスト、次に実装、1ステップ1コミット [35min]
テストを先に足し、変更前のコードでそれが落ちることを CI で確認してから実装に入ります。落ちないテストは、通っても何も保証しません。
- 1VSCode で
tests/phpunit/EstimateSearchTest.phpを開き、既存の5本がこの画面をどう確認しているかを読みます。$_GETに検索条件を入れ、capture_view()で出力を文字列にし、count_list_rows()で件数を数える形です。 - 2顧客名に
'を含む入力で例外が出ず0件になるテストを1本足します。Claude Desktop に頼む場合の文面の例です。入力値と確認する件数の2か所を、自分で決めた内容に替えてください。tests/phpunit/EstimateSearchTest.php に、$_GET['customer_name'] に半角の引用符を含む文字列(例: オライオン'商会)を入れて index を表示し、例外が出ず count_list_rows が 0 になることを確認するテスト testAcceptsSingleQuoteInCustomerName を、既存の5本と同じ書き方で1本足してください。app/ 以下のファイルは変更しないでください
- 3VSCode の ソース管理 ペインを開きます。変更 の一覧に
tests/phpunit/EstimateSearchTest.phpだけが出ていることを確認してください。ファイル名をクリックすると差分が開きます。増えたのがテスト1本だけで、既存の5本が変わっていないことを目で見ます。 - 4ファイル名の右の + を押してステージに上げ、上のメッセージ欄に「顧客名に引用符が入る場合のテストを足す」と書いて コミット を押します。
- 5同じペインの 変更の同期(または ブランチの発行)を押して GitLab へ送ります。初回は「このブランチを公開しますか」と聞かれるので、そのまま進めてください。ユーザー名とパスワードを聞かれたら、事前セットアップで決めた GitLab のものを入れます。
- 6ブラウザで GitLab を開き、左メニューの Build から Pipelines を選びます。検索欄に控えたブランチ名(この資料の
worktree-issue-1)を入れて絞り、Stages の丸からtest-phpunitを開いて、赤になっていることと落ちた理由が SQL の構文エラーであることを確認します。 - 7実装に入ります。Step 1 で書いた改修方針を、Claude Desktop に渡してください。渡すのは方針と範囲外の2つです。角括弧の中を自分の紙の内容で埋めます。
app/controllers/estimate_ctl.php の index() で、customer_name を [自分の方針。例: db_select の第2引数にパラメータ配列で渡す形] に直してください。 参考にする前例は [Step 2 の調査で確認した前例のファイルと関数] です。 date_from と date_to の連結と、app/libraries/sql_lib.php は今回触りません。 このプロジェクトの対象は PHP 7.4 です。CLAUDE.md の制約の表に従ってください。 変更したらファイル名と変更した行を一覧で教えてください。コミットは私がします
- 8ソース管理ペインで
app/controllers/estimate_ctl.phpをクリックし、差分を自分で読みます。方針と違う場所に手が入っていたら、その場で Claude Desktop に戻させてください。 - 9手順4と同じ要領でステージに上げ、「見積一覧の検索条件をプレースホルダに置き換える」でコミットし、同期を押して送ります。1つの変更意図につき1コミットです。
- 10Pipelines の一覧でいちばん上に増えた行を開き、
lint-phpdev、test、test-phpunitの3つとlint-phpverが緑になることを確認します。終わるまで3分から5分かかります。lint-phpverが赤のあいだは、後ろの段のtestとtest-phpunitが skipped になって動きません。赤になった場合は、Step 6 の記録の表に沿って混入した構文と行を控えてから、実装したセッションに「PHP 7.4 で動く書き方に直して」と頼んで直し、もう一度コミットして同期します。Issue #1 の受入条件3 がこの扱いです。
main には default と protected のバッジが付いていて、ここへ直接 push できないことがこの画面でも読めます。この画面は講師環境で撮ったもので、ブランチ名は皆さんが Step 2 で控えた名前と異なります
- コミットが2本以上ある。テストのコミットが実装より前にある
- テストだけのコミットで
test-phpunitが赤になったことを見た - 実装後に
lint-phpdev、lint-phpver、test、test-phpunitが緑になっている。lint-phpverが一度赤になった方は、構文と行を控えてから直してある git grep -n "customer_name" -- app/controllers/estimate_ctl.phpの結果に、$_GETを$whereへ連結している行が無い
1ステップ1コミットにする理由と、混ざった後の戻し方
AI は1回の依頼で複数のファイルをまとめて書き換えます。そこに自分の手直しが乗ると、後から「AI が書いた部分」と「自分が直した部分」を切り分けられなくなります。コミットを分けておくと、混ざる前の状態に境界線が引かれるので、あとで AI の変更だけを戻せます。
すでに混ざってしまった場合の手当てです。コミットを丸ごと戻すのではなく、AI が触ったファイルだけを1つ前のコミットの状態へ戻します。
git log --stat -3 git checkout HEAD~1 -- app/controllers/estimate_ctl.php
1行目で、直前のコミットにどのファイルが入ったかを一覧で見ます。2行目で、そのうち戻したいファイルだけを1つ前の状態に戻します。他のファイルは触りません。戻した結果はソース管理ペインに変更として現れるので、差分を読んでからコミットしてください。
1つのファイルの中で混ざっている場合は、ソース管理ペインの行単位ステージを使います。差分の表示で、残したい行の左の余白を右クリックし、選択した範囲をステージ を選ぶと、その行だけがコミットの対象になります。自分の手直しだけを先にコミットして退避させ、残った AI の変更を 変更を破棄 で捨てる形です。
戻す手間そのものを減らすには、依頼の時点で範囲を絞るほうが効きます。手順7の文面が「触らないファイル」を名指ししているのはこのためです。
PHP 7.4 に合わせる書き方と、静的検査を通り抜けるもの
このプロジェクトの対象は PHP 7.4 です。AI は素で PHP 8.x の構文を書きたがるので、match 式、enum、readonly、名前付き引数、コンストラクタプロモーション、?-> は使いません。代替の書き方はリポジトリの CLAUDE.md の表にまとまっています。テストは既存の5本と同じ書き方にそろえてください。
php -l が通っても動かないものがあります。str_contains() や str_starts_with() は PHP 8.0 で入った関数で、構文としては正しいので php -l を通ります。7.4 で実行すると Call to undefined function で落ちます。そのため lint-phpver は2段で見ています。1段目の php -l が構文を、2段目の php ci/undefined_functions.php が 7.4 の実行環境に関数があるかを function_exists() で確かめ、str_contains() はこの2段目で落ちます。構文検査だけでは対象バージョンを守りきれない、という話がここで出ます。
この時点では対象バージョンを機械で止めるフックをまだ入れていません。制約は CLAUDE.md の文章だけで守られている状態です。文章に書いた制約が数ターン後に薄れる様子は、Step 6 の lint-phpver の結果に出ます。出たら記録してください。Day2 の最初のセッションでその記録を使います。
現行環境がもっと古い場合でも、対象には移行先の版を書くほうが移行が進みます。古い版を対象にして書かせると、移行のたびに書き直しが発生するためです。
CI の出力の読み方と、赤と緑の見分け
ジョブの名前の左に緑のチェックが付いていれば成功、赤い × は失敗、橙色の ! は「失敗したが allow_failure で全体は止めない」です。ジョブを開いたログの最終行が Job succeeded なら緑、ERROR: Job failed: exit code 1 なら赤です。test-phpunit は他のジョブより2分ほど長くかかります。
手順6でこう見えていれば正しい状態です。テストが落ちる理由が「まだ直っていないから」であることを、エラー文で確認してください。落ちた理由はログのいちばん下ではなく、赤い行の直前にあります。
.....E 6 / 6 (100%) There was 1 error: 1) EstimateSearchTest::testAcceptsSingleQuoteInCustomerName PDOException: SQLSTATE[42601]: Syntax error: 7 ERROR: syntax error at or near "商会" LINE 6: WHERE e.del_flg = 0 AND c.customer_name LIKE '%オライオン'商会%' ERRORS! Tests: 6, Assertions: 12, Errors: 1.
手順10でこう見えていれば正しい状態です。アサーションの数は自分が足したテストの書き方で変わります。見るのは赤のほうの E が1つ出ていることと、緑のほうの OK の行です。
...... 6 / 6 (100%) OK (6 tests, 13 assertions)
手が止まったときの確認先
テストが赤にならない場合は、足したテストが実際に新しい経路を通っているかを見てください。$_GET['customer_name'] に入れた文字列が ' を含んでいるかが最初の確認点です。
実装後もまだ赤い場合は、db_select() の第2引数にパラメータ配列を渡せているかを見ます。app/controllers/stock_ctl.php の index() に、同じライブラリをプレースホルダで使っている前例があります。
同期が拒否された場合は、main に対して送っていないかを確認してください。main は保護されていて直接 push できません。pre-receive hook declined と出るのがこの状態です。VSCode 左下のブランチ表示が Step 2 で控えた名前であることを見てから、もう一度押してください。
Authentication failed と出た場合はパスワードが違います。何度も続くときは、事前セットアップの FAQ にある資格情報マネージャーの手順で、記憶された古いパスワードを消してください。
手順6で test-phpunit が緑になってしまった場合は、テストが worktree 側ではなく元のフォルダ側のファイルに書かれています。VSCode のタブのパスに .claude\worktrees\ が含まれているかを見てください。
ハンズオン1 3通りのレビューと採否
3通りのレビューと記録 [25min]
ここがこの演習の本体です。差分は1つ、読ませ方は3つ。読み手から情報を1つずつ外していき、指摘がどう変わるかを自分の数字で見ます。
| 読ませ方 | 読み手が持っているもの | 1つ前から外したもの |
|---|---|---|
| 1 実装したセッションでそのまま依頼 | 実装の履歴、REVIEW.md、役割の指定 | |
2 review-other サブエージェント | REVIEW.md、定義ファイルで固定した役割と観点 | 実装の履歴 |
| 3 履歴を切った新しい会話 | 差分ファイルと REVIEW.md だけ | 役割と観点の定義 |
4 CI の ai_review | 差分と REVIEW.md。人の言い方が一切入らない。結果は Step 6 で受ける | 人がその場で足す言葉 |
1から2で履歴が外れ、2から3で役割の定義が外れます。どちらの段で指摘が変わったかで、効いていたのが履歴なのか定義なのかが分かれます。
review-input.diff を4つの経路で読ませます。1の実装したセッションそのまま依頼だけが履歴を持っていて、自分の変更を甘く見ます。2の review-other サブエージェントは定義ファイルで役割を固定した読み手、3の履歴を切った新しい会話は差分だけを渡した素の状態、4は runner 上で動く CI の ai_review です。3通りの結果は ho1-reviews.md に貼って比べます。4は Step 6 で受けます。右の表は、その4つがそれぞれ何を持って差分を読むかです- 11通り目。実装したセッションでそのまま頼みます。新しい会話にせず、続けてください。
この変更をレビューしてください。REVIEW.md の基準で、重大な順に最大8件です
- 2差分をファイルに出します。ここはコマンドを使います。サブエージェントは
ReadとGrepとGlobしか持たず、自分で差分を取れないためです。Claude Desktop の右上の>_アイコン、またはCtrl+バッククォートで統合ターミナルを開きます。セッションと同じ場所、つまりワークツリーの中で開くので、移動は要りません。yamadaはご自身のユーザー名に置き換えます。git diff ex/p3-yamada --output=review-input.diff
比べる相手はmainではなく、ワークツリーを切った元のex/p3-のブランチです。mainと比べると、プチ演習1〜3 で足した設定の変更まで差分に入ります。 - 32通り目。実装の経緯を渡さずに、定義ファイルで役割を固定したサブエージェントへ読ませます。
@"review-other (agent)" review-input.diff を、同僚が書いたコードとして読んでください。差分に出ていない前提はリポジトリのファイルで裏を取ってください
結果は「重大度、場所、内容、根拠、直し方」の5列の表と、最後の判定: 合格か判定: 要修正の1行で返ります。表の行数が指摘件数です。 - 43通り目。
Ctrl+Nで新しいセッションを開き、プロジェクトフォルダに演習フォルダの.claude/worktrees/の下の自分のフォルダを選びます。ワークツリーのチェックは入れません。初めて開くフォルダなので、ワークスペースの信頼を聞かれたら ワークスペースを信頼 を押します。押さないとフックとdenyが読まれません。役割の定義は渡さず、差分ファイルと基準だけを渡してください。review-input.diff と REVIEW.md を読み、重大な順に最大8件で指摘してください。重大度、場所、内容、根拠、直し方の5列の表で返してください
2通り目と同じ形で返るように、返し方だけを指定しています。役割も観点も与えていないので、2通り目と出方が変わります。 - 53通りの出力を、VSCode で新しいファイルに貼り付けて残します(保存先は演習フォルダの外、たとえばデスクトップに
ho1-reviews.md)。Step 6 で MR に貼ります。 - 6
review-input.diffを削除します。ソース管理ペインの 変更 にこのファイルが残っていないことまで見てください。 - 7下の表の1行目から3行目を埋めます。開始と終了の時刻を控えて所要時間も入れてください。4行目は Step 6 で CI の結果を受けてから足します。
| 読ませ方 | 指摘件数 | P0/P1 の件数 | 所要時間 | 気づいた差 |
|---|---|---|---|---|
| 1 実装したセッションでそのまま依頼 | ||||
2 review-other サブエージェント | ||||
| 3 履歴を切った新しい会話 | ||||
4 CI の ai_review | Step 6 で埋めます | |||
- 表の1行目から3行目が埋まっている。所要時間の列も埋まっている
- 「気づいた差」の列に、件数以外のことが1行書いてある
- 3通りの出力をファイルに貼って残してある
review-input.diffを消している。ソース管理ペインの変更一覧に出ていない
基準が空のままレビューさせる意味と、JSON で受け取る形
REVIEW.md は今日の時点では見出しだけの空の雛形です。自社版を起案するのは Day2 です。基準が空でもレビューは返りますが、返り方が基準のある場合とどう違うかを「気づいた差」の列に書いておくと、Day2 の起案の材料になります。
件数を機械で数えたい場合は、JSON スキーマを指定して受け取る形があります。パイプで渡す形は Claude Desktop の入力欄からは実行できないので、統合ターミナルを開いて CLI から打ちます。研修では使いませんが、自社で集計に載せるときはこちらが楽です。
git diff ex/p3-yamada | claude -p --output-format json --json-schema ((Get-Content .claude\review.schema.json -Raw -Encoding UTF8) -replace '"','\"') "REVIEW.md の基準でレビューし、findings を返す"
--json-schema にはファイル名ではなく、スキーマの中身を文字列で渡します。PowerShell 5.1 は引数の中の " をそのまま渡さないので、-replace で \" に置き換えています。Mac では --json-schema "$(cat .claude/review.schema.json)" です。
findings は返ってきた JSON の structured_output に入ります。形はこう固定されるので、Step 5 の仕分けにそのまま使えます。
{
"summary": "見積一覧の検索条件の変更を読み、3件見つけました。",
"findings": [
{
"severity": "P1",
"confidence": 0.9,
"file": "app/controllers/estimate_ctl.php",
"line": 22,
"message": "date_from が文字列のまま SQL に連結されています。db_select の第2引数へ移してください。",
"category": "sql-injection"
}
]
}
Windows の PowerShell では、先に次の1行を打って文字コードを UTF-8 にそろえないと、日本語が ? に化けて渡ります。
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
委譲判定シートへの仕分けと限定修正 [15min]
集まった指摘を4つに分けます。分けるのは人です。指摘を全部受け入れると、意図してそうしていた設計が巻き戻ります。
| 軸 | 見る点 | 軽い側 | 重い側 |
|---|---|---|---|
| 可逆性 | 直して外したとき、何を戻せば元に戻るか | コードを戻せば元に戻る | DB、外部連携、権限に届く |
| 機械検証 | 直ったかどうかを機械が判定できるか | lint かテストで差が出る | 目で見ないと判定できない |
| 影響範囲 | 触る画面の数 | 1画面 | 2画面以上、または共通処理 |
| 分類 | この分類にする条件 | この場でやること |
|---|---|---|
| 即修正 | 3軸すべてが軽い側 | 直す。1件1コミット |
| 要調査 | 機械検証が重い側。正しいかどうかが読んだだけでは決まらない | 誰が何を調べるかを書く。直さない |
| 将来課題 | Issue の範囲外 | Issue を切る。番号を採否の表に書く |
| 意図した設計 | 可逆性が重い側、または背景があって今の形にしている | 直さない。理由を書く |
- 13通りで出た指摘を1つの表にまとめます。同じ指摘が複数のレビューから出ている場合は1行にし、どのレビューから出たかを残します。
- 2各行に3軸を付けます。付けられない行は、その場でファイルを開いて確かめてください。
- 33軸から分類を決めます。分類から先に決めると、感覚で仕分けることになります。
- 4
date_fromとdate_toの指摘は範囲外なので将来課題です。GitLab の Plan > Issues から右上の New issue を押し、Title に「見積一覧の見積日の検索条件の組み立て」のように対象が分かる1行を書きます。 - 5Description には Issue #1 と同じ6つの見出しのうち、少なくとも
現状と範囲外の2つを書いて Create issue を押します。作成後のページの見出しの#の後ろの数字が Issue の番号です。 - 6「即修正」に入れたものだけを直します。1件1コミットで、コミットメッセージは「レビュー指摘: 直した内容を1行で」の形にします。ソース管理ペインからコミットまで行い、同期はまだ押しません。送るのは Step 6 の手順1でまとめてです。
- 7直したら、その指摘を出した読ませ方でもう一度レビューし、その指摘が消えたことだけを確認します。新しく出てきた P2 以下は追いません。
現状 と 範囲外 を書き、左下の Create Issue を押します。作成後に開くページの見出しに出る # の後ろの数字が Issue の番号なので、そこで控えてください。他の受講者も同じ Issue を切るので、番号は人によって違います- すべての指摘が4分類のどれかに入っている。空欄が無い
- 「将来課題」にしたものは Issue の番号が書いてある
- 「意図した設計」にしたものは理由が書いてある
- 直したのは「即修正」だけになっている
「意図した設計」に入れた指摘の扱い
AI は業務の背景を知りません。「意図した設計」に入れた指摘は、次の改修でも同じ形で出ます。同じ判断を毎回やり直さずに済ませるには、理由を REVIEW.md の Do not report に移すかどうかをその場で検討してください。Day2 のレビュー基準の起案で、この判断が材料になります。
ハンズオン1 MR と CI の結果
MR と6ジョブの読み方 [20min]
MR を GitLab の画面で作ります。MR ができると、push のたびに動く lint と test の4ジョブに加えて、MR 用のパイプラインで AI レビューの2ジョブが動きます。合流前の最終判定をここで受けます。
- 1VSCode のソース管理ペインで 変更の同期 を押し、Step 5 の修正を送ります。即修正が0件で新しいコミットが無い場合は押さずに次へ進んでください。
- 2ブラウザで GitLab の左メニュー Code > Merge requests を開き、右上の New merge request を押します。
- 3Source branch に Step 2 で控えたブランチ(この資料の
worktree-issue-1)、Target branch にmainを選び、Compare branches and continue を押します。 - 4次の画面で Title を「Issue #1 見積一覧の検索条件をプレースホルダに置き換える」に書き換えます。
- 5Description には、リポジトリの既定のテンプレート(
Default.md)の節が最初から入っています。「変更概要」に何をなぜ変えたかを2行とCloses #1、「影響範囲」に Step 2 の突き合わせ結果、「テスト」に Step 3 で見た CI の結果、「AI レビュー結果」に Step 4 の表の4行、「観点別レビュー結果」に Step 4 でreview-otherが返した指摘、「人が判断した採否」に Step 5 の仕分けを書きます。観点別の3体(security / spec / test-gap)を当てるのは Day2 なので、今日はreview-otherの分だけで足ります。 - 6Preview タブで表の罫線が崩れていないかを見てから Create merge request を押します。
- 7MR のページ上部の Pipelines タブを開きます。いちばん上の2本が最新で、
merge requestのラベルが付いた行が MR 用(ai_reviewとai_gate)、branchのラベルが付いた行がブランチ用(残りの4つ)です。合わせて6つのジョブの結果を確認してください。 - 8Changes タブを開き、Step 4 で読ませた
review-input.diffの変更が含まれているかを見ます。MR の Changes と CI のai_reviewはmainとの差なので、プチ演習1〜3 で足した.claude配下の設定やutil.phpの1行も一緒に出ます。Step 4 の3通りより読む量が多いことを踏まえて、4行目の件数を比べてください。
nightly_implement は押さないでください。 手動ジョブなので6つに数えません。| ジョブ | 見ているもの | 落ちたときにすること |
|---|---|---|
lint-phpdev | 現行環境(PHP 8.3)で構文が通るか | エラー行を直す |
lint-phpver | 対象の PHP 7.4 で動くか。1段目の php -l で match 式や enum のような 8.x の構文を、2段目の php ci/undefined_functions.php で str_contains() のような 7.4 に無い関数の呼び出しを落とす | 混入した構文と行を MR の説明欄に記録してから直す(Issue #1 の受入条件3。記録は Day2 の題材になります) |
test | 簡易ランナー11件 | 落ちたケースの名前から該当箇所を探す |
test-phpunit | 機能テスト。対象と同じ PHP 7.4 で回る | 手元では再現できないので、ジョブのログを読む |
ai_review | 差分を読み、findings.json を作って MR にコメントを1本置く。落とす判断はしない | 落ちるのは応答がスキーマの形にならなかったときだけ |
ai_gate | findings.json に P0 か P1 があれば exit 1 | 今日は allow_failure が付いている。赤くても止まらない |
ai_review が動くと、MR の Overview タブの下のほうに「AIレビュー(モデル: ... / 深さ: ... / 差分 ... 行)」で始まるコメントが1本付きます。見出し行の件数を表の4行目に写してください。所要時間はジョブの画面右側の Duration を使います。
ここで一度立ち止まってください。ai_gate は allow_failure: true なので、赤くなってもパイプライン全体は赤になりません。ゲートが赤いのにマージできる状態は、ゲートではありません。列挙しているだけです。この1行を外してゲートを赤にするのが Day2 のハンズオン2です。Merge ボタンまで止めるには、プロジェクトで Pipelines must succeed を有効にしておく必要があります。演習のプロジェクトはこの設定を入れていないので、ボタンは押せますが押しません。
lint-phpver が落ちた方は、直す前に記録してください。記録先は MR の説明欄です。落ちた構文は Day2 の最初のセッションでそのまま教材になります。
| 記録すること | 書き方 |
|---|---|
| 落ちた行 | ファイル名と行番号 |
| 出たエラー文 | ログに出たエラーの1行目を、そのまま貼る |
| 混入した構文 | match のように、どの構文が入ったか |
| 指示のどこに書いてあったか | CLAUDE.md に書いてあったのに混入したのか、書いていなかったのか |
!1 のような MR の番号が出ます。この画面は講師環境のものです
worktree-issue-1 を選び、右の Target branch が main になっていることを見てから、左下の Compare branches and continue を押します
2 files +14 -2 のようにファイル数と足した行数、消した行数が出ます。差分の左の2列の数字が変更前と変更後の行番号で、赤い行が消した行、緑の行が足した行です
lint-phpdev と lint-phpver のあとに test と test-phpunit が動きます。点線の先の nightly_implement は手動なので押しません。下の MR 用は MR がある間の push のたびに走り、ai_review のあとに ai_gate が GATE_LEVEL 以上の指摘があると落ちます
ai_review_bot のコメントです。1行目に「AIレビュー(モデル: ... / 深さ: ... / 差分 ... 行)」、次に要約、その下に P0 0 件 / P1 2 件 / P2 3 件 / P3 1 件 のような件数の行が出るので、これを表の4行目に写します。この画面は差分 146 行で claude-opus-5 が6件指摘した例です。差分が 43 行だと sonnet に切り替わり、指摘の件数は減ります
ai_review を開くと、右側の Duration に秒数が出ます(この例は 15 seconds)。ログの bash ci/ai_review.sh の次の行に「レビュー結果を findings.json に保存し、MRコメントに投稿しました。」と出ていれば、コメントが付いています
ai_review と ai_gate が並び、見出しの下に merge request のラベルと Related merge request !1 の行が出ます。下は lint、test の順に4ジョブが並びます
Errors parsing の行でファイル名を確認し、その1つ上の行のエラー文を記録してください- MR が1本あり、
lint-phpdev、lint-phpver、test、test-phpunitの4つが緑。途中でlint-phpverが赤になった方は、その記録が MR の説明欄に残っている - MR 説明の節が埋まっている。「AI レビュー結果」の4行目は、
ai_reviewが動かなかった場合は「未実施」と書いてある ai_gateの赤に関係なく Merge ボタンが押せる状態で出ていることを確認した(押しません)lint-phpverが落ちた場合、4項目の記録が残っている
ai_gate と lint-phpver の出力の読み方
ai_gate の出力はこう見えます。allow_failure が付いているため、赤くてもパイプライン全体は赤になりません。
しきい値 P1 以上、確信度 0.0 以上の指摘を数えます。 confidence の無い指摘は 1.0、P0〜P3 以外の severity は P0 として数えます。 P1: 1 件 / P2: 2 件 ゲートで止めた指摘 1 件 [P1] app/controllers/estimate_ctl.php:22 date_from が文字列のまま SQL に連結されています。 直すか、直さない理由をMRのコメントに書いてから再実行してください。
lint-phpver は2段で見ます。1段目の php -l で落ちたときは、ログに PHP Parse error で始まる行と、Errors parsing に続くファイル名の行が出ます。エラーの文面は PHP の版で変わるので、出た1行目をそのまま記録してください。
2段目は php ci/undefined_functions.php です。str_contains() や str_starts_with() のような PHP 8.0 で入った関数の呼び出しは、構文としては正しいので1段目を通ります。2段目が 7.4 の実行環境に function_exists() で聞き直し、無ければ次のように落とします。
undefined_functions: PHP 7.4 に無い関数を呼んでいます。php -l は通りますが実行時に落ちます。
この行の下に、ファイル名と行番号と関数名が1件1行で並びます。手元で同じものを編集の直後に止めるのが、Day2 のプチ演習4 で登録する check-phpver フックです。
ハンズオン1 持ち帰り物と書き戻し
作業指示書の生成と赤入れ [5min]
実装が終わった後に指示書を作るのは順番が逆ですが、生成したものと自分が書いたものの差を見るのが目的です。
- 1Step 1 で開いたブラウザの Issue #1 を表示し、本文(背景から範囲外まで)を範囲選択してコピーします。
- 2Claude Desktop の入力欄に
/shijisho 1と打ち、Shift+Enterで改行してから、手順1でコピーした Issue 本文を続けて貼り付けて送信します。Enterだけを押すと、本文を貼る前に送られます。途中で grep-scout が再び呼ばれるので、2分から3分かかります。glabやcurlで Issue を取りに行く実行確認が出たら、許可しない側を選んでください。本文は貼ってあるので、取りに行く必要はありません。 - 3VSCode で
docs/shijisho/issue-1.mdを開き、5項目(目的、影響範囲、変更方針、検証、戻し方)で止まっているか確認します。 - 4「目的」と「戻し方」の2つを、自分の言葉に直します。この2つは、Issue の本文を言い換えただけになりやすい箇所です。
- 5ソース管理ペインでステージに上げ、「Issue #1 の作業指示書を足す」でコミットし、同期を押して送ります。worktree を片付けたときに消えないよう、MR に載せておきます。
docs/shijisho/issue-1.mdがある- 「目的」と「戻し方」に自分の言葉が入っている
Issue 本文を手で渡す理由と、直した箇所の読み方
/shijisho は .claude/skills/shijisho/SKILL.md に書かれた手順を Claude Code に実行させるスキルです。この skill は本来 Issue の本文を GitLab から取りに行きますが、受講者の端末には glab もアクセストークンも入っていないので、最初から本文を一緒に渡します。
直した箇所が「目的」と「戻し方」に集中していたら、生成物の使いどころが見えたことになります。影響範囲と検証は grep-scout と CI の出力から機械的に埋まるので、人が書き直す必要はほとんど出ません。
自社ハーネスへの書き戻し [20min]
今日のうちに1本だけ自社ハーネスへ移します。移すのは「実装した本人に採点させない」を仕組みにするものです。2つ用意してありますが、入れるのは片方だけにしてください。
| 選ぶもの | 向いている場合 | 入れるファイル |
|---|---|---|
| 履歴を切るサブエージェント | 手元でレビューを回したい。端末ごとの環境差を減らしたい | .claude/agents/review-other.md |
| CI の AI レビュージョブ | レビューを MR に残したい。全員の端末に同じ設定を配りたくない | ci/ai_review.sh と .gitlab-ci.yml の review ステージ |
- 12つのファイルを開いて、どちらを自社に置くか決めます。決め手は、レビューを人の手元でやるか MR に置くかです。
- 2選んだ1本を自分のハーネスに写します。下の表の置き場所を見てください。
- 3自社のリポジトリ名、ブランチ名、レビュー基準のファイル名に書き換えます。演習リポジトリの名前がそのまま残っていると、次に使う人が混乱します。
- 4持ち帰り台帳の本日分に、今日足した1本を記入します。
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
.claude/agents/review-other.md | 既存のエージェント定義と同じ階層。効いているのは履歴を切ることなので、model: は実装側と同じままで構いません。変えるのは発展課題2 と同じ任意の実験です | 同じ階層に同じ名前で置く |
ci/ai_review.sh | CI 設定と同じ階層。CI の runner(alpine)の上の bash で動くので、PowerShell へ書き直す必要はありません | 同じ階層に同じ名前で置く。コマンドはそのまま使える |
| 3通りレビューの計測表 | 持ち帰り台帳に貼る | 持ち帰り台帳に貼る |
| 委譲判定シート(4分類×3軸) | レビュー基準のファイルの近くに置く | 規約本体のどのタグに属するかを決めてから足す |
| MR テンプレの「AI レビュー結果」欄 | .gitlab/merge_request_templates/ | 同じ |
| worktree 運用手順(2行) | 戻し方の節の近くに追記 | 規約本体の該当節に追記 |
- 自社ハーネスに1本入っている。2本入れていない
- 演習リポジトリの名前が残っていない
- 持ち帰り台帳の本日分に1行増えている
2つの環境で書き方が割れる箇所
ファイルの名前と中身は写しますが、本文のコマンド例は自社の形に直してください。D2 は PowerShell 5.1 で、スクリプトとメッセージを ASCII に保つ約束になっていると伺っています。smart3pm は bash が正です。
同じ目的のものが2つの書き方で並ぶことになるので、どちらを正にするかを決めておくと、次に足す人が迷いません。決められない場合は、決められないこと自体を台帳に書いてください。Day2 の最後にこの判断を扱います。
終わった後に1行ずつ書くこと
次の3つを、持ち帰り台帳の余白に1行ずつ書いてください。Day2 の冒頭で使います。
| 問い | 書き方 |
|---|---|
| 履歴を切った読み手だけが出した指摘 | 件数ではなく、指摘の中身を1つ書く。何も無かった場合は「無し」と書く |
| grep-scout と自分の仮説の差の出どころ | 自分が見落とした理由を、コードの読み方の話として書く |
| 今日の指摘のうち機械で判定できた割合 | 4分類のうち「即修正」に入れた件数を、指摘の総数で割る。この数字が Day2 のレビュー基準の起案の入り口になる |
手が空いた方が進めるもの
上から順に、効き方が大きい順です。全部やる必要はありません。
- 1Step 5 で「将来課題」にした
date_fromとdate_toの Issue を、自分で本文まで書いてください。受入条件を機械で判定できる形にできるかが、この課題の見どころです。 - 2
review-otherのmodel:をopusに書き換えて、同じ差分をもう一度読ませてください。件数と中身がどう変わるかを Step 4 の表に足します。モデルを変える効果は研究では測られていないので、ここは各自の実測になります。 - 3同じ差分を
claude -pに渡し、Claude Desktop で出た指摘と比べてください。-pは入力欄からは実行できないので、統合ターミナルを開いて打ちます。結果をファイルに落とせる形になるので、自社で集計に載せるときの材料になります。 - 4Claude Desktop のサイドバーで、このセッションのアーカイブアイコンを押して worktree を片付けてください。押す前に、GitLab 側に
worktree-issue-1ブランチと MR が残っていることを確認します。手元が消えても送ったものは残る、という関係を見ておくと、Day2 で複数の Issue を並行させるときに迷いません。

