Day2 はプチ演習4〜8 とハンズオン2・3 で、当日の進行順に並べています。左のメニュー(狭い画面では上の[目次])から、いま進めている演習と Step に直接飛べます。課題の正式な文面と提出先は GitLab の Issue にあり、このページはその進め方を1操作ずつ説明します。
プチ演習4 PHP 7.4 制約の機械検査
D2-02 / 所要 [20min]
プロジェクトが対象とするバージョンを設定に書き、それを超える構文が書かれた瞬間に差し戻される状態を作ります。
match を書かせようとしても switch に戻る状態
目的
CLAUDE.md の文章で守っている制約を、フックの機械検査へ移せるようになります。対象のバージョンは設定に書く値として扱い、自社の版に差し替えて持ち帰れる形にします。
準備
| 開いておく画面 | Claude Desktop(プロジェクトは dl-training-app)、VSCode、GitLab のプロジェクト画面 |
|---|---|
| いるファイル | CLAUDE.md、.claude/settings.json、.claude/hooks/check-phpver.ps1、app/libraries/util.php |
| 直前の演習の成果物 | プチ演習3 で登録した PreToolUse(終えていない方もこの演習は進められます) |
| ブランチ | ex/p3-<ユーザー名> から ex/p4-<ユーザー名> を切ります。Step 1 の手順1 と 2 で作ります |
| パーミッションモード | 手動。編集のたびに確認が出る状態で始めます。自動では確認が出ないまま編集が進むので、差し戻しの見え方が変わります。モードはフォルダごとに記憶されるので、dl-training-app を開いた状態で切り替えてください |
このリポジトリが対象とする版は PHP 7.4 です。検査で止めるのは、7.4 に無い 8.x の構文です。どれも AI が指示なしで素で書く書き方なので、放っておくと混ざります。
| 止める書き方(8.x) | 7.4 での書き方 |
|---|---|
match 式 | switch か if の連鎖 |
enum 宣言 | クラス定数を並べる |
readonly プロパティ | private + 取得用のメソッド |
| 名前付き引数 | 並び順で渡す |
| コンストラクタプロモーション | プロパティ宣言と代入を書く |
?-> | isset で受けてから呼ぶ |
str_contains / str_starts_with / str_ends_with | strpos / strncmp / substr で書く |
自分で考える [3min] AI に触る前に、手元のメモへ3つ書き出してください。自社の CLAUDE.md を持ち込んだ方は、その制約節を横に開いてください。
- 1自社の基幹システムで、これを書かれたら本番で落ちる、という構文を3つ挙げてください。バージョン番号ではなく構文の名前で書きます。
- 2その3つを、禁止の列挙ではなく代替の書き方で1行ずつ書き直してください。「
matchは使わない」ではなく「switchで書く」の形です。 - 33つのうち、機械で検査できるものに丸を付けてください。丸が付かなかったものは人が見る側に残ります。その理由も1行書きます。
手順
CLAUDE.md は文脈なので、そこに書いただけでは差し戻しは起きません。止めているのは真ん中のフックと右端の CI です。差し戻しから編集へ戻る下の矢印が、人が何も言わないまま書き直しが始まる経路にあたります。Step 0 代替の書き方を1行足す [2min]
VSCode で CLAUDE.md の「実行環境の制約」の表を開き、自分で挙げた3つのうち表に無い構文を1行足してください。左の列に使えない書き方、右の列にこの環境での書き方を書きます。3つとも載っていた方は、右の列の言い方を自社の言葉に直してください。
Step 1 フックが無い状態で頼む [3min]
- 1VSCode で
dl-training-appを開き、画面左下のステータスバーのブランチ名がex/p3-yamadaになっていることを確かめてください。違う名前なら、ブランチ名をクリックして一覧からex/p3-yamadaを選びます。main から切ると、Day1 に足した deny と PreToolUse が入りません。 - 2もう一度ブランチ名をクリックし、「新しいブランチの作成」で
ex/p4-yamadaを作ってください。yamadaはご自身のユーザー名に置き換えます。いま乗っているex/p3-yamadaから切られます。 - 3VSCode で
.claude/settings.jsonを開き、"hooks"の中に"PostToolUse"がまだ無いことを確認してください。 - 4Claude Desktop を開き、セッション開始時のプロンプト領域で Project folder に
dl-training-appを選んでください。入力欄に次をそのまま送ります。app/libraries/util.php の h() を、引数が null のとき空文字を返すように match 式で書き直してください。
- 5編集が通ったことを確認してください。差し戻しは起きません。CLAUDE.md には制約が書いてありますが、止める力は持っていません。
こう見えていれば正しい状態です。h() は util.php の冒頭にある3行の関数で、書き換わるのはこの1行です。差分は VSCode のソース管理ペインでファイル名をクリックすると開きます。
- return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
+ return match (true) {
+ $s === null => '',
+ default => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'),
+ };
1回目で断られたとき
そのまま「短く書き直してください」と続けてください。数ターン会話を進めてから頼むと通ります。断られることと、止まることは別です。CLAUDE.md は文脈なので、会話が伸びるほど制約が薄れます。
Step 2 PostToolUse に登録する [3min]
- 1VSCode で
.claude/settings.jsonの"hooks"に次のブロックを足してください。.claude/settings.example.jsoncの同じブロックと一字一句同じです。
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "powershell.exe",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-phpver.ps1"]
}
]
}
]
足す場所は "hooks": { と、それを閉じる } の間です。プチ演習3 で PreToolUse を入れた方は、"PreToolUse": [ ... ] を閉じる ] の直後にカンマを1つ置いてから、その下に貼ります。"matcher": "Edit|Write" は「Edit と Write の2つのツールの後だけ走らせる」という意味です。command に実行ファイル、args に引数を分けて書く形なので、パスに空白や日本語が入っても引用符の問題が起きません。
.claude/settings.json の完成形と、各演習で足す範囲です。permissions.deny の上10本(Bash と PowerShell の対5組)と ask がプチ演習2、deny の下3本がプチ演習3、hooks の PreToolUse がプチ演習3、PostToolUse がこの演習、Stop がプチ演習6、SessionStart がハンズオン3 です。〈 〉で省いたフックの中身は4本とも同じ形なので、図の下に1件だけ全文を載せています。丸で囲んだ所が ] や } の直後のカンマで、次の要素が続くときだけ付け、最後の要素には付けません。プチ演習6 Step 2 で Stop を足すときも、この図の位置に戻って確認します- 2保存したら、Claude Desktop で
Ctrl+Nを押して新しいセッションを開いてください。settings.jsonはセッションの開始時に読まれます。登録されているかは、VSCode でsettings.jsonを開き、"hooks"の中に PreToolUse と PostToolUse が1つずつ並び、赤い波線が無いことで確かめます。
Step 3 同じ依頼をもう一度出す [3min]
- 1ソース管理ペインで
app/libraries/util.phpの行にカーソルを合わせ、右側の矢印アイコン(変更の破棄)で Step 1 の変更を戻してください。戻さないと、Claude が編集せずに「対応済みです」と答えることがあります。 - 2Step 1 と同じ文をもう一度送ってください。文面は変えません。
- 3編集の直後にフックが走り、標準エラーの内容がモデルへ返ります。Edit ツールの結果の下に、終了コード 2 で止めた旨と、フックが標準エラーに書いた次の4行が返ります。2行目の先頭のパスと行番号は、ご自身の clone 先で変わります。
[check-phpver] code that PHP 7.4 cannot run: C:\...\app\libraries\util.php:9 match expression (8.0) -> use switch and assign in each branch Rewrite these lines with the form shown after the arrow, then edit again. Arrow functions, typed properties, ?? and the spaceship operator are all fine on 7.4.
- 4そのまま待つと、モデルが
switchの形へ書き直して編集をやり直します。人が指示を足さずに戻ることを確認してください。書き直しが終わったら、ソース管理ペインの差分にmatchが残っていないことを見ます。
PostToolUse:Edit hook returned blocking error と [check-phpver] の3行)、下半分が対象バージョンの形へ戻した書き直しです。この画面は、差し戻しのあとに人が一言送ってから書き直された版です。通常は一言を足さなくても、差し戻しの文がモデルへ返った時点で自動で書き直しが始まります。ご自身の画面では、その一言が無いまま下半分が出ることを確認してくださいStep 4 検査そのものにテストを足す [3min]
- 1VSCode で
.claude/hooks/tests/fixtures/に、str_ends_with()の呼び出しを1行だけ含む見本をng_str_ends_with.php.txtという名前で作ってください。隣にあるng_str_contains.php.txtを開くと形が分かります。1行目が<?php、2行目以降は ASCII だけで数行です。中身は例えば次の形です。<?php // str_ends_with() arrived in PHP 8.0. php -l on 7.4 does not report it. function has_suffix($haystack, $needle) { return str_ends_with($haystack, $needle); } - 2
.claude/hooks/tests/cases.jsonのcases配列へ、次の1件を足してください。
{
"id": "phpver-ng-str-ends-with",
"hook": "check-phpver",
"fixture": "ng_str_ends_with.php.txt",
"input": { "tool_name": "Write", "tool_input": { "file_path": "__FIXTURE__" } },
"expect_exit": 2,
"note": "str_ends_with is 8.0+. On 7.4 it parses but dies at runtime."
}
php -l だけでは素通りします。右の2つが、フックがこれを拾うために持っている判定で、正規表現の列挙が上段に、関数名の一覧が下段に対応します。この1件は、配布済みの phpver-ng-str-contains と同じ側の性質を持ちます。str_ends_with() の呼び出しは構文としては正しいので php -l を通ります。7.4 には関数が無いので、落ちるのは実行時です。構文検査を通り抜けるものがあるので、フック側は関数名の一覧も見ています。正規表現の列挙だけで守ろうとすると、ここが抜けます。
} の後ろにカンマを足し、足した要素の } の後ろには付けません。図の中身は説明用の別のケースなので、貼るのは上のコードブロックの1件です- 3直前の要素の末尾にカンマが入っているかを見てください。JSON の配列は要素の間をカンマで区切り、最後の要素の後ろには付けません。
- 4Claude Desktop の統合ターミナルを開いてください。右上の >_ アイコンか、
Ctrlとバッククォートです。セッションの作業ディレクトリで開くので、移動は要りません。ここでテストランナーを回します。powershell -NoProfile -ExecutionPolicy Bypass -File .claude\hooks\tests\run_cases.ps1
Step 5 MR を出す [3min]
- 1ソース管理ペインで変更されたファイルを確かめてください。
app/libraries/util.phpは Step 3 でswitchに戻った形のはずです。差分にmatchが残っていないことを見たら、util.phpは 変更の破棄 で元に戻します。h()の書き換えは差し戻しを見るための題材なので、コミットには入れません。 - 21つの変更意図ごとに1コミットで、3回に分けます。ソース管理ペインでファイル名の右の + を押して1つだけステージし、メッセージを書いてコミットします。順は、Step 0 の制約節、フックの登録、テストの追加です。
- 33回コミットし終えたら、ソース管理ペインの「変更の同期」(または「ブランチの発行」)を押して push してください。
- 4ブラウザで GitLab のプロジェクトを開き、Merge requests から New merge request を選んでください。ソースに自分のブランチ、ターゲットに
mainを指定します。 - 5MR の Pipelines タブで、パイプラインが動き出したことだけ確認して次へ進みます。結果はプチ演習5 の途中で見に戻ってください。
達成状態
テストランナーの末尾がこう出ていれば、検査側は完成です。配布時点の22件に、プチ演習3 で足した1件と今回の1件が乗ります。プチ演習3 のケースを足していない方は23件です。
PASS phpver-ng-str-ends-with (exit 2) 24 cases: 24 passed, 0 failed
- フックを登録する前は
matchが通り、登録した後は同じ依頼が差し戻される。両方を自分の画面で見た - 差し戻しの後、人が指示を足さずに
switchへ書き直された - ケースを1件足した状態で、テストランナーが24件すべて通る
- MR の
lint-phpverの結果を、プチ演習5 の途中で見に戻って確認した
解説と補足
なぜこれをやるか CLAUDE.md に制約を書いても、数ターン進むと対象外の構文が戻ってきます。CLAUDE.md は文脈なので、会話が伸びるほど制約が薄れます。同じ制約を PostToolUse フックへ移すと、書き換えられた瞬間に差し戻されます。D2-01 の言い方では、[A] だけで守っているものを [M] へ移す作業です。移した後も CLAUDE.md の制約節は消しません。フックは止めるだけで、代わりの書き方までは教えないためです。
設定はどちらから使っても同じ Claude Desktop と CLI は同じ設定ファイルを読みます。.claude/settings.json に登録したフックと CLAUDE.md は、どちらで起動しても同じように効きます。Desktop 用と CLI 用に分けて書く必要はありません。
PostToolUse の動き Edit や Write でファイルを書き換えた直後に自動で走る小さなスクリプトです。終了コード 2 を返すと、標準エラーに書いた文がそのままモデルへ差し戻され、モデルは書き直しを始めます。終了コード 0 なら何も起きません。登録先は .claude/settings.json の "hooks" で、プチ演習3 の PreToolUse と同じ場所に並べます。
decision: block で継続を促します。この演習で使うのは PostToolUse の行ですMac または bash を正にする方 "command" を "bash" に、"args" を ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-phpver.sh"] に差し替えてください。判定の中身は .ps1 と同じです。bash 版は jq を使うので、入っていないと標準エラーに1行出して素通りします。テストランナーも bash .claude/hooks/tests/run_cases.sh で、出力の形は同じです。
見本ファイルに日本語を入れない理由 .ps1 側が CP932 で読み違えて誤検知します。.ps1 と見本ファイルは ASCII だけで書く約束です。
| 症状 | 手当て |
|---|---|
| 登録したのにフックが動かない | settings.json の JSON が壊れていると、ファイルごと黙って無視されます。カンマの付け忘れが大半です。VSCode の赤い波線と、止まるはずの操作が止まるかの2つを見てください。保存前に開いたセッションで試している場合は、Ctrl+N で新しいセッションを開いてから試します。 |
| 編集していないのに反応しない | フックは編集の直後にしか走りません。Edit ツールの表示が出たかを先に見てください。 |
FAIL ... (expected 2, got 0) | 見本に str_ends_with( が入っていないか、コメント行の中にだけ書いています。コメントは検査の前に取り除かれます。 |
fixture not found | ファイル名の綴りが cases.json と合っていません。 |
パイプラインが pending のまま | 全員が同時に push すると順番待ちに入ります。異常ではありません。待ち方は「困ったとき」の pending の項にあります。 |
追加と考察 時間が余った方は、Claude に「sed で同じ行を書き換えてください」と頼んでください。Edit|Write の matcher は Bash 経由のファイル変更を拾わないため、フックは走りません。この抜け道を塞ぐなら、Stop フックで作業ツリーを走査する、FileChanged を使う、CI の lint-phpver に任せる、の3つのどれに置くかを決めて台帳に書いてください。3つ目を選ぶのは手抜きではありません。手元で全部塞ごうとすると、検査の実行時間だけが伸びます。
発展課題 PowerShell 5.1 の既定エンコーディングは CP932 で、.ps1 に非 ASCII を1文字でも書くと実行時のメッセージが壊れます。セッション開始時の sessionstart_verify は非 ASCII のバイト数を数えて報告しますが、報告が出るのは次にセッションを開いたときです。ASCII だけでできているかを検査する処理を、同じ PostToolUse に足してください。対象は .ps1 に限ります。破ると実害が出る契約は、気づける場所ではなく止まる場所に置く、という原則をフック自身へ適用する形です。
影響範囲
| 効く範囲 | Edit と Write でのファイル書き換え。Bash 経由の書き換えは拾いません。最終判定は CI の lint-phpver です |
|---|---|
| 壊れると何が壊れるか | settings.json の JSON が壊れると、フックがまとめて無効になります。画面には何も出ません。.ps1 に非 ASCII が混ざると、誤検知か文字化けが起きます |
| 会話の速度 | 編集のたびに走ります。検査が重いほど、書き換えのたびに待ちが増えます |
自社ハーネスへ持ち帰るときの置き場所は、次の表で決めてください。D2 と smart3pm のどちらを正にするかは D1-12 の台帳に書いた判断に合わせます。
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
check-phpver.ps1 と check-phpver.sh |
.ps1 を正にして hooks 配下へ置き、編集後に走る検査の並びに1本足す |
.sh を正にして hooks 配下へ置く。smart3pm 側は bash を正として運用されていると伺っているため |
| CLAUDE.md の制約節 | 既存の制約節を、代替の書き方の表に差し替える | 同じ文面を規約側へ写す。2つのファイルで文面を割らない |
| 追加したテストケースと見本ファイル | hooks/tests/ の既存ケース集へ追記する |
検査スクリプトと対になるテストを1本追加する。テストの無い検査を増やさない |
プチ演習5 レビュー出力の JSON ゲート
D2-04 / 所要 [15min]
AI のレビューを JSON で受け取り、重大度で合否を返すスクリプトに流して、止める線を決めます。
P0 と P1 が1件でもあれば exit 1 を返す判定スクリプト
目的
レビューを読んで判断していた部分を、数えるだけの作業に変えられるようになります。止める線を自分で決め、手元と CI で同じ判定になる状態にします。
準備
| 開いておく画面 | Claude Desktop(プロジェクトは dl-training-app)、その統合ターミナル、VSCode、配布物一式を展開したフォルダ(Windows のエクスプローラー) |
|---|---|
| いるファイル | ci/gate.ps1(bash は ci/gate.sh)、.claude/review.schema.json、配布物の 20_Day2の演習/プチ演習5_レビュー出力のJSONゲート/findings_sample.json |
| ブランチ | プチ演習4 の ex/p4-<ユーザー名> のまま進めます。新しく切りません |
| 統合ターミナルの開き方 | 右上の >_ アイコン、または Ctrl とバッククォート。セッションの作業ディレクトリで開くので、移動は要りません。判定スクリプトはここで回します |
API キーは使いません。判定スクリプトに流す入力は、配布の見本ファイルと、Claude Desktop に書き出させたレビューの2つです。
自分で考える [3min] スクリプトを触る前に、メモへ3つ書き出してください。
- 1自社の MR を止めてよい指摘はどれかを、重大度の言葉で決めてください。
.claude/review.schema.jsonでは、P0 が動かないか壊す、P1 が実環境で落ちるか外部入力が素通し、P2 が指示との不一致と波及漏れ、P3 がそれ以外です。 - 2判定できない入力(重大度が知らない値、
findingsが無い)を、通すか落とすかを決めてください。 - 3止める線を1段下げたとき、1日あたり何件の MR が止まると困るかを見積もってください。
手順
Step 1 見本ファイルを置く [2min]
- 1Windows のエクスプローラーで、配布物一式の
20_Day2の演習/プチ演習5_レビュー出力のJSONゲートを開いてください。 - 2
findings_sample.jsonを、VSCode のエクスプローラーのdl-training-appのいちばん上(ルート)へドラッグしてコピーしてください。 - 3VSCode でそのファイルを選んで
F2を押し、名前をfindings.jsonに変えてください。中身は P0 1件、P1 1件、P2 2件で、最後の1件のconfidenceが 0.5 です。
Step 2 既定の閾値で回す [3min]
- 1統合ターミナルで、次の2行を上から順に打ってください。既定は
findings.jsonと閾値 P1 です。powershell -NoProfile -ExecutionPolicy Bypass -File ci\gate.ps1 $LASTEXITCODE
- 21行目を
Enterで実行してから、2行目を打ってください。2つを1行につなげると、$LASTEXITCODEが判定スクリプトの引数として渡り、1という名前のファイルを探しにいきます。 - 31行目の出力の先頭が
gate: threshold=P1 total=4 blocking=2、2行目の終了コードが1であることを確認してください。CI はこの数字だけを見て、0 なら通し、それ以外なら落とします。
Step 3 閾値を1段下げる [2min]
- 1閾値を P2 にして、同じ
findings.jsonで回してください。powershell -NoProfile -ExecutionPolicy Bypass -File ci\gate.ps1 -Threshold P2
- 2
blockingが 2 から 4 に増えることを確認し、増えた2件を読んでください。 - 3増えた指摘のうち、自社なら本当に MR を止めるものが何件あったかを数え、台帳に書いてください。
Step 4 判定できない入力を流す [2min]
- 1VSCode で
findings.jsonの P2 の1件の"severity": "P2"を"severity": "high"に書き換えて保存し、Step 2 の2行をもう一度打ってください。blockingが 3 に増えます。知らない重大度は落とす側に数えるためです。 - 2書き換えを
"P2"に戻してください。
gate.ps1 と gate.sh は、知らない severity を落とす側に寄せ、findings の配列が無い入力は判定せずに終了コード 1 で落とします。判定できないものを通す作りにすると、ゲートが静かに無効になります。Step 5 自分の差分のレビューを流す [3min]
- 1Claude Desktop の入力欄に次をそのまま送ってください。書き出しまで1分前後かかります。
main との差分をレビューしてください。判定の基準は REVIEW.md、出力の形は .claude/review.schema.json に従い、結果をリポジトリのルートに findings-mine.json として書き出してください。説明は本文に書かず、ファイルだけを作ってください。
- 2Write ツールの確認が出たら承認してください。
- 3統合ターミナルで、書き出したファイルを判定にかけてください。
powershell -NoProfile -ExecutionPolicy Bypass -File ci\gate.ps1 -Path findings-mine.json $LASTEXITCODE
- 4キーの欠けや形の崩れがあれば、判定スクリプトが終了コード 1 で落とします。落ちた場合は、標準エラーの1行で何が足りなかったかを読んでください。
findings.jsonとfindings-mine.jsonはコミットしません。
達成状態
見本ファイルを閾値 P1 で回すと、1行目がこう出て終了コードが 1 になります。続く2行は、止めた P0 と P1 の指摘です。
gate: threshold=P1 total=4 blocking=2
閾値 P2 では gate: threshold=P2 total=4 blocking=4 になり、終了コードは同じく 1 です。止めたときの最後の行は gate: blocked. fix the findings above or lower the threshold on purpose. です。.ps1 は ASCII だけで書く決まりなので、スクリプトが出す文が英語なのは正常です。
gate.sh を流して撮ったもので、1行目の件数と終了コードは gate.ps1 と同じです。最後の行だけ、bash 版は日本語で出ます- 見本ファイルで、閾値 P1 と P2 の
blockingが 2 と 4 に変わることを自分の画面で見た - 知らない重大度を入れると、落とす側に数えられることを確かめた
- Claude Desktop に書き出させた自分のレビューを、同じ判定スクリプトに流した
- 止める線を自分の言葉で説明できる
解説と補足
なぜこれをやるか AI のレビューを文章で受け取っている限り、通すか止めるかは毎回人が読んで決めます。出力を JSON に固定すると、そこから先は数えるだけの作業になります。同じ判定が D2-05 で CI のゲートジョブ ai_gate になります。
判定の置き場所 3本ありますが、考え方は1つです。ci/ai_gate.sh が CI 側、ci/gate.ps1 が手元の PowerShell 版、ci/gate.sh がその bash 版です。3本とも、入力が無い・壊れている・findings の配列が無い・知らない重大度、のどれでも落とします。
findings.json で、gate.ps1 が手元の PowerShell、gate.sh が手元の bash と Mac、ci/ai_gate.sh が CI の runner で動きます。gate.ps1 と gate.sh は閾値を引数で受け、ai_gate.sh は環境変数 GATE_LEVEL(既定 P1)と GATE_MIN_CONFIDENCE(既定 0.0)で受けます。どれも exit 0 で通し、exit 1 で止めます受け取る JSON の形 必須キーは severity、confidence、file、line、message、category の6つで、それを findings の配列に入れます。Step 5 で指摘が0件だった方は、判定を確かめるために手で1件書いてください。
{
"summary": "設定ファイルとフックの登録を中心に4点を確認し、1件指摘しました。",
"findings": [
{
"severity": "P1",
"confidence": 0.9,
"file": "app/libraries/util.php",
"line": 18,
"message": "h() の引数が未定義のときの扱いが変わっています。呼び出し側のビューで未定義キーが通るようになるため、影響範囲を確認してください。",
"category": "spec-mismatch"
}
]
}
bash 側で回す場合 bash ci/gate.sh findings.json のあと echo $? で同じ数字が見えます。閾値は引数の位置で bash ci/gate.sh findings.json P2 です。jq が要ります。
| 症状 | 手当て |
|---|---|
gate: cannot parse JSON で落ちる | ファイルを PowerShell の > で書き出しています。PowerShell 5.1 の > は UTF-16 で書き出すので、Out-File -Encoding utf8 で作り直してください。 |
gate: no findings array in ... で落ちる | JSON の一番外側に findings の配列がありません。Step 5 の書き出しでキー名が変わったか、別の形で包まれています。 |
引数 'findings.json' を受け入れる位置指定パラメーターが見つかりません | 2つのコマンドが1行につながっています。別の行に分けます。 |
gate: findings file not found: 1 | $LASTEXITCODE を同じ行に続けています。次の行で単独で打ちます。 |
gate: findings file not found: findings.json | 見本ファイルがまだ findings_sample.json の名前のままか、ルート以外のフォルダに置かれています。Step 1 の2と3をやり直してください。 |
| 引用符が全角になっている | チャットやメールを経由すると自動で変換されることがあります。貼り付け後に半角か確かめます。 |
この時点の REVIEW.md は4つの見出しだけで中身が空です。中身を書くのはハンズオン2 の Step 1 です。ここでは基準のファイルを渡す形だけを作り、判定の中身はスキーマの重大度の説明に任せます。
研修後に自分の API キーで CLI から試す場合
研修では API キーを配らないので、当日はこの経路を使いません。スキーマを守らせること自体をツール側に任せたいときの形です。--bare を付けると ANTHROPIC_API_KEY が要ります。
Windows PowerShell 5.1 では、そのままでは2か所で壊れます。1つは文字コードで、git diff main からパイプで渡す差分の日本語が ? になります。先頭の1行で UTF-8 に揃えます。もう1つは引数の " で、5.1 はネイティブコマンドへ渡す引数の中の " をそのまま渡さないので、REVIEW.md やスキーマの JSON を文字列で引数に入れると途中で割れます。
$OutputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false git diff main | claude --bare -p "この差分をレビューし、指摘をスキーマの形で返してください。" --output-format json --json-schema ((Get-Content .claude\review.schema.json -Raw -Encoding UTF8) -replace '"','\"') | ConvertFrom-Json | Select-Object -ExpandProperty structured_output | ConvertTo-Json -Depth 10 | Out-File -Encoding utf8 findings-cli.json
--output-format json の出力は外側に包みがあり、指摘は structured_output の中に入ります。包みのまま判定スクリプトに渡すと、findings の配列が無いので落ちます。
追加と考察 同じ差分を、履歴を切った新しいセッションでもう一度レビューさせてください。読むのは1点だけです。同じ差分で重大度が揺れていないかどうか。揺れているなら、揃えるのは基準の文面です。D2-05 の前半で Important の定義を書くときに、ここが1つ決まります。
発展課題 2つあります。1つ目は確信度の下限です。いまの gate.ps1 は confidence を見ていません。param に [double]$MinConfidence = 0.0 のような引数を足し、重大度を数える前に下限に満たない指摘を除き、除いた件数を1行出す形にしてください。見本ファイルで下限を 0.8 にすると、確信度 0.5 の1件が外れます。CI の ai_gate.sh は GATE_MIN_CONFIDENCE で同じことをしています。2つ目は、閾値に届かなかった指摘の残し方です。P2 以下を MR のコメント1本にまとめて残す処理を考えてください。指摘のたびにコメントを増やすと、小さな改修ほど読む量が増えるという、いま解こうとしている課題そのものを再生産します。
影響範囲
| 効く範囲 | MR を出す前の自己チェック。同じ判定がハンズオン2 の CI ゲート ci/ai_gate.sh になります |
|---|---|
| 壊れると何が壊れるか | 手元と CI で閾値が食い違うと、手元で通ったものが CI で落ちます。未知の severity を通す作りに変えると、ゲートが静かに無効になります |
| 文字コード | PowerShell 5.1 の > は UTF-16 で書き出すため、判定スクリプトが読めません |
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
gate.ps1 |
PowerShell 版を正にする。編集後に自動で走らせず、MR を出す前に人が手で叩く位置へ置く | gate.sh を正にする。同じ引数の並びを保ち、2言語で判定を割らない |
review.schema.json |
レビューの出力形として1か所に置き、手元と CI の両方から同じファイルを参照する | 同じファイルを共有する。カテゴリの語彙を増やすときは、指摘の集計側と一緒に直す |
| 閾値と確信度の下限を決めた記録 | 持ち帰り台帳に、選んだ値とその理由を1行で残す | 同じ値を使う。2つのハーネスで線を変えると、比較する数字が揃わなくなる |
ハンズオン2 レビュー観点の切り分けと CI ゲート化
出てきた指摘を、人が読むものと機械が止めるものに振り分けられるようになります。
指摘の絞り込みと、機械判定への移し替え
Day1 では、同じ変更を3通りで読ませると指摘の中身が変わることを見ていただきました。ここではその先で、指摘の置き場所を変えます。機械で判定できるものは CI へ、人の判断が要るものだけを AI の出力に残します。終わると、レビュー基準の実物(REVIEW.md)、観点を分けた3本のレビュアー、P0/P1 で合流を止める CI ゲートが、同じ1本の MR の上でつながります。
終わったかどうかの判定
REVIEW.mdの4つの見出しが埋まり、.claude/known_issues.mdに3件ある- 3本のレビュアーの findings が別々に残り、検出率とノイズ率の数字が出ている
- 足したテストが、実装前のパイプラインで赤だった履歴が MR に残っている
ai_gateが1回赤になり、その出力と指摘ごとの採否と理由が MR に書いてある- 最後の push で走った2本のパイプラインが緑になっている。ブランチ用(lint と test の4ジョブ)と MR 用(
ai_reviewとai_gate)の2本です
始める前にそろっている状態
| 開いておく画面 | Claude Desktop、VSCode、ブラウザで GitLab の演習プロジェクト |
|---|---|
| いるファイル | 手元にクローン済みの dl-training-app。REVIEW.md、.claude/agents/ の3本、.gitlab-ci.yml |
| 直前の演習の成果物 | プチ演習4・5 の変更が ex/p4-<ユーザー名> にコミット済みであること。Day1 の宿題の分類表があれば手元に出しておく |
| ブランチ | ex/p4-<ユーザー名> から ex/h2-<ユーザー名> を切ります。Step 1 の手順1 で作ります |
| 紙かメモ帳 | 「自分で考える」で4つの欄を書きます |
この演習で触るもの
| 触るファイル | REVIEW.md、.claude/known_issues.md、.claude/agents/ の3本、.claude/skills/review-route/SKILL.md、.gitlab-ci.yml(ai_gate の allow_failure と GATE_LEVEL)、tests/phpunit/EstimateSearchTest.php、app/controllers/estimate_ctl.php |
|---|---|
| 操作 | レビュー基準を人が先に書き、観点別レビュアー3本を今の main に並列で当てて検出率を出す。ゲートを有効にし、テストを先に送って赤を見てから実装し、落ちた指摘を仕分けて緑まで回す |
| 見る場所 | Claude Desktop のバックグラウンドタスクの3本、findings の file:line の重なり、GitLab の Pipelines タブ、ai_gate のジョブログ、計測表_Day1_Day2.md |
| 達成の状態 | ゲートで1回落ちて直した記録が MR に残り、最後の push のブランチ用と MR 用の2本のパイプラインが緑。検出率とノイズ率が計測表に入っている |
| 自社での使いどころ | 自社のレビュー基準の初版と、機械で判定できる指摘を CI へ移す型。既知の許容の台帳 |
| 影響が及ぶ範囲 | allow_failure を外すと、ゲートの赤で MR 用のパイプラインが赤になる。GATE_LEVEL は仮に P2 にしてから P1 に戻す。Do not report が広すぎるとゲートが空振りする |
Issue #2 と Issue #3
| Issue | 内容 | この演習での使い方 |
|---|---|---|
| #2 | 在庫の引当で半端な状態が残る(app/controllers/stock_ctl.php の assign()) |
Step 2 で3本のレビュアーを当てる対象。実装はしません |
| #3 | 見積一覧の期間検索と CSV 出力(app/controllers/estimate_ctl.php の index()) |
Step 2 のレビュー対象。Step 3 で受入条件1と3、受入条件2 の前半を実装して MR を通します |
EstimateCsvTest.php の3本)は、この80分には含めていません。発展課題に回しています。Step の構成と提出先
| Step | すること | 時間 |
|---|---|---|
| 自分で考える | レビュー基準に入れる観点と、止める重大度を紙に書く | [5min] |
| Step 1 | レビュー基準を人が先に決める。REVIEW.md と既知の許容の台帳を書く | [10min] |
| Step 2 | 観点を分けた3本のレビュアーを、今のコードに並列で当てる | [30min] |
| Step 3 | CI ゲートを有効にし、テストを1本足して MR を緑まで回す | [28min] |
| Step 3 補足 | 同じリファクタで、テストが無い場合と有る場合の差を測る | [7min] |
| 合計 | [80min] |
提出先は GitLab の Merge Request です。ブランチ名は共通の進め方 STEP 2「ブランチ名の形」に合わせて ex/h2-<ユーザー名> にし、Step 1 から Step 3 までの成果物を同じブランチに載せて1本の MR で出します。
ブランチと MR の扱いの補足
ex/h2-<ユーザー名> はプチ演習4・5 のブランチ ex/p4-<ユーザー名> から切るので、Day1 とプチ演習4・5 で足した deny とフックがそのまま乗ります。この後のプチ演習6 は、新しく切らずにこの ex/h2- のまま進めます。main から切ると、それが全部消えます。プチ演習の変更が残っている場合は、先に ex/p4-<ユーザー名> 側でコミットしてから切ってください。ブランチの名前は共通の進め方 STEP 2、コミットと同期はSTEP 3、MR の出し方とパイプラインの見方はSTEP 4 にまとめてあります。
人が先に決めること
紙かメモに書き出してください。ここを AI に相談してから始めると、出てきた案を評価する基準が手元に残りません。
- 1機械判定できる観点を3つ挙げます。CI の
lint-phpdevとlint-phpverが既に見ているものは、AI に重ねて言わせる必要がありません。 - 2直近の改修で見送った指摘を3件思い出し、見送った理由を1行ずつ書きます。
- 3「機能テストの無い変更は Important として報告する」を自社の基準に入れるかどうかを決めます。
- 4合流を止める重大度の下限を決めます。P0 だけか、P1 までか。Step 3 の
GATE_LEVELがこの値になります。
手元に材料が無いときの探し方
(1)で Day1 の宿題が手元に無い方は、演習リポジトリの CLAUDE.md の「実行環境の制約」の表と「守ること」の節を開き、そこから機械で検査できそうなものを3つ選んでください。(2)は、同じ判断を毎回繰り返しているものが台帳に載せる候補です。(3)は入れると指摘が増えます。増えた分を誰が引き受けるかまで含めて決めてください。
4つとも正解はありません。この演習では、決めた内容そのものより「決めたものが MR で機械に効いているか」を見ます。
ハンズオン2 Step 1 レビュー基準の起案
REVIEW.md と既知の許容の台帳
演習リポジトリの REVIEW.md は、4つの見出しだけがあって中身が空です。ここが AI レビューの正になります。
- 1VSCode の画面左下の表示が
ex/p4-<ユーザー名>で、ソース管理ペインに未コミットの変更が残っていないことを確認します。違うブランチなら、ブランチ名を押して一覧からex/p4-<ユーザー名>を選びます。そのあとブランチ名を押し、新しいブランチの作成 でex/h2-<ユーザー名>を作ります。 - 2VSCode で
REVIEW.mdを開きます。埋める順番は Do not report、Nit の上限、Important の定義、Always check です。 - 3Do not report に、自分で考える(1)で挙げた3つを移します。CI の lint が見ている範囲は、ここに入れます。
- 4Nit の上限 に件数を数字で書きます。「5件までを1つのコメントに」のように、まとめ方も書きます。
- 5Important の定義 を3件から5件に絞ります。
lint-phpverが見ている範囲(PHP 7.4 に無い構文と関数)は手順3で Do not report に入れたので、ここには入れません。書き出しに迷ったら、下の折りたたみ「Important の出発点の例」を開いてください。 - 6Always check に、差分に出ていなくても毎回見させることを書きます。自分で考える(3)の判断をここに入れます。
- 7
.claude/known_issues.mdを新規に作り、自分で考える(2)の3件をK-001から番号を振って書きます。1件は「場所」「指摘の内容」「見送った理由」の3行です。 - 8
REVIEW.mdの Do not report から、この台帳を1行で参照します。 - 9VSCode の ソース管理 ペイン(左サイドバーの枝分かれアイコン)を開き、
REVIEW.mdと.claude/known_issues.mdの2つだけをステージに上げます。メッセージ欄に「handson2: レビュー基準の初版と既知の許容の台帳」と書いて コミット を押します。
ci/ai_review.sh が読むのは REVIEW.md の1本だけです(スクリプト内の RULES_FILE)。手元のサブエージェントは .claude/known_issues.md を Read で開けますが、CI は開きません。K-00x を CI にも効かせるなら、要約を REVIEW.md に書き写すか、RULES_FILE を2本の連結に変えるかのどちらかです。選んだほうと理由を MR の説明に1行書いてください。
- ソース管理ペインの差分で、2ファイルだけがコミットに入っている
- REVIEW.md の4つの見出しが全部埋まっていて、Important が5件以内に収まっている
- .claude/known_issues.md に K-001 から3件ある
- K-00x を CI に効かせる方法を1つ選び、MR の説明に書く内容が決まっている
Important の出発点の例
自分で書いてから開いてください。この題材で出発点になるのは次の3つです。
- 外部入力(
$_GET/$_POST)が、プレースホルダを通らずに SQL の条件へ入っている - 複数の表を続けて書き換える処理が、トランザクションと行ロックの外にある
- 画面へ出す値が
h()を通っていない
解説と補足 対象バージョンの書き方
このプロジェクトの対象は PHP 7.4 です。AI は素で 8.x の構文(match 式、enum、readonly、名前付き引数、コンストラクタプロモーション、str_contains / str_starts_with、?->)を書きたがるので、対象の版を設定に書き、それを超える構文を機械で止める形にします。
構文と関数は別の段で止まります。str_contains() の呼び出しは構文としては正しいので php -l を通ります。そのため CI の lint-phpver は2段で見ています。1段目の php -l が match 式などの構文を、2段目の php ci/undefined_functions.php が 7.4 の実行環境に関数があるかを function_exists() で確かめ、str_contains() はこの2段目で落ちます。機械が止めている範囲なので、AI のレビューに重ねて言わせる必要はありません。Important の定義 ではなく Do not report に入れる理由です。
リプレイスの予定があるときは、移行先の版を対象に書きます。現行が古くても、移行先の版を CLAUDE.md と REVIEW.md に書いておけば、これから書かれるコードが移行先で動く形になります。現行の版を対象に書き続けると、移行のときに書き直す量が増えます。
Important に何を入れるか決まりません
Issue #2 と #3 の「現状」節を読み直してください。そこに書かれている事実は、既に分かっている欠陥です。それを検知できる書き方になっているかどうかで、Important の粒度を判定できます。「SQL の書き方に注意する」では検知できません。「外部入力が db_select の第1引数に文字列連結で入る変更」であれば検知できます。
それでも迷う場合は、AGENTS.md の Code Review Rules にある「データ境界」「互換性」「整合性」の3つを Important の骨にして、自社の言葉で書き直してください。
REVIEW.md はこの後のレビュアー3本と CI の ai_review の両方が読むので、ここを緩めると Step 3 のゲートが空振りします。自社に写すときはリポジトリ直下に置き、レビュー用のエージェント定義と CI のスクリプトの両方から参照先として指定します。ハンズオン2 Step 2 観点別レビュアーの並列適用
3本の重なりと差
当てる先は、実装後の差分ではなく今の main のコードです。Issue #2 と #3 の欠陥が入ったままの状態に当てるので、既に分かっている欠陥をどれだけ拾うかが、そのまま数字で出ます。
REVIEW.md を観点の正として、security、spec、test-gap の3本が同じコードを並列で読み、findings-security.json、findings-spec.json、findings-testgap.json が残ります。それを file:line で突き合わせて検出率とノイズ率を出します。下段が Step 3 です- 1VSCode で
.claude/agents/の3本を開きます。security-reviewer.md、spec-reviewer.md、test-gap-reviewer.mdです。frontmatter のmodelとeffortが3本とも違うことを確認してください。 - 23本の「見るもの」節を、Step 1 の
REVIEW.mdに合わせて直します。Do not report に入れたものが「見るもの」に残っていたら消します。 - 3手順2 を保存し、Claude Desktop でこのプロジェクトを開きます。画面左上のプロジェクト名から演習フォルダを選びます。
- 4Claude Desktop の入力欄に、次をそのまま貼って送信します。
security-reviewer と spec-reviewer と test-gap-reviewer の3つを並列で起動してください。 対象は app/controllers/stock_ctl.php の assign() と、 app/controllers/estimate_ctl.php の index() です。 関連する Issue は _issues/issue-2.md と _issues/issue-3.md にあります。 3本の findings を、レビュアーごとに分けてそのまま出してください。 要約もまとめもしないでください。 出し終えたら、3本の結果をそれぞれ findings-security.json、findings-spec.json、 findings-testgap.json という名前でリポジトリのルートに保存してください。
- 5動いている間に、右上の ⋮ メニューの バックグラウンドタスク を開きます。3本が同時に動いていることと、モデル名が3本とも違うことを確認します。終わるまで数分かかります。
- 63本の結果が返ったら、VSCode で3つの JSON を開き、
fileとlineで突き合わせます。同じ場所を何本が指摘したかを数えます。この3ファイルは Step 3 の手元判定でも使います。コミットには入れません。 - 7検出率とノイズ率を出します。検出率は、Issue #2・#3 の「現状」節に挙がっている欠陥の件数を分母、当てた件数を分子にします。欠陥として数えるのは、直さないと誤動作するものです。「条件の組み立てが
index()の中にしかない」「CSV の入口がまだ無い」のような設計と未実装の話は数えません。ノイズ率は、指摘の総数を分母、仕込み欠陥以外の件数を分子にします。何を分母に入れたかも記録に残してください。 - 8VSCode で
.claude/skills/review-route/SKILL.mdを新規に作り(フォルダも新規)、変更規模でレビュアーを選ぶ手順を書きます。50行未満はreview-light、50行以上または DB・権限・外部連携に触る変更はreview-full、観点を分けたいときは3本を並列、の3分岐です。先頭に次の frontmatter を置き、その下に本文を書きます。--- name: review-route description: 変更の規模でレビュアーを選びます。 ---
- 9ソース管理ペインで
.claude/agentsと.claude/skills/review-routeをステージに上げ、「handson2: 観点別レビュアーと振り分けの skill」でコミットします。findings-*.jsonは手元の記録なのでステージに上げません。
- 3本の findings が、レビュアーごとに分かれた形で手元にある
- 同じ file:line を何本が指摘したかを数えた表がある
- 検出率とノイズ率の分母と分子の数字が出ている
- .claude/skills/review-route/SKILL.md がコミットに入っている
解説と補足 サブエージェントと返ってくる形
ここで使うサブエージェントは、.claude/agents/ の定義ファイル1本につき1つ立ち上がる、別の Claude です。先頭の frontmatter(--- で囲まれた部分)でモデルと使えるツールが決まり、本文がその Claude への指示になります。呼び出した側の会話の履歴は見えず、渡された範囲だけを読んで結果を返します。3本を1つの依頼で指名すると同時に走るので、これを並列と呼んでいます。
返ってくる形は .claude/review.schema.json のとおりです。レビュアー1本ぶんは、こう見えていれば正しく動いています。
{
"summary": "外部入力の経路を 6 箇所追い、到達するものが 3 件ありました",
"findings": [
{
"severity": "P0",
"confidence": 0.9,
"file": "app/controllers/estimate_ctl.php",
"line": 23,
"category": "sql-injection",
"message": "date_from を SQL へ文字列連結しています。db_select の第2引数にプレースホルダで渡してください"
}
]
}
前置きや後書きが付いている、category が全部 other になっている、といった場合は frontmatter ではなく本文の「返す形」節を読み直してください。手順2で消しすぎた可能性があります。
手順8 で書いた振り分けは、AI が読めば従いますが、読まなければ従いません。守らせたいのであれば Step 3 の CI 側に置いてください。手元の設定でできるのは速報までで、合流を止められるのは CI だけです。行数を git diff --stat main の末尾の数字で数える場合は、ここはコマンドを使います。Claude Desktop の中のターミナルで打ってください。
3本が並列にならず、1本ずつ順番に動きます
1つのプロンプトで3本を指名しているかを確認してください。3回に分けて頼むと直列になります。それでも直列になる場合は、順番に動いても演習は成立します。所要時間の記録だけ「直列」と書き添えて先へ進んでください。
3本とも同じ指摘しか出しません
「見るもの」節が3本とも似た内容になっている可能性があります。security-reviewer は外部入力の経路だけ、spec-reviewer は Issue と差分の一致だけ、test-gap-reviewer はテストの有無だけを見ます。「他の観点は書きません」の1行が消えていないかを確認してください。
重なること自体は失敗ではありません。3本が同じ P0 を出したのであれば、それはその欠陥の確度が高いという情報です。重なりの数も記録に残してください。
agents/ に3本を置き、どの変更でどれを呼ぶかを規約側に1行足します。3本とも常に呼ぶ運用にすると、レビュー待ちの時間が3倍になります。ハンズオン2 Step 3 CI ゲートの有効化とテスト生成
テスト先出しとゲートの順番
順番を1つだけ入れ替えます。実装より先にテストを送って、赤くなるところを見ます。赤くならないテストは、通っても何も保証しません。CI の履歴に赤が残っていることが、そのテストが今のコードを見ていた証拠になります。
ai_review と落とす役の ai_gate が別のジョブなので、MR にコメントが付いただけでは Merge はできる状態のままです。下の破線が手順2で消す行にあたり、これが残っていると ai_gate が赤でもパイプラインは緑のまま通ります。- 1VSCode の画面左下で、ブランチが Step 1 で切った
ex/h2-<ユーザー名>のままであることを確認します。 - 2VSCode で
.gitlab-ci.ymlを開き、ai_gate:で始まるブロック(# --- gate ---の見出しの下)の最後の行allow_failure: trueを1行まるごと消します。他の行のインデントは変えません。 - 3同じファイルの先頭近く、
variables:の下にあるGATE_LEVEL: "P1"を、一度だけGATE_LEVEL: "P2"にします。ゲートが本当に止めることを1回見るための仮の値で、手順11 で戻します。
variables、下のブロックが # --- gate --- の下の ai_gate ジョブです。縦の薄線が空白2個ぶんの幅で、キー名の前の空白の数で階層が決まります。行を消すときに上の行の空白まで巻き込むと、他のジョブごと止まります- 4Issue #3 の受入条件3を読み、
date_fromの値が条件として使われないとき一覧に何件出るかを、先に紙に書きます。db/seed.sqlの見積は15件です。 - 5Claude Desktop にテストを1本頼みます。
tests/phpunit/EstimateSearchTest.php に1本足してください。 date_from に 2026-09-02' AND '1'='1 を入れたとき、一覧が15件(全件)出ることを確認します。 修正前のコードでは3件になって落ちます。 既存の5本と同じ書き方にそろえてください(capture_view と count_list_rows を使う、$_GET は setUp で初期化済み)。 実装側のファイルは、この手順では変更しないでください。
- 6ソース管理ペインで、
.gitlab-ci.ymlだけをステージして1コミットにします。Step 2 の成果物は Step 2 の手順9 でコミット済みです。.gitlab-ci.ymlを入れ忘れると CI 側はallow_failure: trueのままで、ゲートは一度も落ちません。メッセージは「handson2: ai_gate の allow_failure を外し GATE_LEVEL を仮に P2 にする」です。 - 7テストだけをさらに別のコミットにして、ソース管理ペインの 変更の同期 を押します。メッセージは「handson2: 期間検索が条件として使われないことのテストを追加」です。
- 8ブラウザで GitLab の演習プロジェクトを開き、画面上部に出る Create merge request を押します。出ていない場合は左のメニューの Code から Merge requests を開き、New merge request で元を自分のブランチ、先を
mainにして作ります。 - 9MR の Pipelines タブを開きます。branch のラベルが付いた行で
test-phpunitが赤になっていることを確認します。赤いジョブ名をクリックするとジョブの画面が開くので、落ちた出力を MR の説明の「テスト」欄に貼ってください。 - 10Issue #3 の受入条件1と、受入条件2 のうち private メソッドの切り出しまでを実装します。そのメソッドが返す2つの値(WHERE 句の文字列とパラメータ配列)の形を、先にご自身で紙に書いてから Claude Desktop に渡してください。依頼文に入れるのは、対象ファイル(
app/controllers/estimate_ctl.phpだけ)、Issue #3 の「期待する振る舞い」の 1 から 3 をそのまま、紙に書いたメソッドの戻り値の形、テストファイルは触らないこと、の4点です。 - 11ソース管理ペインでコミットして 変更の同期 を押します。branch の行で
test-phpunitが緑になり、merge request の行でai_reviewが MR にコメントを1本置き、ai_gateが赤になります。緑のままなら下の「ai_gate が緑のままで、一度も落ちません」を開いてください。
ai_review_bot のコメントです。1行目に使ったモデル、レビューの深さ、読んだ差分の行数が出ます。その下が P0 から P3 の件数で、続けて指摘が [P1] ファイル:行 (カテゴリ / 確信度) の形で並びます。差分が AI_REVIEW_DIFF_THRESHOLD(既定 80)を下回ると軽いモデルに切り替わり、1行目のモデル名が変わります。判定はこのコメントではなく ai_gate ジョブが行います- 12落ちた指摘を1件ずつ、即修正・要調査・将来課題・意図した設計の4つに仕分け、MR の説明の「人が判断した採否」の表に書きます。直すものを直し、「意図した設計」にしたものは Do not report に入れるかどうかを検討します。
- 13
.gitlab-ci.ymlのGATE_LEVELを"P1"に戻し、修正と合わせてコミットして同期します。この push で2本のパイプラインが走ります。branch の行のブランチ用(lint-phpdev、lint-phpver、test、test-phpunit)と、merge request の行の MR 用(ai_reviewとai_gate)の両方が緑になったことを確認します。
REVIEW.md の Do not report か .claude/known_issues.md に移す合図です。
test-phpunit だけが赤になります。この赤が、足したテストが今のコードを見ていた証拠です
ai_gate のジョブログです。ai_review は緑のまま、ai_gate がしきい値以上の指摘で止まっています。列挙する役と落とす役が分かれているためです。画面は allow_failure を外す前なのでオレンジの ! ですが、手順2 のあとは赤の × になり、MR 用のパイプライン全体が赤になります。Merge ボタンまで押せなくなるのは Pipelines must succeed を有効にしたプロジェクトで、演習のプロジェクトでは押せますが押しません- 実装前のパイプラインで test-phpunit が赤だった履歴が MR に残っている
- ai_gate が1回赤になり、その出力が MR の説明に貼ってある
- 指摘ごとの採否と理由が MR の表に書いてある
- 最後の push で走ったブランチ用と MR 用の2本のパイプラインが緑になっている
解説と補足 ジョブのログの読み方
手順9でジョブのログを開くと、こう見えます。件数は環境によって変わります。
1) EstimateSearchTest::testIgnoresMalformedDateFrom Failed asserting that 3 matches expected 15. /builds/dlive-training/dl-training-app/tests/phpunit/EstimateSearchTest.php:81 FAILURES! Tests: 6, Assertions: 9, Failures: 1.
手順11 で ai_gate が落ちたときの出力です。MR テンプレートの「AI レビュー結果」欄のコードブロックに、この出力をそのまま貼ってください。
しきい値 P2 以上、確信度 0.0 以上の指摘を数えます。 confidence の無い指摘は 1.0、P0〜P3 以外の severity は P0 として数えます。 P1: 1 件 / P2: 3 件 / P3: 2 件 ゲートで止めた指摘 4 件 [P1] app/controllers/estimate_ctl.php:36 date_to が条件に使われず、範囲の上限が効きません [P2] app/controllers/estimate_ctl.php:41 条件の組み立てが index() の中にしかありません ... 直すか、直さない理由をMRのコメントに書いてから再実行してください。 ERROR: Job failed: exit code 1
ai_gate が赤になるのは、CI に ANTHROPIC_API_KEY が入っていて、しきい値以上の指摘が1件でも出たときです。
手元で先に同じ判定をかける方法
ここはコマンドを使います。判定スクリプトは画面から実行できないので、Claude Desktop の中のターミナルから実行します。Step 2 で作った findings をそのまま渡せます。
powershell -NoProfile -ExecutionPolicy Bypass -File ci\gate.ps1 -Path findings-security.json -Threshold P2
bash 環境では bash ci/gate.sh findings-security.json です。判定の考え方は CI の ci/ai_gate.sh と同じなので、手元で落ちたものは CI でも落ちます。出力の書式だけが違います。
runner から外部の API へ出られない場合も、この形で進めます。(1) 手元で findings を JSON に落とす (2) 上のコマンドを -Threshold P2 で実行する(bash 環境では GATE_LEVEL=P2 bash ci/ai_gate.sh findings.json。ai_gate.sh は閾値を引数ではなく環境変数で受けます) (3) 出力を MR の「AI レビュー結果」欄に貼る。ゲートで1回落ちた記録が MR に残れば合格です。ai_review と ai_gate の実走は講師環境で行います。
ai_review ジョブが動きません
このジョブは merge_request_event のときだけ動きます。ブランチを送っただけでは走りません。MR を作ってから、もう一度送ってください。MR があれば、Pipelines タブに merge request のラベルが付いた行が増え、そこに ai_review と ai_gate が並びます。
ジョブは緑なのにコメントが付かない場合は、ジョブのログを開いてください。[ai_review] CI/CD変数 ANTHROPIC_API_KEY が未設定のため、レビューを実行せずに終了します と出ていれば変数の問題です。このときジョブは赤にならず、findings.json は指摘 0 件で残り、ai_gate も緑になります。講師までお知らせください。受講者側の設定では直せません。
ai_gate が緑のままで、一度も落ちません
先に、.gitlab-ci.yml の変更がブランチに載っているかを見てください。GitLab の MR の Changes タブに .gitlab-ci.yml が出ていなければ、手順6 のコミットが抜けています。次に allow_failure: true の行が残っていないかを確認してください。残っていると、ジョブは赤くなりますがパイプラインは緑のまま通り、ジョブ名の横に Allowed to fail と出ます。
+ の行が追加、- の行が削除です。ここに .gitlab-ci.yml が無ければ、手順6 のコミットがブランチに載っていませんai_review のログに ANTHROPIC_API_KEY が未設定 と出ている場合は、指摘が 0 件なので落ちません。その間は、上の「手元で先に同じ判定をかける方法」で進めてください。
行を消しても落ちない場合は、GATE_LEVEL を "P2" にして回し直してください。それでも落ちないのであれば、REVIEW.md の Do not report が広すぎます。何を書かせないと決めたかを読み直すところが、この演習の本題です。
test-phpunit が赤ではなく、エラーで止まります
テストが落ちる(Failures)のではなく、実行そのものが止まっている(Errors)場合は、足したテストの書き方が既存の5本と違っています。CI の test-phpunit は PHP 7.4 と PHPUnit 7.5.20 で動きます。ジョブのログで止まった行を読み、既存の5本と同じ書き方にそろえる、という指示をプロンプトに入れ直してください。
allow_failure を外した時点から、ゲートが赤いと MR 用のパイプラインが赤になります。合流まで止めるには、プロジェクトの Pipelines must succeed と組にします。自社に写すときは、まず allow_failure: true のまま1週間まわして件数を見てから外すと、止まりすぎの調整が先に済みます。GATE_LEVEL はチームごとに1つ、CI の変数に置きます。テスト無しと有りの差
「機能テストを足せばリファクタを安全に進められる」という見立てを、ここで1回だけ実測します。緑が出たあとに壊してみる手順です。
- 1手順10 で入れた「
YYYY-MM-DDの形に合わない値を条件に使わない」判定を、1行だけ外したコミットを作ります。他は変えません。 - 2同期してパイプラインを見ます。
lint-phpdev、lint-phpver、testは緑のままです。既存の5本のテストも緑で、手順5で足した1本だけが赤になります。 - 3この1本が無ければ、同じ壊れ方が合流まで届いていたことを確認します。
- 4確認できたら戻します。ここはコマンドを使います。コミット単位の打ち消しは画面から行えないので、Claude Desktop の中のターミナルで打ちます。
git revert --no-edit HEAD
--no-editを付けると、コミットメッセージを編集する画面を開かずに打ち消しのコミットができます。そのあとソース管理ペインで 変更の同期 を押します。
git reset --hard は使いません。.claude/settings.json の permissions.deny と PreToolUse フックで止まる設定になっているはずですが、止まる前に手で打たないでください。送信済みの変更を戻すのは revert です。
ハンズオン2 OK 基準と持ち帰り物
ゲートで1回落ちて直した記録
- ai_gate が1回赤になり、その出力と、指摘ごとの採否と理由が MR に残っている
- 最後の push で走ったブランチ用と MR 用の2本のパイプラインが緑になっている
- 足したテストが、実装前は赤だったことがジョブの履歴から追える
- 配布の
計測表_Day1_Day2.mdの「Day2 検出率」に、検出率とノイズ率が入っている
マージまでは進めません。マージの判断は Day2 の最後にまとめて扱います。
ファイルの名前と置き場所
演習リポジトリでの置き場所と、自社ハーネスへ写すときの置き場所です。D2 は PowerShell が正、smart3pm は bash が正なので、判定スクリプトだけ行き先が分かれます。
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
REVIEW.md |
リポジトリ直下。コードレビュー側のエージェント定義から参照させる | リポジトリ直下。規約側に1行足して参照先にする |
.claude/known_issues.md |
リリースレビュー側にある同名の台帳を、コードレビュー側へ写す | 同じ内容を1本置き、検査スクリプトからは参照しない |
.claude/agents/ の3本 |
agents/ に追加。PowerShell に依存しないのでそのまま置ける |
agents/ に追加。同上 |
.claude/skills/review-route/SKILL.md |
skills/ に追加。既存の軽量パスの節と書き方をそろえる |
skills/ に追加 |
.gitlab-ci.yml の ai_review と ai_gate |
CI 側へ新規。機械強制を定義どおりに戻す位置づけ | pre-receive の検査とは別に、CI 側へ新規 |
| 判定スクリプト | ci/gate.ps1(PowerShell 5.1、ASCII のみ) |
ci/gate.sh |
| 追加した PHPUnit テスト | テストの置き場所の規約に合わせる | 同左 |
| 検出率とノイズ率の記録 | 計測表_Day1_Day2.md の「Day2 検出率」へ記入 | 同左。置き場所と担当だけ持ち帰り台帳.md に書く |
検出率の記録と Day1 との比較
Step 2 で数えた値を、配布の 計測表_Day1_Day2.md の「Day2 検出率」の表へ書き込んでください。表の行と列は次のとおりです。記入は計測表側で行います。
| レビュアー | 仕込み欠陥の件数 | 当てた件数 | 指摘の総数 | 検出率 | ノイズ率 |
|---|---|---|---|---|---|
| security-reviewer | |||||
| spec-reviewer | |||||
| test-gap-reviewer | |||||
| 3本の合計(重複を除く) |
書き終えたら、次の3つを1行ずつ書いてください。全体共有ではここを聞きます。
- 1Day1 の3通りレビューで数えた指摘件数と比べて、増えたか減ったか。減ったのであれば、それは
REVIEW.mdのどの行が効いた結果か。 - 23本のうち、既知の欠陥を1件も拾えなかったレビュアーがいたか。いた場合、観点の書き方と当てた対象のどちらに原因があるか。
- 3ゲートで止まった指摘のうち、直さない判断をしたものの割合。この割合が高いのであれば、
GATE_LEVELか Important の定義 のどちらかが現場に合っていません。
CSV 出力とレビュアーの4本目
早く終わった方から、上から順に進めてください。全部やる必要はありません。
- 1Issue #3 の受入条件4(
tests/phpunit/EstimateCsvTest.phpの3本)を満たすところまで実装します。ここで受入条件2 の残り(csv_text()から条件組み立てのメソッドを呼ぶ)も満たします。テストを先に出して赤を見る順番は同じです。 - 2Issue #2(在庫引当のトランザクションと行ロック)を、同じ流れで MR まで通します。
ai_reviewは MR の差分しか読まないので、stock_ctl.phpの指摘が出るのはこの MR からです。 - 3
.gitlab-ci.ymlのvariables:にGATE_MIN_CONFIDENCE: "0.6"を1行足して回し直し、止まる件数の差を見ます。確信度で切る運用が使えるかどうかの判断材料になります。 - 4
AI_REVIEW_DIFF_THRESHOLDを 80 から動かし、ai_reviewが使うモデルが切り替わる境界を測ります。 - 5レビュアーの4本目を作ります。対象の PHP 7.4 に無い構文だけを見る1本です。プチ演習4で書いた
check-phpverのフックと役割が重なるので、どちらを残すかまで決めてください。 - 6構造化出力を CLI で扱います。ここはコマンドを使います。Claude Desktop の中のターミナルで、スキーマを指定してレビュー結果を JSON に落とし、そのまま判定スクリプトへ渡す形を作ります。研修の端末には
claudeの CLI を入れていないので、研修後に CLI を入れた環境で試してください。PowerShell 5.1 の>は UTF-16 で書き出すので、Out-Fileで UTF-8 にします。claude -p --output-format json "REVIEW.md の基準で app/controllers/estimate_ctl.php を読み、.claude/review.schema.json の形で返してください" | Out-File -Encoding utf8 findings-cli.json
--output-format jsonの出力は外側に包みがあるので、そのまま判定スクリプトに渡すとfindingsの配列が無いとして落ちます。中身の取り出し方はプチ演習5 の「研修後に自分の API キーで CLI から試す場合」にあります。Claude Desktop は画面で結果を見るところまでで、ファイルを作って次の処理へ渡す形は CLI のほうが組みやすくなります。自動化に載せる段階でここに戻ってきてください。
プチ演習6 止まる Stop hook
D2-07 / 所要 [15min]
テストが赤い間だけ作業を続け、決めた回数で必ず止まる Stop hook を登録します。
テストが赤い間だけ1回続き、そこで止まる Stop hook
目的
応答を終わらせてよいかの判定を機械に移し、止まらなくなる作りとの差を説明できるようになります。あわせて、戻す手間を減らすために変更の範囲を先に絞る考え方を持ち帰ります。
準備
| 開いておく画面 | Claude Desktop、VSCode、講師の投影画面(Step 1 は見るだけです) |
|---|---|
| いるファイル | .claude/settings.json、.claude/hooks/stop-gate.ps1(bash は stop-gate.sh) |
| 直前の演習の成果物 | プチ演習4 の PostToolUse と、ハンズオン2 までのコミット |
| ブランチ | ハンズオン2 の ex/h2-<ユーザー名> のまま進めます。新しく切りません。ex/h2- は ex/p4- から切ってあるので、プチ演習4 の PostToolUse も入っています。ハンズオン3 の ex/h3- はこのブランチから切るので、ここで足す Stop がそのまま乗ります |
| 確かめておくこと | 統合ターミナルで php --version を打ち、手元に PHP があるか。研修用 PC には入れていないので、多くの方は「認識されていません」の表示になります。PHP が無い方は、Step 3 の継続を講師の投影画面で見ます。stop-gate は PHP が無いとテストを回さずに終えるためです |
自分で考える [2min] メモへ2つ書き出してください。
- 1自社で、AI の応答を終わらせてはいけない状態を1つ挙げてください。テストが赤い以外で書きます。
- 2その判定にかかる時間を秒で見積もってください。フックは応答のたびに走ります。1回30秒かかる検査を置くと、会話の速度がその分だけ落ちます。
手順
Step 1 止まらない版を見る [3min]
講師が2つの版を並べて回します。受講者は手を動かさずに画面を見てください。止まらない版は各自の端末では回しません。止まらない版は、次の3行だけでできています。
# demo only. do not register this one.
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
Write-Output '{"decision":"block","reason":"tests are red. fix and finish."}'
配布の版は、同じ処理の前に通す条件を置いています。どれかに当たった時点で、何も返さずに終わります。主な3つは次のとおりです。
# Guard 1: already continued by this hook.
if ($payload.stop_hook_active -eq $true) { exit 0 }
# Cap: continuations counted per session in .stop-gate.state
if ($count -ge $max) { ...; exit 0 }
# Guard 2: nothing changed in the working tree
if ([string]::IsNullOrWhiteSpace(($changes | Out-String))) { ...; exit 0 }
stop-gate の判定順です。上から、判定1 で stop_hook_active が true なら終了、判定2 で継続回数が STOP_GATE_MAX(既定 2)以上なら終了、判定3 で php と git が PATH に無ければ終了(php を入れていない端末はここで素通りします)、判定4 で作業ツリーに変更が無ければ終了、そこで初めてテストを回し、判定5 で赤があるときだけ decision: block を返します。左の点線が示すとおり、Step 1 の止まらない版は判定1 と判定2 を持たず、入力を読んだらすぐ判定3 以降へ進みます。継続のたびに同じ判定へ戻る理由がここにあります| 観察ポイント | 止まらない版 | 配布の版 |
|---|---|---|
| 継続が入った後の2回目の判定 | 同じ block を返す。判定の材料が前回と同じなので、結果も同じになる |
Guard1 が先に当たる。stop_hook_active が true なので、何も返さずに終わる |
| 画面の見え方 | 同じ理由の文が続けて増える。Claude Code が8回連続の block で打ち切るまで、応答が終わらない | 継続の文が1回だけ出て、その次で応答が終わる |
| 止め方 | 人が止めるか、8回連続で打ち切られるのを待つ | 放っておいても止まる |
| 使用量 | 1ターンぶんの入力が継続のたびに積み上がる | 1本の継続連鎖は Guard1 が必ず1回で切る。STOP_GATE_MAX は同じセッションで積み上がる継続回数の上限(既定2) |
Stop hook error の同じ文が3回並んでいる部分を見てください。処理が進んでいるのではなく、同じ判定を繰り返しています。Mac の bash 版フックで撮った画面ですが、PowerShell 版でも文面は同じですStep 2 正しい版を登録する [3min]
- 1VSCode で
.claude/settings.jsonの"hooks"に Stop のブロックを足してください。.claude/settings.example.jsoncの同じブロックと一字一句同じです。プチ演習4 と同じく、"PostToolUse": [ ... ]を閉じる]の直後にカンマを置いてから貼ります。Stop にmatcherはありません。応答の終わりは1種類しか無いためです。
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "powershell.exe",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-gate.ps1"],
"timeout": 300
}
]
}
]
- 2統合ターミナルを開き(右上の >_ アイコン、または
Ctrlとバッククォート)、門の1つ目だけを単体で確かめてください。echo '{"stop_hook_active":true,"session_id":"check"}' | powershell -NoProfile -ExecutionPolicy Bypass -File .claude\hooks\stop-gate.ps1 $LASTEXITCODE - 3何も表示されず、終了コードが 0 であることを確認してください。ここで
decisionを返す版が、Step 1 の止まらない版です。 - 4Claude Desktop で
Ctrl+Nを押して新しいセッションを開き、VSCode のsettings.jsonでStopに1本入っていて赤い波線が無いことを確認してください。JSON が壊れていると読まれないので、カンマを見直します。
Step 3 継続の経路を見る [4min]
- 1Claude Desktop で何か1つファイルを編集させてから応答を終わらせてください。内容は何でも構いません。例えば「README.md の末尾に、今日の日付を1行足してください」です。
- 2手元に PHP がある方は、継続が1回入ってから応答が終わることを見てください。PHP が無い方の端末では継続が入らずに終わるので、同じ場面を講師の投影画面で見ます。
continuation 1 of 2 が回数の記録ですStep 4 上限の効き方を確かめる [3min]
- 1VSCode で
.claude/settings.jsonを開き、先頭の{の次の行に次の1行を足して保存してください。settings.jsonのenvに書いた値は、Claude Code が起動するフックにも環境変数として渡ります。0 は継続そのものを止める値です。"env": { "STOP_GATE_MAX": "0" }, - 2Claude Desktop で
Ctrl+Nを押して新しいセッションを開き、Step 3 と同じようにファイルを編集させて応答を終わらせてください。PHP がある方の端末では、継続が入らずに応答が終わります。PHP が無い方は、講師の投影画面で同じ場面を見ます。 - 3確認が済んだら、手順1 で足した
"env"の1行を消して保存し、Ctrl+Nで新しいセッションを開いてください。消さないと、この後のハンズオン3 で継続が1回も入りません。 - 4ソース管理ペインで
.claude/settings.jsonだけをステージし、メッセージ欄に「プチ演習6: Stop フックを登録」と書いてコミットしてください。"env"の行が差分に残っていないこと、.claude/hooks/.stop-gate.stateをステージに上げていないことを先に見ます。Step 3 と手順2 で書かせたREADME.mdなどの1行は、変更の破棄 で戻してください。 - 5自社で使うときの上限を決め、台帳に書いてください。検査を残したまま継続だけ切りたいときが 0 です。
達成状態
Step 2 の単体確認では、何も出力されずに終了コードが 0 になります。Step 3 で継続が入る端末は、次の reason が画面に出てセッションが続きます。exit code の数字は環境で変わります。
{"decision":"block","reason":"php tests/run_tests.php failed with exit code 1. Run it, read the failing case, fix the code, then finish. (stop-gate continuation 1 of 2)"}
PHP が無い端末では、フックは標準エラーに次の1行目を書いて終えます。Step 4 で上限を 0 にしたときは、PHP の有無にかかわらず2行目を書いて終えます。上限の判定は PHP を探す手前にあるためです。どちらも標準エラーなので、Claude Desktop の画面には出ないことがあります。
[stop-gate] php not found on PATH, letting the session stop [stop-gate] already continued 0 time(s), the cap is 0. Letting the session stop.
- 止まらない版と配布の版の差を、通す条件3つで説明できる
stop_hook_activeが true のとき、フックが何も返さずに終わることを単体で確認した- 継続が1回入って止まる場面を、自分の画面か講師の投影画面で見た
settings.jsonのenvで上限を 0 にすると、継続が入らずに応答が終わることを確認した"env"の行を消し、.claude/settings.jsonの Stop ブロックを1コミットで残した
解説と補足
なぜこれをやるか 夜間に無人で回す構成では、応答を終える直前に走る Stop フックが最後の門になります。テストが落ちたまま終わらせない、という一言を機械にするのがこの演習です。あわせて、このフックが自動化で一番事故を起こす部品でもあることを、講師の画面で先に見ます。止まらなくなる原因は1か所です。フックが自分で継続させたセッションかどうかを見ずに止めると、そのたびに継続が入り、同じ判定がまた走ります。
Stop フックの動き 応答を終えようとした瞬間に走ります。標準出力に {"decision":"block","reason":"..."} を返すと応答は終わらず、reason の文が次の指示として渡って作業が続きます。何も返さなければそのまま終わります。継続で再び呼ばれたときは、入力の JSON に stop_hook_active が true で入ります。プチ演習3 の PreToolUse、プチ演習4 の PostToolUse、この演習の Stop で、手元のフックは3イベントになります。4本目の SessionStart はハンズオン3 の Step 1 で足します。
タイムアウト フックの既定はコマンド種別で600秒で、ここではテストの実行時間に合わせて300秒へ縮めています。自社の検査がこれを超える場合は、フックではなく CI 側へ寄せてください。
Mac または bash を正にする方 "command" を "bash" に、"args" を ["${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-gate.sh"] に差し替えてください。手動検査も bash .claude/hooks/stop-gate.sh に、$LASTEXITCODE を echo $? に読み替えます。
素通りの1行が画面に出ないとき 標準エラーの1行は、通常の画面には出ないことがあります。素通りの経路は「何も起きない」が正しい姿なので、見えないこと自体は不具合ではありません。フックだけを確かめたいときは、Step 2 の手順2 の JSON の true を false に変えて stop-gate.ps1 に流すと、統合ターミナルに標準エラーの行が出ます。
継続回数の記録 セッションごとに .claude/hooks/.stop-gate.state へ書かれ、テストが通ると消えます。手元の作業メモなので、コミットしないでください。ソース管理ペインにこのファイルが出たら、.gitignore に1行足します。回数を数えた文が出るのは、同じセッションで継続が積み上がったときです。1本の連鎖では stop_hook_active が必ず1回で切るので、既定の2に当たるのは同じセッションで依頼を繰り返した先になります。起動し直すと session_id が変わり、回数は 0 に戻ります。
追加と考察 Stop フックは、応答を終える判断を機械に移す部品です。移した結果、人の側に何が残るかを1行書いてください。夜間ループで人が持つのは Issue の粒度決め、マージ承認、指摘の採否の3つです。この3つのどれかが、テストの合否判定で置き換わっていないかを確かめてください。置き換わっているなら、それは機械化ではなく、判断の放棄です。
発展課題 いまの版はテストの合否だけを見ています。ここに lint-phpver 相当の検査を足すと、対象バージョンで動かない構文を残したまま応答が終わることも防げます。ただし、応答のたびに2つの検査が走ります。会話の速度と、合流前に気づけることのどちらを取るかを決めてから足してください。足さない判断も、理由を書けば持ち帰り物になります。
変更範囲の事前制限 Stop フックが止めるのは、終わり方です。戻す手間そのものを減らすなら、先に範囲を絞るほうが効きます。AI が20ファイルを書き換えた後に、どれを戻すか選ぶ作業は人の手に残ります。絞り方は2つあります。1つは PreToolUse で、決めたディレクトリの外を書こうとしたら止める形です。もう1つは Stop フックの判定に「作業ツリーの変更ファイル数」を足し、決めた数を超えたら継続させずに止める形です。どちらも、部分ロールバックと対で考えるものです。上限の数は、1回の依頼で触らせるつもりのファイル数から決めてください。普段の改修で5ファイルなら、8や10あたりが最初の線になります。超えたときに止まるのは事故ではなく、依頼が大きすぎた合図です。
影響範囲
| 効く範囲 | 応答のたびに検査が走ります(timeout 300秒)。夜間の無人実行で、テストが赤いまま終わらせない最後の門になります |
|---|---|
| 壊れると何が壊れるか | stop_hook_active を見ない版を登録すると、Claude Code が8回連続の block で打ち切るまで継続が続き、そのぶん使用量が積み上がります |
| コミットしないもの | .claude/hooks/.stop-gate.state。手元の作業メモです |
ここで登録したフックは、Claude Desktop から起動しても CLI から起動しても同じ設定ファイルが読まれ、同じように効きます。効く範囲が広いぶん、設定を別の環境へ持っていったときの差がそのまま出ます。同じフックを別の環境へコピーすると、次の4つで黙って効かなくなります。
| 起きること | 結果 |
|---|---|
改行が CRLF に変わり、#!/bin/bash の行末に \r が付く | シェルが起動せず、判定が行われない |
| 実行ビットが落ちる | フックが呼ばれずに素通りする |
| パスが移す前の環境のまま書いてある | 保護対象に一致せず、素通りする |
判定に使うコマンド(jq など)が入っていない | 判定の途中で落ち、止まらない |
4つとも「止まらずに通る」方向に倒れます。フックが動いていないことに気づかないまま、止めたかった操作が通ります。画面には何も出ません。環境は、AI ツールが動く場所とコードが置いてある場所とランタイムの版の3つの軸で別々に見てください。判定と実行を分け、止める対象を設定ファイルに出しておくと、環境が増えても入口を1本足すだけで済みます。組み合わせ別の設計は、配布物の 50_環境別のフック設計/環境別のフック設計.md にまとめてあります。
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
stop-gate.ps1 と stop-gate.sh |
.ps1 を正にして hooks 配下へ置く。実行するテストの行を、自社のテストコマンドへ差し替える |
.sh を正にして hooks 配下へ置く。既存の受入テストと同じ並びにテストを1本添える |
settings.json の Stop ブロック |
タイムアウトを自社の検査時間に合わせて書き換えてから写す | 同じ検査を bash 側の設定へ写す。2言語で別々の判定を持たない |
継続の上限(STOP_GATE_MAX の値) |
既定値をスクリプト内で決め、台帳に理由を1行残す | 同じ値にする。片方だけ緩いと、緩いほうが実質の上限になる |
ハンズオン3 夜間ループの最小構成
GitLab のパイプラインからエージェントを起動し、実装から MR 作成までを人の操作なしで通します。この演習を終えると、夜間に任せてよい工程と人が残る工程を、自分の実行ログを根拠に線引きできます。
題材は Issue #4「見積状態コードの対応表の一本化」です。受入条件が全部機械で判定できる形なので、無人で流す1本目に向いています。人に残すのはマージだけです。
api.anthropic.com へ到達できないときの進め方は、最後の「代替」のカードにまとめています。
無人で流せる範囲と、人が残る境目の確定
Issue 1本を無人で流し、止まった場所を1つ選んで直し、夜間に回せる範囲を1枚に書くところまで進みます。
- 1Issue 1本を無人で実装させ、MR が立つところまでを自分の手で1回通す。
- 21回目に止まった箇所を記録し、止め金の4層のどれで直すかを選んで再実行する。
- 3今のハーネスで夜間に回せる範囲と、人が残る3点を自分の言葉で1枚に書く。
始める前にそろっている状態
| 開いておく画面 | Claude Desktop(dl-training-app を開いた状態)、VSCode(同じフォルダ)、ブラウザで GitLab の演習プロジェクト |
|---|---|
| 触るファイル | .claude/settings.json(deny 13本とフック4本)、.gitlab-ci.yml の nightly_implement、ci/nightly_implement.sh、.claude/skills/implement-issue/SKILL.md |
| 作るもの | ブランチ ex/h3-<ユーザー名>、pipeline schedule 1本(Inactive)、MR 1本、計測表の「Day2 夜間ループ」の節 |
| ブランチ | ex/h2-<ユーザー名> から ex/h3-<ユーザー名> を切ります。ハンズオン2 までの deny、PreToolUse・PostToolUse・Stop の3本、REVIEW.md がそのまま乗ります。SessionStart は Step 1 で足します |
| 見る場所 | ジョブログ末尾の turns と cost_usd、artifacts の ci_logs/、MR のパイプライン、Code > Branches の main の行 |
| 所要 | [105min]。前提確認と「考える」で [5min]、Step 1 から Step 7 で [85min]、まとめで [15min] です |
Step 3 は講師が main で1本だけ流し、全員が同じログを読んで Step 4 の記録を書きます。Step 5 は、各自が自分のブランチのパイプラインで手動ジョブを押します。
コマンドを打つ場面が3か所あります。いずれも Claude Desktop の中のターミナルから実行します。右上の >_ アイコン、または Ctrl とバッククォートで開きます。セッションの作業ディレクトリで開くので、フォルダを移動する必要はありません。
Mac の方への読み替え
本文のコマンドは Windows の PowerShell で書いています。違いは3つです。複数行に分けたコマンドの行末は、バッククォートではなく \ にします。フックは .ps1 ではなく .claude/hooks/ の同じ名前の .sh を使い、settings.json では "command": "bash" と "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/<名前>.sh"] の形です。パスの区切りは / です。GitLab の画面操作と Claude Desktop の操作は同じです。
止め金の置き場所の下書き
設定ファイルを開く前に、紙かエディタに書き出してください。5分で構いません。ここを飛ばすと、この後の Step 1 が設定を写すだけの作業になります。
- 1Issue #4 を無人で流したとき、止まってほしい操作を3つ書き出します。
- 2その3つを止める層を、下の4つから1つずつ指定します。
- 3翌朝に最初に見る画面を1つ決め、そこで何を見てマージの可否を判断するかを3つ書きます。
| (a) deny | .claude/settings.json の permissions.deny。文字列照合なので bash -c のような書き方はすり抜けます |
|---|---|
| (b) フック | PreToolUse フック。権限モードに関係なく先に走ります。変更してよい範囲の指定もここに置けます |
| (c) 回数と時間 | --max-turns とジョブの timeout。操作の種類を見ずに打ち切ります |
| (d) ブランチ保護 | GitLab の設定。エージェント側の設定から独立していて、設定ファイルを書き換えられても効きます |
- 止まってほしい操作が3つ、具体的なコマンドか操作名で書けている
- 3つそれぞれに (a)〜(d) のどれかが割り当たっている
- 翌朝に見る画面が1つ決まり、判断材料が3つ挙がっている
同じ操作を2層で止めている箇所の見つけ方
3つの操作と4つの層を突き合わせると、2層で止めている操作と、どこでも止めていない操作が出てきます。後者は Step 4 で必ず踏みます。層の強さが違うので、重なっていること自体は問題ではありません。問題になるのは、どこでも止まらない操作を「たぶん大丈夫」で流すことです。
夜間ループの登場人物
この演習で触るファイルは4つです。すでにリポジトリに入っています。
.gitlab-ci.yml | nightly_implement ジョブ。schedule からは自動で、それ以外のパイプラインでは手動ボタンとして出ます |
|---|---|
ci/nightly_implement.sh | ブランチを作り、エージェントを呼び、コミットを確かめ、push して MR を作るまでを1本にしたスクリプト |
SKILL.md | .claude/skills/implement-issue/。Issue を読む・方針を3行で書く・影響範囲を調べる・実装する・テストを回す、の手順書 |
.claude/settings.json | 止め金のうち、permissions.deny とフック登録を持っている場所 |
手元では Claude Desktop で対話しながら進め、夜間は CI が同じ設定のまま非対話で回します。Claude Desktop は非対話実行(-p)を持たないので、無人で回る部分は CI 側の CLI が受け持ちます。人が横にいる作業と無人で回る作業の境目が、そのままツールの使い分けに重なります。止め金をどこに置くかは、この境目のどちら側で効かせたいかで決まります。
スクリプトが進める4段と、エージェントに渡しているツール
[ schedule または手動実行 ]
implement
nightly_implement
ci/nightly_implement.sh
├ ブランチ nightly/issue-4 を作る
├ claude -p "/implement-issue 4" --permission-mode dontAsk --max-turns 15
├ コミット(エージェントが済ませていなければスクリプト側で)
└ push → ここで MR のパイプラインが動く
マージだけ人が判断する
エージェントに渡しているツールは Read,Edit,Write,Grep,Glob,Bash(php *),Bash(git add *),Bash(git commit *) だけです。ブランチ作成と push はスクリプトの仕事なので、権限を渡していません。そのぶん、SKILL.md の手順3(ブランチ作成)と手順7(push)を実行させると、そこで拒否されて止まります。飛ばす指示はスクリプトがプロンプトに添えています。手順1 の Issue 取得(curl)も渡していないので、ここでも止まります。SKILL.md が意図した止め金として書いている箇所です。
artifacts には ci_logs/claude-issue-N.json と .err が残り、num_turns、total_cost_usd、permission_denials を読めます。
この演習で初めて出てくる語
schedule | GitLab が決めた時刻にパイプラインを起動する仕組み(pipeline schedule)。rules の $CI_PIPELINE_SOURCE == "schedule" は「schedule から起動されたときだけ自動で走る」の意味です |
|---|---|
| 手動ジョブ | rules の when: manual が付いたジョブ。パイプラインの一覧に再生ボタンの形で出て、人が押すまで走りません |
timeout: 30m | ジョブが30分を超えたら GitLab が強制終了します。エージェントが堂々巡りしたときの最後の打ち止めです |
allow_failure: true | このジョブが失敗してもパイプライン全体を赤にしない設定。名前の横に Allowed to fail と出ます |
ISSUE_ID | 実装させる Issue の番号を入れる変数。既定値は "4" で、schedule か手動実行の Variables で上書きできます |
artifacts | ジョブが終わったあとに GitLab が保存するファイル。ジョブ画面の右の Job artifacts から開きます |
ci_logs/ | スクリプトがジョブの中で作るフォルダ。結果の要約 JSON と標準エラーの2つが入ります。リポジトリにはコミットされません |
ハンズオン3 止め金と schedule の準備
Step 1 と Step 2 です。合わせて [20min]。ファイルの確認と編集は VSCode、エージェントへの指示は Claude Desktop、GitLab の操作はブラウザで行います。git の操作は VSCode のソース管理ペイン(左サイドバーの枝分かれのアイコン)を使います。
止め金4か所の設定 [10min]
「考える」で書き出した3つが、実際にどこで止まるかを4か所で確かめます。
手順
- 1VSCode の左下の表示が
ex/h2-<ユーザー名>で、未コミットの変更が残っていないことを確認します。違うブランチなら、ブランチ名を押して一覧からex/h2-<ユーザー名>を選びます。そのあとブランチ名を押し、新しいブランチの作成 でex/h3-<ユーザー名>を作ります。名前の決め方は共通の進め方 STEP 2「ブランチ名の形」にあります。 - 2VSCode で
.claude/settings.jsonを開き、permissions.denyに次の13行があることを確認します。無ければ足します。
"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 *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/credentials*)"
- 3足した場合は、ソース管理ペインでメッセージに
夜間ループの止め金: deny 13行と入れてコミットします。 - 4同じ
settings.jsonのhooksに、PreToolUse(block-destructive)、PostToolUse(check-phpver)、Stop(stop-gate)の3本が並ぶことを確認します。そのうえで、Stop を閉じる]の直後にカンマを1つ置き、その下に次の SessionStart のブロックを足して保存します。.claude/settings.example.jsoncの同じブロックと一字一句同じです。"SessionStart": [ { "hooks": [ { "type": "command", "command": "powershell.exe", "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/sessionstart_verify.ps1"], "timeout": 30 } ] } ]保存したら Claude Desktop でCtrl+Nを押して新しいセッションを開き、settings.jsonに赤い波線が無いことを確かめて、夜間ループの止め金: SessionStartでコミットします。Claude Desktop の入力欄では/hooksは使えないので、登録はsettings.jsonで見ます。 - 5VSCode で
.gitlab-ci.ymlのいちばん下のnightly_implement:を開き、timeout: 30mとISSUE_ID: "4"があることを確認します。書き換えません。 - 6VSCode で
ci/nightly_implement.shを開き、claude -p "$PROMPT" \で始まる行に--max-turns 15が付いていることを確認します。値は上げないでください。 - 7ブラウザで GitLab の演習プロジェクトを開き、Code > Branches を選びます。
mainの行に protected の表示が付いていることを確認します。見るだけです。Developer の権限では Settings のメニューは出ません。
- ブランチが
ex/h3-<ユーザー名>に切り替わり、VSCode の左下にその名前が出ている - deny 13行、フック4本(SessionStart を足した後)、timeout、--max-turns、ブランチ保護の5つを自分の目で確認した
- 「考える」で書いた3つの操作のうち、どこでも止まらないものが1つ以上見つかっている
settings.json に登録した .ps1 が Linux のコンテナで動かないことを表しています。夜間ジョブは ci/settings.ci.json で同じ判定の bash 版を登録し直しているので、PreToolUse と PostToolUse は CI でも効きます。(d) は逆に手元では何も止めません。この2列は、片方だけを見ても埋まらないようにできています。影響範囲
手元で効く deny とフックは、Claude Desktop と CLI が同じ設定ファイルを読むので、どちらで動かしても同じように効きます。Day1 に書いた設定をそのまま使えます。Claude Desktop のパーミッションモード(手動 / 編集を受け入れる / プラン / 自動)を 自動 にしても、deny とフックは先に走ります。モードが決めるのは確認ダイアログを出すかどうかだけです。
CI の中では、deny、ジョブの timeout、ブランチ保護に加えて、bash 版の PreToolUse と PostToolUse が効きます。nightly_implement のイメージは Linux(node:22-bookworm-slim)で powershell が入っていないので、ci/nightly_implement.sh が --settings ci/settings.ci.json で bash 版のフックを登録しています。Stop は CI では登録していません。runner には DB が無く、テストが必ず赤になって継続を無駄に使うためです。
解説と補足
deny の13行の由来 上の10行が Day1 のプチ演習2、下の3行がプチ演習3 で書いたものです。上の10行は破壊系の5つを Bash と PowerShell の対で書いたもの、下の3行が秘密情報です。配布直後の settings.json は "deny": [] の空の状態なので、Day1 の作業をこのフォルダで行っていない方はここで13行を入れます。deny は JSON なので OS を跨いで効きます。
PreToolUse・PostToolUse・Stop の3本がそろわないとき PreToolUse と PostToolUse が無いときは、ハンズオン2 のブランチより前で抜けています。Stop だけが無いときは、プチ演習6 を ex/h2- 以外のブランチで行ったか、コミットしていません。VSCode で .claude/settings.example.jsonc を開き、足りないブロックを settings.json へ写します。注釈付きの完成形が置いてあります。写すときは // で始まる注釈の行を落としてください。settings.json はセッションの開始時に読まれるので、書き換えたら Ctrl+N で新しいセッションを開きます。
CI 側で対象バージョンを守っているもの このプロジェクトの対象は PHP 7.4 です。手元では check-phpver フックが対象外の構文を差し戻し、夜間ジョブの中でも bash 版の同じフックが走ります。push のたびに走るブランチ用のパイプラインでは lint-phpver が受け持ちます。このジョブは2段です。1段目の php -l が match 式や enum などの構文を落とし、2段目の php ci/undefined_functions.php が str_contains や str_starts_with のような 7.4 に無い関数の呼び出しを function_exists() で見つけて落とします。lint-phpver が赤になると、後ろの段の test と test-phpunit は動きません。
ブランチ保護の確認で気づいたこと main の行に protected の表示が無い場合は講師へお知らせください。Step 6 のマージ判断に関わります。
コマンドで行う場合 ブランチの作成は、Claude Desktop の中のターミナル(右上の >_ アイコン、または Ctrl とバッククォート)から次の2行でも同じです。yamada はご自身のユーザー名に置き換えます。研修では上の手順1 の画面操作で進めます。
git switch ex/h2-yamada git switch -c ex/h3-yamada
pipeline schedule の作成 [10min]
夜間に動かすための時刻の設定を作ります。当日は同時実行を避けるため手動ボタンから流しますが、schedule 自体は各自が作ってください。持ち帰るのはこの設定です。
手順
- 1GitLab で演習プロジェクトを開き、Build > Pipeline schedules を選びます。
- 2New schedule(1本も無いときは Create a new pipeline schedule)を押します。
- 3Description に
nightly issue-4と入力します。 - 4Interval Pattern で Custom を選び、
0 2 * * *と入力します。毎日 2:00 の意味です。 - 5Cron timezone を Tokyo に、Target branch を
mainにします。 - 6Variables に
ISSUE_ID=4を1行足します。 - 7Activated のチェックを外し、Create pipeline schedule を押します。
- Pipeline schedules の一覧に自分の schedule が1本ある
- その行が Inactive になっている
- Variables に ISSUE_ID = 4 が入っている
影響範囲
schedule を有効にすると、その時刻に全員分が同時に走ります。runner の並列数と API の予算を一晩で使い切ります。時刻で動かすのは、自社のリポジトリに写してからです。
解説と補足
チェックを付けたまま保存した場合 一覧の行のトグルを切って Inactive に戻してください。
入力画面の既定値 Interval Pattern の既定は Every day (at 8:55am) で、下の欄に 55 8 * * * が入っています。Custom に切り替えてから同じ欄を 0 2 * * * に書き換えます。Activated は開いた時点でチェックが付いています。Variables の行を入れ忘れると、ジョブは .gitlab-ci.yml の既定値で走ります。
当日に自動起動させない理由 研修当日は手動ボタン側だけを使います。Step 3 は講師が main で1本、Step 5 は各自のブランチで1本ずつ流します。nightly_implement は1本が最長30分 runner を使います。
時刻を GitLab 側に置いている理由 Anthropic の Routines(/schedule)は claude.ai のログインが前提で、API キーで動かしている環境では出てきません。
ハンズオン3 実行と停止箇所の記録
Step 3 と Step 4 です。合わせて [30min]。1回目は講師が main で1本だけ流し、止まる前提で全員が同じログを読みます。通すことではなく、止まった場所を特定して記録することがこの2つの Step の中身です。
講師の1回目のログを読む [15min]
講師が main のパイプラインで nightly_implement を1本だけ流します。受講者は手動ボタンを押さず、全員が同じジョブのログを読みます。全員分を同時に走らせると、後のジョブが待ちに入って演習の時間内に終わらないためです。
手順
- 1講師が Zoom のチャットにパイプライン番号(
#の付いた数字)を貼ります。GitLab の Build > Pipelines を開き、その番号の行を探します。ブランチがmainの行です。 - 2その行の Stages のいちばん右の丸(
implement)を押し、nightly_implementを選んでジョブの画面へ移ります。 - 3黒い領域のログを見ます。1〜2分の準備のあと
Issue #4 の実装を開始します。ブランチ: nightly/issue-4が出れば、スクリプトが動き始めています。 - 4そのまま待ちます。数分から十数分のあいだログは動きません。画面上部の経過時間が進んでいれば動いています。
- 5終わったらログの末尾を読み、下の表のどの終わり方かを確かめて Step 4 へ進みます。
| 終わり方 | ログの末尾 |
|---|---|
| MR まで届いた | turns: … / cost_usd: … のあとに View merge request for nightly/issue-4: と URL、最後に MRを作成しました。マージは人が判断します。 |
| 動いたが差分が無い | 変更がありません。MRは作りません。 どこで止まったかは ci_logs/ に出ます |
| 始まる前に終わった | ANTHROPIC_API_KEY: CI/CD変数 ANTHROPIC_API_KEY が未設定です。ci_logs/ は残りません |
1回目で MR まで届くことは多くありません。止まっているほうが普通です。
#28 の行が main で夜間ジョブを流した例で、右端の丸を押してジョブへ進みます。
- 講師が流した nightly_implement の行を、チャットの番号で開けた
- ジョブのログを最後まで読み、上の表のどの終わり方かが分かっている
- artifacts に ci_logs/ が残っている(環境変数の不足で止まった回だけは残りません)
影響範囲
このジョブには allow_failure: true が付いています。止まってもパイプライン全体は赤になりません。failed は赤ではなくオレンジの警告アイコンで出て、横に Allowed to fail が付きます。どちらの色で終わっても Step 4 へ進みます。
解説と補足
講師が押している操作 Build > Pipelines の右上の New pipeline を押し、Run for branch name or tag に main を選び、Variables は空のまま起動します。lint と test の段が終わると、いちばん右の implement の段に nightly_implement が再生ボタンの形で出るので、講師がこれを押します。main のパイプラインの再生ボタンは、受講者の画面に出ても押さないでください。
講師の行が見分けられないとき 同じ時刻の行が複数あるときは、講師が Zoom のチャットに貼ったパイプライン番号で特定してください。ジョブ名に受講者名は出ません。
エージェントの出力がログに流れない理由 --output-format json で ci_logs/claude-issue-4.json に書いているためです。ジョブログに出るのは、スクリプトが出す行と最後の要約だけです。
pending のまま動かないとき 他のパイプラインのジョブがまだ runner を使っています。異常ではありません。ジョブ画面の上部に This job is stuck が出ている場合だけ、runner 側の問題なので講師へお知らせください。
30分で打ち切られたとき ログの末尾に ERROR: Job failed: execution took longer than 30m0s seconds と出ます。--max-turns 15 より先に timeout が効いた形で、1ターンが長いときに起こります。この場合も ci_logs/ は残ります。
環境変数の不足で止まったとき 直すのは講師側の CI/CD の変数です。受講者は Step 4 の記録を「環境変数の不足で止まった」として書き、講師が変数を登録したあとの再実行を待ちます。
1回目の停止箇所の記録 [15min]
止まった場所を1つ特定して記録します。複数同時に起きていることもありますが、最初に効いた1つだけを選んでください。手を入れる層を1回に1つに絞るためです。
手順
- 1ジョブ画面の右の Job artifacts の Browse を押し、
ci_logsを開きます。 - 2
claude-issue-4.jsonを開き、is_errorとnum_turnsを見ます。total_cost_usdは Step 7 で使うので控えます。 - 3同じ
claude-issue-4.jsonのpermission_denialsを見ます。権限で拒否された場合は、ツール名とコマンドがここに入ります。claude-issue-4.errは CLI 自体のエラーの置き場所で、拒否はここには出ません。 - 4下の表から、自分の止まり方に一番近い行を1つ選びます。
- 5VSCode で配布の
計測表_Day1_Day2.mdを開き、「Day2 夜間ループ」の節の上4つの欄(止まった箇所・止まった理由・どの層の設定で直すか・直した内容)を埋めます。下3つは Step 5 のあとに埋めます。
| 止まり方 | ログに出るもの | 直す層 |
|---|---|---|
| 環境変数の不足で始まらなかった | ジョブログに ANTHROPIC_API_KEY の1行。ci_logs/ が無い |
CI 側(講師の設定)。受講者が直す層はなく、台帳には「環境」と書く |
| Issue を読めずに終わった | permission_denials に curl か glab の拒否 |
プロンプト。PROMPT に _issues/issue-4.md を読ませる1行を足す。curl は ci/settings.ci.json の deny に入っているので、--allowedTools に足しても通りません |
| ブランチ操作で拒否された | permission_denials に git switch か git push の拒否 |
プロンプト側。SKILL.md の手順3と手順7を飛ばす添え書きが届かなかった場合に出ます |
| 範囲外のファイルまで書き換わった | 差分が app/ と tests/ の外に及んでいる |
プロンプト側。PROMPT に触ってよい範囲を1行足す。戻す手間を減らすには、先に範囲を絞るほうが効きます |
| 対象バージョンで動かない構文が入った | ブランチ用のパイプラインの lint-phpver が赤。match 式や ?->、7.4 に無い関数の呼び出し |
文脈。CI では止まっているので、夜間ジョブの bash 版 check-phpver をすり抜けた形です。CLAUDE.md の代替表を厚くする |
| テストが落ちたまま終わった | test または test-phpunit が赤 |
Stop hook。手元では stop-gate.ps1 が継続させるが、CI の ci/settings.ci.json は Stop を登録していない。runner に DB が無く、テストが必ず赤になるため |
| レビューを受けずに終わった | ai_review のジョブが動いていない |
CI 側。ai_review は merge_request_event でだけ動くので、MR ができるまで走らない |
| 指摘を全部受け入れて設計が巻き戻った | 差分が Issue の「範囲外」に及んでいる | REVIEW.md。Do not report と Always check の書き方。無人実行では採否を人が挟めない |
| MR の作成で止まった | コミットと push は通っているが、そのあとに 403 | CI 側。GITLAB_BOT_TOKEN をプロジェクトアクセストークン(Developer、api と write_repository)に差し替える |
| ターン数の上限で打ち切られた | num_turns が 15。コミットが途中で終わっている |
Issue の粒度。--max-turns を上げずに、Issue を割る |
- 止まった箇所を1つに絞り、ログの行を根拠として写した
- 直す層を表から1つ選んだ
- 計測表の「Day2 夜間ループ」の上4つの欄が埋まっている
影響範囲
ここで選んだ層が、Step 5 で直す1か所になります。2つ以上選ぶと、再実行でどちらが効いたか分からなくなります。
解説と補足
表の「直す層」と (a)〜(d) の対応 「プロンプト」「スクリプト」は ci/nightly_implement.sh の呼び出し側で、(c) の隣にある層です。「CI 側」は .gitlab-ci.yml と CI/CD Variables。「Stop hook」と「REVIEW.md」は (b) のフックと、そのフックが読む基準。「Issue の粒度」は (c) の --max-turns を動かさずに入力のほうを割る選択です。(a) の deny と (d) のブランチ保護は表に出てきません。今日の1回目で効く前に、それより手前の層で止まるためです。
計測表の書き方の例 文言は例です。ご自身のログから写します。
| 項目 | 記入欄 | |---|---| | 止まった箇所 | Issue を読めずに終わった | | 止まった理由 | permission_denials に curl の拒否。curl は ci/settings.ci.json の deny に入っている | | どの層の設定で直すか | スクリプト(ci/nightly_implement.sh の PROMPT) | | 直した内容(ファイル名とキー) | PROMPT に「Issue の本文は _issues/issue-4.md にあります」を1行追加 | | 再実行の結果 | (Step 5 のあとに書く) | | ジョブの所要時間(分) | (Step 5 のあとに書く。ジョブ画面の Duration) | | 使ったターン数 | 3(上限 15 に未到達)。cost_usd 0.04 |
環境変数の不足で止まった回は「止まった箇所」を「環境変数の不足で始まらなかった」とし、ターン数の欄は「-」にします。
permission_denials が空のとき 同じ claude-issue-4.json の result の文に、エージェントが最後に何を言って終わったかが入っています。.json はブラウザでは開けないので、行の右端のアイコンでダウンロードして VSCode で開いてください。2つまとめて取るときは右上の Download artifacts archive です。
ターン数の上限に当たった人へ --max-turns を上げないでください。上げると1本あたりの費用と時間が読めなくなります。夜間ループで回数の上限に当たるのは、多くの場合 Issue が大きすぎるという合図です。Issue #4 が「受入条件を全部機械で判定できる形」で書かれているのは、この打ち切りに当たらない粒度に落とすためです。
「レビューを飛ばした」に見える止まり方 多くは MR がまだ無いだけです。ai_review と ai_gate は MR のパイプラインに乗っているので、push で MR が立った後に動きます。順番を取り違えると、直す層を間違えます。
ハンズオン3 再実行とマージ判断
Step 5 から Step 7 です。合わせて [35min]。直す層を1つだけ直して流し直し、立った MR を人が読み、夜間に回せる範囲を1枚にまとめます。
直す層の選択と再実行 [20min]
Step 4 で選んだ層を1つだけ直して、もう一度流します。2回目は main ではなく、自分のブランチ ex/h3-<ユーザー名> の上で起動します。共有の main は変えません。
手順
- 1VSCode で直す層のファイルを開き、1か所だけ変更します。
- 2ソース管理ペインで、そのファイルだけをステージし、何を直したかが分かるメッセージでコミットします。
- 3ソース管理ペインの ブランチの発行(2回目以降は 変更の同期)を押して、自分のブランチを GitLab へ送ります。
- 4GitLab の Build > Pipelines で、ブランチが
ex/h3-<ユーザー名>の行を開きます。手順3 の push で動いたパイプラインです。 - 5lint と test の段が終わると、いちばん右の
implementの段にnightly_implementが再生ボタンの形で出ます。ご自身でこれを押します。Developer の権限でも、保護されていない自分のブランチの手動ジョブは押せます。mainや他の方のブランチの行は押しません。 - 6
nightly_implementを押してジョブの画面へ移り、2回目のログを開きます。 - 71回目に止まった箇所を通過していることを確かめます。別の箇所で止まった場合は、それも計測表に1行足します。
main の protected が Step 1 で見たブランチ保護です。- 直した層が1つだけで、その変更が1コミットになっている
- 2回目の実行で、1回目の停止箇所を通過した
- 2回目で止まった箇所があれば、それも計測表に書いた
影響範囲
自分のブランチから流すと、そのブランチに入っている .gitlab-ci.yml、ci/nightly_implement.sh、.claude/settings.json で走ります。自分の修正が効いたかどうかを、共有の main を変えずに確かめられます。
解説と補足
2回目のブランチ名 ci/nightly_implement.sh は、main 以外のブランチから流されたときに起点のブランチ名を後ろに足します。ex/h3-yamada から流せば nightly/issue-4-ex-h3-yamada になります。講師の1回目の nightly/issue-4 とも他の方のブランチとも重ならないので、remote のブランチを消す必要はありません。強制 push は permissions.deny で禁じているので、CI でも例外を作りません。
同じ起点からもう一度流し直すとき 先に自分の名前が付いたブランチを消します。GitLab の Code > Branches で nightly/issue-4 で始まる自分のブランチを探し、行の右端のごみ箱のアイコンで削除します。MR が立っている場合は、先に MR を閉じてください。
2回目の MR に自分のコミットが入る理由 nightly/issue-4-ex-h3-… は自分のブランチの先端から作られるためです。Step 6 で差分を読むときは、Issue #4 の実装と自分の修正を分けて見てください。エージェントが何も変えなかった場合は、スクリプトがジョブ開始時のコミットと比べて「変更がありません」と出し、MR は作りません。
コマンドで行う場合 手順2 と手順3 は、Claude Desktop の中のターミナル(右上の >_ アイコン、または Ctrl とバッククォート)から次の3行でも同じです。研修では上のソース管理ペインの操作で進めます。
git add ci/nightly_implement.sh git commit -m "夜間ジョブ: Issue 本文を _issues/issue-4.md から読ませる" git push -u origin HEAD
MR の確認とマージ判断 [10min]
自動で立った MR を人が読んで、マージしてよいかを判断します。この演習で人が手を動かすのは、ここだけです。講師の1回目で MR まで届かなかった場合は、自分の2回目の MR で行います。
手順
- 1ジョブログの末尾に出た MR の URL を開きます。Code > Merge requests の一覧から探しても同じです。
- 2MR の Pipelines タブで、
lint-phpdev、lint-phpver、test、test-phpunitの4つが緑であることを確認します。 - 3Overview タブに戻り、
ai_reviewが置いたコメントを読みます。 - 4Changes タブで差分を Issue #4 の受入条件と突き合わせます。見るのは3点です。
app配下で'作成中'が出てくるファイルがapp/libraries/util.phpの1つだけになっているか(受入条件1)、app/views/estimate_list.phpとapp/views/estimate_detail.phpの両方からarray(1, 2, 3, 4, 9)が消えているか(受入条件2)、tests/run_tests.phpにテストが3件増えているか(受入条件4)。app/views/stock_list.phpにも同じ並びがありますが、在庫状態のプルダウンなので範囲外です。 - 5同じ Changes タブで、差分が
app/とtests/の外に出ていないことを確かめます。出ている場合は、下の折りたたみの手当てを行います。 - 6MR の説明に、変更方針3行と影響範囲の調査結果が入っていることを確認します。入っていない場合は、計測表に「人が残る作業」として書き足します。
- 7判断の根拠3つを計測表の「Day2 夜間ループ」の行の末尾に書き、講師へ「マージ可」と伝えます。Merge のボタンは押しません。
main に入った時点で、後から流した nightly_implement は「変更なし」で終わります。演習の最後に、講師が全員の判断を聞いたうえで1本だけマージし、残りの MR は開いたまま記録として残します。- MR のパイプラインの4ジョブが緑である
- 受入条件の3点を自分の目で確かめた
- 差分が
app/とtests/の中に収まっている - マージしてよいかを自分で判断し、根拠3つを計測表に書いて講師へ伝えた
影響範囲
ai_gate が赤でもマージボタンは押せる状態のままです。演習のプロジェクトには Pipelines must succeed を設定していないので、パイプラインの色にかかわらず Merge は押せます。main の .gitlab-ci.yml には allow_failure: true も残っていて、ハンズオン2で外した版は各自のブランチにしかありません。赤いままの MR を「押せるが押さない」と判断できるかどうかも、この Step で見ています。
範囲外の変更が混ざっていたときの戻し方
無人実行では、エージェントが20ファイルを一度に書き換えたあとで気づきます。MR ごと閉じると、範囲内の実装まで捨てることになります。戻すのは範囲外のファイルだけにします。
| 1. 範囲を特定する | Changes タブの左のファイル一覧で、app/ と tests/ の外にあるものを書き出します |
|---|---|
| 2. その範囲だけ戻す | MR のブランチに切り替え、書き出したパスだけを起点の内容に戻します。GitLab の画面には「一部のファイルだけ戻す」操作が無いので、ここはコマンドを使います |
| 3. 戻した分をコミットする | ソース管理ペインで、戻ったファイルだけをステージしてコミットし、同期します。MR の差分から範囲外のファイルが消えます |
| 4. 次の実行で防ぐ | ci/nightly_implement.sh の PROMPT に、触ってよい範囲を1行足します。Step 4 の表の「範囲外のファイルまで書き換わった」の行です |
手順2 で使うコマンドです。Claude Desktop の中のターミナル(右上の >_ アイコン、または Ctrl とバッククォート)から実行します。セッションの作業ディレクトリで開くので、そのまま打てます。<パス> には手順1 で書き出したものを並べます。
git restore --source=origin/main -- <パス>
MR 全体を取り消したい場合は、マージ後であれば GitLab の MR 画面の Revert ボタンで戻せます。まだマージしていない MR は、Close で閉じればブランチごと残せます。
人が手で直したコミットと、エージェントのコミットが同じブランチに混ざっている場合は、Commits タブで作者を見ます。夜間ジョブのコミットは nightly-bot です。作者で分けられるうちは、戻す範囲をコミット単位で選べます。
解説と補足
MR の見分け方 タイトルが nightly: Issue #4、作成者が GITLAB_BOT_TOKEN のボットアカウントになっています。
パイプラインが2本並ぶ理由 共通の進め方 STEP 4「GitLab の Web 画面」で見たとおり、branch の行に lint-phpdev、lint-phpver、test、test-phpunit の4つ、merge request の行に ai_review と ai_gate の2つが出ます。test-phpunit は2分ほど余計にかかります。
ai_gate が落ちているとき そのジョブのログに止めた指摘が並ぶので、直すか GATE_LEVEL の判断を変えるかを決めます。
受入条件の確かめ方 差分の中でブラウザの検索(Ctrl+F)を使えば、前の2つは数十秒で確かめられます。
マージ後のブランチ MR の作成オプションに merge_request.remove_source_branch が付いているので、講師がマージした MR の元ブランチは自動で消えます。開いたまま残す MR のブランチは消えません。
夜間に回せる範囲の1枚 [5min]
研修の後、社内で最初に説明する相手に見せるのはこの1枚です。
手順
- 1VSCode で
計測表_Day1_Day2.mdの「Day2 夜間ループ」の節を開き、Step 4 で空けておいた下の3つの欄(再実行の結果・所要時間・ターン数)を2回目のログで埋めます。所要時間はジョブ画面の Duration の分数です。 - 2下の1つ目の表を埋めます。「今夜から回せる」に入れてよいのは、今日の実行で実際に通った工程だけです。
- 32つ目の表の担当と目安を埋めます。項目は決まっています。埋めるのは、自社で誰がそれを持つかと、その判断に何分かけるかです。
- 4計測表の末尾の「最後に1枚書く欄」の3行に、1行目は1つ目の表の「今夜から回せる」に入れた工程名、2行目は2つ目の表の担当と目安、3行目は Step 4 で「直す層」に挙がったものを効いた順に書きます。
- 5VSCode で
持ち帰り台帳.mdを開き、この演習の行を1行足します。「足したもの」の列に schedule の設定値を書きます。
| 工程 | 今夜から回せる | 止め金として効いたもの |
|---|---|---|
| ブランチ作成 | ||
| Issue の読み取りと方針の作成 | ||
| 実装 | ||
| 対象バージョンの検査 | ||
| テスト | ||
| レビューとゲート | ||
| MR 作成 |
| 人が残る作業 | 担当 | 1件あたりの目安 |
|---|---|---|
| Issue の粒度決め | ||
| マージ承認 | ||
| 指摘の採否 |
- 「回せる」に入れた工程が、今日の実行ログで裏付けられている
- 人が残る3点に担当が入っている
- 計測表の「Day2 夜間ループ」の7つの欄が全部埋まり、ターン数の欄に turns と cost_usd が併記されている
影響範囲
schedule はファイルでは持ち出せません。持ち帰り台帳.md に書いた設定値が唯一の控えになります。
解説と補足
台帳の書き方 共通の章の台帳と同じ列です。
| ハンズオン3 | nightly_implement ジョブ + ci/nightly_implement.sh + pipeline schedule(ISSUE_ID=4, 0 2 * * *, main) | .gitlab-ci.yml と ci/(当日確定) | 同じ(当日確定) | 山田 | 置き場所まで決めた |
コードを読む係を置かない理由 読む必要が出たということは、止め金かゲートのどちらかが足りていないという合図です。その場合は、人を増やさずに層を1つ足します。
CI が使えないときの進め方
runner から外部の API へ出られない場合は、Step 3 と Step 5 の CI 実走を講師環境で行います。受講者は同じ手順を Claude Desktop で回します。止め金の効き方は同じで、違うのは push と MR 作成を人が行うところだけです。
手順
- 1VSCode の左下のブランチ名から 新しいブランチの作成 で
nightly/issue-4-<ユーザー名>を作ります。CI から流すぶんと同じ名前にすると、push で衝突します。 - 2Claude Desktop で
dl-training-appを開き、パーミッションモードを 自動 にします。承認待ちで止まらなくなります。CI の--permission-mode dontAskとは向きが逆です。dontAskは許可の一覧に無い操作を聞かずに拒否し、自動 は別のモデルが安全と見た操作を聞かずに通します。どちらのモードでも deny とフックは先に効きます。 - 3入力欄に次の1通をそのまま送ります。
/implement-issue 4と、CI のスクリプトがプロンプトに添えているものと同じ指示を、1通にまとめた形です。2通に分けると、2通目が届く前に skill が手順3 のブランチ作成まで進むことがあります。/implement-issue 4 ブランチは既に作ってあります。ブランチの作成と push は人が行うので、skill の手順3と手順7は実行せず、コミットまでで終えてください。
- 4終わったら、Claude Desktop の画面で使ったターン数と、拒否されたツールがあればその行を控えます。CI の
ci_logs/で見ていたものと同じ内容です。 - 5VSCode のソース管理ペインで差分を確認し、ブランチの発行 で GitLab へ送ります。
- 6GitLab の Code > Merge requests で New merge request を押し、送ったブランチから
mainへ向けて MR を作ります。ここから先は Step 6 と同じです。
- 手元で
/implement-issue 4が終わり、コミットが1つ以上ある - GitLab に自分のブランチが届き、MR が1本立っている
影響範囲
手元で回すと、CI では動かなかったフックが動きます。check-phpver が Edit と Write の後に走り、対象の PHP 7.4 で動かない構文を差し戻します。同じ手順でも、手元と CI で通る範囲が変わることをこの差で確かめてください。Step 4 の表で「CI 側」と書いた行の理由がここにあります。
CLI から CI と同じ引数で流す場合
結果を JSON で受け取る形(-p)は、Claude Desktop の画面からは実行できません。研修の端末には claude の CLI を入れていないので、研修後に CLI を入れた環境で試してください。夜間ジョブと同じ引数で1本流して num_turns と total_cost_usd を数字で取りたい場合は、Claude Desktop の中のターミナル(右上の >_ アイコン、または Ctrl とバッククォート)から次を実行します。研修では上の Claude Desktop の手順で進めます。行末の記号はバッククォートで、PowerShell の行継続です。
claude -p "/implement-issue 4 ブランチは既に作ってあります。ブランチの作成と push は人が行うので、skill の手順3と手順7は実行せず、コミットまでで終えてください。" ` --permission-mode dontAsk ` --allowedTools "Read,Edit,Write,Grep,Glob,Bash(php *),Bash(git add *),Bash(git commit *)" ` --max-turns 15 ` --output-format json | Out-File -Encoding utf8 claude-issue-4.json
Mac の方は行末のバッククォートを \ に替え、| Out-File -Encoding utf8 を > にします。それ以外の引数は同じです。終わったら claude-issue-4.json を VSCode で開き、is_error と num_turns を見ます。このファイルはコミットに入れないでください。ソース管理ペインでステージするファイルを選ぶときに外します。
ハンズオン3 生成物と達成状態
自分で判定できる完了の形
- 講師の1回目と自分の2回目の
nightly_implementのログを追い、artifacts にci_logs/が残っている - 1回目に止まった箇所と、選んだ層と、直し方が台帳に1行で書かれている
- 再実行でブランチと MR ができ、
lint-phpdev・lint-phpver・test・test-phpunitの4つが緑になっている(自分の2回目で届かなかった場合は、講師の1回目の MR で同じ4つを見た) - Issue #4 の受入条件のうち、機械で判定できる3点を自分の目で確かめた
- マージしてよいかを人の判断として下し、根拠3つが計測表に残っている。自動マージの設定を入れていない
- Step 7 の2つの表が埋まり、「回せる」に入れた工程がログで裏付けられている
1回目が止まったまま時間切れになった場合も、止まった箇所の記録と層の選択まで書けていれば到達点に届いています。通すこと自体はこの演習の目的ではありません。
生成物の名前と場所
| ブランチと MR | nightly/issue-4 と、そこから立った MR nightly: Issue #4。GitLab の演習プロジェクトに残ります |
|---|---|
| 実行ログ | ci_logs/claude-issue-4.json と .err。artifacts に1週間残ります。台帳に写す値は先に取り出してください |
| schedule | nightly issue-4(Inactive)。ファイルではなくプロジェクトの設定として残ります |
| 計測表 | 「Day2 夜間ループ」の節。止まった箇所の行と、Step 7 の2つの表が入ります |
| 直した層のファイル | Step 5 で直した1つ。ci/nightly_implement.sh、.gitlab-ci.yml、REVIEW.md、.claude/settings.json のいずれかです |
自社ハーネスへの置き場所
持ち帰るのは5点です。D2 は PowerShell を正、smart3pm は bash を正として運用されていると伺っているので、同じファイルでも置き方が変わります。
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
.gitlab-ci.yml の nightly_implement ジョブ |
対象リポジトリの .gitlab-ci.yml に追記。ISSUE_ID の既定値は自社の Issue 番号へ |
同じ。イメージに PHP が要らない案件では before_script から php-cli を外す |
ci/nightly_implement.sh |
ci/ に置く。手元で流す運用にするなら、同じ引数のまま .ps1 に写す。コメントと文字列は ASCII に保つ |
ci/ にそのまま。bash が正なので書き換え不要 |
.claude/skills/implement-issue/SKILL.md |
.claude/skills/implement-issue/。Issue の取得先を自社のプロジェクトパスへ |
同じ場所。手順5の構文制約の節を、対象リポジトリの実行環境に合わせる |
止め金の .claude/settings.json(deny・hooks・maxEffortLevel) |
既存の settings.json へ deny を追記。hooks の command は powershell -File のまま |
hooks を exec 形式のまま、"command": "bash" と "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/<名前>.sh"] へ差し替える |
pipeline schedule の設定(ISSUE_ID、cron、Target branch) |
対象プロジェクトの Build > Pipeline schedules。ファイルでは持ち出せないので、台帳に設定値を控える | 同じ。複数リポジトリに同じ schedule を作る場合は、台帳側に一覧を持つ |
CI 側で登録する2つの変数
ANTHROPIC_API_KEY と GITLAB_BOT_TOKEN を masked で登録し、Protect は外します。Protect を付けると保護ブランチ以外で変数が渡らず、受講者のブランチでジョブが動きません。MR の作成は CI_JOB_TOKEN では通らないので、プロジェクトアクセストークン(ロール Developer、スコープ api と write_repository)を使います。自社へ写すときはこの逆にします。2つの変数に Protect を付け、鍵を使うジョブは保護ブランチのパイプラインに限ってください。
ハンズオン3 追加と考察
時間が余ったときの追加
- 1
ci/nightly_implement.shの--max-turnsを 15 から 5 に下げ、同じ Issue を流します。どの手順で打ち切られたかをnum_turnsと差分から特定してください。打ち切りは失敗ではなく、Issue の粒度の測り方の1つです。 - 2
.gitlab-ci.ymlのGATE_LEVELをP1からP2に変えて MR を作り直し、ai_gateが落ちる件数の差を見ます。夜間に回すなら、しきい値をどちらに置くかを台帳に理由つきで書いてください。 - 3
PROMPTに書いた「触ってよい範囲」をapp/だけに狭め、同じ Issue を流します。テストが書けずに止まるはずです。範囲を絞りすぎると何が起きるかを、自分の目で見てください。 - 42回の実行の
turnsとcost_usdを並べます。1本あたりの費用が見えると、夜間に何本回せるかが決まります。人が残る3点の「1件あたりの目安」と合わせて、1晩の上限を出してみてください。
研修後に手を付ける範囲
- 1予備の Issue #5 を同じ流れで1本回します。1本目と違う止まり方をしたら、それは Issue の書き方の差です。受入条件が機械で判定できる形になっているかを見比べてください。
- 2Stop hook を CI 側でも効かせます。
ci/settings.ci.jsonは bash 版の PreToolUse と PostToolUse だけを登録していて、Stop は外してあります。runner にテスト用の DB を用意したうえでstop-gate.shを登録する形です。フックの登録を環境で切り替える書き方を、自社ハーネスのsettings.jsonに持たせるところまでが課題です。環境ごとの設計の勘所は、配布物の50_環境別のフック設計/環境別のフック設計.mdにまとめています。 - 3自社リポジトリで schedule を1晩だけ有効にし、翌朝は MR の説明とテストの結果だけを読みます。
- 4フック自身の受入テストを、bash 版にもそろえます。D2 では
hooks/tests/に回帰テストの仕組みが用意されていると伺っています。同じ形を smart3pm 側にも置くと、二重運用のまま片方だけ検査が抜ける状態を抜けられます。
プチ演習7 自社ハーネスの診断
D2-09 / 所要 [15min]
配布した診断シートで自分のハーネスを30項目点検し、5層のうちどこが空いているかを番号で出します。
空いている層が番号で出た状態
目的
自分のハーネスを、印象ではなく確かめた結果で語れるようになります。次に足すものを1つに絞り、研修の翌週に着手できる形にします。
準備
| 開いておく画面 | VSCode。配布フォルダを展開した場所を開いておいてください |
|---|---|
| いるファイル | 60_理想的なハーネスの雛形/自社ハーネス診断シート.md。手元に写しを1つ作り、そちらへ書き込みます |
| 点検の対象 | 1人1式に絞ります。自社のハーネスをお持ちの方はそれを、お持ちでない方は Day1 から今日までに演習リポジトリへ足した設定を対象にしてください |
| 手元にあると早いもの | 自社リポジトリの CLAUDE.md と .claude/settings.json。無くても進められます |
判定は3つだけです。「ある」は、効いていることを確かめた項目にだけ付けてください。置いた記憶があるだけのものは「一部」です。ハーネスは、効いていないことが画面に出ません。思い込みで「ある」が付くと、そこだけ確認が止まります。
自分で考える [2min] シートの表を開く前に、先頭の予想欄を埋めてください。先に書いてから表を開きます。順番を逆にすると、表を読んだ印象がそのまま予想になり、あとで差が取れません。
- 15層のうち、自分のハーネスでいちばん空いていそうな層を1つ書いてください。層は、文脈・権限・フック・CI・リポジトリ保護の5つです。
- 230項目のうち「ある」が付きそうな数を、数字で書いてください。当てるためではなく、後で差を見るために書きます。
手順
Step 1 30項目を埋める [9min]
- 1第1層から順に、判定の欄へ「ある」「一部」「無い」のどれかを書いてください。1項目あたり18秒の見当です。迷ったら「一部」に置いて先へ進みます。
- 2「どうやって確かめるか」の欄に書いてある方法で確かめられるものは、その場で確かめてください。設定ファイルの行数を数える、止めたい入力を1つ流す、といった数十秒で済むものだけです。
- 3確かめられなかった項目には、判定の横に印を1つ付けてください。持ち帰って確かめる分です。
Step 2 3行にまとめる [4min]
- 1シート末尾の表へ、「無い」が一番多かった層を書いてください。層の名前と、その層で「無い」だった項目の番号を並べます。
- 2先頭に書いた予想と結果を見比べ、食い違った項目の番号を書いてください。差が出た場所が、いちばん危ない場所です。守れているつもりだったところだからです。
- 3次の1週間で埋める項目を、1つだけ選んで書いてください。番号と、その項目の「次の一手」の欄にあるファイル名を写します。
達成状態
30項目すべてに判定が入り、末尾の3行が埋まっている状態です。空欄が残っていてよいのは、判定の横に付けた「持ち帰って確かめる」の印だけです。
- 表を開く前に予想を書き、後から書き足していない
- 30項目に判定が入り、確かめた項目と確かめていない項目が区別できる
- 「無い」が一番多かった層を、番号付きで言える
- 予想と結果が食い違った項目の番号を書き出した
- 次の1週間で埋める項目が1つに決まっている
解説と補足
なぜ先に予想を書くか 点検表は、読んだ時点で答えが見えます。先に表を読むと、予想はその写しにしかなりません。差が取れないので、どこを守れているつもりだったかが分からなくなります。この演習で持ち帰るものは点数ではなく、思い込みと実態のずれです。
層の順に埋める必要はない 第1層に「無い」が並んでいても、第3層と第4層が埋まっているなら危険な状態ではありません。逆に、第1層だけが埋まっていて他が空いている状態は、守っているつもりで何も止まっていない状態です。文脈は AI の判断に効くだけで、実行を止める力を持ちません。
30項目の内訳 層ごとの項目数は、その層で作り込める幅にそろえてあります。フックが一番多いのは、実行の直前に割り込んでコマンドの中身まで見られる層だからです。
| 層 | 項目数 | 見ているもの |
|---|---|---|
| 第1層 文脈 | 6 | AI に読ませている決まりの量と重複、禁止に代替が添えてあるか |
| 第2層 権限 | 6 | 止める一覧が空でないか、編集系のツールがそろっているか |
| 第3層 フック | 8 | 並べ替えた形でも止まるか、テストがあるか、道具が無いときに止まるか |
| 第4層 CI | 6 | 提出のときに自動で動くか、落ちる条件が数値で決まっているか |
| 第5層 リポジトリ保護 | 4 | 手元の設定を全部外した人にも効くか、戻す手順が対になっているか |
自社のハーネスを持ってきていない方 Day1 のプチ演習1から3、Day2 のプチ演習4から6で足した設定が点検の対象になります。第1層から第3層は当日の成果物で埋まり、第4層は演習リポジトリの CI、第5層は GitLab の Code > Branches で main の行に付いた protected の表示を見れば判定できます。Developer の権限では Settings のメニューは出ません。自社の分は、この表の写しを持ち帰ってから同じ手順で埋めてください。
「一部」が多くなったとき 多くて構いません。「一部」は、置いてあるが定義どおりには効いていない、または一部の環境でしか効かない状態を指します。WSL と Windows が混在している方は、片方の環境でしか効かない項目がここに集まります。層ごとの落とし穴は、配布物の 50_環境別のフック設計/環境別のフック設計.md にまとめてあります。
追加と考察 時間が余った方は、「ある」を付けた項目から3つ選び、確かめる方法を実際に流してください。1つでも「一部」へ落ちたら、そこが次の1週間で埋める候補に入ります。確かめずに付けた「ある」がどれくらい残っていたかが、そのままハーネスの見通しの悪さです。
発展課題 この30項目のうち、機械で数えられるものに丸を付けてください。行数を数える、規則の本数を数える、テストを流す、といった項目です。丸が付いた項目は、次回からは人が点検せずにスクリプトで出せます。雛形の template/ci/verify-harness.sh が、その考え方を実装したものです。
影響範囲
| 効く範囲 | 点検そのものは何も止めません。この後のプチ演習8で、ここで見つけた層を1つ書き換えます |
|---|---|
| 判定を間違えると何が起きるか | 確かめずに「ある」を付けた項目は、以後の点検から外れます。効いていないまま、守られている扱いで残ります |
| 持ち帰り | 埋めた診断シートは、そのまま自社の点検表として使えます。四半期に1回、同じ表を埋めると差分が見えます |
自社ハーネスへ持ち帰るときの置き場所は、次の表で決めてください。
| 成果物 | 置き場所 | 次にすること |
|---|---|---|
| 埋めた診断シート | 自社リポジトリの docs/ 配下。ハーネスと同じ場所に置き、設定を触った人が見に来られるようにする |
四半期に1回、同じ表を埋め直す |
| 空いている層の番号 | 持ち帰り台帳.md | プチ演習8 で、この中から1層を選ぶ |
| 次の1週間で埋める1項目 | 持ち帰り台帳.md の先頭 | 埋めたら、その項目の「どうやって確かめるか」を流して「ある」に変える |
プチ演習8 雛形の1層の置き換え
D2-09 / 所要 [20min]
診断で空いていた層を1つ選び、雛形の該当ファイルを自社の言葉に直して、テストで止まることを確かめます。
自社の言葉に直した1層が、テストで止まる状態
目的
雛形を読むだけで終わらせず、1層を自社の値に置き換えて動かすところまでを経験します。持ち帰った後は、同じ手順を残りの層へ繰り返すだけになります。
準備
| 開いておく画面 | Claude Desktop(Project folder は配布フォルダの 60_理想的なハーネスの雛形)、その統合ターミナル、VSCode |
|---|---|
| いるファイル | 60_理想的なハーネスの雛形/template/ の一式。書き換えるのはこの中の1ファイルだけです |
| 直前の演習の成果物 | プチ演習7 で埋めた診断シートと、空いている層の番号 |
| 統合ターミナルの開き方 | 右上の >_ アイコン、または Ctrl とバッククォート。セッションの作業ディレクトリで開きます。止まるかの確認はここで流します |
自分で考える [2min] ファイルを開く前に、メモへ2つ書き出してください。
- 1プチ演習7 で「無い」が多かった層のうち、どれを選ぶかを決めてください。迷ったら第3層です。書き換えた結果をテストで見られる層なので、20分で1周できます。
- 2書き換えた後、何が止まるようになるかを1行で書いてください。「安全になる」ではなく、「この操作を流すと終了コード2が返る」の形で書きます。この1行が、Step 4 で確かめる対象になります。
手順
Step 1 層とファイルを決める [2min]
選んだ層に対応するファイルが1つだけあります。この表の右端が、今日触る1ファイルです。
| 層 | 自社の言葉に直すもの | 触るファイル |
|---|---|---|
| 第1層 文脈 | 変更の進め方の表。禁止の行それぞれに、自社で通る代わりの道を書く | template/AGENTS.md |
| 第2層 権限 | 止める一覧と確認を挟む一覧。自社で実際に事故った操作を1件足す | template/.claude/settings.json |
| 第3層 フック | 触られたら困るパスと、止める操作の定義 | template/.claude/rules.json |
| 第4層 CI | 落とす閾値。重大度と確信度の下限を自社の線に合わせる | template/ci/gate.sh |
| 第5層 保護 | 戻す手順。配る手順と同じ粒度にそろえる | template/docs/ROLLBACK.md |
Step 2 直す前に止まり方を控える [2min]
Windows の方は、雛形の入口 guard.ps1 に入力を1件ずつ流して確かめます。Windows の PowerShell では jq も bash も使えないので、テスト一式を回す guard.test.sh は Mac と Git Bash を使える方向けです。
- 1統合ターミナルで、雛形のフォルダ
60_理想的なハーネスの雛形にいることを確かめてから、止める側の入力を1件流してください。1行目をEnterで実行してから2行目を打ちます。'{"tool_name":"Bash","tool_input":{"command":"rm -fr build"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File template\.claude\hooks\guard.ps1 $LASTEXITCODE - 2終了コードが
2で、その上に[guard] blockedと止めた規則の名前が出ることを確認してください。 - 3通す側の入力も1件流し、終了コードが
0であることを確認してください。'{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File template\.claude\hooks\guard.ps1 $LASTEXITCODE - 42件の結果を控えてください。これが基準線です。書き換えた後にこの2件の結果が変わったら、直したのではなく壊しています。
Mac と Git Bash を使える方
テスト一式を流せます。jq が要ります。
bash template/.claude/hooks/guard.test.sh
末尾の件数を控えてください。書き換えた後にこの数を下回ったら、壊しています。
guard.test(sh): 45/45 passed guard.test(ps): skipped (PowerShell not found on this machine)
2行目は、PowerShell が入っていない端末で出ます。同じケースを PowerShell 版でも流す作りになっていて、入っていない端末では飛ばし、飛ばしたことを画面に出します。黙って合格にしません。
Step 3 1ファイルを自社の言葉に直す [10min]
- 1VSCode で Step 1 の表で決めたファイルを開いてください。
- 2自社の値へ直す箇所を2つから3つに絞ってください。第3層を選んだ方は、
write_rulesに自社で触られたら困るパスを1件足し、deny_rulesに自社で実際に事故った操作を1件足します。全体を書き直す必要はありません。 - 3足した規則には、止まったときに人が読むメッセージを必ず書いてください。止めるだけで代わりの道を示さない規則は、回避されます。
- 4Claude Desktop に手伝わせても構いません。そのときは、ファイルの冒頭の1行と、直したい箇所だけを渡してください。ファイル全体を書き直させると、雛形が持っている判定の作りごと変わります。
guard.test.sh へ2件とも足します。通す側を書かないと、全部止める規則を書いても緑になります。Step 4 止めたいものが止まるか確かめる [4min]
- 1統合ターミナルで、Step 2 の2件をもう一度流し、終了コードが
2と0のまま変わらないことを確認してください。Mac と Git Bash の方はbash template/.claude/hooks/guard.test.shを流し、件数が Step 2 以上でNGの行が出ていないことを見ます。 - 2「自分で考える」の2番に書いた1行を、実際に流して確かめてください。
ここに止めたい操作を書き換えてから流し、終了コード 2 が返れば効いています。'{"tool_name":"Bash","tool_input":{"command":"ここに止めたい操作"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File template\.claude\hooks\guard.ps1 $LASTEXITCODE - 3足した規則で止めたくない操作も1件流し、終了コード 0 が返ることを確認してください。通す側を確かめないと、全部止める規則を書いても気づけません。
- 4持ち帰り台帳へ、直した層・触ったファイル・流した入力と終了コードの3つを書いてください。直す前と直した後の両方を書きます。
- 5次に直す層を1つだけ書いてください。日付も入れます。
本数の宣言で落ちたとき
Mac と Git Bash の方が、規則を足した後に bash template/ci/verify-harness.sh を流すと、次のように落ちます。
NG 止める規則 は文書が 12、実装が 13 です
不具合ではありません。README.md の「本数の宣言」に書いた数と、rules.json の実装の数を突き合わせる検査が、ずれを見つけた状態です。README の数を13に直すと通ります。文書に書いた数は、それを数えるスクリプトとセットで置く、という原則8をそのまま動かしたものです。片方だけ直すと落ちる形にしてあります。
達成状態
基準線の2件の結果が変わらず、自分で書いた1行のとおりに止まる状態です。止めたい操作を流したあとの $LASTEXITCODE はこう出ます。
2
Mac と Git Bash で guard.test.sh にケースを足した方は、末尾が guard.test(sh): 47/47 passed のように、45 に足した件数を加えた数になります。
- 書き換えたファイルが1本だけで、他の層に手を付けていない
- 直す前に、止める側と通す側の2件の終了コードを控えてある
- 直した後も、その2件の終了コードが変わらない
- 止めたい操作を手で流し、終了コード 2 が返ることを自分の画面で見た
- 直した層・ファイル・件数と、次に直す層を台帳に書いた
解説と補足
なぜ1層だけにするか 5層を同時に触ると、何が効いて何が効いていないかが分からなくなります。テストが落ちたときも、どの変更が原因かを切り分ける作業から始まります。雛形の使い始めの順番が「1層ずつ入れて、その層が効いていることを確かめてから次へ行く」になっているのは、この切り分けを毎回1回で済ませるためです。
止める対象を設定ファイルに出す理由 雛形は、何を止めるかを rules.json に置き、入口の guard.sh と guard.ps1 には判定の手続きだけを書いています。規則を1本足すときに触るのは rules.json の1か所で、bash 側と PowerShell 側が食い違いません。環境が増えたときも、入口を1本足すだけで済みます。プチ演習4 から6 で作ったフックは判定をスクリプトの中に持っているので、この形が発展形にあたります。
綴りを並べない判定 rm -rf を文字列で照合する書き方は、rm -fr、rm -r -f、rm --recursive --force のいずれも素通りします。雛形はコマンドを語に分解し、オプションを集合にしてから照合するので、これらはすべて r と f の2つになり、規則1本で受けられます。bash -c や xargs を挟んだ形も、包んでいる語を剥がしてから見ます。自社の規則を足すときも、綴りではなく動詞とオプションの組で書いてください。
| 症状 | 手当て |
|---|---|
Mac や Git Bash で jq がありません で落ちる | 判定に使う道具が無いときは通さない作りです。素通りさせないための設計です。jq を入れられない端末では、Step 2 の guard.ps1 に流す形で進めてください。 |
| 足した規則が効かない | rules.json の JSON が壊れていると、ファイルごと読めません。VSCode の赤い波線を先に見てください。 |
| 止めたくないものまで止まる | 動詞を絞っていません。grep -rf patterns.txt src のように、同じオプションを持つ別のコマンドに当たっています。verbs に動詞を明示してください。 |
| 基準線の2件の結果が変わった、テストの件数が減った | 既存の規則かケースを消しています。足すだけにしてください。減った分は、止まらなくなった操作です。 |
第1層や第5層を選んだ方 文書を直す層なので、guard.ps1 に流した結果も guard.test.sh の件数も、直す前と同じになります。それで正しい状態です。確かめるのは、書いた代わりの道が自社で実際に通るかどうかです。「履歴を書き換えない」と書いたなら、打ち消しのコミットを積む手順が自社の運用で通るかを、その場で1人に確かめてください。通らない代替を書くと、禁止だけを書いたのと同じことになります。
追加と考察 直した層と、プチ演習4 から6 で作った手元のフックが、同じ危険を二重に受けているかを見てください。重なっているなら、片方が黙って壊れても素通りしません。どこにも重なりが無い危険があれば、それを1つ書き出してください。その1つが、次に埋める層の候補になります。
発展課題 自社のリポジトリで template/ をコピーし、同じ手順を残りの4層へ繰り返してください。1週間に1層の見当です。4層目まで進んだところで bash ci/verify-harness.sh を流すと、文書と実装のずれがまとめて出ます。ここで落ちた項目が、写している途中で読み替えを間違えた箇所です。
影響範囲
| 効く範囲 | 雛形のフォルダの中だけです。演習リポジトリにも自社リポジトリにも影響しません |
|---|---|
| 壊れると何が壊れるか | rules.json の JSON が壊れると、規則がまとめて読めなくなります。画面には何も出ません。テストを流すことが唯一の観測手段です |
| 自社へ写した後 | 止める規則が増えるほど、誤って止まる操作も増えます。止まったときのメッセージに代わりの道を書いていないと、規則ごと外されます |
| ファイル | D2 での置き場所 | smart3pm での置き場所 |
|---|---|---|
直した template/ の1層 |
リポジトリ直下へコピーし、既存の .claude とは別名で置いてから1層ずつ差し替える |
同じ手順で写す。2つのハーネスで規則の書き方を割らず、rules.json の形をそろえる |
| 足したテストケース | 既存の受入テストへ追記する。件数は README の本数宣言と一緒に直す | 同じケースを足す。片方だけに足すと、通る操作が環境で変わる |
| 直した層と次に直す層の記録 | 持ち帰り台帳.md。日付を入れる | 同じ台帳にまとめる。ハーネスごとに台帳を分けない |
