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

motir-ai: the retire / un-retire endpoint, tenant-scoped, recording the actor

The write half, on the /v1 surface beside the lessons read: retire a lesson, and un-retire it.

Both directions, one endpoint pair. Un-retiring is not an afterthought — it is what makes retiring a cheap decision rather than one people avoid, and shipping the two together stops the reverse being designed later by someone with less context.

Tenant only, and enforced at the query. A retire request naming a global lesson is refused, not silently no-op'd: a project may switch off what it taught its own planner, never what the product ships to everyone. Same project resolution as the read — from the caller's core project id, never a parameter the caller supplies.

Record the actor and the moment. The surface has to answer who turned this off and when, so the write carries the acting user through from core rather than stamping a service identity. A retirement attributed to "the system" is not an audit trail.

Idempotent. Retiring a retired lesson succeeds and changes nothing; the same for un-retiring a live one. A UI that can double-submit should not be able to produce an error a user cannot act on.

Build on the state model the sibling bug establishes — do not introduce a second notion of "off" beside it.

⚠️ Amendment (2026-08-23) — the endpoint pair sets ONE axis with three values

Authored 2026-08-21; the parent story MOTIR-3330 was amended by Yue on 2026-08-23 to add an exempt from retirement value on the SAME axis this endpoint writes, so the amendment outranks the card's original two-direction wording.

The sibling bug MOTIR-3343 ships humanOverride = retired | exempt | unset. This card writes it, and the server decides which value apply means rather than taking it as a parameter — the client must not choose server state:

  • POST /v1/lessons/:id/retireretired.
  • POST /v1/lessons/:id/applyexempt when the row's own clock has already fallen behind the retention window, otherwise unset. That is exactly what Apply again means on the design's two not-applied rows (design/ai-settings/design-notes.md §L6), computed from the row rather than asserted by the caller — so no fourth state can be reached and nothing fabricates an occurrence.

A global lesson is refused as not_found, not as a distinct code. The tenant predicate is the same findTenantById the read uses, so a global id is indistinguishable from an unknown one — the existence-oracle posture getTenantLesson already documents, and preserving it is what keeps the endpoint from disclosing that a row exists elsewhere. Adding a new problem code would be a §5 union change landing in both repos, which this card does not own.

Acceptance criteria

  • Retire and un-retire both exist on /v1, following the existing envelope, auth and error conventions.
  • A retired lesson stops being returned by the injection read, asserted at the query.
  • A retired lesson is still returned by the inspection read, carrying its retired state, the actor and the timestamp.
  • Retiring a global lesson is refused with a typed error, asserted.
  • A lesson belonging to another project cannot be retired, asserted.
  • Both operations are idempotent.
  • The acting user is recorded; no write attributes a retirement to the service.
  • Unit + integration tests against the real test database.

Context refs

  • motir-ai src/repositories/lessonRepository.ts — the state model and the injection clause this must satisfy.
  • motir-ai src/routes/ — the /v1 envelope, auth and typed errors.
  • MOTIR-3343 — the state model and audit fields this writes.
  • MOTIR-3335 — the read this sits beside, and its project-scoping pattern.