Skip to content
All releases

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 your LETTER.md and 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 writes render/LETTER.eml (and LETTER.pt-BR.eml when 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 that mailto: gives Gmail no one-click button (#6239).
  • The same table, from the command line. flowforge marketing get and set read and write the tab's keys (a secret is refused on the command line), flowforge marketing test images|sender|recipients|unsubscribe proves each seam — test sender probes the provider and sends exactly one message, to your own seed address — and flowforge 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 doctor grew marketing and marketing-dns rows (#6235, #6241).
  • Where the send happens. The cut reads your Release Letters settings and prints one line — none (the .eml is the delivery), partial (every missing key named), or ready — and never refuses the cut over it. The send itself runs at flowforge release promote, after the flip, once the pages are live: a seed to you, a y/N for the batch, batches under a daily cap, and a send log that lets a re-run skip everyone already accepted. A partial setup refuses the send, never the promote. flowforge marketing send <version> is the same step on its own, and release-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 cut refuses (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 through release-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 promote now 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 ### Agents block 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-audit passed 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 sprint feature emit --live mints — before any call to your ticket system. flowforge sprint-audit reads every ticket of a sprint, open and closed. flowforge sprint-dates is the one fixer: it writes a begin and a due through one path, dry-run by default, and --derive-due takes 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's sprint-dates row 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 on main (#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-DD turns 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 its Depends-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 absent until two verbs were typed by hand. flowforge install and flowforge update now 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 resolves origin/<lane> first, notes (checkout N behind), and names a plan landed locally and not yet pushed (#6198).
  • Shelf sprints read ◇ SPRINT in 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 pane line (#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 main comes 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 ports main into the lane at the end of every pass through the down-merge verb, never duplicating an open sync PR; every attempt is recorded on merge-advance.log, and the lane-drift doctor 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 cleared answers the question for a worker, and flowforge review voided plus the golden-void doctor 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 unacked and a verdict-unacked doctor 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 reads needs your action with 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=bug ticket with [EPIC] in its title stayed open after its PR merged. The kind= 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 its kind=epic marker 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 /float or /critical path finds it. ? legend joins the footer at every width, and ACE mentions it the first time the Gantt opens in a session (#6248).
  • b filters 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, o opens 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: ff project 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. p toggles 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, and r reloads the whole board in place, keeping your cursor, scope and filters. A filtered view says filtered · 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_COLOR by 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 doctor grew a voice-sidecars row 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. f wrote 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, /station and /favourite find it, and flowforge help names it (#6200).

Updates you can act on

  • "v1.2.22 available" can be acted on. The header chip and the U key read two different detectors, so on some machines the chip lit and U did nothing. One seam now records a newer release, the chip reads ⬆ vX available · U update · N what's new, and a chip-fired update shows UPDATING… or an honest UPDATE 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 file under 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 doctor reads 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 the claude-outside-read row 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 .eml and 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 with flowforge marketing set, and flowforge doctor names 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 sender probes it and sends the one test message. The tab also offers none and resend as providers; an SMTP relay is set with flowforge marketing set. And its verdict line points at flowforge marketing status, which is not a verb — flowforge doctor (the marketing row) and flowforge marketing get give 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 update on an existing install, or curl -fsSL https://get.flowforgesoft.com/install.sh | sh for a fresh one. Both write the same release binary to /usr/local/bin and ~/.flowforge/bin together. flowforge version update does not update anything — it prints the installed version and exits.

v1.2.23 — 82 PRs, 132k lines, made with FlowForge