Maaaring pagmulan ng ilang draft ang aprubadong artikulo o transcript, ngunit kailangan ng bawat draft ng malinaw na bakas pabalik sa pinagmulan. Pinagsasama ng gabay ang apat na magkahiwalay na project skills ng Claude Code upang imbentaryuhin ang tekstong aprubado ng may-ari, gumawa ng LinkedIn post, bahagi ng newsletter at ad sa magkakahiwalay na direktoryo, at suriin ang mga kontrata ng file sa lokal na Python join. Ang resulta ay mga draft para sa isang pinangalanang taong tagasuri, hindi pahintulot na maglathala o magpadala.

Sinuri ng BIG CHANGE ang skills, mga kontrata at join code sa ibaba. Isang hiwalay na tagasuri ang nagsagawa ng 16 lokal na structural check. Hindi namin in-install o pinatakbo ang skills sa Claude Code, binayaran ang isang model run, gumawa ng totoong channel draft o sumubok ng tunay na kampanya. Ang mga hakbang ay para subukan ng mambabasa gamit ang sariling aprubadong sanggunian.

Ang malaking pagbabago

  • Ano ang nagbago: May malinaw na kontrata na ang apat na sariling skills para sa sanggunian, imbentaryo at resulta ng channel, bago ang deterministikong join.
  • Bakit mahalaga: Matutunton ang bawat draft sa mga sipi ng pinagmulan, habang makikitang naka-block ang nawawalang channel.
  • Dapat bantayan: Hindi pinatutunayan ng file check ang katotohanan, karapatang umangkop o pag-apruba ng editor; kailangang suriin ng tao ang bawat handang draft.

1. Tiyakin ang access, karapatan at sanggunian

Gumamit ng sariling kontroladong Claude Code repository at account na pinapayagang magproseso ng sanggunian. Bago gumamit ng kumpidensiyal na materyal, suriin ang mga modelong magagamit, paggamit at gastos, patakaran sa data at pahintulot ng mga tool. Maaaring ipakita ng command na /usage ng Claude Code ang paggamit sa kasalukuyang session; nakadepende ang singil sa account at plano. Walang sinukat na gastos o natipid para sa prosesong ito. Dapat tunay na aprubadong UTF-8 na artikulo o nasuring transcript ang input, na may tala ng may-ari, bersiyon at pag-apruba. Linawin muna sa may-ari ang katumpakan ng transcript, karapatan para sa bawat channel, privacy at anumang alok. Hindi sapat na input ang video URL o ang blangkong template sa ibaba.

Ginagamit ng apat na reference skills ang dokumentadong lokasyong .claude/skills/<name>/SKILL.md ng Claude Code, pagtawag gamit ang slash command at pagpapalit ng $ARGUMENTS. Ang setting nilang context: fork ay nagbibigay sa bawat tawag ng sariling context na walang kasaysayan ng usapan. Gumagamit ang draft skills ng background: true, na nangangailangan ng Claude Code v2.1.218 o mas bago. Ginagamit nila ang general-purpose agent; sumusunod ang kakayahang magsulat ng file at aktuwal na pahintulot sa setup ng gumagamit. Hindi nagbibigay ng permiso ang teksto ng skill at hindi nito maipatutupad ang read-only na hangganan. Maaaring magsabay ang background work kung pinapayagan ng host; hindi garantisado ang parallel execution. Suriin muna ang mga kasalukuyang skill na kapangalan at ang pagtuklas ng command, at tiyaking tumutukoy ito sa project file na in-install. Tingnan ang manwal ng skills at manwal ng subagents.

2. Ilagay ang apat na file at mga kontrata

Gawin sa proyekto ang eksaktong mga path sa listahan. Kopyahin nang buo ang bawat block sa reference files nang hindi binabago ang laman. Tinutukoy ng limang file sa .claude/repurpose-contracts/ ang mga anyo ng input, imbentaryo, blocked state at channel result, pati ang dependency plan. Ilagay ang join.py sa project root. Ang dalawang template ay nasa repurpose-inputs/; palitan ang mga placeholder ng tunay na aprubadong nilalaman bago tumakbo. Gabay sa istruktura lang ang output tree. Nasa ibaba ang kumpletong mga file na makokopya.

