feat(moments): mount the corpus by proposal - a pass and an open-after-moment signal, both stopping at the operator (milestone 458 step 7, #4925)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / integration (push) Failing after 43s
CI & Build / Python tests (push) Failing after 46s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / integration (push) Failing after 43s
CI & Build / Python tests (push) Failing after 46s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Build & push image (push) Skipped
A rule written before moments existed is mounted on nothing. Step 7 records, per (rule, moment), whether it belongs there and who said so: - rule_moment_judgments (migration 0118, backup v23): suggested / confirmed / rejected, from a pass, the signal, or an edit. Moment "" is "no moment fits". - The pass: rules_to_mount lists unjudged rules; propose_rule_moments records suggestions that mount nothing; rule_moment_proposals and judge_rule_moments put them to the operator. A confirm mounts, a reject is kept so the pair is never proposed again. Same service behind REST and a "Waiting on you" panel in Settings > Moments. - Edits are judgments: set_rule_moments, the one mount write path, confirms what was added and rejects what was removed in the same transaction. - The signal: scribe_moment.sh keeps a per-session acts ledger; when a rule is opened, scribe_record_opened.sh sends the last three minutes of it to /api/plugin/rule-opened. The acts resolve through the install's mappings; work.run and work.change are not evidence. Counted per distinct session with lesson_rules' evidence model, and once due the open returns one line asking the reader to offer the mount. - scribe_session_end.sh removes the session's scribe-moment files. Plugin 2026.10.05.2003. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ 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
|
||||
from scribe.services import rule_moment_judgments as judgments_svc
|
||||
|
||||
|
||||
async def list_moments() -> dict:
|
||||
@@ -105,7 +106,73 @@ async def unmap_action(tool: str, moment: str, match: str = "", reason: str = ""
|
||||
)
|
||||
|
||||
|
||||
async def rules_to_mount(limit: int = 25, offset: int = 0) -> dict:
|
||||
"""The rules nobody has decided the moments of — the pass over the corpus, a page at a time.
|
||||
|
||||
A rule written before moments existed is mounted on nothing and arrives
|
||||
only when its words resemble the work. Read each rule here, decide WHEN it
|
||||
applies, and record the answer with `propose_rule_moments`: the moments it
|
||||
belongs on, or that none fits because it is about WHAT is done rather than
|
||||
when. A rule leaves this list as soon as any answer is recorded, so the
|
||||
next call returns the next unread rules.
|
||||
|
||||
Returns `rules` (id, title, kind, statement, when_to_apply, home),
|
||||
`total_unjudged`, and the `catalog` to choose from.
|
||||
"""
|
||||
return await judgments_svc.unjudged_rules(current_user_id(), limit=limit, offset=offset)
|
||||
|
||||
|
||||
async def propose_rule_moments(proposals: list[dict]) -> dict:
|
||||
"""Propose the moments rules belong on — for the operator to confirm, never mounted by this call.
|
||||
|
||||
Each item is either
|
||||
`{"rule_id": N, "moments": ["work.finish", "reply.report"], "why": "…"}`
|
||||
or `{"rule_id": N, "none": "why no moment fits"}`.
|
||||
|
||||
Propose a moment when the rule governs that point in the work whatever
|
||||
the work is about — a rule about when work counts as done belongs where
|
||||
work is finished, delivered, verified and reported. Say none fits when
|
||||
the rule is about a subject (a library, a file, a style) and is best
|
||||
reached by meaning. `why` is shown to the operator beside the proposal;
|
||||
write it so they can say yes or no without opening the rule.
|
||||
|
||||
A moment proposal waits as `suggested` until judged; show the operator
|
||||
what you proposed (`rule_moment_proposals`) and let them decide. A pair
|
||||
already mounted or judged is skipped and listed under `skipped`; a bad
|
||||
item is refused alone under `refused`.
|
||||
"""
|
||||
return await judgments_svc.propose(current_user_id(), proposals)
|
||||
|
||||
|
||||
async def rule_moment_proposals(rule_id: int = 0) -> dict:
|
||||
"""The moment proposals waiting on the operator, grouped by rule.
|
||||
|
||||
Each rule carries what it is mounted on now and its proposals: the moment,
|
||||
where the proposal came from (`pass` — read from the rule; `signal` — the
|
||||
rule kept being opened just after that moment fired), the reason, and the
|
||||
evidence counts. `rule_id` narrows to one rule.
|
||||
"""
|
||||
return await judgments_svc.pending(current_user_id(), rule_id=rule_id or None)
|
||||
|
||||
|
||||
async def judge_rule_moments(judgments: list[dict]) -> dict:
|
||||
"""Confirm or reject moment proposals — on the operator's word, since a confirm mounts the rule.
|
||||
|
||||
Each item is `{"rule_id": N, "moment": "work.finish", "verdict":
|
||||
"confirm" | "reject", "note": "why"}`. Confirm mounts the rule on that
|
||||
moment beside what it already has; reject records that it does not belong
|
||||
there (and unmounts it if it was mounted), so neither the pass nor the
|
||||
signal proposes the pair again. Moment `""` judges a "no moment fits"
|
||||
answer. Put the operator's reason in `note`.
|
||||
"""
|
||||
return await judgments_svc.judge(current_user_id(), judgments)
|
||||
|
||||
|
||||
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)
|
||||
mcp.tool(name="rules_to_mount")(rules_to_mount)
|
||||
mcp.tool(name="propose_rule_moments")(propose_rule_moments)
|
||||
mcp.tool(name="rule_moment_proposals")(rule_moment_proposals)
|
||||
mcp.tool(name="judge_rule_moments")(judge_rule_moments)
|
||||
|
||||
Reference in New Issue
Block a user