A rule proposal has five answers, and three of them route #168
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "scribe",
|
||||
"description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).",
|
||||
"version": "2026.09.18.1606",
|
||||
"version": "2026.09.19.1614",
|
||||
"author": {
|
||||
"name": "Bryan Van Deusen"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
infer.** A **rule** must be followed: ignoring it breaks something or
|
||||
crosses a boundary. A **preference** records how the operator wants work
|
||||
done, and ignoring it costs consistency rather than correctness. Both are
|
||||
worth following and both arrive the same way; only one is a mistake to
|
||||
miss. An injected line names which in its opening words — *"Standing rule
|
||||
that may apply…"* against *"Preference that may apply…"* — and every
|
||||
payload carries `kind` outright.
|
||||
done, and ignoring it costs consistency rather than correctness. A
|
||||
**lesson** is what is worth knowing — a better way to think about a
|
||||
problem, a solution that transfers — and ignoring it costs time. All three
|
||||
are worth reading and all three arrive beside your work; only the first is
|
||||
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
|
||||
operator corrects you, or the preference on file no longer matches how they
|
||||
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
|
||||
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,
|
||||
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
|
||||
outward-facing act — changing shared state, publishing, deleting, sending
|
||||
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
|
||||
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
|
||||
|
||||
Retrieval misjudging is ordinary, and it is fixable — but only by whoever
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -119,6 +119,19 @@ TOPICS: tuple[Topic, ...] = (
|
||||
index=('content_type="rule"', "nothing matched")),
|
||||
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",)),
|
||||
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("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",)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,6 +76,20 @@ _ELEMENTS = {
|
||||
),
|
||||
"how it would be enforced": ("enforc",),
|
||||
"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",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user