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
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:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|||||||
@@ -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") == []
|
||||||
|
|||||||
Reference in New Issue
Block a user