ハンズオンガイドDay2

AIカスタム研修 /2026年10月6日(火)

Day2のハンズオンの手順です。午前はGitLabで1本のIssueをマージまで手で回し、午後は同じ流れを、IssueからAIが実装するサイクルとして回します。左のメニュー(狭い画面では上の[目次])から、いま進めている節に直接飛べます。

今日の地図

全体

人が手を動かす3か所

今日の研修で人が手を動かすのは3か所だけです。その間の実装、テスト、構文の検査、レビューの前さばきは、AIとパイプラインが持ちます。任せられる理由は、手元のフックとGitLabのゲートが、この3か所を飛ばして先へ進まないように止めているからです。

人1
Issueに受け入れ条件まで書く

何ができたら終わりかを、読んだ人で結果が変わらない形で書きます。AIも同じ文を読みます。

人2
AIに渡してよいと承認する

Issueにラベルai::approvedを付けた時点が承認です。付いていないIssueをAIは実装しません。

人3
MRを読んでマージする

差分、パイプライン、AIレビューを見て、マージするかを人が決めます。mainに直接入れる道はありません。

午前は、この流れをGitLabで1本、手で回します。午後は、同じ流れをIssueからAIが実装するサイクルとして回します。

動く図で見る

人が触る3か所と、1本のIssueがmainに入るまでの流れを、本Projectの仕組みで再生できます。

時間割

このページで進める節

節時間成果物
準備[10min]3点のチェック
1周を手で回す(レベル1)[50min]自分のMRがマージされ、Issueが閉じている
任せて大丈夫な理由(手元)[25min]止まった画面と、止まらなかった画面を1つずつ
サイクルを回す[90min]AIが作った自分のMR
任せて大丈夫な理由(GitLab)[30min]パイプラインで止まった自分のブランチ
運用しながら直す[40min]自社のハーネスを直す順番の表
夜に回す[25min]今夜流れる自分のIssue
クイズ[15min]自分の点数
持ち帰り[10min]
Tips:節の横の分数は、スライドで説明する時間を含めた章の時間で、休憩は含みません。サイクルを回すの節は、全体像の[15min]と、レベル1〜3の[75min]に分かれます。節の途中の休憩は、1周のプチ演習2の後と、サイクルのレベル1にラベルを付けた後の2回です。全体の時間割はDay2トップにあります。
区別

コミットと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が入っていません。講師に伝えてください。

Tips:演習で扱うアプリの画面(見積管理と在庫管理)は、上のメニューの「演習のアプリ」から、ブラウザで操作できます。ユーザー名とパスワードは講師が案内します。

1周を手で回す(レベル1) [50min]

Step 1

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回目です
Issue の画面。右の Assignee に user01、Labels に Doing と level::1。本文の下に Create merge request
手順2が終わった所です。右のAssigneeが自分になり、LabelsにDoingが付いています。次は本文の下のCreate merge requestを押します。
New merge request の画面。From 29-fmt_money-8 into main、題は Draft: Resolve から始まり、Mark as draft にチェック
手順5の画面です。ブランチ名は、題の日本語が落ちて英数字だけが残ります(この例は29-fmt_money-8)。説明にはMRの型が入り、一番下にGitLabがCloses #番号を足しています。
終わった状態
  • Issueの担当が自分になり、ラベルDoingが付いている
  • MRがDraftでできていて、説明の最後にCloses #番号がある(型の途中にある空のCloses #はそのままで構いません)
  • ブランチ名をメモした(Step 4で使います)
できた MR の画面。題の頭に Draft:、Open の印、右に Closing として元の Issue
できたMRです。右のWork itemsのClosingに元のIssueが出ていれば、マージしたときにIssueが閉じます。CommitsとChangesはまだ0です。
Tips:Issueの画面からMRを作ると、ブランチ名にIssue番号が入り、MRの説明にCloses #番号が入ります。マージするとIssueが自動で閉じます。MRを先に作っておけば、最初のpushで走るのはMRのパイプライン1本だけです。
Step 2

C:\devにクローンする [5min]

プロジェクトのトップで Code を押した所。Clone with SSH と Clone with HTTPS の URL が出ている
クローンのURLは、プロジェクトのトップのCodeを押すと出ます。使うのはClone with HTTPSの方で、下の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では出ません)
  • 3destination 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の変更が混ざらない、きれいな状態から始めるためです。

