feat(instructions): a rule proposal has five answers, and three of them route (#3733, #3896)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Failing after 10s
CI & Build / integration (push) Successful in 51s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 24s

Step 6 of both milestone 385 (lessons) and 399 (preferences). #3896 asked for
the fourth and fifth answers in one pass, because two people each adding one
branch to a three-way distinction produce a list that does not read as a set.

create_rule now opens by asking what kind of thing is being held, with one
question that sorts it — what happens if someone doesn't do this? Something
breaks, a boundary is crossed: a rule. It gets done a way the operator didn't
want: a preference. They lose time rediscovering it: a lesson. The closing
question grew the two matching answers, and they are named as first-class
outcomes rather than places a proposal lands when it fails. An observation
that turns out to be a lesson has been routed, not dropped.

Stated as a practice, not a prohibition (rule 165). #3557's first cut opened
"NOT YOURS TO CALL UNPROMPTED" and cost the noticing; the wanted behaviour
here is still more proposals, and what changes is only which door they go
through.

create_note says the same from its side, so routing does not depend on having
opened create_rule first — and its existing rule test ("a mistake, not merely
uninformed") turned out to name the lesson exactly. create_lesson names the
fifth kind so the set is complete from every door. create_project_rule's
citation of the loop names five answers, since it cites rather than repeats.

The force axis has one owner (decision #4027): using-scribe states all three
strengths, the sorting question, that updating a preference mid-work is the
normal case, and that preferences shape how work is done and never what gets
recorded. _INSTRUCTIONS carries the pointer — "Rules bind; preferences guide
and you keep them current; lessons inform." It had 14 characters of headroom,
so the clause is paid for by trimming atmosphere from three other lines; 1998
of 2000 now.

Guards: the proposal-loop test learns the preference branch, the lesson
branch and the force question, on both rule surfaces; guidance-ownership gains
the force-axis topic (shared with the docstrings, for the moment a proposal is
actually written) and the preference-scope topic; a new guard pins that the
index names all three strengths and who keeps the middle one current, with its
can-fail case being the omission that actually happens — a kind added to the
product while the index still describes the corpus that came before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-19 12:12:14 -04:00
co-authored by Claude Opus 5
parent 1fcfd47ab6
commit 0ed8e86cd5
8 changed files with 171 additions and 28 deletions
+31 -6
View File
@@ -93,21 +93,39 @@ Two constraints on *how* that's achieved:
**`kind` says how much force a record carries, and it is never something to **`kind` says how much force a record carries, and it is never something to
infer.** A **rule** must be followed: ignoring it breaks something or infer.** A **rule** must be followed: ignoring it breaks something or
crosses a boundary. A **preference** records how the operator wants work crosses a boundary. A **preference** records how the operator wants work
done, and ignoring it costs consistency rather than correctness. Both are done, and ignoring it costs consistency rather than correctness. A
worth following and both arrive the same way; only one is a mistake to **lesson** is what is worth knowing — a better way to think about a
miss. An injected line names which in its opening words — *"Standing rule problem, a solution that transfers — and ignoring it costs time. All three
that may apply…"* against *"Preference that may apply…"* — and every are worth reading and all three arrive beside your work; only the first is
payload carries `kind` outright. a mistake to miss. An injected line names which in its opening words —
*"Standing rule that may apply…"* against *"Preference that may apply…"*
a lesson arrives in the records menu marked `lesson`, under a header that
says outright that it binds nothing, and every payload carries `kind`.
**One question sorts them, and it is worth asking out loud: what happens if
someone doesn't do this?** *Something breaks, or a boundary is crossed* — a
rule, and the operator's to agree to. *It gets done a way they didn't want*
— a preference, recorded without a loop. *They lose time rediscovering it*
— a lesson, binding nobody. Ask it before proposing rather than settling it
silently, because it sometimes answers that what you are holding was never
a rule — and routing an observation is not losing it.
**A preference is the one record you keep current yourself.** When the **A preference is the one record you keep current yourself.** When the
operator corrects you, or the preference on file no longer matches how they operator corrects you, or the preference on file no longer matches how they
actually want something done, `update_preference` — that is expected, not a actually want something done, `update_preference` — that is expected, not a
liberty, and it wants the task or note that taught the change. Say in the liberty, and updating one mid-work is the normal case rather than an
interruption of it. It wants the task or note that taught the change. Say in the
same turn that you did it, so they can disagree while it is in front of same turn that you did it, so they can disagree while it is in front of
them. A rule waits for the operator instead: `create_rule` proposes and them. A rule waits for the operator instead: `create_rule` proposes and
asks. If what you learned is that something MUST be done a certain way, asks. If what you learned is that something MUST be done a certain way,
that is a rule to propose, not a preference to harden in place. that is a rule to propose, not a preference to harden in place.
**Preferences shape how work is done, never what gets recorded.** They
govern your conduct — how you report, how carefully you pace, which form
you reach for. What ends up in Scribe is decided by what the record is:
a preference never makes a task into a note, downgrades a rule, or keeps
something out of the corpus that belongs there.
**A retrieved rule outranks a default habit.** Before a hard-to-reverse or **A retrieved rule outranks a default habit.** Before a hard-to-reverse or
outward-facing act — changing shared state, publishing, deleting, sending outward-facing act — changing shared state, publishing, deleting, sending
something outside the session — the operator's rules decide what to do, not something outside the session — the operator's rules decide what to do, not
@@ -339,6 +357,13 @@ and none of that survives being written as a rule. A repeatable procedure is a
**process**; reusable code is a **snippet**. Reach for a rule when the thing **process**; reusable code is a **snippet**. Reach for a rule when the thing
really is a standing instruction about how to work. really is a standing instruction about how to work.
Then ask what force it carries, by the question above. A standing instruction
that merely costs consistency is a **preference** (`create_preference`), and a
transferable insight that costs time is a **lesson** (`create_lesson`), keyed
to the situation it applies to so a later session meets it there. Both are
first-class outcomes of noticing something, not what's left when a rule
proposal fails.
## When a record doesn't reach the moment it should ## When a record doesn't reach the moment it should
Retrieval misjudging is ordinary, and it is fixable — but only by whoever Retrieval misjudging is ordinary, and it is fixable — but only by whoever
+4 -4
View File
@@ -45,18 +45,18 @@ client reads Agent Skills) and in each tool's description. The index:
decide_project_inception. decide_project_inception.
- RULES: nothing preloads; a rule arrives when your work matches it. Before a - RULES: nothing preloads; a rule arrives when your work matches it. Before a
consequential act — or before handing work back unsure you may finish it — consequential act — or before handing work back unsure you may finish it —
what_might_apply("what you are about to do"): the wide net, fifty ranked what_might_apply("what you are about to do"): fifty ranked
candidates, no bar. search(content_type="rule") reads one you already candidates, no bar. search(content_type="rule") reads one you already
suspect. Silence means nothing matched, not none. Rules bind; preferences suspect. Silence means nothing matched, not none. Rules bind; preferences
guide. guide and you keep them current; lessons inform.
- MISSED: a rule that should have reached you is a trigger to fix, not a - MISSED: a rule that missed you is a trigger to fix, not a
floor to move (retrieval_telemetry). floor to move (retrieval_telemetry).
- RECALL: search before acting, scoped with the active project_id. - RECALL: search before acting, scoped with the active project_id.
- RECORD: create_task; a fix is kind="issue". add_task_log as you go; status - RECORD: create_task; a fix is kind="issue". add_task_log as you go; status
in_progress on start, done on finish. Tag system_ids as you write. in_progress on start, done on finish. Tag system_ids as you write.
- PLAN work with an arc: find the existing plan first - PLAN work with an arc: find the existing plan first
(search(content_type="milestone")) and add steps to it; else (search(content_type="milestone")) and add steps to it; else
start_planning(steps=[...]). The plan is a milestone and each step a task. start_planning(steps=[...]). The plan is a milestone, each step a task.
- IDS exist only once a create returns them. Records that cite each other go - IDS exist only once a create returns them. Records that cite each other go
through create_records, writing {{ref:N}} for the Nth record. through create_records, writing {{ref:N}} for the Nth record.
- REUSE: search snippets before building; create_snippet what you build. - REUSE: search snippets before building; create_snippet what you build.
+7 -4
View File
@@ -111,10 +111,13 @@ async def create_lesson(
surface that is both global and situation-keyed, and proposing rules for surface that is both global and situation-keyed, and proposing rules for
things that should never have bound anyone. things that should never have bound anyone.
WHAT A LESSON IS NOT: a shape to copy is a SNIPPET (create_snippet); a WHAT A LESSON IS NOT: how the operator wants work DONE is a PREFERENCE
procedure followed start to finish is a PROCESS (create_process); a record (create_preference) — it guides every session rather than informing one,
of what happened, findable by topic, is a NOTE (create_note). A lesson is and it is kept current as they correct you. A shape to copy is a SNIPPET
the claim you would want handed to you in the same situation next time. (create_snippet); a procedure followed start to finish is a PROCESS
(create_process); a record of what happened, findable by topic, is a NOTE
(create_note). A lesson is the claim you would want handed to you in the
same situation next time.
`when_to_apply` IS THE RECORD. Everything else is the payload. `when_to_apply` IS THE RECORD. Everything else is the payload.
+9
View File
@@ -120,6 +120,15 @@ async def create_note(
is a task nobody will ever see again. is a task nobody will ever see again.
- future sessions must OBEY it -> create_rule. The test is whether - future sessions must OBEY it -> create_rule. The test is whether
ignoring it would be a mistake, not merely uninformed. ignoring it would be a mistake, not merely uninformed.
- it is how the operator wants work DONE, and ignoring it costs
consistency rather than correctness -> create_preference. No approval
loop, and it stays yours to keep current as they correct you.
- "merely uninformed" is the other half of the rule test, and it has its
own home: something you learned that a later session should meet at the
moment it applies -> create_lesson. A note is found by topic when
someone goes looking; a lesson arrives on its own, keyed to the
SITUATION. If you would want this handed to you next time you were in
that spot, it is a lesson.
- reusable code with a place in a repo -> create_snippet. The location is - reusable code with a place in a repo -> create_snippet. The location is
what lets it be found from the file someone is about to edit. what lets it be found from the file someone is about to edit.
- a procedure followed start to finish -> create_process. - a procedure followed start to finish -> create_process.
+46 -14
View File
@@ -268,6 +268,23 @@ async def create_rule(
When the operator asks for a rule in so many words, that IS the yes — When the operator asks for a rule in so many words, that IS the yes —
write it and move on. The loop below is for the rule you thought of. write it and move on. The loop below is for the rule you thought of.
FIRST, ASK WHAT KIND OF THING YOU ARE HOLDING. Force is the axis, and one
question sorts it: *what happens if someone doesn't do this?*
* "something breaks, or a boundary is crossed" → a RULE. It must be
followed, so it is the operator's to agree to. Propose it here.
* "it gets done a way the operator didn't want" → a PREFERENCE
(create_preference). How they want work done; ignoring it costs
consistency rather than correctness. No approval loop — record it.
* "they lose time rediscovering it" → a LESSON (create_lesson). A better
way to think about a problem, or a solution that transfers, met again
at the moment it applies. It binds nobody, so it needs no yes.
Asking this raises the value of noticing rather than lowering it: the
observation is worth keeping in all three cases, and what changes is
only which door it goes through. A proposal that turns out to be a
lesson has not failed — it has been routed.
A PROPOSAL CARRIES FOUR THINGS, and the fourth is the one that decides it: A PROPOSAL CARRIES FOUR THINGS, and the fourth is the one that decides it:
1. WHAT it would require — the statement, in the words it would carry, 1. WHAT it would require — the statement, in the words it would carry,
@@ -289,8 +306,8 @@ async def create_rule(
shrinks only on purpose, so a question that prevents a rule is worth shrinks only on purpose, so a question that prevents a rule is worth
more than any question that improves one's wording. more than any question that improves one's wording.
THEN CLOSE WITH A QUESTION THEY CAN ANSWER IN ONE WORD. Offer three THEN CLOSE WITH A QUESTION THEY CAN ANSWER IN ONE WORD. Offer these
answers, and make the middle one the easy one: answers, and make "LET'S TALK ABOUT IT" the easy one:
* "Approve it AS WRITTEN" — you create it with the statement exactly as * "Approve it AS WRITTEN" — you create it with the statement exactly as
shown. This is what makes element 1 load-bearing: they approved TEXT, shown. This is what makes element 1 load-bearing: they approved TEXT,
@@ -298,14 +315,27 @@ async def create_rule(
* "LET'S TALK ABOUT IT" — the wording, the scope, whether it * "LET'S TALK ABOUT IT" — the wording, the scope, whether it
wants to be a rule at all. Most good rules arrive this way, so treat wants to be a rule at all. Most good rules arrive this way, so treat
this answer as the expected one rather than a setback. this answer as the expected one rather than a setback.
* "NO" — let it go. If the observation is still worth keeping, it is a * "MAKE IT A PREFERENCE" — they want it done this way, but nothing
note (create_note): recorded, findable, and binding on nobody. breaks if it isn't. create_preference records it without a loop, and
it stays yours to keep current as they correct you.
* "MAKE IT A LESSON" — worth knowing, binding nobody. create_lesson
stores it against the SITUATION it applies to, so a later session
meets it at that moment rather than having to go looking. Reach for
this whenever the answer to "what happens if someone doesn't do
this" was "they lose time".
* "NO" — let it go. If the observation is still worth keeping and none
of the above fits, it is a note (create_note): recorded, findable,
and binding on nobody.
The middle three are not consolation prizes. They are where most good
observations belong, and the reason the kind question is worth asking
out loud rather than settled silently before the proposal.
Where the interface offers structured choices, ask it that way — a Where the interface offers structured choices, ask it that way — a
question with named options is answered in a click, while the same question with named options is answered in a click, while the same
question inside a paragraph is answered by scrolling past. Where it does question inside a paragraph is answered by scrolling past. Where it does
not, write the three options out as three options. Either way ask once not, write the answers out as a list. Either way ask once and let the
and let the answer stand; re-raising a declined proposal argues a rule answer stand; re-raising a declined proposal argues a rule
into existence, which is the thing this whole loop exists to prevent. into existence, which is the thing this whole loop exists to prevent.
A rulebook rule is GLOBAL — it applies in every project — so it must read as A rulebook rule is GLOBAL — it applies in every project — so it must read as
@@ -431,14 +461,16 @@ async def create_project_rule(
in get_project's project_rules and in list_rules(project_id=...). in get_project's project_rules and in list_rules(project_id=...).
PROPOSE, THEN WRITE ON A YES — create_rule's opening carries the whole PROPOSE, THEN WRITE ON A YES — create_rule's opening carries the whole
loop: the four things a proposal states (what it would require, its loop: the kind question that comes first (what happens if someone doesn't
intent, why now, and how it would be enforced) and the one-word question do this — a rule binds, a preference guides, a lesson informs), the four
that closes it (approve as written / talk about it / no). All of it things a proposal states (what it would require, its intent, why now, and
applies here unchanged. Reach for that loop MORE readily on this surface, how it would be enforced) and the one-word question that closes it
not less: a project rule stays out of an unfiltered list_rules(), and a (approve as written / talk about it / make it a preference / make it a
conditional one stays out of session start too, so the operator's yes is lesson / no). All of it applies here unchanged. Reach for that loop MORE
the one moment this rule is certain to have been seen by the person it readily on this surface, not less: a project rule stays out of an
binds. unfiltered list_rules(), and a conditional one stays out of session start
too, so the operator's yes is the one moment this rule is certain to have
been seen by the person it binds.
Check first whether a rule is the right shape at all — create_rule's Check first whether a rule is the right shape at all — create_rule's
opening asks that question and it applies identically here. A visual opening asks that question and it applies identically here. A visual
+13
View File
@@ -119,6 +119,19 @@ TOPICS: tuple[Topic, ...] = (
index=('content_type="rule"', "nothing matched")), index=('content_type="rule"', "nothing matched")),
Topic("rules bind, preferences guide and are kept current", U, ("preference", "update_preference"), Topic("rules bind, preferences guide and are kept current", U, ("preference", "update_preference"),
"a preference is the one record you keep current yourself", index=("preferences guide",)), "a preference is the one record you keep current yourself", index=("preferences guide",)),
Topic("kind is force: a rule binds, a preference guides, a lesson informs", U,
("lesson", "costs time", "costs consistency"),
"what happens if someone doesn't do this",
index=("lessons inform",),
# Stated twice on purpose, at the two moments it is needed: the
# skill while deciding what a record IS, and create_rule while a
# proposal is actually being written — which is where the
# mis-routing #3733 recorded happens, and the caller there has not
# necessarily read the skill.
shared_with=("docstrings",)),
Topic("preferences shape how work is done, never what is recorded", U,
("never what gets recorded",),
"a preference never makes a task into a note"),
Topic("recall before acting", U, ("recall before acting",), "for related prior work", index=("recall",)), Topic("recall before acting", U, ("recall before acting",), "for related prior work", index=("recall",)),
Topic("stay inside the active project's scope", U, ("stay inside the active project", "cross-project"), Topic("stay inside the active project's scope", U, ("stay inside the active project", "cross-project"),
"stay inside the active project's scope", index=("project_id",)), "stay inside the active project's scope", index=("project_id",)),
+47
View File
@@ -131,6 +131,53 @@ def test_a_surface_claiming_rules_bind_also_names_what_does_not():
) )
# ── the index names every strength, and who keeps the middle one current ──
#
# Milestones 385 and 399 both end here, and for the same reason. The index is
# the one guidance surface EVERY MCP client receives — a client that reads no
# Agent Skills still gets it — so a strength the index omits does not exist for
# that session. Before the lesson kind, "rules bind; preferences guide" was the
# whole corpus; a session that meets a third line shape with no name for it
# falls back on the nearest one it has, which is the binding one.
#
# The currency half is the claim #3896 was written around: a preference is the
# one record a session updates ITSELF, without asking. Stated only in the
# skill, it reaches a session that loaded the skill — which is exactly the
# session least likely to need telling. In the index it reaches everyone.
#
# Words, not wording: each entry is a family, and a rewrite satisfies it by
# saying the thing in any of these ways.
INDEX_STRENGTHS = {
"that rules bind": ("rules bind",),
"that preferences guide": ("preferences guide",),
"that lessons inform": ("lessons inform", "lessons are", "a lesson informs"),
"that a preference is kept current": ("current",),
}
def test_the_index_names_all_three_strengths():
text = " ".join(_instructions_text().split()).lower()
missing = [claim for claim, family in INDEX_STRENGTHS.items()
if not any(t in text for t in family)]
assert not missing, (
f"_INSTRUCTIONS no longer says {missing}. It is the only guidance every "
f"client receives, and a record kind it does not name is one the session "
f"has to guess the force of — which it does by reaching for the kind it "
f"does know. Keep it to a clause; the full statement lives on "
f"using-scribe (decision #4027)."
)
def test_the_strength_guard_can_fail():
"""Rule 167. The guard has to be able to bite, and it bites on omission —
the failure that actually happens, when a kind is added to the product and
the index is left describing the corpus that came before it."""
text = "rules bind; preferences guide and you keep them current."
missing = [claim for claim, family in INDEX_STRENGTHS.items()
if not any(t in text for t in family)]
assert missing == ["that lessons inform"]
# ── Reporting back (milestone 409 step 3) ────────────────────────────── # ── Reporting back (milestone 409 step 3) ──────────────────────────────
# #
# The reporting-back skill carries the shapes, but a skill only helps if it # The reporting-back skill carries the shapes, but a skill only helps if it
+14
View File
@@ -76,6 +76,20 @@ _ELEMENTS = {
), ),
"how it would be enforced": ("enforc",), "how it would be enforced": ("enforc",),
"the answers offered back": ("as written", "talk about it", "discuss"), "the answers offered back": ("as written", "talk about it", "discuss"),
# The fourth and fifth answers (#3733, #3896). They are what makes the
# question a ROUTING question rather than a yes/no one: an observation
# that turns out to be a preference or a lesson has found its home, not
# failed to become a rule. Written in one pass, because two people each
# adding one branch to a three-way distinction produce a list that does
# not read as a set.
"the preference branch": ("create_preference", "a preference"),
"the lesson branch": ("create_lesson", "a lesson"),
# And the test that routes BEFORE the proposal is drafted, so the kind is
# a question asked out loud rather than one settled silently.
"the force question": (
"what happens if someone doesn't", "force is the axis",
"a rule binds, a preference guides",
),
} }