# Round 3 Final Consultation — ClientSequenceRunner Architecture

## Convergence Answers

### 1. Grid Exploration: Orchestrator-Only State + Cost Event Registration

**Final recommendation: Your lean is correct. Don't map grid to keyframe states.**

The argument for reusing keyframe states is pragmatic but ultimately wrong:

- **Keyframe states exist to gate downstream work.** `keyframe_complete` means "this shot has a locked start frame, proceed to video generation." A grid is the opposite — it's an array of candidates, none of which are locked. Mapping grid → keyframe_complete would either (a) trigger false "ready for video" signals in Console, or (b) require adding special-case logic to suppress those signals for grid-type keyframes.

- **Cost tracking doesn't require state machine residency.** The orchestrator calls `execute_keyframe()`, which already writes cost events to ExecutionStore. The money is tracked. The workflow state — which grid was selected, which cells were rejected — is orchestration-level context.

- **Console/Dailies showing grids is a UI concern, not a state concern.**

**Concrete approach:**
- `sequences.json` holds: `grid_state`, `grid_path`, `selected_cell`, `grid_history`
- ExecutionStore holds: nothing about grids, except cost events that flow through `execute_keyframe()`
- When user picks a cell, the orchestrator extracts it — THAT goes into ExecutionStore as a normal keyframe

### 2. Shot-Level Prompt Tracking: Option (a), Small StepRunner Change

**Final recommendation: (a). Pass through StepRunner.**

Option (b) is a trap — the moment the orchestrator reaches into ExecutionStore to mutate a take record after StepRunner created it, you've broken the ownership boundary.

The StepRunner change is minimal:

```python
def execute_multi_shot(self, shot_id, prompts, ..., shot_prompts=None):
    # ... existing logic ...
    take = self._create_take(shot_id, ...)
    if shot_prompts is not None:
        take["shot_prompts"] = shot_prompts
    # ... rest unchanged ...
```

One optional parameter, one conditional assignment. Clean data flow, single writer.

### 3. generate_grid() — No. Reuse execute_keyframe()

**Do not create `generate_grid()`. Call `execute_keyframe()` directly.**

The orchestrator calls `execute_keyframe(shot_id="SEQ01_grid", prompt=grid_prompt, ...)`. This:
- Hits the Gemini API (already handled)
- Saves the output image (already handled)
- Records cost (already handled)
- Creates a take record (already handled)

The `SEQ01_grid` shot_id is a naming convention, not a state machine concept. Grid cell extraction is a utility function — pure image manipulation, not pipeline execution.

This means StepRunner gets **zero new methods**.

---

## Final Deliverable

### 1. Files to Create

| Path | Description |
|------|-------------|
| `starsend/tools/client_generate.py` | CLI entry point — parses args, dispatches commands (grid, pick, generate, status) |
| `starsend/tools/client_sequence_runner.py` | Orchestrator — manages sequence lifecycle, grid exploration, calls StepRunner |
| `starsend/tools/image_utils.py` | Grid cell extraction + image manipulation helpers |
| `projects/{project}/state/client/sequences.json` | Per-project sequence state (created at runtime) |

### 2. Files to Modify

| Path | Change |
|------|--------|
| `starsend/orchestrator/step_runner.py` | Add optional `shot_prompts` parameter to `execute_multi_shot()`. ~3 lines. |

### 3. Files NOT to Modify

- `execution_store.py` — No state machine changes
- `elements.py` — Works as-is
- `api_client.py` — Works as-is
- `pipeline.py` — DO NOT TOUCH
- `starsend_config.json` — No config changes

### 4. Day 1 Implementation Order

1. **`image_utils.py`** — Write `extract_grid_cell()`. Pure function, unit-testable. 10 min.
2. **`step_runner.py` modification** — Add `shot_prompts` optional param. 5 min.
3. **`client_sequence_runner.py` — grid workflow** — `explore_grid()` calls `execute_keyframe()` with grid prompt, `pick_cell()` extracts cell. 60-90 min.
4. **`client_sequence_runner.py` — generate workflow** — `generate()` reads locked start frame, builds prompts, calls `execute_multi_shot()`. 45-60 min.
5. **`client_generate.py` CLI** — Wire up argparse. Commands: grid, pick, generate, status. 20 min.
6. **End-to-end test** — One sequence, grid → pick → generate. Verify sequences.json, cost tracking, shot_prompts on takes.

### 5. The One Thing Most Likely to Go Wrong

**The grid prompt will produce inconsistent results and you'll want to add retry/variation logic to StepRunner.**

Gemini sometimes produces a clean 4-panel grid, sometimes a single image with subtle variations, sometimes ignores the grid instruction.

**Don't push retry logic into StepRunner.** Keep it in the orchestrator:
1. Call `execute_keyframe()`
2. Validate grid format (check dimensions suggest grid layout)
3. If invalid, retry with modified prompt
4. After N failures, generate 4 separate images and composite in `image_utils.py`

This retry/fallback is orchestration behavior, not execution behavior. StepRunner stays clean.

**Summary:** ClientSequenceRunner composes StepRunner's existing `execute_keyframe()` and `execute_multi_shot()` primitives — adding grid state, prompt arrays, and retry logic at the orchestration layer — with a single 3-line change to StepRunner and zero changes to ExecutionStore.
