`ai:plan` (2/4) — the plan-editing jobs: augment, expand, replan, the explanation drafter and the per-item planner
The second ai:plan card: the jobs that CHANGE an existing plan. Five submission routes across three services.
| Operation | Service method | Today |
|---|---|---|
POST /api/ai/augment | aiPlanEditsService.submitAugment | reaches a gate indirectly |
POST /api/ai/expand | aiPlanEditsService.submitExpand | reaches a gate indirectly |
POST /api/ai/replan | aiPlanEditsService.submitReplan | reaches a gate indirectly |
POST /api/ai/explanation | aiExplanationService.submitExplanationDraft | nothing |
POST /api/work-items/[id]/ai/plan | the per-item planner | reaches a gate indirectly |
aiPlanEditsService and aiExplanationService contain no assertion of their own on origin/main — the guard reads four of these as governed only because of something else the route calls, which is exactly the indirection the story exists to replace with a named key.
The [id]-scoped route resolves its project from the WORK ITEM, not from the active-project context. Resolve it explicitly and assert against that project id: a gate that asserts on the actor's active project while acting on an item in another one is not a gate at all.
Acceptance criteria
- Each of the five service methods asserts
projectAccessService.assertPermission(projectId, ctx, 'ai:plan')before submitting anything to motir-ai — before the job is created, so a refusal costs no credits. - The per-item planner resolves its project from the work item it acts on, and a test proves an item in a project the actor cannot plan in is refused.
- A unit test proves a project
vieweris refused all five, and anadminandmemberpass. ai:plandoes NOT flip toenforcement: 'enforced'here — the close-out card owns the flip once everyai:plancard has landed, sotests/permissions/catalog.test.tsis untouched by this PR.- The inventory rows for these five record the gate that landed.
- The guard's pinned counts are re-derived by running it on this branch.
pnpm test tests/permissions/ tests/services/is green.
Context refs
lib/services/aiPlanEditsService.ts,lib/services/aiExplanationService.ts— neither holds an assertion today.app/api/work-items/[id]/ai/plan/route.ts— the route whose project comes from the item.lib/services/projectAccessService.ts—assertPermission.docs/decisions/member-facing-permissions.md— the role assignment forai:plan.- The role-set seam — the prerequisite.
ai:plan(1/4) — the sibling card; read its PR for the gate shape before repeating it.