Lokale Erfassung (Local Ingest)
Onboarden eines Changelogs ohne den Remote-Fetch-Pfad zu durchlaufen. Der Remote-Pfad (releases admin source fetch → POST /v1/workflows/update → der DeterministicUpdateWorkflow des API-Workers, #1946) führt die Extraktion von Body → Datensätzen serverseitig durch (inkrementelles Haiku oder die Tool-Schleife) und berechnet diese Inferenz — selbst wenn dabei null Releases geschrieben werden. Ein lokaler Claude-Code-Agent ist bereits ein leistungsfähiges Modell mit Web-Fetch- und Datei-Tools. Daher kann er die seitenweise Extraktion inline durchführen (in die laufende Session integriert) und benötigt nur einen deterministischen Schreibpfad.
Kernprinzip: Der Agent ist der Extraktor. Du rufst ab, du parse, du strukturierst die Datensätze, und du POSTest sie an den Batch-Endpunkt, der einen einfachen idempotenten Upsert mit keiner AI beim Einfügen durchführt. Die Remote-Extraktionsschleife wird nie ausgeführt.
Nur für lokales Claude Code. Dieses Skill verwendet das Agent-Tool für Sub-Agent-Fan-Out und setzt ein persistentes lokales Dateisystem sowie die RELEASES_API_*-Umgebungsvariablen der CLI voraus. Es ist zusammen mit seeding-playbooks ein reines Operator-Skill für den lokalen Einsatz — es wird nicht in der Managed-Agent-Flotte bereitgestellt.
Kostenkalkulation (Cost contract)
Teile dies dem Operator vor einem großen Durchlauf explizit mit und halte dich daran:
- Ausgaben: die eigenen Token deines Agenten für Fetch + Extraktion sowie die Token aller Sub-Agenten, die du ausrollst (deine Modellwahl —
sonnetfür Qualität,haikufür Massenverarbeitung). - Gibt KEINE Ausgaben: keine serverseitige Extraktion (die inkrementelle Haiku- / Tool-Schleife des Update-Workflows), kein Remote-
web_fetch-Extraktion.POST /v1/workflows/updatewird nie aufgerufen. - Der Batch-Endpunkt führt beim Einfügen keine AI aus — keine Zusammenfassung, kein Marketing-Klassifizierer, keine Feed-Anreicherung. Er führt einen
insertplus einen Fire-and-Forget-Vektor-Embed (waitUntil) durch. Die AI-Felder (title_generated/title_short/summary) werden absichtlich leer gelassen; siehe Manuelle Anreicherung unten für die bewussten Folgepfade.
Das negative Signal, das bestätigt, dass kein Remote-Durchlauf stattgefunden hat: in Axiom releases-cloudflare-logs gibt es kein POST .../fetch?sessionId=det-… und kein extract-deps-worker-Ereignis für die Quelle.
Wann verwenden
- Onboarding einer neuen Quelle (oder Backfilling einer vorhandenen) interaktiv in Claude Code, und du möchtest die Remote-Extraktionskosten vermeiden.
- Ein Remote-Fetch hat eine vollständige Extraktionsschleife verbraucht und 0 Releases geschrieben (der Conductor / Large-Body /
max_tokens-Fehlermodus) — das Inline-Extrahieren umgeht die Schleife vollständig. - Die Seite ist mit deinen eigenen Web-Tools abrufbar (die folgende Ablehnung ist eine Policy-Einschränkung, kein Fetch-Limit).
Wann NICHT verwenden:
- Du befindest dich in einer Managed-Agent-Session. Dies ist nur lokal verfügbar (Agent-Tool-Fan-Out + lokales Dateisystem + CLI-Umgebung). MA-Sessions sollten den normalen Fetch-Pfad verwenden.
- Die Quelle hat einen sauberen, inhaltsreichen Feed. Füge sie einfach hinzu (
managing-sources) und lass die Cron-Pipeline sie abrufen. Local Ingest ist für Fälle gedacht, in denen der Agent, der die Extraktion inline durchführt, das richtige Werkzeug ist, kein Ersatz für Feed-/GitHub-Adapter. - Der Herausgeber widerspricht (Preflight lehnt ab — siehe Schritt 1).
Workflow
Schritt 1 — Preflight (zwingende Sicherheitsprüfung)
Führe die Opt-out-Prüfung durch, bevor du etwas abrufst oder schreibst:
bun .claude/skills/local-ingest/preflight.ts <url> --json
Es ruft robots.txt ab (mit apex→www-Weiterleitungen), parst die Cloudflare Content Signals-Policy und bewertet anhand des Exit-Codes:
| Exit | Ergebnis | Aktion |
|---|---|---|
0 | proceed | Erlaubend oder abwesend — fortfahren. |
1 | refuse | ai-input=no deklariert. STOP. Dem Benutzer mitteilen. Nicht abrufen oder schreiben. |
2 | unknown | robots.txt konnte nicht abgerufen/geparst werden. Einmal wiederholen; falls immer noch unbekannt, dem Benutzer mitteilen — nicht von Fortfahren ausgehen. |
Die Hürde betrifft nur ai-input. Local Ingest ist die Aufnahme von Such-/Eingabe-Daten — die Befüllung eines Suchindex —, was genau das ist, was ai-input regelt. ai-train=no verweigert das Training von Modellen, eine Nutzungsart, die dieser Pfad nicht durchführt; daher wird ai-train=no allein (z. B. vercel.com) korrekt mit proceed bewertet.
Dies ist die Lektion von Conductor. conductor.build liefert Content-Signal: ai-train=no, search=yes, ai-input=no — seine Organisation/Quelle ist bereits aus genau diesem Grund im Registry angehalten (releases admin playbook conductor). Ein lokales Onboarding darf nicht stillschweigend Tokens ausgeben oder Inhalte gegen einen Publisher-Opt-out erfassen. conductor.build muss abgelehnt werden (über sein ai-input=no) — es ist das Regressionsziel für diese Hürde.
Die Ablehnung ist eine Policy-Entscheidung, keine technische Einschränkung: web_fetch und CF Browser Rendering können diese Seiten trotzdem abrufen (nur Cloudflares /crawl-Endpunkt hat das Signal für Conductor hart durchgesetzt). Halte dich trotzdem an den Opt-out. Überschreibe ihn nur mit expliziter Genehmigung des Herausgebers, die vom Betreiber dokumentiert wurde — es gibt keine stillschweigende Umgehung.
Das Preflight gibt auch alle Sitemap:-URLs aus robots.txt aus — behalte sie für Schritt 3.
Schritt 2 — Ziel auflösen
Stelle sicher, dass Organisation und Quelle existieren, bevor du schreibst — Releases werden an eine source_id angehängt. Greife auf managing-sources für die Erstellungs-/Benennungs-/Primärregeln und auf finding-changelogs zurück, um zu prüfen, ob ein strukturierter Feed existiert (wenn ja, bevorzuge die normale Pipeline). Dann:
- Lies das Playbook der Organisation für Parse-Hinweise:
releases admin playbook <org>(siehemanaging-sources→ Playbooks). - Erfasse den
sourceSlugund diesrc_…-ID aus der erstellten/aufgelösten Zeile — du benötigst einen von beiden für die Batch-URL.
Wenn die Quelle brandneu ist, erstelle sie (z. B. releases admin source create "<name>" --url <url> --org <org> [--primary]), aber löse keinen Fetch aus — das würde den Remote-Update-Workflow auslösen, den du vermeiden möchtest.
Schritt 3 — Seiten zuordnen
Klassifiziere die Seitenform (releases admin discovery evaluate <url> --json meldet pageStructure: single-page | index | unknown):
- Einseitiger Changelog → ein Fetch in Schritt 4.
- Index → Detail → zähle die URLs pro Release auf. Bevorzuge
/sitemap.xml(wurde vom Preflight gefunden), gefiltert auf den Changelog-Pfad; andernfalls parse das Index-HTML auf Links zu einzelnen Releases. - Client-seitig gerenderter Index → der Index ist eine JS-Hülle, deren Karten über einen Client-seitigen Fetch geladen werden. Daher liefern
curl/ Nicht-JS-Scraping null Einträge. Zähle sie durch Rendern auf: durchlaufe den Index mit Claude in Chrome (oder rolle einen rendernen Sub-Agenten aus), paginiere, wenn die Liste gefenstert ist, und sammle die Links zu den einzelnen Releases. Überprüfe dann, ob die Detail-Seiten statisch gerendert sind — das sind sie oft, auch wenn der Index es nicht ist. Wenn ja, rufe diese massenhaft mitcurldirekt ab (HTML→Markdown lokal, kein Rendern, keine AI-Kosten). Harveyshelp.harvey.ai/release-notesist genau das: Client-seitig gerenderter Index, statische Detailseiten. Ziehe nicht den Schluss "keine Releases" aus einem leeren Nicht-JS-Scrape des Index.
Begrenze auf ein aktuelles Fenster (Standard ~25–50 aktuellste Releases) und protokolliere, was du übersprungen hast — niemals stillschweigend abschneiden. Wiederholungen sind idempotent, daher fügt ein späteres tieferes Backfilling einfach Zeilen hinzu.
Schritt 4 — Abrufen + Extrahieren
Verwende die Konventionen von parsing-changelogs für Typ/Version/Datum/Medien (siehe Parität unten).
- Wenige Seiten: rufe ab und extrahiere inline in diesem Agenten.
- Viele Seiten: rolle parallele Sub-Agenten aus — jeder einen URL-Abschnitt, jeder gibt Datensätze im Batch-Schema zurück. Verwende das
Agent-Tool-Dispatch-Muster ausseeding-playbooks(und superpowers:dispatching-parallel-agents für die Fan-Out-Disziplin). Rufe keine Release-URLs im Parent-Agenten ab — delegiere, um den Kontext des Parent sauber zu halten. - Parent-saves-Vorbehalt: Sub-Agenten können möglicherweise keinen Batch-Write durchführen (Berechtigungsbeschränkungen). Plane dafür — lasse jeden Sub-Agenten seine Datensätze zurückgeben, und lass den Parent (dich) die Writes in Schritt 5 durchführen. Gleiches Muster, das
seeding-playbooksfür das Speichern von Notizen verwendet. - Extrahiere Medien — sende nicht nur Text. Ziehe die Bilder/Videos aus jedem Release-Body und fülle
media, entferne_next/image/_vercel/imageOptimizer-Wrapper zur zugrunde liegenden CDN-URL (normalizeMediaUrlauspackages/rendering/src/media-url.ts). Ein reiner Text-Durchgang ist ein häufiger Fehler, der erst bei der Überprüfung durch den Betreiber auffällt (Harvey hat nur Text gesendet und musste dann_next/image→cdn.sanity.ioneu extrahieren und neu befüllen); das erneute POST zur Anreicherung erfolgt mitmode: "upsert-content"(Schritt 5).
Jeder Datensatz muss dem Batch-Schema (siehe Schritt 5) und den Paritätsregeln entsprechen.
Optional — deterministische Extraktion (Ansatz A, nur im Monorepo): anstatt von Hand zu extrahieren, kannst du die Produktions-Extraktionsbibliotheken in packages/adapters/src/extract/ aufrufen — extractFromBody, extractWithTools, runDirectFetchExtraction. ExtractDeps.cloudflare ist nullable und repo kann ein Noop sein, sodass sie nur mit einem Anthropic-Key ausgeführt werden (Beweis: scripts/smoke-toolloop.ts). Dies tauscht Agent-Tokens gegen direkte Anthropic-API-Tokens, reproduziert jedoch die Produktions-Tool-Schleife exakt. Diese Pakete sind private: true — nur aus dem Monorepo erreichbar, niemals aus der schlanken CLI. Der Standard bleibt Ansatz B (der Agent ist der Extraktor).
Schritt 5 — Schreiben (Batch-Upsert)
POSTe Datensätze an den Batch-Endpunkt in Blöcken (~50/Request hält Requests klein; der Handler führt das D1-Bind-Limit-Chunking serverseitig durch):
# Verwende den Organisations-spezifischen Pfad. Die bloße Form /v1/sources/:slug akzeptiert NUR eine typisierte
# `src_…`-ID — ein menschlicher Slug liefert dort 400 bare_slug_rejected zurück (Slugs sind
# Organisations-spezifisch, #690). Mit einer typisierten ID funktioniert die bloße Form auch:
# 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": [ … ] }
Felder pro Release (die tatsächlich akzeptierte Form — workers/api/src/routes/sources.ts, postReleasesBatchHandler):
{
"version": "1.4.0", // optional; weglassen/null für datumsbasierte oder Blog-Quellen
"title": "Dark mode", // erforderlich
"content": "Markdown-Text…", // erforderlich
"url": "https://…/changelog/dark-mode", // optional, aber IMMER ausfüllen — es ist der Dedup-Key
"contentHash": "…", // optional
"publishedAt": "2026-05-30", // optional; ISO-8601 (siehe Parität)
"media": "https://…/shot.png", // optional
"type": "feature", // optional; "feature" | "rollup"
"prerelease": false, // optional
}
- Idempotent. Upsert auf
UNIQUE(source_id, url), füllt, überschreibt nicht: bei einer Kollision werden nur Inhalt/Medien nachgefüllt, wenn die gespeicherte Zeile leer ist. Daher überschreibt eine Wiederholung niemals eine gute Zeile. Sicher wiederholbar und seitenweise durchführbar — gleicheurlwird aktualisiert, nicht dupliziert. - Anreicherungs-Neu-POST (
mode: "upsert-content"). Da der Standard nur füllt, ist ein erneutes POST reicherer Inhalte für eine URL, die bereits Inhalte hat, ein Noop — der Platzhalter bleibt. Für einen bewussten zweiten Durchgang (zuerst Index-Zusammenfassungen, dann vollständige Detailseiten-Bodys + Medien) füge"mode": "upsert-content"auf oberster Ebene des Batch-Bodys hinzu: es überschreibt Inhalt/Medien bei einer URL-Kollision und überspringt den Scrape-Titel-Dedup-Präfilter (#1526). Verwende es nur für gezielte Anreicherung; der Standardpfad bleibt füllend, sodass ein Routine-Neu-Fetch nicht überschreiben kann. Dieurlmuss mit der gesäten Zeile identisch sein — die Anreicherung erfolgt über die URL, eine geänderte URL-Scheme erzeugt stattdessen ein Duplikat. Wenn sich die URL-Scheme geändert hat, säe stattdessen neu:releases release delete --source <src_…> --harddann ein frisches/batch(Standardmodus). - Auth. Erfordert
write-Bereich; der statischeRELEASES_API_KEY(Root) erfüllt dies..envlädtRELEASES_API_URL+RELEASES_API_KEYin diesem Repository automatisch. - Keine AI beim Einfügen (siehe Kostenkalkulation).
Chunking-Hilfe (liest ein records.json-Array und POSTet in Blöcken von 50):
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
Schritt 6 — Validieren
releases tail <slug> --json # oder: get_latest_releases (typisiertes Tool)
Bestätige, dass Releases nicht-leere Titel, Daten, Inhalte und Medien haben. Wenn eine Zeile leer ist, korrigiere die Extraktion und wiederhole den Vorgang — der Standard-Upsert füllt leere Zeilen sicher nach. Wenn eine Zeile bereits dünne, aber nicht-leere Inhalte hat (z. B. einen Index-Zusammenfassungs-Platzhalter, den du jetzt durch den vollständigen Body ersetzen möchtest), ist eine einfache Wiederholung ein Noop — verwende den mode: "upsert-content"-Anreicherungs-Neu-POST aus Schritt 5.
Stellst du eine Quelle wieder her, nicht onboards du sie? Dieser Schritt bestätigt nur, was geschrieben wurde — er sagt nichts darüber aus, was nicht geschrieben wurde. Bevor du jemandem mitteilst, dass eine Quelle "wiederhergestellt" ist oder "nichts fehlt", siehe backfilling-sources → Bevor du eine Quelle als "sauber" oder "wiederhergestellt" deklarierst für die erforderliche Live-Seiten-Stichprobenprüfung.
Schritt 7 — Playbook-Notiz
Halte fest, wie diese Quelle erfasst wurde, damit zukünftige Agenten sie nicht neu ableiten. Über manage_playbook (Aktion update_notes) oder releases admin playbook <org> --notes-file -. Notiere, dass die Quelle lokal erfasst wurde (noch nicht auf dem Cron-Pfad), ihre beobachtete Kadenz und alle Extraktions-Besonderheiten. Folge der Playbook-Autorungsrichtlinie von managing-sources (Imperativ, keep-test, ### Fetch instructions / ### Traps / ### Coverage). Schreibe keine …