11.5.23 `nextReady` off MCP — pick from the RANKED ready set, and route from the dispatch prompt
The LAST method still speaking MCP. MotirClient.nextReady calls the next_ready tool; nothing else in packages/cli/src does, so this card is what makes 11.5.6's deletion possible.
There is no next_ready endpoint on /api/v1, and there should not be one. The ready collection already answers the question: lib/api/v1/ready/schema.ts documents that rows arrive in the dispatch rank (type asc, priority desc, key asc) and that items[0] is what an agent should take next. "Next ready" is a client-side pick from a ranked page, not an operation.
Where targetRepo comes from — SEED FIRST, THEN RESOLVE
targetRepo is per card and can differ on every dispatch, so it belongs with the card's instruction payload — the "what to do" — not on a collection row. That is Amendment 10 Q2's position and it stands: the ready row does NOT gain the field, and the loop reads it from GET /api/v1/work-items/{key}/dispatch-prompt.
⚠️ The obvious ordering is circular, and the fix is to invert it. The loop cannot resolve the checkout before the prompt read (it needs targetRepo), and it cannot seed the prompt after (repos.ensure creates the session branch and the seed is repo?.branch ?? null). So:
- Read the prompt WITH the run's branch as the seed. The prompt is a pure READ — it never claims the item and never moves its status — so seeding before knowing whether a checkout exists has no side effect and costs nothing.
resolveDispatchTarget(dir, config, dispatch.targetRepo)→repos.ensure(target), which creates the session branch on origin.- A checkout exists → the prompt already reads correctly. ONE read, no regression on the common path.
- No checkout → the prompt's lineage instruction is wrong for an item that will ship as its own pull request, so re-read seedless and hand the agent that text instead. A second read, on a path that was already the exceptional one.
What to build
nextReadyfetches a ready PAGE through the transport and returns the first row not held out, followingnextCursorwhen a whole page is excluded.kindsrides as the collection's own repeated filter.- Exclusion by KEY, and the id goes. MOTIR-2338 already made the persisted list key-based and then translated keys back to row ids for the tool; with the pick client-side there is nothing to translate.
DispatchItem.idandclaimNextNotExcluded's extra round trip both go away. - The seed-first ordering above, in
commands/auto.tsandcommands/batch.ts. DispatchItemshedsidandtargetRepo, and carriesinheritedSessionBranch— Amendment 15's readiness qualifier (MOTIR-2400), which is whatclassifySnapshotItemreads to refuse a lineage item inmotir batch.listReadyForDispatch— the whole ranked set formotir batch's snapshot, one page walk where it used to be onenext_readyper item with a growing exclusion list.
Scope BOUNDARY
nextReady, DispatchItem, and the pick/route ordering in the three dispatch commands. It does NOT add a field to the ready row or change any endpoint. It does NOT touch render.ts. It does NOT delete the MCP transport or the SDK (11.5.6, which this unblocks). It does NOT change any output string: motir run, motir auto and motir batch print byte-identically.
Acceptance criteria
nextReadycallsGET /api/v1/projects/{projectKey}/readythrough the transport; no MCP tool call remains anywhere inpackages/cli/src.- A run holding out every row of the first page follows
nextCursorand picks from the second — asserted, because a client-side pick that stops at one page silently drains a run early. - The pick honours the SERVER's rank:
items[0]of the first page, unexcluded, is what comes back. Asserted against a page whose rank matches no field the client could sort on. - Exclusion is by KEY end to end; no row id is sent, read or persisted, and
claimNextNotExcluded's translating round trip is gone. - An item WITH a checkout makes exactly ONE dispatch-prompt request, asserted on the request count — the seed-first ordering exists to keep the common path at one read, and an implementation that reads seedless-then-seeded passes every output assertion while doubling the calls.
- An item with NO checkout re-reads seedless, and the agent receives the
per_item_prtext rather than an instruction to integrate into a branch that does not exist. - Reading a prompt still neither claims the item nor moves its status — asserted by reading the item's status before and after, because this card now reads it before deciding anything.
motir run,motir autoandmotir batchproduce byte-identical output for the same data, asserted by the existing suites with their assertion strings untouched.- The per-file coverage floor (≥90%) holds on every new and modified file.
Context refs
lib/api/v1/ready/schema.ts— the dispatch rank as contract, and whytargetRepois not on the row.- ADR
docs/decisions/public-api-conventions.mdAmendment 10 Q2 (the prompt is wheretargetRepolives) and Amendment 15 (the row'sinheritedSessionBranch). packages/cli/src/mcpClient.ts—nextReady,DispatchItem.packages/cli/src/commands/dispatch.ts(claimNextNotExcluded) ·auto.ts(repos.ensure,resolveDispatchTarget, the seed) ·batch.ts.- MOTIR-2338 — made the exclusion list key-based; this removes the id translation it had to add.
- MOTIR-2400 — shipped
inheritedSessionBranch, without whichmotir batch's snapshot could not refuse a lineage item. - Story: MOTIR-1855.