Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2297Done

`workflow:manage` + `automation:manage` — the statuses a column projects, the automation rules, and the status-derivation switches

⚠️ Re-scoped 2026-08-06 against the branch — the workflow half of this card was BACKWARDS

The card was written from the guard's output. It claimed the workflow Server Actions have "no gate at all" and that /api/board/columns/[columnId]/statuses reaches boardsService.mapStatusToColumn "with only a session", concluding that "today any signed-in workspace member can delete a workflow status." Read on the branch, that is false and the opposite is true.

PathReal gate todayAdmits
every workflowsService status / transition writeassertProjectAdmin (workflowsService.ts:47)isOwnerRole(...)workspace OWNER only
boardsService.mapStatusToColumn / unmapStatusassertBoardConfigAdmin (boardsService.ts:1138)isOwnerRole(...)workspace OWNER only
automationRulesService / projectStatusAutomationService writesprojectAccessService.assertCanManageproject:administerunchanged by the split

The Server Actions are transport-only, exactly as their own header says; the gate is one layer down, and it is TIGHTER than project:administer. The guard could not see either helper because its GATE pattern is a whitelist of names — MOTIR-2304 repairs it and this card is blocked_by it.

So this card is TWO different changes, and they must be argued separately:

  • workflow:manage LOOSENS. Statuses, transitions and column→status mapping move from workspace-OWNER-only to a key a workspace admin and a project admin also hold. That is a real grant to real actors. It is also required: MOTIR-2293 already put workflow:manage in BUILTIN_ROLE_PERMISSIONS.admin, so leaving the gate where it is would have the catalog advertise a permission the code refuses — the lie the model exists to prevent.
  • automation:manage is NEUTRAL. Both automation services already ask project:administer, so re-pointing them changes nothing for anyone. The card's original text is right about this half.

The catalog's workflow domain holds two keys and they are wired together because they are one settings page and one mental model — how work moves through this project.

workflow:manage — the statuses themselves and the columns they project into: workflowsService's status / transition writes (driven by app/(authed)/settings/project/workflow/actions.ts, which is transport-only) and boardsService.mapStatusToColumn / unmapStatus. A workflow status is a project-wide contract — every work item's state, every board column, every automation rule and every ready-set computation reads it — and today only the workspace OWNER may touch one, which is why a project admin cannot set up their own project's workflow.

automation:manageautomationRulesService (rules CRUD, enable/disable, executions) and projectStatusAutomationService (the parent-rollup and child-cascade switches). These are the closest to a pure re-pointing in the story: projectStatusAutomationService already asserts assertCanManage in three places, and the automation routes are the ones the settings nav already gates on manage.

The automation nav entry is the one exception to the story's hands-off rule for projectSettingsNav.ts. It is the only entry whose access predicate is manage rather than browse. Since Admin holds automation:manage exactly when it holds project:administer, moving that entry is behaviour-neutral — but the nav's SettingsNavCapabilities shape is a client-side contract that MOTIR-2258 is about to redesign around a resolved permission SET. So leave the nav entry alone and record why: changing one predicate now buys nothing and collides with that story. The page's own server guard is a different matter and is in scope.

Acceptance criteria

  • Enumerate first, against the REPAIRED guard (MOTIR-2304 is blocked_by): list its workflow:manage and automation:manage operations, and git grep -n assertCanManage -- lib/services/automationRulesService.ts lib/services/projectStatusAutomationService.ts lib/services/workflowsService.ts app/\\(authed\\)/settings/project/workflow. The PR body carries operation / gate today / gate after.
  • Every status and transition WRITE in workflowsService asserts workflow:manage through projectAccessService.assertPermission, and the module-private assertProjectAdmin is DELETED — it is one of the private re-implementations of the access policy this story exists to end, and it is the one that decides a DIFFERENT answer. The Server Actions stay transport-only, matching their own header; a comment there records that the gate is one layer down, so a future reader does not re-add a second one.
  • NotProjectAdminError from lib/workflows/errors.ts: git grep -n "NOT_PROJECT_ADMIN" -- app lib components tests e2e FIRST — app/(authed)/settings/project/workflow/actions.ts and tests/components/components-settings-editor.test.tsx read that string, and MOTIR-2293 kept it reaching them deliberately. Say in the PR what each consumer receives after; a workflow:manage refusal is PERMISSION_DENIED, so either those consumers are updated or the compatibility is preserved explicitly.
  • The WIDENING is asserted, not just performed. A test proves the new actor set: a project admin can now create and delete a status and map a column (they could not before this card); a workspace admin can too; a project member and viewer are refused 403. The project-admin case is the one that changes, so it is the one that must be named.
  • boardsService.mapStatusToColumn and its unmap sibling assert workflow:manage — the mapping is a workflow decision surfaced on a board, and the inventory's R10 says so. This is the only boardsService method this card touches; the rest is the board card's.
  • Every write in automationRulesService and projectStatusAutomationService asserts automation:manage, replacing their assertCanManage calls; the READS (list, executions, the settings GET) keep the posture they have today rather than gaining an administrative gate.
  • lib/settings/projectSettingsNav.ts is NOT modified. The server guard on app/(authed)/settings/project/automation/**/page.tsx moves to automation:manage; a comment there records that the nav predicate deliberately still reads canManage until MOTIR-2258.
  • Both keys flip to enforcement: 'enforced' in lib/permissions/catalog.ts; the inventory's workflow rows carry their corrected gates and move newexisting.
  • Tests: a project admin creates and deletes a status, writes a rule and flips a derivation switch; a project member is refused 403 on each; a member can still TRANSITION a work item through existing statuses (the regression this card could cause); a non-browser gets 404.
  • The automation half asserts NEUTRALITY rather than a grant: the actors who could write a rule before can write one after, and no one else can.
  • The guard's pending pin is re-derived by running the suite on the branch.
  • pnpm test green; pnpm lint and the prettier check pass repo-wide.

Context refs

  • lib/services/workflowsService.ts — the statuses and transitions, and assertProjectAdmin (line ~47), the workspace-OWNER-only gate this card replaces.
  • lib/services/automationRulesService.ts · lib/services/projectStatusAutomationService.ts — the rules and the derivation switches; both already call assertCanManage.
  • lib/services/boardsService.tsmapStatusToColumn only.
  • app/(authed)/settings/project/workflow/actions.tscreateStatusAction, deleteStatusAction, addTransitionAction; listed with no gate in the inventory's Server-Action table.
  • app/api/board/columns/[columnId]/statuses/route.ts · .../statuses/[statusId]/route.ts · app/api/projects/[key]/automation-rules/** · app/api/projects/[key]/status-automation/route.ts.
  • lib/settings/projectSettingsNav.ts — the automation entry, for reading, not editing.
  • docs/decisions/permission-inventory.md — the workflow section, reasons R10 and R28.
  • The seam this calls · the board card.