로컬 인제스트
원격 가져오기 경로를 사용하지 않고 변경 로그를 온보딩합니다. 원격 경로(releases admin source fetch → POST /v1/workflows/update → API 워커의 DeterministicUpdateWorkflow, #1946)는 본문→레코드 추출을 서버 측에서 실행하고(증분 Haiku 또는 도구 루프), 0개의 릴리스를 작성하더라도 해당 추론 비용을 청구합니다. 로컬 Claude Code 에이전트는 이미 웹 가져오기 + 파일 도구를 갖춘 유능한 모델이므로, 페이지별 추출을 인라인으로 수행하고(이미 진행 중인 세션에 통합) 결정론적 쓰기 경로만 필요로 합니다.
핵심 원칙: 에이전트가 추출기입니다. 가져오고, 파싱하고, 레코드를 구조화한 후 배치 엔드포인트에 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실패 모드) — 인라인 추출은 루프를 완전히 우회합니다. - 페이지가 자체 웹 도구로 가져올 수 있는 경우(아래 거부는 정책 게이트이며 가져오기 제한이 아닙니다).
사용하지 말아야 할 경우:
- 관리형 에이전트 세션에 있는 경우. 이것은 로컬 전용입니다(Agent-도구 분산 + 로컬 파일 시스템 + CLI 환경). 관리형 에이전트 세션은 일반 가져오기 경로를 계속 사용해야 합니다.
- 소스에 깔끔하고 콘텐츠가 풍부한 피드가 있는 경우. 그냥 추가하고(
managing-sources) 크론 파이프라인이 가져오도록 두세요. 로컬 인제스트는 인라인 추출을 수행하는 에이전트가 올바른 도구인 경우를 위한 것이지, 피드/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은 여전히 이러한 페이지를 검색할 수 있습니다(Cloudflare의 /crawl 엔드포인트만 Conductor에 대해 신호를 강제로 적용했습니다). 어쨌든 옵트아웃을 존중하세요. 운영자가 문서화한 명시적인 게시자 허가가 있는 경우에만 재정의하세요 — 자동 우회는 없습니다.
사전 점검은 robots.txt에서 Sitemap: URL을 출력합니다 — 3단계를 위해 보관하세요.
2단계 — 대상 확인
쓰기 전에 조직과 소스가 존재하는지 확인하세요 — 릴리스는 source_id에 연결됩니다. 생성/이름 지정/기본 규칙은 managing-sources 를 참조하고, 구조화된 피드가 존재하는지 여부는 finding-changelogs 를 참조하세요(존재하면 일반 파이프라인을 선호합니다). 그런 다음:
- 파싱 힌트를 위해 조직 플레이북을 읽으세요:
releases admin playbook <org>(managing-sources→ 플레이북 참조). - 생성/확인된 행에서
sourceSlug와src_…아이디를 캡처하세요 — 배치 URL에 둘 중 하나가 필요합니다.
소스가 완전히 새로운 경우 생성하세요(예: releases admin source create "<name>" --url <url> --org <org> [--primary]) 그러나 가져오기를 트리거하지 마세요 — 그러면 피하려는 원격 업데이트 워크플로우가 실행됩니다.
3단계 — 페이지 매핑
페이지 모양 분류 (releases admin discovery evaluate <url> --json은 pageStructure: single-page | index | unknown 보고):
- 단일 페이지 변경 로그 → 4단계에서 한 번 가져오기.
- 인덱스 → 상세 → 릴리스별 URL을 열거합니다.
/sitemap.xml(사전 점검에서 찾은)을 변경 로그 경로로 필터링하여 선호하고, 그렇지 않으면 인덱스 HTML을 파싱하여 릴리스별 링크를 찾습니다. - 클라이언트 렌더링 인덱스 → 인덱스는 JS 셸로, 카드가 클라이언트 측 가져오기를 통해 로드되므로
curl/비JS 스크랩은 0개의 항목을 반환합니다. 렌더링을 통해 열거: Chrome에서 Claude로 인덱스를 구동하거나(또는 렌더링 하위 에이전트 분산), 목록이 윈도우 처리된 경우 페이지를 매기고 릴리스별 링크를 수집합니다. 그런 다음 상세 페이지가 정적으로 렌더링되는지 확인하세요 — 인덱스가 그렇지 않더라도 종종 그렇습니다. 정적인 경우 해당 URL을 직접 벌크curl하세요(HTML→마크다운 로컬, 렌더링 없음, AI 비용 없음). Harvey의help.harvey.ai/release-notes가 정확히 이것입니다: 클라이언트 렌더링 인덱스, 정적 상세 페이지. 인덱스의 비JS 스크랩이 비어 있다고 "릴리스 없음"으로 결론 내리지 마세요.
최근 윈도우(기본 ~25–50개의 가장 최근 릴리스)로 제한하고 건너뛴 것을 기록하세요 — 자동으로 잘라내지 마세요. 재실행은 멱등성이므로, 나중에 더 깊은 백필은 단순히 행을 추가합니다.
4단계 — 가져오기 + 추출
parsing-changelogs 의 type/version/date/media 규칙을 재사용하세요(아래 일치성 참조).
- 몇 페이지: 이 에이전트에서 인라인으로 가져오고 추출합니다.
- 많은 페이지: 병렬 하위 에이전트로 분산 — 각각 하나의 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_…` 아이디만 허용합니다.
# 사람이 읽을 수 있는 슬러그는 400 bare_slug_rejected를 반환합니다(슬러그는 조직 범위, #690).
# 형식화된 아이디를 사용하면 기본 형식도 작동합니다:
# 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": "마크다운 본문…", // 필수
"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)에서 업서트, 채우고 덮어쓰지 않음: 충돌 시 저장된 행이 비어 있을 때만 content/media를 백필하므로 재실행이 좋은 행을 덮어쓰지 않습니다. 재실행 및 페이지 매김에 안전합니다 — 동일한url이 중복되지 않고 업데이트됩니다. - 보강 재-POST (
mode: "upsert-content"). 기본값이 채우기 전용이므로, 이미 콘텐츠가 있는 URL에 더 풍부한 콘텐츠를 재-POST하는 것은 no-op입니다 — 스텁이 그대로 유지됩니다. 의도적인 두 번째 패스(먼저 인덱스 요약 시드, 그 다음 전체 상세 페이지 본문 + 미디어)를 위해 배치 본문의 최상위 레벨에"mode": "upsert-content"를 추가하세요: 동일 URL 충돌 시 content/media를 덮어쓰고 스크랩 제목 중복 제거 사전 필터(#1526)를 건너뜁니다. 의도적인 보강에만 사용하고, 기본 경로는 채우기 전용으로 유지하여 일상적인 재가져오기가 덮어쓰지 못하게 하세요.url은 시드된 행과 동일해야 합니다 — 보강은 URL을 키로 하므로, URL 체계가 변경되면 대신 중복이 삽입됩니다. URL 체계가 변경된 경우 대신 재시드하세요:releases release delete --source <src_…> --hard후 새/batch(기본 모드). - 인증.
write범위 필요; 정적RELEASES_API_KEY(루트)가 이를 충족합니다. 이 리포지토리에서.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 -을 통해. 소스가 로컬로 인제스트되었고(아직 크론 경로에 있지 않음), 관찰된 주기, 추출 문제점을 기록하세요. managing-sources 플레이북 작성 규칙(명령형 어조, keep-test, ### Fetch instructions / ### Traps / ### Coverage)을 따르세요.