feat(issues): S2 MCP tools — system CRUD + issue/system wiring
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 1m3s

Second slice of Issues + Systems (spec #825).

New mcp/tools/systems.py: create_system, list_systems, get_system (records
split into issues/tasks/notes), update_system (incl. archive via status),
list_system_records (kind/open_only filters), delete_system. Registered in
register_all; read tools (get_system, list_systems, list_system_records) added
to the read-only-key allowlist (write tools default-deny).

create_task/update_task: kind now accepts 'issue'; new system_ids (set-semantics
associations) and arose_from_id (provenance, 0=unchanged/-1=clear) args.
create_note/update_note: new system_ids arg (notes associate with systems too).
services/notes.create_note: arose_from_id passthrough (update_note already
handles it via setattr).

Tests: MCP system tools + create_task issue-wiring (kind/provenance/systems),
service layer mocked.

Refs plan 825 (S2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 23:07:37 -04:00
parent b91c447b0b
commit 85e0501705
7 changed files with 291 additions and 7 deletions
+38 -4
View File
@@ -22,6 +22,7 @@ from scribe.mcp._context import current_user_id
from scribe.services import notes as notes_svc
from scribe.services import planning as planning_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from scribe.services import task_logs as task_logs_svc
from scribe.services import trash as trash_svc
@@ -41,7 +42,7 @@ async def list_tasks(
whenever a project is in scope so you list that project's tasks, not
every project's. 0 = no filter (all projects — use only for a
deliberate cross-project view).
kind: Filter by task kind — 'work' or 'plan'. Omit (empty) for all kinds.
kind: Filter by task kind — 'work', 'plan', or 'issue'. Omit (empty) for all kinds.
Results are ordered by last-updated descending.
"""
@@ -101,6 +102,8 @@ async def create_task(
parent_id: int = 0,
tags: list[str] | None = None,
kind: str = "work",
system_ids: list[int] | None = None,
arose_from_id: int = 0,
) -> dict:
"""Create a new task in Scribe.
@@ -113,7 +116,13 @@ async def create_task(
milestone_id: Place within a project milestone (0 = no milestone).
parent_id: Make this a sub-task of another task (0 = top-level).
tags: List of plain-string tags without # prefix.
kind: 'work' (default) or 'plan'. Prefer the start_planning tool to create plans.
kind: 'work' (default), 'plan', or 'issue'. An issue is corrective work —
a problem you fixed or are fixing; record symptom → root cause → fix
in the body. Prefer start_planning to create plans.
system_ids: Ids of the project's Systems (reusable subsystem/area
objects; see list_systems / create_system) to associate this task with.
arose_from_id: For an issue, the id of the task/feature it arose from
(provenance). 0 = none.
"""
uid = current_user_id()
note = await notes_svc.create_note(
@@ -127,8 +136,16 @@ async def create_task(
parent_id=parent_id or None,
tags=tags,
task_kind=kind,
arose_from_id=arose_from_id or None,
)
return note.to_dict()
if system_ids:
await systems_svc.set_record_systems(uid, note.id, system_ids)
data = note.to_dict()
if system_ids:
data["systems"] = [
s.to_dict() for s in await systems_svc.list_record_systems(uid, note.id)
]
return data
async def update_task(
@@ -139,6 +156,8 @@ async def update_task(
priority: str = "",
project_id: int = 0,
milestone_id: int = 0,
system_ids: list[int] | None = None,
arose_from_id: int = 0,
) -> dict:
"""Update an existing Scribe task. Only explicitly provided fields are changed.
@@ -154,6 +173,10 @@ async def update_task(
its project; also clears the milestone), positive = set.
milestone_id: New milestone. 0 = leave unchanged, -1 = clear (remove
from its milestone), positive = set.
system_ids: Replace this task's System associations with these ids
(set-semantics). None = leave unchanged; [] = clear all.
arose_from_id: Provenance (issue → originating task). 0 = leave unchanged,
-1 = clear, positive = set.
"""
uid = current_user_id()
fields: dict = {}
@@ -175,10 +198,21 @@ async def update_task(
fields["milestone_id"] = None
elif milestone_id:
fields["milestone_id"] = milestone_id
if arose_from_id == -1:
fields["arose_from_id"] = None
elif arose_from_id:
fields["arose_from_id"] = arose_from_id
note = await notes_svc.update_note(uid, task_id, **fields)
if note is None:
raise ValueError(f"task {task_id} not found")
return note.to_dict()
if system_ids is not None:
await systems_svc.set_record_systems(uid, task_id, system_ids)
data = note.to_dict()
if system_ids is not None:
data["systems"] = [
s.to_dict() for s in await systems_svc.list_record_systems(uid, task_id)
]
return data
async def add_task_log(task_id: int, content: str) -> dict: