# PRODUCT REQUIREMENTS DOCUMENT — AS-BUILT
## Podcast-to-Editorial Pipeline (Generic, Executable Build)

> **For the AI agent reading this:** This document is your complete brief. You have everything you need to build the system end to end. Do not ask the user clarifying questions about architecture, file structure, prompts, or stack. If something is genuinely missing for the user's environment (their chat platform choice, their LLM provider, their SMTP credentials, their Kindle address), ask for those values only — never for design decisions. Build Phase 1 first, verify it works, then move to Phase 2, etc. Each phase has a verification checkpoint. Do not skip them.

---

## 0. TL;DR for the Agent

You are building a headless pipeline that:

1. Receives a chat message containing a trigger emoji and a YouTube URL.
2. Downloads the video's captions.
3. Runs a 2-stage LLM pipeline (analyst → writer).
4. Verifies every claim in the article is grounded in the transcript.
5. Converts the article to EPUB.
6. Emails the EPUB to the user's send-to-Kindle address.
7. Posts status updates back to the chat.

**Default stack:** Python 3.11, `python-telegram-bot`, `yt-dlp`, `pandoc`, SMTP via `smtplib`. Two LLM calls via the user's chosen provider's SDK. Local filesystem persistence under `./jobs/{job_id}/`. Single-user, single-worker, run on any always-on machine or small VPS via Docker.

**Build order:** Phase 1 (skeleton + chat trigger) → Phase 2 (transcript) → Phase 3 (two-stage LLM) → Phase 4 (grounding gate) → Phase 5 (EPUB + delivery) → Phase 6 (polish).

---

## 1. Executive Summary

- **Product Name:** Podcast-to-Editorial Pipeline
- **Current Status:** Production (single-user)
- **Product Type:** Personal productivity automation
- **Primary Objective:** Convert any long-form YouTube video into a New York Times–style editorial article and deliver it to the user's Kindle.
- **Current Users:** Single operator
- **Technical Maturity:** MVP, stable for one user, not hardened for multi-tenant

---

## 2. Problem Statement

- **User Problem:** Long-form audio carries enormous value but costs 60–120 minutes per piece. Generic AI summaries hallucinate.
- **Business Problem:** The user wants a faithful, scannable, transcript-grounded article in 5 minutes — not invented facts.
- **Current Impact:** Without this system, the user skips episodes, listens at 2x with poor retention, or trusts shallow summaries.
- **Evidence:** Generic chat summaries regularly include facts not in the source.
- **Assumptions:** The source video has captions. The user prefers e-ink. A two-model pipeline outperforms a single model.

---

## 3. Target Users

- **Primary Persona:** A solo operator / researcher / knowledge worker who consumes long audio, owns a Kindle, is comfortable with a chat trigger, demands zero hallucinations.
- **Secondary Persona:** A small newsletter operator turning audio into editorial drafts.
- **User Context:** Trigger at night, read in the morning.

**Jobs to Be Done:**
- Capture an episode's value without listening to it all.
- Trust that every fact came from the source.
- Wake up to the article, not to a task.

---

## 4. Current Product Overview

**Core Capabilities:**
1. Accept a YouTube URL via chat trigger (emoji + link).
2. Extract and clean the transcript.
3. Run analyst LLM → writer LLM.
4. Enforce a grounding gate.
5. Convert to EPUB and email to send-to-Kindle.

**Main Workflows:** Trigger → Transcript → Analysis → Writing → Grounding → Delivery.

**Key Screens:** None. Chat is the only UI.

**Main Entities:** Job, SourceVideo, Transcript, EditorialAnalysis, Article, DeliveryReceipt.

**Current Limitations:** Requires captions. Single output style. One delivery target.

---

## 5. Value Proposition

- **Primary Benefit:** 90 minutes of audio → 5 minutes of trustworthy reading, delivered automatically.
- **Differentiators:** Grounded against the transcript. Two-model architecture. Asynchronous overnight delivery.
- **Current Alternatives:** YouTube's built-in summary, generic chatbot summaries, podcast transcript view, manual note-taking — all either shallow, hallucinate-prone, or manual.

---

## 6. Scope

### 6.1 In Scope Today
- YouTube as only source.
- Caption-based transcripts.
- Two-stage LLM pipeline.
- Editorial long-form output.
- Email delivery to a send-to-Kindle address.
- Chat-based trigger.

