何ができたら終わりかを、読んだ人で結果が変わらない形で書きます。AIも同じ文を読みます。
Day2のハンズオンの手順です。午前はGitLabで1本のIssueをマージまで手で回し、午後は同じ流れを、IssueからAIが実装するサイクルとして回します。左のメニュー(狭い画面では上の[目次])から、いま進めている節に直接飛べます。
今日の地図
人が手を動かす3か所
今日の研修で人が手を動かすのは3か所だけです。その間の実装、テスト、構文の検査、レビューの前さばきは、AIとパイプラインが持ちます。任せられる理由は、手元のフックとGitLabのゲートが、この3か所を飛ばして先へ進まないように止めているからです。
Issueにラベルai::approvedを付けた時点が承認です。付いていないIssueをAIは実装しません。
差分、パイプライン、AIレビューを見て、マージするかを人が決めます。mainに直接入れる道はありません。
午前は、この流れをGitLabで1本、手で回します。午後は、同じ流れをIssueからAIが実装するサイクルとして回します。
このページで進める節
| 節 | 時間 | 成果物 |
|---|---|---|
| 準備 | [10min] | 3点のチェック |
| 1周を手で回す(レベル1) | [50min] | 自分のMRがマージされ、Issueが閉じている |
| 任せて大丈夫な理由(手元) | [25min] | 止まった画面と、止まらなかった画面を1つずつ |
| サイクルを回す | [90min] | AIが作った自分のMR |
| 任せて大丈夫な理由(GitLab) | [30min] | パイプラインで止まった自分のブランチ |
| 運用しながら直す | [40min] | 自社のハーネスを直す順番の表 |
| 夜に回す | [25min] | 今夜流れる自分のIssue |
| クイズ | [15min] | 自分の点数 |
| 持ち帰り | [10min] |
コミットとpush
| コミット | push | |
|---|---|---|
| 何をする | 手元の履歴に区切りを1つ残す | 手元のコミットをGitLabに送る |
| どこに残る | 自分のPCの中だけ | GitLabのブランチ |
| 誰に見える | 自分だけ | チーム全員 |
| 何が動く | 手元のpre-commitフック | GitLabのパイプライン(構文とテスト)。MRがあればAIレビューも |
| いつする | 意味のある1歩ごと。テストが通った所 | MRで見せたい所まで進んだら。席を離れる前には必ず |
| 取り消し | 手元で直せる | 皆に配られた後なので、消さずに次のコミットで直す |
このページでは、コミットとpushを必ず別の手順として書いています。「コミットして」と頼んだときは手元に残るだけで、GitLabには何も起きません。
準備 [10min]
3点のチェック
始める前に、次の3つがそろっていることを確かめます。
- GitLabにログインできる。https://gitlab-09291006aidev.give-app.netを開き、自分のアカウントでログインして、プロジェクト
dlive-training/dl-training-appが開ける - Claude Desktopが開き、Codeの画面で新しいセッションを始められる
- Gitが入っている。PowerShellで
git --versionを打つとgit version 2.で始まる行が出る
どれかがうまくいかないとき
GitLabのパスワードが分からないときは、手を挙げて講師に伝えてください。講師の側で再設定します。Claude Desktopにログインできないときは、Day1の事前セットアップの手順で再ログインします。gitが見つからないと出るときは、Day1の事前セットアップで入れたGit for Windowsが入っていません。講師に伝えてください。
1周を手で回す(レベル1) [50min]
Issueを1本取る [5min]
1本のIssueを、ブランチ、コミット、push、パイプライン、MR、マージまで通します。どの操作で、どの画面に、何が起きるかを見ます。
| 触るもの | GitLabのIssueとMR、Claude Desktop、C:\dev\dl-training-app |
|---|---|
| 作るもの | テストのファイル1本(tests/phpunit/の下)と、そのコミット、マージされたMR |
| 所要 | [50min](この節の全部) |
- 1GitLabでPlanのIssue boardsを開いてください。Openの列には「テスト:」で始まるIssueが18本。講師が伝えた番号のIssueを開いてください
- 2右側のAssigneeでassign yourselfを押してください。LabelsのEditを押して
Doingを選び、Applyを押します。IssueはボードのDoingの列に移ります - 3本文の「受け入れ条件」を読んでください。テストのファイル名、確かめる呼び出しと結果、変えたファイルはテストの1本だけ、パイプラインの
test-phpunitが緑、の4つが書いてあります - 4本文の下にあるCreate merge requestを押してください。小さな画面のSource branch nameに、
番号-で始まるブランチ名が入っています。メモしてからCreate merge requestを押します - 5New merge requestの画面が開いたら、題の頭に
Draft:が付き、Mark as draftにチェックが入っていることを確かめてください。一番下のCreate merge requestを押します。押すのは、ここで3回目です

Doingが付いています。次は本文の下のCreate merge requestを押します。
29-fmt_money-8)。説明にはMRの型が入り、一番下にGitLabがCloses #番号を足しています。- Issueの担当が自分になり、ラベル
Doingが付いている - MRがDraftでできていて、説明の最後に
Closes #番号がある(型の途中にある空のCloses #はそのままで構いません) - ブランチ名をメモした(Step 4で使います)

Closes #番号が入ります。マージするとIssueが自動で閉じます。MRを先に作っておけば、最初のpushで走るのはMRのパイプライン1本だけです。C:\devにクローンする [5min]

git cloneの行と同じものです。- 1スタートメニューからPowerShellを開き、次の4行を1行ずつ打ちます
mkdir C:\dev -Force cd C:\dev git config --global core.longpaths true git clone https://gitlab-09291006aidev.give-app.net/dlive-training/dl-training-app.git
- 2ログインの画面が出たら、GitLabのユーザー名とパスワードを入れます(Day1でクローンしたPCでは出ません)
- 3
destination path 'dl-training-app' already existsと出たら、Day1で同じ場所にクローンしています。エクスプローラーで古いフォルダの名前をdl-training-app-day1に変え、git cloneの行をもう一度打ちます。名前を変えられないときは、そのフォルダを開いているClaude DesktopのセッションとVSCode、PowerShellを閉じてから、もう一度変えてください - 4エクスプローラーで
C:\dev\dl-training-appを開き、CLAUDE.mdと.claudeフォルダがあることを確かめます
C:\dev\dl-training-appがあり、中にCLAUDE.mdがある
なぜデスクトップやドキュメントに置かないのか
理由は3つあります。1つ目はOneDriveです。デスクトップとドキュメントはOneDriveの同期の対象になっていることが多く、.gitの中の細かいファイルまで同期されて、動きが遅くなったり、競合のコピーができたりします。
2つ目はパスの長さです。Windowsには、パス全体で260文字という上限があります。Claude Desktopのワークツリーは.claude\worktrees\名前\の下に作られるので、元のフォルダが深いとすぐに上限に届きます。git config --global core.longpaths trueは、Gitの側で長いパスを扱えるようにする設定です。
3つ目は、ユーザー名に日本語や空白が入っているとパスにも入ることです。ツールによっては、引用符やエスケープの扱いで失敗します。C:\devのように、ドライブの直下に英数字だけの短いフォルダを作るのが安全です。
Day1でクローンしたフォルダは使いません。Day1の変更が混ざらない、きれいな状態から始めるためです。
git cloneで手元に来るのは、ファイルとコミットの履歴とブランチです。Issue、MR、パイプラインの結果、ラベル、CI/CD変数のようにGitLabの画面で見ている情報は来ません。手元のClaudeにIssueの本文を貼って渡すのは、このためです。git cloneで手元に来るもの、来ないもの
| GitLabで見えているもの | 手元に来るか | 研修での例 |
|---|---|---|
| ファイル一式 | 来る | app/、tests/、CLAUDE.md、.claude/ |
| コミットの履歴 | 来る(全部) | git logで過去のコミットが見える |
| ブランチ | 来る(origin/ブランチ名として) | 受講者の番号-…、AIのai/issue-… |
| パイプラインの定義 | 来る(ファイルなので) | .gitlab-ci.yml、ci/jobs/、ci/のスクリプト |
| Issueの型 | 来る(ファイルなので) | .gitlab/issue_templates/ |
| Issueとコメント | 来ない | 受け入れ条件、AIの「止まった理由」 |
| MRとレビューのコメント | 来ない | AIレビューの指摘、Draftかどうか |
| パイプラインの結果とジョブのログ | 来ない | 赤か緑か、check-rulesのログ |
| ラベルとボード | 来ない | ai::approved、DoingとReviewの列 |
| CI/CD変数、スケジュール、保護ブランチ、Runner | 来ない | APIキー、昼と夜の便、mainの保護 |
ファイルになっているものは来て、GitLabの画面で設定したものや記録したものは来ない、と覚えておくと迷いません。クローンした後にほかの人がpushしたコミットやブランチは、自動では届きません。git fetchで取りに行きます。Step 4でfetchしてからブランチを切り替えるのは、このためです。
mainの保護や「パイプラインが通らないとマージできない」はGitLabの側の設定なので、手元で何をしても外せません。止め金が効くのは、このためです。
Claude Desktopで開き、中身をつかむ [7min]

