Un artículo o una transcripción aprobados pueden alimentar varios borradores, pero cada uno debe permitir volver claramente a su fuente. Esta guía combina cuatro skills de proyecto de Claude Code independientes para inventariar una fuente escrita aprobada por su propietario, redactar una publicación de LinkedIn, una sección de boletín y un anuncio en directorios separados, y comprobar los contratos de archivo mediante una unión local con Python. El resultado son borradores para una persona revisora identificada, no permiso para publicarlos o enviarlos.

BIG CHANGE revisó los skills, contratos y código de unión siguientes. Una persona independiente ejecutó 16 comprobaciones estructurales locales. No instalamos ni ejecutamos los skills en Claude Code, pagamos una ejecución del modelo, generamos un borrador factual para un canal ni probamos una campaña real. Los pasos son un procedimiento que el lector debe probar con su propia fuente aprobada.

El gran cambio

  • Qué cambia: Cuatro skills propios tienen contratos explícitos para la fuente, el inventario y los resultados de cada canal, seguidos de una unión determinista.
  • Por qué importa: Cada borrador puede vincularse a pasajes de la fuente y cada canal ausente sigue visible como bloqueado.
  • Qué vigilar: Las comprobaciones de archivos no demuestran veracidad, derechos de adaptación ni aprobación editorial; una persona debe revisar cada borrador listo.

1. Confirma acceso, derechos y fuente

Usa un repositorio de Claude Code bajo tu control y una cuenta autorizada para procesar la fuente. Antes de usar material confidencial, comprueba los modelos disponibles, el uso y los costes, la política de datos y los permisos de herramientas. El comando /usage de Claude Code puede mostrar el consumo de la sesión; la facturación depende de la cuenta y el plan. No se midieron costes ni ahorros en este flujo. La fuente debe ser un artículo UTF-8 realmente aprobado o una transcripción comprobada, con propietario, versión y aprobación registrados. Resuelve antes con el propietario la exactitud de la transcripción, los derechos para cada canal, la privacidad y cualquier oferta. Una URL de vídeo o la plantilla vacía de abajo no satisfacen este requisito.

Los cuatro skills de referencia usan la ubicación documentada .claude/skills/<name>/SKILL.md de Claude Code, la invocación mediante comandos con barra y la sustitución $ARGUMENTS. Su ajuste context: fork da a cada invocación un contexto propio sin el historial de conversación. Los skills de redacción usan background: true, que requiere Claude Code v2.1.218 o posterior. Usan el agente de propósito general; su capacidad de escribir archivos y sus permisos efectivos dependen de la configuración del usuario. El texto del skill no concede permisos ni impone una frontera de solo lectura. Las tareas en segundo plano pueden solaparse si el host lo permite; no se garantiza ejecución paralela. Inspecciona los skills existentes con el mismo nombre y el descubrimiento de comandos antes de usar estos nombres, y verifica que el comando apunta al archivo del proyecto instalado. Consulta el manual de skills y manual de subagents.

2. Instala los cuatro archivos y los contratos

Crea en tu proyecto las rutas exactas de la lista inferior. Copia íntegro cada bloque de los archivos de referencia sin modificarlo. Los cinco archivos de .claude/repurpose-contracts/ definen los formatos de entrada, inventario, bloqueo y resultado por canal, además del plan de dependencias. Pon join.py en la raíz del proyecto. Las dos plantillas van en repurpose-inputs/; sustituye sus marcadores por contenido real aprobado antes de ejecutar nada. El árbol de salida solo orienta sobre la estructura. Abajo están los archivos completos para copiar.

Archivo

Función

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

Comprueba aprobaciones e inventaría cada línea fuente antes de redactar.

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

Gestiona únicamente el borrador y resultado de LinkedIn.

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

Gestiona únicamente el borrador y resultado del boletín.

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

Gestiona únicamente el borrador y resultado del anuncio, o un bloqueo explícito de la oferta.

