fix(rules): name a repeat violation instead of calling it uncertain
Why
A rule check that is sure, and then goes quiet, feels broken. After two blocks on the same rule and file, the next act-band hit was shown as uncertain about. The agent was not told, and you could not tell a shrug from a spent budget.
Two instruction bullets that slug to the same id also shared one probability. The question key was already unique. The verdict map was not, so the later rule's score replaced the earlier one and both citations used it.
The catch rate stays 17 of 29 planted violations blocked on the v0.23.0 rescore. Changing the questions or the 0.80 bar without a new run would widen a guard VISION.md says to measure first. This change does not touch either.
Scope
withheldNoticeinsrc/rules.tsandwithheld_noticeinscripts/rules.py. On an edit, a later act-band hit for that rule and file says it was already raised. A flag-band hit staysuncertain about.- A Stop hit held back by the session cap says the session already used its two turn blocks. A hit held back because the agent is still finishing says it is not sent again. Neither claims that rule was cited. AFK's sweep uses the session-cap wording (
claude-jev-afk0.6.1). assignQuestionKeysruns on the full in-scope list and keeps those keys for the escalation call. An unasked duplicate scores 0.actForreads the question key, then the rule id.invokedDirectlycompares realpaths, so a symlinked launch still runsmain.- Versions move to 0.29.0 on the Claude plugin and 0.6.1 on the AFK adapter.
Tradeoffs
PreToolUse blocking on Claude Code would stop the write, which is what AFK already does. The edit-phase evidence is read from disk after the write. Moving the hook would change what Jev sees, and that needs a measured run.
The Stop notice does not record which rule ids were cited. The session cap is one counter. The sentence describes the cap instead of inventing a per-rule history.
Blast Radius
Claude Code still blocks an edit rule at most twice per file, and Stop still blocks at most twice per session. A missing key still prints nothing and exits 0. Duplicate-id rules now keep separate scores, including on the escalation pass.
Verification
withheld_notice and collect_verdicts were called directly. A 0.91 edit hit with an empty acting list returns already raised this session. A 0.62 hit still returns uncertain about. A Stop hit returns the session-cap sentence and does not say already raised.
Two rules with id same keep keys same and same-2 after a second assign_question_keys on only the second rule. An answer present only for same leaves same-2 at 0. act_for on same-2 uses a same-2 calibration row when present, and the same row otherwise.
A direct launch and a symlink to src/rules.ts both append a rules-error row for non-JSON stdin. ruff format --check, ruff check, and node --experimental-strip-types --check passed for the changed TypeScript. check_no_comments.py and check_no_stubs.py passed. No live key, so the third block in a real session was not driven.


Summary by cubic
Names a rule that repeats after its block budget is spent "already raised this session" instead of "uncertain about", and keeps separate probabilities for rules that share an id.
- Flag-band hits (0.50–0.80) keep the
uncertain aboutnotice; act-band hits (≥0.80) past the budget say the rule was already raised. The agent is still not told again, and blocking still happens at most twice per session. - Question keys are assigned once and kept through escalation, so a second pass or a rule the subject filter skips cannot write one rule's score onto another.
- Stop hits held back by the two turn blocks say the session used its cap; a hit held back while the agent is still finishing says it was not sent again. AFK's Stop sweep uses the same split, and its text still rides in the next prompt.
- Versions bump to
claude-jev0.29.0 and@claude-jev/afk-adapter0.6.1,src/rules.tscan be imported without runningmain, and the entry check compares realpaths so a symlinked launch still runs.