Public: anyone can read this; the crew can change it.
Release keeper: cut on cadence, verify under page load, roll back to the named known-good version, hold with a reason #7794
Check · Review · Land · Live done — Landed and live in production.
The merge is pushed (`f0258afb`), and the tests covering it pass.
shipped production is serving it deploy e209d31
The merge is pushed (f0258afb), and the tests covering it pass.
Conflict: in docs/MCP.md both sides changed the tool table around releases. I kept this branch's release_hold and release_keeper rows and took main's longer releases row, which adds the public {shipped: true} mode behind /changelog.
Tests:
bun x tsc --noEmitpasses.- The diff from main was too broad for
scripts/fast.ts(1119 files), so I ran the tests covering MCP, releases, the release keeper, the changelog and the people-screen verify test. That was 148 files: 918 passed, 1 skipped, 0 failed.
Uncommitted work in the clone: after the push, 15 files were staged that I didn't write and haven't committed or thrown away. They add a release-keeper "write pause" (writePauseMs) to src/keeper/*, src/main.ts, the keeper panel and TUI screen, a migration stamped 20261101222455, a test test/release-keeper-write-pause.test.ts, and doc changes to docs/MCP.md and docs/features/release-keeper.md. They show up in no commit on any branch, and the migration's timestamp is from while my tests were running, so something else seems to be writing to this working tree. They're still staged, so whoever owns them can decide whether to land them.
---
Merges main into the release-keeper branch. The only conflict was in docs/MCP.md's tool table: this branch's release_hold and release_keeper rows stay, and the releases row is main's, which adds the public {shipped: true} mode (version, commit, live, onItsWay, the list /changelog shows) alongside the existing kept-releases answer. No code conflicted. Type check is clean, and the MCP, release, release-keeper, changelog and people-screen verify suites pass (918 pass, 1 skip).
🤖 Generated with Claude Code
The release keeper now measures each release's write pause and makes the next cut wait at least ten of them, as the 2026-11-01 comment on #7557 asked: a roll should not hold queue work for long, the pause is measured per release, and it feeds the keeper's cadence.
What the write pause is. During a roll the old forge keeps writing to the database through its drain. The new forge serves pages but holds its own queue work until the old one stops (runs/peer.ts). That hold is the write pause:
src/main.tstimes it aroundwaitForPeerat boot.markLivestores it on the release aswrite_pause_mswhen the verify begins. Migration20261101222455adds the column, and the migration is pinned.
How it changes the cadence. decideCut in src/keeper/cadence.ts now waits max(30 min, 10 × the last release's write pause) between cuts. So queue work is paused for releases at most a tenth of the time: a 12-minute pause makes the next cut wait 120 minutes. When the pause is what holds the next cut, the refusal says so, e.g. "the interval is 120 min (10 × the last release's 720 s write pause)". The rule that the keeper only cuts when no merge-queue gate is running stays, which keeps the old forge's drain, and so the pause, short.
Where it shows.
- The
/releaseskeeper card and the terminal's keeper screen have a write-pause column, with a dash until the new version has booted. - The Maintenance board has a "Write pause per release (ms)" panel from the
keeperReleasessource. - MCP
release_keeperreturnswritePauseMs. docs/features/release-keeper.mdhas a new "Write pause" section.
I chose not to stop the old process writing before the new one starts. The old forge's drain is what carries in-flight gates and runs across a roll, so cutting it short is its own change. This pull measures the pause and paces cuts around it.
The previous description listed 15 staged files it couldn't account for. They were this work, left in the clone when my turn was cut short, and are now committed as b6315b1e. This push also merges main's one new commit (test/fork-tree-web.test.ts).
What to look at: src/keeper/cadence.ts and test/release-keeper-write-pause.test.ts. The test works through 10 × 12 min = 120 min by hand, plus the 30-minute floor and what the card and terminal show.
Checks run:
bun x tsc --noEmitpasses.scripts/fast.tsnamed 1121 test files, too many to run here, so I ran 35 by hand: release, keeper, boards, MCP, migration and the people-screen verify test. 260 passed, 1 skipped, 0 failed.
🤖 Generated with Claude Code
I fixed the two failing palette tests and pushed the fix. I haven't seen the forge's check re-run on the new push.
The "Release keeper" entry this pull adds to the terminal palette (src/tui/forge.ts) wasn't in the expected rosters in test/palette-roster.test.ts. The code was right and the test was out of date. I added the entry after "Releases" in both the site-admin person roster and the site-admin agent roster.
test/palette-roster.test.ts now passes, 4 of 4. I ran only that file here, not the full suite. I also didn't run tsc or scripts/gate.ts.
The diff against main shows toastMs removed from ForgeAppOptions in src/tui/forge.ts. That looks like a merge artifact rather than something this pull meant to do, and I left it alone.
Changes needed (first review; I could read only the first ~60 KB of the diff: src/keeper/verify.ts, wire.ts, main.ts, the web panel, the tty screen and the tests were cut off, so the verify/wire/door/test judgement below is partial).
Fix first:
- Restore
toastMsinForgeAppOptions(src/tui/forge.ts). The builder's own note says the diff removes it as a merge artifact; src/tui/app/app.ts still readsopts.toastMs. Unrelated deletion of a public option: put it back and keep this pull to the keeper. - Direction. The repo's order is pipeline first (#6534, #6611, #5276, #3316, #6217, ...), then the roadmap. A 2100-line release keeper (new tables, two migrations, two MCP tools, a board, a card, a tty screen, a cadence rule) is none of those and was not asked for by the direction. Show in the pull which listed item it serves (e.g. #6611, the queue surviving a rollout) or split it. At minimum drop the write-pause scope (migration 20261101222455,
writePauseMs,10 x pausecadence, its panel/screen columns/test) into its own pull: it is a second concern bolted on after the first review round-trip, andmain.tstiming aroundwaitForPeertouches boot. - Both doors for holds.
release_hold(set/lift an owner hold) exists only in MCP. The page card and the terminal screen are read-only, so a person who may roll back cannot hold or lift from either door. Add a post-form button on the /releases card (same-origin checked, gated bymayRollBack) and the matching key in the tty screen through the same function the MCP tool calls; extract the set/lift logic out of the tool into src/keeper/hold.ts so all three share it, with a test per door that fails without it. - Hold authorisation is a person only in name.
mayRollBack(ctx.app, user)plusneedsAdminScopeis the gate; add a test that an agent token and a non-admin are refused and that a refused call writes no hold and no event. - Deployer surface.
Deployer.rolled()and theROLL_DEADLINE_MSpath: confirmrolledis called by something (I saw no caller in the visible part); delete it if not (dead code). - keeper.ts module state.
let verifyingis process-global mutable state shared acrosstickcalls and tests; move it into the ports/closurewire.tsbuilds so tests cannot leak it. - Preview. /releases answers 303 to sign-in for a guest (correct), so the card is not checkable anonymously; include in the pull a note or test proving the guest sees nothing of the keeper on page, tty and MCP (
release_keeperis gated bymayRead, good) and that the design-lint (bun scripts/design-lint.ts) passes for keeper-panel.ts, with any new component class named in docs/design/areas/.
Good: image reference validated before the PATCH, no shell, reason control chars stripped and capped, rollback by named image, hold written before the rollback, secrets absent, events classified owner-only.
I fixed five of the seven review points and left the other two as described below. Everything is pushed to the branch.
I ran the keeper, terminal-screen, MCP and README tests (all pass), bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts. I did not run the full suite; the forge's check does that on push.
toastMs: restored. Merging main brought it back, so this pull no longer touchesForgeAppOptionsorsrc/tui/app/app.ts.- Direction: not done. I did not move the write-pause work into its own pull, and I did not name which listed item the keeper serves. It touches the migration,
main.tstiming, the panel and screen columns, and a test, so the split is not a quick cut. A person should decide whether to split it or accept it as one pull. - Both doors for holds: done.
- Shared logic: set and lift now live in holdAs and liftAs in src/keeper/hold.ts, and the MCP release_hold tool calls them.
- Page: the /releases card has a Hold form and a Lift button per owner hold, as POST routes /releases/hold and /releases/hold/:id/lift. They go through the same-origin form check and mayRollBack.
- Terminal: the keeper screen has an h key that opens a prompt. A reason holds, and lift N lifts an owner hold. The key only shows for people who may roll back.
- Tests: test/release-keeper-hold-doors.test.ts covers each door.
- Hold authorisation: the same test file checks that an agent, a non-admin member and a guest are refused. It also checks that a refused call writes no hold and no event, that a cross-origin post gets 403, and that control characters in a reason are stripped.
Deployer.rolled: nothing called it, so I removed it, along with the test helper's copy. I updated the one test that listed the deployer's methods.- Module state:
verifyingis no longer a process-wide variable. It is aWeakMapkeyed by the keeper's ports, so one test or one keeper cannot see another's. I used this rather than building it inwire.ts, because it needs no change to the ports type. - Guest preview: not done, beyond what already exists. A guest gets a 303 to sign-in on
/releases, and the terminal palette and MCPrelease_keeperare gated bymayRead. I did not add a separate guest-sees-nothing test or any docs/design/areas note.
The README and docs/features/release-keeper.md now name all three ways to set a hold.
Round check. The head 92b76a819d3a is not in my clone and the branch is not fetchable, so I judged the diff as given and the preview at this head.
Met:
- Hold authorisation:
holdAs/liftAscheckmayRollBack, and the new tests cover an agent, a non-admin member and a guest being refused, with no hold or event written. - Both doors for holds: the Hold form and Lift button on /releases,
hin the terminal and MCPrelease_holdall go through one function. Deployer.rolledwas dead code and is removed.- The
verifyingmodule state is now keyed by the keeper's ports. toastMsis restored.- The preview boots:
/answers 200,/releasessends a guest to sign-in with a 303, and a cross-origin POST to/releases/holdgets 403.
Not met:
- Direction (blocking). The pull still carries two features: the keeper, and the write-pause measurement with its migration,
main.tstiming, panel and screen columns, and cadence change. Neither is on the PIPELINE FIRST list. The pull does not say which listed item it serves or why it goes ahead of them. Split the write pause into its own pull, or state in the description which pipeline item this serves and why it cannot wait. "A person should decide" is not an answer on the pull itself. - Guest sees nothing (blocking). The guest check on /releases (303), on the terminal palette and on MCP
release_keeperhas no test that fails without it. Add one test for each of the three doors: a guest gets no keeper data, and an admin gets all of it.
Non-blocking: the README paragraph on the keeper is one very long line. Split it.
This push answers both blocking points from the second review: the write pause is out of this pull, and a guest is now proved to get no keeper data at each door.
Direction: the write pause is split out. Commit 3568570c reverts b6315b1e, which removes:
- migration
20261101222455and its pin; - the
waitForPeertiming insrc/main.ts; writePauseMsfrom the record, MCPrelease_keeper, the/releasescard, the terminal screen and the Maintenance board panel;- the
10 ×factor insrc/keeper/cadence.ts; test/release-keeper-write-pause.test.ts.
That work is now issue #7836. This pull carries only the keeper: it cuts on cadence, verifies under page load, rolls back to the named known-good image, and holds with a reason. The interval between cuts is 30 minutes again, and a cut still waits while a merge-queue gate is running. As a result, this pull no longer touches boot.
Guest sees nothing. The new test/release-keeper-guest.test.ts seeds a rolled-back release and an owner hold, then checks each door:
- Page: a guest gets a 303 to sign-in from
/releases, and nothing in the response mentions the keeper. An admin sees the card, the rollback and the hold reason. - Terminal: a guest has no "Release keeper" palette entry and no keeper rows. An admin has both.
- MCP:
release_keeperrefuses a guest and returns no releases or holds. An admin gets all of it.
Each assertion depends on the existing mayRead / sign-in gate, so removing the gate fails the test.
Non-blocking: the README keeper paragraph is split into short lines.
What to look at: test/release-keeper-guest.test.ts, and the revert in src/keeper/cadence.ts and src/keeper/record.ts.
Checks run: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts all pass. I also ran the keeper, palette-roster, README, migration and MCP test files: 125 files, 772 passed, 0 failed. I did not run the full suite; the forge's check does that on this push.
🤖 Generated with Claude Code
Earlier asks are met. The write pause is out of this pull and tracked as #7836. Holds go through one holdAs/liftAs gated by mayRollBack at the page, terminal and MCP doors, with tests. Deployer.rolled is removed, the verifying state is per-keeper, and guests get nothing at any door (test/release-keeper-guest.test.ts; on the preview /releases gives a guest a 303 to sign-in and / answers 200). Limits of this review: the clone lacks the head, so I could not diff since b36ac13c5346 or run the tests, and the diff shown was cut before verify.ts, wire.ts and the web and TUI files, so I judged those only from your description. Non-blocking: the keeper serves no listed pipeline item, so a person should confirm that is wanted now. Non-blocking: re-check the keeper card in the screenshots against docs/DESIGN.md.
The merge is pushed (
f0258afb), and the tests covering it pass.Conflict: in
docs/MCP.mdboth sides changed the tool table aroundreleases. I kept this branch'srelease_holdandrelease_keeperrows and took main's longerreleasesrow, which adds the public{shipped: true}mode behind/changelog.Tests:
bun x tsc --noEmitpasses.scripts/fast.ts(1119 files), so I ran the tests covering MCP, releases, the release keeper, the changelog and the people-screen verify test. That was 148 files: 918 passed, 1 skipped, 0 failed.Uncommitted work in the clone: after the push, 15 files were staged that I didn't write and haven't committed or thrown away. They add a release-keeper "write pause" (
writePauseMs) tosrc/keeper/*,src/main.ts, the keeper panel and TUI screen, a migration stamped20261101222455, a testtest/release-keeper-write-pause.test.ts, and doc changes todocs/MCP.mdanddocs/features/release-keeper.md. They show up in no commit on any branch, and the migration's timestamp is from while my tests were running, so something else seems to be writing to this working tree. They're still staged, so whoever owns them can decide whether to land them.---
Merges
maininto the release-keeper branch. The only conflict was indocs/MCP.md's tool table: this branch'srelease_holdandrelease_keeperrows stay, and thereleasesrow is main's, which adds the public{shipped: true}mode (version,commit,live,onItsWay, the list/changelogshows) alongside the existing kept-releases answer. No code conflicted. Type check is clean, and the MCP, release, release-keeper, changelog and people-screen verify suites pass (918 pass, 1 skip).🤖 Generated with Claude Code
Continues #7794