Skip to content

chore(release): v0.76.0 - #838

Merged
ffroliva merged 33 commits into
mainfrom
chore/release-v0.76.0
Sep 16, 2026
Merged

ffroliva merged 33 commits into
mainfrom
chore/release-v0.76.0

Conversation

@ffroliva

@ffroliva ffroliva commented Sep 16, 2026

Copy link
Copy Markdown
Owner

v0.76.0 — alpha

Three surfaces stop lying to you, and the last unshipped distribution channel starts publishing itself.

#833 An account with no Flow access gets its own exit 39 instead of UiSelectorDriftError (exit 23) — gflow blaming its own selectors and inviting a bug report
#830 Containerised sign-in works on Windows (WSLg X11 socket) and no longer points at a VNC service that does not exist; the image pins its own version by ARG
#628 An MCP agent's project_name is finally consumed — the worker read a key nothing in the repo has ever written
#829 / #834 The Official MCP Registry publishes itself on release via OIDC; mcpservers.org listing live

Live verification

docs/LIVE_VERIFICATION_v0.76.0.md$0, reads only.

The decisive arm is #833's negative control, run first-hand rather than relayed:

profile     unavailable_screen   aisandbox_root   settled url
ffroliva    0                    1                https://flow.google.com/about
denon82     0                    1                https://flow.google.com/about

The guard does not fire on accounts that have Flow access, and aisandbox_root = 1 makes that zero a real absence rather than a page that never booted. Both accounts were in the /about state — the state where a presence/absence signal would be mimicked — and this one was not, because it reads a component that exists only on the unavailable screen.

Recorded as NOT verified, with reasons: the Docker sign-in needs a WSL-integrated Docker host; the MCP Registry publish cannot be observed until this tag reaches main, since both triggers resolve the workflow from the default branch.

#791 is not in this release

A fix passed eight council dimensions and was then withdrawn. It read the rendered DOM and claimed the signal was server-attested; #756 refutes that — the /about hop is decided client-side with zero requests to Flow, and "the backend grants access while the frontend declines to open it." The probe would have reported authenticated users as logged out, reintroducing #791 as its own cure. PR #835 stays a draft; the HTTP findings and plumbing are reusable.

Nothing in the changelog over-promises without it — #833's entry explicitly records the auth/REST rows as still unfixed.

Gates at this tree

ruff · ruff format --check · repo hygiene · doc links · website PII · website mirror --check · council memory · release artifacts · uv lock --checkall pass.

Wheel builds as 0.76.0: 138 entries, zero ZIP duplicates, console script present.

Note for the next cut

skills/release/SKILL.md step 6 lists four version sites. There are six — it omits server.json (x2) and uv.lock, and only the hygiene gate caught them. That list wants fixing separately rather than as a rider here.

The Glama build spec lived only in a web form on glama.ai, so nothing in this
repository described it and nothing could check it. Two builds failed with

    could not start the proxy Error: spawn gflow ENOENT

The image built fine. `uv sync` installs the project into /app/.venv and puts
console scripts in /app/.venv/bin, which is never added to PATH, so a CMD
naming a bare `gflow` misses and the container exits before answering a ping.
A failing container is listed but excluded from Glama search results, which
also blocks the awesome-mcp-servers listing that gates on a Glama score.

glama.json is now the tracked copy of that spec, and tests/test_glama_build_spec.py
guards it the way test_server_json.py and test_plugin_manifests.py guard the
other distribution artifacts: the spawned executable must be a console script
this project defines, it must be an absolute path into the uv venv rather than
a bare name, a build step must create that venv, and the subcommand must match
every other artifact.

Written test-first, and A/B-controlled against the spec exactly as it failed:
with `gflow` the venv-path test fails with the real cause, with the corrected
path all five pass.

Verified end to end on Glama build 01a0a3c1-c0a3-7462-bb4b-350a715ca63b —
success in 31.2s, answering initialize plus tools/list with 15 tools, 2 prompts
and 3 resources, no browser and no credentials.

No docs change here on purpose: docs/DISTRIBUTION.md is rewritten on the pending
v0.75.0 release branch, so its Glama row is updated there rather than conflicting.
Review finding on #819: _server_argv split on `--` without ever checking the
prefix, so a spec that dropped or swapped mcp-proxy would pass every test.

