Un article approuvé ou une transcription peut alimenter plusieurs brouillons, mais chacun doit permettre de remonter clairement à sa source. Ce guide assemble quatre skills de projet Claude Code distincts pour inventorier une source écrite approuvée par son propriétaire, rédiger un post LinkedIn, une section de newsletter et une publicité dans des répertoires séparés, puis vérifier leurs contrats de fichiers avec un script local de rapprochement en Python. Le résultat est un ensemble de brouillons destinés à une personne nommée, sans autorisation de les publier ou de les envoyer.

BIG CHANGE a examiné les skills, les contrats et le code de rapprochement ci-dessous. Une personne indépendante a exécuté 16 vérifications structurelles locales. Nous n’avons pas installé ni exécuté ces skills dans Claude Code, payé une exécution de modèle, produit un brouillon factuel pour un canal ou testé une véritable campagne. Les étapes suivantes sont une procédure à tester par le lecteur avec sa propre source approuvée.

Le grand changement

  • Ce qui change : Quatre skills contrôlés ont désormais des contrats explicites pour la source, l’inventaire et les résultats de chaque canal, suivis d’un rapprochement déterministe.
  • Pourquoi c’est important : Chaque brouillon peut être relié aux passages sources, tandis qu’un canal manquant reste visible comme emplacement bloqué.
  • À surveiller : Les contrôles de fichiers ne prouvent ni la véracité, ni les droits d’adaptation, ni l’approbation éditoriale ; une personne doit vérifier chaque brouillon prêt.

1. Confirmer l’accès, les droits et la source

Utilisez un dépôt Claude Code que vous contrôlez et un compte autorisé à traiter la source. Vérifiez les modèles accessibles au compte, l’utilisation et les coûts, la politique de données et les autorisations des outils avant d’utiliser des informations confidentielles. La commande /usage de Claude Code peut afficher la consommation de la session ; la facturation dépend du compte et de l’offre. Aucun coût ni aucune économie n’a été mesuré pour ce circuit. La source doit être un véritable article UTF-8 approuvé ou une transcription vérifiée, avec son propriétaire, sa version et son approbation consignés. Réglez d’abord avec le propriétaire l’exactitude de la transcription, les droits pour chaque canal, la confidentialité et toute offre commerciale. Une URL vidéo ou le modèle vide ci-dessous ne répond pas à cette exigence.

Les quatre skills de référence utilisent l’emplacement documenté .claude/skills/<name>/SKILL.md de Claude Code, l’appel par commande slash et la substitution $ARGUMENTS. Leur paramètre context: fork donne à chaque appel son propre contexte, sans l’historique de conversation. Les skills de rédaction utilisent background: true, qui nécessite Claude Code v2.1.218 ou ultérieure. Ils utilisent l’agent généraliste, dont les capacités d’écriture de fichiers et les autorisations effectives suivent la configuration Claude Code de l’utilisateur. Le texte du skill n’accorde aucune permission et ne peut imposer une limite de lecture seule. Les tâches en arrière-plan peuvent se chevaucher si l’hôte le permet ; cette procédure ne garantit pas une exécution parallèle. Examinez les skills portant déjà le même nom et la découverte des commandes avant d’adopter ces noms, puis vérifiez que la commande pointe vers le fichier de projet installé. Consultez le manuel des skills et manuel des subagents.

2. Installer les quatre fichiers et les contrats

Créez dans votre projet les chemins exacts de la liste ci-dessous. Copiez chaque bloc complet de la section des fichiers de référence sans en modifier le contenu. Les cinq fichiers sous .claude/repurpose-contracts/ définissent les formats d’entrée, d’inventaire, d’état bloqué et de résultat par canal, ainsi que le plan des dépendances. Placez join.py à la racine du projet. Les deux modèles vont sous repurpose-inputs/ ; remplacez leurs champs provisoires par un véritable contenu approuvé avant toute exécution. L’arborescence de sortie n’est qu’un guide structurel. Les fichiers complets sont fournis ci-dessous pour être copiés.

Fichier

Rôle

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

Vérifie les approbations et inventorie chaque ligne source avant la rédaction.

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

Gère uniquement le brouillon et le résultat LinkedIn.

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

Gère uniquement le brouillon et le résultat de la newsletter.

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

Gère uniquement le brouillon et le résultat publicitaires, ou un blocage explicite lié à l’offre.

