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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.ts and runs.ts. The example duty, pulls waiting over 24 h, is duties/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/runs lists the latest rows for a forge admin and is described in the OpenAPI contract.
  • Tests: test/maintain.test.ts and test/job-slices.test.ts cover the schedule, the record, the refusal and the 50 ms loop slice. They pass.

What failed and the fix

  • test/contract-openapi-tags.test.ts failed because the new route's tag, maintenance, had no line in TAG_WORDS. I aliased maintenance to meta in src/contract/tags.ts, the same way drills is.
  • After the fix, 580 tests pass across the contract, OpenAPI, security-floor, route-timing, principles and maintain files. bun x tsc --noEmit and bun scripts/gate.ts are 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

Closes #11204

commented

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.ts and runs.ts. The example duty, pulls waiting over 24 h, is duties/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/runs lists the latest rows for a forge admin and is described in the OpenAPI contract.
  • Tests: test/maintain.test.ts and test/job-slices.test.ts cover the schedule, the record, the refusal and the 50 ms loop slice. They pass.

What failed and the fix

  • test/contract-openapi-tags.test.ts failed because the new route's tag, maintenance, had no line in TAG_WORDS. I aliased maintenance to meta in src/contract/tags.ts, the same way drills is.
  • After the fix, 580 tests pass across the contract, OpenAPI, security-floor, route-timing, principles and maintain files. bun x tsc --noEmit and bun scripts/gate.ts are 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

Changes requested — before the latest push

Core is sound (registry, runner, migration with pin, probe refusal, admin-only list, tests). Changes needed in one round:

  1. 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 in docs/features/maintain.md.
  2. The contract route says role: member and refusal forbidden, but the handler answers 404 to a non-admin. Make the contract and handler agree.
  3. 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 record failed: timed out.
  4. maintenance_run grows forever. Prune old rows (cap per duty) in the runner, with a test.
  5. validName is exported but never used: enforce it when registering/running duties (with a test) or delete it.
  6. The 'a finding says nothing' outcome lacks the refused: prefix the doc promises; make it consistent.
  7. The migration header comment is garbled (maintenance_run_duty sentence); fix it.
commented

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.

  1. Terminal door: src/tui/screens/maintainruns.ts adds a "Maintenance duty runs" palette entry, registered in entries.ts. It reads the same latest() 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 in test/palette-roster.test.ts, and docs/features/maintain.md names the screen.
  2. Contract and handler: the handler now answers 403 forbidden to a non-admin token. The route's contract says role: admin with the matching refusal. The test expects 403.
  3. Timeout: each duty run is raced against a 30-second deadline (timeoutMs in the runner options), and a late duty is recorded as failed: 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.
  4. Pruning: the runner keeps each duty's newest 200 rows (keep) and deletes the rest after each run. A test covers it.
  5. Duty names: validName is now enforced. startMaintain throws on an invalid or duplicate duty name, and a test covers it.
  6. Empty finding: it now reads refused: a finding says nothing, with a test.
  7. 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs