"""Moments as MCP tools: the catalog, and the in-session corrections to it (milestone 458). A session needs the vocabulary in hand to mount a rule, to read a line that names a moment, and to fix a misfire — and the operator's ruling is that the fix happens in the session, not on a settings page: "making the user leave the session to fix a misfire is not desirable and the llm session should be able to offer corrections" So the mapping writes are tools, built to be offered mid-work and made on the operator's yes. """ from __future__ import annotations from scribe.mcp._context import current_user_id from scribe.services import moment_actions as actions_svc from scribe.services import moments as moments_svc async def list_moments() -> dict: """The moments of work that rules mount on, and which actions reach each one here. A rule mounted on a moment arrives whenever that moment happens, whatever the words of the work look like. That is how a rule reaches you when it is about WHEN something is done rather than WHAT it is about: a rule on finishing work belongs at `work.finish` and `reply.report`, where nothing said needs to resemble it. Read this when you are about to mount a rule, when a line you were shown names a moment and you want its meaning, or when an action reached the wrong moment — or none — and you are about to correct it with `map_action` / `unmap_action`. Names are `.`; a named procedure (a skill or a stored process) is its own moment, `skill.`, listed under `families`. Each moment says what is happening at it (`means`) and the kinds of action that typically reach it (`reached_by`). `actions` maps each moment to the actions that reach it on this install: `via: "default"` ships with the product, `via: "install"` is this install's own mapping. `removed_defaults` are shipped defaults this install switched off. """ out = moments_svc.catalog() out.update(await actions_svc.actions_by_moment(current_user_id())) return out async def map_action(tool: str, moment: str, match: str = "", reason: str = "") -> dict: """Make an action reach a moment on this install — the in-session fix for a missed moment. Use it when an action plainly happened at a moment and the moment did not fire: the operator ships with `make ship`, and nothing mounted on `work.deliver` arrived. Offer the mapping when you notice, in one line ("that `make ship` was a deliver and nothing fired — map it?"), and make it on their yes. The correction lasts: every later session on this install gets it. Args: tool: the tool as the harness names it — `Bash`, `Edit`, `update_task`. An MCP server prefix is ignored. moment: a moment from `list_moments`, or `skill.`. match: which calls of the tool. Empty = every call. For a tool that runs a command, how the command starts (`make ship`, `./deploy.sh`) — it is checked against each part of a compound command line. For any other tool, its arguments as `field=value` pairs, comma-separated (`status=done`). reason: what misfired, or what this action is for here. Optional; it is shown beside the mapping when someone reviews it later. Returns the change made and `now_reaches`: every moment that action reaches after the change, so you can confirm it in the same reply. Mapping a shipped default this install had switched off switches it back on. """ return await actions_svc.map_action( current_user_id(), tool, match, moment, reason=reason, actor="model", ) async def unmap_action(tool: str, moment: str, match: str = "", reason: str = "") -> dict: """Stop an action reaching a moment on this install — the fix for a moment that fires wrongly. Use it when a moment fires on an action that is not that moment here — a `curl` to a local test server reaching `env.reach`, say — and the rules it brings are noise every time. Offer it when you notice, and make it on the operator's yes. The arguments name the mapping exactly as `list_moments` shows it. This install's own mapping is removed; a shipped default is switched off for this install only, and stays off across upgrades. `map_action` with the same arguments switches a default back on. Args: tool: the tool as `list_moments` names it. moment: the moment it should stop reaching. match: the mapping's match, as listed (empty for a whole-tool mapping). reason: why it misfires here — worth giving for a default, since it is the record of why this install differs from the product. Returns the change made and `now_reaches` for that action. """ return await actions_svc.unmap_action( current_user_id(), tool, match, moment, reason=reason, actor="model", ) def register(mcp) -> None: mcp.tool(name="list_moments")(list_moments) mcp.tool(name="map_action")(map_action) mcp.tool(name="unmap_action")(unmap_action)