.claude/repurpose-contracts/*.json

Définit les champs exacts, les liens entre empreintes et l’ordre des étapes.

join.py

Vérifie la structure et la provenance, puis crée une carte des affirmations et un dossier de relecture.

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

Formulaires de départ vides, jamais preuves d’une exécution.

Les contraintes exigent des droits d’adaptation explicites pour LinkedIn, la newsletter et la publicité, même si la branche publicitaire sera bloquée faute d’offre approuvée. forbiddenClaims peut être une liste vide si le propriétaire n’a pas d’autres interdictions. Une publicité nécessite une offre approuvée, une destination HTTP(S) et un appel à l’action ; toute valeur absente bloque cette branche. Nommez une véritable personne chargée de la relecture. Ne modifiez pas la source approuvée ni les contraintes pendant une exécution.

3. Tester chaque liaison, puis lancer l’inventaire

Testez d’abord chaque skill séparément sur une véritable source approuvée de taille limitée. Comparez ses fichiers réels et leurs empreintes aux contrats. BIG CHANGE n’a pas réalisé ce test dans Claude Code. Pour une exécution complète, créez de nouveaux répertoires absolus pour les entrées, les résultats et le rapprochement. Dans le dépôt, appelez la commande d’inventaire avec trois chemins absolus :

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

Remplacez chaque argument entre chevrons par un chemin réel. Le skill devrait écrire inventory.json, contenant des plages de passages littérales et ordonnées couvrant toutes les lignes, ou inventory-blocked.json avec l’approbation ou le droit manquant exact. Ne lancez aucune rédaction si l’inventaire est bloqué. Vérifiez les valeurs SHA-256 enregistrées contre les deux fichiers d’entrée et examinez les limites des passages, y compris les lignes vides.

4. Rédiger dans trois canaux isolés

Une fois que inventory.json existe et que les deux entrées conservent les empreintes enregistrées, appelez les trois skills séparés. Utilisez le même répertoire d’exécution absolu comme premier argument et trois répertoires de sortie distincts comme second. Chaque skill lit la source approuvée, les contraintes et l’inventaire ; aucun ne lit le brouillon voisin. Ils peuvent être lancés indépendamment après l’inventaire, sous réserve de l’hôte Claude Code et des autorisations.

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

Chaque canal gère result.json et, seulement s’il est prêt, draft.md. Son résultat indique le canal et les SHA-256 de la source, des contraintes et de l’inventaire, puis relie les affirmations importantes ou citations aux identifiants de passages et aux clés de contraintes pertinentes. ready signifie prêt pour la vérification factuelle. L’absence d’offre publicitaire approuvée, de destination ou d’appel à l’action doit produire un résultat bloqué sans brouillon publicitaire. Un appel en arrière-plan ou un fichier JSON valide ne prouve pas à lui seul qu’un brouillon est utile ou exact.

5. Rapprocher les résultats et prendre une décision humaine

Après le retour ou l’échec définitif des trois étapes, lancez le fichier Python fourni avec le répertoire d’exécution et un nouveau répertoire de rapprochement. Ce script utilise la bibliothèque standard Python ; vérifiez votre installation locale avant de l’appeler.

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

Le rapprochement vérifie les empreintes de la source et des contraintes, la couverture de toutes les lignes par l’inventaire, son empreinte dans chaque canal, l’identité de l’étape, les références des affirmations, la propriété des sorties et les prérequis publicitaires. Il écrit claim-map.md, review.md et summary.json. Les étapes absentes ou invalides sont marquées bloquées, tandis que les brouillons voisins valides restent visibles. Un désaccord global sur la source arrête le rapprochement. Il ne vérifie ni la vérité d’une phrase, ni la fidélité d’une citation, ni le fondement juridique d’une affirmation, ni l’autorisation de publication du propriétaire. review.md commence avec des décisions PENDING.

La personne nommée doit ouvrir chaque draft.md prêt, comparer toute affirmation importante et citation au passage indiqué et aux contraintes, vérifier les ambiguïtés, le ton du canal, les droits d’adaptation, la destination et l’offre publicitaires, puis consigner accepter, réviser ou rejeter pour chaque canal en dehors des champs d’attente générés. Publier, envoyer, programmer ou diffuser des publicités exige une autorisation humaine distincte et les contrôles de la plateforme.

6. Réparer une branche sans perdre le travail valide

Si l’approbation ou les droits de la source sont incertains, réglez précisément le manque avec le propriétaire et commencez une nouvelle exécution avec les entrées corrigées. Toute modification d’empreinte de la source ou des contraintes impose un nouvel inventaire complet. Si un canal échoue alors que les autres sont valides, examinez d’abord ses fichiers et son erreur réels. Copiez les octets inchangés de l’inventaire et les dossiers voisins vérifiés dans un nouveau répertoire d’exécution, relancez seulement l’étape défaillante, puis rapprochez dans un autre nouveau répertoire. Les résultats copiés doivent toujours correspondre à l’empreinte de l’inventaire. Si l’issue d’un processus est incertaine, lisez son résultat et ses empreintes avant de décider de réessayer. Conservez les anciennes exécutions et corrections humaines ; ne les écrasez jamais par une relance. Réparez une référence d’affirmation absente ou non prise en charge dans l’étape responsable, puis répétez le rapprochement déterministe dans un répertoire neuf.

Il s’agit d’une composition dirigée par un modèle et lancée manuellement. Elle n’offre ni orchestration imposée par le code, ni relances automatiques, ni concurrence fixe, ni récupération des effets externes. Partez d’une source écrite déjà approuvée : cette procédure n’acquiert ni ne vérifie une transcription et ne crée pas les éléments vidéo, audio ou visuels d’une campagne.

Fichiers de référence complets

Conservez ensemble les chemins et les contenus. Les blocs de code ci-dessous sont des copies complètes du dossier examiné indépendamment ; ce sont des fichiers de référence, pas des traces d’une exécution de 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

Sources et lectures complémentaires

  • Manuel des skills Claude Code — chemins des skills de projet, frontmatter, appel, substitution des arguments, contexte séparé et comportement en arrière-plan. Vérifié le 19 octobre 2026 ; le manuel n’indique pas de version unique pour cette recette.
  • Manuel des subagents Claude Code — agent généraliste, contexte et autorisations. Vérifié le 19 octobre 2026.
  • Coûts de Claude Code, utilisation des données et autorisations — consommation selon le compte, traitement des données et contrôles des outils. Vérifié le 19 octobre 2026 ; aucune dépense de ce circuit n’a été mesurée.
  • Le dossier local examiné indépendamment fournit les quatre skills, cinq contrats JSON, deux modèles d’entrée, l’arborescence de sortie et le script de rapprochement. Ses 16 contrôles n’utilisaient que des jeux de données structurels temporaires ; ils n’ont produit ni prose générée ni approbation factuelle.
  • Guide BIG CHANGE associé : relecture parallèle de documents présente un autre usage de Claude fondé sur la relecture de documents, plutôt que l’adaptation locale d’une source en trois canaux dans Code.