Tips: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の側の設定なので、手元で何をしても外せません。止め金が効くのは、このためです。

Step 3

Claude Desktopで開き、中身をつかむ [7min]

新しいセッションの入力欄。上にローカル、dl-training-app、main、ワークツリー。下に権限のモードの手動
新しいセッションの入力欄です。上にフォルダとブランチ、下に権限のモードが出ます。送る前に、フォルダがdl-training-app、モードが「手動」、右下のモデルがOpus 5.5になっていることを確かめます。
手順
  • 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に足りない所と、書き足す案を返します。案を読んだら、反映は頼まずに次の手順へ進んでください。確認のダイアログが出たときは、許可しない側を選びます
要約の返事。架空の会社の基幹システム、index.php の振り分け、研修の題材の3行と、今いるブランチは main という注意
手順4の返事です。CLAUDE.mdをもとに3行でまとめ、最後に今いるブランチがmainだと添えています。
終わった状態
  • モデルがOpus 5.5になっている
  • 要約が返ってきた
  • /initの案を読み、CLAUDE.mdは変えていない
Tips:送る前に、入力欄の下の権限のモードが「手動」のままかを見ます。研修の準備で試したときは、やり取りの途中で「編集を受け入れる」に変わり、確認のダイアログが出ずに書き込まれたことがありました。変わっていたら、押して「手動」に戻します。
Tips:Claude Desktopに頼めることはGitLabの基礎の「Claude Desktopへの頼み方」に、頼んでも止まることは本Projectの仕組みの「Claude Desktopに頼んだ操作の行き先」にまとめてあります。今日の送る文は、1つの文で1つの操作を頼む形にそろえています。
/initで何が起きているか

/initは、Claudeがリポジトリの構成、ビルドとテストの方法、書き方の約束を読み取り、CLAUDE.mdを作るか直す依頼です。すでにCLAUDE.mdがある場合は、書き足しや直しを提案します。

公式の目安は200行未満です。この研修リポジトリのCLAUDE.mdは55行で、行数の上限を80行と決めています。CLAUDE.mdはどのセッションでも開始時に全文が読まれるので、長くなるほど毎回の文脈を食い、大事な約束が埋もれます。1行ずつ残すかどうかの基準は、その行を消したらClaudeが間違えるかどうかです。フォルダの一覧のように、コードを読めば分かることは書きません。

提案をそのまま取り込まないのは、Claudeが推測で書いた行と、人が決めた約束が混ざるからです。取り込むなら、1行ずつ読んで選びます。

プチ演習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 で元に戻してください。
確認のダイアログ。Run the CLAUDE.md rule checker の run を許可しますか、の下に powershell のコマンド。拒否、常に許可、一度だけ許可のボタン
手順2で出る確認のダイアログです。実行するコマンドが出るので、読んでから「一度だけ許可」を押します。「常に許可」は押しません。撮影したMacの画面で、Windowsではコマンドの形が少し違います。
検査の出力。OK CLAUDE.md: 56 lines (max 80)
手順2の出力です。600文字の行を足しても、行数は56行なのでOKになります。
見る所
  • 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文字まで)と、@が指すファイルがあるかを見ます。手元より厳しくしてあります。

プチ演習2

@で取り込むファイルと、名前で指すだけのファイル [4min]

目的

セッションの開始時に読み込まれるものと、必要になったときにClaudeが開くものの違いを見ます。

自分で考える

送る前に、2と3のそれぞれで、Claudeがファイルを開くかを予想します。

手順
  • 1VSCodeでCLAUDE.mdを開き、2つの行を探します。@docs/context/php-target.mdの行と、「レビューの基準はREVIEW.mdにあります」の行です
  • 2次の文を送りますPHP 7.4 で str_contains は使えますか。ファイルは開かずに、今わかっていることだけで答えてください。
  • 3次の文を送りますこのリポジトリで、レビューの指摘を P1 にする基準を教えてください。