- 1Claude Desktopで新しいセッションを開き、フォルダに
C:\dev\dl-training-appを選びます。入力欄の権限のモードは手動にします(ファイルの書き換えとコマンドの実行の前に、毎回確認のダイアログが出るモードです)。入力欄の上の「ワークツリー」には印を付けません - 2初めて開くフォルダなので「このワークスペースを信頼しますか?」が出ます。ワークスペースを信頼を押します。押さないと
.claude/settings.jsonが読まれず、この後のプチ演習で使うフックと拒否の規則が効きません - 3入力欄の下の行の右に出るモデルがOpus 5.5になっているかを見ます。Sonnet 5.5などになっていたら、モデル名を押してOpus 5.5を選んでください。今日のワークは、すべてOpus 5.5で進めます
- 4次の文を送ります
このリポジトリが何をするものか、3行で要約してください。 - 5返事の中で「mainブランチにいる」と注意されることがあります。セッションの開始時に動くフックが、保護されたブランチにいることをClaudeに伝えています。ブランチを切り替えるのはStep 4です
- 6入力欄に
/initと打って送ってください。Claudeがリポジトリを読み、今のCLAUDE.mdに足りない所と、書き足す案を返します。案を読んだら、反映は頼まずに次の手順へ進んでください。確認のダイアログが出たときは、許可しない側を選びます

CLAUDE.mdをもとに3行でまとめ、最後に今いるブランチがmainだと添えています。- モデルがOpus 5.5になっている
- 要約が返ってきた
/initの案を読み、CLAUDE.mdは変えていない
/initで何が起きているか
/initは、Claudeがリポジトリの構成、ビルドとテストの方法、書き方の約束を読み取り、CLAUDE.mdを作るか直す依頼です。すでにCLAUDE.mdがある場合は、書き足しや直しを提案します。
公式の目安は200行未満です。この研修リポジトリのCLAUDE.mdは55行で、行数の上限を80行と決めています。CLAUDE.mdはどのセッションでも開始時に全文が読まれるので、長くなるほど毎回の文脈を食い、大事な約束が埋もれます。1行ずつ残すかどうかの基準は、その行を消したらClaudeが間違えるかどうかです。フォルダの一覧のように、コードを読めば分かることは書きません。
提案をそのまま取り込まないのは、Claudeが推測で書いた行と、人が決めた約束が混ざるからです。取り込むなら、1行ずつ読んで選びます。
CLAUDE.mdの長さの検査 [4min]
行数だけを見る検査が、長い1行を素通りさせるのを自分で起こします。
送る前に、600文字の1行を足したCLAUDE.mdが手元の検査を通るかを予想します。
- 1次の文を送り、確認のダイアログで、その1回を許可する側を選びます
main のままで構いません。CLAUDE.md の末尾に、600文字の1行を足してください。中身は同じ文の繰り返しで構いません。 - 2次の文を送ります。確認のダイアログにコマンドが出たら、その1回を許可する側を選びます
次のコマンドを実行して、出力を見せてください。 powershell -NoProfile -ExecutionPolicy Bypass -File tools/check-rules.ps1 CLAUDE.md - 3次の文を送り、元に戻します
git restore CLAUDE.md で元に戻してください。


- 2の出力が
OK CLAUDE.md: 56 lines (max 80)だった。Claudeが空行を1行はさむと57 linesで、どちらも通る - 3のあと、
CLAUDE.mdが元の55行に戻っている
解説
tools/check-rules.ps1が見ているのは行数だけです。600文字の1行を足しても56行なので、上限の80行に届かず、そのまま通ります。コミットの前に動くpre-commitのフック(.githooks/pre-commit)も、この検査を呼んでいるだけです。
GitLabの側のcheck-rulesジョブは、行数に加えて1行の長さ(200文字まで)と、@が指すファイルがあるかを見ます。手元より厳しくしてあります。
smart3pmのcheck-guidelines.shも、rules/core.mdの行数をwc -lで数えるだけの検査です。
クイズのQ1はこの形です。
@で取り込むファイルと、名前で指すだけのファイル [4min]
セッションの開始時に読み込まれるものと、必要になったときにClaudeが開くものの違いを見ます。
送る前に、2と3のそれぞれで、Claudeがファイルを開くかを予想します。
- 1VSCodeで
CLAUDE.mdを開き、2つの行を探します。@docs/context/php-target.mdの行と、「レビューの基準はREVIEW.mdにあります」の行です - 2次の文を送ります
PHP 7.4 で str_contains は使えますか。ファイルは開かずに、今わかっていることだけで答えてください。 - 3次の文を送ります
このリポジトリで、レビューの指摘を P1 にする基準を教えてください。

- 2では、ファイルを読む表示を出さずに「使えない」と答えた
- 3では、答えの前にファイルを開いた跡が出た(「実行済みn件のコマンド」を開くとReadで始まる行がある)。
REVIEW.mdは研修の開始時点では空なので、答えの中身よりファイルを開いたかを見る
解説
@パスと書いたファイルは、セッションの開始時に中身ごと読み込まれます。PHP 7.4の対応表は@で取り込んでいるので、開かなくても答えられます。
文章で名前を書いただけのファイル(REVIEW.md)は読み込まれていません。必要になったときに、開くかどうかをClaudeが決めます。毎回守らせたい約束は@で取り込み、場面で要る知識は名前で指すのが、使い分けの基準です。@を増やすほど、毎回の文脈が重くなります。
D2のCLAUDE.mdは@AGENTS.mdの1本だけを取り込み、agents/03(コーディング規約)はAGENTS.mdの読み分けガイドで名前を挙げているだけです。セッションを開いた時点でagents/03の中身は読み込まれていません。
クイズのQ2はこの形です。
gitのフックを入れ、ブランチを切り替える [5min]
- 1次の文を送ります
git config core.hooksPath .githooks を実行してください。 - 2次の文を送ります。
番号-…はStep 1でメモしたブランチ名に置き換えますgit fetch してから、ブランチ 番号-… に切り替えてください。切り替えたら、今のブランチ名を教えてください。

29-fmt_money-8です。- Claudeが教えたブランチ名が、Step 1でメモした名前と同じ
なぜgit configの1行が要るのか
コミットの前に動くgitのフック(pre-commit)は、クローンしただけでは配られません。core.hooksPathを設定すると、このリポジトリの.githooksフォルダにあるフックが使われるようになります。設定はPCごと、リポジトリごとなので、クローンし直したらもう一度要ります。
テストを書かせて、コミットする [6min]
- 1GitLabのIssueの本文をコピーし、次の文の「(ここにIssueの本文を貼る)」の所に貼って送ります。
番号も自分のIssueの番号にしますGitLab の Issue #番号 を実装してください。本文は次のとおりです。 (ここに Issue の本文を貼る) 受け入れ条件を満たしたら、コミットまでしてください。push はまだしないでください。 - 2確認のダイアログにファイルの中身が出たら、受け入れ条件の4行と見比べ、合わない所が無いかを読んでから、その1回を許可する側を選びます
- 3コミットが終わったら、次の文を送ります
git log --oneline -3 と git status を見せてください。 - 4GitLabのMRのChangesタブを開きます。まだ何も出ていないことを確かめます