File

Gamit

.claude/skills/source-inventory/SKILL.md

Sinusuri ang pag-apruba at iniimbentaryo ang lahat ng linya bago mag-draft.

.claude/skills/draft-linkedin/SKILL.md

LinkedIn draft at resulta lamang ang saklaw.

.claude/skills/draft-newsletter/SKILL.md

Newsletter draft at resulta lamang ang saklaw.

.claude/skills/draft-ad/SKILL.md

Ad draft at resulta lamang, o malinaw na dahilan ng pagkaka-block ng alok.

.claude/repurpose-contracts/*.json

Tinutukoy ang eksaktong field, mga ugnayan ng hash at pagkakasunod-sunod.

join.py

Sinusuri ang istruktura at pinagmulan, saka gumagawa ng mapa ng pahayag at handoff para sa pagsusuri.

repurpose-inputs/approved-source.template.md, repurpose-inputs/constraints.template.json

Blangkong panimulang form, hindi kailanman ebidensiya ng pagtakbo.

Kailangan ng tahasang karapatang umangkop para sa LinkedIn, newsletter at ad, kahit ma-block ang ad dahil walang aprubadong alok. forbiddenClaims ay maaaring blangkong listahan kung walang iba pang pagbabawal ang may-ari. Kailangan ng ad ang aprubadong alok, HTTP(S) na destinasyon at panawagan sa pagkilos; ang anumang kulang ay magba-block sa sangay. Itala ang totoong pinangalanang tagasuri. Huwag baguhin ang aprubadong sanggunian at constraints habang tumatakbo.

3. Subukan ang bawat ugnayan bago ang imbentaryo

Subukan muna ang bawat skill nang hiwalay sa tunay, maliit at aprubadong sanggunian. Ihambing ang aktuwal na mga file at hash sa kontrata. Hindi ginawa ng BIG CHANGE ang test na ito sa Claude Code. Para sa buong run, gumawa ng bagong absolute directory para sa inputs, run outputs at join outputs. Sa repository, tawagin ang inventory command gamit ang tatlong absolute path:

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

Palitan ang lahat ng argumentong nasa angle bracket ng tunay na path. Dapat isulat ng skill ang inventory.json na may literal at sunod-sunod na saklaw ng sipi para sa bawat linya, o inventory-blocked.json na may eksaktong kulang na pag-apruba o karapatan. Huwag magsimula ng draft kapag blocked ang inventory. Ihambing ang nakatalang SHA-256 sa parehong input file at siyasatin ang mga hangganan ng sipi, kasama ang blangkong linya.

4. Gumawa ng draft sa tatlong hiwalay na channel

Kapag mayroon nang inventory.json at tugma pa ang hash ng dalawang input, tawagin ang tatlong magkahiwalay na skills. Parehong absolute run directory ang unang argumento, at tatlong magkaibang output directory ang ikalawa. Binabasa ng bawat skill ang aprubadong sanggunian, constraints at inventory, hindi ang draft ng kapatid na channel. Maaari silang simulan nang hiwalay pagkatapos ng inventory, ayon sa host at pahintulot ng 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

Saklaw ng bawat channel ang result.json at, kapag handa lamang, draft.md. Tinutukoy ng resulta ang channel at SHA-256 ng sanggunian, constraints at inventory, saka iniuugnay ang mahahalagang pahayag o sipi sa passage ID at naaangkop na constraint key. ready ay nangangahulugang handa sa fact review. Kapag kulang ang aprubadong alok, destinasyon o panawagan sa pagkilos, dapat blocked ang resulta at walang ad draft. Ang background invocation o valid JSON lamang ay hindi patunay na kapaki-pakinabang o tama ang draft.

5. Pagsamahin ang resulta at magpasya bilang tao

Pagbalik o tiyak na pagkabigo ng lahat ng tatlong yugto, patakbuhin ang ibinigay na Python file gamit ang run directory at isang bagong join directory. Python standard library ang gamit; suriin muna ang lokal na Python installation.

Terminal
python join.py <absolute-run-directory> <absolute-new-join-directory>

Sinusuri ng join ang source at constraints hash, kumpletong saklaw ng mga linya, inventory hash sa bawat channel, stage identity, claim pointer, pagmamay-ari ng output at mga kailangan ng ad. Isinusulat nito ang claim-map.md, review.md at summary.json. Naka-block ang kulang o invalid na yugto, habang nananatiling nakikita ang wastong draft ng ibang channel. Tumitigil ang join kapag hindi tugma ang global source. Hindi nito pinapatunayan ang katotohanan ng pangungusap, patas na sipi, legal na batayan o pahintulot sa publikasyon. review.md ay nagsisimula sa mga pasyang PENDING.

Dapat buksan ng pinangalanang tagasuri ang bawat handang draft.md, ihambing ang mahahalagang pahayag at sipi sa tinutukoy na bahagi at constraints, suriin ang kalabuan, boses ng channel, karapatang umangkop, destinasyon at alok ng ad, saka itala ang tanggapin, baguhin o tanggihan para sa bawat channel sa labas ng ginawang pending field. Ang publikasyon, pagpapadala, pag-iskedyul at paglalagay ng ad ay nangangailangan ng hiwalay na pahintulot ng tao at pagsusuri ng platform.

6. Ayusin ang bigong sangay nang hindi nawawala ang magandang gawa

Kung hindi malinaw ang pag-apruba o karapatan, lutasin ang tiyak na kakulangan sa may-ari at magsimula ng bagong run gamit ang naitamang input. Anumang pagbabago ng source o constraints hash ay kailangan ng bagong buong inventory. Kung isang channel lang ang bumigo, siyasatin muna ang tunay nitong file at error. Kopyahin ang hindi nabagong inventory bytes at napatunayang sibling folder sa bagong run directory, patakbuhin doon ang bigong yugto lamang, at mag-join sa isa pang bagong direktoryo. Dapat tumugma pa rin ang kinopyang resulta sa inventory hash. Kung hindi tiyak ang proseso, basahin ang resulta at hash bago magpasiyang umulit. Panatilihin ang mga lumang run at edit ng tagasuri; huwag patungan ng rerun. Ayusin ang kulang o hindi suportadong claim pointer sa may-aring yugto, saka ulitin ang deterministic join sa bagong direktoryo.

Ito ay manu-manong tinatawag at pinapatakbo ng modelo na komposisyon. Wala itong code-enforced dispatch, awtomatikong retry, nakatakdang concurrency o pagbawi ng panlabas na epekto. Magsimula sa aprubadong nakasulat na sanggunian; hindi nito kinukuha o pinatutunayan ang transcript, o ginagawa ang video, audio at visual ng kampanya.

Kumpletong reference files

Panatilihing magkasama ang mga path at nilalaman. Kumpletong kopya ng independently reviewed packet ang mga code block sa ibaba; reference file ang mga ito, hindi tala ng Claude run.

.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

Mga sanggunian at karagdagang babasahin

  • Manwal ng Claude Code skills — project skill path, frontmatter, pagtawag, pagpapalit ng argumento, hiwalay na context at background behavior. Sinuri noong 19 Oktubre 2026; walang iisang recipe version ang manwal.
  • Manwal ng Claude Code subagents — general-purpose agent, context at pahintulot. Sinuri noong 19 Oktubre 2026.
  • Gastos ng Claude Code, paggamit ng data at mga pahintulot — paggamit ayon sa account, paghawak ng data at tool controls. Sinuri noong 19 Oktubre 2026; walang sinukat na gastos sa pipeline.
  • Ang independently reviewed na lokal na packet ang pinagmulan ng apat na skills, limang JSON contract, dalawang input template, output tree at join script. Temporaryong structural fixture lamang ang ginamit sa 16 check; walang nabuong prosa o fact approval.
  • Kaugnay na gabay ng BIG CHANGE: sabayang pagsusuri ng dokumento ay ibang Claude workflow para sa document review, hindi lokal na pag-angkop ng sanggunian sa tatlong channel sa Code.