Podcast Curation & Daily Brief
The podcast system has three pipelines that share infrastructure and publish to a common private RSS feed:
- Podcast curation β finding and queuing identity-relevant episodes in Pocket Casts.
- Daily brief generation β synthesizing a short spoken podcast from VDB context.
- Deep produced episodes β a Stepwise flow (
podcast-deep) that researches, writes, critiques, and TTS-produces 15-40 minute episodes.
Pipelines 1 and 2 use the media signal refinery for episode discovery and the scheduler for automation. A fourth, smaller path β the listened-episode digest β bridges what bio-zack actually listened to back into the transcription/summary pipeline.
System Architecture
PODCAST CURATION PIPELINE DAILY BRIEF PIPELINE
(every 12 hours) (after VDB send, ~5-6:30 AM PT)
+------------------+ +------------------+
| OPML Subscriptions| | gather_context() |
| + Pocket Casts | | (VDB data) |
+--------+---------+ +--------+---------+
| |
v v
+------------------+ +------------------+
| Media Pipeline | | Claude (Sonnet |
| sources/podcasts | | 4.6) via |
| (4h poll) | | OpenRouter API |
+--------+---------+ +--------+---------+
| |
v v
+------------------+ +------------------+
| Triage + Score | | Podcast |
| (media pipeline) | | Transcript |
+--------+---------+ | (.txt) |
| +--------+---------+
v |
+------------------+ v
| podcast_curator | +------------------+
| Rubric Scoring | | ElevenLabs TTS |
| (6 dimensions) | | generate.py |
+--------+---------+ +--------+---------+
| |
v v
+------------------+ +------------------+
| Queue Selection | | RSS Feed |
| (diversity, | | /api/v1/podcast/ |
| dedup, capacity) | | {token}/feed.xml |
+--------+---------+ +--------+---------+
| |
v v
+------------------+ +------------------+
| Pocket Casts | | Pocket Casts |
| Up Next Queue | | (subscribed) |
+------------------+ +------------------+
DEEP PRODUCED EPISODES (podcast-deep Stepwise flow, on demand)
topic -> research -> scratchpad -> outline -> fill-transcript
-> rubric-critique -> targeted-refine -> text-quality-gate (loops up to 3x)
-> chunk+tag (ElevenLabs v3 tags) -> per-chunk TTS
-> ffmpeg assembly (crossfade + LUFS normalize) -> publish to RSS feed
Why This Architecture?
-
Two-stage scoring: The media pipeline does cheap triage (Gemini Flash) to filter the firehose. The curation engine then applies an expensive identity-aware rubric (Gemini 2.5 Flash) to the survivors. Cost-effective filtering without sacrificing personalization.
-
Identity-driven curation: The scoring prompt includes active goals, projects, identity observations, heartbeat topics, and listening history. Episodes are scored against who bio-zack is right now, not a static interest profile.
-
Feedback loop: Listening signals (completed, abandoned, skipped) are synced from Pocket Casts history and fed back into the scoring prompt. The system learns from what bio-zack actually listens to.
-
Cloned voice for spoken episodes: ElevenLabs synthesizes the daily brief and deep episodes with a cloned voice, making the podcast feel like silicon-zack talking directly to bio-zack.
Pocket Casts Integration
Client: scripts/integrations/pocketcasts.py
API reverse-engineered from the open source Pocket Casts iOS/Android apps (Automattic). Authentication via email/password login, with token refresh.
Credentials: ~/.config/vita/pocketcasts.json
{ "email": "user@example.com", "password": "REDACTED" }
Setup:
uv run python scripts/media/curate_podcasts_setup.py user@example.com yourpassword
API operations used:
| Operation | Method | Purpose |
|---|---|---|
get_up_next() |
Up Next sync | Read current queue |
add_to_queue() |
Up Next sync (play_last) | Add curated episodes |
remove_from_queue() |
Up Next sync (remove) | Clean finished episodes |
get_listening_history() |
User history | Feedback loop signals |
get_subscriptions() |
User podcast list | Subscription management |
get_new_releases() |
New releases | Fresh episodes from subs |
search_episodes() |
Episode search | Discovery + UUID resolution |
search_podcasts() |
Discover search | Podcast UUID lookup |
get_podcast_episodes() |
Cache endpoint | Episode listing for a show |
Sync state (serverModified timestamp) is tracked in data/podcast/curation/pc_sync_state.json to enable incremental Up Next syncs.
Episode Scoring (6 Dimensions)
Config: config/podcast-curation.yaml
Each candidate is scored 0-10 on six dimensions, weighted to produce a 0-100 total:
| Dimension | Weight | Description |
|---|---|---|
topic_relevance |
0.30 | Match to current interests, projects, goals, and identity |
guest_quality |
0.20 | Notable, expert, or someone bio-zack would respect/learn from |
intellectual_depth |
0.20 | Substance over surface. Complex ideas, not hot takes |
production_quality |
0.10 | Host engagement quality, conversational chemistry, humor |
timeliness |
0.10 | Relevant NOW vs evergreen? Breaking or trending topic? |
novelty |
0.10 | New perspective vs rehash? Unique angle or guest? |
The total score is sum(dimension_score * weight * 10) across all dimensions.
Model: google/gemini-2.5-flash via OpenRouter. Candidates are batched (8 per call) with a system prompt that includes full identity context.
Curation Rubric
Positive Signals (Boosters)
The rubric defines seven booster categories (paraphrased β actual prompt is in config/podcast-curation.yaml):
- Founder stories with specific tactical details
- Technical deep dives with genuine expertise
- Contrarian or counterintuitive arguments with evidence
- Action-sports content matching personal interests
- AI/LLM technical content or agent architecture
- Dry humor, irreverent tone
- System design or organizational architecture
Negative Signals (Penalties)
Six penalty categories:
- Excessive emotional validation or self-help tone
- Participation trophy mentality
- Surface-level hot takes without depth
- Corporate buzzwords (synergy, paradigm, ecosystem)
- Overly promotional or advertorial content
- Fluffy lifestyle or generic motivation content
Queue Rules
| Rule | Value | Purpose |
|---|---|---|
target_size |
10 | Target number of episodes in Up Next |
min_score |
65 | Minimum curation score to queue |
max_per_podcast |
2 | Max episodes from same podcast |
max_same_category |
3 | Diversity cap per category |
cleanup_remaining_threshold_seconds |
120 | Remove episodes with < 2min left |
max_discovery_candidates |
20 | Discovery searches per cycle |
Diversity Categories
Episodes are classified into one of six categories to enforce queue diversity:
politics_current_eventstechnology_aibusiness_entrepreneurshipscience_healthculture_philosophysports_action
Curation Pipeline Detail
Orchestrator: scripts/media/podcast_curator.py (PodcastCurator.run())
The full cycle runs every 12 hours:
-
Sync listening signals - Cross-reference queue_log with Pocket Casts listening history. Classify outcomes: completed (>90%), partial (>50%), abandoned (<50% after 48h), skipped (never played after 72h).
-
Gather identity context - Read active goals, projects, identity observations, heartbeat topics, current attention items, personality traits, and listening pattern history. Builds a rich text context for the scoring prompt.
-
Gather candidates from pipeline - Pull podcast items from
data/media/items/anddata/media/scores/that scored>= 40in media triage (last 7 days), viagather_candidates_from_pipeline(min_score=40). -
Gather discovery candidates - Search Pocket Casts for episodes matching goal/project keywords and hardcoded high-value terms. Up to
max_discovery_candidates(20) discovery candidates per cycle. -
Score against rubric - Send all candidates to Gemini with the identity-aware prompt. Parse dimension scores and compute weighted total.
-
Clean up Pocket Casts queue - Remove finished episodes (< 2min remaining) from Up Next.
-
Select for queue - Apply diversity constraints, dedup against recently queued items, fill available slots up to target_size.
-
Push to Pocket Casts - Resolve episode UUIDs (multi-strategy: existing UUID, podcast lookup + title match, global search fallback) and add to Up Next.
Listening Feedback Loop
After episodes are queued, the system tracks what happens to them:
| Outcome | Condition | Signal |
|---|---|---|
completed |
playingStatus == 3 or >= 90% played | Strong positive |
partial |
>= 50% played | Mild positive |
abandoned |
< 50% played, queued > 48h ago | Negative |
skipped |
Never appeared in history, queued > 72h ago | Strong negative |
starred |
User starred in Pocket Casts | Very strong positive |
These signals are stored in data/podcast/curation/listening_signals.jsonl and included in the scoring prompt for future runs. The system reports per-podcast and per-category completion rates, top correct predictions, and mispredictions.
Listened-Episode Digest
Location: scripts/media/listened_digest.py (digest_listened_episodes())
The curation feedback loop only sees episodes the system itself queued. The digest closes that gap: it queries the full Pocket Casts listening history directly and pulls anything substantively listened to (default min_played_minutes=15) into the transcription/summary pipeline β so episodes bio-zack found on his own get captured too.
For each listened episode (up to max_episodes=5 per run):
- Classify outcome β
completed(playingStatus 3 or β₯90%),partial(β₯50%), orsampled(below). - Find or create transcript β reuse an existing AssemblyAI transcript (title fuzzy-match over a 14-day lookback) or download audio and transcribe it via
PodcastTranscriber. - Find or create summary β reuse an existing summary or generate one with
PodcastSummarizer. - Save a digest record to
data/podcast/digests/YYYY-MM-DD.jsonl. - Notify β for completed episodes with a summary, queue a Telegram message with the top takeaways (
trigger="listened_digest", dedup keyed on episode UUID).
Already-digested UUIDs are tracked across the digest files so each episode is processed once. get_recent_digests(days) exposes records for surfaces like the /now dashboard.
# Full run
uv run python scripts/media/listened_digest.py
# Preview what would be digested
uv run python scripts/media/listened_digest.py --dry-run
Episode Source: Subscription Feed
Adapter: scripts/media/sources/podcasts.py (PodcastAdapter)
Podcast subscriptions are managed via OPML export from Pocket Casts, stored at data/podcasts/subscriptions.opml. The adapter parses this file and fetches RSS feeds using feedparser.
- Poll interval: every 4 hours
- Max episodes per feed: 3 (configurable)
- Max age: 7 days (
max_age_days=7) - Episodes enter the media pipeline as
source: "podcast"items - Triage scoring happens at the media pipeline level
- Higher-scoring episodes may get transcribed via AssemblyAI
Transcription Pipeline
Location: scripts/media/transcribe.py (PodcastTranscriber)
High-scoring podcast episodes (above the transcribe_score threshold) are transcribed via AssemblyAI:
- Download audio to temp directory (cached by URL hash)
- Upload to AssemblyAI with speaker labels enabled
- Store transcript in
data/media/transcripts/YYYY-MM-DD.jsonl - Cost:
price_per_minute($0.012/min), budget-capped attranscribe_daily_usd
Note β config overrides: the dataclass defaults in
scripts/media/config.pyaretranscribe_score=75andtranscribe_daily_usd=1.50, but the liveconfig/media-intel.yamloverrides them to a low transcribe threshold (25) and a higher daily budget ($3.00). The YAML values win at runtime, so in practice the system transcribes generously.
Podcast Summarization
Location: scripts/media/podcast_summary.py (PodcastSummarizer)
Transcribed episodes are summarized by LLM:
- Executive summary (2-3 paragraphs)
- Key topics (3-7)
- Highlights with speaker attribution (3-5 notable quotes)
- Actionable takeaways (3-5)
Summaries stored in data/media/podcast_summaries/YYYY-MM-DD.jsonl.
Daily Brief (VDB Podcast)
Location: scripts/podcast/generate_transcript.py
After the VDB email is sent (~5-6:30 AM PT), the scheduler generates a podcast episode:
-
Gather context - Same
gather_context()used for the VDB email, imported fromscripts.vdb.gather_context(sleep, calendar, tasks, media signal, etc.). The daily brief is directly coupled to the VDB context builder. -
Generate transcript - Claude (Sonnet 4.6) via the OpenRouter HTTP API (
anthropic/claude-sonnet-4-6,temperature=0.8,max_tokens=16000) generates a 600-1000 word conversational narrative. The prompt instructs silicon-zack voice: casual, direct, analytical. Content priority: sleep/body, day ahead, the one thing, action items, training, signal, overnight activity. (Afind_claude_binary()helper exists in the module but is unused dead code β generation goes over HTTP, not the local CLI.) -
Synthesize audio - ElevenLabs TTS with cloned voice (
generate.py). Text is cleaned (markdown stripped, unicode normalized), chunked at 4500 chars (MAX_CHUNK_CHARS), and synthesized with context-aware boundaries (previous/next ~500 chars passed for continuity). Engineeleven_multilingual_v2, output formatmp3_44100_128. Falls back to Kokoro ONNX if no ElevenLabs key is configured. -
Publish - Episode metadata appended to
data/podcast/episodes.json. MP3 saved todata/podcast/episodes/, transcript todata/podcast/transcripts/.
Credentials: OpenRouter API key at ~/.config/vita/openrouter.json ({"api_key": "..."}).
RSS Feed
The daily brief β and every other generated episode β is served as a single private podcast feed:
- Feed URL:
/api/v1/podcast/{token}/feed.xml - Episode serving (by filename):
/api/v1/podcast/{token}/episodes/{filename} - Cover art:
/api/v1/podcast/{token}/cover.jpg - Token: Random hex string stored in
data/podcast/.feed_token(get_or_create_feed_token()) - Discovery:
GET /api/v1/podcast/feed-urlreturns the subscription URL + token
Two access-control schemes coexist:
- Token-in-path for the feed and direct file serving. A wrong token returns
404, and the filename path is sanitized against traversal (..,/,\). - HMAC-signed play URLs for episode-by-ID access.
/episodesreturns each episode with aplay_urlof the form/play/{episode_id}?sig=..., wheresigis the first 16 hex chars ofHMAC-SHA256(feed_token, episode_id).GET /play/{episode_id}verifies the signature withhmac.compare_digest(403 on mismatch) before serving the MP3 β so a play link can be shared without exposing the feed token itself.
Subscribe in Pocket Casts or any podcast app via the feed URL. The token in the path prevents public discovery.
Episode Characteristics
Typical daily brief episode:
- Duration: 4-6 minutes
- Size: ~300-500 KB (MP3 128kbps 44.1kHz)
- Character count: ~5000-5700
- Engine: ElevenLabs
eleven_multilingual_v2
Idempotency
generate_transcript.py checks episodes.json before generating. If an ElevenLabs "Daily Brief" episode already exists for today, it skips (unless --force or --preview).
Deep Produced Episodes (podcast-deep)
Location: flows/podcast-deep/ (Stepwise flow, FLOW.yaml version 4.0)
Beyond the short daily brief, podcast-deep is a full produced-podcast generator for 15-40 minute episodes. It publishes to the same RSS feed (data/podcast/episodes.json + .feed_token). The flow chains agent steps (Claude with web browsing), LLM steps (OpenRouter), and Python run steps.
Run:
stepwise run podcast-deep --name "my-episode" \
--input topic="The future of autonomous agents" \
--config host_name="Your Name" \
--config host_perspective="AI, software engineering, philosophy"
Inputs: topic (required), guidance, audience (general|technical|expert), episode_type (deep_dive|briefing|explainer).
Pipeline steps (FLOW.yaml):
| Step | Executor | Model / tool | Purpose |
|---|---|---|---|
research |
agent (claude) | web browse + search | Deep-dive research (cost cap $5, β€25 min) |
scratchpad |
llm | anthropic/claude-sonnet-4.6 |
Creative angles, hooks, analogies (temp 0.8) |
outline |
llm | claude-sonnet-4.6 |
Section structure + target minutes (temp 0.6) |
fill-transcript |
agent (claude) | β | Full transcript from outline (cost cap $3, β€15 min) |
rubric-critique |
llm | claude-sonnet-4.6 |
Score draft + emit targeted edits (temp 0.3) |
targeted-refine |
llm | claude-sonnet-4.6 |
Apply edits surgically (temp 0.7) |
text-quality-check / repair-transcript |
script + agent | text_quality_check.py |
Validate length/format; repair loop (up to ~3x) |
chunk-and-tag |
script | chunk_and_tag.py |
Split into TTS chunks with ElevenLabs v3 expression tags |
synthesize |
script | synthesize.py (ElevenLabs eleven_v3) |
Per-chunk TTS |
audio-assembly |
script | audio_assembly.py (ffmpeg) |
Concatenate, crossfade, two-pass LUFS normalize (target -16 LUFS) |
publish |
script | publish.py |
Register metadata, save transcript, update RSS feed |
Episode types map to iTunes episode types in the feed (deep_dive β full, briefing/quick β bonus). The flow's cloned voice id defaults to the same 4NykfJgp4HqPHbp1OwTB as the daily brief, configurable via the voice_id config. ElevenLabs key resolution order: STEPWISE_VAR_ELEVENLABS_API_KEY β ELEVENLABS_API_KEY β secrets.toml in the flow directory.
Note: the text-quality gate is a single repair pass with verification rather than a true loop β Stepwise loop semantics don't reliably re-trigger upstream
any_ofcheck steps, so the flow usesinitial-check β repair-transcript β text-quality-checkwith an escalate exit if still failing.
API Endpoints
Podcast Feed (api/src/routers/podcast.py)
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/v1/podcast/{token}/feed.xml |
GET | Token in URL | RSS feed for podcast apps |
/api/v1/podcast/{token}/episodes/{filename} |
GET | Token in URL | Serve MP3 file (path-traversal guarded) |
/api/v1/podcast/{token}/cover.jpg |
GET | Token in URL | Serve podcast cover art |
/api/v1/podcast/play/{episode_id} |
GET | HMAC sig query param |
Serve MP3 by episode ID (signature-verified) |
/api/v1/podcast/episodes |
GET | None | List episodes with play_url + has_transcript flags |
/api/v1/podcast/episodes/{episode_id}/transcript |
GET | None | Return the episode transcript text |
/api/v1/podcast/feed-url |
GET | None | Get subscription URL + token |
Podcast Curation (api/src/routers/podcast_curation.py)
| Endpoint | Method | Description |
|---|---|---|
/api/v1/podcast-curation/candidates |
GET | Scored candidates (filterable by score, category, search) |
/api/v1/podcast-curation/candidates/{item_id} |
GET | Single candidate detail |
/api/v1/podcast-curation/queue-log |
GET | History of episodes pushed to Pocket Casts |
/api/v1/podcast-curation/stats |
GET | Aggregated stats (score distribution, top podcasts, costs) |
/api/v1/podcast-curation/summaries |
GET | Episode summaries with topics and highlights |
/api/v1/podcast-curation/rubric |
GET | Current rubric dimensions, penalties, boosters, queue rules |
Candidate query params: sort (score|recent), min_score, category, search, limit, offset
The API runs on port 33800 (
./run api). See Architecture for the full service map.
Scheduler Integration
| Task | Interval | Script | Purpose |
|---|---|---|---|
media_podcast |
4 hours (90s startup delay) | scripts/media/run_pipeline.py podcast |
Fetch new episodes from subscriptions |
podcast_curation |
12 hours (300s delay) | scripts/media/podcast_curator.py |
Score + queue to Pocket Casts |
listened_digest |
2 hours | scripts/media/listened_digest.py |
Bridge listened episodes into transcription/summary |
| VDB podcast | After VDB send (~5-6:30 AM) | scripts/podcast/generate_transcript.py |
Daily brief audio |
The podcast fetch runs with a 90-second startup delay; curation runs with a 300-second delay (to let the fetch run first). Interval constants live in api/src/scheduler.py (MEDIA_PODCAST_INTERVAL, PODCAST_CURATION_INTERVAL, LISTENED_DIGEST_INTERVAL).
CLI Usage
# Full curation run
uv run python scripts/media/podcast_curator.py
# Dry run (score but don't push)
uv run python scripts/media/podcast_curator.py --dry-run
# Show candidates only
uv run python scripts/media/podcast_curator.py --candidates
# Show identity context used for scoring
uv run python scripts/media/podcast_curator.py --context
# Clean up finished episodes only
uv run python scripts/media/podcast_curator.py --cleanup
# Show listening signal feedback
uv run python scripts/media/podcast_curator.py --signals
# Digest listened episodes
uv run python scripts/media/listened_digest.py
uv run python scripts/media/listened_digest.py --dry-run
# Generate daily brief (transcript + audio)
uv run python scripts/podcast/generate_transcript.py
# Preview transcript only (no audio)
uv run python scripts/podcast/generate_transcript.py --preview
# Force regeneration
uv run python scripts/podcast/generate_transcript.py --force
# List ElevenLabs voices
uv run python scripts/podcast/generate.py voices
# Produce a deep episode
stepwise run podcast-deep --name "my-episode" --input topic="..."
# Pocket Casts CLI
uv run python scripts/integrations/pocketcasts.py queue
uv run python scripts/integrations/pocketcasts.py subs
uv run python scripts/integrations/pocketcasts.py new
uv run python scripts/integrations/pocketcasts.py search "AI agents"
File Locations
| File | Purpose |
|---|---|
scripts/media/podcast_curator.py |
Curation engine (scoring, selection, queue push) |
scripts/media/listened_digest.py |
Listened-episode digest bridge |
scripts/media/sources/podcasts.py |
OPML parser + RSS episode adapter |
scripts/media/transcribe.py |
AssemblyAI transcription pipeline |
scripts/media/podcast_summary.py |
LLM-based transcript summarization |
scripts/media/curate_podcasts_setup.py |
One-shot setup + first curation run |
scripts/integrations/pocketcasts.py |
Pocket Casts API client |
scripts/podcast/generate.py |
ElevenLabs TTS episode generation |
scripts/podcast/generate_transcript.py |
Daily brief transcript (Claude via OpenRouter) |
scripts/podcast/generate_initial.py |
Initial episode generation helper |
flows/podcast-deep/FLOW.yaml |
Deep produced-episode Stepwise flow |
api/src/routers/podcast.py |
Feed + episode serving endpoints |
api/src/routers/podcast_curation.py |
Curation API endpoints |
api/src/schemas/podcast_curation.py |
Pydantic schemas for curation API |
config/podcast-curation.yaml |
Rubric, penalties, boosters, queue rules |
config/media-intel.yaml |
Live transcribe thresholds + budgets (overrides config.py defaults) |
data/podcast/episodes.json |
Episode metadata (all generated episodes) |
data/podcast/episodes/*.mp3 |
Generated audio files |
data/podcast/transcripts/*.txt |
Generated episode transcripts |
data/podcast/.feed_token |
RSS feed access token |
data/podcast/cover.jpg |
RSS feed cover art |
data/podcast/curation/candidates.jsonl |
All scored curation candidates |
data/podcast/curation/queue_log.jsonl |
Episodes pushed to Pocket Casts |
data/podcast/curation/listening_signals.jsonl |
Listening outcome feedback |
data/podcast/curation/pc_sync_state.json |
Pocket Casts sync state |
data/podcast/digests/*.jsonl |
Listened-episode digest records |
data/podcasts/subscriptions.opml |
Podcast subscriptions (OPML export) |
data/media/items/YYYY-MM-DD.jsonl |
Raw podcast items (media pipeline) |
data/media/scores/YYYY-MM-DD.jsonl |
Triage scores (media pipeline) |
data/media/transcripts/YYYY-MM-DD.jsonl |
AssemblyAI transcripts |
data/media/podcast_summaries/YYYY-MM-DD.jsonl |
Episode summaries |
~/.config/vita/pocketcasts.json |
Pocket Casts credentials |
~/.config/vita/pocketcasts_tokens.json |
Cached auth tokens |
~/.config/vita/openrouter.json |
OpenRouter API key (daily brief transcript) |
External Integrations
| Integration | Purpose | Auth | Cost |
|---|---|---|---|
| Pocket Casts API | Queue management, listening history | Email/password login | Free |
| OpenRouter (Gemini) | Curation scoring, triage, summarization | API key | ~$0.01-0.05/run |
| OpenRouter (Claude Sonnet 4.6) | Daily brief transcript generation | API key | ~per-episode tokens |
| ElevenLabs | Daily brief + deep-episode TTS | API key | ~$0.15-0.30/brief, more per deep episode |
| AssemblyAI | Podcast transcription | API key | $0.012/minute |
| Feedparser | RSS episode fetching | None | Free |
Known Limitations
- Pocket Casts API is unofficial - Reverse-engineered from mobile apps, may break on API changes.
- Discovery search is keyword-based - Limited to Pocket Casts search, no semantic discovery.
- Listening signals need time - Outcomes classified after 48-72h delay; cold start with < 5 signals skips listening context.
- UUID resolution is fuzzy - Title matching can occasionally match wrong episodes; the listened-digest also uses fuzzy title matching to reuse existing transcripts.
- Daily brief is English-only - ElevenLabs multilingual model used but transcript generated in English.
- OPML requires manual export - Subscription list updated via
/sync-podcastsbrowser skill, not automatic. - Deep-episode quality gate is single-pass - Stepwise loop semantics force a single repair pass with verification rather than a true retry loop.
Where to Go Next
- VDB β the daily brief reuses the VDB context builder.
- Media β the signal refinery that supplies podcast triage candidates.
- Stepwise / Flows β how
podcast-deepis orchestrated. - Model Guidance β current model roster and routing.