From 5265d11a6a39f2c09ecb9a38e9101770c5b7a73b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 19 Aug 2026 20:05:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(systems):=20bootstrap=20mints=20directly?= =?UTF-8?q?=20=E2=80=94=20the=20standard=20vocabulary=20replaces=20operato?= =?UTF-8?q?r=20approval=20(#2798)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator directive: creating Systems is the agent's call, not an approval flow — "propose to the operator, create each confirmed one" made the operator a permission gate. The bootstrap ask now says create_system 3-6 directly, in-session, and the consistency that approval was covering moves to a standard cross-project vocabulary (CI & Release, Auth & Access, Data Model & Storage, API Surface, UI & Design, Import & Export, Background Jobs, Observability): use the standard name verbatim where the area fits, mint freely beyond it, the duplicate gate guards sprawl. create_system and enter_project docstrings carry the same contract. Co-Authored-By: Claude Fable 5 --- src/scribe/mcp/tools/projects.py | 10 +++---- src/scribe/mcp/tools/systems.py | 48 +++++++++++++++++++++++--------- tests/test_mcp_tool_projects.py | 2 +- tests/test_mcp_tool_systems.py | 6 +++- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/scribe/mcp/tools/projects.py b/src/scribe/mcp/tools/projects.py index 9701846..5a5fc7b 100644 --- a/src/scribe/mcp/tools/projects.py +++ b/src/scribe/mcp/tools/projects.py @@ -78,10 +78,10 @@ async def enter_project(project_id: int) -> dict: 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. + Systems at all — act on it before starting other work: create_system a + starter vocabulary from the areas the project's records name, directly + and without asking permission, preferring the standard names the ask + lists. 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 @@ -129,7 +129,7 @@ async def enter_project(project_id: int) -> dict: # 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 + # task — the one moment minting 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 diff --git a/src/scribe/mcp/tools/systems.py b/src/scribe/mcp/tools/systems.py index fd33f19..acaa9a0 100644 --- a/src/scribe/mcp/tools/systems.py +++ b/src/scribe/mcp/tools/systems.py @@ -23,6 +23,18 @@ from scribe.services import systems as systems_svc _BOOTSTRAP_MIN_RECORDS = 20 _BOOTSTRAP_TITLES = 6 +# The standard vocabulary (#2798): area names that recur across software +# projects, offered so "CI & Release" means the same thing in every project +# on the instance. Consistency comes from the shared names — NOT from asking +# the operator to approve each System; agents mint directly. Generic by +# design (rule #115): archetypes any codebase could have, never one +# install's subsystems. Mint freely beyond the list; the duplicate gate +# guards sprawl. +_STANDARD_SYSTEMS = ( + "CI & Release", "Auth & Access", "Data Model & Storage", "API Surface", + "UI & Design", "Import & Export", "Background Jobs", "Observability", +) + async def bootstrap_systems_ask(user_id: int, project_id: int) -> str | None: """The escalated vocabulary-bootstrap ask for a mature zero-Systems project. @@ -33,8 +45,10 @@ async def bootstrap_systems_ask(user_id: int, project_id: int) -> str | None: (#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. + count and the recent titles, and asks for a concrete deliverable: create + a starter set directly, preferring the standard cross-project names. + Deliberately NOT an approval flow (#2798): the operator is not a + permission gate for vocabulary; the standard names carry consistency. Self-retiring by construction: callers only reach for it while the project has zero Systems, so the first create_system ends it everywhere. @@ -56,12 +70,15 @@ async def bootstrap_systems_ask(user_id: int, project_id: int) -> str | None: 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." + "invisible. Bootstrap the vocabulary now, in this session, without " + "asking permission — creating Systems is your call, not an approval " + f"flow. From the areas the records themselves name (recent: {titles}), " + "create_system 3-6 Systems, each with a one-paragraph charter, then " + "tag this record (system_ids=[...]). Where an area fits a standard " + f"name, use it verbatim so it means the same thing in every project: " + f"{', '.join(_STANDARD_SYSTEMS)}. Mint freely beyond that list — the " + "duplicate gate guards sprawl. This ask repeats until the first " + "System exists; answering it once retires it for every future record." ) @@ -148,11 +165,16 @@ async def create_system( Create one the moment two records would share an area that has no System yet — the same two-or-more test snippets use. Don't wait to be asked to - name an area that plainly exists in the code; an unmodelled area means - every record about it stays untaggable. An audit or sweep that walks the - codebase is a DISCOVERY moment: mint the Systems it names as it names - them — the duplicate gate below, plus reviewing the existing list, is what - guards against sprawl, not holding back. Give each one a one-paragraph + name an area that plainly exists in the code, and don't route the + creation through operator approval — minting vocabulary is the agent's + call (#2798); an unmodelled area means every record about it stays + untaggable. An audit or sweep that walks the codebase is a DISCOVERY + moment: mint the Systems it names as it names them — the duplicate gate + below, plus reviewing the existing list, is what guards against sprawl, + not holding back. Prefer the standard cross-project names where the area + fits one (CI & Release, Auth & Access, Data Model & Storage, API Surface, + UI & Design, Import & Export, Background Jobs, Observability) so the same + word means the same thing in every project. Give each one a one-paragraph charter, not just a label: the description is what tells a later session whether a record belongs here. diff --git a/tests/test_mcp_tool_projects.py b/tests/test_mcp_tool_projects.py index 74fe848..b464370 100644 --- a/tests/test_mcp_tool_projects.py +++ b/tests/test_mcp_tool_projects.py @@ -285,7 +285,7 @@ def _enter_project_stubs(p): async def test_enter_project_carries_the_bootstrap_ask_when_it_fires(): """The arrival-moment half of #2683: a mature zero-Systems project greets the session with the concrete bootstrap ask, before it is deep in a task — - the moment "propose a starter vocabulary" is cheapest.""" + the moment minting a starter vocabulary is cheapest.""" import contextlib ask = "This project has 282 records and NO Systems modelled — ..." diff --git a/tests/test_mcp_tool_systems.py b/tests/test_mcp_tool_systems.py index 2526af1..c51fcb9 100644 --- a/tests/test_mcp_tool_systems.py +++ b/tests/test_mcp_tool_systems.py @@ -129,7 +129,11 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project(): assert "282 records" in hint assert "Fix scrape retry backoff" in hint # the project's own evidence assert "3-6" in hint and "create_system" in hint - assert "propose" in hint + # NOT an approval flow (#2798): the agent mints directly, and the + # standard cross-project names carry the consistency instead. + assert "without asking permission" in hint + assert "propose" not in hint and "confirmed" not in hint + assert "CI & Release" in hint and "Auth & Access" in hint # The generic wording is REPLACED, not appended — two questions is noise. assert "no Systems yet" not in hint