fix(mcp): project reads list milestones without their plans, and cap done ones (#4045)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m0s
CI & Build / Python tests (push) Successful in 1m32s
CI & Build / Build & push image (push) Successful in 30s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m0s
CI & Build / Python tests (push) Successful in 1m32s
CI & Build / Build & push image (push) Successful in 30s
enter_project returned every milestone's full plan body. On a project with 39 milestones the handshake came to ~222k characters, 168k of them bodies (110k from done milestones). That is past what an MCP client accepts as a tool result, so the call meant to orient a session arrived as a file to page through. It grows with a project's history, so any long-lived project on any install gets there. - brief_milestone_summary (services/milestones.py) trims summary rows to the listing fields: id, title, description, status, order_index and progress. The plan is get_milestone's job. user_id, project_id and timestamps repeat what the caller knows. - enter_project and get_project share one block: every open milestone plus the 5 most recently updated done ones, in order. milestone_summary_omitted is attached only when older done ones were left out, and names list_milestones and get_milestone. - list_milestones lists every milestone, done included, without bodies. It is the call the omitted line points to, and it had the same size problem. - The REST project summary is unchanged; the web UI reads it. Tests: trimming, the done cap and its order, the omitted key present and absent, get_project and list_milestones, and a size ceiling on enter_project's milestone block for a 200-milestone history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -42,6 +42,32 @@ async def list_projects() -> dict:
|
||||
return {"projects": [p.to_dict() for p in rows]}
|
||||
|
||||
|
||||
# Done milestones a project read still lists: the recent ones say what just
|
||||
# finished, and older ones are a list_milestones call away (#4045).
|
||||
_DONE_MILESTONES_KEPT = 5
|
||||
|
||||
|
||||
async def _milestone_block(uid: int, project_id: int) -> dict:
|
||||
"""`milestone_summary` for a project read, brief and bounded (#4045).
|
||||
|
||||
Every open milestone plus the most recent done ones, without plan bodies.
|
||||
`milestone_summary_omitted` is attached only when older done milestones
|
||||
were left out, and names the calls that reach them (#2483).
|
||||
"""
|
||||
rows = await milestones_svc.get_project_milestone_summary(uid, project_id)
|
||||
brief, omitted = milestones_svc.brief_milestone_summary(
|
||||
rows, done_kept=_DONE_MILESTONES_KEPT,
|
||||
)
|
||||
out: dict = {"milestone_summary": brief}
|
||||
if omitted:
|
||||
out["milestone_summary_omitted"] = (
|
||||
f"{omitted} older done milestone(s) not listed. "
|
||||
f"list_milestones({project_id}) lists every milestone; "
|
||||
"get_milestone(id) has one milestone's plan and steps."
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
async def enter_project(project_id: int) -> dict:
|
||||
"""Session-start handshake: load full context for working on a project.
|
||||
|
||||
@@ -63,6 +89,11 @@ async def enter_project(project_id: int) -> dict:
|
||||
open_tasks, recent_notes, design_system, systems, pattern_coverage —
|
||||
plus systems_bootstrap, present only when it applies (see below).
|
||||
|
||||
`milestone_summary` lists every open milestone and the most recently
|
||||
finished done ones, each with its description and progress but NOT its
|
||||
plan: get_milestone(id) reads a plan. `milestone_summary_omitted` appears
|
||||
only when older done milestones were left out, and says how many.
|
||||
|
||||
`pattern_coverage` (usually null) is the shape-accounting line — how many
|
||||
of the bound repo's extracted shapes carry a classification against canon
|
||||
(note 2786) — e.g. "shape accounting: 3100/4573 shapes accounted for —
|
||||
@@ -120,9 +151,7 @@ async def enter_project(project_id: int) -> dict:
|
||||
applicable = await rulebooks_svc.get_applicable_rules(
|
||||
project_id=project_id, user_id=uid,
|
||||
)
|
||||
milestone_summary = await milestones_svc.get_project_milestone_summary(
|
||||
uid, project_id,
|
||||
)
|
||||
milestones = await _milestone_block(uid, project_id)
|
||||
open_tasks, _ = await notes_svc.list_notes(
|
||||
uid, is_task=True, project_id=project_id,
|
||||
status=["todo", "in_progress"], sort="updated_at", limit=10,
|
||||
@@ -206,7 +235,7 @@ async def enter_project(project_id: int) -> dict:
|
||||
for s in systems
|
||||
],
|
||||
"design_system": design_system,
|
||||
"milestone_summary": milestone_summary,
|
||||
**milestones,
|
||||
**rulebooks_svc.rules_payload(applicable, user_id=uid, source="enter_project"),
|
||||
"open_tasks": [
|
||||
{
|
||||
@@ -236,8 +265,10 @@ async def enter_project(project_id: int) -> dict:
|
||||
async def get_project(project_id: int) -> dict:
|
||||
"""Fetch a Scribe project by ID.
|
||||
|
||||
Returns full project fields, a milestone_summary list, and the
|
||||
rulebook-applicable_rules / subscribed_rulebooks pair the assistant
|
||||
Returns full project fields, a milestone_summary list (shaped as in
|
||||
enter_project: open milestones and the recent done ones, no plan bodies,
|
||||
with milestone_summary_omitted when older done ones were left out), and
|
||||
the rulebook-applicable_rules / subscribed_rulebooks pair the assistant
|
||||
should consult when working on this project.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
@@ -245,9 +276,7 @@ async def get_project(project_id: int) -> dict:
|
||||
if project is None:
|
||||
raise ValueError(f"project {project_id} not found")
|
||||
data = project.to_dict()
|
||||
data["milestone_summary"] = await milestones_svc.get_project_milestone_summary(
|
||||
uid, project_id,
|
||||
)
|
||||
data.update(await _milestone_block(uid, project_id))
|
||||
applicable = await rulebooks_svc.get_applicable_rules(
|
||||
project_id=project_id, user_id=uid,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user