feat(placement): a record you only cite carries its status (#4154)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Failing after 1m4s
CI & Build / Build & push image (push) Skipped

Step 1 made placement cheap for a task whose status CHANGES: create_task
and update_task return where it sits, and the report is written from
that. It did nothing for a task a reply merely cites.

This milestone's own step-6 review reported "#4014 is the open step of
milestone 409". #4014 had been done for four days; the open step was
#4015. The id did not come from a read — it came from a retrieval hint,
which carries an id, a kind and a title and says nothing about status,
while list_milestones said "8 of 9" and would not say which one. The
gap was there to be filled and the nearest-looking id filled it.

Two surfaces, one principle: the status arrives with the id.

1. get_project_milestone_summaries gains next_step — the earliest open
   step, {id, title, status} or None — carried through _BRIEF_FIELDS to
   enter_project, get_project and list_milestones. One extra flat query
   for the whole batch, so #2384's fan-out does not come back.

   OPEN_STEP_STATUSES moves to services/milestones.py and placement.py
   imports it; both surfaces now answer "what is next" and must not
   drift on what counts as open. Both step queries take the same
   readable_notes_clause (rule 78), so a row cannot name a step its own
   progress numbers exclude.

2. _record_kind renders a task's status: [task (done)], [issue (todo)].
   A finished step and an open one read identically before, which is
   exactly the line the misreport was taken from. Only tasks — is_task
   IS status-is-not-None on the model, so there is no fallback branch.

reporting-back gains the practice, owned and registered in the guidance
ownership table: a record you only mention is a record to read.

The guards are structural and each fails on the regression it names:
the query count is asserted rather than the payload shape, and the two
surfaces' agreement is pinned on the rendered ORDER BY, since a mocked
session hands back whatever order the test chose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-18 12:08:00 -04:00
co-authored by Claude Opus 5
parent b5df9d6dca
commit 5c6175ad97
13 changed files with 440 additions and 29 deletions
+12 -5
View File
@@ -77,9 +77,15 @@ async def enter_project(project_id: int) -> dict:
`milestone_summary` is the 5 most recently touched milestones, any status,
most recent first. Touched counts a step changing, not only the milestone
itself. Each carries its description and progress but NOT its plan:
get_milestone(id) reads a plan and its steps. `milestone_summary_omitted`
says how many others exist; list_milestones lists them all.
itself. Each carries its description, its progress and its `next_step` —
the earliest step still open, {id, title, status}, or null when none is —
but NOT its plan: get_milestone(id) reads a plan and its steps.
`milestone_summary_omitted` says how many others exist; list_milestones
lists them all.
Name the next step from `next_step`, not from recall: progress alone says
a plan has an open step and not which, and a step named from memory reads
exactly like one that was read.
`unplanned_milestones` is the active milestones that have NO steps yet,
in roadmap order (up to 10; `unplanned_milestones_omitted` counts the
@@ -290,8 +296,9 @@ async def get_project(project_id: int) -> dict:
"""Fetch a Scribe project by ID.
Returns full project fields, a milestone_summary list (every milestone,
with description and progress but no plan body; get_milestone reads a
plan), the project's own rules (project_rules), and applicable_rules: the
with description, progress and `next_step` — the earliest still-open step,
or null — but no plan body; get_milestone reads a plan), the project's own
rules (project_rules), and applicable_rules: the
global rules tagged to an area this project works in. Every other global
rule applies too and arrives by retrieval when the work matches it.
"""