# Changelog

All notable changes to `gflow-cli` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.20.0] — 2026-06-15

### Added

- **Forced Agentic UI detection** (exit code 25): dynamically detects if the account's Flow editor page has been placed in Google Flow's new Agentic UI A/B cohort. Raises `FlowAgentUiError` and captures a diagnostic viewport screenshot (`debug_forced_agent_ui.png`) to exit cleanly and prevent CLI hangs.
- **Pluggable Flow UI driver strategy** (`FlowUiDriver`): the UI-automation transport now probes the editor DOM **per generation** (the cohort flaps per page load) and binds a `ClassicFlowUiDriver` or `AgenticFlowUiDriver`, so the two cohorts' selectors never share code. Classic generation is unchanged.
- **Agentic-cohort image generation** (validated live): when the editor is served the Agentic UI, `gflow image` drives it by encoding settings (count / aspect) into the conversational prompt directive and scraping generated assets directly from the DOM — **deduplicated by media UUID** — because the Agentic UI routes generation through a background Web Worker that defeats page-level network capture. Content-policy blocks (detected in alert/dialog regions) fail fast with `ContentPolicyError` (exit 5). Agentic **video** still raises `FlowAgentUiError` (exit 25) pending a validated scraping path.
- **`GFLOW_CLI_FORCE_AGENT_UI`** (testing/diagnostic opt-in): forces the agentic composer by clicking the in-input "Agent" toggle after entering the editor, so the agentic path can be exercised deterministically regardless of the server-assigned A/B cohort (which has no client-readable flag). Unset by default — the cohort is auto-detected per generation. See `docs/AGENT_UI_E2E.md`.

## [0.19.0] — 2026-06-12

### Added

- **Opt-in Patchright browser engine** via `GFLOW_CLI_BROWSER_ENGINE=patchright`
  (default `playwright`, unchanged). Patchright is a drop-in patched Playwright
  (Chromium) that runs page evaluations in an isolated execution context to
  avoid the `Runtime.enable` CDP leak, for stronger reCAPTCHA-Enterprise evasion
  on the **headed** path. It is **not** a headless unlock and must be installed
  separately: `pip install 'gflow-cli[patchright]'`. The default engine is
  byte-identical to before. `gflow auth status` now reports the active engine.
- `BrowserEngineUnavailableError` (exit code 24): selecting `patchright` without
  the package installed now fails with a clear `pip install patchright`
  remediation hint instead of a raw `ImportError` hashed to a generic exit 1.

### Fixed

- An invalid `GFLOW_CLI_*` enum value (e.g. a typo'd `GFLOW_CLI_BROWSER_ENGINE`,
  `GFLOW_CLI_PROVIDER`, or `GFLOW_CLI_LOG_LEVEL`) now fails with a clean
  configuration error and **exit code 11** naming the offending variable, instead
  of leaking a raw pydantic `ValidationError` traceback and exiting 1.

## [0.18.0] — 2026-06-12

### Added