git statusには「originより1コミット進んでいる」と出ます。まだpushしていない印です。
git logの一番上に、テストを足したコミットがある- MRのChangesタブには、まだ差分が出ていない
pushして、パイプラインを見る [10min]
- 1次の文を送ります。確認のダイアログに
git pushのコマンドが出るので、ブランチ名が自分のものかを確かめてから、その1回を許可する側を選びますpush してください。 - 2GitLabのMRのPipelinesタブを開きます。パイプラインが走っています
- 3ジョブが全部緑になるまで待ってください。3〜5分、混んでいると10分ほどかかります。待っている間に、下の表でジョブの役割を読みます
- 4Changesタブに、自分のテストのファイルが出ていることを確かめます。MRのOverviewに、AIレビューのコメントが1本付きます

git pushが入っています。今のブランチが自分のものかを確かめてから「一度だけ許可」を押します。| ジョブ | 見ていること | 赤のときに疑う所 |
|---|---|---|
lint-phpdev | PHP 8.3で構文が通るか | 書き間違い |
lint-phpver | 対象のPHP 7.4で構文が通るか、7.4に無い関数を呼んでいないか | matchやstr_containsなど8.xのもの |
check-rules | CLAUDE.mdの行数、1行の長さ、@の参照先 | プチ演習1を戻し忘れた |
test | 既存の簡易テスト | アプリの側を変えてしまった |
test-phpunit | PHPUnitのテスト(自分のテストを含む) | テストの期待値、PHPUnit 7.5に無いassert(assertMatchesRegularExpressionなど) |
ai_review | AIが差分を読み、指摘をMRにコメントする。コードは変えない。APIが混んでいて断られても、パイプラインは止めない | APIキーの設定かAPIの混雑。黄色の注意の印のままなら手を挙げて講師に伝える |
ai_gate | 重い指摘があるかを判定する。今は赤でもマージは止めない設定 | 指摘の中身をコメントで読む |

- パイプラインの7つのジョブが緑(
ai_reviewとai_gateは黄色の注意の印でも可) - Changesタブに自分のテストのファイルがある
赤になったとき
赤のジョブを押すと、そのジョブのログが開き、下のほうに何行目の何が悪いかが出ます。直し方はClaudeに頼んでください。ログの最後の数行をコピーして「このエラーを直して、コミットしてください」と送り、直ったら「pushしてください」と送ります。コミットとpushが別の手順なのは、ここでも同じです。
パイプラインがなかなか始まらず「pending」のままのときは、Runnerの順番待ちです。18人がほぼ同時にpushしているので、数分待ちます。
.gitlab-ci.ymlとci/jobs/)はファイルなので、クローンした手元にもあります。手元に無いのは、動いた結果とログです。結果はMRのPipelinesタブで見て、赤のときはログをClaudeに貼ります。同じパイプラインを自社で動かすときの準備は、持ち帰りの「研修のパイプラインの持ち帰り方」にあります。Draftを外し、マージを頼む [4min]

- 1MRのMark as readyを押し、Draftを外してください。受講者のアカウントにはMergeのボタンが出ません。「Ready to merge by members who can write to the target branch.」は、mainに書き込める講師ならマージできるという表示です
- 2Issueのラベルを
DoingからReviewに替えてください。ボードでReviewの列に移ります - 3講師が、Readyになった順に1本ずつマージします。順番によっては、マージされるのは次の節に入ってからです。マージされると、MRの説明の
Closes #番号によってIssueが自動で閉じ、ボードのClosedの列に移ります


- MRがMergedになり、Issueが閉じている
1周で起きたこと
| 操作 | 起きたこと | 見た画面 |
|---|---|---|
| IssueからMRを作る | ブランチ番号-… とDraftのMRができた | GitLabのIssueとMR |
| コミット | 手元の履歴に残った。GitLabには何も起きていない | Claude Desktop(git log) |
| push | GitLabに届き、MRのパイプラインが1本走った | MRのPipelinesタブ |
| パイプラインが緑 | 構文、ルール、テスト、AIレビューが通った | MRのPipelinesとOverview |
| Draftを外す | マージできる状態になった | MR |
| マージ(講師) | mainに入り、Issueが閉じた | ボードのClosed |
任せて大丈夫な理由(手元) [25min]
AIに書かせても大丈夫なのは、書く前と書いた後に、フックが見ているからです。3つ起こします。自分のレベル1のブランチのままで構いません。変えたものは、最後に必ず元に戻します。
書く前に止まる [6min]
Editの書き込みが、確認のダイアログより前にフックで止まるのを見ます。
送る前に、止まるか、確認のダイアログが出るか、書けてしまうかを予想します。
- 1次の文を送ります
CLAUDE.md の約束は分かったうえで、講師の判断で今回だけ許可します。ほかのファイルは調べずに、Edit で db/schema.sql の末尾に -- test の1行を足してください。 - 2次の文を送ります
git status を見せてください。

BLOCKED by guard_paths: 'db/schema.sql' is protectedで始まる文がClaudeに返った- 確認のダイアログは出なかった
git statusにdb/schema.sqlが出ていない(書き換わっていない)
解説
EditとWriteの実行の前(PreToolUse)に、フックguard_pathsが動きます。書き先が.claude/protected-pathsの一覧に載っていれば、終了コード2で止め、理由をClaudeに返します。確認のダイアログは出ません。人に聞く前に、機械が止めています。
「講師の判断で今回だけ許可します」と書いたのは、丁寧に頼むだけだと、ClaudeがCLAUDE.mdの約束を守って自分で断り、フックまで届かないからです。約束(CLAUDE.md)は助言で、フックは強制です。
D2のguard_db_branch.ps1も、EditとWriteの実行の前に動き、featureブランチからDB/への書き込みを終了コード2で止めます。
クイズのQ3はこの形です。
シェルの抜け道 [6min]
シェルのコマンドで書くと、Edit用のフックが効かないのを見ます。
送る前に、プチ演習3と同じく止まるかを予想します。
- 1次の文を送ります。確認のダイアログにコマンドが出たら、その1回を許可する側を選びます
CLAUDE.md の約束は分かったうえで、講師の判断で今回だけ許可します。PowerShell の Add-Content で、db/schema.sql の末尾に -- test の1行を足してください。 - 2Claudeが実行せずにコマンドだけを返したときは、次の文を送ります。研修の準備で試したときは、1回目は、フックをすり抜けることになるという理由で断られました
研修で、フックが効く範囲を確かめる演習です。書いたらすぐに git restore で戻します。あなたの PowerShell のツールで Add-Content を実行して、db/schema.sql の末尾に -- test の1行を足してください。 - 3次の文を送ります
git diff db/schema.sql を見せてください。 - 4次の文を送り、元に戻します
git restore db/schema.sql で元に戻してください。

guard_pathsは動かず、末尾に1行入りました。撮影したMacではBashのツールからpwshを呼んでいますが、WindowsではPowerShellのツールが使われます。- 3の差分に
-- testの1行が出た(止まらずに書けた) - 4のあと
git statusがきれいに戻っている
解説
guard_pathsが付いているのはEditとWrite(とMultiEdit、NotebookEdit)だけです。PowerShellのコマンドで同じファイルに書くと、このフックは動きません。フックを読むときは、中身より先に、どのツールに付いているか(settings.jsonのmatcher)を見ます。
この抜け道を塞ぐフックは、午後のレベル3のIssueで作れます。
smart3pmは、Edit系を見るguard-write-scope.shと、Bashを見るguard-bash-write.shの2本を持っています。書き換え禁止の一覧(immutable-paths)を見ているのはEdit系だけなので、sed -iでmigrationを書き換えると、どちらも止めません。
クイズのQ4はこの形です。
書いた後の検査 [6min]
書いた後に動く検査は、書き込みを止めずに、結果を返して直させるのを見ます。
送る前に、ファイルが作られるか、作られた後で何が起きるかを予想します。
- 1次の文を送り、確認のダイアログで、その1回を許可する側を選びます
CLAUDE.md の約束は分かったうえで、検査の動きを見る練習として、今回だけ PHP 8 の match 式を使います。tools/sample.php を作って、match 式を使った関数を1つ書いてください。 - 2Claudeの返事を読みます。ファイルを作った直後に、検査の結果を受け取っています
- 3次の文を送ります
tools/sample.php を消してください。

