Milestone #399, steps 1–3, plus the instruction-surface fix those steps made urgent.
A preference records how the operator wants work done. It differs from a rule in exactly one dimension — force — and in one behaviour: it is expected to change as the work teaches it, so the agent keeps it current where a rule waits for its author.
Operator: "preferences are rules that scribe can and should update during use… they are a preferred way of doing something that will change and grow. but they need to be referenced for consistency."
Why a column and not a table
Everything a preference needs already lives on rules and nowhere else: a real trigger column, a trigger-dominated embedding document, ownership-scoped search, three retrieval arms with per-arm telemetry, typed relations, and versioning.
This is the same ranking test milestone #385 used for lessons, pointed the other way. A lesson must rank against issues and dev-logs — "have I solved this before" is answered by all three — so a lesson lives in notes. A preference answers "how should I behave right now", which only rules also answer.
The decisive argument was migrating the existing set: the rules that turn out to be preferences change one column and keep their id, history and relations. A cross-table move would have changed those ids and silently invalidated every record in the corpus citing a rule by number.
What's here
c631722 — kind on rules (CHECK-constrained, defaulting to rule so nothing changes force on upgrade) and on rule_versions. Service, backup, and seven real-Postgres cases.
4aae497 — the backup stand-ins predated the column; fixture fix plus the guard the column-coverage check cannot make.
89d16d8 — create_preference / update_preference, and kind on both HTTP doors.
26e0dff — a preference does not speak in a rule's voice.
8406871 — the instruction surfaces still said every rule binds.
Two things worth a reviewer's attention
rule_versions needed the column, and not for symmetry.record_if_changed compares SNAPSHOT_FIELDS to decide whether an edit deserves a snapshot, so a field absent from that tuple is a field whose change records no history at all. Without it, turning a rule into a preference — the moment something stops binding — would have left the history silent. That is a hole through the whole safety story, which is that drift is auditable.
Backup enumerates rule columns by hand in four places. A preference would have restored as a rule: no error, no missing field, just a record that binds when it never should have. The import defaults differ deliberately — a rule with no kind defaults to rule (every rule in a pre-0098 file was one), a version with no kind stays null, because a version records what was and inventing a value puts an artifact where a measurement belongs.
Operator-facing
Migration 0098 runs on the next deploy. Additive; every existing row keeps the force it had.
Plugin version minted2026.09.11.0319 — the skill and static-context edits reach an install only on a version bump (#2209), so this needs a deploy plus /reload-plugins.
No preference exists in the corpus yet. The eight always-on rules that are really preferences (76, 77, 140, 151, 152, 30, 28, 29) move under #3855, which this unblocks.
Steps 4, 5 and 7 remain open; step 4 waits on #3852's prompt-boundary arm.
Milestone #399, steps 1–3, plus the instruction-surface fix those steps made urgent.
A **preference** records how the operator wants work done. It differs from a rule in exactly one dimension — force — and in one behaviour: it is expected to change as the work teaches it, so the agent keeps it current where a rule waits for its author.
Operator: *"preferences are rules that scribe can and should update during use… they are a preferred way of doing something that will change and grow. but they need to be referenced for consistency."*
## Why a column and not a table
Everything a preference needs already lives on `rules` and nowhere else: a real trigger column, a trigger-dominated embedding document, ownership-scoped search, three retrieval arms with per-arm telemetry, typed relations, and versioning.
This is the same ranking test milestone #385 used for lessons, pointed the other way. A lesson must rank against issues and dev-logs — "have I solved this before" is answered by all three — so a lesson lives in `notes`. A preference answers "how should I behave right now", which only rules also answer.
The decisive argument was migrating the existing set: the rules that turn out to be preferences change one column and keep their id, history and relations. A cross-table move would have changed those ids and silently invalidated every record in the corpus citing a rule by number.
## What's here
- **`c631722`** — `kind` on `rules` (CHECK-constrained, defaulting to `rule` so nothing changes force on upgrade) and on `rule_versions`. Service, backup, and seven real-Postgres cases.
- **`4aae497`** — the backup stand-ins predated the column; fixture fix plus the guard the column-coverage check cannot make.
- **`89d16d8`** — `create_preference` / `update_preference`, and `kind` on both HTTP doors.
- **`26e0dff`** — a preference does not speak in a rule's voice.
- **`8406871`** — the instruction surfaces still said every rule binds.
## Two things worth a reviewer's attention
**`rule_versions` needed the column, and not for symmetry.** `record_if_changed` compares `SNAPSHOT_FIELDS` to decide whether an edit deserves a snapshot, so a field absent from that tuple is a field whose change records no history at all. Without it, turning a rule into a preference — the moment something stops binding — would have left the history silent. That is a hole through the whole safety story, which is that drift is auditable.
**Backup enumerates rule columns by hand in four places.** A preference would have restored as a rule: no error, no missing field, just a record that binds when it never should have. The import defaults differ deliberately — a rule with no kind defaults to `rule` (every rule in a pre-0098 file was one), a *version* with no kind stays null, because a version records what was and inventing a value puts an artifact where a measurement belongs.
## Operator-facing
- **Migration 0098** runs on the next deploy. Additive; every existing row keeps the force it had.
- **Plugin version minted** `2026.09.11.0319` — the skill and static-context edits reach an install only on a version bump (#2209), so this needs a deploy plus `/reload-plugins`.
- **No preference exists in the corpus yet.** The eight always-on rules that are really preferences (76, 77, 140, 151, 152, 30, 28, 29) move under #3855, which this unblocks.
Steps 4, 5 and 7 remain open; step 4 waits on #3852's prompt-boundary arm.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Adds `kind` to rules — `rule` binds, `preference` is how the operator wants
work done. One column, because the two differ in exactly one dimension and
everything else a preference needs already lives on `rules`: a trigger
column, a trigger-dominated embedding document, ownership-scoped search,
three retrieval arms with telemetry, typed relations, and versioning.
Defaults to `rule`, so nothing changes force on upgrade — 0088's argument
for `tier`, unchanged.
`rule_versions` gets the column too, and that half is not bookkeeping.
`record_if_changed` decides whether an edit deserves a snapshot by comparing
the fields a version carries, so a field absent from SNAPSHOT_FIELDS is a
field whose change records no history at all. Without it, turning a rule
into a preference — the moment something stops binding, and the single most
consequential edit either kind can undergo — would leave the history silent.
Backup carries it through all four seams. A missed one would have restored
every preference as a rule, quietly.
Guarded on real Postgres in three halves: a preference writes, a typo is
refused (without which every other assertion would pass against a table
whose CHECK had been dropped), and a row written with no kind reads back as
`rule` — the migration's whole safety claim, asserted rather than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Two unit tests build a rule with SimpleNamespace rather than the model, so
adding a column broke them — the fixture has no `kind` for `_rule_rows` to
read. Fixture-only; the export itself was already right, which the existing
column-coverage guard confirmed by passing.
Adds the guard that coverage check cannot make. `_stand_in` walks
`__table__.columns` and proves the KEY is emitted; it cannot prove the VALUE
survives. A preference exported as a rule is a silent failure — the restored
rule reads fine and simply binds when it was only ever a preference — and a
fixture carrying the default would pass against a `_rule_rows` that dropped
the field and let the importer's `or "rule"` refill it. So the new test
asserts on `preference`, the one value that cannot be reconstructed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
The write path, and the step where a preference stops being a relabelled
rule. `create_preference` / `update_preference` on the MCP surface, plus
`kind` on update_rule and both HTTP doors.
SEPARATE TOOLS, NOT A `kind=` ARGUMENT. create_rule's docstring IS the
approval gate (#3557): propose, offer three answers, wait. That is right for
a rule — the person it binds should have agreed. A preference inverts it, and
one reached through create_rule would be read through that prose, so the
caller would hesitate over exactly the act this kind exists to make routine.
Two doors, two contracts, one table. Reads stay shared: a preference IS a
rule row, and "what governs this" wants both.
Two required fields, each buying something:
- `when_to_apply`, because the trigger is two-thirds of the embedded
document. Without one the record is written, stored, and silently never
delivered — indistinguishable from one nobody wrote.
- `arose_from_id`, the price of the ungated write. A corpus that drifts with
no record of what taught each change cannot be audited, and the operator's
veto over drift is worth exactly as much as their ability to read why it
happened.
The near-duplicate gate is what lets this corpus be written freely and stay
small: the second preference about a thing updates the first. It is
title-scoped and kind-blind, so it also catches a preference restating a rule
that already binds.
The asymmetry is guarded as two PRESENCE facts — the rule door still asks,
the preference door still says write it — never as an absence. An absence
check passes against a docstring that was deleted or rewritten into something
else, which is snippet #3352's warning and would read as coverage here while
proving nothing.
`_plain_detail` moved to tests/helpers on its second copy, per that module's
own reason for existing (#2825).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Two independent axes on one hint line. `kind` decides the head, `seen`
decides the tail, and neither reads the other — which is what let a second
kind arrive without reopening #3750's repeat question. Whether a record is
already on the exclusion ledger has nothing to do with how much force it
carries, so the seen branch is shared verbatim.
The noun carries the whole visual difference, deliberately. A reader skimming
an injected block gets one word to place the register, so the word that moves
is the one naming force: "Standing rule" / "Preference". Everything
structural after it is identical, so the kinds read as one set rather than
two formats.
Force is asserted in exactly one other place, and that moves too. A rule's
line says to read it BEFORE DECIDING IT DOES NOT APPLY, because dismissing a
rule unread is how the thing it prevents happens. A preference makes no such
claim: it says where to find HOW THIS HAS BEEN DONE BEFORE, and following it
buys consistency rather than correctness.
Guarded on both places at once. Pinning the noun alone would pass a line
reading "Preference … before deciding it does not apply" — label swapped,
instruction kept — which is worse than not distinguishing them, because it
looks handled.
And a guard on the independence claim itself, exercising all four
combinations: the way this breaks silently is a seen branch that grows a kind
test, leaving one combination rendered by nobody's intention.
Noted, not fixed: plugin/skills/using-scribe/SKILL.md still says "Standing
rules are binding" with no room for a kind that does not. That surface is
step 6's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
Step 3 shipped a line a session can receive — "Preference that may apply
here …" — into surfaces that told it, in the most authoritative voice it
has, that anything arriving in that shape is binding. That is the confusion
milestone 399 exists to prevent, arriving through the one channel a session
has least reason to doubt.
Silent in both directions, which is why it could not wait for step 6. A
session treating a preference as a rule refuses to proceed over something
the operator merely preferred; and it loses the whole reason preferences
exist, which is that they are brought up to date rather than obeyed.
Three surfaces, each to its own budget:
- SKILL.md gets the full account: kind decides force, the injected line names
which in its opening words, and a preference is the one record a session
keeps current itself (update_preference, with what taught the change).
- scribe_static_context.md gets six lines — enough to tell the kinds apart
and to say a preference is yours to update.
- _INSTRUCTIONS gets four words. It is a MAP at 1978 of its 2000-char budget
(#2562), and the detail belongs in the surfaces above and in the tool
docstrings, which is what that budget exists to force.
Guarded so it cannot drift back: a surface that claims rules bind must name
the kind that does not. Pinned on the CLAIM rather than the word "bind",
because a bare substring also matches bind_repo, list_repo_bindings and
server.py's DNS-rebinding comment — a guard that would one day fail a skill
about repo binding is rule 167's named failure, raising a false alarm about
the very thing it protects. Falsified against all three surfaces losing the
mention.
Plugin version minted: the cache refreshes only on a version bump (#2209),
so a skill edit without one reaches no installed plugin.
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.
Milestone #399, steps 1–3, plus the instruction-surface fix those steps made urgent.
A preference records how the operator wants work done. It differs from a rule in exactly one dimension — force — and in one behaviour: it is expected to change as the work teaches it, so the agent keeps it current where a rule waits for its author.
Operator: "preferences are rules that scribe can and should update during use… they are a preferred way of doing something that will change and grow. but they need to be referenced for consistency."
Why a column and not a table
Everything a preference needs already lives on
rulesand nowhere else: a real trigger column, a trigger-dominated embedding document, ownership-scoped search, three retrieval arms with per-arm telemetry, typed relations, and versioning.This is the same ranking test milestone #385 used for lessons, pointed the other way. A lesson must rank against issues and dev-logs — "have I solved this before" is answered by all three — so a lesson lives in
notes. A preference answers "how should I behave right now", which only rules also answer.The decisive argument was migrating the existing set: the rules that turn out to be preferences change one column and keep their id, history and relations. A cross-table move would have changed those ids and silently invalidated every record in the corpus citing a rule by number.
What's here
c631722—kindonrules(CHECK-constrained, defaulting toruleso nothing changes force on upgrade) and onrule_versions. Service, backup, and seven real-Postgres cases.4aae497— the backup stand-ins predated the column; fixture fix plus the guard the column-coverage check cannot make.89d16d8—create_preference/update_preference, andkindon both HTTP doors.26e0dff— a preference does not speak in a rule's voice.8406871— the instruction surfaces still said every rule binds.Two things worth a reviewer's attention
rule_versionsneeded the column, and not for symmetry.record_if_changedcomparesSNAPSHOT_FIELDSto decide whether an edit deserves a snapshot, so a field absent from that tuple is a field whose change records no history at all. Without it, turning a rule into a preference — the moment something stops binding — would have left the history silent. That is a hole through the whole safety story, which is that drift is auditable.Backup enumerates rule columns by hand in four places. A preference would have restored as a rule: no error, no missing field, just a record that binds when it never should have. The import defaults differ deliberately — a rule with no kind defaults to
rule(every rule in a pre-0098 file was one), a version with no kind stays null, because a version records what was and inventing a value puts an artifact where a measurement belongs.Operator-facing
2026.09.11.0319— the skill and static-context edits reach an install only on a version bump (#2209), so this needs a deploy plus/reload-plugins.Steps 4, 5 and 7 remain open; step 4 waits on #3852's prompt-boundary arm.
🤖 Generated with Claude Code
https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
kind(#3849 step 1)