- `UiSelectorDriftError` (exit code 23): UI-automation selector-probe failures — e.g. the
  mode-switch `crop_*` trigger missing from the Flow editor (issue #183) — now raise a typed
  error carrying the probe name, the debug-screenshot path, and a remediation hint, instead
  of an opaque "Unexpected error" (exit 1) whose message was hashed away in logs. Converted
  probes: mode-switch trigger, Image/Video mode tabs, and video sub-mode tabs, in both the
  image and video transports.

### Fixed

- `gflow image` commands (`t2i` / `i2i` / `upscale` / `upload`) now plumb their output
  directory into the API client, so debug screenshots are actually captured on
  UI-automation failures. Previously the transport's screenshot directory was never set on
  the image path and drift errors reported `Screenshot: None` even with `--verbose`.

## [0.17.0] — 2026-06-12

### Added

- Added `verify_flow_profile` in `gflow_cli.auth.verification` using `browser_cookie3` and `httpx` to verify sessions directly from the Chrome cookie store (fast path), with a marker-gated Playwright fallback for encrypted/locked stores. `RealChromeStrategy` now writes the Chrome marker before verification (the fallback reads it) and, on failure, rolls back only a speculative write — a marker that legitimately pre-existed (a previously-verified chrome profile) survives a transient probe failure, and an interrupted verification never leaves an unverified profile claiming the chrome strategy. Cookie extraction is centralised in the new `gflow_cli.auth.cookies` module.

### Fixed

- `gflow_cli.auth.cookies._get_chrome_cookies3` now catches `RuntimeError` (Windows DPAPI failure — `RuntimeError('Failed to decrypt the cipher text with DPAPI')`) in addition to `browser_cookie3.BrowserCookieError`, and re-raises both as `PermissionError` so the Playwright fallback is triggered instead of propagating an unhandled exception.
- `verify_flow_profile` now retries transient HTTP failures (429/503/504) and network errors up to `_MAX_ATTEMPTS` times with exponential backoff, matching the retry behaviour of the existing Playwright-based `verify_flow_session`.

### Changed

- Entity-attach `WireFormatError` failures (exit 7) now carry a remediation hint pointing at Flow's new full-page media-library UI rollout ([#174](https://github.com/ffroliva/gflow-cli/issues/174)) — affected accounts can stage entities via the include action but the submit never carries `referenceEntities`; the error now explains how to tell which UI an account has and where to follow the fix, instead of the generic file-a-bug hint. Both backstops also emit an `entity_attach_context` discovery field (`video`/`image`) for drift telemetry. New KNOWN_ISSUES entry documents the rollout

## [0.16.0] — 2026-06-12

### Fixed

- **`--reference-entity` no longer fails on non-Portuguese Flow accounts** ([#170](https://github.com/ffroliva/gflow-cli/issues/170)) — the resource-picker include selectors hardcoded the pt-BR caption "Incluir no comando"; they are now locale-free tier cascades (context-menu `add`-ligature anchor + pt/ru/en text fallback), fixing `gflow image t2i --reference-entity`, movie R2V entity attach, and Vozes voice attach for every account language. Failures are now typed (`TransportTimeoutError`, exit 9) with a locale-neutral remediation hint, the matched selector tier is logged (`include_selector_tier`) for drift telemetry, and a new image-side submit backstop raises `WireFormatError` (exit 7) if a staged character never reaches the wire instead of silently returning a text-only generation. Thanks @papushin7987 for the live-verified report

### Added

- **`gflow image upscale <mediaId> --scale 2k|4k`** — upscale a platform-generated image to 2K or 4K (Flow's download-menu 1K/2K/4K options) and save it locally; credit-free. The owning project is resolved from the local catalog (or pass `--project` for images gflow didn't record). 4K requires a Flow Ultra subscription — a non-Ultra 4K request fails fast with exit code 22 (`UpscaleUnavailableError`) rather than a generic 403. Reverse-engineered wire documented in [docs/IMAGE_UPSCALE_RECON.md](docs/IMAGE_UPSCALE_RECON.md); live-verified end-to-end (issue #171)

## [0.15.1] — 2026-06-10

### Changed

- Added `--disable-dev-shm-usage` to Chrome launch args in both `FlowApiClient._persistent_context_kwargs()` and `UiAutomationTransport.setup()` — prevents OOM in Docker containers with the default 64 MB `/dev/shm` allocation; no effect on developer machines with adequate shared memory
- README Stats badges hardened against shields.io outages — GitHub-stat badges now pass `cacheSeconds=3600` (mitigates shields.io's shared GitHub token-pool exhaustion, the "Unable to select next GitHub token from pool" error) and the PyPI downloads badge moved from `img.shields.io/pypi/dm` to pepy.tech (pypistats rate-limits shields.io upstream)

### Added

- `scripts/diag/` directory — documented home for investigation scripts that require a live authenticated profile; includes `memory_profile.py` (Chrome process-tree RSS profiler for issue #155), `capture_flow_traffic.py`, and `recaptcha_mint.py` (both moved from `scripts/` root via git mv, history preserved)

### Fixed

- Video status poll now raises `AuthExpiredError` (exit 3) immediately on HTTP 401 from `batchCheckAsyncVideoGenerationStatus` instead of silently timing out after 600 s with a bare `TimeoutError` (exit 1) — session expiry is now detected mid-workflow, not only at login time (issue #156)

## [0.15.0] — 2026-06-09

### Added

- **`gflow image` can reference locked CHARACTER entities** for character-consistent
  stills. New `--reference-entity <id>` (repeatable) + `--reference-entity-name` on
  `image t2i` / `i2i`, plus `--project <id>` to generate in an EXISTING project (where
  the entities live) instead of a throwaway scratch project. Entities attach through
  the editor's **Personagens picker** and ride the submit as `referenceEntities`
  (confirmed against the live API); `_build_batch_generate_images_body` serializes them
  so non-UI/headless transports get parity too. Entities count toward the per-model
  reference cap. `--project` / `--reference-entity` are single-prompt only; for a pure
  character reference use `t2i` (`i2i` still needs a `--ref`). See `docs/USAGE.md` →
  "Character-consistent images (entity references)".

### Fixed

- `--project` no longer overwrites an existing project's stored title/source in the
  local history DB (the recorder preserves the curated title for non-created projects).

### Security

- `--project` / `--reference-entity` ids are validated at the CLI boundary
  (`[A-Za-z0-9-]{1,128}`), closing an unvalidated `page.goto` navigation path
  (`project_editor_url` lacked the allowlist its sibling routes enforce) and a
  CSS-selector injection vector (`data-tile-id='fe_id_<id>'`). The request-body debug
  logger elides large reference-field values so Flow-built image bytes can't leak into
  logs.

### Internal

- Bump dev/CI `ruff` to `0.15.16` (both the `dev` extra and the `dependency-groups`
  pin; supersedes Dependabot PR #165, which updated only the soft `>=` extra and not
  the hard `==` group pin CI actually uses). `ruff check` / `format --check` clean.

## [0.14.0] — 2026-06-07

### Added

- **`gflow movie` — multi-scene, character-consistent video generation.** A TOML
  manifest (`gflow movie template` / `gflow movie run`) drives a sequence of clips
  that reuse a single Flow CHARACTER entity (reference-to-video) so the same face
  and voice carry across every scene. Generate-only by default; `--stitch`
  produces an ffmpeg preview concat; runs are crash-resumable via the sibling
  `<manifest>-state.json`; a versioned handoff manifest is written for downstream
  composition (e.g. Remotion). Deterministic prompt assembly (`composition.py`),
  scene = clip.
- **`docs/MOVIE.md`** — manifest format, the run lifecycle (the headed browser is
  required through generate → poll → download), the character-entity attach
  mechanism, and the best-effort consistency model.
- Dev utilities: `scripts/dev/make_project.py` (create a Flow project) and
  `scripts/dev/patch_character.py` (rename / set voice + personality on an entity).

### Fixed

- **R2V character reuse now actually rides the wire.** The entity is attached via
  the resource picker's **Personagens tab → right-click → "Incluir no comando"**
  (which stages `referenceEntities`; a left-click on the Tudo tile only stages the
  thumbnail as a `referenceImage`). The submit backstop now reads the response's
  real `media[].mediaMetadata.requestData.videoGenerationRequestData.videoGenerationEntityInputs`
  path instead of the request-shape `requests[].referenceEntities` — which had
  false-rejected every successful entity generation. `omni-flash` R2V verified to
  carry the entity.
- Cleared pre-existing type/test debt: `pyright src` is clean again (the missing
  `project_id` parameter was added to the `VideoCapableTransport` protocol and the
  `_enter_editor` type stub); regenerated `uv.lock` (jsonschema dev dependency).

## [0.13.0] — 2026-06-04

### Added

- **`gflow character rm` — delete a Character entity (#150).**
  `gflow character rm --project <id> (--id <entityId> | --name <name>) [-y/--yes] [--json]`
  deletes a Character via `POST flow:batchDeleteAssets` (Bearer; **FREE** — no
  reCAPTCHA, no credit). Resolves by id or exact name (ambiguous name exits
  **11**); prompts for confirmation unless `--yes`/`--json`.

- **In-project governance enforcement (advisory-first).** Made the AI-driven
  development flow followable and partly machine-enforced in-repo, modeled on the
  reference AI-DLC governance orchestrator's advisory-by-default behavior:
  ruff `T20` now bans raw `print()` in `src/`; an advisory branch-naming check and a
  non-blocking materiality + traceability classifier (`scripts/ci/check_materiality.py`
  + `governance-advisory.yml`) recommend `/gflow:predict` + council review when
  sensitive paths (`auth/`, `api/transports/`, `api/client.py`, `_sapisidhash.py`,
  `data/`, `recaptcha`) are touched, without ever blocking a merge. A
  history-replay backtest (`scripts/dev/materiality_backtest.py`) calibrates the
  gate — it measured a 1.1% false-positive rate (vs. a 20-30% estimate) and
  raised fix-coverage from 61% to 74% by surfacing auth-token plumbing that lived
  outside `auth/`. The backtest is a first-class, repeatable artifact (`--json`
  mode, a monthly `governance-benchmark.yml` dashboard job) fully documented in
  [`docs/GOVERNANCE_BENCHMARK.md`](docs/GOVERNANCE_BENCHMARK.md); the gate itself is
  described in
  [`docs/AGENT_GUIDE.md` § Governance & Enforcement](docs/AGENT_GUIDE.md#governance--enforcement).

### Changed

- **`gflow video i2v` frame flags aligned with Flow UI terminology (#122).**
  `--initial-frame FILE` is the new canonical flag for the start image (matches
  Flow's "initial frame" label). `--end-frame FILE` replaces `--end-image` as the
  canonical end-frame flag. `--end-image` is kept as a **deprecated alias** (emits
  `DeprecationWarning`; will be removed in a future minor release). The positional
  `IMAGE` argument remains supported for back-compatibility.

### Fixed

- **Character editor 404 on non-English locales (#153).** `gflow character`'s
  editor URL interpolated the locale verbatim, so the genuine default BCP-47
  `en-US` produced `/fx/en-US/…`, which 404s the Flow character editor (only the
  short primary subtag is a valid `/fx/<seg>/` segment). `character_editor_url`
  now normalizes a BCP-47 tag to its lower-cased short segment (`en-US → en`,
  `pt-BR → pt`), so the tool stays language-agnostic with the real default
  locale.

## [0.12.0] — 2026-06-03

### Fixed

- **Create-project generation failing when Flow opens the Agent _chat panel_.**
  A follow-up to the earlier Agent-pill fix: Flow now also surfaces Agent mode as
  a docked chat side-panel ("Untitled session") on some project opens, and while
  it is up the in-composer Agent pill is absent from the DOM — so the pill-only
  recovery could not find anything to click and generation still failed with
  "mode-switch dropdown trigger not found". `_exit_agent_mode` now handles both
  Agent shapes in one pass: it dismisses the chat panel (locale-stable, aria-free
  structural close anchor) which reveals the pill, then turns the pill off,
  looping until the media panel re-mounts. Keyed on the outcome (`crop_*` is
  back), so it covers pill-only, panel-only, and panel-then-pill without assuming
  which control is present.

### Added

- **`gflow character` command group — reusable Flow Character entities (#145).**
  Mint a project-scoped **Character** (a named subject with reference images, an
  optional voice, and an optional personality) so the same subject appears
  consistently across generations:
  - `gflow character create --project <pid> --name <name> --face-prompt "…"
    [--body-prompt "…"] [--voice <Name>] [--personality "…"]
    [--model nano2|nanopro]` — two-step generation: a **face** reference (slot 0),
    then a self-contained **front/side/back triptych body** (slot 1) seeded by the
    generated face. gflow injects its own triptych instruction, so one body
    generation yields all three angles. Characters have **no aspect-ratio
    control** and exactly two models — `nano2` (Nano Banana 2, default) and
    `nanopro` (Nano Banana Pro). Generated images are **downloaded** to local (or
    cloud) storage; the signed `fifeUrl` is used only at download and never
    persisted.
  - `gflow character list --project <pid>` — list every Character in a project.
  - `gflow character show --project <pid> (--id <entityId> | --name <name>)` —
    show one Character; an ambiguous `--name` exits 11.
  - `gflow character voices` — list the 29-name Gemini voice catalog
    (name / description / sample-url); `--voice` is validated case-insensitively.

  The credited generation rides Flow's own page JS in the character editor
  (Option B, UI passive-capture — a self-assembled direct POST is reCAPTCHA-403
  walled); the structural calls (createEntity, workflow/entity PATCH,
  projectInitialData) are credit-free REST. Creation runs as a
  **persist-before-spend, crash-recoverable saga**: the `entityId` and each
  completed slot are recorded before/as credits are spent, so a crashed run
  resumes without orphaning a paid generation or double-charging. Live-verified
  end-to-end on 2026-06-02 (face + triptych body, both bound, downloaded, read
  back). See [docs/CHARACTER.md](docs/CHARACTER.md).

- **`gflow video chain` — last-frame I2V chaining.** Render a JSONL manifest of
  *links* into one continuous sequence: link 0 is a text-to-video generation,
  and every later link is an image-to-video generation **seeded by the extracted
  last frame of the previous clip**, giving visual continuity with no
  server-side stitching. Each link is a sequential paid Veo generation (**one
  credit per link**); a cost-confirmation gate (`-y`/`--yes` to skip),
  `--dry-run` plan preview, `--max-links` cap (exit 11), and
  `--resume-from <chain-id>` (skips already-paid links, no re-billing) make the
  spend explicit and recoverable. Per-link wire-route checking aborts loudly if
  Flow drops the seed frame and routes an i2v link to the text-only endpoint
  (issue #125), so a misroute can never be reported as a successful chain.
  Only the Veo 3.1 models (`veo-lite`/`veo-fast`/`veo-quality`/`veo-lite-lp`)
  are accepted; `omni-flash` is rejected. Chain links are recorded locally
  (SQLite migration `0005`) to drive `--resume-from`. The frame extractor uses
  PyAV via a new optional **`[chain]`** extra (`pip install 'gflow-cli[chain]'`)
  — no system ffmpeg required. Each link is saved as its own mp4; concatenating
  the clips into one file is a separate step — use `gflow scene` (auto-concat is
  deferred, see [KNOWN_ISSUES.md](KNOWN_ISSUES.md)).

- **`gflow scene` command group (Add Clip / Scenes).** Compose ordered,
  trimmable video clips into a Flow **Scene** over the credit-free aisandbox
  REST surface (no reCAPTCHA, no credits):
  - `gflow scene create --project <pid> <workflowId>[:<start>-<end>] [...]` —
    compose a scene from one or more existing clips (repeat an id to duplicate;
    optional per-clip trim in seconds).
  - `gflow scene show --scene <sid> --project <pid>` — read back a scene's clip
    order and trims.
  - `gflow scene create … --output extended.mp4` — render the composed scene
    into a single **extended video** via Flow's server-side concatenation
    (`runVideoFxConcatenation`) — credit-free, no reCAPTCHA, **no ffmpeg**. The
    combined MP4 is fetched inline and written locally (or to the configured
    cloud `storage_uri`). `--force` overwrites an existing output.

  Scene compositions are recorded locally (SQLite migration `0003`) — including
  each clip's media id + trims, and the rendered extended-video path (migration
  `0004`) — so a compose survives a later render failure and the output is
  discoverable for recovery. The append-to-existing-scene verb (`add-clip`) is
  deferred — see the project backlog.

## [0.11.0] — 2026-05-31

### Changed

- **`gflow video i2v` default model is now `veo-lite`** (was: inherit Flow's
  last-used model, which was typically `omni-flash`). The Veo 3.1 family is
  the only model line that supports i2v interpolation; `omni-flash` is now
  rejected for any i2v invocation (start-only or start+end) and has been
  removed from the i2v `--model` choices. Because `--duration 10` is
  omni-flash-only, the i2v `--duration` choices are now `[4|6|8]`. `omni-flash`
  (and `--duration 10`) remain valid for `gflow video t2v` and `gflow video r2v`.
  See issue #125.

### Fixed

- **`gflow video i2v` silently produced text-to-video output, ignoring the
  start/end frames (issue #125).** When the model was `omni-flash` (Flow's
  last-used default in most sessions), Flow's frontend dropped the bound
  start/end frame references at submit time and routed every call to
  `batchAsyncGenerateVideoText` with `image_inputs: null` — charging a credit
  for a pure text-to-video generation that had no visual relationship to the
  supplied frames. **Every i2v paid run on v0.10.0 before this fix produced
  T2V output regardless of the start/end frames.** Fix: `omni-flash` is
  dropped from the i2v `--model` choices and the i2v default is now `veo-lite`;
  a defense-in-depth transport guard raises `ModelModeIncompatibilityError`
  (exit code 17) for direct `FlowApiClient` callers that bypass the CLI.

- **`gflow video i2v` could still route to T2V even with a valid Veo model,
  because the model-picker option `Veo 3.1 - Lite` was never selected (issue
  #125, second path).** The picker selector was an exact-match
  (`:text-is('volume_upVeo 3.1 - Lite')`) that hardcoded a Material Symbols
  icon-ligature prefix; when it missed, `_select_video_model` warned and
  continued, leaving Flow on `omni-flash` → the frames were dropped to T2V.
  Fixes: (1) the selector is now a robust substring match
  (`:has-text('Veo 3.1 - Lite'):not(:has-text('[Lower Priority]'))`); (2) for
  i2v, a model-select miss is now FATAL — `_select_video_model(required=True)`
  retries the picker then raises `VideoModelSelectionError` (exit code 18)
  *before* any frame attach or submit, spending no credit; (3) a post-submit
  backstop raises `WireFormatError` if an i2v request is still observed routing
  to the T2V endpoint, so a "successful" T2V is never reported as i2v.

- **Create-project generation failing when Flow's "Agent" composer mode is active.**
  Flow's newer editor adds an Agent toggle next to the prompt box; when it is on,
  the media-generation panel (the `crop_*` settings trigger, Image/Video mode
  tablist, and count/model controls) is removed from the DOM, so the UI-automation
  transport raised "mode-switch dropdown trigger not found". `_switch_to_image_mode`
  and `_switch_to_video_mode` now call `_exit_agent_mode()` first, which re-mounts
  the panel by clicking the toggle off. Detection is locale-invariant and uses no
  UI text and no `aria-` attribute (`button:has(span.content)` plus the absence of
  the locale-stable `crop_*` trigger), so it works in every Flow UI language.

- **`gflow image t2i` / `i2i` model selection hardened for non-English Flow UIs
  (issue #94).** `IMAGE_MODEL_OPTION_SELECTORS` is now a selector *cascade*
  (consistent with every other selector group) instead of a single exact-match
  string, so `_select_image_model` no longer silently fails to select the
  requested model when Flow's menu markup shifts. The redundant `--lang=en-US`
  Chromium launch arg was removed — Flow's branded model names ("Nano Banana 2",
  "Nano Banana Pro", "Imagen 4") are not localised and `FLOW_URL`'s `?hl=en`
  already locks the SPA to English, so the override was a no-op.

## [0.10.0] — 2026-05-29

### Fixed

- **Image and video counts leaking across profiles in `gflow data list projects` (issue #113).**
  Fixed an issue where project media counts were combined if two different profiles
  happened to share the same `flow_project_id`. The counts in `_LIST_PROJECTS_SQL`
  are now strictly scoped to the active `profile_name`.

- **List queries fan-out when multiple operations claim an asset (issue #111).**
  Fixed a bug in `gflow data list images` and `gflow data list videos` where 
  assets would be duplicated or have non-deterministic prompts if multiple 
  operations (e.g., retries) claimed the same output asset. The SQL queries 
  now use a deterministic subquery grouping by `asset_id` to ensure exactly 
  one-to-one cardinality.

### Added

- **Google account identity persisted to every profile (`issue #92`).** Both
  auth strategies (`real_chrome`, `internal_chromium`) now write a
  `.gflow_account` file to the profile directory immediately after the session
  is verified, durably associating the signed-in email with the profile on disk.
  `ProfileMeta` gains a `google_account: str | None` field populated by
  `profile_store.list_profiles()` from that file. `gflow auth list` (table and
  `--json`) now includes a **Google account** column so every profile is
  immediately identifiable — no more opaque `default` entries.  The `--json`
  output gains the `google_account` key for programmatic callers.  Closes #92.

- **Auto-rename of the first-run `default` profile to email local-part.** When
  `gflow auth login` creates the first profile and no `--profile` flag was given,
  the profile is named `default` as a placeholder. After the session is verified
  and the email is known, `auth login` automatically renames `profile_default` to
  `profile_<email-local-part>` (e.g. `profile_ffroliva`) and updates
  `config.toml`'s `default_profile` pointer atomically. The local-part is
  sanitized to a filesystem-safe name (characters outside letters, digits, `-`,
  and `_` become `-`), so `flavio.oliva@gmail.com` → `profile_flavio-oliva` and
  `user+flow@gmail.com` → `profile_user-flow`. Existing `default`
  profiles that were created before this change continue to work; they gain the
  email column the next time `gflow auth login` is run against them.  Closes #92.

- **`profile_store.rename_profile(old_name, new_name)`** — reusable primitive that
  renames a profile directory and updates `config.toml` when the renamed profile
  was the default. Raises `FileNotFoundError` / `FileExistsError` on invalid input.

- **Zero-credit smoke test for profile account persistence
  (`tests/smoke/test_profile_account_smoke.py`).** Three smoke tests that verify
  the full observable chain — account file present + readable, `list_profiles()`
  surfaces `google_account`, `gflow auth list --json` includes the field — against
  a real authenticated profile. No image or video generation; zero Flow credits.
  Backfills the `.gflow_account` file for profiles created before the fix so the
  tests work on existing sessions. Opt-in via
  `GFLOW_CLI_E2E_PROFILE=<name> pytest -m smoke tests/smoke/test_profile_account_smoke.py`.

- **Aggregated asset view in `gflow data list images/videos`.** By default,
  listing images or videos now returns one row per asset (Flow media ID),
  collapsing multiple local copies into a single entry with a `COPIES` count
  and the path of the latest copy. This prevents duplicate rows when
  re-downloading the same media to different directories. The `copy_count`
  is also exposed in JSONL output.
- **`--all-copies` flag on `gflow data list images/videos`.** Restores the
  previous behavior of showing every local file as a separate row.
- **`gflow data prune` command.** New maintenance utility to remove stale
  `local_files` database entries for local paths that no longer exist on
  disk. Only targets local files (ignores cloud-stored assets). Supports
  `--dry-run` to preview deletions and `--profile` to limit the scan.
- **External storage documentation for S3, MinIO, and Google Cloud Storage.**
  Adds `docs/EXTERNAL_STORAGE.md`, cross-links it from the README, docs index,
  configuration, data-layer, usage, security, and user-guide docs, and clarifies
  that `GFLOW_CLI_STORAGE_URI` is a cloud-only output mode rather than
  local-plus-cloud dual-write.
- **`--json` flag on `gflow video t2v`, `gflow video i2v`, and `gflow video r2v`.**
  Emits the `VideoResult` (status / command / media_id / generation_status /
  succeeded / local_path / failure_reasons / error_message) plus the request
  echo (model / mode / aspect / duration / count / seed) as a single JSON
  object on stdout. A failed generation still emits its JSON payload and
  then exits 1. The data-layer recorder
  (`record_started_video` / `record_completed_video`) fires regardless of
  `--json` so audit history is independent of the output channel. E2e
  coverage for `--json` shape across image / video / auth / models lives at
  `tests/e2e/test_json_output_e2e.py`.
- **`--json` flag on `gflow image t2i` and `gflow image i2i`.** Emits the
  complete `GeneratedImage` result (every field — `media_name`,
  `workflow_id`, `seed`, `prompt`, `model_name_type`, `aspect_ratio`,
  `dimensions`, `fife_url`, `is_signed_url`) plus the on-disk `local_path`
  as a single JSON object on stdout. A worker keys `images[0].seed` for
  refine-regen seed continuity. Single-prompt only (`--json` rejects
  multi-prompt batch with a Click usage error); progress chatter is
  suppressed so stdout is pure JSON. `ref_count` surfaces only on i2i.
- **`gflow models` catalog command.** New top-level command that enumerates
  the image and video model catalog as a Rich table (default) or as a single
  JSON object (`--json`). Per-model: `name`, CLI aliases (filtered to what
  the generation command's `--model` Choice actually accepts), `ref_cap`,
  `default` (image), `max_duration` (video). Built from the
  `Model` / `VideoModel` enums + their alias maps so the catalog can never
  drift from what the generation commands accept. A UI populating its model
  picker from `gflow models --json` is guaranteed to pass any selected alias
  back to `--model`.
- **`gflow_cli.json_output` module + `--json` error path on
  `run_with_handlers`.** Pure builders for image/video result payloads and
  RFC 9457 problem-details errors (plus a `retryable` flag worker schedulers
  key their retry-vs-absorb decision off — WAF / rate-limit / network /
  timeout). When `as_json=True`, errors emit a parseable JSON payload on
  stdout with the same exit code as the Rich path; the observability event
  still fires.
- **`--json` flag on `gflow auth list`.** Emits the profile inventory as a JSON
  array (`name` / `is_default` / `cookies_present` / `profile_dir` /
  `last_used_at`) on stdout so a programmatic caller (e.g. a worker discovering
  authenticated profiles) can `json.loads(stdout)` instead of scraping the
  Rich table.
- **Per-model r2v reference-image cap rebuilt as a data table.** Replaces the
  prior pair of constants (`OMNI_REFERENCE_CAP=7`, `VEO_REFERENCE_CAP=3`) with
  a `VideoModel -> int` mapping consulted by
  `gflow_cli.api.video.reference_cap_for(model)`. New entries:
  `veo_3_1_lite_lower_priority=3` (was implicitly covered by the veo branch),
  and `veo_3_1_quality=0` — Veo 3.1 Quality does NOT support
  Ingredients/References to Video at all per Google Flow's official support
  page; passing it to r2v raises a clear `does not support R2V
  (reference-to-video)` error rather than letting the request fail at the
  wire. CLI guard added on `gflow video r2v` (`click.UsageError`, exit 2)
  mirroring the i2i pattern so over-cap and quality+r2v fail before any
  profile/network work. E2e tripwire at
  `tests/e2e/test_video_r2v_ref_cap_e2e.py` asserts Flow actually consumes
  all `cap` refs at the at-cap boundary.
- **Per-model i2i reference-image cap.** Flow silently keeps only the first N
  reference images when an i2i request attaches more than the model accepts,
  so a caller could believe every ref was used. `gflow_cli.api.image.reference_cap_for(model)`
  exposes the live-observed per-model cap: NARWHAL (Nano Banana 2) and GEM_PIX_2
  (Nano Pro) accept 10, IMAGEN_3_5 (Imagen 4) accepts 3. Enforced as a domain
  invariant in `GenerateImageRequest.__post_init__` and at the CLI boundary in
  `gflow image i2i` (clean `click.UsageError` / exit 2 before any
  profile/network work). Mirrors the existing video r2v cap pattern. E2e
  tripwire at `tests/e2e/test_image_i2i_ref_cap_e2e.py` asserts Flow actually
  consumes all `cap` refs (one `reference_attached` event per ref) so a future
  silent truncation on the Flow side fails the test.
- **Layered e2e test strategy with cost sub-markers.** The single `e2e` marker is
  now augmented by cost sub-markers (`e2e_auth`, `e2e_image`, `e2e_video`,
  `e2e_batch`, `e2e_data`, `smoke`) so callers can run only the tier they can
  afford (zero-credit auth checks, single-credit image smoke, etc.). See
  `docs/E2E_TESTING.md` for the full reference.
- `tests/e2e/conftest.py` — shared `e2e_profile_dir`, `e2e_nosession_profile`,
  and `e2e_env` fixtures replace duplicated inline helpers in individual test files.
- `tests/api/transports/test_transport_timeout.py` — extracted from e2e as a
  pure-mock integration test; also fixes `Path("/dev/null")` → `Path(os.devnull)`
  for Windows portability.
- `tests/test_marker_registry.py` — invariant checks that every e2e test carries a
  cost sub-marker, and self-tests for the auto-marker conftest hook.
- `docs/E2E_TESTING.md` — comprehensive e2e strategy and layer reference document.

### Fixed

- Structlog logs are now routed to stderr (via
  `PrintLoggerFactory(file=sys.stderr)`) instead of stdout. Previously every
  CLI event leaked onto stdout, which broke the `--json` contract for
  programmatic callers — `json.loads(stdout)` failed because the JSON
  payload was preceded by event-log lines. Logs are diagnostics; stdout is
  data. No-op for human users (terminals still show logs the same way).

### Changed

- `gflow data media` now labels cloud-backed asset records as `cloud_uri_N`
  while keeping local assets under the existing `local_path_N` labels.
- `gflow_cli.api.video` no longer exposes the standalone `OMNI_REFERENCE_CAP` /
  `VEO_REFERENCE_CAP` constants. Callers that need a per-model R2V cap should
  use `reference_cap_for(model)` (which returns `0` for `VEO_3_1_QUALITY` —
  R2V is unsupported there). `MAX_REFERENCE_IMAGES` (= 7) is unchanged and
  still the absolute ceiling used when the model is unknown.
- `GFLOW_CLI_E2E_RUN_VIDEO` default flipped from `"1"` to `"0"`. The Veo step in
  `test_data_layer_e2e.py` is now **opt-in**: set `GFLOW_CLI_E2E_RUN_VIDEO=1` to
  include it. This prevents accidental Veo credit burns on unattended CI runs.
- `pytest` `addopts` now excludes `smoke` in addition to `e2e` and `live`:
  `not e2e and not live and not smoke`. Bare `pytest` never launches a live
  browser session.
- Auto-marker conftest hook uses `item.path.parts` instead of a slash-delimited
  string substring, fixing Windows backslash path compatibility.

## [0.9.1] — 2026-05-27

> **Locale and catalog patch release.** Hardens the headed-browser UI
> automation path for localized Flow profiles, fixes I2V start/end-frame
> attachment on non-English Chrome/Flow sessions, and repairs first-run catalog
> edge cases found after v0.9.0.

### Changed

- `NEW_PROJECT_SELECTORS` now covers all 14 supported locales (EN / PT / ES /
  FR / DE / IT / NL / JA / ZH / KO / PL / RU / TR / ID) and leads with
  locale-stable icon selectors (`add_2` Material Symbols ligature on
  `<button>` and on `[role='button']` ARIA-role variants, plus an anchored
  `^\+\s+\S+$` regex for `+ <word>` host elements). English-only
  `[aria-label*='New project']` and `[aria-label*='Project']` ARIA fallbacks
  removed.
  `SUBMIT_BUTTON_SELECTORS` drops its English-only
  `button[aria-label*="Create"]` fallback — the preceding `arrow_forward` icon
  entries already cover this button in every locale. Both selector tuples are
  now fully locale-invariant for non-English Chrome profiles. The `--lang=en-US`
  Chromium launch arg is retained only to stabilise `IMAGE_MODEL_OPTION_SELECTORS`
  (English product names); its removal is tracked as issue #24 Phase 5 (#94).

### Fixed

- Bare `pytest` no longer collects live/e2e tests by default. The project-wide
  pytest `addopts` now excludes `e2e` and `live` unless callers explicitly pass
  a different `-m` expression, and `tests/smoke/test_real_flow.py` is marked
  with both markers so marker-filtered local and CI runs cannot accidentally
  launch a real Flow browser session.
- Browser-manager PID tests no longer call the real `os.kill` while pretending
  to be on POSIX from a Windows runner. The POSIX liveness branches are now
  tested with mocked `os.kill`, avoiding hard interpreter/session exits during
  local test runs.

- Running the pytest suite no longer writes fixture rows into the developer's
  production `gflow.db` catalog. A new autouse `_isolate_settings` fixture in
  `tests/conftest.py` redirects `GFLOW_CLI_HOME` and `GFLOW_CLI_DB_PATH` to
  per-test `tmp_path` dirs and clears the `get_settings()` `lru_cache` before
  and after every test, preventing the cached singleton from ever resolving to
  a `platformdirs` production path. Closes
  [#86](https://github.com/ffroliva/gflow-cli/issues/86).

- `gflow video i2v` no longer silently breaks on non-English Chrome profiles.
  PR #70's structural-first `_attach_frame` cascade matched **zero** real
  slots — its anchor selector assumed the `swap_horiz` icon used class
  `google-symbols` (it uses `material-icons`) and the slots were `<button>`
  (they're `<div type="button">`). Production I2V therefore relied on the
  English-text fallback, which silently misses on any non-EN profile (pt-BR
  shows `Inicial`/`Final`, DE shows `Anfang`/`Ende`, etc.). Replaced
  `FRAME_SLOTS_STRUCT` with the locale-free pattern
  `div[type='button'][aria-haspopup='dialog']` and added a `.first`-of-remaining
  fallback for the End-frame case (after Start is attached, only one slot
  matches and the prior `.nth(slot_index)` went out-of-bounds). Live-verified
  with `tests/e2e/test_transports_e2e.py::test_e2e_i2v_start_end_frame_attach`
  on `ffroliva` + `GFLOW_CLI_LOCALE=de-DE` (Chrome rendered pt-BR; both
  non-EN). Closes [#63](https://github.com/ffroliva/gflow-cli/issues/63).

### Changed

- `gflow data media <id>` now searches across **all** profiles by default,
  matching the cross-profile default of `gflow data list`. Pass
  `--profile NAME` to disambiguate the rare case where the same Flow
  media ID exists under multiple profiles (the command refuses to
  guess and prints the list of candidate profiles, each annotated with
  its `kind`). Closes
  [#87](https://github.com/ffroliva/gflow-cli/issues/87).

### Fixed

- `gflow data list` no longer crashes with `no such table: assets` on a
  missing or freshly-created catalog DB. The query path now routes through
  `DataStore.open`, which applies schema migrations on first connect —
  first-time users and anyone recovering from a wiped DB get an empty
  table and exit 0 instead of a `DataStoreError`. Closes
  [#88](https://github.com/ffroliva/gflow-cli/issues/88).
- `gflow auth list` no longer crashes with `UnicodeEncodeError` on Windows
  consoles whose code page cannot encode the default-profile marker `●`
  (cp1252 in PowerShell / cmd by default). The renderer now picks a glyph
  safe for the active `sys.stdout.encoding` — `●` on UTF-8, ASCII `*` on
  cp1252 / ascii / latin-1 / unknown. Closes [#82](https://github.com/ffroliva/gflow-cli/issues/82).

### Documentation

- `PLAN.md` refreshed to reflect develop state through v0.9.0 — marks Phase 6
  (data layer) shipped via PR #58 + #78 + #81, Phase 7 Issue #24 Phase 2
  shipped via PR #70, Phase B I2V/R2V shipped via PR #48, and resolves the
  duplicate Phase 7 numbering (pluggable storage renumbered to Phase 8).

## [0.9.0] — 2026-05-25

> **Maturity & Visibility release.** Surfaces the SQLite catalog (PR #52/#58)
> via a read-only `gflow data list {projects,images,videos,profiles}` CLI,
> publishes `ROADMAP.md`, and ships the locale-agnostic media-dialog
> selectors that unblock non-English Chrome profiles. Plus the previously-
> unreleased video model picker, i2v/r2v, and the I2I ref-attach + model-
> select fixes. Sponsorship wiring will land in a follow-up patch release
> once GitHub Sponsors / Buy Me a Coffee accounts are fully provisioned.

### Added

- `gflow data list {projects,images,videos,profiles}` — read-only catalog
  query CLI over the local SQLite data layer. Flags: `--limit` (1..1000,
  default 20), `--offset` (≥0, default 0), `--profile NAME`, `--json`.
  Rich table on TTY, JSONL on pipe or `--json`. Default sort: newest first.
  `DataStoreError` family maps to exit code 16. See
  [`docs/DATA_LAYER.md § Querying the data layer`](docs/DATA_LAYER.md#querying-the-data-layer).
- `ROADMAP.md` at repo root — themed milestones from v0.9 through v1.0 (no
  dates).
- `gflow video t2v` model picker: `--model` (`omni-flash` | `veo-lite` |
  `veo-fast` | `veo-quality` | `veo-lite-lp`), `--duration` (`4`/`6`/`8`, plus
  `10` for `omni-flash` only), and `--count` (1–4). Driven via the editor's
  generation-settings panel; live-verified against a Pro/Ultra profile.
- `gflow video i2v <image> "<prompt>"` — image-to-video with a start frame and
  an optional `--end-image` (interpolation). Fires
  `batchAsyncGenerateVideoStartImage` / `…StartAndEndImage`.
- `gflow video r2v "<prompt>" --ref <img> [--ref …]` — reference-to-video
  (Flow "ingredients"). Model-aware reference cap (omni_flash ≤7, veo_3_1_* ≤3)
  enforced in the request DTO; the transport stops gracefully if Flow hides the
  add-media button at the cap. Fires `batchAsyncGenerateVideoReferenceImages`.
- `GFLOW_CLI_LOCALE` env var — overrides Playwright's launch `locale=` parameter
  (default: `en-US`). Controls `Accept-Language` only; Chrome's UI language is
  still forced to en-US via `--lang=en-US`. Prep for issue #24 (locale-agnostic
  selectors); live-verified end-to-end with `GFLOW_CLI_LOCALE=pt-BR` against a
  Pro/Ultra account. See `docs/CONFIGURATION.md § GFLOW_CLI_LOCALE`.
- **Local data layer** — `gflow-cli` now keeps a SQLite catalog of every new
  image, batch, and video operation under `$GFLOW_CLI_DB_PATH` (default:
  `~/.local/share/gflow-cli/data.db`). Records profile, project, asset
  (model / aspect / dimensions / Flow media ID), operation provenance
  (mode / prompt / model / timing / error), input↔output links, and
  downloaded local files. New `gflow data media <id>` command resolves a
  Flow media ID to its origin. `DataRepository` exposes seed-image resolvers
  (`resolve_seed_image_by_path` / `resolve_seed_image` /
  `resolve_latest_image`) — foundation for the upcoming I2V seed-reuse
  path. Pre-Flow store failures exit `16` (`DataStoreError` /
  `DataMigrationError` / `DataIntegrityError`); post-success store
  failures warn and exit `0` (Flow already charged the credits). See
  [`docs/DATA_LAYER.md`](docs/DATA_LAYER.md). (PR #58, stacked on #52.)

### Changed

- `MAX_REFERENCE_IMAGES` (in `api/video.py`) now tracks the `omni_flash`
  ceiling of **7** (was **3**). The tighter per-model cap (`veo_3_1_* ≤ 3`) is
  still enforced in `GenerateVideoRequest.__post_init__` when the model is
  known; the constant is only the absolute upper bound. Anyone pinning to the
  old value of 3 should re-check against the per-model caps.

### Fixed

- `FlowApiClient.__aenter__` now tears down a partially-launched browser if any
  step after the Playwright driver starts raises (e.g. the persistent-context
  launch, the bootstrap navigation, or `transport.setup`). Python does not call
  `__aexit__` when `__aenter__` raises, so an unguarded failure orphaned the
  chrome process, which then held the profile's user-data-dir lock — the next
  run could not acquire it and spiralled into rapid `about:blank` tabs +
  `TargetClosedError`. Context close + driver stop are now shared by
  `__aenter__`'s guard and `__aexit__` via `_close_browser_resources`.
- `gflow image i2i --ref <local-file>` now binds the reference through the
  editor's media dialog instead of the REST `uploadImage` endpoint (which 401s —
  same root as #15/#39). Local-path refs ride a new `GenerateImageRequest.ref_paths`
  field and are attached via the inherited R2V `_attach_references` (the image-mode
  add-media dialog is the same `add_2` surface). Bare-UUID `--ref` still flows
  through `refs` unchanged. Re-introduces #50 (reverted in #57 for the account/
  locale variant tracked in #56); the media-dialog selectors are now
  locale-agnostic (see the next entry).
- The media-dialog upload selectors are now **locale-agnostic** (issue #56/#24).
  `UPLOAD_MEDIA_BUTTON` matched localized text (`has-text('Upload media')`), so on
  a non-English Chrome profile (Flow follows the *Chrome profile* language, which
  the `--lang=en-US` arg cannot override) the click missed and the file chooser
  never opened — a silent ~34s hang. It now anchors on the locale-free `upload`
  icon ligature (`:text-is('upload')`, exact, so it doesn't grab the `Uploads`
  tab), with the original English-text selector kept as a graceful **fallback
  tier** (matches if Google ever changes the icon but keeps the English label);
  'Add to Prompt' (which has no icon) is selected structurally as the only
  iconless button in the open dialog. If neither tier opens a chooser,
  `_upload_via_open_dialog` raises a clear error + writes a screenshot (no silent
  hang) and points the operator at the Chrome-profile-language workaround. Fixes
  I2I/I2V/R2V upload alike.
- `gflow image t2i/i2i --model` now actually selects the requested model. It was
  a no-op under `ui_automation` (the wire field was set but the model picker was
  never clicked, so Flow used its UI default). Adds `_select_image_model`.
- Video selector mismatches: the output-count selector `[id*=-trigger-1]`
  collided with the `-trigger-10` duration tab; the aspect selector matched a
  non-existent `aria-controls*=9_16`; the video-mode tab match was ambiguous.
  All now use exact `[id$=-trigger-X]` suffixes + aria-label text.

### Build

- **Wheel build no longer emits duplicate ZIP entries.** An earlier attempt at
  tagging v0.9.0 was rejected by PyPI with HTTP 400 ("Duplicate filename in
  local headers") because `pyproject.toml` had
  `[tool.hatch.build.targets.wheel.force-include]` and
  `[tool.hatch.build.targets.sdist.force-include]` blocks pointing at
  `src/gflow_cli/data/migrations`, on top of the already-comprehensive
  `packages = ["src/gflow_cli"]` directive — hatchling included the
  migrations directory twice (both `__init__.py` and `0001_initial.sql`). The
  force-include blocks have been removed; hatchling's default package
  inclusion already covers `.sql` files inside the package tree. (PR #74.)

### Notes

- I2V/R2V image inputs bind through the editor's media dialog (frame slot /
  add-media → "Upload media" → file chooser → "Add to Prompt"). `set_input_files`
  on the generic hidden input only adds to the library and Flow then ignores the
  image (plain Text route). The editor is forced to English via the
  `--lang=en-US` Chromium launch arg because the slot/dialog labels are localized
  with no locale-free anchor.

## [0.8.1] — 2026-05-23

### Documentation

- README rewritten as a hybrid router (~150 lines, was 398). New: prominent unofficial-tool + headed-browser callouts above the fold, polished 60-second quick start, in-depth-quick-start link, "For AI agents & LLMs" routing table, ripgrep-style documentation TOC.
- New [AGENTS.md](AGENTS.md) at repo root — universal agent spec consumed by 60k+ repos' tooling (Cursor, Codex, Aider, Gemini CLI, Claude Code, Copilot, opencode, etc.). Closes the gap left by having Claude-Code-only memory.
- New [llms.txt](llms.txt) at repo root — llmstxt.org-format summary for end-users feeding the project into an LLM. Forward-staged for a future docs site.
- New [docs/PROJECT_STATUS.md](docs/PROJECT_STATUS.md) — moved the full milestone table out of README; added lifecycle policy section.
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) absorbed the ASCII system diagram + Auth strategy paragraphs from README; gained a new "Headed-browser dependency — current limitation" subsection that documents the three retired HTTP transports and invites contributor work on a pure-REST path.
- [CLAUDE.md](CLAUDE.md) trimmed from ~80 to ~25 lines; universal coding-agent rules now live in AGENTS.md, with CLAUDE.md carrying Claude-Code-specific session protocol only.
- All `v0.7.0` references that meant "current" updated to `v0.8.1`. Historical mentions preserved in CHANGELOG and `docs/LIVE_VERIFICATION_v0.7.0.md`.
- New per-release evidence file [docs/LIVE_VERIFICATION_v0.8.1.md](docs/LIVE_VERIFICATION_v0.8.1.md).

### Released

- PyPI: https://pypi.org/project/gflow-cli/0.8.1/ — refreshes the stale README rendering left over from v0.8.0.

## [0.8.0] — 2026-05-23

> **Multi-image-prompt release + transport hardening.** Introduces the
> `gflow image batch` subcommand backed by a stay-mounted editor session,
> restores `gflow video t2v` with first-class auto-download, and ships the
> image/video mode-switch symmetry invariant that closes the historical
> "first-attempt listener-miss flake." Also clears all SonarCloud findings
> on the multi-image-prompt PR (cognitive-complexity refactors of
> `_set_count`, `parse_tsv_manifest`, and `_generate_images_batch_locked`).

### Added

- `gflow image batch <manifest>` subcommand for batch image generation from
  JSON or TSV manifests, with `--continue-on-error`. `MAX_BATCH_PROMPTS = 5`.
  All prompts share one Flow project; jitter (3–7 s default) spaces the
  submission clicks for anti-bot cadence, not completion wait. Closes
  [#14](https://github.com/ffroliva/gflow-cli/issues/14) part 2.
- Application-layer structlog events for image-batch submission:
  `image_batch.submission_attempt`, `image_batch.submission_result`,
  `image_batch.row_completed`, `image_batch.inter_submission_latency_ms`.
  Use these to debug Flow throttling regressions without re-instrumenting.
- `BatchPartialError` (in `errors`) — raised by fail-fast batch when
  earlier prompts produced downloadable images before the failing one;
  carries `partial_results` so the orchestrator can salvage them.
- `BatchIntegrityError` (in `errors`) — raised by the orchestrator when
  post-download file count does not match the expected count.
- `BatchSubmissionResult` (in `api.dto`) — new transport-layer per-prompt
  outcome with `project_id`, `prompt_idx`, `prompt_hash` fields. Public
  `list[BatchOutcome]` orchestrator return is unchanged.
- `ui_automation.image_mode_entered` structlog event — emitted when the
  editor is switched into Image mode. Companion to the existing
  `ui_automation_video.video_mode_entered`.
- `ui_automation.orphaned_project_warning` structlog event — emitted when
  `_enter_editor` succeeded but a later setup step
  (`_dismiss_blocking_overlays` / `_switch_to_image_mode`) raises, so the
  user can find their server-side project record.
- `ui_automation.batch_403_body` structlog event — emitted (warning level)
  with a 200-char body prefix when a `batchGenerateImages` response is HTTP
  403 (WAF / reCAPTCHA), immediately before the `WafRejectionError` raise.
- `VideoResult` dataclass — return type of `generate_video`, carries
  `status` and `local_path` ([#29](https://github.com/ffroliva/gflow-cli/issues/29)).
- `UiAutomationTransport._download_video` — downloads a generated mp4 via
  `media.getMediaUrlRedirect` using the authenticated page; falls back to
  `self._out_dir` then `tmp/` when no `out_dir` is supplied
  ([#29](https://github.com/ffroliva/gflow-cli/issues/29)).
- `FlowApiClient.download_video(media_id, out_path)` — public API, mirrors
  `download_image` ([#29](https://github.com/ffroliva/gflow-cli/issues/29)).
- `gflow video t2v PROMPT` restored — generates and downloads a video
  end-to-end on `UiAutomationTransport`; supports `--aspect`
  (`9:16` / `16:9`), `--profile`, and `--out-dir`
  ([#29](https://github.com/ffroliva/gflow-cli/issues/29)).
- `UiAutomationTransport._switch_to_image_mode` static method + module-level
  `IMAGE_TAB_IN_MENU_SELECTORS` cascade — mirror of the video side's
  `_switch_to_video_mode`. Called from both `generate_images` and
  `_generate_images_batch_locked` after `_dismiss_blocking_overlays`,
  before `_configure_generation_settings`.

### Changed

- `gflow image batch` editor session is now persistent across all prompts
  in a batch. The transport's stay-mounted-session pattern is the
  canonical shape; same-project semantics are the only supported mode.
- `_attach_batch_response_listener` now returns `(captured, detach_fn)`;
  callers that used the single-list return need to unpack accordingly.
- `UiAutomationTransport.generate_video` now accepts `download: bool = True`
  and returns `VideoResult` instead of `VideoStatus` — **breaking change
  for direct transport callers** (the `FlowApiClient` boundary is
  unaffected). Pass `download=False` to skip the auto-download step.
- `_set_count` (count-tab selector) is locale-invariant: regex
  `^(1x|x[2-4])$` (Flow renders digit+x identically in every locale) +
  positional `.nth(count - 1)` fallback when read-back text is
  unrecognised. Partial fix for
  [#24](https://github.com/ffroliva/gflow-cli/issues/24); `ONBOARDING_SELECTORS`
  still localized text — see KNOWN_ISSUES.

### Fixed

- `gflow image t2i` and `gflow image batch` now explicitly select Image
  mode in the Flow editor before submitting. Previously, if the account
  was last in Video mode, prompts were silently routed to the video
  endpoint — no `batchGenerateImages` response was observed and the
  listener timed out after 3 minutes. Also resolves the historical
  "first-attempt listener-miss flake" recorded in `phase-b-followups`
  memory item #1. Live-verified on profile `ffroliva` (1 t2i shot + full
  batch e2e); evidence in
  [`docs/LIVE_VERIFICATION_image_batch.md`](docs/LIVE_VERIFICATION_image_batch.md)
  § Post-mode-switch-fix verification.
- `gflow image batch` now actually shares one Flow project across all
  prompts in a batch. Previously the `--same-project=1` flag was a no-op
  at the `ui_automation` transport layer; each prompt landed in its own
  Flow project.
- `gflow image t2i -n N` now makes one transport call using Flow's native
  xN count selector instead of fanning out N parallel single-image
  submissions. Closes [#14](https://github.com/ffroliva/gflow-cli/issues/14) part 1.
- Structlog now uses `cache_logger_on_first_use=False` so per-test
  `LogCapture` fixtures see events fired from production modules
  (previously the cached logger froze the processor chain at import).
- All SonarCloud findings on the multi-image-prompt PR (S5655 / S5890 /
  S1192 / S1172 / S3776 ×3) — see PR #40 commit `a0cb010` for the
  cognitive-complexity refactors and the cast+pragma pattern for
  `dataclasses.replace`.

### Removed

- **BREAKING:** `--same-project` flag on `gflow image batch`. The flag
  collapsed to a single behaviour (always-same-project) — no toggle
  remains. For different-project results, loop `gflow image t2i`
  externally.
- **BREAKING:** `--seed` flag from `gflow image t2i` and `gflow image i2i`.
  The flag was a no-op under the active UI transport since v0.7.0
  (silently discarded inside the client before reaching the transport).
  If reproducibility via user-controlled seed becomes possible again —
  either through Flow UI exposing a seed control or via HTTP transport
  revival — the surface will be re-introduced at that layer. The
  wire-format body builder retains its `seed` / `batch_id` parameters for
  the experimental HTTP transports' internal use.
- **BREAKING (library):** `FlowApiClient.generate_image` no longer accepts
  `seed=` or `batch_id=` kwargs. `FlowApiClient.generate_images_batch` no
  longer accepts `seeds=`. Callers passing these will get a `TypeError`.
  Same justification as the CLI removal.
- **BREAKING (library):** `project_title` parameter removed from
  `run_manifest_image_batch` — the transport now owns project creation
  via `_enter_editor`, making this orchestrator-side knob dead weight.

## [0.7.0] — 2026-05-20

> **Downstream-worker ergonomics release.** Hardens `FlowApiClient` for
> long-lived integrations: standard exception module name, optional
> `project_id`, `health_check()` for liveness probes, `out_dir` for
> debug-screenshot plumbing, and a stable library-owned error when the
> underlying browser session dies. Plus auth-flow fixes from issues #15 and
> #17 and overlay-dismiss for first-run profiles (#26).

### Added

- `gflow_cli.exceptions` module as a standard alias for `gflow_cli.errors` — both module names resolve identically. Closes [#16](https://github.com/ffroliva/gflow-cli/issues/16).
- `FlowApiClient.health_check()` async method — returns `True` if browser context is alive and on a Google domain; safe to call from long-lived workers without try/except. Closes [#16](https://github.com/ffroliva/gflow-cli/issues/16).
- `FlowApiClient(out_dir=...)` constructor argument — when set, the resolved transport stores it as `_out_dir` so internal `_capture_debug_screenshot` calls inside `UiAutomationTransport._generate_images_locked` (entering the editor, dismissing overlays, sending prompts) save artifacts to that directory. Long-lived workers can now diagnose selector failures without restructuring their call sites. Closes [#18](https://github.com/ffroliva/gflow-cli/issues/18).
- `BrowserSessionClosedError` (`gflow_cli.errors`, exit code 15) — raised from `FlowApiClient.generate_image()` / `generate_images_batch()` when the underlying Playwright page/context is closed mid-call (Playwright `TargetClosedError`). Callers can now catch a stable library-owned class and recreate the client via `async with FlowApiClient(...)` instead of importing from `playwright._impl._errors`. Closes [#18](https://github.com/ffroliva/gflow-cli/issues/18).
- `UiAutomationTransport._dismiss_blocking_overlays(page)` — generic overlay-dismiss helper that detects Flow changelog ("What's new") iframes and dismisses them via a close-button selector cascade with an Escape-key fallback. Invoked after editor entry on both image and video flows so first-run profiles no longer fail on the next click. Closes [#26](https://github.com/ffroliva/gflow-cli/issues/26).
- Release tags must now be **signed annotated tags** (`git tag -s vX.Y.Z`). CI's release job rejects unsigned tags so the GitHub release surfaces as Verified ([#30](https://github.com/ffroliva/gflow-cli/issues/30)).
- New documentation: [`docs/DEBUGGING.md`](docs/DEBUGGING.md) — evergreen reference for debugging, testing, and troubleshooting (listener log keys, selector-cascade discipline, lifecycle errors, Windows console encoding, test-suite memory). [`docs/LIVE_VERIFICATION_v0.7.0.md`](docs/LIVE_VERIFICATION_v0.7.0.md) — per-release end-to-end evidence (every CLI aspect ratio live-tested).

### Changed

- `FlowApiClient.generate_image()` and `generate_images_batch()`: `project_id` is now optional (`str | None = None`). When omitted, a new Flow project is created automatically. Existing callers passing an explicit `project_id` are unaffected. Closes [#16](https://github.com/ffroliva/gflow-cli/issues/16).
- `gflow video t2v/i2v/batch` now report "temporarily unavailable" — video generation is being rebuilt on the UI-automation transport (Phase A ships the T2V transport; CLI commands return in Phase B).

### Removed

- The 401-dead HTTP video API path (`FlowApiClient.generate_video`, `get_video_status`) — retired in favour of the new UI-automation transport (`VideoGenerationMixin` in `api/transports/ui_automation_video.py`).

### Fixed

- `gflow auth login` now verifies a real Flow app session before reporting
  success — fixes issue [#15](https://github.com/ffroliva/gflow-cli/issues/15), where a Google-only sign-in was wrongly accepted
  and later failed with HTTP 401.
- **`gflow auth login --browser internal` now fails fast when Google rejects
  Playwright's bundled Chromium**, returning `AuthBrowserRejectedError` exit
  code 14 with guidance to rerun using real Chrome
  (`gflow auth login --browser chrome`) or set
  `GFLOW_CLI_AUTH_BROWSER=chrome` ([#17](https://github.com/ffroliva/gflow-cli/issues/17)).
- **`gflow image t2i --aspect 1:1` aspect-ratio tab regression** — Flow's
  `1:1` tab is now selected via an exact-match (`:text-is`) cascade against
  the labels `1:1`, `Square`, `1×1`, `1x1` instead of the prior
  `:has-text("1:1")` substring match. The substring selector was matching an
  invisible parent on some Flow UI variants, causing a 3 s timeout and a
  silent fallback to Flow's default aspect. All five CLI aspect ratios
  (`16:9`, `9:16`, `1:1`, `4:3`, `3:4`) are now live-verified.
- `UiAutomationTransport._attach_batch_response_listener` now emits a
  `ui_automation.batch_response_seen` log for every `batchGenerateImages`
  URL observed (BEFORE the per-project filter) and a
  `ui_automation.batch_response_dropped_project_id_mismatch` log when the
  filter rejects a response. Eliminates the silent black-hole that hid
  listener-miss bugs during live verification.

## [0.6.0a6] — 2026-05-17

> **Stability & code-quality release.** Fixes a concurrency bug in image
> generation, restores a green CI pipeline (the test job had been hanging
> indefinitely), and clears every open SonarCloud issue so the project's
> Quality Gate passes.

### Fixed

- **Concurrent `generate_images` calls are now serialized**, and every batch
  creates a fresh Flow project — prevents project-reuse races when multiple
  image generations overlap.
- **CI test job no longer hangs.** `RealChromeStrategy` launches Chrome with
  `asyncio.create_subprocess_exec`, but its tests patched `subprocess.Popen`;
  asyncio's POSIX subprocess transport uses `Popen` internally, so the mock
  left the event loop's child watcher unresolved forever — the test job ran
  until cancelled and never wrote a coverage report. Tests now patch
  `asyncio.create_subprocess_exec` directly.
- **structlog log-capture test isolation** — a `browser_manager` test asserted
  on a log event that an earlier test had already cached onto the production
  logger chain (`cache_logger_on_first_use=True`). It now patches in a fresh
  logger proxy and passes regardless of suite order.

### Changed

- **All open SonarCloud issues resolved** and the Quality Gate now passes:
  the S6418 BLOCKER and 10× S5443 CRITICAL test findings, 16 mechanical
  issues, async-hygiene rules (S7503 / S7487 / S7493), and 5
  cognitive-complexity (S3776) extractions. The two remaining Security
  Hotspots — `random`-based retry jitter and protocol-mandated SHA-1 in
  `sapisidhash` — were reviewed and marked Safe.

### Security / Compliance

- **Removed accidentally tracked artefacts** — 7 files were untracked from git:
  `denon82/.gflow-cdp.lock`, `test_assets/debug_editor/buttons.json`,
  `test_assets/debug_settings/settings_panel.json`, and 4 AI-generated JPGs
  in `test_assets/smoke_e2e_*/`. None contained credentials or API tokens, but
  the CDP lock file exposed a profile name and browser PID and the debug JSON
  files contained Flow UI text. Files were removed from HEAD forward (no history
  rewrite — see decision rationale in `PLAN.md` ADR #3).

- **`.gitignore` hardened** — added `*.jpg`, `*.jpeg`, `**/.gflow-cdp.lock`,
  `test_assets/smoke_*/`, `test_assets/debug_*/`, and `gflow-output/` to
  prevent recurrence. Fixture allowlist added (`!test_assets/fixtures/**/*.jpg`).

- **Hygiene gate added to CI** — `scripts/ci/check_repo_hygiene.py` runs on
  every push and PR before lint. Fails if tracked files match the denylist or
  if any `scripts/**/*.py` contains a hardcoded Windows absolute path or writes
  output to `test_assets/`.

- **`.pre-commit-config.yaml` added** — ships ruff (lint + format) and the
  hygiene gate as pre-commit hooks. Install with:
  `pip install pre-commit && pre-commit install`.

- **Debug scripts de-hardcoded** — `scripts/debug_editor.py`,
  `scripts/debug_gen_settings.py`, `scripts/debug_settings.py` previously
  contained `PROFILE = r"C:\Users\ffrol\..."` (Windows username + Google
  profile name) and wrote output to `test_assets/`. Replaced with argparse
  `--profile` flag + `auth.profile_dir(args.profile)` and output redirected to
  `tmp/debug/<name>/`.

- **CI workflow scrubbed** — removed a hardcoded profile name (`denon82`) from
  a comment in `.github/workflows/ci.yml`.

### CI / Tooling

- **GitHub Actions migrated to Node.js 24** ahead of the June 2026 forced
  migration (`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`).
- **SonarCloud Quality Gate badge** added to the README.

## [0.6.0a5] — 2026-05-16

> **CLI transport proven end-to-end.** The `ui_automation` transport now
> generates images correctly from the `gflow image t2i` command — count,
> aspect ratio, and file download all work. Root cause of the persistent 403
> was `headless=True`; reCAPTCHA Enterprise immediately rejects headless
> Chromium.

### Fixed

- **`headless` default changed `True` → `False`** in `config.py` and
  `FlowApiClient.__init__` — the `ui_automation` transport requires a headed
  (visible) Chrome window; reCAPTCHA Enterprise scores headless browsers as
  bots and returns an immediate 403 on `batchGenerateImages`.
- **13 unit test mock regressions** fixed after the v0.6.0a4 transport rewrite:
  - `add_init_script = AsyncMock()` added to `_patch_playwright` and
    `fake_context` fixtures (`test_client.py`, `test_concurrency.py`).
  - `keyboard.insert_text = AsyncMock()` added to `_make_prompt_page`
    (transport now uses `insert_text` instead of `type`).
  - `_FakeHttpxResponse.headers` added (download auto-detects `.jpg`/`.png`
    from `Content-Type`).
  - `_capture_batch_response` / `_await_captured` return `list[dict]` —
    test assertions updated throughout `test_ui_automation.py`.

## [0.6.0a4] — 2026-05-17

> **Unified output resolution + batch orchestration refactor.** This release
> aligns the CLI output structure across all commands and refactors the batch
> runner to be more generic, preparing the codebase for Phase 6.

### Added

- **`resolve_batch_output_dir` helper** in `paths.py` — centralizes the
  date-partitioned output directory logic used by all generation commands.
- **`parse_batch_item_dict` helper** in `image_batch.py` — deduplicates JSON
  prompt validation between `gflow run` and other batch sources.

### Changed

- **`gflow run` output directory** — now defaults to date-partitioned
  `$GFLOW_CLI_OUTPUT_DIR/images/<YYYY-MM-DD>/` instead of the legacy
  `out/<UTC-timestamp>/`, matching the `gflow image` convention.
- **Refactored `run_image_batch`** into a generic `run_sequential_batch`
  orchestrator — now accepts a swappable worker callback, allowing for uniform
  video and image batch handling in the future.

### Fixed

- Removed ~80 lines of duplicate validation logic from `cli_run.py`.
- Corrected test imports and expectations for unified output resolution.

## [0.6.0a3] — 2026-05-17

> **Deterministic timeouts + agent-friendly exit codes.** This release hardens
> the auth login flow for unattended / agentic use: timeouts now raise distinct
> errors with dedicated exit codes instead of silently swallowing failures.

### Added

- **`AuthLoginTimeoutError`** (exit code **12**) — raised by both strategies
  when the user/agent does not complete sign-in within `timeout_seconds`.
  Distinct from `ConfigurationError` (11) and `SecurityError` (13) so agents
  can branch on failure type without parsing stderr.
- **`SecurityError`** exit code **13** — now registered in `EXIT_CODE_MAP`.
- **`timeout_seconds=600` parameter** on both `RealChromeStrategy` and
  `InternalChromiumStrategy` — configurable upper bound for the login window.
- **Broad `GFlowError` catch** in `auth_login` CLI command — previously only
  caught `ConfigurationError`; now looks up any `GFlowError` subclass in
  `EXIT_CODE_MAP` and exits with the correct code plus a `remediation_hint`.

### Fixed

- `InternalChromiumStrategy` had an infinite `while True:` polling loop that
  never timed out; replaced with a bounded loop that raises
  `AuthLoginTimeoutError` on expiry.
- `auth login --browser chrome` when Chrome is missing now exits with code
  **11** (ConfigurationError) instead of 1.

## [0.6.0a2] — 2026-05-16

> **Real Chrome auth strategy — G12 block resolved.** This release restores
> `gflow auth login` reliability by implementing a new **Passive Capture**
> strategy. This method providing a 100% clean browser environment by launching
> your system's real Google Chrome as a standard process, completely bypassing
> Google's bot-detection.

### Added

- **`--browser [auto|chrome|internal]` flag** on `gflow auth login` — selects
  the browser strategy. `chrome` uses real system Chrome (**Passive Capture**).
  `internal` falls back to bundled Chromium. `auto` (default) probes for real
  Chrome and falls back gracefully.
- **`GFLOW_CLI_AUTH_BROWSER` env var** — overrides the browser strategy without
  a CLI flag.
- **`RealChromeStrategy`** (`src/gflow_cli/auth/real_chrome.py`) — zero-automation
  login flow: launches clean Chrome, waits for user to close window, then extracts
  the session.
- **`InternalChromiumStrategy`** — extracted from the previous `auth.py` monolith
  as an explicit fallback strategy.
- **`AuthStrategyFactory`** — routes `auto`/`chrome`/`internal` to the
  appropriate strategy based on system state.

### Fixed

- **G12 bot-detection block** — Google's "browser not secure" rejection (`/v3/signin/rejected`)
  is bypassed by the Passive Capture workflow. By removing all automation signals
  (CDP, WebDriver flags) during login, the browser is indistinguishable from a
  regular user session.
- **Privacy Guard** — `RealChromeStrategy` validates that `profile_dir` is inside
  `GFLOW_CLI_HOME` and raises `SecurityError` if it is not, preventing accidental
  interference with your primary personal Chrome profile.
  use of the user's primary system Chrome profile.
- **`ConfigurationError` on missing Chrome** — clear "Chrome binary not found"
  message with install guidance when `--browser chrome` is requested but Chrome
  is not on the system.
- **Two pyright `TypedDict` errors** in cookie access (`c["name"]` → `c.get("name")`).

### Changed

- `src/gflow_cli/auth.py` promoted to `src/gflow_cli/auth/` package with
  `__init__.py`, `base.py`, `factory.py`, `internal_chromium.py`,
  `real_chrome.py`, `strategies.py`.
- `gflow auth login` now prints the launch strategy announcement before opening
  any browser window.



> **Shell-friendly multi-prompt `t2i` + performance hardening.** This release 
> promotes `gflow image t2i` to a variadic command that can consume multiple 
> prompts from positional arguments, a line-delimited text file, or standard 
> input. Core generation logic has been consolidated into a shared 
> `image_batch` module, ensuring architectural consistency between shell runs 
> and JSON-described batches. This version also ships critical resource 
> cleanup fixes for SQLite connections and OOM protection for stdin streams.

### Added

- **Variadic `gflow image t2i`** — now accepts multiple positional prompts. 
  Example: `gflow image t2i "prompt 1" "prompt 2"`.
- **`--prompts-file <PATH>` and `--stdin`** — read batches of prompts from 
  text files or pipes. All prompts in a batch share a single Flow session 
  and project, significantly reducing reCAPTCHA and project-init overhead.
- **Shared `image_batch` logic** (`src/gflow_cli/image_batch.py`) — unified 
  orchestration, validation, and rendering for all multi-prompt generation 
  surfaces.
- **Memory safety for stdin** — bounded read on standard input prevents 
  memory exhaustion when piping large or infinite streams.
- **`examples/multi_prompt_t2i.py` + `examples/sample_prompts.txt`** — 
  runnable template for the new shell-multi-prompt surface.

### Fixed

- **Resource leaks in SQLite** — ensured all `sqlite3` connections are 
  properly closed via `try...finally` blocks, resolving resource exhaust 
  warnings and potential hangs in long-running processes.
- **Output directory partitioning** — `t2i` batches now correctly land in 
  date-partitioned folders (`images/YYYY-MM-DD/`) by default, aligning 
  with the core design spec.

### Changed

- **CLI validation alignment** — `t2i` and `i2i` subcommands now use 
  authoritative domain constants for model, aspect, and count validation, 
  ensuring UI help text and defaults stay in perfect sync with the engine.

## [0.5.0a1] — 2026-05-12

> **Pluggable image transport + JSON-described batch runs.** The image
> generation surface now ships a new default `ui_automation` transport —
> a Playwright-driven UI mimicry strategy validated end-to-end against
> real Flow on a Google AI Pro/Ultra profile. Three earlier HTTP
> transport strategies (`evaluate_fetch`, `bearer`, `sapisidhash`) move
> into a new `experimental/` subpackage; they remain importable for
> research but are hidden from the CLI by default. New top-level
> `gflow run --config <file>` command drives JSON-described sequential
> batches through one shared session.

### Added

- **`UiAutomationTransport`** (`gflow_cli.api.transports.ui_automation`)
  — new default transport. Drives the Flow editor on a logged-in
  profile through a Playwright-managed persistent context (internal CDP
  port; no externally-exposed debug port). Mirrors the validated
  reference flow in `scripts/smoke_worker_style.py`.
- **`gflow run --config <file>`** — sequential JSON-described batch
  command. Schema covers `profile`, `transport`, `output_dir`, and a
  `prompts` list (1–50 entries) with per-prompt `text`,
  `aspect_ratio`, `model`, `count`, and `output_filename`. Supports
  `--continue-on-error` (default) and `--fail-fast` semantics; final
  exit code is the max per-prompt exit code. ONE `FlowApiClient`
  session wraps the whole loop so the browser/project persist across
  prompts.
- **`examples/` directory** — three runnable scripts (`single_image_t2i.py`,
  `batch_from_config.py`) + a copy-and-edit `sample_config.json` + an
  index `examples/README.md`. All sanitised: no hardcoded profile
  names, generic placeholder prompts, parameterised via `--profile` /
  `$GFLOW_EXAMPLE_PROFILE`.
- **Opt-in real-Flow smoke test** at `tests/smoke/test_real_flow.py`,
  gated by `GFLOW_E2E=1` + `GFLOW_E2E_PROFILE`. Runs the full
  `UiAutomationTransport` flow against real Flow and asserts a
  non-trivial PNG was written.
- **`EXPERIMENTAL_TRANSPORTS` constant** + **`transport_choices()`
  helper** in `gflow_cli.api.transports`. The factory continues to
  accept every registered key; the CLI `--transport` Choice list is
  the gated surface (default = `ui_automation` only;
  `GFLOW_CLI_EXPERIMENTAL_TRANSPORTS=1` expands to all four).
- **Download host allow-list** in `UiAutomationTransport._download`
  (`googleusercontent.com`, `googleapis.com`, `google.com` suffix
  match). `follow_redirects=False`. Prevents session cookies from
  reaching a non-Google host through a malformed or compromised
  `fifeUrl`.

### Changed

- **Default `--transport` flag** flipped from `evaluate_fetch` to
  `ui_automation` across `gflow image t2i`, `gflow image i2i`, and
  `gflow image upload`. The change is transparent to existing scripts
  unless they pinned `--transport evaluate_fetch` explicitly.
- **`evaluate_fetch` / `bearer` / `sapisidhash` strategies moved** to
  `gflow_cli.api.transports.experimental.*`. Public registry keys
  (the strings used by `make_transport()` and the
  `GFLOW_CLI_TRANSPORT` env var) are unchanged. Import paths within
  the package are the only user-visible delta.
- **Debug screenshots** captured by the strategy on `_enter_editor` /
  `_send_prompt` failures are now **viewport-only**
  (`full_page=False`) and emit a `WARNING` log line noting the file
  may contain identifying information from the authenticated session.

### Fixed

- Listener-attach race in `generate_images`. The earlier
  `asyncio.create_task(_capture_batch_response(page))` scheduled the
  listener registration AFTER the next event-loop tick; on a busy
  loop the prompt click could fire before the listener attached,
  causing the capture to time out. Refactored into a synchronous
  `_attach_batch_response_listener(page)` + an `async
  _await_captured(captured, ...)`. No more orphaned task on partial
  failure.

### Removed

- Dead `_extract_image_urls(response)` helper on
  `UiAutomationTransport` and its five tests.
  `generate_images` parses `body.media[]` directly through
  `GeneratedImage.from_response_item`; the parallel helper was
  unreachable.

### Documentation

- `BrowserManager` module docstring updated to make explicit that the
  module is retained for research / non-Flow use, not on the
  v0.5.0a1 image-generation critical path. No behavior change.
- README "Project status" table updated with v0.5.0a1 row.
- `docs/USAGE.md` gains a `gflow run` section.

## [0.4.0a2] — 2026-05-11

> **Documentation polish.** Same release surface as v0.4.0a1; this tag fixes
> a doc-council pass: four broken Python snippets in the README, a shell
> exit-code branching example that silently dropped failures, a stale anchor
> link in `AUTHENTICATION.md`, three USER_GUIDE journeys the target audience
> needs (credit budgeting, pipeline wiring, error recovery), and a sweep of
> "planned v0.3 / v0.4" callouts across 9 files that had been overtaken by
> the Phase 4 release. No code changes. No tests changed.

### Fixed (docs)

- **`README.md` Python quick-start snippet rewritten** — the prior block had
  four real bugs (`from gflow_cli.paths import profile_dir` → import error,
  `upload_image(path, project_id)` args reversed, `generate_video(prompt=,
  start_asset=, aspect=)` wrong kwargs, `poll_video_status` method does not
  exist). Snippet now uses the same invocation pattern as
  `gflow_cli.cli_video._run_i2v` and would actually run.
- **`docs/USAGE.md` exit-code branching example** — `if ! cmd; then case $?`
  always saw `0` because the `if` consumed the exit code; rewritten to
  capture `rc=$?` first. Exit code `2` re-labelled "Bad CLI usage" (auth is
  exit `3`).
- **`docs/AUTHENTICATION.md` anchor link** to the Phase 4 PLAN heading
  fixed (was `#phase-4--hardening--post-v030a1`, did not exist).
- **`CHANGELOG.md` footer** — added `[0.4.0a1]:` and `[0.4.0a2]:` compare
  links; reset `[Unreleased]` to compare from v0.4.0a2.
- **`docs/USER_GUIDE.md` Journey 2** endpoint name `flowMedia:batchGenerateVeoVideo`
  → real route `/v1/video:batchAsyncGenerateVideoText`.
- **`docs/USER_GUIDE.md` Journey 5.2** invalid placeholder UUID
  (`media-uuid-abc-...`) → canonical hex shape.
- **`docs/USER_GUIDE.md` Journey 7.1** `echo $?` placement — previously
  captured the exit code of an intermediate command, not the failing batch.
- **`docs/USER_GUIDE.md` Journey 7.3** softened the "Flow doesn't re-bill"
  claim — billing is a private-API contract we cannot assert.
- **`KNOWN_ISSUES.md` same-profile examples** swapped `gflow image batch`
  (does not exist) → `gflow video batch`.

### Added (docs)

- **`docs/USER_GUIDE.md` Journey on credit budgeting** — rule-of-thumb credit
  cost per `video t2v` / `video i2v` / `image t2i` / `image i2i` call, links
  to Flow's credit-balance UI, batch-cost math example.
- **`docs/USER_GUIDE.md` Journey on wiring outputs into a pipeline** —
  deterministic output-dir layout, `find` (POSIX) + `Get-ChildItem`
  (PowerShell) recipes, `ffmpeg` consumer example.
- **`docs/USER_GUIDE.md` Journey on `ContentPolicyError` / `RateLimitError`
  recovery** — what each error means, how long to wait, prompt-rewrite
  pattern, when retry is futile.
- **`README.md` doc-nav** now links `docs/USER_GUIDE.md` (was missing).
- **`README.md` Stack table** lists `tenacity` and `structlog` (were
  shipped in v0.4.0a1 but not in the stack overview).

### Changed (docs)

- **`CHANGELOG.md` [0.4.0a1] section reordered** — `Added — Phase 4
  hardening` now appears before `Breaking`. The hardening release was
  user-visible value; the env-var rename was a one-line update for most
  users.
- **Per-class exit codes 3–7** promoted from a bullet to its own "Migration
  notes" subsection in the [0.4.0a1] block.
- **Version-time-warp sweep across 9 files** — every `(planned v0.3)`,
  `(planned v0.4)`, `v0.3+ will add`, `v0.4 will add`, and `current scaffold
  ignores this` line either describes shipped behaviour or points at v0.5+.
  Files touched: `README.md`, `CHANGELOG.md`, `CLAUDE.md`, `PLAN.md`,
  `KNOWN_ISSUES.md`, `CONFIGURATION.md`, `AUTHENTICATION.md`,
  `ARCHITECTURE.md`, `DISCLAIMER.md`, `SECURITY.md`, `CONTRIBUTING.md`,
  `.env.template`.
- **`docs/ARCHITECTURE.md` Concurrency section** describes the shipped
  `asyncio.Queue` Page pool, not the target-DDD `Semaphore` model.
- **`docs/ARCHITECTURE.md` Observability section** describes the shipped
  `error_raised` / `error_unhandled` event names, not the target-DDD
  dot-path names.
- **`docs/ARCHITECTURE.md` DDD error class names** annotated as target —
  shipped Phase 4 names (`AuthExpiredError`, `RateLimitError`,
  `ContentPolicyError`, `NetworkError`, `WireFormatError`) listed alongside.
- **`docs/CONFIGURATION.md` `GFLOW_CLI_CONCURRENCY`** describes shipped
  behaviour (per-worker Page pool, `asyncio.gather` fan-out).

## [0.4.0a1] — 2026-05-11

> **Phase 4 hardening release.** Concurrency, retry/backoff, typed errors,
> and structured logs ship — your existing scripts keep running (the
> `FLOW_CLI_*` env-var shim is in place until v0.5.0). The user-visible
> contract that changed: shell scripts can now branch on stable per-class
> exit codes (3–7) for auth / rate-limit / content-policy / network /
> wire-format failures.

### Added — Phase 4 hardening

- **Per-worker Playwright Page pool.** `FlowApiClient.__aenter__` opens
  `Settings.concurrency` Pages inside a single persistent BrowserContext.
  Operations check out a Page via `asyncio.Queue` (FIFO, bounded by
  `maxsize=N`). `GFLOW_CLI_CONCURRENCY=N` (1–16) now actually parallelizes.
- **`gflow video batch` fans out via `asyncio.gather`** over manifest
  entries — was sequential pre-v0.4.0a1.
- **`tenacity`-based retry layer** (3 attempts, exponential jittered
  backoff 1s±25% → 2s±25% → 4s±25%) on 5xx / 429 / `playwright.async_api.Error`
  / `TimeoutError`. `Retry-After` honoured, **capped at 60 s**. `reraise=True`
  so the original exception's `__cause__` chain is preserved. reCAPTCHA
  token re-minted **inside the retry loop, every attempt**, on the worker's
  own Page.
- **RFC 9457 Problem Details exception hierarchy:**
  `GFlowError → FlowApiError → {AuthExpiredError, RateLimitError,
  ContentPolicyError, NetworkError, WireFormatError}`. `except FlowApiError`
  catches the typed subclasses (back-compat). Each carries
  `problem_type` URI, `title`, `status`, `detail`, `instance`
  (`gflow:error:<correlation_id>`), `remediation_hint`, and `route`.
  `to_problem_details()` serializes to the RFC 9457 JSON shape.
- **Per-class exit codes**: 3 (auth) / 4 (rate-limit) / 5 (content-policy) /
  6 (network) / 7 (wire-format). Exit 1 = unhandled. Exit 130 = SIGINT.
- **`WireFormatError` discovery payload** — `route_name`, `http_status`,
  `content_type`, `top_level_keys`, `body_prefix_redacted` so log mining
  can propose new error subclasses for unexpected response shapes.
- **`structlog` bootstrap** with TTY auto-detection (text on TTY, JSON when
  piped). `show_locals=False` mandatory on the exception renderer so frame
  locals (which may contain auth tokens) NEVER reach the log stream.
  `correlation_id` + `cli_version` bound via `contextvars` at the process
  boundary.
- **`error_raised` and `error_unhandled` events.** `error_raised` for caught
  `GFlowError`s — carries Problem Details. `error_unhandled` for anything
  else — privacy-safe: hashes message + stack with SHA-256, never logs raw
  payload.
- **12 `pytest-bdd` scenarios** across `auth.feature`, `video.feature`,
  `image.feature` — all use a mocked `FlowApiClient`. A
  `_forbid_live_playwright` autouse tripwire fails any scenario that
  accidentally tries to start a real browser.

### Migration notes — stable exit codes

Shell scripts that previously branched on exit code `1` for any failure
can now distinguish the failure class. The mapping is locked by an
ordering-invariant test in `tests/test_errors.py`:

| Exit | Error class           | Meaning                              | Retry?         |
|------|-----------------------|--------------------------------------|----------------|
| 0    | —                     | Success                              | —              |
| 1    | (unhandled)           | Bug. Filed via `error_unhandled`     | No             |
| 2    | (Click)               | Bad CLI usage / missing arg          | Fix the call   |
| 3    | `AuthExpiredError`    | Session cookies invalidated          | After re-login |
| 4    | `RateLimitError`      | Flow returned 429                    | Yes, with wait |
| 5    | `ContentPolicyError`  | Prompt blocked upstream              | After rewrite  |
| 6    | `NetworkError`        | DNS / TLS / 5xx after retry          | Yes            |
| 7    | `WireFormatError`     | Response shape changed (Flow update) | File a bug     |
| 130  | (SIGINT)              | User Ctrl-C                          | —              |

See [`docs/USAGE.md § Exit codes`](docs/USAGE.md#exit-codes) for a
shell-script template that branches on these codes.

### Breaking — package + env-var rename

- **Python package renamed: `flow_cli` → `gflow_cli`.** All imports must
  change: `from gflow_cli...` (was `from flow_cli...`). The PyPI distribution
  name (`gflow-cli`), the CLI binary (`gflow`), and the user data directory
  (`gflow-cli/` under `platformdirs`) are unchanged.
- **Env var prefix renamed: `FLOW_CLI_*` → `GFLOW_CLI_*`.** Affected vars:
  `GFLOW_CLI_HOME`, `GFLOW_CLI_OUTPUT_DIR`, `GFLOW_CLI_PROFILE`,
  `GFLOW_CLI_HEADLESS`, `GFLOW_CLI_LOG_LEVEL`, `GFLOW_CLI_LOG_FORMAT`,
  `GFLOW_CLI_PROVIDER`, `GFLOW_CLI_TIMEOUT_SECONDS`, `GFLOW_CLI_CONCURRENCY`,
  `GFLOW_CLI_GEMINI_API_KEY`.
- **Backwards-compat shim.** Legacy `FLOW_CLI_*` env vars continue to work
  in v0.4.x; on first encounter the process emits a single
  `DeprecationWarning` to stderr summarising the promoted keys. The shim
  will be removed in v0.5.0 — update your `.env` files and shell exports.

### Changed

- `FlowApiError` re-parented under `GFlowError`. Legacy positional
  constructor `FlowApiError(status, body, *, route)` preserved (auto-detected
  via `isinstance(args[0], int) and not isinstance(args[0], bool)`).
- `_resolve_profile` and `_make_provider_dir` deduped — relocated from
  `cli_image.py` + `cli_video.py` to `gflow_cli._cli_helpers`. AST-based
  drift guard in `tests/cli/test_helpers.py` prevents regression.
- All `logging.*` callsites in `src/` migrated to `structlog`. The
  remaining `print()` in `auth.py` swapped to Rich `console.print()`.

### Internal

- New module: `gflow_cli.errors` (RFC 9457 hierarchy + `EXIT_CODE_MAP`).
- New module: `gflow_cli.observability` (structlog bootstrap + event
  emitters; `show_locals=False` via
  `ExceptionRenderer(ExceptionDictTransformer(show_locals=False))`).
- New module: `gflow_cli.api._retry` (tenacity `AsyncRetrying` +
  `Retry-After` parser, capped at 60 s).
- New module: `gflow_cli._cli_helpers` (shared CLI-boundary handlers +
  profile/provider helpers).

## [0.3.0a1] — 2026-05-10

### Added
- **`gflow image upload PATH`** — upload a single local image (PNG/JPEG) into a
  fresh Flow project and print the asset UUID + dimensions Flow inferred. The
  UUID is reusable as a starting frame for `gflow image i2i --ref` and
  `gflow video i2v`.
- **`gflow image t2i PROMPT`** — text-to-image generation (1–4 images per call)
  via Google Flow's Imagen / Nano Banana models.
  Flags: `--model {nano2|nano-pro|image4}`, `--aspect {9:16|16:9|1:1|4:3|3:4}`,
  `-n/--count` (1–4), `--seed` (single-image only), `--out DIR`, `--profile`.
  Files land date-partitioned under `$GFLOW_CLI_OUTPUT_DIR/images/<YYYY-MM-DD>/`
  by default; `--out DIR` writes flat as `<DIR>/<media_name>_<n>.png`.
- **`gflow image i2i PROMPT --ref PATH_OR_UUID`** — image-to-image generation
  with one or more reference images. Each `--ref` is classified at the CLI
  boundary: case-insensitive 8-4-4-4-12 hex UUIDs are passed through verbatim
  (no upload), anything else is canonicalized (symlinks resolved at validation
  time) and uploaded before use. `--ref` is repeatable; UUIDs and paths can mix
  freely on the same call. Same flag set as `t2i` otherwise.
- **Multi-image fan-out** — `t2i` / `i2i` with `-n {2..4}` mint a single shared
  `batch_id` and issue N parallel POSTs (one per shot, each with its own random
  seed). Same-batch images share the prompt + refs; per-shot variation comes
  from independent seeds.
- **Three image models** wired behind CLI aliases:
  `nano2` → `NARWHAL` (Nano Banana 2; default, fast/balanced),
  `nano-pro` → `GEM_PIX_2` (Nano Banana Pro; higher quality),
  `image4` → `IMAGEN_3_5` (Imagen 4; photoreal-leaning).
- **Five aspect ratios** for image generation: `9:16`, `16:9`, `1:1`, `4:3`,
  `3:4` (default `9:16`, matching the Flow web UI).
- `download_image()` on `FlowApiClient` — direct download of a generated
  image's signed `fifeUrl` to disk. Streams to a temp file and atomically
  renames on success; enforces an SSRF host allowlist (only Google-controlled
  CDNs accepted).
- `scripts/smoke_image.py` — live single-image E2E smoke script (image
  counterpart of `scripts/smoke_e2e.py` for video). Run after
  `gflow auth login` to exercise the full happy path: project create →
  `batchGenerateImages` → fifeUrl download.

### Changed
- `FlowApiClient.upload_image` now validates **PNG/JPEG/WebP/GIF magic
  bytes** and rejects files larger than **20 MB** before issuing the upload
  request. Existing callers (`gflow video i2v`, `gflow video batch`) inherit
  the stricter validation; previously-undocumented use of `upload_image` for
  non-image payloads no longer works (was never officially supported).
- Project renamed `flow-cli` → `gflow-cli` across all docs and source. The
  PyPI package and GitHub repo were already at the new name in v0.2.0a1;
  this commit completes the in-source rename. Local clones may want to
  rename their working directory to match `gh clone https://github.com/ffroliva/gflow-cli`
  behavior.

### Security
- DEBUG-level body logs now redact reCAPTCHA Enterprise tokens and other
  bearer-style fields before emission, eliminating a token-leak vector when
  users share verbose logs while filing bug reports.
- `download_image()` enforces an **SSRF host allowlist** on the signed
  `fifeUrl` returned by Flow — only Google-controlled image CDNs
  (`*.googleusercontent.com`, etc.) are followed; any other host raises
  before the GET is issued. Defends against a Flow-side bug or compromise
  redirecting downloads to an attacker-controlled origin.
- `project_id` allowlist regex `^[A-Za-z0-9-]{1,128}$` on
  `batch_generate_images_url` — closes percent-encoded slash (`%2F`),
  Unicode-lookalike (U+FF0F / U+2215 / U+29F8), and CRLF/NUL injection
  bypasses that the previous denylist guard let through.

### CI
- Test matrix now includes Python 3.13 alongside 3.11 and 3.12.

## [0.2.0a1] — 2026-05-09

### Added
- **`gflow video t2v`** — generate a video from a text prompt via Veo 3.1.
  Flags: `--aspect 9:16|16:9|1:1`, `--seed`, `--output`, `--profile`, `--poll-interval`.
- **`gflow video i2v`** — generate a video from a start image + text prompt (Veo 3.1 I2V).
- **`gflow video batch`** — run a TSV manifest of video generations against one shared project.
- `gflow_cli.api` package — low-level REST client (`FlowApiClient`) + value objects
  (`GenerateVideoRequest`, `VideoOperation`, `VideoStatus`) for video generation.
- `gflow_cli.api.recaptcha` — reCAPTCHA Enterprise token minting via Playwright `page.evaluate`.
  `TokenMinter` caches the discovered site key per session; `mint(action)` is called immediately
  before each generation request.
- `gflow_cli.manifest` — TSV manifest parser for `gflow video batch`. Supports optional
  `start_image`, `end_image`, `aspect`, `output_path` columns; skips `# `-prefixed comments.
- `GFLOW_CLI_HEADLESS` env var (`bool`, default `true`). Set to `false` if reCAPTCHA refuses
  to mint tokens in headless mode (Google bot detection fallback).
- `scripts/smoke_e2e.py` — one-shot live T2V smoke test; run after `gflow auth login` to
  verify the full happy path (create project → generate_video → poll → download).
- **`CLAUDE.md`** at repo root — project memory hub for AI coding agents
  (Claude Code reads natively; Cursor/Codex/Gemini/Aider can read as reference).
- **`.claude/`** directory — repo-local Claude Code surface for maintainers.
  - `.claude/README.md` — what goes here, how to extend.
  - `.claude/commands/release.md` — `/release` slash command that automates
    version bump + CHANGELOG migration + tag + push, with quality gates.
- `gflow_cli.profile_store` — profile inventory + default-profile persistence
  in `$GFLOW_CLI_HOME/config.toml`. Five-step resolution chain (CLI flag > env >
  config > auto-select > raise) with named exceptions
  (`NoProfilesError`, `NoDefaultProfileError`).
- New auth subcommands: bare `gflow auth`, `gflow auth list`, `gflow auth use <name>`,
  `gflow auth logout [--profile NAME] [-y]`. First login auto-sets default profile.
- `KNOWN_ISSUES.md` at repo root — open/mitigated/resolved issues with workarounds.
- `docs/` tree (INDEX, AUTHENTICATION, CONFIGURATION, ARCHITECTURE, USAGE, SECURITY).
- `.env.template` documenting every supported env var.
- GitHub Actions CI: ruff, pyright, pytest on Python 3.11 and 3.12.
- GitHub Actions release workflow: tag-triggered PyPI publish via Trusted Publishing.
- MIT license, comprehensive README, [`DISCLAIMER.md`](DISCLAIMER.md), [`CONTRIBUTING.md`](CONTRIBUTING.md).
- [`skills/gflow-cli/SKILL.md`](skills/gflow-cli/SKILL.md) — installable Claude Code Skill.

### Removed
- `gflow_cli.providers.FlowProvider` and `gflow_cli.models` — superseded by `gflow_cli.api`.
- Legacy CLI stubs: `gflow upload`, `gflow generate`, `gflow status`, `gflow download`,
  `gflow i2v`. Replaced by the wired `gflow video` subgroup.

## [0.1.0] — _unreleased_

First skeleton. Not functional end-to-end yet.

[Unreleased]: https://github.com/ffroliva/gflow-cli/compare/v0.20.0...HEAD
[0.20.0]: https://github.com/ffroliva/gflow-cli/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/ffroliva/gflow-cli/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/ffroliva/gflow-cli/compare/v0.17.0...v0.18.0
[0.17.0]: https://github.com/ffroliva/gflow-cli/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/ffroliva/gflow-cli/compare/v0.15.1...v0.16.0
[0.15.1]: https://github.com/ffroliva/gflow-cli/compare/v0.15.0...v0.15.1
[0.15.0]: https://github.com/ffroliva/gflow-cli/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/ffroliva/gflow-cli/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/ffroliva/gflow-cli/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/ffroliva/gflow-cli/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/ffroliva/gflow-cli/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/ffroliva/gflow-cli/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/ffroliva/gflow-cli/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/ffroliva/gflow-cli/compare/v0.8.1...v0.9.0
[0.8.1]: https://github.com/ffroliva/gflow-cli/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/ffroliva/gflow-cli/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/ffroliva/gflow-cli/compare/v0.6.0a6...v0.7.0
[0.6.0a6]: https://github.com/ffroliva/gflow-cli/compare/v0.6.0a5...v0.6.0a6
[0.6.0a5]: https://github.com/ffroliva/gflow-cli/compare/v0.6.0a4...v0.6.0a5
[0.6.0a1]: https://github.com/ffroliva/gflow-cli/compare/v0.5.0a1...v0.6.0a1
[0.5.0a1]: https://github.com/ffroliva/gflow-cli/compare/v0.4.0a2...v0.5.0a1
[0.3.0a1]: https://github.com/ffroliva/gflow-cli/releases/tag/v0.3.0a1
[0.2.0a1]: https://github.com/ffroliva/gflow-cli/releases/tag/v0.2.0a1
[0.1.0]: https://github.com/ffroliva/gflow-cli/releases/tag/v0.1.0
