# Prompt Intelligence System — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace 60+ hardcoded production constants across 8 files with config-driven values, add Flash enrichment layer, externalize system prompts, and capture generation data for debugging.

**Architecture:** Config files (`prompt_constants.json`, `lexicon.json`) as single source of truth, loaded by a shared `prompt_config.py` module. Flash enrichment sits between Python prompt builders and generation APIs, with model-specific versioned system prompts. Generation data (prompt text, seed, version) injected into per-take JSON in existing SQLite store.

**Tech Stack:** Python 3, SQLite (existing), Gemini Flash 3.1 API (existing), JSON config files

**Design Doc:** `docs/plans/2026-03-04-prompt-intelligence-system-design.md`
**Consultation Transcript:** `consultations/prompt_intelligence_system/` (4 rounds with Gemini 3.1 Pro)

---

## Phase 1: The Great Consolidation (D1 + D2 + D6)

Foundation: config files, loader module, data capture. Everything else depends on this.

---

### Task 1: Create prompt_constants.json

**Files:**
- Create: `starsend/config/prompt_constants.json`

**Step 1: Create the config file with Gemini-drafted canonical schema**

```json
{
  "_doc": "Canonical prompt constants. Single source of truth for all production and pre-production guard texts. See docs/plans/2026-03-04-prompt-intelligence-system-design.md for architecture.",
  "production": {
    "camera_body": "Arri Alexa Mini LF",
    "film_stock": "Kodak Vision3 500T",
    "film_style_suffix": "visible grain, photorealistic",
    "quality_guard": "Correct human anatomy, anatomically correct proportions, five fingers per hand, sharp focus, clean detailed image, natural skin texture with pores",
    "negative_prompt": "deformed hands, extra fingers, mutated hands, poorly drawn hands, extra limbs, fused fingers, too many fingers, long neck, blurry, low quality, illustration, cartoon, painting, drawing, 3d render, anime, cgi, digital art, smooth skin",
    "wide_shot_footer": "Focus on full body silhouette, posture, and environmental scale. Facial features are indistinct at this distance. Do not attempt high-detail eyes or mouth.",
    "medium_shot_footer": "Anatomically flawless hands, exactly five fingers per hand. Natural body proportions, correct skeletal structure.",
    "close_shot_footer": "Anatomically flawless hands, perfect skeletal symmetry. Highly detailed facial features, accurate skin texture with pores.",
    "non_human_identity_lock": "This character is NOT a baseline human. Preserve whatever helmet, chassis, head covering, or non-human structure is visible in the reference. Do NOT add human hair where none exists. Do NOT infer a bare human head.",
    "camera_direction_guard": "Camera direction: Subject does not look directly into the lens.",
    "env_only_guard": "CRITICAL: This is an ENVIRONMENT-ONLY shot. ABSOLUTELY NO PEOPLE in this image.",
    "cinematic_quality_baseline": "Photorealistic, cinematic lighting, high budget film."
  },
  "casting": {
    "casting_camera": "Arri Alexa 65, 85mm f/2.8",
    "casting_lighting": "5600K daylight-balanced or pure white diffusion",
    "casting_texture_human": "Unretouched photorealism. Visible skin pores, peach fuzz, micro-imperfections, natural subsurface scattering, matte skin. Cinematic makeup test.",
    "casting_texture_synthetic": "Stan Winston Studio style practical effects, brushed polycarbonate, realistic weathering, mechanical micro-details. Tangible, physical materials shot in-camera.",
    "casting_background": "18% neutral gray seamless studio backdrop",
    "casting_anti_airbrush": "DO NOT AIRBRUSH. NO ILLUSTRATION. NO 3D RENDER. NO CONCEPT ART.",
    "grid_diegetic_framing": "photographic contact sheet"
  },
  "shared": {
    "kinetic_fallback": "natural posture, documentary framing, ambient atmosphere",
    "universal_expression_subject": "A generic, bald, androgynous human actor with no distinct features, no makeup, and no styling."
  }
}
```

**Step 2: Commit**
```bash
git add config/prompt_constants.json
git commit -m "feat: add prompt_constants.json — single source of truth for all guard texts"
```

---

### Task 2: Create lexicon.json

**Files:**
- Create: `starsend/config/lexicon.json`

