Repository navigation
Description
What
#703 swept three selector constants for the labs/migrated ligature-carrier split (<i class="google-symbols"> vs <mat-icon>) — add_2, arrow_drop_down, accessibility_new. It missed PROMPT_FORMAT_SELECTORS, which became a silent no-op and shipped as #727.
Nothing proves that was the last one. Measured on 2026-09-07 at 7c1d4c6d:
git grep "i\.google-symbols" src/gflow_cli --include=*.py | grep -v "mat-icon" | wc -l
55
55 single-carrier literals across ui_automation.py, ui_automation_video.py, mode_control.py, diagnostics.py and drivers/{agentic,factory}.py. Two consecutive PRs have now fixed one of these by hand, which is the signal that this is a sweep problem, not a selector problem.
Named candidates from the #729 council (all pre-existing, none introduced there):
| Constant | Location | Exposure |
|---|---|---|
IMAGE_MODEL_PICKER_TRIGGER |
ui_automation.py:104 |
Sharpest. Sole selector, no mat-icon twin — while its sibling _CHARACTER_MODEL_PICKER_TRIGGER_SELECTORS in the same file carries both and a comment recording that omitting mat-icon logged model-picker trigger not found on the migrated host. Likely masked today because _select_image_model only runs after a panel that warn-returns first — not verified on the migrated host. |
ui_automation_video.py:251 |
same shape | same class |
OVERLAY_CLOSE_BUTTON_SELECTORS |
ui_automation.py:490-494 |
clear / close are <i>-only. Degraded, not dead — the [role=dialog]:has(a[href*=changelog]), button[data-dismiss] and Escape fallbacks remain. |
NEW_PROJECT_SELECTORS |
ui_automation.py:329-331 |
add_2 tier is <i>-only; falls back to a regex + 14 localised labels. |
Why nothing caught it, and why nothing will catch the next one
.github/workflows/selector-probe.yml drives run_probe.py --surface editor against src/gflow_cli/flow_selectors/registry.py. That registry has zero entries for the Format button:
grep -rn "personal_recommendations\|format" src/gflow_cli/flow_selectors/ --include=*.py
(no output)
And e2e_character is opt-in (GFLOW_CLI_E2E_RUN_CHARACTER=1); the nightly canary runs e2e_auth only — 18 of 87 tests. So the drift was invisible from both directions at once: no probe watched the selector, and no nightly test exercised the command.
Suggested shape
- Sweep the 55, both carriers — or better, anchor on the shared
.google-symbolsclass, which matches<i>and<mat-icon>alike, since the class was never the discriminator (the tag was). - Prefer custom elements where one exists —
<flow-format-prompt-button>,<flow-slot-chip-button>,<flow-generate-icon-button>are component boundaries that survive both a carrier change and a translation. - Register the surfaced anchors in
flow_selectors/registry.pysoselector-probe.ymlwatches them nightly. This is the half that makes the fix durable.
Worth a /gflow:predict first — item 1 touches ~55 call sites across 6 files and item 3 changes what the nightly probe asserts.
Evidence
docs/superpowers/spikes/2026-09-07-character-format-button-anchor.mddocs/superpowers/memory/ligature-carrier-differs-by-host.md- character create --format-prompt silently skips Flow's Format button — the personal_recommendations anchor has drifted #727, fix(character): re-anchor the Format button on its custom element, not a tag that moved #729, fix: drive character create on the migrated Flow host #703
Activity
fix(character): council fixes for the Format-button re-anchor
fix(scripts): delta-review fixes - E501 from the re-indent, and a rat…
fix(selectors): de-blind the diagnostics, and fix the two selectors h…
ffroliva commentedon Sep 7, 2026
Partially fixed in #735 — and this issue''s original framing was wrong
First, a correction to my own issue text. "55 single-carrier literals" counts literals and double-counts cascades that alternate carriers across entries. The actionable unit is the constant:
<i>-only ← the real gapShipped in #735 (verified live, both surfaces)
SUBMIT_BUTTON_SELECTORSIMAGE_MODEL_PICKER_TRIGGERui_automation_gsQuery+diagnostics.STRUCTURAL_DOM_JSThe diagnostics one is the headline and it was not in this issue. Our incident-bundle DOM dump queried
i.google-symbols, span.google-symbols, so onflow.google.comit returned zero ligatures. The instrument used to diagnose selector drift was blind to the host the drift lives on — which is why #727 and #731 stayed invisible, and why a bundle from a migrated user would have named nothing.SUBMIT_BUTTON_SELECTORSwas also working by luck: its tag-qualified entries matched nothing and the submit only landed via a text fallback matching the<mat-icon>''s text.Also: it moved to
:text-is.:textis a substring match that acceptsarrow_forward_ios, a real Material Symbol — the<i>qualifier had been containing that, and the class-only form made it reachable. So "one character each" is wrong: dropping a tag qualifier can expose a latent substring over-match, and each site needs its exactness checked too.Deliberately still open, with measured reasons
The negation sites —
button:not(:has(i.google-symbols))(ui_automation_video.py:432) and ahas_notfilter (:2199). Broadening inverts them: they would exclude more buttons, and if a dialog''s buttons carry mat-icons the iconless set collapses to zero → no include click → a 120s hang (REMOTE_PICKER_CLOSE_TIMEOUT_S). They are also already broken the other way on migrated, wherei.gs=0makes them match every dialog button — including the tabs their docstring says they exclude. Needs its own spike with the picker dialog open, counting.google-symbolsdescendants per dialog button.The 10 mode-detection selectors (
drivers/factory.py,mode_control.py). They feed a classifier that callsraise_if_migratedevery tick, so they never execute on the host this fixes. Zero upside, nonzero risk on labs — unmeasured.The
add_2name drift — a different bug, and arguably worse. The migrated host rendersaddand rendersadd_2nowhere (composeradd=1/add_2=0, editoradd=2/add_2=0). That is a ligature name change, not a carrier one.NEW_PROJECT_SELECTORS,ADD_MEDIA_BUTTONand_CHARACTER_SLOT_ADD_SELECTORall anchor onadd_2— and the last one already covers both carriers, so fix: drive character create on the migrated Flow host #703 swept it and left it broken. It also feeds_CHARACTER_SLOT_ADD_LIGATUREinto a JS exact-match heuristic, so the fix is not a string swap. This deserves its own issue.The nightly probe cannot see any of this.
flow_selectors/registry.py''surl_templateandrun_probe.py''s cookie domain are both labs.google. It never touches the migrated host and covers 2 of the 24 constants — it would have stayed green through this entire drift.Evidence:
docs/superpowers/spikes/2026-09-07-ligature-carrier-and-name-drift.md, probescripts/dev/spike_ligature_carrier_sweep.py, offline prooftests/api/transports/test_ligature_carrier.py.ffroliva commentedon Sep 7, 2026
Correction: the
add_2drift is ONE reachable defect, not three — and it is an error message, not a selectorMy earlier comment on this issue said
NEW_PROJECT_SELECTORS,ADD_MEDIA_BUTTONand_CHARACTER_SLOT_ADD_SELECTOR"all anchor onadd_2and stay broken". Two of those three are wrong. Traced properly before editing anything:_CHARACTER_SLOT_ADD_SELECTOR_click_character_slot_addtries_CHARACTER_BODY_MODE_SELECTORfirst, which already covers the migratedflow-slot-chip-button. Theadd_2path is the legacy fallback — and the constant''s own docstring already said "the migrated host has noadd_2control at all"ADD_MEDIA_BUTTONui_automation_video.pyreturnsFlowHostMigratedErrorbefore any of its ligature selectors runNEW_PROJECT_SELECTORS--project— andmigrated_can_serverefuses without oneAll three are labs-only in practice. The name drift has no live production impact on the migrated host today.
That is the third count in this issue I have had to revise downward (55 literals → 24 constants → 3 broken → 1 reachable defect). The mistake was the same each time: generalising from a grep without checking reachability.
What was actually fixed — #739
The reachable defect is the error message. On the migrated host the sweep produced:
which blames the selector for a control that host does not render, with a remediation the reader cannot act on. Now
FlowHostMigratedError, naming the host and--project. Labs still reports genuine CTA absence as drift.Why it still mattered
The
add_2drift is harmless because of guards, not because the selectors are right — and guards are what a port relaxes. My own spike hit that message and learned nothing from it. The measured fact (add, neveradd_2; composeradd=1/add_2=0, editoradd=2/add_2=0) now sits beside the constant, with why amat-icontwin would not help.Still open on this issue
ui_automation_video.py:432,:2199) — broadening inverts them; risk of a 120s hang. Needs a spike with the picker dialog open.registry.pyurl_templateandrun_probe.pycookie domain both point atlabs.google, covering 2 of 24 constants. It would have stayed green through all of this. This is arguably the most valuable item left, because it is what makes the next drift self-reporting.docs(character): re-derive the selector table from shipped code, drop…
spike(730): the carrier is one character; the ligature NAME is the ot…
fix(migrated): name the host, not the selector, when the gallery has …
fix(selectors): anchor the New project CTA on `add`, and revert a gua…