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

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 requestsPR一覧と作成結果をAPIで扱う基本仕様
GitHub CLI manual: gh pr listPR一覧のJSON項目と絞り込み方法
GitHub CLI manual: gh pr diffPR差分のファイル名を取得する方法
Astro Docs: Content collectionsMarkdown frontmatterをschemaで検証する仕組み
ImageMagick: Identify画像の幅と高さを実ファイルから取得する方法
Hermes Agent: Scheduled Taskscron jobを独立セッションで実行し、結果を管理する仕組み
この記事をシェア