Feature #1524
closedFeature #1510: Implement internal user roles with ticket progression and assignment controls
Role-gated transition on REST + MCP, and the MCP capability-parity fix
0%
docs/superpowers/specs/2026-07-31-internal-roles-design.md
Description
**Surface:** backend-only
Wire the target-aware predicate into the REST transition route and the MCP `transition_ticket` tool so both planes enforce the identical rule, and close the standing MCP contract breach: an ordinary client can no longer transition a ticket at all.
`TransitionTask` takes the caller (client id, `internal_role`, `is_admin`), resolves the ticket's Redmine assignee back to a dashboard client via `clients.redmine_user_id`, and raises a new `ProgressionForbidden` when the predicate refuses. The route maps it to 403; the MCP handler maps it to a ToolError.
**The credit gate is untouched:** `BacklogNotSubmitted` still runs FIRST, before any role evaluation, and Backlog(16) remains sealed in both directions for everyone.
Also enriches `TaskDetailOut` with `next_status_id` and `can_move_forward`, computed server-side from the domain predicates. (`can_assign` belongs to the assign subtask, not here.)
## Acceptance criteria
- [ ] The route resolves the caller's role, admin flag and assignee-ness and calls the predicate **with the requested target status**; a refusal is a 403 whose message says what the caller may do instead.
- [ ] **Developer picks a non-next status → refused**: an assigned developer requesting anything other than `next_status(current, allowed)` (backward, a skip, or Shipped(13)) gets 403 and **no PUT is issued to Redmine** — asserted for at least three distinct non-next targets.
- [ ] Assigned developer requesting exactly the next status → 200 and the ticket moves.
- [ ] Developer who is not the assignee → 403 even for the next status; an unassigned ticket likewise.
- [ ] cto and admin can set any target in `allowed_statuses`, including backward → 200.
- [ ] cto who is not a project member still gets the unchanged 403 from `owned_project` — the tenant boundary did not widen.
- [ ] **HARD:** a Backlog(16) ticket raises `BacklogNotSubmitted` before any role evaluation — asserted for CTO, admin, assigned Developer and ordinary client, on REST (409) **and** through MCP. A test pins the guard ordering.
- [ ] **HARD:** no path introduced here moves a ticket out of status 16; `submit_ticket` remains the only door and `ticket_charges` is untouched by every test in this subtask.
- [ ] MCP `transition_ticket` called by an ordinary client → ToolError that **names the remaining doors explicitly** (`submit_ticket`, reopen, comment, pause, edit), not a bare "forbidden". Old behaviour removed outright: no flag, no grace path.
- [ ] A shared parameterised test table drives both the REST and the MCP tests so the two planes cannot drift.
- [ ] The `transition_ticket` tool description **and** the capability-parity claim in `docs/superpowers/specs/2026-07-22-client-mcp-server-design.md` are updated in the same change.
- [ ] Regression test: an ordinary client's full remaining verb set (list/read, create, edit, comment, reopen, pause, submit) still works end to end.
- [ ] Before merge, the MCP request logs are grepped for ordinary-client `transition_ticket` calls; the result is recorded in the PR. If a tenant is using it, a one-line release note ships with the deploy.
- [ ] The progression rule appears in no `application` or `presentation` module — only `domain/tasks/progression.py` encodes it.
- [ ] `TaskDetailOut` gains `next_status_id: int | null` and `can_move_forward: bool`.
- [ ] `python -m pytest -q` (serial), `ruff check .`, `mypy src` green.