# 스틸 생성 개편 — 변형 2종×콘티 유무 4택1 구현 플랜

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 스틸 생성의 이미지 콜 수를 줄이면서 품질 선택폭을 유지 — 동일 프롬프트 반복 롤을 LLM 저작 변형 2종으로 대체하고, A/B 샷의 2단(브랜치별 3롤 inner + outer 블라인드)을 변형2×콘티유무2=4장 병렬 → 단일 VLM 4택1 → critique → i2i 수정 1장으로 대체한다. (2026-07-17 사용자 확정 스펙)

**Architecture:** 기존 `run_multiroll_select` 공통 파이프를 3점 확장(라벨별 참조 `roll_refs`, 롤 병렬 생성 `parallel_rolls`, 정·역순 2회 판정 `judge_flip`)하고, 신규 `still_variants` 모듈+팩이 샷별 변형 절 2종을 저작한다. `still_recipe_service`는 flag ON 시 A/B 샷=1회 4택1 호출, 비A/B 샷=2롤 변형 호출로 배선한다. base 스틸 프롬프트(불변 안전절 전체)는 기존 `build_still_prompt` 코드 조립을 그대로 유지 — LLM 저작은 "시각 접근" 절만 담당한다.

**Tech Stack:** 기존 스택 그대로 — nb2(gemini image) 생성, gemini-pro VLM 판정, Sol(gpt) 저작, ThreadPoolExecutor+`bind_current_budget`.

## Global Constraints (CLAUDE.md·메모리 규율)

- LLM 전달 데이터 절대 무절단(원문 전체) / 시나리오 의존·하드코딩·하드 프롬프팅 금지(판정·저작 전부 LLM 계약)
- 프롬프트 팩=신규 버전 디렉토리(`prompts/_base/<module>/N.YYYYMMDDHHmm/`), 덮어쓰기 금지
- 기존 모듈 삭제 금지, flag default OFF, OFF=기존 경로 byte-identical(지문 불변)
- TDD=결정론 영역만(스키마·지문·조립·정책 함수·배선), 이미지/VLM 완성도=E2E 육안
- 스크립트 실행=backend cwd + `backend/.venv` 절대경로(.env 미로드 사고 주의)
- ThreadPool 병렬 생성=`bind_current_budget` 필수(W20E5 Codex B1)
- 커밋=태스크 단위, Codex 리뷰=구현 diff 코드 리뷰

---

## 배경 — 현행 vs 개편 콜 수 (critique 결함 1건 가정)

| 샷 유형 | 현행 | 개편 |
|---|---|---|
| A/B (일반 conti_ab·복잡 구조물) | 이미지 8콜(롤3×2+fix2) + VLM 6콜(judge2+critique2+outer2) | 이미지 5콜(롤4+fix1) + VLM 3콜(judge flip2+critique1) + 저작 1콜 |
| 비A/B (prev/bgonly/레인1 스케치) | 이미지 4콜(롤3+fix1) + VLM 2콜 | 이미지 3콜(롤2+fix1) + VLM 2콜 + 저작 1콜 |

이미지 429 쿼터(2026-07-17 소진 이력)가 축소의 직접 동기 — 이미지 콜 A/B 37.5%↓, 비A/B 25%↓. 병렬화로 벽시계 시간도 단축.

## 설계 결정 (R1~R8 — Codex 의논 대상)

### R1. 변형 저작 = base 프롬프트 유지 + "VISUAL APPROACH" 절 주입 (full 재저작 기각)

seed 2R은 저작 계약 팩이 프롬프트 본문 전체를 소유했지만, 스틸 base 프롬프트는 대부분이 불변 안전계약(LOCATION lock·pose canon·immobile physics·EXPRESSION_REALISM·PROP_ORIENTATION·carried·people traits·no_text — 전부 E2E 실측 결함의 해법)이다. full 재저작은 안전절 소실 위험이 구조적. 따라서:

