Repository navigation
fix(devloop): a failed approval must not project success or completed #702
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't workingdev-loopTemporal dev-loop (DevLoopWorkflow) migration: phases, stages, governanceTemporal dev-loop (DevLoopWorkflow) migration: phases, stages, governance
Description
Problem
When the approve CWFT fails or refuses, the canonical DevLoop ends its we_ execution as Succeeded and the Temporal workflow ends as Completed. A refused approval is reported as a success, and it ends in a state that mctl-api will not restart.
Production incident (2026-10-18, mctlhq/.github#182)
- 14:17:58Z
mctl-agents-approve-6f23268b→Failed: the approve CWFT refused a stale grant (digest mismatch, mctl-agents#681 working as intended; see fix(devloop): reinvestigate must supersede the pending approval grant #701). - 14:18:26Z audit
work_item.executionforwe_dadf9c08-0156-4ffa-a306-a11e188ae921:phase: Succeeded. mctl_get_dev_loop(dev-loop-mctlhq-.github-182)→status: Completed.- mctl-api restarts only executions that ended
abandoned: …before implement (restartIfAbandoned, mctl-api#640), so this loop answersalready_existsand cannot be retried on its id. The only clean way forward was a new carrier issue.
Cause (orchestrator/temporal/workflows/dev_loop.py@50e703d)
- On
not approve_result.succeededthe workflow returns a normalDevLoopResult(..., ended="approve flip ended Failed")(around line 3740). - The exit path then ends the canonical
we_withself._canonical_pending_phase or self._canonical_end_phase or "Failed"(lines 3183–3188)._canonical_end_phaseis set from the investigator's result (lines 3344/3488), so a loop that ends "before any flip" ends with the investigator's phase, hereSucceeded.
Expected
- An approve CWFT that fails or refuses must not project
Succeededonto the WorkItem execution. - The loop ends with an explicit, typed terminal outcome, for example
approval_refused(CWFT refused: digest mismatch or drift) orapproval_failed(infrastructure failure), visible in the execution record, theDevLoopResultandmctl_get_dev_loop. - Retry semantics are defined for those outcomes: either the loop can request a fresh grant on the same
we_(preferred forapproval_refusedafter a rewrite), or mctl-api treats the outcome as restartable the same way it treatsabandoned: …. The choice belongs to the design, but a refused approval must not be a dead end.
Acceptance criteria
- Test: canonical loop, approve CWFT returns
Failed→we_is notSucceeded; the terminal outcome is the typed refusal or failure. - Test: approve succeeds → unchanged (
Succeeded, implementer runs). - The retry path for the new outcome is covered by a test (re-grant on the same
we_, or restart via mctl-api). - The tests fail if the fix is reverted.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingdev-loopTemporal dev-loop (DevLoopWorkflow) migration: phases, stages, governanceTemporal dev-loop (DevLoopWorkflow) migration: phases, stages, governance
Activity
mctl-agents commentedon Oct 18, 2026
mctl-agents issue-investigator has analyzed this issue and created a proposal:
https://github.com/mctlhq/mctl-gitops/tree/main/platform-gitops/agents-state/mctl-agents/proposals/issue-702-fix-devloop-a-failed-approval-must-not-p/
Status:
proposed— pending human approval. Reviewrequirements.md,design.mdandtasks.md, then approve: signal this issue's DevLoopWorkflow (mctl-apiPOST /api/v1/agents/dev-loop/dev-loop-mctlhq-mctl-agents-702/approve, orpython -m orchestrator.temporal.cli approve dev-loop-mctlhq-mctl-agents-702 --approver <your-identity>) — the workflow flips.status.yamltoacceptedvia themctl-agents-approveoperation and runs the Tier 2 implementer. If no DevLoopWorkflow is running for this issue (pre-Temporal proposal), run themctl-agents-approveoperation directly withservice=mctl-agents slug=issue-702-fix-devloop-a-failed-approval-must-not-p.The approver is recorded in
.status.yamland in the gitops commit, and the implementer refuses a proposal whosecontrol.requires_human_approvalcarries no named approver — so prefer the mctl-api endpoint, which takes the approver from the authenticated caller rather than from what the caller types.chore(agents): investigate mctlhq/mctl-agents#702 2026-10-18
Merge pull request #711 from mctlhq/test/702-approval-failure-tests