يمكن لمقال أو نص مفرغ معتمد أن يغذي عدة مسودات، لكن كل مسودة تحتاج إلى مسار واضح يعود إلى مصدرها. يجمع هذا الدليل أربع مهارات مشروع في Claude Code منفصلة لجرد مصدر مكتوب وافق عليه مالكه، وصياغة منشور LinkedIn وقسم نشرة وإعلان في مجلدات مستقلة، ثم فحص عقود الملفات بعملية دمج محلية عبر Python. الناتج مسودات لمراجع بشري محدد بالاسم، وليس إذنا بالنشر أو الإرسال.

راجعت BIG CHANGE المهارات والعقود وشيفرة الدمج أدناه. وأجرى مراجع مستقل 16 فحصا بنيويا محليا. نحن لم نثبت المهارات أو نشغلها في Claude Code، ولم ندفع لتشغيل نموذج أو ننتج مسودة واقعية لقناة أو نختبر حملة حقيقية. الخطوات التالية إجراء يجربه القارئ بمصدره المعتمد.

التغيير الكبير

  • ما الذي تغير: أصبحت لأربع مهارات يملكها المستخدم عقود صريحة للمصدر والجرد ونتيجة القناة، تليها عملية دمج حتمية.
  • لماذا يهم: يمكن تتبع كل مسودة إلى مقاطع المصدر، فيما تبقى القناة المفقودة ظاهرة كخانة متوقفة.
  • ما ينبغي الانتباه إليه: لا تثبت فحوص الملفات صحة الوقائع أو حقوق الاقتباس والتكييف أو الموافقة التحريرية؛ يجب أن يفحص إنسان كل مسودة جاهزة.

1. أكد الوصول والحقوق والمصدر

استخدم مستودع Claude Code تسيطر عليه وحسابا مخولا بمعالجة المصدر. قبل استخدام مواد سرية، افحص النماذج المتاحة و الاستخدام والتكاليف, سياسة البيانات و أذونات الأدوات. يمكن لأمر Claude Code المسمى /usage عرض استهلاك الجلسة الحالية؛ وتعتمد الفوترة على الحساب والخطة. لم نقس تكلفة هذا المسار أو وفره. يجب أن يكون المصدر مقالا فعليا معتمدا بترميز UTF-8 أو تفريغا مدققا، مع تسجيل المالك والإصدار والموافقة. عالج أولا مع المالك دقة التفريغ وحقوق كل قناة والخصوصية وأي عرض إعلاني. لا يفي رابط فيديو أو القالب الفارغ أدناه بهذا الإدخال.

تستخدم المهارات المرجعية الأربع موضع Claude Code الموثق .claude/skills/<name>/SKILL.md والاستدعاء بأمر ذي شرطة مائلة والاستبدال $ARGUMENTS. يمنح إعداد context: fork كل استدعاء سياقه الخاص دون سجل المحادثة. تستخدم مهارات المسودات background: true، وهو يتطلب Claude Code v2.1.218 أو أحدث. تستخدم الوكيل العام، وتتبع قدرته على كتابة الملفات وأذوناته الفعلية إعداد المستخدم في Claude Code. نص المهارة لا يمنح صلاحيات ولا يفرض حدا للقراءة فقط. قد يتداخل العمل الخلفي إذا سمحت البيئة، لكن الإجراء لا يضمن التشغيل المتوازي. افحص المهارات الموجودة بالأسماء نفسها وآلية اكتشاف الأوامر قبل اعتماد الأسماء، وتحقق من أن الأمر يشير إلى ملف المشروع المثبت. راجع دليل المهارات و دليل الوكلاء الفرعيين.

2. ثبت الملفات الأربعة والعقود

أنشئ داخل المشروع المسارات الدقيقة في القائمة أدناه. انسخ كل كتلة كاملة من قسم الملفات المرجعية دون تغيير المحتوى. تحدد الملفات الخمسة ضمن .claude/repurpose-contracts/ أشكال الإدخال والجرد والتوقف ونتيجة القناة وخطة الاعتماديات. ضع join.py في جذر المشروع. يوضع القالبان ضمن repurpose-inputs/؛ استبدل العناصر المؤقتة بمحتوى حقيقي معتمد قبل أي تشغيل. شجرة المخرجات دليل بنيوي فقط. تتوفر الملفات كاملة أدناه للنسخ.