**Step 1: Extract kinetic descriptors from prompt_engine.py lines 35-81**

```json
{
  "_doc": "Kinetic descriptor lexicon. Maps semantic action regex patterns to camera-artifact language. Loaded by prompt_config.py, injected as strict instructions to Flash enrichment.",
  "kinetic_map": [
    {
      "id": "physical_strain",
      "pattern": "\\b(?:push|shov|pry|wrench|wedge|lever|haul|heav|strain|brace|forc)\\w*\\b",
      "descriptor": "muscles taut, unbalanced dynamic pose, off-axis framing"
    },
    {
      "id": "impact",
      "pattern": "\\b(?:impact|strike|hit|slam|clang|smash|crash|burst|explod|shatter)\\w*\\b",
      "descriptor": "motion blur on point of contact, kinetic energy frozen mid-transfer, dust kicked into lens"
    },
    {
      "id": "running",
      "pattern": "\\b(?:run|sprint|chase|flee|dash|scrambl|bolt)\\w*\\b",
      "descriptor": "motion blur on trailing limbs, forward lean, kinetic diagonal composition"
    },
    {
      "id": "falling",
      "pattern": "\\b(?:fall|drop|plummet|descend|tumbl|plunge|hang|dangl|suspend)\\w*\\b",
      "descriptor": "vertigo framing, dutch angle, gravitational pull in composition"
    },
    {
      "id": "grabbing",
      "pattern": "\\b(?:grab|grip|seize|clutch|clamp|choke|throttle|restrain|hold|close[sd]?\\s+on)\\w*\\b",
      "descriptor": "locked muscles, white-knuckle tension, veins visible, compression force"
    },
    {
      "id": "fear",
      "pattern": "\\b(?:terror|fear|dread|panic|horror|shock|startl)\\w*\\b",
      "descriptor": "wide-eyed shallow breath, micro-tremor in hands, sweat catching light"
    },
    {
      "id": "awe",
      "pattern": "\\b(?:awe|wonder|discover|reveal|marvel|astonish|pristine)\\w*\\b",
      "descriptor": "held breath, eyes tracking slowly, stillness against active environment"
    },
    {
      "id": "determination",
      "pattern": "\\b(?:determin|resolv|grit|clench|set jaw|steely|focus|concentrat)\\w*\\b",
      "descriptor": "jaw set, tendons visible in neck, center-weighted composition, static framing"
    },
    {
      "id": "exhaustion",
      "pattern": "\\b(?:exhaust|fatigu|weary|drain|gasp|labored|surviv|precarious)\\w*\\b",
      "descriptor": "heavy limbs, slumped posture, sweat and grime catching ambient light"
    },
    {
      "id": "stillness",
      "pattern": "\\b(?:still|static|patient|stealth|silent|quiet|calm|slow)\\w*\\b",
      "descriptor": "still, static framing, held breath, negative space pressing in"
    },
    {
      "id": "combat",
      "pattern": "\\b(?:combat|fight|war|weapon|attack|assault|threat|menac)\\w*\\b",
      "descriptor": "coiled tension, center of gravity low, sharp angular shadows"
    },
    {
      "id": "abandonment",
      "pattern": "\\b(?:abandon|lone|isolat|void|empty|desolat|forsaken)\\w*\\b",
      "descriptor": "deep negative space, subject small in frame, echoing geometry"
    },
    {
      "id": "urgency",
      "pattern": "\\b(?:urgent|desperate|frantic|hurr|rush|press)\\w*\\b",
      "descriptor": "canted frame, compressed depth of field, blur at frame edges"
    },
    {
      "id": "mechanical",
      "pattern": "\\b(?:mechanical|android|chassis|processor|restart|boot|activat|power)\\w*\\b",
      "descriptor": "precision in stillness, machine-perfect posture, light cycling through optics"
    }
  ],
  "fallback": "natural posture, documentary framing, ambient atmosphere",
  "lighting_direction_map": [
    {"pattern": "\\bfloor\\s+level|uplight|from\\s+below|under\\w*\\s+light", "direction": "BOTTOM"},
    {"pattern": "\\boverhead|from\\s+above|top\\s*light|ceili?ng", "direction": "TOP"},
    {"pattern": "\\bbackli[gt]|silhouett|rim\\s+light|from\\s+behind", "direction": "BEHIND"},
    {"pattern": "\\bfront\\w*\\s+light|fill\\s+light|key\\s+light", "direction": "FRONT LEFT"},
    {"pattern": "\\bself[\\s-]illuminat|self[\\s-]lit|glow\\w*\\s+from\\s+(?:the\\s+)?(?:device|counter|screen|display|pod|chest)", "direction": "SELF-ILLUMINATED"},
    {"pattern": "\\bstrip\\s+light|emergency\\s+strip|narrow\\s+beam", "direction": "SIDE"},
    {"pattern": "\\bheadlamp|head\\s+lamp", "direction": "FROM SUBJECT"},
    {"pattern": "\\bspark|burst|flash|momentary", "direction": "BURST FROM POINT SOURCE"}
  ],
  "lighting_quality_map": [
    {"pattern": "\\bhard\\b|harsh|sharp\\s+shadow|directional", "quality": "hard"},
    {"pattern": "\\bsoft\\b|diffuse|ambient|scatter", "quality": "soft diffuse"},
    {"pattern": "\\bpulse|puls\\w+|glow\\w*|throb", "quality": "pulsing"},
    {"pattern": "\\bflicker|strobe", "quality": "flickering"}
  ]
}
```