The proxy is load-bearing. Glama pings the container over HTTP while gflow
speaks stdio; mcp-proxy is the bridge, and the successful build logged
`starting server on port 8080` from it. Without it the container would start
gflow and still fail the check because nothing is listening — the same shape
as the ENOENT bug: a container that runs but never answers.

A/B-controlled by swapping the proxy for another name, leaving the rest of the
spec intact: exactly one test fails, with its own message, and the other five
still pass.
… gate see them

Adds the two dimensions of distribution that nothing described.

docs/MARKETPLACES.md — where gflow-cli comes from, and what each channel
actually delivers. The channels differ more than they look: PyPI gives the CLI,
the Claude Code plugin gives skills plus the MCP server but not the CLI, the
Codex plugin gives skills only, the MCP Registry gives metadata, and the Glama
container gives a server that introspects but cannot generate. PyPI is named as
the single authoritative source.

docs/CONTAINER.md — the container dimension. What glama.json is, what the
automatically-built image provably does (initialize + tools/list, 15 tools, no
browser, no credentials) and provably cannot do (anything that reaches Flow),
plus the recorded reasons there is no Docker Hub image and no Docker MCP
Catalog entry: their CONTRIBUTING requires shareable test credentials, and a
container cannot hold a logged-in Chrome session. Written as a deliberate "not
yet" with the condition that would change it.

Charters are kept disjoint on purpose: DISTRIBUTION.md owns submission status,
MARKETPLACES.md owns what a channel delivers, MCP.md keeps the plugin consent
explanation. Status rots weekly; what a channel delivers does not.

Neither page is mirrored to the website. The mirror only rewrites links for
root-sourced files, so publishing one while its siblings stay unpublished would
ship broken site links.

check_doc_links.py now globs docs/*.md. This was found by control, not by
reading: a deliberately broken link in the new page passed a green gate. FILES
listed 22 of 122 top-level docs, so 100 -- every LIVE_VERIFICATION_* record
among them -- were unaudited while the gate reported success. Coverage goes from
64 files to 186. That surfaced 9 genuinely broken links, now fixed by unlinking
targets nobody can follow (absolute file:// paths that only resolved on the
author's machine; specs/plans since consolidated into memory) while keeping the
evidence text verbatim. Nothing is grandfathered, and the glob means a new page
is covered without anyone remembering a list.

Verified by A/B: the same control link passes the old gate and fails the new one
with exit 1.
Several cross-platform instructions were shown only in PowerShell, so a reader
on Linux or macOS met a block they could not run and reasonably concluded the
project expects Windows. The CI matrix says otherwise: ProfileLease runs on
ubuntu, macos and windows, and the test matrix runs three Python versions.

Fixed, POSIX-first with the PowerShell difference noted inline:

- AGENTS.md — the Impeccable Routine, the contributor gate list, was a
  ```powershell block whose only Windows-specific line was the env var. All
  nine gates are unchanged and none was trimmed.
- docs/DEBUGGING.md — the incident-bundle grader was `.venv/Scripts/python.exe`
  inside a ```bash fence, which is wrong on both counts; now `uv run python`.
  The --verbose filter and the image-gen repro gain POSIX forms alongside the
  PowerShell ones.
- docs/MCP.md — the Claude Desktop and Cursor examples hardcoded the
  maintainer's own dev path; now /path/to/gflow-cli.
- AGENTS.md, docs/MARKETPLACES.md — `codex plugin ...` is shell-agnostic and
  was fenced as powershell.
- docs/AGENT_UI_E2E.md — the FORCE_AGENT_UI repro.

Deliberately left alone, because they are genuinely Windows and already say so:
DEBUGGING's "Windows console encoding" section (cp1252, and it already tells
Bash/WSL users to skip it), the gflow.exe launcher file-lock in USAGE and
PROJECT_STATUS, the chrome.exe/python.exe process scans in KNOWN_ISSUES, and
the .ps1 scripts (record_demo, register_task, agentic_image_e2e) which really
are PowerShell. USAGE.md and CONFIGURATION.md needed nothing — they already
lead with bash and label the PowerShell variant.
The comment said the canonical routing index is `docs/index.md`. It is
`docs/INDEX.md` — there is no lowercase file, and git records only the
uppercase one.

No behaviour change and no latent bug: "index.md" is in WEBSITE_ONLY, and
_source_for() returns on that membership check before it ever touches the
filesystem, so the casing never reached a lookup that a case-sensitive CI
runner would resolve differently from a Windows dev box. The comment was
simply wrong about a filename, which is the kind of thing someone later
trusts instead of checking.

Noted while auditing index coverage: every top-level doc except the 57
LIVE_VERIFICATION_* release records is referenced from docs/INDEX.md, and
no top-level doc carries YAML frontmatter (only superpowers/memory notes
and skills/*/SKILL.md do, where a loader consumes it).
feat(distribution): track the container build spec, document every channel, and close the link-gate blind spot
A fork PR whose CI is awaiting approval renders as fully green.

