Compare commits

...
5 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 0c237172e4 docs(6206): app.py startup comments state the hazard, not the outage
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 1m11s
CI & Build / integration (push) Successful in 1m35s
CI & Build / Python tests (push) Successful in 2m23s
CI & Build / Build & push image (push) Successful in 23s
The deferred-backfill and inline-release-defaults comments describe the
lifespan-overrun hazard they guard without the dated incident; backfill
passes say what they fill rather than which milestone added the vectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:38:30 -04:00
bvandeusenandClaude Opus 5.5 cad74df190 docs(6206): rulebook model comments state the design, not the history of the rulebook
The relation kinds say what each prevents without the rule-46 split story;
the removed tier, subscriptions and suppressions are stated as what the
model does not have, with the migration that dropped them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:38:01 -04:00
bvandeusenandClaude Opus 5.5 a01f5492bc docs(6206): code_shape model comments drop record and milestone citations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:37:22 -04:00
bvandeusenandClaude Opus 5.5 edf725687a docs(6206): routes/plugin comments drop record and milestone citations
The pre-tool rules docstring says why action rules need their own surface
instead of narrating that they once lived in the preload; the write-path
checkpoint note states the reason the hook never blocks rather than whose
decision it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:36:52 -04:00
bvandeusenandClaude Opus 5.5 87e8f8f777 docs(6205): last id-less stories in services — a ported-from project name and two "this milestone" references
backup_s3 says what its signing is checked against instead of which of the
operator's projects it was ported from; rulebooks and shape_check state what
the code is for rather than which milestone it belonged to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:35:04 -04:00
7 changed files with 107 additions and 121 deletions
+20 -23
View File
@@ -187,12 +187,12 @@ def create_app() -> Quart:
start_notification_loop()
start_auth_token_retention_loop()
# Write down any shipped default that moved in this release (#4225).
# Write down any shipped default that moved in this release.
#
# INLINE AND AWAITED, deliberately, against the instinct the block
# below argues for. What #4181 cost three hours was CONCURRENCY — a
# background task created here racing this hook for the same pool.
# Running sequentially creates no such contention, and this is twelve
# below argues for. The startup hazard is CONCURRENCY — a background
# task created here racing this hook for the same pool. Running
# sequentially creates no such contention, and this is a dozen
# single-row reads on an index against a table with a handful of rows.
#
# It has to finish before serving because the thing it protects is a
@@ -216,16 +216,14 @@ def create_app() -> Quart:
# Backfill embeddings for any notes that don't have one. Runs in the
# background so it never blocks the server from accepting requests —
# and, since #4181, not until the rest of this hook has finished.
# and not until the rest of this hook has finished.
#
# "Background" was true of REQUESTS and false of STARTUP. A task
# "Background" is true of REQUESTS and false of STARTUP. A task
# created here begins immediately, while `before_serving` is still
# running, so its four passes competed with the startup hook's own
# running, so its passes would compete with the startup hook's own
# database reads for the same connection pool. That is survivable on a
# healthy disk and fatal on a sick one: on 2026-09-19 both this
# backfill's first query and the hook's maintenance-hour read were
# cancelled together when Hypercorn killed the worker at its lifespan
# timeout, and the instance stayed down for three hours.
# healthy disk and fatal on a sick one: Hypercorn kills a worker whose
# lifespan hook overruns, and nothing retries it.
#
# The wait is on the SERVING FLAG rather than a sleep, because a sleep
# would be a guess about how long the rest of the hook takes and would
@@ -236,21 +234,20 @@ def create_app() -> Quart:
await backfill_note_embeddings()
except Exception:
logger.warning("Embedding backfill failed", exc_info=True)
# Rules got vectors in milestone 307; every rule written before it
# has none, so this is the pass that makes them findable at all.
# A rule with no vector is unfindable by meaning; this pass fills
# any that are missing.
try:
await backfill_rule_embeddings()
except Exception:
logger.warning("Rule embedding backfill failed", exc_info=True)
# Milestones got vectors in milestone 415, so a plan written before
# it is findable only after this pass.
# Likewise a milestone: a plan with no vector is findable only
# after this pass.
try:
await backfill_milestone_embeddings()
except Exception:
logger.warning("Milestone embedding backfill failed", exc_info=True)
# Systems got vectors in #4251, so before this pass every charter
# ever written is unfindable — a System could narrow a search and
# never be the answer to one.
# A System charter with no vector is unfindable — the System could
# narrow a search and never be the answer to one.
try:
await backfill_system_embeddings()
except Exception:
@@ -270,7 +267,7 @@ def create_app() -> Quart:
# work overlaps startup's own.
asyncio.create_task(_delayed_backfill())
# RELEASED IN A `finally`, never after the work (rules 156 and 157).
# RELEASED IN A `finally`, never after the work.
# The waiter above has no deadline of its own, and the way to make an
# undeadlined wait safe is to make the release unmissable: if anything
# below raises, the flag is still set and the task ends instead of
@@ -292,7 +289,7 @@ def create_app() -> Quart:
# DB maintenance scheduler (daily targeted VACUUM ANALYZE, default
# 04:00 UTC). `get_maintenance_hour` is the FIRST database read in
# this hook and is bounded for that reason (#4181) — see the long
# this hook and is bounded for that reason — see the long
# comment above `_STARTUP_READ_TIMEOUT`. Anything added here that
# touches the database needs the same treatment: a lifespan hook
# that does not return is a worker that never serves.
@@ -304,8 +301,8 @@ def create_app() -> Quart:
asyncio.get_running_loop(), await get_maintenance_hour()
)
# Scheduled backups (milestone 523). An hourly tick that reads its
# config when it fires, so starting it reads nothing here (#4181).
# Scheduled backups. An hourly tick that reads its config when it
# fires, so starting it reads nothing here.
from scribe.services.backup_job import start_backup_scheduler
start_backup_scheduler(asyncio.get_running_loop())
@@ -315,7 +312,7 @@ def create_app() -> Quart:
from scribe.services.diagnostics import start_diagnostics
start_diagnostics(asyncio.get_running_loop())
finally:
# STARTUP IS OVER — release the backfill (#4181). Everything above
# STARTUP IS OVER — release the backfill. Everything above
# is work the server needs done before it serves; everything the
# backfill does is work that can wait for a server already up.
logger.info("Startup complete; releasing deferred backfill")
+21 -22
View File
@@ -15,9 +15,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
# The classification vocabulary (note 2786). `unclassified` is the default and
# The classification vocabulary. `unclassified` is the default and
# THE todo state; every other status is a judgment, stamped with who made it —
# except `scoped` (#2869): the coverage sync's mechanical stamp on shapes that
# except `scoped`: the coverage sync's mechanical stamp on shapes that
# are one-offs BY CONSTRUCTION (a Vue component's scoped <style> rules and its
# <script setup> functions — unreachable from any other file). Scoped rows are
# accounted for without a human judging them, so `exempt` keeps meaning "a
@@ -26,11 +26,11 @@ from scribe.models.base import TimestampMixin, iso
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "scoped", "unclassified")
SHAPE_CLASSIFIERS = ("agent", "audit", "hook", "mechanical", "import")
# The reason catalogue (#2874): an OPTIONAL code beside the prose reason on
# The reason catalogue: an OPTIONAL code beside the prose reason on
# variant/exempt rows, so the ledger can be filtered and aggregated by kind
# of one-off. The prose remains the record; the code is the index.
REASON_CODES = (
"scoped-css", # a scoped rule styling one element (pre-#2869 rows)
"scoped-css", # a scoped rule styling one element (rows judged before `scoped`)
"one-off-handler", # a view/component handler or loader, one per surface
"test-helper", # a test module's stub, driver or fixture data
"convention-plumbing", # registration, wiring, app factory — one of each
@@ -40,17 +40,16 @@ REASON_CODES = (
"typed-record", # a NamedTuple / dataclass / error class — one each
)
# How the mechanical proposer (#2792) arrived at a proposal, strongest first.
# How the mechanical proposer arrived at a proposal, strongest first.
# `derive` is the odd one out: not "this is an instance of #N" but "this
# shape repeats with NO canon — derive one first" (note 2786's derive-first
# rule), so it carries a group key instead of a snippet.
# shape repeats with NO canon — derive one first", so it carries a group key instead of a snippet.
PROPOSAL_BASES = ("symbol", "text", "reference", "signature", "semantic", "derive")
class CodeShape(Base, TimestampMixin):
"""One extracted code shape and its classification against canon (#2787).
"""One extracted code shape and its classification against canon.
The accounting half of the pattern system (governing note 2786): the
The accounting half of the pattern system: the
snippet library records CANON (small); this ledger accounts for EVERY
shape the coverage extractor finds in a bound repo (total). A row's
status says how the shape relates to canon — it IS a snippet's reference
@@ -62,15 +61,15 @@ class CodeShape(Base, TimestampMixin):
Identity is (project, repo_key, path, symbol, kind) — kind is part of it
because one file can define `.foo` (css) and `foo` (sym) as distinct
shapes. A rename therefore reads as vanish + new row: accepted for v1,
shapes. A rename therefore reads as vanish + new row: accepted,
because chasing renames needs content identity the extractor doesn't
have. `vanished_at` keeps the history instead of deleting it.
snippet_id is SET NULL on snippet deletion: the classification's target
is gone but the judgment happened; the sync pass (step 2) re-files such
is gone but the judgment happened; the sync pass re-files such
rows as unclassified so they rejoin the todo instead of dangling.
`signature` / `body_sha` (#2792) are the shape's content fingerprint —
`signature` / `body_sha` are the shape's content fingerprint —
its definition line and a whitespace/comment-insensitive hash of its
block — refreshed by every sync. They are what the mechanical proposer
matches on and what a later drift recheck compares against; the ledger
@@ -80,7 +79,7 @@ class CodeShape(Base, TimestampMixin):
when a later sync sees the body change under an instance/variant, the
row is flagged `recheck_at` (the judgment stands, it just asks to be
confirmed again) and a `drifted` event is written. `diverges_from`
(#2793) is the button-B flag: a shape new since the previous refresh, in
is the button-B flag: a shape new since the previous refresh, in
a directory+kind where one canon dominates the judged siblings, that the
proposer did not match to that canon — "button B appeared where button
A is canon: divergence or variant? classify it." Both clear on judgment.
@@ -119,7 +118,7 @@ class CodeShape(Base, TimestampMixin):
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
)
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
reason_code: Mapped[str | None] = mapped_column(Text, nullable=True) # REASON_CODES (#2874)
reason_code: Mapped[str | None] = mapped_column(Text, nullable=True) # REASON_CODES
classified_by: Mapped[str | None] = mapped_column(Text, nullable=True)
classified_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
@@ -190,7 +189,7 @@ class CodeShape(Base, TimestampMixin):
}
def to_compact(self) -> dict:
"""The row as an audit reads it (#2868): identity, standing, the
"""The row as an audit reads it: identity, standing, the
definition line and the proposer's word — none of the bookkeeping
(commits, shas, timestamps). A 500-row page of these fits the tool
budget; a page of to_dict() does not."""
@@ -217,16 +216,16 @@ class CodeShape(Base, TimestampMixin):
return out
# How a uses edge was established (#2870): who/what said "this shape calls
# How a uses edge was established: who/what said "this shape calls
# that canon". `reference` is the proposer's mechanical by-name hit on the
# body (language-gated, #2871); `hook` is write-path evidence (pulled the
# body (language-gated); `hook` is write-path evidence (pulled the
# snippet, then wrote code naming its symbol); agent/audit/import are
# judgments carried on classify_shapes(..., uses=[...]).
USE_BASES = ("reference", "hook", "agent", "audit", "import")
class CodeShapeUse(Base):
"""One consumption edge: shape → canonical snippet it calls/uses (#2870).
"""One consumption edge: shape → canonical snippet it calls/uses.
Conformance (CodeShape.status/snippet_id) answers "what shape is this";
this table answers "what does it use" — many per shape. A service function
@@ -265,7 +264,7 @@ class CodeShapeUse(Base):
}
# How a consumer edge was established (milestone 302). `template` is the
# How a consumer edge was established. `template` is the
# sync's mechanical read of a file's markup (class= / :class= / className=);
# the vocabulary is a list so a later basis (a stylesheet `@apply`, a script's
# classList) has a name without a schema change.
@@ -274,8 +273,8 @@ CONSUMER_BASES = ("template",)
class CodeShapeConsumer(Base):
"""One consumer edge: CSS shape → the file whose markup names its class
(milestone 302; note 2917 — CSS is watched by name, by recipe, by token
and by WHAT USES IT). The analogue of CodeShapeUse for styling: `uses`
(CSS is watched by name, by recipe, by token and by WHAT USES IT). The
analogue of CodeShapeUse for styling: `uses`
says what a shape calls, this says who renders a class. Rows, not prose,
so "is this recipe shared or scoped?" is a count, not a guess.
@@ -311,7 +310,7 @@ class CodeShapeConsumer(Base):
}
# What a shape's history records (#2793). Not "appeared" — first_seen and
# What a shape's history records. Not "appeared" — first_seen and
# created_at already say that on the row; history is for what CHANGED:
SHAPE_EVENTS = ("classified", "vanished", "reappeared", "drifted")
+23 -30
View File
@@ -92,15 +92,13 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
# WHEN this rule applies — the trigger, not the instruction. Required of
# new rules at the service layer and nullable here, because rules written
# before migration 0088 have none and a migration cannot invent one.
# It carries three jobs at once (note 3026): it is the readable form of
# the canon tag, the half of the document that makes a rule findable by
# meaning, and — since milestone 394 removed the always-on tier — the ONLY
# thing that decides whether a rule ever reaches a session at all. A rule
# It carries three jobs at once: it is the readable form of the canon tag,
# the half of the document that makes a rule findable by meaning, and —
# with no always-on tier — the ONLY thing that decides whether a rule ever reaches a session at all. A rule
# with no trigger is not a quiet rule, it is an unreachable one.
when_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
# WHAT KIND of instruction this is. `tier` used to sit beside this and
# carry delivery; milestone 394 removed it, so kind is now the only axis
# on a rule and delivery belongs entirely to retrieval.
# WHAT KIND of instruction this is — the only axis on a rule; delivery
# belongs entirely to retrieval.
# `rule` must be FOLLOWED: ignoring it breaks something or crosses a
# boundary. `preference` is how this person wants work DONE: ignoring it
# costs consistency, not correctness.
@@ -116,7 +114,7 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
# trigger-dominated document, ownership-scoped search, the retrieval arms,
# relations, and `rule_versions`, which is where its drift is recorded.
# Defaults to `rule` so nothing changes force on upgrade.
# CHECK ck_rules_kind (migration 0098, rule 36).
# CHECK ck_rules_kind (migration 0098).
kind: Mapped[str] = mapped_column(Text, default="rule", server_default="rule")
why: Mapped[str | None] = mapped_column(Text, nullable=True)
how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -141,7 +139,7 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
DateTime(timezone=True), nullable=True
)
# The record that caused this rule — the edge notes and tasks already
# have. Rule 46's `why` names note 2813 in prose; this is that link as a
# have. A `why` can name its origin in prose; this is that link as a
# field, so it survives a rewording of the paragraph.
arose_from_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
@@ -175,10 +173,9 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
}
# Which global AREA a rule is about (milestone 307). Points at the canonical
# catalog, NEVER at a project's `systems` row: a rule that spans projects
# cannot be chained to one project's vocabulary. This is the edge three
# projects were drawing by hand, as rule text copied into a System's charter.
# Which global AREA a rule is about. Points at the canonical catalog, NEVER at
# a project's `systems` row: a rule that spans projects cannot be chained to
# one project's vocabulary.
rule_systems = Table(
"rule_systems",
Base.metadata,
@@ -188,7 +185,7 @@ rule_systems = Table(
)
# Which MOMENTS of work a rule arrives at (milestone 458). A rule mounted on a
# Which MOMENTS of work a rule arrives at. A rule mounted on a
# moment is delivered whenever that moment happens, by lookup and with no
# score — the route for a rule that is about WHEN something is done rather
# than what it is about, which semantic retrieval structurally misses. The
@@ -204,17 +201,15 @@ rule_moments = Table(
)
class RuleRelation(Base, CreatedAtMixin):
"""A typed edge between two rules. Each kind exists because its ABSENCE
forced a workaround somewhere in the operator's rulebook (note 3026).
"""A typed edge between two rules. Each kind exists because without it a
rulebook needs a workaround.
- ``co_surfaces`` — these fail together, so they must arrive together.
Without it, the only way to guarantee that was to merge them into one
row, which is what happened to rule 46: split into 144, folded back the
same day because "either rule could surface without the other."
Without it, the only way to guarantee that is to merge them into one
row, because either rule could surface without the other.
Symmetric in meaning; stored once and read both ways.
- ``overrides`` — this rule supersedes that one for its scope. Only
*suppression* existed, so an override had to be written as a parallel
rule that then drifts from its parent.
- ``overrides`` — this rule supersedes that one for its scope. Without
it, an override is a parallel rule that drifts from its parent.
- ``elaborates`` — this rule adds local specifics to that one; surfacing
the parent brings the addendum with it.
@@ -232,7 +227,7 @@ class RuleRelation(Base, CreatedAtMixin):
to_rule_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("rules.id", ondelete="CASCADE"), index=True
)
# CHECK ck_rule_relations_kind (migration 0088, rule 36).
# CHECK ck_rule_relations_kind (migration 0088).
kind: Mapped[str] = mapped_column(Text)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -251,10 +246,8 @@ class RuleRelation(Base, CreatedAtMixin):
}
# `project_rulebook_subscriptions`, `project_rule_suppressions` and
# `project_topic_suppressions` lived here until milestone 414 (migration 0101).
# A rule's home is its scope now: a rule in a rulebook topic is global, a rule
# on a project applies to that project, and retrieval reads that directly. A
# subscription had stopped changing anything a session received, and a
# suppression muted rules from a subscription. A project that departs from a
# global rule writes a project rule with an `overrides` relation instead.
# A rule's home is its scope: a rule in a rulebook topic is global, a rule on
# a project applies to that project, and retrieval reads that directly. There
# are no per-project subscriptions or suppressions (migration 0101 dropped
# those tables); a project that departs from a global rule writes a project
# rule with an `overrides` relation.
+33 -35
View File
@@ -72,14 +72,14 @@ async def session_context():
unbound repo yields a "bind this repo" hint instead. This is how the
active project is determined — the plugin never pins a project id.
project_id (optional int) — the project named directly, and the only
key a caller outside a git repo has (#4085): the plugin's hooks
key a caller outside a git repo has: the plugin's hooks
send it when a `.scribe` marker file names a project. Takes
precedence over `repo`. Access-checked like any other read — an id
this account cannot read loads no project rather than failing.
source (optional str) — the host's SessionStart source (startup,
resume, compact, clear, fork); decides what the claim section says.
session_id (optional str) — the session's id, so a claim bound to it
reads as this session's own (milestone 381).
reads as this session's own.
"""
project_id, _repo, unbound_repo = await _project_scope()
result = await plugin_ctx_svc.build_session_context(
@@ -109,7 +109,7 @@ async def autoinject_retrieve():
project_id (opt) — explicit project scope override (ad-hoc/testing).
exclude_ids (opt) — comma-separated note ids already injected this
session; skipped so each note injects at most once.
ctx (opt) — the tail of the last assistant reply (#4364). The
ctx (opt) — the tail of the last assistant reply. The
notes AND rule arms append it to a short prompt,
so a follow-up like "yes do that" still names
what it is about — and a rule or lesson arrives
@@ -119,22 +119,22 @@ async def autoinject_retrieve():
(opt) session. SHARED with /prior-art and /tool-rules on
purpose: one session keeps ONE rule ledger, so a
rule named by any arm is not re-announced by
another. Ages out (#3751), so salience decays.
another. Ages out, so salience decays.
held_rule_ids — comma-separated rule ids the session actually
(opt) OPENED, observed from the `get_rule` call itself
rather than claimed. A rule on exclude_rule_ids
was NAMED; a rule here was READ, and the two say
different things about what the reader holds —
so they get different lines (#4100). Shares the
so they get different lines. Shares the
ledger directory and the same clear-on-compact.
shapes_seen (opt) — comma-separated reply-shape keys this session was
already shown in full (milestone 500). The core
already shown in full. The core
goes out in full when `core` is absent, as its
one-line reminder when present; the hook's
ledger is swept on compaction, which is how the
full core comes back after one.
THE TURN'S REPLY SHAPE COMES FIRST (milestone 500 step 3). Every turn ends
THE TURN'S REPLY SHAPE COMES FIRST. Every turn ends
in a reply and this is the last point before it is written, so the core
shape and whatever is mounted on `reply.report` lead the payload. Fresh
shape keys come back as `shape_keys` for the ledger.
@@ -165,7 +165,7 @@ async def autoinject_retrieve():
g.user.id, q, project_id=project_id, exclude_ids=exclude_ids, context=ctx,
)
# The one place both arms' lines meet, so the one place a lesson and a
# rule arriving TOGETHER can be counted (#4637). Keyed on the operator's
# rule arriving TOGETHER can be counted. Keyed on the operator's
# prompt — the situation — and fails open to "".
proposal = await lesson_rules_svc.co_surfaced(
g.user.id, result.get("lesson_ids") or [], rules.get("shown_rule_ids") or [],
@@ -191,13 +191,13 @@ async def autoinject_retrieve():
@login_required
@memoized_query_embeddings
async def pre_tool_rules():
"""Standing rules for the plugin's PreToolUse hook on ACTIONS (#3476).
"""Standing rules for the plugin's PreToolUse hook on ACTIONS.
Answers "does a recorded rule speak to the command about to be run?" — the
sibling of /prior-art, which can only answer that question about a code
write. Rules about which tool to reach for (don't curl the forge, don't
stand up a stack, don't run the suite locally) had no retrieval surface at
all before this, which is why they all had to live in the resident preload.
stand up a stack, don't run the suite locally) have no other retrieval
surface: no code write ever happens for them to ride on.
Titles + trigger only, never the statement: the hint says a rule may apply
and hands over `get_rule(id)`. One rule at most (RULEHINT_LIMIT), and empty
@@ -219,18 +219,17 @@ async def pre_tool_rules():
held_rule_ids (opt) — rule ids the session actually OPENED, as
against merely named. Named and read are
different claims about the reader's
context, so they get different lines
(#4100).
context, so they get different lines.
root, cwd (opt) — the repo's absolute root and the command's
working directory. They turn the paths the
command names into repo-relative ones, for
the rulings arm (milestone 444).
the rulings arm.
seen_ruling_systems (opt) — comma-separated System ids whose rulings
were already shown this session, by either
arm; those get a one-line reference. SHARED
with /prior-art, like exclude_rule_ids.
Returns `context`, `rule_ids`, and `checkpoint` (#4214, milestone 419),
Returns `context`, `rule_ids`, and `checkpoint`,
plus `ruling_system_ids` — the Systems whose rulings were shown in full on
this call — when there were any.
@@ -268,7 +267,7 @@ async def pre_tool_rules():
@plugin_bp.get("/moment-tools")
@login_required
async def moment_tools():
"""The tools whose calls can deliver a mounted rule (milestone 458 step 4).
"""The tools whose calls can deliver a mounted rule.
Read by the plugin's catch-all PreToolUse hook once per session window and
kept on disk, so a call that cannot reach a mounted rule costs no request.
@@ -282,7 +281,7 @@ async def moment_tools():
@plugin_bp.post("/moment")
@login_required
async def moment():
"""The rules mounted on the moments this tool call reaches (milestone 458).
"""The rules mounted on the moments this tool call reaches.
Body: the PreToolUse event as Claude Code delivers it — `tool_name` and
`tool_input` are read, nothing else. Sent whole because which input field
@@ -298,7 +297,7 @@ async def moment():
`moments` (the names reached). A lookup, not a ranked search: no
retrieval_logs row; each fresh rule is recorded surfaced with its moment.
A moment that carries a default reply shape (milestone 500) brings it too,
A moment that carries a default reply shape brings it too,
ahead of the rules: in full unless `shapes_seen` names it, then as its
reminder. `shape_keys` are the ones sent in full, for the hook's ledger.
"""
@@ -330,7 +329,7 @@ async def moment():
@plugin_bp.post("/rule-opened")
@login_required
async def rule_opened():
"""A session opened a rule shortly after these acts (milestone 458 step 7).
"""A session opened a rule shortly after these acts.
Body: `{"rule_id", "session_id", "acts": [<PreToolUse event>, ...]}` —
the calls the plugin saw in the window before the `get_rule`. Each is
@@ -360,16 +359,15 @@ async def rule_opened():
@login_required
@memoized_query_embeddings
async def reply_rules():
"""Whether the reply that ends a turn is held for one read (milestone 458).
"""Whether the reply that ends a turn is held for one read.
Called by the plugin's Stop hook with the finished reply. Two halves: the
rules MOUNTED on the reply moments (`reply.report`, and `reply.ask` when
the reply asks something), and the reply text against every rule's
trigger — the backstop for whatever the earlier arms missed.
ONE END-OF-TURN REQUEST (milestone 500 step 4): a reply that closed tasks
is also checked here for the completion sections (services/report_check),
which used to be a Stop hook and an endpoint of its own. Both holds fold
ONE END-OF-TURN REQUEST: a reply that closed tasks is also checked here
for the completion sections (services/report_check). Both holds fold
into one `reason`; `report_check` says what the section check recorded, so
the hook knows the rewrite is one to report.
@@ -447,7 +445,7 @@ async def write_path_prior_art():
exclude_ids (opt) — comma-separated REUSE-class ids already surfaced
this session.
exclude_sync_ids (opt) — comma-separated SYNC-class ids (snippets
recorded AT the edited file, #2708) already
recorded AT the edited file) already
surfaced. A separate channel on purpose: a reuse
hint shown early must not suppress the record-sync
nudge when the recorded file is edited later.
@@ -458,31 +456,31 @@ async def write_path_prior_art():
every subsequent write.
held_rule_ids (opt) — RULE ids the session actually OPENED, as
against merely named; drives the third
reference wording (#4100).
reference wording.
exclude_derive (opt) — comma-separated derive keys (a derive group id
or `canon:<snippet_id>`) already named this
session by the ledger arm (#2900); its own
session by the ledger arm; its own
channel, like the two above.
seen_ruling_systems (opt) — System ids whose rulings were already shown
this session (milestone 444); shared with
this session; shared with
/tool-rules. The response's `ruling_system_ids`
are the ones shown in full on this call.
(Returns a `checkpoint` block on the same contract as /tool-rules —
see that endpoint. The write-path HOOK deliberately does not act on
it: scribe_prior_art.sh carries a tested property that it never
returns a permissionDecision, on the operator's decision that a recall
aid may not stand in the way of a write. The block is computed and
returns a permissionDecision, because a recall aid must not stand in
the way of a write. The block is computed and
returned so that decision can be revisited with evidence rather than
re-argued, and so a change of mind is a hook edit and not a feature.)
shapes (opt) — comma-separated `kind:name` definitions the hook
found in (or enclosing) the payload, kind being
css|sym. The shape ledger's write-path feed
(#2791): when the session recently PULLED a
css|sym. The shape ledger's write-path feed:
when the session recently PULLED a
snippet this payload references or resembles,
these carry it as a proposal — evidence for the
agent's own end-of-turn judgment, never a
verdict (milestone 439). Honoured only for a
verdict. Honoured only for a
caller allowed to write — a read-scoped key still
gets the hint, and never changes the ledger on a
GET.
@@ -537,7 +535,7 @@ def _parse_shapes(raw: str) -> list[tuple[str, str]]:
@plugin_bp.get("/shape-check")
@login_required
async def shape_check():
"""The end-of-turn question (milestone 439): what did this turn write that
"""The end-of-turn question: what did this turn write that
nobody has judged?
The client's write hooks keep a ledger of the definitions each turn wrote;
@@ -602,7 +600,7 @@ async def shape_check():
@plugin_bp.get("/claim-session")
@login_required
async def claim_session():
"""Bind the harness's session id to the caller's live claim on a task (milestone 381).
"""Bind the harness's session id to the caller's live claim on a task.
Called by `scribe_claim_session.sh` after `update_task` / `add_task_log`.
The server has already stamped the claim on that write; this only says
@@ -631,7 +629,7 @@ async def claim_session():
@plugin_bp.get("/release-session")
@login_required
async def release_session():
"""Release the claims a session held, as it ends (milestone 381 step 4).
"""Release the claims a session held, as it ends.
Called by `scribe_session_end.sh`. Best-effort by design: SessionEnd does
not fire on a crash, so the claim's lease — not this call — is what makes a
+2 -2
View File
@@ -1,8 +1,8 @@
"""A small S3 client for sending backups: AWS Signature Version 4 over httpx,
path-style addressing (endpoint/bucket/key), and the two calls a backup needs.
Ported from FabledSteward's `steward/backup/s3.py`, which is tested against
the same AWS examples as tests/test_services_backup_s3.py.
The signing is checked against AWS's published SigV4 examples in
tests/test_services_backup_s3.py.
Scribe puts and lists; it never deletes. The bucket keeps the history and
thins it on its own policy, so a key that may only add objects is enough —
+6 -7
View File
@@ -324,8 +324,8 @@ def rule_brief(rule: Rule, **extra) -> dict:
of payload for a precision nobody reads.
`why` and `how_to_apply` are deliberately NOT here — they are the depth a
caller gets from get_rule, and putting them in every listing is the bloat
this milestone is about.
caller gets from get_rule, and putting them in every listing would bloat
every payload that lists rules.
"""
out = {
"id": rule.id,
@@ -1583,11 +1583,10 @@ async def rules_due_for_verification(
) -> list[Rule]:
"""Rules that carry a check, oldest verification first, never-checked top.
THE QUERY THIS MILESTONE EXISTS FOR. `verify_with` and `expires_when` are
storage; this is what turns them into something that gets acted on. The
307 audit cost a session and found four broken rules by luck — this makes
the same question a list, and staleness measurable by age instead of
discoverable by accident.
`verify_with` and `expires_when` are storage; this is what turns them into
something that gets acted on. Without it, finding broken rules takes an
audit and luck — this makes the same question a list, and staleness
measurable by age instead of discoverable by accident.
Ordered `verified_at` ASC NULLS FIRST: never-checked outranks
checked-long-ago, because a rule nobody has ever confirmed is a claim
+2 -2
View File
@@ -161,8 +161,8 @@ def summarise(outcomes: list[str]) -> dict:
`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."""
unjudged shapes. That last number is the slip this check 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")