ローカルインジェスト
リモートフェッチパスを経由せずにチェンジログをオンボーディングします。リモートパス(releases admin source fetch → POST /v1/workflows/update → APIワーカーのDeterministicUpdateWorkflow、#1946)は、サーバーサイドで本文→レコード抽出(インクリメンタルHaikuまたはツールループ)を実行し、その推論に課金します — たとえリリースがゼロ件でもです。ローカルのClaude Codeエージェントは、Webフェッチとファイルツールを備えた有能なモデルであるため、ページごとの抽出をインラインで(すでに開始しているセッション内で)実行でき、決定論的な書き込みパスのみが必要です。
核となる原則: エージェントが抽出者です。 あなたがフェッチし、解析し、レコードを構造化し、それらをバッチエンドポイントにPOSTします。バッチエンドポイントは挿入時にAIを使用せずに、単純な冪等なアップサートを実行します。リモート抽出ループは決して実行されません。
ローカルClaude Codeのみ。 このスキルは、サブエージェントファンアウトのためのAgentツールを使用し、永続的なローカルファイルシステムとCLIのRELEASES_API_*環境変数を想定しています。これはseeding-playbooksと並んで、ローカル専用のオペレータースキルとして位置付けられています — マネージドエージェントフリートにはデプロイされません。
コスト契約
大規模な実行の前に、オペレーターにこれを明示的に伝え、それを遵守します:
- 消費するもの: フェッチ+抽出のためのエージェント自身のトークン、およびファンアウトするサブエージェントのトークン(モデルは選択可能 — 品質重視なら
sonnet、バルク処理ならhaiku)。 - 消費しないもの: サーバーサイド抽出(更新ワークフローのインクリメンタルHaiku / ツールループパス)は不要。リモートの
web_fetch抽出も不要。POST /v1/workflows/updateは決して呼び出されません。 - バッチエンドポイントは挿入時にAIを実行しません — 要約、マーケティング分類器、フィードエンリッチメントはありません。
insertと、fire-and-forgetのベクトル埋め込み(waitUntil)を実行します。AIフィールド(title_generated/title_short/summary)は意図的に空のままにされます。意図的なフォローアップパスについては、以下の_手動エンリッチメント_を参照してください。
リモート実行が行われなかったことを確認する否定的なシグナル: Axiom releases-cloudflare-logs に POST .../fetch?sessionId=det-… がなく、そのソースに対する extract-deps-worker イベントもありません。
使用すべき場合
- 新しいソースをClaude Codeで対話的にオンボーディング(または既存のソースをバックフィル)しており、リモート抽出コストをスキップしたい場合。
- リモートフェッチが完全な抽出ループを消費したにもかかわらず0件のリリースを書き込んだ場合(Conductor / 大規模ボディ /
max_tokens障害モード)— インライン抽出はループを完全に回避します。 - ページが自身のWebツールでフェッチ可能な場合(以下の拒否はポリシーゲートであり、フェッチ制限ではありません)。
使用すべきでない場合:
- マネージドエージェントセッションにいる場合。 これはローカル専用です(Agentツールファンアウト + ローカルファイルシステム + CLI環境)。MAセッションでは通常のフェッチパスを使用し続けるべきです。
- ソースがクリーンでコンテンツ豊富なフィードを持っている場合。 単に追加して(
managing-sources)、cronパイプラインにフェッチさせます。ローカルインジェストは、インラインで抽出を行うエージェントが適切なツールである場合のためのものであり、フィード/GitHubアダプターの代替品ではありません。 - パブリッシャーがオプトアウトしている場合(プリフライトが拒否 — ステップ1を参照)。
ワークフロー
ステップ1 — プリフライト(必須の安全ゲート)
何かをフェッチまたは書き込む前に、オプトアウトチェックを実行します:
bun .claude/skills/local-ingest/preflight.ts <url> --json
これはrobots.txtをフェッチし(apex→wwwリダイレクトに従う)、Cloudflare Content Signals ポリシーを解析し、終了コードでゲートします:
| 終了コード | 判定 | アクション |
|---|---|---|
0 | proceed | 許容的、または不在 — 続行。 |
1 | refuse | ai-input=no が宣言されています。停止。ユーザーに通知。 フェッチも書き込みもしないでください。 |
2 | unknown | robots.txtをフェッチ/解析できませんでした。一度リトライし、それでも不明な場合はユーザーに通知 — 続行を想定しないでください。 |
ゲートはai-inputのみです。 ローカルインジェストは検索/入力の取り込み — 検索インデックスへの供給 — であり、これはまさにai-inputが管理するものです。ai-train=noはモデルのトレーニングをオプトアウトするものであり、このパスでは実行されないため、ai-train=noのみ(例: vercel.com)は正しく続行されます。
これこそがConductorの教訓です。 conductor.build は Content-Signal: ai-train=no, search=yes, ai-input=no を提供します — その組織/ソースは、まさにこの理由ですでにレジストリ内で一時停止状態にあります(releases admin playbook conductor)。ローカルオンボードは、パブリッシャーのオプトアウトに反して、静かにトークンを消費したりコンテンツを取り込んだりしてはなりません。conductor.build は拒否されなければなりません(そのai-input=noにより)— このゲートのリグレッションターゲットです。
拒否はポリシー上の選択であり、技術的な制限ではありません: web_fetch と CF Browser Rendering はこれらのページを依然として取得できます(Conductorに対してシグナルを強制していたのはCloudflareの/crawlエンドポイントのみです)。それでもオプトアウトを尊重してください。オペレーターによって文書化された明示的なパブリッシャー許可がある場合のみ上書き可能です — 黙認のバイパスはありません。
プリフライトはrobots.txtからSitemap: URLも出力します — ステップ3のために保持しておいてください。
ステップ2 — ターゲットの解決
書き込みの前に、組織とソースが存在することを確認します — リリースはsource_idにアタッチされます。作成/命名/プライマリルールについては**managing-sourcesを、構造化フィードが存在するかどうか(存在する場合は通常のパイプラインを優先)についてはfinding-changelogs**を参照してください。その後:
- 解析ヒントのために組織のプレイブックを読む:
releases admin playbook <org>(managing-sources→ プレイブックを参照)。 - 作成/解決された行から**
sourceSlugとsrc_…ID**を取得します — バッチURLにはこれらのいずれかが必要です。
ソースが新しい場合は、それを作成します(例: releases admin source create "<name>" --url <url> --org <org> [--primary])が、フェッチはトリガーしないでください — それは回避しようとしているリモート更新ワークフローをディスパッチしてしまいます。
ステップ3 — ページのマッピング
ページの形状を分類します(releases admin discovery evaluate <url> --json は pageStructure: single-page | index | unknown を報告します):
- 単一ページのチェンジログ → ステップ4で1回フェッチ。
- インデックス → 詳細 → リリースごとのURLを列挙します。
/sitemap.xml(プリフライトが表示したもの)をチェンジログパスにフィルタリングして優先的に使用し、そうでなければインデックスHTMLを解析してリリースごとのリンクを取得します。 - クライアントレンダリングされたインデックス → インデックスはJSシェルであり、そのカードはクライアントサイドフェッチを介して読み込まれるため、
curl/非JSスクレイピングでは0件のエントリを返します。レンダリングによって列挙します: Chrome内のClaudeでインデックスを駆動するか(またはレンダリングサブエージェントをファンアウトする)、リストがウィンドウ化されている場合はページネーションし、リリースごとのリンクを収集します。次に、_詳細_ページが静的にレンダリングされているかどうかを確認します — インデックスがそうでない場合でも、多くの場合そうなっています — そして該当する場合はそれらを直接一括curlします(HTML→markdownをローカルで、レンダリング不要、AI課金なし)。Harveyのhelp.harvey.ai/release-notesがまさにこれです: クライアントレンダリングされたインデックス、静的な詳細ページ。インデックスの空の非JSスクレイピングから「リリースなし」と結論付けないでください。
最近のウィンドウに制限します(デフォルトは最新の約25~50件のリリース)そしてスキップしたものをログに記録します — 決して静かに切り捨てないでください。再実行は冪等であるため、後でのより深いバックフィルは単に行を追加するだけです。
ステップ4 — フェッチ + 抽出
タイプ/バージョン/日付/メディアについては**parsing-changelogs**の規則を再利用します(以下の_パリティ_を参照)。
- 少数のページ: このエージェント内でインラインでフェッチおよび抽出します。
- 多数のページ: 並列サブエージェントをファンアウトします — それぞれが1つのURLスライスを担当し、それぞれがバッチスキーマのレコードを返します。
seeding-playbooks(およびファンアウトの規律については**superpowers:dispatching-parallel-agents)のAgentツールディスパッチパターンを使用します。親エージェントでリリースURLをフェッチしないでください** — 親のコンテキストをクリーンに保つために、サブエージェントに委譲します。 - 親保存の注意点: サブエージェントはバッチ書き込みがブロックされる可能性があります(権限制限)。それに備えて計画します — 各サブエージェントにその結果内でレコードを返させ、親(あなた)がステップ5で書き込みを行います。
seeding-playbooksがノート保存に使用するのと同じパターンです。 - メディアを抽出 — テキストのみで送信しないでください。 各リリース本文から画像/動画を取得して
mediaを設定し、_next/image/_vercel/imageオプティマイザーラッパーを元のCDN URLにアンラップします(packages/rendering/src/media-url.tsのnormalizeMediaUrl)。テキストのみの最初のパスは、オペレーターレビューでのみ発見される一般的なミスです(Harveyはテキストのみで出荷し、その後_next/image→cdn.sanity.ioを再抽出して再シードする必要がありました)。エンリッチメントの再POSTはmode: "upsert-content"です(ステップ5)。
各レコードはバッチスキーマ(ステップ5を参照)とパリティルールに一致する必要があります。
オプション — 決定論的抽出(アプローチA、モノレポのみ): 手動で抽出する代わりに、packages/adapters/src/extract/ のプロダクション抽出ライブラリ — extractFromBody、extractWithTools、runDirectFetchExtraction — を呼び出すことができます。ExtractDeps.cloudflare はnull許容であり、repo はnoopにできるため、これらはAnthropicキーのみで実行されます(証明: scripts/smoke-toolloop.ts)。これはエージェントトークンを直接のAnthropic APIトークンと交換しますが、プロダクションツールループを正確に再現します。これらのパッケージは private: true であり、モノレポからのみ到達可能で、シンCLIからは決して到達できません。デフォルトはアプローチB(エージェントが抽出者)のままです。
ステップ5 — 書き込み(バッチアップサート)
レコードをバッチエンドポイントにチャンク単位でPOSTします(~50/リクエストでリクエストを抑制し、ハンドラーはサーバーサイドでD1バインド制限チャンクを実行します):
# 組織スコープのパスを使用します。裸の /v1/sources/:slug 形式は、型指定された `src_…` IDのみを受け入れます
# 人間のスラッグは 400 bare_slug_rejected を返します(スラッグは組織スコープです、#690)。型指定されたIDがあれば、裸の形式も機能します:
# POST $RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch
# POST $RELEASES_API_URL/v1/sources/<src_…>/releases/batch
curl -sS -X POST "$RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch" \
-H "Authorization: Bearer $RELEASES_API_KEY" \
-H "Content-Type: application/json" \
-d @chunk.json # { "releases": [ … ] }
リリースごとのフィールド(実際の受け入れ形状 — workers/api/src/routes/sources.ts、postReleasesBatchHandler):
{
"version": "1.4.0", // オプション; 日付ヘッダーまたはブログソースの場合は省略/null
"title": "ダークモード", // 必須
"content": "Markdown本文…", // 必須
"url": "https://…/changelog/dark-mode", // オプションですが、常に設定 — 重複排除キーです
"contentHash": "…", // オプション
"publishedAt": "2026-05-30", // オプション; ISO-8601(パリティを参照)
"media": "https://…/shot.png", // オプション
"type": "feature", // オプション; "feature" | "rollup"
"prerelease": false, // オプション
}
- 冪等。
UNIQUE(source_id, url)でアップサートし、埋めるが上書きしない: 衝突時には、保存された行が_空_の場合にのみコンテンツ/メディアをバックフィルするため、再実行が良い行を上書きすることはありません。再実行やページングしても安全です — 同じurlは重複ではなく更新されます。 - エンリッチメント再POST(
mode: "upsert-content")。 デフォルトはfill-onlyであるため、すでにコンテンツがあるURLに対してリッチなコンテンツを再POSTしても、スタブはそのままです — no-opになります。意図的なセカンドパス(最初にインデックスサマリーをシードし、次に完全な詳細ページ本文+メディアを追加)の場合は、バッチボディのトップレベルに"mode": "upsert-content"を追加します: 同じURLの衝突時にコンテンツ/メディアを上書きし、スクレイプタイトルの重複排除プリフィルターをスキップします(#1526)。意図的なエンリッチメントにのみ使用します。デフォルトパスはfill-onlyのままなので、ルーチンの再フェッチが上書きすることはありません。urlはシードされた行と同一でなければなりません — エンリッチメントはURLをキーとするため、URLスキームが変更されると重複が挿入されます。URLスキームが変更された場合は、代わりに再シードします:releases release delete --source <src_…> --hardの後に新しい/batch(デフォルトモード)。 - 認証。
writeスコープが必要です。静的RELEASES_API_KEY(root)はそれを満たします。このリポジトリでは.envがRELEASES_API_URL+RELEASES_API_KEYを自動ロードします。 - 挿入時にAIなし(コスト契約を参照)。
チャンク化ヘルパー(records.json配列を読み取り、50件のバッチでPOSTします):
bun -e '
const orgSlug = process.argv[1], sourceSlug = process.argv[2], file = process.argv[3];
const all = JSON.parse(await Bun.file(file).text());
const base = process.env.RELEASES_API_URL, key = process.env.RELEASES_API_KEY;
for (let i = 0; i < all.length; i += 50) {
const releases = all.slice(i, i + 50);
const res = await fetch(`${base}/v1/orgs/${orgSlug}/sources/${sourceSlug}/releases/batch`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ releases }),
});
console.log(`chunk ${i / 50}: ${res.status} ${(await res.text()).slice(0, 200)}`);
}
' <orgSlug> <sourceSlug> records.json
ステップ6 — 検証
releases tail <slug> --json # または: get_latest_releases (型指定ツール)
リリースに空でないタイトル、日付、コンテンツ、メディアがあることを確認します。行が_空_の場合は、抽出を修正して再実行します — デフォルトのアップサートは空の行を安全にバックフィルします。行にすでに_薄いが空ではない_コンテンツがある場合(例えば、今は完全な本文に置き換えたいインデックスサマリースタブ)、プレーンな再実行はno-opです — ステップ5の mode: "upsert-content" エンリッチメント再POSTを使用します。
ソースをリカバリー中で、オンボーディング中ではないですか? このステップは何が書き込まれたかを確認するだけであり、何が書き込まれなかったかについては何も語りません。誰かにソースが「リカバリーされた」または「欠落はない」と伝える前に、backfilling-sources → ソースを「クリーン」または「リカバリー済み」と宣言する前に を参照して、必要なライブページスポットチェックを行ってください。
ステップ7 — プレイブックノート
このソースがどのように取り込まれたかを記録して、将来のエージェントがそれを再導出しないようにします。manage_playbook(アクション update_notes)または releases admin playbook <org> --notes-file - を介して行います。ソースがローカルで取り込まれたこと(まだcronパス上にないこと)、観測されたケイデンス、および抽出の癖を記述します。managing-sources のプレイブック作成ルーブリック(命令形、keep-test、### Fetch instructions / ### Traps / ### Coverage)に従います。
…
[翻訳のために切り詰められました]