# Epics, roadmap nodes and work items Three resources, one hierarchy: ``` Epic the vision, and the criteria delivery is measured against └─ EpicTask a roadmap node; owns a subset of the Epic's criteria └─ Task a work item that moves through states and lands a PR ``` The split exists because "every task is done" and "we delivered the thing" are different claims. Tasks close; criteria are what say whether closing them amounted to anything. Nothing derives one from the other automatically — see [Reconciliation](#reconciliation). ## Epic The versioned statement of intent. `content` is the vision prose; `doneCriteria` is the list the work is graded against. A criterion is `id=text`, and prefixing the id with `q:` marks it a **quality** criterion rather than an outcome one. Both count toward completeness — grading only the mechanical half is how you end up with every task done and no car. ```sh mcpctl create epic checkout-rewrite \ --project payments \ --content-file vision.md \ --criterion 'cart=A cart survives a session drop' \ --criterion 'q:latency=p99 checkout under 400ms' mcpctl get epics mcpctl describe epic checkout-rewrite # vision, criteria, completeness, reconciliation, roadmap ``` `status` is `open`, `delivered`, `delivered_with_tradeoffs` or `cancelled`. Closing an Epic with unmet criteria requires the tradeoffs path — you may ship something short of the vision, but not while calling it whole. > **API only, no CLI yet.** Marking a criterion `met`, setting status and > recording tradeoffs are `POST /api/v1/epics/:id/{criteria,status,tradeoffs}`. > The CLI reads all of it (`describe epic`) but cannot write it. Tracked as > task 180. ## EpicTask A roadmap node. Its `--criterion` ids must match ids on the parent Epic: the node is claiming *"these parts of the vision are mine"*. ```sh mcpctl create epictask cart-persistence \ --epic checkout-rewrite \ --criterion 'cart=A cart survives a session drop' \ --depends-on session-store mcpctl get epictasks mcpctl describe epictask cart-persistence ``` `--depends-on` builds the dependency graph `describe epic` renders as a roadmap. ## Task The unit an agent actually works on. It carries a `risk` level, which decides how much review it must survive, and optionally a repo and a pull request. ```sh mcpctl create task cart-cookie-ttl \ --epictask cart-persistence \ --title 'Extend cart cookie TTL to 30 days' \ --risk normal \ --repo mcpctl get tasks mcpctl describe task cart-cookie-ttl # state, legal moves, verdicts, git, history ``` > **`tasks` means work items.** It used to mean the durable inference queue, > which now answers only to `inference-tasks` / `itasks`. See > [inference-tasks.md](inference-tasks.md). ### States ``` todo → in_progress → building → self_testing → qa_review ↓ ↓ done ← security_review → sre_review ↓ awaiting_review ⇄ (reject → in_progress) ↓ accept merged → reopened → in_progress ``` Every state can go back to `in_progress` and out to `cancelled`; a failed build goes *back* to work rather than forward. `merged` has exactly one exit, `reopened`, and only CI drives it (see [gitproviders.md](gitproviders.md#post-merge-failure)). ```sh mcpctl tasks advance cart-cookie-ttl building --note 'kicked off CI' mcpctl tasks history cart-cookie-ttl ``` Transitions are compare-and-swap: two agents racing the same task, one wins and the other gets a 409 naming the state it lost to. ### Risk decides the gates | Risk | Must pass before `done` | |---|---| | `trivial` | nothing — docs, formatting | | `normal` | `qa_review` | | `high` | `qa_review`, `security_review`, `sre_review` | Verification effort scales with risk instead of running one fixed pipeline over everything. A verdict is pinned to the commit it judged, so a later push invalidates it rather than silently carrying forward: ```sh mcpctl tasks verdict cart-cookie-ttl \ --stage qa_review --commit 4f2a91c --pass \ --summary 'covers the session-drop path; added a regression test' ``` Reaching `done` without the required passing verdicts is refused, and the error names which stage is missing. ### Linking the pull request Accept only merges something if the task knows what its pull request is. An agent writes these when it pushes; from the CLI they go through `patch`: ```sh mcpctl patch task cart-cookie-ttl \ branch=feat/cart-cookie-ttl \ headCommit=4f2a91c \ prNumber=214 \ prUrl=https://mysources.co.uk/michal/mcpctl/pulls/214 ``` `headCommit` is what the review is pinned to. Accept refuses if the PR's head has moved past it — see below. A task with no `prNumber` can still be accepted; it records the approval and merges nothing, and says so rather than implying code landed. ### The human gate `done` means it left the developer's loop, not that it shipped. `awaiting_review` is where a human decides: ```sh mcpctl tasks accept cart-cookie-ttl --note 'ships with the friday release' mcpctl tasks reject cart-cookie-ttl --note 'cookie is set before consent — move it after' ``` **Accept lands the code.** If the task has a PR, accept merges it through the repo's gitprovider and records the merge commit. If the merge fails — conflict, or someone pushed after the review — the task stays un-merged and says why. A stale head is refused rather than merged, because accepting a commit nobody reviewed is the one thing the gate exists to prevent. Reject requires its comment. That comment is what the agent works from, so an empty rejection is a bug report with the contents removed. ## Reconciliation `describe epic` prints claimed completeness and, directly under it, what the delivered work actually says. Those two disagree by design: - **Completeness** counts `met` flags. Nothing flips them automatically, so an Epic whose every Task is merged still reads 0% until someone asserts the vision was met. - **Reconciliation** walks the roadmap and reports which criteria have merged work behind them, which are claimed but undelivered, and which no roadmap node owns at all (`unowned`). Attribution is by criterion id, which is a *convention* — nothing enforces that an EpicTask's criteria ids exist on the parent. Reconciliation reports where the convention doesn't hold rather than guessing an owner. It never writes `met`: deriving it from task state would assert that finishing the tasks delivered the vision, which is exactly the assumption under audit. ## RBAC | Resource | Verbs | |---|---| | `epics` | `view`, `create`, `edit`, `delete` — `edit` also covers status/criteria/tradeoffs | | `epictasks` | `view`, `create`, `edit`, `delete` — `edit` also covers dependency edges | | `tasks` | `view`, `create`, `edit`, `delete`, **`run`** | `run:tasks` is the gated one: advancing state, recording verdicts, accepting and rejecting are `run`, not `create`. A reviewer who may drive tasks through the board should not need permission to invent them, and — more to the point — anyone who can *file* a task should not thereby be able to *accept* one. ```sh mcpctl create rbac task-reviewer --role run --resource tasks mcpctl create rbac epic-reader --role view --resource epics ``` Bindings written before the `tasks` / `inference-tasks` split are rewritten once at startup, so an old `view:tasks` grant keeps authorizing the inference queue and does not silently become authority over work items. ## See also - [gitproviders.md](gitproviders.md) — forges, repo import, merge and post-merge reopen - [agent-authority.md](agent-authority.md) — whose authority an agent acts under - [inference-tasks.md](inference-tasks.md) — the durable inference queue that used to own this name