feat(410): the skills own the full reflexes, in words any client can read (#4029)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m34s
CI & Build / Build & push image (push) Successful in 16s

Step 2 of milestone 410 "One owner per piece of guidance". Skills are the
part of every client package shared verbatim (Agent Skills, decision #4027),
so they state each reflex in full and name no particular client.

using-scribe gains what only the static session context said:
- a retrieved rule outranks a default habit; ask when no rule speaks to it
- log on completing a task and on hitting a problem, not only successes
- the systems_hint on an untagged record is the tagging question, answered
  at the moment of work

Client-specific text leaves the skills, rewritten as the universal idea:
- using-scribe: "keep one copy" no longer names CLAUDE.md, MEMORY.md, native
  auto-memory or autoMemoryEnabled; "this plugin" becomes Scribe
- reusing-code / shape-accounting: Write/Edit and Bash become editor tools
  and shell edits; the prior-art "hook" becomes the prior-art hint; a plugin
  version number is dropped

tests/test_guidance_ownership.py:
- test_the_skills_name_no_particular_client fails on any Claude Code path,
  memory file, slash command, hook event or tool name in a skill, each
  marker commented with why it is client-specific; a companion test shows
  it can fail
- three registry topics for what using-scribe now owns; the loss guard
  stays green

Plugin version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:50:30 -04:00
co-authored by Claude Opus 5
parent f3036f0cd7
commit 0a29252f9b
5 changed files with 95 additions and 29 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "name": "scribe",
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your active-project context, process-skills (writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", "description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your active-project context, process-skills (writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
"version": "2026.09.14.1438", "version": "2026.09.14.1550",
"author": { "author": {
"name": "Bryan Van Deusen" "name": "Bryan Van Deusen"
}, },
+2 -2
View File
@@ -47,8 +47,8 @@ through recall/auto-inject; this skill is the active reflex around that.
- **A `Shape ledger at …` line is the ledger speaking, not the record.** It - **A `Shape ledger at …` line is the ledger speaking, not the record.** It
names a duplicate family ("identical body in N other files, no canon"), a names a duplicate family ("identical body in N other files, no canon"), a
repeated name ("defined in N other files") or a canon elsewhere for a name repeated name ("defined in N other files") or a canon elsewhere for a name
you just wrote — for edits made through Bash you just wrote — for edits made through a shell
(sed, heredocs, scripts) as much as through Write/Edit. Derive the family or (sed, heredocs, scripts) as much as through your editor tools. Derive the family or
reuse the canon *now*; a family that is convention rather than copies is reuse the canon *now*; a family that is convention rather than copies is
dismissed with `classify_shapes(..., status="exempt", dismissed with `classify_shapes(..., status="exempt",
reason_code="convention-plumbing")`, never ignored. reason_code="convention-plumbing")`, never ignored.
+6 -6
View File
@@ -47,12 +47,12 @@ need a hand judgment:
- **The sync** stamps a snippet's own reference location `canonical` - **The sync** stamps a snippet's own reference location `canonical`
(`classified_by: mechanical`). (`classified_by: mechanical`).
- **The write path** stamps instances as you work: when you `get_snippet` a - **The write path** stamps instances as you work: when you `get_snippet` a
canon and then Write/Edit code that references or resembles it, the canon and then write code that references or resembles it, the
definitions being written land as `instance` rows (`classified_by: hook`, definitions being written land as `instance` rows (`classified_by: hook`,
the evidence in `reason`), and the prior-art hook tells you what landed the evidence in `reason`), and the prior-art hint tells you what landed
("Shape accounting: recorded at … → instance of #N"). Offered-but-unopened ("Shape accounting: recorded at … → instance of #N"). Offered-but-unopened
snippets stamp nothing — so *pull the canon you are instantiating*; that snippets stamp nothing — so *pull the canon you are instantiating*; that
pull is what turns your reuse into accounting. A hook row is evidence, not pull is what turns your reuse into accounting. A `hook` row is evidence, not
judgment: it never overrides a classification you made, and a judgment: it never overrides a classification you made, and a
`classify_shapes` call overrides it. `classify_shapes` call overrides it.
@@ -91,8 +91,8 @@ The catalogue exists so the codebase is DRY **from inception**, not as DRY as
the last sweep left it. Three surfaces say so without anyone running an audit the last sweep left it. Three surfaces say so without anyone running an audit
(milestone 299): (milestone 299):
- **At the write** — the prior-art hint (the Write/Edit hook, and since - **At the write** — the prior-art hint (delivered beside a write where your
0.1.39 the after-write hook on Bash, so sed/heredoc/script edits count too) client supports it; shell edits count as much as editor writes)
carries a `Shape ledger at <path>` line when a name just written is a known carries a `Shape ledger at <path>` line when a name just written is a known
**duplicate family** ("identical body in N other files, no canon"), a **duplicate family** ("identical body in N other files, no canon"), a
**repeated name** ("defined in N other files, no canon") or a **repeated name** ("defined in N other files, no canon") or a
@@ -144,7 +144,7 @@ Three questions the ledger answers mechanically (#2793):
proposer did not match to that canon. `diverges_from` names the canon. proposer did not match to that canon. `diverges_from` names the canon.
Judge it: `instance` if it should be built from the canon (and rebuild Judge it: `instance` if it should be built from the canon (and rebuild
it), `variant` with the why if the departure is deliberate. The write-path it), `variant` with the why if the departure is deliberate. The write-path
hook asks the same question in-band the moment such a shape is written. hint asks the same question in-band the moment such a shape is written.
- **History** — `shape_history(project_id, path, symbol?)`: the current rows - **History** — `shape_history(project_id, path, symbol?)`: the current rows
plus every `classified` / `vanished` / `reappeared` / `drifted` event with plus every `classified` / `vanished` / `reappeared` / `drifted` event with
its commit — "instance of #N from <date>, re-judged variant of #M because its commit — "instance of #N from <date>, re-judged variant of #M because
+31 -20
View File
@@ -31,28 +31,25 @@ Do this actively. Nothing is handed to a session up front to stand in for it —
rules arrive by retrieval, when your work or the operator's message matches rules arrive by retrieval, when your work or the operator's message matches
one — so asking and entering the project are the reliable path. one — so asking and entering the project are the reliable path.
## Scribe holds these functions — don't keep a second copy ## Scribe holds these functions — keep one copy
This plugin makes Scribe the home for the operator's **rules, recall, and Scribe is the home for the operator's **rules, recall, and planning** — the
planning** — the jobs Claude's native auto-memory would otherwise do. When the jobs your client's own local memory files would otherwise do. Route those jobs
plugin is present, route those jobs to Scribe and **do not also write them to to Scribe instead of also writing them locally: codify rules with
native memory**: codify rules with `create_rule` / `create_project_rule`, `create_rule` / `create_project_rule`, capture durable knowledge as Scribe
capture durable knowledge as Scribe notes, and keep plans in Scribe milestones notes, and keep plans in Scribe milestones (via `start_planning`). One copy, in
(via `start_planning`) — not in `MEMORY.md` or `CLAUDE.md`. One copy, in Scribe; let any existing local Scribe; let any existing local memory shrink as Scribe takes over.
memory shrink as Scribe takes over. Don't maintain both stores in parallel.
Two constraints on *how* that's achieved: Two constraints on *how* that's achieved:
- **Steer behavior; never flip a native switch.** The plugin must work with - **Steer behaviour; leave the client's own settings as they are.** Scribe works
native auto-memory at its default (ON). Never tell the operator to set alongside a client's built-in memory at its defaults. You replace memory's
`autoMemoryEnabled:false` or otherwise disable a built-in function to make functions by *doing the work in Scribe*, so there is no reason to ask the
Scribe "win" — a setting the operator may not know was changed (and wouldn't operator to switch a built-in feature off — a setting they didn't knowingly
know to restore) is exactly the hidden breakage to avoid. You replace memory's change is breakage they won't know to restore.
functions by *doing the work in Scribe*, not by turning memory off. - **A Scribe-shaped hole is acceptable.** If Scribe is later removed, the
- **A Scribe-shaped hole is acceptable.** If the plugin is later removed, the operator recovers context over time — that's fine. Local memory doesn't need
operator recovers context over time — that's fine. You do **not** need to keep to be kept as a self-sufficient fallback.
native memory as a self-sufficient fallback. The only thing to avoid is
breakage caused by a settings change the operator didn't make knowingly.
## The reflex ## The reflex
@@ -98,6 +95,13 @@ Two constraints on *how* that's achieved:
session it has nothing to do with — but retrieval only fires if something session it has nothing to do with — but retrieval only fires if something
asks. asks.
**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
the generic conventions your client or your training suggest. When a rule
and a habit disagree, the rule wins; when no rule speaks to it, ask rather
than assume.
**Ask hardest where you feel most certain.** Rules about which TOOL to reach **Ask hardest where you feel most certain.** Rules about which TOOL to reach
for — use the forge's MCP client rather than curling its API, don't stand up for — use the forge's MCP client rather than curling its API, don't stand up
a local stack, don't run the suite CI owns — govern moves that feel like a local stack, don't run the suite CI owns — govern moves that feel like
@@ -118,7 +122,9 @@ Two constraints on *how* that's achieved:
plans/specs to local `.md` files. See the **writing-plans** skill. plans/specs to local `.md` files. See the **writing-plans** skill.
5. **Keep state honest.** Set a task `in_progress` when you start it, `done` the 5. **Keep state honest.** Set a task `in_progress` when you start it, `done` the
moment it's complete; log progress as you go. moment it's complete; log progress as you go. Always log when you
**complete** a task and when you **hit or discover a problem**, so a change
of direction is on the record and not only the successes.
6. **Fixes are issues, not work-logs.** When you fix a problem — even one solved 6. **Fixes are issues, not work-logs.** When you fix a problem — even one solved
in passing — record it as its own issue (`create_task(kind="issue")`) with in passing — record it as its own issue (`create_task(kind="issue")`) with
@@ -141,6 +147,11 @@ Two constraints on *how* that's achieved:
not restraint. Only a record genuinely about no particular area goes not restraint. Only a record genuinely about no particular area goes
untagged. untagged.
Every read and write of a project record shows its `systems`. An untagged
one carries a `systems_hint` question instead — on creates, updates and
work-logs alike. Treat it as the tagging question asked at the moment of
work: tag the record, create the missing System, or deliberately leave it.
8. **Name the record, never just its number.** Whenever you refer to a Scribe 8. **Name the record, never just its number.** Whenever you refer to a Scribe
record — in a message to the operator, a commit message, a task body, a record — in a message to the operator, a commit message, a task body, a
work-log — write the id *and* its title: `#3244 "the staleness signal"`, work-log — write the id *and* its title: `#3244 "the staleness signal"`,
@@ -307,6 +318,6 @@ nothing will tell you it drifted.
## Other Scribe process-skills ## Other Scribe process-skills
This plugin also ships focused process-skills — writing-plans, reporting-back, Scribe also ships focused process-skills — writing-plans, reporting-back,
systematic debugging, verification, and brainstorming. Reach for the matching one when its systematic debugging, verification, and brainstorming. Reach for the matching one when its
situation arises, the same way you reach for this skill. situation arises, the same way you reach for this skill.
+55
View File
@@ -112,6 +112,9 @@ TOPICS: tuple[Topic, ...] = (
Topic("an id exists only once a create returns it", "skill:using-scribe", Topic("an id exists only once a create returns it", "skill:using-scribe",
("exists only once a create", "{{ref:")), ("exists only once a create", "{{ref:")),
Topic("tag records to systems as you write", "skill:using-scribe", ("system_ids", "create_system")), Topic("tag records to systems as you write", "skill:using-scribe", ("system_ids", "create_system")),
Topic("answer the systems_hint at the moment of work", "skill:using-scribe", ("systems_hint",)),
Topic("a retrieved rule outranks a default habit", "skill:using-scribe", ("outranks a default habit",)),
Topic("log on completion and on a problem", "skill:using-scribe", ("hit or discover a problem",)),
Topic("the project's design system binds ui", "skill:using-scribe", ("resolve_design_system",)), Topic("the project's design system binds ui", "skill:using-scribe", ("resolve_design_system",)),
Topic("name the record, never just its number", "skill:using-scribe", ("name the record",)), Topic("name the record, never just its number", "skill:using-scribe", ("name the record",)),
Topic("project inception is a decision", "skill:using-scribe", ("decide_project_inception",)), Topic("project inception is a decision", "skill:using-scribe", ("decide_project_inception",)),
@@ -196,3 +199,55 @@ def test_the_loss_guard_can_fail():
reported = missing_topics((split, absent, whole), surfaces) reported = missing_topics((split, absent, whole), surfaces)
assert len(reported) == 2 assert len(reported) == 2
assert reported[0].startswith("'split'") and "nowhere" in reported[1] assert reported[0].startswith("'split'") and "nowhere" in reported[1]
# ── The skills are client-neutral (milestone 410 step 2) ────────────────
#
# Agent Skills is an open format read by dozens of clients (#4023), so the
# skills folder is the part of every client package that is shared verbatim.
# Anything only one client understands belongs in that client's adapter —
# for Claude Code, the plugin's static context, hooks and commands — or the
# skill reads as nonsense everywhere else. Each marker below names a thing
# exactly one client has:
# - "claude", "~/.claude", ".claude/" the client itself and its paths
# - "claude.md", "memory.md", "auto-memory", "automemoryenabled"
# Claude Code's local memory files/setting
# - "/compact", "/scribe:" Claude Code slash commands
# - "sessionstart", "userpromptsubmit", "pretooluse", "posttooluse"
# Claude Code hook event names
# - "write/edit" Claude Code's editor tool names
# - "claude_plugin_root" the Claude Code plugin root variable
# `Bash` is matched case-sensitively as a word: it is Claude Code's shell tool
# name, while "bash" in prose is just the shell.
CLIENT_SPECIFIC = (
"claude", "~/.claude", ".claude/", "claude.md", "memory.md", "auto-memory",
"automemoryenabled", "/compact", "/scribe:", "sessionstart", "userpromptsubmit",
"pretooluse", "posttooluse", "write/edit", "claude_plugin_root",
)
CLIENT_TOOL_NAME = re.compile(r"\bBash\b")
def client_specific_hits(text: str) -> list[str]:
hits = [m for m in CLIENT_SPECIFIC if m in text.lower()]
if CLIENT_TOOL_NAME.search(text):
hits.append("Bash")
return hits
def test_the_skills_name_no_particular_client():
offenders = {
str(p.relative_to(ROOT)): client_specific_hits(p.read_text())
for p in sorted((ROOT / "plugin/skills").glob("*/SKILL.md"))
}
offenders = {path: hits for path, hits in offenders.items() if hits}
assert not offenders, (
f"skills that name one client: {offenders}. Skills are shared by every "
f"client package (decision #4027); say the universal thing in the skill "
f"and put the client's own name for it in that client's adapter."
)
def test_the_client_guard_can_fail():
assert client_specific_hits("keep a copy in CLAUDE.md") == ["claude", "claude.md"]
assert client_specific_hits("edits made through Bash") == ["Bash"]
assert client_specific_hits("edits made through a bash shell") == []