Five resources shipped with no documentation and one documented lie: docs/inference-tasks.md still told people `mcpctl get tasks` lists the inference queue. Work items own that name now. Adds docs/epics.md (the Epic -> EpicTask -> Task hierarchy, the state machine, risk-scaled review gates, the accept-merges gate, and reconciliation), docs/gitproviders.md (forge registration, discovery/import, merge-on-accept, post-merge reopen) and docs/agent-authority.md (the three run modes, and plainly which two are not implemented). Two things found while writing them, both fixed rather than filed: - `webhookSecretKey` was read by the webhook route but writable from NOWHERE -- absent from the create schema, update schema, service, CLI and apply spec. Since the endpoint fails closed on a null key, post-merge CI failures could never reopen their task; the board would keep claiming `merged` for work that broke the build. Adds --webhook-secret-key, the schema fields and the apply key, with tests covering persist / omit / explicit-null / reject-empty. Omission deliberately does not clear the column, so an unrelated edit cannot silently disarm the webhook. - src/cli/src/commands/create.ts contained a literal NUL byte inside a string literal (a binary-file check written with the raw character rather than an escape). Identical behaviour, but it made the whole file read as binary, so `grep -I` skipped it entirely and 1120 lines of CLI were invisible to every text search. Replaced with the escape. The Epic write path (criteria/status/tradeoffs) is API-only -- documented as such rather than papered over, and filed as task 180. Full suite 2612 green; lint at baseline 794/77. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
191 lines
7.2 KiB
Markdown
191 lines
7.2 KiB
Markdown
# 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 <repo-id>
|
|
|
|
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.
|
|
|
|
### 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
|