Release
v1.2.23
What shipped, in numbers
- 82
- PRs merged
- 132,379
- Lines added
- 48,021
- Code lines
- Code
- 48,021
- Tests
- 48,923
- Docs
- 35,435
- Sum · code + tests + docs
- 132,379
of 82 merged
- bug 34 41%
- feature 26 32%
- polish 0 0%
- docs 16 20%
- groundwork 0 0%
- unclassified 6 7%
since v1.2.22 · tagged
The letter
What shipped
The release where FlowForge stops re-doing work it already finished. A ticket merged, and fifteen seconds later the pipeline spawned a new worker for it and started its timer — three times in two days. A review voided by a polishing push that nobody was told about. A hotfix that landed on main while the open release lane sat thirteen commits behind for eleven hours. A sprint with a due date and no begin date, which the audit passed. Each of those is closed here. Around them, the release carries the thing v1.2.22 promised: the release letter you are reading is now a pipeline you can point at your own product — a .eml with zero configuration, and a send step when you configure one. What is live, what needs your own setup, and what does not run yet are each stated plainly below.
Release Letters — the letter pipeline is yours now
v1.2.22 announced this for v1.2.23: "a release letter and a release page generated from your own release notes, sent to your own customer list … opt-in, in Settings, off by default." It ships. One ratified study (#6219) and one ADR of seams (ADR-0137) carried it, as fifteen phases on one lane (#6229).
- Every cut can hand you a sendable letter, with nothing configured.
flowforge release letter render <version>builds an email-safe, 600-pixel HTML render and a plain-text twin from yourLETTER.mdand your project's brand tokens — no designer required, and a hand-made render is never overwritten without--force(#6231).flowforge release letter export <version>then writesrender/LETTER.eml(andLETTER.pt-BR.emlwhen the pack has one): a standards-compliant message any mail client opens as a draft and sends. No sender, no key, no host and no recipient list are read to produce it (#6230). - Settings → Release Letters is the ninth Settings tab. Sender, image host, recipients, unsubscribe and the legal footer, each a seam you own — FlowForge hosts nothing for you. Delivery is off by default; a secret you type is written to the OS keychain and never to
settings.toml, it echoes nothing as you type or paste, and there is no reveal (#6235, #6240). - The seams behind it. Sender: Resend, or your own SMTP relay (#6237). Image host: an S3 bucket behind a CDN, or a directory you already serve; images are proven with a real GET that must answer an image, not an error page (#6236). Recipients: a CSV file (refused if it is tracked by git or readable by others), a manual list, or a Supabase query — each row carries its locale and consent state, and no address is ever printed or logged (#6238). Unsubscribe: a one-click link to an endpoint you host, with a per-recipient token, a
mailto:, or none — and the footer says which, including thatmailto:gives Gmail no one-click button (#6239). - The same table, from the command line.
flowforge marketing getandsetread and write the tab's keys (a secret is refused on the command line),flowforge marketing test images|sender|recipients|unsubscribeproves each seam —test senderprobes the provider and sends exactly one message, to your own seed address — andflowforge marketing preflight <version>runs the pre-send checks: SPF, DKIM, DMARC, HTML size, every link, merge fields, and the text/HTML diff. None of those checks ever blocks (#6241, #6239).flowforge doctorgrewmarketingandmarketing-dnsrows (#6235, #6241). - Where the send happens. The cut reads your Release Letters settings and prints one line —
none(the.emlis the delivery),partial(every missing key named), orready— and never refuses the cut over it. The send itself runs atflowforge release promote, after the flip, once the pages are live: a seed to you, ay/Nfor the batch, batches under a daily cap, and a send log that lets a re-run skip everyone already accepted. Apartialsetup refuses the send, never the promote.flowforge marketing send <version>is the same step on its own, andrelease-prep.sh X.Y.Z --no-send "<why>"records a version that ships its letter on the site but is not e-mailed (#6242, #6244). - A customer guide walks the four increments — image host, sender, recipients, unsubscribe — each with its commands and the doctor row to read afterwards, in English and Brazilian Portuguese:
documentation/2.0/guides/marketing-settings.md(#6244).
The cut writes the letter, and the letter says what the roster did
- The letter is a step of the cut.
flowforge release cutrefuses (release-letter) a version whose lane carries neither its letter pack nor a recorded decision to ship without one. A small bugfix can skip the letter, but only throughrelease-prep.sh X.Y.Z --no-letter "<why>", which lands in the prep commit and is reviewed like everything else — a forgotten letter is no longer possible.flowforge release promotenow refuses unless the release page and the letter page both answer 200, and the site build fails a version with neither a letter nor a recorded skip (#6212). - A roster change the notes do not name refuses the cut. v1.2.22 shipped three new specialists and neither its notes nor its letter said so. The cut now diffs the installed specialist roster against the previous release and refuses (
relnotes-agents-block) a section with no### Agentsblock naming every agent added, removed or renamed — with the exact list in the refusal (#6201).
Sprints carry their dates, and finished containers close
- A sprint cannot exist without its begin date and its end date. Milestone 191 was created with a due date and no begin, received tickets, and
sprint-auditpassed it — the audit read only open tickets and never checked the begin. Every sprint door now refuses a sprint missing either date — the cockpit's milestone form and its convert-to-sprint keys, and the sprintfeature emit --livemints — before any call to your ticket system.flowforge sprint-auditreads every ticket of a sprint, open and closed.flowforge sprint-datesis the one fixer: it writes a begin and a due through one path, dry-run by default, and--derive-duetakes the latest ticket end — refused while any ticket is undated. Sprints created before 2026-06-29, when the rule was ratified, are graded legacy and warned, never refused;flowforge doctor'ssprint-datesrow splits the two (#6302, #6313). - Epics, milestones, sprints and initiatives close by themselves once every child is done — no one is asked. An epic ticket counts as structure, never as a work item, in every count: the percent, the lane pips, the header and the auto-closer. And a container that is 100% done never shows under the War Room's
▼ Active. Container closes run from code on your default branch — the merge Action and a 30-minute sweep — so they begin once this release is onmain(#6290). - A plan comes in two kinds now: paved, then fired. Landing a ratified plan (
flowforge doc ratify --land,flowforge plans land) no longer mints a sprint: it paves the plan as an undated milestone on the shelf — ratified documents and a DAG, no begin, no due, no tickets — and re-landing a newer ratified revision re-stamps it in place (#6314). flowforge plans fire <plan> --begin YYYY-MM-DDturns a paved plan into its sprint and tickets. It refuses unless the plan carries a ratification at its current revision, asks the begin date on every first fire and never derives it, then — with--live, in this order — converts the milestone into a sprint with its begin and its derived end before any ticket exists, creates the epic, one ticket per work unit in dependency order with itsDepends-on:,Duration:and gate trailers, and writes their dates.--into <sprint>fires the plan as an epic inside a sprint you already have. Every refusal is collected in one pass, and a fire stopped part-way prints the command that resumes it.flowforge plans lint <doc>runs the same derivation and writes nothing, so a plan is known to be fireable before it is ratified.flowforge ticket new --study --phase … --phase …creates a study's epic and its gated phases in one step (#6301, #6314).- The "Plans ready to fire" shelf exists by construction. v1.2.22's notes called the shelf install-created, and it was not: a 1.2.22 install answered
shelf absentuntil two verbs were typed by hand.flowforge installandflowforge updatenow land it for every registered project, and the cockpit lands it once per installed version in the background; offline, it says why in one line (#6197). - The shelf's drift read grades against the lane on your remote, not your checkout. On a checkout 275 commits behind, every plan read
○ unknown. The read now resolvesorigin/<lane>first, notes(checkout N behind), and names a plan landed locally and not yet pushed (#6198). - Shelf sprints read
◇ SPRINTin the War Room's TYPE column, where they read○ unknown, and a sprint's name stops repeating its date (#6280).
The pipeline stops re-doing delivered work
- 🔴 A merged ticket got a new worker and a new timer fifteen seconds later. Three times in two days. The spawn chokepoint already graded the ticket
owned-in-lane— and only logged it. It now refuses the spawn before the tmux session, the model process and the billable timer, and removes the worktree it created, unless the ticket was reopened or is an epic (#6286). - The wave drain reads "merged" from the merge pipeline's own record. In the twelve seconds between a merge retiring its worker and the cockpit noticing the merge, the drain read the ticket as queued and fired it again — the source of every
BRIEF UNDELIVERED … can't find paneline (#6053). - Wave runs advance by themselves. A run waiting on a free worker slot was retired as residue after 30 seconds, and a run whose cockpit died was retired by the next one. A live, armed run waiting on a slot now keeps waiting, a new cockpit adopts the run of a cockpit that died less than an hour ago, and the waves screen names the next step:
⇢ next ≈W2 fires when #6240 merges,⋯ waiting … for a free slot (cap 8/8),○ not armed … press S(#6065). - A hotfix on
maincomes down into the open lane by itself. On 2026-09-13 the lane sat 13 commits behind for eleven hours until a sync was run by hand. The autofire loop now portsmaininto the lane at the end of every pass through the down-merge verb, never duplicating an open sync PR; every attempt is recorded onmerge-advance.log, and thelane-driftdoctor row names the last one (#6220). - A push that would void a GOLDEN review is refused. A worker pushed a polish commit two minutes after its PR went GOLDEN; the next round came back NOT-GOLDEN with a defect that commit introduced. Measured on this machine over two weeks: 220 GOLDEN verdicts bound, 42 voided by a later push. The pre-push hook now refuses a push onto a cleared head, names the verdict it would void and the review round it would cost, and takes a recorded reason to proceed:
FF_GOLDEN_PUSH_OVERRIDE="<why>".flowforge review clearedanswers the question for a worker, andflowforge review voidedplus thegolden-voiddoctor row count the waste (#6111). - A verdict nobody acknowledged is now measured. A NOT-GOLDEN verdict sat unread for 43 minutes beside an idle worker, and nothing counted the gap.
flowforge review unackedand averdict-unackeddoctor row report every verdict with no remediation commit, no remediation comment and no delivered relay past a threshold, and ACE's dashboard carries the same list (#6103). - A worker blocked on something only you may run now pages you, with the ask. A worker whose next step was a command reserved for the founder read "with ACE", counted zero on the needs-you tally, and pushed nothing to the desktop. It now ends with one line —
⏸ FOUNDER-ACTION-NEEDED: <the exact ask>— and its row readsneeds your actionwith the ask on the badge (#5837). - An escalation from the stall actor pages you and names its author, and it no longer overwrites a page a person had already left in the same flag (#6117).
- ACE declares its controller identity at launch. A throwaway cockpit started from a worker's shell resumed the founder's ACE conversation, and ACE inherited the worker's role and terminal — so its own merges were refused as a worker's. ACE's environment is now cleared of every worker marker and foreign terminal at launch, and the cockpit logs one loud line if it ever inherits one (#6281).
- The commit-message wall judges your branch's real range. It read GitHub's cached commit list for a PR, which does not shrink when the base later absorbs those commits, and refused six commits the base already had. It now reads
origin/<base>..<head>from git and names the range and count it judged (#6221). - An epic is decided by what the ticket declares. A
kind=bugticket with[EPIC]in its title stayed open after its PR merged. Thekind=marker now outranks a title tag, and the closer and the boards read one verdict (#5757). The wave engine uses the same verdict: an epic marked only by itskind=epicmarker is never layered and fired as work, and a customer ticket that shares a number with a FlowForge epic is never hidden (#6152).
The Gantt explains itself
?opens a legend from inside the Gantt. Every glyph and header term — total float, free float, saved early, depends, today, due, critical path, velocity, load — gets one sentence, sourced from the constants the screen draws with, and/floator/critical pathfinds it.? legendjoins the footer at every width, and ACE mentions it the first time the Gantt opens in a session (#6248).bfilters the Gantt like the War Room — All (the default, done tickets included) · In play · Live · Planned. The filter changes only the rows drawn: the critical path, float, load, sprint intelligence and the axis stay computed over the whole sprint. In team view,oopens a developer picker with multi-select and an "unassigned" choice. The header and footer name both filters, and they are remembered per sprint for the session (#6254).- Done tickets reach the Gantt on every provider kind. On a
kind: ffproject the timeline read open tickets only, so a finished ticket vanished from its own sprint (#6253). - A sprint proposal row. One header row proposes a greedy placement of the open work into this sprint and after it — predecessors closed first, capacity per person in the measured tickets-per-day, never a nominal day — and says
greedy, never optimal.ptoggles its detail. It never writes a date (#5595).
The War Room says whether its data loaded
- A load-state slot on the title row —
■ as of 12:00Z,⋯ loading,◔ stale,◧ partial — open tickets,□ failed — offline— glyph and word, never colour alone. When a read is missing, a notice names what is missing, the numbers that depend on it read—instead of a quiet zero, andrreloads the whole board in place, keeping your cursor, scope and filters. A filtered view saysfiltered · N of M sprints shown, so a near-empty ladder never reads as a failed load (#6297). - The legend names every circle.
◉is an epic,◇a sprint,▣an initiative — none of them a wave — and the worker states◯ ◌ ● ◎ ✓ ✗are drawn on the Workers panel, never on a War Room ticket row, where an in-review worker's ticket reads⟳ Review(#6073).
ACE's voice and the rail
- Every ACE reply reaches exactly one recorded decision to speak or not. On a headless cockpit, 10 of 15 replies in one morning were decided by nobody: two settle paths raced, and the loser logged nothing and said nothing. Every settle path now decides once, every silent exit logs why, and a voice sidecar whose playback device died — after a radio stop, for example — is caught by a watchdog and rebuilt (#6213).
- The rail spectrum is the hero-car scanner. The founder found the full K.I.T.T. field too loud for the rail and picked the car itself: one row, one light with a hard front and a decaying tail, sweeping while ACE is idle and built to widen and brighten as it speaks, readable under
NO_COLORby run-length. The Radio screen keeps its own field, byte-identical (#6222, #6262). - "Speech spectrum" is a switch in Settings → Display & Flags, on by default. Off collapses the rail's spectrum, never the voice (#6223).
- The voice picker stops sending you to a product update that cannot help. On a fully updated 1.2.22 it said to update FlowForge; the preview actually needs a newer speech engine, which is pinned separately from the product. The copy now says so, and the cockpit is ready to install that engine on its next launch and restart the sidecar, on a new file — never over the running one — once it is released (#6202).
- FlowForge's CDN mirrors only each language's default voice — Bryce for English. The four English alternates download from HuggingFace, by design (#6224).
flowforge doctorgrew avoice-sidecarsrow that reports every running speech sidecar and names an orphan — the same process-table truth the next launch reaps (#6208).
Radio
- Marking a favourite works.
fwrote to disk and the screen never heard back, so nothing appeared to happen — and a second press silently un-marked it (#6199). - The radio is discoverable. It had no row in the
?help and nothing a help search could find. It has a Radio group now,/radio,/stationand/favouritefind it, andflowforge helpnames it (#6200).
Updates you can act on
- "v1.2.22 available" can be acted on. The header chip and the
Ukey read two different detectors, so on some machines the chip lit andUdid nothing. One seam now records a newer release, the chip reads⬆ vX available · U update · N what's new, and a chip-fired update showsUPDATING…or an honestUPDATE FAILED — <reason>. FlowForge Desktop's Check for Updates… is a real check now: up to date, a newer version available, or could not reach the release server — with Update now running the shipped desktop installer (#6191). - A Documents Drawer entry survives its worker's worktree being removed. An entry previewed from a worker worktree dead-ended the moment its PR merged. The drawer now falls back to the merged copy, or to the snapshot taken at preview time, says which, and marks a truly dead entry before you click it (#6062).
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 closes these gaps, each one a door that now refuses instead of a line that only logged:
- A delivered ticket can no longer be spawned again — the spawn is refused before any timer starts (#6286).
- A push can no longer silently void a GOLDEN review — it is refused, with the cost named, and proceeds only with a recorded reason (#6111).
- A sprint can no longer be created without its begin and end dates — at every sprint door, with the audit reading every ticket (#6302, #6313).
- A release can no longer be cut without its letter or a recorded reason (#6212), or with a roster change its notes do not name (#6201).
- The commit-message wall can no longer refuse commits the base already holds, because it reads the range from git (#6221).
Small things that change everything
Not every change earns a headline. These fit in a line each, and you feel them every day.
- The background-agents rail stopped printing
⋯ 129 older on fileunder live rows — a count of what it was not showing you (#6282). - The Gantt, the War Room and the radio each explain their own symbols from
?(#6248, #6073, #6200). - Radio favourites stick on the first press of
f(#6199). - Shelf sprints read as sprints, and their names stop repeating the date (#6280).
flowforge doctorreads Claude Code's outside-the-working-directory consent prompt (2.1.257 and later) and never answers it: a worker nobody is watching stops at that prompt until a human answers, so theclaude-outside-readrow reports whether it is still armed on this machine, and ACE logs one line before a wave when it is (#5897).
Designed in this release, built in a later one
These landed as ratified or ratification-ready documents — plans, not behaviour — so the next releases can be built from them rather than re-argued:
- One pace rule for behind, ahead, at-risk, percent complete and the War Room header, ADR-0138, with its implementation plan and DAG (#6294).
- Fleet read, phases 2 and 3: one work-item projection for the Workers queue and the Fleet, its grouping, and the screen spec (#6139, #6140).
- The console War Room, phases 1 and 2: the data and API contract study, and the plan and DAG (#6299, #6300).
- The epic-scope foundations — one typed scope value, and waves that hold a ticket on an open blocker outside the sprint — are in the code with no screen change yet (#6151, #6153).
Smaller, and under the hood
- The marketing-consent migration no longer rolls back on its own guard. Its first production apply did: the hosting platform grants a service role full rights on every new table, and the migration never revoked them. It now revokes before the narrow grant, so the log is append-only as designed, and the test harness models that default, so the guard can actually fail (#6183).
- ADR-0135's "the payload precedes the clock" clause is ruled to bind the recap cadence, not the stall actor, with the record of that ruling (#6118).
- A checkout test fixture was reconciled with a production hotfix that split reached and unreached API failures; no runtime change (#6225).
- The v1.2.22 letter's figures were re-derived at its tag, and its three owed images were resolved (#6177, #6112).
One setup step
- Release Letters sends only what you configure. With nothing configured, the cut hands you a
.emland that is the whole delivery. A send needs a sender (a Resend key or an SMTP relay), and a letter with images needs an image host; recipients and unsubscribe complete it. Each is set in Settings → Release Letters or withflowforge marketing set, andflowforge doctornames what is still missing. A Supabase recipient source also needs a read function in your own Supabase project (#6235–#6239). - The marketing-consent migration fix (#6183) goes live when it is applied to the database it belongs to.
Not yet
- The Release Letters tab's own "send a test to me" is not wired. It answers
✗ not sent · no sender provider is in this build yet— nothing is sent. The sender itself is built:flowforge marketing test senderprobes it and sends the one test message. The tab also offersnoneandresendas providers; an SMTP relay is set withflowforge marketing set. And its verdict line points atflowforge marketing status, which is not a verb —flowforge doctor(themarketingrow) andflowforge marketing getgive the same answer (#6240, #6237). - The voice preview still waits on a speech-engine release. Every install today runs the pinned engine that cannot preview another voice; pick and save to hear one. The same release carries two things already merged on the cockpit side: the rail spectrum moving with ACE's real voice levels — until then it shows the idle sweep while ACE speaks, never an invented spectrum — and the sidecar's exit when its cockpit dies, so until then an orphaned sidecar is reaped at the next cockpit launch, as before (#6202, #5725, #6208).
get.flowforgesoft.com/itself still answers an access-denied page. The redirect to the releases page is written and tested, and goes live when its infrastructure change is applied; the installer and update paths are unaffected (#6191).- The one pace rule, Fleet read and the console War Room are designs in this release, not behaviour — see "Designed in this release".
Agents
No roster change in this release: FlowForge installs the same 44 specialists v1.2.22 shipped (git diff --name-status -M v1.2.22 origin/release/1.2.23 -- v3/packages/core/agents is empty).
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.