## Summary
This PR is Phase 10 of 13 in the larger [Human Task Assignment from Aerie](https://linear.app/builder-team/project/human-task-assignment-from-aerie-b3582376ae6e) project.
It adds the approval and review workflow for Tasks after completion submissions and Task-linked documents. This phase is tracked by [AERIE-2741: Add Task approval and review actions](https://linear.app/builder-team/issue/AERIE-2741/add-task-approval-and-review-actions).
Production effect: dormant/additive. The new Task Board remains behind AERIE_TASK_BOARD_ENABLED. Merging this PR does not expose the approval interface, API operations, agent tools, or notification delivery in production. The final launch phase owns removing that gate.
---
## Why
A Task that needs review must not close merely because an assignee submits work. This slice gives Aerie one consistent approval workflow across the UI, public API, and agent tools, while preserving the submission and review history when changes are requested or an approval is removed.
AERIE-2742 depends on these actions so comments, watchers, and digests can describe approval work correctly.
---
## Business Value
- Assignees can submit work for review without presenting it as completed.
- Eligible approvers can approve, request changes, or remove an approval from the same Task.
- Requesters can choose whether approval is required and name one or more eligible approvers.
- People and authorized agents use the same rules and receive the same Task result.
- Review history and notifications make approval decisions traceable.
### What it means for end users and consumers
| User or consumer | What this phase adds when the Task Board is launched |
| --- | --- |
| Assignee | Submitting a Task that does not require approval completes it. Submitting one that requires approval changes it to Awaiting approval. |
| Eligible approver | Can approve the current submission, request changes with an explanation, or remove an approval that was applied by mistake. Any one listed approver may approve. |
| Requester or creator | Can make approval optional or required and choose eligible approvers while the Task is still editable. If approval is required and no approver is supplied, the requester becomes the approver. |
| Task manager | Can manage the Task, but must be listed as an eligible approver before approving it. |
| API and agent clients | Receive the same requiresApproval, Awaiting approval, approval, request-changes, and remove-approval behavior through the existing Task API and tools. No second Task API is introduced. |
| Notification consumer | Receives gated in-app and email events when approval is requested, granted, or returned for changes. |
| Existing Task | Remains unchanged unless the gated Task Board workflow is used. Approval is not inferred from an old approver list. |
---
## How does it work
1. Task creation and editing accept an explicit requiresApproval choice and an eligible-approver list. Requiring approval with no supplied approver uses the requester; disabling approval clears the list.
2. A completion submission either completes the Task immediately or changes it to awaitingApproval, depending on that explicit choice.
3. Approval checks the authenticated actor against the eligible approvers, binds the decision to the current immutable submission, records the actor and time, and completes the Task.
4. Request changes requires an explanation, keeps the reviewed submission and evidence, and returns the Task to inProgress. Remove approval preserves history and returns a completed Task to awaitingApproval.
5. The UI, public API, and agent tools call the same approval rules with idempotency and concurrency protection. Gated notification events are queued for pending approval, approval, and requested changes.
6. Stale browser responses are ignored when a user moves between Tasks, so an old request cannot alter the newly opened Task's saving state, messages, or idempotency keys.
---
## Scope
### Included in this phase
- Explicit optional approval configuration with one or more eligible approvers.
- awaitingApproval submission, approval, request-changes, remove-approval, and rejection rules.
- Task-scoped approver access without broader site access.
- Approval actions in the gated Task detail experience.
- Matching public API and agent actions on the existing Task contract.
- Gated in-app and email notification events with retry-safe append receipts.
- Protection against stale approval responses after navigating between Tasks.
- Exact final diff paths:
chat/components/dashboards/portfolio/__tests__/portfolio-rhodes-workbench.test.tsxchat/components/dashboards/portfolio/portfolio-rhodes-workbench.tsx
chat/components/rhodes-cards/rhodes-read-card.tsx
chat/components/task-board/my-tasks-view.test.tsx
chat/components/task-board/my-tasks-view.tsx
chat/components/task-board/task-approval-actions.test.tsx
chat/components/task-board/task-approval-actions.tsx
chat/components/task-board/task-editor.test.tsx
chat/components/task-board/task-editor.tsx
chat/convex/_generated/api.d.ts
chat/convex/notifications/events.ts
chat/convex/notifications/schema.ts
chat/convex/publicApi/v2/domains/workManagement.ts
chat/convex/publicApi/v2/domains/workManagementTaskBoardWrites.test.ts
chat/convex/publicApi/v2/domains/workManagementWrites.test.ts
chat/convex/publicApi/v2/http.ts
chat/convex/publicApi/v2/workManagementData.ts
chat/convex/publicApi/v2/workManagementWrites.ts
chat/convex/rhodes/portfolioWorkbench.ts
chat/convex/rhodes/runtime/constants.ts
chat/convex/rhodes/runtime/mutationAuthorization.ts
chat/convex/rhodes/runtime/mutationDispatcher.ts
chat/convex/rhodes/runtime/writes/taskWrites.ts
chat/convex/rhodes/schema.ts
chat/convex/rhodesMcpMutationParity.test.ts
chat/convex/taskBoard/appendReceipts.ts
chat/convex/taskBoard/approval.test.ts
chat/convex/taskBoard/approval.ts
chat/convex/taskBoard/assignmentModel.ts
chat/convex/taskBoard/assignments.test.ts
chat/convex/taskBoard/completion.test.ts
chat/convex/taskBoard/completion.ts
chat/convex/taskBoard/mutations.test.ts
chat/convex/taskBoard/mutations.ts
chat/convex/taskBoard/notifications.ts
chat/convex/taskBoard/peopleBound.ts
chat/convex/taskBoard/queries.test.ts
chat/convex/taskBoard/queries.ts
chat/lib/public-api/v2/domains/work-management-schemas.ts
chat/lib/public-api/v2/domains/work-management-task-board.node.test.ts
chat/lib/public-api/v2/domains/work-management.ts
chat/lib/rhodes-mutation-tools.ts
chat/rhodes-worker/mcp-server/tools/tasks.ts
docs/task-board/FEATURE.md
packages/contracts/src/agent-tool-registry.ts
packages/contracts/src/public-api-v2.ts
packages/contracts/src/rhodes-mutation-proposal.ts
### Deliberately excluded for later phases
- Participant comments, watchers, and the daily Task Scenario digest: AERIE-2742.
- Team Tasks views and filters: AERIE-2743.
- Removing the launch gate, production verification, and final Task Board launch: AERIE-2744.
- Multi-stage or unanimous approval. V1 closes after any one eligible approver approves.
- A general Reopen action. Request changes and Remove approval cover the approved V1 cases.
---
## Test plan
### Automated validation
- Focused approval and regression suites — 182/182 passed across 11 files (including legacy flag-off writes, gated Task Board writes, approval lifecycle, UI, API, MCP, and Rhodes parity)
- Flag-off assignability regression — red before repair (create returned 201, expected 422), then green 10/10 after restoring shared userIds validation for requiredApprovers; both create and update assert requiredApprovers_user_not_assignable
- Original hosted-failure reproduction after repair — 96/96 passed across 3 files (route-manifest parity, legacy work-management writes, and portfolio workbench)
- Full repository tests — all suites passed with the timing-sensitive contracts workspace constrained to one worker: contracts 1,337/1,337, Chat 12,136 passed / 18 skipped, all other workspace suites green, and root 154/154. Unconstrained pnpm test attempts hit only the unrelated existing 5-second agent-run-protocol timing limit; that file passed alone 20/20 and in the complete constrained contracts suite.
- Full repository check — passed (pnpm check), covering full lint/static validation and every workspace typecheck
- Architecture boundaries, Convex paths, bounded reads, test architecture, and knowledge hygiene — passed as part of pnpm check
- Biome — passed across 3,124 files; two pre-existing unrelated warnings remain in chat/skill/forge-api/scripts/sindri.mjs
- Pre-commit checks and git diff --check — passed
- Reviewed-slice equivalence before incremental repairs — stable patch ID 7df84e8a170e2ab8b67e788f992c9a1922ac667d was identical for prior reviewed slice de068754bc35663ef6cde6e7651d45f988e097fd..80d378569992dbf4056d5c716cc0b99f0fc395a0 and its exact 45-path rebuild c17409f3802fe73909732a3c9392b1f147e2a871..371a6ff7e5a34d379ba8dc77717f97f8ad270c4f
- Post-equivalence hosted-CI repair — five targeted compatibility/gating/test-harness edits only: gate the request-changes handler and operation ID, preserve legacy flag-off approver behavior and people-limit error translation, use valid unique-user cap fixtures, and add the three approval hooks to the portfolio workbench test mock
- Post-equivalence independent-review repair — restored assignability validation for both assignees and legacy flag-off requiredApprovers, with create and update regressions; no product behavior was newly selected
- Exact-parent ancestry — final head 0a22c45fd638b2c58cf19ffa63d84846ee1419d4 has merged main commit c17409f3802fe73909732a3c9392b1f147e2a871 as its direct parent
- Exact-head diff scope — only the 47 authorized paths listed above (+3363/-176)
- Hosted CI — passed on exact final head 0a22c45fd638b2c58cf19ffa63d84846ee1419d4: Test, Build, Cloudflare Workers build, both Docker builds, Typecheck, Lint + Boundaries, and Secret Scan are green in run [37766704117](https://github.com/AI-Builder-Team/Aerie/actions/runs/37766704117); Praxis and automatic Mercy also passed
### Time for Implementation
About 2 to 3 weeks for one engineer without AI assistance, including contract design, UI and API implementation, agent parity, notification behavior, regression coverage, and review repairs.
---
## Review repairs and contract clarifications
[Mercy exact-head review of 049384a94](https://github.com/AI-Builder-Team/Aerie/pull/1729#pullrequestreview-5456173366) reported one blocking requester-identity concern. No code change is warranted because the finding conflates the authenticated creator/audit actor with the Task's requester business role.
The approved Task contract deliberately keeps those identities distinct: creator is always derived from the authenticated Aerie user or API-credential owner, while requester may differ and defaults to creator when omitted. The agent flow preserves that boundary. requireCanMutateTool authenticates and authorizes the Aerie user before execution; the server passes that immutable user ID as actorUserId; createTask stores creator and audit actor from actorUserId; and the optional requester input is resolved only into the Task's requester participant. When approval is required and no approver is supplied, using requester as the default approver is also an explicit locked product decision. Supplying a different requester therefore neither changes the authenticated actor nor grants the caller another identity. Forcing requester to equal actor would remove an approved workflow rather than close an authorization bypass.
The remaining review items are explicitly deferred or nonblocking and do not alter the Task result, authorization boundary, or dormant production effect of this phase. Validation remains 182/182 focused tests, full repository checks green, and hosted CI green on the exact head. Production behavior remains gated by AERIE_TASK_BOARD_ENABLED; there is no deployment, migration, or external write from merging this phase.
---
## Mercy reconsideration on 049384a94
The [reconsideration review](https://github.com/AI-Builder-Team/Aerie/pull/1729#pullrequestreview-5456630835) withdrew the requester-identity concern after following the authenticated actor and requester paths separately. The same exact-code review found one reachable UI defect: removing the task query parameter through browser navigation left the old Task detail selected. The next head clears that selection and adds a focused browser regression.
The four findings marked blocking do not require production changes:
- Notification events persist actorUserId: args.actorUserId directly for every event type. The task-type conditional immediately below applies only to sourceTaskId and taskTitle; it does not control actor forwarding. Approval events therefore retain the actor used by eventToEmitArgs.
- The public Task response status enum already includes awaitingApproval at work-management-schemas.ts:333 under the same taskBoardEnabled() gate that exposes approval fields and operations. Gated approval responses validate against the gated enum.
- Completion submission clears closureReason, delayedReason, and blockedReason before a Task can enter awaitingApproval. Approval can only complete that current submission, so Remove approval cannot inherit those fields through any supported lifecycle path.
- Approval configuration resolves every approver from a current assignable person record. The production codebase has no path that hard-deletes a user row or converts a person principal into a non-person after configuration, so the proposed orphaned sole-approver state is not reachable through an owned production transition. Reconfiguration continues to revalidate every selected approver.
This branch has already incorporated review findings that did demonstrate real behavior, including request-changes launch gating, flag-off compatibility, assignability validation, and stale Task-switch response protection. The distinction here is therefore evidentiary, not a refusal to address review feedback: reachable defects have been repaired, while adding duplicate branches for states already excluded by the current code would increase complexity without changing production behavior.
---
## Mercy exact-head review on f625543cb
The [exact-head review](https://github.com/AI-Builder-Team/Aerie/pull/1729#pullrequestreview-5457313488) confirms that the earlier notification actor, gated status enum, stale metadata, orphaned approver, and deep-link concerns are resolved. The deep-link issue was the only reachable defect in that round and was repaired with a focused browser regression.
The four newly reported blockers do not identify reachable incorrect behavior:
- Gated Work Plan creation ignores the supplied status and always creates new. Gated edits require the supplied status to equal the stored status and omit status from updateTask. The broad argument validator therefore cannot create or transition a Task to awaitingApproval; only completion submission emits that state.
- The approved contract defines Awaiting approval as open review work and Completed and Rejected as the only generally immutable statuses. The immutable object under review is the completion submission and its evidence. Generic edits cannot change status, approval fields, the current submission, or review history; approval configuration remains separately guarded, including atomic eligible-approver replacement while review is pending.
- publicApiV2RequestGuardError rejects a missing or blank Idempotency-Key with 400 idempotency_key_required before dispatch. Approve, Remove approval, and Request changes all declare that guard as required when the Task Board gate is enabled, so their handlers never hash an absent public header.
- The optional idempotency hash on the two internal compatibility mutations exists because the same mutations also serve the flag-off legacy approval endpoints, which do not use append receipts. Every gated public call reaches them only after the router guard and passes the resulting non-empty hash. There is no accepted public payload that reaches the empty fallback.
No production code change is warranted for these four findings. The branch continues to accept and repair demonstrated defects while declining changes that would either duplicate an existing boundary or contradict the approved open-status lifecycle. Current main advanced only through unrelated dbt forecast files after this verdict; the next head will merge that main once without changing any reviewed Task path.