GitHub holds `pull_request` workflows from forks at conclusion
`action_required` until a maintainer clicks "Approve and run" — and such a run
does **not appear in the PR's statusCheckRollup at all**. Branch protection
evaluates that rollup, and so does every human glancing at the checks list, so
the PR presents as passing with zero test evidence behind it.

Measured on 2026-09-15 across three open fork PRs, identical labels, all three
cross-repository — so "it's a fork" is not the discriminator, the approval click
is:

    #781  CI conclusion=success          -> 16 checks shown, all green
    #793  CI conclusion=action_required  ->  2 checks shown, both green
    #787  no pull_request run at all     ->  2 checks shown, both green

#793 had already shipped broken tests and a ruff-format failure through exactly
this gap with nothing red anywhere.

check_fork_pr_ci.py reports only the states the rollup hides. An outright
failure is already visible and an in-progress run shows as pending, so neither
is flagged — a gate that cries wolf is a gate people learn to ignore. A passing
`External PR Triage` never counts as coverage either: it runs on
pull_request_target, which executes for forks regardless of approval, and
treating it as evidence would restate the bug.

Wired into pr-council-review step 0.6, whose existing fallback was blind here:
it inspects `gh pr checks`, which returns all-green for exactly these PRs.

Verified against the three real PRs it was built from: #781 exits 0, #793 and
#787 exit 1 with distinct reasons.
Two rows were honestly marked "only observable after upload". v0.75.0 is now on
PyPI, so they are closed with evidence rather than left as permanent question
marks — which is what the ledger asked for.

Row 6, PyPI metadata -> verified: version 0.75.0, the rewritten summary, 18
classifiers and the Documentation/Repository/Changelog/Homepage/Funding sidebar
links, read back from the PyPI JSON API.

Row 7, MCP Registry -> unblocked, not done. The ownership token in the
*published* README reads `io.github.ffroliva/gflow-cli` and matches server.json
exactly, which was the whole blocker. What remains is tooling, not this release:
mcp-publisher is not installed and its `login github` is an interactive
device-code flow. Also recorded: `mcp-publisher init` would OVERWRITE the
existing server.json — use `validate`.

DISTRIBUTION.md's Glama row said `submitted`, which stopped being true hours
later. It is listed, claimed and rated A, with a passing container build. The
section now also records what the channel actually is: Glama generates the
Dockerfile from a web form and clones the repo at the default branch's head, so
it never touches PyPI; two builds failed on `spawn gflow ENOENT` because
`uv sync` puts console scripts in /app/.venv/bin, which is never on PATH; and
the admin panel's percentage is profile completeness, not the public letter
grade that awesome-mcp-servers asks for.
fix(ci): catch fork PRs whose required CI never ran, and close the v0.75.0 ledger
…ect the claim

I wrote "the container cannot generate, because generation needs a signed-in
Chrome session" into docs/CONTAINER.md, issue #822 and a marketing draft. It
came from architecture, not from a run. The run contradicts the strong form.

Its evidence base was the Glama health-check image, which deliberately installs
no browser. "It cannot generate" was true of THAT image and was generalised into
a fact about containers — the exact failure skills/spike/SKILL.md exists to
prevent: evidence about the image, read as evidence about the feature.

Measured today, $0, no generation attempted:

  gflow --version in container            -> gflow, version 0.75.0
  /opt/google/chrome/chrome               -> present, channel stable
  is_playwright_chrome_channel_available  -> True
  is_chrome_available                     -> True
  real Chrome headed under Xvfb           -> 153.0.8010.36, real page title
  navigator.webdriver                     -> None
  headless UA marker                      -> False

gflow's OWN detector passes in the container, not merely Playwright's. And with
gflow's actual launch arguments the browser presents no automation markers —
G12 keys on navigator.webdriver, which is absent here. Xvfb is a real X server,
so Chrome runs genuinely headed; that is not Chrome's --headless mode, which is
what Google rejects.

