Repository navigation
merge: upstream v0.78.0 into main-tun-tmp-fix - #3
Merged
Merged
Conversation
…e upload
1. verification.py: fail-closed migrated-host session fallback — for accounts
Google moved to flow.google.com the labs NextAuth session is never minted
(labs cookies absent entirely); upgrade GOOGLE_SESSION_ONLY to AUTHENTICATED
only when SAPISID + __Secure-OSID/OSID are present AND the email resolves
from myaccount.google.com.
2. migrated_composer.py::_pick_frame_by_name: Flow's Frames picker now requires
an explicit confirm ('Add to prompt') after picking — click it when the
overlay stays up instead of timing out into UiSelectorDriftError (5/5 i2v
failures on a migrated account).
3. migrated_composer.py::attach_start_frame: upload a run-unique COPY of the
start frame so the picker's display-name search has exactly one match —
repeated same-name uploads made the search bind a stale duplicate and the
submit-body assertion fired post-click (credits spent, clip lost).
Verified live on a migrated ULTRA account: login verified via the fallback;
i2v 9:16/8s end-to-end (submit YhhmEf, polls jwpduf, result as29s, download)
with ffprobe-confirmed output. Evidence and repro details in the linked issue.
) A missing .gflow_browser_strategy marker made the Playwright cookie reader raise SecurityError, which escaped normalization and was flattened into VERIFICATION_ERROR — whose guidance is 'check network connectivity', for a file on the user's own disk. It is also the state a failed FIRST login leaves behind, and VERIFICATION_ERROR is not GOOGLE_SESSION_ONLY, so a migrated-host fallback keyed on the latter would never run on those profiles. Adds a distinct PROFILE_MARKER_MISSING outcome, mirrored on the MCP twin as 409 / retryable=false. gflow credits raised 'aisandbox-pa authentication failed' with the remediation 'SAPISID cookie missing, expired, or unreadable' when labs.google answered 200 with no access_token — the normal shape for a migrated account. aisandbox-pa had not been contacted and SAPISID was fine, so the suggested re-login loop could never terminate. The raise now carries an accurate hint; credits stays labs-only on migrated accounts, tracked in ffroliva#795. Refs ffroliva#795, ffroliva#796, ffroliva#791
…ked (ffroliva#794) The migrated composer parked its pooled page on about:blank in a bare `finally`, so on the FAILURE path it navigated away before FlowApiClient._capture_incident read the page -- whose own docstring promises to stage the bundle 'while the page is still alive'. Every migrated video and image failure shipped tag_counts.div = 0, a white screenshot and host_category = 'other' beside a network journal proving the app was alive. Reported in ffroliva#792 as the driver losing its page handle on a portrait run; it is neither portrait-specific nor a lost handle -- the reporter's own viewport (1280x720, client.py:495) proves it was the driven page, captured one navigation too late. The park is deferred and drained at the top of the next locked body, before the route decision reads page.url. That placement covers the post_with_retry path, where a retryable 5xx re-enters the transport without passing the client's failure boundary -- attempt 2 would otherwise resume on a still-mounted composer and risk a double submit. Also removes a 4 s stall: _settle_if_redirecting on about:blank matches neither short-circuit and waits out URL_SETTLE_TIMEOUT_MS for a pattern a blank page can never take. The goto/settle ratchet carries the exemption with its reason. Verified live on a migrated account at zero cost by a new committed e2e case: FAILED in 46.3 s on develop, passed in 37.8 s with the fix, with div 0 -> 25, title 0 -> 81, host_category other -> flow_app. Refs ffroliva#792
chore(release): v0.73.2
…unique (ffroliva#800) Two defects on Flow's migrated flow.google.com host, reported and root-caused by @ai4U23, who verified the fix end-to-end on the affected cohort. 1. The Frames picker no longer commits on the option click for some cohorts; it waits for its own "Add to prompt". gflow sat out FRAME_COMMIT_HIDDEN_S for an auto-close that never came and raised UiSelectorDriftError with the asset already picked, failing i2v before submit (no credits spent). The picker now gets a grace period and, if still up, its confirm is clicked -- scoped to the live pane and gated on is_visible(), since the method reopens the picker up to three times and a detached pane's button still counts. Anchored on button.detail-add-to-prompt-btn, measured on the Frames entry (scripts/dev/spike_frames_picker_confirm.py) rather than on the translated label the report used. The same spike killed the obvious structural shortcut: the popover's other visible button is header-close-btn, so "the button that is not an asset option" would have clicked Close. 2. Both attach paths find their upload again BY DISPLAY NAME -- the Frames picker searches it (i2v), the @ mention queries it (r2v) -- and Flow lists an upload under the name it was given. A re-run of the same file left two identical entries and the lookup could bind the stale one. i2v leaned on the picker's newest-first sort to break the tie; that is a sort assumption about someone else's app, and it lost. Uploads now carry a run-unique display name, handed over as a Playwright FilePayload so nothing is copied to disk. Fixed once in _unique_display_name + _upload_via_toolbar, not on the path the issue names: attach_references reaches the same upload leg and re-uses reference images across runs by design, so r2v was the likelier victim. Applies to video i2v, video r2v and image i2i alike. The fake could not model any of this -- it closed the picker unconditionally on an option click, so the commit assertion tested the fake, not Flow. It now models a picker that stays open, one with no confirm, one that ignores its own confirm, and a library that lists an upload under the name it was uploaded with. It also raises the REAL Playwright TimeoutError on waits: with the stand-in, a narrowed except silently tests nothing. Also adds docs/MCP.md section 6 "Troubleshooting a failed tool call", routed from docs/INDEX.md -- cohort triage, an envelope -> cause -> action table, and how to recognise a pre-v0.73.2 blank incident bundle as no evidence at all. Verified: full offline suite (4248 passed, 92%), ruff, pyright strict, 5 doc and hygiene gates, SonarCloud green. Live $0 e2e on a migrated account: the happy path passed, and a forced-grace run drove the confirm branch itself against real Flow (confirm_clicked=False -- the picker had already closed, is_visible() correctly skipped the click, the chip bound, which is the measurement the no-op safety claim needed). Eleven-dimension council review; every finding addressed or answered. Not verified, named external blocker: the confirm CLICK has never executed against live Flow, because no picker on this cohort stays open. Covered offline across all three outcomes and A/B-controlled; the ffroliva#792 reporter is the verifier. The r2v half of the naming fix is offline-only -- it shares the upload leg the i2v e2e exercised live, but its own attach was not re-run. Refs ffroliva#792
…ffroliva#801) The canary had been RED with an identical failure set since 2026-09-11, and every run posted the same comment ending "Canary gates nothing. Triage at your convenience." A standing condition -- a capability Flow took away from this account, already understood -- looked exactly like fresh drift, which is the thing the canary exists to catch. The set being UNCHANGED was the informative part, and nothing reported it. RED is not demoted. The module docstring is emphatic that labelling a real regression "ignore me" is how the signal dies, and ffroliva#560's council review already caught one such demotion. This only adds what a single run cannot say: continuity(previous, failing) -> (state, note) a pure function beside `classify`, fingerprinting the sorted failing set (order-insensitive -- pytest promises no stable order and a reshuffle is not new). Same set as last time -> "Unchanged for N consecutive runs", and say that a red nobody owns is how a canary trains red-blindness. Different set -> "The failing set changed since the last run", the arm worth looking at today. The note REPLACES the flat triage line rather than sitting beside it: a report saying both "at your convenience" and "broken for four nights" is arguing with itself, and the reader believes the softer half. Only RED and GREEN update the streak. AUTH-EXPIRED and DEFERRED learned nothing about Flow, so one night with Chrome open must not erase a week-old red. State lives in tmp/canary-state.json (gitignored, disposable). Every read and write degrades to "no continuity known" rather than taking the nightly down -- a test caught int() raising on a hand-edited value, which would have done exactly that. Verified by running the real script twice, dry-run, $0: run 1 -> RED, first-ever fingerprint, no streak claimed (correct) run 2 -> RED, {"fingerprint": "7ac1eca0bf29", "runs": 2}, "Unchanged for 2 consecutive runs." Those runs also produced the finding the feature exists to surface: today's set is NOT the nightly's. The four credits/aisandbox failures are identical -- the standing condition (ffroliva#795, this account losing the ya29 Bearer around 2026-09-11) -- while the two agent-mode transport tests now pass and a different test has joined. A stable core with flapping edges, which is exactly what the report could not say. Gates: ruff, ruff format, pyright strict, 4 doc/hygiene checks, 4261 passed, 92%. Refs ffroliva#559
…froliva#802) stop re-deriving a credits auth verdict through a browser (ffroliva#802) `gflow credits` on a migrated account told users their SAPISID cookie was broken and to re-run `gflow auth login`. Every clause was wrong, and the advice is harmful: on a profile with no browser-strategy marker a failed FIRST login rolls that marker back -- the ffroliva#791 spiral. v0.73.2 fixed the remediation for the raise site where labs mints no token. It could not help the cohort that gets a token and has aisandbox-pa reject it, because that message was never coming from a credits raise site at all: services/credits.py::_fetch caught its own accurate verdict in a bare `except Exception` and re-derived it through Chrome, failing inside the SHARED, route-blind _run_with_aisandbox_retry. The good message was being computed and then discarded. The browser is no longer consulted once aisandbox-pa has ANSWERED -- it asks the same endpoint for the same Bearer and gets the same refusal. The other site (labs 200, no token) KEEPS its browser rescue: httpx sends labs.google cookies only while the browser carries the full jar and bootstraps a real navigation, which can renew a session httpx cannot. If that rescue fails too, the fast path's diagnosis survives rather than the route-blind default. Council D1 falsified the first commit's blanket re-raise; D14 reached the same place independently. `credits list` now reports the remediation per profile -- it rendered only the class title, so the multi-profile surface and gflow_get_credits(all_profiles=true) were the one door that still could not say why. Measured live on a migrated profile at $0: `credits user` ~7 s -> 2 s with no browser launch; across 9 saved profiles `credits list` drops from 9 Chrome launches to 6 (3 skip it, 4 keep their rescue, 2 fail unrelated); 9 of 9 rows now carry a remediation where none did. Two e2e tests, cohort-agnostic by construction, both run live on the migrated cohort and both took the failure branch. The pre-existing happy-path e2e tests stay RED -- that is the real capability loss (ffroliva#559), not test rot. Nine council dimensions resolved; CodeRabbit's two findings fixed. `credits` itself remains unavailable on the migrated host, so ffroliva#795 stays open; the class default still reaches every non-credits aisandbox route, filed as ffroliva#803. Refs ffroliva#795, ffroliva#791
… drift (ffroliva#804) name the agent-only composer instead of calling it selector drift (ffroliva#804) Google has put some accounts on a flow.google.com composer with NO classic arm: the only prompt box is the agent panel, and aspect/model/count are Agent-settings defaults rather than per-request controls. Every control the migrated driver reaches for is structurally absent, so gflow waited out the 30 s readiness gate and raised UiSelectorDriftError -- exit 23, which reads as OUR frontend bug and invites a retry that cannot work. @Cstanish127 reported it with the DOM capture that made it diagnosable. It now raises FlowAgentUiError: exit 25, retryable=False, $0, pre-submit. THE DISCRIMINATOR IS THE CHIP. The DOM is identical to ffroliva#749's recoverable agent mode -- settings trigger present under a bare `hidden` -- which is why it read as drift. What separates them is that a recoverable account still has a button.agent-mode-chip to turn off and this one has none, so AGENT_MODE_CHIP_ANY exists alongside AGENT_MODE_CHIP (pressed-only). A trigger that has LEFT the DOM stays drift: that is a renamed selector, our bug. MEASURED, not assumed. The fix rests on whether Playwright reports <button hidden=""> as present-but-invisible; a fake answers whatever it was written to answer, so tests/api/transports/test_agent_only_composer.py measures it against real Chromium on the reporter's markup. Two negative controls were then run against LIVE Flow at $0: healthy migrated composer (ffroliva, real project page) trigger visible, agent-mode-chip present and UN-PRESSED -> False live /about landing (denon82, ffroliva#756) trigger absent -> False The first corrected a claim in this PR's own comment. An un-pressed chip is not "never observed" -- it is the NORMAL state of a healthy migrated composer, which is the point: a working account on this host HAS a chip, so having none is genuinely anomalous. The state never observed is an un-pressed chip beside a HIDDEN trigger. `retryable` moved from FlowAppError to the error base, under the condition FlowAppError's own comment set ("move it up if, and only if, a second class needs it"), then narrowed in documentation: six subclasses with their own __init__ reject it loudly rather than being threaded through for a case no raise site has. A test pins that split so the note cannot rot. Eight council dimensions plus CodeRabbit, all resolved. D4 proved the fail-closed branch was uncovered (migrated_composer.py 80% -> 93%); D9 found two prose contradictions that survived a clean rebase; D15 moved the 23->25 note to ### Changed and corrected "exit 25" to "exit-25-equivalent"; D14 de-duplicated a third copy of the Chromium fixture; D1 found FlowApiError's legacy branch eating `retryable=`. NAMED BLOCKER: that Flow still SERVES this DOM is unverified -- no account available to us is in that cohort. ffroliva#799 stays open for that and for the agent-panel driver, which is not implemented. Refs ffroliva#799
…from host membership (ffroliva#807) measure both Flow domains, and stop asserting capability from host membership (ffroliva#807) Two spikes and the vocabulary change they force. No behaviour changes: the source edits are error strings, docstrings and one env-var description. SPIKE 1 -- labs vs flow.google.com, protocol by protocol. A sweep of all 31 prior spike notes found ZERO mentions of gRPC, gRPC-Web, raw protobuf, WebSockets, SSE or the negotiated HTTP version. Never ruled out; never looked for. 3 accounts x 2 entry points x 2 runs: * labs.google/fx/tools/flow returns HTTP 308 PERMANENT REDIRECT, 6/6 visits -- server-side, and a different mechanism from the client-side /about redirect. * The graduated app's Angular root is `aisandbox-root`, the same name as aisandbox-pa.googleapis.com. One product lineage, two frontends. * No WebSocket, SSE, gRPC or raw protobuf on Flow's hosts. The one application/json+protobuf per run is OneGoogle's account bar. STOMP is moot -- it rides on a WebSocket and there is none. * HTTP/3 (QUIC) dominates. Never recorded, and invisible through Playwright. SPIKE 2 -- is there a push channel during an ACTIVE generation? No. Zero WebSocket events, zero streaming, across the full submit->completion lifecycle at zero Veo credits. The mechanism is neither polling nor push: ogiZ0b is dispatched ONCE and answered 19.687s / 19.841s later with the finished images in the reply. The instrument had to be fixed mid-spike to see it -- the first version recorded response ARRIVAL only, which cannot tell "sent late" from "held open for 20s". THE VOCABULARY. "X is labs-only" and "on a migrated account, X fails" use host membership to predict capability. Host membership is uniform across the surveyed profiles where capability is not, and no account here can reach labs to test a labs-only claim -- making it unfalsifiable, not merely unproven. Corrected wherever it was a live claim, including two strings people actually read: credits.py told USERS the endpoint was labs-only (aisandbox is shared), mcp/tools.py told AGENTS a capability was labs-only (gflow has not ported it), and migrated_composer.py blamed Flow for a model missing from our own matcher table. AGENTS.md carries the rule so it is enforced rather than swept once. NOT changed: "migrated host" as a NAME, the identifiers FlowHostMigratedError / GFLOW_CLI_FLOW_HOST / migrated_composer.py, released CHANGELOG entries and dated spike notes, and the skills that QUOTE "labs-only" as the phrase to avoid. THREE AUDITS, and the sharpest findings were against this branch's own work: a listener leak that corrupted the saved evidence (fixed, all evidence regenerated); a pre-registered "unmeasured" verdict that the findings then failed to apply to their own data; two false claims introduced while removing false claims, one contradicting its own docstring five lines above; and an over-broad rule that could have read as "delete the host branching". The WebSocket negative was A/B-validated by pointing the detector at a page that does open a socket, and the one blind-spot class (COOP: same-origin) was ruled out for both hosts. HONEST LIMITS: the video path is unmeasured for push, and spike 2 CORRECTS its own premise there -- images use a held response, video polls, so an absence on one is not an absence on the other. 3 of 9 profiles were surveyed (the three with a live Flow session). This is not "labs is gone": v0.67.0 drove a pt-locale account through the labs route, and migrated_route's labs arm is still reached routinely for reasons unrelated to host. Spike 2's script was committed BEFORE its data existed, which is the ordering gap spike 1 could not prove. Refs ffroliva#639, ffroliva#791
Bumps the github-actions group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `astral-sh/setup-uv` from 10.0.1 to 10.1.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@20cfd1b...bec219d) Updates `github/codeql-action/upload-sarif` from 4.37.9 to 4.38.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@cdf488f...b96794f) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…va#806) Bumps the python-minor-patch group with 5 updates: | Package | From | To | | --- | --- | --- | | [platformdirs](https://github.com/tox-dev/platformdirs) | `4.11.7` | `4.11.8` | | [mcp](https://github.com/modelcontextprotocol/python-sdk) | `2.1.1` | `2.2.0` | | [sse-starlette](https://github.com/sysid/sse-starlette) | `3.4.10` | `3.4.11` | | [ruff](https://github.com/astral-sh/ruff) | `0.16.6` | `0.16.7` | | [pyright](https://github.com/RobertCraigie/pyright-python) | `1.1.411` | `1.1.414` | Updates `platformdirs` from 4.11.7 to 4.11.8 - [Release notes](https://github.com/tox-dev/platformdirs/releases) - [Changelog](https://github.com/tox-dev/platformdirs/blob/main/docs/changelog.rst) - [Commits](tox-dev/platformdirs@4.11.7...4.11.8) Updates `mcp` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](modelcontextprotocol/python-sdk@v2.1.1...v2.2.0) Updates `sse-starlette` from 3.4.10 to 3.4.11 - [Release notes](https://github.com/sysid/sse-starlette/releases) - [Commits](sysid/sse-starlette@v3.4.10...v3.4.11) Updates `ruff` from 0.16.6 to 0.16.7 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](astral-sh/ruff@0.16.6...0.16.7) Updates `pyright` from 1.1.411 to 1.1.414 - [Release notes](https://github.com/RobertCraigie/pyright-python/releases) - [Commits](RobertCraigie/pyright-python@v1.1.411...v1.1.414) --- updated-dependencies: - dependency-name: platformdirs dependency-version: 4.11.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-minor-patch - dependency-name: mcp dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-minor-patch - dependency-name: sse-starlette dependency-version: 3.4.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-minor-patch - dependency-name: ruff dependency-version: 0.16.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-minor-patch - dependency-name: pyright dependency-version: 1.1.414 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…s client timer (ffroliva#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.
…ing link (ffroliva#809) README gains a Support section with one-click sponsor links, a Gold logo row at the top and a hall of fame; docs/SPONSORS.md lists every tier (mirrored to the website). scripts/ci/update_sponsors.py regenerates both blocks from GitHub Sponsors, rendering only public sponsorships with escaped, validated data; .github/workflows/sponsors.yml runs it daily and commits only the three generated pages to develop. pyproject.toml adds the Funding URL. No CLI or MCP surface changes.
It had been sitting untracked AND un-ignored in the repo root -- the state one `git add -A` turns into an accidental commit. Tracking it deliberately closes that; ignoring it was the other valid answer. The file is gflow-cli-specific (`wing: gflow-cli`, 11 rooms, 39 exclude patterns) and the miner reads it from THIS project's root, so it is this repo's own config rather than a sibling-repo fixture. Its comments carry measured tuning, not preferences: source and generated data are deliberately not mined because memory should hold what the repo cannot give back -- code is already here at full fidelity and greppable, and chunking it severs function bodies from their call sites (.py alone was 12,797 of 19,951 drawers here, 64%). The `diary` room carries no keywords on purpose: log/notes/session hijacked USAGE.md into 180 drawers. Two other repository names appeared in those comments as supporting evidence. This repo is public and neither name appeared anywhere else in it, so they are described positionally instead; every figure and every lesson is unchanged. Five exclude patterns are inert against the miner's current readable-extension set and are kept deliberately, with a note saying why, so the next reviewer does not re-derive it.
chore(release): v0.74.0
…alance not found
Two zero-credit runs of the pre-registered probe. denon82 landed on /about
(unmeasured). ci-probe: the composer settings pane states the declared
cost before submit ('Generating will use 0 credits' for an image model);
no price on the wire (HTrJv is a model catalog, tRARke the tools gallery);
no balance in page, aria or watched responses; the labs credits route
returns 401. A cap on declared price is buildable; a balance comparison
stays blocked on ffroliva#795. Refs ffroliva#795
…t and spiked Umbrella plan from the 2026-09-14 ecosystem review. Five-persona predict review took it from STOP to CAUTION (6/10): the Pillow fix gains a pre-flight so a missing chain extra fails before a paid clip, WS2 is redesigned around Flow's declared per-model price, onboarding waits for the ffroliva#791 login fix, and WS10 adds a Claude Code plugin marketplace plus a distribution catalog. WS2 and WS4 decisions are backed by spikes.
Council review of ffroliva#812 ran 8 dimensions (D0 mechanical CI, D1-D5, D9, D13, D14). Consensus was YELLOW with 22 must-fix items; all are applied here. Spike script — three of these invalidate how the write-up should be read: - No `finally:` anywhere. The capture was written after the `async with build_client(...)` closed, so any raise inside lost every response and DOM read collected. Wrapped the body in try/finally that gathers pending recorders, screenshots and writes the capture while the page is still alive. - `except Exception: body = ""` made an unreadable body indistinguishable in the capture from one carrying no credit data -- which is exactly the negative sections 2 and 3 report. Now records `body_error` (class name only) and prints a count of unreadable bodies. - Only the route *count* reached stdout, so the rpcid inventory in section 2 was re-derivable only from a gitignored capture that is not retained. Now prints every route id with its max size. - `argparse` had no `description=__doc__`, so `--help` printed nothing. - `[contenteditable="true"]` was re-typed rather than imported from `migrated_composer.COMPOSER`. The try/finally is exercised, not just reviewed: fault injection (goto raises, no browser) confirms the capture lands on the failure path with pre-failure state intact and the screenshot taken before teardown. Spike doc: - Sections 2 and 3 marked provisional -- their numbers came from the instrument before the fixes above. - Section 2 downgraded to UNMEASURED for a paid model: the script reads only the first 12 model-token neighbourhoods per body, and the pre-registered test required "integers matching the pane's number" while the pane read 0, so its antecedent never held. - N=1 stated up front; every conclusion scoped to the profiles we can test. - The labs 401 attributed to these profiles' credential state, not to the route -- host membership does not predict capability. - Script cited as a markdown link so check_doc_links can see it; Evidence filename corrected to the name the script actually writes; Run and Design header lines added to match sibling spikes. Plan: - WS1 rewritten smaller. `cli_video.py:942` imports `chain` as the first statement of `_run_chain`, so PIL already fails before any spend -- only the error type is wrong. Making `av` eager and wrapping that one import beats a lazy PIL plus a find_spec pre-flight that rebuilds the earliness it gave away, and drops a test that would have passed without the fix. - Task 1.3 loses the pkgutil walk; the `.[chain]` import line is the gate. - Locale-invariance added to the per-PR contract, both halves: the spike asked for a structural anchor AND a locale-independent number parse, and WS2 carried only the anchor. - WS2 drops the per-process cumulative budget and exit 39; WS3 uses the existing `_cli_helpers` validator, not a third copy of the project-id regex. - WS10 tasked out, and now also curates `.codex-plugin`, which ships all 18 skills -- release, check, pr-council-review included -- to Codex users today. - WS9 dropped rather than deferred; WS0 and WS10 added to the definition of done; private repo name and a private-analytics ranking removed. Refs ffroliva#795
`gflow serve` accepted a shared secret via GFLOW_CLI_DAEMON_TOKEN / GFLOW_DAEMON_TOKEN and refused to bind a non-loopback address without one, but the token was never checked on a request. It gated startup only, so a no-token, wrong-token and correct-token client were all served identically. `git log -S` confirms no release has ever enforced it. Fix: `build_app()` builds the Starlette app for either transport and, when a token is configured, wraps it in a pure-ASGI Bearer middleware. Missing header, wrong scheme or wrong token -> 401 with `WWW-Authenticate: Bearer`, compared with hmac.compare_digest. Both `run_http` and `run_sse` go through it, so the tests cover the real wiring rather than a middleware in isolation. Three details that are load-bearing: - Auth is added LAST, so it is the outermost layer: an unauthenticated caller gets 401 and cannot probe the transport-security configuration behind it. - `host` is forwarded to the SDK's app builders. The SDK auto-enables DNS-rebinding protection only when it sees a loopback host, so dropping the argument would have silently disabled protection that works today (measured: bad Host -> 421, bad Origin -> 403 on a loopback bind). - The app is rebuilt per call; its session manager is single-use. Verified as an A/B against released 0.74.0, same probe, loopback, --no-spend: BEFORE no token 200/200 (13 tools) wrong 200/200 right 200/200 AFTER no token 401/401 (0 tools) wrong 401/401 right 200/200 19 new tests in tests/mcp/test_serve_auth.py; `tests/mcp/` 212 passed. Two tests in test_server.py asserted the exact call this replaces (`run_streamable_http_async` / `run_sse_async`) -- their intent (path, host, port, stateless_http untouched) is kept, re-pointed at the new seam, because the old assertion pinned the vulnerable wiring. docs/MCP.md and docs/CONFIGURATION.md described the token as bind-gating only, which is no longer true.
…-migrated docs(plan)+spike: community-feedback uplift plan, credits on flow.google.com
…yload `/plugin marketplace add ffroliva/gflow-cli` then `/plugin install gflow@gflow-cli` installs the gflow-cli and video-production skills and registers the MCP server in one step. The bug this also fixes: `.codex-plugin/plugin.json` declared `"skills": "./skills/"` and `.agents/plugins/marketplace.json` declared `path: "./"`, so both shipped all EIGHTEEN skills -- release, check, pr-council-review, sonar, doc-review, issue-resolve -- to every Codex and ChatGPT-desktop user. Those drive this repo's own development lifecycle; an agent that reads release/SKILL.md will try to cut a release of whatever project it is pointed at. All three channels now point at one curated payload. The payload is generated, never hand-edited: scripts/ci/generate_plugin_skills.py copies the two user-facing skills out of skills/ and rewrites only the links that climb out of the repo (../../KNOWN_ISSUES.md -> an absolute URL). Intra-skill and sibling-skill links still resolve under the plugin's own layout and are left alone. `--check` runs in CI, the same contract generate_website_docs.py already provides for website/docs/. Spend consent. Claude Code starts a plugin's MCP servers automatically when the plugin is enabled -- there is no separate prompt for the server. This one drives the user's own Google account and Veo video bills their credits, so the plugin ships `defaultEnabled: false` with a required acknowledgement. Verified: install reports "This plugin is disabled by default" plus "1 userConfig option not yet set (1 required)". Live-verified in a clean CLAUDE_CONFIG_DIR: marketplace add -> install -> enable -> 9 files, 2 skills, 0 maintainer-only skills, mcpServers wired to `gflow mcp run`. `claude plugin validate` passes for both manifests. That install run found a real bug in the generator: it deleted stray files but left their empty directories, so --check went green while skills/release/ sat in the installed tree. Directories now count as strays; A/B'd both ways. Two pre-existing assertions in tests/scripts/test_codex_plugin.py pinned the old behaviour (`skills == "./skills/"`, `path == "./"`). They were asserting the bug, so they are updated with the reason inline.
…paid clip
`uvx --from "gflow-cli[chain]==0.74.0" gflow video chain one.jsonl --dry-run`
exits 1 with a generic "Unexpected error ... file a bug". The extra installs av
but not Pillow, and media.py imports PIL at module level.
Three changes:
- pillow>=12.3.0 joins the chain extra. It was declared only in the dev group,
which is why this was never seen in a checkout.
- `import av` moves to module level in media.py, beside `from PIL import Image`,
so both optional deps fail at the same point. It was lazy inside
_decode_frame, which runs only AFTER a link completes -- so a missing av
failed after the user had already paid for link 0.
- The import block at cli_video.py:942 -- the first statement of _run_chain,
ahead of the manifest parse, --dry-run, the confirm prompt and any client --
is wrapped in try/except ImportError and re-raised as the existing
FrameExtractionError (exit 20), naming the extra and both packages.
Rejected: a lazy PIL import plus a find_spec pre-flight. It makes the import
late, then adds a second mechanism to restore the earliness it gave away,
carrying a package tuple that can drift from the extra it mirrors.
A SECOND, INDEPENDENT BUG surfaced while making the red tests green.
`_cli_helpers._handle_gflow_error` interpolated detail and remediation_hint into
a Rich markup f-string unescaped. Rich reads `[chain]` as a style tag, finds no
such style, and silently drops it -- so every `install gflow-cli[chain]` hint
the CLI has ever printed rendered as `install gflow-cli`, advice that reinstalls
what the user already has. Verified directly: unescaped renders
"pip install 'gflow-cli'", escaped renders "pip install 'gflow-cli[chain]'".
--json was never affected, which is why nobody noticed. Fixed at both render
sites (_cli_helpers.py:310-312 and cli.py:316) rather than by rewording the new
message, so it covers every error class.
A/B against the released wheel, three arms:
control 0.74.0 + [chain], missing manifest -> exit 1, ModuleNotFoundError
arm A fixed tree + [chain] -> all three import; encoded a real mp4 and
extracted a frame (691-byte JPEG, magic verified)
arm B fixed tree, no extra -> exit 20, and `gflow-cli[chain]` survives to
the console, proving the escape fix in a real terminal
Two pre-existing tests asserted premises this invalidates, both explained in
their docstrings: test_media.py blessed the late `av` guard that fired after
billing, and test_errors.py pinned the old remediation string verbatim.
Full suite: 4330 passed, 7 skipped.
Closes ffroliva#813
Council review found the escape fix from the previous commit was HALF-APPLIED: it covered the two sites that rendered the new chain message and missed the rest, which is the [[review-findings-get-half-fixed]] shape this project keeps repeating. A bracket-bearing hint really exists on the missed path -- `api/_engine.py` tells the user to `uv pip install gflow-cli[patchright]`, and `cli.py` rendered that ConfigurationError detail unescaped. Proven on the real in-tree hint: UNESCAPED has '[patchright]': False ESCAPED has '[patchright]': True Escaped every site that interpolates error text into Rich markup, across _cli_helpers.py, cli.py, cli_movie.py, cli_run.py and cli_scene.py. Fixing sites one at a time is what let this recur, so the durable part is tests/test_rich_markup_safety.py: it walks the AST of every module under src/gflow_cli and fails on a console.print f-string that interpolates exception text without escape(). It carries its own negative control -- a test asserting the guard flags the bad shape and passes the good one -- so it cannot quietly become a no-op. The guard is scoped to error text (`exc`, `.detail`, `.remediation_hint`) rather than every interpolation. Any interpolated value can lose a `[...]`, but error text is the class that produces wrong INSTRUCTIONS, and it is where this project writes bracketed package names. Two rounds of tightening removed six false positives: `.title` matches a tool spec, a project record and a movie manifest, and `type(exc).__name__` is a class name that cannot carry a bracket. Widening the set later is cheap. 661 passed across tests/cli, test_errors, test_media, test_chain and the guard.
… bump site Council review of ffroliva#815 found three things worth correcting. The docs overclaimed the consent step. `acknowledgeBilling` has exactly one occurrence in the tree -- its own declaration -- and `claude plugin enable` succeeds without it being set, so calling it something the plugin "asks you to acknowledge before it runs" was not true. `defaultEnabled: false` is the control that actually holds; the userConfig field is a UI prompt. Both README and docs/MCP.md now say so, and point at `--no-spend` as the hard guarantee, since it never registers the credit-spending tools at all. `/plugin marketplace add ffroliva/gflow-cli` reads the repository's DEFAULT branch, and this project's default is `develop`. So a user installing the plugin tracks the integration branch while the manifest version and the rewritten doc links both name the last release -- three snapshots in one install. Documented rather than papered over; pinning the marketplace source to a tag is the eventual fix and needs the plugin to exist on `main` first. plugins/gflow/.claude-plugin/plugin.json is a fifth place the version is declared and nothing bumped it. RELEASE.md step 6, its `git add` line, and skills/release/SKILL.md now name it, so the release run has a documented bump site instead of discovering a red test. Also from the review: the byte-identity test restated the generator's link rewrite rule inline, a third copy that could drift while the test still passed -- it now calls the generator's own `render()` through a shared loader helper. Dropped `test_codex_version_tracks_pyproject`, a byte-identical duplicate of the assertion in tests/scripts/test_codex_plugin.py. 336 passed; ruff, hygiene, doc links, PII guard, website mirror, council memory and the plugin drift gate all green.
…a, PyPI polish
Three things, all about how people find and install this project.
1. docs/DISTRIBUTION.md -- every channel, with audience, how to submit,
requirements, status and a last-verified date per row. Every row was checked
live today, and the ones that could not be checked say UNVERIFIED and name
what blocked them. It records what the research actually found rather than
what we assumed:
- We are ALREADY listed on MCP Market and on skills.sh (21 installs), and
twice in Arnon-hs/open-source -- none of which anyone submitted.
- Arnon-hs states we are "available under the Python licence". We are MIT.
- printing-press-library does NOT list us; our one mention is under
"Sources & Inspiration" in a competing Go CLI's README.
- Smithery cannot list a PyPI-distributed server at all; PulseMCP has
submissions closed and auto-ingests from the official MCP Registry instead.
- skills.sh indexes skills/ directly, so it still surfaces the
maintainer-only skills the plugin work curates out of the other channels.
2. server.json for the official MCP Registry, plus tests/test_server_json.py
pinning version lockstep, the schema's 100-char description cap, the
one-slash name pattern, the mcp-name ownership token in the README, and that
the command it advertises actually exists.
That last test exists because of a blocker that would have shipped a broken
listing: the registry builds `uvx <identifier>` from the PyPI identifier and
has no field for a differently-named executable, but [project.scripts]
defined only `gflow` and `flow`. Measured against 0.74.0, `uvx gflow-cli mcp
run` fails with uv's own "Use `uvx --from gflow-cli <EXECUTABLE-NAME>`
instead". A `gflow-cli` console script is added.
check_repo_hygiene's version gate now covers server.json's two version
fields, tolerating absence so a fork without one is not failed.
3. PyPI metadata. The summary described only image-to-video and never mentioned
MCP; added the Documentation/Repository/Changelog sidebar links and ten trove
classifiers, each checked against the official 895-entry list.
Also drops "unofficial" as a label from the summary, README, docs site,
llms.txt, ROADMAP, the skills and DISCLAIMER.md. The substance is unchanged and
still prominent: the README warning block reads "alpha and reverse-engineered --
not affiliated with Google", and DISCLAIMER.md still opens "not affiliated with,
endorsed by, sponsored by, or otherwise connected to Google LLC" -- only the
word "unofficial" left its first sentence. It was leading with a negative in the
one line PyPI shows in search results.
Refs ffroliva#601
…version A package manager replaces files in place, so an interrupted upgrade leaves a venv whose metadata reads perfectly and whose imports are dead. `gflow update` only ever re-read the version, so it called that state "still 0.69.0" and sent the user to a plain `uv tool upgrade` — which cannot repair it: it reads the same intact metadata, finds it current and changes nothing. Measured on Windows (ffroliva#848): an aborted native-dependency swap left every command dying on `AttributeError: module 'greenlet' has no attribute 'greenlet'`, and only `uv tool install "gflow-cli==<version>" --force --reinstall` completed. After the manager runs, import gflow-cli in a fresh isolated interpreter before looking at any version. `-I` so a `gflow_cli` directory in the caller's CWD cannot answer for the venv; a probe that could not run at all is never reported as breakage. Refs ffroliva#848
….com The auto-close detector watches the labs.google session endpoint, and for a migrated account labs hands off without ever minting a session — so the one oracle it had could not answer. The window stayed open for the full 600 s while the banner promised gflow would close it, and the timeout then propagated past `_verify_and_record`, so a sign-in that had completed perfectly was discarded unread and the user got exit 12 for a login that worked. Two changes, and neither moves the authentication decision into a cookie. Stop signal: when the labs oracle answers GOOGLE_SESSION_ONLY and the jar carries both halves of a migrated session, stop waiting. That ends a wait which had no other way to end; `verify_flow_profile` still asks a server about what landed on disk and is still the only thing that can call a login successful. Gated on `raise_on_close=False`, which already means "this caller owns that fallback oracle" — `--browser internal` has none and keeps waiting as before. Reached only past `_is_safe_to_probe_session`, so a user on Google's password screen never gets there and their window is never closed out from under them. Timeout: read the profile before failing. A sign-in the detector missed is reported as the success it is; otherwise the timeout's own wording stands, because it is the message that explains the wait. This reverses a contract the suite pinned as "verification must not run on timeout" — that described the control flow rather than requiring it, and ffroliva#849 is what it cost. The poll now returns the status that ended the wait rather than a bare email: a session can be proven without a label, and `str | None` made "no email" and "window closed" indistinguishable, so the caller skipped both the notice and the cookie-flush wait for a login that had just succeeded. Closes ffroliva#849
`[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}` has no nested quantifier, so this is not
catastrophic backtracking — it is the plainer problem that `findall` retries
from every start position and each start rescans its run before failing to find
an `@`. Measured: 5 000 chars 0.12 s, 10 000 0.48 s, 20 000 2.00 s, 40 000
12.07 s. Clean 4x per doubling, so 200 KB would be minutes.
It is reachable, not theoretical. The class accepts `_`, `-`, `.` and `+`,
which is the entire URL-safe base64 alphabet, and the body being scanned is a
1.28 MB myaccount.google.com response full of exactly those blobs. The scan
runs synchronously inside an `async def`, so a stall blocks the event loop and
`CancelledError` cannot land until it returns.
Anchoring on the literal `@` lets CPython's `re` use its literal-prefix fast
search, and the local part is then read backwards over a bounded 64-character
window — the RFC 5321 cap, which is also what keeps each `@` constant-cost.
Not fixed with a leading `\b`, which is the tempting one-character answer: the
class also matches `-`, so a base64 blob still has a word boundary roughly
every sixteenth character. That is a 16x constant on a curve that stays
quadratic.
Same output for every address shape, including the Workspace and custom domains
ffroliva#791 turned on. 40 000 chars now scan in under a millisecond, 440 000
base64-shaped ones in 0.2 ms.
Closes ffroliva#852
…ts-broken-install fix(update): detect a half-replaced install instead of reporting its version
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
SonarCloud python:S8786 on `[\w.+-]+\Z`, and it is right: that pattern is super-linear over whatever it is searched in. The 64-character window bounded it in practice, but neither a scanner nor a reader can see that from the pattern — and "it is fine because of a constant three lines down" is exactly the reasoning that rots. Python's `\w` is documented as "`str.isalnum()` or underscore", so the scan reproduces `[\w.+-]` character for character, is O(1) per character, and is visibly so. Fuzzing the replacement against the pattern it replaces turned up a real difference this had introduced, unrelated to Sonar: `findall` resumed at the end of its previous match, so a local part could never reach back into one, while an `@`-anchored scan could. Two `@` within the 64-character window is the only shape where it shows — 2 differences in 4 000 random bodies, now 0 in 20 000 with the previous match's end as a floor, and pinned by a test. Refs ffroliva#852
…in-autoclose fix(auth): close the sign-in window for an account served flow.google.com
# Conflicts: # CHANGELOG.md # src/gflow_cli/auth/verification.py
…-scan fix(auth): make the address scan linear in the migrated-host probe
ffroliva#848's three bad states came from an MCP server this repo tells people to install. An editor with gflow registered starts one `gflow mcp run` per session; they are long-lived and usually plural, and on Windows they hold files inside the install open, so a package manager can abort part-way and leave the environment broken. The advice to stop those processes already existed — in the remediation `gflow update` prints once the damage is done. Saying it in the upgrade section is the difference between a recovery step and not needing one. Also states why `gflow update` cannot pre-flight this itself: it is one of those processes. `structlog` imports `greenlet` unconditionally, so every `gflow` maps the venv's native modules, and refusing while any gflow holds the install would refuse every run. Mechanism reported with a reproduction by Cstanish127 on ffroliva#824. Closes ffroliva#848
`configure_logging` renders TEXT logs with `structlog.dev.ConsoleRenderer(colors=True)`, and structlog's Windows `_init_terminal` raises `SystemError` outright when colorama is missing. The call sits in the Click group callback, so it fires before any subcommand body — a clean Windows install aborted every interactive command with a traceback that named structlog and never gflow, and the README's Windows quick-start failed as written. Nothing in the runtime closure supplied it: structlog keeps colorama an optional extra and rich ships its own Windows console handling. The only colorama edge in uv.lock came from pytest, a DEV dependency, so every developer machine and every CI job had it transitively and no gate could see the gap. Declaring the dependency keeps the coloured output the call site intends, rather than degrading to colors=False. Piped stderr escaped the crash (AUTO then resolves to the JSON renderer), which is why non-interactive and CI use never tripped over it. Reproduced on a clean Python 3.13 venv against develop, then verified by reinstalling the fix into that same venv: the `gflow doctor` run that raised SystemError now completes, on both the CLI and the MCP stdio server. A behavioural test cannot catch the absence (pytest supplies colorama), so tests/test_observability.py asserts the declaration. Closes ffroliva#846
…dows-dependency fix(deps): declare colorama on Windows so a clean install can run (ffroliva#846)
chore(release): v0.77.1
…e holder is installed Council review of ffroliva#856 converged on three findings. Three dimensions independently flagged that the new "if an upgrade does abort" paragraph restated the exit-11 / forced-reinstall paragraph fifteen lines above it, with less precision — the original also names the repair command and why a plain `upgrade` cannot do it. Replaced with a back-reference, so the mechanism is stated once and cannot drift. The block also contradicted the paragraph immediately before it: that one says a running `gflow.exe` holding its own launcher is benign and the upgrade lands anyway, this one said a running gflow leaves the environment broken. Same-sounding phenomenon, opposite guidance, adjacent. Named the distinguisher — the launcher you are running versus a different gflow holding the install's contents. `tasklist` matches image names only, so it finds the `gflow.exe` shim but not a server registered as `uv run gflow mcp run`; said so rather than implying parity with `pgrep -f`, which is now `-fa` so the command line that matched is visible before anything is stopped. Softened two frequency claims ("people actually hit", "usually more than one") that a single reproduction does not support, per the claims discipline in AGENTS.md, and pointed the citation at the comment carrying that reproduction instead of the issue body, which still carries the refuted chrome.exe attribution. docs/MCP.md is the page that installs the lock holder and never said registering it gates upgrades; one note at the end of § 4 now covers every client listed there. USAGE.md's own instruction landed 67 lines into the section, so the section head now carries a one-line pointer.
…olders docs(update): name the lock holder before the upgrade, not after
…gle.com (ffroliva#831) Ports `--end-frame` (start+end interpolation i2v) to the migrated host. Refs ffroliva#639. Contributed by @stgmt, who found that the timeout was happening AFTER a successful server-side submit — the difference between a broken feature and one that bills you while reporting failure — and traced it to rpc `nprQif`. Verified on a second cohort (ffroliva/flow.google.com, 2026-09-17) at $0 via route-abort: rpcid and both media ids confirmed; model key is cohort-dependent (`omni_flash_i2v_8s_first_last` here vs `veo_3_1_interpolation_lite` there), so matching is on key shape. Findings in docs/superpowers/spikes/2026-09-17-migrated-end-frame-submit-contract.md. Maintainer follow-up: dropped adoption-by-content (dead for the veo key, and able to hard-fail an already-billed run), armed the billing guard for t2v, fixed a spike script that could not run, added the missing attach_end_frame coverage and a zero-credit e2e, de-vacuumed the BDD scenarios (ablation-verified), stopped an existing e2e from spending a credit and failing on a labs-only event name, and corrected seven doc sites. Offline suite 4489 passed; new e2e passes live.
…uses (ffroliva#868) Closes ffroliva#561. Supersedes ffroliva#862 and ffroliva#863 (thanks @iceblue03). Refs ffroliva#639.
…hored match (ffroliva#860) (ffroliva#866) `_pick_frame_by_name` matched a picker option with `_exact(name)` -- `^\s*<name>\s*$`. A picker tile is a `mat-icon` ligature followed by the file name, and a locator reads both nodes as one string, so the option offered for `hero-ab12cd34.png` reads `imagehero-ab12cd34.png`. The anchors could never hold: every i2v run on this cohort ended in exit 32, "the frame picker lists no asset named ...", while the picker had been listing it the whole time. The reported listing is what pinned it -- the `image` prefix is the icon, not a rename. Root cause at migrated_composer.py:1980, and only there. The other three `_exact` callers (a `mat-icon` ligature, two aspect-ratio radios) match a node's own text and are left alone. `_mention_by_name` never matches text at all -- it types `@name`, presses Enter and counts chips -- so the r2v mention misses seen on the same account are a different defect and stay open. The match is now containment on the display name, which `_unique_display_name` makes run-unique (ffroliva#792), so it still cannot bind a stale copy of the same file. Formalized as e2e, not a mocked proxy: a fake locator returns the list the test wrote into it and matches the anchored pattern happily, so the mocked version of this test passes against the bug. `tests/features/frame_picker_binding.feature` serves the tile through route interception ($0, no Google, no profile, no credits) and real Chromium reports `imagehero-ab12cd34.png` -- the same shape as the `images1-cbbc098b.jpg` the issue was filed with. Red before green, measured both ways with the final test file: fix stashed -> 2 failed, 1 passed (62.22s; the miss is 3/3 searches) fix restored -> 3 passed (5.49s) The third scenario is the A/B control: an asset the picker genuinely does not list is still refused with exit 32, so containment is not "bind anything".
…nly on load (ffroliva#859) (ffroliva#867) A `role="dialog"` promo panel over a freshly loaded editor made `image t2i` fail as exit 23 -- ".settings-trigger-button did not accept a click within 5000 ms -- it is covered by div.cdk-overlay-backdrop" -- with a remediation telling the user to file a selector-drift bug, when one click on the page fixes it permanently and nothing had drifted. The issue was filed saying the dismissal was ordered AFTER the click. It is not: `ensure_editor` calls `_dismiss_dialog` at :804, before it waits for the trigger. It calls it one frame after `domcontentloaded`, before Angular has rendered anything, so `is_visible()` answers False and the method returns having done nothing. That is why a blocked run logs NEITHER `migrated.dialog_dismissed` nor `migrated.dialog_not_dismissed` -- the silence in the incident's event stream is the evidence, and the "wrong position" reading does not explain it. The dismissal now also runs in `_open_pane`, where the image and video paths take their FIRST click and where the `glue` consent bar is already cleared for the same reason. It stays best-effort: a modal that refuses to go falls through to the click post-mortem, which names the backdrop, rather than becoming a second competing error path. Formalized as e2e because the whole defect is WHEN the DOM has the dialog in it. A mocked page is whatever the test constructed before the test ran; it has no "later", so the broken ordering and the fixed one look identical to it. `tests/features/blocking_dialog_dismissal.feature` mounts the modal 250 ms after load through route interception ($0, no Google, no profile, no credits). Red before green: fix stashed -> 2 failed, 2 passed (22.64s) fix restored -> 4 passed (12.68s) The two that flip are the dismissals. The other two hold both ways on purpose: a modal that ignores its close button is still named rather than swallowed, and an editor with NO modal records no gesture at all -- the control that matters here, because `_dismiss_dialog` falls back to Escape, and an Escape pressed on a healthy page would close the settings pane the next line opens.
…rom the CLI (ffroliva#861) (ffroliva#869) * feat(docs): `gflow docs` — list, print and search the documentation from the CLI (ffroliva#861) 126 pages in `docs/` and nothing in the CLI pointed at any of them, so at the moment of use the knowledge was unreachable and the reader guessed. The failure that filed this issue was `video r2v --duration 10` against a host that offers reference-to-video at 8 s only -- a rule that was already written down and never found. gflow docs every topic, with what it covers gflow docs usage one page, raw Markdown (it pipes) gflow docs --search "r2v duration" the line that answers the question Read-only and offline: no network, no account, no credits, no database. THE PAGES SHIP IN THE WHEEL. The user this was filed for installed from PyPI and has no checkout, so a command that could only print GitHub links would not solve it. `hatch_build.py` globs `docs/*.md` into `gflow_cli/_docs` (+1.3 MB). The two-line alternative -- `force-include` plus `exclude` -- was built and inspected first: hatchling ignores exclusions for forced inclusions, and 148 files it was told to exclude shipped anyway, 4.3 MB of plans, spikes and PNGs. The glob is also the list, so the topics cannot drift as pages are added. SEARCH RETURNS A LINE, NOT A FILE NAME. The rule above lives inside a 4 000-character bullet in `MCP.md`; answering "it is in MCP.md" leaves the reader where they started. Hits carry `docs/FILE.md:LINE` and the line windowed around the match. Query terms are ANDed, curated `INDEX.md` shortcut rows rank above raw body text, and a vague query reports how many hits it held back. Ranking's limit is recorded rather than papered over: `--search duration` alone has 133 hits and three orderings were measured, including one that reused `INDEX.md`'s routing as a reference-vs-record signal. That one is false -- `INDEX.md` links 93 of the 126 pages -- and was removed rather than kept as a knob that does nothing. `_score`'s docstring says so, so it is not reinvented. A topic resolves from its slug, file name, either case, or an unambiguous prefix, and NEVER by joining user input onto a path -- the argument is a key looked up in the enumerated page set, so `gflow docs ../../etc/passwd` is an ordinary unknown-topic refusal (exit 11, an existing class; no new exception, no new exit code). Every printed string round-trips through the console's own encoding, which is the ffroliva#846 shape from one release ago. No MCP twin yet, registered in `_MCP_EXEMPT` so the decision is enforced rather than claimed in a docstring: documentation is a resources provider, not a tool. Every decision therefore lives in `docs_catalog`, Click-free and returning data, which is the upgrade path. Scenario + plan: docs/superpowers/plans/2026-09-17-gflow-docs-command/. Tests: 40 offline, plus an integration test that builds a wheel, installs it in a scratch venv and runs the command with the repository nowhere in sight -- the only one that can tell "it works here" from "it works for the user who filed it". * fix(ci): allowlist hatch_build.py as a canonical root file (ffroliva#861) The repo-hygiene gate flags any tracked top-level *.py outside ROOT_DOC_ALLOWLIST as a stray artefact. `hatch_build.py` is not stray: hatchling resolves a build hook's `path` from the project root and defaults to that exact name, so it cannot live under scripts/ without the config pointing back out to the root anyway. Same category as the already-allowlisted `conftest.py` — a root Python file the tooling requires there. Caught by the council's D0 mechanical gate, which only turns red once the new files are tracked; the pre-commit run before `git add` was green and would have shipped this to CI. * fix(docs): apply the council review's findings to `gflow docs` (ffroliva#861) Six review dimensions ran against bfc0afc. Security found nothing exploitable (19 hostile topic names, zero reads). The rest found real defects, all fixed here. CORRECTNESS - `_plain()` stripped single `_`, so every curated search hit printed a file name that does not exist -- `REFERENCE_STRATEGIES.md` came back as `REFERENCESTRATEGIES.md`, in the rows that rank FIRST, and 106 of the 126 pages carry an underscore. Only `**`, `__` and backticks are stripped now. - `_read()` passed `errors=` to `read_text`, which `Traversable.read_text` does not accept -- a TypeError on the one path that matters, a page read out of a zipimport, uncaught by `except (OSError, ValueError)`. Every test passed because an editable checkout exercises the *fallback* branch. Now reads bytes and decodes. - The heading bonus fired on `#` shell comments inside fenced code blocks. That is what put eight LIVE_VERIFICATION records on top of `--search duration` -- the noise the ranking was added to remove. Fence tracking, four lines. - `--search ""` defeated the topic/search mutual exclusion: the guard tested truthiness while the dispatch tested `is not None`, so the topic was silently discarded. Both test `is not None` now. - `_snippet` windowed on the first term typed; it now centres on the least frequent one, which is the selective half of a two-word query. - The search table's title reported the SHOWN count while the footer reported the total -- "20 match(es)" above "and 116 more". - Every refusal inherited ConfigurationError's default remediation: "Run `gflow config list-transports`", advice about a transport registry printed at someone who asked for a page. - An unknown topic now uses `difflib.get_close_matches`, so `usge` is offered `usage`. USAGE.md already promised this; a substring scan could not do it. BUILD - The hook refuses to build when it finds no pages. `Path.glob` on a missing directory returns empty, and a sdist with `docs/` removed produced a valid 767 KB wheel carrying zero pages, exit 0 -- and `release.yml` is `uv build` then publish with nothing in between that would notice. - The hook now skips editable installs, which were copying 126 pages into a `site-packages/gflow_cli/` that has no `__init__.py`, is never read, and goes stale on the next doc edit. TESTS - The cp1252 test proved nothing: a mutation replacing every `_console_safe(` call site with `str(` left 33/33 green, because the test called the helper directly and never ran the command. Replaced with a subprocess run under PYTHONIOENCODING=cp1252 across three invocations -- and `_console_safe` is gone, replaced by one `sys.stdout.reconfigure(errors="replace")` that also covers Rich's own box glyphs, which the helper never did. - Added: hostile names driven through the CLI (not just `resolve()`), the NUL case, `docs ""`, `--search ""`, the remediation text, the typo suggestion, and the code-fence scoring. - Loosened the acceptance assertion from "MCP.md is first" to "MCP.md is in the top three": a heading scores +10, so a future page could take first place without the feature being any worse, and an ordinary docs edit should not red the build. - Corrected this file's own claim that the wheel test is opt-in. `integration` is not in `addopts`' exclusion list, so it runs on every plain pytest and on PR CI -- which is the right outcome and was worth stating accurately. DOCS - `gflow docs` was missing from every place the command surface is enumerated: AGENTS.md, llms.txt, skills/gflow-cli/SKILL.md, and the synopsis block of the very USAGE.md section this feature added. - Two numbers in that section were wrong (133 lines, five hits) and one promise was false until difflib landed. - CHANGELOG said +1.3 MB; measured, the wheel goes 0.74 -> 1.32 MB, so the cost is +0.58 MB compressed. NOT FIXED, ON PURPOSE - `A_B.md` and `A-B.md` would collide on one slug. Measured: zero collisions across all 126 pages. A guard for a condition that does not exist is a guard nobody can test. * fix(ci): regenerate the plugin skills mirror after the SKILL.md edit (ffroliva#861) CI's three `test` jobs failed in 11-14 seconds -- too fast to be tests. The cause was the "Plugin skills in sync" step, which runs BEFORE pytest: plugins/gflow/skills is stale -- regenerate with: python scripts/ci/generate_plugin_skills.py DRIFT: plugins/gflow/skills/gflow-cli/SKILL.md Adding the `gflow docs` line to `skills/gflow-cli/SKILL.md` left its plugin mirror behind. `website/docs` has the same shape and I did regenerate that one; this generator was simply not in the local gate I ran, so the drift reached CI.
Seven version sites, CHANGELOG migrated, PROJECT_STATUS rewritten, and the live-verification ledger extended to cover the three PRs that landed after this release branch was first cut. The branch sat at 3bf8ad5 with its prep already written. ffroliva#866, ffroliva#867 and ffroliva#869 merged into develop after that, so develop was merged in and their CHANGELOG entries moved under [0.78.0] rather than left under a heading that no longer exists. Per step 12 of the release skill, newcomers each need their own 4b row: - ffroliva#860 (Frames picker binding) is VERIFIED against live Flow. It is the one item in this release that could not be proven credit-free -- the only evidence that a start frame binds is a real generation -- so clips were generated and downloaded on a migrated account, every run logging migrated.frame_uploaded -> frame_bound -> submit_observed -> download. frame_bound is the event that could not fire before this release. - ffroliva#859 (promo modal ordering) is recorded as NOT OBSERVED. Its dismissal path ran on every one of those runs and no modal ever appeared, so nothing here proves the ordering fix; the $0 route-intercepted e2e is what covers it. Recorded rather than folded into ffroliva#860's green. - ffroliva#861 (gflow docs) is verified from the built artefact, which is its whole contract: 127 topics, docs/FILE.md:LINE search hits, no network, no checkout. PROJECT_STATUS and the INDEX live-verified row both still claimed the release was verified at ZERO credits. That was true of the release as cut and is not true of the release being tagged; both are corrected rather than left flattering. Gates: ruff, ruff format, pyright, repo hygiene (1159 files), doc links (196 files), website PII, website mirror, plugin skills, check_release_artifacts -- all green. pytest 4567 passed, 7 skipped, coverage 92.47%. NOT done, said out loud: the shipped-planning-artifact consolidation in step 10. docs/superpowers carries ~100 plan/spec/spike files from many past releases, so a this-release-only sweep would be inconsistent, and hatch_build.py cites plans/2026-09-17-gflow-docs-command/SCENARIO.md as the record of a measurement.
chore(release): v0.78.0
127 upstream commits (v0.73.2 .. v0.78.0) onto the fork's 27. Conflicts resolved: - errors.py: FlowHostMigratedError text lists both upstream's end frames and the fork's character references. - EXIT CODE CLASH: upstream released 39 as FlowAccessUnavailableError; the fork had given 39 to AvatarUnavailableError. The Avatar gate moves to 40 (code, CLI help, tests, USAGE, KNOWN_ISSUES, AGENTS.md; CHANGELOG ### Changed). - client.generate_video: the fork's Avatar pre-flight, then upstream's create-project-when-missing (ffroliva#864). - migrated_composer: upstream's start+end frame docstrings with the fork's entity-chip comments; submit observer keeps the fork's reply-shape log and upstream's frame-rpcid recording. - r2v --duration keeps the fork's rule (refuse 4s/6s, allow 10s on omni-flash; measured 2026-09-12, newer than upstream's 2026-09-06). - cli.py: `escape` is now module-level upstream; `datetime` stays local. - CHANGELOG: fork items stay under [Unreleased], upstream releases follow. Follow-ups the merge required: - test_likeness_eligibility: pass a project_id; since ffroliva#864 a missing one is created first, which that guard is not about. - plugins/gflow/skills regenerated from skills/. Gates: ruff, format, pyright, doc links, website mirror, hygiene, PII, council memory; full offline suite 4703 passed after the two fixes above.
A profile whose cached deadline has passed is re-minted at client start, which uses the one attempt per client; the test then cleared the cookie and expected a second. Reset the state so it measures the reactive path. Live: two auth.flow_session_refresh outcome=ok (startup + reactive), 1 passed.
…ot Enter The @ picker lists media and characters together, unranked, and the mention was committed with Enter - the FIRST option. Live 2026-09-18 on kittinansr2, `@tun` offered fourteen `tun_portrait-*.jpg` files before the character `Tun`, so every character run refused at the chip read-back (exit 32, no credits spent) and a character whose name any file shared was unusable. attach_character_entities now: `@` -> the picker's Characters tab (by its `accessibility_new` ligature, locale-invariant) -> search in the picker's own box (the tab click moves focus off the composer) -> click the option whose title matches exactly (or the sole option). A miss is closed with Escape, which also drops the `@` (measured), instead of Backspace, which could eat a chip. The entity kind + id read-back is unchanged. Spiked live before the change (free, no submit): the tab narrows `tun` to one option, clicking it commits `data-reference-type="entity"` with the right entity id, and the `@` is consumed on success and on Escape. Live after: `video t2v --model veo-lite-lp --reference-entity 42a8618b... --reference-entity-name tun` -> exit 0, 8.0 s 720x1280 h264, character on screen. Docs: USAGE (Characters tab; no `@name` in the prompt - exit 29 on migrated), KNOWN_ISSUES (character create/list broken by the retired labs RPCs, measured the same day), CHANGELOG.
…InitialData is retired `@Name` in a prompt and `gflow character list` both read the project's characters from labs `flow.projectInitialData`, which on migrated accounts now answers 404 "Flow RPCs have been deprecated and disabled" (measured 2026-09-18). Every prompt carrying an `@` stopped at exit 29 before submitting; `character list` at exit 7. Reported by the owner running `video t2v "@tun ..." --model veo-lite-lp` on kittinansr2-reel. On that 404 (and only that - other labs failures still surface), list_characters now reads the flow.google.com app's own project load: a temporary page opens /project/<id> and keeps the `Zzl0ze` batchexecute reply. Its payload holds one row per character, located by shape, not position: [project_id, entity_id, null, [1, name, [[[wf]..],[[wf]..], personality], true], thumbnail_media_id, [w,h], created, updated] Found by a free network spike of a project open (no submit), then parsed against the real captured body before any code was wired. Live after the fix: - `character list --profile kittinansr2-reel` -> exit 0, Tun, 3 workflow ids - the owner's exact `video t2v "@tun ..."` -> exit 0, `mention_resolved` -> `character_entities_attached`, 8.0 s 720x1280 h264 + aac, Tun on screen `character create` stays labs-only and broken on these accounts (KNOWN_ISSUES). The earlier USAGE line "do not write @name in the prompt" is replaced - it is no longer true.
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
Brings upstream
ffroliva/gflow-cliv0.78.0 (origin/main, 127 commits, v0.73.2 → v0.78.0) into the fork's 27 commits. Merge-base was7095dccf(2026-09-11).Conflict decisions
AvatarUnavailableError39 → 40. Upstream released 39 asFlowAccessUnavailableError(account has no Flow entitlement); the fork had independently used 39 for the Avatar gate. Updated in code, CLI help, tests, USAGE, KNOWN_ISSUES, AGENTS.md; CHANGELOG### Changedflags it for scripts.--durationkeeps the fork's rule — refuse 4s/6s, allow 10s onomni-flash(measured 2026-09-12, newer than upstream's 2026-09-06 blanket "8s only").client.generate_video: fork's Avatar pre-flight, then upstream's create-project-when-missing (Bundle: no-project generation on flow.google.com (createProject 401 as expired login) + MCP profile contention ffroliva/gflow-cli#864).migrated_composer: upstream start+end frame support and docstrings + the fork's character-entity chip comments; submit observer keeps both the fork'smigrated.submit_reply_shapelog and upstream's frame-rpcid recording.errors.py/ KNOWN_ISSUES / USAGE: capability text merged (end frames + character references + Avatar; 3:4 image aspect).[Unreleased], upstream releases follow unchanged.Follow-ups the merge required
test_likeness_eligibility: pass aproject_id— since Bundle: no-project generation on flow.google.com (createProject 401 as expired login) + MCP profile contention ffroliva/gflow-cli#864 a missing one is created first, which that guard is not about.plugins/gflow/skillsregenerated fromskills/.tests/e2e/test_auto_relogin_e2e.py: reset the one relogin attempt the startup revive (from feat(auth): re-mint an expired Flow session without a click #2) already spends on a lapsed profile.Verification
tests/e2e/test_auto_relogin_e2e.py— 1 passed,auth.flow_session_refresh outcome=oktwice (startup revive + reactive)MCP
No new surface from the conflict resolution. Upstream's MCP changes come in as released; the exit-code move reaches MCP through
EXIT_CODE_MAP.