手順3の返事の頭。実行済み 2件のコマンドを開くと、Read review criteria and severity references の行。その下に、REVIEW.md はまだ空という答え
手順3の返事の頭です。「実行済み2件のコマンド」を押して開くと、ファイルを読んだ行が出ます。手順2の返事には、この行が出ません。
見る所
  • 2では、ファイルを読む表示を出さずに「使えない」と答えた
  • 3では、答えの前にファイルを開いた跡が出た(「実行済みn件のコマンド」を開くとReadで始まる行がある)。REVIEW.mdは研修の開始時点では空なので、答えの中身よりファイルを開いたかを見る
解説

@パスと書いたファイルは、セッションの開始時に中身ごと読み込まれます。PHP 7.4の対応表は@で取り込んでいるので、開かなくても答えられます。

文章で名前を書いただけのファイル(REVIEW.md)は読み込まれていません。必要になったときに、開くかどうかをClaudeが決めます。毎回守らせたい約束は@で取り込み、場面で要る知識は名前で指すのが、使い分けの基準です。@を増やすほど、毎回の文脈が重くなります。

ここで休憩です [10min] 休憩の後はStep 4から進めます。Claude Desktopのセッションは開いたままで構いません。
Step 4

gitのフックを入れ、ブランチを切り替える [5min]

確認のダイアログの扱い。 このページで指示の無い確認のダイアログは、コマンドが送った文のとおりかを読んでから「一度だけ許可」を押します。「常に許可」は押しません。
手順
  • 1次の文を送りますgit config core.hooksPath .githooks を実行してください。
  • 2次の文を送ります。番号-… はStep 1でメモしたブランチ名に置き換えますgit fetch してから、ブランチ 番号-… に切り替えてください。切り替えたら、今のブランチ名を教えてください。
手順2の返事。git fetch のあとブランチを切り替え、今のブランチは 29-fmt_money-8 です
手順2の返事です。Claudeがfetchしてからブランチを切り替え、今のブランチ名を教えています。この例のブランチ名は29-fmt_money-8です。
終わった状態
  • Claudeが教えたブランチ名が、Step 1でメモした名前と同じ
なぜgit configの1行が要るのか

コミットの前に動くgitのフック(pre-commit)は、クローンしただけでは配られません。core.hooksPathを設定すると、このリポジトリの.githooksフォルダにあるフックが使われるようになります。設定はPCごと、リポジトリごとなので、クローンし直したらもう一度要ります。

Step 5

テストを書かせて、コミットする [6min]

手順
  • 1GitLabのIssueの本文をコピーし、次の文の「(ここにIssueの本文を貼る)」の所に貼って送ります。番号も自分のIssueの番号にしますGitLab の Issue #番号 を実装してください。本文は次のとおりです。 (ここに Issue の本文を貼る) 受け入れ条件を満たしたら、コミットまでしてください。push はまだしないでください。
  • 2確認のダイアログにファイルの中身が出たら、受け入れ条件の4行と見比べ、合わない所が無いかを読んでから、その1回を許可する側を選びます
  • 3コミットが終わったら、次の文を送りますgit log --oneline -3 と git status を見せてください。
  • 4GitLabのMRのChangesタブを開きます。まだ何も出ていないことを確かめます
git log の3行と git status。一番上が #29 のテストのコミット、Your branch is ahead of origin by 1 commit
手順3の出力です。一番上が今のコミットで、git statusには「originより1コミット進んでいる」と出ます。まだpushしていない印です。
MR の Changes タブ。There are no changes yet
手順4の画面です。コミットした後でも、MRのChangesは0のままです。
見る所
  • git logの一番上に、テストを足したコミットがある
  • MRのChangesタブには、まだ差分が出ていない
ここが今日いちばん大事な区別です。 コミットは手元の履歴に区切りを残しただけで、GitLabには届いていません。MRに差分が出ないのはそのためです。GitLabに届けるのは次のStepのpushです。
Step 6

pushして、パイプラインを見る [10min]

手順
  • 1次の文を送ります。確認のダイアログにgit pushのコマンドが出るので、ブランチ名が自分のものかを確かめてから、その1回を許可する側を選びますpush してください。
  • 2GitLabのMRのPipelinesタブを開きます。パイプラインが走っています
  • 3ジョブが全部緑になるまで待ってください。3〜5分、混んでいると10分ほどかかります。待っている間に、下の表でジョブの役割を読みます
  • 4Changesタブに、自分のテストのファイルが出ていることを確かめます。MRのOverviewに、AIレビューのコメントが1本付きます
