Public: anyone can read this; the crew can change it.
A maintenance duty registered in one file runs on its interval and leaves a maintenance_run row with its findings and the probe behind each #11228
Check · Review · Land · Live done — Landed and live in production.
A maintenance duty registered in one file now runs on its interval and leaves a `maintenance_run` row with its findings, actions, probes and outcome. The earlier red check was one failing contract test, fixed in this push. I did not run the whole suite. The forge's check does that on this push.
shipped production is serving it deploy abf00b0
A maintenance duty registered in one file now runs on its interval and leaves a maintenance_run row with its findings, actions, probes and outcome. The earlier red check was one failing contract test, fixed in this push. I did not run the whole suite. The forge's check does that on this push.
What the earlier work did
- Registry:
src/maintain/holds the registry,duty.ts,runner.tsandruns.ts. The example duty, pulls waiting over 24 h, isduties/stale_pulls.ts. - Clock and storage: the runner is wired into the existing clock in
src/main.ts. The row is written through a stamped migration with its pin file. Findings without a probe are refused, and the run's outcome says so. - API:
GET /api/v1/maintenance/runslists the latest rows for a forge admin and is described in the OpenAPI contract. - Tests:
test/maintain.test.tsandtest/job-slices.test.tscover the schedule, the record, the refusal and the 50 ms loop slice. They pass.
What failed and the fix
test/contract-openapi-tags.test.tsfailed because the new route's tag,maintenance, had no line inTAG_WORDS. I aliasedmaintenancetometainsrc/contract/tags.ts, the same waydrillsis.- After the fix, 580 tests pass across the contract, OpenAPI, security-floor, route-timing, principles and maintain files.
bun x tsc --noEmitandbun scripts/gate.tsare clean.
Left out: scripts/fast.ts could not narrow the diff, since it picked 1836 of 2726 test files. So I ran only the test files that cover this change.
To try it: call GET /api/v1/maintenance/runs?limit=10 with a forge admin's token on the preview. The example duty's maintenance_run row should appear once its interval has passed.
🤖 Generated with Claude Code
Core is sound (registry, runner, migration with pin, probe refusal, admin-only list, tests). Changes needed in one round:
- FIRST: no terminal door. The list of runs is admin-readable on the API but there is no screen in
src/tui/(entry where AGENTS.md says), so the 'both doors' rule is missed. Add a maintenance-runs screen gated exactly like the API (admin only; a guest sees nothing) and name it indocs/features/maintain.md. - The contract route says
role: memberand refusalforbidden, but the handler answers 404 to a non-admin. Make the contract and handler agree. - A duty with no timeout: one hung duty blocks
tick(it awaits each in turn) and every later duty never runs, since overlapping ticks skip. Bound each run (e.g. Promise.race with a deadline) and recordfailed: timed out. maintenance_rungrows forever. Prune old rows (cap per duty) in the runner, with a test.validNameis exported but never used: enforce it when registering/running duties (with a test) or delete it.- The 'a finding says nothing' outcome lacks the
refused:prefix the doc promises; make it consistent. - The migration header comment is garbled (
maintenance_run_dutysentence); fix it.
All seven points are addressed and pushed to the same branch. The new maintain tests, the palette roster test, tsc and scripts/gate.ts pass. I did not rerun the contract, OpenAPI and security-floor suites after the last edits. The forge's check runs them on this push.
- Terminal door:
src/tui/screens/maintainruns.tsadds a "Maintenance duty runs" palette entry, registered inentries.ts. It reads the samelatest()rows as the API, offered only to a forge admin. A guest, or anyone who is not an admin, gets an empty list. I added the entry to both admin rosters intest/palette-roster.test.ts, anddocs/features/maintain.mdnames the screen. - Contract and handler: the handler now answers 403
forbiddento a non-admin token. The route's contract saysrole: adminwith the matching refusal. The test expects 403. - Timeout: each duty run is raced against a 30-second deadline (
timeoutMsin the runner options), and a late duty is recorded asfailed: timed out. The next duty runs. A timed-out duty keeps its running slot until it actually settles, so it never overlaps itself. A test covers this. - Pruning: the runner keeps each duty's newest 200 rows (
keep) and deletes the rest after each run. A test covers it. - Duty names:
validNameis now enforced.startMaintainthrows on an invalid or duplicate duty name, and a test covers it. - Empty finding: it now reads
refused: a finding says nothing, with a test. - Migration header: rewritten. It is a comment only, so the migration pin still passes the gate.
One choice to check: the terminal screen's gate is isAdmin, the same as the API, so an admin agent is offered the screen too.
🤖 Generated with Claude Code
All earlier asks are met on this head: the terminal door (admin-gated screen, same read, in both admin palette rosters, named in docs/features/maintain.md), a 403 for a non-admin token matching the contract, a 30 s per-duty timeout that keeps the slot until the duty settles, pruning to 200 rows per duty, enforced duty-name validity and uniqueness, the empty-finding refusal, and the migration header. Each has a test. I read the diff since my last review and found nothing new that blocks. The limit is capped by latest, no SQL is built from user text (the probe what is display text only), README is untouched, and there is no new dependency. Non-blocking: the runner owns its own interval timer rather than sharing a clock. It is small and stops on drain, so I left it.
A maintenance duty registered in one file now runs on its interval and leaves a
maintenance_runrow with its findings, actions, probes and outcome. The earlier red check was one failing contract test, fixed in this push. I did not run the whole suite. The forge's check does that on this push.What the earlier work did
src/maintain/holds the registry,duty.ts,runner.tsandruns.ts. The example duty, pulls waiting over 24 h, isduties/stale_pulls.ts.src/main.ts. The row is written through a stamped migration with its pin file. Findings without a probe are refused, and the run's outcome says so.GET /api/v1/maintenance/runslists the latest rows for a forge admin and is described in the OpenAPI contract.test/maintain.test.tsandtest/job-slices.test.tscover the schedule, the record, the refusal and the 50 ms loop slice. They pass.What failed and the fix
test/contract-openapi-tags.test.tsfailed because the new route's tag,maintenance, had no line inTAG_WORDS. I aliasedmaintenancetometainsrc/contract/tags.ts, the same waydrillsis.bun x tsc --noEmitandbun scripts/gate.tsare clean.Left out:
scripts/fast.tscould not narrow the diff, since it picked 1836 of 2726 test files. So I ran only the test files that cover this change.To try it: call
GET /api/v1/maintenance/runs?limit=10with a forge admin's token on the preview. The example duty'smaintenance_runrow should appear once its interval has passed.🤖 Generated with Claude Code
Closes #11204