人が触る3か所
Issueを書く
型に沿って、背景、やること、受け入れ条件を書きます。受け入れ条件は、読んだ人によって結果が変わらない形にします。AIも同じ文を読むからです。
ラベルで承認する
ラベルai::approvedを付けた時点が、AIに任せてよいという承認です。付いていないIssueを、AIは拾いません。
MRを読んでマージする
差分を受け入れ条件と突き合わせ、mainに入れるかを人が決めます。マージのボタンは、パイプラインが通ったMRにしか出ません。
その間の実装、テスト、レビューの前さばきは、AIとパイプラインが受け持ちます。昼は10分おき、夜は22時のスケジュールで動き、流れはどちらも同じです。Issueを夕方までに承認しておけば、夜のうちにMRができ、朝の人はMRを読むだけで済みます。
手元のClaudeとサイクルのClaude
研修で動くClaudeは2つあります。受講者が一日中使うのは、自分のPCのClaude Desktopです。午後のAIのサイクルで動くのは、GitLabのパイプラインの中のclaude -pです。同じClaude Codeでも、頼む人と、任せる範囲が違います。
| 項目 | 手元のClaude Desktop | サイクルのClaude |
|---|---|---|
| 動く場所 | 受講者のPCのC:\dev\dl-training-app | GitLabのRunner。ブランチはai/issue-番号-… |
| 頼む人 | 受講者が、入力欄に文を送って頼む | 人がai::approvedを付けたIssueを、ジョブが渡す |
| モデル | Opus 5.5 | Sonnet 5(claude-sonnet-5。変数AI_MODELで固定) |
| 権限 | 手動。書き換えとコマンドの前に、確認のダイアログが出る | dontAsk。ダイアログを出さず、許可の外を全部拒否する |
| Gitの操作 | ブランチの切り替え、コミット、pushを、頼まれた分だけ行う。pushは毎回ダイアログで聞く | 書き込みはgit addとgit commitだけ。pushとMRの作成は、ジョブのスクリプトが行う |
| 設定の場所 | .claude/settings.json | ci/ai_implement.shのclaude -pの引数と、ci/settings.ci.json(.claude/settings.jsonに足して読む) |
| できないこと | mainへのpush、MRのマージ、GitLabの画面を読むこと | push、curl、触らない場所の変更。1本30ターン、25分で打ち切る |
受講者がGitLabの画面とClaude Desktopをどう使い分けるか、Claude Desktopへの頼み方は、GitLabの基礎の「GitLabの画面とClaude Desktopの分担」にまとめています。
1本のIssueがmainに入るまで
2026年10月2日の試走で、Issue #30が通った道です。書いた人をAさんとして追います。再生を押すと札が箱から箱へ移り、その場で動いた仕組みが吹き出しで話す作りです。下の段を押すと、その段へ飛べます。
- Aさんが、型
ai-taskでIssueを書きます。受け入れ条件は、テストのファイル名、確かめる呼び出しと結果、パイプラインが緑、変えるのはテスト1本だけ、の4行です。Aさんwarehouse_name('')が「不明」を返すことを、テストで固定したい。 - Aさんがラベル
ai::approvedを付けました。ここが人の承認で、付けるまでAIは何もしません。Aさん受け入れ条件まで書けたので、AIに任せます。 - 講師がmainで、変数
AI_CYCLEをonにしてパイプラインを動かします。研修の当日は、スケジュールが10分おきに同じことをします。GitLabAIのサイクル用に、パイプライン#89を作りました。 ai_pickが、承認済みで未着手のIssueを探します。見つけた3本にai::runningを付け、1本に1つのジョブを並べた子パイプラインを作りました。ai_pickのログ承認済みで未着手の Issue: 3 本- Runnerが、子パイプラインの3つのジョブを同時に動かします。Runnerは研修用のEC2の上にあり、8本まで同時に動かせる設定です。Runnerimplement-issue-30、31、32を並べて動かします。
- ジョブはラベルを確かめ直してから、Claudeに実装させます。Claudeに渡すのは、ファイルの読み書きと
php、git add、git commit、git status、git diffだけです。implement-issue-30のログターン 7 / 費用 0.13 ドル / 終わり方 success - ジョブのスクリプトがpushしてMRを作り、Issueにコメントを付けます。Claudeにはpushの権限を渡していないので、ここはスクリプトだけの仕事です。ai-cycle-bot@AさんAIの実装サイクルがMRを出しました。
- MRのパイプラインが走ります。構文、ルール、テスト、AIレビューに加え、AIのMRでだけ
ai_scopeも動きました。ai_scopeのログOK 触ってはいけない場所は変えていません。大きさも目安の中です。 - Aさんが差分を開き、受け入れ条件と突き合わせます。変わったファイルは
tests/phpunit/WarehouseNameEmptyTest.phpの1本で、14行でした。Aさんテストが1本だけで、受け入れ条件のとおりです。 - 講師がマージします。MRの説明の
Closes #30によって、Issueは自動で閉じました。GitLab!13をmainに入れ、Issue #30を閉じました。
試走の記録
10月2日に、書き方の違う3本のIssueを1回の起動で流しました。3本は同時に動き、どれも1分前後で終わっています。
| Issue | 書き方 | 結果 | 時間 | ターンと費用 |
|---|---|---|---|---|
| #30 | 受け入れ条件を4行で書いた | 通るMR !13ができ、人が読んでマージ | 61秒 | 7ターン、0.13ドル |
| #31 | 受け入れ条件は「見やすくなっている」の1行だけ | 止まる実装せずに、質問を4つ返した | 63秒 | 5ターン、0.07ドル |
| #32 | 「決まっていないこと」に1行を書いた | 止まるClaudeを呼ぶ前に、ジョブが止めた | 48秒 | 0ターン、0ドル |
上限は1本あたり30ターンと25分です。9月30日の試走では、状態コードを1つにまとめる広めのIssueが、19ターンと約0.55ドルでコミットまで進みました。
研修リポジトリの地図
dlive-training/dl-training-appは、中古IT機器の保守と販売を行う会社(演習用の架空の会社)の基幹システムです。PHP 7.4で動く、見積と在庫の画面を持っています。

