← All posts

Vocion 2.88

Six minor versions and four patches in one day, on top of the morning's 2.82. A structural gate on outbound copy, the briefing rebuilt as a typed document around the decisions only a person can take, four surfaces on one list row and a Search you can read, record pages at full width with a rail that points instead of repeating, @artifact as a typed contract on the turn — and the release train's leftovers finally cleaned up, including an accessibility bug that had quietly renamed every decision button in the product.

Chris Fitkinvocion-v2.88.0

This is the fourth release notes post in two days and the second dated 2026-09-16. The others are Release notes — 2026-09-15, Release notes — 2026-09-15 (afternoon) and, from this morning, Vocion 2.82. Nothing here replaces anything in any of them.

Vocion 2.82 ended by warning that its tag carried a broken schema and that the one-line fix was on main but not in the release. That fix is v2.82.1, and it is the first thing in this post. Ten more tags followed it the same day.

The tags

Vocion core is at v2.88.0, cut from main at 6829a9bb. semantic-release cuts a version on every push, so this is ten tags rather than one:

TagWhat it carries
v2.82.1the schema fix Vocion 2.82 promised — build from this, not from v2.82.0
v2.82.2the accessibility fix below, a render-time ref write, the Storybook session fixture
v2.82.3the canvas removal finished, and two browser expectations the rail had moved
v2.82.4#367 — green CI on main, and the Snooze verb that vanished
v2.83.0#368 @artifact, #232 the Notion connector, #257 the docs pass
v2.84.0#339 — the pattern library and one list
v2.85.0#341 — who approved an action run
v2.86.0#370 — the voice gate
v2.87.0#371 — the briefing rebuilt
v2.88.0#372 — full-width record pages

And one more thing landed on main after the tag: #378, 29b90f44, the rail is the conversation, never a second copy of the page. It is written up here because it finishes the story #372 starts, and it is not in v2.88.0. It will be in the next tag.

Three lines of work, from three people. The product line — the deliverable contract, the connector, the pattern library, the voice gate, the briefing and the record-page work — is one. Recording whether an agent or a person approved a run is another engineer's. The accessibility fix, the render-time ref, the Storybook fixture and the browser-test repairs are a third's. As on every post here since this feed started, the write-ups are published under the team rather than under individual bylines.

What you can do now that you could not this morning

Feature releases

ChangePostSource
Outbound copy linted against workspace-authored voice rules at three seams; reviewer deletions propose new rulesOutbound copy has to pass your voice rules#370
The briefing as a typed document: nine sections, an attention budget in code, deltas joined against yesterday, no green status without evidenceThe briefing is a list of your decisions now#371
Search, Needs you, Artifacts and Learnings on one row; a Search page with a measured filter row and a document you can openOne list, and a Search you can read#339
Full-width record pages, an overlaying rail, an end to nested cards, and a rail that points at the page instead of repeating itRecord pages go full width#372, #378
@artifact as a typed contract on the turn, with a backstop that guarantees a document or an honest stubTag @artifact, and the turn owes you a document#368

Developer updates

SubjectPostSource
Why a voice guide produces the tells it bans, and what a twelve-line schema refinement does that a better prompt cannotThe prompt that produced the tells it forbade#370
A decorative <kbd> that joined every decision button's accessible name, and the exact-name test that was right all alongThe keyboard hint that renamed every button04796795, #367
Two failure shapes from one framework, a trace node stuck at "start", and a hydrate that rewrote every failure as a successA failed hand-off that never reached you#368

Also in this release