- **effective prompt** = `base_prompt + "\n\nVISUAL APPROACH — this take (camera, framing, composition):\n" + variant.prompt_en + "\n\n" + hierarchy_tail`
  - base_prompt = 기존 `build_still_prompt(...)` 산출 그대로(팩 selector·lane/complex/seed_bg 분기 불변)
  - **hierarchy_tail(Codex 설계 리뷰 BLOCKING-1)**: 팩 소유 불변절을 코드가 항상 **맨 마지막**에 부착 — "VISUAL APPROACH는 카메라·프레이밍·구도 해석만 담당, 위 base 계약(LOCATION/pose/physics/people/no_text 등)과 충돌 시 base가 항상 이긴다". LLM 저작 approach가 프롬프트 말미 위치로 base SOT를 상쇄하는 경로 차단.
- judge 공유 브리프 = base_prompt. **critique = effective prompt 정확히 1회(BLOCKING-1)**: 현행 `_compose_critique_prompt`는 `roll_prompts[selected] + "\n\n" + prompt`라 roll_prompts가 base를 포함하면 base 2회 중복 — 신규 `critique_selected_prompt_only: bool = False` param으로 still 경로에서는 `roll_prompts[selected]`만 전달(default False=seed 2R 합성 semantics byte-identical). 테스트=base 핵심절 각 1회·critique base 중복 0·hierarchy tail이 approach 뒤 존재.

### R2. 저작 계약 — Sol(gpt), 샷당 변형 2종, 사실 발명 금지

- 신규 팩 `still_prompt_variants` v1: `author_sys.md` + `author_schema.json`. 출력=`{"variants":[{"approach_ko","prompt_en"},×2]}`.
- 입력=base_prompt 전문(무절단)+샷 텍스트 원문. 계약: 두 변형은 **동일 사실·상이한 시각 실현**(카메라 거리/앵글·프레이밍·구도·미장센 접근) — 인물·소품·장소·시간·행동의 발명/삭제 금지, base 계약과 모순 금지, 각 변형은 서로 실질적으로 다른 접근일 것.
- 검증(결정론, Codex HIGH-5 보강): variants 정확히 2개, `approach_ko`·`prompt_en` **둘 다** non-empty·str 타입, **whitespace+case 정규화 후 두 필드 각각 상호 상이**(approach만 다르고 prompt_en 동일한 통과 경로 차단). 위반=1회 재시도(재시도 프롬프트에 이전 violations 병기, base/샷 전문은 그대로 유지)→fail-closed(해당 샷 mark_failed).
- 재사용: `author_fp` = base_prompt+샷 텍스트+팩 해석값+`STILL_VARIANTS_CONTRACT_VERSION`+**물리 모델**(alias 아님) 지문 — records(`{tag}::variants`)에 저장, 일치 시 저작 0콜 재사용 (seed `_author_seed_variants` 패턴). force 시 records archive 후 재저작 경로를 테스트로 잠금.

### R3. A/B = 단일 run_multiroll_select 4택1 (2단 제거)

- 라벨 고정 매핑: **A=v1+콘티 / B=v2+콘티 / C=v1+무콘티 / D=v2+무콘티** (record["variant_map"]에 감사 기록, 판정자에게 미노출).
- 신규 `roll_refs: Mapping[label, refs]` — A/B에 콘티 포함, C/D 제외. 나머지 참조(플레이트·seed·seed_bg·엔티티)는 4라벨 동일(`build_ab_branch_refs` 재사용 — refs_a→A·B, refs_b→C·D).
- 블라인드: judge에 전달되는 공유 `labeled_refs`=**콘티 제외** refs(refs_b와 동일) — 어느 후보가 콘티 참조인지 구조적으로 미노출. 후보는 파일만 제시.
- **judge header(Codex HIGH-6)**: `make_gemini_judge_fn` 기본 header("all candidates were generated from this")는 변형 모드에서 거짓 — 변형 모드 전용 중립 header("shared immutable brief; candidates were generated with different camera/framing approaches" 의미, 팩 소유 문구, variant/conti 매핑 미노출)를 `prompt_header` param(seed 2R에서 기추가)으로 주입. 이 header도 지문 계약에 포함.
- critique: 승자 라벨의 **실제 생성 refs**(`roll_refs[selected]`) 사용 — 기존 A브랜치 critique가 콘티를 보던 의미 보존.
- 승자∈{A,B}→콘티 lineage 부착(기존 유예-후-부착 지점 재사용). `{tag}::conti_ab_decision`은 변형 모드에서 미기록(4택1 judge record가 대체). out_stem=`recipe_dir/tag` — prev 앵커 관례 경로(`{tag}_sel.png`) 자동 성립(별도 copy 제거).
- ab_active 게이팅 조건(conti_ab_on or complex_ab, 콘티·플레이트 전제, complex fail-closed)은 전부 불변.