**Step 2: Commit**
```bash
git add config/lexicon.json
git commit -m "feat: add lexicon.json — externalized kinetic descriptors and lighting maps"
```

---

### Task 3: Create prompt_config.py loader module

**Files:**
- Create: `starsend/lib/prompt_config.py`
- Create: `starsend/tests/test_prompt_config.py`

**Step 1: Write the failing tests**

```python
# tests/test_prompt_config.py
"""Tests for prompt_config.py — config loader for prompt constants and lexicon."""
import re
import pytest

def test_load_constants_returns_dict():
    from lib.prompt_config import load_constants
    c = load_constants()
    assert isinstance(c, dict)
    assert "production" in c
    assert "casting" in c
    assert "shared" in c

def test_get_production_constant():
    from lib.prompt_config import get_constant
    val = get_constant("production", "camera_body")
    assert val == "Arri Alexa Mini LF"

def test_get_casting_constant():
    from lib.prompt_config import get_constant
    val = get_constant("casting", "casting_camera")
    assert "Alexa 65" in val

def test_get_constant_with_project_override(tmp_path):
    """Project-level prompt_constants.json overrides global."""
    import json
    from lib.prompt_config import get_constant
    override = {"production": {"camera_body": "RED V-Raptor"}}
    override_path = tmp_path / "prompt_constants.json"
    override_path.write_text(json.dumps(override))
    val = get_constant("production", "camera_body", project_dir=str(tmp_path))
    assert val == "RED V-Raptor"

def test_get_constant_missing_key_returns_default():
    from lib.prompt_config import get_constant
    val = get_constant("production", "nonexistent_key", default="fallback")
    assert val == "fallback"

def test_load_lexicon_returns_kinetic_map():
    from lib.prompt_config import load_lexicon
    lex = load_lexicon()
    assert "kinetic_map" in lex
    assert len(lex["kinetic_map"]) == 14
    assert "fallback" in lex

def test_get_kinetic_descriptor_matches():
    from lib.prompt_config import get_kinetic_descriptor
    desc = get_kinetic_descriptor("She pushes the door open")
    assert "muscles taut" in desc

def test_get_kinetic_descriptor_fallback():
    from lib.prompt_config import get_kinetic_descriptor
    desc = get_kinetic_descriptor("She stands in the doorway")
    # "still" or "static" should match stillness pattern
    # But if nothing matches: fallback
    # Actually "stands" won't match any pattern, so this tests fallback
    # Let's use a truly non-matching string
    desc = get_kinetic_descriptor("The table is brown")
    assert desc == "natural posture, documentary framing, ambient atmosphere"

def test_get_lighting_direction():
    from lib.prompt_config import get_lighting_direction
    d = get_lighting_direction("overhead fluorescent strips")
    assert d == "TOP"

def test_get_lighting_direction_unknown():
    from lib.prompt_config import get_lighting_direction
    d = get_lighting_direction("some random lighting")
    assert d is None
```

