refactor(skills): using-scribe keeps the every-turn practices; moment-specific depth moves to reference files (#4398)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 21s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 21s
Anthropic's skill guidance: keep SKILL.md under 500 lines, split into reference files linked one level deep as it nears that. using-scribe was 478 and every new practice lands there. - SKILL.md 478 -> 317 lines. It keeps orientation, one copy, the reflexes, scope, the judge section, UI and the process-skill index, plus a "Read these when the moment comes" list naming each file with its moment. - projects.md: binding a non-git directory (.scribe) and project inception. - writing-records.md: where a new rule goes, lesson growth, and notes that carry their own check (reflex 10 keeps a pointer). - missed-retrieval.md: the record-before-dial route, verbatim. - Text moved, not rewritten, except for the seams and one cross-reference. Tests: - tests.helpers.skill_text reads SKILL.md plus its reference files. The ownership registry, the miss-route and the verification tests use it, so a topic stays owned by its skill whichever file holds it. - The force test scans every skill .md on its own, since each file is read on its own. - New test_skill_structure: SKILL.md <= 350 lines, every reference file is linked from SKILL.md, none links another, and one over 100 lines opens with Contents. Each guard is shown to fail. The plugin version is minted. That also clears 4fb53b8's red Plugin hooks lane, which failed only because PACKAGING.md changed without a mint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,19 +17,8 @@ If the working directory maps to a Scribe project, call `enter_project(id)` —
|
||||
it returns the project's goal, the milestones and open tasks worked on most
|
||||
recently, its Systems and the titles of its own rules in one shot.
|
||||
|
||||
**A directory does not have to be a git repo to have a project.** A repo is
|
||||
bound by its remote (`list_repo_bindings` shows the bindings). Anything else —
|
||||
a notes folder, a server's config directory, a scratch directory — is bound by
|
||||
a `.scribe` file naming the project:
|
||||
|
||||
{"instance": "https://scribe.example.com", "project_id": 2, "project": "Homelab"}
|
||||
|
||||
`instance` is what makes the id trustworthy. An id means nothing on its own —
|
||||
it is a different project on every Scribe — so a marker that has travelled to
|
||||
another instance is ignored rather than followed to the wrong project. A bare
|
||||
`2` also works when writing the file by hand. When work plainly belongs to a
|
||||
project and the directory names none, offer to write the marker;
|
||||
`list_projects` has the id.
|
||||
Binding a directory that is not a git repo, or starting a new project, has
|
||||
its own steps: read [projects.md](projects.md).
|
||||
|
||||
Then **ask before you act**: before anything hard to reverse or outward-facing,
|
||||
search the rules for what you are about to do. Reflex 2 below is why asking,
|
||||
@@ -120,17 +109,9 @@ Two constraints on *how* that's achieved:
|
||||
learned is that something MUST be done a certain way, that is a rule to
|
||||
propose, not a preference to harden in place.
|
||||
|
||||
**A lesson grows each time it proves itself.** When one arrives and the
|
||||
situation it names is the one you are actually in, you are the single
|
||||
reader placed to tell whether its trigger is keyed right and whether its
|
||||
claim covers what you are seeing. `update_lesson` takes what you now know:
|
||||
another incident added to what taught it, the claim stated more exactly,
|
||||
or — the edit worth most — a trigger re-keyed to the situation that really
|
||||
fired. A lesson nobody reaches is seldom wrong; far more often it is
|
||||
waiting in a situation nobody is in. One claim that has met the same
|
||||
failure four times is worth more than four claims that each met it once,
|
||||
so when a near-duplicate create hands back an existing id, that is the
|
||||
record to grow.
|
||||
**A lesson grows each time it proves itself.** When one arrives and names the
|
||||
situation you are actually in, `update_lesson` it with what you now know;
|
||||
[writing-records.md](writing-records.md) says what is worth adding.
|
||||
|
||||
**Preferences shape how work is done, never what gets recorded.** They
|
||||
govern your conduct — how you report, how carefully you pace, which form
|
||||
@@ -246,47 +227,10 @@ Two constraints on *how* that's achieved:
|
||||
re-measurement, a reversed decision), pass the old id in `supersedes` so the
|
||||
stale record is demoted and labelled rather than left competing.
|
||||
|
||||
10. **A few notes assert a FACT, and those can carry their own check.**
|
||||
Supersession only fires once somebody has read a note and disagreed — which
|
||||
is the case where it was already believed. A note asserting something about
|
||||
*someone else's* software — what a service does on a duplicate upload, how a
|
||||
forge numbers its CI runs, what an updater compares — can instead carry
|
||||
`verify_with` (how to check it) and `expires_when` (the STATE that ends it:
|
||||
"when the forge numbers runs per workflow", never "in six months").
|
||||
`notes_due_for_verification` lists them least-recently-confirmed first, with
|
||||
never-checked at the top; `mark_note_verified` records what you found, and
|
||||
`still_true=False` deliberately writes nothing — a note whose check failed
|
||||
is wrong rather than in a state worth recording, so it keeps its place.
|
||||
|
||||
**The test is one question: could this note become false without anyone
|
||||
editing it?** If no, leave both fields empty. That is the normal case, and
|
||||
an empty `verify_with` is the positive marker for "this is a decision, there
|
||||
is nothing to go and check" — not an unfinished record. The sweep is only
|
||||
worth reading while almost nothing is on it, so a check added out of
|
||||
tidiness costs the whole surface, not just that note.
|
||||
|
||||
**The sharper form of the same test: is the thing this note describes yours
|
||||
to change?** If yes it is a decision — editing your own software is how it
|
||||
changes, and you will know you did it. Measured against a real corpus, every
|
||||
note that earned a check was about somebody ELSE's software: a signing
|
||||
service, a forge, a hub, an SDK, a model, a dependency set.
|
||||
|
||||
**Three that look like candidates and are not:**
|
||||
- **Resume pointers and "current state" notes.** They go stale fastest of
|
||||
anything, which is exactly why they tempt — but the cure is to update or
|
||||
delete them, not to schedule a check. A sweep full of pointers is a sweep
|
||||
nobody reads.
|
||||
- **Measurements of your own system.** They go false because you changed
|
||||
something, and you knew. A measurement earns a check only when what it
|
||||
measures is outside your control.
|
||||
- **A decision that RESTS on somebody else's behaviour.** The decision is
|
||||
still a decision. Put the check on the note asserting the fact, and link
|
||||
the decision to it.
|
||||
|
||||
Not for tasks — a task's decay is its status, and a done issue records what
|
||||
happened rather than asserting something that can go false. Not for snippets
|
||||
either: `verify_snippet` compares the recorded location and code against the
|
||||
repo, which is richer and already wired to drift detection.
|
||||
10. **A note that asserts a fact about someone else's software can carry its
|
||||
own check** (`verify_with`, `expires_when`), swept by
|
||||
`notes_due_for_verification`. Most notes should not: read
|
||||
[writing-records.md](writing-records.md) before filling either field.
|
||||
|
||||
11. **Report back in a shape the operator can read.** They were not there while
|
||||
you worked, so organise the reply around **where the work stands**, not the
|
||||
@@ -311,80 +255,18 @@ bound — confine the session to it:
|
||||
- If something clearly belongs to a *different* project, say so and **ask before
|
||||
switching** — never silently operate cross-project.
|
||||
|
||||
## Starting a project: decide what it inherits
|
||||
## Read these when the moment comes
|
||||
|
||||
A project's inheritance is a **decision, not a default**. Before
|
||||
`create_project`, ask the operator the two inception questions and pass the
|
||||
answers — never create a project bare by default:
|
||||
Each is part of this skill, kept in its own file because it matters at one
|
||||
moment rather than on every turn:
|
||||
|
||||
- which **design system** its UI is built from (`list_design_systems`; or
|
||||
none) → `design_system_id=<id | -1>`
|
||||
- whether to **seed the standard starter Systems** so records can be tagged
|
||||
from day one → `seed_systems=true|false`
|
||||
|
||||
Rules are not an inception question: a global rule already applies to every
|
||||
project, and a project's own rules are written on it as they come up.
|
||||
|
||||
If `enter_project` returns an `inception` key, the project was never decided
|
||||
(it inherits its defaults silently): raise that ask once, with the defaults it
|
||||
carries, then `decide_project_inception(project_id, …)`. Existing projects
|
||||
were stamped "legacy" and do not ask; any project can be re-decided. The
|
||||
design-system and Systems tools still work one at a time — inception is the
|
||||
moment they are decided together, and the record of why.
|
||||
|
||||
## Where a new rule goes
|
||||
|
||||
A rule has one of two homes, and the home IS its reach:
|
||||
|
||||
- **Global** — in a rulebook (`create_rule` into a topic). It applies in every
|
||||
project, and reaches a session wherever the work matches it. A rulebook is a
|
||||
*themed* grouping of general rules (e.g. a review checklist), not a list of
|
||||
projects it binds — there is no subscribing a project to one.
|
||||
- **Project** (`create_project_rule`) — anything specific to one project (its
|
||||
files, paths, quirks). It reaches only that project's sessions.
|
||||
|
||||
Names one project's specifics → project rule; a standard that holds wherever
|
||||
the kind of work it describes happens → global. Never put project-specific
|
||||
detail in a rulebook — it would reach every other project. A project that
|
||||
departs from a global rule writes its own and links it with
|
||||
`relate_rules(kind="overrides")`, which says why. A rule that turns out to be
|
||||
in the wrong home — a project rule that holds everywhere, a global one only a
|
||||
single project needs — moves with `move_rule`, which keeps its id, history,
|
||||
areas and edges. Propose the move and make it on a yes.
|
||||
|
||||
**Whichever home it gets, a rule needs `when_to_apply`.** It is the only thing
|
||||
that decides whether the rule is ever seen: nothing is preloaded, so a rule
|
||||
with no trigger is not a quiet rule, it is an unreachable one. Write the moment
|
||||
in the words a session actually produces — the command, the error, the
|
||||
half-formed ask — not the category it belongs to.
|
||||
|
||||
**Before writing one, ask what already covers that moment.**
|
||||
`what_might_apply("the moment you are about to write a record for")` — fifty
|
||||
candidates and no bar, so an existing record cannot hide under a threshold the
|
||||
way it can from `search`. When something already covers the moment, the reach
|
||||
is that record: improve its trigger or its statement rather than standing a
|
||||
second one beside it. Two records describing the same moment compete in one
|
||||
ranked list against one budget, and the slot they take from each other is the
|
||||
third candidate that would have said something different. Two records may
|
||||
legitimately share a moment and say *different* things — a rule for what must
|
||||
happen, a preference for how to report it. What this catches is the same thing
|
||||
said twice at two strengths, which is worse than either alone: a session that
|
||||
retrieves the softer copy has been told that binding guidance is optional.
|
||||
|
||||
**First ask whether it's a rule at all.** A rule is prose you have to remember
|
||||
and apply; Scribe's other entities are structure a tool can resolve and check.
|
||||
Visual standards belong in a **design system**, not a rulebook — a token can be
|
||||
inherited, resolved per mode, rendered to a stylesheet and diffed against code,
|
||||
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.
|
||||
- [projects.md](projects.md) — a directory with no project; starting a project
|
||||
(`decide_project_inception` when `enter_project` returns `inception`).
|
||||
- [writing-records.md](writing-records.md) — before writing a rule, preference
|
||||
or lesson (where it goes, its `when_to_apply`, what already covers the
|
||||
moment), and before giving a note a check.
|
||||
- [missed-retrieval.md](missed-retrieval.md) — a rule that missed the moment it
|
||||
governed, or keeps arriving where it doesn't apply.
|
||||
|
||||
## You are the judge of what the record says
|
||||
|
||||
@@ -415,49 +297,6 @@ stored, so when you fix how judgments are made, go and look at the ones
|
||||
already there. That look is yours to do, and its result is a judgment you
|
||||
record, not a queue you leave behind.
|
||||
|
||||
## When a record doesn't reach the moment it should
|
||||
|
||||
Retrieval misjudging is ordinary, and it is fixable — but only by whoever
|
||||
notices. **Either direction counts:** a rule that should have governed a moment
|
||||
and never arrived, and a rule that arrives on every turn and never applies. So
|
||||
does **either noticer**: the operator saying *"that should have fired"*, and you
|
||||
noticing it yourself — you reached for a rule nobody offered you, or you were
|
||||
handed the same rule five times and set it aside five times.
|
||||
|
||||
**Take it to the record first and the dial second.** A rule's `when_to_apply`
|
||||
IS the text its similarity score is computed against, so when a rule misses a
|
||||
moment it governs, the overwhelmingly likely cause is that its trigger does not
|
||||
describe that moment in the words a session actually produces. Rewording one
|
||||
trigger changes one rule's reach. Moving a floor changes what every record on
|
||||
that surface does, and a floor cannot tell a badly-worded trigger from a
|
||||
genuinely distant record — so one lowered to rescue a single rule admits
|
||||
everything else that was sitting in the same band.
|
||||
|
||||
1. **Read the refused records.** `retrieval_telemetry(days=N,
|
||||
near_miss_samples=5)` names by id what each surface refused and by how much.
|
||||
Open them with `get_rule` / `get_note`. This is the step that carries the
|
||||
answer: the statistic says a record was close, and only the record says
|
||||
whether it was *right*.
|
||||
2. **Fix the trigger.** `update_rule(when_to_apply=...)`, written as the
|
||||
symptom — what the session was doing or saying at the moment it needed this
|
||||
rule — not the situation the rule belongs to. Then check that it worked:
|
||||
`what_might_apply("the moment, in the operator's own words")` and read where
|
||||
the rule now ranks. The change is measurable, so measure it, and say the
|
||||
before and after when you report it.
|
||||
3. **Then consider the dial.** `retrieval_surfaces` shows what is in force per
|
||||
arm and whether the number is still calibrated; `tune_retrieval` moves it.
|
||||
`reason` is required and has to say what you read, because it is what lets
|
||||
the operator disagree with a number they did not choose.
|
||||
|
||||
Reaching for `tune_retrieval` before opening a single record is the wrong move,
|
||||
and it is the one that feels efficient. Worked example, measured on this
|
||||
install: a rule granting a routine push scored 0.6515 and ranked 5th for the
|
||||
moment it governed, behind three rules that *restrained* the same act. Every
|
||||
percentile said "lower the floor" — and lowering it would have delivered those
|
||||
three restraints and still not the rule. Rewriting the trigger to lead with the
|
||||
symptom moved the same rule to 1st at 0.7130, ahead of all three. Only then was
|
||||
the floor worth touching.
|
||||
|
||||
## Building UI: the project's design system binds
|
||||
|
||||
`enter_project` names the project's `design_system` when it has one. Before
|
||||
|
||||
Reference in New Issue
Block a user