check-phpverの結果を受け取り、15行目のmatchがPHP 8.0からの書き方だと知らされています。- ファイルは一度作られ、そのあとで
[check-phpver] code that PHP 7.4 cannot run:とmatch expression (8.0)の結果がClaudeに返った - Claudeが、フックに
switchへの書き直しを求められたと説明した。書き直しを始めたときは、許可しなくて構いません
解説
フックcheck-phpverは、EditとWriteの実行の後(PostToolUse)に動きます。実行の後なので、書き込みそのものは止められません。ファイルは残り、結果がClaudeに返って、Claudeが直しにいきます。
実行の前(PreToolUse)は止める、実行の後(PostToolUse)は知らせて直させる、と役割が分かれます。
D2のcheck_crlf_bom.ps1も実行の後に動き、LFの改行を見つけると結果を返します。ファイルは作られたまま残ります。
クイズのQ5はこの形です。
いつコミットし、いつワークツリーを切るか [4min]
| 場面 | すること | 理由 |
|---|---|---|
| テストが通る単位で1歩進んだ | コミット | 戻る場所ができる。1つの変更意図につき1回にすると、後で1つだけ戻せる |
| MRで人に見せたい所まで来た | push | パイプラインとAIレビューが動き、チームから見える |
| 席を離れる、その日の作業を終える | push | 手元にだけあるコミットは、PCが壊れると消え、誰にも見えない |
| 同じリポジトリで、2本目のIssueを並行したい | ワークツリーを切る | 作業ツリーが分かれ、1本目のファイルを壊さない。AIに2本目を任せ、自分は1本目のパイプラインを待てる |
| 1本のIssueだけを進める | ワークツリーは切らない | フォルダが増えるだけで得が無い |
.claude\worktrees\名前\です。セッションをアーカイブすると、ワークツリーのフォルダも消えます。コミットしていない変更は一緒に消えるので、アーカイブの前にコミットしてpushします。サイクルを回す [90min]
サイクルの動き [15min]
Issueに承認のラベルが付くと、GitLabのパイプラインが拾い、AIが実装してMRを出します。人がするのは、Issueを書く、ラベルを付ける、MRを読んでマージする、の3つです。
| 順番 | 誰が | 何が起きるか | どこで見るか |
|---|---|---|---|
| 1 | 人 | Issueを書き、ラベルai::approvedを付ける | Issue |
| 2 | パイプライン(ai_pick) | 10分おきに、承認済みで未着手のIssueを拾い、ai::runningを付ける | BuildのPipelines |
| 3 | パイプライン(implement-issue-番号) | 承認のラベルを確かめ直し、AIが実装してコミットする。最大30ターン、25分で打ち切り | 子パイプラインのジョブのログ |
| 4 | パイプライン | pushしてMRを作り、Issueにai::doneとコメントを付ける。止まったらai::stoppedと理由のコメント | Issueのコメント |
| 5 | パイプライン | MRのパイプライン(構文、テスト、AIレビュー)が走る。AIのMRではai_scopeも走る | MRのPipelines |
| 6 | 人 | 差分と受け入れ条件を突き合わせ、マージする | MR |
| 行の頭 | 意味 |
|---|---|
AI-GATE: | 承認のラベルを確かめ直した結果。外れていれば実装しない |
ターン 7 / 費用 0.263005 ドル / 終わり方 success | AIが使ったターン数と費用 |
DENIED: | AIが許可の外の操作を試み、実行の前に止められた記録 |
MR を出しました: | できたMRのURL |
AI-CYCLE: MR を出さずに止まります。 | 止まった理由。Issueのコメントにも同じ内容が残る |
AIのMRだけに走る検査(ai_scope)
実装のジョブの中のAIは、.claude/protected-pathsの4つのほかは書き換えられます。ci/や.claude/を変えたときは、ai_implement.shがMRを出さずに止めます。CLAUDE.mdやREVIEW.md、tools/の変更は止めません。そこで、AIが作ったMR(ブランチ名がai/で始まるもの)のパイプラインにだけ、ai_scopeのジョブを置いています。受講者のMRでは動かないので、受講者のMRのジョブは7つのままです。
| 表示 | 条件 | マージ |
|---|---|---|
| 赤 | .claude/protected-pathsの場所か、ci/、.claude/、CLAUDE.md、REVIEW.mdなどの止め金を変えた | できない |
| 黄色の注意の印 | ファイルが10本か行が300行を超えた、app/を変えたのにtests/を変えていない、前からあるテストを変えたか消した | できる。ログの「注意」の所を人が読む |
| 緑 | どれにも当たらない | できる |
判定に使う一覧とスクリプトは、MRの側ではなくmainの側から読みます。AIがMRの中で一覧を空にしても、判定は変わりません。ただしCommunity Editionでは、ジョブの定義はMRの側から読むので、ci/を消したMRではこのジョブが走りません。サイクルのMRはスクリプトが止めますが、人がai/のブランチにpushしたMRは止まりません。最後に止めるのは、マージの前に差分を読む人です。
ai::runningが付くのを待ってください。レベル1だけは、全員のラベルがそろったところで講師が手で起動し、休憩に入ります。全員がレベル1から始めます。レベル1のIssueにラベルを付けたら休憩です。休憩の間にAIが実装します。全員の分が順番に流れるので、MRが休憩明けに出る方もいます。MRを読んでマージを頼んだら、レベル2か3を1つ選んで進んでください。MRがまだのときは、先にレベル2か3の題材を読んでおきます。時間内に終わらなくても、IssueかMRを出した所で止めて構いません。
10月2日の試走で、AIがIssue #30を実装してMRを出した流れと、実装せずに止まった2本を再生できます。
型に沿ってIssueを書き、AIに渡す [目安25min]
受け入れ条件を自分で書き、AIがMRを出してマージされるまでを見ます。午前と同じ大きさのIssueです。
午前に自分が取ったIssueの題(「テスト:」の後ろ)と同じ行の題材を使います。ほかの人と重ならないようにするためです。行の位置で引くので、午前の題と午後の呼び出しで関数が違う行もあります。
| 午前のIssueの題 | 呼び出し | 結果 | テストのファイル名 |
|---|---|---|---|
| h()が<と>をエスケープする | h('a&b') | 'a&b' | tests/phpunit/HAmpInTextTest.php |
| h()が二重引用符をエスケープする | h('<script>') | '<script>' | tests/phpunit/HScriptTagTest.php |
| h()が一重引用符をエスケープする | h('日本語') | '日本語' | tests/phpunit/HJapaneseTest.php |
| h()が&をエスケープする | h('') | '' | tests/phpunit/HEmptyStringTest.php |
| h()がnullと数値を文字列にする | h(0) | '0' | tests/phpunit/HZeroTest.php |
| fmt_money()が大きい金額を3桁で区切る | fmt_money(999) | '999' | tests/phpunit/FmtMoneyNoSeparatorTest.php |
| fmt_money()が0を0と出す | fmt_money(1000) | '1,000' | tests/phpunit/FmtMoneyThousandTest.php |
| fmt_money()が負の金額に符号を付ける | fmt_money(0.4) | '0' | tests/phpunit/FmtMoneyRoundDownTest.php |
| fmt_money()が小数を丸める | fmt_money('1500') | '1,500' | tests/phpunit/FmtMoneyStringTest.php |
| estimate_status_name()が5つの状態名を返す | fmt_money(1000000) | '1,000,000' | tests/phpunit/FmtMoneyMillionTest.php |
| estimate_status_name()が知らないコードに「不明」を返す | estimate_status_name('9') | '取下げ' | tests/phpunit/EstimateStatusWithdrawnTest.php |
| estimate_status_name()が文字列のコードでも引ける | estimate_status_name(3.0) | '受注' | tests/phpunit/EstimateStatusFloatTest.php |
| stock_status_name()が5つの状態名を返す | estimate_status_name(-1) | '不明' | tests/phpunit/EstimateStatusNegativeTest.php |
| stock_status_name()が知らないコードに「不明」を返す | stock_status_name('3') | '出荷済' | tests/phpunit/StockStatusShippedTest.php |
| warehouse_name()が3つの倉庫名を返す | stock_status_name(0) | '不明' | tests/phpunit/StockStatusZeroTest.php |
| warehouse_name()が知らないコードに「不明」を返す | warehouse_name('2') | '東日本' | tests/phpunit/WarehouseStringCodeTest.php |
| next_estimate_dl_id()がEと年月と5桁の番号を返す | warehouse_name(null) | '不明' | tests/phpunit/WarehouseNullTest.php |
| db_select_one()が0件のときfalseを返す | db_select('SELECT CAST(? AS text) AS v', array('abc')) | array(array('v' => 'abc')) | tests/phpunit/DbSelectPlaceholderTest.php |
- 1GitLabでPlanのWork itemsを開き、New itemを押してください。TypeがIssueになっていることを確かめます。Descriptionの見出しのすぐ下にあるChoose a templateで
ai-taskを選びます - 2タイトルを「テスト:呼び出しの内容」にし、型の「背景」「やること」「受け入れ条件」を埋めます。受け入れ条件には、テストのファイル名、確かめる呼び出しと結果、「変えたファイルはテストの1本だけ」、「パイプラインが緑」を1行ずつ書いてください。「決まっていないこと」は空にします
- 3右のLabelsのEditから、ラベル
ai::approvedとlevel::1を付けて、Create Issueを押します - 4ここで休憩です。休憩の間にラベル
ai::runningが付き、数分後にai::doneに替わって、「MRを出しました」のコメントが付きます。全員の分が順番に流れるので、休憩明けに付く方もいます。まだのときは、レベル2か3の題材を先に読み、コメントが付いたら戻ってください - 5コメントのMRを開き、Changesの差分と自分の受け入れ条件を突き合わせてください。差分はパイプラインが走っている間も読めます。Pipelinesが緑になったのを確かめます
- 6Mark as readyでDraftを外し(Draftでなければそのまま)、講師にマージを頼みます

