AI自動開発の設定一式

本Projectの仕組み

研修のGitLabでは、人がIssueを書いてラベルで承認すると、AIが実装してMRを出します。人が手を動かすのは、書く、承認する、読んでマージする、の3か所だけです。

人が触る3か所

1

Issueを書く

型に沿って、背景、やること、受け入れ条件を書きます。受け入れ条件は、読んだ人によって結果が変わらない形にします。AIも同じ文を読むからです。

2

ラベルで承認する

ラベルai::approvedを付けた時点が、AIに任せてよいという承認です。付いていないIssueを、AIは拾いません。

3

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-appGitLabのRunner。ブランチはai/issue-番号-…
頼む人受講者が、入力欄に文を送って頼む人がai::approvedを付けたIssueを、ジョブが渡す
モデルOpus 5.5Sonnet 5(claude-sonnet-5。変数AI_MODELで固定)
権限手動。書き換えとコマンドの前に、確認のダイアログが出るdontAsk。ダイアログを出さず、許可の外を全部拒否する
Gitの操作ブランチの切り替え、コミット、pushを、頼まれた分だけ行う。pushは毎回ダイアログで聞く書き込みはgit addとgit commitだけ。pushとMRの作成は、ジョブのスクリプトが行う
設定の場所.claude/settings.jsonci/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さんとして追います。再生を押すと札が箱から箱へ移り、その場で動いた仕組みが吹き出しで話す作りです。下の段を押すと、その段へ飛べます。

手元のPC
Claude Desktop、フック、pre-commit
人
書く、承認する、読んでマージする
Issueとボード
型、受け入れ条件、ラベル
起動
スケジュールかNew pipeline
ai_pick
承認済みのIssueを拾う
Runner
EC2の上でジョブを動かす
main
保護。マージはMaintainerだけ
MRのパイプライン
lint、test、AIレビュー、ai_scope
MR
ai-cycle-botか人が作る
AIの実装
Claudeが実装してコミット
時刻なしIssueのラベルなしMRなしAIの費用なし
  1. Aさんが、型ai-taskでIssueを書きます。受け入れ条件は、テストのファイル名、確かめる呼び出しと結果、パイプラインが緑、変えるのはテスト1本だけ、の4行です。Aさんwarehouse_name('')が「不明」を返すことを、テストで固定したい。
  2. Aさんがラベルai::approvedを付けました。ここが人の承認で、付けるまでAIは何もしません。Aさん受け入れ条件まで書けたので、AIに任せます。
  3. 講師がmainで、変数AI_CYCLEをonにしてパイプラインを動かします。研修の当日は、スケジュールが10分おきに同じことをします。GitLabAIのサイクル用に、パイプライン#89を作りました。
  4. ai_pickが、承認済みで未着手のIssueを探します。見つけた3本にai::runningを付け、1本に1つのジョブを並べた子パイプラインを作りました。ai_pickのログ承認済みで未着手の Issue: 3 本
  5. Runnerが、子パイプラインの3つのジョブを同時に動かします。Runnerは研修用のEC2の上にあり、8本まで同時に動かせる設定です。Runnerimplement-issue-30、31、32を並べて動かします。
  6. ジョブはラベルを確かめ直してから、Claudeに実装させます。Claudeに渡すのは、ファイルの読み書きとphp、git add、git commit、git status、git diffだけです。implement-issue-30のログターン 7 / 費用 0.13 ドル / 終わり方 success
  7. ジョブのスクリプトがpushしてMRを作り、Issueにコメントを付けます。Claudeにはpushの権限を渡していないので、ここはスクリプトだけの仕事です。ai-cycle-bot@AさんAIの実装サイクルがMRを出しました。
  8. MRのパイプラインが走ります。構文、ルール、テスト、AIレビューに加え、AIのMRでだけai_scopeも動きました。ai_scopeのログOK 触ってはいけない場所は変えていません。大きさも目安の中です。
  9. Aさんが差分を開き、受け入れ条件と突き合わせます。変わったファイルはtests/phpunit/WarehouseNameEmptyTest.phpの1本で、14行でした。Aさんテストが1本だけで、受け入れ条件のとおりです。
  10. 講師がマージします。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で動く、見積と在庫の画面を持っています。