Three of the spike's own failures are recorded in the note because each looked
like a finding and was not: xvfb without xauth; a probe that bypassed the
entrypoint so Chrome had no DISPLAY; and navigator.webdriver=True read from a
raw Playwright launch gflow never performs. The third would have confirmed the
prior belief.

Ships the protocol as runnable artifacts — docker/Dockerfile (real Chrome, not
Chromium, because browser_manager accepts exactly /opt/google/chrome/chrome) and
docker/docker-compose.yml with a named auth volume, so the container stays
disposable and the identity does not. `serve` refuses to start without
GFLOW_CLI_DAEMON_TOKEN by design; docker compose config fails loudly, which is
the intended posture and documented as such.

Still UNMEASURED and labelled as such, not inferred: the one-time interactive
Google sign-in inside a container, whether Flow accepts the session afterwards,
and whether a Windows-made profile survives the move to Linux. A clean
fingerprint is a necessary condition, not a sufficient one.
Follow-up to the spike. Two corrections to my own reasoning, both prompted by
review:

3.11 was chosen because it is the project's requires-python FLOOR. That is a
reason for the minimum supported version, not for a runtime image.

3.13 was then chosen because CI's matrix is ["3.11","3.12","3.13"] and 3.14
would be untested. That argument is weaker than it looks for a container: the
image pins its interpreter, so it is deterministic rather than an ambient
version that might surprise a user. The honest fix is to verify the image, not
to ship an older interpreter.

So: 3.14, with the contract measured rather than assumed.

  python           : 3.14.7
  gflow_cli        : 0.75.0
  chrome channel   : True
  chrome available : True
  initialize       : OK, server "gflow-cli"
  tools/list       : OK, 15 tools

Recorded in docker/README.md as the check to re-run after any base bump.
… pass in it

Runs gflow's own offline suite inside the container to settle the interpreter
question with a measurement instead of a preference. CI's matrix is
["3.11","3.12","3.13"], so this is the first time the suite has run on 3.14:

  4379 passed, 28 skipped, 138 deselected   in 368.59s (6m08s)

Performance baseline, same machine:

  runtime image (Chrome + Xvfb)          1.65 GB
  test image (repo + dev deps)            714 MB
  cold start -> gflow --version          1475 / 1516 / 1511 ms
  cold start -> initialize + tools/list  2228 / 2406 / 2495 ms
  uv sync during build                   14.8 s

The MCP figure is the one that matters for agents: ~2.3 s from cold container to
a usable tool list, paid once by a long-lived serve and per-call by docker run.

Two tests fail in that run and CANNOT pass in a container built from a git
WORKTREE. test_real_tree_passes_root_doc_check and
test_the_generator_ships_only_what_git_tracks both shell out to `git ls-files`
to introspect the repository. In a worktree `.git` is a 71-byte pointer file:

  gitdir: C:/development/github/gflow-cli/.git/worktrees/container-spike

Docker copies the pointer faithfully and it dangles inside the container:

  fatal: not a git repository: /app/C:/development/.../worktrees/container-spike

giving CalledProcessError exit 128 for both. This is the build context, not the
interpreter — they test repo state, not runtime behaviour.

Recording a wrong diagnosis too, because it cost a rebuild: I first attributed
this to `.git` being in my .dockerignore, fixed that, and the failures were
byte-identical. The dockerignore was never the mechanism. Naming a plausible
cause without measuring it is the same error this whole spike exists to correct.

Also moves the spike scaffolding out of the repo root into docker/ — an
untracked Dockerfile.test and .dockerignore at the root are exactly what the
hygiene gate polices, and dockerignore.example is the copy-to-root form.
docs(container): a container CAN host gflow — measured, and the claim corrected
…en question

Glama 0.75.0 is released from a build that logged cli_version 0.75.0, and
Auto-Release is on. Records what took a session to find: there is no write
API, the synced head can lag GitHub by hours, the pin field rejects unsynced
commits (so it stays empty), and Auto-Release has never fired, so whether it
builds the tag or a stale branch head is unverified until the next release.

Also marks punkpeye #14423's Glama gate as met and Composio #1905 as
ready-to-merge, and points CONTAINER.md at the released build.

Claude-Session: https://claude.ai/code/session_012VFJzGCdPcB22F6oqQonA4
docs(distribution): record the Glama release, Auto-Release and its open question
`mcp-publisher init` only writes a *template* server.json. This repo already
has a real one, committed and version-locked by tests/test_server_json.py, so
init refuses it with "Error: server.json already exists" and exits 1. That
aborted the documented `init && login && publish` one-liner at its very first
command, before login was ever reached.

