The new fixtures fed (milestone_id, status, count) and the query also
carries max(updated_at), which last_touched_at is computed from. Six
tests in the new module died unpacking it; the product path was never
reached, and the rest of the suite was green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
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