Skip to content

chore(release): v0.78.0 - #870

Merged
ffroliva merged 9 commits into
mainfrom
chore/release-v0.78.0
Sep 17, 2026
Merged

ffroliva merged 9 commits into
mainfrom
chore/release-v0.78.0

Conversation

@ffroliva

Copy link
Copy Markdown
Owner

Brings the v0.78.0 bump commit and the full integration history onto main.
The tag is already pushed and Release completed success — PyPI, the GitHub Release
and the MCP Registry publish are all live. This PR is the main catch-up: merge commit,
never squashed.

What shipped

Verification

docs/LIVE_VERIFICATION_v0.78.0.md, with the honest split:

PROJECT_STATUS.md and the INDEX.md live-verified row both still claimed this release was verified at ZERO credits — true of the release as first cut, not of the release being tagged. Both corrected.

Gates

ruff · ruff format · pyright · repo hygiene (1160 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 step-10 consolidation of shipped planning artifacts. 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.

ffroliva and others added 9 commits September 16, 2026 22:40
#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 #824.

Closes #848
…e holder is installed

Council review of #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.
docs(update): name the lock holder before the upgrade, not after
…gle.com (#831)

Ports `--end-frame` (start+end interpolation i2v) to the migrated host. Refs #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.
…hored match (#860) (#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 (#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 (#859) (#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 (#861) (#869)

* feat(docs): `gflow docs` — list, print and search the documentation from the CLI (#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 #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 (#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` (#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 (#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. #866, #867 and #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:

- #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.
- #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 #860's green.
- #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.
@coderabbitai

coderabbitai Bot commented Sep 17, 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: 0e6a8090-f0f2-4c61-bb3c-9afe32678aa6

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.

@ffroliva
ffroliva merged commit 7c396af into main Sep 17, 2026
18 checks passed
@ffroliva
ffroliva deleted the chore/release-v0.78.0 branch September 17, 2026 20:28

This branch was previously deployed

1 inactive deployment
pypi — 78ca2078 Deployed Sep 17, 2026 by ffroliva via build-and-publish #104
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.

2 participants