世界は立ち止まらない。RSS
BIG CHANGE.

Markdown版

AI-translated from English; not yet reviewed by a fluent editor.

# Claude Code の skills を組み合わせ、承認済み資料から確認用の下書きを3本作る

> 4つの独立した 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

![Conceptual charcoal illustration of three unbranded layout proofs: a social post on a phone, an open newsletter on a wooden stand, and a small ad in a metal holder.](https://bigchange.ai/api/media/file/three-channel-proof-hero-v1.png)
AI-generated conceptual illustration by BIG CHANGE; no actual Claude Code interface, model output, reviewed draft or campaign is depicted.

承認済みの記事や書き起こしから複数の下書きを作れますが、各下書きには出典へ遡る明確な経路が必要です。このガイドは独立した4つの [Claude Code のプロジェクト skills](https://code.claude.com/docs/en/skills) を組み合わせ、所有者が承認した文章を棚卸しし、LinkedIn 投稿、ニュースレターの一節、広告を別ディレクトリで作成します。その後、ローカルの Python 統合処理でファイル契約を検査します。出力は指名された人間のレビュアー向けの下書きであり、公開や送信の許可ではありません。

BIG CHANGE は以下の skills、契約、統合コードを確認し、独立したレビュアーがローカルで16項目の構造検査を実行しました。ただし、**実施していません**:Claude Code での skills の導入・実行、有料モデル実行、事実に基づく媒体用下書きの生成、実際のキャンペーンのテスト。以下は読者自身の承認済み資料で試す手順です。

## 大きな変化

- **何が変わるか:** 管理する4つの 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 記事または確認済み書き起こしを使い、所有者、版、承認を記録します。書き起こしの正確さ、各媒体への翻案権、プライバシー、広告の提案条件を先に所有者と解決します。動画 URL や空のひな型は入力要件を満たしません。

4つの参照 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. 4ファイルと契約を配置する

プロジェクト内に一覧どおりの正確なパスを作り、参照ファイルの各ブロックを内容を変えず全文コピーします。`.claude/repurpose-contracts/` 配下の5ファイルは入力、一覧、ブロック状態、媒体結果の形式と依存順序を定義します。`join.py` をプロジェクトルートに置き、2つのひな型を `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 でこのテストをしていません。全体実行には入力、実行結果、統合結果の新しい絶対パスディレクトリを作ります。リポジトリで3つの絶対パスを指定して一覧コマンドを呼び出します。

```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. 独立した3媒体の下書きを作る

まず、`inventory.json` が存在し、両入力のハッシュが記録に一致していることを確認して3つの skills を呼び出します。第1引数は同じ絶対実行ディレクトリ、第2引数は別々の3出力ディレクトリです。各 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. 結果を統合し、人が判断する

3段階すべてが戻るか明確に失敗したら、実行ディレクトリと**新しい**統合ディレクトリを指定して付属 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. 有効な作業を残して失敗した分岐を復旧する

承認や権利が不明なら所有者と不足を解決し、修正入力で新規実行します。資料または制約のハッシュが変われば一覧全体を作り直します。他媒体が有効で1媒体が失敗したら、その実ファイルとエラーを先に調べます。不変の一覧バイト列と確認済み他媒体フォルダを新しい実行ディレクトリへコピーし、失敗段階だけを実行して別の新規ディレクトリへ統合します。コピー結果も一覧ハッシュに一致する必要があります。結果が不確かなプロセスは結果とハッシュを読んでから再試行を決めます。以前の実行と人の修正を残し、上書きしません。欠落・未対応の主張参照は担当段階で修正し、新規ディレクトリに決定論的統合を繰り返します。

これは手動呼び出しによるモデル主導の組み合わせです。コードによる強制ディスパッチ、自動再試行、固定並列数、外部副作用の復旧はありません。承認済み文章から始めます。書き起こしの取得・検証やキャンペーンの動画・音声・画像作成は対象外です。

## 完全な参照ファイル

パスと内容を一緒に保管します。以下のコードブロックは独立レビュー済み資料の完全なコピーで、参照ファイルです。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日確認。このフローの支出は測定していません。
- 独立レビュー済みのローカル資料は4 skills、5 JSON 契約、2入力ひな型、出力ツリー、統合スクリプトを提供します。16項目の検査は一時的な構造テストデータだけを使用し、文章生成も事実承認も行っていません。
- [BIG CHANGE 関連ガイド:文書の並列レビュー](https://bigchange.ai/blog/claude-managed-agents-parallel-document-review-guide)は、Code 内で元資料を3媒体に翻案するものとは異なる文書レビュー中心の Claude フローを扱います。

## 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 確認。