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

An admin can RETIRE a lesson — a soft status change that stops injection and keeps the record

Reading a lesson you disagree with and being unable to do anything about it is worse than not seeing it. This story gives the admin the one action that matters: stop applying this.

A status change, never a delete. The lesson stops being injected and stays fully readable, with who retired it and when. A lesson that was applied for two months is part of the explanation for every plan generated in that window; deleting it makes those plans unexplainable afterwards.

Reversible. An admin who retires one and changes their mind can bring it back. That is what makes retiring a low-stakes action rather than one people avoid.

⚠️ A manual retirement is NOT the same state as ageing out, and the difference is the whole design. The corpus already retires by non-recurrence — a lesson unseen for the retention window drops out of results, and a recurrence revives it automatically. If a manual retirement writes the same state, then the next recurrence un-retires what a human deliberately switched off, and the planner resumes injecting a lesson its owner rejected. The two must be distinguishable: a human decision outranks the clock and survives a recurrence.

⚠️ AND THE SAME RULE RUNS THE OTHER WAY — an exempt from retirement FLAG (Yue, 2026-08-23). The block above says a human's no must outrank the clock. This decides the mirror case: a human's yes must outrank it too.

MOTIR-3332's design draws two not-applied rows — Not applied (switched off) and Not seen in 90 days (aged out by non-recurrence) — and gives BOTH the action Apply again. On the switched-off row that is a flip of the retirement state this card already owes. On the aged-out row there was nothing to write, and the two candidates were not equal:

  • bump lastOccurredAt — REJECTED. It fabricates an occurrence that did not happen, and recordRecurrence writes the clock and recurrenceCount together (MOTIR-3326) precisely so the two can never disagree; a bump would make the count lie or diverge.
  • an exempt from retirement flagCHOSEN. An exempt lesson is never dropped by the lastOccurredAt >= staleCutoff predicate, whatever its clock says. Nothing is fabricated: the lesson's history stays exactly what happened, and a separate field records that a person decided it still applies.

⚠️ These are ONE mechanism with two values, not two unrelated booleans. Manual retirement is a human said no, and the clock must not override it; exemption is a human said yes, and the clock must not override it. Both are the sentence this card already states — a human decision outranks the clock and survives a recurrence. Modelling them as one human override (unset → the clock decides · retired → never injected, and a recurrence does not revive it · exempt → always injected, and ageing out does not drop it) keeps the two rules that matter in one place; two independent flags invite the fourth state nobody has defined, and let a later edit satisfy one rule while breaking the other. enabled stays what it is; this is the axis beside it.

On the surface, exempting an aged-out lesson returns it to the ordinary applied row — the Not seen in 90 days badge disappears, because the statement it makes is no longer true of what the planner is being told.

That is the sharp end of this story. Everything else is a row, a button and an audit field.

Permission-guarded, on the mutation as well as the control — a request without the permission is refused at the API, not merely un-rendered. Tenant lessons only: a project may retire what it taught its own planner, never the shipped global corpus.

Acceptance criteria

  • An admin can retire a lesson from the list or the detail view, and un-retire it.
  • A retired lesson is excluded from injection, asserted at the query — not filtered in the view.
  • A retired lesson remains readable, and shows who retired it and when.
  • A recurrence does NOT revive a manually retired lesson, asserted directly. Ageing out and being switched off are different states.
  • An aged-out lesson can be brought back and STAYS backApply again on a Not seen in 90 days row exempts it from retirement, and it is still injected after the retention window passes again, asserted with the clock moved rather than waited out.
  • Nothing writes lastOccurredAt or recurrenceCount to express a human decision, asserted: the occurrence history records only occurrences.
  • A retire request without the permission is refused at the API.
  • A global lesson cannot be retired through this surface, asserted.
  • The page-state contract is honoured: the row's own state updates from the response, and any server-rendered count elsewhere on the surface refreshes.
  • An E2E walks: retire → confirm it no longer applies → un-retire → confirm it does.

Context refs

  • motir-ai src/repositories/lessonRepository.tsenabled, the lastOccurredAt >= staleCutoff clause, and recordRecurrence, which re-asserts enabled and is exactly what must not override a human.
  • motir-ai src/services/lessonService.tsretirementCutoff, the injection read.
  • motir-core lib/permissions/catalog.ts — the keys added by the sibling story.
  • MOTIR-3329 — the surface and the design this action lives in.
  • MOTIR-3323 — the automatic revival this must not collide with.