feat(mcp): enter_project names active milestones with no steps as open work (#4076)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / integration (push) Canceled after 42s
CI & Build / TypeScript typecheck (push) Canceled after 42s
CI & Build / Python tests (push) Canceled after 47s

A plan written as a milestone with a description and no steps was invisible to
the session handshake: it lists the 5 most recently touched milestones (#4045),
and touching is a step changing, so a step-less milestone can never qualify.
FabledLibrarian's roadmap (nine such milestones) sat unseen while later plans
were opened as new milestones beside the ones that already described them.

enter_project adds `unplanned_milestones`: active milestones with no steps, in
roadmap order, id/title/description, up to 10 with an omitted count, none
repeated from the recent list, and absent when there are none. The docstring
says what they are for: check them before starting a new milestone, and add
steps to a match with create_records(milestone_id=...).

Milestone 415 step 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-15 13:28:05 -04:00
co-authored by Claude Opus 5
parent ac01eee040
commit 184a3e026d
3 changed files with 105 additions and 1 deletions
+26
View File
@@ -283,3 +283,29 @@ def brief_milestone_summary(
)[:limit]
brief = [{k: r[k] for k in _BRIEF_FIELDS if k in r} for r in kept]
return brief, len(rows) - len(kept)
def unplanned_milestones(
rows: list[dict], *, exclude_ids: set[int] = frozenset(), limit: int | None = None,
) -> tuple[list[dict], int]:
"""Active milestones with no steps yet, as (rows, omitted).
A plan written as a milestone with a description and no steps is open work
that nothing else names. It is never "touched" — touching is a step
changing — so the recency list that brief_milestone_summary(limit=) builds
can never reach it, and progress reads 0% either way. A project whose
roadmap was written that way ended up with every later plan opened as a
new milestone beside the one that already described it (milestone 415).
`exclude_ids` drops milestones a caller already listed. Kept in roadmap
order (order_index, then creation), the order they were written in.
Rows are id, title and description: what a reader needs to recognise the
plan, and not its body, which get_milestone reads.
"""
found = [
{"id": r["id"], "title": r.get("title"), "description": r.get("description")}
for r in rows
if r.get("status") == "active" and not r.get("total") and r["id"] not in exclude_ids
]
kept = found if limit is None else found[:limit]
return kept, len(found) - len(kept)