docs(notes): the surfaces say WHEN a note earns a check, and a guard keeps them saying it (#3168, milestone 317 step 5)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / integration (push) Successful in 43s
CI & Build / Python tests (push) Successful in 1m16s
CI & Build / Build & push image (push) Successful in 28s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / integration (push) Successful in 43s
CI & Build / Python tests (push) Successful in 1m16s
CI & Build / Build & push image (push) Successful in 28s
The step that decides whether steps 1-4 were worth building. `verify_with` is a free-text field on the highest-volume record kind in the product; described only as "how to verify this note" it gets filled in on every note within a week, and at that point the sweep returns the whole corpus and means nothing. The signal is not "has a check" — it is "has a check AND almost nothing else does". Rule 119 puts this in the app's own instruction surfaces, never in a Scribe rule. So: - the using-scribe skill gains the reflex, next to "state updates in place; chronicles don't" — its other half, since supersession only fires once somebody has read a note and disagreed, which is the case where it was already believed; - update_note's docstring now states the DEFAULT rather than only deferring to create_note for the test. Found by the new guard on its first run; - plugin 0.1.48. THE _INSTRUCTIONS BUDGET, decided rather than skipped. The payload is ~1980 of the client's ~2048-char cap, so everything in it competes for the last ~68 characters. The operator declined a line for this milestone: the map's own closing sentence says each tool's description carries the full contract, and the sweep is a curation act rather than a session-start reflex like enter_project or list_always_on_rules. That reasoning is now a comment beside the constant, with the accepted cost named — an agent that never opens create_note's docstring never learns the field exists — so the question is not re-litigated blind. The guard pins STRUCTURE, never wording, for the disambiguator's reason (a test that punishes rewriting is a test that gets deleted): each write surface must still draw the norm-vs-constraint distinction, say the empty case is normal, and name where NOT to reach for it — plus that the skill carries the one-question form, because the docstrings only reach a caller who already opened the tool. `_doc` moves to tests/helpers as `tool_doc`; it had been written twice.
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 always-on rules + active-project context, process-skills (writing-plans, 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 always-on rules + active-project context, process-skills (writing-plans, 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": "0.1.47",
|
"version": "0.1.48",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Bryan Van Deusen"
|
"name": "Bryan Van Deusen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,6 +106,30 @@ Two constraints on *how* that's achieved:
|
|||||||
re-measurement, a reversed decision), pass the old id in `supersedes` so the
|
re-measurement, a reversed decision), pass the old id in `supersedes` so the
|
||||||
stale record is demoted and labelled rather than left competing.
|
stale record is demoted and labelled rather than left competing.
|
||||||
|
|
||||||
|
9. **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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Stay inside the active project's scope
|
## Stay inside the active project's scope
|
||||||
|
|
||||||
Once a project is in scope — you called `enter_project`, or the working repo is
|
Once a project is in scope — you called `enter_project`, or the working repo is
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ from quart import Quart
|
|||||||
# duplicate gate, the untagged-record systems_hint) act in-band in tool
|
# duplicate gate, the untagged-record systems_hint) act in-band in tool
|
||||||
# responses, at the moment they apply.
|
# responses, at the moment they apply.
|
||||||
# Grow one of those, not this block.
|
# Grow one of those, not this block.
|
||||||
|
# BUDGET: ~1980 of the client's ~2048-char cap (#2562). Everything below is
|
||||||
|
# competing for the last ~68 characters, so an addition here is a trade, never
|
||||||
|
# an append.
|
||||||
|
#
|
||||||
|
# Milestone 317 (a note's own verify_with / expires_when, and the sweep over
|
||||||
|
# them) was DECLINED a line, deliberately, by the operator — not overlooked.
|
||||||
|
# The reasoning, so it is not re-litigated blind: this is a map, and its own
|
||||||
|
# closing line says each tool's description carries the full contract. The
|
||||||
|
# sweep is a curation act, not a session-start reflex like enter_project or
|
||||||
|
# list_always_on_rules. Spending the last of the budget on it would leave the
|
||||||
|
# map unable to grow for something more central later.
|
||||||
|
#
|
||||||
|
# The accepted cost: an agent that never opens create_note's docstring never
|
||||||
|
# learns the field exists. Guidance lives in the create_note / update_note
|
||||||
|
# docstrings and the using-scribe skill instead.
|
||||||
_INSTRUCTIONS = """
|
_INSTRUCTIONS = """
|
||||||
Scribe is the operator's self-hosted second brain and system of record — and
|
Scribe is the operator's self-hosted second brain and system of record — and
|
||||||
yours: recall from it before acting, record as you go. Keep no parallel copy
|
yours: recall from it before acting, record as you go. Keep no parallel copy
|
||||||
|
|||||||
@@ -224,10 +224,12 @@ async def update_note(
|
|||||||
supersedes: Replace the ids of earlier notes this one replaces
|
supersedes: Replace the ids of earlier notes this one replaces
|
||||||
(set-semantics). None = leave unchanged; [] = clear all. See
|
(set-semantics). None = leave unchanged; [] = clear all. See
|
||||||
create_note for when to reach for it.
|
create_note for when to reach for it.
|
||||||
verify_with: How to check this note is still true. See create_note for
|
verify_with: How to check this note is still true. Almost every note
|
||||||
the norm-vs-constraint test that decides whether it should carry
|
should leave this empty — that is the normal, finished state, not
|
||||||
one at all; the short form is "could this become false without
|
a gap: an empty check is the marker for "this is a decision, there
|
||||||
anyone editing it?".
|
is nothing to go and check". See create_note for the full
|
||||||
|
norm-vs-constraint test; the short form is "could this become
|
||||||
|
false without anyone editing it?".
|
||||||
expires_when: The STATE that ends it, not a date.
|
expires_when: The STATE that ends it, not a date.
|
||||||
clear: Names of fields to UNSET — "verify_with", "expires_when".
|
clear: Names of fields to UNSET — "verify_with", "expires_when".
|
||||||
Needed because "" means "leave this alone" here, so there is no
|
Needed because "" means "leave this alone" here, so there is no
|
||||||
|
|||||||
@@ -38,6 +38,27 @@ async def drive_update_note(note, **kwargs):
|
|||||||
return await update_note(user_id=7, note_id=note.id, **kwargs)
|
return await update_note(user_id=7, note_id=note.id, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_doc(module: str, name: str) -> str:
|
||||||
|
"""An MCP tool's docstring, whitespace-flattened.
|
||||||
|
|
||||||
|
Flattened because these are hard-wrapped at ~76 characters, so any phrase
|
||||||
|
worth asserting on is liable to straddle a line break — a property of the
|
||||||
|
formatter, not of the guidance. The disambiguator guard (#3123) learned
|
||||||
|
that on its own first run, matching raw text and reporting a phrase absent
|
||||||
|
that was plainly there.
|
||||||
|
|
||||||
|
Used by every test that pins the docstring CONTRACT rather than its
|
||||||
|
wording. The tool docstring is the agent-facing contract (rule 119), so
|
||||||
|
these guards exist to catch it being tidied down to a parameter list.
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
import re as _re
|
||||||
|
|
||||||
|
fn = getattr(importlib.import_module(module), name)
|
||||||
|
assert fn.__doc__, f"{name} has no docstring at all"
|
||||||
|
return _re.sub(r"\s+", " ", fn.__doc__)
|
||||||
|
|
||||||
|
|
||||||
def compiled_sql(element) -> str:
|
def compiled_sql(element) -> str:
|
||||||
"""A SQLAlchemy clause or statement rendered as literal SQL text.
|
"""A SQLAlchemy clause or statement rendered as literal SQL text.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ parameters, with the "is this even the right tool" paragraph quietly gone.
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tests.helpers import tool_doc as _doc
|
||||||
|
|
||||||
# The create surfaces and where they live. `start_planning` is here because
|
# The create surfaces and where they live. `start_planning` is here because
|
||||||
# it is a create in everything but name — it is how a plan comes into being.
|
# it is a create in everything but name — it is how a plan comes into being.
|
||||||
@@ -50,21 +51,6 @@ _ALTERNATIVES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _doc(module: str, name: str) -> str:
|
|
||||||
"""The docstring with its whitespace flattened.
|
|
||||||
|
|
||||||
Flattened because a docstring is hard-wrapped: "design system" spans a
|
|
||||||
line break in at least one of these, and matching the raw text would
|
|
||||||
report it absent. The first draft of this check did exactly that, and
|
|
||||||
caught it on itself.
|
|
||||||
"""
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
fn = getattr(importlib.import_module(module), name)
|
|
||||||
assert fn.__doc__, f"{name} has no docstring at all"
|
|
||||||
return re.sub(r"\s+", " ", fn.__doc__)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("module", "name"), _SURFACES)
|
@pytest.mark.parametrize(("module", "name"), _SURFACES)
|
||||||
def test_a_create_surface_names_at_least_two_alternatives(module, name):
|
def test_a_create_surface_names_at_least_two_alternatives(module, name):
|
||||||
"""Reaching for the wrong tool must still put the right one in view."""
|
"""Reaching for the wrong tool must still put the right one in view."""
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""The write surfaces say WHEN a note earns a check — not just that it can.
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
|
||||||
|
`verify_with` is a free-text field on the highest-volume record kind in the
|
||||||
|
product. A field described only as "how to verify this note" gets filled in on
|
||||||
|
every note within a week, and at that point the sweep returns the whole corpus
|
||||||
|
and means nothing. The signal is not "has a check" — it is "has a check AND
|
||||||
|
almost nothing else does".
|
||||||
|
|
||||||
|
So the guidance is not decoration on this feature, it IS the feature's
|
||||||
|
precondition, and rule 119 puts it in the tool docstrings and the skill rather
|
||||||
|
than in a Scribe rule. That makes it exactly the kind of prose a later
|
||||||
|
docstring tidy-up deletes without noticing what it was for.
|
||||||
|
|
||||||
|
WHAT THIS PINS, AND WHAT IT DOES NOT
|
||||||
|
|
||||||
|
Structure, never wording, for `test_create_tools_disambiguate`'s reason: a test
|
||||||
|
that punishes rewriting is a test that gets deleted. Each write surface must
|
||||||
|
still (a) draw the norm-vs-constraint distinction in some form, (b) say the
|
||||||
|
empty case is normal, and (c) name where NOT to reach for it.
|
||||||
|
|
||||||
|
It cannot tell whether the guidance is any good — only that the paragraph
|
||||||
|
explaining when to leave the field alone has not quietly become a parameter
|
||||||
|
list.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tests.helpers import tool_doc as _doc
|
||||||
|
|
||||||
|
# The surfaces that OFFER the field. The read surfaces (the sweep, the stamp)
|
||||||
|
# explain what a RESULT means, which is a different job — they are deliberately
|
||||||
|
# not held to this.
|
||||||
|
_WRITE_SURFACES = [
|
||||||
|
("scribe.mcp.tools.notes", "create_note"),
|
||||||
|
("scribe.mcp.tools.notes", "update_note"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("module,name", _WRITE_SURFACES)
|
||||||
|
def test_the_norm_versus_constraint_distinction_is_stated(module, name):
|
||||||
|
"""The whole discipline in one line: a decision cannot go stale, a claim
|
||||||
|
about someone else's software can. Without it, "how would you check this"
|
||||||
|
reads as a chore to complete rather than a question with a usual answer of
|
||||||
|
"you wouldn't"."""
|
||||||
|
doc = _doc(module, name).lower()
|
||||||
|
assert "norm" in doc and "constraint" in doc, (
|
||||||
|
f"{name} no longer draws the norm-vs-constraint distinction. Without "
|
||||||
|
f"it the field is just a box, and a box gets filled in."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("module,name", _WRITE_SURFACES)
|
||||||
|
def test_the_empty_case_is_stated_as_normal(module, name):
|
||||||
|
"""Said POSITIVELY, or an empty field reads as an unfinished record. This
|
||||||
|
is the single sentence standing between the sweep and irrelevance."""
|
||||||
|
doc = _doc(module, name).lower()
|
||||||
|
assert re.search(r"leave (it|both|them|this|these)? ?empty|empty for", doc), (
|
||||||
|
f"{name} no longer says that leaving the check empty is the normal "
|
||||||
|
f"case. Most notes are decisions; the field's default must read as a "
|
||||||
|
f"deliberate state, not a gap."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("module,name", _WRITE_SURFACES)
|
||||||
|
def test_the_wrong_places_to_reach_for_it_are_named(module, name):
|
||||||
|
"""A task's decay is its status; a snippet has verify_snippet. The service
|
||||||
|
refuses both — this is what should mean nobody ever hits that error."""
|
||||||
|
doc = _doc(module, name).lower()
|
||||||
|
assert "task" in doc and "snippet" in doc, (
|
||||||
|
f"{name} no longer names the records that must NOT carry a check. The "
|
||||||
|
f"gate still refuses them, but a refusal the caller could have "
|
||||||
|
f"foreseen is a worse door than one that explained itself."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_expires_when_is_described_as_a_state_not_a_date():
|
||||||
|
"""The one field whose obvious reading is wrong. A date invents a staleness
|
||||||
|
schedule nobody can justify; a constraint expires when the ground moves,
|
||||||
|
which is a condition and not a time."""
|
||||||
|
doc = _doc("scribe.mcp.tools.notes", "create_note")
|
||||||
|
assert "STATE" in doc, (
|
||||||
|
"create_note no longer says expires_when is a STATE. Left to itself, "
|
||||||
|
'"expires" reads as a date, and every check would get an arbitrary one.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_skill_carries_the_test_a_writer_can_actually_apply():
|
||||||
|
"""The docstrings are read by whatever is holding the tool; the skill is
|
||||||
|
read while deciding what to write. The one-question form has to be in the
|
||||||
|
second place too, or the guidance only reaches callers who already opened
|
||||||
|
the tool."""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
skill = pathlib.Path(__file__).resolve().parents[1] / (
|
||||||
|
"plugin/skills/using-scribe/SKILL.md"
|
||||||
|
)
|
||||||
|
text = " ".join(skill.read_text().split())
|
||||||
|
assert "could this note become false without anyone editing it" in text.lower(), (
|
||||||
|
"the using-scribe skill no longer carries the one-question test. That "
|
||||||
|
"question is what makes the distinction applicable rather than merely "
|
||||||
|
"true."
|
||||||
|
)
|
||||||
|
assert "notes_due_for_verification" in text, (
|
||||||
|
"the skill names the fields but not the surface that reads them — "
|
||||||
|
"guidance for writing a check with no route to acting on one."
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user