الملف

الغرض

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

يفحص الموافقات ويجرد كل سطر في المصدر قبل الصياغة.

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

مسؤول فقط عن مسودة LinkedIn ونتيجتها.

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

مسؤول فقط عن مسودة النشرة ونتيجتها.

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

مسؤول فقط عن مسودة الإعلان ونتيجته أو سبب صريح لتوقف العرض.

.claude/repurpose-contracts/*.json

يحدد الحقول الدقيقة وروابط البصمات وترتيب المراحل.

join.py

يفحص البنية والمنشأ ثم ينشئ خريطة للادعاءات وتسليما للمراجعة.

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

نماذج بدء فارغة، لا تعد أبدا دليلا على التشغيل.

تتطلب القيود حقوق تكييف صريحة لكل من LinkedIn والنشرة والإعلان، حتى إن توقف الإعلان لغياب عرض معتمد. forbiddenClaims يمكن أن يكون قائمة فارغة إن لم يفرض المالك محظورات إضافية. يتطلب الإعلان عرضا معتمدا ووجهة HTTP(S) ودعوة إلى إجراء؛ يؤدي غياب أي قيمة إلى إيقاف ذلك الفرع. سجل مراجعا حقيقيا بالاسم. أبق المصدر المعتمد والقيود دون تغيير أثناء التشغيل.

3. اختبر كل علاقة ثم شغل الجرد

اختبر كل مهارة منفردة أولا على مصدر حقيقي محدود ومعتمد. قارن الملفات الفعلية وبصماتها بالعقود. لم تنفذ BIG CHANGE هذا الاختبار في Claude Code. للتشغيل الكامل، أنشئ مجلدات جديدة بمسارات مطلقة للإدخالات ومخرجات التشغيل والدمج. استدع أمر الجرد داخل المستودع بثلاثة مسارات مطلقة:

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

استبدل كل وسيطة بين علامتي الزاوية بمسار فعلي. ينبغي أن تكتب المهارة inventory.json متضمنا نطاقات مقاطع حرفية ومرتبة تغطي جميع السطور، أو inventory-blocked.json مع الموافقة أو الحق الناقص تحديدا. لا تبدأ الصياغة إن توقف الجرد. قارن قيم SHA-256 المسجلة بملفي الإدخال وافحص حدود المقاطع، بما فيها السطور الفارغة.

4. اكتب في ثلاث قنوات معزولة

بعد وجود inventory.json وبقاء بصمتي الإدخال مطابقتين للسجل، استدع المهارات الثلاث المنفصلة. استخدم مجلد التشغيل المطلق نفسه للوسيطة الأولى وثلاثة مجلدات إخراج مختلفة للثانية. تقرأ كل مهارة المصدر المعتمد والقيود والجرد، ولا تقرأ مسودة شقيقة. يمكن تشغيلها بصورة مستقلة بعد الجرد ضمن حدود بيئة 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

تملك كل قناة مسؤولية result.json وفقط عند الجاهزية draft.md. تحدد النتيجة القناة وبصمات SHA-256 للمصدر والقيود والجرد، ثم تربط الادعاءات المهمة أو الاقتباسات بمعرفات المقاطع ومفاتيح القيود المناسبة. ready يعني الجاهزية للتحقق من الوقائع. غياب عرض إعلاني معتمد أو وجهة أو دعوة إلى إجراء يجب أن ينتج حالة توقف دون مسودة إعلان. الاستدعاء الخلفي أو JSON صالح لا يثبت وحده فائدة المسودة أو دقتها.

5. ادمج النتائج واتخذ قرارا بشريا

بعد عودة المراحل الثلاث أو ثبوت فشلها، شغل ملف Python المرفق مع مجلد التشغيل ومجلد دمج جديد. يستخدم الدمج مكتبة Python القياسية؛ تحقق من التثبيت المحلي قبل استدعائه.

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

يفحص الدمج بصمات المصدر والقيود وتغطية الجرد لجميع السطور وبصمته في كل قناة وهوية المرحلة ومؤشرات الادعاءات وملكية المخرجات ومتطلبات الإعلان. يكتب claim-map.md, review.md و summary.json. تظهر المراحل الناقصة أو غير الصالحة متوقفة، فيما تبقى المسودات الشقيقة الصالحة ظاهرة. يوقف اختلاف المصدر العام الدمج. ولا يتحقق من صدق جملة أو إنصاف اقتباس أو الأساس القانوني لادعاء أو موافقة المالك على النشر. review.md يبدأ بقرارات PENDING.

ينبغي للمراجع المحدد فتح كل draft.md جاهز، ومقارنة كل تصريح مهم واقتباس بالمقطع المشار إليه والقيود، وفحص الغموض وصوت القناة وحقوق التكييف ووجهة الإعلان وعرضه، ثم تسجيل قبول أو تعديل أو رفض لكل قناة خارج حقول الانتظار المولدة. يتطلب النشر والإرسال والجدولة ووضع الإعلانات إذنا بشريا مستقلا وفحوص المنصة.

6. أصلح الفرع الفاشل دون فقد العمل السليم

إذا غمضت موافقة المصدر أو الحقوق، عالج النقص المحدد مع المالك وابدأ تشغيلا جديدا بإدخالات مصححة. كل تغيير في بصمة المصدر أو القيود يتطلب جردا كاملا جديدا. إذا فشلت قناة وبقيت الأخريات سليمة، افحص أولا ملفات تلك القناة الفعلية وخطأها. انسخ بايتات الجرد دون تغيير ومجلدات القنوات السليمة المتحققة إلى مجلد تشغيل جديد، وأعد المرحلة الفاشلة فقط، ثم ادمج في مجلد جديد آخر. يجب أن تظل النتائج المنسوخة مطابقة لبصمة الجرد. إن غمض مصير عملية، اقرأ النتيجة والبصمات قبل قرار الإعادة. احتفظ بالتشغيلات السابقة وتعديلات المراجع، ولا تستبدلها بإعادة التشغيل. أصلح مؤشر الادعاء المفقود أو غير المدعوم في المرحلة المسؤولة، ثم أعد الدمج الحتمي في مجلد جديد.

هذا تركيب يستدعى يدويا ويوجهه النموذج. لا يوفر إسنادا تفرضه الشيفرة أو إعادة تلقائية أو تزامنا ثابتا أو استرداد الآثار الخارجية. ابدأ بمصدر مكتوب معتمد؛ لا يجلب هذا الإجراء تفريغا ولا يتحقق منه، ولا يصنع فيديو أو صوتا أو مواد بصرية للحملة.

الملفات المرجعية الكاملة

احفظ المسارات والمحتويات معا. كتل الشيفرة أدناه نسخ كاملة للحزمة المراجعة باستقلال، وهي ملفات مرجعية وليست سجلات تشغيل 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

المصادر وقراءات إضافية

  • دليل مهارات Claude Code — مسارات مهارات المشروع وfrontmatter والاستدعاء واستبدال الوسائط والسياق المنفصل والعمل الخلفي. فحص في 19 أكتوبر 2026؛ لا يذكر الدليل إصدارا واحدا لهذه الوصفة.
  • دليل وكلاء Claude Code الفرعيين — الوكيل العام والسياق والأذونات. فحص في 19 أكتوبر 2026.
  • تكاليف Claude Code, استخدام البيانات و الأذونات — استهلاك حسب الحساب ومعالجة البيانات وضوابط الأدوات. فحص في 19 أكتوبر 2026؛ لم يقس إنفاق هذا المسار.
  • قدمت الحزمة المحلية المراجعة باستقلال أربع مهارات وخمسة عقود JSON وقالبَي إدخال وشجرة مخرجات ونص الدمج. استخدمت فحوصها الـ16 بيانات اختبار بنيوية مؤقتة فقط، ولم تنتج نصا مولدا أو إقرارا بصحة الوقائع.
  • دليل BIG CHANGE ذي الصلة: مراجعة الوثائق بالتوازي يتناول مسارا مختلفا في Claude لمراجعة الوثائق، لا لتكييف مصدر محليا إلى ثلاث قنوات في Code.