Release
v1.2.22
What shipped, in numbers
- 181
- PRs merged
- 346,971
- Lines added
- 108,957
- Code lines
- Code
- 108,957
- Tests
- 119,838
- Docs
- 118,176
- Sum · code + tests + docs
- 346,971
of 181 merged
- bug 55 30%
- feature 59 33%
- polish 0 0%
- docs 49 27%
- groundwork 10 6%
- unclassified 8 4%
since v1.2.21 · tagged
The letter
What shipped
The release that stops FlowForge reporting "I cannot tell" as a verdict. A job the platform never started, read as a failing build. A machine-written suggestion sitting in a text box, read as a human's unsent draft. A worker waiting on its own compile, read as a worker waiting on you. Each of those cost whole days, and each is closed here — along with the one that cost the most: work that finished, pushed the fix a review asked for, and then waited for a review nobody started. Around that, the release carries four things you can open: every ratified plan on one shelf, a Gantt that plans instead of drawing, a radio that plays on macOS and Linux, and a War Room that stopped repainting frames that had not changed. What is live, what needs one setup step, and what does not run yet are each stated plainly below.
A job that never started is no longer read as a broken build
-
The whole board stopped behind one wrong word. Five reviewed, mergeable PRs were refused with "CI is RED at head" on a day when nothing was red: the repository's Actions budget ran out, GitHub declined to start the jobs, and a job that never runs still reports a
failureconclusion. FlowForge read the conclusion and nothing else, so an ABSENCE of a verdict was treated as a negative one — and no-merge-on-red correctly has no override, so there was no way through.A check that reports failure over zero executed steps is now classified as no verdict, never as red — and the refusal says so, in its own words, with an action attached. On a base the operator's own dials exclude from gating CI (
ci_on = "main-only"), that never-started reading does not hold the merge at all, because on such a base the gate was always reviewer GOLDEN. Both merge chokepoints — theflowforge mergecommand and the cockpit's autofire lane — resolve it through one predicate, so they cannot disagree.no-merge-on-red is unchanged, on every branch. A check that RAN and FAILED still refuses everywhere, still with no override — a version line included. So does a check still running. The only reading a branch policy can set aside is a job the platform never started, which is the one that was never a verdict about your code (#5763).
-
Four states used to print one sentence. A by-design base, a check that never started, a genuine red, and a run still in flight all read "CI is RED at head". Each now names itself and names a different action — which is the difference between an operator reading a refusal and an operator digging through the GitHub API to find out what their own tool meant (#5763).
A finished worker stops waiting for a review nobody started
- 🔴 A worker that fixed exactly what a review asked for never got the review it earned. The "already dispatched" latch was keyed on the TICKET, with no record of which commit it had reviewed — and a worker that receives findings and fixes them in place never leaves the waiting set, so the latch was never cleared. Every fix pushed after the first was, to the pipeline, work it had already looked at. Measured on 2026-09-02 with zero reviews running: three PRs finished, pushed and waiting on a reviewer nobody started, each dispatched once at an older commit. The latch is now bound to the commit it fired at, so a new head earns a new review. Every state it cannot read — no row, an unreadable head, two open PRs on one ticket — holds the latch instead of firing, because a duplicate review costs money and a missing one costs a day (#5840).
- 🔴 A relay could refuse to deliver a review's findings and blame a human who typed nothing. Claude Code writes a suggested next message into the same line you type into, and the guard protecting your unsent draft read only the bytes — where a suggestion and a typed sentence are identical. So it refused, quoted the machine's own words back as "the target composer already holds unsubmitted text", and sent the founder looking for a message he never wrote. The guard now reads the STYLING as well: a dimmed machine hint is not a draft. The cost is measurable on the fix's own PR — two of its review rounds were refused by the very defect under repair and had to be carried in by hand (#5671).
- 🔴 The one mechanism FlowForge runs on was the one thing the install never authorized. Every install hands Claude Code a short list of FlowForge actions that may proceed without a prompt — and the worker relay, which is how a review's findings reach the worker that has to act on them, was never on it. On a machine with nobody at the keyboard the relay was simply refused: the unattended driver retried each NOT-GOLDEN verdict for over an hour, advising a human to press a key with nobody present, while the board sat still and every instrument blamed the worker. The relay is authorized on install now. An install performed before this fix gains the authorization on the next update rather than staying quietly broken, and
flowforge doctorgrew arelay-permissionsrow that asks whether the relay can actually run on THIS machine — including whether adenyrule silences it, which an allow-list check alone would miss (#5847). - A worker waiting on its own build read as a worker waiting on YOU. "Waiting for a machine" and "waiting for a human" produced the same observation — a rendered prompt over bytes that do not change — so a worker running a test showed
blockedand asked for attention it did not need. Worse, attaching to it flipped the row toworkingand leaving flipped it back: looking at the state changed the state. A third detection channel now reads whether a shell is actually running, and a block with no evidence names nobody (#5667). - That fix moves one number on your invoice, and it is not the total. A worker waiting on its own job now counts as working for the whole wait. What you are charged is de-overlapped wall-clock time, and that does not move; what moves is the agent-hours figure disclosed on the invoice face as evidence of work delivered. v1.2.21 named this as the reason the fix was held back. It ships here with the distinction stated rather than left to be derived (#5667).
- 🔴 A stuck worker was seen by every screen and acted on by none. The detector, the classifier and the remedy verbs all shipped in an earlier release — and nothing ever called them, so a worker that went quiet, or a review verdict nobody passed on, sat until a human happened to look. The caller now lives in
flowforge daemon serve, which outlives any terminal and sees every registered repository, including the ones with no cockpit open: every ten minutes it nudges a worker gone quiet past its band — once per spell, never a stream — and relays a verdict the worker was never told. A finished worker still waiting on a review is escalated to you by name rather than dispatched on its own, because dispatching a review starts a paid model session with nobody present, and that is a decision for a person. Every decision is one line in~/.flowforge/logs/stall-actor.jsonl(#6104). - A PR that reached GOLDEN and waits on your ratification now freezes its branch. The verdict binds to a commit and the ratification to a document revision, so a warm worker that kept polishing voided the very review it was waiting behind — the longer the ratification took, the more certain it was there would be nothing left to ratify. The branch holds still, every surface says why, and the worker has a place to put what it finds in the meantime (#5866).
- The two things v1.2.21 said would land here, landed here — the relay guard that refused on a machine-generated hint (#5671), and the worker that read as blocked while waiting on its own job, with the invoice question answered rather than deferred again (#5667).
Plans ready to fire
-
🔴 Every finished plan used to disappear the moment it was finished. A study ran, the design was ratified, the tickets were emitted — and then there was nowhere to look. The founder, on his own repo, two weeks out: "we did several plans from the past 2 weeks, and I don't remember half of it, and worst than that I don't know where they are, and how to find it." Eleven finished plans sat in eleven documentation folders with a seven-week spread between them, and nothing on any screen said which were ready to build, which had already started, and which had been planned against a codebase eight releases ago. A customer, with no memory of the sessions, had no chance at all.
There is a shelf now. "Plans ready to fire" is an initiative that exists once per project, created for you and found again rather than duplicated; every ratified plan lands on it as a dated sprint —
2026-09-11 — <the plan's own title>— the moment it becomes a plan. Two doors put it there, and both are steps you already take:flowforge feature emit --livelands a Loop-2 feature as it emits the tickets, andflowforge doc ratify <doc> --landlands a study or a DAG as you ratify it (flowforge plans land <doc>does the same for a document ratified earlier, and refuses when no ratification exists — the shelf is not a way around the wall). A plan landed with no tickets yet is shown as exactly that, ratified · 0 tickets, because hiding that state is how the eleven were lost (#5991, #5992, #5994, #5995). -
The shelf says whether a plan is still current, and it never guesses. "Ready to fire" is a claim that decays: a plan written against 1.2.14 is not a plan for 1.2.22. So every sprint on the shelf carries the commit, the release tag and the lane it was planned against — every one of them derived from the ratification record and from git at the moment it landed, none of them typed by anyone — and every read grades it against the lane now:
● current,◐ behind(planned against 1.2.14 · lane is now 1.2.22 · 8 releases, 1,140 commits since), or○ unknownwhen the planned-at commit is no longer on this lane. A field whose derivation fails is left out and rendered as unknown, never filled with a default that looks like a fact. Behind is a signal, not a verdict: it means re-read the plan against the lane before firing it, andflowforge lane-freshnesson its tickets tells you which of them the ground moved under (#5991, #5993). -
Where to look: the War Room,
flowforge plans, and ACE. The shelf pins first in the War Room's initiative tier, each plan a dated row with its state (ready · 9 tickets, firing · 2/9 in flight) and its drift chip — glyph, colour and word together, so it reads without colour.flowforge plansprints the same as a table,--jsonfor an agent,--allto include plans that have since moved out. And ACE answers "what is ready to fire?" from that one verb: a shipped skill names the states, the drift line and the two doors, and forbids the archaeology in so many words — never grep the documentation folders for plans, because a folder has no ratification, no ticket count and no lane, and its date is the last typo fix (#5996, #5997, #6000). -
A plan leaves the shelf the way you said it would. "I can move that sprint later to a proper initiative like 1.2.22, or transform it into an epic inside a sprint/milestone." Both are
Eon the shelf sprint in the War Room. Attaching it to a real initiative rewrites one line and preserves everything else — the stamp keeps reading, so the drift stays visible under its new home. Fold into an epic in a sprint… (alsoflowforge plans fold) creates the epic through the ticket chokepoint, re-parents and moves every ticket through the seams that already existed, closes the emptied shelf sprint with a note saying where the plan went, and prints — never runs — the one command that re-derives the dates in the new home, because rewriting a date is typing it (#5998). -
Plans you finished before this release are not lost either.
flowforge plans backfillfinds the sprint-shaped milestones that predate the shelf and stamps each one from its own history — the lane's head on the day it was minted, the release tag at that commit — dry-run by default,--applyto write. Ratification is written only where a record actually matches; on a second machine that never saw the ratification it is honestly unknown.flowforge plans auditis the detector behind both doors: an open, ticketed, homeless sprint with no stamp is reported with its one-line fix, and it is aflowforge doctorrow alongsideplans-shelf, which says whether the shelf itself is present, closed or duplicated (#5992, #5995, #5999). -
Designed and ratified as one piece — #5983 — and shipped as ten nodes on one lane, #5991 through #6000.
-
A finished plan must be seen, not merely merged. Worker-done, green CI, a merged PR and a closed ticket all report success, and not one of them is responsible for anyone's eyes — two finished studies merged clean on 2026-09-11 and went unseen for about fourteen hours. An installed skill now carries the habit to every install — surface the plan before its PR merges, in the preview pane or the docs engine, never a GitHub diff — and a
plan-surfaceddoctor row checks it rather than trusting it (#6070).
The Gantt as a planning instrument
ADR-0130 turns the timeline from a picture of a plan into a planning instrument. Thirteen tickets carried it, in milestone #172; the release is cut after the last of them. The rule behind all of them: a number the screen cannot vouch for is shown as exactly that, never as a quiet zero or a dash.
-
The gantt's numbers re-aged while you looked at them. The sprint-intelligence strip, the overrun region and the today-line were all measured against the wall clock at every repaint, so a timeline left open overnight quietly moved its own verdict — behind 1 became behind 3 — without a single row of data having changed, and nothing on the frame said when the roster it showed had last been true. Every elapsed and remaining number is now measured against the instant the roster was READ, and the frame prints that instant with its provenance and age —
◔ as of Sep11 14:03overread 7m ago, in the label column above the bars;clock · no read yetbefore the first fetch — so no progress since Aug 11 and not measured since Aug 11 are two different things you can tell apart. A refresh that fails keeps the numbers of the read they came from, and the age under them keeps counting. The same instant is what the board's sprint strip and the cockpit's estimate tile receive, so no two surfaces can disagree about when a number was true (#5582). -
Duration, effort and elapsed are three fields, not one. A bar used to start at the ticket's creation date, so a ticket filed in July and started in September drew two months it never worked. The bar now starts at the ticket's first recorded session; effort is the corrected billable time the invoice is built from; and a ticket with no session has ABSENT effort, never
0.0(#5583). -
The plan's own estimate rides through to the ticket, as a range.
flowforge feature emit --livewrites aDuration:trailer on every ticket from the plan's estimate —Duration: 3d, orDuration: 2d-4d (med)with a confidence — and refuses the whole batch, before creating anything, when a work unit has none. The high end draws the bar, because a schedule is a promise and the optimistic end breaks the first time anything runs long; the low end is kept, not discarded (#5584). -
Every field says how it was measured — MEASURED, ABSENT, UNREADABLE or PARTIAL — and a value nobody read defaults to UNREADABLE, never to a successful empty read. PARTIAL had no representation before, which is how
0/8once printed over a milestone of 18 (#5585). -
Those states render apart.
∅ absent · act,? unreadable · retry,◐ partial · wait— glyph and word at every width, colour never the only channel — so the reader knows whether to supply an input, re-read, or use the number with its qualifier (#5588). -
A degraded instrument names what it lost, why, and the way back —
⚠ planned timeline off — no sprint or milestone scope · esc, pick a sprint, then t— on every Gantt instrument, down to 60 columns (#5586). -
The count publishes its denominator. A bare
8 tickets seenbecomes the population the panel already held, and the word seen goes away once every ticket is accounted for (#5587). -
The estimate-accuracy number was measured against a plan that could move to meet it. The strip's
◷ est N% on-timeand the cockpit's◷ on-time N%compared a ticket's close date with its current planned end — so a date pushed the day before the ticket closed scored as a perfect estimate, and the number told you how recently the plan had been edited, not how good the estimate was. A plan is now baselined the moment it is committed — a second, frozen block on the ticket that no later edit, push, or clear rewrites — and the number is measured against that baseline only. Where no baseline exists yet (every plan written before this release, and any window a ticket merely inherits from its milestone's due date) the number is retired and the screen says why:◷ est — (no baseline yet)on the strip,◷ on-time — no baseline yeton the cockpit, never a blank tile and never a percentage over sand. The baseline also finally separates two questions that looked like one: are we late (against the frozen finish) and did we rescope (the live plan against the frozen one) are now two different answers (#5589, ADR-0130 G-3/G-23). -
Dependencies carry a type and a lag.
Depends-on: #123 SS+2d, #124 FF— start-to-start, finish-to-finish, finish-to-start, with a lead or a lag in days or weeks. A bare#123keeps its exact meaning, proven byte-for-byte over 80 live ticket bodies, and a malformed suffix is UNREADABLE and named — never quietly coerced to finish-to-start (#5590). -
The network says when it is wrong. A cycle, a dependency pointing backwards across a milestone boundary, an edge to a ticket that is not on the chart, and a root marked blocked with nothing in its way are each named in words on the header row — and a cycle refuses the critical path outright rather than drawing one through it (#5591).
-
The critical path is computed, not eyeballed — a forward and backward pass over the typed network, reporting total float (can this chain slip without moving the release?) and free float (can this one ticket slip without disturbing anyone?) separately, against the same padded durations
flowforge backfill-dateswrites. A path through a ticket with no duration is ABSENT and names the ticket (#5592). -
Eight overlapping bars on one person is a conflict the screen now states —
⚠ load ⧉ @cruzalex 8 bars on Aug 08 — measured 2/day …— against the most tickets that person has actually worked in one day, never a nominal eight-hour day. It never moves a date; with nothing measured it discloses that instead of accusing anyone (#5594). -
Percent complete has a named basis, and says what it is a share of. The strip prints
◇ 50% of tracked hours (4 of 5): earned hours over the hours tracked so far, every hour from the same timer that pays you, and the population it folded printed beside it. Read it as a share of the hours tracked, not of the work — a ticket that has just started has tracked only a few of the hours it will take, so early in a sprint the share reads high, and one ticket closed with nine just started can read 92%. Nobody is ever asked for a percentage — a ticket claims 100 when it closes, from a machine fact, so a subjective "90% done" cannot enter the number. (A half-step for tickets above size M, claimed on the first session, is built and latent: FlowForge emits only sizes S and M today.)100%is reserved for a scope with nothing open, and a ticket the timer never saw is left out and counted, never weighed as zero (#5593).
Radio and the K.I.T.T. spectrum
- Search 30,000 stations, see what is set and playing, play, stop.
Jopens it; it is a full screen of its own and spends no cockpit rows (#5738). - The audio runs in a separate process, with frames reaching the cockpit over a bounded channel that drops rather than growing or blocking. It is silent by default — nothing is spawned until you ask. Having no audio device is a normal state, not an error: search and browse keep working and the screen says why it cannot play (#5737).
- The radio has sound on Linux. The one audio backend available to a Go program on Linux reaches ALSA through C, and FlowForge ships a binary with no C in it at all — which is why the first cut of the radio played on macOS only. That constraint has not moved; the way around it has. FlowForge speaks the PulseAudio protocol directly, in pure Go, which is what nearly every desktop Linux runs — and PipeWire answers the same protocol, so those machines are covered by the same code. Same spectrum, same behaviour, same single static binary (#6064).
- A machine that still cannot play says so precisely. A host with bare ALSA and no sound server has no pure-Go route, so it stays silent — and names that as the reason rather than reporting the platform.
flowforge doctorgrew a radio-sink row that answers the question from outside the cockpit:coreaudio,pulse, orunavailable:with the reason the cockpit itself would show (#6064). - Proven on Linux, not asserted from a Mac. A CI job starts a PulseAudio server with a null sink, plays through the real backend, records what the server actually received, and fails if that recording is silence — so "it plays" cannot be claimed by a test that merely believed it (#6064).
- A resting radio costs the cockpit nothing — proven by comparing the rendered bytes, not by measuring around them (#5738).
- Keep the station you liked.
fmarks the focused station as a favourite andFshows only the favourites — every one you ever marked, not just the fifty the last search loaded. The mark is a glyph and a word, so it reads without colour; with nothing marked yet the screen says so and names the key. Favourites live in one small SQLite file under your FlowForge home and survive a restart (#6001). - The K.I.T.T. spectrum. Bars bloom out of the centre on a red-to-white ramp, driven by the real frequency frames of what is playing — and when there is nothing to analyse, the idle Knight Rider sweep travels across a lit floor. Height carries the value and colour only echoes it, so the spectrum reads under
NO_COLORtoo (#5739).
ACE has a voice, and you choose it by ear
- 🔴 Spoken replies had no door macOS could open. The only way to switch them on was ⌥S — and on a stock macOS terminal Option composes rather than sending Meta, so ⌥S arrives as the character
ßand nothing fires. Founder, twice at the cockpit: "alt s or alt shift s doesn't do a thing." A switched-off mouth is indistinguishable from a broken engine, which is why the whole capability read as broken. There is a reachable door now (#5720). - A language used to mean exactly one voice. It carries a set now, so there is something to pick along: English gains three American voices alongside Amy (#5722).
- The English default is Bryce. Ten voices were benched through the shipped speech engine on the same four lines, on register, pitch movement, cadence, whether speech-to-text transcribes them back without an error, and licence; Bryce is the one that met all five. Amy is one keystroke away in Settings. An install that already speaks with Amy keeps Amy and is told the default moved — nothing is re-voiced without a word, and nothing large is downloaded behind your back (#5724).
- A voice picker on Settings ▸ Display & Flags, with a preview that commits nothing.
pspeaks a fixed sample line in the highlighted voice — fixed, because two voices are only comparable when they say the same words, and a name tells nobody which one they want to hear all day. Audition four, press Esc, and nothing was saved. Every state of the row names both cause and remedy — not downloaded, downloading, no speech engine, muted — and every one of them still shows the voice's name and its action key, because a key advertised only when things are healthy disappears exactly when you need it (#5723). - Open the cockpit on a project you already have a session for, and ACE tells you where you are — two or three sentences, read from the resume pointer your last handoff actually wrote, never a summary invented for the occasion (#5726).
- Spoken replies remain off by default. This release gives them a door, not a new default (#5720).
The War Room got fast
- 🔴 The War Room repainted ten times a second to draw a frame that had not changed. The cockpit's busy-spinner clock re-armed off four latches that had no idea what was on screen, so opening the War Room over a busy fleet paid ten full rebuilds a second of identical bytes, at 30.61 ms a frame. One rule now decides whether that clock runs at all, and no framed screen inherits it. Repaints with a busy fleet and no input: 11.675/s → 1.675/s (#5689).
- Every keystroke's scroll arithmetic was computed against a viewport that was not the one on screen. Two pieces of code sized the board and disagreed: at 200×62 the panel handling your keys worked against 55 rows while 47 were displayed. There is now one sizing authority, and the two agree (#5693).
- The board stopped being rebuilt on frames where nothing changed size. Same commit, guard forced off versus on, allocations per idle frame at 200×62: at 359 workers, 108,508 → 46,870; at 1,600, 169,723 → 50,602 (#5694).
- 🔴 Pressing Up in the middle of the table scrolled the whole table. The founder's report: "it should just change the row that is selected." The window was re-derived from the cursor on every keystroke, so moving the selection moved the view. The list now owns its own window — selection moves, the viewport holds, and it moves only when you are at the top and press Up or at the bottom and press Down (#5695).
- A mid-list keystroke painted the screen twice and threw the first one away — 2.94 ms of a 10.14 ms keystroke, 29%, on the most repeated interaction on the screen you show your own client (#5699).
- A populated board could go blank on a refresh that found nothing and reported no error. The lane count went with the cards —
To-do 13toTo-do 0— because a zero-row read with a nil error was written over the good roster. It has a real cause: a renamed repository, where the milestone query resolves against the stale name, finds nothing and exits clean. An empty answer no longer replaces a populated one (#5744). - A sprint's row could stay hours behind its own board. The War Room showed a sprint at
✓ 4closed while its kanban, opened seconds later, showed Done 10 — six tickets had been closed by merges elsewhere and nothing on the screen had looked since. While the War Room is open, a merge now re-reads the roster within seconds, and a steady cadence catches what a merge cannot see, re-reading the roster and its milestone at least once a minute (#6066). - The speed is a gate now rather than a claim. The frame is asserted byte-identical at all four terminal sizes it was measured at (#5698), the measurements behind these numbers run as a regression gate (#5688), and the sizing, discard, render-pass and resize behaviours are each pinned by a test that fails if they come back (#5687, #5690, #5692, #5696).
What you reported
Four of this release's fixes came from one customer's own install, each reported with the evidence that made it fixable.
-
🔴 A dead process could hold the self-driving pipeline hostage for days, and nothing said so. The sprint-autofire lease records the process that took it — and nothing ever asked whether that process was still alive. So when the cockpit died without cleanup (a crash, a
kill -9, a sleep that ended it), the lease outlived it and every later autorun was refused by an owner that no longer existed. There was no timeout, no staleness check and no reclaim. It fails closed and quiet: no error, no chip, nothing on any screen — autofire simply never runs again and the board looks idle rather than broken. A customer captured three of these on one machine; one had been sitting there four days when a human finally noticed and deleted the file by hand.A lease whose owner is gone is now reclaimed automatically on the next autorun, and pressing S in the cockpit reclaims it too — no file to find, no flag to know about. Two conditions do it, and neither alone: the owner's process is gone, or the lease has shown no sign of life for fifteen minutes. Both are needed, because an operating system reuses process ids — a recycled id is alive and is not the owner, so "is that number running" answers the wrong question on its own. A lease that IS alive is never taken, since two autofire drivers on one repository would be worse than the outage this fixes.
The reclaim also does not trust the machine's NAME. The customer's two preserved leases record two different hostnames for the same iMac —
iMacandVolans-iMac.local— because macOS renames the machine when the network changes, so any rule keyed on the name would have shipped looking correct and stranded exactly the leases he reported. Identity now rests on a stable machine id, and reclaim is never gated on it: the name decides only how fast a stale lease goes, and protects a live one. The same rename used to stop a running cockpit from recognising its own lease, which is fixed here too.And it says so. The reclaim is announced on the cockpit rail and on the command line, and
flowforge doctorgrew anautofire-leaserow that names a stale lease, its recorded owner and how long it has been silent — because a reclaim that ever fails should still be visible rather than silent, and the silence is what cost four days (#5950). -
🔴 Every ticket emission recorded a gate bypass that nobody performed. The audit record is the artefact FlowForge sells as proof of process, and on a customer's install it was being falsified on every single planning round —
GATE_BYPASS_UNLINKED role="adr"on a repo where the ADR plainly existed, the same line on 2026-08-11, on 2026-08-12, and again on rounds R2 and R3 of three consecutive sessions. Two causes, both about a path. The renderer wrote a blueprint's sidecar todocs/blueprints/<slug>/<slug>/…— the feature name twice, because the directory and the document key each contributed it — while the reader that resolves that document looked somewhere else entirely. And the ADR had exactly one route to the gate: a registry record allocated by a command the planning loop never runs, so in a customer repo that route was never taken. Writer and reader now derive the location from one function, which is pinned by a test that fails the moment either one builds a path of its own, and the ADR is linked through its rendered artefact as well as its registry record — so a repo shaped like the customer's emits with no bypass at all.The cost being removed is what the operator was left to do about it. The gate's advice was to link the artefact, the link resolved nowhere, and the only remaining move was to copy the file to the path the reader expected — which satisfies the gate by forging the record the gate exists to prove. He refused, and he was right to. A gate whose only remedy is forgery teaches customers to forge. The remedy line now prints the exact command with the exact document key, and a file left at the old path by an earlier version is found and linked where it lies — nothing to move by hand, and nothing relocated on your disk without you asking. One knock-on if you script the portable lane:
flowforge docs render-blueprint --out <dir>now names the rendered-docs ROOT rather than the per-feature directory, so the files land under<dir>/blueprints/<slug>/— the same shape the trail resolves, which is the whole point (#5951). -
🔴 An ADR that exists as a document could never be ratified, and the error told you to run the one command that cannot help.
flowforge chart ratifyreads a registry record, and the only two verbs that could produce one wereadr new— which allocates a NEW number and so cannot adopt a decision you already wrote — andadr import, a whole-corpus rebuild. So the refusal pointed atadr new,adr newcould not do it, and the only remaining move was to hand-write the TOML the gate checks. A customer sat three consecutive rounds of charts in DRAFT rather than do that, and said so: "No ledger record was forged and no SVG was faked in any round." A gate whose only remedy is forgery teaches forgery.flowforge adr register ADR-NNNNis the missing verb. It adopts the number the document already carries and writes the record from what the document says — title, status, ticket and the document's own date, never the wall clock. It verifies before it writes, because a back-fill that does not verify is the fabrication it replaces: it refuses a document that does not exist, one whose name claims a different number, a companion file that folds under another ADR, a number any other document claims (on both of its paths — naming the file explicitly earns no exemption), a document the founder has quarantined in[pending], and — hardest of all — it never overwrites an existing record, because a record is what a ratification is stamped against. Every refusal writes nothing at all.The refusal you actually hit changed too. It goes and looks for the document before it speaks, and hands back the register command with the path already filled in, so nothing has to be discovered. When no document claims the number it says which directories it searched, and only then offers
adr new— the one case where allocating really is the answer. A repo running two ADR conventions side by side is the normal case, not an exotic one: this one does, and the wall now steers you to the registry the repo already has instead of quietly starting a second one. The same defect one command over is closed with it —flowforge trail link --adrread the registry only at its default location, so the hint behind this one named a command that would look in the wrong place;--adr-dirgives it the root (#5952). -
The ratification wall kept re-firing on a merge that had already landed, lecturing about work that was done. The observer now reads the wall's own facts before it fires, and the driver that kept re-firing is named (#5953).
Colado
Colado is a jiu-jitsu term — a position is colado when it's justo, perfectly adjusted, no gaps, full pressure — and it's the name for the family of FlowForge gates that leave no space between the rule and the work. This release ships the first customer-visible slice of that family, milestone #3052–#3085.
- A billing identity could drift between machines, so a fresh laptop could quietly bill under the wrong account. It now fails closed to one
dev_idfor the hooks and the writer, healed automatically at the start of a session (#3060). - The fail-closed policy that identity depends on was implicit, so the tree could contradict it and nobody would notice. It is written down once now, with every place the tree used to disagree named and closed (#3059).
- A merge could land on a CI check that never actually ran. The server-side no-merge-on-red backstop is one shared detector now, with a
flowforge doctorrow, aconfigureverb that refuses to write blind, and a plain statement of where it cannot run at all (#3055). - A protected-branch read that failed used to be indistinguishable from a branch that was simply unprotected. The Rule #18 backstop is one shared detector with a doctor row — COVERED, UNCOVERED, UNAVAILABLE or UNKNOWN — and a failed read is never worded as UNCOVERED (#3056).
- Autofire used to treat a plan waiting on your own ratification the same as a broken build. It now asks the merge ladder before it fires, so a pending ratification opens RATIFY, never a red failure (#5973).
- An old Claude Code install used to make a missing capability read as a FlowForge bug. A minimum-version floor with per-capability refusal now says, by name, that the platform needs an upgrade (#5974).
- The door FlowForge opens to your ticket system was 64 untracked
ghcalls, so a second provider meant a rewrite. 168 forge seams are inventoried and ratcheted now — a second provider is a seam change, not a rewrite (#5976).
Three more close the same kind of gap from outside that milestone family:
- The CI gate dial said
main-onlywhile the repository ran CI everywhere. On one private repository in twelve hours: 200 workflow runs, 193 of them offmain, 2,319 billed minutes — money a customer would never expect, because the setting said otherwise.flowforge release ci statusnow reads the workflow YAML back and prints the actual triggers beside the policy, and aci-trigger-driftdoctor row fires without anyone asking (#6113). - A hotfix that lands on
maincomes back down. Every active release lane gets a sync PR, a conflict becomes a labelled PR, and nothing is ever silently skipped — so a fix is not lost to the next release just because it went through the trunk (#3525). - A release cut declares its plan and refuses on a drifted machine.
flowforge release cut --dry-runprints every precondition it will enforce and every hand-off, touching nothing; the cut refuses a detached head, a machine behind origin, a dirty tracked tree, the two installed binaries disagreeing, and a lane with no release-notes section — with arelease-cut-preflightdoctor row for the same facts (#5130).
Small things that change everything
Not every change earns a headline. These fit in a line each, and you feel them every day.
- 🔴 A bare backtick used to open a terminal instead of reaching your text. Typed into any cockpit text field — the ACE composer, the spawner's ticket field, the milestone form, a ticket comment —
`was caught by the global terminal toggle, a tmux terminal opened over your work, and the sentence was silently cut at that point. The toggle is a chord now, and a backtick is a letter (#5255). - 🔴
rwas not a letter. With the ACE rail focused and the box empty, typing "review the board" opened the Live Reviews screen with nothing typed. Every time.rbegins run, review, relay, restart, retry, read, report — the whole vocabulary of talking to a controller about engineering work — and it was the only lowercase member of a shortcut set whose others are shift-typed and so effectively never begin a sentence. It is a letter again, and Live Reviews still opens onrfrom everywhere else (#5804). - 🔴 The rail deleted an answer because it contained the word "token". The filter that recognises Claude Code's own hint strip matched the substring anywhere, so a reply that named
SUPABASE_ACCESS_TOKENwas discarded as chrome while the complete reply sat intact on disk. Those patterns are anchored now, and a reply whose final section renders empty says so instead of leaving a heading over nothing (#5815). - ACE's window used to land wherever tmux put it, with quitting as the only way back to the cockpit. It opens at a reserved slot now, one key brings you home, and when that slot is taken the window says which one it used instead (#5865).
flowforge doctorused to call your own agents an install defect. It counted every.mdfile in~/.claude/agents, so one agent you wrote yourself read asagents: FAILwith advice to re-install. It counts only the agents FlowForge installs now (#5877).- ACE's model is a choice in Settings — Opus, Fable or Latest — applied to the session you are already running from ACE's next turn. Latest resolves through Claude Code's own account-aware alias, so it can never pick a model your account cannot run (#5870).
- Your commits stop carrying a trailer your own wall refuses. Claude Code adds a
Co-Authored-Byline by default, and FlowForge refused it at the wall and stripped it afterwards without ever switching off the thing that writes it. The install now setsincludeCoAuthoredBy: falsein your global Claude Code settings, so the commit is right the first time (#5943, #3057). - Three new specialists: a data-analyst crew.
fft-analytics-architectdesigns what a number means — population, window, denominator, all named —fft-data-analystcomputes it against real rows, andfft-dataviz-designerpicks the chart that cannot mislead. FlowForge now installs 44 specialists, andflowforge doctorcounts 44 (#5924).
Multi-phase work, and the gates that protect it
-
A ticket with five stages was closed by the merge that finished stage one. Auto-close attributes one merge to one ticket, which is correct and is not changing — so the shape was wrong, not the closer. Multi-phase work is an EPIC now, with one ticket per phase, and the same merge is simply right.
flowforge ticket newguards the door, andflowforge epic-auditreads the board for the ones that got through anyway (#5715). -
A ticket that declares a founder ratification gate is never closed by a merge. Four gated tickets were closed in three days — the last of them the ticket for putting a door on the ratification wall. Ordinary tickets still close on merge; this reaches only the planning tier that has a gate to protect (#5713).
-
🔴 The ratification wall blocked merges on a document the change did not contain, and printed a command that could not run. A ratification binds to a revision of a document, and a change that does not ship that document has no revision to bind — so the printed remedy was missing its own argument, and the only way through was an environment variable a customer would never discover. A gate belongs to whichever change ships the document it names. On every other change the wall now stands down and says so, instead of dead-ending (#5742).
-
That last one is also the argument for cutting a release promptly. The fix merged on 2026-08-25 and this is the first release to carry it. In the eight days between, it was still being hit, still being diagnosed and still being filed as new — because an installed copy cannot run a fix that exists only in a branch. Its follow-up settled it probe by probe: every check against the source found the fix present, and every check against the installed binary found the defect (#5843).
-
🔴 And when that wall did fire, the escape hatch it advertised did nothing from the cockpit. The red screen offers a Bypass, takes a mandatory reason, and — until now — delivered that reason to three flags the ratification wall does not read. So the identical refusal came back. The founder pressed it twice on one PR, typed a real reason both times, and merged on GitHub instead: "we have a bypass that don't bypass nothing." The same hole sat under the emergency hotfix-while-frozen escape. The bypass is gate-aware now: it reads which wall actually refused and carries your reason to that wall's own hatch — naming the gate on screen when it is a founder gate, waiving that one gate and no other, and recording the attributed reason before anything merges. It reads the wall's own NAME and not the prose around it, because a line-cap refusal names the file it tripped on, and a file called
ADR-0116-...mdis not the ADR-0116 wall. And the refusal that fires when the wall cannot READ your PR — the case the escape is most needed for — reached the soft trio too, for a reason worth naming: the wall's own refusal cites the 2026-08-15 incident in which a merge went through, the words "merged PR #" in that sentence were read as a progress note, and the screen replaced the wall's explanation with "the merge did not complete". You now read what the wall actually said.The refusal text was part of the cost too, so it changed: it used to print an environment variable and a flag side by side, which reads as needing both. The flag alone is enough, the message says so, and it now names the in-cockpit door as well — because that is where you are sitting when you read it. All three walls that offer a recorded hatch say it the same way now (#5853).
-
🔴 The ratification wall refused documents that merely DESCRIBE gates — four times, the last one blocking this very release's cut. A markdown heading whose text carried the ⏸ glyph or the words "design-gate" / "founder-gate" was read as the document declaring a founder gate over itself, so any PR that so much as touched the ForgePlay-4 playbook — whose headings name the play's own DAG gates — was refused, and PRs #5110, #5112 and #5960 each merged only on a recorded founder override. Measured at source over every tracked document, that heading form fired on 15 files and not one of them carried a gate; across the whole history no document ever wrote the heading it was designed for. It is retired, not narrowed — a heading's text is prose, and a regex over prose cannot tell "this document is gated" from "this document is about gates". What still gates, and is now pinned as the negative control in the same test run: the canonical
Gate: founder-ratificationtrailer, a document's ownStatus: … awaiting founder ratificationfield, and a ⏸ design-gate PR title. The playbook was not edited to appease the detector (#5117, ADR-0116 Amendment 3).
Paying for FlowForge, and getting out of it
- 🔴 The page after checkout promised an email that was never going to arrive. It said your receipt and account details were "on their way to your inbox" and made "Check your email" step 1 of 4 — while FlowForge sent no email at all: no welcome, no confirmation, no receipt. Two customers paid and neither got a word. The promise is off the page, and what remains is three steps you can act on unaided plus a real address to write to. The sender is built and is not sending yet. Three things stand between it and your inbox, and the binding one is neither of the obvious two: no payment-webhook endpoint is registered on the live account at all, so nothing that runs after a purchase — this sender included — is invoked. The other two are a verified sending domain and the sender's credentials. The promise stays off the page until a real delivery is observed, which a test now enforces so the claim cannot return before the mail does. Stripe's own receipt setting was also found switched off, with both unsent receipts named rather than summarised (#5793).
- 🔴 Marketing consent was demanded at sign-up and stored nowhere. It is asked rather than required now, and it defaults on — that half is live on the page. Unticking it is recorded as a positive no rather than as silence, and the record keeps the exact wording you answered and the moment you answered it, appended rather than overwritten, so a withdrawal never erases the consent it withdraws — which is the row you need the day someone says they never agreed. Until the store behind it is switched on nothing is recorded, and an unrecorded answer means no marketing at all, which is the safe direction of the two (#5792).
- 🔴 An admin could switch your access off and leave your card being charged. There were four admin verbs and not one of them touched Stripe, so revoking a paying customer produced the worst combination available: product off, billing on. There is now a subscription layer with its own verbs — status, cancel, resume — and revoke refuses while a live subscription exists, naming the cancel verb and printing the amount that would have kept being charged. Where it cannot reach Stripe at all it refuses too: proceeding requires proof of absence, never an unanswered question read as a no. Until the subscription verbs are configured for an account they answer not configured and revoke still refuses — so the stranding is closed either way, before and after setup. Founder's ruling, in his words: "I don't want anyone hostage to the system." (#5796)
- The welcome email quoted the full price to a customer with a discount. It read the total off the price, and a Stripe coupon discounts the invoice without touching the price — so every field arrived intact, nothing failed, and the mail stated an amount the customer did not owe. It reads the invoice now (#5868).
- The admin console reads the invoice's own basis. Time is de-overlapped once, where the invoice is built, and that figure crosses the wire — the console no longer re-sums raw sessions into a different number (#5306).
Teams, and the ticket systems they already use
- Six of the eight ticket-system field roles now reach FlowForge, up from two. Tell FlowForge which of your Jira fields is the title, the description, the assignee or the milestone, and it reads them. The two that still have nowhere to land — status and priority — are named with their reason instead of being given a switch that quietly does nothing (#5809).
- Company membership stops being a script somebody ran by hand. It was 16,941 bytes of SQL, re-run per customer after every membership change. The write moves into the product itself, proven row-for-row identical against the script it retires, and takes effect for a workspace once the new data functions are installed there (#5805).
Smaller, and under the hood
- The Markdown parser under every rendered document moved a major version — goldmark v1.7.8 → v2.0.0 — with a golden test now standing behind the rendered output, so the next move cannot be silent (#5789).
- 78 of 330 test packages behind this release had been going untested. One group of tests was finishing four seconds inside its time limit; when a slow machine pushed it over, it was cut off, and the packages past the cut were never reported at all rather than reported as failing. The limit is raised and the packages this release ships are among the ones being tested again (#5785).
- A take-over is not a user quit. Reopening FlowForge Desktop over a live cockpit ended the old one with a signal that was recorded as a clean exit, so the crash surface stayed silent and the next launch overwrote the evidence. Every exit you did not ask for is now named, its dump is kept, and the next launch never starts silent (#6046).
- A brief that was delivered is no longer reported lost. The paste check called a landed paste lost when the composer had already drained it, so a brief could be sent twice; it is delivered once now (#6047).
- Every release has a public page.
flowforgesoft.com/releases/<version>renders these notes verbatim beside a numbers block computed from git — PRs merged, lines by class, the delivery mix — andflowforge release stats <version>prints the same numbers on the command line (#6059).
One setup step
- Four of the changes above ship their code here and go live after a one-time setup step. The consent record (#5792), the subscription verbs (#5796) and the company-membership write (#5805) each go live when their data functions and credentials are installed, and every one of them fails in the safe direction until then: nothing recorded means nobody is marketed to, and an unconfigured subscription layer refuses rather than proceeding. The new English voices (#5724) download from HuggingFace until FlowForge's own CDN mirror carries them.
Not yet
- Team tier is not finished. The database half of the team-attribution defect ships here: signing up creates a personal organisation of your own, and its existence made every real member of a shared company ambiguous to the code that has to decide which company you belong to. The half you can see needs a one-time production step and a console change that are not in this release, so an owner selecting their company may still not see every member (#5827, #5833).
- The post-checkout email is built and not sending. No payment-webhook endpoint is registered on the live account yet, so nothing that runs after a purchase is invoked; the page makes no promise until a real delivery is observed (#5793).
- ACE's own voice drives the spectrum only after the voice sidecar is re-cut. The cockpit side is merged — the ACE rail draws the same K.I.T.T. field from real levels while ACE speaks — but the installed speech sidecar is a pinned prebuilt that does not send levels yet, so while ACE talks the rail shows the idle sweep, never an invented spectrum (#5725).
- Radio on a Linux host with bare ALSA and no sound server stays silent, and says that is the reason (#6064).
- That test-timeout fix raises a limit rather than removing the cause. One open-ended group absorbing every package the codebase grows is the real problem; a higher limit buys room and does not solve it. Recorded here so it reads as a decision, not a silence (#5785).
Agents
The specialist roster grew from 41 to 44. A whole org — measurement — with a head that decides how a number is defined and two leaves that compute it and draw it, so a metric is never re-derived wrong per ticket (#5924, #5926).
- Added
fft-analytics-architect— the head of the data-analytics org, the measurement analog offft-architecture. Owns metric and indicator design (every number's population, join, window and denominator named, never implied), study design, threshold derivation from the population a gate actually fires on, and back-test discipline. Emits the MEASUREMENT BRIEF its leaves execute; decides how truth is measured and never computes a production number itself. - Added
fft-data-analyst— the execution leaf. Runs the brief against real data: pulls from named sources with pull timestamps, projects rules over real rows before they ship, computes cohorts, trends and deltas exactly as briefed, reports unknown as unknown, and returns an EVIDENCE REPORT a stranger can re-derive. Never re-authors method or moves a threshold. - Added
fft-dataviz-designer— the presentation leaf that decides which chart tells which truth. Maps indicator kind to chart form per the perception canon, holds the accessibility floor (CVD-safe palette, colour never the sole channel, unknown rendered as unknown), and emits the CHART SPEC the design org styles and the builders render. Decides form, never styling; never defines a metric.
Routing: what does this number mean → the architect first; computing it against real rows → the analyst; choosing the chart that carries it → the dataviz designer. Installed by flowforge update under ~/.claude/agents/ with the other 41, and listed at flowforgesoft.com/agents.
Update
flowforge updateon an existing install, orcurl -fsSL https://get.flowforgesoft.com/install.sh | shfor a fresh one. Both write the same release binary to/usr/local/binand~/.flowforge/bintogether.flowforge version updatedoes not update anything — it prints the installed version and exits.