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
144
planned
1,362
shipped

Motir · Work items

MOTIR-1853Done

11.3 Projects, sprints, backlog and the ready set over `/api/v1`

The planning resources — the surfaces that make Motir an agile tool rather than an issue database: projects, sprints, the backlog and the ready set. Together with 11.2 they complete the API's coverage of the PM core.

blocked_by 11.1 and 11.2 — it composes 11.1's wrapper, scope gate, error envelope and cursor, and 11.2's zod-response-schema convention and work-item summary shape (the row both ranked collections and the ready set return).

The journey step

A developer automating a team's cadence: list the workspace's projects → list a project's sprints → see what is in the active sprint → move items in or out → read the backlog → ask what is ready to pick up. That last one is the endpoint that makes external agent orchestration possible without MCP, and it is independently testable via curl the moment the story lands.

What to build

  • GET /api/v1/projects and GET /api/v1/projects/{projectKey} — the project list + read.
  • GET /api/v1/projects/{projectKey}/sprints — every sprint with its state, window, goal, issue count and the activation-baseline fields (committedPoints / committedIssueCount, both null on a never-started sprint — a real distinction the response must preserve rather than flatten to zero).
  • GET /api/v1/sprints/{sprintId} and GET /api/v1/sprints/{sprintId}/work-items — one sprint and its members, paginated.
  • POST /api/v1/projects/{projectKey}/sprints (create) and PATCH /api/v1/sprints/{sprintId} (edit), plus the lifecycle moves POST /api/v1/sprints/{sprintId}/start and /complete — all gated on sprints:write.
  • POST /api/v1/sprints/{sprintId}/work-items and POST /api/v1/projects/{projectKey}/backlog/work-items — sprint membership in both directions.
  • GET /api/v1/projects/{projectKey}/backlog — the paginated backlog in rank order.
  • GET /api/v1/projects/{projectKey}/ready — the ready set, carrying each row's dependency edges (the dependencies: { blockedBy, blocks } block 7.9.0f already attaches to list_ready) so a client can see downstream impact without a second call.

Scope mapping: reads read; sprint writes and membership moves sprints:write.

The readiness cascade is load-bearing — do not re-derive it

GET .../ready MUST return exactly what the product's own ready computation returns, including the parent-ready cascade (an item is ready ⟺ its parent is ready AND every same-level sibling it is blocked_by is done). A flat "all blockers done" check is a different, wrong answer. So this endpoint calls workItemsService.listReady — it never reimplements readiness in the route. An external agent loop that disagrees with the board about what is ready is worse than no endpoint at all.

What the expansion pass CORRECTED (2026-08-04)

This story was authored as "thin adapters over already-shipped services, same as 11.2 — each operation's service path is proven by the MCP tool that calls it today". Verified against origin/main @ 94a65035, that is false in four places, each recorded rather than absorbed:

  1. One endpoint has no service path at all, and the nearest method is wrong. GET /api/v1/sprints/{sprintId} needs a single-sprint read; sprintsService exposes createSprint / updateSprint / deleteSprint / startSprint / completeSprint / getActiveSprint / listByProject / validateSprint / getSprintReport — and no by-id DTO read. Only sprintRepository.findById exists, a Prisma row a route may not touch. getActiveSprint cannot stand in either: it returns toSprintDto(row, 0), so its issueCount is hard-coded 0 — an endpoint built on it would report every active sprint as empty. So a by-id service read is added, under a recorded carve-out, computing the count the way listByProject does.
  2. The v1 cursor is hardwired to (createdAt, id) and none of this story's four collections sort that way. The backlog and sprint members sort on backlogRank; the ready set on (type asc, priority desc, key asc) — the dispatch rank, which is the whole value of the endpoint; sprints on sequence. paginateKeyset additionally requires Keyed { id, createdAt }, and SprintDto has no createdAt field at all while ProjectDTO's is optional and unloaded on the list path. Paging any of them through the shipped codec would silently re-sort the collection into a different order than the product's. So the codec is generalized to sign an opaque, service-owned position, keeping §5's three properties (keyset, opaque, 422-never-reset) and the 100 ceiling.
  3. Two envelope questions the shipped reads force, before ten endpoints improvise them. getBacklog / getSprintIssues return a totalCount the v1 list envelope has no field for; and the ready row's dependencies block is a second service read at the transport (getDependencyEdgesForItems), which the ADR's one service method rule does not obviously admit. Both are settled in the decision card, not per endpoint.
  4. Sprint writes are ADMIN-gated, not merely scope-gated. Every sprint write calls assertSprintAdmin and raises NotSprintAdminError (403). A token carrying sprints:write whose OWNER is an ordinary project member is refused — scope NARROWS the owner's role and never widens it (ADR §3). That is testable behaviour this story must assert, not a surprise a client discovers. (startSprint also idempotently provisions a scrum board before activating — a shipped side effect the API inherits, not one it adds.)

Completeness — planned to real-product scale

