Repository navigation
fix: drive character create on the migrated Flow host - #703
Merged
Merged
Conversation
`gflow character create` was believed impossible on flow.google.com. It was
not: the editor is fully present there, backed by the SAME labs tRPC and
aisandbox APIs (the migrated page calls flow.projectInitialData and
v1:checkAppAvailability itself). Only the view layer differs — labs renders
React + Slate, flow.google.com renders Angular + ProseMirror — so our
selectors missed, the 20 s readiness gate timed out, and the absence of a
match was recorded as the absence of the feature.
v0.69.0 then hardened that misreading: a `raise_if_migrated` guard aborted
with exit 36 BEFORE probing the DOM, so the premise could never be
disproved by running the code. That guard was the defect, and it is gone.
Seven anchors, all the same class of mistake:
* readiness gate Slate -> also ProseMirror
* migrated-host guard removed from _enter_character_editor
* result listener labs batchGenerateImages never fires; read the ids
off the ENTITY, which proves the character binding
by construction rather than trusting a
self-reported parentEntityId
* model chip i.google-symbols -> also mat-icon
* body mode anchored on <flow-slot-chip-button>, the component
boundary (the Characters nav shares the ligature,
and there is no add_2 control on that host)
* body settle signal the face reference mounts from
flow-content.google/image/, a different origin
* prompt-box counter counted Slate only, reporting 0 boxes on a visibly
mounted editor
--model is now deterministic. The picker was best-effort: every failure
logged a warning and let the generation run on whatever tier the editor
showed, so `--model nano2` could return a Nano Banana Pro image silently.
It now reads the visible menu, matches in Python with an explicit
exclusion (`Nano Banana 2` is a prefix of `Nano Banana 2 Lite`), refuses an
ambiguous or absent match, and VERIFIES by re-reading the chip instead of
trusting the click. Anything else raises. Aborting is free — the picker
runs before submit, so a refusal costs no quota, while proceeding produces
a paid artifact from the wrong model.
Each slot now takes its own primaryMediaId, read from the project listing.
The entity exposes only a thumbnail id, and reusing it across slots made
the body slot claim the face's media — which commit_workflow then PATCHed
onto the body workflow, corrupting it.
A failed create no longer strands an "Untitled Character". The saga keeps
its STARTED row for resume, but that row is keyed on (project_id, name),
so a retry under any other name missed it and minted a second entity. A
zero-progress failure now rolls the free entity back and marks the row
FAILED — after asking the backend, because an empty local workflow list
means only that this process failed to read a result.
The agent-browser CDP/HAR spike harness is vendored into
scripts/dev/har-spike/ and documented in AGENTS.md as one of two spike
modes. It lived outside git, so no agent could discover it, nothing pinned
its agent-browser version, and its one test ran nowhere — which is why
this session wrote three capture scripts from scratch before learning it
existed. Its artifacts and caches are gitignored; its test now runs, and
repo hygiene immediately caught a hardcoded user path in it.
Verified live on a moved account (ci-probe): a complete character — name,
personality, voice, portrait and body — with two distinct workflow ids,
two distinct media ids and both images on disk, confirmed by read-back.
Model selection verified over four consecutive runs alternating tiers.
The orphan rollback verified by A/B control: 1 -> 2 entities without the
fix, 2 -> 2 with it.
4 tasks done
ffroliva
added a commit
that referenced
this pull request
Sep 6, 2026
The ledger recorded the bug as the fix. Its exit-code table presented `character create` on a moved account exiting 36 as a v0.70.0 improvement, and its not-verified list said CHARACTER entities "cannot run on any account available here". Both were false, from one root: a 20 s readiness timeout was read as proof the feature does not exist on flow.google.com, and #701 then added a guard that aborted before probing the DOM — which made the claim unfalsifiable, because no run could look. #703 disproved it. The editor is fully present on the migrated host, on the same labs tRPC + aisandbox backend; only the view layer differs (React + Slate vs Angular + ProseMirror). Replaces both statements with what was actually run: a complete character on ci-probe — name, personality, voice, portrait and body, two distinct workflow ids, two distinct media ids, both images on disk with different checksums, confirmed by read-back. Plus `--model` determinism over four alternating runs and the orphan-rollback A/B (1 -> 2 entities without the fix, 2 -> 2 with). Records what was NOT verified rather than omitting it: the labs.google path for all seven changed anchors, since every account here is migrated. Also updates the #692 entry with evidence that changes what that issue is. flow.google.com CAN mint a reCAPTCHA Enterprise token: on /project/<id> the enterprise script and site key are present and a mint returns a 2404-char token, while the root grid carries no script at all. The mint runs on the pooled bootstrap page, which after the handoff IS the root grid — which is exactly why the reporter's bundle showed route: "/". So "any mint failure on a migrated page yields exit 36" refuses the host rather than fixing the cause. The probe and its write-up are deliberately not linked from here while they sit on the spike-skill branch: `check_doc_links.py` does not cover this file, so a cross-branch reference would have shipped dangling with every gate green.
5 tasks done
ffroliva
added a commit
that referenced
this pull request
Sep 6, 2026
git never descends into an ignored directory, so the negation added in #703 for scripts/dev/har-spike/artifacts/.gitkeep could never take effect — the file was never tracked. Removed rather than restructured: the capture scripts create the directory at runtime, so nothing needs to hold it open.
5 tasks done
ffroliva
added a commit
that referenced
this pull request
Sep 6, 2026
The ledger recorded the bug as the fix. Its exit-code table presented `character create` on a moved account exiting 36 as a v0.70.0 improvement, and its not-verified list said CHARACTER entities "cannot run on any account available here". Both were false, from one root: a 20 s readiness timeout was read as proof the feature does not exist on flow.google.com, and #701 then added a guard that aborted before probing the DOM — which made the claim unfalsifiable, because no run could look. #703 disproved it. The editor is fully present on the migrated host, on the same labs tRPC + aisandbox backend; only the view layer differs (React + Slate vs Angular + ProseMirror). Replaces both statements with what was actually run: a complete character on ci-probe — name, personality, voice, portrait and body, two distinct workflow ids, two distinct media ids, both images on disk with different checksums, confirmed by read-back. Plus `--model` determinism over four alternating runs and the orphan-rollback A/B (1 -> 2 entities without the fix, 2 -> 2 with). Records what was NOT verified rather than omitting it: the labs.google path for all seven changed anchors, since every account here is migrated. Also updates the #692 entry with evidence that changes what that issue is. flow.google.com CAN mint a reCAPTCHA Enterprise token: on /project/<id> the enterprise script and site key are present and a mint returns a 2404-char token, while the root grid carries no script at all. The mint runs on the pooled bootstrap page, which after the handoff IS the root grid — which is exactly why the reporter's bundle showed route: "/". So "any mint failure on a migrated page yields exit 36" refuses the host rather than fixing the cause. The probe and its write-up are deliberately not linked from here while they sit on the spike-skill branch: `check_doc_links.py` does not cover this file, so a cross-branch reference would have shipped dangling with every gate green.
This was referenced Sep 6, 2026
Merged
The ligature-carrier sweep is unfinished: 55 single-carrier selectors, and nothing watches them
#730
Open
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
Nine-dimension council on 7c1d4c6 returned 4 GREEN / 5 YELLOW. Every finding below was raised by a reviewer, not by the author. D1/D14 - the retained 2026-07-27 comment still said, present tense, that the EN `span:text-is('Format')` fallback "has to be" the structural anchor, describing an entry the same commit deletes. The block also told the spike doc's story a second time, in the copy that will drift. Cut to the two DOM captures (the evidence) plus four lines, and a link to the finding. D4/D6 - `test_clicks_ligature_selector_first` kept its name and docstring while only its assertion moved to the custom element; the e2e's failure message still told a future drift victim the cascade is "anchored on the personal_recommendations ligature". A label that lies outlives the bug it describes. Renamed, reworded, and the e2e message now names the probe to run. Assertions themselves untouched - only the string a failure prints. D4 - the fall-through test drove only `PROMPT_FORMAT_SELECTORS[-1]`, so the new `<mat-icon>` entry at index 1 was never exercised. Parametrized over the whole cascade. D6 - `is_visible(timeout=1000)` is inert: Playwright documents the argument as ignored, and the call answers from the current DOM without waiting. Dropped, with the reason, so nobody re-derives "a 4-entry cascade costs 4s before the submit". D9 - the CHANGELOG called the capture "DOM read only" while the method it cites does a free `createEntity`, which is a write. Two files, one method, disagreeing. D5 - `flow-locale-leak-icon-ligatures.md` still prescribed the `<i>`-only pattern that caused this: #703 fixed three constants for the carrier split and never updated the memory that teaches it, so the fourth was written the old way on purpose. Corrected, and the split promoted to its own entry. D13/D2 - the spike put its guard in FRONT of its probe: a control miss raised before the dump, the screenshots and the report, deleting the evidence that would correct it. That is the #701 failure this file cites in its own docstring. Both controls now raise from a `finally` that has already written everything. A second control was missing entirely - nothing checked that the typed text landed, so a click intercepted by a consent overlay would have recorded a false absence indistinguishable from a real measurement. The scratch entity is deleted again, and the output path is root-anchored: `.gitignore`'s entry is slash-anchored, so running the spike from `scripts/dev/` wrote an UNIGNORED capture carrying the profile name, live URL, entity ids and authenticated outer_html. D14 - the occlusion probe measured the prompt box, not the button under investigation, and the spike doc filed that under "confirmed, not assumed". Deleted; the live e2e click landing is stronger evidence anyway. Re-verified live on denon82 after the rewrite ($0, 14s, clean exit): control_check count=1 typed_control chars=47 flow-format-prompt-button button -> 1 button:has(mat-icon:text-is('personal_recommendations')) -> 1 button:has(i.google-symbols:text-is(...)) -> 0 button:has(i:text-is(...)) -> 0 Declined, with reason: dropping `button:has(i.google-symbols:...)` as redundant with the broader `button:has(i:...)`. The superset argument is correct, but those are the labs anchors and no unmoved account exists here to re-verify against - narrowing an untested surface to save one line is the wrong trade. Refs #727, #730, #731
Merged
6 tasks done
tranzmatt
pushed a commit
to tranzmatt/gflow-cli-unofficial
that referenced
this pull request
Sep 9, 2026
…t a tag that moved
`--format-prompt` had degraded to a silent no-op on the migrated host: all three
entries of PROMPT_FORMAT_SELECTORS missed, `format_button_not_found` fired, the
prompt submitted as typed and the run exited 0 having spent image quota on a
prompt-engineering step that never happened.
Nothing was absent. Measured live on denon82 ($0 — navigation, one free
createEntity, DOM reads), with a control anchor so a flat zero could not be read
as absence:
control_check count=1 (prompt box resolved — the probe was in the editor)
personal_recommendations via i.google-symbols -> 0
personal_recommendations via i -> 0
span:text-is('Format') -> 0
the button itself, visible the whole time -> 1
<flow-format-prompt-button>
<button aria-label="Formatar">
<mat-icon class="... google-symbols ...">personal_recommendations</mat-icon>
Two independent misses, either one fatal:
a. Angular renders the ligature in <mat-icon>, not <i>. The ligature never
changed and mat-icon even carries the google-symbols class, so the class
was never the problem — the `i` tag was. This is the same carrier split
already fixed for add_2 / arrow_drop_down / accessibility_new in ffroliva#703;
this constant was simply not swept with them, and nothing proves it was
the last one that was missed.
b. The button now has an aria-label, but Flow localises it ("Formatar" on a
pt account), as it does the <span>. The EN text fallback is deleted rather
than translated — display labels are banned as anchors (AGENTS.md).
The cascade now leads with the custom element <flow-format-prompt-button>: a
component boundary rather than a layout accident, unique among the six composer
buttons, and the strongest anchor class this frontend offers.
Two unit tests pin the contract that was missing: the cascade must carry BOTH
ligature carriers, and it must carry no display-label anchor. Both were watched
red against the old constant before the fix.
Verified by a live run, not by relaxing the assertion:
tests/e2e/test_character_create_e2e.py::test_character_create_format_prompt_clicks_format_button
1 passed in 219.90s — denon82, real generation, zero credits
Not measured: the labs frontend. No unmoved account exists here, so the two <i>
entries ride on the 2026-07-27 capture alone and were not re-verified today.
Closes ffroliva#727
tranzmatt
pushed a commit
to tranzmatt/gflow-cli-unofficial
that referenced
this pull request
Sep 9, 2026
… the false VERIFIED Fixes ffroliva#731. docs/CHARACTER.md section 12 marked rows **VERIFIED** that stopped being true at ffroliva#703, and the file is mirrored to website/docs, so the false claims are published. A row marked verified that is not is worse than no row: it is what a reader trusts INSTEAD of reading the code. That is how ffroliva#727 got written - the carrier split was documented nowhere a selector author would look, and the table confidently said otherwise. Every row re-derived by reading the constant, not edited from memory. What that turned up, beyond what ffroliva#731 reported: - Model picker: the documented selector carried `aria-haspopup='menu'`, which appears in NONE of the four shipped entries. Not merely stale - it never matched the shipped tuple. - Generate / submit: SUBMIT_BUTTON_SELECTORS is single-carrier, and on the migrated host BOTH `<i>` entries miss. Only the third, `has-text('arrow_forward')`, fires - matching the mat-icon's text rather than its tag. Observed live 2026-09-07: `prompt_submitted via="button:has-text('arrow_forward')"`. It works by luck, not by design; noted in the row and tracked in ffroliva#730. - Prompt box: the row claimed PROMPT_INPUT_SELECTORS[0]. On the migrated host [0] misses and [1] `div[contenteditable="true"]` is what matches (live, same run). - Editor-ready anchor and body-mode chip: both gained mat-icon / custom-element alternatives at ffroliva#703 that the rows never picked up. The undated **VERIFIED** marker is replaced by a per-row date naming what the row was checked against - code, live run, or both - so a row cannot silently outlive its evidence again. The carrier split is stated at the top of the section, where someone writing a selector will actually meet it. Deliberately NOT included: the Format-button row and the docs/USAGE.md `--format-prompt` entry. Both describe behaviour that is still changing in ffroliva#729, and documenting a flag from this branch would describe the broken version. They ship with that PR. Also removed before commit: a [[ligature-carrier-differs-by-host]] wikilink I had added here. That memory file lands with ffroliva#729, not develop - a pointer to something that does not exist yet is the same defect this PR is fixing. Verified: website mirror regenerated and in sync, doc links resolve, PII scan clean, hygiene 1023 files. Three pre-existing dangling wikilinks remain in this file (private-store-only memories); left alone, out of scope.
tranzmatt
pushed a commit
to tranzmatt/gflow-cli-unofficial
that referenced
this pull request
Sep 9, 2026
…her half Phase 0 for ffroliva#730, $0, denon82 migrated host. Two surfaces, controls recorded before any count, hard-error only after the dump is written. CARRIER RESULT - unambiguous. i.google-symbols matched ZERO for every ligature on both surfaces; mat-icon matched; and .google-symbols matched exactly the same count as mat-icon every time. The class sits on both carriers, so a class-only anchor covers labs AND migrated with no extra cascade entry: the fix for the 24 <i>-only constants is one character each, not 24 duplicated entries. Two selectors confirmed broken-but-masked: - SUBMIT_BUTTON_SELECTORS: both <i> entries miss; only has-text('arrow_forward') fires, matching the mat-icon's TEXT rather than its tag. Independently seen live the same day. It works by luck. - IMAGE_MODEL_PICKER_TRIGGER: arrow_drop_down i=0/mat=1, no twin, no fallback. THE FINDING A CARRIER-ONLY FIX WOULD HAVE SHIPPED PAST: the migrated host renders `add`, and renders `add_2` NOWHERE (composer add=1/add_2=0, editor add=2/add_2=0). That is a ligature NAME change, not a carrier change. NEW_PROJECT_SELECTORS, ADD_MEDIA_BUTTON and _CHARACTER_SLOT_ADD_SELECTOR all anchor on add_2 - and the last one ALREADY covers both carriers, so ffroliva#703 swept it and left it broken. Changing the carrier on those three would have produced a green sweep and three selectors that still match nothing. It also corrects ffroliva#730's own number: 55 counts literals and double-counts cascades that alternate carriers. 24 constants is the actionable figure. The probe found the name drift only because it counted whether the ligature was present AT ALL, alongside the carrier comparison - a control column added so a zero could not be misread as absence. Added for epistemic hygiene; it is what turned up the second defect class. Also recorded: the first run of this spike died at `Could not find 'New project' CTA`, because it entered via _enter_editor, which clicks NEW_PROJECT_SELECTORS - one of the constants under test. A probe must not route through its own subject. Now navigates straight to the project URL, with the reason in the code. NOT measured, and stated in the doc: labs (no unmoved account here), ligatures absent from both surfaces are not proven absent from the product, and whether broadening to .google-symbols changes .first picks where document order matters.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gflow character createwas believed impossible onflow.google.com. It was not. The editor is fully present there, backed by the same labs tRPC and aisandbox APIs — the migrated page callsflow.projectInitialDataandv1:checkAppAvailabilityitself. Only the view layer differs: labs renders React + Slate, flow.google.com renders Angular + ProseMirror. Our selectors missed, the 20 s readiness gate timed out, and the absence of a match was recorded as the absence of the feature.v0.69.0 then hardened that misreading: a
raise_if_migratedguard aborted with exit 36 before probing the DOM, so the premise could never be disproved by running the code. That guard was the defect. It is gone.What changed
Seven anchors, all the same class of mistake:
div.ProseMirror[contenteditable]_enter_character_editorbatchGenerateImages(never fires there)i.google-symbolsmat-icon<flow-slot-chip-button>component boundarymedia.getMediaUrlRedirectflow-content.google/image/Reading the result off the entity also proves the character binding by construction, which is stronger than trusting the labs path's self-reported
parentEntityId.--modelis now deterministicThe picker was best-effort: every failure logged a warning and let the generation run on whatever tier the editor showed, so
--model nano2could silently return a Nano Banana Pro image. Three faults did exactly that — it skipped the click entirely fornano2(assuming it was the editor default, true on labs, false on migrated); its option selector:has-text(...)was unanchored so.firstresolved to<html>; and its menu lookup was unscoped, so[role='menuitem']mixed the open menu with hidden entries from the editor's other menus andnth(i)clicked an unclickable node.It now reads the visible menu, matches in Python with an explicit exclusion (
Nano Banana 2is a prefix ofNano Banana 2 Lite; the menu offers three tiers, not the documented two), refuses an ambiguous or absent match, and verifies by re-reading the chip rather than trusting the click. Anything else raisesConfigurationErrororUiSelectorDriftError. Aborting is free — the picker runs before submit, so a refusal costs no quota, while proceeding produces a paid artifact from the wrong model.Two data-integrity fixes
primaryMediaId, read from the project listing. The entity exposes only a thumbnail id; reusing it across slots made the body slot claim the face's media, whichcommit_workflowthen PATCHed onto the body workflow, corrupting it.(project_id, name), so a retry under any other name missed it and minted a second entity. A zero-progress failure now rolls the free entity back and marks the row FAILED — after asking the backend, because an empty local workflow list means only that this process failed to read a result.HAR/DOM spike harness vendored in
scripts/dev/har-spike/(88 KB) with itsagent-browserversion pinned, artifacts and caches gitignored, its test now running intests/scripts/, and AGENTS.md documenting the two spike modes. It lived outside git, so no agent could discover it and nothing pinned it — which is why this session wrote three capture scripts from scratch before learning it existed. Repo hygiene immediately caught a hardcoded user path in it.Test plan
Live on a moved account (
ci-probe), verified by independent read-back:--model nano2/--model nanopro, correct tier every timemedia.getMediaUrlRedirectprobed against a migrated portrait id: HTTP 200, 696 767 bytes, JFIF (the 404 recorded inmigrated_composer.pywas measured on a video id and does not generalise)Not verified: the labs.google path. All three accounts here are migrated, so the labs branches of these selectors are covered by unit tests only. Named as a blocker rather than claimed.
Recon:
scripts/dev/spike_migrated_character_*.py,spike_migrated_vs_labs_provenance.py.