feat(shapes): the practice is written where it is read, and the coverage line measures the slip (milestone 439 step 6)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 47s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / Build & push image (push) Successful in 29s

- reusing-code: "Before the turn ends — say what you built" — the four
  verdicts, the one classify_shapes(repo=…) call, and the component file as a
  shape. Description names the end-of-turn moment.
- shape-accounting: the writer judges; audits are the check that it held.
  The write path SUGGESTS (no more hook instances); component file rows and
  whole-file canon described; scoped covers Svelte too.
- _INSTRUCTIONS reuse line: "before the turn ends, say what you built
  (create_snippet the reusable, classify_shapes the rest)" — 1570/1600.
- Coverage line: "written-shape check (7d): N turns checked, M asked, K left
  unjudged", from the Stop hook's recorded outcomes; silent until the
  question has been put.
- test_guidance_ownership pins the new topic on reusing-code.
- prior-art hook header no longer says it stamps instance rows.

Plugin version minted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:46:58 -04:00
co-authored by Claude Opus 5.5
parent 8b1567cf2c
commit 582a5a4f48
10 changed files with 153 additions and 22 deletions
+1 -1
View File
@@ -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.10.01.1245",
"version": "2026.10.01.1246",
"author": {
"name": "Bryan Van Deusen"
},
+5 -4
View File
@@ -17,8 +17,9 @@
# It is also the shape ledger's write-path feed (#2791): it names the
# definitions being written (`shapes=`), and the server — only when the
# session has PULLED a snippet this code references or resembles — records
# them as instance rows, classified_by=hook. Evidence, not judgment; the
# context line says what landed so a wrong stamp is corrected in the moment.
# it as a PROPOSAL on them ("looks like #N"), never a status (milestone 439).
# The agent's own end-of-turn judgment is the verdict; the same names go to
# the written-shapes ledger the Stop hook asks about.
#
# NEVER BLOCKS. It returns `additionalContext` with no `permissionDecision`, so
# the write proceeds untouched and Claude sees the note beside the tool result.
@@ -103,8 +104,8 @@ fi
# changes the inside of a function rather than its signature — the definition
# enclosing the edit, found by walking the target file upward from the edited
# lines. The server decides whether evidence exists (the session pulled a
# snippet this code references or resembles) and stamps instance rows; with
# no pulled canon in play, nothing is recorded. Titles only still — this sends
# snippet this code references or resembles) and records it as a proposal;
# with no pulled canon in play, nothing is recorded. Titles only still — this sends
# names, not bodies.
# ---------------------------------------------------------------------------
#
+27 -1
View File
@@ -1,6 +1,6 @@
---
name: reusing-code
description: Use when you're about to build ANY shape — a component, control, route handler, service class, helper, test scaffold — search recorded snippets FIRST and start from the recorded shape instead of re-solving it. And the FIRST time a shape is built, record it as a snippet so every later instance starts from it. Triggers on "write a util/helper", "I need a function that…", "let me add a component/button/field/route", or having just built the first instance of anything.
description: Use when you're about to build ANY shape — a component, control, route handler, service class, helper, test scaffold — search recorded snippets FIRST and start from the recorded shape instead of re-solving it. And before the turn that built it ends, say what it is — record the reusable as a snippet, classify the rest — so every later instance starts from it. Triggers on "write a util/helper", "I need a function that…", "let me add a component/button/field/route", having just built the first instance of anything, or the end-of-turn list of shapes to judge.
---
# Reusing code — the pattern library
@@ -85,6 +85,32 @@ through recall/auto-inject; this skill is the active reflex around that.
per reusable thing. If it already exists, `update_snippet` it instead of
recording a second copy (the create gate will flag a near-duplicate anyway).
## Before the turn ends — say what you built
You are the one who knows what the code you just wrote is. The shape ledger
records it from your answer, not from a guess made later, so give the answer
in the turn that built it — while it is still true. When a turn wrote
definitions nobody has judged, the plugin's Stop hook lists them once, with
whatever evidence the machinery holds ("looks like #N", "#M is canon in this
directory", "new"); other clients reach the same moment through
`list_shapes(project_id, path=…)`. For each one, decide:
- **Something another part of the code should reuse** → record it with
`create_snippet` (the fields above). Its own row becomes the canon.
- **Built from a recorded snippet** → `instance` of it.
- **A deliberate departure from one** → `variant`, with the why.
- **A genuine one-off** → `exempt`, with a reason (`reason_code` such as
`one-off-handler`, `test-helper`, `scoped-css` or `pure-helper` indexes it).
One call records them all: `classify_shapes(project_id, repo="<the repo's
remote>", classifications=[{path, symbol, kind, status, snippet_id?, reason?}])`.
`repo` lets a verdict land on a shape the ledger has not synced yet — the one
you wrote a minute ago. A component file is a shape too (`kind: "file"`, named
by its stem): when a card, chip or dialog is the reusable thing, the file is
what you record. An unsure call is still yours to make — the reason you write
is what the next reader judges it by — and a verdict is permanent, so each
shape is asked about once.
## A shared snippet is a suggestion, not a standard
Scribe is multi-user, so a search can return snippets other people own. Those
+25 -15
View File
@@ -18,8 +18,8 @@ row carries a status:
- `exempt` — judged genuinely one-off. **Reason required.** A recorded
judgment, not silence — it stops the next pass re-litigating it.
- `scoped` — one-off **by construction**, stamped by the coverage sync
(a Vue component's scoped `<style>` rules and its `<script setup>`
functions — unreachable from any other file). Accounted for without a
(a Vue or Svelte component's own styles and instance-script functions —
unreachable from any other file). Accounted for without a
judgment; still proposed against, grouped and flagged; any judgment you
make overrides it. Not the todo.
- `unclassified` — nobody has judged it yet. **This is the todo list.**
@@ -39,22 +39,32 @@ row carries a status:
nothing. Rows, never prose — a consumer list in a note or verification
detail cannot be sorted, queried, or diffed.
## Rows that arrive on their own
## The writer judges; audits check that it happened
Two feeds keep the ledger current between your batches, so most shapes never
need a hand judgment:
The ledger is filled from your answers, given while you still know them. When
a turn wrote definitions nobody has judged, the plugin's Stop hook lists them
once — with whatever the machinery holds as evidence — and you classify them
in one `classify_shapes(project_id, repo=…, …)` call (the `reusing-code` skill
says what each verdict means). `repo` lets a verdict land on a shape the sync
has not read yet. So a project worked this way stays accounted as it grows,
and an audit pass is a CHECK that the reflex held — the coverage line names
what slipped — not the way rows get classified.
What still arrives without you:
- **The sync** stamps a snippet's own reference location `canonical`
(`classified_by: mechanical`).
- **The write path** stamps instances as you work: when you `get_snippet` a
canon and then write code that references or resembles it, the
definitions being written land as `instance` rows (`classified_by: hook`,
the evidence in `reason`), and the prior-art hint tells you what landed
("Shape accounting: recorded at … → instance of #N"). Offered-but-unopened
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
judgment: it never overrides a classification you made, and a
`classify_shapes` call overrides it.
(`classified_by: mechanical`) — including a component FILE row when the
snippet's location names the file and no symbol.
- **The write path suggests**: when you `get_snippet` a canon and then write
code that references or resembles it, the shapes being written carry it as
a proposal, and the prior-art hint says "→ looks like #N". It is evidence
for your verdict, never a verdict — so *pull the canon you are
instantiating*; that pull is what puts it in front of you at the end of the
turn. Rows the hook stamped `instance` before it stopped doing so are listed
by `stamps_to_review` for judgment.
- **A component is a row of its own** (`kind: "file"`, named by its stem): a
file that renders and defines nothing named after itself. It is judged like
any other shape.
## The machine proposes, judgment classifies
+2 -1
View File
@@ -56,7 +56,8 @@ reads Agent Skills) and in each tool's description.
matched, not none. Rules bind; preferences guide and you keep them current;
lessons inform.
- Search before acting or building, scoped with the active project_id; start
from a recorded snippet, and create_snippet what you build.
from a recorded snippet; before the turn ends, say what you built
(create_snippet the reusable, classify_shapes the rest).
- Work is tasks (a fix is kind="issue"): in_progress on start, add_task_log as
you go, done on finish; tag system_ids.
- A plan is a milestone: find the existing one
+22
View File
@@ -922,6 +922,16 @@ async def compute_coverage(
except Exception:
logger.warning("stamp review read failed", exc_info=True)
review = {}
# The write-time figure (milestone 439): how often the end-of-turn
# question was put on this project, and how often it was walked past.
# An audit reads the ledger; this reads whether the reflex held.
try:
from scribe.services import shape_check as shape_check_svc
write_time = await shape_check_svc.window_summary(project_id)
except Exception:
logger.warning("write-time figure read failed", exc_info=True)
write_time = None
return {
"total": len(rows),
"accounted": len(rows) - unclassified,
@@ -950,6 +960,8 @@ async def compute_coverage(
# holds (#4608).
"weak_stamps": review.get("weak_count", 0),
"incoherent_canons": review.get("incoherent_count", 0),
# {checked, asked, left, days} from the Stop hook's recorded outcomes.
"write_time": write_time,
# Honesty flag, not decoration: every surface that shows the number
# is expected to carry it through.
"estimate": True,
@@ -1154,4 +1166,14 @@ def coverage_line(coverage: dict) -> str:
review.append(f"{n_i} incoherent canon{'s' if n_i != 1 else ''}")
if review:
line += f"; {' · '.join(review)} to judge — stamps_to_review"
# Whether the writers judged what they wrote (milestone 439). Silent until
# the question has been put at least once: a project nobody has written to
# through the plugin has no figure, not a perfect one.
wt = coverage.get("write_time") or {}
if wt.get("checked"):
line += (
f"; written-shape check ({wt.get('days', 7)}d): {wt['checked']} "
f"turn{'s' if wt['checked'] != 1 else ''} checked, {wt.get('asked', 0)} asked, "
f"{wt.get('left', 0)} left unjudged"
)
return line
+45
View File
@@ -27,6 +27,9 @@ report_check gives.
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from scribe.models import async_session
from scribe.models.app_log import AppLog
@@ -145,3 +148,45 @@ def parse_written(raw: str, *, cap: int = 200) -> list[tuple[str, str, str]]:
if len(out) >= cap:
break
return out
# The window the coverage line reports the write-time figure over. A week is
# long enough to hold a few working sessions on a project and short enough
# that a reflex that started slipping shows up while it is still news.
WINDOW_DAYS = 7
def summarise(outcomes: list[str]) -> dict:
"""{checked, asked, left} from a list of recorded outcomes.
`checked` is turns the question was put to (a first stop that wrote
something): passed + blocked. `asked` is the blocked ones. `left` is
asks the agent walked past — the stop after a block that still had
unjudged shapes. That last number is the slip the milestone exists to
make visible."""
checked = sum(1 for o in outcomes if o in ("passed", "blocked"))
asked = sum(1 for o in outcomes if o == "blocked")
left = sum(1 for o in outcomes if o == "left_after_block")
return {"checked": checked, "asked": asked, "left": left}
async def window_summary(project_id: int, *, days: int = WINDOW_DAYS) -> dict:
"""The write-time figure for one project over the last ``days``."""
since = datetime.now(timezone.utc) - timedelta(days=days)
async with async_session() as session:
rows = (await session.execute(
select(AppLog.details).where(
AppLog.category == "plugin",
AppLog.action == "shape_check",
AppLog.created_at >= since,
)
)).scalars().all()
outcomes: list[str] = []
for raw in rows:
try:
details = json.loads(raw or "{}")
except (TypeError, ValueError):
continue
if details.get("project_id") == project_id:
outcomes.append(details.get("outcome") or "")
return {**summarise(outcomes), "days": days}
+5
View File
@@ -198,6 +198,11 @@ TOPICS: tuple[Topic, ...] = (
Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
("create_snippet", "when_to_use", "first build", "second copy"),
"prior art offered beside a write is not noise", index=("create_snippet",)),
# Milestone 439: the writer judges what it wrote, at the end of the turn,
# asked by the plugin's Stop hook — the moment the knowledge exists.
Topic("judge what you wrote before the turn ends", "skill:reusing-code",
("before the turn ends", "classify_shapes", 'kind: "file"'),
"you are the one who knows what the code you just wrote is"),
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
"take the placement from the record"),
Topic("the operator's own reply shapes come first", "skill:reporting-back",
+13
View File
@@ -907,3 +907,16 @@ def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(pa
from scribe.services.coverage import class_references, extract_definitions, is_file_unit
assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit
def test_the_line_says_whether_writers_judged_what_they_wrote():
"""Milestone 439: the write-time figure — silent until the question has
been put, then turns checked, asked, and asks walked past."""
from scribe.services.coverage import coverage_line
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
"unclassified": 0, "computed_at": "2026-10-01T00:00:00"}
assert "written-shape check" not in coverage_line(base)
assert "written-shape check" not in coverage_line(
{**base, "write_time": {"checked": 0, "asked": 0, "left": 0, "days": 7}})
line = coverage_line({**base, "write_time": {"checked": 12, "asked": 4, "left": 1, "days": 7}})
assert line.endswith("; written-shape check (7d): 12 turns checked, 4 asked, 1 left unjudged")
+8
View File
@@ -79,3 +79,11 @@ async def test_an_unknown_outcome_is_refused_before_anything_is_written():
pytest.raises(ValueError):
await record_shape_check(7, "skipped", written=1, unjudged=1)
session.add.assert_not_called()
def test_the_window_counts_turns_asks_and_asks_walked_past():
from scribe.services.shape_check import summarise
assert summarise([]) == {"checked": 0, "asked": 0, "left": 0}
got = summarise(["passed", "blocked", "judged_after_block", "blocked", "left_after_block"])
assert got == {"checked": 3, "asked": 2, "left": 1}