確認のダイアログ。Push the branch to origin の run を許可しますか、の下に git push を含むコマンド
手順1で出る確認のダイアログです。コマンドにgit pushが入っています。今のブランチが自分のものかを確かめてから「一度だけ許可」を押します。
ジョブ見ていること赤のときに疑う所
lint-phpdevPHP 8.3で構文が通るか書き間違い
lint-phpver対象のPHP 7.4で構文が通るか、7.4に無い関数を呼んでいないかmatchやstr_containsなど8.xのもの
check-rulesCLAUDE.mdの行数、1行の長さ、@の参照先プチ演習1を戻し忘れた
test既存の簡易テストアプリの側を変えてしまった
test-phpunitPHPUnitのテスト(自分のテストを含む)テストの期待値、PHPUnit 7.5に無いassert(assertMatchesRegularExpressionなど)
ai_reviewAIが差分を読み、指摘をMRにコメントする。コードは変えない。APIが混んでいて断られても、パイプラインは止めないAPIキーの設定かAPIの混雑。黄色の注意の印のままなら手を挙げて講師に伝える
ai_gate重い指摘があるかを判定する。今は赤でもマージは止めない設定指摘の中身をコメントで読む
パイプラインの画面。lint に check-rules、lint-phpdev、lint-phpver、test に test と test-phpunit、review に ai_review、gate に ai_gate。全部緑
緑になったパイプラインです。lintの3本、testの2本、review、gateの順に段が並びます。研修の準備で流したときは、全部が緑になるまで1分ほどでした。
終わった状態
  • パイプラインの7つのジョブが緑(ai_reviewとai_gateは黄色の注意の印でも可)
  • Changesタブに自分のテストのファイルがある
赤になったとき

赤のジョブを押すと、そのジョブのログが開き、下のほうに何行目の何が悪いかが出ます。直し方はClaudeに頼んでください。ログの最後の数行をコピーして「このエラーを直して、コミットしてください」と送り、直ったら「pushしてください」と送ります。コミットとpushが別の手順なのは、ここでも同じです。

パイプラインがなかなか始まらず「pending」のままのときは、Runnerの順番待ちです。18人がほぼ同時にpushしているので、数分待ちます。

Tips:パイプラインの定義(.gitlab-ci.ymlとci/jobs/)はファイルなので、クローンした手元にもあります。手元に無いのは、動いた結果とログです。結果はMRのPipelinesタブで見て、赤のときはログをClaudeに貼ります。同じパイプラインを自社で動かすときの準備は、持ち帰りの「研修のパイプラインの持ち帰り方」にあります。
Step 7

Draftを外し、マージを頼む [4min]

MR のマージの欄。Merge blocked: 1 check failed、Merge request must not be draft.、右に Mark as ready
DraftのままのMRです。マージの欄に「Merge request must not be draft.」と出ています。右のMark as readyを押すとDraftが外れます。
手順
  • 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 のマージの欄。Ready to merge by members who can write to the target branch.、1 commit and 1 merge commit will be added to main、Closes issue #29
手順1の後の画面です。Draftが外れ、マージの欄が緑になりました。受講者の画面にMergeのボタンは出ず、講師がマージします。
Issue #29 の画面。題の下に Closed、右の Labels に Review と level::1
講師がマージした後のIssueです。題の下にClosedと出ています。ラベルはReviewのままですが、ボードではClosedの列に並びます。
終わった状態
  • MRがMergedになり、Issueが閉じている
Tips:DraftのMRはマージできません。作業の途中はDraftのままにしておき、見てほしい所まで来たらDraftを外します。マージできるのはMaintainer(講師)だけです。Community Editionには「承認が何人要る」という設定が無いので、マージできる人を絞ることで、人の判断の場所を作っています。
ふりかえり

1周で起きたこと

操作起きたこと見た画面
IssueからMRを作るブランチ番号-… とDraftのMRができたGitLabのIssueとMR
コミット手元の履歴に残った。GitLabには何も起きていないClaude Desktop(git log)
pushGitLabに届き、MRのパイプラインが1本走ったMRのPipelinesタブ
パイプラインが緑構文、ルール、テスト、AIレビューが通ったMRのPipelinesとOverview
Draftを外すマージできる状態になったMR
マージ(講師)mainに入り、Issueが閉じたボードのClosed