Replaced init with validate in both places, and recorded why. validate checks
the committed file against the live registry and leaves it untouched.

Measured with mcp-publisher 1.8.1 on 2026-09-15:
  init     -> "Error: server.json already exists", exit 1, file byte-identical
  validate -> "server.json is valid", exit 0

The earlier note claiming init OVERWRITES server.json was wrong; it refuses.
The defect is the non-zero exit breaking the chain, not data loss.
…ence

docs(distribution): fix the MCP Registry publish sequence
tests/mcp/test_cli_parity.py enforces parity at the command and option level.
Neither can see the queue payload, where a key written under one name and read
under another type-checks, lints, passes every test, and does nothing at runtime.

The new gate extracts the payload keys mcp/tools.py writes (dict literals and
later subscript stores) and the keys anything under worker/ reads (.get(), [],
and "k" in payload), then fails on any written key the worker never reads. The
reader set is the whole worker package on purpose: codec.py builds the request
objects, but daemon.py consumes project_id, project_name, tool_specs and
output_file directly, so diffing tools.py against codec.py alone would report
four false positives.

Only that direction is an error. A key the worker reads but the MCP tools never
write is legitimate — the CLI serve path and worker/queue.py enqueue tasks too.

It failed on its first run, on a real defect:

  mcp/tools.py:875,954   write payload["project_name"]
  worker/daemon.py:193   read  payload["project_title"]

Nothing in the repository has ever written project_title — `git log -S` over all
history returns nothing — so create_project(title=...) always received the
hardcoded fallback "gflow-cli images". An agent calling gflow_generate_image with
project_name="Client pitch deck" got no error and a project named "gflow-cli
images". The parameter is documented on both generate tools as the title for a
freshly created project, and has been dead since it shipped on 2026-07-26: the
daemon's read landed four days earlier, and the --project-name feature wired up a
new key instead of the one already being read. The CLI was unaffected; it resolves
the title itself in cli_image.py.

The fix is the one-line read. The behavioural test asserts the created project
carries the agent's title, and was watched failing first:

  Expected: mock(title='Client pitch deck')
    Actual: mock(title='gflow-cli images')

The extractors carry their own tests against synthetic sources, including a
written-but-unread key and a misspelled reader, so the gate is proven able to go
red rather than merely observed green.

Verified: 279 passed across tests/worker and tests/mcp; ruff, ruff format,
pyright src (0 errors), hygiene, doc links, website mirror + PII, council memory.

Closes #628
test(mcp): gate the payload-key round trip, and fix the drop it found
The registry listing is the last unshipped distribution channel, and the one that
feeds the others: PulseMCP ingests it and GitHub's MCP gallery is built on it. It
was documented as a manual three-command sequence that nobody had run.

Ordering is why this hangs off `release: published` rather than the tag push.
mcp-publisher proves namespace ownership by reading the `mcp-name:` token out of
the PUBLISHED PyPI README, and PyPI metadata is frozen per release, so the wheel
has to be up first. release.yml publishes to PyPI and only then creates the
GitHub Release, so by the time this fires the token is live.

Auth is GitHub Actions OIDC. No personal access token is created, stored, or
handed to a third-party service — the runner exchanges its own short-lived
identity token, which is why the job takes `id-token: write` and nothing else.
The mcp-publisher download is pinned by version AND sha256: that job publishes
publicly under io.github.ffroliva while holding an OIDC write scope, so a moved
upstream tag must not be able to change the binary that does it.

workflow_dispatch covers the re-run case — a release whose PyPI upload succeeded
but whose registry publish did not. `validate` runs first on both paths, so a bad
server.json stops before anything is sent.

