获批文章或转录稿可以支持多份草稿,但每份都必须能清楚追溯到来源。本指南组合四个独立的 Claude Code 项目 skills,清点内容所有者批准的书面来源,在不同目录中起草 LinkedIn 帖子、新闻邮件段落和广告,再用本地 Python 汇合脚本检查文件契约。输出是交给指定人工审核者的草稿,不代表获准发布或发送。
BIG CHANGE 审阅了下方的 skills、契约和汇合代码。独立审核者运行了 16 项本地结构检查。我们没有在 Claude Code 中安装或运行这些 skills,没有付费运行模型、生成真实的渠道文稿,也没有测试实际营销活动。以下是供读者使用自己的获批来源进行测试的操作步骤。
关键变化
- 变化是什么: 四个自有 skills 采用明确的来源、清单和渠道结果契约,随后进行确定性的结果汇合。
- 为什么重要: 每份草稿都可追溯至来源段落;缺失渠道则明确保留为受阻项。
- 需要注意: 文件检查不能证明事实真实、改编权有效或编辑已批准;每份就绪草稿仍须由人核查。
1. 确认权限、权利和来源
使用自己控制的 Claude Code 仓库,以及获准处理该来源的账户。处理机密材料前,检查账户可用模型、用量与费用, 数据政策和工具权限。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 手册和subagents 手册.
2. 安装四个文件和契约
在项目内创建下方文件列表中的精确路径。完整复制参考文件部分的每个代码块,不修改其内容。.claude/repurpose-contracts/ 下的五个文件定义输入、清单、受阻状态、渠道结果的数据形状,以及依赖计划。将 join.py 放在项目根目录。两个模板放在 repurpose-inputs/ 下;任何运行前都须用真正获批的内容替换占位符。输出目录树仅说明结构。下方提供可复制的完整文件。
文件 | 用途 |
|---|---|
| 检查批准情况,在起草前清点来源的每一行。 |
| 仅负责 LinkedIn 草稿及结果。 |
| 仅负责新闻邮件草稿及结果。 |
| 仅负责广告草稿及结果,或明确的报价受阻原因。 |
| 定义精确字段、哈希关联及阶段顺序。 |
| 检查结构和溯源,创建陈述映射和审核交接材料。 |
| 空白起始表格,绝不构成运行证据。 |
约束必须明确许可 LinkedIn、新闻邮件和广告的改编权,即使广告分支因缺少获批报价而受阻。forbiddenClaims 在所有者没有其他禁止事项时可为空列表。广告需要获批报价、HTTP(S) 目标地址和行动号召;任何一项缺失都会阻塞该分支。记录真实的指定审核者。运行期间不要更改获批来源或约束。
3. 测试每条依赖关系,再运行清单
先用规模有限、真实获批的来源单独测试每个 skill。将实际文件及哈希与契约比较。BIG CHANGE 未在 Claude Code 中做过这项测试。完整运行时,为输入、运行输出和汇合输出创建新的绝对路径目录。在仓库中用三个绝对路径调用清单命令:
/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 主机及权限限制。
/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 安装。
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
---
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
---
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
---
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
---
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
{
"$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
{
"$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
{
"$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
{
"$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
{
"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
"""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
# 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
{
"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
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 手册——项目 skill 路径、frontmatter、调用、参数替换、独立上下文及后台行为。2026 年 10 月 19 日核查;手册未给出适用于整个步骤的单一版本号。
- Claude Code subagents 手册——通用代理、上下文及权限行为。2026 年 10 月 19 日核查。
- Claude Code 费用, 数据使用和权限——依账户而定的用量、数据处理及工具控制。2026 年 10 月 19 日核查;未测量此流程的花费。
- 经独立审阅的本地材料提供四个 skills、五个 JSON 契约、两个输入模板、输出目录树和汇合脚本。16 项检查只使用临时结构测试数据,没有生成文稿或产生事实批准。
- BIG CHANGE 相关指南:并行文档审核介绍基于文档审核的另一种 Claude 工作流,而非在 Code 本地把来源改编为三个渠道。