The backlog and sprint-member collections are unbounded (this project's sprints have run to 117 issues); every list is cursor-paginated, no endpoint loads a whole collection. The backlog and sprint-member reads window in the DATABASE (findBacklogPage / findSprintIssues); the bounded collections (a workspace's projects, a project's sprints) page in memory over an already-bounded read, the same way GET /api/v1/workspaces does. The one honest exception is the ready set, whose shipped computation materializes the project's ready leaves by a top-down traversal before slicing — that is a property of the readiness predicate itself (bounded by tree depth, never a whole-table scan), and re-deriving it to page differently is exactly what the section above forbids. Sprint state transitions are read-derived writes (start/complete guard on current state), so they inherit the services' existing SELECT … FOR UPDATE discipline — this story must not introduce a new count-then-write guard in a route, and its tests must include a genuine-concurrency case for the lifecycle moves rather than a serial one.

Type sweep — what this story does NOT need, deliberately

Swept the complete subtask-type set so an omission is a decision: design — no rendered surface (the reference page is 11.4's and carries its own design subtask). manual/human — no external account, secret, DNS or dashboard step; no new process.env read (the rate-limit and cursor-signing variables landed with 11.1). deploy — no new infrastructure. copy / translate — no user-visible string. legal — no new artifact. research/spike — the decision card carries the grounding. doc — 11.4 owns the published reference. So: one decision, eight code, two test.

No acceptance video. No user-observable surface — exempt under the acceptance-video rule's non-UI carve-out, accepting on its tests alone, exactly as 11.1 and 11.2 did. The conformance suite is this story's E2E, in the form the story actually has: the client is curl.

Scope BOUNDARY

Ends at projects, sprints, backlog and the ready set. It does NOT ship boards, swimlanes or WIP limits (a board is a projection of a filter and a workflow — exposing it publicly is a separate design question, and no card here defers work to it); does NOT ship reports, dashboards or velocity/burndown series (getSprintReport stays internal); does NOT expose validate_sprint; does NOT ship workspace or project administration (create/delete/rename a project, membership, roles, workflow editing) — those are privileged surfaces needing their own scope design; does NOT ship the AI planning endpoints (motir-ai is behind the open-core boundary and reachable only through decisions this epic does not make); does NOT write the OpenAPI spec (11.4).

Two deliberate omissions, on the same reasoning the ADR gave for the cascade delete — adding either later is additive under §8, withdrawing it would not be:

  • Sprint DELETE. deleteSprint is a hard delete, not a reversible soft-remove; v1's first cut exposes no irreversible operation.
  • The ready CLAIM. GET .../ready READS the set. claimNextReady flips an item to in_progress under FOR UPDATE SKIP LOCKED and passes the CI-credit gate — a work_items:write operation with different semantics, different failure modes and a billing interaction. A read endpoint is the useful half and the safe half; nothing in this story defers work to a claim endpoint.

On services: beyond the two bounded changes named in the corrections above (the generalized cursor codec, which is v1's own module, and the single-sprint service read), it changes no service, repository or migration. If an endpoint here wants DATA a service does not expose, or a new filter axis, gate or field, that is a card in the owning feature's epic — not a service change smuggled in at the edge.

Acceptance criteria

  • Every listed endpoint exists, enforces its mapped scope, and returns 11.1's envelope shapes.
  • GET .../ready returns the SAME set, in the SAME order, as workItemsService.listReady for the same project and filters — asserted against the shipped service, including a case where the parent-ready cascade excludes an item whose own sibling blockers are all done (the exact case a flat check would get wrong).
  • Each ready row carries its dependencies: { blockedBy, blocks } edges with the shipped key names.
  • No v1 route re-derives readiness, rank or sprint state — asserted as a guard, not by review.
  • A never-started sprint returns committedPoints / committedIssueCount as null, not 0; and a sprint read never reports issueCount: 0 for a sprint that has members.
  • Sprint lifecycle moves (start / complete) are guarded under real concurrency: two simultaneous starts do not both succeed, and the losing caller gets a typed error rather than a raw DB error.
  • Sprint membership moves are atomic — a multi-item move either lands entirely or not at all, and one unknown or cross-project member rejects the whole batch.
  • A read-only token is refused (403) on every sprint write and membership move, and a sprints:write token whose owner is not a sprint admin is refused too.
  • Every list endpoint pages by cursor over ITS OWN sort order; none loads an unbounded collection; a cursor issued for one collection is not accepted by another.
  • Cross-tenant isolation holds on every endpoint taking a project key or sprint id (404-not-403).
  • No route calls Prisma or opens a transaction (the 4-layer contract, asserted by 11.1's shipped tree-wide guard).
  • The per-file coverage floor (≥90%) holds on every new file.

Context refs

  • lib/services/sprintsService.ts · lib/services/projectsService.ts · lib/services/backlogService.ts — the services these endpoints adapt.
  • lib/workItems/readyFilter.ts — the ready set's own sort order + cursor codec, and why it encodes a tuple rather than an offset.
  • lib/dto/ready.ts · lib/dto/sprints.ts · lib/dto/backlog.ts · lib/dto/projects.ts — the internal DTOs the v1 schemas map FROM.
  • lib/mcp/tools/listReady.ts — the shipped ready read + its batched dependencies projection, as a reference SHAPE. A v1 route must not import from lib/mcp/ — the two surfaces align through the service, not through each other.
  • lib/api/v1/ — the shipped wrapper, error envelope, pagination and rate limiter this story composes.
  • lib/api/v1/workItems/schema.ts — the response-schema module pattern (field-by-field mappers, TOTAL enum vocabularies, no spread) this story's resource modules mirror.
  • app/api/v1/projects/[projectKey]/work-items/route.ts — the shipped project-scoped collection route: resolve the key, page in the database, present through a schema.
  • docs/decisions/public-api-conventions.md — the contract, as amended by 11.2.1.
  • Foundation: 11.1. Sibling: 11.2. Parent epic: the public REST API.