| 場所 | 中身 |
|---|---|
app/ | アプリの本体。app/core/は触らない約束 |
db/ | スキーマと初期データ。触らない約束 |
tests/ | テスト。tests/phpunit/にPHPUnitのテストを足す |
CLAUDE.md | Claude Codeが毎回読む約束。55行、上限80行 |
docs/context/ | CLAUDE.mdから@で取り込む知識(PHP 7.4の対応表) |
.claude/ | Claude Codeの設定(settings.json)、フック、サブエージェント、スキル |
.githooks/ | コミットの前に動くgitのフック |
.gitlab-ci.ymlとci/jobs/ | パイプラインの定義。ジョブはci/jobs/に種類ごとのファイルで置く |
ci/ | パイプラインが呼ぶスクリプト(AIレビュー、ゲート、AIの実装サイクル) |
.gitlab/issue_templates/ | AIに渡すIssueの型 |
docs/requests/ | 社内からの要望メモ(午後のレベル2の題材) |
docs/plan/ | 計画書の見本(プチ演習9で使う) |
tools/ | 手元で使う道具(長さの検査、承認のレシート) |

ci/jobs/には、ジョブの定義が種類ごとに6つのファイルで並んでいます。.gitlab-ci.ymlは、これをincludeでまとめて読み込みます。パイプラインの3種類
MRのパイプライン
受講者がMRのあるブランチにpushしたときと、MRを作ったときに走ります。所要は1〜2分で、受講者のMRでは7つ、AIのMRでは8つのジョブが並びます。
サイクルのパイプライン
スケジュールか、mainで変数AI_CYCLE=onを付けたNew pipelineで走ります。コードの検査のジョブは動きません。
mainのパイプライン
マージのあとに走り、mainが緑のままかを確かめます。AIレビューは動きません。
MRがあるブランチへのpushでは、MRのパイプラインだけが走ります。.gitlab-ci.ymlのworkflow:rulesで決めています。ブランチ用とMR用の2本に分かれると、マージの条件はMR用の1本しか見ないので、ブランチ用で赤のテストがあってもマージできてしまうからです。
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"
when: never
- if: $CI_COMMIT_BRANCH
include:
- local: ci/jobs/*.yml
同じブランチに続けてpushすると、前のパイプラインは取り消し(canceled)になります。各ジョブにinterruptible: trueを付けているためで、AIの実装ジョブには付けていません。段とジョブ、きっかけの読み方は、GitLabの基礎の「パイプラインの読み方」にあります。
仕込んだパイプラインのジョブ
研修リポジトリには、11種類のジョブが仕込んであります。定義は.gitlab-ci.ymlとci/jobs/の6つのファイルにあり、実際の処理はci/のシェルスクリプトに書いてあります。
MRとmainのパイプラインのジョブ
| ジョブ(段) | 動くとき | 見ていること | 赤や橙のとき | 定義 |
|---|---|---|---|---|
lint-phpdev(lint) | MR、main | PHP 8.3で、全PHPファイルの構文が通るか | 書き間違い | lint.yml |
lint-phpver(lint) | MR、main | 対象のPHP 7.4で構文が通り、7.4に無い関数を呼んでいないか。php -lとci/undefined_functions.phpの2段で見る。イメージのタグと.claude/project.jsonの対象バージョンがずれていても落とす | 赤。match、enum、str_contains()など8.xの書き方 | lint.yml |
check-rules(lint) | MR、main | CLAUDE.mdの行数、1行の長さ、@の参照先。手元のpre-commitと同じ約束を、飛ばせないサーバー側で確かめる | 赤。CLAUDE.mdの上限超え | rules.yml、ci/check_rules.php |
ai_scope(lint) | AIのMRだけ(ブランチ名がai/で始まる) | AIが変えてよい範囲か。判定のスクリプトと一覧は、MRではなくmainの側から読む | 赤は止め金(ci/、.claude/、CLAUDE.mdなど)の変更。橙は、ファイルが10本か300行を超えた、app/を変えたのにtests/を変えていない、前からあるテストを変えた、のとき | ai-scope.yml、ci/ai_scope.sh |
test(test) | MR、main | 簡易ランナーtests/run_tests.php。PHP 8.3とPostgres 9.5を立て、スキーマと初期データを流し込んで動かす | 赤。アプリを変えて、既存の期待値が変わった | test.yml |
test-phpunit(test) | MR、main | PHPUnit 7.5.20のテスト。PHP 7.4とPostgres 9.5で動かす。結果の行が出ないまま終わったら赤にする(ci/run_phpunit.sh) | 赤。テストの期待値の誤り、PHPUnit 7.5に無いassert | test.yml |
ai_review(review) | MRだけ | AIが差分を読み、指摘をfindings.jsonに出して、MRにコメントを1本置く。コードは変えず、採否も決めない。差分が80行までならSonnet 5、超えたらOpus 5で読む | 橙。APIキーの設定かAPIの混雑。断られても、パイプラインは赤にしない | review.yml、ci/ai_review.sh |
ai_gate(gate) | MRだけ。ai_reviewのあと | findings.jsonに、重大度P1以上の指摘があるか | 橙。重い指摘がある。今は赤でもマージを止めない設定で、本物のゲートにするのがレベル3の課題 | review.yml、ci/ai_gate.sh |
lintとtestの5つは、共通の設定.code_checksを引き継ぎます。スケジュールとAI_CYCLE=onのパイプラインでは動きません。同じブランチに続けてpushした場合、古い方は途中で止まる設定です。同じ段のジョブは並んで動き、次の段は、前の段が全部緑になってから始まります。
サイクルのパイプラインのジョブ
| ジョブ(段) | 動くとき | していること | 定義 |
|---|---|---|---|
ai_pick(pick) | スケジュール、またはmainでAI_CYCLE=on | 承認ラベルai::approvedが付いた未着手のIssueを、古い順に最大20本拾い、ai::runningを付ける。子パイプラインの設計図ai_child.ymlを書き出す。resource_groupで、同時に1本だけ動かす | ai.yml、ci/ai_pick.sh |
ai_run(implement) | 同上 | ai_child.ymlを使って、子パイプラインを起動する | ai.yml |
implement-issue-番号(子パイプライン) | 拾った本数だけ並ぶ | 1つのIssueを実装して、MRを出す。下の7つの手順で進む | ci/ai_implement.sh、ci/settings.ci.json |
implement-issue-番号の中では、ci/ai_implement.shが次の順に動きます。
- 承認のラベルが付いているかを確かめ直します。
- Issue本文の「決まっていないこと」に何か書いてあれば、Claudeを呼ばずに止めます。
- ブランチ
ai/issue-番号-パイプライン番号を切ります。 claude -pで実装します。使える道具はallowedToolsで絞り、権限のモードはdontAskです。pushとブランチの操作は渡しません。最大30ターン、25分で打ち切ります。- 触らない場所(
app/core/、ci/、.claude/など)を変えていたら、MRを出さずに止めます。 - pushして、MRを作ります。ブランチ用のパイプラインは
ci.skipで走らせず、MR用の1本だけが走ります。 - Issueにラベル
ai::doneとコメントを付けます。止まったときはai::stoppedと理由を付けます。
パイプラインの色の読み方
| 色 | 意味 | マージ |
|---|---|---|
| 緑 | 通った | できる |
| 橙(警告) | 失敗を許したジョブ(ai_review、ai_gate、ai_scopeの注意)が失敗した。ログを人が読む | できる |
| 赤 | 止めるジョブ(lint、test、check-rules、ai_scopeの止め金)が失敗した | できない |
| 灰色(Skipped) | 動かさなかった。前の段が赤だったか、ci.skipで止めた | 色だけでは決まらない。ほかのジョブの結果を見る |
コードを読む順
.gitlab-ci.ymlで、段の順、パイプラインを1本に絞るworkflow:rules、共通の設定、変数を見ます。ci/jobs/の6つのファイルで、ジョブの定義を見ます。ci/のシェルスクリプトで、ジョブの中身を見ます。ci/settings.ci.jsonで、AIの実装ジョブに渡す権限を見ます。
資料の案内
仕込んだ34の設定
置き場所、役割、触ったときに起きることを、1枚ずつに分けました。
10の場面の再生
誰が何をすると、どの仕組みが動き、AIがどこまでやるのかを再生します。
一日のシミュレーター
自分でIssueを承認して時計を進めると、昼の便と夜の便の違いが分かります。
操作ごとの止まる場所
操作を1つ選ぶと、8つの層のどこで止まるかを玉が示します。
Git、GitLab、パイプラインの一般の操作と言葉は、GitLabの基礎にまとめています。このページの資料で扱うのは、研修のために仕込んだ設定と、その動き方だけです。