.claude/repurpose-contracts/*.json

Define campos exactos, relaciones entre hashes y orden de etapas.

join.py

Comprueba estructura y procedencia, y crea un mapa de afirmaciones y una entrega para revisión.

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

Formularios iniciales vacíos; nunca evidencia de una ejecución.

Las restricciones exigen derechos de adaptación explícitos para LinkedIn, boletín y anuncio, incluso si la rama del anuncio se bloquea por no tener oferta aprobada. forbiddenClaims puede ser una lista vacía si el propietario no impone otras prohibiciones. Un anuncio necesita una oferta aprobada, destino HTTP(S) y llamada a la acción; cualquier ausencia bloquea esa rama. Registra una persona revisora real y con nombre. No cambies la fuente aprobada ni las restricciones durante la ejecución.

3. Prueba cada dependencia y ejecuta el inventario

Prueba primero cada skill por separado con una fuente real, aprobada y acotada. Compara los archivos reales y sus hashes con los contratos. BIG CHANGE no ha hecho esta prueba en Claude Code. Para ejecutar todo, crea directorios absolutos nuevos para entradas, resultados y unión. En el repositorio, invoca el comando de inventario con tres rutas absolutas:

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

Sustituye cada argumento entre corchetes angulares por una ruta real. El skill debería escribir inventory.json, con rangos de pasajes literales y ordenados que cubran todas las líneas, o inventory-blocked.json con la aprobación o el derecho concreto que falta. No inicies la redacción si el inventario está bloqueado. Contrasta los SHA-256 registrados con ambos archivos de entrada y revisa los límites de los pasajes, incluidas las líneas vacías.

4. Redacta en tres canales aislados

Cuando exista inventory.json y las dos entradas mantengan los hashes registrados, invoca los tres skills independientes. Usa el mismo directorio absoluto de ejecución como primer argumento y tres directorios de salida distintos como segundo. Cada skill lee la fuente aprobada, las restricciones y el inventario; ninguno lee un borrador hermano. Pueden iniciarse de forma independiente tras el inventario, sujetos al host y los permisos de 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 gestiona result.json y, solo cuando está listo, draft.md. Su resultado identifica el canal y los SHA-256 de fuente, restricciones e inventario, y vincula afirmaciones importantes o citas a los ID de pasajes y claves de restricciones pertinentes. ready significa listo para revisión factual. Si falta la oferta aprobada, el destino o la llamada a la acción del anuncio, debe producirse un resultado bloqueado sin borrador publicitario. Una invocación en segundo plano o un JSON válido no demuestran por sí solos utilidad o exactitud.

5. Une los resultados y toma una decisión humana

Cuando las tres etapas hayan terminado o fallado de forma definitiva, ejecuta el archivo Python suministrado con el directorio de ejecución y un directorio de unión nuevo. La unión usa la biblioteca estándar de Python; comprueba tu instalación local antes de invocarla.

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

La unión comprueba los hashes de fuente y restricciones, la cobertura completa de líneas del inventario, su hash en cada canal, la identidad de la etapa, las referencias de las afirmaciones, la titularidad de las salidas y los requisitos publicitarios. Escribe claim-map.md, review.md y summary.json. Las etapas ausentes o inválidas aparecen bloqueadas y los borradores hermanos válidos siguen visibles. Una discrepancia global de la fuente detiene la unión. No verifica si una frase es cierta, una cita es justa, una afirmación tiene base legal adecuada o el propietario aprobó publicar. review.md comienza con decisiones PENDING.

La persona designada debe abrir cada draft.md listo, comparar todas las afirmaciones importantes y citas con el pasaje citado y las restricciones, revisar ambigüedades, voz del canal, derechos de adaptación, destino y oferta del anuncio, y registrar aceptar, revisar o rechazar para cada canal fuera de los campos pendientes generados. Publicar, enviar, programar y contratar anuncios exigen autorización humana independiente y comprobaciones de la plataforma.

6. Recupera una rama fallida sin perder lo válido

Si no están claros la aprobación o los derechos, resuelve la carencia concreta con el propietario e inicia una ejecución nueva con entradas corregidas. Cualquier cambio del hash de fuente o restricciones exige un inventario completo nuevo. Si falla un canal y los demás son válidos, inspecciona primero sus archivos y su error reales. Copia los bytes intactos del inventario y las carpetas hermanas verificadas a un directorio de ejecución nuevo, ejecuta solo la etapa fallida y une en otro directorio nuevo. Los resultados copiados deben seguir coincidiendo con el hash del inventario. Si el resultado de un proceso es incierto, lee su resultado y hashes antes de decidir reintentar. Conserva ejecuciones anteriores y cambios de revisión; nunca los sobrescribas. Corrige una referencia de afirmación ausente o no admitida en la etapa responsable y repite la unión determinista en un directorio nuevo.

Esta composición se invoca manualmente y la dirige el modelo. No aporta despacho impuesto por código, reintentos automáticos, concurrencia fija ni recuperación de efectos externos. Empieza con una fuente escrita ya aprobada: no adquiere ni verifica transcripciones, ni crea vídeos, audio o recursos visuales de campaña.

Archivos de referencia completos

Conserva juntos rutas y contenidos. Los bloques de código son copias completas del paquete revisado de forma independiente; son referencias, no registros de una ejecución 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

Fuentes y lecturas adicionales

  • Manual de skills de Claude Code — rutas de proyecto, frontmatter, invocación, sustitución de argumentos, contexto aislado y segundo plano. Comprobado el 19 de octubre de 2026; no se indica una versión única para esta receta.
  • Manual de subagents de Claude Code — agente de propósito general, contexto y permisos. Comprobado el 19 de octubre de 2026.
  • Costes de Claude Code, uso de datos y permisos — consumo según la cuenta, tratamiento de datos y controles de herramientas. Comprobado el 19 de octubre de 2026; no se midió gasto del flujo.
  • El paquete local revisado de forma independiente aporta cuatro skills, cinco contratos JSON, dos plantillas de entrada, árbol de salida y script de unión. Sus 16 comprobaciones usaron solo datos estructurales temporales; no generaron prosa ni aprobación factual.
  • Guía relacionada de BIG CHANGE: revisión paralela de documentos cubre otro flujo de Claude centrado en revisar documentos, no en adaptar una fuente a tres canales locales de Code.