feat(moments): a mount that keeps arriving where it does not apply proposes its own removal (milestone 458 step 7b, #4955)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m3s
CI & Build / Build & push image (push) Successful in 18s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m3s
CI & Build / Build & push image (push) Successful in 18s
The open-after-moment signal proposes a mount; nothing proposed taking one off, so a wrong mount was noise at every occurrence until someone happened to notice. rule_misfired(rule_id, moment, why, reached_by) records a report against a MOUNTED pair, counted per distinct day (the MCP door carries no session id) on a new rule_moment_judgments.misfire column (migration 0119, backup v24). At three days the response carries a line asking the agent to offer the operator the fix - reject takes the rule off, unmap_action stops the action reaching the moment, confirm keeps the mount and stops the asking - and Settings > Moments lists it as an unmount proposal with the reasons and the actions that reached it. A re-mount clears the count. Taught in moments.md, missed-retrieval.md and the reply hold's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ the signal hands a session names the tool that answers it.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -58,3 +59,46 @@ def test_the_proposal_line_names_the_tool_that_answers_it():
|
||||
assert "`work.verify`" in line and "3 distinct sessions" in line
|
||||
assert '"rule_id": 11' in line and '"moment": "work.verify"' in line
|
||||
assert "judge_rule_moments" in line and '"reject"' in line
|
||||
|
||||
|
||||
# ── The misfire (step 7b) ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_a_day_is_one_situation_and_days_are_distinct():
|
||||
"""The day arm keys verbatim: tokenised, "10" and "05" fall under the
|
||||
minimum token length and every day of a year would be one situation."""
|
||||
a = datetime(2026, 10, 5, 1, tzinfo=timezone.utc)
|
||||
assert svc.misfire_situation(a) == svc.misfire_situation(a + timedelta(hours=20))
|
||||
assert svc.misfire_situation(a) != svc.misfire_situation(a + timedelta(days=1))
|
||||
assert svc.misfire_situation(a) != svc.misfire_situation(a + timedelta(days=31))
|
||||
|
||||
|
||||
def test_a_misfire_counts_reasons_and_actions():
|
||||
now = datetime(2026, 10, 5, tzinfo=timezone.utc)
|
||||
mf = None
|
||||
for i in range(svc.MISFIRE_REASONS + 2):
|
||||
mf = svc.add_misfire(mf, svc.misfire_situation(now + timedelta(days=i)), 7, now,
|
||||
why=f"reason {i}", reached_by="git push" if i % 2 else "make ship")
|
||||
assert len(mf["situations"]) == svc.MISFIRE_REASONS + 2
|
||||
assert [r["why"] for r in mf["reasons"]][-1] == f"reason {svc.MISFIRE_REASONS + 1}"
|
||||
assert len(mf["reasons"]) == svc.MISFIRE_REASONS
|
||||
assert mf["reached_by"] == {"make ship": 4, "git push": 3}
|
||||
assert mf["projects"] == [7]
|
||||
|
||||
|
||||
def test_the_unmount_line_offers_both_fixes():
|
||||
rule = SimpleNamespace(id=10, title="CI verifies", kind="rule")
|
||||
mf = {"situations": ["a", "b", "c"], "reached_by": {"actions_run_read": 3}}
|
||||
line = svc._unmount_line(rule, "work.verify", mf)
|
||||
assert line.startswith("> Rule #10") and "3 distinct days" in line
|
||||
assert "`actions_run_read` ×3" in line
|
||||
assert '"verdict": "reject"' in line and "unmap_action" in line and '"confirm"' in line
|
||||
|
||||
|
||||
def test_a_held_reply_names_the_misfire_call_only_for_a_mount():
|
||||
from scribe.services.moment_delivery import reply_hold_reason
|
||||
|
||||
mounted = {"rule_id": 11, "title": "Definition of done", "moment": "reply.report", "trigger": ""}
|
||||
scored = {"rule_id": 12, "title": "Other", "moment": "", "score": 0.81, "trigger": ""}
|
||||
assert 'rule_misfired(11, "reply.report", why)' in reply_hold_reason([mounted])
|
||||
assert "rule_misfired" not in reply_hold_reason([scored])
|
||||
|
||||
Reference in New Issue
Block a user