Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2048Done

11.2.7 `GET` + `POST /api/v1/work-items/{key}/transitions` — the legal-move list and the status move that teaches on refusal

Moving an item through its workflow is the operation an integration reaches for second (after reading), and Motir's shipped behaviour on refusal — an illegal move returns the allowed targets — is what the story calls the thing that makes the API self-teaching. This card ships it as a sub-resource, in the shape the mirror uses.

Rung 1 — the shape is Jira's, not invented: Jira exposes GET /issue/{key}/transitions (what can I do from here?) and POST /issue/{key}/transitions (do it). A client that can ASK is not reduced to attempting a move and parsing the failure, and a UI built on this API can render exactly the buttons the workflow allows. app/api/v1/work-items/[key]/transitions/route.ts carries both exports.

What to build

  • GET, scope: 'read' — the transitions legal FROM the item's current status, each { key, label, category }, read through workflowsService.getWorkflow for the item's project. Not the whole workflow graph: the question is "what can this item do now", and the full graph is a project resource (11.3's territory if it is ever wanted).
  • POST, scope: 'work_items:write' — body { status }, applied via workItemsService.updateStatus(id, toKey, ctx). The service owns legality (workflowsService.canTransition) — no legality logic in the route, which is why the same rules govern the board, the MCP tool and this endpoint. On success return the updated detail resource, so a client sees the new status without a second read.
  • The refusal carries the allowed targets as DATA, not prose. transition_status appends them to the human message because an agent reads English; a machine client must not parse a sentence. So ILLEGAL_TRANSITION returns 422 with the pinned { code, error } plus an additive allowedTransitions array — a new field on a response object, explicitly allowed under ADR §8, and the schema module is where its shape is declared. UNKNOWN_STATUS (a status key the project's workflow does not define at all) is a distinct 422 code from ILLEGAL_TRANSITION (a real status that is not reachable from here) — collapsing the two would make a typo and a workflow rule indistinguishable.
  • The domain rows added to DOMAIN_ERROR_STATUS: ILLEGAL_TRANSITION and UNKNOWN_STATUS → 422; WORK_ITEM_NOT_FOUND → 404 (already added by 11.2.2 — do not duplicate the row).

Scope BOUNDARY

Ends at this item's status moves. It does not expose workflow ADMINISTRATION (defining statuses or transitions is an admin surface, not this), does not touch markIntegrated / completeSession (those are integration-scoped external-agent writes and are not in this story's endpoint set), and does not change updateStatus, applyStatusTransition or the default workflow. It emits no notification and no side effect of its own — the service already owns what a transition triggers.

Acceptance criteria

  • GET …/transitions returns exactly the targets legal from the item's current status for a read token, matching workflowsService.canTransition for each — asserted by driving every returned target successfully and at least one omitted target unsuccessfully.
  • A legal POST …/transitions persists the move and returns the updated resource; a subsequent GET /api/v1/work-items/{key} shows the new status.
  • An illegal move returns 422 with code: 'ILLEGAL_TRANSITION' and an allowedTransitions array whose contents equal what GET …/transitions returns for that item — the two surfaces cannot disagree.
  • An unknown status key returns 422 with code: 'UNKNOWN_STATUS', distinct from the illegal-move code.
  • A read-only token gets 200 on GET and 403 on POST — the scope split asserted on one endpoint pair.
  • Cross-tenant and unbrowsable targets return 404 on both methods.
  • The route calls no Prisma and opens no transaction, and each export declares its own scope (the shipped guard).
  • The per-file coverage floor (≥90%) holds on every new file.

Context refs

  • lib/services/workItemsService.tsupdateStatus, and applyStatusTransition beneath it.
  • lib/services/workflowsService.tsgetWorkflow / canTransition, the single source of "legal".
  • lib/workflows/defaultWorkflow.ts — the shipped status set and its transition edges (note there is no direct in_progress → done edge).
  • lib/mcp/tools/transitionStatus.ts — the shipped tool that proves the service path and the allowed-targets-on-refusal behaviour this card turns into data.
  • lib/api/v1/errors.tsDOMAIN_ERROR_STATUS, extended with the two transition codes.
  • Producer: 11.2.2 (the schema module + key resolution). Parent story: 11.2.