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

motir-core: the retire mutation route + client, guarded by the retire permission

The core half of the write: client methods for retire and un-retire, and the mutation route that calls them.

Guarded by the RETIRE permission, not the view one. The two keys exist precisely so a role can read the list without changing what the planner does; checking the wrong key here collapses that distinction on the first call site and nobody notices, because an admin holds both.

Check before calling, as the read route does — a caller without the permission is refused and no upstream request is made.

Pass the acting user through. The upstream records who retired the lesson, and it can only do that if core sends it. This is the field most likely to be dropped as "not needed to make it work", and its absence is invisible until somebody opens the detail view and finds an audit line that says nothing.

Map the upstream's typed refusals to something the UI can render — a global lesson refused, a lesson in another project, a lesson that no longer exists. A generic 500 for a refusal the product deliberately makes is a design failure one layer down.

⚠️ Amendment (2026-08-23) — two client methods over one axis, and the server picks apply's value

Authored 2026-08-21; the parent story MOTIR-3330's 2026-08-23 exempt from retirement amendment outranks this card's original wording.

The upstream (MOTIR-3344) exposes retire and apply over one humanOverride axis and decides apply's value itself from the row's clock. So core sends no override value: retireLesson and applyLesson carry the acting user and nothing else. The retire permission is lesson:manage (MOTIR-3336); lesson:view must not satisfy it.

Upstream refusals to map: not_found (unknown id, another project's id, and a global lesson — deliberately the same answer) and the unavailable/config arms projectLessonsService already degrades on for the reads.

Acceptance criteria

  • Client methods for retire and un-retire, matching the neighbouring methods' shape and error handling.
  • The route checks the retire permission specifically; a caller holding only the view permission is refused, asserted.
  • No upstream call is made when the permission check fails, asserted on the call count.
  • The acting user is sent upstream and appears on the read-back, asserted end to end.
  • Upstream typed refusals surface as distinguishable, renderable errors — not a generic failure.
  • Route tests cover the guard, the pass-through and each refusal; no UI in this card.

Context refs

  • motir-core lib/ai/motirAiClient.ts — the client conventions.
  • motir-core lib/permissions/resolve.ts — the check.
  • MOTIR-3336 — the two keys, and why the retire one is the right one here.
  • MOTIR-3344 — the upstream endpoint and its typed errors.
  • MOTIR-3337 — the read seam this mirrors.