**Step 2: Run tests to verify they fail**
```bash
cd /Users/joeturnerlin/Dropbox/CLAUDE_PROJECTS/starsend
python -m pytest tests/test_prompt_config.py -v
```
Expected: FAIL (module not found)

**Step 3: Implement prompt_config.py**

```python
# lib/prompt_config.py
"""
prompt_config.py — Loader for prompt_constants.json and lexicon.json.

Single source of truth for all production and pre-production guard texts,
kinetic descriptors, and lighting maps. Supports project-level overrides.

Usage:
    from lib.prompt_config import get_constant, get_kinetic_descriptor

    camera = get_constant("production", "camera_body")
    kinetic = get_kinetic_descriptor("She pushes the door open")
"""

import json
import re
from pathlib import Path
from typing import Optional

from lib.constants import STARSEND_ROOT

_CONSTANTS_PATH = STARSEND_ROOT / "config" / "prompt_constants.json"
_LEXICON_PATH = STARSEND_ROOT / "config" / "lexicon.json"

# Cached after first load
_constants: Optional[dict] = None
_lexicon: Optional[dict] = None
_compiled_kinetic: Optional[list] = None
_compiled_light_dir: Optional[list] = None
_compiled_light_qual: Optional[list] = None


def load_constants() -> dict:
    """Load prompt constants from config file (cached)."""
    global _constants
    if _constants is None:
        _constants = json.loads(_CONSTANTS_PATH.read_text(encoding="utf-8"))
    return _constants


def get_constant(
    category: str,
    key: str,
    default: str = "",
    project_dir: Optional[str] = None,
) -> str:
    """Get a prompt constant by category and key.

    Args:
        category: "production", "casting", or "shared"
        key: The constant key (e.g. "camera_body")
        default: Fallback if key not found
        project_dir: Optional project directory for per-project overrides
    """
    # Check project-level override first
    if project_dir:
        override_path = Path(project_dir) / "prompt_constants.json"
        if override_path.exists():
            try:
                overrides = json.loads(override_path.read_text(encoding="utf-8"))
                if category in overrides and key in overrides[category]:
                    return overrides[category][key]
            except (json.JSONDecodeError, OSError):
                pass

    constants = load_constants()
    return constants.get(category, {}).get(key, default)


def load_lexicon() -> dict:
    """Load lexicon from config file (cached)."""
    global _lexicon
    if _lexicon is None:
        _lexicon = json.loads(_LEXICON_PATH.read_text(encoding="utf-8"))
    return _lexicon


def _compile_kinetic() -> list:
    """Compile kinetic regex patterns (cached)."""
    global _compiled_kinetic
    if _compiled_kinetic is None:
        lex = load_lexicon()
        _compiled_kinetic = [
            (re.compile(entry["pattern"], re.I), entry["descriptor"])
            for entry in lex["kinetic_map"]
        ]
    return _compiled_kinetic


def get_kinetic_descriptor(text: str) -> str:
    """Match text against kinetic lexicon, return first matching descriptor."""
    for pattern, descriptor in _compile_kinetic():
        if pattern.search(text):
            return descriptor
    return load_lexicon()["fallback"]


def _compile_light_dir() -> list:
    """Compile lighting direction patterns (cached)."""
    global _compiled_light_dir
    if _compiled_light_dir is None:
        lex = load_lexicon()
        _compiled_light_dir = [
            (re.compile(entry["pattern"], re.I), entry["direction"])
            for entry in lex.get("lighting_direction_map", [])
        ]
    return _compiled_light_dir


def get_lighting_direction(text: str) -> Optional[str]:
    """Match text against lighting direction map."""
    for pattern, direction in _compile_light_dir():
        if pattern.search(text):
            return direction
    return None


def _compile_light_qual() -> list:
    """Compile lighting quality patterns (cached)."""
    global _compiled_light_qual
    if _compiled_light_qual is None:
        lex = load_lexicon()
        _compiled_light_qual = [
            (re.compile(entry["pattern"], re.I), entry["quality"])
            for entry in lex.get("lighting_quality_map", [])
        ]
    return _compiled_light_qual


def get_lighting_quality(text: str) -> Optional[str]:
    """Match text against lighting quality map."""
    for pattern, quality in _compile_light_qual():
        if pattern.search(text):
            return quality
    return None


def reload():
    """Force reload all config from disk."""
    global _constants, _lexicon, _compiled_kinetic, _compiled_light_dir, _compiled_light_qual
    _constants = None
    _lexicon = None
    _compiled_kinetic = None
    _compiled_light_dir = None
    _compiled_light_qual = None
```

