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

11.2.8 `GET` + `POST /api/v1/work-items/{key}/comments` — the discussion sub-resource, paged in the v1 cursor envelope

Comments are the half of a work item that is conversation rather than fields, and until MOTIR-1999 shipped get_work_item_activity no agent surface could read them at all. app/api/v1/work-items/[key]/comments/route.ts carries GET (scope: 'read') and POST (scope: 'work_items:write'), both thin adapters over commentsService.

What to build

GET — the comment page, re-wrapped in the v1 cursor envelope. commentsService.listComments(workItemId, { cursor, order }, ctx) already returns a cursor-paged CommentsPageDTO (threaded: root comments with their replies), so the data path is shipped. Two seams need care, and neither is optional:

  • The service's cursor must NOT be passed through raw. It is a bare root-comment id — not opaque, not signed, and therefore forgeable, which is exactly what lib/api/v1/pagination.ts refuses ("a client that can hand-craft a cursor has made the underlying sort key public API"). Wrap it: encodePageCursor the last root comment's { createdAt, id }, and feed the decoded id back to the service on the next call. The v1 cursor stays signed and opaque; a foreign or tampered one is the same 422 every other collection returns.
  • limit must mean something. ListCommentsOptions is { cursor, order } — the page size is the fixed, Jira-faithful COMMENT_PAGE_SIZE = 20 — so a v1 ?limit= would silently do nothing, which is worse than not offering it. Add an optional limit to listComments (and the repository read beneath it), clamped to the v1 ceiling, defaulting to the shipped 20 so every existing caller is unaffected. This is the same bounded read-addressing carve-out 11.2.1 records — a page-size parameter over an unchanged predicate, no new filter, gate or field.

?order=asc|desc passes through to the service's own option (oldest-first default, the Jira sort).

POSTcommentsService.addComment(workItemId, { bodyMd, parentCommentId }, ctx), returning 201 with the created comment. parentCommentId makes it a reply; the service owns depth and parent validation.

Identifiers: a comment has no MOTIR-<n> key, so its cuid IS its identifier and appears on the wire — the deliberate exception to the key-only rule, which governs WORK-ITEM references. Note it in the schema so the exception is read as a decision rather than a leak.

Domain rows added to DOMAIN_ERROR_STATUS: COMMENT_NOT_FOUND → 404, EMPTY_COMMENT_BODY / INVALID_PARENT_COMMENT / REPLY_DEPTH_EXCEEDED → 422, COMMENT_FORBIDDEN403 (this one is genuinely a "you may not do this KIND of thing" refusal on an item the caller can already see, so the existence-oracle argument does not apply — the item's own visibility is settled before the comment gate runs).

Scope BOUNDARY

Ends at reading and adding comments. It ships no edit and no delete of a comment (editComment / deleteComment are moderation-gated operations whose scope mapping the ADR's §3 table does not cover — exposing them is additive under §8 and belongs with that mapping, not smuggled in here), no reactions, no attachments (the upload path is presigned and session-bound — a separate, later card), and not the full ACTIVITY stream (activityService.listAll mixes comments with history; the v1 activity resource is not in this story's endpoint set). It changes no comment behaviour, no mention handling and no notification.

Acceptance criteria

  • GET …/comments returns the threaded page for a read token, each row parseing against the comment schema, with replies nested as the service returns them.
  • The nextCursor is a v1-issued, signed cursor: a hand-constructed or tampered cursor is 422, and a raw service cursor value is not accepted.
  • Walking the pages to exhaustion returns every comment exactly once for a work item seeded past one page — including the short-page case, where a page may be shorter than limit while more remains.
  • ?limit= is honoured up to the v1 ceiling and defaults to the shipped 20 when omitted; a test proves an existing caller of listComments that passes no limit still gets 20.
  • ?order=desc reverses the walk and still pages correctly.
  • POST creates a comment (201) and a reply when parentCommentId is given; both are visible on the next GET.
  • A read-only token is 403 on POST; a comment on an item outside the token's workspace is 404.
  • Each mapped domain error is exercised through the wrapper, including the 403 on the comment gate.
  • Routes pass the shipped architecture guard; the service change carries its own unit + integration tests against real Postgres.
  • The per-file coverage floor (≥90%) holds on every new and changed file.

Context refs

  • lib/services/commentsService.tslistComments, addComment, ListCommentsOptions, COMMENT_PAGE_SIZE.
  • lib/dto/comments.tsCommentDTO / CommentsPageDTO, the shapes the v1 schema maps from.
  • lib/comments/errors.ts — the typed codes to map.
  • lib/mcp/tools/getWorkItemActivity.ts — the shipped cursor pass-through, and its note that a short page with a non-null cursor is normal.
  • lib/mcp/tools/addComment.ts — the shipped write path.
  • lib/api/v1/pagination.tsencodePageCursor / decodePageCursor and why an unsigned cursor is refused.
  • Carve-out that authorises the limit option: 11.2.1. Producer: 11.2.2. Parent story: 11.2.