Not automated, deliberately: `init`. It only writes a template server.json,
refuses when a real one exists, and exits 1 (see #827).

Both triggers resolve the workflow from the default branch, so this goes live
when it reaches main at the next release. v0.75.0 itself still needs either one
manual publish or the arrival of v0.76.0; DISTRIBUTION.md says so rather than
implying the channel is already shipped.

Verified: yaml parses; `zizmor --offline --min-severity low` clean over the new
file and over .github/workflows/ as a whole; doc links, hygiene, website mirror
and PII gates green. mcp-publisher 1.8.1 `validate` already passes against the
live registry on this server.json.
Two defects in the compose stack, both mine from the container spike earlier today.

1. It pointed at a service that does not exist. The `login` comment told non-Linux
   users to "use the VNC service below instead". There is no VNC service — the file
   defines `login`, `gflow` and `serve`, and nothing in docker/ mentions VNC anywhere
   else. That is a false instruction shipped in the file people read first.

2. The X11 socket was hard-coded to /tmp/.X11-unix, which is the wrong path on the
   one non-Linux host that can actually run this. Measured on Windows 11 + WSL2
   (Ubuntu), 2026-09-15:

     /mnt/wslg/.X11-unix   ->  srwxrwxrwx ... X0     (live socket, owned by the user)
     /tmp/.X11-unix        ->  empty directory
     DISPLAY               ->  :0

   So WSLg does provide a real display, and the default mounted an empty directory
   next to it. Chrome then exits immediately against a display that is not there,
   which reads as "containers cannot do interactive login" rather than "wrong path".

The mount is now ${X11_SOCKET:-/tmp/.X11-unix}: unchanged for Linux, one variable for
WSL. Verified both resolutions with `docker compose config`:

  default            source: /tmp/.X11-unix
  X11_SOCKET set     source: /mnt/wslg/.X11-unix

docker/README.md gains a "Where the display comes from" section with the per-host
table and the two prerequisites that actually bite: run it from inside WSL (Git Bash
rewrites /mnt/wslg/... into C:/Program Files/Git/mnt/wslg/...), and switch on Docker's
WSL integration for that distro or the CLI inside WSL cannot reach the daemon at all.

Not fixed here, because it is not ours to fix: the Google sign-in itself still needs a
human at the keyboard. This change is about making sure that human gets a window.

Verified: compose config parses and both mount paths resolve; doc links, repo hygiene,
website mirror and PII gates green.
Follow-up on the same branch, from two questions worth answering with evidence.

1. The version pin had no gate.

`RUN pip install --no-cache-dir gflow-cli==0.75.0` was hardcoded and nothing tied it
to pyproject.toml. At v0.76.0 the image would keep installing 0.75.0 while
docker/README.md's "Verified on this image" table went on quoting the new version —
the same defect class as #628: one value in two places with nothing checking they
agree. tests/test_server_json.py already does this for server.json; this is the
container's equivalent.

The version is now ARG GFLOW_VERSION, gated by tests/test_dockerfile_version_pin.py.
The gate was A/B-controlled: set to 0.74.0 it fails naming both versions, restored it
passes. It also asserts the install step actually interpolates the ARG, because a pin
nothing reads is decoration.

2. Layer ordering, and a trap in it.

The order was already right — Chrome's apt layer before pip — so a version bump
reuses ~1.6 GB from cache and only the last layer rebuilds. But adding an ARG can
silently destroy that, and the instinctive placement (beside FROM) is the wrong one.

Measured on a throwaway image with a unique marker, so no stale cache entry could
match, and with a positive control confirming the version layer did rebuild in both
arms:

  ARG after  the expensive layer -> expensive layer CACHED
  ARG before the expensive layer -> expensive layer REBUILT

A first attempt at this A/B was contaminated: an earlier run had already cached the
target value, every layer reported CACHED, and the experiment discriminated nothing.
The positive control is what caught it. The measurement above is the clean rerun.

The ordering is asserted by the same test file, so a tidy-up that moves the ARG up
fails rather than quietly costing minutes per build.

3. Ownership of every input is now named.

Base image -> Dependabot, via a new docker ecosystem entry for /docker.
gflow-cli   -> ARG + the gate above.
Chrome      -> a rebuild, deliberately unpinned: Google's apt repo keeps only the
               current build so a pinned version stops resolving within weeks, and
               Dependabot cannot see packages installed by a RUN regardless. The
               consequence is now written down: that layer caches indefinitely, and
               --no-cache is what refreshes it.

Verified: 4 new tests pass and fail correctly under mutation; ruff, ruff format, doc
links, repo hygiene, website mirror, PII and council-memory gates green; dependabot.yml
parses with all four ecosystems.
fix(docker): the containerised sign-in could not work on Windows
feat(ci): publish to the Official MCP Registry on release, via OIDC
…it 39)