任せて大丈夫な理由(手元) [25min]

AIに書かせても大丈夫なのは、書く前と書いた後に、フックが見ているからです。3つ起こします。自分のレベル1のブランチのままで構いません。変えたものは、最後に必ず元に戻します。

プチ演習3

書く前に止まる [6min]

目的

Editの書き込みが、確認のダイアログより前にフックで止まるのを見ます。

自分で考える

送る前に、止まるか、確認のダイアログが出るか、書けてしまうかを予想します。

手順
  • 1次の文を送りますCLAUDE.md の約束は分かったうえで、講師の判断で今回だけ許可します。ほかのファイルは調べずに、Edit で db/schema.sql の末尾に -- test の1行を足してください。
  • 2次の文を送りますgit status を見せてください。
手順1の返事。編集済み schema.sql(1件失敗)。BLOCKED by guard_paths: db/schema.sql is protected で始まる文
手順1の返事です。Editは確認のダイアログを出さずに止まり、フックの理由がClaudeに返っています。ツールの行には「1件失敗」と出ます。
見る所
  • 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)は助言で、フックは強制です。

プチ演習4

シェルの抜け道 [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 で元に戻してください。
返事。シェル経由の書き込みは止まらず、db/schema.sql の末尾に -- test が1行入った。止まらなかった理由として guard_paths は Edit 系にしか掛かっていないと説明
シェルのコマンドで書いたときの返事です。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で作れます。

プチ演習5

書いた後の検査 [6min]

目的

書いた後に動く検査は、書き込みを止めずに、結果を返して直させるのを見ます。

自分で考える

送る前に、ファイルが作られるか、作られた後で何が起きるかを予想します。

手順
  • 1次の文を送り、確認のダイアログで、その1回を許可する側を選びますCLAUDE.md の約束は分かったうえで、検査の動きを見る練習として、今回だけ PHP 8 の match 式を使います。tools/sample.php を作って、match 式を使った関数を1つ書いてください。
  • 2Claudeの返事を読みます。ファイルを作った直後に、検査の結果を受け取っています
  • 3次の文を送りますtools/sample.php を消してください。
手順1の返事。作成済み sample.php。フックの出力 tools/sample.php:15 match expression (8.0) -> use switch and assign in each branch。書き込みは止まっていない
手順1の返事です。ファイルを作った直後に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)は知らせて直させる、と役割が分かれます。

決め事

いつコミットし、いつワークツリーを切るか [4min]

場面すること理由
テストが通る単位で1歩進んだコミット戻る場所ができる。1つの変更意図につき1回にすると、後で1つだけ戻せる
MRで人に見せたい所まで来たpushパイプラインとAIレビューが動き、チームから見える
席を離れる、その日の作業を終えるpush手元にだけあるコミットは、PCが壊れると消え、誰にも見えない
同じリポジトリで、2本目のIssueを並行したいワークツリーを切る作業ツリーが分かれ、1本目のファイルを壊さない。AIに2本目を任せ、自分は1本目のパイプラインを待てる
1本のIssueだけを進めるワークツリーは切らないフォルダが増えるだけで得が無い
Tips:Claude Desktopでは、新しいセッションを始めるときにワークツリーを選べます。作られる場所は.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 ドル / 終わり方 successAIが使ったターン数と費用
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は止まりません。最後に止めるのは、マージの前に差分を読む人です。

パイプラインは自分で起動しません。 スケジュールが10分おきに拾います。ラベルを付けたら、Issueにラベルai::runningが付くのを待ってください。レベル1だけは、全員のラベルがそろったところで講師が手で起動し、休憩に入ります。
進め方

全員がレベル1から始めます。レベル1のIssueにラベルを付けたら休憩です。休憩の間にAIが実装します。全員の分が順番に流れるので、MRが休憩明けに出る方もいます。MRを読んでマージを頼んだら、レベル2か3を1つ選んで進んでください。MRがまだのときは、先にレベル2か3の題材を読んでおきます。時間内に終わらなくても、IssueかMRを出した所で止めて構いません。

動く図で見る

10月2日の試走で、AIがIssue #30を実装してMRを出した流れと、実装せずに止まった2本を再生できます。

