feat(rules): enter_project handshake (S4)
New enter_project(project_id) MCP tool composes get_project + get_applicable_rules + get_project_milestone_summary + recent open-tasks + recent notes into one round-trip, intended to be called at session start (or whenever the active project changes) so Claude has the full project context loaded before it starts mutating. _INSTRUCTIONS now points Claude at enter_project for project-scoped work, alongside the existing list_always_on_rules instruction. No schema change; pure composition over existing services. Closes the four-slice rules-consolidation plan (Scribe task #508): S1+S2 (always_on flag + Scribe-first prompt,658348f), S3 (project- scoped rules,43a860c), and now S4. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import pytest
|
||||
from fabledassistant.mcp._context import _user_id_ctx
|
||||
from fabledassistant.mcp.tools.projects import (
|
||||
list_projects, get_project, create_project,
|
||||
update_project,
|
||||
update_project, enter_project,
|
||||
)
|
||||
|
||||
|
||||
@@ -130,3 +130,75 @@ async def test_update_project_raises_when_not_found():
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
await update_project(project_id=999, title="x")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_composes_full_context():
|
||||
"""enter_project pulls project + rules + milestone summary + open tasks +
|
||||
recent notes in one composed call."""
|
||||
p = _fake_project(id=5, title="P")
|
||||
applicable_payload = {
|
||||
"rules": [{"id": 1, "title": "r1", "statement": "s",
|
||||
"topic_title": "t", "rulebook_title": "rb"}],
|
||||
"project_rules": [{"id": 99, "title": "pr1", "statement": "ps"}],
|
||||
"truncated": False,
|
||||
"subscribed_rulebooks": [{"id": 2, "title": "rb"}],
|
||||
}
|
||||
milestone_summary = [{"id": 10, "title": "MS", "task_count": 3}]
|
||||
|
||||
task1 = MagicMock()
|
||||
task1.id = 100; task1.title = "T1"; task1.status = "in_progress"
|
||||
task1.priority = "high"; task1.task_kind = "work"; task1.milestone_id = 10
|
||||
note1 = MagicMock()
|
||||
note1.id = 200; note1.title = "N1"
|
||||
note1.updated_at = None # avoids datetime mocking
|
||||
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.notes_svc.list_notes",
|
||||
AsyncMock(side_effect=[([task1], 1), ([note1], 1)]),
|
||||
):
|
||||
out = await enter_project(project_id=5)
|
||||
|
||||
assert out["project"]["id"] == 5
|
||||
assert out["milestone_summary"] == milestone_summary
|
||||
assert out["applicable_rules"][0]["title"] == "r1"
|
||||
assert out["project_rules"][0]["id"] == 99
|
||||
assert out["subscribed_rulebooks"] == [{"id": 2, "title": "rb"}]
|
||||
assert out["open_tasks"][0]["id"] == 100
|
||||
assert out["open_tasks"][0]["status"] == "in_progress"
|
||||
assert out["recent_notes"][0]["id"] == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_raises_when_project_not_found():
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="project 999 not found"):
|
||||
await enter_project(project_id=999)
|
||||
|
||||
|
||||
def test_enter_project_registered_in_register():
|
||||
"""register(mcp) registers enter_project alongside the existing tools."""
|
||||
from fabledassistant.mcp.tools.projects import register
|
||||
registered: list[str] = []
|
||||
|
||||
class FakeMCP:
|
||||
def tool(self, name=None):
|
||||
def decorator(fn):
|
||||
registered.append(name)
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
register(FakeMCP())
|
||||
assert "enter_project" in registered
|
||||
|
||||
Reference in New Issue
Block a user