An account with no Flow subscription is served Flow's own "you don't have
access" screen instead of the app. gflow had no route for that state, so the
editor sweep reported the only thing it could see -- the settings trigger never
appeared -- and told the user Google had changed their frontend and to file a
bug, attaching an incident bundle holding a screenshot of their own signed-in
account page.

raise_if_known_landing() is already the single chokepoint all three "we landed
somewhere unexpected" raise sites route through, so the guard goes there: it
probes the DOM for flow-pinhole-unavailable-screen before it reads the URL and
raises FlowAccessUnavailableError (exit 39, not retryable), whose remediation
names the subscription and links Google's eligibility page.

Detection is by component because the spike refuted every cheaper signal: no
entitlement field on the wire across 21 responses, HTTP 200 with a client-side
hop so no 3xx to key on, and an unstable path (/unavailable and /u/8/unavailable
both observed, the second carrying Google's account-index segment). The
component is locale-invariant by construction; the screen's prose is not.

Verified live at $0 on a real unentitled account as an A/B through the same
command: probe neutered gives exit 23 "file a frontend bug", probe restored
gives exit 39. The MCP twin is run rather than reasoned about -- the new worker
test drives the real process_task and reads the persisted queue row back.

Scope, measured rather than assumed: the same account without --project never
reaches the editor. It takes the labs REST arm and dies at
project.createProject with HTTP 401, still reported as AuthExpiredError. That
row, and auth login / auth status, read an oracle that cannot discriminate an
abandoned sign-in from a missing entitlement, and on the login path Chrome has
already closed by the time the verdict runs. Left for its own change.

Refs #756, #773
…d act on the council

Eleven review dimensions ran against bf171b0. Three findings were defects in that
commit, not polish.

The labs arm was unguarded (D1). `_enter_editor` returns early when --project is
supplied, with no readiness gate, so the guard in its gallery arm never ran and the
failure surfaced from _switch_to_{image,video}_mode instead -- still exit 23, still
"file a bug". Both raise sites now consult raise_if_known_landing first, and a new
offline scenario covers them. A/B'd like the migrated arm: with the guard removed the
scenario fails with UiSelectorDriftError("... may be a new Flow UI layout this
gflow-cli version does not recognize yet"). It cannot be run live here -- every
profile we hold is served flow.google.com -- so the neutered control is the proof.

The CHANGELOG rewrite had deleted PR #828's shipped release note (D9). Git would have
taken that on merge with no conflict. Restored.

safe_page_url leaked Google's account-index segment into error text and, through
redact_error_detail, into the failed-operation row (D3). It is never diagnostic -- gflow
has no /u/N handling anywhere -- and an earlier draft of the test pinned the raw
ordinal in place. Collapsed to /u/N/; the test now pins its absence.

Also: the 47-line class docstring was a second copy of the CHANGELOG entry and carried
the over-claim D1 falsified, so it is 18 lines and a link to the spike (D14); the probe
lost 10 lines of scaffolding the try already covered and gained a log.debug so a dead
locator engine is visible (D14, D3); a replicated EXIT_CODE_MAP walk that no daemon
change could fail was deleted in favour of the test that drives the real process_task
(D14, D4); the explode stand-in now fails at `await count()`, where Playwright does,
and the retryable kwarg is asserted rather than inferred from class-default behaviour
(D4); the spike awaits Request.response() and redacts before truncating (D15, D3).

Docs: carve-out 5 recorded in the drift memory with the control-flow lesson; MCP.md
stops asserting one cause for "Signed in to Google, but not to the Flow app" and gains
an exit-39 envelope row; USER_GUIDE.md likewise; the spike names the command it
measured.

Refs #756, #773
mcpservers.org approved the submission on 2026-09-15 and the listing is now
live at /servers/ffroliva/gflow-cli. Adds the badge their approval mail
supplies, and corrects DISTRIBUTION.md, which still recorded the channel as
"submitted".

Verified rather than assumed, because the listing 403s to curl:
  badge.svg                         200, image/svg+xml
  /servers/ffroliva/gflow-cli       200, 115643 B, mentions gflow-cli
  /servers/ffroliva/<bogus>         404   (control — the 200 is not a catch-all)
The 403 is user-agent bot-blocking; with a browser UA the page serves.

Their follow-up pitches a paid sponsorship for placement on the site and in
awesome-mcp-servers. The free listing is what we took, and the badge does not
depend on it — recorded so the next person does not read the mail as a
prerequisite.
fix(transports): route Flow's unavailable screen to its own error (exit 39)
docs(distribution): mcpservers.org listing is live — badge + status
Three surfaces stop lying to you, and the last unshipped distribution channel
starts publishing itself.

- An account with no Flow access is told so, via its own exit 39, instead of
  being shown a selector-drift error and invited to file a bug (#833).
- The containerised sign-in works on Windows and no longer points at a service
  that does not exist; the image pins its own version (#830).
- An MCP agent's `project_name` is finally consumed — the worker had been
  reading a key nothing ever wrote (#628).
- The Official MCP Registry publishes itself on release via OIDC (#829), and
  mcpservers.org approved the listing (#834).

Version bumped in all six sites the hygiene gate checks: pyproject.toml,
src/gflow_cli/__init__.py, .codex-plugin/plugin.json,
plugins/gflow/.claude-plugin/plugin.json, server.json (x2) and uv.lock. Note
for the next cut: skills/release/SKILL.md step 6 lists only the first three
plus the plugin manifest — server.json and uv.lock are not in it, and only the
hygiene gate caught them. That list wants fixing separately.

Live verification: docs/LIVE_VERIFICATION_v0.76.0.md. $0, reads only. The
decisive arm is #833's NEGATIVE control, run first-hand rather than relayed —
`flow-pinhole-unavailable-screen` is 0 on both ffroliva and denon82 while
`aisandbox-root` is 1, so the guard does not fire on accounts that have Flow
access and the zero is a real absence rather than a page that never booted.
Both accounts were in the /about state, which is where a presence/absence
signal would have been mimicked, and this one was not.

Recorded as NOT verified, with reasons: the Docker sign-in needs a
WSL-integrated Docker host; the MCP Registry publish cannot be observed until
this tag reaches main, since both triggers resolve the workflow from the
default branch.

#791 is NOT in this release. A fix passed eight council dimensions and was then
withdrawn: it read the rendered DOM and claimed the signal was server-attested,
which #756 refutes — the /about hop is decided client-side with zero requests
to Flow, and the backend grants access while the frontend declines to open it.
The probe would have reported authenticated users as logged out. PR #835 stays
a draft.

Gates at this tree: ruff, ruff format, repo hygiene, doc links, website PII,
website mirror, council memory, release artifacts, uv lock --check — all pass.
Wheel builds as 0.76.0: 138 entries, zero ZIP duplicates, console script
present.
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 55141f9b-9a13-43a4-9ebe-cec828ebc55c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

CI caught what no checklist names: `docker/Dockerfile`'s `ARG GFLOW_VERSION`.
The gate that caught it shipped in #830, in this same release.

  tests/test_dockerfile_version_pin.py
  AssertionError: docker/Dockerfile installs gflow-cli==0.75.0 but this repo
  is at 0.76.0

That makes seven version sites across four disagreeing lists (#839): the four
in skills/release/SKILL.md step 6, plus server.json (x2) and uv.lock which only
check_repo_hygiene.py knows, plus this one which only its own test knows.

SonarCloud's red was downstream, not independent — "No report was found for
sonar.python.coverage.reportPaths using pattern coverage.xml", because the test
job died before writing one.

What was NOT bumped, deliberately:

- docker/README.md's "Verified on this image" block and its what-is-verified
  table quote MEASUREMENTS — `gflow_cli : 0.75.0`, `✅ measured — gflow,
  version 0.75.0`. The container was not re-measured for 0.76.0 (it needs a
  WSL-integrated Docker host, recorded as a named blocker in
  LIVE_VERIFICATION_v0.76.0.md), so editing those numbers to match would
  fabricate a run. A quoted measurement that tracks pyproject.toml is not a
  measurement. The staleness is now stated in the file instead, next to the
  numbers, with what would settle it.
  The test's own docstring describes exactly this failure — the table "quietly
  became a false claim" — so making it true by editing would have inverted the
  gate's intent while satisfying it.
- "as of v0.75.0" in docker-compose.yml, "Fixed in v0.75.0" in KNOWN_ISSUES,
  and the measured build output in docs/CONTAINER.md are all historical.

Also updated: DISTRIBUTION.md's Official MCP Registry row said "ready — submit
once v0.75.0 is on PyPI". #829 automates it and ships here, so the first run is
this release's own job.

Gates re-run green: hygiene, doc links, website PII, website mirror, council
memory, release artifacts, and the docker pin test (4 passed).
@ffroliva
ffroliva merged commit d98595a into main Sep 16, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant