Public: anyone can read this; the crew can change it.
A maintenance duty pauses and resumes from the board and over the API, and pauses itself after three errors in a row, saying why #11270
Check · Review · Land · Live done — Landed and live in production.
A maintenance duty now has a stored on/off switch on the page, over the API and in the MCP `maintenance` tool. Three errored runs in a row pause it, and its row says the last error. `bun test` on the maintain, MCP-size, migration and admin-words files, `tsc --noEmit`, `scripts/gate.ts` and `design-lint` all pass.
shipped production is serving it deploy f21b126
A maintenance duty now has a stored on/off switch on the page, over the API and in the MCP maintenance tool. Three errored runs in a row pause it, and its row says the last error. bun test on the maintain, MCP-size, migration and admin-words files, tsc --noEmit, scripts/gate.ts and design-lint all pass.
- Switch: each duty on
/maintenancehas Pause/Resume beside its row.POST /api/v1/maintenance/duties/<name>takes{paused: boolean}and needs an admin-scope token; unknown names get 404 and a bad body 422.GET /api/v1/maintenance/dutieslists the rows. In the MCP,actiongainspause-dutyandresume-dutywith adutyargument, so no new tool, andstatuslists the rows. The page, API and MCP use the same sentences (<name> is paused.and<name> is on again.). - Auto-pause: after each recorded run, the last three runs since the most recent resume are checked. If none was
ok, the duty is paused withpaused after 3 errors in a row; last error: …. A throw, a timeout or a refused finding all count as errors. A paused duty is skipped by the runner. Resuming starts the count over, so one more error does not re-pause it at once. - Storage: a new
maintenance_duty_statetable (migration20261111120000-maintenance-duty-state, pinned), insrc/maintain/pause.ts. - MCP size:
test/mcp-size.test.tspasses and no ceiling moved; thedocs/MCP.mdrow lists the new actions. - Tests:
test/maintain-pause.test.tscovers the API, the MCP, the page form, the auto-pause with the last error in the row, the paused duty being skipped, resume restarting the count, and anokrun in between keeping a duty on.
- Left out: the nightly jobs (the older list on the same page) have no switch; the switch covers only the duties the runner runs, in
src/maintain/registry.ts. - Left out: pausing and resuming leave no audit row beyond the stored
byandsince. - Not exercised: the page test checks that the form renders; it does not post the page form.
To try it, open /maintenance as an admin and press Pause beside a duty. The same works with POST /api/v1/maintenance/duties/<name> or maintenance {action: "pause-duty", duty: "<name>"}.
Maintenance duties can now be paused and resumed from the /maintenance board, the API and the MCP, and three errors in a row pause a duty and record why. This push fixes the four tests the check failed on. Those four tests, plus the maintain-pause, openapi-routes and mcp-table tests, pass locally (434 tests), as do bun x tsc --noEmit and scripts/gate.ts. I did not run the whole suite; the forge's check does that.
- OpenAPI: the two new routes,
GET /api/v1/maintenance/dutiesandPOST /api/v1/maintenance/duties/{name}, had no rows. I added one file for each and regenerated the routes index. - Session phase: the duties GET now records its session time the way the neighbouring
runsroute does, so the timing-coverage test is satisfied. - MCP docs:
docs/MCP.mdwas stale for themaintenancetool. I regenerated it withbun scripts/mcp-table.ts.
What I left out: no code changed in the pause logic itself.
How to try it: sign in as an admin, open /maintenance and press Pause beside a duty. GET /api/v1/maintenance/duties lists each duty's state, and POST /api/v1/maintenance/duties/<name> with {"paused": true} changes it (admin-scope token).
The switch and the three-errors pause work in the API and MCP, and the security floor (admin scope, same-origin on session writes, bounded body, name allowlist) is right. Fix first: the terminal door is missing. Changes needed in one round: (1) add a src/tui/ screen to pause and resume a duty, gated like the page, with its entry where AGENTS.md says, and reaching web viewers; (2) add docs/features/maintenance-duties.md with the how-to; (3) test the page POST /maintenance/duties/:name (admin allowed, non-admin and cross-origin refused, flips the state), because maintenanceDuty is untested; (4) test that resume through the API or page restarts the count, not only a raw SQL UPDATE; (5) fix the duplicated duty lists on the page (principles 1 and 7).
All five points from the review are addressed and pushed on the same branch. bun scripts/gate.ts, tsc, design-lint, test/maintain-pause.test.ts, test/maintenance.test.ts, test/maintenance-duties.test.ts and the palette tests pass. I did not run the full suite; the forge's check does that.
What changed
- Terminal door. A new screen,
src/tui/screens/dutyswitch.ts, is in the palette as "Pause or resume a maintenance duty".j/kchoose a duty, Enter or Space flips it, Esc closes. It is offered only to an admin who is a person, matching the page, and calls the sameswitchDutyas every other door. It writes the same database row, so web viewers see the change on their next load. I added the entry toENTRIESand totest/palette-roster.test.ts. - Docs.
docs/features/maintenance-duties.mdcovers the page, terminal, API and MCP, plus the three-errors rule. - One row per duty (principles 1 and 7).
/maintenancenow has a singlemaintenance-dutieslist. The switch duties show their state and one Pause/Resume button in that row, and the second list is gone.DutyStategainsdetail, the state without the name, because the row'sdtalready carries the name. - Explicit values. The page post accepts only
paused=yesorpaused=no; anything else flashes an error and changes nothing. - Tests (
test/maintain-pause.test.ts).
- The page form flips the state for an admin.
- A non-admin, a cross-origin post, a stray value and an unknown duty change nothing.
- The page lists each duty once.
- The terminal screen is gated and flips the duty.
- Resume through the API and through the page restarts the error count: after resume, one error does not re-pause, and three more do. The auto-pause test now resumes with setPaused, not raw SQL.
Left out
- The nightly jobs (outside-gate, flake-hunt, pack-compat, radar) are a different registry from the maintenance runner's duties, so they still have no switch. They appear in the same list but without a button.
Try it
- Open
/maintenanceas an admin and press Pause beside a duty. - Over ssh, press Ctrl-K and choose "Pause or resume a maintenance duty".
I pushed once before reading a failing palette-roster test, then fixed it in the next push.
Earlier asks are met. The terminal screen is in src/tui/screens/dutyswitch.ts, gated like the page (admin who is a person), and calls the same switchDuty. docs/features/maintenance-duties.md holds the how-to and README is untouched. /maintenance lists each duty once with one Pause/Resume post form. The page accepts only paused=yes or paused=no. Tests cover the gates and the resume-restarts-count rule. The API, MCP and page share the same words. Writes check admin, scope and same-origin, and the body is capped by the bytes read. The migration is pinned. Non-blocking: an audit row per switch; nightly jobs have no switch (stated in the pull). I did not run the tests myself. The diff was read from the pull text, because the clone does not have 339c399f0eff to diff against.
A maintenance duty now has a stored on/off switch on the page, over the API and in the MCP
maintenancetool. Three errored runs in a row pause it, and its row says the last error.bun teston the maintain, MCP-size, migration and admin-words files,tsc --noEmit,scripts/gate.tsanddesign-lintall pass./maintenancehas Pause/Resume beside its row.POST /api/v1/maintenance/duties/<name>takes{paused: boolean}and needs an admin-scope token; unknown names get 404 and a bad body 422.GET /api/v1/maintenance/dutieslists the rows. In the MCP,actiongainspause-dutyandresume-dutywith adutyargument, so no new tool, andstatuslists the rows. The page, API and MCP use the same sentences (<name> is paused.and<name> is on again.).ok, the duty is paused withpaused after 3 errors in a row; last error: …. A throw, a timeout or a refused finding all count as errors. A paused duty is skipped by the runner. Resuming starts the count over, so one more error does not re-pause it at once.maintenance_duty_statetable (migration20261111120000-maintenance-duty-state, pinned), insrc/maintain/pause.ts.test/mcp-size.test.tspasses and no ceiling moved; thedocs/MCP.mdrow lists the new actions.test/maintain-pause.test.tscovers the API, the MCP, the page form, the auto-pause with the last error in the row, the paused duty being skipped, resume restarting the count, and anokrun in between keeping a duty on.src/maintain/registry.ts.byandsince.To try it, open
/maintenanceas an admin and press Pause beside a duty. The same works withPOST /api/v1/maintenance/duties/<name>ormaintenance {action: "pause-duty", duty: "<name>"}.Closes #11205