### R4. 순서 편향 방지 = A/B 4택1에만 judge 2회(정순/역순)

- `judge_flip=True` 시: 후보 제시 순서 정순(A..D)·역순(D..A) 2회 판정(라벨은 제시 순서 기준 중립 라벨로 재부여 — 블라인드 유지), 합의=승자 / 불일치=양회 점수 합산 최대 / 동점=고정 우선순위 A>B>C>D(콘티 우선 — 기존 tie_keeps_conti 정책 연속).
- **canonical remap 완전성(Codex BLOCKING-2)**: 역순 호출의 display 라벨은 canonical 역매핑 — winner뿐 아니라 **ranking·verdicts의 모든 label**을 canonical로 역매핑 후 결합(미이행 시 역순 점수가 다른 후보에 합산). 각 판정은 fail-closed 검증: verdict label 정확히 N개·중복 0, ranking=정확한 permutation, winner 유효 — 위반=raise(샷 격리). record에 raw(display)와 normalized(canonical) 병록, `combine_flip_verdicts`는 normalized만 소비.
- **flip 1회차 durable persist(Codex NARROW-2)**: 정순 판정 완료 직후 `persist_record_fn`으로 중간 persist — crash 시 resume이 정순 VLM 재호출을 생략.
- 비A/B 2롤은 **기존 단일 판정 유지** — 순서 편향 계약은 콘티 유무 '모드 결정'이 걸린 판정에 요구된 것(conti_ab 계약 승계). 같은 모드 내 품질 선택까지 flip하면 VLM 콜이 순증(축소 목적 역행).

### R5. 병렬 생성 — multiroll 공통 `parallel_rolls`

- `run_multiroll_select(parallel_rolls=True)`: 미존재 롤만 `ThreadPoolExecutor(max_workers=len(labels))`로 병렬 생성, **`bind_current_budget(gen_fn)` 필수 래핑**. 전 future 완료 대기 후 **canonical 라벨 순 첫 예외** raise(Codex NARROW-1 — completion timing 무관 결정론적 오류 선택; 완료된 롤 파일은 남아 resume 재사용 — 기존 skip-if-exists 계약 그대로).
- `GeminiImageClient`는 `threading.local` 컨텍스트(실측 확인) — worker 스레드별 set_context 안전. `make_nb2_gen_fn`의 호출별 multiroll_tag 기록도 스레드 로컬이라 유지.
- **generation_context 명시 전파(Codex BLOCKING-3, 실측 확인)**: `image_capture` GenerationContext는 ContextVar — `context.py` docstring이 "worker thread 자동 전파 안 됨, bind_context 필요" 명시. budget bind만으로는 병렬 worker의 candidate capture가 skipped_no_context로 유실. 신규 공개 헬퍼 `bind_current_generation_context(fn)`(부모의 `current_context()` 캡처→worker에서 `bind_context`/`reset(token)`)를 budget wrapper와 **함께** 적용. `CaptureQueue.append`에 `threading.Lock` 추가(공유 큐 동시 append 계약 명시화). 테스트=worker에서 budget cap과 current_context 둘 다 관측 + 병렬 4롤 candidate가 capture queue에 전부 적재.
- 스틸(변형 모드)=상시 병렬. 플레이트 multiroll 호출부에도 동일 param 노출(별도 커밋 아님, 같은 시그니처) — ON 여부는 Codex 의논(429 burst 트레이드오프).
- 병렬 여부는 입력 지문에 **불포함**(산출 동일성 가정 — 실행 방식일 뿐, Codex 동의).
- **effective inputs의 generic 지문 포함(Codex HIGH-4)**: `roll_refs` per-label 내용 해시뿐 아니라 `judge_flip`·`flip_priority`·canonical remap/tie 정책 식별자·`critique_selected_prompt_only`·judge `prompt_header`를 **run_multiroll_select 자체 지문**에 포함 — 서비스 extra에만 의존하면 generic caller가 모드를 바꿔도 기존 sel을 재사용하는 구멍. 미사용(default) 시 지문 기여 0=기존 byte-identical.

