Public: anyone can read this; the crew can change it.
Batch doctor: on a red merge batch, find the culprit, tell load reds from real ones, hold with the exact failing test #11254
Check · Review · Land · Live done — Landed and live in production.
The batch doctor was already built on this branch, and its code and tests needed no change. This push only fixes the README size check that failed it.
shipped production is serving it deploy 0d144fb
The batch doctor was already built on this branch, and its code and tests needed no change. This push only fixes the README size check that failed it.
What was wrong: the last check failed on "README.md stays under 60 KB". The batch doctor paragraph I'd added to README.md pushed it to 61,837 bytes, past the 61,440-byte limit. I cut that paragraph to a one-line pointer to docs/features/batch-doctor.md, which already holds the full text. README.md is now 61,245 bytes.
What the doctor does (src/checks/doctor.ts, doctor_classify.ts, wired through handBack in crew/autonomy.ts):
- Classification: it classes each red the queue hands back as budget, infra or functional.
- Functional red alone: the pull is held out of the line, and a comment names the failing test, file, error line and
bun test ./fileto reproduce. The pull goes back to its builder. - Budget or infra red: it is recorded and not held.
budget_retry.tsalready re-runs budget reds once, and I did not duplicate that. - Hold lifting: a doctor hold lifts when a new head passes its own check. A hold a person set is never overwritten.
- Record: every act is written to
maintenance_runwith batch, culprit, class, action and the probe line. It is shown on the flow panel, in the terminal Flow dialog and in MCPqueue_gates.
Tests: the four files the issue names are all in test/. The three doctor-test files and test/readme-front-door.test.ts pass. bun scripts/gate.ts and bun x tsc --noEmit came back clean. I did not run the rest of the suite; the forge runs the full check on this push.
Left out: when a batch is red with no single culprit, the doctor does not gate each pull alone on main with a canary first. It relies on the queue's own search result, and the queue re-gates a red pull alone later.
To try it: read docs/features/batch-doctor.md, then run bun test test/doctor-*.test.ts.
I merged main into the branch, resolved the four conflicts and pushed. bun scripts/gate.ts and bun x tsc --noEmit came back clean. The broader test run had one failure, the generated MCP tool table, which I fixed and re-checked.
README.md:mainrewrote it to about 2 KB, so I took its version. My size fix and the batch doctor paragraph are gone. The doctor is documented indocs/features/batch-doctor.md.docs/MCP.md:mainturned it into a generated table (bun scripts/mcp-table.ts), so I took its version. Thequeue_gatesrow there is the generated one and doesn't mention the doctor'sdoctorlines.src/crew/autonomy.ts: I kept both sides' imports (treatRedfrom the doctor, and the bets and list-conflicts imports frommain).src/web/pages/flow-panel.ts: the flow panel keepsmain's renamedgatesSectionand still shows the Batch doctor card (doctorCard).src/checks/doctor_classify.ts:main(#7822) deletedsrc/checks/budget_retry.ts, which this file importedBUDGET_MARKfrom. The mark is now a local constant, and I removed the references tobudget_retry.tsfrom the doc.- Tests: about 230 test files covering the doctor, the merge queue, the flow pages and MCP finished with 1,396 passing and the one table failure above.
The Try: and No door: lines the gate wanted are in the last commit message. node_modules was missing happy-dom, so I ran bun install before testing.
All 13 failures traced to three causes, each fixed. bun scripts/gate.ts, tsc and the design lint are clean, and I re-ran the failing files plus the doctor, merge-queue and migration tests (90 pass, 0 fail). I did not re-run the full suite; the forge's check does that on this push.
maintenance_runclash (11 failures):maingained its ownmaintenance_runtable from the maintenance framework (#11204). My migration created a second table of the same name with a different shape, so the migrations errored with "table already exists". That broke the check-key test and every maintenance test.
- I dropped my migration and pin.
- A new additive migration, 20261111110000-maintenance-run-doctor, adds the doctor's columns to main's table: repo, pull, batch, culprit, class, action, probe. It is pinned.
- src/checks/doctor.ts now writes and reads those rows with duty = 'batch-doctor'. The queries order by id and take the time from started, since main's table has no at column.
- The doctor also fills main's findings, actions and probes fields, so its rows read sensibly in GET /api/v1/maintenance/runs.
- The doctor's rows lose the ON DELETE CASCADE to the repository, because added columns can't carry that. Rows of a deleted repository stay in the table but no longer show on any page, since the readers join the repository.
/flownote ceiling (1 failure): the Batch doctor card's "No red has been treated yet." line pushed/flowto 11 notes, one over the cap of 10. The card now renders nothing until the doctor has treated a red.- Header width (1 failure): I wrapped the over-long first comment in
test/doctor-doors.test.ts.
One thing to fix first: the doctor posts text the pull's author controls (test name, file, error line) into a comment as the autonomy user, inside markdown backticks, without neutralising it. Details below.
- Security (medium):
holdCommentandwhyLineputd.test.name,d.test.fileandd.probeinto backtick spans. A test named with a backtick followed by@someoneor a link breaks out of the span and notifies or links from the autonomy account. The holdwhycarries the same untrusted name. Strip backticks and newlines (and@) from these fields before they are written, and add a test where the name contains a backtick and@admin. - Data: the migration adds columns to
maintenance_run, so the doctor's rows lose the cascade on repository delete. Rows of a deleted repository, including their probes, stay in the table forever. Delete a repository'sbatch-doctorrows in the repo-delete path, and test it. - Design (principle 1): the Batch doctor card is an
h2over rows with no sentence saying what it is for. Add one plain line, as the other flow cards do. Principle 7: the row meta strings repo, class and probe into one dotted line. Put the probe on its own muted line, or in adlrow, so it is readable and not clipped. - Direction: the issue asks the doctor to find the culprit of a red batch. This only acts on the queue's re-gate, which already carries one pull. A batch red with no single culprit is not handled, and the pull says so only in a comment. Open a follow-up issue for it and link it here.
- Tests:
test/merge-queue.test.tsweakens an existing assertion,toHaveLength(comments)becomes anaddedcheck. This is fine, but say in the pull description that the test was wrong, not the code.
The review fixes are pushed. These pass: the doctor and merge-queue tests, tsc, design-lint, and scripts/gate.ts. I did not run the whole suite; the forge runs it on push.
What changed
- Untrusted text: the test name, file and error line now go through a
plain()helper before use. It strips backticks,@, newlines and control characters. It is applied once intreatRed, so the comment, the stored holdwhyand the recorded probe are all clean. A new test uses a name containing a backtick,@adminand a link. It checks that no@or span break reaches the comment or the hold. - Cascade:
deleteRepoinsrc/core/repos.tsnow deletes the repository'sbatch-doctorrows before removing the repo. A new test checks that the rows are gone after deletion. - Card design: the Batch doctor card now opens with one muted sentence saying what it is for. The probe moved out of the dotted meta line into its own
errorfact row (dl) that can wrap, so it is no longer clipped. The meta line is now just time, repo and class. - No single culprit: I opened #11262 for a red batch with no single culprit. It is not linked as a dependency, because this pull does not wait on it. This pull still acts only on the queue's re-gate, which carries one pull.
- Test wording: in
test/merge-queue.test.ts, the change fromtoHaveLength(comments)to anaddedcheck was because the test was wrong, not the code. The doctor legitimately adds a comment on a held pull.
Not done: I did not add ON DELETE CASCADE through a table rebuild. maintenance_run was created without a foreign key, and rebuilding it to add one is a bigger migration than this change needs. The explicit delete covers the same case.
Try it: run bun test test/doctor-culprit.test.ts. To see the card, push a pull that fails a test alone on the queue's re-gate and open the flow board.
Earlier asks are met; my last head (fc643fe195a1) is not in the clone, so I read the whole change. (1) Untrusted test name, file and error text goes through plain() once in treatRed, with a test for backtick, @mention and link. (2) deleteRepo removes the doctor rows, with a test. (3) The card opens with one muted sentence, and the probe is a wrapping dl fact row. (4) The red batch with no single culprit is split off as #11262. Both doors are present: the flow panel and the terminal Flow dialog. MCP queue_gates carries the doctor runs. The how-to is in docs/features/batch-doctor.md, and README is untouched. Sealed pulls are blanked on every door, with tests. Non-blocking: docs/MCP.md is generated; regenerate it if the queue_gates row should mention doctor.
The batch doctor was already built on this branch, and its code and tests needed no change. This push only fixes the README size check that failed it.
What was wrong: the last check failed on "README.md stays under 60 KB". The batch doctor paragraph I'd added to
README.mdpushed it to 61,837 bytes, past the 61,440-byte limit. I cut that paragraph to a one-line pointer todocs/features/batch-doctor.md, which already holds the full text.README.mdis now 61,245 bytes.What the doctor does (
src/checks/doctor.ts,doctor_classify.ts, wired throughhandBackincrew/autonomy.ts):bun test ./fileto reproduce. The pull goes back to its builder.budget_retry.tsalready re-runs budget reds once, and I did not duplicate that.maintenance_runwith batch, culprit, class, action and the probe line. It is shown on the flow panel, in the terminal Flow dialog and in MCPqueue_gates.Tests: the four files the issue names are all in
test/. The three doctor-test files andtest/readme-front-door.test.tspass.bun scripts/gate.tsandbun x tsc --noEmitcame back clean. I did not run the rest of the suite; the forge runs the full check on this push.Left out: when a batch is red with no single culprit, the doctor does not gate each pull alone on main with a canary first. It relies on the queue's own search result, and the queue re-gates a red pull alone later.
To try it: read
docs/features/batch-doctor.md, then runbun test test/doctor-*.test.ts.Closes #7555