A Notion source connector (#232). Pages and databases, discovered through Notion's search endpoint and paginated to a cap, with page bodies read from the block tree to a bounded depth and a bounded request budget — a page that hits either bound is still emitted, marked as truncated, and reported as skipped content rather than silently shortened. A database contributes its title and description only; its rows are pages, and they arrive if they are shared too. An incremental sync sorts newest-first and stops at the watermark; a full sync sorts oldest-first so the tombstone pass sees everything.

Two details worth naming. The connector deliberately sends no object-type filter to the search endpoint and branches on the result instead, because that filter's accepted values changed meaning between API versions — the version header is configuration, and a connector that hard-codes a filter value is a connector that breaks on a date somebody else chose. And the client is built per call, inside the sync, from the credentials on the context: there is no module-level client and no cached headers, which is what makes two organisations syncing in sequence use two different keys. There is a test that asserts exactly that.

Credentials come from the encrypted per-organisation store or the sync refuses. There is no environment-variable fallback on this path — the fall-through-to-the-server's-key pattern exists for model and paid-tool providers, not for connectors.

No migration, no new environment variables, seventeen unit tests — and every one of them mocks the network. This has not been run against a live Notion workspace, and nothing here should be read as saying it has.

Who approved an action run (#341). A nullable boolean on the action run, written true when the trust ladder released it, false when a person approved, executed or rejected it, and null while it is undecided — a rejection is a decision and it is never an agent's. A re-proposed run clears all three of the decision fields rather than inheriting a stale one, and a person who retries or rejects something an agent auto-approved stamps their own answer outright.

It is queryable — the reviews API takes approvedByAgent=true|false|null and rejects anything else with a 400 — and the adoption overview counts auto-approvals off the decision timestamp rather than off the activity stream. The migration carries a one-shot backfill for runs decided before the column existed, and a partial index on the true rows.

It has no screen yet. Nothing in the dashboard displays it; this shipped as a column, a query and a REST parameter. A post claiming you can see it today would be wrong, so: you cannot.

Three documented traps (#257, documentation only — three files, no code). The migration conventions gained a rule for the collision described below. Contributing gained a section on git worktrees: a worktree does not share node_modules or the environment file, and the environment schema validates on import, so a test run dies during module load with an error that never mentions the worktree; a stale incremental TypeScript cache survives a branch switch and invents type errors in files the branch never touched; a development database shared with another worktree can be ahead of the branch; and macOS has no timeout, so a command copied from a Linux runner fails in a way that reads as the wrapped command failing. And a stale comment in CI claiming the linter always exits non-zero was replaced, since it no longer does.

The migration numbers, which is why that docs pass exists

Three branches were in flight at once, and all three needed a migration on the same day. 0108 went to the approval column. 0109 went to the voice rules. The briefing document had claimed 0108 first, and had to move twice — to 0109, then to 0110 — because an applied migration is immutable, and two files sharing a number means one of them never runs somewhere.

The conventions file now says the rule plainly: renumber the later migration to the tail of the sequence; do not edit the conflict into something that merges. Restore the journal to the base branch's file verbatim, append exactly one fresh entry, and rename the concurrent sibling too, because the applier matches on the filename's first four characters. It also says what does not catch this: the migration check verifies lock safety, not number allocation, and a collision is perfectly legal on each branch in isolation.

The renumber was checked by assertion rather than by eye — every journal tag has a file, every file has an entry, no duplicate index or tag, no two files sharing a numeric prefix, each tag's prefix equal to its index, the entries before the new one identical to the base branch's. Then applied to a fresh database: 111 migrations, with all three of the day's new columns present.

Two of the three PR bodies in that group still quote the wrong number, because they were written before the renumber and were not corrected. The files on disk are the authority, and the posts here cite the files.

How to read these

Each post opens with a status line saying what state the change is in and which commits it is. Every falsifiable claim cites a file, a pull request or a commit, and developer posts end with a "Where this lives in the repo" section.

Where something was not verified end to end, the post says which part — and in this release that list is long enough to collect in one place:

One claim from a commit message is quoted in these posts as the author's own observation rather than as a verified figure: the count of lint errors coming out of the Playwright artifact directories cannot be reproduced from the repository, because those directories are generated and were never committed.

Where this lives in the repo