Two commits: milestone #394's step 2 and milestone #399's step 4. Together they are how a rule or preference reaches a session at the moment it governs what to say, rather than only what to do.
44e0b05 — rules retrieve against the operator's message (#3852)
Both existing rule arms are keyed on an act: write_path_rule on code, pre_tool_rule on a command. A rule that governs a RESPONSE has no such trigger — extract intent from loose phrasing, raise a conflict before acting, hand off an action with its reason, end a finding with an offer all bind on what gets said, and no tool call precedes a response.
The operator's message is the only query that exists before one is composed. That hook searched notes alone, so no rule had ever been retrieved against something the operator actually said — and residency was the only surface those rules had, which is what milestone #394 removes.
A separate function, not a branch in build_autoinject_hint. That arm bails when auto-inject is disabled, when the query is blank, when nothing clears the note bar — every one a statement about NOTES. Folded in, an operator who turned the awareness menu off would silently lose their rules, and the coupling has no symptom because both look like a quiet hook. Guarded as "the rule arm never asks the notes arm's config".
The bar is INHERITED from the act arms and the code says so. 0.72 was tuned against code and shell commands; prose is a different query shape, and triggers are written in the vocabulary of the moment — which for most rules is act vocabulary. Every call logs under prompt_rule from the first deploy, zeros included, so a few days of traffic settles it rather than a guess.
A rule crowded out at the prompt boundary still fires at an act arm; a preference about how to answer has no second chance, because the response IS the act. A straight ranking favours the record whose loss is recoverable over the one whose loss is total, and does it invisibly — the rule that won is a legitimate hit and the telemetry reads healthy.
semantic_search_rules gains a kind filter so the reserved query can only answer with a preference. The slot buys POSITION, not a lower bar.
It extends rather than displaces, parting from reuse_slot. A displaced hit would sit in prompt_rule's retrieval_logs row while never being surfaced, so that source's two tables would stop agreeing and #3668's identity would break for a reason nothing in the data explains — which is what milestone #379 cost to learn.
Operator-facing
No migration. 0098 already shipped in #150; this is delivery only.
Plugin version minted2026.09.11.1154 — the UserPromptSubmit hook now sends and stores rule ids on the shared session ledger, so this needs a deploy plus /reload-plugins.
Two new retrieval_logs sources begin recording at the deploy: prompt_rule and preference_slot. Their complete_from starts then — read covers_window before quoting any rate over a window that straddles it (#379's lesson).
Expect overlap for a while. The prompt arm fires against a corpus that is still entirely always_on, so a rule will surface at the prompt boundary AND already be resident. Not a bug — being present in context and being salient at the moment are different properties (#3750's argument one layer up) — and it resolves when #394 removes residency.
Unblocks #3855, the curation pass that moves rules 76, 77, 140, 151, 152, 30, 28, 29 to kind='preference' — one UPDATE each, ids, history and relations intact.
Two commits: milestone #394's step 2 and milestone #399's step 4. Together they are how a rule or preference reaches a session at the moment it governs what to **say**, rather than only what to do.
## `44e0b05` — rules retrieve against the operator's message (#3852)
Both existing rule arms are keyed on an act: `write_path_rule` on code, `pre_tool_rule` on a command. A rule that governs a RESPONSE has no such trigger — extract intent from loose phrasing, raise a conflict before acting, hand off an action with its reason, end a finding with an offer all bind on what gets said, and no tool call precedes a response.
The operator's message is the only query that exists before one is composed. That hook searched notes alone, so no rule had ever been retrieved against something the operator actually said — and residency was the only surface those rules had, which is what milestone #394 removes.
**A separate function, not a branch in `build_autoinject_hint`.** That arm bails when auto-inject is disabled, when the query is blank, when nothing clears the note bar — every one a statement about NOTES. Folded in, an operator who turned the awareness menu off would silently lose their rules, and the coupling has no symptom because both look like a quiet hook. Guarded as "the rule arm never asks the notes arm's config".
The bar is INHERITED from the act arms and the code says so. 0.72 was tuned against code and shell commands; prose is a different query shape, and triggers are written in the vocabulary of the moment — which for most rules is act vocabulary. Every call logs under `prompt_rule` from the first deploy, zeros included, so a few days of traffic settles it rather than a guess.
## `d5f9656` — a slot a preference cannot lose (#3894)
A rule crowded out at the prompt boundary still fires at an act arm; a preference about how to answer has no second chance, because the response IS the act. A straight ranking favours the record whose loss is recoverable over the one whose loss is total, and does it invisibly — the rule that won is a legitimate hit and the telemetry reads healthy.
`semantic_search_rules` gains a `kind` filter so the reserved query can only answer with a preference. The slot buys POSITION, not a lower bar.
**It extends rather than displaces**, parting from `reuse_slot`. A displaced hit would sit in `prompt_rule`'s `retrieval_logs` row while never being surfaced, so that source's two tables would stop agreeing and #3668's identity would break for a reason nothing in the data explains — which is what milestone #379 cost to learn.
## Operator-facing
- **No migration.** 0098 already shipped in #150; this is delivery only.
- **Plugin version minted** `2026.09.11.1154` — the UserPromptSubmit hook now sends and stores rule ids on the shared session ledger, so this needs a deploy plus `/reload-plugins`.
- **Two new `retrieval_logs` sources** begin recording at the deploy: `prompt_rule` and `preference_slot`. Their `complete_from` starts then — read `covers_window` before quoting any rate over a window that straddles it (#379's lesson).
- **Expect overlap for a while.** The prompt arm fires against a corpus that is still entirely `always_on`, so a rule will surface at the prompt boundary AND already be resident. Not a bug — being present in context and being salient at the moment are different properties (#3750's argument one layer up) — and it resolves when #394 removes residency.
Unblocks **#3855**, the curation pass that moves rules 76, 77, 140, 151, 152, 30, 28, 29 to `kind='preference'` — one UPDATE each, ids, history and relations intact.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
The third rule arm, and the one the other two cannot reach. `write_path_rule`
is keyed on code, `pre_tool_rule` on a command — both things the session is
about to DO. A rule that governs what to SAY has no such trigger: extract
intent from loose phrasing, raise a conflict before acting, hand off an
action with its reason, end a finding with an offer all bind on a RESPONSE,
and no tool call precedes one.
The operator's message is the only query that exists before a response is
composed. That hook searched notes alone, so no rule had ever been retrieved
against a thing the operator actually said — and residency was the only
surface those rules had, which is what milestone 394 removes.
A SEPARATE FUNCTION, not a branch in build_autoinject_hint, because of its
early returns. That arm bails when auto-inject is disabled, when the query is
blank, when nothing clears the note bar — every one a statement about NOTES.
Folded in, an operator who turned the awareness menu off would silently lose
their rules, a coupling with no symptom since both look like a quiet hook.
Two functions, two sets of gates, composed in the route. Guarded as "the rule
arm never asks the notes arm's config", which is the structural fact.
Joins _ARMS rather than getting its own test file. #3497's history is that
the pre-tool arm inherited a defect from its sibling by being MODELLED on it
instead of sharing with it, and a third arm modelled on two is two chances to
repeat that. Repeat rendering, fresh-only counting, log-before-bailout, the
kind register and the two-recorders identity are properties of every arm or
of none.
The bar is INHERITED and says so. 0.72 was tuned against code and commands;
prose is a different query shape against the same documents, and triggers are
written in the vocabulary of the moment — which for most rules is act
vocabulary. Starting at the only number with evidence behind it and logging
every call from the first deploy is what makes it settleable; guessing lower
would put an unmeasured bar in front of a corpus that binds.
k=3, anchored on this hook's own budget rather than the act arms'.
RULEHINT_LIMIT is 1 because that arm fires before every Bash call; this one
fires once per turn, beside a notes menu already spending three slots. And a
prompt genuinely contains more than one act — "merge to main and then start
on X" is two — where a command is one thing.
`prompt_rule` added to RANKED_SOURCES: a ranker picked it, and a ranked
source missing from that tuple is silently counted as bulk delivery and drops
out of the pull-through denominator.
The hook reads and writes the SHARED rule ledger under scribe-priorart, not a
private one — one session keeps one list, aged (#3751), so a rule named here
is not re-announced before the next Bash call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Milestone 399 step 4. A rule and a preference are not equally served by one
ranking, because their losses are not equal:
- a RULE crowded out at the prompt boundary still fires at an act arm. A
push reaches pre_tool_rule, a write reaches write_path_rule. The prompt
hit is a preview of a second chance.
- a PREFERENCE about how to answer has no second chance. The response IS
the act, so crowded out there it is never delivered at all.
A straight ranking therefore favours the record whose loss is recoverable
over the one whose loss is total, and does it INVISIBLY: the rule that won is
a legitimate hit, the telemetry reads healthy, and the only symptom is a
preference that quietly never arrives. reuse_slot exists for the same shape
one corpus over (#2463).
`semantic_search_rules` gains a `kind` filter, so the slot's query can only
answer with what the slot is for. Verifying afterwards would be weaker — an
unfiltered search that happened to return a rule would spend the slot on it,
and that line would be indistinguishable from one that earned its place.
THE SLOT BUYS POSITION, NOT A LOWER BAR, matching reuse_slot. A weak
preference cannot buy it, so silence stays the default. The task asked for a
separate threshold; I did not add one, and the reason is that the worry
behind it — reading a miss rate as a fact about preferences — is answered by
`preference_slot` being its own logged source, where best_available_id names
which preference was refused. A knob added on a guess is a way to
misconfigure the surface; a bar moved on evidence is an argument. The
evidence arrives on its own now.
IT EXTENDS, IT NEVER DISPLACES — and here it parts from reuse_slot, which
evicts its menu's weakest hit. A displaced hit sits in prompt_rule's
retrieval_logs row while never being surfaced, so that source's two tables
stop agreeing and #3668's identity breaks for a reason nothing in the data
explains. Milestone #379 is what losing that identity costs: five steps
planned against two counters disagreeing, not a write path dropping rows. One
extra line in a rare case is the cheaper price.
It also runs BEFORE the bail-out. An empty general result is not proof no
preference qualifies: that search overfetches by distance then collapses, so
a preference ranked below the window is invisible to it while a kind-filtered
query finds it at once. Bailing first would make the slot dead in exactly the
corpus it exists for.
One existing assertion repinned from a bare call_count to a per-source
filter: the slot logs its own query on the same call, and a count would pin
the number of arms rather than the property — going red the next time one is
added, which is rule 167's false alarm about the thing it protects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two commits: milestone #394's step 2 and milestone #399's step 4. Together they are how a rule or preference reaches a session at the moment it governs what to say, rather than only what to do.
44e0b05— rules retrieve against the operator's message (#3852)Both existing rule arms are keyed on an act:
write_path_ruleon code,pre_tool_ruleon a command. A rule that governs a RESPONSE has no such trigger — extract intent from loose phrasing, raise a conflict before acting, hand off an action with its reason, end a finding with an offer all bind on what gets said, and no tool call precedes a response.The operator's message is the only query that exists before one is composed. That hook searched notes alone, so no rule had ever been retrieved against something the operator actually said — and residency was the only surface those rules had, which is what milestone #394 removes.
A separate function, not a branch in
build_autoinject_hint. That arm bails when auto-inject is disabled, when the query is blank, when nothing clears the note bar — every one a statement about NOTES. Folded in, an operator who turned the awareness menu off would silently lose their rules, and the coupling has no symptom because both look like a quiet hook. Guarded as "the rule arm never asks the notes arm's config".The bar is INHERITED from the act arms and the code says so. 0.72 was tuned against code and shell commands; prose is a different query shape, and triggers are written in the vocabulary of the moment — which for most rules is act vocabulary. Every call logs under
prompt_rulefrom the first deploy, zeros included, so a few days of traffic settles it rather than a guess.d5f9656— a slot a preference cannot lose (#3894)A rule crowded out at the prompt boundary still fires at an act arm; a preference about how to answer has no second chance, because the response IS the act. A straight ranking favours the record whose loss is recoverable over the one whose loss is total, and does it invisibly — the rule that won is a legitimate hit and the telemetry reads healthy.
semantic_search_rulesgains akindfilter so the reserved query can only answer with a preference. The slot buys POSITION, not a lower bar.It extends rather than displaces, parting from
reuse_slot. A displaced hit would sit inprompt_rule'sretrieval_logsrow while never being surfaced, so that source's two tables would stop agreeing and #3668's identity would break for a reason nothing in the data explains — which is what milestone #379 cost to learn.Operator-facing
2026.09.11.1154— the UserPromptSubmit hook now sends and stores rule ids on the shared session ledger, so this needs a deploy plus/reload-plugins.retrieval_logssources begin recording at the deploy:prompt_ruleandpreference_slot. Theircomplete_fromstarts then — readcovers_windowbefore quoting any rate over a window that straddles it (#379's lesson).always_on, so a rule will surface at the prompt boundary AND already be resident. Not a bug — being present in context and being salient at the moment are different properties (#3750's argument one layer up) — and it resolves when #394 removes residency.Unblocks #3855, the curation pass that moves rules 76, 77, 140, 151, 152, 30, 28, 29 to
kind='preference'— one UPDATE each, ids, history and relations intact.🤖 Generated with Claude Code
https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Milestone 399 step 4. A rule and a preference are not equally served by one ranking, because their losses are not equal: - a RULE crowded out at the prompt boundary still fires at an act arm. A push reaches pre_tool_rule, a write reaches write_path_rule. The prompt hit is a preview of a second chance. - a PREFERENCE about how to answer has no second chance. The response IS the act, so crowded out there it is never delivered at all. A straight ranking therefore favours the record whose loss is recoverable over the one whose loss is total, and does it INVISIBLY: the rule that won is a legitimate hit, the telemetry reads healthy, and the only symptom is a preference that quietly never arrives. reuse_slot exists for the same shape one corpus over (#2463). `semantic_search_rules` gains a `kind` filter, so the slot's query can only answer with what the slot is for. Verifying afterwards would be weaker — an unfiltered search that happened to return a rule would spend the slot on it, and that line would be indistinguishable from one that earned its place. THE SLOT BUYS POSITION, NOT A LOWER BAR, matching reuse_slot. A weak preference cannot buy it, so silence stays the default. The task asked for a separate threshold; I did not add one, and the reason is that the worry behind it — reading a miss rate as a fact about preferences — is answered by `preference_slot` being its own logged source, where best_available_id names which preference was refused. A knob added on a guess is a way to misconfigure the surface; a bar moved on evidence is an argument. The evidence arrives on its own now. IT EXTENDS, IT NEVER DISPLACES — and here it parts from reuse_slot, which evicts its menu's weakest hit. A displaced hit sits in prompt_rule's retrieval_logs row while never being surfaced, so that source's two tables stop agreeing and #3668's identity breaks for a reason nothing in the data explains. Milestone #379 is what losing that identity costs: five steps planned against two counters disagreeing, not a write path dropping rows. One extra line in a rare case is the cheaper price. It also runs BEFORE the bail-out. An empty general result is not proof no preference qualifies: that search overfetches by distance then collapses, so a preference ranked below the window is invisible to it while a kind-filtered query finds it at once. Bailing first would make the slot dead in exactly the corpus it exists for. One existing assertion repinned from a bare call_count to a per-source filter: the slot logs its own query on the same call, and a count would pin the number of arms rather than the property — going red the next time one is added, which is rule 167's false alarm about the thing it protects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