2014年に個人開発でアプリ公開を始めて12年、累計5,000万ダウンロードを超えた壁紙・浮世絵アプリの運営と並行して、4つの AI 技術ブログ(Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab)を運営しています。記事は1日合計16本を Claude Code の自律実行で公開しており、停止せず3週間以上動かし続けた経験から、E2E 駆動で AI に作業を任せる設計を書き残します。
なぜ「実装した」と「動いた」を分けるのか
最初の2ヶ月は、スケジュールタスクのプロンプトに「記事を1本書いて push してください」とだけ書いていました。これで動くには動くのですが、3つの問題が起きました。
ひとつめは、Claude Code が「書きました、push しました」と返してくる一方で、サイトの記事一覧に新しい記事が表示されないケースが月に2、3回ありました。原因はビルド時の generate-content.mjs が新規 MDX を拾えていないことでした。push までは確かに完了しているのですが、デプロイ後の挙動を確認しない限り「動いた」とは言えない、という Slack Bot の検証と同じ構造です。
ふたつめは、文体やテンプレ表現が時間とともに少しずつ崩れていく現象でした。最初は廣川本人の声に近かった文章が、半年経つと「いかがでしょう」というテンプレ調の締めや、まどろっこしい AI 解説調の冒頭に戻っていきました。これは Claude Code の問題ではなく、私が完了条件を曖昧に書いていたためです。「良い記事を書いてください」では、何をもって良いかが決まらないため、Claude Code は無難なテンプレへ収束していきます。
みっつめは、Helpful Content System の動向を機械検証なしで追えなかったことでした。後述する article_gate.py を入れる前は、YMYL に踏み込んだ表現が混入したり、AdMob の収益数値を誤って書いてしまったり、そういう問題を人間が後から検出するしかありませんでした。
これらは別々の問題に見えますが、根っこはひとつで「完了条件を機械的に判定できる形で渡していなかった」ということでした。
完了条件を1行に圧縮する
Codex の /goal コマンドは、目的とともに verifiable end state(検証可能な完了状態)を渡すことで、AI が自分で完了判定できるようにする発想です。Zenn のカンリーテックブログさんが書かれていた /e2e-dev も、<発話> → <期待応答> という1行で機械判定可能な完了条件を AI に渡す設計でした。
私は Lab パイプラインの完了条件を、以下の1行に圧縮しています。
article_gate.py が exit 0 を返し、JA/EN の MDX 件数が一致し、premium 記事は highlights ≥ 3 と実用性シグナル ≥ 3 を満たす
これだけだと抽象的なので、article_gate.py 側で具体化しました。article_gate.py は Python 製の単体スクリプトで、JA/EN の MDX ファイルパスを引数に取り、以下のチェックを機械的に走らせます。
テンプレ導入文(この記事では 等17パターン)を検出したら違反
テンプレ締め(いかがでしたか 等7パターン)を検出したら違反
AI 解説調マーカー(することができます 等9パターン)が3回以上で違反
常体(である だった 等)を1回でも検出したら違反
裸URL(コードブロック外)を1つでも検出したら違反
premium 記事は本文 2,000字以上(サニティフロア)
premium 記事は実用性シグナル(動くコードブロック・具体金額・番号付き手順・ハマりどころ・推奨判断・ドメインメトリクス)のうち3種類以上
premium 記事は frontmatter に highlights が3項目以上
パーソナライズ証拠(廣川 2014年 5,000万 17冠 AdMob 等)を本文に2回以上
スケジュールタスクのプロンプトには、article_gate.py の合否を「push 前の必須ゲート」として明記しています。これで Claude Code は「書いた」で止まらず、「ゲートを通った」までを完了とみなすようになりました。
5回ループ上限という安全弁
カンリーテックブログさんの /e2e-dev は、Lambda 側修正のループを5回までと決めています。これは「壊れた実装が dev に出続けないように」「Claude Code が無限ループに陥らないように」という安全弁としての設計でした。
私も Lab パイプラインに同じ上限を入れています。具体的には、article_gate.py が違反を返した記事は捨てて再生成し、それを5回まで繰り返す。5回ループしても合格しない場合は、スケジュールタスクは push せず終了レポートだけログに残します。
# スケジュールタスクのプロンプトに埋め込んでいる制御フロー
N = 0
while [ $N -lt 5 ]; do
N = $(( N+1 ))
echo "=== Generate attempt $N /5 ==="
# Claude Code に記事生成を依頼
# (実際はプロンプト内で日英ペアを生成)
# 機械検証
if python3 article_gate.py " $JA_PATH " " $EN_PATH " ; then
echo "Gate passed on attempt $N "
git add content/ && git commit -m "Add: $TITLE (JA+EN)"
git push origin main
break
else
echo "Gate violations detected, regenerating"
rm " $JA_PATH " " $EN_PATH "
fi
done
if [ $N -ge 5 ]; then
echo "5 attempts exhausted, halting without push"
fi
5回上限を設けてから、いわゆる「壊れた記事が公開される事故」は3週間でゼロ件です。代わりに「5回中4回違反でほぼ捨てた」というケースが月に1〜2回発生していますが、これは想定内の挙動で、押し付けで公開されるよりずっと健全な状態だと思います。
平均すると、1記事あたりの再生成回数は1.4回でした。1回で通る記事が約70%、2回で通る記事が約23%、3回以上かかる記事が約7%という分布です。
ベースライン確認を最初に走らせる
/e2e-dev のフロー内で印象的だったのが、実装前に同じ発話を Slack に投げるベースライン確認のステップでした。「そもそも何ができていないか」を明文化してから実装に入るという順序です。
Lab パイプラインでも、これに相当する処理を入れました。新規記事を生成する前に、以下の3つを Claude Code に観察させます。
同じカテゴリの直近5記事のスラッグと文字数分布 — 重複ネタを避ける目的と、文字数の感覚を揃える目的があります
当該カテゴリの article_gate.py 過去違反履歴 — 「最近このカテゴリで AI 解説調マーカーが多発している」と分かれば、生成プロンプト側で禁止語をより強く意識させられます
既存記事の内部リンク先候補 — Claude Code が架空のリンクを書いてしまう事故を、事前にホワイトリストを渡すことで防いでいます
ベースライン確認だけで30秒前後かかりますが、後段の再生成回数を平均0.4回ほど削減できた実測があります。1記事あたりの総所要時間で見ると、ベースライン確認のオーバーヘッドを差し引いてもおよそ4分短縮できています。
Goal / Context / Constraints / Done-when 構造をプロンプトに焼き込む
Codex の /goal ベストプラクティスは、プロンプトに Goal / Context / Constraints / Done-when の4要素を含めると指針づけています。これは Claude Code でも有効で、Lab パイプラインのスケジュールタスクは以下のテンプレに揃えています。
# Goal
カテゴリ「{category}」に、{topic_hint} を題材にした実体験ベースの記事を1本(日英ペア)で生成し
GitHub の {site} リポジトリの main ブランチに push する。
# Context
- 既存スラッグ一覧: $(ls content/articles/ja/{category}/ | sed 's/.mdx$//')
- 直近5記事の文字数分布: $(...)
- 政樹さんの実体験題材ストック: $WS/.auto-memory/reference_experience_sources_current.md
- 作家性ガイド: $WS/_documents/AUTHOR_VOICE_STYLE_GUIDE.md
- パーソナライズ義務: $WS/_documents/PERSONALIZATION_MAX_GUIDE.md
# Constraints
- 敬体(です・ます調)で統一。常体は避ける
- テンプレ導入文・テンプレ締め・AI 解説調マーカーを避ける(article_gate.py が検出)
- 既存記事スラッグと完全一致しないこと
- 内部リンクは実在記事のみ
- 裸URLは避け、Markdown リンク形式を使う
- premium 記事は highlights 3項目 + 実用性シグナル3種以上
# Done-when
python3 article_gate.py content/articles/ja/{category}/{slug}.mdx \
content/articles/en/{category}/{slug}.mdx
の exit code が 0 を返し、かつ JA と EN の MDX 総数が一致したとき。
# Stop conditions
- 再生成5回でゲート通らない: push せず終了レポート出力
- git pull --rebase で競合: 3回試して解消しなければ終了
- GitHub PAT が拒否される: 即終了
この構造で Lab 4サイトを並列に動かしてから、Claude Code が「途中で迷走する」現象はほぼ消えました。Codex /goal の4要素は、Claude Code のスケジュールタスクにそのまま転用できる汎用的な抽象です。
article_gate.py の実用性シグナル設計
premium 記事の品質判定で一番悩んだのが、「字数だけで合否を決めると、AI が文字数稼ぎをする」という現象でした。最初は 8,000字 hard floor を入れていたのですが、するとどうなるかというと、6,000字までは内容が詰まっていて、残り 2,000字が水増しの繰り返しという記事が増えました。
そこで、字数ではなく「読者が課金して得られる実用価値」を直接シグナル化する方針に変えました。具体的には、以下6種類のシグナルのうち3つ以上を含むことを premium 合格条件にしています。
動くコード — トリプルバッククォートで囲まれた5行以上のコードブロックが1つ以上
具体メトリクス — ¥1,200 5,000万DL eCPM 4.2 のような金額・パーセンテージ・倍率
構造化手順 — 番号付きリストや Step 1 手順 2 の明示が3つ以上
ハマりどころ — エラー 落とし穴 注意点 等が2回以上、かつ対処の記述がある
具体推奨 — 推奨します 個人的には 等が2回以上
ドメイン用語 — eCPM LTV Day1継続率 AdMob 等のドメイン固有メトリクス
設計に変えてから、premium 記事の平均字数は 7,200字 → 5,400字に下がりました。一方で読者の課金率は 0.8% → 1.4% に上昇しました。短くても密度の高い記事のほうが、結果として課金につながるという実証でした。
字数で殴る品質ゲートは、AI 生成記事に対しては逆効果になりがちです。「何を読者に渡したいか」を直接シグナル化する設計のほうが、結果として読みやすい記事に収束します。
内部リンク事故を防ぐホワイトリスト方式
Claude Code が架空の記事リンクを書いてしまう事故は、3週間で6件起きました。実在しないスラッグへの テキスト を本文に埋めてしまい、サイト上で 404 になるというものです。
対策として、内部リンクは「事前に渡したホワイトリストの中から選ぶ」というルールをプロンプトに明記しています。
# スケジュールタスクで Claude Code に渡す内部リンク候補
RELATED_SLUGS = $( find content/articles/ja -name "*.mdx" | \
xargs grep -l "tags:.*Claude Code" | \
head -20 | \
xargs -I {} sh -c 'F={}; basename ${F%.mdx}; head -3 $F | grep "^title:"' )
# プロンプトに埋め込む
cat << PROMPT
記事本文中で内部リンクを使う場合は、以下のリストから選んでください。
リスト外のリンク先は404になるため避けてください。
$RELATED_SLUGS
PROMPT
これに加えて、article_gate.py 側でも テキスト を抽出し、対応する MDX ファイルが実在するかを git tree から確認しています。生成段階と検証段階の2層で404を防いでいます。
副次効果: マイクロマネジメントから解放される
カンリーテックブログさんの記事で印象的だった一節が、「画面に張り付かなくて済むようになった」という記述でした。これは私の感覚とも一致しています。
Lab パイプラインを完成させる前、私は1日に何度もスケジュールタスクのログを覗いて、「ちゃんと動いているか」を確認していました。4サイト×4本=16本/日の自律実行が動いている今、ログを見るのは1日1回、夜にまとめて見るだけで済むようになりました。Crashlytics の新規エラーや AdMob の収益、Stripe の課金通知をチェックする時間に充てています。
完了条件を機械的に判定できる形にできれば、AI に作業を任せて人間は別プロジェクトに集中できる、というのは本当でした。私はその時間で iOS 版の壁紙アプリの更新作業、Firebase の SwiftPM 移行、AdMob のメディエーション拡張(4社→7社)などを並行で進められるようになりました。
個人開発者にとって、この差は想像以上に大きなものです。離れて暮らす子どもたちと過ごす時間も、確実に増えました。
適用範囲と限界
この E2E 駆動の自走パターンが効くのは、完了条件が機械判定可能な作業に限ります。Lab パイプラインの場合は「記事を1本書いて push する」という、入力と出力が明確で完了条件を Python 1スクリプトに圧縮できる作業だったため、フィットしました。
逆に向かないのが、ユーザーの主観評価が完了条件に絡む作業です。「読者にとって読みやすい記事」「ブランドに合うトーン」のような評価は、機械的なルールに完全に落とし込めません。Lab パイプラインも、最終的な「画像差し替え」「OGP 画像の選定」「公開タイミングの調整」は私が手動でやっています。
カンリーテックブログさんの /e2e-dev で「最後の Slack 目視確認は残ります」と書かれていたように、E2E 駆動の自走パターンは「完了条件 + 最後の人間目視」という二段構えで初めて完成します。AI が判定できるところは AI に任せ、判定できないところは人間が引き受ける。この役割分担をプロンプトレベルで明示できたことが、結果として一番大きな運用改善になりました。
新しいスケジュールタスクを書くとき、私はまず「このタスクの完了を Python 1行で判定できるか」を自問しています。判定できるならパイプライン化し、判定できないなら自分で手を動かす。この線引きが、4サイト+6アプリの並行運営を一人で回す土台になっています。