11.7.6 The planning CONVERSATION over `/api/v1` — open/resume by scope, append one turn, submit the accumulated thread
Three endpoints that make motir plan possible over HTTP, and the only resource in this story addressed by a composite scope rather than an id.
What to build
- Open — or RESUME — the conversation for a scope (
planChangeSessionsService.getOrCreateForScope). Addressed byprojectKey+ an optional set of target keys, never by a session id: re-opening the same anchor set returns the SAME row the web panel is looking at, which is what makes it impossible to fork a second conversation about one set of items. Opening submits nothing and costs nothing. Scope:read. - Append ONE turn (
planChangeSessionsService.appendTurn). Appending is not submitting — the turn is persisted the moment this returns, so quitting can never lose it, and no job starts, no credits are spent, and no work item changes. Scope:work_items:write. - Submit the accumulated thread (
planChangeSessionsService.submit) — every turn, in order, as ONE change, returning the job handle. A thread with no turns is refused by the server; a failed submit leaves the thread intact. Scope:work_items:write.
The composite address is this card's real design problem
A plan session is identified by a project plus an optional ordered anchor set, which has no natural path form. 11.7.1 Q1 pins where that address travels — follow it. Two properties it has to preserve whatever the shape:
- Same anchor set ⇒ same session. Two clients naming the same items must land on one thread, or the "you cannot fork a conversation about these items" guarantee is gone and the web panel and the API are talking past each other.
- Anchor-set identity is not order-sensitive if the service says it is not — check
getOrCreateForScoperather than assuming either way, because a client that sorts its keys and one that does not must not get different threads.
Scope BOUNDARY
Ends at these three endpoints. It does NOT ship expansion or the plan reads (11.7.5) — a submitted session and a submitted expansion both produce a Plan, and the plan reads serve both. It does NOT ship session close-out (11.7.4) — an unrelated meaning of "session". It does NOT change how a thread accumulates, what the planner does with it, or the approval gate. It does NOT expose editing or deleting a turn — the shipped service offers no such operation and inventing one is a feature, not a transport.
Acceptance criteria
- All three endpoints exist, are declared, appear in the emitted spec, and carry their MCP counterparts' scopes from
lib/mcp/scopes.ts— asserted against that map. - Opening the same anchor set twice returns the SAME session, and opening it from the API returns the same row the web panel resolves for that scope — asserted across both surfaces, since that identity is the guarantee.
- Anchor-set identity behaves exactly as
getOrCreateForScopedefines it with respect to ordering and duplicates — asserted against the service rather than assumed. - Appending a turn persists it and starts NO job, spends NO credits and changes NO work item — asserted by inspecting the job queue and the item table after an append.
- Submitting sends every turn in order and returns the job handle without waiting for the planner.
- A submit on an empty thread is refused with a mapped status, not a 500.
- A failed submit leaves the thread intact and re-submittable — asserted by failing the planner and re-reading the session.
- Every domain error the service raises — including the too-many-targets and turn-conflict cases — has a deliberate row in the v1 status map, proven by a test that drives it.
- All three payloads match their MCP counterparts field for field.
- Cross-tenant keys return 404; a token lacking the mapped scope is refused 403.
- The per-file coverage floor (≥90%) holds on every new file.
Context refs
- 11.7.1 Q1 — where the composite scope address travels.
- 11.7.3 — the schema module, including the plan-session and turn shapes.
lib/services/planChangeSessionsService.ts—getOrCreateForScope,appendTurn,submit.lib/mcp/tools/planSession.ts— the scope-building logic (buildScope,PROJECT_SCOPE) and the append-is-not-submit contract. Read, not imported.lib/dto/planChange.ts— the thread and turn shapes mapped FROM.lib/planChange/errors.ts—PlanChangeSessionNotFoundError,PlanChangeTurnConflictError,TooManyPlanChangeTargetsError: three of the errors the status map must place deliberately.tests/mcp/plan-session.test.ts— the shipped behaviour to agree with.- Story: 11.7.