**Step 4: Run tests to verify they pass**
```bash
python -m pytest tests/test_prompt_config.py -v
```
Expected: ALL PASS

**Step 5: Commit**
```bash
git add lib/prompt_config.py tests/test_prompt_config.py
git commit -m "feat: add prompt_config.py — loader for prompt constants and lexicon"
```

---

### Task 4: Extend take logging in execution_store.py

**Files:**
- Modify: `starsend/lib/execution_store.py` (the `update_shot` method)
- Create: `starsend/tests/test_take_logging.py`

**Step 1: Write failing test**

```python
# tests/test_take_logging.py
"""Tests for take-level prompt/seed/version logging in ExecutionStore."""
import json
import pytest
from pathlib import Path


def test_append_take_includes_prompt_and_seed(tmp_path):
    from lib.execution_store import ExecutionStore
    db_path = tmp_path / "test.db"
    store = ExecutionStore(project="test", db_path=db_path)

    # Insert a shot
    store.upsert_shot("ep001_sh001", "ep001", pipeline="still", model="nbp", status="pending")

    # Append a take with prompt and seed
    take = {
        "take_id": "take_001",
        "prompt": "A cinematic close-up of Kit...",
        "seed": 42,
        "system_prompt_version": "v1.0",
        "model": "gemini-3-pro-image-preview",
        "cost": 0.134,
    }
    store.update_shot("ep001_sh001", append_take=take)

    # Verify take was stored with prompt data
    shot = store.get_shot("ep001_sh001")
    takes = json.loads(shot["takes"]) if shot["takes"] else []
    assert len(takes) == 1
    assert takes[0]["prompt"] == "A cinematic close-up of Kit..."
    assert takes[0]["seed"] == 42
    assert takes[0]["system_prompt_version"] == "v1.0"


def test_multiple_takes_preserve_history(tmp_path):
    from lib.execution_store import ExecutionStore
    db_path = tmp_path / "test.db"
    store = ExecutionStore(project="test", db_path=db_path)

    store.upsert_shot("ep001_sh001", "ep001", pipeline="still", model="nbp", status="pending")

    # Append two takes
    store.update_shot("ep001_sh001", append_take={
        "take_id": "take_001", "prompt": "First prompt", "seed": 1
    })
    store.update_shot("ep001_sh001", append_take={
        "take_id": "take_002", "prompt": "Second prompt", "seed": 2
    })

    shot = store.get_shot("ep001_sh001")
    takes = json.loads(shot["takes"])
    assert len(takes) == 2
    assert takes[0]["prompt"] == "First prompt"
    assert takes[1]["prompt"] == "Second prompt"
    assert takes[0]["seed"] == 1
    assert takes[1]["seed"] == 2
```

**Step 2: Run tests — check if they already pass (existing append_take logic may already handle arbitrary dict keys)**
```bash
python -m pytest tests/test_take_logging.py -v
```

If they pass: the existing `append_take` already stores arbitrary JSON — we just need to ensure callers pass prompt/seed/version. Commit the tests as documentation.

If they fail: modify `update_shot()` to handle the take dict correctly. The fix depends on the current implementation — read `execution_store.py` `update_shot()` method to see how `append_take` works.

**Step 3: Commit**
```bash
git add tests/test_take_logging.py lib/execution_store.py
git commit -m "feat: verify take-level prompt/seed/version logging in ExecutionStore"
```

---

### Task 5: Add prompt_enrichment pass_type to CostTracker

**Files:**
- Modify: `starsend/orchestrator/cost_tracker.py`

**Step 1: Verify pass_type is just a string field (no enum validation)**

Read `cost_tracker.py` — the `pass_type` field on `GenerationRecord` is a plain `str`. If so, no code change is needed — callers can already pass `pass_type="prompt_enrichment"`. Just add a comment documenting the valid values.

**Step 2: Add documentation comment**

At the `pass_type` field definition, add:
```python
pass_type: str              # "grid", "pro", "expression", "video_gen", "prompt_enrichment"
```

**Step 3: Commit**
```bash
git add orchestrator/cost_tracker.py
git commit -m "docs: document prompt_enrichment pass_type in CostTracker"
```

