Files
mcpctl/docs/epics.md
Michal 6d38953a96 docs(agentic-teams): document the new resources, and make the webhook reachable
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
2026-08-16 00:14:58 +01:00

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 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".

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

tasks means work items. It used to mean the durable inference queue, which now answers only to inference-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 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.

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