Repository navigation
feat(roadmap): expose EpicDefinition status and ready work items through REST/MCP #333
Description
Parent roadmap: mctlhq/.github#66
Depends on: mctlhq/.github#99 (readiness evaluator, closed), mctlhq/.github#119 (published RoadmapPublication)
Corrected 2026-09-23: this line used to name mctlhq/.github#96, a merged CI fix (
fix(ci): allow mctl-agents pushes to be reviewed). The readiness evaluator is #99, and the published contract this issue consumes is #119.
Related: mctlhq/.github#95, #329
Goal
Expose the canonical EpicDefinition-derived roadmap read model so MCP clients do not need to reconstruct roadmap state from GitHub issues manually.
Add read-only operations equivalent to:
mctl_get_epic_status
mctl_get_ready_work_items
Exact REST/MCP names may follow existing conventions, but these two user intents must be directly answerable.
mctl_get_epic_status
Given an epic by canonical name or bound root issue, return a structured composition of the existing deterministic views:
- EpicDefinition identity + manifest path/hash/revision where available;
- lifecycle/title/goal;
- RoadmapHealth;
- allRequired completion summary;
- work-item state;
- dependency-aware readiness from feat(roadmap-control-plane): derive dependency-aware ready work items from EpicDefinition .github#99;
- bindings to repo/issue for bound work items;
- explicit unbound/unknown state;
- live execution projection from feat(roadmap-control-plane): project live DevLoop execution state beside RoadmapHealth .github#95 when available, as a separate optional field.
Example intent:
"What is the status of lifecycle-ownership?"
→ complete 5/6 required
→ guarded-recovery ready
→ no other required ready items
mctl_get_ready_work_items
Return only executable candidates, with enough provenance to start exact DevLoops later:
{
"epic": "lifecycle-ownership",
"manifest_sha256": "...",
"source": {"mode":"live-capture","captured_at":"..."},
"items": [
{
"id":"guarded-recovery",
"required":true,
"repository":"mctlhq/mctl-api",
"issue":294,
"state":"ready"
}
]
}Support at least:
- one epic;
- all active EpicDefinitions;
required_only=trueas the safe/default wave-selection behavior.
Source-of-truth boundary
mctl-api MUST NOT reimplement the dependency algorithm independently.
Consume the versioned readiness contract produced by the Roadmap Control Plane: the RoadmapPublication that mctlhq/.github#119 publishes to the generated roadmap-state branch. mctl-api is a read-only consumer. It never runs the evaluator (see the design comment on this issue). The same manifest+snapshot must not produce different readiness depending on which client asks.
Safety / unknown semantics
- Unbound is not ready.
- Unobserved/ambiguous/not-found is not ready.
- Failed roadmap observation must remain explicit.
- Read-only calls never add labels, start DevLoops, approve proposals, mutate GitHub or update Project metadata.
- Runtime execution status is supplemental and may be unknown without invalidating deterministic readiness.
Acceptance criteria
- MCP can answer "what is the status of epic X?" without GitHub reconstruction by the caller.
- MCP can answer "what required work is ready now?" for one or all active EpicDefinitions.
- Lifecycle deps: bump k8s.io/api from 0.32.3 to 0.36.1 #57 returns feat(lifecycle-ownership): guarded operator recovery for lifecycle ownership #294 as ready when its predecessors are complete.
- Human Input feat(mcp): add mctl_trigger_shepherd tool #42 returns feat(roadmap): expose EpicDefinition status and ready work items through REST/MCP #333 before feat(human-input): canonical request/response API and pending-input read model #261/feat(unified-identity): bind DevLoop approvals to canonical delegation grants (#376 slice C) #571.
- Enterprise MCP fix: raise OpenClaw tenant quota default/floor to limits.cpu=3 #35 can return multiple independent ready required items when the DAG permits it.
- Unified Identity chore(main): release 4.21.2 #91 does not surface unbound principal-model as executable.
- Each result carries manifest + observation provenance sufficient to explain why an item was ready.
- Existing roadmap health/completion contracts remain backward compatible.
Non-goals
- Starting or approving DevLoops.
- Choosing business priority among independent ready items.
- Treating Project priority as part of EpicDefinition.
Activity
mashkovd commentedon Sep 23, 2026
Blocker: no distribution path for the canonical readiness evaluator (architecture decision needed)
Checked on 2026-09-23 against
mctl-apiorigin/main(c963963) andmctlhq/.githubmain(48e8140), before starting implementation. This issue cannot be done as a narrow change. The source-of-truth boundary it requires ("mctl-api MUST NOT reimplement the dependency algorithm") has no existing transport to rely on.Facts
docs/work-context-contract.md, there is nothing matchingroadmap,RoadmapHealth,ReadySetorcompletion. The acceptance line "existing roadmap health/completion contracts remain backward compatible" can therefore only mean the.githubschemas (roadmap-health.schema.jsonand itscompletionblock), not an existing mctl-api API.roadmap/scripts/ready.py(RoadmapReadySet/RoadmapReadySetList,roadmap.mctl.ai/v1alpha1) is CLI-only.roadmap-validate.ymlreplays fixtures offline and states that--livenever runs in CI. There is no artifact upload, release, scheduled capture or committed snapshot, and the three JSONs underroadmap/fixtures/are frozen test inputs. The#99design (agents-state/.github/proposals/issue-99-…/design.md) concludes that "feat(roadmap): expose EpicDefinition status and ready work items through REST/MCP #333 consumes the published document", but it never defines where that document is published.alpinewith onlyca-certificates git openssh-client, while the evaluator is Python (jsonschema,PyYAML)..github#96, which is an unrelated CI fix. The readiness contract is.github#99, delivered in PR feat(alerts): fingerprint-based dedup for incident creation #107. The native blocked-by edge already points at fix(api): explicitly empty tenant allowlist now denies all skill enables #99, so only the prose is wrong.The choice that needs an owner
mctlhq/.github(existinginternal/gitops/reader.go, which already clones mctl-gitops the same way) and runsready.py/health.py --liveviaos/execwith a read-only token. It adds the clone's git SHA as the revision, runsconsistency_errorson the output, and wraps the result inGET /api/v1/roadmap/epics/{name}/status,/readyandmctl_get_epic_status/mctl_get_ready_work_items.capturedAtexposed.operations.Executor) or a scheduled.githubjob captures aRoadmapReadySetList+RoadmapHealthList, and mctl-api reads the published artifact.Recommendation: (a). It is the only option in which "the same manifest + snapshot yields the same readiness regardless of client" holds by construction, because it runs the very evaluator
.githubCI tests. It also reuses an existing clone mechanism. Both (b) and (c) add a component whose staleness becomes a new failure mode that #333 would have to surface.Two contract gaps must be settled whichever option is chosen:
pathandsha256but no git revision, so mctl-api must add the SHA of the checkout it evaluated.lifecycle,titleandgoalare not inRoadmapHealthorRoadmapReadySet. They have to be read from the same pinned EpicDefinition YAML, not re-derived.Implementation is on hold until the option is chosen.
#334stays blocked behind this.mashkovd commentedon Sep 23, 2026
Proposed design (2026-09-23): one recommendation, for owner confirmation before any code
Facts, re-verified on
mainmctlhq/.github:roadmap/scripts/ready.pyandhealth.py, on top ofgithub_graph.py/completion.py.compute()is pure (no clock, no I/O); onlymain()touches the network, and only with GET requests.ready.pyis a CLI.--livecaptures GitHub state and--capturewrites the snapshot. Its output is schema-validated (roadmap/schemas/roadmap-ready-set.schema.json) and already carries, per epic,epic.manifest.{path, sha256},source.{mode, capturedAt, apiBase}, and per-itemstatewith typedblockers, including explicitunknown.roadmap-validate.ymlonly replays fixtures, and no workflow is scheduled or writes an artifact.alpinewithgit/openssh-clientand no Python. The rejected alternative is mctl-api cloning.githuband exec'ingready.py.internal/gitops.Readeralready keeps a periodically refreshed git clone of an arbitraryrepoURL+branch(NewReader(repoURL, branch, localPath, token, …))..github,mctl-api,mctl-agents,mctl-telegram,mctl-portal,mctl-docs,mctl-agent,mctl-gitops,mctl-claude-remote). The snapshot schema captures "identity and native relations only; issue prose is never captured". Publishing it exposes nothing that is not already public.Recommended boundary
Producer (
.github). It is the only place that evaluates. It writes one commit per publication to its own repository withcontents: writeon that repo only: no cross-repo credential, no GitHub mutation, no issue writes. The git SHA of that commit is the publication version.Consumer (mctl-api). A read adapter only. It never calls GitHub for roadmap state and never re-derives readiness. It serves exactly what one publication says, so every consumer that reads the same publication sees the same readiness.
Contract (
publication.json, schema-versioned in.github/roadmap/schemas/)apiVersion/kind: RoadmapPublication.evaluator.revision: the.githubcommit the evaluator and manifests ran from (GITHUB_SHA).manifests[]:{path, sha256}, already produced per epic.observation:{source.mode, capturedAt, apiBase}andsnapshot.sha256(digest ofsnapshot.json).outputs: digests ofready-set.json/health.json.The determinism guarantee is that
ready.py --snapshot snapshot.jsonatevaluator.revisionreproducesready-set.jsonbyte for byte, so a publication can be verified offline. Item states stay the evaluator's four (complete/ready/blocked/unknown) with typedblockersas the explanation, and unbound or unobserved items stayunknown, never defaulted. The consumer answers 503 when no publication exists or theapiVersionis unsupported. It exposescapturedAtand the publication age on every response so a client can judge freshness itself, and it serves nothing it cannot read.Why this mechanism and not the others
mctl-gitopsneeds a cross-repo App token and puts data churn into the deploy repository.ready.pymeans Python in the image and a second evaluation site.The orphan-branch approach reuses the evaluator as is, the workflow token, and the existing
gitops.Reader, so no new infrastructure is needed.Minimal decomposition
.githubissue A (new): publish a versioned RoadmapPublication. Covers the envelope schema, the publish workflow, the orphanroadmap-statebranch, the byte-reproducibility test, and a commit policy of "on readiness change, or a heartbeat when the last capture is older than N minutes". The heartbeat keepscapturedAthonest without a commit per tick. This becomes a new required roadmap node, and feat(roadmap): expose EpicDefinition status and ready work items through REST/MCP #333dependsOnit.Reader,internal/roadmap, REST plus OpenAPI, and the two read-only MCP tools. The live DevLoop projection from.github#95stays a separate optional field, as the issue already says.A note on the dependency in the issue body: "Depends on
.github#96" points at a CI fix. The readiness evaluator was.github#99(closed), so issue A is the real prerequisite.Stopping here. No implementation, and no issue A filed, until the owner confirms this boundary.
Merge pull request #356 from mctlhq/feat/333-roadmap-read-api
mashkovd commentedon Sep 23, 2026
Closure evidence (closed by #356, merge commit
2717a3e):mctlhq/.github@roadmap-statevia a secondgitops.Readerand never evaluates. It verifies apiVersion, the sha256 of every derived file, the manifest↔ready-set↔health bijection, the ready list against item states, and epic identity/lifecycle (published since .github#123657575c). Anything unverified answers 503, meaning "cannot say".GET /api/v1/roadmap/{epics,epic-status,ready}(required_onlydefaults to true; withoutepic, everylifecycle: activeepic), MCPmctl_get_epic_status/mctl_get_ready_work_items. Every answer carries provenance: state/evaluator/source revisions, capturedAt, age.roadmap-state@a36af84). Served documents equal the published ones; ready items equal the published ready list; Enterprise MCP fix: raise OpenClaw tenant quota default/floor to limits.cpu=3 #35 serves ≥2 ready required items; no unbound or unknown item is served as ready. Tamper table, cache/no-stale, MCP handler and route-registration tests; every guard mutation-checked.639bdcc, agy PASS; Copilot could not review (quota).enabled: false; exposing them on the shared portal is an owner decision. Not released or deployed.Next on the graph: #334 (governed wave start) consumes this.