feat(plugin): a high-confidence rule is put in front of a command, not beside its result (#4214)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Failing after 1m13s
CI & Build / Build & push image (push) Skipped

Milestone 419 step 3, the pre-act checkpoint. Every rule surface in this
plugin returns `additionalContext`, which Claude Code delivers alongside the
tool RESULT — so the rule is read after the call is written and lands as
commentary on a decision already made. That is the milestone's central
finding, measured over a session with seven misses, three caught by the
operator and none by this system.

The action arm can now return a `deny` instead. The act does not run, the
rule's text can be read before the call exists, and the remedy is one
`get_rule` call after which the act may be re-submitted unchanged. Nothing
reaches the operator: a deny is a message to the model.

"CONSEQUENTIAL" IS DERIVED, NOT ENUMERATED. The obvious implementation lists
act kinds — a write to product code, a schema change, a bulk classification, a
merge. Every one of those is consequential because THIS operator wrote rules
about it, and shipping that list is this instance's corpus hard-coded into the
product (rule 115). So the corpus decides: an act is consequential when the
install's own rules speak to it above the checkpoint bar. A fresh install with
no rules never stops anything.

FOUR CONDITIONS, EACH PREVENTING A DIFFERENT WRONG. Above the bar; a rule and
never a preference (which claims no such force); the band's top hit only (the
ranker's confidence claim attaches to its first element); and only a rule the
session has NOT opened — `held` is observable from the get_rule PostToolUse
hook (#4100), not self-report.

WHY "NOT OPENED" RATHER THAN "NO OUTCOME RECORDED". An outcome can be
satisfied with one cheap call asserting compliance without producing any, and
a checkpoint dismissible that way manufactures exactly the compliance data
step 2 was built to measure. Reading a rule cannot be faked in that direction:
after `get_rule` the statement is in context, which is the whole of what was
wanted.

THE BAR IS MEASURED. `retrieval_telemetry(days=30)`: write_path_rule p90
0.7628 max 0.8817; pre_tool_rule p90 0.7373 max 0.8293. 0.80 is above p90 on
both and below max on both, so it selects from the top decile of an already
selective arm and is still reachable. It ships as a setting with a Settings
card, because a cosine distance in one model's geometry over one corpus cannot
transfer.

TWO GUARDS ON THE WORST CASE: at most one hold per rule and five per session,
so a mis-set floor degrades to a noisy session rather than one that cannot
proceed. The ledger lives in the swept directory and is named `.ids`, so the
existing compaction-clear guards cover it.

WRITES ARE NOT HELD, AND THAT IS THE OPERATOR'S DECISION RATHER THAN MINE.
`scribe_prior_art.sh` carries a tested property that it never returns a
permissionDecision — a recall aid may not stand in the way of a write. Three
of the milestone's seven misses were file edits and none are reachable from
the command side, so there is a live argument for extending this; that
argument is exactly why the boundary is now asserted by a test rather than
left to memory. The write-path arm computes and returns the same block so the
decision can be revisited with evidence; the hook ignores it, and a change of
mind is a hook edit rather than a feature.

Verified by lifting `checkpoint_for` and `_rule_band` out of source with `ast`
and exercising the shipped functions over 17 populations, by running the
ledger and deny envelope in bash (10 cases, including that a refused hold is
not written and that a garbled rule id fails closed), and by
scripts/check_plugin.py — which caught the unminted plugin version, 0300 ->
0426.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-21 00:26:54 -04:00
co-authored by Claude Opus 5
parent bb8013928f
commit 91bc0fb01e
7 changed files with 685 additions and 4 deletions
+26
View File
@@ -170,6 +170,24 @@ async def pre_tool_rules():
different claims about the reader's
context, so they get different lines
(#4100).
Returns `context`, `rule_ids`, and `checkpoint` (#4214, milestone 419).
`checkpoint` IS THE ONE PART OF THIS RESPONSE THAT IS NOT A HINT. It is
empty on almost every call. When present it carries `rule_id`, `title`,
`trigger`, `score` and a rendered `reason`, and it means the hook should
DENY the call rather than annotate it — because every other line this
endpoint returns is delivered by Claude Code alongside the tool RESULT,
so it reaches the reader after the act is composed and reads as
commentary on a decision already made.
It is raised only for a rule (never a preference, which claims no such
force), only for the ranker's top hit, only above the checkpoint
threshold — well above this arm's own floor — and only when the session
has NOT opened that rule. The remedy is one `get_rule` call, and the act
may then be re-submitted unchanged; the hook caps stops at one per rule
and five per session so a mis-set floor degrades to noise rather than to
a session that cannot proceed.
"""
tool = (request.args.get("tool") or "tool").strip()
command = request.args.get("command") or ""
@@ -223,6 +241,14 @@ async def write_path_prior_art():
or `canon:<snippet_id>`) already named this
session by the ledger arm (#2900); its own
channel, like the two above.
(Returns a `checkpoint` block on the same contract as /tool-rules —
see that endpoint. The write-path HOOK deliberately does not act on
it: scribe_prior_art.sh carries a tested property that it never
returns a permissionDecision, on the operator's decision that a recall
aid may not stand in the way of a write. The block is computed and
returned so that decision can be revisited with evidence rather than
re-argued, and so a change of mind is a hook edit and not a feature.)
shapes (opt) — comma-separated `kind:name` definitions the hook
found in (or enclosing) the payload, kind being
css|sym. The shape ledger's write-path feed
+211 -1
View File
@@ -271,6 +271,66 @@ RULEHINT_LIMIT = SURFACES["write_path_rule"].budget_default
# stops working and the answer is a reranker (#1038), not a smaller number.
_RULEHINT_BAND = 0.05
# ── The pre-act checkpoint (#4214, milestone 419) ───────────────────────
#
# WHAT A CHECKPOINT IS, AND WHY IT IS NOT A LOUDER HINT. Every arm above
# appends text to an act the agent has already composed. Claude Code delivers
# `additionalContext` alongside the tool result, so by the time the line is
# read the call is written and the rule reads as commentary on a decision
# already made. That is milestone 419's central finding, measured over a
# session where three misses were caught by the operator and none by this
# system.
#
# A checkpoint is the same retrieval, spent differently: the hook returns a
# `deny` and the act does not run, so the rule's own text is read BEFORE the
# call exists. The remedy is `get_rule(id)` and nothing else — which is why
# the condition below is "the session has not opened it" rather than "the
# session has not recorded an outcome for it". An outcome can be satisfied
# with one cheap call that asserts compliance without producing any, and a
# checkpoint that can be dismissed that way manufactures exactly the
# compliance data step 2 exists to measure. Reading a rule cannot be faked in
# that direction: after `get_rule` the statement is in context, which is the
# whole of what was wanted.
#
# WHAT "CONSEQUENTIAL" MEANS HERE, AND WHY IT IS NOT A LIST. The obvious
# implementation enumerates act kinds — a write to product code, a schema
# change, a bulk classification, a merge. Every one of those is consequential
# because THIS operator wrote rules about it, and a shipped list would be this
# instance's corpus hard-coded into the product (rule 115). So the corpus
# decides: an act is consequential when the install's own rules speak to it
# with high confidence. A fresh install with no rules never stops anything,
# and an install whose rules are about something else entirely stops on that
# instead.
_CHECKPOINT_THRESHOLD_KEY = "kb_checkpoint_threshold"
# MEASURED, not chosen (2026-09-21, `retrieval_telemetry(days=30)` on the
# instance this was built on — recorded as provenance for the number, not as
# a defence of it under rule 115):
#
# write_path_rule floor 0.72 p10 0.6984 p50 0.7319 p90 0.7628 max 0.8817
# pre_tool_rule floor 0.68 p10 0.6838 p50 0.7018 p90 0.7373 max 0.8293
#
# 0.80 sits above p90 on BOTH arms and below max on both, so it selects from
# the top decile of an already-selective arm rather than from its bulk — and
# it is reachable, which a bar above 0.8293 would not be for the busier arm.
# Over that window the two arms returned ~3,584 non-empty calls between them,
# so an upper bound of one tenth of those is ~12 a day across a very heavy
# install, and the true rate is lower because 0.80 is not p90 but above it.
#
# A SETTING, because the number above is a distance in one embedding model's
# geometry over one corpus and cannot transfer (retrieval_surfaces' opening
# argument). The default ships as a starting point with the means to correct
# it, which is the only honest form for a number like this.
_CHECKPOINT_DEFAULT = 0.80
# A session cannot be stopped more than this many times, however the corpus
# scores. Not a tuning value — a guard on the worst case, like MAX_BUDGET: a
# mis-set floor or a corpus that suddenly resembles everything must degrade to
# a noisy session, never to one that cannot make progress. The hook enforces
# it, because only the hook knows what a session is.
CHECKPOINT_SESSION_CAP = 5
# AND A REPEAT COMPETES ON RANK ALONE (#3750), WHICH SURVIVES THE BAND.
#
# Since #3750 a hit already on the session's exclusion ledger is RENDERED
@@ -1524,8 +1584,35 @@ async def get_writepath_config(user_id: int) -> dict:
"rule_top_k": await budget_for(user_id, "write_path_rule"),
"tool_rule_threshold": await floor_for(user_id, "pre_tool_rule"),
"tool_rule_top_k": await budget_for(user_id, "pre_tool_rule"),
# NOT from the surfaces registry, and deliberately so. Everything in
# that table is a pair belonging to one QUERY — a floor saying what is
# worth ranking and a budget saying how many lines it may spend. The
# checkpoint runs no query of its own; it re-reads hits the two rule
# arms already produced and asks a different question of them. Putting
# it in the registry would give it a phantom budget and make the
# tuning tool offer to change how many checkpoints an act may raise,
# which is not a number anyone should have.
"checkpoint_threshold": await _checkpoint_floor(user_id),
}
async def _checkpoint_floor(user_id: int) -> float:
"""The confidence at which a hint becomes a stop. Clamped, never trusted.
A floor read out of settings reaches here as operator-typed text. Below
zero it would stop every act with a rule anywhere near it; above one it
can never fire and the feature is silently dead, which is the failure mode
#3430 found and the reason this clamps rather than validating at the door.
"""
raw = await get_setting(
user_id, _CHECKPOINT_THRESHOLD_KEY, str(_CHECKPOINT_DEFAULT),
)
try:
value = float(str(raw).strip())
except (TypeError, ValueError):
return _CHECKPOINT_DEFAULT
return min(1.0, max(0.0, value))
def _rule_band(hits: list) -> list:
"""The top hit, plus every hit within `_RULEHINT_BAND` of it (#3851).
@@ -1547,6 +1634,104 @@ def _rule_band(hits: list) -> list:
return [(s, r) for s, r in hits if s >= top - _RULEHINT_BAND]
def checkpoint_for(
kept: list, *, held: set[int], floor: float, where: str,
) -> dict:
"""The one rule, if any, that should STOP this act rather than annotate it.
Sync and pure so it can be read against a fixed list of hits without a
database — the two arms share it for the reason `_rule_band` is shared:
#3497 is the record of these two drifting apart by being modelled on each
other instead of sharing one function.
FOUR CONDITIONS, AND EACH IS A DIFFERENT KIND OF WRONG IT PREVENTS.
1. THE SCORE CLEARS `floor`. Not the arm's own floor — a much higher bar,
measured at `_CHECKPOINT_DEFAULT`. The hint arms keep nudging at their
floor; only a hit the corpus is confident about is allowed to stop
anything.
2. IT IS A RULE, NEVER A PREFERENCE. A preference says how something has
been done before and following it is what keeps work consistent; a rule
says what happens if you do not. Stopping an act over a preference
would assert a force the record explicitly does not claim, and
`_rule_hint_line` already keeps that distinction in the one word that
names it.
3. THE SESSION HAS NOT OPENED IT. `held` is observable — a PostToolUse
hook watches for the `get_rule` call (#4100) — so this is a recorded
event and not a model's self-report about its own context. A session
that read the rule has already had the thing the checkpoint exists to
produce, and stopping it again would be punishing the behaviour being
asked for.
4. IT IS THE TOP HIT. `kept` is a band, and a band's tail is there to let
an act surface a SET; the ranker's confidence claim attaches to its
first element only. A checkpoint raised on the fourth line of a band is
a stop justified by a score nobody claimed.
Returns a dict rather than a rule or a tuple. Widening a tuple is an
interface change to every unpack site that the compiler does not report
(#4207, learned the expensive way in this same milestone's first week), and
this value crosses a JSON boundary into a shell script where a missing
field is a silently empty variable.
"""
if not kept or floor <= 0:
return {}
score, rule = kept[0]
if score < floor:
return {}
if getattr(rule, "kind", "") == "preference":
return {}
if rule.id in held:
return {}
found = {
"rule_id": rule.id,
"title": rule.title,
"trigger": (rule.when_to_apply or "").strip(),
"score": round(float(score), 4),
"where": where,
}
# RENDERED HERE, at the one call site, rather than by each arm. Two arms
# that each remember to render it are two arms that can stop agreeing on
# what a stop says — which is #3497's history for this exact pair. The
# text stays a separate function so it can be read and tested without a
# rule object, but nothing outside this line decides whether to call it.
found["reason"] = checkpoint_reason(found)
return found
def checkpoint_reason(checkpoint: dict) -> str:
"""The text the agent reads INSTEAD of running the act.
Written as a practice rather than a prohibition (rule 165): it says what
to do and why it is worth doing, not what is forbidden. The act is not
wrong — nothing here knows whether it is — and saying so plainly is what
keeps the stop from reading as an accusation the system is in no position
to make.
It names the remedy as ONE call, because a stop whose remedy is vague
costs more than the miss it prevents. And it says the act may simply be
re-submitted afterwards, so a reader who finds the rule irrelevant is out
in two calls rather than negotiating with a hook.
"""
if not checkpoint:
return ""
trigger = checkpoint.get("trigger") or ""
return (
f"Held for one read. “{checkpoint['title']}” is a standing rule "
f"this session has not opened, and it scores {checkpoint['score']} "
f"against what you are about to do"
+ (f" ({trigger})" if trigger else "")
+ f". Read it with get_rule({checkpoint['rule_id']}), then go ahead — "
f"re-submit this call unchanged if the rule does not apply, which is a "
f"judgement only you can make. Nothing here has decided the act is "
f"wrong; the rule is being put in front of it rather than beside it, "
f"because a rule delivered alongside a result arrives after the "
f"decision it was meant to inform."
)
def _rule_hint_line(
rule, *, where: str, seen: bool, held: bool = False, compact: bool = False,
) -> str:
@@ -2203,6 +2388,7 @@ async def build_write_path_hint(
#
# Fails open like every other arm: a rule hint must never break a write.
rule_ids: list[int] = []
checkpoint: dict = {}
try:
already = set(exclude_rule_ids or [])
held = set(held_rule_ids or [])
@@ -2242,6 +2428,14 @@ async def build_write_path_hint(
# would inflate pull_through's denominator with a choice this arm never
# made. A reference is a RENDERING decision, not a retrieval outcome.
rule_ids.extend(rule.id for _score, rule in fresh)
# The stop, beside the lines rather than instead of them — see
# `checkpoint_for`. `kept` is passed, not `fresh`: whether a rule
# was named earlier this session says nothing about whether this
# act should wait for it to be READ, and those are the two axes
# #3750 exists to keep apart.
checkpoint = checkpoint_for(
kept, held=held, floor=cfg["checkpoint_threshold"], where="here",
)
# TWO tables, and the split is not arbitrary. retrieval_logs is one
# row per CALL, keyed on the score distribution a threshold is tuned
# from. rule_usage_events is one row per RULE per event, which is the
@@ -2312,6 +2506,7 @@ async def build_write_path_hint(
"derive": derive,
"derive_keys": [d["key"] for d in derive],
"rule_ids": rule_ids,
"checkpoint": checkpoint,
}
@@ -2351,7 +2546,14 @@ async def build_tool_rule_hint(
Fails open and returns an empty context on any error: a recall aid may
never break the operator's action.
"""
out: dict = {"context": "", "rule_ids": []}
# `checkpoint` is present on EVERY return, including the early ones. The
# two arms feed one shell reader, and a key that exists on some responses
# and not others is read there as an empty variable either way — so the
# difference is invisible at the point it would bite and only shows up in
# a test that asserts the contract. Same reason `warnings` is always a
# list in the telemetry readout: an absent key and an empty one must not
# be two ways of saying nothing.
out: dict = {"context": "", "rule_ids": [], "checkpoint": {}}
command = (command or "").strip()
if not command:
return out
@@ -2440,6 +2642,14 @@ async def build_tool_rule_hint(
)
out["context"] = "\n".join(lines)
out["rule_ids"] = rule_ids
# AFTER the lines, never instead of them. A checkpoint stops the act;
# it does not decide what the act should be told, and a reader who
# reads the rule and re-submits must find the same hint waiting. The
# two are independent renderings of one retrieval.
out["checkpoint"] = checkpoint_for(
kept, held=held, floor=cfg["checkpoint_threshold"],
where=f"this {tool_name} call",
)
except Exception:
logger.debug("pre-tool rule arm failed", exc_info=True)
return out