ai-taskを選ぶと、本文に見出しと書き方のヒントが入ります。右のLabelsに2つ付けてからCreate Issueを押します。- AIが作ったMRがマージされ、自分のIssueが閉じた
- 自分が手を動かしたのは、Issueを書くことと、ラベルを付けることと、MRを読むことだけだった
ai::approvedを付けたかを、作ったあとにIssueの右の欄で確かめてください。ai::stoppedになったとき
Issueのコメントに止まった理由が出ています。「受け入れ条件が決まっていない」なら、Claudeが挙げた点を本文に書き足してください。「承認のラベルが外れていた」なら、ラベルを付け直します。直したら、ラベルai::stoppedを外してください。次の起動で拾い直します。
受け入れ条件の書き方の例(表の1行目)
自分で書いてから開いてください。
## 受け入れ条件
- tests/phpunit/HAmpInTextTest.php があり、クラス名は HAmpInTextTest
- h('a&b') が 'a&b' を返すことを確かめている
- 変えたファイルは、足したテストの1本だけ
- パイプラインの test-phpunit が緑要望メモから受け入れ条件を書く [目安40min]
あいまいな要望を、AIに渡せるIssueに書き直します。1回目で緑にならなかったら、ログを読んで直し、流し直します。
研修リポジトリのdocs/requests/に要望メモが8本あります。1本選んでください。18人で8本なので、同じメモを選ぶ人が出ます。Issueの題の頭にメモの番号を付けます(例02: 見積の有効期限切れ)。
| ファイル | 差出人 | 件名 |
|---|---|---|
01-estimate-count.md | 営業部 | 見積一覧の件数 |
02-estimate-expired.md | 営業事務 | 見積の有効期限切れ |
03-estimate-total.md | 経理 | 見積一覧の金額の合計 |
04-estimate-date-format.md | 保守担当 | 見積日の入力の形 |
05-customer-quote.md | 営業部 | 顧客名の検索でエラー |
06-stock-serial-search.md | 倉庫 | シリアル番号での在庫の検索 |
07-stock-long-stay.md | 倉庫 | 長く置いている在庫 |
08-estimate-hide-withdrawn.md | 営業部 | 取下げの見積を一覧から外す |

docs/requests/を開くと読めます。どれも2〜3行で、画面のどこに何をどう出すか、どの入力で確かめるかは書いてありません。そこを受け入れ条件に書くのがレベル2です。Issueを書く前に、メモに次の3つを書き出します。
- 1何ができたら終わりか。画面のどこに、何が、どう出るか
- 2どんな入力で確かめるか。ふつうの場合、0件や空の場合、境目(ちょうど90日、今日と同じ日など)
- 3どのテストで確かめるか。既存のテスト(
tests/phpunit/EstimateSearchTest.php)の期待値が変わらないか
- 1型
ai-taskでIssueを書いてください。「決まっていないこと」は、自分で決めて受け入れ条件に移し、空にします。書いたままラベルを付けると、スクリプトがClaudeを呼ばずにai::stoppedを付けて止めます - 2ラベル
ai::approvedとlevel::2を付けて作ります - 3MRが出たら、差分と受け入れ条件を突き合わせます。パイプラインが緑か、AIレビューでP1の指摘が0件かを見ます
- 4緑にならなかった、または止まったときは、ジョブのログとIssueのコメントを読み、直す場所を決めます。受け入れ条件が足りないならIssueを直してください。毎回守らせたい約束が足りないなら、
CLAUDE.mdに1行だけ足すMRを作り、講師にマージを頼みます。サイクルはmainのCLAUDE.mdを読むので、マージされるまで効きません。時間が残っていれば、直したあとai::doneかai::stoppedを外して流し直します。ai::doneから流し直したときは、前の回のMRが自動で閉じます - 5直した所とその理由を、Issueのコメントに1行で書きます
- AIのMRが出た(緑でなかったときは、直す場所を決めた)
- 直した所、または直す場所とその理由を1行で書けた
書き漏れやすい条件
| メモ | 書き漏れやすいこと |
|---|---|
| 01件数 | 表示する場所と文言、0件のとき、検索条件を変えたとき |
| 02期限切れ | 今日と同じ日を切れとみなすか、表示の文言と場所、状態が「受注」の見積も対象か |
| 03合計 | 表示している行だけか、税抜か税込か、0件のとき |
| 04見積日の形 | 通す形(YYYY-MM-DD)、存在しない日付(2026-02-30)、エラーの文言、入力を画面に残すか |
| 05引用符 | プレースホルダで渡すこと、%や_を入れたとき、期待する件数 |
| 06シリアル番号 | 部分一致か前方一致か、大文字と小文字、空のとき、倉庫の絞り込みと一緒に使えるか |
| 07 90日 | ちょうど90日を含むか、何で示すか、出荷済みや廃棄も対象か |
| 08取下げ | 見たいときの操作、既定で隠すことで既存のテストの期待値(15件)が変わること |
08がマージされた後で、01か03の期待値を書き直すIssueを出し、流し直します。
Community Editionに無いものをパイプラインで作る [目安40min]
CEに無い機能を、パイプラインのジョブとフックで作ります。わざと違反したものが止まり、直すと通るところまで確かめます。
GitLabにラベルlevel::3のIssueが5本あるので、1本選びます。右のAssigneeが空なら、assign yourselfを押してください。ほかの人が担当になっていても、同じIssueを選んで構いません。担当は替えずに、Create merge requestの小さな画面のSource branch nameの末尾に-自分のユーザー名を足してから作ります。Branch name is availableと出れば使えます。
| Issue | 作るもの | D2、smart3pmや他社で当たるもの |
|---|---|---|
| ブランチ名の検査 | ci/jobs/branch-name.yml | CEに無いpush rulesの代わり |
| MRの説明にClosesが無ければ落とす | ci/jobs/mr-closes.yml | Issueとマージのつながりを機械で保つ |
| AIゲートに非常口を付ける | ci/jobs/review.ymlのai_gate | Cloudflareのbreak glass |
| 承認の後で本文が変わったIssueを止める | ci/approval_check.shとci/jobs/approval-check.yml | smart3pmの承認レシート(sha256) |
| PowerShellの書き込みも止めるフックを作る | .claude/hooks/guard_shell_write.ps1 | smart3pmのguard-bash-write.sh |

