Explanation:
**Planning test implementation**

I’m thinking about implementing tests for the social/draft.py file. First, I need to inspect the candidate classes within that file to understand what I’m dealing with. It’s essential to know the structure before diving into testing. Once I have a grasp of the classes, I can outline the tests I want to create, ensuring they cover all necessary cases. Let’s keep moving forward!


     1|"""Video candidate domain model and validation."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|
     7|from autoshorts.review.package import ReviewPackage
     8|from autoshorts.scripting.quality import ScriptQualityScore
     9|
    10|SUPPORTED_PLATFORMS = ("youtube", "tiktok", "instagram")
    11|
    12|
    13|@dataclass(frozen=True)
    14|class ValidationResult:
    15|    is_valid: bool
    16|    errors: tuple[str, ...] = ()
    17|
    18|
    19|@dataclass(frozen=True)
    20|class VideoCandidate:
    21|    candidate_id: str
    22|    pillar: str
    23|    title: str
    24|    platforms: tuple[str, ...]
    25|    duration_seconds: int
    26|    hook: str
    27|    script_outline: str
    28|    visual_concept: str
    29|    caption: str
    30|    cta: str
    31|    hashtags: tuple[str, ...]
    32|    quality_score: ScriptQualityScore
    33|    policy_notes: tuple[str, ...]
    34|    hypothesis: str
    35|
    36|    def to_review_package(self, preview_path: str | None = None) -> ReviewPackage:
    37|        return ReviewPackage(
    38|            candidate_id=self.candidate_id,
    39|            title=self.title,
    40|            pillar=self.pillar,
    41|            duration_seconds=self.duration_seconds,
    42|            hook=self.hook,
    43|            script_summary=self.script_outline,
    44|            visual_concept=self.visual_concept,
    45|            caption=self.caption,
    46|            hashtags=self.hashtags,
    47|            platforms=self.platforms,
    48|            quality_score=self.quality_score,
    49|            policy_notes=self.policy_notes,
    50|            preview_path=preview_path,
    51|        )
    52|
    53|
    54|def _is_blank(value: str) -> bool:
    55|    return not (value or "").strip()
    56|
    57|
    58|def validate_candidate(candidate: VideoCandidate) -> ValidationResult:
    59|    errors: list[str] = []
    60|
    61|    required_fields = {
    62|        "candidate_id": candidate.candidate_id,
    63|        "pillar": candidate.pillar,
    64|        "title": candidate.title,
    65|        "hook": candidate.hook,
    66|        "script_outline": candidate.script_outline,
    67|        "visual_concept": candidate.visual_concept,
    68|        "caption": candidate.caption,
    69|        "cta": candidate.cta,
    70|        "hypothesis": candidate.hypothesis,
    71|    }
    72|    for field_name, value in required_fields.items():
    73|        if _is_blank(value):
    74|            errors.append(f"{field_name} is required")
    75|
    76|    if not 10 <= candidate.duration_seconds <= 180:
    77|        errors.append("duration_seconds must be between 10 and 180")
    78|
    79|    if not candidate.platforms:
    80|        errors.append("at least one platform is required")
    81|    for platform in candidate.platforms:
    82|        if platform not in SUPPORTED_PLATFORMS:
    83|            errors.append(f"unsupported platform: {platform}")
    84|
    85|    return ValidationResult(is_valid=not errors, errors=tuple(errors))
    86|


File unchanged since last read. The content from the earlier read_file result in this conversation is still current — refer to that instead of re-reading.


1|"""Side-effect-free HTML preview generation for render manifests."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|from html import escape
     7|
     8|from autoshorts.ideas.candidate import ValidationResult
     9|from autoshorts.rendering.manifest import RenderManifest, validate_render_manifest
    10|
    11|
    12|@dataclass(frozen=True)
    13|class RenderPreview:
    14|    script_id: str
    15|    title: str
    16|    preview_type: str
    17|    html: str
    18|    side_effects: tuple[str, ...] = ()
    19|    external_calls: tuple[str, ...] = ()
    20|
    21|
    22|def _is_blank(value: str) -> bool:
    23|    return not (value or "").strip()
    24|
    25|
    26|def _timeline_row(index: int, item) -> str:
    27|    time_range = f"{item.start_seconds}–{item.end_seconds}s"
    28|    return "".join(
    29|        (
    30|            '
',
    31|            f"
{index:02d}. {escape(item.scene_purpose)}
",
    32|            f"
Time: {escape(time_range)}
",
    33|            f"
Asset mode: {escape(item.asset_mode)}
",
    34|            f"
Motion: {escape(item.motion_instruction)}
",
    35|            f"
Caption: {escape(item.caption_layer.text)}
",
    36|            f"
Label: {escape(item.label_layer.text)}
",
    37|            f"
Placeholder: {escape(item.placeholder_asset_path)}
",
    38|            "
",
    39|        )
    40|    )
    41|
    42|
    43|def build_html_preview(manifest: RenderManifest) -> RenderPreview:
    44|    """Build an inspectable HTML timeline preview without rendering media."""
    45|
    46|    validation = validate_render_manifest(manifest)
    47|    if not validation.is_valid:
    48|        raise ValueError("; ".join(validation.errors))
    49|
    50|    settings = manifest.settings
    51|    rows = "\n".join(_timeline_row(index, item) for index, item in enumerate(manifest.timeline, start=1))
    52|    html = f"""<!doctype html>
    53|
    54|
    55|  <meta charset="utf-8">
    56|  <title>{escape(manifest.title)} — render preview</title>
    57|
    63|
    64|
    65|
    66|
{escape(manifest.title)}

    67|

    68|
Script: {escape(manifest.script_id)}

    69|
Format: {settings.width}x{settings.height}, {settings.fps}fps

    70|
Text policy: captions and labels are renderer-owned; no generated text inside assets.

    71|

    72|    {rows}
    73|
    74|
    75|"""
    76|    return RenderPreview(
    77|        script_id=manifest.script_id,
    78|        title=manifest.title,
    79|        preview_type="html_timeline",
    80|        html=html,
    81|        side_effects=(),
    82|        external_calls=(),
    83|    )
    84|
    85|
    86|def validate_render_preview(preview: RenderPreview) -> ValidationResult:
    87|    errors: list[str] = []
    88|    if _is_blank(preview.script_id):
    89|        errors.append("script_id is required")
    90|    if _is_blank(preview.title):
    91|        errors.append("title is required")
    92|    if preview.preview_type != "html_timeline":
    93|        errors.append("preview_type must be html_timeline")
    94|    if _is_blank(preview.html):
    95|        errors.append("html is required")
    96|    if "<html" not in preview.html.casefold():
    97|        errors.append("html preview must contain an html document")
    98|    if preview.side_effects:
    99|        errors.append("preview must not have side effects")
   100|    if preview.external_calls:
   101|        errors.append("preview must not call external renderers")
   102|    return ValidationResult(is_valid=not errors, errors=tuple(errors))
   103|


Explanation:
**Planning for tests**

I see that I need to write tests for the social draft, which is a key part of the process. There’s also the need to build a fixture. Alright, so I'm thinking about how to approach this. I want to ensure that the tests are comprehensive enough to cover all the necessary aspects to make sure everything works smoothly! I'll take it step by step as I work through it.

import pytest

from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.content.to_candidate import convert_script_draft_to_candidate
from autoshorts.rendering.manifest import build_render_manifest
from autoshorts.rendering.preview import build_html_preview
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.social.draft import (
    SocialMediaDraft,
    build_social_media_draft,
    validate_social_media_draft,
)

def _candidate_and_preview():
    brief = create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    script = create_script_draft(brief)
    candidate = convert_script_draft_to_candidate(brief, script)
    preview = build_html_preview(build_render_manifest(create_shot_plan(script)))
    return candidate, preview

def test_build_social_media_draft_creates_approval_first_post_draft():
    candidate, preview = _candidate_and_preview()

    draft = build_social_media_draft(candidate, preview)

    assert isinstance(draft, SocialMediaDraft)
    assert draft.candidate_id == "ai_life_systems-ai-weekly-review-routine-35s"
    assert draft.platforms == ("youtube", "tiktok", "instagram")
    assert draft.platform_labels == ("YouTube Shorts", "TikTok", "Instagram Reels")
    assert draft.title_variants == (
        "AI weekly review routine",
        "If AI runs your week, you have already lost control.",
    )
    assert draft.caption == candidate.caption
    assert draft.hashtags == ("#AI", "#LifeSystems", "#Productivity")
    assert draft.cta == candidate.cta
    assert draft.hook == candidate.hook
    assert draft.planned_duration_seconds == 35
    assert draft.preview_reference == "HTML timeline preview available"
    assert draft.preview_type == "html_timeline"
    assert draft.requires_human_approval is True
    assert draft.side_effects == ()
    assert "Quality average: 8.0" in draft.quality_risk_summary
    assert "AI-slop risk: 3" in draft.quality_risk_summary
    assert "Policy risk: low" in draft.quality_risk_summary

def test_validate_social_media_draft_accepts_default_draft():
    candidate, preview = _candidate_and_preview()

    result = validate_social_media_draft(build_social_media_draft(candidate, preview))

    assert result.is_valid is True
    assert result.errors == ()

def test_build_social_media_draft_rejects_preview_for_different_script():
    candidate, preview = _candidate_and_preview()
    mismatched = type(preview)(
        script_id="other-script",
        title=preview.title,
        preview_type=preview.preview_type,
        html=preview.html,
        side_effects=preview.side_effects,
        external_calls=preview.external_calls,
    )

    with pytest.raises(ValueError, match="preview script_id must match candidate source brief"):
        build_social_media_draft(candidate, mismatched)

def test_validate_social_media_draft_rejects_missing_approval_and_side_effects():
    candidate, preview = _candidate_and_preview()
    draft = build_social_media_draft(candidate, preview)
    unsafe = SocialMediaDraft(
        candidate_id=draft.candidate_id,
        platforms=draft.platforms,
        platform_labels=draft.platform_labels,
        title_variants=draft.title_variants,
        caption=draft.caption,
        hashtags=draft.hashtags,
        cta=draft.cta,
        hook=draft.hook,
        planned_duration_seconds=draft.planned_duration_seconds,
        quality_risk_summary=draft.quality_risk_summary,
        preview_reference=draft.preview_reference,
        preview_type=draft.preview_type,
        requires_human_approval=False,
        side_effects=("platform_upload",),
    )

    result = validate_social_media_draft(unsafe)

    assert result.is_valid is False
    assert "human approval is required" in result.errors
    assert "social media drafts must not have side effects" in result.errors

def test_validate_social_media_draft_rejects_unsupported_platform_and_blank_fields():
    draft = SocialMediaDraft(
        candidate_id="",
        platforms=("linkedin",),
        platform_labels=("LinkedIn",),
        title_variants=(),
        caption="",
        hashtags=(),
        cta="",
        hook="",
        planned_duration_seconds=9,
        quality_risk_summary="",
        preview_reference="",
        preview_type="",
        requires_human_approval=True,
        side_effects=(),
    )

    result = validate_social_media_draft(draft)

    assert result.is_valid is False
    assert "candidate_id is required" in result.errors
    assert "unsupported platform: linkedin" in result.errors
    assert "at least one title variant is required" in result.errors
    assert "caption is required" in result.errors
    assert "at least one hashtag is required" in result.errors
    assert "cta is required" in result.errors
    assert "hook is required" in result.errors
    assert "planned_duration_seconds must be between 10 and 180" in result.errors
    assert "quality_risk_summary is required" in result.errors
    assert "preview_reference is required" in result.errors
    assert "preview_type is required" in result.errors


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_draft.py">
ERROR [9:6] Import "autoshorts.social.draft" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


terminal

python -m pytest tests/test_social_draft.py -q


Output:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_social_draft.py __________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_draft.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_social_draft.py:9: in <module>
    from autoshorts.social.draft import (
E   ModuleNotFoundError: No module named 'autoshorts.social'
=========================== short test summary info ============================
ERROR tests/test_social_draft.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.14s


Explanation:
**Determining ScriptQualityScore fields**

I need to figure out which fields are involved in the ScriptQualityScore. It’s crucial to identify the specific requirements for this part before I can move forward. I should explore the documentation or guidelines that might outline what exactly those fields are and how they contribute to the overall score. This will help ensure I have all the necessary components to implement it correctly. So, let's clarify those details!


     1|"""Deterministic pre-render quality gate for script candidates."""
     2|
     3|from __future__ import annotations
     4|
     5|from dataclasses import dataclass
     6|from typing import Literal
     7|
     8|PolicyRisk = Literal["low", "medium", "high"]
     9|
    10|
    11|@dataclass(frozen=True)
    12|class ScriptQualityScore:
    13|    """Human/LLM-reviewable scores for one short-form script candidate.
    14|
    15|    Numeric dimensions use a 0-10 scale. ``ai_slop_risk`` is inverse: higher
    16|    means more generic, uncanny, or trust-damaging output risk.
    17|    """
    18|
    19|    hook_strength: int
    20|    novelty: int
    21|    clarity: int
    22|    retention: int
    23|    ai_slop_risk: int
    24|    policy_risk: PolicyRisk = "low"
    25|
    26|    @property
    27|    def average_quality(self) -> float:
    28|        return (self.hook_strength + self.novelty + self.clarity + self.retention) / 4
    29|
    30|
    31|def should_render(
    32|    score: ScriptQualityScore,
    33|    *,
    34|    minimum_average: float = 8.0,
    35|    maximum_ai_slop_risk: int = 5,
    36|) -> bool:
    37|    """Return whether a script should proceed to rendering.
    38|
    39|    The gate is intentionally conservative: content below threshold waits for
    40|    rewrite instead of spending render time on AI slop. A dreary but economical
    41|    little kindness.
    42|    """
    43|
    44|    if score.policy_risk == "high":
    45|        return False
    46|    if score.ai_slop_risk > maximum_ai_slop_risk:
    47|        return False
    48|    return score.average_quality >= minimum_average
    49|


"""Approval-first social media draft artifacts."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import SUPPORTED_PLATFORMS, ValidationResult, VideoCandidate, validate_candidate
from autoshorts.rendering.preview import RenderPreview, validate_render_preview

_PLATFORM_LABELS = {
    "youtube": "YouTube Shorts",
    "tiktok": "TikTok",
    "instagram": "Instagram Reels",
}

@dataclass(frozen=True)
class SocialMediaDraft:
    candidate_id: str
    platforms: tuple[str, ...]
    platform_labels: tuple[str, ...]
    title_variants: tuple[str, ...]
    caption: str
    hashtags: tuple[str, ...]
    cta: str
    hook: str
    planned_duration_seconds: int
    quality_risk_summary: str
    preview_reference: str
    preview_type: str
    requires_human_approval: bool = True
    side_effects: tuple[str, ...] = ()

def _is_blank(value: str) -> bool:
    return not (value or "").strip()

def _quality_risk_summary(candidate: VideoCandidate) -> str:
    score = candidate.quality_score
    return (
        f"Quality average: {score.average_quality:.1f}; "
        f"AI-slop risk: {score.ai_slop_risk}; "
        f"Policy risk: {score.policy_risk}"
    )

def _source_brief_id(candidate: VideoCandidate) -> str | None:
    prefix = "Source brief: "
    for note in candidate.policy_notes:
        if note.startswith(prefix):
            return note.removeprefix(prefix).strip()
    return None

def build_social_media_draft(candidate: VideoCandidate, preview: RenderPreview) -> SocialMediaDraft:
    """Build a side-effect-free platform draft from a candidate and preview."""

    candidate_validation = validate_candidate(candidate)
    if not candidate_validation.is_valid:
        raise ValueError("; ".join(candidate_validation.errors))

    preview_validation = validate_render_preview(preview)
    if not preview_validation.is_valid:
        raise ValueError("; ".join(preview_validation.errors))

    source_brief_id = _source_brief_id(candidate)
    if source_brief_id is not None and preview.script_id != source_brief_id:
        raise ValueError("preview script_id must match candidate source brief")

    draft = SocialMediaDraft(
        candidate_id=candidate.candidate_id,
        platforms=candidate.platforms,
        platform_labels=tuple(_PLATFORM_LABELS[platform] for platform in candidate.platforms),
        title_variants=(candidate.title, candidate.hook),
        caption=candidate.caption,
        hashtags=candidate.hashtags,
        cta=candidate.cta,
        hook=candidate.hook,
        planned_duration_seconds=candidate.duration_seconds,
        quality_risk_summary=_quality_risk_summary(candidate),
        preview_reference="HTML timeline preview available",
        preview_type=preview.preview_type,
        requires_human_approval=True,
        side_effects=(),
    )
    validation = validate_social_media_draft(draft)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return draft

def validate_social_media_draft(draft: SocialMediaDraft) -> ValidationResult:
    errors: list[str] = []
    for field_name, value in {
        "candidate_id": draft.candidate_id,
        "caption": draft.caption,
        "cta": draft.cta,
        "hook": draft.hook,
        "quality_risk_summary": draft.quality_risk_summary,
        "preview_reference": draft.preview_reference,
        "preview_type": draft.preview_type,
    }.items():
        if _is_blank(value):
            errors.append(f"{field_name} is required")

    if not draft.platforms:
        errors.append("at least one platform is required")
    for platform in draft.platforms:
        if platform not in SUPPORTED_PLATFORMS:
            errors.append(f"unsupported platform: {platform}")

    if len(draft.platform_labels) != len(draft.platforms):
        errors.append("platform_labels must match platforms")
    if not draft.title_variants:
        errors.append("at least one title variant is required")
    elif any(_is_blank(title) for title in draft.title_variants):
        errors.append("title variants must not be blank")
    if not draft.hashtags:
        errors.append("at least one hashtag is required")
    elif any(_is_blank(hashtag) for hashtag in draft.hashtags):
        errors.append("hashtags must not be blank")
    if not 10 <= draft.planned_duration_seconds <= 180:
        errors.append("planned_duration_seconds must be between 10 and 180")
    if not draft.requires_human_approval:
        errors.append("human approval is required")
    if draft.side_effects:
        errors.append("social media drafts must not have side effects")

    return ValidationResult(is_valid=not errors, errors=tuple(errors))


"""Social-media approval draft artifacts."""


Output:
.....                                                                    [100%]
5 passed in 0.05s


Output:
........................................................................ [ 84%]
.............                                                            [100%]
85 passed in 0.15s


Explanation:
**Updating README file**

I need to make sure the README file is up to date. It’s important for anyone using it to have the latest information. I should review the current content and see what needs to be added or changed. Maybe I’ll clarify instructions or update any outdated examples. I want it to be clear and helpful, so the user can easily understand and follow along. Let's not miss any crucial details!

patch

- `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
- `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -25,6 +25,7 @@
 - `autoshorts.rendering.preview` — side-effect-free HTML timeline previews for render manifests.
 - `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
+- `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
 - `autoshorts.review.render` — renders complete review batches into Telegram-safe text.
 - `autoshorts.review.workflow` — splits candidates into ready, blocked, and invalid review buckets.
 - `autoshorts.telegram.review_sender` — validates Telegram review text and prepares side-effect-free delivery plans.

No linter for .md files


It prints preview metadata first, then the HTML document. Redirect stdout to a local `.html` file if you want to inspect it in a browser; the CLI itself still writes nothing.

## Social media draft

`autoshorts.social.draft.build_social_media_draft()` turns a validated `VideoCandidate` plus `RenderPreview` into an approval-first `SocialMediaDraft` containing:

- candidate id;
- target platforms and human-readable platform labels;
- title variants;
- caption, hashtags, CTA, and hook;
- planned duration;
- quality/risk summary;
- preview reference and preview type;
- `requires_human_approval=True`;
- `side_effects=()`.

`validate_social_media_draft()` rejects unsupported platforms, blank required fields, missing titles/hashtags, invalid durations, missing human approval, and any side effects. This is the first platform-facing post draft, but still performs no upload, no Telegram call, no rendering, and no credential access.

## Human approval commands

It prints preview metadata first, then the HTML document. Redirect stdout to a local `.html` file if you want to inspect it in a browser; the CLI itself still writes nothing.

## Human approval commands


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -107,6 +107,22 @@

 It prints preview metadata first, then the HTML document. Redirect stdout to a local `.html` file if you want to inspect it in a browser; the CLI itself still writes nothing.

+## Social media draft
+
+`autoshorts.social.draft.build_social_media_draft()` turns a validated `VideoCandidate` plus `RenderPreview` into an approval-first `SocialMediaDraft` containing:
+
+- candidate id;
+- target platforms and human-readable platform labels;
+- title variants;
+- caption, hashtags, CTA, and hook;
+- planned duration;
+- quality/risk summary;
+- preview reference and preview type;
+- `requires_human_approval=True`;
+- `side_effects=()`.
+
+`validate_social_media_draft()` rejects unsupported platforms, blank required fields, missing titles/hashtags, invalid durations, missing human approval, and any side effects. This is the first platform-facing post draft, but still performs no upload, no Telegram call, no rendering, and no credential access.
+
 ## Human approval commands

 Supported review responses:

No linter for .md files


terminal

python -m pytest tests -q && python - <<'PY'
from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.content.to_candidate import convert_script_draft_to_candidate
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.rendering.manifest import build_render_manifest
from autoshorts.rendering.preview import build_html_preview
from autoshorts.social.draft import build_social_media_draft, validate_social_media_draft
brief = create_content_brief(
    pillar_id='ai_life_systems',
    topic='AI weekly review routine',
    audience_problem='I use AI tools but still lose control of priorities.',
    payoff='a 10-minute Sunday review that keeps AI as assistant, not boss',
)
script = create_script_draft(brief)
candidate = convert_script_draft_to_candidate(brief, script)
preview = build_html_preview(build_render_manifest(create_shot_plan(script)))
draft = build_social_media_draft(candidate, preview)
print(draft.candidate_id)
print(draft.platform_labels)
print(draft.preview_reference)
print(draft.requires_human_approval, draft.side_effects)
print(validate_social_media_draft(draft).is_valid)
PY


Output:
........................................................................ [ 84%]
.............                                                            [100%]
85 passed in 0.13s
ai_life_systems-ai-weekly-review-routine-35s
('YouTube Shorts', 'TikTok', 'Instagram Reels')
HTML timeline preview available
True ()
True


git status --short && git add README.md autoshorts/social/__init__.py autoshorts/social/draft.py tests/test_social_draft.py && git commit -m "feat: add social media draft model" && git log --oneline -n 8 && git status --short


Output:
M README.md
?? autoshorts/social/
?? tests/test_social_draft.py
[feat/core-approval-quality-gates 1f6aa2c] feat: add social media draft model
 4 files changed, 286 insertions(+)
 create mode 100644 autoshorts/social/__init__.py
 create mode 100644 autoshorts/social/draft.py
 create mode 100644 tests/test_social_draft.py
1f6aa2c feat: add social media draft model
f8c5588 feat: add demo render preview cli
42ac32f feat: add side-effect-free render preview
e258635 feat: add render manifest planning
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates
9c125f9 feat: add content brief and script draft models


Explanation:
**Planning the next steps**

I think the user might want me to keep going with the social review in this same turn. It seems like I should keep working until everything is done, right? So, I’m planning to implement the social review next, maybe even following the phrase "machen wir so weiter." I believe getting things in order is important, and I need to focus on writing tests for the social review using TDD. Let’s proceed!

import pytest

from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.content.to_candidate import convert_script_draft_to_candidate
from autoshorts.rendering.manifest import build_render_manifest
from autoshorts.rendering.preview import build_html_preview
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.social.draft import SocialMediaDraft, build_social_media_draft
from autoshorts.social.review import render_social_media_draft_review

def _draft():
    brief = create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    script = create_script_draft(brief)
    candidate = convert_script_draft_to_candidate(brief, script)
    preview = build_html_preview(build_render_manifest(create_shot_plan(script)))
    return build_social_media_draft(candidate, preview)

def test_render_social_media_draft_review_creates_telegram_safe_approval_text():
    review_text = render_social_media_draft_review(_draft())

    assert review_text.startswith("## Social Media Draft Review")
    assert "Candidate: ai_life_systems-ai-weekly-review-routine-35s" in review_text
    assert "Platforms: YouTube Shorts, TikTok, Instagram Reels" in review_text
    assert "Duration: 35s" in review_text
    assert "Hook:" in review_text
    assert "If AI runs your week, you have already lost control." in review_text
    assert "Title variants:" in review_text
    assert "Caption:" in review_text
    assert "Hashtags: #AI #LifeSystems #Productivity" in review_text
    assert "Preview: HTML timeline preview available (html_timeline)" in review_text
    assert "Quality/Risk: Quality average: 8.0; AI-slop risk: 3; Policy risk: low" in review_text
    assert "Requires human approval: yes" in review_text
    assert "Side effects: none" in review_text
    assert "Approval commands:" in review_text
    assert "Freigabe YouTube" in review_text
    assert "Freigabe TikTok" in review_text
    assert "Freigabe Instagram" in review_text
    assert "Freigabe alle" in review_text
    assert "Ablehnen" in review_text
    assert "Ändern: <requested change>" in review_text
    assert len(review_text) <= 4096
    assert "publish" not in review_text.casefold()
    assert "post" not in review_text.casefold()

def test_render_social_media_draft_review_rejects_invalid_draft():
    draft = _draft()
    invalid = SocialMediaDraft(
        candidate_id="",
        platforms=draft.platforms,
        platform_labels=draft.platform_labels,
        title_variants=draft.title_variants,
        caption=draft.caption,
        hashtags=draft.hashtags,
        cta=draft.cta,
        hook=draft.hook,
        planned_duration_seconds=draft.planned_duration_seconds,
        quality_risk_summary=draft.quality_risk_summary,
        preview_reference=draft.preview_reference,
        preview_type=draft.preview_type,
        requires_human_approval=draft.requires_human_approval,
        side_effects=draft.side_effects,
    )

    with pytest.raises(ValueError, match="candidate_id is required"):
        render_social_media_draft_review(invalid)


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_review.py">
ERROR [10:6] Import "autoshorts.social.review" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


python -m pytest tests/test_social_review.py -q


Output:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_social_review.py _________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_review.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_social_review.py:10: in <module>
    from autoshorts.social.review import render_social_media_draft_review
E   ModuleNotFoundError: No module named 'autoshorts.social.review'
=========================== short test summary info ============================
ERROR tests/test_social_review.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.13s


"""Telegram-safe social media draft review rendering."""

from __future__ import annotations

from autoshorts.social.draft import SocialMediaDraft, validate_social_media_draft

def _platform_approval_command(platform: str) -> str:
    if platform == "youtube":
        return "Freigabe YouTube"
    if platform == "tiktok":
        return "Freigabe TikTok"
    if platform == "instagram":
        return "Freigabe Instagram"
    return f"Freigabe {platform}"

def render_social_media_draft_review(draft: SocialMediaDraft) -> str:
    """Render a social-media draft as a Telegram-safe approval message."""

    validation = validate_social_media_draft(draft)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))

    approval_commands = tuple(_platform_approval_command(platform) for platform in draft.platforms)
    title_lines = "\n".join(f"- {title}" for title in draft.title_variants)
    command_lines = "\n".join(
        f"- {command}"
        for command in (
            *approval_commands,
            "Freigabe alle",
            "Ablehnen",
            "Ändern: <requested change>",
        )
    )
    side_effects = ", ".join(draft.side_effects) if draft.side_effects else "none"
    approval = "yes" if draft.requires_human_approval else "no"

    return "\n".join(
        (
            "## Social Media Draft Review",
            "",
            f"Candidate: {draft.candidate_id}",
            f"Platforms: {', '.join(draft.platform_labels)}",
            f"Duration: {draft.planned_duration_seconds}s",
            "",
            "Hook:",
            draft.hook,
            "",
            "Title variants:",
            title_lines,
            "",
            "Caption:",
            draft.caption,
            "",
            f"Hashtags: {' '.join(draft.hashtags)}",
            f"CTA: {draft.cta}",
            f"Preview: {draft.preview_reference} ({draft.preview_type})",
            f"Quality/Risk: {draft.quality_risk_summary}",
            f"Requires human approval: {approval}",
            f"Side effects: {side_effects}",
            "",
            "Approval commands:",
            command_lines,
        )
    )


Output:
..                                                                       [100%]
2 passed in 0.05s


Output:
........................................................................ [ 82%]
...............                                                          [100%]
87 passed in 0.14s


Explanation:
**Updating README file**

I need to make sure I update the README file and prepare a commit for it. This is essential for keeping documentation accurate and clear for users. I want to ensure that any changes or additions I've made are reflected in that file. So, I’m planning to review the current contents, assess what's needed, and make those updates before finalizing the commit. It's important to keep everything up-to-date!

patch

- `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
- `autoshorts.social.review` — Telegram-safe approval text renderer for social media drafts.
- `autoshorts.review.render` — renders complete review batches into Telegram-safe text.

- `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
- `autoshorts.review.render` — renders complete review batches into Telegram-safe text.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -26,6 +26,7 @@
 - `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
 - `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
+- `autoshorts.social.review` — Telegram-safe approval text renderer for social media drafts.
 - `autoshorts.review.render` — renders complete review batches into Telegram-safe text.
 - `autoshorts.review.workflow` — splits candidates into ready, blocked, and invalid review buckets.
 - `autoshorts.telegram.review_sender` — validates Telegram review text and prepares side-effect-free delivery plans.

No linter for .md files


`validate_social_media_draft()` rejects unsupported platforms, blank required fields, missing titles/hashtags, invalid durations, missing human approval, and any side effects. This is the first platform-facing post draft, but still performs no upload, no Telegram call, no rendering, and no credential access.

`autoshorts.social.review.render_social_media_draft_review()` turns a valid `SocialMediaDraft` into a Telegram-safe approval message with candidate id, platforms, duration, hook, title variants, caption, hashtags, CTA, preview reference, quality/risk summary, explicit human-approval status, and approval commands:

- `Freigabe YouTube`
- `Freigabe TikTok`
- `Freigabe Instagram`
- `Freigabe alle`
- `Ablehnen`
- `Ändern: <requested change>`

The renderer avoids publish/post language and keeps approval semantics separate from any future platform action.

## Human approval commands

`validate_social_media_draft()` rejects unsupported platforms, blank required fields, missing titles/hashtags, invalid durations, missing human approval, and any side effects. This is the first platform-facing post draft, but still performs no upload, no Telegram call, no rendering, and no credential access.

## Human approval commands


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -124,6 +124,17 @@

 `validate_social_media_draft()` rejects unsupported platforms, blank required fields, missing titles/hashtags, invalid durations, missing human approval, and any side effects. This is the first platform-facing post draft, but still performs no upload, no Telegram call, no rendering, and no credential access.

+`autoshorts.social.review.render_social_media_draft_review()` turns a valid `SocialMediaDraft` into a Telegram-safe approval message with candidate id, platforms, duration, hook, title variants, caption, hashtags, CTA, preview reference, quality/risk summary, explicit human-approval status, and approval commands:
+
+- `Freigabe YouTube`
+- `Freigabe TikTok`
+- `Freigabe Instagram`
+- `Freigabe alle`
+- `Ablehnen`
+- `Ändern: <requested change>`
+
+The renderer avoids publish/post language and keeps approval semantics separate from any future platform action.
+
 ## Human approval commands

 Supported review responses:

No linter for .md files


terminal

python -m pytest tests -q && python - <<'PY'
from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.content.to_candidate import convert_script_draft_to_candidate
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.rendering.manifest import build_render_manifest
from autoshorts.rendering.preview import build_html_preview
from autoshorts.social.draft import build_social_media_draft
from autoshorts.social.review import render_social_media_draft_review
brief = create_content_brief(
    pillar_id='ai_life_systems',
    topic='AI weekly review routine',
    audience_problem='I use AI tools but still lose control of priorities.',
    payoff='a 10-minute Sunday review that keeps AI as assistant, not boss',
)
script = create_script_draft(brief)
candidate = convert_script_draft_to_candidate(brief, script)
preview = build_html_preview(build_render_manifest(create_shot_plan(script)))
text = render_social_media_draft_review(build_social_media_draft(candidate, preview))
print(text.splitlines()[0])
print(len(text))
print('Freigabe alle' in text)
print('publish' in text.casefold(), 'post' in text.casefold())
PY


Output:
........................................................................ [ 82%]
...............                                                          [100%]
87 passed in 0.16s
## Social Media Draft Review
776
True
False False


git status --short && git add README.md autoshorts/social/review.py tests/test_social_review.py && git commit -m "feat: render social media draft reviews" && git log --oneline -n 8 && git status --short


Output:
M README.md
?? autoshorts/social/review.py
?? tests/test_social_review.py
[feat/core-approval-quality-gates a7d10fa] feat: render social media draft reviews
 3 files changed, 152 insertions(+)
 create mode 100644 autoshorts/social/review.py
 create mode 100644 tests/test_social_review.py
a7d10fa feat: render social media draft reviews
1f6aa2c feat: add social media draft model
f8c5588 feat: add demo render preview cli
42ac32f feat: add side-effect-free render preview
e258635 feat: add render manifest planning
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates


Explanation:
**Deciding on delivery**

I'm thinking about whether I should continue with the delivery process since the user indicated "so weiter." My previous plan involves drafting and reviewing before going ahead with the delivery. It seems reasonable to proceed with the delivery as well. Also, I need to ensure that everything is tested properly to guarantee a smooth process. Making sure all parts are in place is crucial!

import pytest

from autoshorts.social.delivery import SocialDraftDelivery, prepare_social_draft_delivery

def test_prepare_social_draft_delivery_wraps_review_text_without_side_effects():
    text = "## Social Media Draft Review\nCandidate: example\nFreigabe alle"

    delivery = prepare_social_draft_delivery(text, topic="review")

    assert isinstance(delivery, SocialDraftDelivery)
    assert delivery.delivery_type == "social_draft_review"
    assert delivery.topic == "review"
    assert delivery.text == text
    assert delivery.requires_human_approval is True
    assert delivery.side_effects == ()

def test_prepare_social_draft_delivery_rejects_blank_text():
    with pytest.raises(ValueError, match="review text is required"):
        prepare_social_draft_delivery("   ")

def test_prepare_social_draft_delivery_rejects_long_text():
    with pytest.raises(ValueError, match="Telegram message exceeds 4096 characters"):
        prepare_social_draft_delivery("x" * 4097)

def test_prepare_social_draft_delivery_rejects_action_language():
    with pytest.raises(ValueError, match="social draft delivery must not imply publishing"):
        prepare_social_draft_delivery("please publish this")

    with pytest.raises(ValueError, match="social draft delivery must not imply publishing"):
        prepare_social_draft_delivery("please post this")


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_delivery.py">
ERROR [3:6] Import "autoshorts.social.delivery" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


python -m pytest tests/test_social_delivery.py -q


Output:
==================================== ERRORS ====================================
________________ ERROR collecting tests/test_social_delivery.py ________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_social_delivery.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_social_delivery.py:3: in <module>
    from autoshorts.social.delivery import SocialDraftDelivery, prepare_social_draft_delivery
E   ModuleNotFoundError: No module named 'autoshorts.social.delivery'
=========================== short test summary info ============================
ERROR tests/test_social_delivery.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.11s


"""Side-effect-free delivery planning for social media draft reviews."""

from __future__ import annotations

from dataclasses import dataclass

_TELEGRAM_MESSAGE_LIMIT = 4096
_FORBIDDEN_ACTION_WORDS = ("publish", "post")

@dataclass(frozen=True)
class SocialDraftDelivery:
    text: str
    topic: str = "review"
    delivery_type: str = "social_draft_review"
    requires_human_approval: bool = True
    side_effects: tuple[str, ...] = ()

def prepare_social_draft_delivery(review_text: str, *, topic: str = "review") -> SocialDraftDelivery:
    """Prepare a Telegram-bound social draft review without sending it."""

    if not (review_text or "").strip():
        raise ValueError("review text is required")
    if len(review_text) > _TELEGRAM_MESSAGE_LIMIT:
        raise ValueError("Telegram message exceeds 4096 characters")

    lowered = review_text.casefold()
    if any(word in lowered for word in _FORBIDDEN_ACTION_WORDS):
        raise ValueError("social draft delivery must not imply publishing")

    return SocialDraftDelivery(
        text=review_text,
        topic=topic,
        delivery_type="social_draft_review",
        requires_human_approval=True,
        side_effects=(),
    )


python -m pytest tests/test_social_delivery.py -q && python -m pytest tests -q


Output:
....                                                                     [100%]
4 passed in 0.04s
........................................................................ [ 79%]
...................                                                      [100%]
91 passed in 0.15s


patch

- `autoshorts.social.delivery` — side-effect-free delivery plan for social media draft review text.
- `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
- `autoshorts.social.review` — Telegram-safe approval text renderer for social media drafts.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -25,6 +25,7 @@
 - `autoshorts.rendering.preview` — side-effect-free HTML timeline previews for render manifests.
 - `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
+- `autoshorts.social.delivery` — side-effect-free delivery plan for social media draft review text.
 - `autoshorts.social.draft` — approval-first social media draft model from candidate plus render preview.
 - `autoshorts.social.review` — Telegram-safe approval text renderer for social media drafts.
 - `autoshorts.review.render` — renders complete review batches into Telegram-safe text.

No linter for .md files


The renderer avoids publish/post language and keeps approval semantics separate from any future platform action.

`autoshorts.social.delivery.prepare_social_draft_delivery()` wraps social draft review text in a side-effect-free `SocialDraftDelivery` object with delivery type `social_draft_review`, topic metadata, `requires_human_approval=True`, and `side_effects=()`. It validates non-empty text, Telegram's 4096-character limit, and the same no publish/post language boundary.

## Human approval commands

The renderer avoids publish/post language and keeps approval semantics separate from any future platform action.

## Human approval commands


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -136,6 +136,8 @@

 The renderer avoids publish/post language and keeps approval semantics separate from any future platform action.

+`autoshorts.social.delivery.prepare_social_draft_delivery()` wraps social draft review text in a side-effect-free `SocialDraftDelivery` object with delivery type `social_draft_review`, topic metadata, `requires_human_approval=True`, and `side_effects=()`. It validates non-empty text, Telegram's 4096-character limit, and the same no publish/post language boundary.
+
 ## Human approval commands

 Supported review responses:

No linter for .md files


terminal

python -m pytest tests -q && git status --short && git add README.md autoshorts/social/delivery.py tests/test_social_delivery.py && git commit -m "feat: add social draft delivery preparation" && git log --oneline -n 8 && git status --short


Output:
........................................................................ [ 79%]
...................                                                      [100%]
91 passed in 0.14s
 M README.md
?? autoshorts/social/delivery.py
?? tests/test_social_delivery.py
[feat/core-approval-quality-gates 515c549] feat: add social draft delivery preparation
 3 files changed, 75 insertions(+)
 create mode 100644 autoshorts/social/delivery.py
 create mode 100644 tests/test_social_delivery.py
515c549 feat: add social draft delivery preparation
a7d10fa feat: render social media draft reviews
1f6aa2c feat: add social media draft model
f8c5588 feat: add demo render preview cli
42ac32f feat: add side-effect-free render preview
e258635 feat: add render manifest planning
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline


Explanation:
**Considering CLI and next steps**

I’m thinking about whether I can tackle the CLI now. The earlier plan was to include a demo for the social draft. It seems like a logical next step to continue with that. Plus, I realize we have some time available now to get this done. So, let’s go for it! I’ll work on the CLI and the demo together.

from autoshorts.cli.demo_social_draft import main

def test_demo_social_draft_cli_prints_delivery_metadata_and_review_text(capsys):
    exit_code = main([])

    captured = capsys.readouterr()

    assert exit_code == 0
    assert "Delivery type: social_draft_review" in captured.out
    assert "Topic: review" in captured.out
    assert "Requires human approval: yes" in captured.out
    assert "Side effects: none" in captured.out
    assert "Text length:" in captured.out
    assert "## Social Media Draft Review" in captured.out
    assert "Candidate: ai_life_systems-ai-weekly-review-routine-35s" in captured.out
    assert "Platforms: YouTube Shorts, TikTok, Instagram Reels" in captured.out
    assert "Freigabe alle" in captured.out
    assert captured.err == ""
    assert "publish" not in captured.out.casefold()
    assert "post" not in captured.out.casefold()


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_demo_social_draft_cli.py">
ERROR [1:6] Import "autoshorts.cli.demo_social_draft" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


python -m pytest tests/test_demo_social_draft_cli.py -q


Output:
==================================== ERRORS ====================================
_____________ ERROR collecting tests/test_demo_social_draft_cli.py _____________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_demo_social_draft_cli.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_demo_social_draft_cli.py:1: in <module>
    from autoshorts.cli.demo_social_draft import main
E   ModuleNotFoundError: No module named 'autoshorts.cli.demo_social_draft'
=========================== short test summary info ============================
ERROR tests/test_demo_social_draft_cli.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.11s


"""Print the deterministic social media draft review delivery."""

from __future__ import annotations

import sys
from collections.abc import Sequence
from typing import TextIO

from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.content.to_candidate import convert_script_draft_to_candidate
from autoshorts.rendering.manifest import build_render_manifest
from autoshorts.rendering.preview import build_html_preview
from autoshorts.rendering.shot_plan import create_shot_plan
from autoshorts.social.delivery import prepare_social_draft_delivery
from autoshorts.social.draft import build_social_media_draft
from autoshorts.social.review import render_social_media_draft_review

def _build_demo_delivery():
    brief = create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    script = create_script_draft(brief)
    candidate = convert_script_draft_to_candidate(brief, script)
    preview = build_html_preview(build_render_manifest(create_shot_plan(script)))
    draft = build_social_media_draft(candidate, preview)
    review_text = render_social_media_draft_review(draft)
    return prepare_social_draft_delivery(review_text, topic="review")

def main(argv: Sequence[str] | None = None, *, stdout: TextIO | None = None) -> int:
    """Print social draft delivery metadata and review text without sending anything."""

    _ = argv or ()
    output = stdout or sys.stdout
    delivery = _build_demo_delivery()
    side_effects = ", ".join(delivery.side_effects) if delivery.side_effects else "none"
    approval = "yes" if delivery.requires_human_approval else "no"

    print(f"Delivery type: {delivery.delivery_type}", file=output)
    print(f"Topic: {delivery.topic}", file=output)
    print(f"Requires human approval: {approval}", file=output)
    print(f"Side effects: {side_effects}", file=output)
    print(f"Text length: {len(delivery.text)}", file=output)
    print("", file=output)
    print(delivery.text, file=output)
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


python -m pytest tests/test_demo_social_draft_cli.py -q && python -m pytest tests -q && python -m autoshorts.cli.demo_social_draft >/tmp/autoshorts-social-draft.txt && python - <<'PY'
from pathlib import Path
text = Path('/tmp/autoshorts-social-draft.txt').read_text()
print(len(text))
for line in text.splitlines()[:8]:
    print(line)
print('Freigabe alle' in text)
print('publish' in text.casefold(), 'post' in text.casefold())
PY


Output:
.                                                                        [100%]
1 passed in 0.05s
........................................................................ [ 78%]
....................                                                     [100%]
92 passed in 0.16s
892
Delivery type: social_draft_review
Topic: review
Requires human approval: yes
Side effects: none
Text length: 776

## Social Media Draft Review

True
False False


patch

- `autoshorts.cli.demo_render_preview` — local CLI entry point that prints side-effect-free render-preview metadata plus HTML.
- `autoshorts.cli.demo_review_batch` — local CLI entry point that prints the deterministic review demo.
- `autoshorts.cli.demo_social_draft` — local CLI entry point that prints a side-effect-free social media draft review delivery.
- `autoshorts.cli.demo_telegram_delivery` — local CLI entry point that prints side-effect-free Telegram delivery metadata plus review text.

- `autoshorts.cli.demo_render_preview` — local CLI entry point that prints side-effect-free render-preview metadata plus HTML.
- `autoshorts.cli.demo_review_batch` — local CLI entry point that prints the deterministic review demo.
- `autoshorts.cli.demo_telegram_delivery` — local CLI entry point that prints side-effect-free Telegram delivery metadata plus review text.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -10,6 +10,7 @@

 - `autoshorts.cli.demo_render_preview` — local CLI entry point that prints side-effect-free render-preview metadata plus HTML.
 - `autoshorts.cli.demo_review_batch` — local CLI entry point that prints the deterministic review demo.
+- `autoshorts.cli.demo_social_draft` — local CLI entry point that prints a side-effect-free social media draft review delivery.
 - `autoshorts.cli.demo_telegram_delivery` — local CLI entry point that prints side-effect-free Telegram delivery metadata plus review text.
 - `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
 - `autoshorts.content.brief` — strategy brief model for retention-led content development before scripting.

No linter for .md files


`autoshorts.social.delivery.prepare_social_draft_delivery()` wraps social draft review text in a side-effect-free `SocialDraftDelivery` object with delivery type `social_draft_review`, topic metadata, `requires_human_approval=True`, and `side_effects=()`. It validates non-empty text, Telegram's 4096-character limit, and the same no publish/post language boundary.

Preview the complete social draft delivery locally with:

```bash
python -m autoshorts.cli.demo_social_draft
```

It prints delivery metadata first, then the social draft review text. It still sends nothing and does not call platform APIs.

## Human approval commands

`autoshorts.social.delivery.prepare_social_draft_delivery()` wraps social draft review text in a side-effect-free `SocialDraftDelivery` object with delivery type `social_draft_review`, topic metadata, `requires_human_approval=True`, and `side_effects=()`. It validates non-empty text, Telegram's 4096-character limit, and the same no publish/post language boundary.

## Human approval commands


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -139,6 +139,14 @@

 `autoshorts.social.delivery.prepare_social_draft_delivery()` wraps social draft review text in a side-effect-free `SocialDraftDelivery` object with delivery type `social_draft_review`, topic metadata, `requires_human_approval=True`, and `side_effects=()`. It validates non-empty text, Telegram's 4096-character limit, and the same no publish/post language boundary.

+Preview the complete social draft delivery locally with:
+
+```bash
+python -m autoshorts.cli.demo_social_draft
+```
+
+It prints delivery metadata first, then the social draft review text. It still sends nothing and does not call platform APIs.
+
 ## Human approval commands

 Supported review responses:

No linter for .md files


terminal

python -m pytest tests -q && git status --short && git add README.md autoshorts/cli/demo_social_draft.py tests/test_demo_social_draft_cli.py && git commit -m "feat: add demo social draft cli" && git log --oneline -n 10 && git status --short


Output:
........................................................................ [ 78%]
....................                                                     [100%]
92 passed in 0.14s
 M README.md
?? autoshorts/cli/demo_social_draft.py
?? tests/test_demo_social_draft_cli.py
[feat/core-approval-quality-gates 3b387f4] feat: add demo social draft cli
 3 files changed, 85 insertions(+)
 create mode 100644 autoshorts/cli/demo_social_draft.py
 create mode 100644 tests/test_demo_social_draft_cli.py
3b387f4 feat: add demo social draft cli
515c549 feat: add social draft delivery preparation
a7d10fa feat: render social media draft reviews
1f6aa2c feat: add social media draft model
f8c5588 feat: add demo render preview cli
42ac32f feat: add side-effect-free render preview
e258635 feat: add render manifest planning
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates


Erledigt, Sir.

Wir haben den nächsten Block vollständig durchgezogen:

```text
SocialMediaDraft
→ Social Draft Review Text
→ Social Draft Delivery Preparation
→ Demo CLI
```

Alles weiterhin ohne Upload, ohne Telegram-Call, ohne Renderer-Call, ohne Credentials. Also ausnahmsweise eine Social-Media-Pipeline ohne eingebauten Selbstzerstörungsknopf.

## 1. SocialMediaDraft Model

Neue Dateien:

```text
autoshorts/social/__init__.py
autoshorts/social/draft.py
tests/test_social_draft.py
```

Neue API:

```python
build_social_media_draft(candidate, preview) -> SocialMediaDraft
validate_social_media_draft(draft) -> ValidationResult
```

Der Draft enthält:

- Candidate ID
- Plattformen:
  - YouTube Shorts
  - TikTok
  - Instagram Reels
- Titelvarianten
- Caption
- Hashtags
- CTA
- Hook
- geplante Dauer
- Quality/Risk Summary
- Preview Reference
- Preview Type
- `requires_human_approval=True`
- `side_effects=()`

Verifiziert:

```text
ai_life_systems-ai-weekly-review-routine-35s
('YouTube Shorts', 'TikTok', 'Instagram Reels')
HTML timeline preview available
True ()
True
```

Commit:

```text
1f6aa2c feat: add social media draft model
```

## 2. Social Draft Review Renderer

Neue Dateien:

```text
autoshorts/social/review.py
tests/test_social_review.py
```

Neue API:

```python
render_social_media_draft_review(draft: SocialMediaDraft) -> str
```

Erzeugt Telegram-safe Review Text:

```text
## Social Media Draft Review

Candidate: ai_life_systems-ai-weekly-review-routine-35s
Platforms: YouTube Shorts, TikTok, Instagram Reels
Duration: 35s

Hook:
...

Title variants:
...

Caption:
...

Hashtags:
...

Approval commands:
- Freigabe YouTube
- Freigabe TikTok
- Freigabe Instagram
- Freigabe alle
- Ablehnen
- Ändern: <requested change>
```

Guardrails:

- validiert Draft vorher
- max. Telegram-sicher im aktuellen Output
- keine `publish`-/`post`-Sprache
- Approval bleibt explizit getrennt

Verifiziert:

```text
## Social Media Draft Review
776
True
False False
```

Bedeutung:

- Text beginnt korrekt
- Länge `776`
- enthält `Freigabe alle`
- enthält kein `publish`
- enthält kein `post`

Commit:

```text
a7d10fa feat: render social media draft reviews
```

## 3. Social Draft Delivery Preparation

Neue Dateien:

```text
autoshorts/social/delivery.py
tests/test_social_delivery.py
```

Neue API:

```python
prepare_social_draft_delivery(review_text, topic="review") -> SocialDraftDelivery
```

Delivery Objekt:

```python
SocialDraftDelivery(
    delivery_type="social_draft_review",
    topic="review",
    requires_human_approval=True,
    side_effects=(),
)
```

Validiert:

- Text nicht leer
- Telegram Limit `4096`
- keine `publish`-/`post`-Sprache
- keine Side Effects

Commit:

```text
515c549 feat: add social draft delivery preparation
```

## 4. Demo CLI für ersten Social-Media-Freigabeentwurf

Neue Dateien:

```text
autoshorts/cli/demo_social_draft.py
tests/test_demo_social_draft_cli.py
```

Neuer Command:

```bash
python -m autoshorts.cli.demo_social_draft
```

Der läuft deterministisch durch:

```text
ContentBrief
→ ScriptDraft
→ VideoCandidate
→ ShotPlan
→ RenderManifest
→ HTML Preview
→ SocialMediaDraft
→ Social Draft Review Text
→ SocialDraftDelivery
→ stdout
```

Output beginnt:

```text
Delivery type: social_draft_review
Topic: review
Requires human approval: yes
Side effects: none
Text length: 776

## Social Media Draft Review
```

Manuell verifiziert:

```text
892
Delivery type: social_draft_review
Topic: review
Requires human approval: yes
Side effects: none
Text length: 776

## Social Media Draft Review

True
False False
```

Bedeutung:

- Gesamtoutput Länge `892`
- Delivery Metadata korrekt
- Review Text enthalten
- `Freigabe alle` enthalten
- kein `publish`
- kein `post`

Commit:

```text
3b387f4 feat: add demo social draft cli
```

## Tests

Finaler Stand:

```text
92 passed in 0.14s
```

## Aktueller Commit-Verlauf

```text
3b387f4 feat: add demo social draft cli
515c549 feat: add social draft delivery preparation
a7d10fa feat: render social media draft reviews
1f6aa2c feat: add social media draft model
f8c5588 feat: add demo render preview cli
42ac32f feat: add side-effect-free render preview
e258635 feat: add render manifest planning
d118a76 feat: add retention shot planning
6e6569a feat: add content demo review pipeline
72dc887 feat: convert content drafts to video candidates
```

Arbeitsverzeichnis ist sauber.

## Wo wir jetzt stehen

Die kontrollierte Approval-Pipeline ist jetzt:

```text
ContentBrief
→ ScriptDraft
→ VideoCandidate
→ ReviewBatch
→ Telegram ReviewDelivery
→ ShotPlan
→ RenderManifest
→ HTML Preview
→ SocialMediaDraft
→ Social Draft Review
→ SocialDraftDelivery
→ Demo CLI
```

## Nächster sinnvoller Schritt

Jetzt erst würde ich Richtung erstem visuellen Video-Artefakt gehen:

```text
RenderManifest
→ Local MP4 Timing Preview
```

Vorschlag:

```text
autoshorts/rendering/local_preview.py
tests/test_local_preview.py
```

Ziel:

- kein AI-Video
- kein ComfyUI
- kein Wan
- kein Qwen-Image
- lokal deterministische Preview
- aus Manifest:
  - simple vertical scene cards
  - captions renderer-owned
  - scene timings
  - placeholder visuals
  - optional FFmpeg MP4 Assembly später

Erster Schritt wäre wahrscheinlich noch side-effect-free:

```text
RenderManifest
→ LocalPreviewPlan
```

Danach:

```text
LocalPreviewPlan
→ actual files in temp/output dir
```

So trennen wir Planung von Dateischreiben. Langweilig, aber genau die Sorte langweilig, die verhindert, dass ein Bot “mal eben” 400 MB Clip-Müll in Git ablegt.


Explanation:
**Determining necessary actions**

I need to figure out what actions to take here. First, I’m wondering if I should load skills that are already relevant. Before replying, it’s crucial to scan through what’s available. I might consider integrating TDD or social features, or perhaps looking into the manifest-preview-renderer reference. It seems like I should utilize the tools I have on hand to get everything right.


test-driven-development

---
name: test-driven-development
description: "TDD: enforce RED-GREEN-REFACTOR, tests before code."
version: 1.1.0
author: Hermes Agent (adapted from obra/superpowers)
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [testing, tdd, development, quality, red-green-refactor]
    related_skills: [systematic-debugging, writing-plans, subagent-driven-development]
---

# Test-Driven Development (TDD)

## Overview

Write the test first. Watch it fail. Write minimal code to pass.

**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.

**Violating the letter of the rules is violating the spirit of the rules.**

## When to Use

**Always:**
- New features
- Bug fixes
- Refactoring
- Behavior changes

**Exceptions (ask the user first):**
- Throwaway prototypes
- Generated code
- Configuration files

Thinking "skip TDD just this once"? Stop. That's rationalization.

## The Iron Law

```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```

Write code before the test? Delete it. Start over.

**No exceptions:**
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete

Implement fresh from tests. Period.

## Red-Green-Refactor Cycle

### RED — Write Failing Test

Write one minimal test showing what should happen.

**Good test:**
```python
def test_retries_failed_operations_3_times():
    attempts = 0
    def operation():
        nonlocal attempts
        attempts += 1
        if attempts < 3:
            raise Exception('fail')
        return 'success'

    result = retry_operation(operation)

    assert result == 'success'
    assert attempts == 3
```
Clear name, tests real behavior, one thing.

**Bad test:**
```python
def test_retry_works():
    mock = MagicMock()
    mock.side_effect = [Exception(), Exception(), 'success']
    result = retry_operation(mock)
    assert result == 'success'  # What about retry count? Timing?
```
Vague name, tests mock not real code.

**Requirements:**
- One behavior per test
- Clear descriptive name ("and" in name? Split it)
- Real code, not mocks (unless truly unavoidable)
- Name describes behavior, not implementation

### Verify RED — Watch It Fail

**MANDATORY. Never skip.**

```bash
# Use terminal tool to run the specific test
pytest tests/test_feature.py::test_specific_behavior -v
```

Confirm:
- Test fails (not errors from typos)
- Failure message is expected
- Fails because the feature is missing

**Test passes immediately?** You're testing existing behavior. Fix the test.

**Test errors?** Fix the error, re-run until it fails correctly.

### GREEN — Minimal Code

Write the simplest code to pass the test. Nothing more.

**Good:**
```python
def add(a, b):
    return a + b  # Nothing extra
```

**Bad:**
```python
def add(a, b):
    result = a + b
    logging.info(f"Adding {a} + {b} = {result}")  # Extra!
    return result
```

Don't add features, refactor other code, or "improve" beyond the test.

**Cheating is OK in GREEN:**
- Hardcode return values
- Copy-paste
- Duplicate code
- Skip edge cases

We'll fix it in REFACTOR.

### Verify GREEN — Watch It Pass

**MANDATORY.**

```bash
# Run the specific test
pytest tests/test_feature.py::test_specific_behavior -v

# Then run ALL tests to check for regressions
pytest tests/ -q
```

Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)

**Test fails?** Fix the code, not the test.

**Other tests fail?** Fix regressions now.

### REFACTOR — Clean Up

After green only:
- Remove duplication
- Improve names
- Extract helpers
- Simplify expressions

Keep tests green throughout. Don't add behavior.

**If tests fail during refactor:** Undo immediately. Take smaller steps.

### Repeat

Next failing test for next behavior. One cycle at a time.

## Why Order Matters

**"I'll write tests after to verify it works"**

Tests written after code pass immediately. Passing immediately proves nothing:
- Might test the wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug

Test-first forces you to see the test fail, proving it actually tests something.

**"I already manually tested all the edge cases"**

Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive

Automated tests are systematic. They run the same way every time.

**"Deleting X hours of work is wasteful"**

Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (high confidence)
- Keep it and add tests after (low confidence, likely bugs)

The "waste" is keeping code you can't trust.

**"TDD is dogmatic, being pragmatic means adapting"**

TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)

"Pragmatic" shortcuts = debugging in production = slower.

**"Tests after achieve the same goals — it's spirit not ritual"**

No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"

Tests-after are biased by your implementation. You test what you built, not what's required. Tests-first force edge case discovery before implementing.

## Common Rationalizations

| Excuse | Reality |
|--------|---------|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to the test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for the code you touch. |

## Red Flags — STOP and Start Over

If you catch yourself doing any of these, delete the code and restart with TDD:

- Code before test
- Test after implementation
- Test passes immediately on first run
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."

**All of these mean: Delete code. Start over with TDD.**

## Verification Checklist

Before marking work complete:

- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Each test failed for expected reason (feature missing, not typo)
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Output pristine (no errors, warnings)
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered

Can't check all boxes? You skipped TDD. Start over.

## When Stuck

| Problem | Solution |
|---------|----------|
| Don't know how to test | Write the wished-for API. Write the assertion first. Ask the user. |
| Test too complicated | Design too complicated. Simplify the interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify the design. |

## Hermes Agent Integration

### Running Tests

Use the `terminal` tool to run tests at each step:

```python
# RED — verify failure
terminal("pytest tests/test_feature.py::test_name -v")

# GREEN — verify pass
terminal("pytest tests/test_feature.py::test_name -v")

# Full suite — verify no regressions
terminal("pytest tests/ -q")
```

### CLI Entry Points

For new `python -m package.cli.command` style entry points, keep the TDD loop boring and side-effect free:

1. Write a test that imports `main` from the missing CLI module and calls `main([])`.
2. Capture stdout/stderr with the project test framework (`capsys` in pytest) or inject a `stdout` file-like object if supported.
3. RED should usually be an import failure for the missing CLI module/package, not a vague end-to-end shell failure.
4. Implement `main(argv: Sequence[str] | None = None, *, stdout: TextIO | None = None) -> int` so tests can call it without spawning a subprocess.
5. Add the `if __name__ == "__main__": raise SystemExit(main())` guard only after the callable path is green.
6. Verify both the unit test and the real module invocation, e.g. `python -m package.cli.command`, before committing.

This catches command wiring while avoiding accidental network calls, publishing actions, or other theatrical nonsense masquerading as a CLI test.

### With delegate_task

When dispatching subagents for implementation, enforce TDD in the goal:

```python
delegate_task(
    goal="Implement [feature] using strict TDD",
    context="""
    Follow test-driven-development skill:
    1. Write failing test FIRST
    2. Run test to verify it fails
    3. Write minimal code to pass
    4. Run test to verify it passes
    5. Refactor if needed
    6. Commit

    Project test command: pytest tests/ -q
    Project structure: [describe relevant files]
    """,
    toolsets=['terminal', 'file']
)
```

### With systematic-debugging

Bug found? Write failing test reproducing it. Follow TDD cycle. The test proves the fix and prevents regression.

Never fix bugs without a test.

## Demo / Fixture Features

When adding a deterministic demo fixture for later integration wiring, still use RED-GREEN-REFACTOR. Write the wished-for import/API in the test first, verify the missing module/API failure, then implement the smallest fixture that proves the real workflow path. Assert stable IDs/order, bucket counts/classifications, and safety invariants such as review text not implying publication. See `references/deterministic-demo-fixtures.md` for the concise pattern.

## Side-Effect-Free Integration Boundaries

When building toward an external integration (Telegram, email, publishing APIs, render farms, payment flows, etc.), add a tested preparation boundary before any real I/O:

1. Write the wished-for pure function in the test first, e.g. `prepare_review_delivery(text) -> DeliveryPlan`.
2. RED should be a missing module/API or missing validation failure, not a real network/send failure.
3. The GREEN implementation should return a typed/dataclass plan object with explicit metadata: destination/topic, delivery type, human-approval requirement, and `side_effects=()`.
4. Validate safety constraints at the boundary: non-empty content, platform size limits, no forbidden action language, no implicit approval, no credentials or generated media unless explicitly expected.
5. Add a demo CLI only after the pure boundary is green. The CLI should print metadata first, then payload, and still perform no external I/O.
6. Verify both the unit tests and a real `python -m package.cli.demo_*` invocation before committing.

This pattern lets later sending/rendering/publishing adapters consume a boring, audited object instead of smuggling side effects into formatting code. Boring is how we avoid building a content catapult with a moustache.

### Media / Rendering Pipeline Boundaries

When building toward image/video generation or renderer integration, add deterministic planning artifacts before any GPU/API work:

1. Write tests for the wished-for pure API first, e.g. `create_shot_plan(script)`, `build_render_manifest(plan)`, or `prepare_preview(manifest)`.
2. RED should be a missing module/API or missing validation behavior — never a failed ComfyUI/Wan/Qwen/API invocation.
3. Keep each stage inspectable and side-effect-free until the explicit adapter layer: `ScriptDraft -> ShotPlan -> RenderManifest -> PreviewPlan/PreviewArtifact -> renderer adapter`.
4. Assert stable IDs/order, scene counts, timing continuity, vertical settings (`1080x1920`, `30fps` when building Shorts/Reels/TikTok), and placeholder paths before generating media.
5. Add guardrail tests that reject static slideshows, baked-in/generated text inside assets, non-renderer-owned captions/labels, unsafe render settings, missing fields, and bad timings.
6. Only after the manifest/preview boundary is green should a later adapter call local GPU tools or external services. Planning first; GPU roulette later.

This prevents wasting render time on unclear direction and keeps expensive media generation behind a small, audited boundary.

### Approval-First Media/Social Pipelines

When a feature crosses from content planning toward media/social delivery, prefer many small RED-GREEN-README-COMMIT slices instead of one large integration jump:

1. typed artifact/model (`SocialMediaDraft`, `RenderManifest`, etc.)
2. renderer/formatter for human review text
3. side-effect-free delivery/preparation boundary
4. demo CLI that prints metadata first, then payload

For each slice, write the missing import/API test first, verify the expected import/API failure, implement only that slice, run the focused test, run the full suite, update docs, and commit. Keep invariant tests explicit: `requires_human_approval=True`, `side_effects=()`, no external calls, no credential access, no file writes unless that adapter is the explicit feature, and no accidental action language such as `publish`/`post` in review/delivery text.

This staged approach keeps approval semantics auditable and prevents a later renderer, Telegram sender, or platform adapter from being smuggled into formatting code.

## Testing Anti-Patterns

- **Testing mock behavior instead of real behavior** — mocks should verify interactions, not replace the system under test
- **Testing implementation details** — test behavior/results, not internal method calls
- **Happy path only** — always test edge cases, errors, and boundaries
- **Brittle tests** — tests should verify behavior, not structure; refactoring shouldn't break them
- **Substring sentinels that collide with generated IDs** — when asserting that amounts or fixture values are excluded from a result, assert the full formatted token (e.g. `999.00`) or a typed field, not a short substring like `999` that can appear inside UUIDs/hash-like IDs.
- **Domain classes named `Test*` trigger pytest collection warnings** — if a production dataclass/model must be named like `TestBatch`, set `__test__ = False` on the class or choose a non-`Test` prefix. Keep the fix in production code when the name is part of the domain language, and verify the warning is gone before committing.
- **Defaulting with `x or default` hides invalid explicit inputs** — when a factory/test helper accepts optional overrides, use `x if x is not None else default` so tests can pass intentionally blank strings (`""`) or zero values and watch validation reject them. Otherwise invalid-input tests may silently receive defaults and give false confidence.

## Final Rule

```
Production code → test exists and failed first
Otherwise → not TDD
```

No exceptions without the user's explicit permission.

To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'


social-media-shorts-automation

# Manifest-driven preview renderer for Shorts

Use this when a shorts automation project already has render manifests and needs the first video output without slipping into generic AI slideshow rendering.

## Durable pattern

Keep the renderer deterministic and manifest-driven:

```text
Render manifest JSON -> scene/card frames -> FFmpeg MP4 -> preview/approval
```

The LLM should not render frames. It may help create scripts, shot plans, and manifests, but the renderer should consume structured timeline fields and produce reproducible preview media.

## Renderer v1 architecture

A pragmatic first renderer can use:

- Pillow/PIL to draw vertical scene frames/cards.
- FFmpeg concat demuxer to turn frame images into an MP4.
- Render manifest fields for timing, text, asset type, visual direction, retention goal, resolution, aspect ratio, and fps.

Recommended module shape:

- `build_preview_frames(manifest, width, height)`
  - one representative image per visual timeline item
  - uses the manifest text, asset type, timing, and retention goal
- `write_frame_images(frames, output_dir)`
  - writes numbered PNGs for inspection/debugging
- `render_preview_video(manifest, output_path, width, height, fps)`
  - renders frames to temporary PNGs
  - writes an FFmpeg concat file with per-scene durations
  - exports `yuv420p` MP4 with `+faststart`

This is a preview/timing renderer, not the final beauty renderer. It validates scene flow, text weight, timing, and approval review before spending time on polished assets.

## TDD checks worth preserving

Write tests before implementation that assert:

- preview frames equal the number of visual scene items
- first frame starts at `0ms`
- image dimensions match requested vertical preview size
- first frame text matches the first visual timeline item
- PNG frame files are written and valid
- MP4 output exists and is non-empty
- `ffprobe` verifies width, height, frame rate, and duration for real render smoke tests

## CLI pattern

Provide a small script such as `scripts/render_preview.py`:

```bash
python3 scripts/render_preview.py \
  data/render_manifests/<batch>/<video>.json \
  --out-dir data/previews/<batch> \
  --width 540 \
  --height 960 \
  --fps 24
```

Use reduced preview resolution like `540x960` for fast local review. Keep final `1080x1920` as a later renderer-quality target.

## Repo hygiene

- Keep generated MP4/PNG media out of Git via `.gitignore`.
- Commit renderer code, tests, scripts, manifests, and lightweight planning artifacts.
- Do not commit rendered previews unless explicitly needed as small fixtures.
- Add runtime dependencies explicitly, e.g. `Pillow>=10.0`; treat FFmpeg as an external system prerequisite checked at runtime.

## Pitfalls

- Do not let the renderer infer timing or visuals from prose when the manifest already encodes them.
- Do not start with AI video generation as the renderer; use it later only as an asset supplier.
- Do not over-polish the first renderer. Its job is approval/timing validation.
- Do not publish from this step. Preview rendering still requires human approval before posting.
- Watch typography consistency in early previews: fixed-size fonts can make some scenes look huge and others tiny. Add tests/metadata for fitted font sizes, max line counts, and minimum retention-goal readability before scaling to more videos.


terminal

git status --short && git branch --show-current


Output: feat/core-approval-quality-gates


Explanation:
**Testing local preview plan**

I need to start with tests for the local_preview.py plan. Maybe I should use frame specifications to guide this. It's essential to gather the relevant manifest data as part of the process. I'm also considering implementing an approach that ensures side effects are kept to a minimum. I want to ensure that everything functions smoothly without unwanted changes. Let's lay the groundwork properly and get this sorted out!

import pytest

from autoshorts.content.brief import create_content_brief
from autoshorts.content.script_draft import create_script_draft
from autoshorts.rendering.local_preview import (
    LocalPreviewPlan,
    build_local_preview_plan,
    validate_local_preview_plan,
)
from autoshorts.rendering.manifest import RenderManifest, RenderSettings, build_render_manifest
from autoshorts.rendering.shot_plan import create_shot_plan

def _manifest():
    brief = create_content_brief(
        pillar_id="ai_life_systems",
        topic="AI weekly review routine",
        audience_problem="I use AI tools but still lose control of priorities.",
        payoff="a 10-minute Sunday review that keeps AI as assistant, not boss",
    )
    script = create_script_draft(brief)
    return build_render_manifest(create_shot_plan(script))

def test_build_local_preview_plan_creates_side_effect_free_frame_plan():
    manifest = _manifest()

    plan = build_local_preview_plan(manifest)

    assert isinstance(plan, LocalPreviewPlan)
    assert plan.script_id == "ai_life_systems-ai-weekly-review-routine"
    assert plan.title == "AI weekly review routine"
    assert plan.width == 540
    assert plan.height == 960
    assert plan.fps == 24
    assert plan.output_dir == "data/previews/ai_life_systems-ai-weekly-review-routine"
    assert plan.side_effects == ()
    assert plan.external_calls == ()
    assert len(plan.frames) == len(manifest.timeline)
    assert plan.frames[0].frame_id == "frame-01-first_frame_hook"
    assert plan.frames[0].start_ms == 0
    assert plan.frames[0].duration_ms == 3000
    assert plan.frames[0].scene_purpose == "first_frame_hook"
    assert plan.frames[0].caption_text == "If AI runs your week, you have already lost control."
    assert plan.frames[0].asset_mode == "kinetic_text_overlay"
    assert plan.frames[0].placeholder_image_path == (
        "data/previews/ai_life_systems-ai-weekly-review-routine/frame-01-first_frame_hook.png"
    )
    assert plan.frames[-1].start_ms == 30000
    assert plan.frames[-1].duration_ms == 5000

def test_validate_local_preview_plan_accepts_default_plan():
    result = validate_local_preview_plan(build_local_preview_plan(_manifest()))

    assert result.is_valid is True
    assert result.errors == ()

def test_build_local_preview_plan_rejects_invalid_manifest():
    manifest = _manifest()
    invalid = RenderManifest(
        script_id=manifest.script_id,
        title=manifest.title,
        settings=RenderSettings(width=1920, height=1080),
        timeline=manifest.timeline,
    )

    with pytest.raises(ValueError, match="render size must be vertical 1080x1920"):
        build_local_preview_plan(invalid)

def test_validate_local_preview_plan_rejects_side_effects_external_calls_and_bad_size():
    plan = build_local_preview_plan(_manifest())
    unsafe = LocalPreviewPlan(
        script_id=plan.script_id,
        title=plan.title,
        width=1920,
        height=1080,
        fps=0,
        output_dir=plan.output_dir,
        frames=plan.frames,
        side_effects=("write_png",),
        external_calls=("ffmpeg",),
    )

    result = validate_local_preview_plan(unsafe)

    assert result.is_valid is False
    assert "preview size must be vertical" in result.errors
    assert "fps must be positive" in result.errors
    assert "preview plan must not have side effects" in result.errors
    assert "preview plan must not call external tools" in result.errors

def test_validate_local_preview_plan_rejects_bad_frame_timing_and_missing_fields():
    plan = build_local_preview_plan(_manifest())
    first = plan.frames[0]
    bad_frame = type(first)(
        frame_id="",
        scene_purpose="",
        start_ms=1000,
        duration_ms=0,
        caption_text="",
        label_text="",
        asset_mode="",
        motion_instruction="",
        placeholder_image_path="",
    )
    unsafe = LocalPreviewPlan(
        script_id=plan.script_id,
        title=plan.title,
        width=plan.width,
        height=plan.height,
        fps=plan.fps,
        output_dir=plan.output_dir,
        frames=(bad_frame,) + plan.frames[1:],
        side_effects=(),
        external_calls=(),
    )

    result = validate_local_preview_plan(unsafe)

    assert result.is_valid is False
    assert "frame 1 frame_id is required" in result.errors
    assert "frame 1 duration_ms must be positive" in result.errors
    assert "first frame must start at 0ms" in result.errors
    assert "frame 1 caption_text is required" in result.errors
    assert "frame 1 placeholder_image_path is required" in result.errors


LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_local_preview.py">
ERROR [5:6] Import "autoshorts.rendering.local_preview" could not be resolved [reportMissingImports] (Pyright)
</diagnostics>


python -m pytest tests/test_local_preview.py -q


Output:
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_local_preview.py _________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_local_preview.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_local_preview.py:5: in <module>
    from autoshorts.rendering.local_preview import (
E   ModuleNotFoundError: No module named 'autoshorts.rendering.local_preview'
=========================== short test summary info ============================
ERROR tests/test_local_preview.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.11s


"""Side-effect-free local preview planning from render manifests."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult
from autoshorts.rendering.manifest import RenderManifest, validate_render_manifest

@dataclass(frozen=True)
class LocalPreviewFrame:
    frame_id: str
    scene_purpose: str
    start_ms: int
    duration_ms: int
    caption_text: str
    label_text: str
    asset_mode: str
    motion_instruction: str
    placeholder_image_path: str

@dataclass(frozen=True)
class LocalPreviewPlan:
    script_id: str
    title: str
    width: int
    height: int
    fps: int
    output_dir: str
    frames: tuple[LocalPreviewFrame, ...]
    side_effects: tuple[str, ...] = ()
    external_calls: tuple[str, ...] = ()

def _is_blank(value: str) -> bool:
    return not (value or "").strip()

def _frame_id(index: int, purpose: str) -> str:
    return f"frame-{index:02d}-{purpose}"

def build_local_preview_plan(
    manifest: RenderManifest,
    *,
    width: int = 540,
    height: int = 960,
    fps: int = 24,
) -> LocalPreviewPlan:
    """Build a deterministic frame plan without writing images or calling FFmpeg."""

    manifest_validation = validate_render_manifest(manifest)
    if not manifest_validation.is_valid:
        raise ValueError("; ".join(manifest_validation.errors))

    output_dir = f"data/previews/{manifest.script_id}"
    frames = tuple(
        LocalPreviewFrame(
            frame_id=_frame_id(index, item.scene_purpose),
            scene_purpose=item.scene_purpose,
            start_ms=item.start_seconds * 1000,
            duration_ms=(item.end_seconds - item.start_seconds) * 1000,
            caption_text=item.caption_layer.text,
            label_text=item.label_layer.text,
            asset_mode=item.asset_mode,
            motion_instruction=item.motion_instruction,
            placeholder_image_path=f"{output_dir}/{_frame_id(index, item.scene_purpose)}.png",
        )
        for index, item in enumerate(manifest.timeline, start=1)
    )
    plan = LocalPreviewPlan(
        script_id=manifest.script_id,
        title=manifest.title,
        width=width,
        height=height,
        fps=fps,
        output_dir=output_dir,
        frames=frames,
        side_effects=(),
        external_calls=(),
    )
    validation = validate_local_preview_plan(plan)
    if not validation.is_valid:
        raise ValueError("; ".join(validation.errors))
    return plan

def validate_local_preview_plan(plan: LocalPreviewPlan) -> ValidationResult:
    errors: list[str] = []
    if _is_blank(plan.script_id):
        errors.append("script_id is required")
    if _is_blank(plan.title):
        errors.append("title is required")
    if plan.width <= 0 or plan.height <= 0 or plan.height <= plan.width:
        errors.append("preview size must be vertical")
    if plan.fps <= 0:
        errors.append("fps must be positive")
    if _is_blank(plan.output_dir):
        errors.append("output_dir is required")
    if plan.side_effects:
        errors.append("preview plan must not have side effects")
    if plan.external_calls:
        errors.append("preview plan must not call external tools")
    if not plan.frames:
        errors.append("at least one preview frame is required")

    previous_end: int | None = None
    for index, frame in enumerate(plan.frames, start=1):
        for field_name, value in {
            "frame_id": frame.frame_id,
            "scene_purpose": frame.scene_purpose,
            "caption_text": frame.caption_text,
            "label_text": frame.label_text,
            "asset_mode": frame.asset_mode,
            "motion_instruction": frame.motion_instruction,
            "placeholder_image_path": frame.placeholder_image_path,
        }.items():
            if _is_blank(value):
                errors.append(f"frame {index} {field_name} is required")
        if frame.duration_ms <= 0:
            errors.append(f"frame {index} duration_ms must be positive")
        if index == 1 and frame.start_ms != 0:
            errors.append("first frame must start at 0ms")
        if previous_end is not None and frame.start_ms < previous_end:
            errors.append(f"frame {index} start_ms overlaps previous frame")
        previous_end = frame.start_ms + frame.duration_ms

    return ValidationResult(is_valid=not errors, errors=tuple(errors))


python -m pytest tests/test_local_preview.py -q && python -m pytest tests -q


Output:
.....                                                                    [100%]
5 passed in 0.05s
........................................................................ [ 74%]
.........................                                                [100%]
97 passed in 0.14s


patch

- `autoshorts.rendering.local_preview` — side-effect-free local preview frame planning from render manifests.
- `autoshorts.rendering.manifest` — side-effect-free vertical render manifest generation from shot plans.
- `autoshorts.rendering.preview` — side-effect-free HTML timeline previews for render manifests.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -22,6 +22,7 @@
 - `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
 - `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.
+- `autoshorts.rendering.local_preview` — side-effect-free local preview frame planning from render manifests.
 - `autoshorts.rendering.manifest` — side-effect-free vertical render manifest generation from shot plans.
 - `autoshorts.rendering.preview` — side-effect-free HTML timeline previews for render manifests.
 - `autoshorts.rendering.shot_plan` — retention-led shot planner before render manifests or video generation.

No linter for .md files