### R6. 비A/B 샷 = roll_count 2(변형 2종 병렬)

- bgonly·prev·레인1 스케치 스틸 전부 대상 — roll_prompts={A:v1, B:v2}, 판정·critique·fix 기존 계약 그대로.
- bgonly도 변형 저작 대상(구도 접근은 인물 무관) — no_people 절은 base에 있으므로 변형이 훼손 불가.

### R7. 플래그·config_hash

- `still_variants_enabled: bool = False` (env `STILL_VARIANTS_ENABLED`). OFF=기존 경로 byte-identical(신규 지문 요소 0).
- ON 시 `image_steps.py` scene_image hash 스탬프: `still_variants_pack`(해석값)·`still_variants_author_model`(alias+물리)·`STILL_VARIANTS_CONTRACT_VERSION=1`(라벨 매핑·flip·tie 정책 계약 버전). 변형 모드에서 conti_ab outer 팩·모델 스탬프는 제외(경로 소멸)하되 flag 자체가 스탬프되므로 전환 감지됨.
- `still_recipe_roll_count`는 legacy 경로 전용으로 유지(변형 모드에서 미사용 — A/B=4·비A/B=2 계약 고정).

### R8. 감사·lineage 연속성

- `winner_exact_multiroll_tag`: 변형 모드 롤 태그=`still_{tag}_{a..d}`/`still_{tag}_fix` — 단일 호출이라 기존 exact 해석이 그대로 성립하는지 유닛으로 잠금.
- record 신규 키: `variant_map`(라벨→variant/conti 매핑), `variants`(저작 전문·approach), `judge_flip`(양회 verdicts·합산·정책), `roll_refs` 표식(bytes 오염 방지 — path/`<bytes:n>` 마커).
- ref_mode 문자열: `"...+콘티+엔티티 (4택1: A승=v1+콘티)"` 형식으로 승자 라벨·의미 병기.

## 열린 쟁점 — Codex 합의 완료 (2026-07-17 설계 리뷰)

1. **비A/B 단일 판정 유지** — 합의(같은 mode 내부 선택까지 flip하면 비용 절감 목적 훼손).
2. **플레이트 multiroll 병렬** — param만 노출, **E2E8에서는 플레이트 OFF**(스틸 개편과 429 burst 변수 동시 투입 금지 — 별도 canary 후 전환).
3. **4후보 1콜×2 flip** — 합의(토너먼트=비추이성·결승 순서 편향·호출 증가).
4. **저작 실패=retry 1회 후 샷 fail-closed** — 합의(legacy 3롤 degrade는 같은 run에 상이 계약 산출 혼입).

---

## Task 1: multiroll_select 확장 — roll_refs·parallel_rolls·judge_flip

