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

# Combine skills do Claude Code para três rascunhos verificados a partir de uma fonte aprovada

> Use quatro skills separados do Claude Code para inventariar uma fonte escrita aprovada, redigir para LinkedIn, newsletter e anúncio e conferir hashes e afirmações antes da revisão humana.

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.

Um artigo ou uma transcrição aprovados podem alimentar vários rascunhos, mas cada um precisa de um caminho claro até a fonte. Este guia combina quatro [skills de projeto do Claude Code](https://code.claude.com/docs/en/skills) separados para inventariar uma fonte escrita aprovada pelo proprietário, redigir uma publicação do LinkedIn, uma seção de newsletter e um anúncio em diretórios separados e conferir os contratos de arquivo com uma junção local em Python. A saída é um conjunto de rascunhos para um revisor identificado, não autorização para publicar ou enviar.

A BIG CHANGE examinou os skills, contratos e código de junção abaixo. Um revisor independente executou 16 verificações estruturais locais. Nós **não** instalamos nem executamos os skills no Claude Code, pagamos uma execução de modelo, geramos um rascunho factual para um canal ou testamos uma campanha real. Os passos são um procedimento para o leitor testar com sua própria fonte aprovada.

## A grande mudança

- **O que mudou:** Quatro skills próprios têm contratos explícitos para fonte, inventário e resultado por canal, seguidos de uma junção determinística.
- **Por que importa:** Cada rascunho pode ser rastreado até trechos da fonte, enquanto um canal ausente continua visível como bloqueado.
- **O que observar:** Verificações de arquivo não comprovam a verdade, direitos de adaptação ou aprovação editorial; uma pessoa precisa revisar cada rascunho pronto.

## 1. Confirme acesso, direitos e fonte

Use um repositório Claude Code sob seu controle e uma conta autorizada a processar a fonte. Antes de usar material confidencial, confira os modelos disponíveis, [uso e custos](https://code.claude.com/docs/en/costs), [política de dados](https://code.claude.com/docs/en/data-usage) e [permissões de ferramentas](https://code.claude.com/docs/en/permissions). O comando `/usage` do Claude Code pode mostrar o uso da sessão atual; a cobrança depende da conta e do plano. Nenhum custo ou economia foi medido neste fluxo. A fonte deve ser um artigo UTF-8 realmente aprovado ou uma transcrição conferida, com proprietário, versão e aprovação registrados. Resolva antes com o proprietário a precisão da transcrição, direitos por canal, privacidade e qualquer oferta. Uma URL de vídeo ou o modelo vazio abaixo não atende ao requisito.

Os quatro skills de referência usam o local documentado `.claude/skills/<name>/SKILL.md` do Claude Code, chamadas por comando com barra e substituição `$ARGUMENTS`. A configuração `context: fork` dá a cada chamada seu próprio contexto sem histórico de conversa. Os skills de rascunho usam `background: true`, que exige Claude Code **v2.1.218 ou posterior**. Eles usam o agente de uso geral, cujas capacidades de escrita e permissões efetivas seguem a configuração do usuário. O texto do skill não concede permissões nem impõe uma fronteira de somente leitura. Trabalhos em segundo plano podem se sobrepor se o host permitir; não há garantia de execução paralela. Examine skills existentes com o mesmo nome e a descoberta de comandos antes de adotar esses nomes, e confira se o comando aponta para o arquivo de projeto instalado. Consulte o [manual de skills](https://code.claude.com/docs/en/skills) e [manual de subagents](https://code.claude.com/docs/en/sub-agents).

## 2. Instale os quatro arquivos e os contratos

Crie no projeto os caminhos exatos da lista abaixo. Copie cada bloco completo dos arquivos de referência sem alterar o conteúdo. Os cinco arquivos em `.claude/repurpose-contracts/` definem os formatos de entrada, inventário, bloqueio e resultado por canal, além do plano de dependências. Coloque `join.py` na raiz do projeto. Os dois modelos ficam em `repurpose-inputs/`; substitua os campos provisórios por conteúdo real aprovado antes de executar. A árvore de saída orienta apenas a estrutura. Os arquivos completos estão abaixo para cópia.

| Arquivo | Finalidade |
| --- | --- |
| `.claude/skills/source-inventory/SKILL.md` | Confere aprovações e inventaria todas as linhas antes de redigir. |
| `.claude/skills/draft-linkedin/SKILL.md` | Gerencia apenas o rascunho e o resultado do LinkedIn. |
| `.claude/skills/draft-newsletter/SKILL.md` | Gerencia apenas o rascunho e o resultado da newsletter. |
| `.claude/skills/draft-ad/SKILL.md` | Gerencia apenas o rascunho e o resultado do anúncio, ou um bloqueio explícito da oferta. |
| `.claude/repurpose-contracts/*.json` | Define campos exatos, vínculos de hash e ordem das etapas. |
| `join.py` | Confere estrutura e procedência, depois cria um mapa de afirmações e um pacote de revisão. |
| `repurpose-inputs/approved-source.template.md`, `repurpose-inputs/constraints.template.json` | Formulários iniciais vazios, nunca evidências de execução. |

As restrições exigem direitos explícitos de adaptação para LinkedIn, newsletter e anúncio, mesmo quando o anúncio fica bloqueado por falta de oferta aprovada. `forbiddenClaims` pode ser uma lista vazia se o proprietário não tiver outras proibições. Um anúncio exige oferta aprovada, destino HTTP(S) e chamada para ação; qualquer ausência bloqueia esse ramo. Registre um revisor real e identificado. Não altere fonte e restrições durante a execução.

## 3. Teste cada dependência e execute o inventário

Teste primeiro cada skill isoladamente em uma fonte real, aprovada e limitada. Compare os arquivos reais e hashes com os contratos. A BIG CHANGE não fez esse teste no Claude Code. Para executar o fluxo inteiro, crie diretórios absolutos novos para entradas, resultados e junção. No repositório, chame o comando de inventário com três caminhos absolutos:

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

Substitua cada argumento entre sinais de menor e maior por um caminho real. O skill deve escrever `inventory.json`, com intervalos literais e ordenados de trechos cobrindo todas as linhas, ou `inventory-blocked.json` com a aprovação ou o direito exato que falta. Não comece um rascunho se o inventário estiver bloqueado. Compare os SHA-256 registrados aos dois arquivos de entrada e examine os limites dos trechos, incluindo linhas em branco.

## 4. Redija em três canais isolados

Quando `inventory.json` existir e os dois inputs ainda tiverem os hashes registrados, chame os três skills separados. Use o mesmo diretório absoluto de execução como primeiro argumento e três diretórios de saída distintos como segundo. Cada skill lê a fonte aprovada, restrições e inventário; nenhum lê um rascunho vizinho. Eles podem começar de forma independente após o inventário, sujeitos ao host e às permissões do 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
```

Cada canal gerencia `result.json` e, somente quando pronto, `draft.md`. Seu resultado identifica canal e SHA-256 de fonte, restrições e inventário, depois vincula afirmações relevantes ou citações aos IDs de trechos e às chaves de restrição aplicáveis. `ready` significa pronto para revisão factual. A ausência de oferta aprovada, destino ou chamada para ação exige resultado bloqueado sem rascunho de anúncio. Uma chamada em segundo plano ou JSON válido não prova, por si só, que o texto é útil ou correto.

## 5. Una os resultados e tome uma decisão humana

Quando as três etapas tiverem terminado ou falhado definitivamente, execute o arquivo Python fornecido com o diretório de execução e um **novo** diretório de junção. O código usa a biblioteca padrão Python; confira sua instalação local antes de chamá-lo.

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

A junção confere hashes de fonte e restrições, cobertura completa de linhas pelo inventário, seu hash em cada canal, identidade da etapa, referências de afirmações, responsabilidade pelas saídas e requisitos publicitários. Ela escreve `claim-map.md`, `review.md` e `summary.json`. Etapas ausentes ou inválidas aparecem bloqueadas, preservando a visibilidade dos rascunhos vizinhos válidos. Uma divergência global da fonte interrompe a junção. Ela não verifica se uma frase é verdadeira, uma citação é justa, uma afirmação tem suporte jurídico ou o proprietário aprovou a publicação. `review.md` começa com decisões `PENDING`.

O revisor identificado deve abrir cada `draft.md` pronto, comparar toda afirmação relevante e citação ao trecho e às restrições indicados, verificar ambiguidades, voz do canal, direitos de adaptação, destino e oferta do anúncio e registrar **aceitar, revisar ou rejeitar** para cada canal fora dos campos pendentes gerados. Publicar, enviar, agendar e veicular anúncios exige autorização humana própria e verificações da plataforma.

## 6. Recupere um ramo com falha sem perder trabalho válido

Se a aprovação ou os direitos forem incertos, resolva a lacuna com o proprietário e inicie nova execução com as entradas corrigidas. Qualquer mudança do hash da fonte ou restrições exige inventário completo novo. Se um canal falhar e os demais forem válidos, examine primeiro seus arquivos e erro reais. Copie os bytes intactos do inventário e as pastas vizinhas verificadas para um novo diretório, execute apenas a etapa que falhou e faça a junção em outro diretório novo. Os resultados copiados ainda devem corresponder ao hash do inventário. Se o resultado de um processo for incerto, leia resultado e hashes antes de decidir repetir. Preserve execuções anteriores e edições do revisor; nunca as sobrescreva. Corrija referências ausentes ou não suportadas na etapa responsável e repita a junção determinística em um diretório novo.

Esta composição é acionada manualmente e guiada pelo modelo. Não oferece despacho imposto por código, novas tentativas automáticas, concorrência fixa ou recuperação de efeitos externos. Comece com uma fonte escrita já aprovada: o procedimento não obtém nem verifica transcrições e não cria vídeos, áudio ou recursos visuais de campanha.

## Arquivos de referência completos

Mantenha caminhos e conteúdos juntos. Os blocos de código são cópias completas do pacote revisado independentemente; são referências, não registros de execução do 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
```

## Fontes e leitura adicional

- [Manual de skills do Claude Code](https://code.claude.com/docs/en/skills) — caminhos de projeto, frontmatter, chamada, substituição de argumentos, contexto separado e segundo plano. Conferido em 19 de outubro de 2026; não há versão única indicada para a receita.
- [Manual de subagents do Claude Code](https://code.claude.com/docs/en/sub-agents) — agente de uso geral, contexto e permissões. Conferido em 19 de outubro de 2026.
- [Custos do Claude Code](https://code.claude.com/docs/en/costs), [uso de dados](https://code.claude.com/docs/en/data-usage) e [permissões](https://code.claude.com/docs/en/permissions) — uso conforme a conta, tratamento de dados e controles de ferramentas. Conferido em 19 de outubro de 2026; não foram medidos gastos do fluxo.
- O pacote local revisado independentemente fornece quatro skills, cinco contratos JSON, dois modelos de entrada, árvore de saída e script de junção. As 16 verificações usaram apenas dados estruturais temporários; não produziram prosa gerada nem aprovação factual.
- [Guia relacionado da BIG CHANGE: revisão paralela de documentos](https://bigchange.ai/blog/claude-managed-agents-parallel-document-review-guide) aborda outro fluxo do Claude, baseado em revisão documental, não adaptação local de fonte para três canais no Code.

## Sources

- [Manual de skills do Claude Code](https://code.claude.com/docs/en/skills) — Caminho de projeto, chamada, contexto separado e segundo plano; HTTP 200 e hash conferidos em 2026-10-19.
- [Manual de subagents do Claude Code](https://code.claude.com/docs/en/sub-agents) — Agente de uso geral e permissões; HTTP 200 e hash conferidos em 2026-10-19.
- [Custos do Claude Code](https://code.claude.com/docs/en/costs) — Uso por conta e relatório da sessão; conferidos em 2026-10-19.
- [Uso de dados do Claude Code](https://code.claude.com/docs/en/data-usage) — Política de dados da conta e organização; conferida em 2026-10-19.
- [Permissões do Claude Code](https://code.claude.com/docs/en/permissions) — O acesso real a ferramentas é configurado fora do texto do skill; conferido em 2026-10-19.
