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:
@@ -88,13 +88,22 @@ async def test_untagged_hint_names_the_projects_systems():
|
||||
assert "create_system" in hint
|
||||
|
||||
|
||||
def _fake_note(title):
|
||||
n = MagicMock()
|
||||
n.title = title
|
||||
return n
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
# Zero Systems is the state nothing else nudges out of — the hint must
|
||||
# prompt the FIRST create_system, not go silent.
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
# Zero Systems in a YOUNG project (below the bootstrap threshold):
|
||||
# the mild question stays proportionate — it must prompt the FIRST
|
||||
# create_system, not go silent.
|
||||
svc.list_systems = AsyncMock(return_value=[])
|
||||
notes.list_notes = AsyncMock(return_value=([], 3))
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
assert "no Systems yet" in hint and "create_system" in hint
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc:
|
||||
@@ -103,6 +112,57 @@ async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
||||
assert await untagged_systems_hint(1, 5) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||
"""#2683: at 282 records and zero Systems (Minstrel), the generic question
|
||||
had demonstrably never converted. The escalated ask must carry the
|
||||
project's OWN evidence — record count and recent titles — and demand a
|
||||
concrete deliverable, because that is the property separating the nudges
|
||||
that convert from the prose that doesn't."""
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
recent = [_fake_note("Fix scrape retry backoff"), _fake_note("Worker pool sizing")]
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[])
|
||||
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
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
|
||||
# The generic wording is REPLACED, not appended — two questions is noise.
|
||||
assert "no Systems yet" not in hint
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bootstrap_ask_stays_quiet_below_threshold_and_fails_open():
|
||||
from scribe.mcp.tools import systems as tools
|
||||
with patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
notes.list_notes = AsyncMock(
|
||||
return_value=([], tools._BOOTSTRAP_MIN_RECORDS - 1))
|
||||
assert await tools.bootstrap_systems_ask(1, 5) is None
|
||||
with patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
# The record count is decoration on a hint on a create — three layers
|
||||
# deep, nothing there may break the call.
|
||||
notes.list_notes = AsyncMock(side_effect=RuntimeError("db down"))
|
||||
assert await tools.bootstrap_systems_ask(1, 5) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_populated_vocabulary_never_counts_records():
|
||||
"""The bootstrap query is zero-state-only: once Systems exist the hint
|
||||
must not spend a count query per untagged record."""
|
||||
from scribe.mcp.tools.systems import untagged_systems_hint
|
||||
sys_a = MagicMock(); sys_a.id = 1; sys_a.name = "workers"
|
||||
with patch("scribe.mcp.tools.systems.systems_svc") as svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[sys_a])
|
||||
notes.list_notes = AsyncMock()
|
||||
hint = await untagged_systems_hint(1, 5)
|
||||
assert "workers" in hint
|
||||
notes.list_notes.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_system_same_normalized_name_is_duplicate_gated():
|
||||
existing = _fake_system(sid=7, name="Scrape Pipeline")
|
||||
@@ -204,11 +264,15 @@ async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
||||
with patch("scribe.mcp.tools.tasks.current_user_id", return_value=1), \
|
||||
patch("scribe.mcp.tools.tasks.task_logs_svc") as logs_svc, \
|
||||
patch("scribe.mcp.tools.tasks.notes_svc") as notes_svc, \
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc:
|
||||
patch("scribe.mcp.tools.systems.systems_svc") as seam_svc, \
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as seam_notes:
|
||||
logs_svc.create_log = AsyncMock(return_value=log)
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
seam_svc.list_systems = AsyncMock(return_value=[])
|
||||
# Young project: below the #2683 bootstrap threshold, so the mild
|
||||
# question is the expected form here.
|
||||
seam_notes.list_notes = AsyncMock(return_value=([], 2))
|
||||
from scribe.mcp.tools.tasks import add_task_log
|
||||
result = await add_task_log(task_id=70, content="progress")
|
||||
assert "no Systems yet" in result["systems_hint"]
|
||||
|
||||
Reference in New Issue
Block a user