Tasks
Task CRUD, reordering, and approval
List tasks for a project
List tasks for a project. When called without query params, returns the full task list (with relations). Requires `tasks.read`. Pass `?fields=minimal` to receive only `id`, `title`, `taskNumber`, and `taskKey` (used for gate criterion linking and MCP key resolution) — that mode accepts full-scope `tasks.read`, `gates.create`, or `gates.update`. The `.own` scope on `tasks.read` does NOT grant access here, since minimal mode returns every task in the project regardless of ownership. PT-602/PT-684/PT-685/PT-905: `?ready=true`, `?review=true`, `?blocked=true`, and `?awaiting_decision=true` (mutually exclusive) switch to the flow views — the response becomes `{ data: FlowTaskRow[], pagination }`, the same row shape registered under `taskListResponseSchema` for `GET /api/tasks`. Every flow row carries `epic` and `slaUrgency`. Review rows additionally carry `reviewWaitMinutes` and `pullRequestUrl` (the open `pr_link` value, or null when the review row has no PR). Blocked-reveal rows additionally carry `blockedBy`. Decisions rows (ADR-028) additionally carry `decisionWaitMinutes`, `decisionQuestion`, `decisionOptions`, `decisionRequestId`, and `decisionAskedByUserId`, ordered oldest-first by asked_at.
Create a task in a project
Create a task in a project. Cross-field constraints: when isRecurring is true, recurrenceIntervalDays (1–365) is required and parentTaskId must be absent (subtasks cannot be recurring). PT-1020: the route probes for similar OPEN tasks inside the create transaction and creates only when `duplicatesReviewed` NAMES every candidate that probe finds; otherwise it answers 409 `DUPLICATE_CANDIDATES` with that list and creates nothing. Presence of the field is not sufficient — an absent, empty, stale or partial list is refused for the candidates it does not name. Resubmit listing exactly the keys/ids the 409 reported; that always converges. Subtask creates (`parentTaskId` present) are exempt from the guard.
PT-448 cross-project 'goal' view
PT-448 cross-project 'goal' view. Lists every task carrying any of the requested `labels` across the projects the caller can read (tenant-scoped). Requires full `tasks.read` (the list is not ownership-scoped, so `tasks.read.own` is rejected) AND `projects.read` (or `.own`) plus an active membership. A GUEST member or a `projects.read.own`-only caller is restricted to collaborator/creator projects. Tasks in projects the caller cannot read are omitted; cross-project dependency references are redacted (taskKey + projectId omitted, title + status replaced with sentinels, isCrossProject=true) per the PT-283 precedent, so the real key/title never leak. Each row carries project, key, title, status, priority, and `isBlocked`/`isBlocking` flags; sorted by priority (URGENT first), task key as tiebreak. Capped at the 500 highest-priority rows; `truncated` is true when more match. This endpoint also serves four cross-project flow roll-ups (mutually exclusive, and ignoring `labels`): `?ready=true` — the dependency-gated ready set plus per-project agent `wip` (PT-667); `?review=true` — the review queue (PT-602); `?blocked=true` — the blocked reveal, would-be-ready tasks held out by an unfinished predecessor, each row carrying `blockedBy` (PT-685); and `?awaiting_decision=true` — the Decisions queue (ADR-028), open agent decision-requests ordered oldest-first by `askedAt`, each row carrying `decisionWaitMinutes`, `decisionQuestion`, `decisionOptions`, `decisionRequestId`, and `decisionAskedByUserId` (PT-905). Narrow any roll-up to one project with `?projectId=<uuid>`.
PT-482 label suggestions
PT-482 label suggestions. Returns the distinct labels in use for the tenant (optionally narrowed to one project), most-used first, as the autocomplete vocabulary behind the Jira-style label combobox. Tenant-scoped; the response is non-sensitive autocomplete strings, never task content — so the gate is lighter than the goal view: an active member who can read OR create tasks. Any scope satisfies it — full `tasks.read` / `tasks.create` or the `.own`-scoped variants (`tasks.read.own` / `tasks.create.own`).
PT-491 cross-project epic-children listing
PT-491 cross-project epic-children listing. Lists every task whose `epicId` points at the given EPIC, across every project in the tenant (the `epicId` edge is tenant-scoped as of PT-446), PLUS the subtasks (`parentTaskId`) of those readable children so the Child Issues panel can render a nested task → subtask tree (PT-528). The project-scoped `GET /api/projects/:id/tasks?epicId=` filter is unchanged and only returns children in that one project — this route is its cross-project complement. Requires full `tasks.read` (the list is not ownership-scoped) AND `projects.read` (or `.own`) plus an active membership. Envelope: the caller must be able to read the EPIC itself — a non-existent, cross-tenant, or unreadable-project epic all return 403 (no existence oracle); a readable non-EPIC target returns 400. Children in projects the caller cannot read are redacted to an opaque id + the PT-283 sentinels (taskKey/projectId/issueType/progress dropped, title/status sentinelled, isCrossProject=true), so the real key/title never leak. Only a full org-wide `projects.read` holder sees every child un-redacted. Readable rows also carry `parentTaskId` (PT-528); a parent edge is nulled when its parent is unreadable, and a subtask of an unreadable child — or one in a project the caller cannot read — is omitted, so no cross-project parent–child relationship leaks.
PT-577 initiative-scoped Epic picker options for the task-detail Epic field
PT-577 initiative-scoped Epic picker options for the task-detail Epic field. Returns every EPIC in the current project PLUS every EPIC tagged to an initiative this project participates in (PT-447 Initiative → Epic membership, unioned with the legacy project-containment FK), across the other projects in the tenant. Cross-project epics are bounded to the caller readable-project set so an unreadable epic is never offered (PT-283 — omitted, not sentinelled); a tenant-wide epic list is out of scope. Requires the same epic-list read gate as the project-scoped minimal listing (full-scope `tasks.read | gates.create | gates.update`). The cross-project expansion additionally needs full `tasks.read` + `projects.read` (or `.own`) + an active membership, and degrades gracefully to same-project epics when that is missing rather than 403ing. Options carry `projectName` / `isCrossProject` / `initiativeName` so the picker can group by project (current project first).
PT-611 write-guard-aligned Assignee picker options for the task-detail Assignee field and the task create / add-subtask form
PT-611 write-guard-aligned Assignee picker options for the task-detail Assignee field and the task create / add-subtask form. The people analogue of `/api/projects/{id}/epic-options`. Unlike `mentionable-users?assignable=true` — a *visibility* list that offers every ACTIVE non-guest org member — this endpoint offers only users who pass the same predicate the task write guard (`assertAssigneeAssignable` → `verifyUserProjectAccess`) enforces on save: the project creator, an ACTIVE collaborator (including cross-org collaborators), or an INTERNAL member with full-scope `projects.read`. `Guest Customer`s and synthetic agent users are excluded. An INTERNAL member holding only `projects.read.own` who is neither creator nor collaborator is therefore NOT offered — the picker no longer surfaces candidates the PATCH would reject with a 400. The candidate pool is the same requester-scoped visibility set `mentionable-users` builds, then narrowed, so a scoped caller can never enumerate more org members here than there. Options are the minimal `{ id, name }` picker shape (no email), sorted by name. The guard is evaluated against fetch-time state; the PATCH independently re-validates on save.
PT-431 task velocity
PT-431 task velocity. Returns completed-tasks-per-week buckets over the trailing `weeks` window (default 12, clamped to [1, 52]), the data source for the dashboard Task Velocity widget. A completion is a task whose `completedAt` (stamped when it transitions into a project-configured completed status) falls in the window. Weeks are ISO-8601 (Monday 00:00 UTC). Requires `tasks.read | tasks.read.own` AND `projects.read | projects.read.own` plus an active membership. A `tasks.read.own` caller counts only their own completions; a GUEST / `projects.read.own` caller is restricted to collaborator/creator projects. Archived (COMPLETED / CANCELLED) projects are excluded.
PT-1020 duplicate probe: returns the same tenant-wide ranked open-task title-similarity candidates the create gate consults, for a bare title — subtasks included, trigram matched
PT-1020 duplicate probe: returns the same tenant-wide ranked open-task title-similarity candidates the create gate consults, for a bare title — subtasks included, trigram matched. Pure suggestion, never blocks anything by itself; the gate lives on POST /api/projects/{id}/tasks. GUEST members only see candidates on projects they actively collaborate on, and callers without full `tasks.read` receive an empty list.
PT-604: atomically claim the top task in the project ready set (PT-602) for `assigneeId`
PT-604: atomically claim the top task in the project ready set (PT-602) for `assigneeId`. Race-free (READ COMMITTED conditional CAS — concurrent callers never both claim the same task; the loser advances to the next ready row) and audited in the same transaction. The claimed task moves to the project's first in_progress-category status (by sortOrder); a project with no such status keeps the task status unchanged. Returns `{ data: <task> }` on success, or `{ data: null }` (still HTTP 200) when nothing is ready.
Get task details
Get task details. The response carries `decisionRequest` (PT-905) — by default the task's LATEST decision-request, suppressed to `null` while the task is complete. Pass `decisionRequestId` to hydrate a specific one: that is the only correlation path a resuming agent has once a NEWER request has opened on the same task, which would otherwise hide its answered one for good. An id-addressed request that is already `answered` or `cancelled` is returned even on a complete task — the suppression exists to stop a stale OPEN question being offered as a live control, not to withhold the answer an agent is resuming from, which is frequently what completed the task. An `open` request stays suppressed on a complete task whether or not it was addressed by id.
Update a task
Update a task. When a status change starts the task but its automatic cycle attachment is skipped because the project requires a story-point estimate for cycle work, `data` carries `cycleAutoAttach: { outcome: 'estimate-required', storyPointScale }`.
Delete a task
Delete a task
Reorder a task within its status column
Reorder a task within its status column. A move that starts the task but whose automatic cycle attachment is skipped under the project's story-point estimate policy carries `cycleAutoAttach: { outcome: 'estimate-required', storyPointScale }` in `data`.
Approve or reject a task
Approve or reject a task
Check dependency status for a task
Check dependency status for a task
Raise a decision request — an in-progress agent marks its task as awaiting a human decision (question + optional proposed options)
Raise a decision request — an in-progress agent marks its task as awaiting a human decision (question + optional proposed options). At most one OPEN request exists per task; a re-mark while one is already open idempotently returns the existing row (HTTP 200) rather than creating a second one (HTTP 201 on genuine creation). Authorised as the task's CURRENT assignee — which under the INF-113 claim convention is a synthetic `agent+<class>@constellation.local` user rather than the token owner, so a caller driving that agent supplies the optional `agentId` body field naming it and the call then acts as that agent. Supplying it requires `tasks.update.any` (403 without), and the id must be an ACTIVE synthetic agent in this tenant — a human, suspended, soft-deleted or foreign-tenant id is rejected (400), so this delegates to a robot account and cannot impersonate a person. Omit it when the token itself is the assignee. Authorised only while the task is in an in_progress-category status AND is not already complete — a task counts as complete by ANY of a completed-category status, a stamped `completedAt`, or `progress >= 100`, because a task can be completed without its status saying so (progress-only completion). All re-validated authoritatively under a row lock, never trusted from client input. Never mutates task.status/assigneeId and never emits projects.task.unblocked. On first creation only, best-effort posts a task comment carrying the question.
Answer the decision request named by the `requestId` query parameter
Answer the decision request named by the `requestId` query parameter. Exactly one of `chosenOptionIndex` (an in-range index into the request's own `options`) or a non-empty free-text `answer` must be given — the effective answer is the chosen option's label, or the free text. Addressing by explicit id (not 'the task's latest request') keeps a stale retry idempotent: it resolves the exact request it named and can never hit a newer one that opened since. Open-guarded: answering an already-terminal request is a no-op that returns the existing (resolved) row rather than erroring. Requires `tasks.update.any`. Records one auditCritical entry (metadata only — never the answer body) on the transition that actually resolves it.
Cancel the decision request named by the `requestId` query parameter
Cancel the decision request named by the `requestId` query parameter. Authorised for a caller who holds `tasks.update.any` OR IS the request's asking user (ADR-028 — the raising agent may always cancel its own question). Addressed by explicit id like PATCH; open-guarded, same no-op-on-terminal shape.
Create a typed lateral link from the source task ([taskId]) to another task in the same TENANT — the target may be in a different project (PT-909)
Create a typed lateral link from the source task ([taskId]) to another task in the same TENANT — the target may be in a different project (PT-909). The body either links an existing task (`{ targetTaskId, linkType }`) or creates a new follow-up task and links it (`{ newTaskTitle, linkType }`, status BACKLOG or the project default, reporter = caller — PT-479). Supported link types: BLOCKS | RELATES | DUPLICATES | CLONES | CAUSES | FOLLOWS. Both tasks must be in the same tenant. When the target is in a different project, the caller must also have access to that project; an unauthorized target and an absent one return the same 404, so the endpoint is not an existence oracle. The create-new branch also requires `tasks.create`.
Delete a typed lateral link by id
Delete a typed lateral link by id. The link must belong to the route tenant, and the route task must be one of its endpoints — returns 404 otherwise. Since PT-909 a link may span two projects; when the OTHER endpoint is in a different project, the caller must also have access to that project, because deleting a BLOCKS edge changes what is ready there. Unauthorized and absent both return 404.
Chronological task activity feed merging comments, audit changes, and file attachments
Chronological task activity feed merging comments, audit changes, and file attachments. Change entries are included only when the caller has the `audit.read` permission; file entries only when they have `files.read`. For null-tenant projects (no organisationId) both gates are denied, so the feed contains comments only.
PT-602: ready-set and review-queue sizes for the project, for the Flow area card
PT-602: ready-set and review-queue sizes for the project, for the Flow area card. Split out of the project overview aggregate so the readiness scan is fetched lazily and never blocks the hub. Gated on `projects.read | projects.read.own` evaluated against the project’s own org (plus project access). Returns `{ data: { readyCount, reviewCount } }`.
PT-803: the flow-metrics read API over the `task_flow_events` capture stream (PT-801/PT-802)
PT-803: the flow-metrics read API over the `task_flow_events` capture stream (PT-801/PT-802). Returns four series in ONE payload — cumulative flow, review-queue depth/age, rework rate, and cycle time by agent class — because the first three derive from ONE scan of the same window; a chart-per-endpoint split would repeat that scan for a response measured in kilobytes. (Cycle time is the series that scan cannot answer on its own — a task completed inside the window may have started long before it — so it adds a second, targeted read for completions and their histories.) The cumulative-flow and review-queue series are built by reconstructing each task’s timeline PER TASK from that task’s own transitions, walking backward from live state, and their population figures are a COUNT OF TASKS rather than an accumulated `+1 to_category / −1 from_category` delta: a cross-project move, which the stream records with no discriminator, can therefore misplace at most the one task it belongs to instead of desynchronising the whole series. Rework and cycle time do not use that reconstruction — rework buckets qualifying events directly, and cycle time works from the histories of the tasks that completed inside the window. The window is the RESOLVED one, not necessarily the requested one: `to` is capped at the present (sampling the future would report fabricated history, since the reconstruction projects each task’s live status forward) and an over-long range clamps its LEFT edge. A third normalisation is silent: the grammar accepts arbitrary fractional seconds, but the value is carried as a JS `Date` and so is TRUNCATED to milliseconds — `…:00.123999Z` becomes `…:00.123Z`, which is what the returned `window` reports and what the exclusive-`from` / inclusive-`to` predicates apply, while the stored timestamps keep microsecond precision. Sub-millisecond boundary membership is therefore unreliable (PT-913). `truncated` reports when an internal row cap clipped the reads. Residuals a chart reader must not misread as data are documented at /modules/project-tracker/flow. Gated on `projects.read | projects.read.own` evaluated against the project’s own org (plus project access), like the sibling flow-counts and overview reads.
PT-1099: the four flow series of /api/projects/{id}/flow-metrics — cumulative flow, review-queue depth/age, rework rate and cycle time by agent class — aggregated over the ACTIVE organisation’s readable projects in ONE snapshot
PT-1099: the four flow series of /api/projects/{id}/flow-metrics — cumulative flow, review-queue depth/age, rework rate and cycle time by agent class — aggregated over the ACTIVE organisation’s readable projects in ONE snapshot. The set is every non-archived project in the organisation for a full `projects.read` holder, and the collaborator-or-creator projects for a `projects.read.own` caller or a guest; projects of another organisation are never included, even where the caller collaborates on them. Percentiles, means and rework rates are computed from the raw samples of the whole set, never by merging per-project summaries. At most 100 projects are aggregated, the most recently updated first: `totalProjectCount` reports how many the caller can read, `projectCount` how many were aggregated, and `truncated` is true whenever they differ or any internal row cap clipped the reads. The window is `range` days ending at the SERVER’s now, sampled daily; the returned `window` reports the one actually served. Requires full `tasks.read`, any `projects.read` and an active membership in the active organisation. Residuals a chart reader must not misread as data are documented at /modules/project-tracker/flow.
List the distinct task labels for the Tasks-page Labels filter chip (PT-483)
List the distinct task labels for the Tasks-page Labels filter chip (PT-483). Scoped to exactly the tasks the caller can list via /api/my-tasks: the collaborator-or-creator project window for GUEST / projects.read.own-only callers, and pinned to the caller's own assigned tasks for tasks.read.own-only callers. Requires (tasks.read OR tasks.read.own) AND (projects.read OR projects.read.own) plus active membership.
Apply a sparse field patch to up to 100 tasks in a single transaction
Apply a sparse field patch to up to 100 tasks in a single transaction. The patch accepts any non-empty subset of status, assigneeId, dueDate, priority and cycleId — at least one field must be present, otherwise the request is rejected with 400. `cycleCapacityOverrideReason` is a request control, not a task field: supply it only when retrying a `CYCLE_CAPACITY_EXCEEDED` response. Fields left out are untouched on every selected task. All-or-nothing semantics: if any task in the batch fails validation or update, the whole batch rolls back. Status values are validated per task's project — a status name missing from any selected task's project rejects the batch. cycleId (null detaches) is validated per task against the target cycle's scope arm before anything is written: a partially eligible selection is rejected whole, naming how many tasks failed and which. A completed cycle is frozen in both directions — the batch is rejected if any task would join it or leave one.
The cycles a whole SELECTION may join — the intersection of the eligible sets of every task in the selection, computed server-side
The cycles a whole SELECTION may join — the intersection of the eligible sets of every task in the selection, computed server-side. Feeds the 'Add to cycle' bulk action so it does not offer a cycle the bulk patch would refuse AS OF THIS RESPONSE — eligibility is a fact about the moment it was read, and the write revalidates it under row locks, so a cycle completed or re-scoped in between is still rejected there. POST rather than GET because the selection (up to 100 ids) is the input; it reads and mutates nothing. `canClear` reports separately whether 'no cycle' is offerable, which is false when any selected task sits in a completed (scope-frozen) cycle. Gated at the same bar as the write it precedes, so an affordance is never refused to a caller the write would accept; initiative- and project-set-scoped cycles are included only for callers holding the corresponding org-wide grants. Under the per-project cycle estimate policy (PT-1091), a planned or active cycle the bulk write would refuse with CYCLE_ESTIMATE_REQUIRED is still returned but carries an ESTIMATE_REQUIRED `blocker` naming the blocked tasks with their allowed values.
Move up to 100 tasks from their current project(s) to a target project within the same tenant
Move up to 100 tasks from their current project(s) to a target project within the same tenant. Allocates new task keys from the target project, reconciles statuses, creates redirect entries for old keys, and emits per-task audit + event entries. Refuses the move if any selected task has subtasks or a parent not fully included in the selection (SUBTREE_NOT_INTACT). Cross-tenant moves are rejected.