feat(systems): evidence-carrying bootstrap ask for mature zero-Systems projects (#2683)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 39s

The generic zero-state systems_hint never converts: identical on every
record, maximal in scope, asked at wrap-up time — Minstrel reached 282
records with zero Systems while vocabularied projects grew organically.
What converts is the project's own evidence at the moment of action.

bootstrap_systems_ask (mcp/tools/systems.py) fires only in a project
with >=20 records and no Systems: it names the record count and recent
titles, and asks for a concrete deliverable — propose 3-6 Systems,
confirm with the operator, create_system the set. Self-retiring: the
first System ends it everywhere. Wired at both moments the task named:
untagged_systems_hint escalates to it at write time, and enter_project
carries it as systems_bootstrap at arrival (attached only when it
applies). Young projects keep the mild question; populated vocabularies
never pay the count query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:53:46 -04:00
co-authored by Claude Fable 5
parent d6c9f08a59
commit 7a5e2b18d9
4 changed files with 221 additions and 6 deletions
+26 -2
View File
@@ -17,6 +17,7 @@ keeps working.
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import coverage as coverage_svc
from scribe.services import design_systems as design_systems_svc
from scribe.services import milestones as milestones_svc
@@ -57,7 +58,8 @@ async def enter_project(project_id: int) -> dict:
Returns a dict with keys: project, milestone_summary, applicable_rules,
project_rules, subscribed_rulebooks, applicable_rules_truncated,
open_tasks, recent_notes, design_system, systems, pattern_coverage.
open_tasks, recent_notes, design_system, systems, pattern_coverage
plus systems_bootstrap, present only when it applies (see below).
`pattern_coverage` (usually null) is a one-line estimate of how much of
the bound repo's code has recorded snippets — e.g. "pattern-library
@@ -72,6 +74,12 @@ async def enter_project(project_id: int) -> dict:
create it with create_system rather than leaving the area unmodelled. Read
a subsystem's accumulated records with list_system_records.
`systems_bootstrap` appears ONLY when the project has many records and no
Systems at all — act on it before starting other work: propose a starter
vocabulary from the areas the project's records name, confirm it with the
operator, and create_system the confirmed set. It stops appearing the
moment the first System exists.
`design_system` is null unless the project points at one. When present it
carries the chain-merged guidance (the house style AND this project's
departures from it) plus a summary of the token set — treat it as binding
@@ -116,6 +124,17 @@ async def enter_project(project_id: int) -> dict:
# three days of the feature landing (#2546's audit).
systems = await systems_svc.list_systems(uid, project_id)
# The arrival-moment half of the bootstrap ask (#2683): session start is
# when the agent has just read the project map and is not yet deep in a
# task — the one moment "propose a starter vocabulary" is cheap. The
# write-moment half rides untagged-record responses (attach_systems);
# both retire the instant the first System exists.
systems_bootstrap = None
if not systems:
systems_bootstrap = await systems_tools.bootstrap_systems_ask(
uid, project_id
)
# Probably the largest surfacing by volume, and it emitted nothing — so
# the pulls it caused floated unattributed and the surfaced:pulled ratio
# ran against a denominator missing its biggest contributor (#2477). An
@@ -143,7 +162,7 @@ async def enter_project(project_id: int) -> dict:
project.user_id or uid, project_id
)
return {
out = {
"project": project.to_dict(),
"pattern_coverage": coverage_svc.coverage_line(coverage) if coverage else None,
# Trimmed to what tagging needs. The full charter is get_system's job —
@@ -179,6 +198,11 @@ async def enter_project(project_id: int) -> dict:
for n in recent_notes
],
}
# Attached only when it applies — a key that usually says null trains
# readers to skip it (#2483), and this one exists to be acted on.
if systems_bootstrap:
out["systems_bootstrap"] = systems_bootstrap
return out
async def get_project(project_id: int) -> dict:
+56
View File
@@ -13,8 +13,57 @@ Sentinels (match the milestone/task tool conventions):
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
# Below this, a project is young enough that the mild "which area is this
# about?" question stays proportionate; at or above it, a zero-Systems project
# has demonstrated that the question never converts (#2683 — Minstrel reached
# 282 records without a single System) and the ask escalates.
_BOOTSTRAP_MIN_RECORDS = 20
_BOOTSTRAP_TITLES = 6
async def bootstrap_systems_ask(user_id: int, project_id: int) -> str | None:
"""The escalated vocabulary-bootstrap ask for a mature zero-Systems project.
The generic zero-state question habituates: identical on every record,
maximal in scope ("invent the taxonomy"), asked at wrap-up time — so
organic sessions skip it forever and only audit-shaped sessions ever mint
(#2683). What separates the nudges that convert from the prose that
doesn't (the duplicate gate, the prior-art "already defined in 2 files")
is the project's OWN evidence in the ask — so this one carries the record
count and the recent titles, and asks for a concrete deliverable: propose
a starter set, confirm, create.
Self-retiring by construction: callers only reach for it while the
project has zero Systems, so the first create_system ends it everywhere.
Returns None below the record threshold or on any failure (fail-open —
a hint must never break the call it rides on).
"""
try:
recent, total = await notes_svc.list_notes(
user_id, project_id=project_id, sort="updated_at",
limit=_BOOTSTRAP_TITLES,
)
except Exception:
return None
if total < _BOOTSTRAP_MIN_RECORDS:
return None
titles = "; ".join(
'"' + " ".join((n.title or "").split())[:70] + '"' for n in recent
)
return (
f"This project has {total} records and NO Systems modelled — none of "
"them can be tagged to an area, so recurring problem-spots stay "
"invisible. Bootstrap the vocabulary now, in this session: from the "
f"areas the records themselves name (recent: {titles}), propose 3-6 "
"Systems to the operator, create_system each confirmed one with a "
"one-paragraph charter, then tag this record (system_ids=[...]). "
"This ask repeats until the first System exists; answering it once "
"retires it for every future record."
)
async def untagged_systems_hint(user_id: int, project_id: int) -> str | None:
"""The Systems question, for an untagged project record.
@@ -26,6 +75,10 @@ async def untagged_systems_hint(user_id: int, project_id: int) -> str | None:
vocabulary is not an exemption — it is the question at its most urgent
(#2562, #2569). Instruction prose alone demonstrably doesn't fire at write
time; in-band behavior (the duplicate gate) does.
In a MATURE zero-Systems project the question escalates to the bootstrap
ask instead (#2683): the mild form demonstrably never converts there, and
an evidence-carrying, deliverable-shaped ask is the form that does.
"""
# Fail-open like the dedup gate: a hint must never break the call.
try:
@@ -37,6 +90,9 @@ async def untagged_systems_hint(user_id: int, project_id: int) -> str | None:
f"#{s.id} {s.name}" for s in systems
) + "."
else:
ask = await bootstrap_systems_ask(user_id, project_id)
if ask:
return ask
vocab = "This project has no Systems yet."
return (
"This record is untagged — which area(s) of the project is it about? "