---

### Task 6: Create prompts/ directory with placeholder system prompt files

**Files:**
- Create: `starsend/config/prompts/flash_base_instructions.txt`
- Create: `starsend/config/prompts/flash_to_nbp_v1.0.txt` (externalize from keyframe_context.py)
- Create: `starsend/config/prompts/flash_previz_v1.0.txt` (externalize from previz_context.py)
- Create: remaining prompt files as stubs

**Step 1: Create flash_base_instructions.txt**

```
You are an expert cinematographer translating a shot breakdown into a generation prompt.

RULES:
1. You MUST include every string in the LOCKED_TERMS section exactly as written. Do not alter, paraphrase, or omit any locked term.
2. Use camera-artifact language for physical actions (motion blur, kinetic diagonal, etc.), NOT emotional/narrative descriptions.
3. When KINETIC_DESCRIPTOR is provided, you MUST use it verbatim — do not generate your own.
4. Write flowing, natural prose. No section headers like "CAMERA:" or "LIGHTING:" — models render text overlays from headers.
5. Do not add narrative commentary, metaphors, or subjective interpretation. Describe only what the camera sees.
```

**Step 2: Externalize keyframe_context.py system prompt to flash_to_nbp_v1.0.txt**

Read `keyframe_context.py` `build_smart_prompt()` (around line 160) to extract the existing system prompt. Write it to `flash_to_nbp_v1.0.txt`. This is the EXISTING enrichment — we're just moving it to a versioned file.

**Step 3: Externalize previz_context.py system instruction to flash_previz_v1.0.txt**

Read `previz_context.py` `build_system_instruction()` (around line 316) to extract the existing system instruction. Write it to `flash_previz_v1.0.txt`.

**Step 4: Create stub files for remaining prompts**

For each of these, create a file with a TODO header and the model-specific constraints from the design doc:
- `flash_to_kling_i2v_v1.0.txt` — 30 words max, action-focused
- `flash_to_kling_t2v_v1.0.txt` — 75-100 words, balanced
- `flash_to_seeddance_v1.0.txt` — Multi-shot JSON, scene coherence
- `flash_to_veo_v1.0.txt` — 1500 char max, ENV-friendly
- `flash_casting_grid_v1.0.txt` — Externalize from ref_selector.py `_enrich_continuity_grid_prompt()`
- `flash_casting_turnaround_v1.0.txt`
- `flash_screen_test_v1.0.txt`
- `flash_location_ref_v1.0.txt`

**Step 5: Commit**
```bash
git add config/prompts/
git commit -m "feat: add versioned Flash system prompt files — externalized from keyframe_context and previz_context"
```

---

## Phase 2: Wire Production Pipeline (D3 + D5)

Replace hardcoded values in production files with config reads. Add Flash enrichment wrapper.

---

### Task 7: Wire prompt_engine.py to prompt_config

**Files:**
- Modify: `starsend/lib/prompt_engine.py`

**What to do:**

1. Add import: `from lib.prompt_config import get_constant, get_kinetic_descriptor, get_lighting_direction, get_lighting_quality, load_lexicon`

2. Replace hardcoded `_KINETIC_MAP` (lines 35-78) with `get_kinetic_descriptor()` calls. Keep `_KINETIC_MAP` as a comment pointing to `config/lexicon.json`.

3. Replace hardcoded `_LIGHT_DIRECTION_MAP` (lines 87-96) and `_LIGHT_QUALITY_MAP` (lines 98-103) with `get_lighting_direction()` and `get_lighting_quality()` calls.

4. Replace hardcoded `_KINETIC_FALLBACK` (line 81) with `load_lexicon()["fallback"]`.

5. In `build_prompt_from_plan()`: replace hardcoded defaults for `camera_body`, `film_stock`, `film_style_suffix` with `get_constant("production", ...)` calls. Keep `project_config.get()` as first priority, `get_constant()` as fallback.

6. Replace all hardcoded quality/guard texts (6 locations for cinematic baseline, 4 for anatomical, 4 for non-human identity lock, 2 for camera direction guard, 2 for ENV guard) with `get_constant()` calls.

