AI-translated from English; not yet reviewed by a fluent editor.
# 組合 Claude Code skills:從一個核准來源產生三份經檢查的草稿
> 用四個獨立的 Claude Code skills 盤點核准的書面來源,起草 LinkedIn 貼文、電子報與廣告,再核對檔案雜湊及陳述,交由人工審閱。
By BIG CHANGE Editorial
Published: 2026-10-19T19:41:34.834Z
Updated: 2026-10-19T19:41:34.834Z
Canonical: https://bigchange.ai/blog/claude-code-three-channel-skill-pipeline-guide

AI-generated conceptual illustration by BIG CHANGE; no actual Claude Code interface, model output, reviewed draft or campaign is depicted.
核准的文章或逐字稿可以供應多份草稿,但每份都需要清楚的來源追溯方式。本指南組合四個獨立的 [Claude Code 專案 skills](https://code.claude.com/docs/en/skills),盤點內容所有者核准的書面來源,在不同目錄起草 LinkedIn 貼文、電子報段落與廣告,再用本機 Python 彙整程式檢查檔案契約。輸出是交給指定人工審閱者的草稿,不代表獲准發布或傳送。
BIG CHANGE 審查了下方的 skills、契約與彙整程式碼。獨立審閱者執行了 16 項本機結構檢查。我們**沒有**在 Claude Code 安裝或執行這些 skills,沒有付費執行模型、產生真實的管道文稿,也沒有測試實際行銷活動。以下是供讀者用自己的核准來源測試的步驟。
## 關鍵改變
- **改變了什麼:** 四個自有 skills 有明確的來源、清單與管道結果契約,接著進行確定性的彙整。
- **為何重要:** 每份草稿都可追溯至來源段落;缺少的管道則明確保留為受阻項目。
- **需要留意:** 檔案檢查不能證明事實真確、改編權有效或編輯已核准;每份就緒草稿仍須人工核查。
## 1. 確認存取權、權利與來源
使用自己控制的 Claude Code 儲存庫,以及獲准處理來源的帳戶。處理機密材料前,檢查帳戶可用模型、[用量與費用](https://code.claude.com/docs/en/costs), [資料政策](https://code.claude.com/docs/en/data-usage)與[工具權限](https://code.claude.com/docs/en/permissions)。Claude Code 的 `/usage` 命令可顯示目前工作階段的用量;計費取決於帳戶與方案。本流程未量測成本或節省金額。輸入必須是真正核准的 UTF-8 文章或經核對的逐字稿,並記錄所有者、版本及核准情況。先與所有者釐清轉錄正確性、各管道改編權、隱私及任何商業優惠。影片網址或下方空白範本不符合輸入要求。
四個參考 skills 使用 Claude Code 文件記載的 `.claude/skills/<name>/SKILL.md` 位置、斜線命令呼叫與 `$ARGUMENTS` 替換。它們的 `context: fork` 設定讓每次呼叫擁有不含對話紀錄的獨立上下文。草稿 skills 使用 `background: true`,需要 Claude Code **v2.1.218 或更新版本**。它們使用通用代理,其檔案寫入能力及有效權限取決於使用者的 Claude Code 設定。skill 文字不授予權限,也無法強制唯讀邊界。主機允許時背景作業可能重疊,但本流程不保證平行執行。採用這些名稱前,先檢查現有同名 skills 與命令探索機制,確認命令指向安裝的專案檔案。請參閱[skills 手冊](https://code.claude.com/docs/en/skills)與[subagents 手冊](https://code.claude.com/docs/en/sub-agents).
## 2. 安裝四個檔案與契約
在專案內建立下方清單的精確路徑。完整複製參考檔案區的每個區塊,不變更內容。`.claude/repurpose-contracts/` 下的五個檔案定義輸入、清單、受阻狀態與管道結果的格式,以及相依計畫。將 `join.py` 放在專案根目錄。兩個範本放在 `repurpose-inputs/` 下;執行前必須用真正核准的內容取代佔位文字。輸出目錄樹僅說明結構。下方提供可複製的完整檔案。
| 檔案 | 用途 |
| --- | --- |
| `.claude/skills/source-inventory/SKILL.md` | 檢查核准情況,並在起草前盤點來源的每一行。 |
| `.claude/skills/draft-linkedin/SKILL.md` | 僅負責 LinkedIn 草稿與結果。 |
| `.claude/skills/draft-newsletter/SKILL.md` | 僅負責電子報草稿與結果。 |
| `.claude/skills/draft-ad/SKILL.md` | 僅負責廣告草稿與結果,或明確的優惠受阻原因。 |
| `.claude/repurpose-contracts/*.json` | 定義精確欄位、雜湊關聯與階段順序。 |
| `join.py` | 檢查結構及來源,再建立陳述對照與審閱交接材料。 |
| `repurpose-inputs/approved-source.template.md`, `repurpose-inputs/constraints.template.json` | 空白起始表單,不是執行證據。 |
即使廣告分支因缺少核准優惠而受阻,約束仍須明確許可 LinkedIn、電子報與廣告的改編權。`forbiddenClaims` 在所有者沒有其他禁止事項時可為空清單。廣告需要核准的優惠、HTTP(S) 目的地與行動呼籲;任一值缺少都會阻擋該分支。記錄真實的指定審閱者。執行中不要更改核准來源與約束。
## 3. 測試每條相依關係,再執行清單
先用範圍有限且真正核准的來源,分別測試每個 skill。將實際檔案及雜湊與契約比較。BIG CHANGE 未在 Claude Code 做過此測試。完整執行時,為輸入、執行輸出與彙整輸出建立新的絕對路徑目錄。在儲存庫內以三個絕對路徑呼叫清單命令:
```prompt
/source-inventory <absolute-path-to-approved-source.md> <absolute-path-to-constraints.json> <absolute-path-to-new-run-directory>
```
將每個角括號參數換成實際路徑。skill 應寫出 `inventory.json`,依原文與順序列出涵蓋每行的段落範圍;或寫出 `inventory-blocked.json`,指出確切缺少的核准或權利。清單受阻時不要開始起草。以兩個輸入檔核對記錄的 SHA-256,並檢查段落邊界,包括空白行。
## 4. 在三個隔離管道起草
當 `inventory.json` 存在,且兩個輸入的雜湊仍符合記錄時,呼叫三個獨立 skills。第一個參數使用相同的絕對執行目錄,第二個參數使用三個不同的輸出目錄。每個 skill 讀取核准來源、約束與清單,不讀取同級草稿。清單完成後可獨立啟動,但仍受 Claude Code 主機及權限限制。
```prompt
/draft-linkedin <absolute-run-directory> <absolute-run-directory>/linkedin
/draft-newsletter <absolute-run-directory> <absolute-run-directory>/newsletter
/draft-ad <absolute-run-directory> <absolute-run-directory>/ad
```
每個管道負責 `result.json`,且僅在就緒時負責 `draft.md`。結果標示管道,以及來源、約束與清單的 SHA-256,再把重要陳述或引文對應到段落 ID 與適用約束鍵。`ready` 表示可進行事實審閱。缺少核准廣告優惠、目的地或行動呼籲時,必須產生受阻結果,不得產生廣告草稿。背景呼叫或有效 JSON 檔案本身不能證明草稿實用或正確。
## 5. 彙整結果,由人作決定
三個階段都已回傳或明確失敗後,用執行目錄與一個**新的**彙整目錄執行提供的 Python 檔。程式使用 Python 標準函式庫;呼叫前確認本機 Python 已安裝。
```bash
python join.py <absolute-run-directory> <absolute-new-join-directory>
```
彙整會檢查來源與約束雜湊、清單是否涵蓋所有行、各管道記錄的清單雜湊、階段身分、陳述參照、輸出歸屬與廣告前提。它寫出 `claim-map.md`, `review.md`與`summary.json`。缺少或無效的階段會標為受阻,有效的同級草稿仍可見。整體來源不符會停止彙整。它不判斷句子是否真實、引文是否公平、陳述是否有適當法律依據,或所有者是否已核准發布。`review.md` 起始包含 `PENDING` 決定。
指定審閱者應開啟每份就緒的 `draft.md`,將所有重要陳述與引文對照所指段落及約束,檢查歧義、管道語氣、改編權、廣告目的地及優惠,然後在產生的待定欄位以外,為各管道記錄**接受、修改或拒絕**。發布、傳送、排程與投放廣告,都需要另外的人工授權與平台檢查。
## 6. 修復失敗分支,同時保留有效成果
來源核准或權利不明時,與所有者解決具體缺口,用修正後的輸入開始新一輪執行。來源或約束雜湊有任何變更,都需要重新製作完整清單。一個管道失敗而其他有效時,先檢查該管道的實際檔案與錯誤。將位元組未變的清單及已驗證的同級資料夾複製到新執行目錄,只在該處重跑失敗階段,再彙整至另一個新目錄。複製的結果仍須符合清單雜湊。程序結果不確定時,先讀取結果與雜湊再決定是否重試。保留先前執行與審閱者修改,絕不用重跑覆寫。缺少或不支援的陳述參照,應在負責階段修復,再於新目錄重做確定性彙整。
這是手動呼叫、由模型執行的組合流程,不提供程式碼強制調度、自動重試、固定並行數或外部副作用復原。請由已核准的書面來源開始;本流程不取得或核驗逐字稿,也不製作行銷影片、音訊或視覺素材。
## 完整參考檔案
請一併保留路徑及內容。下方程式碼區塊是獨立審查材料的完整副本,屬於參考檔案,不是 Claude 的執行紀錄。
### `.claude/skills/source-inventory/SKILL.md`
```markdown
---
name: source-inventory
description: Build a bounded passage inventory from one approved owned source for a three-format draft pipeline.
disable-model-invocation: true
context: fork
agent: general-purpose
background: false
---
Input arguments: `$ARGUMENTS[0]` is the approved UTF-8 source Markdown path, `$ARGUMENTS[1]` is the constraints JSON path, and `$ARGUMENTS[2]` is a new run directory. Read only those inputs and the run directory contract. Do not infer inputs from conversation history.
1. Verify both files exist. Read `constraints.json` against `.claude/repurpose-contracts/constraints.schema.json`; require `sourceOwner`, `sourceVersion`, `approvedAt`, `adaptationRights`, `audience`, `permittedChannels`, `reviewer`, `forbiddenClaims`, and `offer`. `adaptationRights` must explicitly mark all three channels `true`; a template or unapproved value does not count. `offer` may be null; that blocks ad copy requiring an offer or destination. If any approval or right is unclear, create only `inventory-blocked.json` under `.claude/repurpose-contracts/inventory-blocked.schema.json` explaining the gap and stop.
2. Record SHA-256 hashes of the exact two input files. Divide the entire approved source into numbered, contiguous, verbatim line ranges with no omitted lines. Give each range a stable `P001`-style ID, start/end line and exact text, including blank lines where they occur in a range. Do not add outside facts, inferred transcript words, or quotations not present in the source.
3. Write `inventory.json` in the run directory according to `.claude/repurpose-contracts/inventory.schema.json` in the reader project. Record the source and constraints paths/hashes, source owner/version/approval, passages and any ambiguous passages requiring human review. Do not edit either input.
4. Return the inventory path and blockers. This stage never drafts, publishes, fetches video, contacts services or approves source rights on behalf of the owner.
```
### `.claude/skills/draft-linkedin/SKILL.md`
```markdown
---
name: draft-linkedin
description: Draft one review-only LinkedIn post from an approved source inventory.
disable-model-invocation: true
context: fork
agent: general-purpose
background: true
---
Inputs: `$ARGUMENTS[0]` is the run directory containing `inventory.json`; `$ARGUMENTS[1]` is the dedicated `linkedin` output directory. Read the source and constraints paths recorded in the inventory and verify their hashes still match. Do not rely on prior conversation or sibling-stage results.
Write only under the dedicated output directory. If required source, permission, audience or voice information is missing or conflicting, write `result.json` with `status: blocked` and exact blockers; do not invent it. Otherwise create `draft.md`: one self-contained LinkedIn post with a factual opening and concrete takeaway, no invented statistics, quotes, benefits, offers or links. Write `result.json` according to `.claude/repurpose-contracts/stage-result.schema.json` in the reader project, with `channel: linkedin`, input hashes, inventory hash, draft path, and every material claim or quote paired with exact passage IDs and any applicable constraints keys. Mark uncertainty in the draft and result. `status: ready` means ready for human fact review, not approved or published. Return the two file paths. Never post or schedule.
```
### `.claude/skills/draft-newsletter/SKILL.md`
```markdown
---
name: draft-newsletter
description: Draft one review-only newsletter section from an approved source inventory.
disable-model-invocation: true
context: fork
agent: general-purpose
background: true
---
Inputs: `$ARGUMENTS[0]` is the run directory containing `inventory.json`; `$ARGUMENTS[1]` is the dedicated `newsletter` output directory. Read the source and constraints paths recorded in the inventory and verify their hashes still match. Do not rely on prior conversation or sibling-stage results.
Write only under the dedicated output directory. If required source, permission, audience or voice information is missing or conflicting, write `result.json` with `status: blocked` and exact blockers. Otherwise create `draft.md`: a descriptive heading, short summary and evidence-linked detail suited to the approved newsletter audience. Add no invented statistics, quotes, benefits or outside news. Write `result.json` according to `.claude/repurpose-contracts/stage-result.schema.json` in the reader project, with `channel: newsletter`, input hashes, inventory hash, draft path, and every material claim or quote paired with exact passage IDs and applicable constraints keys. Mark uncertainty in the draft and result. `status: ready` means ready for human fact review, not approved or sent. Return the two file paths. Never send or schedule.
```
### `.claude/skills/draft-ad/SKILL.md`
```markdown
---
name: draft-ad
description: Draft review-only ad copy from an approved source inventory and approved offer.
disable-model-invocation: true
context: fork
agent: general-purpose
background: true
---
Inputs: `$ARGUMENTS[0]` is the run directory containing `inventory.json`; `$ARGUMENTS[1]` is the dedicated `ad` output directory. Read the source and constraints paths recorded in the inventory and verify their hashes still match. Do not rely on prior conversation or sibling-stage results.
Write only under the dedicated output directory. Require an approved offer, destination URL and call to action in constraints, plus adaptation rights for advertising. If any are absent, write `result.json` with `status: blocked`, the missing exact fields and no draft. Otherwise create `draft.md` with one headline, one body and the approved call to action. No invented metrics, testimonials, urgency, claims, price or performance. Write `result.json` according to `.claude/repurpose-contracts/stage-result.schema.json` in the reader project, with `channel: ad`, input hashes, inventory hash, draft path, and every material claim or quote paired with exact passage IDs and applicable constraints keys. Mark uncertainty in the draft and result. `status: ready` means ready for human fact/legal review, not approved or placed. Return the two file paths. Never upload, buy or schedule ads.
```
### `.claude/repurpose-contracts/constraints.schema.json`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Reader-approved source constraints",
"type": "object",
"required": ["sourceOwner", "sourceVersion", "approvedAt", "adaptationRights", "audience", "permittedChannels", "reviewer", "forbiddenClaims", "voiceRules", "offer"],
"properties": {
"sourceOwner": {"type": "string", "minLength": 1},
"sourceVersion": {"type": "string", "minLength": 1},
"approvedAt": {"type": "string", "minLength": 1},
"adaptationRights": {"type": "object", "required": ["linkedin", "newsletter", "ad"], "properties": {"linkedin": {"type": "boolean"}, "newsletter": {"type": "boolean"}, "ad": {"type": "boolean"}}},
"audience": {"type": "string", "minLength": 1},
"permittedChannels": {"type": "array", "uniqueItems": true, "items": {"enum": ["linkedin", "newsletter", "ad"]}},
"reviewer": {"type": "string", "minLength": 1},
"forbiddenClaims": {"type": "array", "items": {"type": "string"}},
"voiceRules": {"type": "string", "minLength": 1},
"offer": {"type": ["object", "null"], "required": ["approvedOffer", "destinationUrl", "callToAction"], "properties": {"approvedOffer": {"type": "string", "minLength": 1}, "destinationUrl": {"type": "string", "format": "uri"}, "callToAction": {"type": "string", "minLength": 1}}}
}
}
```
### `.claude/repurpose-contracts/inventory.schema.json`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Approved source inventory",
"type": "object",
"additionalProperties": false,
"required": ["sourcePath", "sourceSha256", "constraintsPath", "constraintsSha256", "sourceOwner", "sourceVersion", "approvedAt", "passages", "ambiguities"],
"properties": {
"sourcePath": {"type": "string", "minLength": 1},
"sourceSha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"constraintsPath": {"type": "string", "minLength": 1},
"constraintsSha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"sourceOwner": {"type": "string", "minLength": 1},
"sourceVersion": {"type": "string", "minLength": 1},
"approvedAt": {"type": "string", "minLength": 1},
"passages": {"type": "array", "minItems": 1, "items": {"type": "object", "additionalProperties": false, "required": ["id", "startLine", "endLine", "text"], "properties": {"id": {"type": "string", "pattern": "^P[0-9]{3,}$"}, "startLine": {"type": "integer", "minimum": 1}, "endLine": {"type": "integer", "minimum": 1}, "text": {"type": "string"}}}},
"ambiguities": {"type": "array", "items": {"type": "string"}}
}
}
```
### `.claude/repurpose-contracts/inventory-blocked.schema.json`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Preflight block before drafting",
"type": "object",
"additionalProperties": false,
"required": ["status", "blockers"],
"properties": {
"status": {"const": "blocked"},
"blockers": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}
}
}
```
### `.claude/repurpose-contracts/stage-result.schema.json`
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "One isolated channel result",
"type": "object",
"additionalProperties": false,
"required": ["channel", "status", "sourceSha256", "constraintsSha256", "inventorySha256", "draftFile", "claims", "blockers"],
"properties": {
"channel": {"enum": ["linkedin", "newsletter", "ad"]},
"status": {"enum": ["ready", "blocked"]},
"sourceSha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"constraintsSha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"inventorySha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"draftFile": {"type": ["string", "null"]},
"claims": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["text", "passageIds", "constraintKeys", "status"], "properties": {"text": {"type": "string", "minLength": 1}, "passageIds": {"type": "array", "items": {"type": "string", "pattern": "^P[0-9]{3,}$"}}, "constraintKeys": {"type": "array", "items": {"type": "string"}}, "status": {"enum": ["supported", "needs-human-check", "unsupported"]}}}},
"blockers": {"type": "array", "items": {"type": "string"}}
}
}
```
### `.claude/repurpose-contracts/pipeline-plan.json`
```json
{
"purpose": "Compose reader-owned skills into one source-to-three-format review run",
"execution": "manual Claude Code skill invocation plus deterministic local join",
"sourceStage": {
"skill": "source-inventory",
"inputs": ["approved-source.md", "constraints.json"],
"success": "inventory.json with source and constraints hashes and complete numbered line coverage",
"failure": "inventory-blocked.json; stop all channel stages"
},
"parallelAfterInventory": [
{"skill": "draft-linkedin", "reads": ["approved-source.md", "constraints.json", "inventory.json"], "writesOnly": "linkedin/", "contract": "stage-result.schema.json"},
{"skill": "draft-newsletter", "reads": ["approved-source.md", "constraints.json", "inventory.json"], "writesOnly": "newsletter/", "contract": "stage-result.schema.json"},
{"skill": "draft-ad", "reads": ["approved-source.md", "constraints.json", "inventory.json"], "writesOnly": "ad/", "contract": "stage-result.schema.json"}
],
"join": {
"file": "join.py",
"dependsOn": ["all three stage results or recorded missing/failed slots"],
"writesOnly": "new join directory",
"outputs": ["claim-map.md", "review.md", "summary.json"],
"humanApproval": "pending"
}
}
```
### `join.py`
```python
"""Deterministic join/check for the proposed local, review-only pipeline.
This does not call a model, generate prose, or approve factual support. Run only
on reader-owned paths after separately invoked stage skills have returned.
"""
import hashlib
import json
import sys
from pathlib import Path
from urllib.parse import urlparse
CHANNELS = ("linkedin", "newsletter", "ad")
HEX = set("0123456789abcdef")
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def valid_hash(value):
return isinstance(value, str) and len(value) == 64 and set(value) <= HEX
def read_json(path):
return json.loads(path.read_text(encoding="utf-8"))
def string_list(value):
return isinstance(value, list) and all(isinstance(v, str) and v.strip() for v in value)
def approved_offer(value):
return (isinstance(value, dict) and
all(isinstance(value.get(k), str) and value[k].strip()
for k in ("approvedOffer", "destinationUrl", "callToAction")) and
urlparse(value["destinationUrl"]).scheme in ("http", "https") and
bool(urlparse(value["destinationUrl"]).netloc))
def check_inventory(run):
inv_path = run / "inventory.json"
inv = read_json(inv_path)
expected = {"sourcePath", "sourceSha256", "constraintsPath", "constraintsSha256",
"sourceOwner", "sourceVersion", "approvedAt", "passages", "ambiguities"}
if set(inv) != expected:
raise ValueError("inventory keys differ from contract")
source, constraints = Path(inv["sourcePath"]), Path(inv["constraintsPath"])
if not source.is_file() or not constraints.is_file():
raise ValueError("approved source or constraints missing")
if digest(source) != inv["sourceSha256"] or digest(constraints) != inv["constraintsSha256"]:
raise ValueError("approved input changed since inventory")
rules = read_json(constraints)
required = ("sourceOwner", "sourceVersion", "approvedAt", "adaptationRights",
"audience", "permittedChannels", "reviewer", "forbiddenClaims", "voiceRules", "offer")
if not isinstance(rules, dict) or any(k not in rules for k in required):
raise ValueError("constraints approval/rights fields incomplete")
text_fields = ("sourceOwner", "sourceVersion", "approvedAt", "audience", "reviewer", "voiceRules")
if any(not isinstance(rules[k], str) or not rules[k].strip() for k in text_fields):
raise ValueError("constraints text fields incomplete")
if not string_list(rules["forbiddenClaims"]) or not string_list(rules["permittedChannels"]):
raise ValueError("constraints lists malformed")
if any("REPLACE_WITH" in rules[k] for k in text_fields):
raise ValueError("constraints template was not completed")
if not isinstance(rules["adaptationRights"], dict) or any(rules["adaptationRights"].get(c) is not True for c in CHANNELS):
raise ValueError("all three adaptation rights must be explicit")
if not set(CHANNELS).issubset(set(rules["permittedChannels"])):
raise ValueError("three-channel rights not explicit")
if any(rules[k] != inv[k] for k in ("sourceOwner", "sourceVersion", "approvedAt")):
raise ValueError("source identity differs from constraints")
lines = source.read_text(encoding="utf-8").splitlines()
if not lines:
raise ValueError("source is empty")
next_line, ids = 1, set()
if not isinstance(inv["passages"], list) or not string_list(inv["ambiguities"]):
raise ValueError("inventory passages or ambiguities malformed")
for n, p in enumerate(inv["passages"], 1):
if set(p) != {"id", "startLine", "endLine", "text"}:
raise ValueError("passage keys differ from contract")
if p["id"] != f"P{n:03d}" or p["id"] in ids:
raise ValueError("passage IDs must be ordered and unique")
ids.add(p["id"])
if (type(p["startLine"]) is not int or type(p["endLine"]) is not int or
p["startLine"] != next_line or p["endLine"] < next_line or p["endLine"] > len(lines)):
raise ValueError("source line range gap or overlap")
actual = "\n".join(lines[p["startLine"] - 1:p["endLine"]])
if p["text"] != actual:
raise ValueError("passage text differs from approved source")
next_line = p["endLine"] + 1
if next_line != len(lines) + 1:
raise ValueError("inventory omits source lines")
return inv_path, inv, rules, ids
def check_stage(run, channel, inv, rules, ids, inv_hash):
folder = run / channel
path = folder / "result.json"
if not path.is_file():
return {"channel": channel, "status": "blocked", "blockers": ["result.json missing"], "claims": []}
try:
result = read_json(path)
required = {"channel", "status", "sourceSha256", "constraintsSha256",
"inventorySha256", "draftFile", "claims", "blockers"}
if set(result) != required or result["channel"] != channel:
raise ValueError("result keys or channel differ from contract")
if result["status"] not in ("ready", "blocked"):
raise ValueError("invalid result status")
if (result["sourceSha256"] != inv["sourceSha256"] or
result["constraintsSha256"] != inv["constraintsSha256"] or
result["inventorySha256"] != inv_hash):
raise ValueError("result belongs to another source/inventory")
if not isinstance(result["claims"], list) or not string_list(result["blockers"]):
raise ValueError("claims/blockers must be arrays")
if result["status"] == "ready":
if channel == "ad" and not approved_offer(rules.get("offer")):
raise ValueError("ad requires approved offer, HTTP(S) destination and call to action")
if result["draftFile"] != "draft.md" or not result["claims"] or result["blockers"]:
raise ValueError("ready result lacks draft or claim list")
draft = folder / "draft.md"
if not draft.is_file() or not draft.read_text(encoding="utf-8").strip():
raise ValueError("ready draft missing or empty")
elif not result["blockers"] or result["draftFile"] is not None:
raise ValueError("blocked result needs reasons and no draft")
for claim in result["claims"]:
if set(claim) != {"text", "passageIds", "constraintKeys", "status"}:
raise ValueError("claim keys differ from contract")
if not isinstance(claim["text"], str) or not claim["text"].strip():
raise ValueError("empty claim")
if claim["status"] not in ("supported", "needs-human-check", "unsupported"):
raise ValueError("invalid claim status")
if not string_list(claim["passageIds"]) or not string_list(claim["constraintKeys"]):
raise ValueError("claim evidence pointers must be string arrays")
if not set(claim["passageIds"]).issubset(ids):
raise ValueError("unknown source passage ID")
if not claim["passageIds"] and not claim["constraintKeys"]:
raise ValueError("claim has no evidence pointer")
if not set(claim["constraintKeys"]).issubset(rules):
raise ValueError("unknown constraints key")
if claim["status"] == "unsupported" and result["status"] == "ready":
raise ValueError("unsupported claim in ready draft")
return result
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
return {"channel": channel, "status": "blocked", "blockers": [f"invalid result: {exc}"], "claims": []}
def main():
if len(sys.argv) != 3:
raise SystemExit("Usage: python join.py RUN_DIRECTORY NEW_JOIN_DIRECTORY")
run, target = Path(sys.argv[1]).resolve(), Path(sys.argv[2]).resolve()
if target.exists():
raise SystemExit("join output already exists; use a new directory to preserve prior review")
inv_path, inv, rules, ids = check_inventory(run)
inv_hash = digest(inv_path)
results = [check_stage(run, channel, inv, rules, ids, inv_hash) for channel in CHANNELS]
target.mkdir(parents=True, exist_ok=False)
map_lines = ["# Claim map for human review", "", f"Source SHA-256: {inv['sourceSha256']}",
f"Constraints SHA-256: {inv['constraintsSha256']}", f"Inventory SHA-256: {inv_hash}", ""]
review_lines = ["# Review handoff", "", f"Reviewer: {rules['reviewer']}",
"No draft is approved, sent or published by this join.", ""]
for result in results:
channel = result["channel"]
map_lines += [f"## {channel}", ""]
review_lines += [f"## {channel}: {result['status']}", ""]
for blocker in result["blockers"]:
review_lines.append(f"- Blocker: {blocker}")
for claim in result["claims"]:
refs = ", ".join(claim["passageIds"] + claim["constraintKeys"])
map_lines.append(f"- {claim['status']}: {claim['text']} — {refs}")
map_lines.append("")
review_lines += ["- Human must compare every claim and quote with the approved inputs.",
"- Human must check rights, voice, audience and any offer/destination.",
"- Human approval decision: PENDING", ""]
(target / "claim-map.md").write_text("\n".join(map_lines), encoding="utf-8")
(target / "review.md").write_text("\n".join(review_lines), encoding="utf-8")
summary = {"sourceSha256": inv["sourceSha256"], "constraintsSha256": inv["constraintsSha256"],
"inventorySha256": inv_hash, "channels": {r["channel"]: r["status"] for r in results},
"humanApproval": "pending"}
(target / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(json.dumps(summary))
if __name__ == "__main__":
main()
```
### `repurpose-inputs/approved-source.template.md`
```markdown
# Replace with the owner's approved written source
This template is deliberately empty of publishable facts. Supply the exact approved transcript or article, retain line breaks for passage IDs, and record its owner and version in constraints.json. Do not invoke the pipeline on this template.
```
### `repurpose-inputs/constraints.template.json`
```json
{
"sourceOwner": "REPLACE_WITH_ACTUAL_OWNER",
"sourceVersion": "REPLACE_WITH_ACTUAL_VERSION",
"approvedAt": "REPLACE_WITH_ACTUAL_APPROVAL_DATE",
"adaptationRights": {"linkedin": false, "newsletter": false, "ad": false},
"audience": "REPLACE_WITH_APPROVED_AUDIENCE",
"permittedChannels": ["linkedin", "newsletter", "ad"],
"reviewer": "REPLACE_WITH_ACTUAL_HUMAN_REVIEWER",
"forbiddenClaims": [],
"voiceRules": "REPLACE_WITH_APPROVED_VOICE_RULES",
"offer": null
}
```
### `expected-output-tree.txt`
```markdown
STRUCTURE ONLY. No Claude run or model output exists in this packet.
<new-run>/
inventory.json from source-inventory skill, or inventory-blocked.json
linkedin/result.json ready or blocked; draft.md only when ready
newsletter/result.json ready or blocked; draft.md only when ready
ad/result.json ready or blocked; draft.md only when ready
<new-join>/
claim-map.md generated by join.py from stage claims
review.md all three statuses and PENDING human decisions
summary.json hashes, statuses and humanApproval=pending
```
## 來源與延伸閱讀
- [Claude Code skills 手冊](https://code.claude.com/docs/en/skills)——專案 skill 路徑、frontmatter、呼叫、引數替換、獨立上下文與背景行為。2026 年 10 月 19 日核查;手冊沒有提供涵蓋整個步驟的單一版本號。
- [Claude Code subagents 手冊](https://code.claude.com/docs/en/sub-agents)——通用代理、上下文與權限行為。2026 年 10 月 19 日核查。
- [Claude Code 費用](https://code.claude.com/docs/en/costs), [資料使用](https://code.claude.com/docs/en/data-usage)與[權限](https://code.claude.com/docs/en/permissions)——依帳戶而定的用量、資料處理及工具控制。2026 年 10 月 19 日核查;未量測此流程的花費。
- 經獨立審查的本機材料提供四個 skills、五個 JSON 契約、兩個輸入範本、輸出目錄樹與彙整程式。16 項檢查只使用暫時的結構測試資料,未產生文稿或事實核准。
- [BIG CHANGE 相關指南:平行文件審閱](https://bigchange.ai/blog/claude-managed-agents-parallel-document-review-guide)介紹以文件審閱為基礎的另一種 Claude 工作流程,而非在 Code 本機將來源改編至三個管道。
## Sources
- [Claude Code skills 手冊](https://code.claude.com/docs/en/skills) — 專案路徑、呼叫、獨立上下文及背景行為;2026-10-19 核查 HTTP 200 與雜湊。
- [Claude Code subagents 手冊](https://code.claude.com/docs/en/sub-agents) — 通用代理與權限行為;2026-10-19 核查 HTTP 200 與雜湊。
- [Claude Code 費用](https://code.claude.com/docs/en/costs) — 依帳戶而定的用量及工作階段用量報告;2026-10-19 核查。
- [Claude Code 資料使用](https://code.claude.com/docs/en/data-usage) — 帳戶與組織資料政策;2026-10-19 核查。
- [Claude Code 權限](https://code.claude.com/docs/en/permissions) — 實際工具存取在 skill 文字之外設定;2026-10-19 核查。