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
Step 1 of milestone 409 "Response shapes". An agent reporting finished work
is asked to say which milestone it belongs to, which step of how many, and
what is next. Without those facts to hand it reconstructs them, and a
reconstruction reads exactly like the truth when it is wrong.
create_task and update_task (MCP) and the REST create/update task routes now
return a placement block: project; and for a task in a milestone, the
milestone, position {step, of}, progress {completed, total, pct} and next
(the next open step, falling back to the earliest open one before it).
- Step order is creation order, not get_milestone listing order, which
reshuffles on every update.
- Siblings are read through readable_notes_clause; position and progress
are computed over that same readable set, so a collaborator is never shown
a step title they cannot open, and a note share alone reveals no plan.
- Fail-open and omitted when empty, like every in-band decoration.
- _no_embedding moves into conftest as one opt-in fixture for both
integration modules that need it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>