Estimate: 22m · Depends on: 2.2.3
Ship the write surface so a project admin can edit their workflow: add/rename/recolor/reorder statuses, add/remove transitions, and flip workflow_policy_mode. Built as Server Actions following the 4-layer rule (Action → Service → Repository → Prisma) and mounted under the existing project-settings route shell from Story 1.5.
Service write methods:
createStatus({ projectId, workspaceId, key, label, category, color, position }) — inserts, validates key is project-unique, position via fractional-indexing.updateStatus({ statusId, workspaceId, label?, category?, color?, position?, isInitial? }) — partial update; flipping isInitial to true inside one tx unsets the previous initial-status row (atomic to satisfy the partial unique index from 2.2.1).deleteStatus({ statusId, workspaceId }) — refuses (throws StatusInUseError → 422) if any work_item.status still references this status key OR if it's the initial status OR if removing it would leave the project with zero category = 'done' statuses (the last terminal-status invariant). Same-tx cleanup deletes all workflow_transition rows pointing to or from this status.addTransition({ projectId, workspaceId, fromStatusId, toStatusId }) — inserts; the unique constraint from 2.2.1 makes duplicate inserts idempotent (catch P2002 → return existing).removeTransition({ transitionId, workspaceId }).setPolicyMode({ projectId, workspaceId, mode }) — flips the project's workflow_policy_mode column.Permission gate: every write method calls workspacesService.assertProjectAdmin(userId, projectId, workspaceId). v1 routes "project admin" to the workspace owner role added in Story 1.6.5 (finding #36) — full per-project RBAC is Epic 6 work. This is intentionally narrow: the durable shape is a permission check that exists; the durable scope expansion (to a real "project admin" role) is the Epic-6 add. Owner gate today, admin gate later — same gate function, swapped implementation. Not a "shortcut now, real later" — the gate is the durable shape; the role-set behind it widens in Epic 6.
UI: /settings/project/[projectKey]/workflow route under the existing project-settings shell. Two tabs: Statuses (drag-to-reorder list, inline edit, add/delete) and Transitions (a matrix grid: rows = from-status, columns = to-status, click cell to toggle). A header toggle controls policy mode (restricted vs open); a banner under the toggle explains the consequence ("Open mode: any status can transition to any other"). Uses the Story 1.5 shell primitives (Modal, Toast, Pill); no new design-system components needed beyond a color-swatch picker that composes from the existing color tokens. All actions are optimistic with toast confirmation; failures revert + toast the typed-error message.
assertProjectAdmin first and surfaces the typed-error message on failure.work_item → 422 STATUS_IN_USE; deleting the initial status → 422 CANNOT_DELETE_INITIAL_STATUS; deleting the last category = 'done' status → 422 CANNOT_DELETE_LAST_TERMINAL_STATUS./settings/project/[projectKey]/workflow route renders the two tabs, drives all six service writes through Server Actions, optimistic UI + toast confirmation, A11y (axe) sweep on the page passes.category: done), removes the back-transition in_review→in_progress, flips policy mode → restricted re-applied.AppLayout + settings-route shellJobsDashboard — closest precedent for a project-settings-style admin surface with a Server Action + permission gateWorkspaceMembershipError shapeworkspaceRolesService — the temporary "owner == admin" implementation seam