feat(moments): mounted rules arrive when their moment happens, through every door (milestone 458 step 4a, #4922)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m19s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m19s
CI & Build / Build & push image (push) Skipped
A rule mounted on a moment now reaches the session when an act reaches
that moment, with no semantic match involved:
- run_moment_arm on the pipeline: a lookup, not a ranked search. Each
line names the moment and the act that reached it ("at work.deliver,
reached by `git push`"), so a misfire is visible where it lands and
can be unmapped in-session. A repeat is cited, not quoted; fresh
rules are recorded surfaced under source moment_rule with the moment
in detail. No retrieval_logs row, as for the other lookups, so no
latency is persisted for this arm.
- rule_scope: a rule's home clause, moved out of semantic_search_rules
so the moment lookup scopes by the same one.
- rulebooks.rules_on_moments / mounted_moments.
- The plugin door: a catch-all PreToolUse hook (scribe_moment.sh). It
keeps /moment-tools' answer on disk for five minutes, so a call to a
tool that cannot reach a mounted rule sends nothing, and an install
that has mounted nothing sends one request per window. It shares the
rules ledger with the other arms and fails open silently.
- The MCP door: Scribe's own tools named by the shipped mappings carry
moment_rules in their response, so a client without the plugin gets
them too. The hook skips those tools. A guard pins the attach on
every one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,11 @@ logger = logging.getLogger(__name__)
|
||||
# Add a source here only when a ranker picked it.
|
||||
RANKED_SOURCES = (
|
||||
"write_path_rule", "pre_tool_rule", "prompt_rule",
|
||||
# A rule mounted on a moment (milestone 458). Not a ranker's pick, but
|
||||
# not bulk either: somebody decided this rule applies at this moment, and
|
||||
# that is exactly the claim a pull can confirm or refute. Its
|
||||
# pull-through is the evidence for whether a mount earns its line.
|
||||
"moment_rule",
|
||||
# A reserved slot is a ranker's choice twice over — it ran a query AND
|
||||
# decided a kind was worth guaranteeing a place. Left out, its line would
|
||||
# be counted as bulk delivery and drop out of the denominator, so the one
|
||||
@@ -154,7 +159,8 @@ def _schedule(rows: list[dict]) -> None:
|
||||
|
||||
|
||||
def record_rule_surfaced(
|
||||
*, user_id: int | None, rule_ids: list[int] | set[int], source: str
|
||||
*, user_id: int | None, rule_ids: list[int] | set[int], source: str,
|
||||
detail: dict[int, str] | None = None,
|
||||
) -> None:
|
||||
"""Fire-and-forget: record that these rules were shown to the agent.
|
||||
|
||||
@@ -173,6 +179,10 @@ def record_rule_surfaced(
|
||||
from the other end — everything in a preload IS shown. `source` is what
|
||||
separates the two afterwards (see `RANKED_SOURCES`); this function does not
|
||||
care which kind it is recording.
|
||||
|
||||
`detail` maps a rule id to what to record beside its row — the moment a
|
||||
mounted rule arrived at (milestone 458), so a mount's pull-through can be
|
||||
read per moment rather than only per arm.
|
||||
"""
|
||||
try:
|
||||
rows = [
|
||||
@@ -181,6 +191,7 @@ def record_rule_surfaced(
|
||||
"rule_id": int(rid),
|
||||
"event": SURFACED,
|
||||
"source": source,
|
||||
**({"detail": detail[rid]} if detail and rid in detail else {}),
|
||||
}
|
||||
for rid in rule_ids
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user