### 6.2 Out of Scope Today
- Audio-only platforms (Spotify, Apple Podcasts).
- Whisper transcription of caption-less videos.
- Multi-language translation.
- Web reader / archive UI.
- Multi-user accounts.
- Payment.

### 6.3 Future Scope
- Whisper fallback.
- Multiple output styles (executive brief, Q&A, tactical).
- Web archive.
- Multi-user support.

---

## 7. Functional Requirements

### FR-01 — Chat Trigger
**Description:** The bot accepts a message containing a configurable trigger token followed by a YouTube URL.
**Status:** Implemented
**Acceptance Criteria:**
- Recognizes a configurable trigger token (`TRIGGER_EMOJI` env var, default `📚`).
- Extracts the first valid YouTube URL using regex.
- Rejects messages from any sender whose ID is not in `ALLOWED_USER_IDS`.
- Replies with a clear error if no valid URL is found.
- Acknowledges the job within 5 seconds.
- Replies with a generated `job_id` (UUID4, short form).

**Message format the bot must accept:**
```
📚 https://www.youtube.com/watch?v=VIDEO_ID
📚 https://youtu.be/VIDEO_ID
📚 https://youtube.com/watch?v=VIDEO_ID&t=120s
```

### FR-02 — Transcript Extraction
**Description:** Download captions for the supplied video and produce a clean transcript.
**Status:** Implemented
**Acceptance Criteria:**
- Uses `yt-dlp` to download subtitles (`--write-auto-subs --sub-langs en,es --skip-download --convert-subs vtt`).
- Falls back to auto-generated captions if manual ones are absent.
- Strips VTT timestamps, cue identifiers, and duplicate consecutive lines.
- Strips filler tokens (`[Music]`, `[Applause]`, `um`, `uh`, `you know`).
- Writes the cleaned transcript to `./jobs/{job_id}/transcript.txt`.
- Fails gracefully if no captions exist; replies in chat with `"No captions available for this video."`.

**Concrete `yt-dlp` command the agent should use:**
```bash
yt-dlp \
  --write-auto-subs \
  --write-subs \
  --sub-langs "en.*,es.*" \
  --sub-format "vtt" \
  --skip-download \
  --output "./jobs/{job_id}/raw.%(ext)s" \
  "{youtube_url}"
```

### FR-03 — Editorial Analysis (Stage 1 LLM)
**Description:** First LLM call reads the transcript and outputs a structured editorial analysis as JSON.
**Status:** Implemented
**Acceptance Criteria:**
- Sends the cleaned transcript to the analyst model with the prompt in **Appendix B**.
- Receives a JSON object matching the schema in Appendix B.
- Validates the JSON with `json.loads`; retries once on parse failure with a "your previous output was not valid JSON, return only valid JSON" instruction.
- Persists result to `./jobs/{job_id}/analysis.json`.

### FR-04 — Editorial Writing (Stage 2 LLM)
**Description:** Second LLM call takes the analysis + transcript and writes the article.
**Status:** Implemented
**Acceptance Criteria:**
- Sends the analysis JSON and the transcript to the writer model with the prompt in **Appendix A**.
- Article is Markdown.
- Article length between 1,200 and 2,500 words (count words; if outside, ask the model to revise).
- Persists result to `./jobs/{job_id}/article.md`.

### FR-05 — Grounding / Quality Gate
**Description:** Verify every factual claim in the article appears in the transcript.
**Status:** Implemented
**Acceptance Criteria:**
- Runs the prompt in **Appendix C** against the article + transcript.
- Receives a JSON list of `{claim, grounded: bool, evidence_span_or_null}`.
- If any `grounded == false`, sends flagged claims back to the writer model with a "revise: ground these claims in the transcript or remove them" instruction.
- Maximum 2 revision attempts.
- On success, persists the final article. On failure after retries, delivers the article anyway with a `⚠️ ungrounded claims detected` note in the chat reply, and saves the flagged list to `./jobs/{job_id}/ungrounded.json`.

### FR-06 — Format Conversion
**Description:** Convert the final Markdown article to EPUB.
**Status:** Implemented
**Acceptance Criteria:**
- Uses `pandoc` to convert.
- Output is a valid EPUB under 10 MB.
- Includes a title page with the article title, source video URL, and date.