7. **IMPORTANT:** Do NOT touch `_HUMAN_PATTERNS` (line 108) or `_visual_is_non_human()` regex patterns — these are DETECTION logic, not display text. Keep them in code.

**Verification:**
```bash
python -m pytest tests/test_prompt_config.py -v
python -c "from lib.prompt_engine import build_prompt_from_plan; print('import OK')"
```

**Step: Commit**
```bash
git add lib/prompt_engine.py
git commit -m "refactor: wire prompt_engine.py to prompt_config — replace 28+ hardcoded constants"
```

---

### Task 8: Wire keyframe_context.py to external system prompt

**Files:**
- Modify: `starsend/lib/keyframe_context.py`

**What to do:**

1. In `build_smart_prompt()`, replace the hardcoded system prompt string (around line 160) with a file read from `config/prompts/flash_to_nbp_v1.0.txt`.

2. Replace any hardcoded constants (camera, film, quality) with `get_constant()` calls.

3. Add `flash_base_instructions.txt` concatenation: load base + model-specific at runtime.

**Verification:**
```bash
python -c "from lib.keyframe_context import build_smart_prompt; print('import OK')"
```

**Commit:**
```bash
git add lib/keyframe_context.py
git commit -m "refactor: externalize keyframe_context Flash system prompt to versioned file"
```

---

### Task 9: Wire previz_context.py to external system instruction

**Files:**
- Modify: `starsend/lib/previz_context.py`

**What to do:**

1. In `build_system_instruction()`, replace the hardcoded system instruction (around line 316) with a file read from `config/prompts/flash_previz_v1.0.txt`.

2. Replace any hardcoded constants with `get_constant()` calls.

3. NO Flash enrichment added — previz is Flash-native.

**Verification:**
```bash
python -c "from lib.previz_context import build_system_instruction; print('import OK')"
```

**Commit:**
```bash
git add lib/previz_context.py
git commit -m "refactor: externalize previz_context system instruction to versioned file"
```

---

### Task 10: Add Flash enrichment wrapper to prompt_engine.py

**Files:**
- Modify: `starsend/lib/prompt_engine.py`
- Create: `starsend/tests/test_flash_enrichment.py`

**What to do:**

1. Create `enrich_prompt()` function:

```python
def enrich_prompt(
    base_prompt: str,
    model_target: str,
    locked_terms: list[str],
    kinetic_descriptor: str = "",
    is_env: bool = False,
    project_config: dict = None,
    system_prompt_version: str = "v1.0",
) -> tuple[str, str]:
    """Flash-enrich a compiled prompt for a target generation model.

    Returns:
        (enriched_prompt, system_prompt_version) tuple.
        Falls back to base_prompt if enrichment fails.
    """
    # ENV bypass
    if is_env:
        return base_prompt, "bypass_env"

    # Global bypass flag
    if project_config and project_config.get("skip_flash_enrichment"):
        return base_prompt, "bypass_config"

    # Load system prompt
    base_instructions = _load_prompt_file("flash_base_instructions.txt")
    model_instructions = _load_prompt_file(f"flash_to_{model_target}_v{system_prompt_version}.txt")

    if not model_instructions:
        return base_prompt, "bypass_no_prompt_file"

    system_prompt = base_instructions + "\n\n" + model_instructions

    # Build Flash input
    flash_input = f"LOCKED_TERMS:\n"
    for term in locked_terms:
        flash_input += f"- {term}\n"
    if kinetic_descriptor:
        flash_input += f"\nKINETIC_DESCRIPTOR: {kinetic_descriptor}\n"
    flash_input += f"\nSHOT DATA TO REWRITE:\n{base_prompt}"

    # Call Flash
    try:
        from lib.model_profiles import get_model
        flash_model = get_model("flash", "text")
        # ... Flash API call (use existing Gemini patterns from ref_selector.py)
        enriched = _call_flash(system_prompt, flash_input, flash_model)
    except Exception:
        return base_prompt, "fallback_error"

    # Validate locked terms preserved
    for term in locked_terms:
        if term not in enriched:
            # Retry at temp=0
            try:
                enriched = _call_flash(system_prompt, flash_input, flash_model, temperature=0.0)
            except Exception:
                return base_prompt, "fallback_retry_error"
            # Check again
            for t in locked_terms:
                if t not in enriched:
                    return base_prompt, "fallback_validation"
            break

    return enriched, system_prompt_version
```

