dev → main: rules become measurable at the preload, and retrievable at the tool call #137

Merged
bvandeusen merged 2 commits from dev into main 2026-09-02 23:41:09 -04:00
Owner

Two commits, both halves of the same problem: the rules system could not be judged, and could not be reached at the moment that mattered.

8b9b3a1 — the preload emits (#3473)

M333 made the ranked rule arm measurable. The preload was not, and that is the surface whose value is actually in question. list_always_on_rules, the SessionStart block and every rules_payload caller handed rules over wholesale and emitted nothing — so the resident set's token cost was certain and its usefulness could not be tested even in principle.

Bulk deliveries now record as ambient, reported beside the ranked count and never inside pull_through. Folding them in would mean growing the always-on set depressed the arm's measured precision and trimming it flattered the arm, neither for any reason to do with the arm.

RANKED_SOURCES inverts the note twin's AMBIENT_SOURCES deliberately — naming the rare, slow-moving half means a bulk surface added later and forgotten defaults to ambient (under-counts it) rather than padding the denominator with surfacings nobody chose.

Two lookalike call sites stay silent, with a test to keep them that way: the write-path etag arm and rules_etag_for read the rules to build or compare a marker and show nobody anything.

No migration — event and source are plain Text with no CHECK.

2ee24b9 — rules before tools (#3476)

The only just-in-time rule surface was registered on Write|Edit and queried with code or path, so a rule could be retrieved at the moment of a code write and nowhere else. Every rule about which tool to reach for — don't curl the forge, don't stand up a stack, don't run the suite locally — was unreachable exactly when it mattered, and residency in the always-on preload was the only surface it had. That is the pressure that grew the resident set to 31 against note #3089's ceiling of ~23; it was never a judgment anybody made.

A reflex generates no query, so an instruction to check the rules cannot catch one. A mechanical trigger can: the tool call is the query, and a reflex has to become a tool call before it can do anything.

  • build_tool_rule_hint is tool-agnostic — a name and a string — so widening the matcher later is a hooks.json edit with no server change.
  • Both pre-tool arms share one session ledger of already-named rules; two would mean a rule named by one arm gets re-offered by the other.
  • Deliberately silent on outage: a write is occasional, a Bash call is not.

After this merge

Neither piece reaches anywhere on its own:

ships via needs
2ee24b9 hook the plugin (marketplace checkout tracks main) /plugin update → 2026.09.03.0329
8b9b3a1 server fabledscribe:latest docker service update --force scribe_app

The pre-tool arm filters to conditional, so rule 161 — the rule it was built for — will not surface until it is re-tiered. That is deliberate and is the next step's work.

CI green on 2ee24b9 (run 5433).

🤖 Generated with Claude Code

https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ

Two commits, both halves of the same problem: the rules system could not be judged, and could not be reached at the moment that mattered. ## `8b9b3a1` — the preload emits (#3473) M333 made the *ranked* rule arm measurable. The preload was not, and that is the surface whose value is actually in question. `list_always_on_rules`, the SessionStart block and every `rules_payload` caller handed rules over wholesale and emitted nothing — so the resident set's token cost was certain and its usefulness could not be tested even in principle. Bulk deliveries now record as **ambient**, reported beside the ranked count and never inside `pull_through`. Folding them in would mean growing the always-on set depressed the arm's measured precision and trimming it flattered the arm, neither for any reason to do with the arm. `RANKED_SOURCES` inverts the note twin's `AMBIENT_SOURCES` deliberately — naming the rare, slow-moving half means a bulk surface added later and forgotten defaults to ambient (under-counts it) rather than padding the denominator with surfacings nobody chose. Two lookalike call sites stay silent, with a test to keep them that way: the write-path etag arm and `rules_etag_for` read the rules to build or compare a **marker** and show nobody anything. No migration — `event` and `source` are plain Text with no CHECK. ## `2ee24b9` — rules before tools (#3476) The only just-in-time rule surface was registered on `Write|Edit` and queried with `code or path`, so a rule could be retrieved at the moment of a code write and nowhere else. Every rule about which tool to reach for — don't curl the forge, don't stand up a stack, don't run the suite locally — was unreachable exactly when it mattered, and residency in the always-on preload was the only surface it had. That is the pressure that grew the resident set to 31 against note #3089's ceiling of ~23; it was never a judgment anybody made. A reflex generates no query, so an instruction to check the rules cannot catch one. A mechanical trigger can: **the tool call is the query**, and a reflex has to become a tool call before it can do anything. - `build_tool_rule_hint` is tool-agnostic — a name and a string — so widening the matcher later is a `hooks.json` edit with no server change. - Both pre-tool arms share **one** session ledger of already-named rules; two would mean a rule named by one arm gets re-offered by the other. - Deliberately silent on outage: a write is occasional, a Bash call is not. ## After this merge Neither piece reaches anywhere on its own: | | ships via | needs | |---|---|---| | `2ee24b9` hook | the plugin (marketplace checkout tracks `main`) | `/plugin` update → `2026.09.03.0329` | | `8b9b3a1` server | `fabledscribe:latest` | `docker service update --force scribe_app` | The pre-tool arm filters to `conditional`, so rule 161 — the rule it was built for — will not surface until it is re-tiered. That is deliberate and is the next step's work. CI green on `2ee24b9` (run 5433). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
bvandeusen added 2 commits 2026-09-02 23:41:03 -04:00
feat(telemetry): the preload emits, and the always-on set stops being unfalsifiable (#3473)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Successful in 31s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 27s
8b9b3a1d9b
The ranked rule arm became measurable in M333. The preload did not — and
that is the surface whose value is actually in question. `list_always_on_rules`,
the SessionStart block and every `rules_payload` caller handed rules over
wholesale and emitted nothing, so the resident set's token cost was certain
and its usefulness could not be tested even in principle.

Bulk deliveries now record as AMBIENT, beside the ranked count and never
inside pull-through. Folding them in would mean growing the always-on set
depressed the arm's measured precision and trimming it flattered the arm,
neither for any reason to do with the arm.

`RANKED_SOURCES` inverts the note twin's `AMBIENT_SOURCES` deliberately: there
is one ranked rule source and this change adds seven bulk ones, so naming the
rare half makes a forgotten surface default to ambient — under-counting it —
rather than padding the denominator with surfacings nobody chose.

Two lookalike call sites are deliberately left silent, with a test to keep
them that way: the write-path etag arm and `rules_etag_for` read the rules to
build or compare a MARKER and show nobody anything.

No migration — `event` and `source` are plain Text with no CHECK (rule 36
does not apply). Snippet #2858 updated to the new `rules_payload` contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
feat(rules): rules before tools — a PreToolUse arm keyed on the action (#3476)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 32s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m9s
CI & Build / Build & push image (push) Successful in 25s
2ee24b9d2b
The only just-in-time rule surface was registered on `Write|Edit` and queried
with `code or path`, so a rule could be retrieved at the moment of a code
write and nowhere else. Every rule about which tool to reach for — don't curl
the forge, don't stand up a stack, don't run the suite locally, don't branch —
was unreachable exactly when it mattered, and residency in the always-on
preload was the only surface it had. That is the pressure that grew the
resident set to 31 against #3089's ceiling of ~23; it was never a judgment
anybody made.

A reflex generates no query, so an instruction to check the rules cannot catch
one. A mechanical trigger can: the tool call IS the query, and a reflex has to
become a tool call before it can do anything.

`build_tool_rule_hint` is deliberately tool-agnostic — a name and a string —
so widening the matcher later is a hooks.json edit with no server change. The
hook starts on Bash, which is where the action reflexes live.

The two pre-tool arms share ONE session ledger of already-named rules
(`<state>/<sid>.rules.ids`). Two ledgers would mean a rule named by one arm
gets re-offered by the other, and the hint that fires most often is exactly
the one that must not repeat itself. A test asserts both scripts build the
same path, and another checks the shell hook and the Python route agree on
every query-arg name (rule 33) — a rename there fails silently, looking like
a surface that never finds anything rather than a broken one.

Deliberately silent on outage, unlike the prior-art hook: a write is
occasional, a Bash call is not, and an outage line before every command is
what gets a channel muted.

`tier="conditional"` matches the write arm and is the transition point — an
always-on rule is already resident, so re-tier one and it starts arriving here
instead of in every session's preamble. `pre_tool_rule` joins RANKED_SOURCES:
this arm chose what it showed, so a pull can settle whether the choice landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
bvandeusen merged commit 5b02908dfd into main 2026-09-02 23:41:09 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#137