Repository navigation
feat(lifecycle-ownership): guarded operator recovery for lifecycle ownership #294
Description
Split out of
mctlhq/mctl-api#293on 2026-09-12.#293keeps the read-only
inspection surface and moves earlier — it is what makes the observe-mode
soak diagnosable. This issue keeps the mutating half, which cannot be
specified until the reconciler defines what a legitimate recovery is.
Parent roadmap: mctlhq/.github#57
Depends on: mctlhq/mctl-api#293, mctlhq/mctl-agents#352, mctlhq/mctl-agents#353
Goal
Expose narrowly-scoped, auditable operator recovery for lifecycle ownership —
the operations an operator needs when reconciliation has correctly refused to
act and a human must break the tie.
Why this is separate from #293
#293's read surface is needed during rollout: without it, the observe-mode
soak is diagnosed by grepping logs instead of asking the platform. Recovery is
the opposite — specifying it before mctlhq/mctl-agents#353 has defined what a
legitimate recovery looks like would mean inventing the semantics at the API
layer, which is exactly how a force_owner escape hatch gets built.
Scope
Operations equivalent to:
- request ownership reconciliation for one entity;
- request or retry an explicit handoff;
- release/fence a claim that is provably stale;
- inspect conflict evidence before intervening.
Requirements
- Every recovery operation takes optimistic preconditions — expected owner,
owner epoch, and entity version/head — and fails closed when any of them
has moved. A stale client must never be able to overwrite fresh state. - Recovery is subject to normal mctl policy/authz and is audited. It does not
imply GitHub merge authority, and it does not bypass CODEOWNERS, branch
protection,NEVER_MERGE_SERVICESor repository merge policy. - A caller may not release or progress an ownership row it does not own, except
through an explicit, audited recovery operation that records why. - No generic
force_owner=<anything>mutation. Each operation names a
specific, reviewable transition. - Every recovery records entity, phase, version/head, old and new owner epoch,
claim state, the evidence relied on, the acting principal, reason and outcome.
Acceptance criteria
- Each recovery operation requires preconditions and fails closed when owner
epoch or entity version changed. - A stale client cannot overwrite fresh state; a test proves it.
- No operation can transfer merge or approval authority.
- Recovery is policy/authz controlled and appears in the audit trail.
- Conflict evidence is inspectable before intervention.
- MCP descriptors make mutating semantics explicit; read and recovery tools
are not confusable. - No generic force/override endpoint exists.
Non-goals
- A free-form admin endpoint for reassigning any lifecycle entity.
- Making mctl-api the owner of Temporal/DevLoop lifecycle state.
- Automatic recovery — that is
mctlhq/mctl-agents#353. This is the human path.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
fix(lifecycle): an unrecognised ownership state must fail closed
mctl-agents commentedon Sep 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-api/proposals/issue-294-feat-lifecycle-ownership-guarded-operato/
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-api-294/approve, orpython -m orchestrator.temporal.cli approve dev-loop-mctlhq-mctl-api-294 --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-api slug=issue-294-feat-lifecycle-ownership-guarded-operato.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-api#294 2026-09-18