Two behaviour changes to know about before upgrading.
(1) Section fan-out now starts at ~9 slides, not 15+. A 9–14 slide deck that used to be written
by one author is now authored by parallel section agents against a locked design plan and a shared
style.py. The old threshold was reasoned on TOKEN cost, where one author genuinely wins because
nothing is duplicated — but wall clock was never on the scale, and measured across five real build
sessions the build step is 40–71% of all model-active minutes, generated serially by one agent
whose context reaches ~500k by mid-build. Fan-out spends the same tokens concurrently and hands each
author a fresh ~60k context. Fanning out the BUILD deliberately does not escalate the REVIEW
shape: the sectioned critic panel still belongs to a genuinely large deck (~15+), because one
reviewer reading a 40-slide document reviews it worse — coupling the two thresholds would add
critics to every mid-sized deck and spend the wall clock the fan-out just saved. If you are
optimising for tokens rather than wall clock, the old threshold was correct and is documented as
such.
(2) render_deck.py rejects an unrecognised flag instead of resolving it to the output
directory. render_deck.py deck.pptx --gate-check --briefing used to run the hand-off gate at the
presented legibility floor — a briefing deck, whose budget is ~150 words, held to ~40 — and print
nothing either way.
Why this release exists
A user reported that one deck took ~5 hours to build. Measured across six real build
transcripts, the cause was not the critics, not the scripts and not the user:
- 83–90% of all output tokens are reasoning, not deliverables (tool payloads 9–15%, visible text ~1.5%)
- median context 440–575k per turn, p90 ~900k, hitting the 1M cap and forcing compaction
- ~1,200 tool calls per session at ~1,400 output tokens each
- every deterministic script is irrelevant to this: a full 14-slide render is 5.4s, the whole gate suite ~8s
SKILL.md already stated the governing equation — the cost of a deck is round-trips × context —
and its own measurement of a 12-page build: 122 calls averaging 1.00 tool per round-trip. The
rule was stated once, in the preamble, and never at the two places it binds. It was advisory, and it
was skipped.
Verified by rebuilding the same 14-slide deck on the new pipeline: hard lint findings 11 → 0,
and a real 2-round × 2-lens critic panel ran where the baseline had waived the critic entirely.
Round-trips 512 → 152, output tokens 1,358,258 → 154,154.
Honest bound: the critical path went from ~2.4h to roughly 2h for a 14-slide deck with the full
two-round critic. Most of what remains is subagent stages (planner, art director, section authors,
two critic rounds), which these changes do not touch. Getting under an hour requires cutting a
critic round or reducing scope — both quality trades, and neither is done here.
Fixed — three measurements that could not see what they were measuring
deckkit.measure_textgainedfont=. It never passed a face to the metric, so it always
measured in the deck default. The same command string is 4.04in in Helvetica and 5.44in in Courier
New, so a monospace line came back 26% narrow — enough to report a 9.2in line as fitting an
8.25in box. Nothing downstream can catch that: the box is then BUILT to the wrong size and every
later geometry check agrees with the box.fit_text_sizehad always takenfont=; the asymmetry
was the bug. It cost one measured build a full rebuild.lint_deckhad two definitions of "the title"._find_titleanswers it as "a TITLE
placeholder, else the first text ≥14.5pt in the top 28%". The deck-stats scan answered it
independently: top 20% of the canvas, biggest short box, no size floor at all. A title sitting
between the two bands was invisible to the stats scan, so whatever chrome sat above it won —
reproduced with a 30pt title at 1.30in and a 9.5pt margin word at 0.52in. That is not cosmetic:
title_txtfeeds the title spine, the titles-only test that the coordinator and both critic
lenses read as the deck's argument. One measured deck presented its argument as three pieces of
margin chrome. It also fed INVERTED TYPE HIERARCHY, which then advised that a correct 30pt title
was smaller than its body tier. Zero test coverage before this release.lint_deckmeasured text in a face it refused to look at. It imports deckkit, then dropped
face and weight on the very line that readsr.font, after which every Latin character was charged
a flat 0.52 em. Measured on Helvetica Neue at 26pt: uppercase runs +33% over the estimate and
narrow glyphs and spaces −44% — so it both missed real collisions and invented others. The
invented ones are the expensive half: an author rewrote two correct titles to satisfy a wrong
measurement, which is rework the gate itself caused.code_blocksays when a line will clip. It setsword_wrap=Falseso indentation survives, and
its own comment nominated the docstring's "keep snippets short" as the enforcement. A clip is
invisible to every gate — the height model stays right, the shape stays on canvas, and the end of
the line is simply not on the slide.preflight_check's mono-wrap check stopped crying wolf. It charged the whole paragraph the
monospace advance whenever any run was monospace (~20% over on alabel: <command>line), and
announced "word_wrap is off, so they run off it silently" without having looked atword_wrap.
Three of its findings on one real build were false, and separating them from the true one cost real
time. This precision is load-bearing, not tidiness: the check FAILS a build, and a builder who
learns it cries wolf routes around the one defect it exists for — a shipped install command that
copied as a 404 path.
Added — the cost of a build is measurable, and the repetition is gone
scripts/roundtrip_budget.pymeasures a build from its session transcript: round-trips, the
batching ratio (tool calls per round-trip), median context re-sent, and whether the render
self-check read the slide PNGs in one message or one at a time. Step 6 fills the hand-offcost:
line from it. It refuses to guess when it cannot find the right transcript — it used to fall
back to the newest transcript anywhere on the machine and reported a stranger's session as yours,
including "image reads 0" for a run that made 28.scripts/dispatch_brief.pywrites the deck brief once and prints a pointer prompt for every
dispatch. Measured: nine dispatches cost 41,203 output tokens at ~4,600 each, and almost all of
it was the same interview answers, paths, search cap and CONTRACT CARD retyped nine times. The
generated prompt is ~220 tokens. Second reason: it makes the contract card one artifact instead
of nine reconstructions, whichcritic-panel.mdasks for and has no way to check. It refuses to
emit a prompt while any required section of the brief is unfilled.scripts/slide_index.pyprintsslide N -> file:line functionplus each slide's plan-row
docstring. Section fan-out means the coordinator did not write the code, so a finding on slide 7
otherwise begins by grepping three modules it has never read.- Icons render ~300× faster.
rasterize()had no output cache, and cairocffi cannot find a
Homebrew libcairo, so every icon fell through to headless Chrome: 5.05s → 0.021s cold, 0.003s
warm, output equivalent (ink coverage within 0.0002). That also exposed a latent quality bug —
only the Chrome backend supersampled, so on any machine where cairosvg did load, icons silently
shipped at ⅓ resolution. All three backends now share one supersample constant, andcheck_env
stops reporting the slowest backend as a plain[ok]. - Codex image concurrency scales with the machine (
cores//3, clamped 2–4) instead of a flat 2,
against work that spends its time waiting on a hosted model rather than on a local core. A 4-core
CI box keeps exactly the old behaviour. - Batching restated where it binds. Step 5 now says to read every slide PNG in ONE message and to
apply the whole promoted fix list in ONE message, each with its reason — and the render self-check
gained a required per-slide verdict line, because batching the reads must not blur fourteen
judgments into one impression.
Not done, deliberately
SKILL.md is not slimmed. It is ~44.8k tokens of the ~302k per-call context — the smallest
term — and this repo has a recorded measured regression from exactly that change: a "lossless" split
that scored a perfect content-preservation pass, passed every automated gate, and still shipped a
deck with zero icons and a hand-rolled chart. Context size was also tested directly and dropped as a
lever: latency against context on cache-hit turns regressed at R² = 0.009, and deleting the
entire skill from context has a ceiling of ~2.4 minutes.