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
+4 -4
View File
@@ -45,18 +45,18 @@ client reads Agent Skills) and in each tool's description. The index:
decide_project_inception.
- 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 —
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
suspect. Silence means nothing matched, not none. Rules bind; preferences
guide.
- MISSED: a rule that should have reached you is a trigger to fix, not a
guide and you keep them current; lessons inform.
- MISSED: a rule that missed you is a trigger to fix, not a
floor to move (retrieval_telemetry).
- 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
in_progress on start, done on finish. Tag system_ids as you write.
- PLAN work with an arc: find the existing plan first
(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
through create_records, writing {{ref:N}} for the Nth record.
- 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
things that should never have bound anyone.
WHAT A LESSON IS NOT: a shape to copy is a SNIPPET (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.
WHAT A LESSON IS NOT: how the operator wants work DONE is a PREFERENCE
(create_preference) — it guides every session rather than informing one,
and it is kept current as they correct you. A shape to copy is a SNIPPET
(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.
+9
View File
@@ -120,6 +120,15 @@ async def create_note(
is a task nobody will ever see again.
- future sessions must OBEY it -> create_rule. The test is whether
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
what lets it be found from the file someone is about to edit.
- 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 —
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:
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
more than any question that improves one's wording.
THEN CLOSE WITH A QUESTION THEY CAN ANSWER IN ONE WORD. Offer three
answers, and make the middle one the easy one:
THEN CLOSE WITH A QUESTION THEY CAN ANSWER IN ONE WORD. Offer these
answers, and make "LET'S TALK ABOUT IT" the easy one:
* "Approve it AS WRITTEN" — you create it with the statement exactly as
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
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.
* "NO" — let it go. If the observation is still worth keeping, it is a
note (create_note): recorded, findable, and binding on nobody.
* "MAKE IT A PREFERENCE" — they want it done this way, but nothing
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
question with named options is answered in a click, while the same
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
and let the answer stand; re-raising a declined proposal argues a rule
not, write the answers out as a list. Either way ask once and let the
answer stand; re-raising a declined proposal argues a rule
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
@@ -431,14 +461,16 @@ async def create_project_rule(
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
loop: the four things a proposal states (what it would require, its
intent, why now, and how it would be enforced) and the one-word question
that closes it (approve as written / talk about it / no). All of it
applies here unchanged. Reach for that loop MORE readily on this surface,
not less: a project rule stays out of an 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.
loop: the kind question that comes first (what happens if someone doesn't
do this — a rule binds, a preference guides, a lesson informs), the four
things a proposal states (what it would require, its intent, why now, and
how it would be enforced) and the one-word question that closes it
(approve as written / talk about it / make it a preference / make it a
lesson / no). All of it applies here unchanged. Reach for that loop MORE
readily on this surface, not less: a project rule stays out of an
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
opening asks that question and it applies identically here. A visual