研修リポジトリのトップ。左にメニュー、中央にフォルダの一覧、その上に main に最後に入ったコミットと緑の丸
プロジェクトのトップです。左のメニューで画面を切り替えます。ファイルの一覧の上に、mainに最後に入ったコミットと、そのパイプラインの結果(緑の丸)が出ます。
場所中身
app/アプリの本体。app/core/は触らない約束
db/スキーマと初期データ。触らない約束
tests/テスト。tests/phpunit/にPHPUnitのテストを足す
CLAUDE.mdClaude 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 のフォルダ。ai-scope.yml、ai.yml、lint.yml、review.yml、rules.yml、test.yml の6つのファイル
ci/jobs/には、ジョブの定義が種類ごとに6つのファイルで並んでいます。.gitlab-ci.ymlは、これをincludeでまとめて読み込みます。

パイプラインの3種類

MRのパイプライン

受講者がMRのあるブランチにpushしたときと、MRを作ったときに走ります。所要は1〜2分で、受講者のMRでは7つ、AIのMRでは8つのジョブが並びます。

lintlint-phpdevlint-phpvercheck-rulesai_scope(AIのMRだけ)
testtesttest-phpunit
reviewai_review
gateai_gate

サイクルのパイプライン

スケジュールか、mainで変数AI_CYCLE=onを付けたNew pipelineで走ります。コードの検査のジョブは動きません。

pickai_pick
implementai_run
子パイプラインimplement-issue-番号拾った本数だけ並ぶ

mainのパイプライン

マージのあとに走り、mainが緑のままかを確かめます。AIレビューは動きません。

lintlint-phpdevlint-phpvercheck-rules
testtesttest-phpunit

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、mainPHP 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、mainCLAUDE.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、mainPHPUnit 7.5.20のテスト。PHP 7.4とPostgres 9.5で動かす。結果の行が出ないまま終わったら赤にする(ci/run_phpunit.sh)赤。テストの期待値の誤り、PHPUnit 7.5に無いasserttest.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が次の順に動きます。

  1. 承認のラベルが付いているかを確かめ直します。
  2. Issue本文の「決まっていないこと」に何か書いてあれば、Claudeを呼ばずに止めます。
  3. ブランチai/issue-番号-パイプライン番号を切ります。
  4. claude -pで実装します。使える道具はallowedToolsで絞り、権限のモードはdontAskです。pushとブランチの操作は渡しません。最大30ターン、25分で打ち切ります。
  5. 触らない場所(app/core/、ci/、.claude/など)を変えていたら、MRを出さずに止めます。
  6. pushして、MRを作ります。ブランチ用のパイプラインはci.skipで走らせず、MR用の1本だけが走ります。
  7. Issueにラベルai::doneとコメントを付けます。止まったときはai::stoppedと理由を付けます。

パイプラインの色の読み方

色意味マージ
緑通ったできる
橙(警告)失敗を許したジョブ(ai_review、ai_gate、ai_scopeの注意)が失敗した。ログを人が読むできる
赤止めるジョブ(lint、test、check-rules、ai_scopeの止め金)が失敗したできない
灰色(Skipped)動かさなかった。前の段が赤だったか、ci.skipで止めた色だけでは決まらない。ほかのジョブの結果を見る

コードを読む順

  1. .gitlab-ci.ymlで、段の順、パイプラインを1本に絞るworkflow:rules、共通の設定、変数を見ます。
  2. ci/jobs/の6つのファイルで、ジョブの定義を見ます。
  3. ci/のシェルスクリプトで、ジョブの中身を見ます。
  4. ci/settings.ci.jsonで、AIの実装ジョブに渡す権限を見ます。

どこを変えると何が止まるかは、止め金の地図と設定の一覧にあります。

資料の案内

Git、GitLab、パイプラインの一般の操作と言葉は、GitLabの基礎にまとめています。このページの資料で扱うのは、研修のために仕込んだ設定と、その動き方だけです。