**Files:**
- Modify: `backend/app/modules/pipeline/multiroll_select.py`
- Modify: `backend/app/services/image_capture/context.py` (`bind_current_generation_context(fn)` 공개 헬퍼), `backend/app/services/image_capture/queue.py` (append lock)
- Test: `backend/tests/unit/test_multiroll_select.py` (기존 파일에 추가), `backend/tests/unit/test_image_capture_context.py` (전파 헬퍼)

**Interfaces:**
- Produces: `run_multiroll_select(..., roll_refs: Optional[Mapping[str, Sequence[Tuple[str, Any]]]] = None, parallel_rolls: bool = False, judge_flip: bool = False, flip_priority: Optional[Sequence[str]] = None, critique_selected_prompt_only: bool = False, judge_prompt_header: Optional[str] = None)` — 기존 파라미터·반환 `(sel_path, record)` 불변. 신규 kwargs 전부 default=미사용 시 지문·record shape byte-identical(OFF 경로는 kwargs 자체 미전달).
- Produces: `combine_flip_verdicts(judge_fwd: Dict, judge_rev_normalized: Dict, labels: Sequence[str], priority: Sequence[str]) -> Tuple[str, Dict]` — 순수 함수(합의/합산/동점 정책), **normalized(canonical) 입력만** 소비.
- Produces: `normalize_flip_verdict(judge_raw: Dict, display_to_canonical: Mapping[str, str], labels: Sequence[str]) -> Dict` — winner·ranking·verdicts 전 라벨 canonical 역매핑 + 완전성 fail-closed(N개·중복0·permutation·winner 유효, 위반=ValueError).
- Produces: `bind_current_generation_context(fn)` (image_capture/context.py) — 부모 `current_context()` 캡처→worker에서 `bind_context`/`reset`.

- [ ] **Step 1: 실패 테스트 작성** — ①roll_refs 라벨 집합 불일치=ValueError ②roll_refs가 지문에 포함(같은 입력=동일 지문, 한 라벨 refs 변경=지문 드리프트) + judge_flip/flip_priority/critique 모드/judge header도 지문 기여(HIGH-4)·default 시 기존 지문 byte-identical 골든 ③gen_fn이 라벨별 refs로 호출됨 ④critique가 selected 라벨 refs로 호출됨 + `critique_selected_prompt_only=True`면 critique 프롬프트=`roll_prompts[selected]` 단독(base 중복 0, BLOCKING-1) ⑤`normalize_flip_verdict`: 역순 display→canonical 전 필드 역매핑, malformed(라벨 누락/중복/비permutation)=ValueError(BLOCKING-2) ⑥`combine_flip_verdicts`: 합의=승자/불일치=합산 최대/동점=priority 순 ⑦judge_flip=True 시 judge_fn 2회(정·역순 후보)+record에 raw·normalized 병록+정순 완료 직후 중간 persist(NARROW-2) ⑧parallel_rolls=True에서 gen_fn 동시 실행·예외 시 canonical 순 첫 예외 raise·완료 롤 파일 보존(NARROW-1) ⑨parallel_rolls 시 worker에 부모 budget **및 generation_context** 전파(BLOCKING-3 — `install_budget`+`generation_context` scope 내에서 병렬 실행, worker 관측+capture queue 전건 적재) ⑩CaptureQueue 동시 append 무손실
- [ ] **Step 2: 테스트 FAIL 확인** — `backend/.venv/bin/python -m pytest tests/unit/test_multiroll_select.py -x -q` (backend cwd)
- [ ] **Step 3: 구현** — 검증(라벨 집합)·지문(`extra`에 per-label refs 해시)·N롤 생성 분기(순차/ThreadPool+`bind_current_budget`)·judge_flip(정순 judge 호출→역순 judge 호출→`combine_flip_verdicts`)·critique refs 선택(`roll_refs[selected] if roll_refs else labeled_refs`)·record 키(roll_refs 마커·judge_flip). roll_prompts=None+roll_refs=None+flip=False+parallel=False 경로는 바이트 불변(기존 지문·record 동일 — 골든 잠금 테스트).
- [ ] **Step 4: 전체 유닛 PASS 확인** — `backend/.venv/bin/python -m pytest tests/unit -q`
- [ ] **Step 5: 커밋** — `feat(still-variants): multiroll roll_refs·parallel_rolls·judge_flip 확장`

