Repository navigation
# fix(639): the migrated model picker — veo-lite-lp, [Lower Priority] ambiguity, and the overlay a switch leaves behind - #669
Conversation
|
Thanks for the contribution. This PR has been marked as an external contribution and routed for maintainer review. Before merge, please make sure:
GitHub Copilot code review may be requested as an advisory first pass, but maintainer approval is still required. |
…ority] binding the wrong tier `--model veo-lite-lp` could not be selected at all on an account Google has already moved to flow.google.com: VIDEO_MODEL_MENU_LABELS carried only the four tiers whose product names have been read back from a live menu, so `_select_model` refused with ConfigurationError (exit 11, "not available on the migrated Flow host") before it ever opened the picker. The omission was deliberate, not an oversight — the tier's full menu label is unknown. Flow has never rendered the entry on any account gflow has driven: the 2026-08-14 two-account capability matrix, ffroliva#650's duration capture and v0.61.0's refusal A/B all recorded a picker MISS. The migrated map is keyed by label, so there was nothing to key it with. Match it the way the labs driver has since ffroliva#539 — by the `[Lower Priority]` tag alone. No label is invented, and nothing is asserted about an entry no capture has produced. That exposed a second defect the port had inherited from nowhere: entries were matched by case-insensitive *substring* with `.first`. `Veo 3.1 - Lite` is a substring of `Veo 3.1 - Lite [Lower Priority]`, so on an account whose menu carries the sibling, `--model veo-lite` bound whichever Angular listed first and Flow billed it. The same substring ran against the picker button's read-back, where a pane already showing the lower-priority tier satisfied `startswith("Veo 3.1 - Lite")`, returned "already selected", and silently kept it. labs.google has carried `:not(:has-text('[Lower Priority]'))` against exactly this since ffroliva#539. - ModelMenuMatcher(contains, excludes) replaces the bare label lookup and is used for both the button read-back and the menu scan, so the two cannot drift. Ordinary tiers exclude the tag; the lower-priority tier is the tag. - The menu is matched in Python over `all_text_contents()` rather than through a `has_text` filter: the text is read back for the refusal diagnostic anyway, an exclusion is not expressible as `has_text`, and more than one hit must refuse (naming every candidate) instead of resolving `.first`. - Routing is unchanged on purpose. `migrated_can_serve` still gates on VIDEO_MODEL_MENU_LABELS, so veo-lite-lp does not pull an unmoved account onto the new host — that would trade a working driver for an unverified one. A moved account is routed by its URL and never asks the question. All five new tests fail on the pre-fix source and pass after. Not live-verified: no account available here renders the lower-priority entry, which is the same blocker the labs side has had since August. The tag is inherited from a shipped, live-verified labs selector rather than guessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MK6VHisb4JifQPAmvm724e Signed-off-by: Kittinan <tunxedge@gmail.com>
…ead-back short-circuit Follow-up to 85b87b1, which shipped the matcher unverified because no account here was known to render the lower-priority entry. One does — this one — so the change is now measured rather than argued. The entry has been captured for the first time: the migrated menu renders `Veo 3.1 - Lite [Lower Priority]`, and the account's picker was already *defaulted* to it. That is the likely reason every earlier capture recorded a picker MISS (2026-08-14's two-account matrix, ffroliva#650, v0.61.0): those accounts were not being throttled, so Flow did not serve them the entry. Matching stays on the `[Lower Priority]` tag rather than moving to the now-known label — one account's rendering, the labs driver keys off the same tag, and a tag Flow appends to whichever tier it throttles survives that tier changing. Verified at $0 by driving the shipped `_select_model` for all five tiers and reading the picker back after each switch (selection happens before submit, which is what made v0.61.0's and v0.62.1's matrices free): omni_flash -> Omni 1.1 Flash veo_3_1_lite -> Veo 3.1 - Lite veo_3_1_fast -> Veo 3.1 - Fast veo_3_1_quality -> Veo 3.1 - Quality veo_3_1_lite_lower_priority -> Veo 3.1 - Lite [Lower Priority] Both directions of the exclusion are exercised: plain Lite lands on the plain entry with the throttled sibling in the same menu, and the throttled tier is reached through the menu. The read-back defect is confirmed against the real button text — `"Veo 3.1 - Lite [Lower Priority] arrow_drop_down"` satisfies the pre-fix `startswith("veo 3.1 - lite")`, so `--model veo-lite` on this account generated on the throttled tier without ever opening the menu. Also closes the observability gap the verification ran into. A run that short-circuits at the button read-back emitted no model event at all, so the timeline could not distinguish "bound the tier you asked for" from "never touched the picker"; answering it needed a separate probe. It now logs `migrated.model_already_selected` with the observed button text. - scripts/dev/capture_migrated_model_menu.py: the $0 instrument (`--drive` for the matrix). Loads a fresh document per tier: re-opening the settings pane on one page load leaves a detached overlay that still contains radiogroups, and `_open_pane`'s `.last` then resolves to it. Production opens the pane once per run, so no shipped path is known to hit that — recorded in the spike, not filed. - docs/superpowers/spikes/2026-09-05-migrated-model-menu-lower-priority.md: the capture, the matrix, and what is NOT established (one account, one locale; whether an unthrottled account is ever served the entry stays untested). One 8 s generation was spent reproducing the report before the $0 probe was reached for; the spike records that as a misstep rather than a cost of the work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MK6VHisb4JifQPAmvm724e Signed-off-by: Kittinan <tunxedge@gmail.com>
…r breaks the next run Reported: with a previous run on veo-lite-lp, run 1 on omni-flash dies, run 2 on omni-flash works, run 3 on veo-lite-lp dies. A *switch* fails; a *repeat* succeeds. Reproduced at $0 and measured rather than guessed. Selecting from the model menu leaves Angular with TWO stacked overlays — the settings pane and the menu opened over it — and each Escape dismisses exactly one. `_close_pane` pressed it once, so the settings pane stayed open, covering the composer. `send_prompt`'s click then failed Playwright's actionability check and surfaced ~5 s later as a bare `TimeoutError` naming `[contenteditable='true']`, with nothing pointing at the pane. The click's own message blames stability, and that is a red herring. Sampling every 100 ms for 12 s after a switch: the composer's bounding box is IDENTICAL throughout — it was never unstable — while `.cdk-overlay-pane:visible` sits at 1 for the whole window. One more Escape takes it to 0 and the prompt types. A repeat run binds the model at the button read-back, never opens the menu and never stacks the second overlay, which is exactly why re-running the same model worked and why the offline suite and the `--drive` matrix both missed it: neither touched the composer after a real switch. `_close_pane` now escapes until no overlay is visible (bounded at 4), and raises `UiSelectorDriftError` pre-submit at $0 if one will not close, instead of leaving the composer click to report it as an unattributable timeout. The overlay count is re-queried each pass rather than held in one locator. The signal was already there: `migrated.pane_still_open` DOES fire on the pre-fix source — confirmed by replaying it against the live host, not assumed. It was a warning the run ignored. Only the consequence was missing; it is now the failure. Tests model what the live host does: a menu pick leaves an overlay that eats the next Escape, `:visible` excludes the detached pane, and a covered composer is not clickable. The two switch-path tests fail on the pre-fix source; the repeat-path test passes on both, which is the reported asymmetry. Verified live after the fix, all $0: the reported sequence — omni-flash (switch), omni-flash (repeat), veo-lite-lp (switch) — applies settings and types the prompt in all three, with 0 visible overlays left. One authorised end-to-end run on the switch path (veo-lite-lp -> omni-flash, `migrated.model_selected` in the timeline) submitted, generated and downloaded a 2.6 MB clip, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MK6VHisb4JifQPAmvm724e Signed-off-by: Kittinan <tunxedge@gmail.com>
90bd60c to
1901d85
Compare
ffroliva
left a comment
There was a problem hiding this comment.
PR #669 — Council Review Verdict
Reviewed at 1901d85 (pinned). No SHA drift during review. Protocol: skills/pr-council-review/SKILL.md v2.1.
Consensus: 🔴 RED — two must-fix items, both small
Let me say up front that the evidence quality here is the best I have reviewed on this repo: a $0 instrument, a read-back matrix that separates "clicked what was asked" from "returned success", a measured refutation of the stability red herring, and an explicit "could NOT be verified" section. Defects 2 and 3 are real, well-diagnosed and worth more than the reported one. The blockers below are narrow and none of them touch the core fix.
| Dim | Verdict | Headline |
|---|---|---|
| D0 CI-mechanical | 🟢 | All six gates re-run at 1901d85 + ruff/pyright/34 tests. 14/14 GitHub checks green. |
| D1 Correctness | 🟡 | _close_pane raising inside finally masks the precise error (reproduced). |
| D2 Code quality | 🟡 | _close_pane widens a selector that _open_pane deliberately narrows. |
| D3 Security | 🟡 | Profile name + project UUID in the spike; the PII gate does not cover that path. |
| D4 Tests | 🟢 | 9 new, 8 red pre-fix. Affected surface exercised. |
| D5 Memory hygiene | 🔴 | Two memory files still record the claim this PR disproves. |
| D6 UI / live-verify | 🟡 | Live evidence strong; [Lower Priority] remains an English prose anchor. |
| D9 Docs drift | 🟢 | CHANGELOG + KNOWN_ISSUES + mirror consistent; links and mirror gates pass. |
| D13 Scripts | 🟢 | New probe follows the _spike_common sibling convention. |
| D14 YAGNI | 🟢 | One nit: PANE_CLOSE_ESCAPES = 4 where 2 is the measured need. |
| D15 Surface parity | 🟢 | Your "MCP twin not applicable" claim verified — diff on mcp/, cli_video.py, api/video.py is empty. |
Must-fix (2)
1. 🔴 D5 — the memory store still says this tier is a picker MISS
docs/superpowers/memory/video-model-capability-matrix.md records, as current state:
:103—| veo_3_1_lite_lower_priority | **PICKER MISS** (still) | — | — |:144— "What is NOT settled: … whetherlower_priority's miss is selector or absence."
and flow-capabilities-are-cohort-dependent.md carries "(lower_priority missed its picker, so it stays unmeasured)".
This PR settles that open question: the entry exists, renders as Veo 3.1 - Lite [Lower Priority], and the miss was cohort/absence — not selector drift — because Flow serves it to accounts it is actively throttling. That is exactly the fact the memory store exists to carry, and right now it points the next agent the wrong way. The spike is the capture; memory is the routing layer, and only the spike was updated.
Concretely: update the :103 row, retire the "not settled" clause at :144, and amend the cohort file's "stays unmeasured" parenthetical. check_council_memory.py stays green either way (47 files, all cited) — this is a content edit, not a wiring one.
2. 🟡→blocking D1 — _close_pane raising in finally replaces the real error
apply_video_settings calls _close_pane in a finally: (migrated_composer.py:224-225). It previously could only warn; it can now raise. When the try body already failed, the new raise discards the actionable diagnostic. Reproduced against your own fake DOM:
RAISED : UiSelectorDriftError — "2 overlay(s) still visible after 4 Escape presses" (exit 23)
CONTEXT: ConfigurationError — "model 'veo_3_1_quality' is not offered on this account's
migrated Flow host; offered: Omni 1.1 Flash, Veo 3.1 - Fast" (exit 11)
The user gets exit 23 ("selectors drifted, file a bug") instead of exit 11 ("pass a different --model"), and the offered-models list is gone. __context__ does not survive into the Problem Details output, so it is invisible. Suggested shape: only raise from _close_pane when no exception is in flight — or let apply_video_settings log-and-swallow the close failure when the body already raised.
Nice-to-have (3)
-
_close_paneis broader than the bug requires (:279-297). It aborts on any visible.cdk-overlay-pane, while_open_pane(:236) deliberately narrows the same selector with.filter(has=RADIOGROUP)— the file already knows the class is not unique to the settings pane..cdk-overlay-paneis the generic Angular CDK class (snackbars, tooltips, dialogs), and Escape does not dismiss a snackbar. A transient toast would now hard-abort a run that used to succeed. Cost is $0 and pre-submit, so this is a false-abort risk rather than a credit risk — but scoping the count to panes containingRADIOGROUP/MENU_ITEMwould keep the fix and drop the blast radius. -
PII in the spike —
docs/superpowers/spikes/2026-09-05-…md:3-4carries profilekittinansr2and project66324c59-…. You flagged this yourself. Worth noting the gate gap:check_website_docs_pii.pyscans onlywebsite/docs/(25 files), sodocs/superpowers/is unscanned and CI is green regardless. Maintainer's call, but the redaction is two lines. -
PANE_CLOSE_ESCAPES = 4where the measurement says 2. The comment calls the headroom deliberate, which is fair — flagging only because D14 asks.
Correcting one thing in the PR body
Signed-off-by:is missing. … Needsgit rebase --signoff origin/developbefore merge.
This is not the case — all three commits already carry the trailer:
1901d85 Signed-off-by: Kittinan <tunxedge@gmail.com>
7baa566 Signed-off-by: Kittinan <tunxedge@gmail.com>
ea1f8c0 Signed-off-by: Kittinan <tunxedge@gmail.com>
No rebase needed. Worth fixing the checkbox so it does not cost anyone a round trip.
Confirmed-good
- D0 green at the reviewed SHA, run in a detached worktree, not inherited from your log:
ruff check(all passed),ruff format --check(428 files),pyright src(0/0/0),check_doc_links(29 files),check_repo_hygiene(947 files),check_website_docs_pii,check_council_memory(47 files),generate_website_docs --check(mirror in sync), andpytest tests/api/transports/test_migrated_composer.py→ 34 passed. - The
.first→ refuse change is right, and matches labs.ui_automation_video.py:265-291reasons through this exact hazard and reaches the same conclusion; the port now carries the:not(:has-text('[Lower Priority]'))semantics it had dropped, on both the menu scan and the button read-back. Driving both from oneModelMenuMatcheris the correct structural fix — the two paths drifting is what caused defect 2. - Matching on the tag rather than the newly-captured label is the right call, and for the reason you give.
VEO_3_1_LITE_LOWER_PRIORITYkeys off the same tag on labs (ui_automation_video.py:291), so the two drivers stay symmetric. - Routing left alone is the conservative choice and is well argued:
migrated_can_servestill gates onVIDEO_MODEL_MENU_LABELS, so one account's capture cannot pull an unmoved account onto the new host. migrated.model_already_selectedcloses a real observability hole — a silent short-circuit on a billing-relevant path was worth a log line.- The test fake now models the mechanism, not the symptom: one overlay dismissed per Escape,
:visibleexcluding the detached pane, and a covered composer that is genuinely unclickable. That is why 8 of the 9 fail pre-fix.
Note on gates
SonarCloud analysis is skipped, not green — expected on a fork PR, so it is maintainer-checked here rather than a signal from this PR. Per AGENTS.md it cannot be called merge-ready on the Sonar axis until checked from a maintainer branch.
On the skipped lifecycle phases
Your accounting of what was and was not run is honest and I would not ask for retroactive predict/scenario theatre. The one gap with teeth is check step 1b — but D15 above verifies the mirror axes hold by inspection, so it is satisfied in substance.
Generated by Claude Code
… settle the memory row Review feedback on ffroliva#669. Both must-fix items and all three nice-to-haves. D1 (blocking) — `_close_pane` in a bare `finally` replaced the error already in flight. A `--model` Flow does not offer raises ConfigurationError (exit 11) naming what IS offered; a pane that then refused to close overwrote it with UiSelectorDriftError (exit 23) and the offered-models list was gone. The close is now `strict=False` on the exception path (warn, never raise) and `strict=True` only when the settings pass succeeded, so a stuck pane can still fail a run that would otherwise have typed a prompt into a covered composer — without ever standing in front of a better error. This was a defect introduced by this branch, not a pre-existing one. D5 (blocking) — the memory store still said PICKER MISS. Worse, it twice concluded from that miss that the `:not()` selector was broken. It never was: the entry is simply not served to accounts Flow is not throttling, and a throttled account renders it. Updated the 2026-08-14 row, the "CONFIRMED on a 2nd account" paragraph that drew the wrong conclusion, the 2026-09-03 row, and the "What is NOT settled" clause, and appended the settling capture. `flow-capabilities-are-cohort-dependent` gains the generalisation: an ABSENT menu entry is cohort evidence exactly like a present one, and "missing on N accounts" never by itself indicts the selector. Duration and credits for the tier stay recorded as unmeasured — it has only been selected at $0, never generated on. D2 — `_close_pane` targeted every `.cdk-overlay-pane`, but those classes are generic CDK and Flow mounts snackbars and tooltips in the same container. It now narrows to overlays containing a radiogroup or a menuitem, the two that are ours and the only two that answer to Escape. D14 — PANE_CLOSE_ESCAPES 4 -> 2, the measured stack depth. The headroom was hope; a third stacked overlay is now a named failure instead of something absorbed quietly. PII — the spike named a profile and a project id. Both removed; what mattered was never the identity but that Flow was throttling the account, which is what the doc says now. The gate not covering `docs/superpowers/` is left alone deliberately: it exists to verify the anonymized `website/docs/` mirror, and 65 files in the canonical `docs/` tree legitimately carry the tokens it forbids, so widening its scope would fail CI on all of them. Raised in the PR instead. Three new tests, all failing on the pre-review source: the stuck pane no longer masks the caller's error, an unrelated overlay is not ours to dismiss, and the constant matches the measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MK6VHisb4JifQPAmvm724e Signed-off-by: Kittinan <tunxedge@gmail.com>
…roliva#639 picker entries under [Unreleased] after v0.68.0
|
Re-review at Both must-fix items and all three nice-to-haves from the earlier council are addressed, verified against the SHA rather than the commit message:
Mechanical gate re-run in a detached worktree at What I pushed to your branch ( Evidence bar, forward-looking only. This PR carries live-verify evidence (two Veo generations, the $0 read-back matrix) and meets the current bar. Open PR #675 will additionally require a Merging once CI reports on |
…er clause to the merge Council review of #677 found the substitution still reachable -- not through the wording, but through the verdict colour. D4 said "must-fix" while scoring YELLOW, and a YELLOW is dismissable under section 5 step 8 with a one-line justification. "The live runs in the PR body are good enough" is precisely the justification someone writes, which reopens the ledger-for-e2e substitution through the escape valve rather than through the text. - Absent with no reason given now scores D4 RED, forcing consensus RED under section 5 step 3, and the skill says why it is not YELLOW so nobody softens it back later. - Absent WITH a stated reason stays YELLOW, blocked on a maintainer run -- that is the documented exemption, not an evasion. - Present is now verified rather than trusted: cite the tests/e2e file:line, confirm it exists at REVIEWED_SHA, confirm it exercises the changed surface. A passing e2e test that never touches the changed path is pr-must-verify-on-affected-surface wearing an e2e label. - The grandfather clause said "PRs opened before 2026-09-05", which excluded the only two PRs it exists for: #669 opened 11:28Z and #671 at 13:38Z, both hours before #675 merged at 19:15Z the same day. Now anchored to the merge commit, not the calendar day. The published slug carries the same RED wording, so the memory a future reviewer loads cannot disagree with the skill that cites it.
PR ffroliva#675 put the rule in the contributor-facing docs, but the review skills still scored by the old bar: D4 treated a missing surface test as "automatically YELLOW", and nothing told a reviewer that a PR body full of live runs does not close it. A rule the reviewer does not apply is not a rule. - New published slug `e2e-evidence-is-a-contributor-deliverable`, cited from the D4 mandatory-memory row so `check_council_memory.py` enforces the round trip both ways. - pr-council-review D4: missing e2e evidence on a Flow surface is a must-fix, with the three substitutions that must be refused named explicitly -- a live-verify ledger, a BDD .feature file, and demanding an impossible run from a docs-only change. PRs opened before 2026-09-05 are flagged forward-looking rather than held to it. - pr-council-review section 9: autonomous mode reports e2e evidence, never runs it -- the sandbox has no Flow auth and must not spend credits, so GREEN there means "evidence present and plausible", never "e2e passed". - live-verify Notes: this skill is not the e2e suite and does not satisfy the rule; a Flow-surface PR owes both. The substitution the D4 text refuses was proposed on ffroliva#669 within an hour of ffroliva#675 landing, which is why it is written down rather than left to judgement.
…echanism found, gesture not reliable
The account owner supplied the human gesture (ArrowDown + Enter; a mouse click
also works by hand), which explained every earlier failure: a synthetic Playwright
click reads as a click-away and dismisses the overlay. ArrowDown alone behaves —
the @ survives, the active asset stays active, UpteDb fires for its detail load.
Enter after it still cleared the composer, with and without waiting for that load.
What did attach was typing a query after the @:
<span class="mention-chip"
data-mention-id="c68767a5-..."
data-reference-type="entity"
data-entity-id="c68767a5-..."
contenteditable="false">aloha</span>
Checked rather than assumed: a clean editor load reads chips: [] with an empty
placeholder composer, so the chip was created by the typing and was not left over
from an earlier session.
That settles the SHAPE. A reference is a chip in the prompt document carrying an
entity id, which is consistent with the previous commit's finding that no rpc
fires on attach. A port builds a prompt containing chips and can verify them by
reading `.mention-chip[data-entity-id]` before submitting.
It is NOT reliable and must not be built on as-is: the chip reproduced ONCE, and
two later runs with the same query produced chips: 0, each failing the same way —
the picker stayed open and the next composer click died on a `.cdk-overlay-backdrop`
intercepting pointer events. So the insert is timing- or query-dependent in a way
that is not characterised, and no run has yet produced both a chip and a submit.
Whether the YhhmEf payload carries the entity id therefore remains open.
Side finding worth a look when the port lands: `_close_pane` counts
`.cdk-overlay-pane` only, and a `.cdk-overlay-backdrop` can outlive it and still
intercept pointer events — measured twice here. Same class as the #669 overlay
bug, same symptom. No shipped t2v path opens a picker, so it is not known to be
reachable in production.
No production code touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK6VHisb4JifQPAmvm724e
Signed-off-by: Kittinan <tunxedge@gmail.com>
Summary
Three defects in the migrated
flow.google.commodel picker (#639). The first is thereported one; the other two were found while fixing it and are the more serious pair —
one bills the wrong tier, one breaks the next run outright.
1.
--model veo-lite-lpcould not be selected at all. On an account Google hasalready moved,
_select_modelrefused withConfigurationError(exit 11, "not availableon the migrated Flow host") before it ever opened the picker. The omission was
deliberate: the migrated map is keyed by product label, and that tier had no captured
label — the 2026-08-14 two-account capability matrix, #650's duration capture and
v0.61.0's refusal A/B all recorded a picker MISS. It is now matched the way the labs
driver has since #539: by the
[Lower Priority]tag alone.The entry has since been captured — a migrated account renders
Veo 3.1 - Lite [Lower Priority], and its picker was already defaulted to that tier,which is the likely reason earlier captures missed it (they were taken on accounts Flow
was not throttling). Matching stays on the tag rather than the now-known label: one
account's rendering, the labs driver keys off the same tag, and a tag Flow appends to
whichever tier it throttles survives that tier changing.
2.
--model veo-litecould silently generate on the throttled tier. Entries werematched by case-insensitive substring with
.first, and the same substring ran againstthe picker button's read-back. Against the real button text:
So
--model veo-litereturned "already selected", never opened the menu, and generatedon the throttled tier — which Flow bills. This is the ambiguity #539 fixed on labs.google
(whose selectors carry
:not(:has-text('[Lower Priority]'))); the port had dropped it.3. Switching
--modelbroke the next run. Reported from the field: previous run onveo-lite-lp; run 1 onomni-flashdies, run 2 onomni-flashworks, run 3 onveo-lite-lpdies. Picking from the menu leaves Angular with two stacked overlays(the settings pane and the menu opened over it) and each Escape dismisses exactly one, so
_close_pane's single press left the settings pane covering the composer.send_prompt'sclick then failed actionability and surfaced ~5 s later as a bare
TimeoutErrornaming[contenteditable='true'], pointing nowhere near the pane. A repeat run binds the modelat the button read-back, never opens the menu, and never stacks the second overlay — which
is exactly why re-running the same model worked.
The click's message blames stability, and that is a red herring. Sampling every 100 ms for
12 s after a switch:
.cdk-overlay-pane:visiblemigrated.pane_still_opendoes fire on the pre-fix source — confirmed by replaying itagainst the live host, not assumed. It was a warning the run ignored; only the consequence
was missing, and it is now the failure itself.
What changed
ModelMenuMatcher(contains, excludes)replaces the bare label lookup and drives boththe button read-back and the menu scan, so the two cannot drift.
all_text_contents()rather than ahas_textfilter:an exclusion is not expressible as
has_text, and more than one hit must refuse(naming every candidate) instead of resolving
.first._close_paneescapes until no overlay is visible (bounded at 4) and raises pre-submit at$0 if one will not close.
migrated.model_already_selected(the read-back short-circuit previouslyemitted nothing at all) and a counted
migrated.pane_still_open.migrated_can_servestill gates onVIDEO_MODEL_MENU_LABELS, soveo-lite-lpdoes not pull an unmoved account onto thenew host. A moved account is routed by its URL and never reaches that question.
Refs #639. Not filed as its own issue — reported directly against v0.67.0.
Lifecycle
Issue triaged with— not run. This arrived as a directissue-assessmentfield report against v0.67.0, not a GitHub issue. Surfaces: CLI only — no MCP or CLI
flag surface is touched (
--model veo-lite-lpalready existed and already validated).— not run, and this does touch selectors. It is apredictverdict recordedpre-implementation gate and the work began as a one-line map addition; defects 2 and 3
were then found by measurement rather than design. Running it retroactively would be
theatre — say the word and I will run it against the diff instead.
— not written, samescenario+planunderdocs/superpowers/plans/reason. The investigation is written up instead as
docs/superpowers/spikes/2026-09-05-migrated-model-menu-lower-priority.md, with thecaptures, the measurements and an explicit "recorded, not asserted" section.
check— components run individually, not the skill, and the step 1b CLI↔MCPmirror sweep was not run as such. What did run is listed under Validation, plus the full
MCP suite (
tests/mcp/, 168 passed).MCP twin— not applicable.git diff origin/develop...HEAD -- src/gflow_cli/mcp/ src/gflow_cli/cli_video.py src/gflow_cli/api/video.pyis empty; this changes onlythe migrated transport's internals. No new CLI surface to mirror.
verified.
Council review— not run. Happy to runbranch-reviewbefore this is lookedat if you would rather see it first.
Live verification
Instrument:
scripts/dev/capture_migrated_model_menu.py(new). Selection happens entirelybefore submit — what made v0.61.0's and v0.62.1's refusal matrices free — so all of this
is $0 except where noted.
--drive, the shipped_select_modelfor all five tiers, reading the picker back aftereach switch (the only evidence separating "clicked the entry the user asked for" from
"returned success"):
omni_flashOmni 1.1 Flashveo_3_1_liteVeo 3.1 - Liteveo_3_1_fastVeo 3.1 - Fastveo_3_1_qualityVeo 3.1 - Qualityveo_3_1_lite_lower_priorityVeo 3.1 - Lite [Lower Priority]Both directions of the exclusion are exercised: plain Lite lands on the plain entry with
the throttled sibling in the same menu, and the throttled tier is reached through the
menu.
The reported sequence replayed after the fix —
omni-flash(switch) /omni-flash(repeat) /
veo-lite-lp(switch): all three apply settings and type the prompt, withvisible_overlays_after_settings: 0.Two real generations (not $0): one on
veo-lite-lp, and one end-to-end on the switchpath (
veo-lite-lp→omni-flash,migrated.model_selectedin the timeline) thatsubmitted, generated and downloaded a 2.6 MB clip, exit 0.
Could NOT be verified:
en). Whether Flow serves the[Lower Priority]entry toaccounts it is not throttling is untested, and every earlier capture suggests it does
not. The routing gate is left conservative for exactly this reason.
per matcher, so the >1 branch is covered offline only.
_open_panecannot be called twice on one page load — re-opening the settings paneleaves a detached overlay that still contains radiogroups and
.lastresolves to it("0 option groups"; reproduced twice while building the probe, which now loads a fresh
document per tier). Production opens the pane once per run, so no shipped path is known
to hit it. Recorded in the spike, not filed.
Validation
origin/develop'ssource and pass here. The ninth
(
test_repeating_the_same_model_never_stacks_the_second_overlay) passes on both bydesign: it pins the working half of the reported asymmetry. The fake DOM now models
what the live host does — a menu pick leaves an overlay that eats the next Escape,
:visibleexcludes the detached pane, and a covered composer is not clickable.CHANGELOG.md,KNOWN_ISSUES.md+website/docs/KNOWN_ISSUES.md, and the new spike.uv run python scripts/ci/check_doc_links.py→All links resolved across 29 files.uv run ruff check src tests→All checks passed!(plusruff format --check,426 files already formatted)
uv run pyright src→0 errors, 0 warnings, 0 informationsuv run pytest -q tests/api/transports/test_migrated_composer.py→ 34 passeduv run pytest -q tests/mcp/→ 168 passedOne unrelated pre-existing failure, reproduced on an untouched
origin/mainworktree:tests/features/test_auth_steps.py::test_status_of_a_profile_with_a_live_flow_sessionasserts
"experiments" in output, but Rich wraps the long basetemp path mid-word(
profile_expe\nriments). Environment/path-length artifact, not from this branch.Worth flagging for review: both the offline suite and the
--drivematrix misseddefect 3, because neither touched the composer after a real switch. The new switch-path
tests close that gap, but the matrix alone is not sufficient coverage for this driver.
Contribution Checklist
developKittinan <tunxedge@gmail.com>)Signed-off-by:is missing. This is an external contribution and the threecommits carry no DCO trailer. Needs
git rebase --signoff origin/develop(or a re-commit with-s) before merge.and probe do contain is a profile name, project ID and media IDs from the capture —
the same class of identifier existing
LIVE_VERIFICATION_*.mddocs carry, but they areaccount data, so redact them if you would rather they were not in the repo.
were written by Claude Code with live measurement; the diff, the spike's claims and the
two real generations should be read before this is opened.