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
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:
@@ -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? "
|
||||
|
||||
Reference in New Issue
Block a user