2. Insert call at end of `build_prompt_from_plan()` (line 290): replace `return prompt.strip()` with enrichment call.

3. Wire `compile_all_prompts()` to pass model-specific target names.

**Test:**
```python
# tests/test_flash_enrichment.py
def test_env_bypass():
    from lib.prompt_engine import enrich_prompt
    result, version = enrich_prompt("empty corridor", "nbp", [], is_env=True)
    assert result == "empty corridor"
    assert version == "bypass_env"

def test_config_bypass():
    from lib.prompt_engine import enrich_prompt
    result, version = enrich_prompt("test", "nbp", [], project_config={"skip_flash_enrichment": True})
    assert result == "test"
    assert version == "bypass_config"
```

**Commit:**
```bash
git add lib/prompt_engine.py tests/test_flash_enrichment.py
git commit -m "feat: add Flash enrichment wrapper with ENV bypass, validation, and fallback"
```

---

## Phase 3: Wire Pre-Production Pipeline

### Task 11: Wire ref_selector.py to prompt_config

**Files:**
- Modify: `starsend/lib/ref_selector.py`

**What to do:**
1. Replace hardcoded "Arri Alexa 65, 85mm" with `get_constant("casting", "casting_camera")`
2. Replace hardcoded "5600K" lighting with `get_constant("casting", "casting_lighting")`
3. Replace hardcoded texture rules with `get_constant("casting", "casting_texture_human")` / `casting_texture_synthetic`
4. Replace hardcoded background with `get_constant("casting", "casting_background")`
5. Replace hardcoded anti-airbrush with `get_constant("casting", "casting_anti_airbrush")`
6. Move `_enrich_continuity_grid_prompt()` system prompt to `flash_casting_grid_v1.0.txt`

**Commit:**
```bash
git commit -m "refactor: wire ref_selector.py to prompt_config — replace 6+ casting constants"
```

### Task 12: Wire screen_test_gen.py to prompt_config

**Files:**
- Modify: `starsend/tools/screen_test_gen.py`

Replace hardcoded camera, lighting, texture, and anti-airbrush with `get_constant("casting", ...)` calls.

**Commit:**
```bash
git commit -m "refactor: wire screen_test_gen.py to prompt_config"
```

### Task 13: Wire generate_location_refs.py and prep_expressions.py

**Files:**
- Modify: `starsend/tools/generate_location_refs.py`
- Modify: `starsend/tools/prep_expressions.py`

Replace hardcoded constants. Expression matrix uses `get_constant("shared", "universal_expression_subject")`.

**Commit:**
```bash
git commit -m "refactor: wire location refs and expression gen to prompt_config"
```

---

## Phase 4: Review UI (D7)

### Task 14: Add filmstrip view endpoint to review_server.py

**Files:**
- Modify: `starsend/editors/review_server.py`

Add `GET /api/dailies/filmstrip/{episode}/{shot_id}` that returns previous + current + next frame paths from the generation log.

### Task 15: Add rejection tags to dailies.js

**Files:**
- Modify: `starsend/editors/tabs/dailies.js`

Add checkbox UI for rejection taxonomy (Anatomy, Continuity, Camera, Style) + free-text field. Wire to existing reject endpoint with structured payload.

---

## Verification Checklist (Run After All Phases)

```bash
# 1. All tests pass
python -m pytest tests/ -v

# 2. Config loads correctly
python -c "from lib.prompt_config import load_constants, load_lexicon; print(f'Constants: {len(load_constants())} categories'); print(f'Lexicon: {len(load_lexicon()[\"kinetic_map\"])} patterns')"

# 3. No remaining hardcoded camera bodies in production code
grep -rn "Arri Alexa Mini LF" lib/ tools/ --include="*.py" | grep -v prompt_config | grep -v __pycache__
# Expected: 0 results (all should use get_constant)

grep -rn "Arri Alexa 65" lib/ tools/ --include="*.py" | grep -v prompt_config | grep -v __pycache__
# Expected: 0 results

# 4. No remaining hardcoded quality guards
grep -rn "five fingers per hand" lib/ tools/ --include="*.py" | grep -v prompt_config | grep -v __pycache__
# Expected: 0 results

# 5. Engine check still passes
python -m pytest tests/ -v --tb=short
```
