TL;DR. This release added no feature. It turned on three TypeScript compiler flags and two lint plugins for tests, cleared everything they reported, tightened the review gate and made CI faster. I estimated about 200 type errors for the flags and the compiler reported 4,189. The plan was rewritten the next morning and re-cut several times after that. It ended with slices cancelled, slices that had nothing to do, and a good deal of work that was never in it. Two of the three flags were cleared twice: once overnight, in work that never left the machine it was made on, and once more, in a day, on the release branch. The limit on the whole thing was not typing speed or model cost. It was one code review an hour. And the most useful thing I learned is small: an error count from the compiler can be a floor, not a total, so the only proof that a flag is clean is the type check passing with the flag turned on.
I write these posts to learn in public. This one is a retrospective, so most of it is things that went differently from how I expected, with the numbers. Where I do not have a number, I say so.
Some context. Hearso is a small real-time multiplayer trivia platform. I build it mostly alone, with AI agents doing a large share of the typing: a small software factory, set up on an open-source project and extended with my own gates, that takes a written slice of work to a commit, and an assistant in a terminal that measures, writes specs and reviews. Two companion posts cover those parts in detail: the factory, over 47 runs, and a lint autofix plan that did not survive one file. This post is about the release as a whole.
What this post covers
- What the release was for
- The plan, and what became of it
- The first flag, and an estimate that was off by twenty times
- The test cleanup, briefly
- Two flags, cleared twice
- When a count is a floor
- One review an hour
- A gate that was not actually enforced
- CI, split in four
- What I got wrong
- What is still unfinished
- Closing the loop
1. What the release was for
The next release is for product features. I wanted this one to pay for that: get the type checks, the linting and the CI into a state where a change made by an agent has to pass strict gates before I even look at it.
Concretely, four things:
- Three TypeScript flags.
noPropertyAccessFromIndexSignaturemakes you say which fields you read off a loose record.noUncheckedIndexedAccessmakesitems[0]possibly undefined, which it is.exactOptionalPropertyTypesseparates a key that is absent from a key that holdsundefined. - Two lint plugins for tests, for Testing Library and jest-dom, so tests query a page the way a user would and fail with readable messages.
- A review gate that means something. Every pull request should be read by a reviewer at the exact commit that merges.
- A CI that fits in its time limit, and dependencies without known advisories.
None of that changes what the product does. All of it changes what a mistake has to get past.
The four are not separate. The flags produce errors, the errors become pull requests, every pull request waits for a review, and CI runs on each one. A change in any of them moved the others. A gate is also only worth having if a mistake cannot get around it, and that is the test I ended up applying to every gate here.
2. The plan, and what became of it
The first plan was written on 2026-10-08. It had six slices: turn on one flag and the lint plugins, write a charter for later test consolidation, move environment variables behind a typed module, add branded types for player ids, and diagnose and fix a theme inconsistency. The other two flags were explicitly planned for the release after, with estimates of about 1,688 and about 257 errors.
The next morning it was rewritten to fifteen slices. By the end it looked like this.
Figure 1. The plan of 2026-10-09 and what happened to each slice, with the work that was added later.
| What happened | Slices |
|---|---|
| Done roughly as planned | the first flag and lint plugins, the charter, the stale type cache fix, the final baseline check |
| Done, but late or by hand | environment variables in three areas (two factory runs failed and were finished by hand), branded player ids |
| Cancelled and re-cut | the three slices built around eslint --fix, after the method was run on one file and failed |
| Nothing to do, once measured | environment variables in components (there were none), an implicit-any cleanup (no such error was left) |
| Moved to the backlog | the theme diagnosis and fix. No symptom was ever recorded, so nothing was diagnosed |
| Not in the plan at all | a CI job for Storybook, typed dataset keys, seven groups of typed records, sixteen groups of test cleanup where three slices had been, and seven slices for the two remaining flags |
Two of those rows are the ones I want to remember.
"Nothing to do, once measured." Two planned slices would have been dispatched against work that did not exist. One of them I only caught because, partway through, I had made a rule out of an instruction I gave in frustration: "Make sure we are not repeating any work that we've done in the past. And make sure we are not working on the wrong things." After that, every spec was preceded by a measurement of what was actually there.
"Not in the plan at all." The plan's later slices were written from an early picture of the errors. When the remaining type errors were finally measured by where they came from, only 33 of 1,225 had anything to do with environment variables, which is what three planned slices were aimed at. The real source was something the plan did not mention.
Both rows have one cause: the plan was drawn once, from an early picture, and the code kept moving underneath it.
3. The first flag, and an estimate that was off by twenty times
On the first day I turned on all three flags at once. I had estimated about 200 errors.
The type check reported 4,189.
I reverted two of the flags and kept one, noPropertyAccessFromIndexSignature. That left 1,735. The estimate was wrong by a factor of twenty for all three, and still by a factor of eight for the one I kept.
Figure 2. Type errors under the first flag, from the first measurement to zero on 2026-10-12. The last eight steps happened in one day.
The first real slice moved environment variables behind a typed module and took the count to 1,292. Then for three days the number barely moved, because the work in front of me was the test cleanup in the next section.
When I came back to it on 2026-10-12 the count was 1,225, every one of them the same error code. This time I measured before planning. A small script asked the compiler, for each error, where the type being read was declared:
| Where the loose type came from | Errors |
|---|---|
Record<string, unknown> and similar | 976 |
The DOM's dataset | 175 |
| Blog front matter | 47 |
| A named type with an index signature | 27 |
That table changed the plan. There were three ways to clear these: use bracket access at each of the 1,225 reads, fix each type where it is declared, or turn the flag off. The flag exists so the compiler knows which fields the code reads, and only one of the three gives it that. I chose fixing the types at the source. A loose record became a type that names the fields the code actually reads.
The first step shows why that choice matters. One declaration file that names the 31 data-* keys our code reads removed 175 errors in a single small pull request (#704). Bracket access would have been 175 edits and would have taught the type-checker nothing.
Seven more groups cleared the rest the same day, and the count reached zero at 22:56 on 2026-10-12.
There was one question I could not answer by reading diffs: did any of this change what the code does? A typing change should leave the compiled JavaScript identical. So each of those slices carried a check that compiles every changed file before and after and compares the output. That check was the right idea and was itself wrong three ways in one day. First it hid a changed string. Made exact, it treated a formatter's line break as a change, and an agent "fixed" that by adding eleven formatter-ignore comments to production files. Comparing tokens, it could not see a file's first line. A reviewer found two of those three. I come back to that pattern in section 10.
There is also a limit to what this flag bought, and the release notes say it plainly: naming a field is not checking it. The hand-written validation on request bodies and stored rows is exactly what it was. Replacing it with schemas is in the backlog.
4. The test cleanup, briefly
The two lint plugins reported 4,334 errors, all of them in test files. Clearing them took one pilot and sixteen groups, and the count reached zero on 2026-10-12.
Figure 3. Lint errors in test files, step by step. The large drop in the middle is nine groups run in one night.
I am keeping this section short because the other two posts cover it. The short version has two halves.
The first half: the original plan was to run the linter's automatic fix over 203 files. Tried on one file, that method failed six different ways, so it was cancelled and replaced with cleaning one area at a time.
The second half is the uncomfortable one. Rewriting a test to satisfy a lint rule can change what the test checks while every test stays green. A test is worth only what it fails on, so each group was reviewed by breaking the component under the test and seeing whether the test noticed. That found weakened tests in fifteen of the sixteen groups. After the groups had merged, a search for the same shapes found more: seventeen checks were restored in already-merged work, across three follow-up pull requests (#694, #697, #702). And about 127 further places where a lookup was traded for a role or moved into a helper were listed and have not been judged one by one. That is still open.
All the while, the other two flags sat finished in a place I had stopped looking.
5. Two flags, cleared twice
This is the part of the release where no single day went wrong, and the same work was still done twice.
The first time. On the first night, a second coding agent worked through the two remaining flags in a sandbox. By morning it had all three flags on and the type check printing zero errors, after touching 880 files.
None of it left that sandbox. It could not reach GitHub that night, so there was nothing to push and nothing to review. The plan at that point also put those two flags in the next release, so nothing was waiting for it. The release went forward on a different branch, one small reviewed pull request at a time, with only the first flag on. The other two were written into the configuration as false.
The second time. By the morning of 2026-10-13 the release was prepared: the version was set and the release notes were assembled from 32 fragments. In those notes, in three places, was a "known limitation": two strictness flags stay off.
Then I stated the condition I wanted the release to meet: "Make sure all the flags that we mentioned in the beginning are turned on, nothing is set to lenient only to pass checks, and all tests pass."
A limitation I had read past three times was the condition the release failed. The audit that followed measured it: with the other two flags on, 2,410 errors in 507 files. The estimates from the first plan had been about 1,688 and about 257. The release was held.
The overnight work was the obvious shortcut, and by then it no longer fitted. Four days of merged pull requests had changed 664 of the 880 files it touched, and a trial merge stopped on 191 conflicts. So the second pass started from the code as it stood.
Figure 4. Errors with the last two flags on, 2026-10-13, UTC. Six hours from the first measurement to the type check passing with both flags in the configuration.
What made six hours possible was not speed. It was that the method was already known from the first flag, and the measuring came first.
- Clear by area, with the flags passed on the command line. The configuration stayed as it was until the very end, so the project's own type check kept passing the whole time, and each slice could be reviewed and merged on its own.
- Production code first. By 10:34 no production file had an error with both flags on. The 1,679 that remained were all in tests, stories and browser specs.
- One helper for tests. A test that reads
calls[0]is saying "there is a first call". A tiny function,requiredAt(list, index, label), makes that an assertion that fails with a sentence naming what was expected and how long the list was. By the end, 1,134 reads went through it. - Nothing silenced. No non-null assertion, cast or suppression was allowed, and each slice's contract checked for them. That check had a blind spot too, which a reviewer found; it now reads the code with the compiler's parser.
Six factory runs did the bulk of it for $16.90, which is seven tenths of a cent an error. At 12:21 both flags were switched to true in the configuration and the type check passed with no flag passed by hand.
Was the first attempt wasted? Its code was, for this release. None of it is on the release branch. Whether it bought anything else I do not know, and I do not want to tell a neat story about it. The second pass did happen on a codebase where the first flag, the test cleanup and the typed records were already in, which made the remaining errors smaller in kind. What I can say with numbers is that the same two flags cost one night the first time and six hours the second, and only the second one went through review.
Leaving that work where it was had a price that was not due on the day. It came four days later, as 191 conflicts.
6. When a count is a floor
The last flag slice produced the lesson I would put first if I were only allowed one.
Every slice had a rule in its contract: the rest of the project may not gain a type error. It was measured by count. The last slice was allowed to leave 477 errors, all belonging to another slice. Its builder finished, and the count read 478.
The extra error was in a test file the slice had not touched and was not allowed to touch. It was a line that destructures a recorded call that may not be there, and it had been wrong under the flag all along. So why had the compiler never reported it?
Because TypeScript reports "this type is not iterable" once for each type in the whole program, not once for each place it happens. Another test, in the slice's own files, had the same mistake on the same type, and that one was being reported. When the builder fixed it, the compiler moved on and reported the next one.
So for that kind of error, every count I had been steering by was a floor. Fixing one can reveal another. The check that said "the count outside this slice must not rise" was failing honest work for being correct. I stopped that run by hand, fixed the newly visible line and one more like it that a search turned up, and then the count was zero.
That is one example of a wider thing I kept running into. Here are the counts in this release that turned out to be wrong, and why:
| What I believed | What was true | Why |
|---|---|---|
| About 200 type errors for three flags | 4,189 | an estimate, never run |
| About 1,945 for the last two flags | 2,410 | an estimate from the first day |
| The lint total | 7 too high, every time | the linter also read a folder Git ignores |
| The type-error baseline | 37 too high | the same folder |
| An import costs 27 ms a file | about 74 ms | timed alone, not from the suite's own total |
| "No errors left outside this slice" | one more appeared | the compiler reports that error once per type |
| Ten planted defects | I wrote "eleven" | written from memory, not from the output |
Each row is the same mistake in a different coat: a number that was estimated, remembered, or measured somewhere other than where it would be used. The fix that worked every time was to measure the real thing in the real place. For the flags that meant one thing: the type check exits zero with the flags in the configuration. Not a count.
Turned around, the question to ask of any of these numbers is short. Where could this be wrong and still look right?
7. One review an hour
I expected the constraint on a release like this to be how fast agents could produce changes, or what they cost. Neither was close.
Every pull request goes through an automated code reviewer before it can merge. That reviewer has two limits. It will not read a pull request of more than 150 files. And it has an allowance of included reviews that depends on recent use. Over this release the allowance went from four reviews an hour, to three, to two, to one an hour. I decided not to raise the spending cap: "we will have to be more strategic so that we can get 2 birds with one stone."
So on the last day the release moved at exactly the reviewer's pace. Nothing upstream of it could make the release faster, because every change had to pass through it. Reviews were asked for at 09:04, 10:04, 11:04, 12:05 and 13:06, each one 61 minutes after the last, because the hour runs from when a review is asked for, and the reviewer's own message says the allowance is set by review attempts over the past seven days. An early request is refused and is still an attempt.
That changed how the work was packed. With one review an hour and a 150-file ceiling, the question for every pull request became: what is the most that can honestly go into one review? The last four pull requests were 140, 127, 146 and 131 files. One of them carried a test slice, a dependency update and a CI change together, because each alone would have cost an hour.
It also changed what a mistake cost. I pushed a commit onto a pull request five minutes after it had been reviewed. That moved its head, so the review no longer covered what would merge. Putting it back took a force push and about half an hour, on a day with one review an hour to spend.
Was the review worth an hour each time? The record says yes, with numbers.
Figure 5. The 108 review threads on the 54 pull requests into the release branch, by reviewer and severity. 24 pull requests had none.
Across 54 pull requests, reviewers opened 108 threads. Most were minor. A few changed the release:
- A reviewer pointed out that a link with no
hrefis not a "link" to a role query. That one sentence named a kind of weakened test that none of my own checks contained. Searching for its shape found eleven weakened tests in a pull request that had already been reviewed clean twice. A later search for the same family of change, a tag traded for a role, found thirteen more in work that had already merged. - A second reviewer found that the tool I used to prove "the compiled output did not change" was hiding a changed string.
- A reviewer found, twice, that tests could not tell an absent key from a key holding
undefined, which is the exact difference one of the flags exists to enforce. The standard equality assertion treats them as equal. - Two findings rated Major were about the review gate itself, which is the next section.
The pattern I notice is that a second reader kept finding what the first reader's method did not contain. My own review was a list of ways to break a component. A reviewer who thinks of a break that is not on my list is worth more than a reviewer who agrees with me.
8. A gate that was not actually enforced
While preparing the release I found that the review gate, the thing I had been waiting an hour at a time to satisfy, was not required.
The gate works by posting a status called reviewed on a commit. The branch rules required two other checks to merge, and not that one. Nothing had been stopping a merge with reviewed red. One pull request had in fact merged that way. An audit of the release branch found that of its 48 merged pull requests at that point, 6 had merged at a commit no reviewer read, and 3 commits had been pushed straight to the branch.
So I had been treating a convention as a control. The question that would have found it on the first day is the reverse of the one I had been asking: can an unreviewed pull request merge? The fix had three parts.
- Require it. The status is now required on release branches.
- Handle the pull request that is too big to review. A release's own pull request is always over 150 files; the previous one was 1,186. A required review would have blocked every release. So the gate now reads an oversized pull request by its parts: it passes if every pull request merged into it was reviewed at the commit that merged.
- Do not let the gate vouch for itself. This is where the two Major findings came in. In its first version, the gate accepted its own
reviewedstatus on a part as evidence. That status is posted by the automation's shared token, which any workflow can use. Then it accepted any status with the reviewer's name on it, without checking who posted it. The reviewer caught both.
The second finding is worth stating as a rule, because I broke it without noticing: a gate's evidence must be something the thing being judged cannot write.
I had one more instruction for this work, after seeing how many of the gate's runs were red: "Don't be lenient on the gate. We need to be strict so that we don't have to keep fixing the gate in the future." A run that says "not reviewed yet" while a review is in progress is the gate working. Those red runs stay visible.
Under the new rule this release's own history reads red, because of those six early merges and three direct commits. I have not decided how to settle that, and I would rather say so than quietly turn it green.
9. CI, split in four
The last gate on the list was CI itself. The job had two problems, and I asked for both to be fixed on the last day.
It type-checked twice. A separate type-check step ran, and then the production build ran the same compiler over the same files again. The step was removed. The build is the type check, it runs before the tests, and a test now fails if anyone tells the build to skip type errors.
It was about to time out. The unit tests ran as one step that had taken 670, 1,223 and 1,242 seconds in three measured runs, inside a job limited to 25 minutes. The suite had grown to within a few minutes of failing for being slow.
I was offered a cheap fix, raising the limit, and chose the other one: split the tests across four parallel jobs.
Figure 6. CI before and after splitting the unit tests into four parts. It is faster to wait for and not cheaper to run.
The first real run took 10 minutes 31 seconds. The four parts ran 4,787, 4,753, 5,221 and 5,281 tests, and the merged result reported 20,042, which is their sum and matches a whole run. It also used 27 job minutes and six installs where there had been one. It buys waiting time and distance from the limit, and it costs more.
The interesting part was not the split. It was three ways the new shape could have passed a commit it should fail, each of them silent:
- A skipped job counts as passing. On GitHub, a job whose dependency failed is not failed. It is skipped, and a skipped check satisfies a required-check rule. A naive split would have let a failing test merge with a green tick. The final job now runs no matter what happened before it, and its first step fails unless everything before it passed.
- A merge of fewer parts passes. Merging the results of three of the four parts exits successfully with a quarter of the tests simply missing. I tried it. So the parts are counted by name before they are merged.
- A part nobody runs passes. Writing the split as "2 of 5" next to a list of four parts runs four fifths of the suite. The total is read from the list itself, and a test holds the two together.
Those are tested by running the workflow's own shell steps, not by reading the file. Ten defects planted in the workflow one at a time each failed a test. One survived the first version of the tests, which is how I found the three cases I had missed.
Dependencies got the same day. GitHub listed 20 advisories. Eighteen are closed by one framework patch release and five transitive updates, with no forced overrides. Two have no patched release and stay open, and I chose to leave them visible rather than dismiss them.
10. What I got wrong
The assistant I work with keeps a running record, and it numbers the lessons. By the end there were 148. A lot of them are mine as much as its, because I approved the plan and the instructions. These are the ones I would rather not repeat. Read together they are less a list than one loop: I acted on a picture, the picture was old, and the check that should have told me had never been tried against a failure.
- I estimated where I could have measured. 200 against 4,189. A dry run of the compiler takes under a minute.
- I planned slices from an early picture and did not redraw it. Two slices had nothing to do and three were aimed at 33 errors out of 1,225.
- I read a known limitation three times without asking whether I accepted it. "Two flags stay off" was in the release notes. It was also the one thing I did not want.
- I built rules that caused the problem they were meant to prevent. A cap on lint suppressions made an agent weaken tests to stay under it. An ordering of preferred queries moved tests off the hooks they depended on. The notes count three times a rule in a spec produced the loss it was written to stop.
- I trusted checks I had not tried to fail. One check ended in a print statement, so its exit status was always success. One used a search pattern that matches nothing in this repository's Git. One compared counts, which cancel out. One could not see the last character of a line.
- I treated "it passes alone" as a diagnosis. One slow test failed four times over three days when the machine was busy, and each time it was rerun and left. When someone finally asked why, the answer took minutes: six simulations sharing one time limit.
- I let two things run at once on one machine. Reviewing one run while the next was verifying pushed that timing test past its limit and cost a fix loop with nothing to fix.
- I pushed to a reviewed pull request without reading it first.
- I wrote release notes early and did not reread them when the release changed. They said in four places that two flags stay off, including on the public changelog page, after the flags were on.
- I gave the release a name it did not earn. Its branch is called "leaner tests". Measured against the development branch on the morning of the last day, it had added 15,222 lines of tests and removed 7,839, and added 3,011 lines of production code and removed 840. It deletes no file. The consolidation it is named for is planned, not done.
One thing went wrong that was nobody's mistake and still taught me something. My laptop crashed in the middle of the last night, with a review request a minute from being sent. The session that held the state was gone. A new one read the state back from the written record about twenty minutes later, because the record had been kept as the work happened. What lives only in a session ends with it.
11. What is still unfinished
The release shipped on 2026-10-13. Its pull request merged into the development branch at 16:45 UTC, and it was promoted to production at 17:58. Getting it there added three things to this section.
The browser journeys were the last check and the hardest to turn green. On the release's pull request four of them failed on a measurement that comes out differently on my Mac and on the Linux runner, and a fix (#730) cleared those four. On the promotion, two other journeys ran out of their 30-second limit twice and passed on the third attempt, with no code changed in between. A check that passes one time in three is measuring the runner as much as the product.
The visual review's status was red, and it is still red. Four stories pass our own story run and fail in the hosted one, because ours draws a story once and the hosted one draws it at two widths, in light and dark, under reduced motion. That status had been red on every build since the last release, which is how four real failures sat unread. The fix is written and held for the next release.
And CI does not start by itself for a promotion into the main branch, so the check that branch requires had to be started by hand.
Before any of that, the release notes carried a sentence headed "Not in it". I think it belongs here unedited in substance. The first two lines still stand. The last two were written before the release's own pull request ran, and the paragraphs above are what became of them:
- Two dependency advisories have no patched release and stay open.
- About 127 test lookups that the cleanup traded for roles or moved into helpers have not been judged one by one.
- The rewritten Storybook CI job has not yet done its work on GitHub. Every run of it since the rewrite has been a skip, so "is it faster" has no answer yet.
- The browser journeys run on the release's pull request into the development branch, not before it. A check that runs last finds its failures last, and they are not proven yet.
And from my own list:
- The release's own review status reads red under the rule this release introduced.
- The review requirement is on release branches only, not yet on the two main branches.
- Naming the fields a record has is not validating them.
- Nothing measures mutation testing here, so "the tests are as strong as before" rests on hand-written breaks, which only find what someone thought to try.
- I do not have a total for the time spent waiting on reviews, or for what the reviews cost. Only single waits were recorded.
12. Closing the loop
The title asks what it cost, so here is the answer in two parts.
What I can count: six calendar days, 54 pull requests merged into the release branch, 923 files changed, $95.82 for 47 factory runs, 108 review threads, and a last day that moved at one code review an hour.
What I cannot count: what the code reviews and the terminal sessions cost in money, and the night spent on a first attempt that never shipped. I did not record the first two, and I am not going to put a number on the third.
What the release leaves behind is a set of checks. Each line is what it was at the start, then what it is now.
- Type errors under the three flags: 4,189 at the first measurement, zero now. All three flags are on, and a test fails if any of nine strictness settings is switched off.
- Lint errors in test files: 4,334, then zero.
- Unit tests: 19,833, then 20,087, all passing.
- CI: one job that took 16 to 27 minutes, then four parts that took 10 minutes 31 seconds on their first run.
- Dependency advisories: 20 open, then 18 closed and 2 left open because no patched release exists.
- Review: a convention, then a required status on release branches that judges a large pull request by its parts.
None of this measures whether the code is better, so I do not claim it. What changed is where three kinds of mistake stop. Reading a field by name that no type declares, reading an item from a list or a record as if it were always there, and setting an optional property to undefined now fail the type check before they merge. That is one kind of mistake for each flag.
What I take from it is narrower than "be more careful". Every time this release went wrong, there was a number involved that had been estimated, remembered, or measured in the wrong place. Every time it went right, someone had run the real thing first: one file through the autofix, the compiler asked where each error came from, a break applied to a component, three of four test parts merged to see what happens.
The flags are what the release was for. The habit is what I am keeping.
Key takeaways
- Run the compiler before estimating. My estimate was off by twenty times and the measurement takes a minute.
- Ask where errors come from, not only how many there are. One declaration file removed 175 of them.
- Clear a flag by area with the flag passed on the command line, so the project keeps passing and each slice can merge on its own.
- A compiler's error count can be a floor. The proof is the check passing with the flag on.
- Find your real bottleneck. Mine was one review an hour, and it changed how every pull request was packed.
- A convention is not a control. Check that the gate you wait for is actually required.
- A gate's evidence must be something the judged thing cannot write.
- A skipped check can pass a required rule. Make the final job run regardless and judge what came before it.
- Reread what you wrote early. Release notes, limitations and names are claims, and they go stale.
- Write the state down as you go. A crash cost me twenty minutes, not the night.
- Finished work that is not merged goes stale. A night of it met 191 conflicts four days later.
What I'd do next
- Judge the 127 lookups, or decide in writing not to.
- Run a real mutation tool on the areas the cleanup touched, so "as strong as before" is a measurement.
- Require the review status on the main branches, once the rule for large pull requests has run there.
- Replace the hand-written validators with schemas, starting where the typed records showed the most shapes.
- Record review waiting time as a number, since it turned out to be the constraint.
- Do the consolidation the release is named for.
Evidence
All times UTC unless a line says otherwise.
- The plan: the plan file's history in Git. Six slices at its first commit (2026-10-08T16:41:01-05:00), fifteen at the rewrite (2026-10-09T09:38:08-05:00), and the current file for what became of each. The other two flags planned for the following release with estimates of about 1,688 and about 257. "Only 33 of them read an environment variable" is the plan's own sentence.
- The first flag: the metrics file and the incident log: 4,189 with three flags on against an estimate of about 200; 1,735 after two were reverted; 1,292 after the environment module; 1,225 on 2026-10-12, all one error code, 735 in tests and 490 in production files; then 1,050, 864, 772, 660, 412, 283, 159 and 0, with the pull request for each step (#704, #705, #706, #707, #708, #710, #712, #714). Sources of the 1,225: 976, 175, 47 and 27.
- The emit check: wrong three ways, in the record for 2026-10-12 and incident 30; eleven formatter-ignore comments in five production files, removed in review of #708.
- Lint: 4,334 at the start in tracked files, then 4,094, 4,063, 3,980, 3,946, 3,706, 1,286, 946, 764, 493, 218 and 0 after #701 on 2026-10-12. Seventeen checks restored after merge: thirteen in #702, one in #697, three in #694. "About 127 places" is the changelog's Known limitation.
- The overnight attempt: the retrospective dated 2026-10-09 (all three flags on locally, zero errors, "not pushed from this machine"); 182 local branches named for the two flags; the newest is 201 commits and 880 files ahead of the development branch (
git rev-list --count,git diff --shortstat). None of those commits is on the release branch (git merge-base --is-ancestor). Why it stayed there: the same retrospective says the work was not pushed because that machine could not resolvegithub.com, and on 2026-10-09 it was left dormant because the plan had those two flags in the next release. On 2026-10-13, 664 of its 880 files had been changed again on the release branch and a trial merge gave 191 conflicts. - The second pass: the record for 2026-10-13: the condition at 06:24; 2,410 errors in 507 files at the audit (2,133 in 359 files for one flag alone, 271 in 187 for the other); 1,983 at 07:55; 1,679 with none in production code at 10:34; 789 at 10:58; 312 at 11:44; both flags
trueand the type check passing between 12:16 and 12:21. Reads throughrequiredAt: 650, 281 and 203. Six factory runs, $16.90. - The floor: the last slice's run, stopped in its first fix loop with the count at 478 where 477 were allowed; the error at one line of one test file under
lib; one more of the same shape found by search. - The wrong counts: the lint and type-error totals inflated by 7 and 37 (incident 10); 27 ms against about 74 ms (incident 7); "eleven" for ten (lesson 131).
- Reviews: the reviewer's allowance at four, three, two and one an hour, from the daily records; eight refusals recorded by time; requests on 2026-10-13 at 09:04, 10:04, 11:04:24, 12:05:36 and 13:06. Pull request sizes from GitHub: #725 140 files, #726 127, #727 146, #728 131. 108 review threads on 54 pull requests, #670 to #728 (57 Minor, 8 Trivial, 3 Major from the automated reviewer; 40 from the second reviewer); 24 pull requests had none. The review of #730, two Trivial threads, came later and is not in the 108.
- The gate: the record for 2026-10-13 at 06:29 (only two checks were required; one pull request had merged with the status red); the audit in the gates document (48 merged pull requests, 6 at an unreviewed head, 3 direct commits); the previous release's pull requests at 1,186 files each; the two Major findings on #722, carried into #724.
- CI: the unit tests step at 670, 1,223 and 1,242 seconds in three runs of the single job, limited to 25 minutes; run 38390751372 for the four parts: 247, 247, 246 and 246 files, 4,787, 4,753, 5,221 and 5,281 tests, merged to 986 files and 20,042 tests; 3m30s for the checks job, 3m52s to 6m06s for the parts, 48 seconds for the last job; 10m31s in all, 27 job minutes, six installs. Three of four parts merging with exit 0 was tried by hand. Ten planted defects, one surviving the first version of the tests.
- Dependencies: 20 advisories listed, 18 closed by
next16.3.3 to 16.3.8 and five transitive updates;bracesandsprintf-jshave no patched release. - Size: 54 pull requests merged into the release branch between 2026-10-09T00:12:02Z and 2026-10-13T15:18:42Z (#670 to #730), and #729 open. Against the development branch the release branch changes 923 files and deletes none (
git diff --shortstatat 918802d8). The line counts (production +3,011 and -840, tests +15,222 and -7,839) were measured earlier that day at 915 files and not again. Unit tests 19,833 on 2026-10-10 and 20,087 in 988 files on 2026-10-13 (CI run 38420974432). - The release pull request: #729. Browser journeys, first run: 4 failed and 190 passed, all four failures in one spec that measures the page's width. After #730: 192 passed, 1 passed on retry, and 1 failed on an accessibility scan that timed out; started again at 15:36, that job passed. #729 merged at 16:45:57Z. On the promotion, #732, the journeys failed twice (three failures, then two, both times including the same two journeys at their 30-second limit) and passed on the third attempt; it merged at 17:58:15Z and the production deployment succeeded at 17:59:55Z. CI on #732's head was started by hand (run 38433879853). Chromatic's build 11 lists four stories as failed; the fix is the draft pull request #731.
- Cost: the factory's database: 47 sessions, $95.82. No figure is recorded for the reviewers or for the terminal sessions.
- The crash: the record for 2026-10-13: the laptop down by 05:28, a new session reading the state back at 05:48.
Nothing above contains a credential, a token or a player's identity.