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 #11212
A maintenance duty is now one file that runs on its interval and leaves a `maintenance_run` row. I did not call `keep_plan`; that tool wasn't available in this session, so the issue has no plan on it.
stopped parked by the crew deploy f7ea4d8
A maintenance duty is now one file that runs on its interval and leaves a maintenance_run row. I did not call keep_plan; that tool wasn't available in this session, so the issue has no plan on it.
I ran tsc, bun scripts/gate.ts and the tests for this change; all passed. They were test/maintain.test.ts, job-slices, timers-named and schema-migrations. The full suite is left to the forge's check on this push.
- Duty and registry: a duty is a file in
src/maintain/duties/with a name, an interval and a handler returning findings and actions. It is registered with one line insrc/maintain/registry.ts. - Runner: it ticks every 60 s inside
during("maintain"). It runs a duty when its interval has passed since the duty last started, and it never runs the same duty twice at once. Duties take turns, with the loop yielding between them.main.tsstarts it and stops it on shutdown. - Record: migration
20261111100000-maintenance-run(pinned) creates the table with duty, started, finished, findings, actions, probes and outcome. It also addsissue_open_ageon(kind, state, created_at)so the example duty's count walks an index, which a test proves from the query plan. - Refusal: each finding needs a probe: query or measurement, its number, its time. If any finding lacks one, the run keeps no findings or actions and the outcome reads
refused: finding "…" has no probe. A duty that throws is recorded asfailed: <message>and the next duty still runs. - Example duty:
pulls-waitingcounts open, unparked pulls older than 24 h, with the SQL and its number as the probe. - Endpoint:
GET /api/v1/maintenance/runs?limit=Nneeds a forge admin androscope. It answers 401 without a credential and 404 for a non-admin. The limit is capped at 100 and defaults to 20. - Tests and docs:
test/maintain.test.tscovers schedule, record, one-at-a-time, refusal, failure, the example duty, the index and the endpoint.test/job-slices.test.tsnow timesmaintainagainst the busy fixture; it measured about 0.1 ms. The how-to is indocs/features/maintain.md.
I left out:
- Any terminal screen or page for the runs; they are reachable by API only.
- Pruning old rows.
- Per-duty slice enforcement beyond the shared job-slice test.
- Duties that act. The
actionsfield is recorded but the example takes none.
To try it: as a forge admin, call GET /api/v1/maintenance/runs on the preview. The pulls-waiting row appears about 75 s after boot. The commit carries the Try: line the repo requires.
The check failed on the gate's "pipeline first" rule: this pull adds docs/features/maintain.md and changes no pipeline file. I added a commit carrying a Direction: line instead of touching the pipeline, and pushed it. The code is unchanged.
The line says the duty runner is the base that the pipeline's own upkeep duties will register on, so it comes before them. The forge's check will say whether the gate accepts that.
bun scripts/gate.ts ran clean on this branch before the push, but it takes the base from its own checkout, so it can't stand in for the forge's check of the rule. I did not re-run tsc or the tests, since nothing in the code changed.
The pull description from my earlier message still stands as written.
A maintenance duty registered in one file now runs on its interval and leaves a maintenance_run row holding its findings and their probes. This push fixes four of the five check failures; the Billing one is not mine and is still red.
What the push fixes:
- OpenAPI row:
GET /api/v1/maintenance/runshad none. I addedgetApiV1MaintenanceRuns.tsand regenerated the route index. - Session phase: the same route now records the
sessionphase, as/api/v1/admin/maintenancedoes. - Cold-read plans: my migration created
issue_open_ageonissue. With that index there, the planner stopped usingissue_createdfor the cold-pull window read, which failed "the cold pull reads walk an index" and "200 open pulls … walking an index". I removed the index. Thepulls-waitingduty runs hourly and still searches an existing index. - Migration pin: the migration is not on main yet, so editing it was allowed. I re-pinned it, and
bun scripts/gate.tsandtsc --noEmitare clean. - My test: my test asserted
issue_open_ageby name. It now asserts that the plan searches an index and does not scan the table.
The test files I ran are green: maintain, cold-home-queries, team-queue, schema-migrations, job-slices, openapi-coverage and route-timing-coverage.
Billing failure, not mine: "Settings → Billing: set three links, the home shows them" fails the same way on the base commit 4fe3b8e2b, with the home showing "Get a forge" once instead of three times. I did not change it.
To try it: register a duty in src/maintain/registry.ts, then as a forge admin read GET /api/v1/maintenance/runs?limit=20.
I chose not to add an index on issue for the duty's count.
Parked by the crew: the check stayed red through 2 automatic tries
A maintenance duty is now one file that runs on its interval and leaves a
maintenance_runrow. I did not callkeep_plan; that tool wasn't available in this session, so the issue has no plan on it.I ran
tsc,bun scripts/gate.tsand the tests for this change; all passed. They weretest/maintain.test.ts,job-slices,timers-namedandschema-migrations. The full suite is left to the forge's check on this push.src/maintain/duties/with a name, an interval and a handler returning findings and actions. It is registered with one line insrc/maintain/registry.ts.during("maintain"). It runs a duty when its interval has passed since the duty last started, and it never runs the same duty twice at once. Duties take turns, with the loop yielding between them.main.tsstarts it and stops it on shutdown.20261111100000-maintenance-run(pinned) creates the table with duty, started, finished, findings, actions, probes and outcome. It also addsissue_open_ageon(kind, state, created_at)so the example duty's count walks an index, which a test proves from the query plan.refused: finding "…" has no probe. A duty that throws is recorded asfailed: <message>and the next duty still runs.pulls-waitingcounts open, unparked pulls older than 24 h, with the SQL and its number as the probe.GET /api/v1/maintenance/runs?limit=Nneeds a forge admin androscope. It answers 401 without a credential and 404 for a non-admin. The limit is capped at 100 and defaults to 20.test/maintain.test.tscovers schedule, record, one-at-a-time, refusal, failure, the example duty, the index and the endpoint.test/job-slices.test.tsnow timesmaintainagainst the busy fixture; it measured about 0.1 ms. The how-to is indocs/features/maintain.md.I left out:
actionsfield is recorded but the example takes none.To try it: as a forge admin, call
GET /api/v1/maintenance/runson the preview. Thepulls-waitingrow appears about 75 s after boot. The commit carries theTry:line the repo requires.Closes #11204