MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3553

(motir-core) `reinforce_lesson` — the MCP door a hand-run sweep calls, on the `id` `search_lessons` already hands back

Done
Description

The door a hand-run sweep actually calls. search_lessons already hands the caller everything it needs to name a lesson; this is the write that closes the loop.

What to build — the same six-file chain search_lessons took (MOTIR-3480)

  1. motirAiClient.reinforceLesson — the boundary call onto POST /v1/lessons/:id/reinforce, mirroring searchLessons's request/response typing.
  2. projectLessonsService.reinforceLessonthe permission assert happens HERE, BEFORE the upstream call, exactly as searchLessons asserts lesson:view before spending the caller's budget.
  3. The reinforce_lesson MCP tool — args projectKey, lessonId, occurrenceRef, optional note. Registered in lib/mcp/registry.ts.
  4. The permission + the CLI grant (below).
  5. The payload exemption doc in lib/mcp/payloads/exemptions.ts, beside the search_lessons and add_lesson entries and for the same stated reason: no /api/v1 operation returns a lesson.
  6. The tool DESCRIPTION, which is a deliverable and not a label — it must teach the discriminator, because an agent that reinforces on every edit is worse than one that never reinforces at all. It says: call this when an occurrence you just had matched this lesson — whether or not you then widened it — and do NOT call it for an editorial pass with no incident behind it.

⚠️ A NEW permission, not lesson:manage

lesson:manage is the key add_lesson takes and the key retiring a lesson takes — a policy decision that removes a row from every future injection. Reinforcement is its opposite: additive, idempotent, and the thing we want a routine run to do freely. Granting lesson:manage to reach it would hand every dispatched agent the ability to retire. Add lesson:reinforce and grant that.

⚠️ Widening CLI_TOKEN_GRANT is not a one-line edit — it trips FOUR guards. lib/mcp/toolPermissions.ts is only the first; find and satisfy the rest in the same PR rather than discovering them in CI.

Acceptance criteria

  • reinforce_lesson is registered and callable, taking projectKey, lessonId and occurrenceRef; the lesson id is the one search_lessons returns, and this card changes nothing about search_lessons to obtain it.
  • projectLessonsService.reinforceLesson asserts lesson:reinforce before the motir-ai call; a caller without it is refused without the upstream ever being reached. Asserted.
  • lesson:reinforce exists as its own key and is in CLI_TOKEN_GRANT; lesson:manage is NOT added to that grant, and a test pins that a CLI-minted token can reinforce and cannot retire.
  • Every guard that reads CLI_TOKEN_GRANT is updated in this same PR and the PR body lists them.
  • The tool description states the discriminator in its own words: an occurrence that just happened is a hit whether or not an edit follows; an editorial pass with no incident is not.
  • A replay of the same occurrenceRef succeeds and reports that it did not count — the tool surfaces the ledger's answer rather than swallowing it.
  • The exemptions.ts entry documents the returned payload, in the register its two siblings use.

Context refs

  • lib/mcp/tools/searchLessons.ts — the adapter shape: getByKey, the gate placement, toolOk(prose, exempt(...)).
  • lib/mcp/tools/addLesson.ts — the WRITE-side tool shape and its lesson:manage assert.
  • lib/services/projectLessonsService.tssearchLessons (the assert-before-boundary pattern), toRankedLessonDTO (:162).
  • lib/ai/motirAiClient.tssearchLessons / RawRankedLesson (:1153).
  • lib/mcp/registry.ts, lib/mcp/toolPermissions.ts (CLI_TOKEN_GRANT), lib/mcp/payloads/exemptions.ts (:70).
  • MOTIR-3480 — the six-file precedent this follows. MOTIR-3547 (the parent) — the rule, stated once.