Repository navigation
feat(dev-loop): allow restarting a DevLoop that was abandoned, keep rejecting every other closed run #640
Description
Problem
An abandoned DevLoop permanently closes its issue to DevLoop.
StartDevLoopWorkflow (internal/temporalclient/client.go:171) starts dev-loop-mctlhq-{repo}-{issue} with WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, and classifyDevLoop (internal/api/handlers_dev_loop.go) reports any closed execution as already_exists without starting. That was deliberate (#287: never report a no-op as a start, never restart a finished loop by accident), but it does not distinguish a loop that finished its work from one an operator abandoned via mctl_abandon_dev_loop. Both close as Completed.
Since the legacy approver relay was turned off (mctl-gitops#1913, MCTL_AGENTS_APPROVE_LEGACY_APPROVER: "false"), the only way to approve and implement a proposal is a running DevLoop with an aar_ grant. So a proposal whose loop was abandoned can never be implemented on its original issue.
Observed 2026-10-16: five pre-1.76.0 loops parked at approval were abandoned during the #636 cutover. mctl_trigger_issue(use_temporal=true) for mctl-agents#600 returned already_exists / started:false. The workaround was a new issue (mctl-agents#663, superseding #600). Still affected: mctl-agents#413, mctl-agents#428 and this repo's #489.
Proposal
Allow a new run on the same workflow id only when the latest closed run was abandoned, and keep every other closed outcome rejected:
- Make "abandoned" observable without guessing: the DevLoop result already carries
ended: "abandoned"; prefer an explicit marker (a search attribute or memo set by the abandon path) over parsing the result payload. - In
classifyDevLoop, a closed execution whose marker says abandoned → proceed to start; any other closed status →already_existsas today. An execution whose marker cannot be read →failed, never started blind ("could not observe" is not "abandoned"). - Start that case with
WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATEand report a distinct outcome, e.g.restarted_after_abandon, with the previous run id, so the caller and the audit trail see that it is a restart. - Mirror the policy in mctl-agents
orchestrator/temporal/cli.py, as the existing comment inclient.gorequires. - Record the restart in the audit log with the previous run id and the actor.
Open questions
- The new run re-investigates. What happens to the existing
proposedproposal directory for the same issue: does the investigator overwrite it, write next to it, or must the restart reuse it? (mctl-agents investigator publish guards decide this; check before enabling.) - Should the old work item
wi_/we_be reused or a new execution attached? The canonicalwe_of an abandoned loop is already terminal.
Acceptance criteria
- Restarting an abandoned DevLoop starts a new run on the same workflow id and reports a distinct outcome with the previous run id.
- A loop that completed normally, failed, was terminated or timed out is still
already_exists(table test over every closed status x marker present/absent/unreadable). - An unreadable marker yields
failedwithout a start; proven both ways by a mutation that treats it as abandoned. - mctl-agents
cli.pyuses the same rule. - Documented in the DevLoop runbook, including how to restart fix(operations): tenant ops no longer claim the shared argocd/values.yaml #413/feat(unified-identity): delegation grants for agent principals (#376 slice B) #428/authz: decide tenant roles for work items, approvals and repo sync #489 once shipped.
Refs mctlhq/mctl-agents#636, mctlhq/mctl-agents#600, mctlhq/mctl-agents#663, mctlhq/mctl-gitops#1913
Activity
fix(dev-loop): restart only runs abandoned before implement
fix(dev-loop): address review P3s on restart-after-abandon
fix(dev-loop): never restart a run described without a run id
fix(dev-loop): give the result read its own deadline, explain skips
fix(dev-loop): word skip reasons by what the run did, cap ended text
fix(temporal): treat a null ended as no abandon marker