レベル1

型に沿ってIssueを書き、AIに渡す [目安25min]

目的

受け入れ条件を自分で書き、AIがMRを出してマージされるまでを見ます。午前と同じ大きさのIssueです。

題材

午前に自分が取ったIssueの題(「テスト:」の後ろ)と同じ行の題材を使います。ほかの人と重ならないようにするためです。行の位置で引くので、午前の題と午後の呼び出しで関数が違う行もあります。

午前のIssueの題呼び出し結果テストのファイル名
h()が<と>をエスケープするh('a&b')'a&amp;b'tests/phpunit/HAmpInTextTest.php
h()が二重引用符をエスケープするh('<script>')'&lt;script&gt;'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でなければそのまま)、講師にマージを頼みます
New Issue の画面。Description の下で ai-task を選び、本文に背景、やることの見出し。右の Labels に ai::approved と level::1
手順1〜3の画面です。Choose a templateでai-taskを選ぶと、本文に見出しと書き方のヒントが入ります。右のLabelsに2つ付けてからCreate Issueを押します。
終わった状態
  • AIが作ったMRがマージされ、自分のIssueが閉じた
  • 自分が手を動かしたのは、Issueを書くことと、ラベルを付けることと、MRを読むことだけだった
Tips:Issueはボードの + からも作れますが、その作り方では型が入らず、ラベルも付きません。10月2日の試走では、ボードから作ったIssueが本文の無いまま残り、AIに拾われませんでした。Work itemsのNew itemから作り、ラベルai::approvedを付けたかを、作ったあとにIssueの右の欄で確かめてください。
ai::stoppedになったとき

Issueのコメントに止まった理由が出ています。「受け入れ条件が決まっていない」なら、Claudeが挙げた点を本文に書き足してください。「承認のラベルが外れていた」なら、ラベルを付け直します。直したら、ラベルai::stoppedを外してください。次の起動で拾い直します。

受け入れ条件の書き方の例(表の1行目)

自分で書いてから開いてください。

## 受け入れ条件

- tests/phpunit/HAmpInTextTest.php があり、クラス名は HAmpInTextTest
- h('a&b') が 'a&amp;b' を返すことを確かめている
- 変えたファイルは、足したテストの1本だけ
- パイプラインの test-phpunit が緑
レベル2

要望メモから受け入れ条件を書く [目安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営業部取下げの見積を一覧から外す
要望メモ 01-estimate-count.md を GitLab で開いた所。差出人は営業部、検索した結果の件数を一覧の上に出してほしいという2行
メモはGitLabのCodeのRepositoryからdocs/requests/を開くと読めます。どれも2〜3行で、画面のどこに何をどう出すか、どの入力で確かめるかは書いてありません。そこを受け入れ条件に書くのがレベル2です。
自分で考える [5min]

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件)が変わること
Tips:同じメモのMRが2本できたら、先に緑になったほうを講師がマージします。後のMRはマージせずに、差分を見比べてください。08が先にマージされると、01と03の「条件なし」の件数が15件から14件に変わります。
発展

08がマージされた後で、01か03の期待値を書き直すIssueを出し、流し直します。

レベル3

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.ymlCEに無いpush rulesの代わり
MRの説明にClosesが無ければ落とすci/jobs/mr-closes.ymlIssueとマージのつながりを機械で保つ
AIゲートに非常口を付けるci/jobs/review.ymlのai_gateCloudflareのbreak glass
承認の後で本文が変わったIssueを止めるci/approval_check.shとci/jobs/approval-check.ymlsmart3pmの承認レシート(sha256)
PowerShellの書き込みも止めるフックを作る.claude/hooks/guard_shell_write.ps1smart3pmのguard-bash-write.sh
Work items をラベル level::3 で絞り込んだ一覧。レベル3の Issue が5本
Work itemsの検索欄でラベル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の説明にある
Tips:ハーネスやパイプラインを変える作業は、AIのサイクルに任せきりにしません。止め金そのものをAIが書き換えると、何が止まるのかを人が説明できなくなるからです。Claude Desktopで手伝わせても、差分は人が読んで決めます。
Tips:研修に無い仕組みを自社で足すときも、このレベルと同じ流れで進めます。Issueに止めたいことと「わざと違反した場合」を書き、Claude Desktopで作り、違反で赤、直して緑を確かめてから、人がマージを決めます。Runnerを自分たちで持ちたいときの手順の置き場所は、持ち帰りの「研修に無い仕組みとRunnerの足し方」です。