## Task 2: still_prompt_variants 팩 v1 + still_variants 모듈

**Files:**
- Create: `prompts/_base/still_prompt_variants/1.<YYYYMMDDHHmm>/author_sys.md`, `author_schema.json`
- Create: `backend/app/modules/pipeline/still_variants.py`
- Test: `backend/tests/unit/test_still_variants.py`

**Interfaces:**
- Produces: `resolve_variants_pack_version(selector) -> str`, `AUTHOR_MODEL = "gpt"`, `STILL_VARIANTS_CONTRACT_VERSION = 1`
- Produces: `validate_variants(data) -> None`(2개·비어있지 않음·상호 상이, 위반=ValueError)
- Produces: `author_fingerprint(*, base_prompt, shot_text, pack_resolved, model_physical) -> str`
- Produces: `author_still_variants(*, base_prompt, shot_text, cached_entry, call_structured_fn, project_config, opik_metadata) -> Tuple[Dict, str, bool]` — (저작본, author_fp, 재사용 여부); fp 일치 캐시=0콜
- Produces: `assemble_still_roll_prompts(*, base_prompt, variants, labels) -> Dict[str, str]` — 라벨↔변형 순서 고정(A/C=v1, B/D=v2는 caller 매핑), 각=base+VISUAL APPROACH 절+**hierarchy_tail(팩 소유, 항상 마지막 — BLOCKING-1)**
- Produces: `load_variant_judge_header(version) -> str` — 변형 모드 중립 judge header(HIGH-6, 팩 소유)
- Produces: `ab_label_map() -> Dict[str, Dict[str, Any]]` — `{"A":{"variant":0,"conti":True}, "B":{"variant":1,"conti":True}, "C":{"variant":0,"conti":False}, "D":{"variant":1,"conti":False}}` (계약 상수)

