feat(systems): bootstrap mints directly — the standard vocabulary replaces operator approval (#2798)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 40s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 59s

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 20:05:02 -04:00
co-authored by Claude Fable 5
parent d50ebbad66
commit 5265d11a6a
4 changed files with 46 additions and 20 deletions
+5 -5
View File
@@ -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
+35 -13
View File
@@ -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.
+1 -1
View File
@@ -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 — ..."
+5 -1
View File
@@ -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