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
+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) ──────────────────────────────
#
# The reporting-back skill carries the shapes, but a skill only helps if it