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-2046Done

11.2.6 `POST /api/v1/projects/{projectKey}/work-items` + `PATCH /api/v1/work-items/{key}` — the write pair, with `If-Match` optimistic concurrency

The two mutations that make /api/v1 a write surface rather than a read-only mirror. Both are scope: 'work_items:write' (ADR §3), both are thin adapters over ONE shipped service method each, and both return the detail resource 11.2.2 pins.

POST is a second export in the collection route module 11.2.4 creates; PATCH is a second export in the item route module 11.2.2 creates. Both reuse those cards' project / key resolution rather than re-deriving it — and the shipped guard already catches "a POST that bypasses the wrapper even when a sibling GET does not", so each export declares its own scope.

What to build

Request schemas, in the same lib/api/v1/workItems/schema.ts the responses live in — createWorkItemBodySchema and updateWorkItemBodySchema. Validation failure is a 422 in the { code, error } envelope; the schemas are what 11.4 later emits as the operations' request bodies.

POST …/work-itemsworkItemsService.createWorkItem. The body carries kind, title, and the leaf-authoring fields the shipped CreateWorkItemInput accepts (descriptionMd, priority, type, executor, storyPoints, estimateMinutes, targetRepo, assigneeId, dueDate) plus parentKey — a MOTIR-<n> key the route resolves to the internal parent id, never a cuid on the wire (ADR §7). It stamps planningSource: 'api' (11.2.5), exactly as the MCP tool stamps mcp. Responds 201 with the created resource and a Location header pointing at its item URL.

PATCH /work-items/{key}workItemsService.updateWorkItem. The editable set is the shipped UpdateWorkItemInput PATCH keys — title, descriptionMd, explanationMd, priority, type, executor, storyPoints, estimateMinutes, targetRepo, assigneeId, dueDate, plus parentKey (re-file) and kind (re-classify), both of which that ONE service method already validates against the kind-parent matrix. An omitted field is untouched; an explicit null clears a nullable one — say which is which in the schema, because "absent" and "null" meaning the same thing is the commonest PATCH defect.

Optimistic concurrency — If-Match, honouring the guard the service already has. updateWorkItem accepts { expectedUpdatedAt } and raises StaleWorkItemError when the row moved underneath the caller. Expose it the HTTP-native way rather than inventing a body field: the item read (11.2.2) returns an opaque ETag derived from updatedAt; PATCH accepts If-Match, decodes it with that card's function (do not re-derive the encoding here) and passes it through as the precondition; a mismatch is 412 with a stable code. Omitting If-Match is legal and means last-write-wins — unchanged from today's behaviour, so this adds a guarantee without making one mandatory. Two agents patching the same item is the epic's own stated normal, which is why the guard is exposed rather than left internal.

The DOMAIN_ERROR_STATUS rows these two can raise, each added deliberately and each proven by a test that drives the real error: ILLEGAL_PARENT_TYPE, CROSS_PROJECT_PARENT, PARENT_CYCLE, DEPTH_LIMIT_EXCEEDED, TYPE_NOT_ALLOWED_ON_KIND, ASSIGNEE_NOT_IN_WORKSPACE, REPORTER_NOT_IN_WORKSPACE, UNKNOWN_TARGET_REPO, ARCHIVED_TARGET_REPO, INVALID_ESTIMATE → 422; WORK_ITEM_NOT_FOUND / PROJECT_NOT_FOUND → 404; STALE_WORK_ITEM → 412. 412 is a status the ADR's §4 table does not yet list, so this card appends the row (a new CONDITION getting a status, not an existing condition changing one — additive under §8) rather than silently emitting an undocumented status.

Scope BOUNDARY

Ends at create and update of the work-item resource itself. It ships no transition (a status move is 11.2.7updateStatus is its own service method with its own legality rules), no comment, no link, no archive, and no sprint assignment (moving an item into or out of a sprint is sprints:write and belongs to 11.3). It exposes no delete. It changes no service, repository or migration.

Acceptance criteria

  • POST creates the item, returns 201 with the detail resource and a Location header, and the created row carries planningSource: 'api' — asserted by reading the row back.
  • PATCH applies a partial update, leaves omitted fields untouched, and clears a nullable field on an explicit null; both cases asserted per field class.
  • Both responses parse against workItemDetailSchema; no cuid appears anywhere in either body, parentKey included.
  • parentKey resolves to the parent, and an unknown or cross-project parent key produces the mapped domain error, not a 500.
  • A read-only token is refused 403 on both, and the refusal is not a 200 with an empty body.
  • If-Match carrying a stale ETag returns 412; the same request without If-Match succeeds; the ETag from a fresh read succeeds — all three asserted against a real concurrent update.
  • Every mapped domain error above is exercised through the wrapper, so no row is unproven and no unmapped error becomes a silent 500.
  • Cross-tenant and unbrowsable-project targets return 404, never 403 (the existence-oracle rule).
  • The ADR §4 status table carries the 412 row with its condition.
  • Both exports compose withV1Route with their own declared scope and call no Prisma and no transaction.
  • The per-file coverage floor (≥90%) holds on every new and changed file.

Context refs

  • lib/services/workItemsService.tscreateWorkItem, updateWorkItem(id, patch, ctx, { expectedUpdatedAt }), its PATCH_KEYS set and the StaleWorkItemError path.
  • lib/dto/workItems.tsCreateWorkItemInput / UpdateWorkItemInput, the field sets the schemas mirror.
  • lib/workItems/errors.ts — the typed error classes and their code values.
  • lib/mcp/tools/createWorkItem.ts · lib/mcp/tools/updateWorkItem.ts — the shipped tools that prove both service paths and their argument shapes.
  • lib/api/v1/errors.tsDOMAIN_ERROR_STATUS, extended here.
  • docs/decisions/public-api-conventions.md — §3 (the scope map), §4 (the status table this card appends to), §8 (why adding a status for a new condition is additive).
  • Producers: 11.2.2 (schema + item module + the ETag) · 11.2.4 (collection module) · 11.2.5 (the api source). Parent story: 11.2.