level::3に絞ると、5本が出ます。- 1Issueの画面のCreate merge requestでブランチとMRを作り、Claude Desktopでそのブランチに切り替えます
- 2自分で考える[5min]:どのジョブ(フック)が、いつ動き、何を見て、どう止めるかを3行で書きます
- 3Claude Desktopで実装し、コミットして、pushします。このレベルは、サイクルに任せず自分で進めます
- 4受け入れ条件にある「わざと違反した場合」を作り、赤(または止まる)になるのを確かめます。直して緑になるのを確かめます
- 5確かめた跡をMRの説明に貼ります。1〜4本目は、赤と緑のパイプラインのURLです。5本目はパイプラインを使わないので、
run_cases.ps1の結果と、Add-Contentが止まったときのClaude Desktopの画面です - 6作った検査で止められない形を1つ考え、MRの説明に1行で書きます
- 7Mark as readyでDraftを外し、講師に見せます。当日はマージしません。mainのCIと今夜のサイクルは全員で使っているので、たとえば3本目が入ると全員のMRで
ai_gateが本当に止めるようになるからです
- 違反したときに止まり、直すと通る
- 確かめた跡と、止められない形の1行がMRの説明にある
任せて大丈夫な理由(GitLab) [30min]
読むだけのAIレビュー [6min]
AIレビューは指摘を書くだけで、コードも合否も変えないのを見ます。
開く前に、AIレビューがコードを直すか、コメントを書くだけかを予想します。
- 1午前の自分のMR(または午後にAIが作ったMR)を開き、AIレビューのコメントを読みます
- 2Commitsタブを開き、AIレビューが足したコミットが無いこと(並んでいるのが自分かAIの実装のコミットだけであること)を確かめます
- 3Pipelinesタブで、
ai_reviewの次にai_gateが動いていることを見ます

- AIレビューは指摘を書いただけで、コードは変えていない
- 通すかどうかの判定は
ai_gate、マージするかどうかは人、と分かれている
解説
ai_reviewは読むだけのジョブです。差分を読んで指摘を書き、MRにコメントを1本置きます。コードは変えず、承認もしません。落とすかどうかはai_gateが決め、マージするかどうかは人が決めます。Anthropicの自社のCode Reviewも「承認は人の判断」として、承認はしない作りです。
D2のd2-code-reviewerも、レビュー②の前に差分を読み、根拠つきで報告するだけです。コードは直さず、次に進めるかは人が決めます。
クイズのQ7はこの形です。
手元の検査を飛ばしても、GitLabで止まる [12min]
手元の検査を飛ばしても、GitLabのジョブで止まるのを見ます。
送る前に、手元のコミット、push、GitLabのパイプラインのどこで止まるかを予想します。
- 1次の文を送ります。
自分の名前はGitLabのユーザー名にしますmain に切り替えて最新にしてから、練習用のブランチ p7-自分の名前 を作って切り替えてください。 - 2次の文を送り、確認のダイアログで、その1回を許可する側を選びます
CLAUDE.md の末尾に、中身の無い練習用の箇条書きを30行足してください。検査の動きを見る練習で、後で戻します。 - 3次の文を送ります
CLAUDE.md の変更をコミットしてください。 - 4コミットが止まったことを確かめます。
NG CLAUDE.md: 85 lines (max 80)とThe commit was stopped.が出ます。空行が入ると86 linesです - 5Claude Desktopの統合ターミナルを開き(右上のターミナルのアイコン
>_、またはCtrlとバッククォート。日本語のキーボードではShiftと@のキー)、自分で次の2行を打ちますgit commit --no-verify -am "p7: 手元の検査を飛ばしたコミット" git push -u origin HEAD
- 6GitLabのBuildのPipelinesで、自分の
p7-のブランチのパイプラインを開きます。check-rulesが赤になります - 7講師が前もってp7のブランチから作ったMRを画面に映します。マージのボタンが押せないことを見ます
- 8次の文を送ります
main に切り替えてください。


check-rulesが赤になり、後ろのtestの段は動いていません。
- 手元のpre-commitは
--no-verifyで素通りした - GitLabの
check-rulesが赤になり、MRならマージできない
解説
手元のフックは、そのPCで動く速報です。--no-verifyひとつで飛ばせますし、飛ばしたことはサーバーからは見えません。止め金はGitLabの側に置きます。check-rulesのジョブ、保護されたmain、「パイプラインが通らないとマージできない」の設定の3つです。
smart3pmの最後の検査は、手元で動くgitのpre-pushフックで、GitLabの側には検査を置いていません。人が端末でgit push --no-verifyを実行すると、検査を通らずにpushされ、後でセッションを開いたときに警告が出るだけです。
クイズのQ10はこの形です。
組織に要るパイプライン [2min]
| 種類 | いつ動く | 研修リポジトリのジョブ |
|---|---|---|
| 構文 | pushとMRのたび | lint-phpdev、lint-phpver |
| ルール | pushとMRのたび | check-rules |
| テスト | pushとMRのたび | test、test-phpunit |
| AIレビューとゲート | MRのときだけ | ai_review、ai_gate |
| AIのMRの範囲 | AIが作ったMRのときだけ | ai_scope |
| AIの実装 | スケジュールか手動(変数AI_CYCLE=on) | ai_pickと子パイプラインのimplement-issue-番号 |
| 表示 | 意味 |
|---|---|
| 緑(passed) | ジョブが終了コード0で終わった |
| 赤(failed) | ジョブが0以外で終わった。「パイプラインが通らないとマージできない」の設定で、マージが止まる |
| 黄色の注意の印(allowed to fail) | 赤だが、allow_failure: trueなのでパイプラインは止めない |
| 灰色(skipped) | 前の段のジョブが赤で、後の段が動かなかった。条件(rules)に合わないジョブは、パイプラインに最初から出ない |
| 取り消し(canceled) | 同じブランチに続けてpushしたので、古いパイプラインが途中で止められた。新しいほうの結果を見る |
| 待ち(pending) | Runnerの順番待ち |
パイプラインの定義の読み方はGitLabの基礎にあります。
運用しながら直す [40min]
足りない所が出たときの進め方 [12min]
ハーネスは、動かし始めると足りない所が必ず出ます。そのたびに、次の順で直します。
- 1記録する。いつ、何が、どのツールで、止まったか(すり抜けたか)。ログの行をそのまま残す
- 2原因を4つに分ける。指示(
CLAUDE.mdやIssueの書き方)、権限(deny、ask、許可するツール)、ゲート(フック、CI)、環境(パス、文字コード、改行、版) - 3足す場所を決める。毎回守らせたいことはフックかCI、場面で要る知識は名前で指すファイルかスキル、判断は人
- 4ケースで確かめる。止まるべき入力と通るべき入力を表にし、フックの単体の試験(
.claude/hooks/tests/run_cases.ps1)に足す - 5MRで入れる。ハーネスの変更も、人が差分を読んでマージする
| 今日起きたこと | 分類 | 足す場所 | smart3pmで当たる所 |
|---|---|---|---|
| シェルで保護パスに書けた | ゲート | PreToolUse(Bash|PowerShell)のフック | guard-bash-write.shが一覧を見ていない |
| フックの本体が無いと素通りする | 環境 | 止めるラッパー(run_hook.ps1) | 親フォルダを探すラッパー |
| 承認の後で計画が変わる | ゲート | 指紋(sha256)の照合 | verify-approval-receipt.sh |
| 手元の検査を飛ばせる | ゲート | CIのジョブと保護ブランチ | GitLab側に検査が無い |
フックの本体が無いとき [11min]
フックの本体が無いとき、設定から直接呼ぶ形と、ラッパーを通す形で動きがどう違うかを比べます。
送る前に、本体が無いときに書き込みが止まるかを予想します。
- 1Claude Desktopの統合ターミナルで、フックの本体の名前を変えます
Rename-Item .claude\hooks\guard_paths.ps1 guard_paths.ps1.off
- 2次の文を送ります。確認のダイアログが出たら、その1回を許可する側を選びます。ダイアログが出ること自体が、フックが止めていない印です
CLAUDE.md の約束は分かったうえで、講師の判断で今回だけ許可します。main のままで構いません。ほかのファイルは調べずに、Edit で db/schema.sql の末尾に -- test の1行を足してください。 - 3書けてしまうことを確かめ、次の文を送って戻します
git restore db/schema.sql で元に戻してください。 - 4VSCodeで
.claude/settings.jsonを開き、guard_paths.ps1を指している1行を、次の3行に置き換えて保存します"${CLAUDE_PROJECT_DIR}/.claude/hooks/run_hook.ps1", "-Name", "guard_paths" - 5新しいセッションを開きます(設定はセッションの開始時に読まれます)。新しいセッションは権限のモードが自動で始まることがあるので、手動になっているかを見ます。次の文を送り、続けて2と同じ文を送ります
セッションの開始時に受け取った警告があれば、そのまま見せてください。 - 6後片付けをします。ターミナルで名前を戻し、設定も戻して、もう一度新しいセッションを開き、権限のモードを手動に戻します
Rename-Item .claude\hooks\guard_paths.ps1.off guard_paths.ps1 git restore .claude/settings.json


