AI副業の毎日投稿を「成果物ゲート」で止める|記事だけできた朝を成功にしない

文章はできています。画像だけがありません。それでも自動処理の画面には「成功」と表示されます。個人で AI を使って記事を積み上げると、この小さな食い違いが公開前の確認を重くします。
私は、AI 副業の自動化では生成回数より完了条件を先に決めるべきだと考えています。1 本書けたかではなく、読者へ渡せる一式がそろったかを機械で確かめます。失敗した記事を消すのではありません。下書き PR は残し、定期実行だけを失敗として扱います。
前回完了時から変えたこと
前回は、Hermes Agent の cron で仕事ごとにモデルを固定し、会話中のモデル変更から定期処理を守りました。今回は、その先にある「何ができたら完了か」を固定します。
| 時点 | 判定方法 | 見落とす問題 |
|---|---|---|
| 前回完了時 | AIの処理が最後まで走れば成功です | OG欠落や下書き状態の崩れを見逃します |
| 今回完了後 | GitHub上の成果物を時間窓と件数で検査します | 不完全なPRは残しても、定期実行は失敗になります |
読み飛ばせる章: GitHub CLIと
jqを普段から使う場合は、「完成の定義をコードへ移す」まで進めます。
なぜ、AIの返事を完了条件にしないのか
AI エージェントは、文章の作成、画像生成、変換、Git 操作、PR 作成を順に進めます。途中で画像 API だけが失敗しても、文章と PR は作れます。そこで「PR URL を返せたから成功」と判定すると、画像のない記事が翌朝のレビューへ混ざります。
逆の処理も危険です。画像がないために記事まで削除すると、書けた文章と失敗の証拠を失います。再実行のたびに別の記事を作れば、同じ朝に PR が 2 件できます。
この失敗は、AI の賢さでは直りません。完了の定義を、会話ではなく観測できる成果物へ移す必要があります。
私が朝稿に求める条件は次の形です。
[08:00 定期実行]
│
├── 新規記事 1件 ── draft: true
│ publication_state: draft
│
├── 同じslugのOG WebP 1件 ── 16:9
│
└── 08:00以上08:30未満に作成したPR 1件
│
▼
[成果物ゲート]
│ │
completed failed
│ └── PRは調査用に保持
▼
朝のレビュー
ここで重要なのは、PR を残す判断と、定期実行を成功にする判断を分けることです。文章だけの PR にはレビュー価値があります。しかし、毎朝 1 本の契約は満たしていません。
用語をそろえる
| 用語 | この回での意味 |
|---|---|
| 成果物ゲート | AIの返答ではなく、GitHub上のファイルと状態から完了を決める検査です |
| slug | 記事ファイル名とOGファイル名を結ぶ短い識別子です |
| 時間窓 | その日の朝稿として数えるPRの作成時刻の範囲です |
| fail-closed | 条件が不足した時に成功へ進めず、失敗として止める考え方です |
| draft | 生成済みでも公開対象には入れない記事状態です |
| PR | 記事と画像の差分をmainへ入れる前に確認する単位です |
完成の定義をコードへ移す
対象リポジトリはdicekanbe/hibanas-netです。GitHub CLI で PR を取得し、jqで JST の時間窓へ絞ります。日付を手入力すると再実行時にずれるため、当日の JST から境界を作ります。
DAY=$(TZ=Asia/Tokyo date +%F)
START=$(date -u -d "${DAY} 08:00:00 +0900" +%FT%TZ)
END=$(date -u -d "${DAY} 08:30:00 +0900" +%FT%TZ)
gh pr list \
--repo dicekanbe/hibanas-net \
--state all \
--limit 100 \
--json number,url,createdAt,headRefName \
| jq --arg start "$START" --arg end "$END" '
[.[]
| select(.createdAt >= $start and .createdAt < $end)
| select(.headRefName | startswith("content/"))]
| length'
このコマンドの期待出力は1です。0なら PR がありません。2以上なら、再実行が別の朝稿を作った可能性があります。どちらも成功へ丸めません。
次に、該当 PR の差分を名前と状態で取得します。
PR=$(
gh pr list \
--repo dicekanbe/hibanas-net \
--state all \
--limit 100 \
--json number,createdAt,headRefName \
| jq -r --arg start "$START" --arg end "$END" '
.[]
| select(.createdAt >= $start and .createdAt < $end)
| select(.headRefName | startswith("content/"))
| .number'
)
gh pr diff "$PR" \
--repo dicekanbe/hibanas-net \
--name-only
成功候補の期待出力は、同じ slug を持つ次の 2 行だけです。
public/images/posts/ai-side-business-artifact-gate.webp
src/content/posts/ai-side-business-artifact-gate.md
ファイル数だけを見ると、別名の画像でも通ります。記事名から slug を取り出し、OG のパスを組み立てて一致まで確かめます。
FILES=$(
gh pr diff "$PR" \
--repo dicekanbe/hibanas-net \
--name-only
)
ARTICLE=$(printf '%s\n' "$FILES" | grep '^src/content/posts/.*\.md$')
SLUG=$(basename "$ARTICLE" .md)
OG="public/images/posts/${SLUG}.webp"
test "$(printf '%s\n' "$FILES" | sed '/^$/d' | wc -l)" -eq 2
test "$(printf '%s\n' "$FILES" | grep -Fx "$ARTICLE" | wc -l)" -eq 1
test "$(printf '%s\n' "$FILES" | grep -Fx "$OG" | wc -l)" -eq 1
3 つのtestは、成功時に何も表示せず終了コード0を返します。条件が 1 つでも違えば、終了コードは1です。定期実行側はこの終了コードを受け取り、completedとfailedを分けます。
下書き状態はPRの中身から読む
作業ディレクトリのファイルを読むだけでは足りません。別 branch にいる場合や、PR 作成後に手元を変更した場合があるためです。GitHub API から PR の head SHA を取得し、その commit 時点の記事を読みます。
HEAD_SHA=$(
gh pr view "$PR" \
--repo dicekanbe/hibanas-net \
--json headRefOid \
--jq .headRefOid
)
BODY=$(
gh api \
"repos/dicekanbe/hibanas-net/contents/${ARTICLE}?ref=${HEAD_SHA}" \
--jq .content \
| base64 --decode
)
printf '%s\n' "$BODY" | grep -Fx 'draft: true'
printf '%s\n' "$BODY" | grep -Fx 'publication_state: draft'
期待出力は次の 2 行です。
draft: true
publication_state: draft
draft: falseへ変える処理は、このゲートの責任外です。生成と公開を同じ AI エージェントへ渡すと、不完全な成果物を見つけた処理が、その場で公開状態まで直す危険があります。朝稿は下書きで止め、承認後の公開を別の操作へ分けます。
OGは存在だけでなく比率も見る
同じ slug の WebP があっても、正方形のままなら OG として扱いにくくなります。ImageMagick で幅と高さを読み、16 対 9 を整数の積で比較します。
read WIDTH HEIGHT <<EOF
$(magick identify -format '%w %h' \
public/images/posts/ai-side-business-artifact-gate.webp)
EOF
test "$((WIDTH * 9))" -eq "$((HEIGHT * 16))"
printf '%sx%s\n' "$WIDTH" "$HEIGHT"
この朝稿で得た期待出力は次です。
1536x864
画像 API が別の比率を返したら、中央を基準に切り抜いてから再検査します。それでも変換できない場合は、記事のimage指定を外して PR を残します。ただし、OG を含む完成条件は満たさないため、定期実行はfailedです。
よくあるエラー
| 症状 | 原因 | 対応 |
|---|---|---|
| PR URLはあるのに定期実行が失敗します | 記事、OG、状態のどれかが不足しています | PRを消さず、差分と終了コードを確認します |
| 同じ朝にPRが2件あります | 再実行が前の朝稿を再開せず、新しいbranchを作りました | 作成前に当日の時間窓を検索し、既存PRがあれば止めます |
| WebPがあるのに画像検査で止まります | slug不一致、破損、または16対9ではありません | 記事名からOGパスを組み立て、実寸を読みます |
手元ではdraft: trueなのに失敗します | PRのhead SHAとローカル状態が違います | GitHub APIでhead commitの内容を検査します |
| 記事だけのPRが消えました | 失敗時の掃除が成果物まで削除しています | PR保持とrun失敗を別の分岐にします |
| 08:30以降のPRを朝稿として数えます | 日付だけで絞り、時間窓を使っていません | UTCへ変換した開始時刻と終了時刻を両方渡します |
AI副業では失敗を見える形で残す
毎日投稿を続けると、成功数だけを伸ばしたくなります。しかし、個人ビジネスでは欠落した画像、重複 PR、公開状態の誤りも運用コストです。成功へ丸めるほど、翌朝の人間が不足分を探す時間が増えます。
私は、AI エージェントに文章作成を任せても、完了判定は単純なコードへ戻します。判定に迷いを入れず、不完全な PR は証拠として残します。これなら、画像 API の一時障害で文章を失わず、毎日 1 本という約束も曖昧にしません。
自動化の価値は、止まらないことだけではありません。足りない時に、正しい理由で止まることにもあります。
一次情報
| 資料 | 確認した内容 |
|---|---|
| GitHub Docs: REST API endpoints for pull requests | PR一覧と作成結果をAPIで扱う基本仕様 |
| GitHub CLI manual: gh pr list | PR一覧のJSON項目と絞り込み方法 |
| GitHub CLI manual: gh pr diff | PR差分のファイル名を取得する方法 |
| Astro Docs: Content collections | Markdown frontmatterをschemaで検証する仕組み |
| ImageMagick: Identify | 画像の幅と高さを実ファイルから取得する方法 |
| Hermes Agent: Scheduled Tasks | cron jobを独立セッションで実行し、結果を管理する仕組み |