任せて大丈夫な理由(GitLab) [30min]

プチ演習6

読むだけのAIレビュー [6min]

目的

AIレビューは指摘を書くだけで、コードも合否も変えないのを見ます。

自分で考える

開く前に、AIレビューがコードを直すか、コメントを書くだけかを予想します。

手順
  • 1午前の自分のMR(または午後にAIが作ったMR)を開き、AIレビューのコメントを読みます
  • 2Commitsタブを開き、AIレビューが足したコミットが無いこと(並んでいるのが自分かAIの実装のコミットだけであること)を確かめます
  • 3Pipelinesタブで、ai_reviewの次にai_gateが動いていることを見ます
AI レビューのコメント。AIレビュー(モデル、深さ quick、差分 21 行)、P0 0件から P3 0件、指摘はありません、判定は ai_gate ジョブが行います
Step 6のMRに付いたAIレビューのコメントです。指摘が0件でもコメントは1本付き、最後に「判定はai_gateジョブが行います」と書かれています。写真は準備のときのもので、当日のコメントの作成者はai-cycle-botです。
見る所
  • AIレビューは指摘を書いただけで、コードは変えていない
  • 通すかどうかの判定はai_gate、マージするかどうかは人、と分かれている
解説

ai_reviewは読むだけのジョブです。差分を読んで指摘を書き、MRにコメントを1本置きます。コードは変えず、承認もしません。落とすかどうかはai_gateが決め、マージするかどうかは人が決めます。Anthropicの自社のCode Reviewも「承認は人の判断」として、承認はしない作りです。

プチ演習7

手元の検査を飛ばしても、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 に切り替えてください。
手順3の返事。NG CLAUDE.md: 85 lines (max 80)、pre-commit: CLAUDE.md breaks the limits above. The commit was stopped.
手順3の返事です。pre-commitのフックが85行を見つけて、コミットを止めています。
p7 のブランチのパイプライン。lint の check-rules が赤、lint-phpdev と lint-phpver は緑、test の段は動いていない
手順6の画面です。check-rulesが赤になり、後ろのtestの段は動いていません。
MR のマージの欄。Merge blocked: 1 check failed、Pipeline must succeed.
手順7で講師が映すMRです。マージの欄に「Pipeline must succeed.」と出て、マージのボタンが出ません。
見る所
  • 手元のpre-commitは--no-verifyで素通りした
  • GitLabのcheck-rulesが赤になり、MRならマージできない
解説

手元のフックは、そのPCで動く速報です。--no-verifyひとつで飛ばせますし、飛ばしたことはサーバーからは見えません。止め金はGitLabの側に置きます。check-rulesのジョブ、保護されたmain、「パイプラインが通らないとマージできない」の設定の3つです。

パイプライン