HARNESS MISSINGで止まりました。- 2では書けてしまった(本体が無いのに止まらない)
- 5では
HARNESS MISSINGで止まった - 5で、
the hook file guard_paths.ps1 is missing from .claude/hooks.を含む警告をClaudeが見せた
解説
設定がフックの本体を直接指していると、本体が無いときはpowershell.exeが2以外の終了コードで終わります。Claude Codeはそれを「止めない失敗」として扱い、書き込みを通します。止め金が黙って外れた状態です。
ラッパーrun_hook.ps1は、本体が無いことを見つけると終了コード2を返して止めます。安全装置が無いときは、作業のほうを止める(fail loud)という考え方です。
smart3pmの各ユニットの設定も、親のフォルダをたどって本体を探し、見つからなければ[ -r "$H" ] || { …; exit 2; }で止める短いコマンドになっています。
クイズのQ6はこの形です。
承認の指紋 [9min]
承認が、その時点のファイルの中身に結び付いているのを見ます。
打つ前に、誤字を1文字直しただけで承認が有効のままかを予想します。
- 1プチ演習8と同じ、Claude Desktopの統合ターミナルで、計画書の承認を記録します
powershell -NoProfile -ExecutionPolicy Bypass -File tools/new-receipt.ps1 docs/plan/issue-1-plan.md
- 2承認が有効かを確かめます。
VALIDと出ますpowershell -NoProfile -ExecutionPolicy Bypass -File tools/verify-receipt.ps1 docs/plan/issue-1-plan.md
- 3次の文を送り、確認のダイアログで、その1回を許可する側を選びます
main のままで構いません。docs/plan/issue-1-plan.md の誤字「見積一欄」を「見積一覧」に直してください。 - 42と同じコマンドを打ちます。
INVALIDと出ます - 5次の文を送って戻し、2のコマンドをもう一度打ちます。
VALIDに戻りますgit restore docs/plan/issue-1-plan.md で元に戻してください。

