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

# Gabungkan skills Claude Code untuk tiga draf terperiksa dari satu sumber yang disetujui

> Gunakan empat skills Claude Code terpisah untuk menginventarisasi sumber tertulis yang disetujui, menulis draf LinkedIn, buletin dan iklan, lalu memeriksa hash berkas dan klaim sebelum tinjauan manusia.

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.

Artikel atau transkrip yang disetujui dapat memasok beberapa draf, tetapi setiap draf harus bisa ditelusuri dengan jelas ke sumbernya. Panduan ini menggabungkan empat [skills proyek Claude Code](https://code.claude.com/docs/en/skills) terpisah untuk menginventarisasi sumber tertulis yang disetujui pemiliknya, menulis pos LinkedIn, bagian buletin dan iklan di direktori terpisah, lalu memeriksa kontrak berkas dengan penggabungan Python lokal. Hasilnya adalah draf untuk peninjau manusia yang disebutkan namanya, bukan izin untuk menerbitkan atau mengirim.

BIG CHANGE meninjau skills, kontrak dan kode penggabungan di bawah. Peninjau independen menjalankan 16 pemeriksaan struktural lokal. Kami **tidak** memasang atau menjalankan skills di Claude Code, membayar eksekusi model, menghasilkan draf faktual kanal atau menguji kampanye nyata. Langkah berikut merupakan prosedur untuk diuji pembaca menggunakan sumber mereka sendiri yang telah disetujui.

## Perubahan besar

- **Yang berubah:** Empat skills yang dikelola sendiri memiliki kontrak eksplisit untuk sumber, inventaris dan hasil kanal, diikuti penggabungan deterministik.
- **Mengapa penting:** Setiap draf dapat ditelusuri ke bagian sumber, sedangkan kanal yang hilang tetap terlihat sebagai slot terblokir.
- **Yang perlu diperhatikan:** Pemeriksaan berkas tidak membuktikan kebenaran, hak adaptasi atau persetujuan editorial; manusia harus memeriksa setiap draf siap.

## 1. Pastikan akses, hak dan sumber

Gunakan repositori Claude Code yang Anda kendalikan dan akun yang diizinkan memproses sumber. Sebelum memakai materi rahasia, periksa ketersediaan model, [penggunaan dan biaya](https://code.claude.com/docs/en/costs), [kebijakan data](https://code.claude.com/docs/en/data-usage) dan [izin alat](https://code.claude.com/docs/en/permissions). Perintah Claude Code `/usage` dapat menampilkan penggunaan sesi saat ini; tagihan bergantung pada akun dan paket. Biaya atau penghematan alur ini tidak diukur. Sumber harus berupa artikel UTF-8 yang benar-benar disetujui atau transkrip terperiksa, dengan catatan pemilik, versi dan persetujuan. Selesaikan akurasi transkripsi, hak tiap kanal, privasi dan setiap penawaran dengan pemilik terlebih dahulu. URL video atau templat kosong berikut tidak memenuhi masukan ini.

Empat skills referensi menggunakan lokasi Claude Code yang didokumentasikan, `.claude/skills/<name>/SKILL.md`, pemanggilan perintah garis miring dan substitusi `$ARGUMENTS`. Pengaturan `context: fork` memberi setiap pemanggilan konteks sendiri tanpa riwayat percakapan. Skills draf memakai `background: true`, yang memerlukan Claude Code **v2.1.218 atau lebih baru**. Agen serbaguna digunakan; kemampuan menulis berkas dan izin efektifnya mengikuti konfigurasi Claude Code pengguna. Teks skill tidak memberi izin atau menegakkan batas hanya-baca. Pekerjaan latar dapat bertumpang tindih jika host mengizinkan, tetapi eksekusi paralel tidak dijamin. Periksa skills bernama sama dan penemuan perintah sebelum memakai nama ini, lalu pastikan perintah mengarah ke berkas proyek yang dipasang. Lihat [manual skills](https://code.claude.com/docs/en/skills) dan [manual subagents](https://code.claude.com/docs/en/sub-agents).

## 2. Pasang empat berkas dan kontraknya

Buat jalur persis dalam daftar berikut di proyek Anda. Salin setiap blok lengkap bagian berkas referensi tanpa mengubah isinya. Lima berkas di bawah `.claude/repurpose-contracts/` menetapkan bentuk masukan, inventaris, status terblokir dan hasil kanal, serta rencana dependensi. Letakkan `join.py` di akar proyek. Kedua templat ditempatkan di `repurpose-inputs/`; ganti penandanya dengan konten nyata yang disetujui sebelum dijalankan. Pohon keluaran hanya panduan struktur. Berkas lengkap untuk disalin tersedia di bawah.

| Berkas | Tujuan |
| --- | --- |
| `.claude/skills/source-inventory/SKILL.md` | Memeriksa persetujuan dan menginventarisasi setiap baris sumber sebelum menulis. |
| `.claude/skills/draft-linkedin/SKILL.md` | Hanya bertanggung jawab atas draf dan hasil LinkedIn. |
| `.claude/skills/draft-newsletter/SKILL.md` | Hanya bertanggung jawab atas draf dan hasil buletin. |
| `.claude/skills/draft-ad/SKILL.md` | Hanya bertanggung jawab atas draf dan hasil iklan atau alasan terblokirnya penawaran yang eksplisit. |
| `.claude/repurpose-contracts/*.json` | Menetapkan bidang persis, hubungan hash dan urutan tahap. |
| `join.py` | Memeriksa struktur dan asal-usul, lalu membuat peta klaim dan serah terima tinjauan. |
| `repurpose-inputs/approved-source.template.md`, `repurpose-inputs/constraints.template.json` | Formulir awal kosong, tidak pernah menjadi bukti eksekusi. |

Batasan mewajibkan hak adaptasi eksplisit untuk LinkedIn, buletin dan iklan, sekalipun iklan terblokir karena tidak ada penawaran disetujui. `forbiddenClaims` boleh berupa daftar kosong jika pemilik tidak menetapkan larangan tambahan. Iklan membutuhkan penawaran disetujui, tujuan HTTP(S) dan ajakan bertindak; satu nilai hilang akan memblokir cabang itu. Catat peninjau nyata yang disebutkan namanya. Jangan ubah sumber disetujui atau batasan selama eksekusi.

## 3. Uji setiap hubungan, lalu jalankan inventaris

Uji setiap skill secara terpisah pada sumber nyata, terbatas dan disetujui. Bandingkan berkas dan hash aktual dengan kontrak. BIG CHANGE belum melakukan pengujian Claude Code ini. Untuk menjalankan seluruh alur, buat direktori absolut baru untuk masukan, hasil eksekusi dan hasil penggabungan. Di repositori, panggil perintah inventaris dengan tiga jalur absolut:

```prompt
/source-inventory <absolute-path-to-approved-source.md> <absolute-path-to-constraints.json> <absolute-path-to-new-run-directory>
```

Ganti setiap argumen dalam kurung sudut dengan jalur nyata. Skill seharusnya menulis `inventory.json` yang memuat rentang bagian verbatim berurutan dan mencakup semua baris, atau `inventory-blocked.json` dengan persetujuan atau hak tertentu yang hilang. Jangan mulai tahap draf jika inventaris terblokir. Cocokkan SHA-256 tercatat dengan kedua berkas masukan dan periksa batas bagian termasuk baris kosong.

## 4. Tulis di tiga kanal terisolasi

Setelah `inventory.json` ada dan hash kedua masukan masih sesuai catatan, panggil tiga skills terpisah. Gunakan direktori eksekusi absolut yang sama untuk argumen pertama dan tiga direktori keluaran berbeda untuk argumen kedua. Setiap skill membaca sumber disetujui, batasan dan inventaris, bukan draf saudaranya. Mereka dapat dimulai secara mandiri setelah inventaris, sesuai host dan izin 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
```

Setiap kanal bertanggung jawab atas `result.json` dan, hanya jika siap, `draft.md`. Hasilnya mengidentifikasi kanal dan SHA-256 sumber, batasan dan inventaris, lalu menghubungkan klaim penting atau kutipan ke ID bagian dan kunci batasan yang berlaku. `ready` berarti siap untuk pemeriksaan fakta. Penawaran iklan, tujuan atau ajakan bertindak yang belum disetujui harus menghasilkan status terblokir tanpa draf iklan. Pemanggilan latar atau JSON valid saja tidak membuktikan draf berguna atau akurat.

## 5. Gabungkan hasil dan buat keputusan manusia

Setelah ketiga tahap kembali atau dipastikan gagal, jalankan berkas Python yang disediakan dengan direktori eksekusi dan direktori penggabungan **baru**. Penggabungan memakai pustaka standar Python; periksa pemasangan lokal sebelum memanggilnya.

```bash
python join.py <absolute-run-directory> <absolute-new-join-directory>
```

Penggabungan memeriksa hash sumber dan batasan, cakupan lengkap baris inventaris, hash inventaris tiap kanal, identitas tahap, penunjuk klaim, kepemilikan keluaran dan prasyarat iklan. Ia menulis `claim-map.md`, `review.md` dan `summary.json`. Tahap hilang atau tidak valid tampak terblokir sementara draf saudara yang valid tetap terlihat. Ketidakcocokan sumber global menghentikan penggabungan. Proses ini tidak memverifikasi kebenaran kalimat, kewajaran kutipan, landasan hukum klaim atau persetujuan publikasi dari pemilik. `review.md` dimulai dengan keputusan `PENDING`.

Peninjau yang ditunjuk perlu membuka setiap `draft.md` siap, membandingkan seluruh klaim penting dan kutipan dengan bagian rujukan dan batasan, memeriksa ambiguitas, suara kanal, hak adaptasi, tujuan dan penawaran iklan, lalu mencatat **terima, revisi atau tolak** untuk tiap kanal di luar bidang tertunda yang dibuat. Penerbitan, pengiriman, penjadwalan dan pemasangan iklan membutuhkan izin manusia tersendiri dan pemeriksaan platform.

## 6. Pulihkan cabang gagal tanpa kehilangan pekerjaan valid

Jika persetujuan atau hak belum jelas, selesaikan kekurangan spesifik bersama pemilik dan mulai eksekusi baru dengan masukan diperbaiki. Perubahan hash sumber atau batasan memerlukan inventaris lengkap baru. Jika satu kanal gagal dan lainnya valid, periksa dahulu berkas dan galat aktual kanal itu. Salin byte inventaris yang tidak berubah dan folder saudara terverifikasi ke direktori eksekusi baru, jalankan hanya tahap gagal, lalu gabungkan ke direktori baru lainnya. Hasil salinan tetap harus cocok dengan hash inventaris. Jika hasil proses belum pasti, baca hasil dan hash sebelum memutuskan mengulang. Pertahankan eksekusi sebelumnya dan suntingan peninjau; jangan timpa dengan pengulangan. Perbaiki penunjuk klaim hilang atau tidak didukung pada tahap pemiliknya, lalu ulangi penggabungan deterministik ke direktori baru.

Ini komposisi yang dipanggil manual dan diarahkan model. Tidak ada pembagian tugas yang ditegakkan kode, percobaan ulang otomatis, konkurensi tetap atau pemulihan efek samping eksternal. Mulailah dengan sumber tertulis yang disetujui: prosedur tidak memperoleh atau memverifikasi transkrip, maupun membuat keluaran video, audio atau visual kampanye.

## Berkas referensi lengkap

Simpan jalur dan kontennya bersama. Blok kode berikut adalah salinan lengkap paket yang ditinjau independen, sebagai berkas referensi, bukan catatan eksekusi 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
```

## Sumber dan bacaan lanjutan

- [Manual skills Claude Code](https://code.claude.com/docs/en/skills) — jalur skill proyek, frontmatter, pemanggilan, substitusi argumen, konteks terpisah dan perilaku latar. Diperiksa 19 Oktober 2026; tidak ada satu versi untuk seluruh resep ini.
- [Manual subagents Claude Code](https://code.claude.com/docs/en/sub-agents) — agen serbaguna, konteks dan izin. Diperiksa 19 Oktober 2026.
- [Biaya Claude Code](https://code.claude.com/docs/en/costs), [penggunaan data](https://code.claude.com/docs/en/data-usage) dan [izin](https://code.claude.com/docs/en/permissions) — penggunaan sesuai akun, penanganan data dan kendali alat. Diperiksa 19 Oktober 2026; pengeluaran alur tidak diukur.
- Paket lokal yang ditinjau independen menyediakan empat skills, lima kontrak JSON, dua templat masukan, pohon keluaran dan skrip penggabungan. Ke-16 pemeriksaan hanya memakai data uji struktural sementara; tidak menghasilkan prosa atau persetujuan faktual.
- [Panduan BIG CHANGE terkait: tinjauan dokumen paralel](https://bigchange.ai/blog/claude-managed-agents-parallel-document-review-guide) membahas alur Claude lain untuk peninjauan dokumen, bukan adaptasi sumber ke tiga kanal secara lokal di Code.

## Sources

- [Manual skills Claude Code](https://code.claude.com/docs/en/skills) — Jalur proyek, pemanggilan, konteks terpisah dan perilaku latar; HTTP 200 dan hash diperiksa 2026-10-19.
- [Manual subagents Claude Code](https://code.claude.com/docs/en/sub-agents) — Agen serbaguna dan izin; HTTP 200 dan hash diperiksa 2026-10-19.
- [Biaya Claude Code](https://code.claude.com/docs/en/costs) — Penggunaan menurut akun dan laporan sesi; diperiksa 2026-10-19.
- [Penggunaan data Claude Code](https://code.claude.com/docs/en/data-usage) — Kebijakan data akun dan organisasi; diperiksa 2026-10-19.
- [Izin Claude Code](https://code.claude.com/docs/en/permissions) — Akses alat aktual diatur di luar teks skill; diperiksa 2026-10-19.
