Skip to content

feat(dev-loop): allow restarting a DevLoop that was abandoned, keep rejecting every other closed run #640

Open

Description

@mashkovd

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_exists as 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_DUPLICATE and 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 in client.go requires.
  • 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 proposed proposal 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 canonical we_ of an abandoned loop is already terminal.

Acceptance criteria

Refs mctlhq/mctl-agents#636, mctlhq/mctl-agents#600, mctlhq/mctl-agents#663, mctlhq/mctl-gitops#1913

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions