terminal

Output:
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_ideas.py _____________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_ideas.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_ideas.py:3: in <module>
    from autoshorts.ideas.candidate import VideoCandidate, validate_candidate
E   ModuleNotFoundError: No module named 'autoshorts.ideas'
=========================== short test summary info ============================
ERROR tests/test_ideas.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.10s


todo

Tests für VideoCandidate/TestBatch schreiben und RED verifizieren

Ideas-Module minimal implementieren


Repo-Status prüfen

Ideas-Module minimal implementieren

Alle Tests laufen lassen, README aktualisieren, committen


"""Idea and testbatch domain models."""

"""Video candidate domain model and validation."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.review.package import ReviewPackage
from autoshorts.scripting.quality import ScriptQualityScore

SUPPORTED_PLATFORMS = ("youtube", "tiktok", "instagram")

@dataclass(frozen=True)
class ValidationResult:
    is_valid: bool
    errors: tuple[str, ...] = ()

@dataclass(frozen=True)
class VideoCandidate:
    candidate_id: str
    pillar: str
    title: str
    platforms: tuple[str, ...]
    duration_seconds: int
    hook: str
    script_outline: str
    visual_concept: str
    caption: str
    cta: str
    hashtags: tuple[str, ...]
    quality_score: ScriptQualityScore
    policy_notes: tuple[str, ...]
    hypothesis: str

    def to_review_package(self, preview_path: str | None = None) -> ReviewPackage:
        return ReviewPackage(
            candidate_id=self.candidate_id,
            title=self.title,
            pillar=self.pillar,
            duration_seconds=self.duration_seconds,
            hook=self.hook,
            script_summary=self.script_outline,
            visual_concept=self.visual_concept,
            caption=self.caption,
            hashtags=self.hashtags,
            platforms=self.platforms,
            quality_score=self.quality_score,
            policy_notes=self.policy_notes,
            preview_path=preview_path,
        )

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

def validate_candidate(candidate: VideoCandidate) -> ValidationResult:
    errors: list[str] = []

    required_fields = {
        "candidate_id": candidate.candidate_id,
        "pillar": candidate.pillar,
        "title": candidate.title,
        "hook": candidate.hook,
        "script_outline": candidate.script_outline,
        "visual_concept": candidate.visual_concept,
        "caption": candidate.caption,
        "cta": candidate.cta,
        "hypothesis": candidate.hypothesis,
    }
    for field_name, value in required_fields.items():
        if _is_blank(value):
            errors.append(f"{field_name} is required")

    if not 10 <= candidate.duration_seconds <= 180:
        errors.append("duration_seconds must be between 10 and 180")

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

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


"""Testbatch model and deterministic filtering helpers."""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult, VideoCandidate, validate_candidate
from autoshorts.scripting.quality import should_render

@dataclass(frozen=True)
class TestBatch:
    batch_id: str
    candidates: tuple[VideoCandidate, ...]

def validate_testbatch(batch: TestBatch) -> ValidationResult:
    errors: list[str] = []

    if not (batch.batch_id or "").strip():
        errors.append("batch_id is required")
    if not batch.candidates:
        errors.append("at least one candidate is required")

    ids = [candidate.candidate_id for candidate in batch.candidates]
    if len(ids) != len(set(ids)):
        errors.append("candidate ids must be unique")

    for candidate in batch.candidates:
        result = validate_candidate(candidate)
        errors.extend(f"{candidate.candidate_id}: {error}" for error in result.errors)

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

def renderable_candidates(batch: TestBatch) -> tuple[VideoCandidate, ...]:
    """Return valid candidates that pass the quality/render gate."""

    selected: list[VideoCandidate] = []
    for candidate in batch.candidates:
        if validate_candidate(candidate).is_valid and should_render(candidate.quality_score):
            selected.append(candidate)
    return tuple(selected)


terminal

Output:
.......                                                                  [100%]
=============================== warnings summary ===============================
autoshorts/ideas/testbatch.py:11
  /home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/ideas/testbatch.py:11: PytestCollectionWarning: cannot collect test class 'TestBatch' because it has a __init__ constructor (from: tests/test_ideas.py)
    @dataclass(frozen=True)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 1 warning in 0.04s


Explanation:
**Fixing class warning**

I need to address a warning regarding the class named TestBatch. I have two options: I can either set `__test__` to False or rename the class. Since tests are importing TestBatch, adding `__test__` set to False immediately after the class definition might be the most straightforward solution. I’m thinking this aligns well with a Test-Driven Development (TDD) approach and will help keep things clean. Let’s refactor and see how it goes!

patch

@dataclass(frozen=True)
class TestBatch:
    batch_id: str
    candidates: tuple[VideoCandidate, ...]

    # Prevent pytest from trying to collect this domain model as a test class.
    __test__ = False


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/ideas/testbatch.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/ideas/testbatch.py
@@ -12,6 +12,9 @@
 class TestBatch:
     batch_id: str
     candidates: tuple[VideoCandidate, ...]
+
+    # Prevent pytest from trying to collect this domain model as a test class.
+    __test__ = False

 def validate_testbatch(batch: TestBatch) -> ValidationResult:


terminal

Output:
.......                                                                  [100%]
7 passed in 0.04s


todo

Ideas-Module minimal implementieren


Repo-Status prüfen

Ideas-Module minimal implementieren


patch

- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
- `autoshorts.scripting.quality` — conservative pre-render quality gate.
- Tests for approval parsing, review formatting, and script quality gating.

- `autoshorts.ideas.candidate` — structured video candidate model and validation.
- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
- `autoshorts.scripting.quality` — conservative pre-render quality gate.
- Tests for idea candidates, testbatches, approval parsing, review formatting, and script quality gating.


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

 Implemented foundation:

+- `autoshorts.ideas.candidate` — structured video candidate model and validation.
+- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
 - `autoshorts.scripting.quality` — conservative pre-render quality gate.
-- Tests for approval parsing, review formatting, and script quality gating.
+- Tests for idea candidates, testbatches, approval parsing, review formatting, and script quality gating.

 ## Human approval commands

No linter for .md files


A blocked quality gate is rendered as `Render gate: BLOCKED — nicht automatisch rendern`.

## Idea candidates and testbatches

`VideoCandidate` is the structured unit that moves through the early pipeline. It carries:

- candidate id, pillar, title, target platforms, duration, and hypothesis;
- hook, script outline, visual concept, caption, CTA, and hashtags;
- quality score and policy notes.

`validate_candidate()` rejects missing strategic fields, unsupported platforms, and durations outside `10..180` seconds.

`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

## Development

A blocked quality gate is rendered as `Render gate: BLOCKED — nicht automatisch rendern`.

## Development


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

 A blocked quality gate is rendered as `Render gate: BLOCKED — nicht automatisch rendern`.

+## Idea candidates and testbatches
+
+`VideoCandidate` is the structured unit that moves through the early pipeline. It carries:
+
+- candidate id, pillar, title, target platforms, duration, and hypothesis;
+- hook, script outline, visual concept, caption, CTA, and hashtags;
+- quality score and policy notes.
+
+`validate_candidate()` rejects missing strategic fields, unsupported platforms, and durations outside `10..180` seconds.
+
+`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.
+
 ## Development

 Run tests:

No linter for .md files


terminal

python -m pytest tests -q && git status --short


Output:
...................                                                      [100%]
19 passed in 0.05s
 M README.md
?? autoshorts/ideas/
?? tests/test_ideas.py


git add README.md autoshorts/ideas tests/test_ideas.py && git commit -m "feat: add idea candidates and testbatches" && git status --short --branch && git log --oneline -4


Output:
[feat/core-approval-quality-gates 49d6daf] feat: add idea candidates and testbatches
 5 files changed, 274 insertions(+), 1 deletion(-)
 create mode 100644 autoshorts/ideas/__init__.py
 create mode 100644 autoshorts/ideas/candidate.py
 create mode 100644 autoshorts/ideas/testbatch.py
 create mode 100644 tests/test_ideas.py
## feat/core-approval-quality-gates
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit


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


todo

Repo-Status prüfen

Ideas-Module minimal implementieren


Erledigt, Sir. Nächster Baustein ist drin: **Idea Candidates + Testbatches**.

## Neu gebaut

Neue Module:

```text
autoshorts/ideas/candidate.py
autoshorts/ideas/testbatch.py
```

Neue Tests:

```text
tests/test_ideas.py
```

## Was jetzt funktioniert

### 1. `VideoCandidate`

Der Bot hat jetzt eine strukturierte Einheit für einen Video-Kandidaten.

Ein Kandidat enthält:

- `candidate_id`
- `pillar`
- `title`
- `platforms`
- `duration_seconds`
- `hook`
- `script_outline`
- `visual_concept`
- `caption`
- `cta`
- `hashtags`
- `quality_score`
- `policy_notes`
- `hypothesis`

Damit ist Schluss mit losem Prompt-Gekritzel. Jeder Kandidat ist jetzt ein prüfbares Objekt.

### 2. Kandidaten-Validierung

`validate_candidate()` prüft u.a.:

- Pflichtfelder vorhanden
- Hook vorhanden
- Plattformen unterstützt
- Dauer zwischen `10` und `180` Sekunden
- mindestens eine Zielplattform

Unterstützte Plattformen aktuell:

```text
youtube
tiktok
instagram
```

Nicht unterstützte Plattformen wie `linkedin` werden sauber abgelehnt.

### 3. Umwandlung in Review Package

Ein `VideoCandidate` kann jetzt direkt in ein `ReviewPackage` umgewandelt werden:

```python
candidate.to_review_package(preview_path="data/local_motion_renders/vid-001.mp4")
```

Damit ist die Pipeline jetzt konkret:

```text
VideoCandidate
→ validate_candidate()
→ quality gate
→ ReviewPackage
→ Telegram review
→ explicit approval
```

### 4. `TestBatch`

Ein `TestBatch` gruppiert mehrere Video-Kandidaten für einen Experimentlauf.

`validate_testbatch()` prüft:

- Batch darf nicht leer sein
- Kandidaten-IDs müssen eindeutig sein
- alle Kandidaten müssen valide sein

### 5. `renderable_candidates()`

Diese Funktion filtert automatisch:

```text
nur valide Kandidaten
+
nur Kandidaten, die den Quality Gate schaffen
```

Also keine Renderzeit für schwache Ideen. Eine kleine Revolution gegen digitale Zeitverschwendung.

## TDD-Verifikation

Ich habe zuerst Tests geschrieben und RED verifiziert:

```text
ModuleNotFoundError: No module named 'autoshorts.ideas'
```

Dann minimal implementiert und grün gezogen.

## Tests

Aktueller Stand:

```text
19 passed in 0.05s
```

Keine Warnings.

## Neuer Commit

Auf Branch:

```text
feat/core-approval-quality-gates
```

neuer Commit:

```text
49d6daf feat: add idea candidates and testbatches
```

Aktueller Verlauf:

```text
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit
```

## Stand der Bot-Pipeline

Wir haben jetzt solide:

```text
Idea/TestBatch
→ Candidate Validation
→ Quality Gate
→ Review Package
→ Approval Parser
```

## Nächster sinnvoller Schritt

Jetzt würde ich als Nächstes bauen:

```text
autoshorts/config/pillars.py
```

Ziel:

- Content Pillars definieren
- AI/Life-Systems als erster Pillar
- No-Go-Regeln
- Hook Patterns
- Visual Rules
- Platform Fit
- Validierung, dass jeder Kandidat zu einem erlaubten Pillar gehört

Dann wird aus dem Bot nicht nur ein Generator, sondern ein **positionierter Content Engine**. Genau da trennt sich “mach Videos” von “baue Wachstum”.


[M G] ok, mach weiter.


social-media-shorts-growth-engine

---
name: social-media-shorts-growth-engine
description: Design and operate a growth-oriented short-form video engine for YouTube Shorts, TikTok, Instagram Reels, and adjacent channels, with human approval before posting.
version: 1.0.0
tags: [shorts, tiktok, youtube-shorts, reels, content-strategy, growth, monetization, approval-workflow]
---

# Social-Media Shorts Growth Engine

Use this skill when the user wants to build, improve, or operate an automated/semi-automated system for short-form social media content: YouTube Shorts, TikTok, Instagram Reels, Facebook Reels, Pinterest, LinkedIn video, or similar.

## Core principle

Do **not** treat the task as “make videos.” Treat it as a **growth engine**:

1. Generate many sharply positioned ideas.
2. Score hooks and scripts before rendering.
3. Produce previews.
4. Send the user a review package.
5. Post only after explicit approval.
6. Capture analytics.
7. Feed performance back into the next batch.

A technically correct content generator that produces generic content is failure with extra rendering time. Charming, but still failure.

## Recommended workflow

### 1. Define content pillars

Start with 2–4 pillars, not a vague infinite topic pool. For each pillar define:

- audience
- positioning
- tone
- hook patterns
- visual rules
- no-go topics
- platform fit
- monetization angle

Example pillar structure:

```json
{
  "pillar": "ai_life_systems",
  "positioning": "AI as a servant for ordinary life systems, never as values or authority.",
  "tone": ["clear", "useful", "values-first", "non-hype"],
  "hook_patterns": ["AI is a bad god, but a good servant."],
  "visual_rules": ["screen-recording friendly", "human remains in control"],
  "no_go": ["fake earnings", "overpromising automation"]
}
```

### 2. Build a testbatch before building a full automation platform

For a new channel or unvalidated strategy, create a small batch first:

- 3 ideas per pillar
- 6 short growth candidates, usually 20–45 seconds
- 1–3 monetization-length candidates, usually 60+ seconds where the platform rewards length
- one clear hypothesis per idea

Each idea should include:

- id
- pillar
- title
- platform target
- duration
- hook
- script outline
- visual concept
- caption
- CTA
- quality score
- risk/policy notes

### 3. Add a script quality gate

Before rendering, score each draft for:

- hook strength in the first 1–2 seconds
- novelty / non-generic angle
- emotional pull
- clarity
- retention risk
- comment/share potential
- AI-slop risk
- policy/copyright risk

Render only drafts above the threshold, e.g. 8/10.

### 4. Use a human approval loop

Default to semi-automatic operation first. Send a Telegram review package with:

- preview video or script draft
- pillar and format
- title
- duration
- hook
- caption
- hashtags
- quality score
- policy notes
- suggested platforms

Recommended approval commands:

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

If approval is ambiguous, do not post. Ask once in the project topic.

### 5. Keep platform strategy separate from rendering

Rendering code should not hardcode content strategy. Keep separate config files for:

- niches/pillars
- platforms
- script formats
- visual styles
- approval workflow
- analytics schema

This lets the same renderer support different formats and prevents prompt spaghetti. Prompt spaghetti is still spaghetti even if it is wearing a YAML hat.

## Platform notes

### YouTube Shorts

- Shorts can be vertical or square and up to 3 minutes long.
- YouTube Partner Program Shorts route typically requires 1,000 subscribers and 10M valid public Shorts views in the last 90 days.
- Alternative longform YPP route: 1,000 subscribers and 4,000 valid public watch hours in the last 12 months.
- Shorts views in the Shorts feed do not count toward the 4,000 longform watch hours.
- Practical early growth tests often work best at 20–45 seconds; longer Shorts need a strong story or payoff.

### TikTok

- Creator Rewards availability depends on country/account eligibility.
- TikTok Creator Rewards generally requires a non-business account in good standing, 10,000 followers, and 100,000 authentic views in the last 30 days before joining, subject to country availability and current terms.
- Reward-eligible videos generally need to be at least 1 minute long and meet quality/originality criteria.
- This creates a strategy split:
  - 20–45s for growth and format discovery.
  - 60–75s for monetization candidates once a format works.

### Instagram / Facebook Reels

Useful for republishing 9:16 assets and testing share/save behavior. Treat direct payout as volatile; prefer indirect monetization through audience, affiliate, newsletter, or products.

### LinkedIn / X / Pinterest

Use selectively:

- LinkedIn: AI/productivity/workflows.
- X: AI demos, build-in-public, technical positioning.
- Pinterest: evergreen checklists, routines, faith/life prompts.

## Repository scaffold

A clean shorts-engine repo can start as:

```text
AutoShortsBot/
  autoshorts/
    config/
    ideas/
    scripting/
    rendering/
    review/
    publishing/
    analytics/
  configs/
    niches/
    platforms/
  data/
    drafts/
    performance/
  scripts/
  tests/
  README.md
  .gitignore
  pyproject.toml
```

Never commit:

- `.env`
- OAuth tokens
- API keys
- generated credentials
- rendered videos or large media outputs unless explicitly intended
- raw analytics exports with sensitive account data

## Implementation quality

When coding the engine, prefer TDD for deterministic parts:

- structured video candidate validation
- testbatch validation and duplicate-id checks
- renderable-candidate filtering before any renderer runs
- duration classification
- review formatting
- analytics import
- approval command parsing
- platform metadata generation
- script quality gates

A strong first implementation path is deterministic gating before any renderer exists:

1. Add structured `VideoCandidate` and `TestBatch` models before rendering code. Require candidate id, pillar, title, platforms, duration, hook, script outline, visual concept, caption, CTA, hashtags, quality score, policy notes, and one clear hypothesis.
2. Validate candidates and batches: reject missing strategic fields, unsupported platforms, out-of-range durations, empty batches, and duplicate candidate ids.
3. Add an approval parser that accepts only explicit commands such as `Freigabe alle`, `Freigabe YouTube`, `Freigabe TikTok`, `Freigabe Instagram`, `Ablehnen`, and `Ändern: <requested change>`.
4. Treat vague consent like `passt schon` as `needs_clarification`; never publish on ambiguous approval.
5. Add a conservative script quality gate before rendering, e.g. average hook/novelty/clarity/retention ≥ 8, AI-slop risk below threshold, and no high policy risk.
6. Add a Telegram review package renderer that surfaces the candidate, gate status, policy notes, preview path, and only relevant platform approval commands.
7. Test these deterministic pieces first with RED-GREEN-REFACTOR before adding renderers, image generation, video generation, or publishing APIs.

Rendering and external API calls can be integration-tested separately.

For this AutoShortsBot/Retention-Studio workflow, prefer a free-first renderer path before any paid video provider: `free_production_jobs → local_motion_renderer → FFmpeg MP4`. The local renderer should use Pillow/FFmpeg procedural motion, mechanism layers, kinetic captions, and generated metadata; preserve exact scene durations from render manifests, exclude text-only LLM refinement jobs from rendering, and keep generated local renders out of git (`data/local_motion_renders/`). NVIDIA/build.nvidia.com LLMs are optional text-refinement helpers only, not video generation.

When Hermes has the `openai-codex` image-generation backend available, GPT Image 2 is a good cloud-side keyframe/thumbnail/concept generator for the pipeline: `script idea → GPT Image 2 keyframe → human review → Wan/ComfyUI I2V or local renderer → deterministic captions/overlays`. Because GPT Image 2 runs cloud-side, it does not compete with local/remote GPU VRAM used by Wan/ComfyUI video jobs. Use low/medium for iteration and high for final keyframes; still add captions, labels, numbers, UI text, and audit-sensitive facts later in the deterministic renderer rather than trusting baked image text.

If the user wants more realistic footage without spending money, first check whether a local GPU/ComfyUI host is available. Preferred path when reachable: `scene spec → ComfyUI/Wan API over Tailscale → imported_ai_clips/<video_id>/scene_XX.mp4 → local captions/mechanism layers/FFmpeg assembly`. For a 16 GB A5000 Laptop GPU, use the `comfyui` skill reference `wan-video-tailscale-a5000.md`: Wan2.1 T2V 1.3B FP8, `--lowvram`, CPU text encoding, VAE tiling, and a small smoke-test ladder before quality probes. If no local GPU path is available or quality is insufficient, use the hybrid free-tier handoff rather than pretending local procedural graphics are realistic: `free_production_jobs → free_ai_video_handoff → manual browser generation in free-tier tools (PixVerse/Pika/CapCut/Hailuo only with existing free credits) → import clips under data/imported_ai_clips/ → local captions/mechanism layers/FFmpeg assembly`. Guardrails: no paid top-ups, no subscriptions, avoid Pro templates, no baked captions/logos/watermarks where possible, and keep imported clips out of git.

For this AutoShortsBot/Retention-Studio workflow, prefer a free-first renderer path before any paid video provider: `free_production_jobs → local_motion_renderer → FFmpeg MP4`. The local renderer should produce moving mechanism/caption previews, not final static cards. When the user has a capable local/remote GPU (e.g. RTX A5000 16 GB via Tailscale), promote the path to `ComfyUI/Wan or LTX local clip generation → imported_ai_clips → hybrid local renderer → FFmpeg`, with free web providers (PixVerse/Pika/CapCut) only as fallback. External free-tier AI video tools can still be used manually for realistic b-roll, but keep paid top-ups/subscriptions disabled and store downloaded clips under `data/imported_ai_clips/<video_id>/scene_XX.mp4`. Generate provider handoff packets for manual browser generation and imported-clip assembly plans so missing clips fall back to local placeholders.

## Pitfalls

- **Generic advice content:** “4 tips to be healthier/productive” is usually too weak unless the hook, identity, or story is sharp.
- **AI-image slop:** avoid generated images containing fake UI text, gibberish signs, distorted hands/faces, or abstract generic graphs. These damage trust fast.
- **Too much automation too soon:** validate scripts and formats before building posting APIs.
- **No analytics loop:** producing content without tracking retention, likes, shares, comments, and follows is blind output.
- **Mixed language drift:** decide language per channel or per pillar before rendering. Mixed English/German drafts are fine for ideation, not final voiceover unless intentional.

## References

- `references/autoshortsbot-case-study.md` — case-study details from the AutoShortsBot setup: first testbatch shape, repo scaffold, and approval loop.
- `references/approval-first-bot-foundation.md` — implementation pattern for the first safe bot slice: explicit approval parser, quality gate, Telegram review package, and TDD test shape.
- `references/idea-candidate-testbatch-foundation.md` — deterministic `VideoCandidate`/`TestBatch` foundation before rendering: validation rules, renderable filtering, and test shape.

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


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")
```

### 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.

## 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.

## Final Rule

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

No exceptions without the user's explicit permission.


todo

Repo-Status prüfen

Pillar-Registry-Tests schreiben und RED verifizieren

Content-Pillar-Modul und Kandidaten-Pillar-Validierung implementieren

Tests laufen lassen, README aktualisieren, committen


Repo-Status prüfen


terminal

git status --short --branch && git log --oneline -5


Output:
## feat/core-approval-quality-gates
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit


todo

Repo-Status prüfen


from autoshorts.config.pillars import (
    ContentPillar,
    DEFAULT_PILLARS,
    find_pillar,
    validate_candidate_against_pillars,
    validate_pillar_registry,
)
from autoshorts.ideas.candidate import VideoCandidate
from autoshorts.scripting.quality import ScriptQualityScore

def _score() -> ScriptQualityScore:
    return ScriptQualityScore(
        hook_strength=9,
        novelty=8,
        clarity=9,
        retention=8,
        ai_slop_risk=2,
        policy_risk="low",
    )

def _candidate(**overrides) -> VideoCandidate:
    data = {
        "candidate_id": "vid-001",
        "pillar": "ai_life_systems",
        "title": "AI ist ein schlechter Gott, aber ein guter Diener",
        "platforms": ("youtube", "tiktok"),
        "duration_seconds": 34,
        "hook": "AI sollte dir dienen, nicht dich führen.",
        "script_outline": "Show AI chaos, then a controlled human-approved workflow.",
        "visual_concept": "Calm desk setup, explicit approval checkpoint, no fake dashboards.",
        "caption": "KI als Werkzeug, nicht als Wertekompass.",
        "cta": "Follow for practical AI life systems.",
        "hashtags": ("#AI", "#Productivity"),
        "quality_score": _score(),
        "policy_notes": ("No fake earnings claims",),
        "hypothesis": "Values-first AI positioning gets more saves than hype content.",
    }
    data.update(overrides)
    return VideoCandidate(**data)

def test_default_registry_contains_ai_life_systems_positioning():
    pillar = find_pillar("ai_life_systems", DEFAULT_PILLARS)

    assert pillar is not None
    assert "servant" in pillar.positioning
    assert "human remains in control" in pillar.visual_rules
    assert "fake earnings" in pillar.no_go
    assert "youtube" in pillar.platform_fit

def test_pillar_registry_validation_rejects_duplicate_ids():
    first = ContentPillar(
        pillar_id="dup",
        audience="builders",
        positioning="first",
        tone=("clear",),
        hook_patterns=("hook",),
        visual_rules=("rule",),
        no_go=("no",),
        platform_fit=("youtube",),
        monetization_angle="newsletter",
    )
    second = ContentPillar(
        pillar_id="dup",
        audience="builders",
        positioning="second",
        tone=("clear",),
        hook_patterns=("hook",),
        visual_rules=("rule",),
        no_go=("no",),
        platform_fit=("youtube",),
        monetization_angle="newsletter",
    )

    result = validate_pillar_registry((first, second))

    assert result.is_valid is False
    assert "pillar ids must be unique" in result.errors

def test_candidate_passes_known_pillar_and_platform_fit():
    result = validate_candidate_against_pillars(_candidate(), DEFAULT_PILLARS)

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

def test_candidate_rejects_unknown_pillar():
    result = validate_candidate_against_pillars(
        _candidate(pillar="unknown_pillar"), DEFAULT_PILLARS
    )

    assert result.is_valid is False
    assert "unknown pillar: unknown_pillar" in result.errors

def test_candidate_rejects_platform_not_fit_for_pillar():
    registry = (
        ContentPillar(
            pillar_id="narrow",
            audience="specific audience",
            positioning="narrow test pillar",
            tone=("clear",),
            hook_patterns=("hook",),
            visual_rules=("rule",),
            no_go=("no",),
            platform_fit=("youtube",),
            monetization_angle="newsletter",
        ),
    )

    result = validate_candidate_against_pillars(
        _candidate(pillar="narrow", platforms=("youtube", "tiktok")), registry
    )

    assert result.is_valid is False
    assert "platform tiktok is not configured for pillar narrow" in result.errors

def test_registry_validation_rejects_empty_required_pillar_fields():
    pillar = ContentPillar(
        pillar_id="",
        audience="",
        positioning="",
        tone=(),
        hook_patterns=(),
        visual_rules=(),
        no_go=(),
        platform_fit=(),
        monetization_angle="",
    )

    result = validate_pillar_registry((pillar,))

    assert result.is_valid is False
    assert "pillar_id is required" in result.errors
    assert "audience is required" in result.errors
    assert "at least one hook pattern is required" in result.errors
    assert "at least one platform fit is required" in result.errors


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


terminal

python -m pytest tests/test_pillars.py -q


Output:
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_pillars.py ____________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_pillars.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_pillars.py:1: in <module>
    from autoshorts.config.pillars import (
E   ModuleNotFoundError: No module named 'autoshorts.config'
=========================== short test summary info ============================
ERROR tests/test_pillars.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.13s


todo

Repo-Status prüfen


"""Configuration models for content strategy."""

"""Content pillar registry and validation.

Pillars keep content strategy separate from rendering code. The renderer should
not decide what the channel stands for; that way lies prompt spaghetti wearing
a YAML hat.
"""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult, VideoCandidate

SUPPORTED_PILLAR_PLATFORMS = ("youtube", "tiktok", "instagram")

@dataclass(frozen=True)
class ContentPillar:
    pillar_id: str
    audience: str
    positioning: str
    tone: tuple[str, ...]
    hook_patterns: tuple[str, ...]
    visual_rules: tuple[str, ...]
    no_go: tuple[str, ...]
    platform_fit: tuple[str, ...]
    monetization_angle: str

DEFAULT_PILLARS: tuple[ContentPillar, ...] = (
    ContentPillar(
        pillar_id="ai_life_systems",
        audience="pragmatic adults who want AI to reduce life friction without surrendering judgement",
        positioning="AI as a servant for ordinary life systems, never as values or authority.",
        tone=("clear", "useful", "values-first", "non-hype"),
        hook_patterns=(
            "AI is a bad god, but a good servant.",
            "Stop asking AI to decide your life. Make it run the boring parts.",
            "The point is not more automation. The point is more control.",
        ),
        visual_rules=(
            "human remains in control",
            "explicit approval checkpoints",
            "calm cinematic desk and phone workflows",
            "no fake dashboards or unreadable UI text",
        ),
        no_go=(
            "fake earnings",
            "overpromising automation",
            "medical advice",
            "financial advice",
            "AI as moral authority",
        ),
        platform_fit=("youtube", "tiktok", "instagram"),
        monetization_angle="newsletter, templates, workflow products, consulting later",
    ),
)

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

def find_pillar(pillar_id: str, registry: tuple[ContentPillar, ...]) -> ContentPillar | None:
    for pillar in registry:
        if pillar.pillar_id == pillar_id:
            return pillar
    return None

def validate_pillar_registry(registry: tuple[ContentPillar, ...]) -> ValidationResult:
    errors: list[str] = []
    ids = [pillar.pillar_id for pillar in registry]

    if len(ids) != len(set(ids)):
        errors.append("pillar ids must be unique")

    for pillar in registry:
        if _is_blank(pillar.pillar_id):
            errors.append("pillar_id is required")
        if _is_blank(pillar.audience):
            errors.append("audience is required")
        if _is_blank(pillar.positioning):
            errors.append("positioning is required")
        if _is_blank(pillar.monetization_angle):
            errors.append("monetization_angle is required")
        if not pillar.tone:
            errors.append("at least one tone is required")
        if not pillar.hook_patterns:
            errors.append("at least one hook pattern is required")
        if not pillar.visual_rules:
            errors.append("at least one visual rule is required")
        if not pillar.no_go:
            errors.append("at least one no-go is required")
        if not pillar.platform_fit:
            errors.append("at least one platform fit is required")
        for platform in pillar.platform_fit:
            if platform not in SUPPORTED_PILLAR_PLATFORMS:
                errors.append(f"unsupported platform in pillar {pillar.pillar_id}: {platform}")

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

def validate_candidate_against_pillars(
    candidate: VideoCandidate,
    registry: tuple[ContentPillar, ...] = DEFAULT_PILLARS,
) -> ValidationResult:
    errors: list[str] = []
    pillar = find_pillar(candidate.pillar, registry)

    if pillar is None:
        errors.append(f"unknown pillar: {candidate.pillar}")
        return ValidationResult(is_valid=False, errors=tuple(errors))

    for platform in candidate.platforms:
        if platform not in pillar.platform_fit:
            errors.append(f"platform {platform} is not configured for pillar {pillar.pillar_id}")

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


terminal

Output:
......                                                                   [100%]
6 passed in 0.04s


todo

Repo-Status prüfen


patch

- `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
- `autoshorts.ideas.candidate` — structured video candidate model and validation.
- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
- `autoshorts.review.approvals` — deterministic Telegram approval command parser.


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

 Implemented foundation:

+- `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
 - `autoshorts.ideas.candidate` — structured video candidate model and validation.
 - `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.

No linter for .md files


- Tests for content pillars, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.

- Tests for idea candidates, testbatches, approval parsing, review formatting, and script quality gating.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -14,7 +14,7 @@
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
 - `autoshorts.scripting.quality` — conservative pre-render quality gate.
-- Tests for idea candidates, testbatches, approval parsing, review formatting, and script quality gating.
+- Tests for content pillars, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.

 ## Human approval commands

No linter for .md files


`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

## Content pillars

`ContentPillar` keeps strategy separate from rendering. The default registry currently contains:

- `ai_life_systems` — AI as a servant for ordinary life systems, never as values or authority.

A pillar defines audience, positioning, tone, hook patterns, visual rules, no-go topics, platform fit, and monetization angle.

`validate_pillar_registry()` rejects duplicate pillar ids and incomplete pillar definitions. `validate_candidate_against_pillars()` rejects candidates with unknown pillars or platforms that are not configured for that pillar.

## Development

`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

## Development


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

 `TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

+## Content pillars
+
+`ContentPillar` keeps strategy separate from rendering. The default registry currently contains:
+
+- `ai_life_systems` — AI as a servant for ordinary life systems, never as values or authority.
+
+A pillar defines audience, positioning, tone, hook patterns, visual rules, no-go topics, platform fit, and monetization angle.
+
+`validate_pillar_registry()` rejects duplicate pillar ids and incomplete pillar definitions. `validate_candidate_against_pillars()` rejects candidates with unknown pillars or platforms that are not configured for that pillar.
+
 ## Development

 Run tests:

No linter for .md files


terminal

Output:
.........................                                                [100%]
25 passed in 0.06s
 M README.md
?? autoshorts/config/
?? tests/test_pillars.py


git add README.md autoshorts/config tests/test_pillars.py && git commit -m "feat: add content pillar registry" && git status --short --branch && git log --oneline -5


Output:
[feat/core-approval-quality-gates ae6a4f6] feat: add content pillar registry
 4 files changed, 274 insertions(+), 1 deletion(-)
 create mode 100644 autoshorts/config/__init__.py
 create mode 100644 autoshorts/config/pillars.py
 create mode 100644 tests/test_pillars.py
## feat/core-approval-quality-gates
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit


Output:
.........................                                                [100%]
25 passed in 0.06s


todo

Repo-Status prüfen


Weitergebaut, Sir.

## Neuer Baustein: Content Pillar Registry

Ich habe jetzt die strategische Ebene eingebaut — damit der Bot nicht einfach “irgendwelche Videos” produziert, sondern nur Kandidaten, die zu einer definierten Content-Positionierung passen.

Neue Dateien:

```text
autoshorts/config/__init__.py
autoshorts/config/pillars.py
tests/test_pillars.py
```

## Was jetzt drin ist

### 1. `ContentPillar`

Ein Pillar definiert jetzt:

- `pillar_id`
- `audience`
- `positioning`
- `tone`
- `hook_patterns`
- `visual_rules`
- `no_go`
- `platform_fit`
- `monetization_angle`

Damit ist Strategie getrennt von Rendering. Der Renderer soll später keine Werte, Zielgruppe oder Positionierung erfinden. Das ist sonst der direkte Weg zu Prompt-Spaghetti mit Management-Slide-Geruch.

## Default Pillar

Aktuell gibt es den ersten echten Pillar:

```text
ai_life_systems
```

Positionierung:

```text
AI as a servant for ordinary life systems, never as values or authority.
```

Enthaltene Regeln:

- Tone:
  - clear
  - useful
  - values-first
  - non-hype

- Hook Patterns:
  - “AI is a bad god, but a good servant.”
  - “Stop asking AI to decide your life. Make it run the boring parts.”
  - “The point is not more automation. The point is more control.”

- Visual Rules:
  - human remains in control
  - explicit approval checkpoints
  - calm cinematic desk and phone workflows
  - no fake dashboards or unreadable UI text

- No-Go:
  - fake earnings
  - overpromising automation
  - medical advice
  - financial advice
  - AI as moral authority

- Plattform-Fit:
  - YouTube
  - TikTok
  - Instagram

## Neue Validierungen

### `validate_pillar_registry()`

Prüft:

- doppelte Pillar IDs
- fehlende Pflichtfelder
- mindestens ein Hook Pattern
- mindestens eine Visual Rule
- mindestens ein No-Go
- mindestens eine passende Plattform
- nur unterstützte Plattformen

### `validate_candidate_against_pillars()`

Prüft:

- Kandidat verwendet bekannten Pillar
- Kandidat nutzt nur Plattformen, die für den Pillar erlaubt sind

Beispiel:

```text
unknown_pillar → rejected
platform tiktok is not configured for pillar narrow → rejected
```

## TDD eingehalten

Ablauf:

1. Tests für Pillar Registry geschrieben.
2. RED verifiziert:

```text
ModuleNotFoundError: No module named 'autoshorts.config'
```

3. Modul minimal implementiert.
4. Tests grün.
5. README aktualisiert.
6. Commit erstellt.

## Tests

Aktueller Stand:

```text
25 passed in 0.06s
```

## Neuer Commit

Auf Branch:

```text
feat/core-approval-quality-gates
```

neuer Commit:

```text
ae6a4f6 feat: add content pillar registry
```

Aktueller Verlauf:

```text
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit
```

## Pipeline jetzt

Wir haben jetzt:

```text
Content Pillars
→ VideoCandidate
→ Candidate Validation
→ Pillar Fit Validation
→ TestBatch
→ Quality Gate
→ Review Package
→ Explicit Approval Parser
```

Das ist ein brauchbares Fundament. Noch keine hübsche Maschine, aber wenigstens kein wackliger Jahrmarkt-Roboter mit Upload-Rechten.

## Nächster sinnvoller Schritt

Als Nächstes würde ich bauen:

```text
autoshorts/ideas/factory.py
```

Ziel:

- aus Pillar + Rohidee einen `VideoCandidate` erzeugen
- automatische ID-Generierung
- Standard-Hashtags aus Pillar/Plattform ableiten
- Dauerklasse prüfen:
  - growth short: 20–45s
  - monetization candidate: 60–75s
- Hypothesis erzwingen
- direkt validierten Candidate zurückgeben

Dann können wir aus Content-Strategie tatsächlich Kandidaten erzeugen, statt sie nur manuell zu modellieren.


[M G] ok, mach weiter.


social-media-shorts-growth-engine

---
name: social-media-shorts-growth-engine
description: Design and operate a growth-oriented short-form video engine for YouTube Shorts, TikTok, Instagram Reels, and adjacent channels, with human approval before posting.
version: 1.0.0
tags: [shorts, tiktok, youtube-shorts, reels, content-strategy, growth, monetization, approval-workflow]
---

# Social-Media Shorts Growth Engine

Use this skill when the user wants to build, improve, or operate an automated/semi-automated system for short-form social media content: YouTube Shorts, TikTok, Instagram Reels, Facebook Reels, Pinterest, LinkedIn video, or similar.

## Core principle

Do **not** treat the task as “make videos.” Treat it as a **growth engine**:

1. Generate many sharply positioned ideas.
2. Score hooks and scripts before rendering.
3. Produce previews.
4. Send the user a review package.
5. Post only after explicit approval.
6. Capture analytics.
7. Feed performance back into the next batch.

A technically correct content generator that produces generic content is failure with extra rendering time. Charming, but still failure.

## Recommended workflow

### 1. Define content pillars

Start with 2–4 pillars, not a vague infinite topic pool. For each pillar define:

- audience
- positioning
- tone
- hook patterns
- visual rules
- no-go topics
- platform fit
- monetization angle

Example pillar structure:

```json
{
  "pillar": "ai_life_systems",
  "positioning": "AI as a servant for ordinary life systems, never as values or authority.",
  "tone": ["clear", "useful", "values-first", "non-hype"],
  "hook_patterns": ["AI is a bad god, but a good servant."],
  "visual_rules": ["screen-recording friendly", "human remains in control"],
  "no_go": ["fake earnings", "overpromising automation"]
}
```

### 2. Build a testbatch before building a full automation platform

For a new channel or unvalidated strategy, create a small batch first:

- 3 ideas per pillar
- 6 short growth candidates, usually 20–45 seconds
- 1–3 monetization-length candidates, usually 60+ seconds where the platform rewards length
- one clear hypothesis per idea

Each idea should include:

- id
- pillar
- title
- platform target
- duration
- hook
- script outline
- visual concept
- caption
- CTA
- quality score
- risk/policy notes

### 3. Add a script quality gate

Before rendering, score each draft for:

- hook strength in the first 1–2 seconds
- novelty / non-generic angle
- emotional pull
- clarity
- retention risk
- comment/share potential
- AI-slop risk
- policy/copyright risk

Render only drafts above the threshold, e.g. 8/10.

### 4. Use a human approval loop

Default to semi-automatic operation first. Send a Telegram review package with:

- preview video or script draft
- pillar and format
- title
- duration
- hook
- caption
- hashtags
- quality score
- policy notes
- suggested platforms

Recommended approval commands:

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

If approval is ambiguous, do not post. Ask once in the project topic.

### 5. Keep platform strategy separate from rendering

Rendering code should not hardcode content strategy. Keep separate config files for:

- niches/pillars
- platforms
- script formats
- visual styles
- approval workflow
- analytics schema

This lets the same renderer support different formats and prevents prompt spaghetti. Prompt spaghetti is still spaghetti even if it is wearing a YAML hat.

## Platform notes

### YouTube Shorts

- Shorts can be vertical or square and up to 3 minutes long.
- YouTube Partner Program Shorts route typically requires 1,000 subscribers and 10M valid public Shorts views in the last 90 days.
- Alternative longform YPP route: 1,000 subscribers and 4,000 valid public watch hours in the last 12 months.
- Shorts views in the Shorts feed do not count toward the 4,000 longform watch hours.
- Practical early growth tests often work best at 20–45 seconds; longer Shorts need a strong story or payoff.

### TikTok

- Creator Rewards availability depends on country/account eligibility.
- TikTok Creator Rewards generally requires a non-business account in good standing, 10,000 followers, and 100,000 authentic views in the last 30 days before joining, subject to country availability and current terms.
- Reward-eligible videos generally need to be at least 1 minute long and meet quality/originality criteria.
- This creates a strategy split:
  - 20–45s for growth and format discovery.
  - 60–75s for monetization candidates once a format works.

### Instagram / Facebook Reels

Useful for republishing 9:16 assets and testing share/save behavior. Treat direct payout as volatile; prefer indirect monetization through audience, affiliate, newsletter, or products.

### LinkedIn / X / Pinterest

Use selectively:

- LinkedIn: AI/productivity/workflows.
- X: AI demos, build-in-public, technical positioning.
- Pinterest: evergreen checklists, routines, faith/life prompts.

## Repository scaffold

A clean shorts-engine repo can start as:

```text
AutoShortsBot/
  autoshorts/
    config/
    ideas/
    scripting/
    rendering/
    review/
    publishing/
    analytics/
  configs/
    niches/
    platforms/
  data/
    drafts/
    performance/
  scripts/
  tests/
  README.md
  .gitignore
  pyproject.toml
```

Never commit:

- `.env`
- OAuth tokens
- API keys
- generated credentials
- rendered videos or large media outputs unless explicitly intended
- raw analytics exports with sensitive account data

## Implementation quality

When coding the engine, prefer TDD for deterministic parts:

- structured video candidate validation
- testbatch validation and duplicate-id checks
- renderable-candidate filtering before any renderer runs
- duration classification
- review formatting
- analytics import
- approval command parsing
- platform metadata generation
- script quality gates

A strong first implementation path is deterministic gating before any renderer exists:

1. Add structured `VideoCandidate` and `TestBatch` models before rendering code. Require candidate id, pillar, title, platforms, duration, hook, script outline, visual concept, caption, CTA, hashtags, quality score, policy notes, and one clear hypothesis.
2. Validate candidates and batches: reject missing strategic fields, unsupported platforms, out-of-range durations, empty batches, and duplicate candidate ids.
3. Add an approval parser that accepts only explicit commands such as `Freigabe alle`, `Freigabe YouTube`, `Freigabe TikTok`, `Freigabe Instagram`, `Ablehnen`, and `Ändern: <requested change>`.
4. Treat vague consent like `passt schon` as `needs_clarification`; never publish on ambiguous approval.
5. Add a conservative script quality gate before rendering, e.g. average hook/novelty/clarity/retention ≥ 8, AI-slop risk below threshold, and no high policy risk.
6. Add a Telegram review package renderer that surfaces the candidate, gate status, policy notes, preview path, and only relevant platform approval commands.
7. Test these deterministic pieces first with RED-GREEN-REFACTOR before adding renderers, image generation, video generation, or publishing APIs.

Rendering and external API calls can be integration-tested separately.

For this AutoShortsBot/Retention-Studio workflow, prefer a free-first renderer path before any paid video provider: `free_production_jobs → local_motion_renderer → FFmpeg MP4`. The local renderer should use Pillow/FFmpeg procedural motion, mechanism layers, kinetic captions, and generated metadata; preserve exact scene durations from render manifests, exclude text-only LLM refinement jobs from rendering, and keep generated local renders out of git (`data/local_motion_renders/`). NVIDIA/build.nvidia.com LLMs are optional text-refinement helpers only, not video generation.

When Hermes has the `openai-codex` image-generation backend available, GPT Image 2 is a good cloud-side keyframe/thumbnail/concept generator for the pipeline: `script idea → GPT Image 2 keyframe → human review → Wan/ComfyUI I2V or local renderer → deterministic captions/overlays`. Because GPT Image 2 runs cloud-side, it does not compete with local/remote GPU VRAM used by Wan/ComfyUI video jobs. Use low/medium for iteration and high for final keyframes; still add captions, labels, numbers, UI text, and audit-sensitive facts later in the deterministic renderer rather than trusting baked image text.

If the user wants more realistic footage without spending money, first check whether a local GPU/ComfyUI host is available. Preferred path when reachable: `scene spec → ComfyUI/Wan API over Tailscale → imported_ai_clips/<video_id>/scene_XX.mp4 → local captions/mechanism layers/FFmpeg assembly`. For a 16 GB A5000 Laptop GPU, use the `comfyui` skill reference `wan-video-tailscale-a5000.md`: Wan2.1 T2V 1.3B FP8, `--lowvram`, CPU text encoding, VAE tiling, and a small smoke-test ladder before quality probes. If no local GPU path is available or quality is insufficient, use the hybrid free-tier handoff rather than pretending local procedural graphics are realistic: `free_production_jobs → free_ai_video_handoff → manual browser generation in free-tier tools (PixVerse/Pika/CapCut/Hailuo only with existing free credits) → import clips under data/imported_ai_clips/ → local captions/mechanism layers/FFmpeg assembly`. Guardrails: no paid top-ups, no subscriptions, avoid Pro templates, no baked captions/logos/watermarks where possible, and keep imported clips out of git.

For this AutoShortsBot/Retention-Studio workflow, prefer a free-first renderer path before any paid video provider: `free_production_jobs → local_motion_renderer → FFmpeg MP4`. The local renderer should produce moving mechanism/caption previews, not final static cards. When the user has a capable local/remote GPU (e.g. RTX A5000 16 GB via Tailscale), promote the path to `ComfyUI/Wan or LTX local clip generation → imported_ai_clips → hybrid local renderer → FFmpeg`, with free web providers (PixVerse/Pika/CapCut) only as fallback. External free-tier AI video tools can still be used manually for realistic b-roll, but keep paid top-ups/subscriptions disabled and store downloaded clips under `data/imported_ai_clips/<video_id>/scene_XX.mp4`. Generate provider handoff packets for manual browser generation and imported-clip assembly plans so missing clips fall back to local placeholders.

## Pitfalls

- **Generic advice content:** “4 tips to be healthier/productive” is usually too weak unless the hook, identity, or story is sharp.
- **AI-image slop:** avoid generated images containing fake UI text, gibberish signs, distorted hands/faces, or abstract generic graphs. These damage trust fast.
- **Too much automation too soon:** validate scripts and formats before building posting APIs.
- **No analytics loop:** producing content without tracking retention, likes, shares, comments, and follows is blind output.
- **Mixed language drift:** decide language per channel or per pillar before rendering. Mixed English/German drafts are fine for ideation, not final voiceover unless intentional.

## References

- `references/autoshortsbot-case-study.md` — case-study details from the AutoShortsBot setup: first testbatch shape, repo scaffold, and approval loop.
- `references/approval-first-bot-foundation.md` — implementation pattern for the first safe bot slice: explicit approval parser, quality gate, Telegram review package, and TDD test shape.
- `references/idea-candidate-testbatch-foundation.md` — deterministic `VideoCandidate`/`TestBatch` foundation before rendering: validation rules, renderable filtering, and test shape.
- `references/content-pillar-registry-foundation.md` — strategic `ContentPillar` registry pattern: default `ai_life_systems`, pillar/candidate fit validation, and TDD test shape.


todo

Repo-Status prüfen

Factory-/Duration-Tests schreiben und RED verifizieren

ideas.factory minimal implementieren


Repo-Status prüfen

ideas.factory minimal implementieren


terminal

git status --short --branch && git log --oneline -6


Output:
## feat/core-approval-quality-gates
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit


todo

Repo-Status prüfen


import pytest

from autoshorts.config.pillars import DEFAULT_PILLARS
from autoshorts.ideas.factory import (
    CandidateDraft,
    classify_duration,
    create_candidate,
    create_candidate_id,
)
from autoshorts.scripting.quality import ScriptQualityScore

def _score() -> ScriptQualityScore:
    return ScriptQualityScore(
        hook_strength=9,
        novelty=8,
        clarity=9,
        retention=8,
        ai_slop_risk=2,
        policy_risk="low",
    )

def _draft(**overrides) -> CandidateDraft:
    data = {
        "pillar": "ai_life_systems",
        "title": "AI ist ein schlechter Gott, aber ein guter Diener",
        "platforms": ("youtube", "tiktok"),
        "duration_seconds": 34,
        "hook": "AI sollte dir dienen, nicht dich führen.",
        "script_outline": "Show AI chaos, then a controlled human-approved workflow.",
        "visual_concept": "Calm desk setup, explicit approval checkpoint, no fake dashboards.",
        "caption": "KI als Werkzeug, nicht als Wertekompass.",
        "cta": "Follow for practical AI life systems.",
        "quality_score": _score(),
        "policy_notes": ("No fake earnings claims",),
        "hypothesis": "Values-first AI positioning gets more saves than hype content.",
    }
    data.update(overrides)
    return CandidateDraft(**data)

def test_classify_duration_growth_short():
    assert classify_duration(34) == "growth_short"

def test_classify_duration_monetization_candidate():
    assert classify_duration(68) == "monetization_candidate"

def test_classify_duration_experiment_range():
    assert classify_duration(12) == "experiment"
    assert classify_duration(110) == "experiment"

def test_candidate_id_is_stable_slug_from_pillar_title_and_duration():
    candidate_id = create_candidate_id(
        pillar="ai_life_systems",
        title="AI ist ein schlechter Gott, aber ein guter Diener",
        duration_seconds=34,
    )

    assert candidate_id == "ai_life_systems-ai-ist-ein-schlechter-gott-34s"

def test_create_candidate_adds_id_default_hashtags_and_duration_class_policy_note():
    candidate = create_candidate(_draft(), registry=DEFAULT_PILLARS)

    assert candidate.candidate_id == "ai_life_systems-ai-ist-ein-schlechter-gott-34s"
    assert candidate.hashtags == ("#AI", "#LifeSystems", "#Productivity")
    assert "Duration class: growth_short" in candidate.policy_notes
    assert candidate.pillar == "ai_life_systems"
    assert candidate.platforms == ("youtube", "tiktok")

def test_create_candidate_rejects_missing_hypothesis():
    with pytest.raises(ValueError, match="hypothesis is required"):
        create_candidate(_draft(hypothesis=""), registry=DEFAULT_PILLARS)

def test_create_candidate_rejects_unknown_or_unfit_pillar():
    with pytest.raises(ValueError, match="unknown pillar: unknown"):
        create_candidate(_draft(pillar="unknown"), registry=DEFAULT_PILLARS)

def test_create_candidate_rejects_unfit_platform_for_pillar():
    with pytest.raises(ValueError, match="platform instagram is not configured"):
        create_candidate(
            _draft(platforms=("instagram",)),
            registry=tuple(
                p for p in DEFAULT_PILLARS if p.pillar_id != "ai_life_systems"
            )
            + (DEFAULT_PILLARS[0].__class__(
                pillar_id="ai_life_systems",
                audience=DEFAULT_PILLARS[0].audience,
                positioning=DEFAULT_PILLARS[0].positioning,
                tone=DEFAULT_PILLARS[0].tone,
                hook_patterns=DEFAULT_PILLARS[0].hook_patterns,
                visual_rules=DEFAULT_PILLARS[0].visual_rules,
                no_go=DEFAULT_PILLARS[0].no_go,
                platform_fit=("youtube",),
                monetization_angle=DEFAULT_PILLARS[0].monetization_angle,
            ),),
        )


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


terminal

python -m pytest tests/test_factory.py -q


Output:
==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_factory.py ____________________
ImportError while importing test module '/home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/tests/test_factory.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_factory.py:4: in <module>
    from autoshorts.ideas.factory import (
E   ModuleNotFoundError: No module named 'autoshorts.ideas.factory'
=========================== short test summary info ============================
ERROR tests/test_factory.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.11s


todo

ideas.factory minimal implementieren


"""Factory helpers for turning drafts into validated video candidates."""

from __future__ import annotations

from dataclasses import dataclass
import re
import unicodedata
from typing import Literal

from autoshorts.config.pillars import (
    DEFAULT_PILLARS,
    ContentPillar,
    validate_candidate_against_pillars,
)
from autoshorts.ideas.candidate import VideoCandidate, validate_candidate
from autoshorts.scripting.quality import ScriptQualityScore

DurationClass = Literal["growth_short", "monetization_candidate", "experiment"]

_DEFAULT_HASHTAGS_BY_PILLAR = {
    "ai_life_systems": ("#AI", "#LifeSystems", "#Productivity"),
}

@dataclass(frozen=True)
class CandidateDraft:
    pillar: str
    title: str
    platforms: tuple[str, ...]
    duration_seconds: int
    hook: str
    script_outline: str
    visual_concept: str
    caption: str
    cta: str
    quality_score: ScriptQualityScore
    policy_notes: tuple[str, ...]
    hypothesis: str
    hashtags: tuple[str, ...] = ()

def classify_duration(duration_seconds: int) -> DurationClass:
    if 20 <= duration_seconds <= 45:
        return "growth_short"
    if 60 <= duration_seconds <= 75:
        return "monetization_candidate"
    return "experiment"

def _slugify(text: str, *, max_words: int = 6) -> str:
    normalized = unicodedata.normalize("NFKD", text or "")
    ascii_text = normalized.encode("ascii", "ignore").decode("ascii")
    tokens = re.findall(r"[a-z0-9]+", ascii_text.casefold())
    return "-".join(tokens[:max_words]) or "untitled"

def create_candidate_id(pillar: str, title: str, duration_seconds: int) -> str:
    return f"{pillar}-{_slugify(title)}-{duration_seconds}s"

def _default_hashtags_for_pillar(pillar: str) -> tuple[str, ...]:
    return _DEFAULT_HASHTAGS_BY_PILLAR.get(pillar, ())

def create_candidate(
    draft: CandidateDraft,
    *,
    registry: tuple[ContentPillar, ...] = DEFAULT_PILLARS,
) -> VideoCandidate:
    if not (draft.hypothesis or "").strip():
        raise ValueError("hypothesis is required")

    duration_class = classify_duration(draft.duration_seconds)
    policy_notes = tuple(draft.policy_notes) + (f"Duration class: {duration_class}",)
    hashtags = draft.hashtags or _default_hashtags_for_pillar(draft.pillar)

    candidate = VideoCandidate(
        candidate_id=create_candidate_id(
            pillar=draft.pillar,
            title=draft.title,
            duration_seconds=draft.duration_seconds,
        ),
        pillar=draft.pillar,
        title=draft.title,
        platforms=draft.platforms,
        duration_seconds=draft.duration_seconds,
        hook=draft.hook,
        script_outline=draft.script_outline,
        visual_concept=draft.visual_concept,
        caption=draft.caption,
        cta=draft.cta,
        hashtags=hashtags,
        quality_score=draft.quality_score,
        policy_notes=policy_notes,
        hypothesis=draft.hypothesis,
    )

    candidate_result = validate_candidate(candidate)
    pillar_result = validate_candidate_against_pillars(candidate, registry)
    errors = candidate_result.errors + pillar_result.errors
    if errors:
        raise ValueError("; ".join(errors))
    return candidate


terminal

{"output": "...FF...                                                                 [100%]\n=================================== FAILURES ===================================\n_______ test_candidate_id_is_stable_slug_from_pillar_title_and_duration ________\n\n    def test_candidate_id_is_stable_slug_from_pillar_title_and_duration():\n        candidate_id = create_candidate_id(\n            pillar=\"ai_life_systems\",\n            title=\"AI ist ein schlechter Gott, aber ein guter Diener\",\n            duration_seconds=34,\n        )\n    \n>       assert candidate_id == \"ai_life_systems-ai-ist-ein-schlechter-gott-34s\"\nE       AssertionError: assert 'ai_life_syst...gott-aber-34s' == 'ai_life_syst...hter-gott-34s'\nE         \nE         Skipping 33 identical leading characters in diff, use -v to show\nE         - hter-gott-34s\nE         + hter-gott-aber-34s\nE         ?           +++++\n\ntests/test_factory.py:63: AssertionError\n_ test_create_candidate_adds_id_default_hashtags_and_duration_class_policy_note _\n\n    def test_create_candidate_adds_id_default_hashtags_and_duration_class_policy_note():\n        candidate = create_candidate(_draft(), registry=DEFAULT_PILLARS)\n    \n>       assert candidate.candidate_id == \"ai_life_systems-ai-ist-ein-schlechter-gott-34s\"\nE       AssertionError: assert 'ai_life_syst...gott-aber-34s' == 'ai_life_syst...hter-gott-34s'\nE         \nE         Skipping 33 identical leading characters in diff, use -v to show\nE         - hter-gott-34s\nE         + hter-gott-aber-34s\nE         ?           +++++\n\ntests/test_factory.py:69: AssertionError\n=========================== short test summary info ============================\nFAILED tests/test_factory.py::test_candidate_id_is_stable_slug_from_pillar_title_and_duration\nFAILED tests/test_factory.py::test_create_candidate_adds_id_default_hashtags_and_duration_class_policy_note\n2 failed, 6 passed in 0.06s", "exit_code": 1, "error": null}

[Tool loop warning: repeated_exact_failure_warning; count=2; terminal has failed 2 times with identical arguments. This looks like a loop; inspect the error and change strategy instead of retrying it unchanged.]


patch

def _slugify(text: str, *, max_words: int = 6) -> str:

def _slugify(text: str, *, max_words: int = 5) -> str:


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/ideas/factory.py
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/autoshorts/ideas/factory.py
@@ -47,7 +47,7 @@
     return "experiment"

-def _slugify(text: str, *, max_words: int = 6) -> str:
+def _slugify(text: str, *, max_words: int = 5) -> str:
     normalized = unicodedata.normalize("NFKD", text or "")
     ascii_text = normalized.encode("ascii", "ignore").decode("ascii")
     tokens = re.findall(r"[a-z0-9]+", ascii_text.casefold())


terminal

Output:
........                                                                 [100%]
8 passed in 0.04s


todo

ideas.factory minimal implementieren


     1|# AutoShortsBot
     2|
     3|AutoShortsBot is a semi-automatic short-form content engine for high-quality YouTube Shorts, TikTok, and Instagram Reels.
     4|
     5|The project is deliberately approval-first: it can draft, score, render previews, and prepare publishing metadata, but it must not publish anything without explicit human approval.
     6|
     7|## Current scope
     8|
     9|Implemented foundation:
    10|
    11|- `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
    12|- `autoshorts.ideas.candidate` — structured video candidate model and validation.
    13|- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.
    14|- `autoshorts.review.approvals` — deterministic Telegram approval command parser.
    15|- `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
    16|- `autoshorts.scripting.quality` — conservative pre-render quality gate.
    17|- Tests for content pillars, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.
    18|
    19|## Human approval commands
    20|
    21|Supported review responses:
    22|
    23|- `Freigabe alle` — approve for YouTube, TikTok, and Instagram.
    24|- `Freigabe YouTube`
    25|- `Freigabe TikTok`
    26|- `Freigabe Instagram`
    27|- `Ablehnen`
    28|- `Ändern: <requested change>`
    29|
    30|Ambiguous messages such as `passt schon` intentionally return `needs_clarification` and must not trigger posting.
    31|
    32|## Quality gate
    33|
    34|A script candidate only proceeds to rendering when:
    35|
    36|- average of hook strength, novelty, clarity, and retention is at least `8.0`;
    37|- AI-slop risk is not above `5`;
    38|- policy risk is not `high`.
    39|
    40|This keeps rendering capacity for candidates that might actually deserve to exist. A rare mercy.
    41|
    42|## Review package
    43|
    44|`autoshorts.review.package.render_telegram_review_package()` produces the human approval message for one candidate. It includes:
    45|
    46|- candidate id, title, pillar, target platforms, and duration;
    47|- hook, script summary, visual concept, caption, and hashtags;
    48|- average quality score, AI-slop risk, policy notes, and render-gate status;
    49|- optional preview path;
    50|- only the relevant platform-specific approval commands.
    51|
    52|A blocked quality gate is rendered as `Render gate: BLOCKED — nicht automatisch rendern`.
    53|
    54|## Idea candidates and testbatches
    55|
    56|`VideoCandidate` is the structured unit that moves through the early pipeline. It carries:
    57|
    58|- candidate id, pillar, title, target platforms, duration, and hypothesis;
    59|- hook, script outline, visual concept, caption, CTA, and hashtags;
    60|- quality score and policy notes.
    61|
    62|`validate_candidate()` rejects missing strategic fields, unsupported platforms, and durations outside `10..180` seconds.
    63|
    64|`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.
    65|
    66|## Content pillars
    67|
    68|`ContentPillar` keeps strategy separate from rendering. The default registry currently contains:
    69|
    70|- `ai_life_systems` — AI as a servant for ordinary life systems, never as values or authority.
    71|
    72|A pillar defines audience, positioning, tone, hook patterns, visual rules, no-go topics, platform fit, and monetization angle.
    73|
    74|`validate_pillar_registry()` rejects duplicate pillar ids and incomplete pillar definitions. `validate_candidate_against_pillars()` rejects candidates with unknown pillars or platforms that are not configured for that pillar.
    75|
    76|## Development
    77|
    78|Run tests:
    79|
    80|```bash
    81|python -m pytest tests -q
    82|```
    83|
    84|## Planned architecture
    85|
    86|```text
    87|autoshorts/
    88|  config/       # niches, platforms, workflow settings
    89|  ideas/        # idea generation and scoring
    90|  scripting/    # script drafts, quality gates, retention checks
    91|  rendering/    # local/AI-assisted preview rendering
    92|  review/       # Telegram review package and approval parser
    93|  publishing/   # platform-specific publishing metadata and later APIs
    94|  analytics/    # performance imports and feedback loop
    95|```
    96|
    97|## Non-negotiables
    98|
    99|- No credentials in git.
   100|- No generated media in git unless explicitly intended.
   101|- No automatic publishing without explicit approval.
   102|- Quality over speed; avoid generic AI-slop.
   103|


patch

- `autoshorts.ideas.candidate` — structured video candidate model and validation.
- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.

- `autoshorts.ideas.candidate` — structured video candidate model and validation.
- `autoshorts.ideas.factory` — draft-to-candidate factory with stable ids, default hashtags, and duration classes.
- `autoshorts.ideas.testbatch` — testbatch validation and renderable-candidate filtering.


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

 - `autoshorts.config.pillars` — content pillar registry, default AI/life-systems positioning, and pillar fit validation.
 - `autoshorts.ideas.candidate` — structured video candidate model and validation.
+- `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.review.package` — Telegram-friendly review package renderer for human approval.

No linter for .md files


- Tests for content pillars, candidate factory, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.

- Tests for content pillars, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.


--- a//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
+++ b//home/agent/jarvis_runtime/autoshortsbot/AutoShortsBot/README.md
@@ -15,7 +15,7 @@
 - `autoshorts.review.approvals` — deterministic Telegram approval command parser.
 - `autoshorts.review.package` — Telegram-friendly review package renderer for human approval.
 - `autoshorts.scripting.quality` — conservative pre-render quality gate.
-- Tests for content pillars, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.
+- Tests for content pillars, candidate factory, idea candidates, testbatches, approval parsing, review formatting, and script quality gating.

 ## Human approval commands

No linter for .md files


`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

## Candidate factory

`CandidateDraft` represents a raw idea before it becomes a validated `VideoCandidate`.

`create_candidate()` turns a draft into a candidate by:

- generating a stable id from pillar, title, and duration;
- adding default pillar hashtags when no hashtags are supplied;
- classifying duration as `growth_short`, `monetization_candidate`, or `experiment`;
- appending the duration class to policy notes;
- enforcing candidate validation and pillar/platform fit validation.

Duration classes:

- `growth_short`: 20–45 seconds;
- `monetization_candidate`: 60–75 seconds;
- `experiment`: all other valid durations.

Missing hypothesis, unknown pillar, or unfit platform raises `ValueError`.

## Content pillars

`TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

## Content pillars


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

 `TestBatch` groups candidates for one experiment. `validate_testbatch()` rejects empty batches and duplicate ids. `renderable_candidates()` returns only candidates that are both valid and allowed by the quality gate.

+## Candidate factory
+
+`CandidateDraft` represents a raw idea before it becomes a validated `VideoCandidate`.
+
+`create_candidate()` turns a draft into a candidate by:
+
+- generating a stable id from pillar, title, and duration;
+- adding default pillar hashtags when no hashtags are supplied;
+- classifying duration as `growth_short`, `monetization_candidate`, or `experiment`;
+- appending the duration class to policy notes;
+- enforcing candidate validation and pillar/platform fit validation.
+
+Duration classes:
+
+- `growth_short`: 20–45 seconds;
+- `monetization_candidate`: 60–75 seconds;
+- `experiment`: all other valid durations.
+
+Missing hypothesis, unknown pillar, or unfit platform raises `ValueError`.
+
 ## Content pillars

 `ContentPillar` keeps strategy separate from rendering. The default registry currently contains:

No linter for .md files


terminal

Output:
.................................                                        [100%]
33 passed in 0.07s
 M README.md
?? autoshorts/ideas/factory.py
?? tests/test_factory.py


git add README.md autoshorts/ideas/factory.py tests/test_factory.py && git commit -m "feat: add candidate factory" && git status --short --branch && git log --oneline -6


Output:
[feat/core-approval-quality-gates deea24f] feat: add candidate factory
 3 files changed, 229 insertions(+), 1 deletion(-)
 create mode 100644 autoshorts/ideas/factory.py
 create mode 100644 tests/test_factory.py
## feat/core-approval-quality-gates
deea24f feat: add candidate factory
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit


Output:
.................................                                        [100%]
33 passed in 0.07s


todo

Repo-Status prüfen

ideas.factory minimal implementieren


Weitergebaut, Sir.

## Neuer Baustein: Candidate Factory

Ich habe jetzt den nächsten Schritt implementiert:

```text
autoshorts/ideas/factory.py
tests/test_factory.py
```

Damit können wir aus einer Rohidee / einem Draft jetzt automatisch einen validierten `VideoCandidate` erzeugen.

## Was neu ist

### 1. `CandidateDraft`

Ein Draft ist die Rohform einer Idee vor der finalen Kandidatenstruktur.

Enthält u.a.:

- Pillar
- Titel
- Plattformen
- Dauer
- Hook
- Script Outline
- Visual Concept
- Caption
- CTA
- Quality Score
- Policy Notes
- Hypothesis
- optionale Hashtags

## 2. `create_candidate()`

Macht aus einem `CandidateDraft` einen vollständigen `VideoCandidate`.

Dabei passiert automatisch:

- stabile Candidate-ID erzeugen
- Default-Hashtags ableiten
- Duration Class bestimmen
- Duration Class in Policy Notes schreiben
- Candidate validieren
- Pillar-/Platform-Fit validieren
- bei Fehlern sauber `ValueError` werfen

Beispiel-ID:

```text
ai_life_systems-ai-ist-ein-schlechter-gott-34s
```

## 3. Duration Classification

Neue Funktion:

```python
classify_duration(duration_seconds)
```

Kategorien:

```text
growth_short: 20–45 Sekunden
monetization_candidate: 60–75 Sekunden
experiment: alles andere innerhalb gültiger Dauer
```

Das trennt jetzt frühe Growth-Tests von späteren Monetization-Formaten.

## 4. Default Hashtags

Für den aktuellen Pillar:

```text
ai_life_systems
```

werden automatisch gesetzt:

```text
#AI
#LifeSystems
#Productivity
```

Wenn explizit Hashtags im Draft stehen, werden diese verwendet.

## 5. Harte Fehler bei unbrauchbaren Drafts

`create_candidate()` lehnt ab:

- fehlende Hypothesis
- unbekannter Pillar
- Plattform passt nicht zum Pillar
- normale Candidate-Validation-Fehler

Also kein “wird schon passen” in der Pipeline. “Wird schon passen” ist der natürliche Feind jeder Automation.

## TDD eingehalten

Ablauf:

1. Tests geschrieben.
2. RED verifiziert:

```text
ModuleNotFoundError: No module named 'autoshorts.ideas.factory'
```

3. Minimal implementiert.
4. Tests grün.
5. README aktualisiert.
6. Commit erstellt.

## Tests

Aktueller Stand:

```text
33 passed in 0.07s
```

## Neuer Commit

Branch:

```text
feat/core-approval-quality-gates
```

Commit:

```text
deea24f feat: add candidate factory
```

Aktueller Verlauf:

```text
deea24f feat: add candidate factory
ae6a4f6 feat: add content pillar registry
49d6daf feat: add idea candidates and testbatches
55b17fa feat: add telegram review package
3555ec2 feat: add approval parser and quality gate
7642749 Initial commit
```

## Pipeline jetzt

Wir haben jetzt:

```text
Content Pillar
→ CandidateDraft
→ Candidate Factory
→ VideoCandidate
→ Candidate Validation
→ Pillar Fit Validation
→ TestBatch
→ Quality Gate
→ Review Package
→ Approval Parser
```

## Nächster sinnvoller Schritt

Als Nächstes würde ich bauen:

```text
autoshorts/review/workflow.py
```

Ziel:

- `VideoCandidate` → falls renderbar → `ReviewPackage`
- falls nicht renderbar → trotzdem ReviewPackage mit BLOCKED Gate
- Batch → Liste von ReviewPackages
- eindeutige Trennung:
  - `ready_for_review`
  - `blocked`
  - `invalid`

Dann hätten wir die erste komplette interne Review-Pipeline vor Rendering.


[M G] Ok, mach weiter.


Explanation:
**Continuing with the task**

I need to press on and load the relevant skills again. It's important to make effective use of the tools I have at my disposal. I also want to implement a review workflow based on Test Driven Development (TDD) principles. This approach should help ensure I'm on the right track while I work through the tasks at hand. Let’s keep things moving smoothly!