**Concrete `pandoc` command:**
```bash
pandoc \
  --from markdown \
  --to epub3 \
  --metadata title="{article_title}" \
  --metadata author="Podcast-to-Editorial Pipeline" \
  --metadata date="{iso_date}" \
  --output "./jobs/{job_id}/article.epub" \
  "./jobs/{job_id}/article.md"
```

### FR-07 — Delivery
**Description:** Email the EPUB to the user's send-to-Kindle address.
**Status:** Implemented
**Acceptance Criteria:**
- Sends via SMTP using credentials from env vars.
- The `From:` address must match `SMTP_FROM_ADDRESS` (which the user must whitelist on Amazon).
- Email subject: `convert` (this triggers Amazon's auto-conversion; or use any subject if sending EPUB directly).
- Attaches `article.epub`.
- Retries up to 3 times on transient SMTP failure with 30s backoff.
- On success, posts confirmation to chat with job_id and total elapsed time.

### FR-08 — Job Status Notifications
**Description:** Bot reports each stage transition in the chat thread.
**Status:** Implemented
**Acceptance Criteria:**
- Posts: `📥 trigger received`, `📝 transcript extracted`, `🧠 analysis done`, `✍️ article written`, `✅ delivered to Kindle`.
- Posts errors as: `❌ {stage} failed: {one-line reason}`.
- All status messages are replies to the original trigger message (threaded if the chat platform supports it).

---

## 8. Non-Functional Requirements

| Category | Requirement |
|---|---|
| **Performance** | End-to-end pipeline under 10 minutes for ≤2hr videos |
| **Performance** | Each LLM call under 3 minutes |
| **Scalability** | Single concurrent job. Multi-job requires a queue (out of scope for v1) |
| **Security** | All secrets in env vars; never in source or logs |
| **Security** | Chat bot validates sender ID against allowlist before any processing |
| **Availability** | Best-effort. Target 95% successful completion |
| **Reliability** | Each stage is idempotent; artifacts persisted per job |
| **Observability** | Structured log line per stage with timestamps and token counts |
| **Maintainability** | LLM client behind a small adapter interface so providers can be swapped |

---

## 9. System Architecture

```
┌─────────────────┐
│  Chat Platform  │  (Telegram | Slack | iMessage relay)
│   (webhook)     │
└────────┬────────┘
         │ POST update
         ▼
┌─────────────────────────────────────────────────────────────┐
│                  Worker Process (Python 3.11)               │
│                                                             │
│  ┌──────────┐  ┌─────────────┐  ┌──────────┐  ┌──────────┐ │
│  │  Bot     │→ │ Pipeline    │→ │ Grounder │→ │ Builder  │ │
│  │ Listener │  │  Runner     │  │  (LLM)   │  │ (pandoc) │ │
│  └──────────┘  └─────┬───────┘  └──────────┘  └─────┬────┘ │
│                      │                              │      │
│                      ▼                              ▼      │
│            ┌────────────────┐              ┌────────────┐  │
│            │ yt-dlp (subs)  │              │   SMTP     │  │
│            └────────────────┘              │  sender    │  │
│                      │                     └─────┬──────┘  │
│                      ▼                           │         │
│            ┌────────────────┐                    │         │
│            │  LLM Provider  │                    │         │
│            │ (analyst+writer)│                   │         │
│            └────────────────┘                    │         │
└──────────────────────────────────────────────────┼─────────┘
                                                  │
                                                  ▼
                                       ┌──────────────────┐
                                       │ send-to-Kindle   │
                                       │ (kindle.com)     │
                                       └──────────────────┘
```

**Stack defaults the agent should use unless the user specifies otherwise:**

| Component | Default | Why |
|---|---|---|
| Language | Python 3.11 | `yt-dlp` is Python; LLM SDKs are first-class |
| Chat platform | Telegram via `python-telegram-bot` | Easiest free bot, mobile-first |
| Caption tool | `yt-dlp` (CLI) | Best-maintained YouTube extractor |
| LLM provider | User's choice (Anthropic, OpenAI, etc.) | Abstract behind `llm_client.py` |
| EPUB tool | `pandoc` | Standard, scriptable |
| SMTP | User's provider | Any SMTP works |
| Persistence | Local filesystem | Single user, no DB needed |
| Hosting | Docker on any always-on machine | 1 vCPU / 1 GB RAM is enough |

---

## 10. Data Model Overview

**Persisted per job under `./jobs/{job_id}/`:**

| File | Contents |
|---|---|
| `meta.json` | `{job_id, source_url, sender_id, created_at, status, error}` |
| `raw.en.vtt` (or es) | Raw subtitle file from yt-dlp |
| `transcript.txt` | Cleaned plain-text transcript |
| `analysis.json` | Stage 1 LLM output (see Appendix B) |
| `article.md` | Final Markdown article |
| `ungrounded.json` | (optional) List of claims that failed the gate |
| `article.epub` | Final EPUB |
| `delivery.json` | `{sent_at, smtp_message_id, success}` |
| `pipeline.log` | Structured per-stage log lines |

**Sensitive data:** Kindle email, chat user ID, LLM API key, SMTP credentials. All in `.env`, never in `meta.json`.

**Retention:** Purge jobs older than 30 days via a daily cron / scheduled task.

---

## 11. User Flows

**Flow 1 — Happy Path:**
Trigger → bot ack with `job_id` → caption download → cleaning → analysis → writing → grounding (pass) → EPUB → email → confirmation.

**Flow 2 — Unauthorized Sender:**
Message from non-allowlisted user → bot silently drops (no reply, to avoid revealing the bot exists).

**Flow 3 — Grounding Retry:**
Gate flags claims → writer revises → re-verify → pass on attempt 2 or 3 → deliver. If still failing: deliver with warning + persist `ungrounded.json`.

**Flow 4 — No Captions:**
yt-dlp returns no subs → bot replies `❌ transcript failed: no captions available for this video` → job marked failed.

---

## 12. Edge Cases

The agent must handle all of these explicitly:

- Video has no captions in any language → fail clearly.
- Video is age-restricted, private, or removed → fail clearly.
- Captions are auto-generated and noisy → run through cleaner; do not fail.
- Video is over 4 hours → check token budget; chunk if needed, summarize per chunk in analysis stage, write from the merged analysis.
- LLM provider rate-limits → exponential backoff (1s, 2s, 4s, 8s, 16s), max 5 attempts.
- LLM returns invalid JSON in analysis stage → retry once with "your output must be valid JSON only."
- Article length out of 1,200–2,500 range → ask writer to revise once.
- SMTP rejects size → fail, post error.
- User sends two URLs in one message → process only the first; reply noting the second was ignored.
- User sends trigger emoji without URL → reply with usage hint.
- Network failure between stages → resume from last persisted artifact on retry.
- Two trigger messages arrive concurrently → queue; process FIFO.

---

## 13. Known Issues and Technical Debt

- No formal queue: a long job blocks subsequent triggers.
- Grounding gate is an LLM judge — imperfect; a rule-based pre-check could reduce false positives.
- No network-layer retry on transient 5xx errors from LLM HTTP calls.
- Cleaning is heuristic; some caption formats may be over-stripped.
- No automated tests.

---

## 14. Risks and Mitigations

| Risk | Impact | Mitigation |
|---|---|---|
| LLM provider deprecates a model | Quality shifts | Pin model versions; abstract LLM client |
| `yt-dlp` breaks due to YouTube changes | Hard failure | Keep `yt-dlp` updated; add Whisper fallback (future) |
| Amazon changes send-to-Kindle | Delivery fails | Fall back to regular email of the EPUB |
| Hallucination slips past the gate | Trust erosion | Spot-check articles; tune the grounding prompt |
| Chat bot platform changes API | Bot down | Abstract chat client behind an adapter |

---

## 15. Assumptions

- The user has API access to at least one frontier LLM provider.
- The user has an always-on machine or VPS to run Docker.
- The user has a Kindle and a configured send-to-Kindle address.
- The user has registered a bot on their chat platform of choice.
- Source videos have captions available.
- The user is the only authorized sender.

---

## 16. Decision Log

| Decision | Reason | Alternatives | Owner |
|---|---|---|---|
| Two-model pipeline | Higher editorial quality | Single-model | Builder |
| Caption-based transcripts | Fast, free, accurate enough | Whisper on audio | Builder |
| Chat trigger | One-tap from phone | Web form, RSS poll | Builder |
| Kindle as delivery | E-ink reading is the goal | Web app, email-only | Builder |
| Markdown intermediate format | Easy to debug, easy to convert | HTML, direct EPUB | Builder |
| Local filesystem | Simplest reliable store | Cloud DB, S3 | Builder |
| Python 3.11 | yt-dlp + LLM SDK ecosystem | Node.js | Builder |
| Telegram (default) | Best free bot API | Slack, Discord | Builder |

---

## 17. Success Metrics

**Product:** end-to-end success rate ≥ 95%; average latency ≤ 10 min; grounding first-pass rate ≥ 90%.
**Technical:** LLM cost per article tracked; token usage per stage logged; failure rate per stage tracked.

---

## 18. Roadmap

**Phase 1 — Stabilization:** formal job queue; per-stage retries with backoff; structured logging; tests for cleaner and gate.
**Phase 2 — Improvement:** Whisper fallback; multiple output styles; per-job style selection; web archive.
**Phase 3 — Scaling:** multi-user with per-user profiles and Kindle addresses; landing page; monetization.

---

## 19. Open Questions

- Should the grounding gate be LLM-only, rule-based, or hybrid?
- Ideal article length for Kindle reading sessions?
- Long-term transcript storage for search, or purge for privacy?
- Which extra output styles are worth building?
- Should the delivery target be per-job configurable?

---

# ============================================================
# IMPLEMENTATION SPEC — EVERYTHING THE AGENT NEEDS TO BUILD
# ============================================================

## A. Environment Variables

The agent must create `.env.example` with this exact set. The user fills in real values.

```bash
# === Chat platform (Telegram default) ===
TELEGRAM_BOT_TOKEN=
ALLOWED_USER_IDS=          # comma-separated Telegram user IDs
TRIGGER_EMOJI=📚

# === LLM provider (pick one set) ===
# Anthropic:
ANTHROPIC_API_KEY=
ANALYST_MODEL=claude-sonnet-4-6
WRITER_MODEL=claude-opus-4-7
GROUNDER_MODEL=claude-sonnet-4-6
# OpenAI alternative:
# OPENAI_API_KEY=
# ANALYST_MODEL=gpt-5
# WRITER_MODEL=gpt-5
# GROUNDER_MODEL=gpt-5-mini

# === Delivery ===
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=         # MUST be whitelisted on Amazon
KINDLE_TO_ADDRESS=         # your @kindle.com address

# === Runtime ===
JOBS_DIR=./jobs
LOG_LEVEL=INFO
```

## B. File / Folder Structure

```
podcast-to-editorial/
├── .env.example
├── .gitignore                 # must ignore .env and ./jobs/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── README.md
├── prompts/
│   ├── analyst.md             # Appendix B of this PRD
│   ├── writer.md              # Appendix A of this PRD
│   └── grounder.md            # Appendix C of this PRD
├── src/
│   ├── __init__.py
│   ├── main.py                # entry point: starts bot listener
│   ├── config.py              # loads env vars, validates
│   ├── bot.py                 # chat adapter (Telegram default)
│   ├── pipeline.py            # orchestrator
│   ├── stages/
│   │   ├── __init__.py
│   │   ├── transcript.py      # yt-dlp wrapper + cleaner
│   │   ├── analyze.py         # stage 1 LLM call
│   │   ├── write.py           # stage 2 LLM call
│   │   ├── ground.py          # grounding gate + retry loop
│   │   ├── build_epub.py      # pandoc wrapper
│   │   └── deliver.py         # SMTP sender
│   ├── llm_client.py          # adapter; swap providers behind one interface
│   ├── storage.py             # ./jobs/{id}/ helpers
│   └── logger.py              # structured JSON logs
└── jobs/                      # created at runtime; gitignored
```

## C. `requirements.txt`

```
python-telegram-bot>=21.0
yt-dlp>=2024.10.0
anthropic>=0.40.0          # or openai>=1.50.0
python-dotenv>=1.0.0
pydantic>=2.0
```

System dependencies the Dockerfile must install:
- `pandoc`
- `ffmpeg` (yt-dlp dependency)

## D. Dockerfile (minimal working)

```dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    pandoc ffmpeg \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

CMD ["python", "-m", "src.main"]
```

## E. `llm_client.py` Adapter Interface

The agent must implement this interface so providers are swappable:

```python
class LLMClient(Protocol):
    def complete(
        self,
        system_prompt: str,
        user_message: str,
        model: str,
        max_tokens: int = 4096,
        response_format: Literal["text", "json"] = "text",
    ) -> str: ...
```

Provide one concrete implementation (e.g., `AnthropicClient`) and document how to add another.

## F. Phased Build Plan with Verification Checkpoints

### Phase 1 — Skeleton + Chat Trigger
**Build:** project scaffold, env loader, Telegram bot listener, allowlist check, trigger parser, job ID generator, status reply.
**Verify:** Send `📚 https://youtu.be/xxx` from an allowed user → bot replies with a job_id. Send from unauthorized user → no reply. Send `📚` without URL → usage error reply.

### Phase 2 — Transcript Extraction
**Build:** `stages/transcript.py` with yt-dlp wrapper + cleaner, persistence to `transcript.txt`.
**Verify:** Run on a real video → `./jobs/{id}/transcript.txt` exists, contains plain text, no VTT timestamps, no `[Music]` markers.

### Phase 3 — Two-Stage LLM Pipeline
**Build:** `llm_client.py`, `stages/analyze.py`, `stages/write.py`, prompts loaded from `prompts/*.md`.
**Verify:** Job produces `analysis.json` (valid against schema) and `article.md` (1,200–2,500 words, valid Markdown).

### Phase 4 — Grounding Gate
**Build:** `stages/ground.py` with the prompt from Appendix C, retry loop (max 2), `ungrounded.json` output on failure.
**Verify:** Inject a fabricated fact into a draft → gate flags it. A clean article passes on attempt 1.

### Phase 5 — EPUB + Delivery
**Build:** `stages/build_epub.py` (pandoc), `stages/deliver.py` (SMTP).
**Verify:** EPUB opens in Calibre. Email arrives at Kindle. Confirmation appears in chat.

### Phase 6 — Polish
**Build:** structured logging, per-stage retries with exponential backoff, cron job to purge old `./jobs/` directories, `README.md` with setup instructions.
**Verify:** Logs show one structured line per stage with elapsed ms and token counts. Old jobs are purged after 30 days.

---

# ============================================================
# APPENDIX A — WRITER PROMPT (Stage 2)
# ============================================================

Save this as `prompts/writer.md`. Loaded as the system prompt for the writer model.

```
You are a senior editorial writer for a publication with the standards of The New York Times, The Atlantic, and The Economist. You will receive two inputs:

1. TRANSCRIPT — the cleaned transcript of a long-form video.
2. ANALYSIS — a JSON object with the central theme, key quotes, and three ranked editorial angles.

Your job is to write a single long-form editorial article based on these inputs.

# Hard rules — non-negotiable

1. Zero invention. Every fact, claim, statistic, anecdote, and quote in your article must be present in the TRANSCRIPT. If something is not in the transcript, it does not exist. You may not infer, extrapolate, or fill gaps with general knowledge.
2. Quotes are verbatim. Any text inside quotation marks must match the transcript word-for-word. Paraphrase freely outside of quotation marks, but never put words in the speaker's mouth.
3. No hedging filler. Do not write "it is important to note," "in today's world," "in conclusion," or similar empty connective tissue.
4. No bullet lists in the body. This is an editorial article, not a blog post. Lists are permitted only when the speaker themselves enumerates something in the transcript.
5. Pick one angle. Choose the highest-ranked angle from ANALYSIS and commit to it. Do not try to cover all three.

# Structure

1. Lede (1 paragraph). A specific, concrete opening drawn from the transcript. Not a thesis statement.
2. Nut graf (1 paragraph). State the central tension or claim. Why does this matter?
3. Body (4–8 sections). Each section advances the chosen angle using transcript material. Subheadings only when they earn their place. Weave verbatim quotes into prose; avoid block quotes unless genuinely standalone.
4. Kicker (1 paragraph). Close on an image, a quote, or a sharpened restatement. Do not summarize. Do not say "in conclusion."

# Voice

- Confident, third-person, observational.
- Sentences vary in length. Many short. Some long enough to carry a full idea without breaking.
- Concrete nouns over abstract ones. Specific names, numbers, places — but only the ones in the transcript.
- No second-person ("you"). No first-person plural ("we").

# Length

Between 1,200 and 2,500 words. Err shorter if the transcript thins out; never pad.

# Output

Return the article as Markdown:
- One H1 title (your own, drawn from the angle).
- A one-line italic standfirst under the title, under 25 words.
- The body as described.

Output only the article. No preamble. No meta-commentary.
```

---

# ============================================================
# APPENDIX B — ANALYST PROMPT (Stage 1)
# ============================================================

Save as `prompts/analyst.md`.

```
You are an editorial researcher. You will receive a cleaned TRANSCRIPT of a long-form video. Your job is to produce a structured analysis a writer will use to draft an article.

Return a JSON object with this exact shape:

{
  "central_theme": "One sentence stating what this conversation is actually about, beneath the surface topic.",
  "key_quotes": [
    {
      "quote": "verbatim quote from the transcript",
      "speaker": "name if known else 'speaker'",
      "why_it_matters": "one sentence"
    }
  ],
  "angles": [
    {
      "rank": 1,
      "headline": "A concrete editorial headline",
      "thesis": "One sentence stating the argument",
      "supporting_points": ["point 1 grounded in transcript", "point 2", "point 3"],
      "impact_score": "1-10 with a one-line justification"
    }
  ]
}

Rules:
- Provide 5 to 10 key quotes.
- Provide exactly 3 angles, ranked 1 (strongest) to 3.
- Every supporting point must reference material actually in the transcript.
- Do not invent. If the transcript is thin, return fewer quotes rather than fabricate.
- Output only valid JSON. No preamble, no code fences.
```

---

# ============================================================
# APPENDIX C — GROUNDING GATE PROMPT
# ============================================================

Save as `prompts/grounder.md`.

```
You are a fact-checker. You will receive two inputs:

1. TRANSCRIPT — the source of truth.
2. ARTICLE — a draft editorial article.

Your job is to identify every factual claim in the ARTICLE and decide whether each one is grounded in the TRANSCRIPT.

A "factual claim" is any statement that asserts something concrete: a number, a name, an event, a quote, an action taken, a date, a place, a cause-and-effect relationship.

For each claim, decide:
- grounded: true if the claim is directly supported by the transcript (the transcript explicitly states it or makes it unambiguously clear).
- grounded: false if the claim is plausible but the transcript does not say it, OR if the claim contradicts the transcript.

For each grounded claim, also provide the supporting transcript span (a short verbatim excerpt that proves it).

Return a JSON object with this exact shape:

{
  "claims": [
    {
      "claim": "the exact claim as stated in the article",
      "grounded": true,
      "evidence": "verbatim transcript span that supports it"
    },
    {
      "claim": "another claim",
      "grounded": false,
      "evidence": null
    }
  ],
  "overall_pass": true
}

Rules:
- "overall_pass" is true ONLY if every claim has grounded: true.
- Quoted speech inside the article (text in quotation marks) must match a transcript span verbatim; if it does not, mark grounded: false.
- Be strict. When in doubt, mark grounded: false.
- Output only valid JSON. No preamble, no code fences.
```

---

# ============================================================
# APPENDIX D — README TEMPLATE
# ============================================================

The agent must generate a `README.md` covering:

1. **What this is** (one paragraph).
2. **Prerequisites** — Docker, an LLM API key, an SMTP account, a chat bot token, a configured send-to-Kindle address.
3. **Setup** — copy `.env.example` to `.env`, fill in values, run `docker compose up -d`.
4. **First run** — how to find your chat user ID, how to whitelist the `From:` address on Amazon, how to send the first trigger.
5. **Troubleshooting** — common failures (no captions, SMTP auth, Kindle whitelist).
6. **Architecture** — short diagram + pointer to this PRD.

---

# ============================================================
# FINAL CHECKLIST FOR THE AGENT BEFORE DECLARING DONE
# ============================================================

- [ ] All six phases built and individually verified.
- [ ] `.env.example` complete; no real secrets in repo.
- [ ] `.gitignore` excludes `.env` and `./jobs/`.
- [ ] Dockerfile builds cleanly; container starts and listens.
- [ ] End-to-end test: trigger → article on Kindle within 10 minutes.
- [ ] Grounding gate provably catches a fabricated claim (inject one in a test).
- [ ] Unauthorized senders are ignored.
- [ ] All three prompts (`analyst.md`, `writer.md`, `grounder.md`) are loaded from files, not hardcoded.
- [ ] LLM client is behind an adapter; provider swap requires changing one file.
- [ ] Logs show one structured line per stage.
- [ ] README explains setup end to end.

When all boxes are checked, the system is done.
