Feature #1523
closedFeature #1510: Implement internal user roles with ticket progression and assignment controls
Redmine port + adapter: write ticket assignee, with clean upstream-error mapping
0%
docs/superpowers/specs/2026-07-31-internal-roles-design.md
Description
**Surface:** backend-only
There is no assignee support anywhere in the codebase today. Add it end to end: `assigned_to_id` / `assigned_to_name` on `RedmineIssue` parsed from the issue JSON's `assigned_to: {id, name}` sub-object exactly as `author` is; `assigned_to_id` on `RedminePort.update_issue()` and the HTTP adapter; the assignee carried onto `TaskDetail` via `_to_detail`. Add the Redmine user lookup used later for admin-time validation.
The point of this subtask is that **Redmine saying no is a first-class, mapped outcome**, not a 500.
Per spec decision 7, `TaskDetailOut` reveals the assignee only to a caller whose `internal_role` is cto/developer or who is `is_admin`; every other caller gets nulls.
## Acceptance criteria
- [ ] `RedmineIssue` gains `assigned_to_id: int | None` and `assigned_to_name: str`; a payload with no `assigned_to` key yields `None` / `""` and no exception.
- [ ] `update_issue()` gains a keyword-only assignee argument; the adapter includes `issue[assigned_to_id]` **only** when asked, so payloads emitted by every existing caller are byte-identical (respx asserts the exact body).
- [ ] Unassigning is supported and distinguishable from "do not touch": a sentinel (not `None`) clears the assignee, emitting Redmine's empty-string form. respx asserts all three bodies — set, clear, absent.
- [ ] Port gains a Redmine user lookup; no Redmine call is added anywhere outside the adapter.
- [ ] **Upstream rejection: Redmine returns 422 (unknown user id, or a user without access to the project) → a named domain error (`InvalidRedmineAssignee`) is raised and mapped to a 4xx with an actionable message — never an unhandled exception, never a 500.** The test asserts the status code and that no traceback or upstream body leaks into the response.
- [ ] 404 (issue gone) → 404; 5xx / timeout → mapped upstream error with a stable message.
- [ ] `TaskDetail` carries the assignee; `GetTask` needs no signature change.
- [ ] `TaskDetailOut` populates the assignee for cto/developer/admin callers and nulls it for an ordinary client — one integration test per caller type.
- [ ] The board listing payload is unchanged, so no assignee leaks to clients through the list.
- [ ] `python -m pytest -q` (serial), `ruff check .`, `mypy src` green.
RA Updated by Redmine Admin 4 days ago
- Status changed from Backlog to Submitted
- spec_ref updated (diff)
RA Updated by Redmine Admin 4 days ago
- Status changed from Submitted to Shipped
Shipped to production in master build #107 (2026-07-31). Assignee read/write live end to end, with the UNASSIGN sentinel distinguishing "clear it" from "don't touch it", and Redmine's rejection mapped to a clean 422 rather than a 500.