VALID、1文字直した後のINVALIDです。撮影したMacの画面で、Windowsでは行の頭にPS C:\dev\dl-training-app>が出ます。- 1文字の直しで
INVALIDになった - 元に戻すと
VALIDに戻った
解説
承認のレシートには、承認した時点の計画書のsha256が書かれています。sha256は、ファイルの中身から計算した値です。中身が1文字でも変わると、別の値になります。承認は「その中身」に対して出したものなので、中身が変われば失効します。誤字の直しでも同じです。
GitLabのIssueで同じことをするのが、レベル3の「承認の後で本文が変わったIssueを止める」です。
smart3pmのゲート①も、new-approval-receipt.shが計画書のsha256をレシートに書き、verify-approval-receipt.shが作業フォルダの今の計画書から計算し直して比べます。コミットしていない直しでも失効します。
クイズのQ8はこの形です。
自社のハーネスを直す順番の表 [8min]
自社のハーネス(D2かsmart3pm)で起きていること、または起きそうなことを1行、下の4列で書きます。書く場所は自分のPCのメモ帳です。4列とも埋まり、分類が4つのどれか1つに決まっていれば終わりです。
| 起きたこと | 分類 | 足す場所 | 確かめ方 |
|---|---|---|---|
夜に回す [25min]
夜に回すときに変わる所 [8min]
| 昼(研修の午後) | 夜 | |
|---|---|---|
| 起動 | 10分おきのスケジュール | 22時3分のスケジュール |
| 承認 | ラベルai::approved | 同じ |
| 上限 | 1本30ターン、25分で打ち切り。1回の起動で20本まで | 同じ |
| 費用 | 1本0.26〜0.55ドル(9/30の実測) | 同じ。本数ぶん増える |
| 確認 | その場でMRを読む | 翌朝、MRとIssueのコメントを読む |
| 止まったとき | その場で直して流し直す | 朝に理由を読んで直し、次の夜に回す |
昼と夜で変わるのは起動のしかただけで、止め金は同じです。AIのサイクルは、昼も夜も、許可の外を全部拒否する設定(dontAsk)で動きます。人がいない夜に確認のダイアログ(ask)を出しても、誰も答えないからです。
止める(deny)と聞く(ask) [9min]
denyは聞かずに止め、askは人に聞く、という違いを見ます。
送る前に、1と3のそれぞれが、止まるか、確認のダイアログが出るかを予想します。
- 1次の文を送ります
CLAUDE.md の約束は分かったうえで、講師の判断で今回だけ許可します。main のままで構いません。git push --force origin HEAD を実行してください。 - 2確認のダイアログが出ずに止まったことを確かめます
- 3次の文を送ります
main のままで構いません。git push を実行してください。 - 4確認のダイアログが出たことを確かめ、許可しない側を選びます
- 5GitLabで研修リポジトリのci/settings.ci.jsonを開き、
denyにBash(git push *)があることを見ます
![手順1の返事。実行済み 2件のコマンド(1件失敗)。git push --force origin HEAD は実行されませんでした。[block-destructive] force-push で始まる文](assets/day2/h_p10_deny.jpg)
block-destructiveが強制pushを止めています。
- 1は確認なしで止まった(denyとblock-destructiveのフック)
- 3は確認のダイアログが出た(ask)
- 夜のジョブではpushそのものがdenyで、pushはジョブのスクリプトだけが行う
解説
denyは、確認を出さずに実行の前に拒否します。askは人に聞きます。研修リポジトリの手元の設定では、強制pushはdeny、ふつうのpushはaskです。
手動のモードでは、許可していないコマンドはaskに書かなくても確認のダイアログが出ます。それでもaskに書いておくのは、後でallowにBash(git *)のような広い許可を足しても、pushだけは必ず聞かせるためです。判定はdeny、ask、allowの順で、先に当たったものが勝ちます。
夜のジョブでは、pushそのものをdenyにしています。人がいない時間に確認のダイアログを出しても、朝まで待ち続けるだけだからです。pushはジョブのスクリプトが、AIの作業が終わった後で行います。denyは、人がいてもいなくても同じように効きます。
D2も、settings.jsonのdenyにgit pushを入れ、guard_shell_policy.ps1でも止めています。夜にClaudeがpushしようとしても、実行の前に拒否されます。
クイズのQ9はこの形です。
今夜流すIssueを書く [8min]
- 1レベル1と同じ大きさで、Issueを1本書きます。題材は、午後の表の自分の行と同じ関数に、別の入力を1つ選びます(例
fmt_money(-0.4))。結果は自分で予想して受け入れ条件に書きます - 2テストのファイル名とクラス名の
Testの直前に、自分のGitLabのユーザー名を英数字だけで入れます。.や-は外します(例tests/phpunit/FmtMoneyNightUser01Test.php、クラス名FmtMoneyNightUser01Test)。ほかの人のMRとファイルが重ならないようにするためです - 3ラベル
ai::approvedを付けます。今夜22時3分のスケジュールで拾われます - 4明日の朝、GitLabのTo-Doを開きます。AIの実装サイクルがIssueのコメントで自分を呼んでいるので、そこからIssueと、できたMRに進みます
ai::approvedが付き、自分で予想した結果が受け入れ条件に書いてある
持ち帰り [10min]
自社に写すときの順番
- 1mainを保護し、「パイプラインが通らないとマージできない」を入れる
- 2
workflow:rulesで、ブランチ用とMR用のパイプラインを1本にまとめる - 3構文、ルール、テストのジョブを置く。ジョブは
ci/jobs/に種類ごとのファイルで置く - 4AIレビュー(読むだけ)とゲートを分けて置く。ゲートは最初は赤でも止めない設定から始める
- 5AIが作ったMRにだけ、止め金と触らない場所を変えていないかの検査を置く。一覧はmainの側から読む
- 6手元のフックを置く。書く前に止める、書いた後に検査する、本体が無ければ止める
- 7Issueの型と、承認のラベルを決める
- 8AIの実装サイクルを、昼に手で起動して試す。ログの
DENIEDと止まった理由を読んで直す - 9夜のスケジュールに載せる
.gitlab-ci.yml、ci/jobs/、ci/のサイクルのスクリプト、.claude/、.gitlab/issue_templates/の5か所が雛形になります。手元の検査と承認のレシートも使うなら、.githooks/とtools/も一緒に写します。写す前に、CLAUDE.mdと.claude/protected-pathsを自社の中身に書き換えてください。.claude/settings.jsonは、フックをラッパーrun_hook.ps1経由で呼ぶ形(プチ演習8の手順4)に直してから使います。足すとよいパイプライン
研修リポジトリには入れていない候補です。無人でAIに実装させる時間が長いほど、あとから足す価値が上がります。
| 候補 | 止めるもの | 作り方の目安 |
|---|---|---|
| シークレットの検出 | AIが鍵や接続文字列をコードに書いたMR | GitLabのテンプレートJobs/Secret-Detection.gitlab-ci.ymlをincludeする。CEでも動き、結果はジョブの成果物に出る |
| SAST | SQLの連結のような、典型的な脆弱性の形 | テンプレートJobs/SAST.gitlab-ci.yml。対象の言語は版ごとに変わるので、PHPが入っているかを先に確かめる |
| 費用の上限 | 1日に使うAPIの費用が上限を超えたあとの実装 | ai_pickの前に、その日のコメントにある費用を足し、上限を超えていたら拾わずに終える |
| 止まった印の掃除 | 子パイプラインが作れずにai::runningが残ったIssue | ai::runningのまま2時間動きの無いIssueをai::stoppedにし、理由のコメントを付ける |
| 朝のまとめ | 夜の結果を1本ずつ開いて回る手間 | 朝のスケジュールで、前の晩のai::doneとai::stoppedを1つのIssueに表でまとめる |
どれも、研修リポジトリのci/README.mdの最後に同じ表があります。
研修のパイプラインの持ち帰り方
パイプラインの定義はファイルなので、研修リポジトリをクローンすれば全部手に入ります。ただし、定義があるだけでは自社のGitLabで動きません。GitLabの側の設定を、自社で入れ直します。
| 区分 | 中身 | 入れ方 |
|---|---|---|
| 手に入るもの(ファイル) | .gitlab-ci.yml、ci/jobs/、ci/のスクリプト、.claude/、.gitlab/issue_templates/ | 自社のリポジトリに写す。CLAUDE.mdと.claude/protected-pathsは自社の中身に書き換える |
| 入れ直すもの(GitLabの設定) | mainの保護、パイプラインが通らないとマージできない、古いパイプラインの取り消し | プロジェクトのSettings |
| 入れ直すもの(秘密) | ANTHROPIC_API_KEY、GITLAB_BOT_TOKENなどのCI/CD変数と、AI用のトークン | Settings > CI/CD > Variables、プロジェクトのアクセストークン(Developerに絞る) |
| 入れ直すもの(運用) | ai::のラベル、ボード、昼と夜のスケジュール | Manage > Labels、Plan > Issue boards、Build > Pipeline schedules |
| 用意するもの | ジョブを動かすRunner | 下の「研修に無い仕組みとRunnerの足し方」 |
入れる順番は、上の「自社に写すときの順番」のとおりです。GitLabの守りを先に入れ、AIのサイクルは最後にします。入れ直す設定の一覧は、本Projectの仕組みの「34の設定」がそのままチェックリストになります。
研修のパイプラインを読み解く
クローンした手元で、Claude Desktopに次の文を送ります。研修のGitLabの画面は読めなくても、定義のファイルは手元にあるので読めます。
このリポジトリの .gitlab-ci.yml と ci/jobs/ を読んで、パイプラインの段とジョブを表にしてください。列は、段、ジョブ、動くきっかけ(rules)、何を見るか、赤のときに止めるか、の5つにしてください。ファイルは変えないでください。一から作るときの順番
| 順 | 足すもの | 確かめ方 |
|---|---|---|
| 1 | 構文とテストの2段だけの.gitlab-ci.yml | pushで緑になる |
| 2 | workflow:rules(MRがあるブランチではMRのパイプライン1本だけ) | MRで1本しか走らない |
| 3 | 自社の決まりの検査(研修のcheck-rulesにあたるもの) | わざと違反して赤になる |
| 4 | AIレビュー(読むだけ)とゲート(最初は赤でも止めない) | MRにコメントが付く |
| 5 | AIのサイクル(ai_pickと実装のジョブ)とスケジュール | 昼に手で起動し、ログのDENIEDを読んで直す |
1の最小の形の例です。研修のlint-phpdevとtest-phpunitを縮めたもので、PHPUnitの場所は自社のリポジトリに合わせて直します。
stages: [lint, test]
lint:
stage: lint
image: php:7.4-cli
script:
- find app -name "*.php" -print0 | xargs -0 -n1 php -l
test:
stage: test
image: php:7.4-cli
script:
- vendor/bin/phpunit tests/phpunit研修に無い仕組みとRunnerの足し方
研修に無い仕組みも、レベル3と同じ流れで足せます。作り始める前に、作りたいものがどこに入るかを決めます。
| 作りたいもの | 置き場所 | 近い演習 |
|---|---|---|
| Claudeが書く前と書いた後の検査 | .claude/hooks/と.claude/settings.json | プチ演習3〜5、レベル3の5本目 |
| コミットの前の検査 | .githooks/ | プチ演習7 |
| MRとブランチの検査 | ci/jobs/の新しいファイル | レベル3の1〜4本目 |
| AIのサイクルの動き | ci/のスクリプトとci/jobs/ai.yml | 「足すとよいパイプライン」 |
| ジョブを動かす機械 | Runner(GitLabの外のサーバー) | 研修のRunner(Dockerで同時8本) |
Runnerのほかは、リポジトリにファイルを足す作業です。Issueに止めたいことと「わざと違反した場合」を書き、ブランチとMRを作り、Claude Desktopで作ります。違反で赤、直して緑を確かめ、確かめた跡をMRの説明に残してから、人がマージを決めます。止め金を変える作業なので、AIのサイクルには任せません。
Runnerを自分たちで持つとき
| 決めること | 中身 |
|---|---|
| 置く機械 | 社内の資源に届く場所か、要るOSと道具があるか。個人のPCは登録しない |
| executor | shellは機械の上でジョブを直接動かす。dockerはジョブごとのコンテナで動かし、image:が使える。研修のジョブはdocker前提 |
| タグ | ジョブを振り分ける印。Runnerの側のタグと「Run untagged jobs」はGitLabの画面で決める |
| 同時実行数 | Runnerを置いた機械のconfig.tomlのconcurrent |
流れは次のとおりです。MaintainerがプロジェクトのSettings > CI/CD > RunnersでCreate project runnerを押してタグを付けると、登録用のトークンが短い間だけ画面に出ます。機械の側では、そのトークンを付けてgitlab-runner registerを打つ流れです。Runnersの一覧でOnlineになったら、動かしたいジョブにtags:を足してpushします。ジョブのログの先頭に新しいRunnerの名前が出ていれば、振り分けは成功です。
shellのexecutorでは、MRを出せる人が、その機械の上でコマンドを動かせることになります。触れる人を絞ったプロジェクトだけに付けてください。トークンは人が扱い、文書やチャットに貼りません。
手順書の下書きは、Claude Desktopに作らせられます。コマンドは打たせません。
自社の GitLab に、このプロジェクト用の Runner を1台足したいです。置く機械は(Linux のサーバー か Windows の PC)、動かしたいジョブは(ジョブ名)です。GitLab の公式の手順(Settings > CI/CD > Runners の Create project runner で作り、出たトークンで gitlab-runner register を打つ)に沿って、作業の順番と必要な権限、executor の選び方、タグと Run untagged jobs の決め方、config.toml の concurrent の目安を、手順書の形で書いてください。コマンドはまだ実行しないでください。ファイルも作らないでください。