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
7.2 KiB
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.
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.
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 arePOST /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".
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.
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
tasksmeans work items. It used to mean the durable inference queue, which now answers only toinference-tasks/itasks. See 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).
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:
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:
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
metflags. 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.
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 — forges, repo import, merge and post-merge reopen
- agent-authority.md — whose authority an agent acts under
- inference-tasks.md — the durable inference queue that used to own this name