組織に要るパイプライン [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の基礎にあります。

動く図で見る

危ない操作ごとに、手元のフックからmainの保護までの8つの層のどこで止まるかを確かめられます。

運用しながら直す [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側に検査が無い
動く図で見る

設定を1つ外すと何が起きるかを、設定の一覧と、10月2日に実際に起きた事故の場面で確かめられます。

プチ演習8

フックの本体が無いとき [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
手順2の返事。guard_paths.ps1 が guard_paths.ps1.off に名前を変えられていて、フックはエラーで終わり、Edit が通ったという説明
手順2の返事です。本体が無いのでフックはエラーで終わり、Editが通りました。Claudeも「止めるはずの仕組みが、黙って止めない状態」と説明しています。
手順5の返事。PreToolUse:Edit hook error: run_hook.ps1 -Name guard_paths: HARNESS MISSING: .claude/hooks/guard_paths.ps1 was not found.
手順5で、ラッパーを通したときの返事です。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)という考え方です。

プチ演習9

承認の指紋 [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 で元に戻してください。
統合ターミナル。new-receipt で APPROVED と sha256、verify-receipt で VALID、計画書を1文字直した後の verify-receipt で INVALID: has changed since it was approved (hash differs)
統合ターミナルの出力です。上から、承認の記録、VALID、1文字直した後のINVALIDです。撮影したMacの画面で、Windowsでは行の頭にPS C:\dev\dl-training-app>が出ます。
見る所
  • 1文字の直しでINVALIDになった
  • 元に戻すとVALIDに戻った
解説

承認のレシートには、承認した時点の計画書のsha256が書かれています。sha256は、ファイルの中身から計算した値です。中身が1文字でも変わると、別の値になります。承認は「その中身」に対して出したものなので、中身が変われば失効します。誤字の直しでも同じです。

GitLabのIssueで同じことをするのが、レベル3の「承認の後で本文が変わったIssueを止める」です。

成果物

自社のハーネスを直す順番の表 [8min]

自社のハーネス(D2かsmart3pm)で起きていること、または起きそうなことを1行、下の4列で書きます。書く場所は自分のPCのメモ帳です。4列とも埋まり、分類が4つのどれか1つに決まっていれば終わりです。

起きたこと分類足す場所確かめ方
Tips:分類に迷ったら、「人に言えば直るか(指示)」「設定で止められるか(権限)」「仕組みで止めるか(ゲート)」「置き場所や版の問題か(環境)」の順に当てはめます。

夜に回す [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)を出しても、誰も答えないからです。

動く図で見る

昼の便と夜の便でIssueがどう流れるかを、一日のシミュレーターで動かせます。

プチ演習10

止める(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 で始まる文
手順1の返事です。確認のダイアログは出ず、フックblock-destructiveが強制pushを止めています。
確認のダイアログ。Push current branch to origin の run を許可しますか、の下に git push
手順3で出る確認のダイアログです。ここでは「拒否」を押します。
見る所
  • 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は、人がいてもいなくても同じように効きます。

今夜

今夜流す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が付き、自分で予想した結果が受け入れ条件に書いてある
Tips:夜に流すなら、受け入れ条件はとくに機械で確かめられる形にします。朝まで誰も質問に答えられないので、曖昧なIssueは質問を返して止まるだけになります。

持ち帰り [10min]

持ち帰り

自社に写すときの順番

  • 1mainを保護し、「パイプラインが通らないとマージできない」を入れる
  • 2workflow:rulesで、ブランチ用とMR用のパイプラインを1本にまとめる
  • 3構文、ルール、テストのジョブを置く。ジョブはci/jobs/に種類ごとのファイルで置く
  • 4AIレビュー(読むだけ)とゲートを分けて置く。ゲートは最初は赤でも止めない設定から始める
  • 5AIが作ったMRにだけ、止め金と触らない場所を変えていないかの検査を置く。一覧はmainの側から読む
  • 6手元のフックを置く。書く前に止める、書いた後に検査する、本体が無ければ止める
  • 7Issueの型と、承認のラベルを決める
  • 8AIの実装サイクルを、昼に手で起動して試す。ログのDENIEDと止まった理由を読んで直す
  • 9夜のスケジュールに載せる
Tips:研修リポジトリの.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が鍵や接続文字列をコードに書いたMRGitLabのテンプレートJobs/Secret-Detection.gitlab-ci.ymlをincludeする。CEでも動き、結果はジョブの成果物に出る
SASTSQLの連結のような、典型的な脆弱性の形テンプレートJobs/SAST.gitlab-ci.yml。対象の言語は版ごとに変わるので、PHPが入っているかを先に確かめる
費用の上限1日に使うAPIの費用が上限を超えたあとの実装ai_pickの前に、その日のコメントにある費用を足し、上限を超えていたら拾わずに終える
止まった印の掃除子パイプラインが作れずにai::runningが残ったIssueai::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.ymlpushで緑になる
2workflow:rules(MRがあるブランチではMRのパイプライン1本だけ)MRで1本しか走らない
3自社の決まりの検査(研修のcheck-rulesにあたるもの)わざと違反して赤になる
4AIレビュー(読むだけ)とゲート(最初は赤でも止めない)MRにコメントが付く
5AIのサイクル(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は登録しない
executorshellは機械の上でジョブを直接動かす。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 の目安を、手順書の形で書いてください。コマンドはまだ実行しないでください。ファイルも作らないでください。

公式の資料: Runnerの作り方と権限、gitlab-runner register、Windowsへの入れ方

ページの先頭へ