Repository navigation
agents: the service enum is hard-coded in eight places and has drifted from mctl-agents, which fails the dev loop rather than the call #274
Description
What happens
dev-loop-mctlhq-seerrsense-45 failed 14 seconds after its human approval signal, with no PR and no branch:
19:02:51Z EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED signalName=approve
19:02:55Z EVENT_TYPE_ACTIVITY_TASK_FAILED submit_and_wait
Client error '400 Bad Request' for url
'https://api.mctl.ai/api/v1/operations/mctl-agents-approve/execute'
19:02:55Z EVENT_TYPE_WORKFLOW_EXECUTION_FAILED
The signal itself was accepted — mctl-api logged operation=dev-loop-approve, workflow=dev-loop-mctlhq-seerrsense-45, status=succeeded. What failed is the activity the workflow then runs, which calls the standalone mctl-agents-approve operation over REST.
The 400 comes from parameter validation. internal/operations/registry.go:671:
{Name: "service", Type: "string", Required: true, Description: "Service owning the proposal.",
Enum: []string{"mctl-web", "mctl-openclaw", "mctl-docs", "mctl-api", "mctl-portal",
"mctl-agent", "mctl-gitops", "mctl-agents", "mctl-telegram",
"mctl-design", "mctl-pairdesk", "mctl-academy"}},seerrsense is not there. It is, however, in mctl-agents/config/settings.py:57, in both SERVICES and NON_ROTATING_SERVICES — so it is a registered agent target. The two repositories disagree about which services exist.
Why it matters
A service registered in mctl-agents but absent from this enum is not merely awkward to drive by hand — it kills the dev loop. The Temporal DevLoopWorkflow parks on a wait_condition with no timeout, and the approval that is supposed to release it is the very call that 400s. The result is a workflow in Failed, an approval that appears to have succeeded in the audit log, and a proposal left at status: proposed with nothing to show for the investigator run that produced it.
This has now cost two manual interventions on the same service. seerrsense issue-39 was approved by hand through mctl-gitops#1113 in September; issue-45 needed the same treatment in mctl-gitops#1130. seerrsense issue-44 is currently parked on the identical gate and cannot be approved through the supported path at all.
Cause
The list is not derived from anything. The same twelve literals appear in eight places:
internal/operations/registry.go:596—mctl-agents-implement, optional filterinternal/operations/registry.go:628— optional filterinternal/operations/registry.go:671—mctl-agents-approve, requiredinternal/operations/registry.go:707— reconcile filterinternal/mcp/server.go:2534,:2573,:2616,:2657— the matching MCP tool schemas
There is no shared variable: a grep for agentServices, knownServices or serviceEnum in this repository returns nothing. Adding a service today means editing eight literals in two files and shipping a release, and forgetting any one of them produces a different failure depending on which entry point is used.
Suggested direction
Adding "seerrsense" in eight places would close this ticket and reopen it the next time a service is registered. The invariant worth fixing is that mctl-api should not carry its own opinion about which services exist.
Options, roughly in order of preference:
- Derive the enum from the agents' own registry. mctl-agents already declares
SERVICES; if that list is reachable at build or runtime (a generated file, a small endpoint, a shared manifest in mctl-gitops), mctl-api should consume it rather than restate it. Then registering a service is one edit in one repository. - Failing that, one constant in this repository, referenced by all eight sites, with a test that asserts every operation and MCP tool that takes a
serviceparameter uses it. That keeps the drift to a single line and makes the next omission impossible rather than merely unlikely. - Consider whether
serviceneeds an enum at all onmctl-agents-approve. The operation resolves a proposal directory byserviceandslug; an unknown service is already an error one level down, and a closed enum in the API buys strictly less than the filesystem check does. A free-form string validated against what exists inagents-state/would never drift.
Whichever is chosen, seerrsense must be accepted by mctl-agents-approve, mctl-agents-implement and the reconcile filter, and the MCP tool schemas must agree with the REST operations — they are generated from the same knowledge and currently diverge from mctl-agents together.
Verification
- A test that fails today: every entry in the canonical service list is accepted by
mctl-agents-approve's parameter validation, and the four MCP tool schemas offer exactly that list. Removing a service from the canonical list must break it; adding one must not require touching any other file. - End to end, after deploy:
mctl_trigger_issuewithuse_temporal=trueon a seerrsense issue, thenmctl_approve_dev_loop— the workflow leaveswait_conditionand reaches the implementer instead of failing. seerrsense#44 is a live candidate: its DevLoopWorkflow is parked and waiting for exactly this. - Confirm no existing caller regresses: the twelve services currently listed keep working through both the REST operations and the MCP tools.
Activity
mashkovd commentedon Sep 9, 2026
Third occurrence, same shape —
dev-loop-mctlhq-seerrsense-49, approved2026-09-09T22:37:49Z:mctl_approve_dev_loopreturned{"signalled":"approve"}and the audit log recordedoperation=dev-loop-approve, status=succeeded.mctl-agents-approverun followed.mctl_get_dev_loopnow reportsstatus: Failed.dev-loop-mctlhq-mctl-portal-122(21:15:05) anddev-loop-mctlhq-mctl-telegram-585(22:03:01) each had theirmctl-agents-approverun land within three seconds and are stillRunning.Recovered with a fourth manual flip, mctl-gitops#1149, following #1113 and #1130.
Two details worth adding to the ticket:
The MCP tool schemas carry the same drift, so the manual path is blocked too.
mctl_trigger_approve'sserviceparameter is a closed enum ending atmctl-academy, so even approving by tool rather than by workflow is impossible for seerrsense — hence the gitops commit.mctl_trigger_implementeris the exception that made recovery possible at all: itsslugfilter is independent ofservice, so the implementer can still be pointed at a seerrsense proposal.The count is eight sites in this repo, but the blast radius includes the ChatGPT connector surface tracked in #272 — the same literals are what that surface would expose.
Filed #275 for the separate observability half: the approval is recorded as succeeded and never amended when the workflow it released dies, and
mctl_get_workflow_statuson adev-loop-*id queriesargoproj.iofor what is a Temporal execution, reporting the miss as a cluster-scope RBAC error. This issue stays the root cause; #275 is about not having to discover it by hand.seerrsense#44 remains parked on this gate, and #45 and #49 both reached implementation only through manual gitops flips.