- [ ] **Step 1: 실패 테스트** — validate(1개/빈 prompt_en·approach_ko/비str 타입/**정규화 후 prompt_en 동일**=ValueError — HIGH-5), fp(입력·팩·계약 버전·물리 모델 민감), assemble(base 전문 1회 보존+절 주입+hierarchy tail 최후미+라벨 수 검증), 캐시 재사용(fp 일치=call 0회)+force=archive 후 재저작, retry 프롬프트에 이전 violations 병기+base 전문 유지, ab_label_map 계약 고정
- [ ] **Step 2: FAIL 확인**
- [ ] **Step 3: 팩 저작** — author_sys: 동일 사실·상이 시각 접근 2종 계약(발명·삭제 금지, base 모순 금지, 상호 실질 상이, 시나리오 고유명사 미포함 범용 계약). schema: variants[2]{approach_ko, prompt_en}. hierarchy_tail.md(base 우선 계약)·judge_header.md(중립 브리프 header) 포함
- [ ] **Step 4: 모듈 구현 → PASS 확인**
- [ ] **Step 5: 커밋** — `feat(still-variants): still_prompt_variants 팩 v1 + 저작·조립 모듈`

## Task 3: still_recipe_service 배선 + flag + config_hash

**Files:**
- Modify: `backend/app/core/config.py` (`still_variants_enabled: bool = False`)
- Modify: `backend/app/modules/pipeline/still_recipe.py` (`run_branch_select`에 roll_prompts/roll_refs/parallel_rolls/judge_flip/flip_priority/critique_selected_prompt_only/judge_prompt_header pass-through — 기본값=미전달 시 기존 호출 byte-identical)
- Modify: `backend/app/services/still_recipe_service.py`
- Modify: `backend/app/core/steps/image_steps.py` (hash 스탬프)
- Test: `backend/tests/unit/test_still_recipe_service_variants.py` (신규), 기존 서비스 테스트 불변 확인

**Interfaces:**
- Consumes: Task 1 `run_multiroll_select` 확장 시그니처, Task 2 모듈 전부
- Produces: flag ON 경로 — 샷 루프에서 ①`author_still_variants`(records `{tag}::variants` 캐시) ②ab_active: `build_ab_branch_refs`→roll_refs 4라벨 조립→`run_branch_select` 1회(roll_count=4, judge_flip=True, parallel_rolls=True, out_stem=`recipe_dir/tag`)→승자∈{A,B}시 conti lineage 부착 ③비A/B: roll_count=2·roll_prompts 변형 ④judge_texts/judge_schema를 후보 수(4/2)별 해석 ⑤extra_fingerprint에 variants 팩·계약 버전 추가(ON시만)

- [ ] **Step 1: 실패 테스트** — flag OFF=기존 호출 인자 byte-identical(골든 — **신규 kwargs 자체 미전달** 확인, Codex R6/R7 조건), ON: A/B 샷이 단일 run 호출(roll_count=4·roll_refs 콘티 매핑·judge_flip·critique_selected_prompt_only·중립 judge header)·승자 D면 conti lineage 미부착·`{tag}::conti_ab_decision` 미기록·prev 앵커 경로=`{tag}_sel.png`, 비A/B=2롤 변형(flip 없음·단일 판정), 저작 실패 2회=mark_failed 샷 격리, hash ON시 스탬프(variants 팩·저작 모델 alias+물리·계약 버전), `winner_exact_multiroll_tag` A~D/fix exact + miss=None 실행 경로(NARROW-3), 플레이트 multiroll 호출부=parallel 미전달(OFF) 유지
- [ ] **Step 2: FAIL 확인**
- [ ] **Step 3: 구현** (config flag → 서비스 분기 → hash 스탬프 순)
- [ ] **Step 4: 전체 PASS** — `backend/.venv/bin/python -m pytest tests/unit tests/core tests/pipeline -q` (baseline 9 불변 확인)
- [ ] **Step 5: 커밋** — `feat(still-variants): 서비스 배선 — A/B 4택1·비A/B 2롤 변형·병렬 생성 (flag OFF)`

## Task 4: Codex 리뷰 사이클

- [ ] 구현 diff 전체 Codex 코드 리뷰 요청(tmux-bridge, 규약 준수) → 지적 자체 분석 후 수용/기각(형식 수용 금지) → 반영 커밋 → APPROVED_TO_E2E까지 반복

## Task 5: E2E 8회차 (새 프로젝트)

- [ ] run_e2e_round7.py 기반 러너 사본(신규 프로젝트 생성, 오염된 3d854b4e 재실행 금지) — env: 기존 E2E7 플래그 전부 + `STILL_VARIANTS_ENABLED=true` + `FLOOR_PLAN_PROMPT_VERSION=11`, backend cwd+`backend/.venv` 절대경로, `background_mode` assert 가드 유지
- [ ] acceptance(Codex 합의 반영): ①A/B 샷 전건 4택1 record(variant_map·judge_flip raw+normalized 양회 verdicts) ②비A/B 전건 2롤 변형 record ③승자 라벨 분포 양방향 실선택(전건 동일 라벨 아님) ④콘티 lineage=승자∈{A,B} 샷만 ⑤이미지 콜 수 실측이 표 예상과 정합 + **budget count=실제 provider 시도 일치** ⑥429 발생 시 resume 완주 + **resume에서 완성 롤 재생성 0·missing 롤만 생성** ⑦**forward/reverse canonical 합산 감사값 재계산 일치** ⑧**A/B 후보 4개 wall-clock overlap 실측(병렬 증명)** ⑨**병렬 candidate capture 4건 + selected/fix generation_call_id exact** ⑩**critique prompt의 base 중복 0**
- [ ] 갤러리 빌드(LAN IP·외부 접속 웹서버) → 사용자 육안 대기
