CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 18s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 25s
Step 4 of #278, product half. The audit that motivated it: one System in project 2, thirty records tagged, nothing since July 28 — three days after the feature landed. Not a discipline failure; retrieval was completely blind to the association (zero references in embeddings, knowledge, search, auto-inject, or enter_project), so tagging was a write-side label with no read-side payoff, and labels nobody reads don't get maintained. Three changes, ordered by what makes the others workable: 1. enter_project returns the project's Systems (id, name, first line of the charter). Load-bearing for the tagging instruction: you cannot ask an agent to check a record against a vocabulary it never sees. Trimmed because it rides on every session start; the full charter stays get_system's job. Present-and-empty rather than absent when a project has none — "no named areas yet" is information the create-the-System instruction acts on. 2. search accepts system_id, MCP and REST (#33). Implemented once in semantic_search_notes as an EXISTS against record_systems — an association filter deciding candidate-set membership before scoring, like project_id, not a ranking signal. The REST route's missing project filter stays #2463's: it carries a default-scope UI decision this change must not preempt. 3. The instructions (#119, _INSTRUCTIONS + using-scribe skill; plugin 0.1.25 for the cache): - Tag as you write, with an executable test — "would someone investigating that subsystem want this in the pile list_system_records returns?" — rather than "tag appropriately", which is what died. - Create the System when the area has no record: the two-or-more test snippets use, plus "don't wait to be asked to name an area that plainly exists", because the agent's default was leaving un-modelled areas un-modelled forever. - State vs chronicle: dev-logs are written once and never rewritten; durable findings live in the System's reference note, updated in place — safe because note versions are the changelog, which has existed since the feature shipped and was never named as one. list_system_records' docstring now sells it as the way to READ a subsystem, reference note first. No auto-inject boost by System — vocabulary and filter first, measure before adding ranking behaviour (the #2486 lesson). Refs #278, #2546
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
"""System CRUD + record-association MCP tools — wrappers over services/systems.py.
|
|
|
|
A System is a per-project, reusable, self-describing subsystem/area that any
|
|
record (note, task, or issue) can be associated with — so research, build-work,
|
|
and corrective work line up under the same area and recurring problem-spots are
|
|
visible. The service enforces the multi-user ACL (project permission); these
|
|
tools are thin wrappers.
|
|
|
|
Sentinels (match the milestone/task tool conventions):
|
|
- name="" / description="" / color="" / status="" → "leave unchanged" on update
|
|
- order_index=-1 → "leave unchanged" on update (0 is a valid order_index)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from scribe.mcp._context import current_user_id
|
|
from scribe.services import systems as systems_svc
|
|
|
|
|
|
async def create_system(
|
|
project_id: int,
|
|
name: str,
|
|
description: str = "",
|
|
color: str = "",
|
|
) -> dict:
|
|
"""Create a System (a reusable, self-describing subsystem/area) in a project.
|
|
|
|
Associate records with it via the `system_ids` arg on create/update_task and
|
|
create/update_note.
|
|
|
|
Args:
|
|
project_id: The project this system belongs to (required).
|
|
name: Short label (required).
|
|
description: What the system is and how it's used — a name is rarely enough.
|
|
color: Optional UI accent (hex), or empty.
|
|
"""
|
|
uid = current_user_id()
|
|
system = await systems_svc.create_system(
|
|
uid, project_id=project_id, name=name,
|
|
description=description or None, color=color or None,
|
|
)
|
|
if system is None:
|
|
raise ValueError(f"cannot create system in project {project_id} (no write access)")
|
|
return system.to_dict()
|
|
|
|
|
|
async def list_systems(project_id: int, include_archived: bool = False) -> dict:
|
|
"""List a project's systems (active by default), ordered by order_index.
|
|
|
|
Pass include_archived=True to include archived systems.
|
|
"""
|
|
uid = current_user_id()
|
|
rows = await systems_svc.list_systems(uid, project_id, include_archived=include_archived)
|
|
return {"systems": [s.to_dict() for s in rows]}
|
|
|
|
|
|
async def get_system(system_id: int) -> dict:
|
|
"""Fetch a System plus the records associated with it.
|
|
|
|
Returns the system, plus its associated records split into `issues`,
|
|
`tasks` (work/plan), and `notes`.
|
|
"""
|
|
uid = current_user_id()
|
|
system = await systems_svc.get_system(uid, system_id)
|
|
if system is None:
|
|
raise ValueError(f"system {system_id} not found")
|
|
records = await systems_svc.list_records_for_system(uid, system_id)
|
|
issues, tasks, notes = [], [], []
|
|
for r in records:
|
|
d = r.to_dict()
|
|
if r.status is None:
|
|
notes.append(d)
|
|
elif r.task_kind == "issue":
|
|
issues.append(d)
|
|
else:
|
|
tasks.append(d)
|
|
data = system.to_dict()
|
|
data["issues"] = issues
|
|
data["tasks"] = tasks
|
|
data["notes"] = notes
|
|
return data
|
|
|
|
|
|
async def update_system(
|
|
system_id: int,
|
|
name: str = "",
|
|
description: str = "",
|
|
color: str = "",
|
|
status: str = "",
|
|
order_index: int = -1,
|
|
) -> dict:
|
|
"""Update a System. Only explicitly provided fields change.
|
|
|
|
Args:
|
|
status: 'active' or 'archived'. Archive a system to retire it without
|
|
losing history; archived systems hide from default lists.
|
|
order_index: display position (0-based); -1 = leave unchanged.
|
|
"""
|
|
uid = current_user_id()
|
|
fields: dict = {}
|
|
if name:
|
|
fields["name"] = name
|
|
if description:
|
|
fields["description"] = description
|
|
if color:
|
|
fields["color"] = color
|
|
if status:
|
|
fields["status"] = status
|
|
if order_index >= 0:
|
|
fields["order_index"] = order_index
|
|
system = await systems_svc.update_system(uid, system_id, **fields)
|
|
if system is None:
|
|
raise ValueError(f"system {system_id} not found or no write access")
|
|
return system.to_dict()
|
|
|
|
|
|
async def list_system_records(
|
|
system_id: int, kind: str = "", open_only: bool = False
|
|
) -> dict:
|
|
"""Everything filed under one System — the way to READ a subsystem.
|
|
|
|
Reach for this when investigating a specific area: it returns the notes,
|
|
tasks, issues and snippets someone deliberately tagged to it — the
|
|
subsystem's accumulated record, unranked. Start with its reference note if
|
|
one exists (titled "«System» — reference"); that is the living state, and
|
|
the rest is history and open work around it. For a ranked cut of the same
|
|
slice, search(system_id=...) filters semantic search to this association.
|
|
|
|
Args:
|
|
kind: filter by task_kind — 'issue', 'work', or 'plan'. Omit for all.
|
|
open_only: limit to tasks not done/cancelled (e.g. open issues only).
|
|
"""
|
|
uid = current_user_id()
|
|
rows = await systems_svc.list_records_for_system(
|
|
uid, system_id, kind=kind or None, open_only=open_only,
|
|
)
|
|
return {"records": [r.to_dict() for r in rows]}
|
|
|
|
|
|
async def delete_system(system_id: int) -> dict:
|
|
"""Soft-delete a System (recoverable). Its record associations are removed."""
|
|
uid = current_user_id()
|
|
ok = await systems_svc.delete_system(uid, system_id)
|
|
if not ok:
|
|
raise ValueError(f"system {system_id} not found or no write access")
|
|
return {"message": f"System {system_id} deleted."}
|
|
|
|
|
|
def register(mcp) -> None:
|
|
for fn in (
|
|
create_system,
|
|
list_systems,
|
|
get_system,
|
|
update_system,
|
|
list_system_records,
|
|
delete_system,
|
|
):
|
|
mcp.tool(name=fn.__name__)(fn)
|